From 252a76d25c9344899d67cc1a121a020a2f6531c8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 04:39:31 +0100 Subject: [PATCH 001/215] refactor: stage external output writes through fs-safe --- .../browser/src/browser/output-atomic.ts | 12 ++- .../browser/src/browser/pw-session.test.ts | 20 +++- extensions/browser/src/browser/pw-session.ts | 11 ++- .../src/browser/pw-tools-core.downloads.ts | 10 +- ...-core.waits-next-download-saves-it.test.ts | 21 +++-- .../browser/src/sdk-security-runtime.ts | 1 + extensions/diffs/src/browser.ts | 65 +++++++++---- extensions/discord/src/voice-message.test.ts | 29 +++++- extensions/discord/src/voice-message.ts | 92 ++++++++++++------- extensions/feishu/src/media.ts | 53 ++++++----- .../google/video-generation-provider.test.ts | 40 ++++++++ .../google/video-generation-provider.ts | 16 +++- extensions/microsoft/tts.test.ts | 8 +- extensions/microsoft/tts.ts | 35 ++++++- .../discord/discord-live.runtime.ts | 10 +- .../src/mantis/visual-task.runtime.test.ts | 10 +- .../qa-lab/src/mantis/visual-task.runtime.ts | 48 ++++++++-- extensions/tts-local-cli/speech-provider.ts | 55 +++++++---- .../whatsapp/src/outbound-media-contract.ts | 52 ++++++----- package.json | 2 +- pnpm-lock.yaml | 10 +- src/infra/fs-safe.ts | 5 + src/media-understanding/runner.entries.ts | 32 ++++--- src/media/audio-transcode.test.ts | 2 +- src/media/audio-transcode.ts | 57 +++++++----- src/plugin-sdk/security-runtime.ts | 3 + 26 files changed, 486 insertions(+), 213 deletions(-) diff --git a/extensions/browser/src/browser/output-atomic.ts b/extensions/browser/src/browser/output-atomic.ts index e92c5a6abfd..f0586e81237 100644 --- a/extensions/browser/src/browser/output-atomic.ts +++ b/extensions/browser/src/browser/output-atomic.ts @@ -1,13 +1,15 @@ -import { writeViaSiblingTempPath as writeViaSiblingTempPathBase } from "../sdk-security-runtime.js"; +import fs from "node:fs/promises"; +import { writeExternalFileWithinRoot } from "../sdk-security-runtime.js"; export async function writeViaSiblingTempPath(params: { rootDir: string; targetPath: string; writeTemp: (tempPath: string) => Promise; }): Promise { - await writeViaSiblingTempPathBase({ - ...params, - fallbackFileName: "output.bin", - tempPrefix: ".openclaw-output-", + await fs.mkdir(params.rootDir, { recursive: true }); + await writeExternalFileWithinRoot({ + rootDir: params.rootDir, + path: params.targetPath, + write: params.writeTemp, }); } diff --git a/extensions/browser/src/browser/pw-session.test.ts b/extensions/browser/src/browser/pw-session.test.ts index b4ea01a59db..bb94f039fa2 100644 --- a/extensions/browser/src/browser/pw-session.test.ts +++ b/extensions/browser/src/browser/pw-session.test.ts @@ -138,11 +138,19 @@ describe("pw-session role refs cache", () => { describe("pw-session ensurePageState", () => { it("stores unmanaged downloads under unique managed paths", async () => { const { page, handlers } = fakePage(); - const mkdirSpy = vi.spyOn(fs, "mkdir").mockResolvedValue(undefined); + const mkdirActual = fs.mkdir.bind(fs); + const mkdirSpy = vi.spyOn(fs, "mkdir").mockImplementation(async (target, options) => { + await mkdirActual(target, options); + return undefined; + }); ensurePageState(page); - const saveAsA = vi.fn(async () => {}); - const saveAsB = vi.fn(async () => {}); + const saveAsA = vi.fn(async (outPath: string) => { + await fs.writeFile(outPath, "download-a", "utf8"); + }); + const saveAsB = vi.fn(async (outPath: string) => { + await fs.writeFile(outPath, "download-b", "utf8"); + }); const downloadA: MutableDownload = { suggestedFilename: () => "report.pdf", saveAs: saveAsA, @@ -163,8 +171,10 @@ describe("pw-session ensurePageState", () => { expect(path.dirname(managedPathB ?? "")).toBe(DEFAULT_DOWNLOAD_DIR); expect(path.basename(managedPathA ?? "")).toMatch(/-report\.pdf$/); expect(path.basename(managedPathB ?? "")).toMatch(/-report\.pdf$/); - expect(saveAsA).toHaveBeenCalledWith(managedPathA); - expect(saveAsB).toHaveBeenCalledWith(managedPathB); + expect(saveAsA.mock.calls[0]?.[0]).not.toBe(managedPathA); + expect(saveAsB.mock.calls[0]?.[0]).not.toBe(managedPathB); + await expect(fs.readFile(managedPathA ?? "", "utf8")).resolves.toBe("download-a"); + await expect(fs.readFile(managedPathB ?? "", "utf8")).resolves.toBe("download-b"); expect(mkdirSpy).toHaveBeenCalledWith(DEFAULT_DOWNLOAD_DIR, { recursive: true }); }); diff --git a/extensions/browser/src/browser/pw-session.ts b/extensions/browser/src/browser/pw-session.ts index 6dd1e7ef8dc..31241ed2d7d 100644 --- a/extensions/browser/src/browser/pw-session.ts +++ b/extensions/browser/src/browser/pw-session.ts @@ -1,5 +1,4 @@ import crypto from "node:crypto"; -import fs from "node:fs/promises"; import path from "node:path"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import type { @@ -34,6 +33,7 @@ import { InvalidBrowserNavigationUrlError, withBrowserNavigationPolicy, } from "./navigation-guard.js"; +import { writeViaSiblingTempPath } from "./output-atomic.js"; import { DEFAULT_DOWNLOAD_DIR } from "./paths.js"; import { playwrightCore } from "./playwright-core.runtime.js"; import { BROWSER_REF_MARKER_ATTRIBUTE, withPageScopedCdpClient } from "./pw-session.page-cdp.js"; @@ -466,8 +466,13 @@ export function ensurePageState(page: Page): PageState { ); const managedPath = buildManagedDownloadPath(suggested); const managedSave = (async () => { - await fs.mkdir(DEFAULT_DOWNLOAD_DIR, { recursive: true }); - await download.saveAs?.(managedPath); + await writeViaSiblingTempPath({ + rootDir: DEFAULT_DOWNLOAD_DIR, + targetPath: managedPath, + writeTemp: async (tempPath) => { + await download.saveAs?.(tempPath); + }, + }); return managedPath; })(); managedSave.catch(() => {}); diff --git a/extensions/browser/src/browser/pw-tools-core.downloads.ts b/extensions/browser/src/browser/pw-tools-core.downloads.ts index abecd679d69..6a037fc4ce4 100644 --- a/extensions/browser/src/browser/pw-tools-core.downloads.ts +++ b/extensions/browser/src/browser/pw-tools-core.downloads.ts @@ -1,5 +1,4 @@ import crypto from "node:crypto"; -import fs from "node:fs/promises"; import path from "node:path"; import type { Page } from "playwright-core"; import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; @@ -93,10 +92,15 @@ async function saveDownloadPayload(download: DownloadPayload, outPath: string) { const suggested = download.suggestedFilename?.() || "download.bin"; const requestedPath = outPath?.trim(); const resolvedOutPath = path.resolve(requestedPath || buildTempDownloadPath(suggested)); - await fs.mkdir(path.dirname(resolvedOutPath), { recursive: true }); if (!requestedPath) { - await download.saveAs?.(resolvedOutPath); + await writeViaSiblingTempPath({ + rootDir: path.dirname(resolvedOutPath), + targetPath: resolvedOutPath, + writeTemp: async (tempPath) => { + await download.saveAs?.(tempPath); + }, + }); } else { await writeViaSiblingTempPath({ rootDir: path.dirname(resolvedOutPath), diff --git a/extensions/browser/src/browser/pw-tools-core.waits-next-download-saves-it.test.ts b/extensions/browser/src/browser/pw-tools-core.waits-next-download-saves-it.test.ts index 11f93f9278f..606dd2e92fa 100644 --- a/extensions/browser/src/browser/pw-tools-core.waits-next-download-saves-it.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.waits-next-download-saves-it.test.ts @@ -78,7 +78,9 @@ describe("pw-tools-core", () => { suggestedFilename: string; }) { const harness = createDownloadEventHarness(); - const saveAs = vi.fn(async () => {}); + const saveAs = vi.fn(async (outPath: string) => { + await fs.writeFile(outPath, "download-content", "utf8"); + }); const p = mod.waitForDownloadViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", @@ -135,8 +137,7 @@ describe("pw-tools-core", () => { const savedPath = params.saveAs.mock.calls[0]?.[0]; expect(typeof savedPath).toBe("string"); expect(savedPath).not.toBe(params.targetPath); - expect(path.basename(String(savedPath))).toContain(".openclaw-output-"); - expect(path.basename(String(savedPath))).toContain(".part"); + expect(path.basename(String(savedPath))).toBe(path.basename(params.targetPath)); expect(await fs.readFile(params.targetPath, "utf8")).toBe(params.content); await expect(fs.access(String(savedPath))).rejects.toThrow(); } @@ -189,7 +190,9 @@ describe("pw-tools-core", () => { harness.trigger({ url: () => "https://example.com/file.bin", suggestedFilename: () => "file.bin", - saveAs: vi.fn(async () => {}), + saveAs: vi.fn(async (outPath: string) => { + await fs.writeFile(outPath, "file-content", "utf8"); + }), }); await p; @@ -279,8 +282,9 @@ describe("pw-tools-core", () => { path.join(path.sep, "tmp", "openclaw-preferred", "downloads"), ); const expectedDownloadsTail = `${path.join("tmp", "openclaw-preferred", "downloads")}${path.sep}`; - expect(path.dirname(outPath)).toBe(expectedRootedDownloadsDir); - expect(path.basename(outPath)).toMatch(/-file\.bin$/); + expect(path.dirname(res.path)).toBe(expectedRootedDownloadsDir); + expect(path.basename(outPath)).toBe(path.basename(res.path)); + await expect(fs.readFile(res.path, "utf8")).resolves.toBe("download-content"); expect(path.normalize(res.path)).toContain(path.normalize(expectedDownloadsTail)); expect(tmpDirMocks.resolvePreferredOpenClawTmpDir).toHaveBeenCalled(); }); @@ -292,10 +296,11 @@ describe("pw-tools-core", () => { suggestedFilename: "../../../../etc/passwd", }); expect(typeof outPath).toBe("string"); - expect(path.dirname(outPath)).toBe( + expect(path.dirname(res.path)).toBe( path.resolve(path.join(path.sep, "tmp", "openclaw-preferred", "downloads")), ); - expect(path.basename(outPath)).toMatch(/-passwd$/); + expect(path.basename(outPath)).toBe(path.basename(res.path)); + await expect(fs.readFile(res.path, "utf8")).resolves.toBe("download-content"); expect(path.normalize(res.path)).toContain( path.normalize(`${path.join("tmp", "openclaw-preferred", "downloads")}${path.sep}`), ); diff --git a/extensions/browser/src/sdk-security-runtime.ts b/extensions/browser/src/sdk-security-runtime.ts index e39265146a4..6ed75ed927b 100644 --- a/extensions/browser/src/sdk-security-runtime.ts +++ b/extensions/browser/src/sdk-security-runtime.ts @@ -22,6 +22,7 @@ export { resolveWritablePathWithinRoot, FsSafeError, SsrFBlockedError, + writeExternalFileWithinRoot, writeViaSiblingTempPath, wrapExternalContent, } from "openclaw/plugin-sdk/security-runtime"; diff --git a/extensions/diffs/src/browser.ts b/extensions/diffs/src/browser.ts index 5c2180b500e..ff66a08e651 100644 --- a/extensions/diffs/src/browser.ts +++ b/extensions/diffs/src/browser.ts @@ -2,6 +2,7 @@ import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime"; import { chromium } from "playwright-core"; import type { OpenClawConfig } from "../api.js"; import type { DiffRenderOptions, DiffTheme } from "./types.js"; @@ -61,7 +62,6 @@ export class PlaywrightDiffScreenshotter implements DiffScreenshotter { theme: DiffTheme; image: DiffRenderOptions["image"]; }): Promise { - await fs.mkdir(path.dirname(params.outputPath), { recursive: true }); const lease = await acquireSharedBrowser({ config: this.config, idleMs: this.browserIdleMs, @@ -189,16 +189,22 @@ export class PlaywrightDiffScreenshotter implements DiffScreenshotter { throw new Error(IMAGE_SIZE_LIMIT_ERROR); } - await page.pdf({ - path: params.outputPath, - width: `${pdfWidth}px`, - height: `${pdfHeight}px`, - printBackground: true, - margin: { - top: "0", - right: "0", - bottom: "0", - left: "0", + const pageForPdf = page; + await writeExternalArtifactFile({ + outputPath: params.outputPath, + write: async (tempPath) => { + await pageForPdf.pdf({ + path: tempPath, + width: `${pdfWidth}px`, + height: `${pdfHeight}px`, + printBackground: true, + margin: { + top: "0", + right: "0", + bottom: "0", + left: "0", + }, + }); }, }); return params.outputPath; @@ -238,15 +244,21 @@ export class PlaywrightDiffScreenshotter implements DiffScreenshotter { throw new Error(IMAGE_SIZE_LIMIT_ERROR); } - await page.screenshot({ - path: params.outputPath, - type: "png", - scale: "device", - clip: { - x, - y, - width: cssWidth, - height: cssHeight, + const pageForScreenshot = page; + await writeExternalArtifactFile({ + outputPath: params.outputPath, + write: async (tempPath) => { + await pageForScreenshot.screenshot({ + path: tempPath, + type: "png", + scale: "device", + clip: { + x, + y, + width: cssWidth, + height: cssHeight, + }, + }); }, }); return params.outputPath; @@ -268,6 +280,19 @@ export class PlaywrightDiffScreenshotter implements DiffScreenshotter { } } +async function writeExternalArtifactFile(params: { + outputPath: string; + write: (tempPath: string) => Promise; +}): Promise { + const rootDir = path.dirname(params.outputPath); + await fs.mkdir(rootDir, { recursive: true }); + await writeExternalFileWithinRoot({ + rootDir, + path: path.basename(params.outputPath), + write: params.write, + }); +} + export async function resetSharedBrowserStateForTests(): Promise { executablePathCache = null; await closeSharedBrowser(); diff --git a/extensions/discord/src/voice-message.test.ts b/extensions/discord/src/voice-message.test.ts index c2c81d79e8c..20e4f966b57 100644 --- a/extensions/discord/src/voice-message.test.ts +++ b/extensions/discord/src/voice-message.test.ts @@ -1,3 +1,4 @@ +import fs from "node:fs/promises"; import path from "node:path"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { RequestClient } from "./internal/discord.js"; @@ -71,7 +72,13 @@ describe("ensureOggOpus", () => { it("re-encodes .ogg opus when sample rate is not 48kHz", async () => { runFfprobeMock.mockResolvedValueOnce("opus,24000\n"); - runFfmpegMock.mockResolvedValueOnce(); + runFfmpegMock.mockImplementationOnce(async (args: string[]) => { + const outputPath = args.at(-1); + if (typeof outputPath !== "string") { + throw new Error("missing ffmpeg output path"); + } + await fs.writeFile(outputPath, "ogg"); + }); const result = await ensureOggOpus("/tmp/input.ogg"); @@ -79,20 +86,34 @@ describe("ensureOggOpus", () => { expect(path.dirname(result.path)).toBe(path.normalize("/tmp")); expect(path.basename(result.path)).toMatch(/^voice-.*\.ogg$/); expect(runFfmpegMock).toHaveBeenCalledWith( - expect.arrayContaining(["-t", "1200", "-ar", "48000", "/tmp/input.ogg", result.path]), + expect.arrayContaining(["-t", "1200", "-ar", "48000", "/tmp/input.ogg"]), ); + const ffmpegOutputPath = (runFfmpegMock.mock.calls[0]?.[0] as string[] | undefined)?.at(-1); + expect(ffmpegOutputPath).not.toBe(result.path); + expect(path.basename(ffmpegOutputPath ?? "")).toBe(path.basename(result.path)); + await expect(fs.readFile(result.path, "utf8")).resolves.toBe("ogg"); }); it("re-encodes non-ogg input with bounded ffmpeg execution", async () => { - runFfmpegMock.mockResolvedValueOnce(); + runFfmpegMock.mockImplementationOnce(async (args: string[]) => { + const outputPath = args.at(-1); + if (typeof outputPath !== "string") { + throw new Error("missing ffmpeg output path"); + } + await fs.writeFile(outputPath, "ogg"); + }); const result = await ensureOggOpus("/tmp/input.mp3"); expect(result.cleanup).toBe(true); expect(runFfprobeMock).not.toHaveBeenCalled(); expect(runFfmpegMock).toHaveBeenCalledWith( - expect.arrayContaining(["-vn", "-sn", "-dn", "/tmp/input.mp3", result.path]), + expect.arrayContaining(["-vn", "-sn", "-dn", "/tmp/input.mp3"]), ); + const ffmpegOutputPath = (runFfmpegMock.mock.calls[0]?.[0] as string[] | undefined)?.at(-1); + expect(ffmpegOutputPath).not.toBe(result.path); + expect(path.basename(ffmpegOutputPath ?? "")).toBe(path.basename(result.path)); + await expect(fs.readFile(result.path, "utf8")).resolves.toBe("ogg"); }); }); diff --git a/extensions/discord/src/voice-message.ts b/extensions/discord/src/voice-message.ts index 03cb24d6119..696ac448310 100644 --- a/extensions/discord/src/voice-message.ts +++ b/extensions/discord/src/voice-message.ts @@ -22,6 +22,7 @@ import { import { MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS } from "openclaw/plugin-sdk/media-runtime"; import { unlinkIfExists } from "openclaw/plugin-sdk/media-runtime"; import type { RetryRunner } from "openclaw/plugin-sdk/retry-runtime"; +import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; @@ -33,6 +34,21 @@ const SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12; const WAVEFORM_SAMPLES = 256; const DISCORD_OPUS_SAMPLE_RATE_HZ = 48_000; +async function runFfmpegToOutput(params: { + outputPath: string; + buildArgs: (tempPath: string) => string[]; +}): Promise { + const rootDir = path.dirname(params.outputPath); + await fs.mkdir(rootDir, { recursive: true }); + await writeExternalFileWithinRoot({ + rootDir, + path: path.basename(params.outputPath), + write: async (tempPath) => { + await runFfmpeg(params.buildArgs(tempPath)); + }, + }); +} + function createRateLimitError( response: Response, body: { message: string; retry_after: number; global: boolean }, @@ -104,25 +120,28 @@ async function generateWaveformFromPcm(filePath: string): Promise { try { // Convert to raw 16-bit signed PCM, mono, 8kHz - await runFfmpeg([ - "-y", - "-i", - filePath, - "-vn", - "-sn", - "-dn", - "-t", - String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS), - "-f", - "s16le", - "-acodec", - "pcm_s16le", - "-ac", - "1", - "-ar", - "8000", - tempPcm, - ]); + await runFfmpegToOutput({ + outputPath: tempPcm, + buildArgs: (outputPath) => [ + "-y", + "-i", + filePath, + "-vn", + "-sn", + "-dn", + "-t", + String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS), + "-f", + "s16le", + "-acodec", + "pcm_s16le", + "-ac", + "1", + "-ar", + "8000", + outputPath, + ], + }); const pcmData = await fs.readFile(tempPcm); const samples = new Int16Array(pcmData.buffer, pcmData.byteOffset, pcmData.byteLength / 2); @@ -214,23 +233,26 @@ export async function ensureOggOpus(filePath: string): Promise<{ path: string; c const tempDir = resolvePreferredOpenClawTmpDir(); const outputPath = path.join(tempDir, `voice-${crypto.randomUUID()}.ogg`); - await runFfmpeg([ - "-y", - "-i", - filePath, - "-vn", - "-sn", - "-dn", - "-t", - String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS), - "-ar", - String(DISCORD_OPUS_SAMPLE_RATE_HZ), - "-c:a", - "libopus", - "-b:a", - "64k", + await runFfmpegToOutput({ outputPath, - ]); + buildArgs: (tempPath) => [ + "-y", + "-i", + filePath, + "-vn", + "-sn", + "-dn", + "-t", + String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS), + "-ar", + String(DISCORD_OPUS_SAMPLE_RATE_HZ), + "-c:a", + "libopus", + "-b:a", + "64k", + tempPath, + ], + }); return { path: outputPath, cleanup: true }; } diff --git a/extensions/feishu/src/media.ts b/extensions/feishu/src/media.ts index 21e9ffd3f01..32336d0e1f8 100644 --- a/extensions/feishu/src/media.ts +++ b/extensions/feishu/src/media.ts @@ -5,7 +5,7 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import type { MessageReceipt } from "openclaw/plugin-sdk/channel-message"; import { mediaKindFromMime } from "openclaw/plugin-sdk/media-mime"; import { MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS, runFfmpeg } from "openclaw/plugin-sdk/media-runtime"; -import { readRegularFile } from "openclaw/plugin-sdk/security-runtime"; +import { readRegularFile, writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime"; import { resolvePreferredOpenClawTmpDir, withTempWorkspace, @@ -757,29 +757,34 @@ async function transcodeToFeishuVoiceOpus(params: { const ext = normalizeLowercaseStringOrEmpty(path.extname(params.fileName)); const inputExt = ext && ext.length <= 12 ? ext : ".audio"; const inputPath = await workspace.write(`input${inputExt}`, params.buffer); - const outputPath = workspace.path(FEISHU_VOICE_FILE_NAME); - await runFfmpeg([ - "-hide_banner", - "-loglevel", - "error", - "-y", - "-i", - inputPath, - "-vn", - "-sn", - "-dn", - "-t", - String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS), - "-ar", - String(FEISHU_VOICE_SAMPLE_RATE_HZ), - "-ac", - "1", - "-c:a", - "libopus", - "-b:a", - FEISHU_VOICE_BITRATE, - outputPath, - ]); + await writeExternalFileWithinRoot({ + rootDir: workspace.dir, + path: FEISHU_VOICE_FILE_NAME, + write: async (outputPath) => { + await runFfmpeg([ + "-hide_banner", + "-loglevel", + "error", + "-y", + "-i", + inputPath, + "-vn", + "-sn", + "-dn", + "-t", + String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS), + "-ar", + String(FEISHU_VOICE_SAMPLE_RATE_HZ), + "-ac", + "1", + "-c:a", + "libopus", + "-b:a", + FEISHU_VOICE_BITRATE, + outputPath, + ]); + }, + }); return { buffer: await workspace.read(FEISHU_VOICE_FILE_NAME), fileName: FEISHU_VOICE_FILE_NAME, diff --git a/extensions/google/video-generation-provider.test.ts b/extensions/google/video-generation-provider.test.ts index 1dd194be513..9ea1489297c 100644 --- a/extensions/google/video-generation-provider.test.ts +++ b/extensions/google/video-generation-provider.test.ts @@ -1,3 +1,5 @@ +import { writeFile } from "node:fs/promises"; +import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; const { createGoogleGenAIMock, downloadMock, generateVideosMock, getVideosOperationMock } = @@ -195,6 +197,44 @@ describe("google video generation provider", () => { expect(result.videos[0]?.mimeType).toBe("video/mp4"); }); + it("stages SDK file downloads before finalizing generated video bytes", async () => { + vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({ + apiKey: "google-key", + source: "env", + mode: "api-key", + }); + generateVideosMock.mockResolvedValue({ + done: true, + response: { + generatedVideos: [ + { + video: { + name: "files/generated-video", + mimeType: "video/mp4", + }, + }, + ], + }, + }); + downloadMock.mockImplementation(async ({ downloadPath }: { downloadPath: string }) => { + await writeFile(downloadPath, "sdk-video"); + }); + + const provider = buildGoogleVideoGenerationProvider(); + const result = await provider.generateVideo({ + provider: "google", + model: "veo-3.1-fast-generate-preview", + prompt: "A tiny robot watering a windowsill garden", + cfg: {}, + durationSeconds: 3, + }); + + const [{ downloadPath }] = downloadMock.mock.calls[0] ?? [{}]; + expect(path.basename(String(downloadPath))).toBe("video-1.mp4"); + expect(result.videos[0]?.buffer).toEqual(Buffer.from("sdk-video")); + expect(result.videos[0]?.fileName).toBe("video-1.mp4"); + }); + it("falls back to REST predictLongRunning when text-only SDK video generation returns 404", async () => { vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({ apiKey: "google-key", diff --git a/extensions/google/video-generation-provider.ts b/extensions/google/video-generation-provider.ts index eb8e98838e1..c59e995c507 100644 --- a/extensions/google/video-generation-provider.ts +++ b/extensions/google/video-generation-provider.ts @@ -6,6 +6,7 @@ import { resolveProviderOperationTimeoutMs, waitProviderOperationPollInterval, } from "openclaw/plugin-sdk/provider-http"; +import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import { resolvePreferredOpenClawTmpDir, withTempWorkspace } from "openclaw/plugin-sdk/temp-path"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; @@ -154,10 +155,17 @@ async function downloadGeneratedVideo(params: { return await withTempWorkspace( { rootDir: resolvePreferredOpenClawTmpDir(), prefix: "openclaw-google-video-" }, async ({ dir: tempDir }) => { - const downloadPath = path.join(tempDir, `video-${params.index + 1}.mp4`); - await params.client.files.download({ - file: params.file as never, - downloadPath, + const fileName = `video-${params.index + 1}.mp4`; + const downloadPath = path.join(tempDir, fileName); + await writeExternalFileWithinRoot({ + rootDir: tempDir, + path: fileName, + write: async (downloadPath) => { + await params.client.files.download({ + file: params.file as never, + downloadPath, + }); + }, }); const buffer = await readFile(downloadPath); return { diff --git a/extensions/microsoft/tts.test.ts b/extensions/microsoft/tts.test.ts index 4d9dccbb9d2..ea27de79d1c 100644 --- a/extensions/microsoft/tts.test.ts +++ b/extensions/microsoft/tts.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; @@ -92,8 +92,10 @@ describe("edgeTTS empty audio validation", () => { it("succeeds when the output file has content", async () => { tempDir = mkdtempSync(path.join(tmpdir(), "tts-test-")); const outputPath = path.join(tempDir, "voice.mp3"); + let stagedPath = ""; const deps = createEdgeTTSDeps(async (_text: string, filePath: string) => { + stagedPath = filePath; writeFileSync(filePath, Buffer.from([0xff, 0xfb, 0x90, 0x00])); }); @@ -108,6 +110,10 @@ describe("edgeTTS empty audio validation", () => { deps, ), ).resolves.toBeUndefined(); + expect(stagedPath).not.toBe(outputPath); + expect(path.basename(stagedPath)).toBe(path.basename(outputPath)); + expect(readFileSync(outputPath)).toEqual(Buffer.from([0xff, 0xfb, 0x90, 0x00])); + expect(existsSync(stagedPath)).toBe(false); }); it("retries once when the first output file is empty", async () => { diff --git a/extensions/microsoft/tts.ts b/extensions/microsoft/tts.ts index c4521e2d943..82f495dea75 100644 --- a/extensions/microsoft/tts.ts +++ b/extensions/microsoft/tts.ts @@ -1,4 +1,7 @@ -import { statSync } from "node:fs"; +import { statSync, writeFileSync } from "node:fs"; +import { mkdir } from "node:fs/promises"; +import path from "node:path"; +import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; type EdgeTTSRuntimeConfig = { @@ -99,10 +102,36 @@ export async function edgeTTS( }); for (let attempt = 0; attempt < 2; attempt += 1) { - await tts.ttsPromise(text, outputPath); - if (readOutputSize(outputPath) > 0) { + const outputSize = await writeEdgeTtsOutput({ + outputPath, + ttsPromise: async (tempPath) => { + await tts.ttsPromise(text, tempPath); + }, + }); + if (outputSize > 0) { return; } } throw new Error("Edge TTS produced empty audio file after retry"); } + +async function writeEdgeTtsOutput(params: { + outputPath: string; + ttsPromise: (tempPath: string) => Promise; +}): Promise { + const rootDir = path.dirname(params.outputPath); + await mkdir(rootDir, { recursive: true }); + let outputSize = 0; + await writeExternalFileWithinRoot({ + rootDir, + path: path.basename(params.outputPath), + write: async (tempPath) => { + await params.ttsPromise(tempPath); + outputSize = readOutputSize(tempPath); + if (outputSize === 0) { + writeFileSync(tempPath, ""); + } + }, + }); + return outputSize; +} diff --git a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts index 727c0f7e6a3..e838d858df8 100644 --- a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts +++ b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts @@ -6,6 +6,7 @@ import { handleDiscordMessageAction, requestDiscord } from "@openclaw/discord/ap import { DEFAULT_EMOJIS } from "openclaw/plugin-sdk/channel-feedback"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime"; import { chromium } from "playwright-core"; import { z } from "zod"; import { startQaGatewayChild } from "../../gateway-child.js"; @@ -710,7 +711,14 @@ async function writeHtmlScreenshot(params: { htmlPath: string; screenshotPath: s waitUntil: "domcontentloaded", timeout: 15_000, }); - await page.screenshot({ path: params.screenshotPath, fullPage: true }); + await fs.mkdir(path.dirname(params.screenshotPath), { recursive: true }); + await writeExternalFileWithinRoot({ + rootDir: path.dirname(params.screenshotPath), + path: path.basename(params.screenshotPath), + write: async (tempPath) => { + await page.screenshot({ path: tempPath, fullPage: true }); + }, + }); return { screenshotPath: params.screenshotPath }; } finally { await browser.close(); diff --git a/extensions/qa-lab/src/mantis/visual-task.runtime.test.ts b/extensions/qa-lab/src/mantis/visual-task.runtime.test.ts index bcfd258906a..37d385e8089 100644 --- a/extensions/qa-lab/src/mantis/visual-task.runtime.test.ts +++ b/extensions/qa-lab/src/mantis/visual-task.runtime.test.ts @@ -79,12 +79,17 @@ describe("mantis visual task runtime", () => { ["/tmp/crabbox", "stop"], ]); const recordArgs = commands.find((entry) => entry.args[0] === "record")?.args ?? []; + const finalVideoPath = path.join( + repoRoot, + ".artifacts/qa-e2e/mantis/visual-task-test/visual-task.mp4", + ); + const stagedVideoPath = recordArgs[recordArgs.indexOf("--output") + 1]; expect(recordArgs).toEqual( expect.arrayContaining([ "--duration", "12s", "--output", - path.join(repoRoot, ".artifacts/qa-e2e/mantis/visual-task-test/visual-task.mp4"), + stagedVideoPath, "--while", "--", "pnpm", @@ -96,6 +101,9 @@ describe("mantis visual task runtime", () => { "visual-driver", ]), ); + expect(stagedVideoPath).not.toBe(finalVideoPath); + expect(path.basename(stagedVideoPath ?? "")).toBe(path.basename(finalVideoPath)); + await expect(fs.stat(stagedVideoPath ?? "")).rejects.toThrow(); await expect(fs.readFile(result.screenshotPath ?? "", "utf8")).resolves.toBe("png"); await expect(fs.readFile(result.videoPath ?? "", "utf8")).resolves.toBe("mp4"); const summary = JSON.parse(await fs.readFile(result.summaryPath, "utf8")) as { diff --git a/extensions/qa-lab/src/mantis/visual-task.runtime.ts b/extensions/qa-lab/src/mantis/visual-task.runtime.ts index 57d0e272bcf..f81d655508d 100644 --- a/extensions/qa-lab/src/mantis/visual-task.runtime.ts +++ b/extensions/qa-lab/src/mantis/visual-task.runtime.ts @@ -2,7 +2,7 @@ import { spawn, type SpawnOptions } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; -import { pathExists } from "openclaw/plugin-sdk/security-runtime"; +import { pathExists, writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime"; import { ensureRepoBoundDirectory, resolveRepoRelativeOutputDir } from "../cli-paths.js"; export type MantisVisualTaskVisionMode = "image-describe" | "metadata"; @@ -285,6 +285,30 @@ async function runCommand(params: { }); } +async function runCommandWithExternalOutput(params: { + outputPath: string; + buildArgs: (tempPath: string) => readonly string[]; + command: string; + cwd: string; + env: NodeJS.ProcessEnv; + runner: CommandRunner; + stdio?: "inherit" | "pipe"; +}) { + return await writeExternalFileWithinRoot({ + rootDir: path.dirname(params.outputPath), + path: path.basename(params.outputPath), + write: async (tempPath) => + await runCommand({ + command: params.command, + args: params.buildArgs(tempPath), + cwd: params.cwd, + env: params.env, + runner: params.runner, + stdio: params.stdio, + }), + }); +} + async function warmupCrabbox(params: { crabboxBin: string; cwd: string; @@ -629,16 +653,17 @@ export async function runMantisVisualDriver( stdio: "inherit", }); await new Promise((resolve) => setTimeout(resolve, opts.settleMs ?? DEFAULT_SETTLE_MS)); - await runCommand({ + await runCommandWithExternalOutput({ command: crabboxBin, - args: [ + outputPath: screenshotPath, + buildArgs: (tempPath) => [ "screenshot", "--provider", provider, "--id", leaseId, "--output", - screenshotPath, + tempPath, "--reclaim", ], cwd: repoRoot, @@ -777,19 +802,24 @@ export async function runMantisVisualTask( runner, }); let recordingError: string | undefined; + const activeLeaseId = leaseId; + if (!activeLeaseId) { + throw new Error("Crabbox lease id missing after warmup."); + } try { - await runCommand({ + await runCommandWithExternalOutput({ command: crabboxBin, - args: [ + outputPath: videoPath, + buildArgs: (tempPath) => [ "record", "--provider", provider, "--id", - leaseId, + activeLeaseId, "--duration", trimToValue(opts.duration) ?? DEFAULT_DURATION, "--output", - videoPath, + tempPath, "--while", "--", "pnpm", @@ -797,7 +827,7 @@ export async function runMantisVisualTask( browserUrl, crabboxBin, expectText, - leaseId, + leaseId: activeLeaseId, outputDir, provider, repoRoot, diff --git a/extensions/tts-local-cli/speech-provider.ts b/extensions/tts-local-cli/speech-provider.ts index 432e109ebe4..aece764806c 100644 --- a/extensions/tts-local-cli/speech-provider.ts +++ b/extensions/tts-local-cli/speech-provider.ts @@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"; import path from "node:path"; import { runFfmpeg } from "openclaw/plugin-sdk/media-runtime"; import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; +import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime"; import type { SpeechProviderConfig, SpeechProviderPlugin, @@ -270,36 +271,50 @@ async function convertAudio( outputDir: string, target: OutputFormat, ): Promise { - const outputPath = path.join(outputDir, `converted${getFileExt(target)}`); + const outputFileName = `converted${getFileExt(target)}`; + const outputPath = path.join(outputDir, outputFileName); const args = ["-y", "-i", inputPath]; if (target === "opus") { - args.push("-c:a", "libopus", "-b:a", "64k", outputPath); + args.push("-c:a", "libopus", "-b:a", "64k"); } else if (target === "wav") { - args.push("-c:a", "pcm_s16le", outputPath); + args.push("-c:a", "pcm_s16le"); } else { - args.push("-c:a", "libmp3lame", "-b:a", "128k", outputPath); + args.push("-c:a", "libmp3lame", "-b:a", "128k"); } - await runFfmpeg(args); + await writeExternalFileWithinRoot({ + rootDir: outputDir, + path: outputFileName, + write: async (tempPath) => { + await runFfmpeg([...args, tempPath]); + }, + }); return readFileSync(outputPath); } async function convertToRawPcm(inputPath: string, outputDir: string): Promise { // Output raw 16kHz mono 16-bit little-endian PCM (no WAV headers) - const outputPath = path.join(outputDir, "telephony.pcm"); - await runFfmpeg([ - "-y", - "-i", - inputPath, - "-c:a", - "pcm_s16le", - "-ar", - "16000", - "-ac", - "1", - "-f", - "s16le", - outputPath, - ]); + const outputFileName = "telephony.pcm"; + const outputPath = path.join(outputDir, outputFileName); + await writeExternalFileWithinRoot({ + rootDir: outputDir, + path: outputFileName, + write: async (tempPath) => { + await runFfmpeg([ + "-y", + "-i", + inputPath, + "-c:a", + "pcm_s16le", + "-ar", + "16000", + "-ac", + "1", + "-f", + "s16le", + tempPath, + ]); + }, + }); return readFileSync(outputPath); } diff --git a/extensions/whatsapp/src/outbound-media-contract.ts b/extensions/whatsapp/src/outbound-media-contract.ts index 7ed5d83529d..a40bc576d08 100644 --- a/extensions/whatsapp/src/outbound-media-contract.ts +++ b/extensions/whatsapp/src/outbound-media-contract.ts @@ -1,6 +1,7 @@ import path from "node:path"; import { MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS, runFfmpeg } from "openclaw/plugin-sdk/media-runtime"; import { sanitizeForPlainText } from "openclaw/plugin-sdk/outbound-runtime"; +import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime"; import { resolvePreferredOpenClawTmpDir, withTempWorkspace } from "openclaw/plugin-sdk/temp-path"; import { formatError } from "./session-errors.js"; import { @@ -189,29 +190,34 @@ async function transcodeToWhatsAppVoiceOpus(params: { const ext = path.extname(params.fileName).toLowerCase(); const inputExt = ext && ext.length <= 12 ? ext : ".audio"; const inputPath = await workspace.write(`input${inputExt}`, params.buffer); - const outputPath = workspace.path(WHATSAPP_VOICE_FILE_NAME); - await runFfmpeg([ - "-hide_banner", - "-loglevel", - "error", - "-y", - "-i", - inputPath, - "-vn", - "-sn", - "-dn", - "-t", - String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS), - "-ar", - String(WHATSAPP_VOICE_SAMPLE_RATE_HZ), - "-ac", - "1", - "-c:a", - "libopus", - "-b:a", - WHATSAPP_VOICE_BITRATE, - outputPath, - ]); + await writeExternalFileWithinRoot({ + rootDir: workspace.dir, + path: WHATSAPP_VOICE_FILE_NAME, + write: async (outputPath) => { + await runFfmpeg([ + "-hide_banner", + "-loglevel", + "error", + "-y", + "-i", + inputPath, + "-vn", + "-sn", + "-dn", + "-t", + String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS), + "-ar", + String(WHATSAPP_VOICE_SAMPLE_RATE_HZ), + "-ac", + "1", + "-c:a", + "libopus", + "-b:a", + WHATSAPP_VOICE_BITRATE, + outputPath, + ]); + }, + }); return await workspace.read(WHATSAPP_VOICE_FILE_NAME); }, ); diff --git a/package.json b/package.json index ede72ba12a8..540f06a98fa 100644 --- a/package.json +++ b/package.json @@ -1695,7 +1695,7 @@ "@mariozechner/pi-tui": "0.73.0", "@modelcontextprotocol/sdk": "1.29.0", "@mozilla/readability": "^0.6.0", - "@openclaw/fs-safe": "github:openclaw/fs-safe#3412e03c09cdfd31c2da04b7d74e39ad7a92d07d", + "@openclaw/fs-safe": "github:openclaw/fs-safe#ce4137f028ca4b09f26a93ffa3e0d32fbfbd516c", "@slack/bolt": "^4.7.2", "@slack/types": "^2.21.0", "@slack/web-api": "^7.15.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb0915a9093..ad6229d5872 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,8 +105,8 @@ importers: specifier: ^0.6.0 version: 0.6.0 '@openclaw/fs-safe': - specifier: github:openclaw/fs-safe#3412e03c09cdfd31c2da04b7d74e39ad7a92d07d - version: https://codeload.github.com/openclaw/fs-safe/tar.gz/3412e03c09cdfd31c2da04b7d74e39ad7a92d07d + specifier: github:openclaw/fs-safe#ce4137f028ca4b09f26a93ffa3e0d32fbfbd516c + version: https://codeload.github.com/openclaw/fs-safe/tar.gz/ce4137f028ca4b09f26a93ffa3e0d32fbfbd516c '@slack/bolt': specifier: ^4.7.2 version: 4.7.2(@types/express@5.0.6) @@ -3234,8 +3234,8 @@ packages: cpu: [x64] os: [win32] - '@openclaw/fs-safe@https://codeload.github.com/openclaw/fs-safe/tar.gz/3412e03c09cdfd31c2da04b7d74e39ad7a92d07d': - resolution: {tarball: https://codeload.github.com/openclaw/fs-safe/tar.gz/3412e03c09cdfd31c2da04b7d74e39ad7a92d07d} + '@openclaw/fs-safe@https://codeload.github.com/openclaw/fs-safe/tar.gz/ce4137f028ca4b09f26a93ffa3e0d32fbfbd516c': + resolution: {tarball: https://codeload.github.com/openclaw/fs-safe/tar.gz/ce4137f028ca4b09f26a93ffa3e0d32fbfbd516c} version: 0.1.2 engines: {node: '>=20.11'} @@ -10004,7 +10004,7 @@ snapshots: '@openai/codex@0.128.0-win32-x64': optional: true - '@openclaw/fs-safe@https://codeload.github.com/openclaw/fs-safe/tar.gz/3412e03c09cdfd31c2da04b7d74e39ad7a92d07d': + '@openclaw/fs-safe@https://codeload.github.com/openclaw/fs-safe/tar.gz/ce4137f028ca4b09f26a93ffa3e0d32fbfbd516c': optionalDependencies: jszip: 3.10.1 tar: 7.5.13 diff --git a/src/infra/fs-safe.ts b/src/infra/fs-safe.ts index 298def7cdbc..ce7eac30f18 100644 --- a/src/infra/fs-safe.ts +++ b/src/infra/fs-safe.ts @@ -45,6 +45,11 @@ export { type WalkDirectoryResult, } from "@openclaw/fs-safe/walk"; export { withTimeout } from "@openclaw/fs-safe/advanced"; +export { + writeExternalFileWithinRoot, + type ExternalFileWriteOptions, + type ExternalFileWriteResult, +} from "@openclaw/fs-safe/output"; /** @deprecated Use root(rootDir).read(relativePath, options). */ export async function readFileWithinRoot(params: { diff --git a/src/media-understanding/runner.entries.ts b/src/media-understanding/runner.entries.ts index 1a4f49dec6b..ac1027a7ad2 100644 --- a/src/media-understanding/runner.entries.ts +++ b/src/media-understanding/runner.entries.ts @@ -17,6 +17,7 @@ import type { MediaUnderstandingModelConfig, } from "../config/types.tools.js"; import { logVerbose, shouldLogVerbose } from "../globals.js"; +import { writeExternalFileWithinRoot } from "../infra/fs-safe.js"; import { resolveProxyFetchFromEnv } from "../infra/net/proxy-fetch.js"; import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; import { runFfmpeg } from "../media/ffmpeg-exec.js"; @@ -252,18 +253,25 @@ async function resolveCliMediaPath(params: { } const wavPath = path.join(params.outputDir, `${path.parse(params.mediaPath).name}.wav`); - await runFfmpeg([ - "-y", - "-i", - params.mediaPath, - "-ac", - "1", - "-ar", - "16000", - "-c:a", - "pcm_s16le", - wavPath, - ]); + await fs.mkdir(params.outputDir, { recursive: true }); + await writeExternalFileWithinRoot({ + rootDir: params.outputDir, + path: path.basename(wavPath), + write: async (outputPath) => { + await runFfmpeg([ + "-y", + "-i", + params.mediaPath, + "-ac", + "1", + "-ar", + "16000", + "-c:a", + "pcm_s16le", + outputPath, + ]); + }, + }); return wavPath; } diff --git a/src/media/audio-transcode.test.ts b/src/media/audio-transcode.test.ts index e0df3ac1911..b0c28328e17 100644 --- a/src/media/audio-transcode.test.ts +++ b/src/media/audio-transcode.test.ts @@ -94,6 +94,6 @@ describe("transcodeAudioBufferToOpus", () => { const tempRoot = realpathSync(resolvePreferredOpenClawTmpDir()); expect(capturedInputPath?.startsWith(tempRoot)).toBe(true); - expect(capturedOutputPath?.startsWith(tempRoot)).toBe(true); + expect(capturedOutputPath ? existsSync(capturedOutputPath) : true).toBe(false); }); }); diff --git a/src/media/audio-transcode.ts b/src/media/audio-transcode.ts index c512bb5b998..bd838ca0d3e 100644 --- a/src/media/audio-transcode.ts +++ b/src/media/audio-transcode.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { writeExternalFileWithinRoot } from "../infra/fs-safe.js"; import { withTempWorkspace } from "../infra/private-temp-workspace.js"; import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; import { runFfmpeg } from "./ffmpeg-exec.js"; @@ -60,31 +61,37 @@ export async function transcodeAudioBufferToOpus(params: { `input${normalizeAudioExtension(params)}`, params.audioBuffer, ); - const outputPath = workspace.path(normalizeOutputFileName(params.outputFileName)); - await runFfmpeg( - [ - "-hide_banner", - "-loglevel", - "error", - "-y", - "-i", - inputPath, - "-vn", - "-sn", - "-dn", - "-c:a", - "libopus", - "-b:a", - params.bitrate ?? DEFAULT_OPUS_BITRATE, - "-ar", - String(params.sampleRateHz ?? DEFAULT_OPUS_SAMPLE_RATE_HZ), - "-ac", - String(params.channels ?? DEFAULT_OPUS_CHANNELS), - outputPath, - ], - { timeoutMs: params.timeoutMs }, - ); - return await workspace.read(normalizeOutputFileName(params.outputFileName)); + const outputFileName = normalizeOutputFileName(params.outputFileName); + await writeExternalFileWithinRoot({ + rootDir: workspace.dir, + path: outputFileName, + write: async (outputPath) => { + await runFfmpeg( + [ + "-hide_banner", + "-loglevel", + "error", + "-y", + "-i", + inputPath, + "-vn", + "-sn", + "-dn", + "-c:a", + "libopus", + "-b:a", + params.bitrate ?? DEFAULT_OPUS_BITRATE, + "-ar", + String(params.sampleRateHz ?? DEFAULT_OPUS_SAMPLE_RATE_HZ), + "-ac", + String(params.channels ?? DEFAULT_OPUS_CHANNELS), + outputPath, + ], + { timeoutMs: params.timeoutMs }, + ); + }, + }); + return await workspace.read(outputFileName); }, ); } diff --git a/src/plugin-sdk/security-runtime.ts b/src/plugin-sdk/security-runtime.ts index 72e959b6379..ba49f952e2b 100644 --- a/src/plugin-sdk/security-runtime.ts +++ b/src/plugin-sdk/security-runtime.ts @@ -32,7 +32,10 @@ export { resolveRegularFileAppendFlags, root, statRegularFileSync, + writeExternalFileWithinRoot, withTimeout, + type ExternalFileWriteOptions, + type ExternalFileWriteResult, type FsSafeErrorCode as SafeOpenErrorCode, } from "../infra/fs-safe.js"; From f65e357e00a9b837de662b261011c8e9d84760cd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 04:42:28 +0100 Subject: [PATCH 002/215] test: fix discord external output mock typing --- extensions/discord/src/voice-message.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/discord/src/voice-message.test.ts b/extensions/discord/src/voice-message.test.ts index 20e4f966b57..feee9c0175d 100644 --- a/extensions/discord/src/voice-message.test.ts +++ b/extensions/discord/src/voice-message.test.ts @@ -72,7 +72,8 @@ describe("ensureOggOpus", () => { it("re-encodes .ogg opus when sample rate is not 48kHz", async () => { runFfprobeMock.mockResolvedValueOnce("opus,24000\n"); - runFfmpegMock.mockImplementationOnce(async (args: string[]) => { + runFfmpegMock.mockImplementationOnce(async (...callArgs: unknown[]) => { + const args = callArgs[0] as string[]; const outputPath = args.at(-1); if (typeof outputPath !== "string") { throw new Error("missing ffmpeg output path"); @@ -95,7 +96,8 @@ describe("ensureOggOpus", () => { }); it("re-encodes non-ogg input with bounded ffmpeg execution", async () => { - runFfmpegMock.mockImplementationOnce(async (args: string[]) => { + runFfmpegMock.mockImplementationOnce(async (...callArgs: unknown[]) => { + const args = callArgs[0] as string[]; const outputPath = args.at(-1); if (typeof outputPath !== "string") { throw new Error("missing ffmpeg output path"); From 9f7abf9e3a1ca8a95c6d04e6bdec3eb02b5fe335 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 04:54:59 +0100 Subject: [PATCH 003/215] build: refresh plugin sdk api baseline --- docs/.generated/plugin-sdk-api-baseline.sha256 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 84528541cbf..8d07cef5f88 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -83c7b0a2953a24cac8d576bb561948ccd70d4bac3c06d0a39814a766b7a330b6 plugin-sdk-api-baseline.json -387c0a4b34b0edd3c576658d71f13cdeb64d74bc949c36698798563de08f570d plugin-sdk-api-baseline.jsonl +bf73d3d6b83410753ee782289e4748c96d97bc76459b116e5e03c678996da360 plugin-sdk-api-baseline.json +f6a9f57d7b632391061c5bac78366bcb01318e0fde26a437e48606bdb70fe9fa plugin-sdk-api-baseline.jsonl From 96b7d9e6d8ea072f15d0ea0456efeb537d5c33bf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 05:52:35 +0100 Subject: [PATCH 004/215] fix: preserve mantis recordings on record errors (#78768) --- CHANGELOG.md | 1 + .../src/mantis/visual-task.runtime.test.ts | 87 +++++++++++++++++++ .../qa-lab/src/mantis/visual-task.runtime.ts | 38 +++++--- 3 files changed, 115 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c08f57bfaa9..b03c201b4c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,7 @@ Docs: https://docs.openclaw.ai - Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed. - Plugins/loader: preserve real compiled plugin module evaluation errors on the native fast path instead of treating every thrown `.js` module as a source-transform fallback miss. Thanks @vincentkoc. - Plugin SDK/fs-safe: expose reusable atomic replacement, sibling-temp writes, and cross-device move fallback helpers through `plugin-sdk/security-runtime`, and move OpenClaw's duplicated safe filesystem write paths onto the shared `@openclaw/fs-safe` package. +- Plugin SDK/fs-safe: route browser, media, channel, and QA external output producers through staged fs-safe writes before final publication. (#78768) - Plugin SDK/fs-safe: rename the public temp workspace helpers to `tempWorkspace`, `withTempWorkspace`, `tempWorkspaceSync`, and `withTempWorkspaceSync`, matching the cleaner `@openclaw/fs-safe` API before the package is published. - Providers/OpenRouter: add opt-in response caching params that send OpenRouter's `X-OpenRouter-Cache`, `X-OpenRouter-Cache-TTL`, and cache-clear headers only on verified OpenRouter routes. Thanks @vincentkoc. - Providers/OpenRouter: expand app-attribution categories so OpenClaw advertises coding, programming, writing, chat, and personal-agent usage on verified OpenRouter routes. Thanks @vincentkoc. diff --git a/extensions/qa-lab/src/mantis/visual-task.runtime.test.ts b/extensions/qa-lab/src/mantis/visual-task.runtime.test.ts index 37d385e8089..feceed1e25d 100644 --- a/extensions/qa-lab/src/mantis/visual-task.runtime.test.ts +++ b/extensions/qa-lab/src/mantis/visual-task.runtime.test.ts @@ -199,6 +199,93 @@ describe("mantis visual task runtime", () => { }); }); + it("preserves the video artifact when recording fails after writing output", async () => { + const commands: { args: readonly string[]; command: string }[] = []; + let stagedVideoPath = ""; + const runner = vi.fn(async (command: string, args: readonly string[]) => { + commands.push({ command, args }); + if (command === "/tmp/crabbox" && args[0] === "warmup") { + return { stdout: "ready lease cbx_abc123\n", stderr: "" }; + } + if (command === "/tmp/crabbox" && args[0] === "inspect") { + return { + stdout: `${JSON.stringify({ + id: "cbx_abc123", + provider: "hetzner", + slug: "brisk-mantis", + state: "active", + })}\n`, + stderr: "", + }; + } + if (command === "/tmp/crabbox" && args[0] === "record") { + const outputPath = args[args.indexOf("--output") + 1]; + const outputDir = args[args.indexOf("--output-dir") + 1]; + stagedVideoPath = outputPath; + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await fs.writeFile(outputPath, "mp4"); + await fs.mkdir(outputDir, { recursive: true }); + await fs.writeFile(path.join(outputDir, "visual-task.png"), "png"); + await fs.writeFile( + path.join(outputDir, "mantis-visual-task-driver-result.json"), + `${JSON.stringify({ + browserUrl: "https://example.net", + finishedAt: "2026-05-04T12:00:05.000Z", + matched: true, + outputDir, + screenshotPath: path.join(outputDir, "visual-task.png"), + startedAt: "2026-05-04T12:00:01.000Z", + status: "pass", + vision: { + mode: "metadata", + timeoutMs: 120000, + }, + })}\n`, + ); + throw new Error("crabbox record failed after writing video"); + } + return { stdout: "", stderr: "" }; + }); + + const result = await runMantisVisualTask({ + commandRunner: runner, + crabboxBin: "/tmp/crabbox", + env: { PATH: process.env.PATH }, + now: () => new Date("2026-05-04T12:00:00.000Z"), + outputDir: ".artifacts/qa-e2e/mantis/visual-task-recording-preserved", + repoRoot, + settleMs: 0, + visionMode: "metadata", + }); + + expect(result.status).toBe("fail"); + expect(result.videoPath).toBe( + path.join( + repoRoot, + ".artifacts/qa-e2e/mantis/visual-task-recording-preserved/visual-task.mp4", + ), + ); + await expect(fs.readFile(result.videoPath ?? "", "utf8")).resolves.toBe("mp4"); + await expect(fs.stat(stagedVideoPath)).rejects.toThrow(); + const summary = JSON.parse(await fs.readFile(result.summaryPath, "utf8")) as { + artifacts?: { videoPath?: string }; + error?: string; + recording?: { error?: string; required: boolean }; + status: string; + }; + expect(summary).toMatchObject({ + artifacts: { + videoPath: result.videoPath, + }, + error: "crabbox record failed after writing video", + recording: { + error: "crabbox record failed after writing video", + required: true, + }, + status: "fail", + }); + }); + it("drives a lease, screenshots it, and verifies image-describe text", async () => { const commands: { args: readonly string[]; command: string }[] = []; const runner = vi.fn(async (command: string, args: readonly string[]) => { diff --git a/extensions/qa-lab/src/mantis/visual-task.runtime.ts b/extensions/qa-lab/src/mantis/visual-task.runtime.ts index f81d655508d..64345ddb568 100644 --- a/extensions/qa-lab/src/mantis/visual-task.runtime.ts +++ b/extensions/qa-lab/src/mantis/visual-task.runtime.ts @@ -291,22 +291,36 @@ async function runCommandWithExternalOutput(params: { command: string; cwd: string; env: NodeJS.ProcessEnv; + preserveOutputOnError?: (params: { error: unknown; tempPath: string }) => Promise; runner: CommandRunner; stdio?: "inherit" | "pipe"; -}) { - return await writeExternalFileWithinRoot({ +}): Promise { + let deferredError: unknown; + await writeExternalFileWithinRoot({ rootDir: path.dirname(params.outputPath), path: path.basename(params.outputPath), - write: async (tempPath) => - await runCommand({ - command: params.command, - args: params.buildArgs(tempPath), - cwd: params.cwd, - env: params.env, - runner: params.runner, - stdio: params.stdio, - }), + write: async (tempPath) => { + try { + await runCommand({ + command: params.command, + args: params.buildArgs(tempPath), + cwd: params.cwd, + env: params.env, + runner: params.runner, + stdio: params.stdio, + }); + } catch (error) { + if (await params.preserveOutputOnError?.({ error, tempPath })) { + deferredError = error; + return; + } + throw error; + } + }, }); + if (deferredError) { + throw deferredError; + } } async function warmupCrabbox(params: { @@ -840,6 +854,8 @@ export async function runMantisVisualTask( ], cwd: repoRoot, env, + preserveOutputOnError: async ({ tempPath }) => + (await pathExists(driverResultPath)) && (await nonEmptyFileExists(tempPath)), runner, stdio: "inherit", }); From 0597e8a06598db3176f2579355427d151d071ae2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 05:59:40 +0100 Subject: [PATCH 005/215] test: align task audit fixture with lost-task projection --- src/commands/tasks.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/tasks.test.ts b/src/commands/tasks.test.ts index d6fc4f564cb..9eba1ec46cb 100644 --- a/src/commands/tasks.test.ts +++ b/src/commands/tasks.test.ts @@ -87,7 +87,7 @@ describe("tasks commands", () => { }; }; - expect(payload.summary.byCode.stale_running).toBe(1); + expect(payload.summary.byCode.lost).toBe(1); expect(payload.summary.taskFlows.byCode.stale_waiting).toBe(1); expect(payload.summary.taskFlows.byCode.missing_linked_tasks).toBe(1); expect(payload.summary.combined.total).toBe(3); From 9910cdb7a9c5e2fa7208e9be05fb2d90b6c242f3 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 22:05:45 -0700 Subject: [PATCH 006/215] test(openai): retry stalled websocket reasoning turn --- src/agents/openai-ws-stream.e2e.test.ts | 170 +++++++++++++++--------- 1 file changed, 105 insertions(+), 65 deletions(-) diff --git a/src/agents/openai-ws-stream.e2e.test.ts b/src/agents/openai-ws-stream.e2e.test.ts index 51341d92fbf..0f45d703d92 100644 --- a/src/agents/openai-ws-stream.e2e.test.ts +++ b/src/agents/openai-ws-stream.e2e.test.ts @@ -144,6 +144,37 @@ class MissingDoneEventError extends Error { } } +class WebSocketLiveAttemptTimeoutError extends Error { + constructor(label: string, timeoutMs: number) { + super(`${label} timed out after ${timeoutMs}ms`); + this.name = "WebSocketLiveAttemptTimeoutError"; + } +} + +async function withWebSocketLiveAttemptTimeout( + label: string, + timeoutMs: number, + run: () => Promise, +): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + run(), + new Promise((_, reject) => { + timer = setTimeout( + () => reject(new WebSocketLiveAttemptTimeoutError(label, timeoutMs)), + timeoutMs, + ); + timer.unref?.(); + }), + ]); + } finally { + if (timer) { + clearTimeout(timer); + } + } +} + function isTransientWebSocketLiveError(error: unknown): boolean { if (error instanceof MissingDoneEventError) { return true; @@ -351,78 +382,87 @@ describe("OpenAI WebSocket e2e", () => { async () => { let lastError: unknown; for (let attempt = 0; attempt < 2; attempt += 1) { + const sid = freshSession(`tool-reasoning-${attempt}`); try { - const sid = freshSession(`tool-reasoning-${attempt}`); - const completedResponses: ResponseObject[] = []; - openAIWsStreamModule.__testing.setDepsForTest({ - createManager: (options) => { - const manager = new openAIWsConnectionModule.OpenAIWebSocketManager(options); - manager.onMessage((event) => { - if (event.type === "response.completed") { - completedResponses.push(event.response); - } + await withWebSocketLiveAttemptTimeout( + `OpenAI WebSocket reasoning metadata attempt ${attempt + 1}`, + 75_000, + async () => { + const completedResponses: ResponseObject[] = []; + openAIWsStreamModule.__testing.setDepsForTest({ + createManager: (options) => { + const manager = new openAIWsConnectionModule.OpenAIWebSocketManager(options); + manager.onMessage((event) => { + if (event.type === "response.completed") { + completedResponses.push(event.response); + } + }); + return manager; + }, }); - return manager; + const streamFn = openAIWsStreamModule.createOpenAIWebSocketStreamFn(API_KEY!, sid); + const firstContext = makeToolContext( + "Think carefully, call the tool `noop` with {} first, then after the tool result reply with exactly TOOL_OK.", + ); + const firstDone = expectDone( + await collectEvents( + streamFn(model, firstContext, { + transport: "websocket", + toolChoice: "required", + reasoningEffort: "high", + reasoningSummary: "detailed", + maxTokens: 256, + } as unknown as StreamFnParams[2]), + ), + ); + + const firstResponse = completedResponses[0]; + expect(firstResponse).toBeDefined(); + + const rawReasoningItems = (firstResponse?.output ?? []).filter( + ( + item, + ): item is Extract => + item.type === "reasoning" || item.type.startsWith("reasoning."), + ); + const replayableReasoningItems = rawReasoningItems.filter( + (item) => typeof item.id === "string" && item.id.startsWith("rs_"), + ); + const thinkingBlocks = extractThinkingBlocks(firstDone); + expect(thinkingBlocks).toHaveLength(replayableReasoningItems.length); + expect(thinkingBlocks.map((block) => block.thinking)).toEqual( + replayableReasoningItems.map((item) => extractReasoningText(item)), + ); + expect( + thinkingBlocks.map((block) => parseReasoningSignature(block.thinkingSignature)), + ).toEqual(replayableReasoningItems.map((item) => toExpectedReasoningSignature(item))); + + const rawToolCall = firstResponse?.output.find( + (item): item is Extract => + item.type === "function_call", + ); + expect(rawToolCall).toBeDefined(); + const toolCall = extractToolCall(firstDone); + expect(toolCall?.name).toBe(rawToolCall?.name); + expect(toolCall?.id).toBe( + rawToolCall ? `${rawToolCall.call_id}|${rawToolCall.id}` : undefined, + ); + + const secondDone = await runWebsocketToolFollowupTurn({ + streamFn, + context: firstContext, + firstDone, + toolCallId: toolCall!.id, + output: "TOOL_OK", + }); + + expect(assistantText(secondDone)).toMatch(/TOOL_OK/); }, - }); - const streamFn = openAIWsStreamModule.createOpenAIWebSocketStreamFn(API_KEY!, sid); - const firstContext = makeToolContext( - "Think carefully, call the tool `noop` with {} first, then after the tool result reply with exactly TOOL_OK.", ); - const firstDone = expectDone( - await collectEvents( - streamFn(model, firstContext, { - transport: "websocket", - toolChoice: "required", - reasoningEffort: "high", - reasoningSummary: "detailed", - maxTokens: 256, - } as unknown as StreamFnParams[2]), - ), - ); - - const firstResponse = completedResponses[0]; - expect(firstResponse).toBeDefined(); - - const rawReasoningItems = (firstResponse?.output ?? []).filter( - (item): item is Extract => - item.type === "reasoning" || item.type.startsWith("reasoning."), - ); - const replayableReasoningItems = rawReasoningItems.filter( - (item) => typeof item.id === "string" && item.id.startsWith("rs_"), - ); - const thinkingBlocks = extractThinkingBlocks(firstDone); - expect(thinkingBlocks).toHaveLength(replayableReasoningItems.length); - expect(thinkingBlocks.map((block) => block.thinking)).toEqual( - replayableReasoningItems.map((item) => extractReasoningText(item)), - ); - expect( - thinkingBlocks.map((block) => parseReasoningSignature(block.thinkingSignature)), - ).toEqual(replayableReasoningItems.map((item) => toExpectedReasoningSignature(item))); - - const rawToolCall = firstResponse?.output.find( - (item): item is Extract => - item.type === "function_call", - ); - expect(rawToolCall).toBeDefined(); - const toolCall = extractToolCall(firstDone); - expect(toolCall?.name).toBe(rawToolCall?.name); - expect(toolCall?.id).toBe( - rawToolCall ? `${rawToolCall.call_id}|${rawToolCall.id}` : undefined, - ); - - const secondDone = await runWebsocketToolFollowupTurn({ - streamFn, - context: firstContext, - firstDone, - toolCallId: toolCall!.id, - output: "TOOL_OK", - }); - - expect(assistantText(secondDone)).toMatch(/TOOL_OK/); return; } catch (error) { lastError = error; + openAIWsStreamModule.releaseWsSession(sid); openAIWsStreamModule.__testing.setDepsForTest(); if (!isTransientWebSocketLiveError(error) || attempt === 1) { throw error; From 5f60479f18c505018bb3350efddf82b66ce0dbc4 Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 6 May 2026 23:48:18 +0100 Subject: [PATCH 007/215] fix: scope async model runtime hooks --- .../model.skip-pi-discovery-hooks.test.ts | 19 +++++++ src/agents/pi-embedded-runner/model.ts | 53 +++++++++++++++++-- src/agents/pi-embedded-runner/run.ts | 5 +- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/agents/pi-embedded-runner/model.skip-pi-discovery-hooks.test.ts b/src/agents/pi-embedded-runner/model.skip-pi-discovery-hooks.test.ts index 90c037c02e0..1b43374f901 100644 --- a/src/agents/pi-embedded-runner/model.skip-pi-discovery-hooks.test.ts +++ b/src/agents/pi-embedded-runner/model.skip-pi-discovery-hooks.test.ts @@ -59,6 +59,7 @@ describe("resolveModelAsync skipPiDiscovery runtime hooks", () => { it("uses only target-provider dynamic hooks", async () => { const result = await resolveModelAsync("ollama", "llama3.2:latest", "/tmp/agent", undefined, { skipPiDiscovery: true, + workspaceDir: "/tmp/workspace", }); expect(result.error).toBeUndefined(); @@ -70,8 +71,26 @@ describe("resolveModelAsync skipPiDiscovery runtime hooks", () => { expect(mocks.discoverAuthStorage).not.toHaveBeenCalled(); expect(mocks.discoverModels).not.toHaveBeenCalled(); expect(mocks.prepareProviderDynamicModel).toHaveBeenCalledTimes(1); + expect(mocks.prepareProviderDynamicModel).toHaveBeenCalledWith( + expect.objectContaining({ + workspaceDir: "/tmp/workspace", + context: expect.objectContaining({ workspaceDir: "/tmp/workspace" }), + }), + ); expect(mocks.runProviderDynamicModel).toHaveBeenCalledTimes(1); + expect(mocks.runProviderDynamicModel).toHaveBeenCalledWith( + expect.objectContaining({ + workspaceDir: "/tmp/workspace", + context: expect.objectContaining({ workspaceDir: "/tmp/workspace" }), + }), + ); expect(mocks.normalizeProviderResolvedModelWithPlugin).toHaveBeenCalledTimes(1); + expect(mocks.normalizeProviderResolvedModelWithPlugin).toHaveBeenCalledWith( + expect.objectContaining({ + workspaceDir: "/tmp/workspace", + context: expect.objectContaining({ workspaceDir: "/tmp/workspace" }), + }), + ); expect(mocks.applyProviderResolvedModelCompatWithPlugins).not.toHaveBeenCalled(); expect(mocks.applyProviderResolvedTransportWithPlugin).not.toHaveBeenCalled(); expect(mocks.normalizeProviderTransportWithPlugin).not.toHaveBeenCalled(); diff --git a/src/agents/pi-embedded-runner/model.ts b/src/agents/pi-embedded-runner/model.ts index dde23af3f62..08a31b40f45 100644 --- a/src/agents/pi-embedded-runner/model.ts +++ b/src/agents/pi-embedded-runner/model.ts @@ -155,13 +155,17 @@ function canonicalizeLegacyResolvedModel(params: { function applyResolvedTransportFallback(params: { provider: string; cfg?: OpenClawConfig; + workspaceDir?: string; runtimeHooks: ProviderRuntimeHooks; model: Model; }): Model | undefined { const normalized = params.runtimeHooks.normalizeProviderTransportWithPlugin({ provider: params.provider, config: params.cfg, + workspaceDir: params.workspaceDir, context: { + config: params.cfg, + workspaceDir: params.workspaceDir, provider: params.provider, api: params.model.api, baseUrl: params.model.baseUrl, @@ -187,6 +191,7 @@ function normalizeResolvedModel(params: { model: Model; cfg?: OpenClawConfig; agentDir?: string; + workspaceDir?: string; runtimeHooks?: ProviderRuntimeHooks; }): Model { const normalizeModelCost = (cost: unknown): Model["cost"] => { @@ -237,9 +242,11 @@ function normalizeResolvedModel(params: { const pluginNormalized = runtimeHooks.normalizeProviderResolvedModelWithPlugin({ provider: params.provider, config: params.cfg, + workspaceDir: params.workspaceDir, context: { config: params.cfg, agentDir: params.agentDir, + workspaceDir: params.workspaceDir, provider: params.provider, modelId: normalizedInputModel.id, model: normalizedInputModel, @@ -248,9 +255,11 @@ function normalizeResolvedModel(params: { const compatNormalized = runtimeHooks.applyProviderResolvedModelCompatWithPlugins?.({ provider: params.provider, config: params.cfg, + workspaceDir: params.workspaceDir, context: { config: params.cfg, agentDir: params.agentDir, + workspaceDir: params.workspaceDir, provider: params.provider, modelId: normalizedInputModel.id, model: (pluginNormalized ?? normalizedInputModel) as never, @@ -259,9 +268,11 @@ function normalizeResolvedModel(params: { const transportNormalized = runtimeHooks.applyProviderResolvedTransportWithPlugin?.({ provider: params.provider, config: params.cfg, + workspaceDir: params.workspaceDir, context: { config: params.cfg, agentDir: params.agentDir, + workspaceDir: params.workspaceDir, provider: params.provider, modelId: normalizedInputModel.id, model: (compatNormalized ?? pluginNormalized ?? normalizedInputModel) as never, @@ -272,6 +283,7 @@ function normalizeResolvedModel(params: { applyResolvedTransportFallback({ provider: params.provider, cfg: params.cfg, + workspaceDir: params.workspaceDir, runtimeHooks, model: compatNormalized ?? pluginNormalized ?? normalizedInputModel, }); @@ -290,6 +302,7 @@ function resolveProviderTransport(params: { api?: Api | null; baseUrl?: string; cfg?: OpenClawConfig; + workspaceDir?: string; runtimeHooks?: ProviderRuntimeHooks; }): { api?: Api; @@ -299,7 +312,10 @@ function resolveProviderTransport(params: { const normalized = runtimeHooks.normalizeProviderTransportWithPlugin({ provider: params.provider, config: params.cfg, + workspaceDir: params.workspaceDir, context: { + config: params.cfg, + workspaceDir: params.workspaceDir, provider: params.provider, api: params.api, baseUrl: params.baseUrl, @@ -499,6 +515,7 @@ function applyConfiguredProviderOverrides(params: { cfg?: OpenClawConfig; runtimeHooks?: ProviderRuntimeHooks; preferDiscoveredModelMetadata?: boolean; + workspaceDir?: string; }): ProviderRuntimeModel { const { discoveredModel, providerConfig, modelId } = params; const requestTimeoutMs = resolveProviderRequestTimeoutMs(providerConfig?.timeoutSeconds); @@ -582,6 +599,7 @@ function applyConfiguredProviderOverrides(params: { resolveConfiguredProviderDefaultApi(providerConfig), baseUrl: providerConfig.baseUrl ?? discoveredModel.baseUrl, cfg: params.cfg, + workspaceDir: params.workspaceDir, runtimeHooks: params.runtimeHooks, }); const resolvedContextWindow = @@ -635,9 +653,10 @@ function resolveExplicitModelWithRegistry(params: { modelRegistry: ModelRegistry; cfg?: OpenClawConfig; agentDir?: string; + workspaceDir?: string; runtimeHooks?: ProviderRuntimeHooks; }): { kind: "resolved"; model: Model } | { kind: "suppressed" } | undefined { - const { provider, modelId, modelRegistry, cfg, agentDir, runtimeHooks } = params; + const { provider, modelId, modelRegistry, cfg, agentDir, workspaceDir, runtimeHooks } = params; const providerConfig = resolveConfiguredProviderConfig(cfg, provider); const requestTimeoutMs = resolveProviderRequestTimeoutMs(providerConfig?.timeoutSeconds); const inlineMatch = findInlineModelMatch({ @@ -666,6 +685,7 @@ function resolveExplicitModelWithRegistry(params: { provider, cfg, agentDir, + workspaceDir, model: { ...inlineMatch, ...(resolvedParams ? { params: resolvedParams } : {}), @@ -701,8 +721,10 @@ function resolveExplicitModelWithRegistry(params: { modelId, cfg, runtimeHooks, + workspaceDir, }), runtimeHooks, + workspaceDir, }), }; } @@ -726,6 +748,7 @@ function resolveExplicitModelWithRegistry(params: { provider, cfg, agentDir, + workspaceDir, model: { ...fallbackInlineMatch, ...(resolvedParams ? { params: resolvedParams } : {}), @@ -766,6 +789,7 @@ function resolvePluginDynamicModelWithRegistry(params: { context: { config: cfg, agentDir, + workspaceDir, provider, modelId, modelRegistry, @@ -782,12 +806,14 @@ function resolvePluginDynamicModelWithRegistry(params: { modelId, cfg, runtimeHooks, + workspaceDir, preferDiscoveredModelMetadata, }); return normalizeResolvedModel({ provider, cfg, agentDir, + workspaceDir, model: overriddenDynamicModel, runtimeHooks, }); @@ -798,9 +824,10 @@ function resolveConfiguredFallbackModel(params: { modelId: string; cfg?: OpenClawConfig; agentDir?: string; + workspaceDir?: string; runtimeHooks?: ProviderRuntimeHooks; }): Model | undefined { - const { provider, modelId, cfg, agentDir, runtimeHooks } = params; + const { provider, modelId, cfg, agentDir, workspaceDir, runtimeHooks } = params; const providerConfig = resolveConfiguredProviderConfig(cfg, provider); const requestTimeoutMs = resolveProviderRequestTimeoutMs(providerConfig?.timeoutSeconds); const configuredModel = findConfiguredProviderModel(providerConfig, provider, modelId); @@ -825,6 +852,7 @@ function resolveConfiguredFallbackModel(params: { api: resolveConfiguredProviderDefaultApi(providerConfig) ?? "openai-responses", baseUrl: providerConfig?.baseUrl, cfg, + workspaceDir, runtimeHooks, }); const requestConfig = resolveProviderRequestConfig({ @@ -842,6 +870,7 @@ function resolveConfiguredFallbackModel(params: { provider, cfg, agentDir, + workspaceDir, model: attachModelProviderRequestTransport( { id: modelId, @@ -921,6 +950,7 @@ export function resolveModelWithRegistry(params: { modelRegistry: ModelRegistry; cfg?: OpenClawConfig; agentDir?: string; + workspaceDir?: string; runtimeHooks?: ProviderRuntimeHooks; }): Model | undefined { const normalizedRef = { @@ -933,7 +963,8 @@ export function resolveModelWithRegistry(params: { modelId: normalizedRef.model, }; const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS; - const workspaceDir = normalizedParams.cfg?.agents?.defaults?.workspace; + const workspaceDir = + normalizedParams.workspaceDir ?? normalizedParams.cfg?.agents?.defaults?.workspace; const explicitModel = resolveExplicitModelWithRegistry(normalizedParams); if (explicitModel?.kind === "suppressed") { return undefined; @@ -978,6 +1009,7 @@ export function resolveModel( modelRegistry?: ModelRegistry; runtimeHooks?: ProviderRuntimeHooks; skipProviderRuntimeHooks?: boolean; + workspaceDir?: string; }, ): { model?: Model; @@ -990,6 +1022,7 @@ export function resolveModel( model: normalizeStaticProviderModelId(normalizeProviderId(provider), modelId), }; const resolvedAgentDir = agentDir ?? resolveDefaultAgentDir(cfg ?? {}); + const workspaceDir = options?.workspaceDir ?? cfg?.agents?.defaults?.workspace; const authStorage = options?.authStorage ?? discoverAuthStorage(resolvedAgentDir); const modelRegistry = options?.modelRegistry ?? discoverModels(authStorage, resolvedAgentDir); const runtimeHooks = resolveRuntimeHooks(options); @@ -999,6 +1032,7 @@ export function resolveModel( modelRegistry, cfg, agentDir: resolvedAgentDir, + workspaceDir, runtimeHooks, }); if (model) { @@ -1011,6 +1045,7 @@ export function resolveModel( modelId: normalizedRef.model, cfg, agentDir: resolvedAgentDir, + workspaceDir, runtimeHooks, }), authStorage, @@ -1030,6 +1065,7 @@ export async function resolveModelAsync( runtimeHooks?: ProviderRuntimeHooks; skipProviderRuntimeHooks?: boolean; skipPiDiscovery?: boolean; + workspaceDir?: string; }, ): Promise<{ model?: Model; @@ -1042,6 +1078,7 @@ export async function resolveModelAsync( model: normalizeStaticProviderModelId(normalizeProviderId(provider), modelId), }; const resolvedAgentDir = agentDir ?? resolveDefaultAgentDir(cfg ?? {}); + const workspaceDir = options?.workspaceDir ?? cfg?.agents?.defaults?.workspace; const emptyDiscoveryStores = options?.skipPiDiscovery && (!options.authStorage || !options.modelRegistry) ? createEmptyPiDiscoveryStores() @@ -1061,6 +1098,7 @@ export async function resolveModelAsync( modelRegistry, cfg, agentDir: resolvedAgentDir, + workspaceDir, runtimeHooks, }); if (explicitModel?.kind === "suppressed") { @@ -1070,6 +1108,7 @@ export async function resolveModelAsync( modelId: normalizedRef.model, cfg, agentDir: resolvedAgentDir, + workspaceDir, runtimeHooks, }), authStorage, @@ -1081,9 +1120,11 @@ export async function resolveModelAsync( await runtimeHooks.prepareProviderDynamicModel({ provider: normalizedRef.provider, config: cfg, + workspaceDir, context: { config: cfg, agentDir: resolvedAgentDir, + workspaceDir, provider: normalizedRef.provider, modelId: normalizedRef.model, modelRegistry, @@ -1096,6 +1137,7 @@ export async function resolveModelAsync( modelRegistry, cfg, agentDir: resolvedAgentDir, + workspaceDir, runtimeHooks, }); }; @@ -1106,6 +1148,7 @@ export async function resolveModelAsync( modelId: normalizedRef.model, cfg, agentDir: resolvedAgentDir, + workspaceDir, runtimeHooks, }) ? explicitModel.model @@ -1126,6 +1169,7 @@ export async function resolveModelAsync( modelId: normalizedRef.model, cfg, agentDir: resolvedAgentDir, + workspaceDir, runtimeHooks, }), authStorage, @@ -1148,6 +1192,7 @@ function buildUnknownModelError(params: { modelId: string; cfg?: OpenClawConfig; agentDir?: string; + workspaceDir?: string; runtimeHooks?: ProviderRuntimeHooks; }): string { const suppressed = buildSuppressedBuiltInModelError({ @@ -1163,10 +1208,12 @@ function buildUnknownModelError(params: { const hint = runtimeHooks.buildProviderUnknownModelHintWithPlugin({ provider: params.provider, config: params.cfg, + workspaceDir: params.workspaceDir, env: process.env, context: { config: params.cfg, agentDir: params.agentDir, + workspaceDir: params.workspaceDir, env: process.env, provider: params.provider, modelId: params.modelId, diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index b4880591cb5..c6a573b3af3 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -514,6 +514,7 @@ export async function runEmbeddedPiAgent( // first generating PI models.json. This keeps one-shot model runs from // blocking on unrelated provider discovery. skipPiDiscovery: true, + workspaceDir: resolvedWorkspace, }, ); const modelResolution = @@ -523,7 +524,9 @@ export async function runEmbeddedPiAgent( await ensureOpenClawModelsJson(params.config, agentDir, { workspaceDir: resolvedWorkspace, }); - return await resolveModelAsync(provider, modelId, agentDir, params.config); + return await resolveModelAsync(provider, modelId, agentDir, params.config, { + workspaceDir: resolvedWorkspace, + }); })(); const { model, error, authStorage, modelRegistry } = modelResolution; if (!model) { From 1235f7f981683497ec59bb3f583601ed12ba2fa1 Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 6 May 2026 23:52:36 +0100 Subject: [PATCH 008/215] perf: reuse compatible auto-enable metadata --- src/config/plugin-auto-enable.core.test.ts | 84 ++++++++++++++++++++++ src/config/plugin-auto-enable.shared.ts | 14 +++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/config/plugin-auto-enable.core.test.ts b/src/config/plugin-auto-enable.core.test.ts index 05c04d9acd3..3af4eac8cc8 100644 --- a/src/config/plugin-auto-enable.core.test.ts +++ b/src/config/plugin-auto-enable.core.test.ts @@ -1,6 +1,10 @@ import fs from "node:fs"; import path from "node:path"; import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; +import { setCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; +import { resolveInstalledPluginIndexPolicyHash } from "../plugins/installed-plugin-index-policy.js"; +import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; +import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; import { applyPluginAutoEnable, detectPluginAutoEnableCandidates, @@ -58,6 +62,53 @@ vi.mock("../plugins/setup-registry.js", () => ({ const env = makeIsolatedEnv(); +function createPluginMetadataSnapshot(params: { + config?: OpenClawConfig; + manifestRegistry: PluginManifestRegistry; + workspaceDir?: string; +}): PluginMetadataSnapshot { + const policyHash = resolveInstalledPluginIndexPolicyHash(params.config); + return { + policyHash, + ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), + index: { + version: 1, + hostContractVersion: "test", + compatRegistryVersion: "test", + migrationVersion: 1, + policyHash, + generatedAtMs: 1, + installRecords: {}, + plugins: [], + diagnostics: [], + }, + registryDiagnostics: [], + manifestRegistry: params.manifestRegistry, + plugins: params.manifestRegistry.plugins, + diagnostics: params.manifestRegistry.diagnostics, + byPluginId: new Map(params.manifestRegistry.plugins.map((plugin) => [plugin.id, plugin])), + normalizePluginId: (pluginId) => pluginId, + owners: { + channels: new Map(), + channelConfigs: new Map(), + providers: new Map(), + modelCatalogProviders: new Map(), + cliBackends: new Map(), + setupProviders: new Map(), + commandAliases: new Map(), + contracts: new Map(), + }, + metrics: { + registrySnapshotMs: 0, + manifestRegistryMs: 0, + ownerMapsMs: 0, + totalMs: 0, + indexPluginCount: 0, + manifestPluginCount: params.manifestRegistry.plugins.length, + }, + }; +} + afterAll(() => { resetPluginAutoEnableTestState(); }); @@ -84,6 +135,39 @@ describe("applyPluginAutoEnable core", () => { ]); }); + it("reuses policy-compatible current manifest registry when runtime config differs", () => { + const manifestRegistry = makeRegistry([{ id: "custom-chat", channels: ["custom-chat"] }]); + const snapshotConfig: OpenClawConfig = { plugins: { allow: ["existing"] } }; + setCurrentPluginMetadataSnapshot( + createPluginMetadataSnapshot({ + config: snapshotConfig, + manifestRegistry, + workspaceDir: "/tmp/workspace", + }), + { + config: snapshotConfig, + workspaceDir: "/tmp/workspace", + }, + ); + + const result = applyPluginAutoEnable({ + config: { + plugins: { + allow: ["existing"], + entries: { + "custom-chat": { config: { token: "x" } }, + }, + }, + }, + env, + }); + + expect(result.config.plugins?.allow).toContain("custom-chat"); + expect(result.changes).toContain( + "custom-chat plugin config present, added to plugin allowlist.", + ); + }); + it("formats typed provider-auth candidates into stable reasons", () => { expect( resolvePluginAutoEnableCandidateReason({ diff --git a/src/config/plugin-auto-enable.shared.ts b/src/config/plugin-auto-enable.shared.ts index df27178c228..5aa0e85d4b8 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -10,6 +10,7 @@ import { } from "../channels/plugins/configured-state.js"; import { getChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js"; import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; +import { resolveInstalledPluginIndexPolicyHash } from "../plugins/installed-plugin-index-policy.js"; import { type PluginManifestRecord, type PluginManifestRegistry, @@ -951,8 +952,19 @@ export function resolvePluginAutoEnableManifestRegistry(params: { env: params.env, allowWorkspaceScopedSnapshot: true, }); + const policyCompatibleCurrentSnapshot = + currentSnapshot ?? + (() => { + const snapshot = getCurrentPluginMetadataSnapshot({ + env: params.env, + allowWorkspaceScopedSnapshot: true, + }); + return snapshot?.policyHash === resolveInstalledPluginIndexPolicyHash(params.config) + ? snapshot + : undefined; + })(); return ( - currentSnapshot?.manifestRegistry ?? + policyCompatibleCurrentSnapshot?.manifestRegistry ?? loadPluginMetadataSnapshot({ config: params.config, env: params.env, From e8efb7339eb00dfd681ed4dcd26d71ffd3ae0ff6 Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 6 May 2026 23:58:36 +0100 Subject: [PATCH 009/215] perf: reuse metadata across gateway runtime config --- src/gateway/server.impl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index b5a823efcb2..846d2633083 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -657,7 +657,7 @@ export async function startGatewayServer( } = pluginBootstrap; setCurrentPluginMetadataSnapshot(pluginLookUpTable, { config: startupActivationSourceConfig, - compatibleConfigs: [gatewayPluginConfigAtStart], + compatibleConfigs: [startupRuntimeConfig, cfgAtStart, gatewayPluginConfigAtStart], env: process.env, workspaceDir: defaultWorkspaceDir, }); From 6cc43236994e0d26dc1f7b33f92ac64fc9e39631 Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 00:02:15 +0100 Subject: [PATCH 010/215] perf: reuse activation metadata registry --- src/plugins/activation-context.test.ts | 108 +++++++++++++++++++++++++ src/plugins/activation-context.ts | 39 ++++++++- 2 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 src/plugins/activation-context.test.ts diff --git a/src/plugins/activation-context.test.ts b/src/plugins/activation-context.test.ts new file mode 100644 index 00000000000..0e7d364e5f1 --- /dev/null +++ b/src/plugins/activation-context.test.ts @@ -0,0 +1,108 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { makeRegistry } from "../config/plugin-auto-enable.test-helpers.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { + clearCurrentPluginMetadataSnapshot, + setCurrentPluginMetadataSnapshot, +} from "./current-plugin-metadata-snapshot.js"; +import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js"; +import type { PluginManifestRegistry } from "./manifest-registry.js"; +import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; + +const applyPluginAutoEnableMock = vi.hoisted(() => + vi.fn((params: { config?: OpenClawConfig }) => ({ + config: params.config, + changes: [], + autoEnabledReasons: {}, + })), +); + +vi.mock("../config/plugin-auto-enable.js", () => ({ + applyPluginAutoEnable: applyPluginAutoEnableMock, +})); + +import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js"; + +function createPluginMetadataSnapshot(params: { + config?: OpenClawConfig; + manifestRegistry: PluginManifestRegistry; + workspaceDir?: string; +}): PluginMetadataSnapshot { + const policyHash = resolveInstalledPluginIndexPolicyHash(params.config); + return { + policyHash, + ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), + index: { + version: 1, + hostContractVersion: "test", + compatRegistryVersion: "test", + migrationVersion: 1, + policyHash, + generatedAtMs: 1, + installRecords: {}, + plugins: [], + diagnostics: [], + }, + registryDiagnostics: [], + manifestRegistry: params.manifestRegistry, + plugins: params.manifestRegistry.plugins, + diagnostics: params.manifestRegistry.diagnostics, + byPluginId: new Map(params.manifestRegistry.plugins.map((plugin) => [plugin.id, plugin])), + normalizePluginId: (pluginId) => pluginId, + owners: { + channels: new Map(), + channelConfigs: new Map(), + providers: new Map(), + modelCatalogProviders: new Map(), + cliBackends: new Map(), + setupProviders: new Map(), + commandAliases: new Map(), + contracts: new Map(), + }, + metrics: { + registrySnapshotMs: 0, + manifestRegistryMs: 0, + ownerMapsMs: 0, + totalMs: 0, + indexPluginCount: 0, + manifestPluginCount: params.manifestRegistry.plugins.length, + }, + }; +} + +afterEach(() => { + clearCurrentPluginMetadataSnapshot(); + applyPluginAutoEnableMock.mockClear(); +}); + +describe("resolveBundledPluginCompatibleActivationInputs", () => { + it("passes the current manifest registry into activation auto-enable", () => { + const manifestRegistry = makeRegistry([{ id: "openai", channels: [], providers: ["openai"] }]); + const workspaceDir = "/tmp/openclaw-activation-workspace"; + setCurrentPluginMetadataSnapshot( + createPluginMetadataSnapshot({ + config: {}, + manifestRegistry, + workspaceDir, + }), + { + config: {}, + workspaceDir, + }, + ); + + resolveBundledPluginCompatibleActivationInputs({ + rawConfig: { plugins: { allow: ["openai"] } }, + workspaceDir, + applyAutoEnable: true, + compatMode: {}, + resolveCompatPluginIds: () => [], + }); + + expect(applyPluginAutoEnableMock).toHaveBeenCalledWith({ + config: { plugins: { allow: ["openai"] } }, + env: process.env, + manifestRegistry, + }); + }); +}); diff --git a/src/plugins/activation-context.ts b/src/plugins/activation-context.ts index 851a390f2bc..266270a623e 100644 --- a/src/plugins/activation-context.ts +++ b/src/plugins/activation-context.ts @@ -11,6 +11,7 @@ import { type NormalizedPluginsConfig, type PluginActivationConfigSource, } from "./config-state.js"; +import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js"; export type PluginActivationCompatConfig = { allowlistPluginIds?: readonly string[]; @@ -170,11 +171,39 @@ function createBundledPluginCompatConfig(params: { }; } +function applyPluginAutoEnableForActivation(params: { + config: OpenClawConfig; + env: NodeJS.ProcessEnv; + workspaceDir?: string; +}) { + const currentSnapshot = getCurrentPluginMetadataSnapshot({ + config: params.config, + env: params.env, + workspaceDir: params.workspaceDir, + allowWorkspaceScopedSnapshot: true, + }); + const currentManifestRegistry = + currentSnapshot?.manifestRegistry ?? + (normalizePluginsConfig(params.config.plugins).loadPaths.length === 0 + ? getCurrentPluginMetadataSnapshot({ + env: params.env, + workspaceDir: params.workspaceDir, + allowWorkspaceScopedSnapshot: true, + })?.manifestRegistry + : undefined); + return applyPluginAutoEnable({ + config: params.config, + env: params.env, + ...(currentManifestRegistry ? { manifestRegistry: currentManifestRegistry } : {}), + }); +} + export function resolvePluginActivationSnapshot(params: { rawConfig?: OpenClawConfig; resolvedConfig?: OpenClawConfig; autoEnabledReasons?: Record; env?: NodeJS.ProcessEnv; + workspaceDir?: string; applyAutoEnable?: boolean; }): PluginActivationSnapshot { const env = params.env ?? process.env; @@ -183,9 +212,10 @@ export function resolvePluginActivationSnapshot(params: { let autoEnabledReasons = params.autoEnabledReasons; if (params.applyAutoEnable && rawConfig !== undefined) { - const autoEnabled = applyPluginAutoEnable({ + const autoEnabled = applyPluginAutoEnableForActivation({ config: rawConfig, env, + workspaceDir: params.workspaceDir, }); resolvedConfig = autoEnabled.config; autoEnabledReasons = autoEnabled.autoEnabledReasons; @@ -208,6 +238,7 @@ export function resolvePluginActivationInputs(params: { resolvedConfig?: OpenClawConfig; autoEnabledReasons?: Record; env?: NodeJS.ProcessEnv; + workspaceDir?: string; compat?: PluginActivationCompatConfig; applyAutoEnable?: boolean; }): PluginActivationInputs { @@ -217,6 +248,7 @@ export function resolvePluginActivationInputs(params: { resolvedConfig: params.resolvedConfig, autoEnabledReasons: params.autoEnabledReasons, env, + workspaceDir: params.workspaceDir, applyAutoEnable: params.applyAutoEnable, }); const config = applyPluginCompatibilityOverrides({ @@ -243,6 +275,7 @@ export function resolveBundledPluginCompatibleActivationInputs( resolvedConfig: params.resolvedConfig, autoEnabledReasons: params.autoEnabledReasons, env: params.env, + workspaceDir: params.workspaceDir, applyAutoEnable: params.applyAutoEnable, }); const allowlistCompatEnabled = params.compatMode.allowlist === true; @@ -263,6 +296,7 @@ export function resolveBundledPluginCompatibleActivationInputs( resolvedConfig: snapshot.config, autoEnabledReasons: snapshot.autoEnabledReasons, env: params.env, + workspaceDir: params.workspaceDir, compat: createBundledPluginCompatConfig({ compatMode: params.compatMode, allowlistCompatEnabled, @@ -285,9 +319,10 @@ export function resolveBundledPluginCompatibleLoadValues( let autoEnabledReasons = params.autoEnabledReasons ?? {}; if (params.applyAutoEnable && rawConfig !== undefined) { - const autoEnabled = applyPluginAutoEnable({ + const autoEnabled = applyPluginAutoEnableForActivation({ config: rawConfig, env, + workspaceDir: params.workspaceDir, }); resolvedConfig = autoEnabled.config; autoEnabledReasons = autoEnabled.autoEnabledReasons; From fb49bcaf217b4fd372f879f60e0313c4d361cbd4 Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 00:04:46 +0100 Subject: [PATCH 011/215] perf: reuse metadata for auth lookups --- .../model-auth-env.provider-aliases.test.ts | 1 + src/agents/provider-auth-aliases.ts | 32 +++++++++++-------- src/secrets/provider-env-vars.ts | 12 +++++++ src/secrets/target-registry-data.ts | 19 ++++++++--- 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/agents/model-auth-env.provider-aliases.test.ts b/src/agents/model-auth-env.provider-aliases.test.ts index 3439bbe2bbd..c99992f7cae 100644 --- a/src/agents/model-auth-env.provider-aliases.test.ts +++ b/src/agents/model-auth-env.provider-aliases.test.ts @@ -78,6 +78,7 @@ describe("resolveEnvApiKey provider auth aliases", () => { EXTERNAL_CLOUD_API_KEY: "secret", }, workspaceDir: "/workspace", + allowWorkspaceScopedSnapshot: true, }); }); }); diff --git a/src/agents/provider-auth-aliases.ts b/src/agents/provider-auth-aliases.ts index 33b7e541e4a..e025b503e4f 100644 --- a/src/agents/provider-auth-aliases.ts +++ b/src/agents/provider-auth-aliases.ts @@ -1,4 +1,5 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { normalizePluginsConfig } from "../plugins/config-state.js"; import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; import type { PluginManifestRecord } from "../plugins/manifest-registry.js"; import { @@ -121,21 +122,24 @@ export function resolveProviderAuthAliasMap( } const config = params?.config ?? {}; const snapshot = - params?.workspaceDir !== undefined - ? (getCurrentPluginMetadataSnapshot({ - config, - workspaceDir: params.workspaceDir, + getCurrentPluginMetadataSnapshot({ + config, + ...(params?.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), + env, + allowWorkspaceScopedSnapshot: true, + }) ?? + (normalizePluginsConfig(config.plugins).loadPaths.length === 0 + ? getCurrentPluginMetadataSnapshot({ + ...(params?.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), env, - }) ?? - loadPluginMetadataSnapshot({ - config, - workspaceDir: params.workspaceDir, - env, - })) - : loadPluginMetadataSnapshot({ - config, - env, - }); + allowWorkspaceScopedSnapshot: true, + }) + : undefined) ?? + loadPluginMetadataSnapshot({ + config, + ...(params?.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), + env, + }); const preferredAliases = new Map(); const aliases: Record = Object.create(null) as Record; for (const plugin of snapshot.plugins) { diff --git a/src/secrets/provider-env-vars.ts b/src/secrets/provider-env-vars.ts index 22de99e7cdb..ef6e2d9524a 100644 --- a/src/secrets/provider-env-vars.ts +++ b/src/secrets/provider-env-vars.ts @@ -1,5 +1,6 @@ import { resolveProviderAuthAliasMap } from "../agents/provider-auth-aliases.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { normalizePluginsConfig } from "../plugins/config-state.js"; import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; import { isInstalledPluginEnabled } from "../plugins/installed-plugin-index.js"; import type { PluginManifestRecord } from "../plugins/manifest-registry.js"; @@ -128,10 +129,21 @@ function resolveProviderMetadataSnapshot( config, env, ...(params?.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), + allowWorkspaceScopedSnapshot: true, }); if (current) { return current; } + if (normalizePluginsConfig(config.plugins).loadPaths.length === 0) { + const unscopedCurrent = getCurrentPluginMetadataSnapshot({ + env, + ...(params?.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), + allowWorkspaceScopedSnapshot: true, + }); + if (unscopedCurrent) { + return unscopedCurrent; + } + } return loadPluginMetadataSnapshot({ config, workspaceDir: params?.workspaceDir, diff --git a/src/secrets/target-registry-data.ts b/src/secrets/target-registry-data.ts index 23a26b8a620..218a20cc4d8 100644 --- a/src/secrets/target-registry-data.ts +++ b/src/secrets/target-registry-data.ts @@ -1,3 +1,4 @@ +import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; import type { PluginManifestRecord } from "../plugins/manifest-registry.js"; import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; import { loadChannelSecretContractApiForRecord } from "./channel-contract-api.js"; @@ -445,11 +446,19 @@ function loadSecretTargetRegistryFromPluginMetadata(params: { env: NodeJS.ProcessEnv; preferPersisted?: boolean; }): SecretTargetRegistryEntry[] { - const plugins = loadPluginMetadataSnapshot({ - config: {}, - env: params.env, - ...(params.preferPersisted !== undefined ? { preferPersisted: params.preferPersisted } : {}), - }).plugins; + const plugins = + (params.preferPersisted === false + ? undefined + : getCurrentPluginMetadataSnapshot({ + env: params.env, + allowWorkspaceScopedSnapshot: true, + }) + )?.plugins ?? + loadPluginMetadataSnapshot({ + config: {}, + env: params.env, + ...(params.preferPersisted !== undefined ? { preferPersisted: params.preferPersisted } : {}), + }).plugins; const bundledPlugins = plugins.filter((record) => record.origin === "bundled"); const channelPlugins = plugins.filter((record) => record.channels.length > 0); return [ From 111cef04ca062a78be1dcc6bd49777a73c810ee7 Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 00:06:52 +0100 Subject: [PATCH 012/215] perf: reuse metadata for bundle settings --- src/agents/pi-project-settings-snapshot.ts | 15 +++++- src/agents/pi-project-settings.bundle.test.ts | 50 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/agents/pi-project-settings-snapshot.ts b/src/agents/pi-project-settings-snapshot.ts index d1ca35eefed..d4d06b27374 100644 --- a/src/agents/pi-project-settings-snapshot.ts +++ b/src/agents/pi-project-settings-snapshot.ts @@ -9,6 +9,7 @@ import { normalizePluginsConfigWithResolver, resolveEffectivePluginActivationState, } from "../plugins/config-policy.js"; +import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; import { isPluginMetadataSnapshotCompatible, loadPluginMetadataSnapshot, @@ -84,11 +85,21 @@ export function loadEnabledBundlePiSettingsSnapshot(params: { workspaceDir, }) ? providedSnapshot - : loadPluginMetadataSnapshot({ + : (getCurrentPluginMetadataSnapshot({ + config, + env, + workspaceDir, + }) ?? + getCurrentPluginMetadataSnapshot({ + env, + workspaceDir, + allowWorkspaceScopedSnapshot: true, + }) ?? + loadPluginMetadataSnapshot({ workspaceDir, config, env, - }); + })); const registry = metadataSnapshot.manifestRegistry; if (registry.plugins.length === 0) { return {}; diff --git a/src/agents/pi-project-settings.bundle.test.ts b/src/agents/pi-project-settings.bundle.test.ts index e54682c7053..7c0e3d61257 100644 --- a/src/agents/pi-project-settings.bundle.test.ts +++ b/src/agents/pi-project-settings.bundle.test.ts @@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { createTrackedTempDirs } from "../test-utils/tracked-temp-dirs.js"; const pluginMetadataSnapshotMocks = vi.hoisted(() => ({ + getCurrentPluginMetadataSnapshot: vi.fn(), isPluginMetadataSnapshotCompatible: vi.fn(), loadPluginMetadataSnapshot: vi.fn(), })); @@ -84,6 +85,10 @@ vi.mock("../plugins/plugin-registry.js", async () => { }; }); +vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({ + getCurrentPluginMetadataSnapshot: pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot, +})); + vi.mock("../plugins/plugin-metadata-snapshot.js", async () => { const fs = await import("node:fs"); const path = await import("node:path"); @@ -172,6 +177,8 @@ const tempDirs = createTrackedTempDirs(); afterEach(async () => { await tempDirs.cleanup(); + pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot.mockReset(); + pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot.mockReturnValue(undefined); pluginMetadataSnapshotMocks.isPluginMetadataSnapshotCompatible.mockClear(); pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear(); }); @@ -275,6 +282,49 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce(); }); + it("reuses the current plugin metadata snapshot for bundle settings", async () => { + const workspaceDir = await tempDirs.make("openclaw-workspace-"); + const pluginRoot = await createWorkspaceBundle({ workspaceDir }); + const resolvedPluginRoot = await fs.realpath(pluginRoot); + await fs.writeFile( + path.join(pluginRoot, "settings.json"), + JSON.stringify({ hideThinkingBlock: true }), + "utf-8", + ); + + pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot.mockReturnValueOnce({ + manifestRegistry: { + diagnostics: [], + plugins: [ + { + id: "claude-bundle", + origin: "workspace", + format: "bundle", + bundleFormat: "claude", + settingsFiles: ["settings.json"], + rootDir: resolvedPluginRoot, + }, + ], + }, + normalizePluginId: (id: string) => id.trim(), + }); + pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear(); + + const snapshot = loadEnabledBundlePiSettingsSnapshot({ + cwd: workspaceDir, + cfg: { + plugins: { + entries: { + "claude-bundle": { enabled: true }, + }, + }, + }, + }); + + expect(snapshot.hideThinkingBlock).toBe(true); + expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled(); + }); + it("loads sanitized settings and MCP defaults from enabled bundle plugins", async () => { const workspaceDir = await tempDirs.make("openclaw-workspace-"); const pluginRoot = await createWorkspaceBundle({ workspaceDir }); From 597dcb15c03100bb9e151947da0a470bd55dd0f6 Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 00:08:59 +0100 Subject: [PATCH 013/215] docs: record dashboard metadata scan reduction --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b03c201b4c8..6d2331498e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ Docs: https://docs.openclaw.ai - Docs: clarify that IRC uses raw TCP/TLS sockets outside operator-managed forward proxy routing, so direct IRC egress should be explicitly approved before enabling IRC. Thanks @jesse-merhi. - Gateway/performance: defer non-readiness sidecars until after the ready signal, avoid hot-path channel plugin barrel imports, and fast-path trusted bundled plugin metadata during Gateway startup. - Gateway/performance: reuse the compatible plugin metadata snapshot across dashboard and channel agent turns so auto-enabled runtime config does not repeatedly rescan plugin metadata before provider calls. Thanks @shakkernerd. +- Gateway/performance: reuse current plugin metadata for provider activation, auth/env lookup, secret-target compilation, and bundle settings during dashboard and channel agent turns, eliminating Gateway-process metadata scans before provider calls. Thanks @shakkernerd. - Gateway/performance: avoid resolving plugin auto-enable metadata twice in one runtime config pass, reducing repeated dashboard turn metadata scans. Thanks @shakkernerd. - Auth/providers: pass `config` and `workspaceDir` lookup context through to provider-id resolution so workspace-scoped auth aliases resolve correctly when no explicit alias map is supplied. Thanks @shakkernerd. - Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed. From 25f16f8fe6c901f81542b70e45d37c6669588ad7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 06:10:03 +0100 Subject: [PATCH 014/215] fix: preserve cli oauth session continuity --- CHANGELOG.md | 1 + src/agents/cli-auth-epoch.test.ts | 79 +++++++++++++++++++++++++++++++ src/agents/cli-auth-epoch.ts | 23 +++++++-- src/agents/cli-session.test.ts | 24 +++++++++- src/agents/cli-session.ts | 13 +++-- 5 files changed, 133 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d2331498e1..ad2231ae107 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -132,6 +132,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- CLI backends: keep versioned OAuth identity matches reusable when auth profile ids rotate, so Claude CLI sessions do not reset and lose continuity during same-account OAuth refresh/profile alias changes. Fixes #78541. - Anthropic: reject uppercase provider-prefixed forward-compat model ids locally instead of sending malformed dynamic ids upstream. Fixes #73715. - OpenAI/embeddings: pass configured output dimensionality through single and batched embedding requests so memory embedding indexes can request smaller vectors. Fixes #55126. - CLI/infer: normalize HEIC/HEIF image files to JPEG before model-run requests, avoiding providers that reject Apple image container formats. Fixes #50081. diff --git a/src/agents/cli-auth-epoch.test.ts b/src/agents/cli-auth-epoch.test.ts index feecab85ed2..3e4e9ab9c36 100644 --- a/src/agents/cli-auth-epoch.test.ts +++ b/src/agents/cli-auth-epoch.test.ts @@ -184,6 +184,85 @@ describe("resolveCliAuthEpoch", () => { expect(second).toBe(first); }); + it("keeps oauth auth-profile epochs stable across profile id aliases for the same account", async () => { + const store: AuthProfileStore = { + version: 1, + profiles: { + "anthropic:work": { + type: "oauth", + provider: "anthropic", + access: "access-a", + refresh: "refresh-a", + expires: 1, + email: "user@example.com", + }, + "anthropic:work-alias": { + type: "oauth", + provider: "anthropic", + access: "access-b", + refresh: "refresh-b", + expires: 2, + email: "user@example.com", + }, + }, + }; + setCliAuthEpochTestDeps({ + readGeminiCliCredentialsCached: () => null, + loadAuthProfileStoreForRuntime: () => store, + }); + + const first = await resolveCliAuthEpoch({ + provider: "google-gemini-cli", + authProfileId: "anthropic:work", + }); + const second = await resolveCliAuthEpoch({ + provider: "google-gemini-cli", + authProfileId: "anthropic:work-alias", + }); + + expect(first).toBeDefined(); + expect(second).toBe(first); + }); + + it("keeps identity-less oauth auth-profile epochs scoped to the profile id", async () => { + const store: AuthProfileStore = { + version: 1, + profiles: { + "anthropic:work": { + type: "oauth", + provider: "anthropic", + access: "access-a", + refresh: "refresh-a", + expires: 1, + }, + "anthropic:personal": { + type: "oauth", + provider: "anthropic", + access: "access-b", + refresh: "refresh-b", + expires: 2, + }, + }, + }; + setCliAuthEpochTestDeps({ + readGeminiCliCredentialsCached: () => null, + loadAuthProfileStoreForRuntime: () => store, + }); + + const first = await resolveCliAuthEpoch({ + provider: "google-gemini-cli", + authProfileId: "anthropic:work", + }); + const second = await resolveCliAuthEpoch({ + provider: "google-gemini-cli", + authProfileId: "anthropic:personal", + }); + + expect(first).toBeDefined(); + expect(second).toBeDefined(); + expect(second).not.toBe(first); + }); + it("changes oauth auth-profile epochs when the account identity changes", async () => { let store: AuthProfileStore = { version: 1, diff --git a/src/agents/cli-auth-epoch.ts b/src/agents/cli-auth-epoch.ts index df288e7a3d4..d67aea8c33e 100644 --- a/src/agents/cli-auth-epoch.ts +++ b/src/agents/cli-auth-epoch.ts @@ -114,6 +114,25 @@ function encodeAuthProfileCredential(credential: AuthProfileCredential): string throw new Error("Unsupported auth profile credential type"); } +function hasOAuthAccountIdentity(credential: AuthProfileCredential): boolean { + return ( + credential.type === "oauth" && + (normalizeOptionalString(credential.accountId) !== undefined || + normalizeOptionalString(credential.email) !== undefined) + ); +} + +function encodeAuthProfileEpochPart( + authProfileId: string, + credential: AuthProfileCredential, +): string { + const credentialHash = hashCliAuthEpochPart(encodeAuthProfileCredential(credential)); + if (hasOAuthAccountIdentity(credential)) { + return `profile:oauth-identity:${credentialHash}`; + } + return `profile:${authProfileId}:${credentialHash}`; +} + function getLocalCliCredentialFingerprint(provider: string): string | undefined { switch (provider) { case "claude-cli": { @@ -174,9 +193,7 @@ export async function resolveCliAuthEpoch(params: { }); const credential = getAuthProfileCredential(store, authProfileId); if (credential) { - parts.push( - `profile:${authProfileId}:${hashCliAuthEpochPart(encodeAuthProfileCredential(credential))}`, - ); + parts.push(encodeAuthProfileEpochPart(authProfileId, credential)); } } diff --git a/src/agents/cli-session.test.ts b/src/agents/cli-session.test.ts index ffd21c24d53..dab350f7382 100644 --- a/src/agents/cli-session.test.ts +++ b/src/agents/cli-session.test.ts @@ -128,7 +128,7 @@ describe("cli-session helpers", () => { resolveCliSessionReuse({ binding, authProfileId: "anthropic:personal", - authEpoch: "auth-epoch-a", + authEpoch: "auth-epoch-b", authEpochVersion: 2, extraSystemPromptHash: "prompt-a", mcpConfigHash: "mcp-a", @@ -166,6 +166,28 @@ describe("cli-session helpers", () => { ).toEqual({ invalidatedReason: "mcp" }); }); + it("reuses when auth profile ids rotate but the versioned auth epoch is stable", () => { + const binding = { + sessionId: "cli-session-1", + authProfileId: "anthropic:work", + authEpoch: "auth-epoch-a", + authEpochVersion: 2, + extraSystemPromptHash: "prompt-a", + mcpConfigHash: "mcp-a", + }; + + expect( + resolveCliSessionReuse({ + binding, + authProfileId: "anthropic:work-alias", + authEpoch: "auth-epoch-a", + authEpochVersion: 2, + extraSystemPromptHash: "prompt-a", + mcpConfigHash: "mcp-a", + }), + ).toEqual({ sessionId: "cli-session-1" }); + }); + it("accepts unversioned auth epochs for binding upgrades", () => { const binding = { sessionId: "cli-session-1", diff --git a/src/agents/cli-session.ts b/src/agents/cli-session.ts index 0991e94b0b2..b99a09fc372 100644 --- a/src/agents/cli-session.ts +++ b/src/agents/cli-session.ts @@ -150,10 +150,17 @@ export function resolveCliSessionReuse(params: { const currentMcpConfigHash = normalizeOptionalString(params.mcpConfigHash); const currentMcpResumeHash = normalizeOptionalString(params.mcpResumeHash); const storedAuthProfileId = normalizeOptionalString(binding?.authProfileId); - if (storedAuthProfileId !== currentAuthProfileId) { - return { invalidatedReason: "auth-profile" }; - } const storedAuthEpoch = normalizeOptionalString(binding?.authEpoch); + const hasMatchingVersionedAuthEpoch = + binding?.authEpochVersion === params.authEpochVersion && + storedAuthEpoch !== undefined && + currentAuthEpoch !== undefined && + storedAuthEpoch === currentAuthEpoch; + if (storedAuthProfileId !== currentAuthProfileId) { + if (!hasMatchingVersionedAuthEpoch) { + return { invalidatedReason: "auth-profile" }; + } + } if ( binding?.authEpochVersion === params.authEpochVersion && storedAuthEpoch !== currentAuthEpoch From e2d5e1b38dcadc9d6349c00d0897ad38d5542045 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 22:20:24 -0700 Subject: [PATCH 015/215] fix(plugins): expose config to transport normalization --- src/plugins/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 9ad28497754..27c5c973f7c 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -539,6 +539,8 @@ export type { * plugin-owned transport family. */ export type ProviderNormalizeTransportContext = { + config?: OpenClawConfig; + workspaceDir?: string; provider: string; api?: string | null; baseUrl?: string; From 16b0a6202c69046f8c17885929b858c11fed488d Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 10:49:20 -0700 Subject: [PATCH 016/215] perf(reply): avoid queue churn in dedupe paths --- extensions/telegram/src/fetch.ts | 5 +- .../reply/agent-runner-execution.ts | 5 +- src/auto-reply/reply/history.ts | 5 +- src/auto-reply/reply/reply-payloads-dedupe.ts | 92 +++++++++++++------ 4 files changed, 75 insertions(+), 32 deletions(-) diff --git a/extensions/telegram/src/fetch.ts b/extensions/telegram/src/fetch.ts index 23dc8af19bb..331c034ecb2 100644 --- a/extensions/telegram/src/fetch.ts +++ b/extensions/telegram/src/fetch.ts @@ -410,8 +410,9 @@ function collectErrorCodes(err: unknown): Set { const queue: unknown[] = [err]; const seen = new Set(); - while (queue.length > 0) { - const current = queue.shift(); + let queueIndex = 0; + while (queueIndex < queue.length) { + const current = queue[queueIndex++]; if (!current || seen.has(current)) { continue; } diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 99b7e961e96..1e519e42f3e 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -909,8 +909,9 @@ function resolveRestartLifecycleError( const pending = [err]; const seen = new Set(); - while (pending.length > 0) { - const candidate = pending.shift(); + let pendingIndex = 0; + while (pendingIndex < pending.length) { + const candidate = pending[pendingIndex++]; if (!candidate || seen.has(candidate)) { continue; } diff --git a/src/auto-reply/reply/history.ts b/src/auto-reply/reply/history.ts index 654525c5e48..b5316b7b168 100644 --- a/src/auto-reply/reply/history.ts +++ b/src/auto-reply/reply/history.ts @@ -61,8 +61,9 @@ export function appendHistoryEntry(params: { } const history = historyMap.get(historyKey) ?? []; history.push(entry); - while (history.length > params.limit) { - history.shift(); + const overflowCount = history.length - params.limit; + if (overflowCount > 0) { + history.splice(0, overflowCount); } if (historyMap.has(historyKey)) { // Refresh insertion order so eviction keeps recently used histories. diff --git a/src/auto-reply/reply/reply-payloads-dedupe.ts b/src/auto-reply/reply/reply-payloads-dedupe.ts index cf76e25a187..d370549d609 100644 --- a/src/auto-reply/reply/reply-payloads-dedupe.ts +++ b/src/auto-reply/reply/reply-payloads-dedupe.ts @@ -35,43 +35,83 @@ export function filterMessagingToolMediaDuplicates(params: { payloads: ReplyPayload[]; sentMediaUrls: string[]; }): ReplyPayload[] { - const normalizeMediaForDedupe = (value: string): string => { - const trimmed = value.trim(); - if (!trimmed) { - return ""; - } - if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("file://")) { - return trimmed; - } - try { - const parsed = new URL(trimmed); - if (parsed.protocol === "file:") { - return decodeURIComponent(parsed.pathname || ""); - } - } catch { - // Keep fallback below for non-URL-like inputs. - } - return trimmed.replace(/^file:\/\//i, ""); - }; - const { payloads, sentMediaUrls } = params; if (sentMediaUrls.length === 0) { return payloads; } - const sentSet = new Set(sentMediaUrls.map(normalizeMediaForDedupe).filter(Boolean)); - return payloads.map((payload) => { + const sentSet = new Set(); + for (const sentMediaUrl of sentMediaUrls) { + const normalized = normalizeMediaForDedupe(sentMediaUrl); + if (normalized) { + sentSet.add(normalized); + } + } + if (sentSet.size === 0) { + return payloads; + } + + let nextPayloads: ReplyPayload[] | undefined; + for (let index = 0; index < payloads.length; index++) { + const payload = payloads[index]!; const mediaUrl = payload.mediaUrl; const mediaUrls = payload.mediaUrls; const stripSingle = mediaUrl && sentSet.has(normalizeMediaForDedupe(mediaUrl)); - const filteredUrls = mediaUrls?.filter((u) => !sentSet.has(normalizeMediaForDedupe(u))); - if (!stripSingle && (!mediaUrls || filteredUrls?.length === mediaUrls.length)) { - return payload; + + let filteredUrls: string[] | undefined; + let strippedMediaUrls = false; + if (mediaUrls?.length) { + for (let mediaIndex = 0; mediaIndex < mediaUrls.length; mediaIndex++) { + const url = mediaUrls[mediaIndex]!; + if (sentSet.has(normalizeMediaForDedupe(url))) { + strippedMediaUrls = true; + if (!filteredUrls) { + filteredUrls = mediaUrls.slice(0, mediaIndex); + } + continue; + } + if (filteredUrls) { + filteredUrls.push(url); + } + } } - return Object.assign({}, payload, { + + if (!stripSingle && !strippedMediaUrls) { + if (nextPayloads) { + nextPayloads.push(payload); + } + continue; + } + + const nextPayload = Object.assign({}, payload, { mediaUrl: stripSingle ? undefined : mediaUrl, mediaUrls: filteredUrls?.length ? filteredUrls : undefined, }); - }); + if (!nextPayloads) { + nextPayloads = payloads.slice(0, index); + } + nextPayloads.push(nextPayload); + } + + return nextPayloads ?? payloads; +} + +function normalizeMediaForDedupe(value: string): string { + const trimmed = value.trim(); + if (!trimmed) { + return ""; + } + if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("file://")) { + return trimmed; + } + try { + const parsed = new URL(trimmed); + if (parsed.protocol === "file:") { + return decodeURIComponent(parsed.pathname || ""); + } + } catch { + // Keep fallback below for non-URL-like inputs. + } + return trimmed.replace(/^file:\/\//i, ""); } function normalizeProviderForComparison(value?: string): string | undefined { From 8bff73cfb07f315dbadfd01eed186c6acff08575 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 11:18:57 -0700 Subject: [PATCH 017/215] perf(core): reduce queue head churn --- .../discord/src/internal/event-queue.ts | 31 ++++++++++++++++--- .../mattermost/src/mattermost/client.ts | 26 +++++++++++----- src/agents/bash-process-registry.ts | 14 +++++++-- .../transcript-file-state.ts | 3 +- src/agents/tool-loop-detection.ts | 2 +- 5 files changed, 59 insertions(+), 17 deletions(-) diff --git a/extensions/discord/src/internal/event-queue.ts b/extensions/discord/src/internal/event-queue.ts index c48ded04415..ebe5427c493 100644 --- a/extensions/discord/src/internal/event-queue.ts +++ b/extensions/discord/src/internal/event-queue.ts @@ -31,6 +31,7 @@ const DEFAULT_SLOW_LISTENER_THRESHOLD_MS = 30_000; export class DiscordEventQueue { private readonly options: Required; private readonly queue: DiscordEventQueueJob[] = []; + private queueHead = 0; private processing = 0; private processedCount = 0; private droppedCount = 0; @@ -52,7 +53,7 @@ export class DiscordEventQueue { } enqueue(params: Omit): Promise { - if (this.queue.length >= this.options.maxQueueSize) { + if (this.pendingQueueSize >= this.options.maxQueueSize) { this.droppedCount += 1; return Promise.reject( new Error( @@ -68,7 +69,7 @@ export class DiscordEventQueue { getMetrics(): DiscordEventQueueMetrics { return { - queueSize: this.queue.length, + queueSize: this.pendingQueueSize, processing: this.processing, processed: this.processedCount, dropped: this.droppedCount, @@ -78,9 +79,31 @@ export class DiscordEventQueue { }; } + private get pendingQueueSize(): number { + return Math.max(0, this.queue.length - this.queueHead); + } + + private takeNextJob(): DiscordEventQueueJob | undefined { + if (this.queueHead >= this.queue.length) { + this.queue.length = 0; + this.queueHead = 0; + return undefined; + } + const job = this.queue[this.queueHead]; + this.queueHead += 1; + if (this.queueHead >= this.queue.length) { + this.queue.length = 0; + this.queueHead = 0; + } else if (this.queueHead > 256 && this.queueHead * 2 > this.queue.length) { + this.queue.splice(0, this.queueHead); + this.queueHead = 0; + } + return job; + } + private processNext(): void { - while (this.processing < this.options.maxConcurrency && this.queue.length > 0) { - const job = this.queue.shift(); + while (this.processing < this.options.maxConcurrency && this.pendingQueueSize > 0) { + const job = this.takeNextJob(); if (!job) { return; } diff --git a/extensions/mattermost/src/mattermost/client.ts b/extensions/mattermost/src/mattermost/client.ts index ff048333521..f8e8b7367ee 100644 --- a/extensions/mattermost/src/mattermost/client.ts +++ b/extensions/mattermost/src/mattermost/client.ts @@ -394,16 +394,24 @@ function isRetryableError(error: Error): boolean { return false; } - const codes = candidates - .map((candidate) => readErrorCode(candidate)) - .filter((code): code is string => Boolean(code)); + const codes: string[] = []; + for (const candidate of candidates) { + const code = readErrorCode(candidate); + if (code) { + codes.push(code); + } + } if (codes.some((code) => RETRYABLE_NETWORK_ERROR_CODES.has(code))) { return true; } - const names = candidates - .map((candidate) => readErrorName(candidate)) - .filter((name): name is string => Boolean(name)); + const names: string[] = []; + for (const candidate of candidates) { + const name = readErrorName(candidate); + if (name) { + names.push(name); + } + } if (names.some((name) => RETRYABLE_NETWORK_ERROR_NAMES.has(name))) { return true; } @@ -415,11 +423,13 @@ function isRetryableError(error: Error): boolean { function collectErrorCandidates(error: unknown): unknown[] { const queue: unknown[] = [error]; + let queueIndex = 0; const seen = new Set(); const candidates: unknown[] = []; - while (queue.length > 0) { - const current = queue.shift(); + while (queueIndex < queue.length) { + const current = queue[queueIndex]; + queueIndex += 1; if (!current || seen.has(current)) { continue; } diff --git a/src/agents/bash-process-registry.ts b/src/agents/bash-process-registry.ts index 851f10e309d..2e3ffe48764 100644 --- a/src/agents/bash-process-registry.ts +++ b/src/agents/bash-process-registry.ts @@ -247,9 +247,17 @@ function capPendingBuffer(buffer: string[], pendingChars: number, cap: number) { buffer.push(last.slice(last.length - cap)); return cap; } - while (buffer.length && pendingChars - buffer[0].length >= cap) { - pendingChars -= buffer[0].length; - buffer.shift(); + let dropCount = 0; + while (dropCount < buffer.length) { + const chunk = buffer[dropCount]; + if (chunk === undefined || pendingChars - chunk.length < cap) { + break; + } + pendingChars -= chunk.length; + dropCount += 1; + } + if (dropCount > 0) { + buffer.splice(0, dropCount); } if (buffer.length && pendingChars > cap) { const overflow = pendingChars - cap; diff --git a/src/agents/pi-embedded-runner/transcript-file-state.ts b/src/agents/pi-embedded-runner/transcript-file-state.ts index aa66f77d7fe..e9f6df6db3b 100644 --- a/src/agents/pi-embedded-runner/transcript-file-state.ts +++ b/src/agents/pi-embedded-runner/transcript-file-state.ts @@ -114,9 +114,10 @@ export class TranscriptFileState { const branch: SessionEntry[] = []; let current = (fromId ?? this.leafId) ? this.byId.get((fromId ?? this.leafId)!) : undefined; while (current) { - branch.unshift(current); + branch.push(current); current = current.parentId ? this.byId.get(current.parentId) : undefined; } + branch.reverse(); return branch; } diff --git a/src/agents/tool-loop-detection.ts b/src/agents/tool-loop-detection.ts index 25ae3baf620..246cefce1c9 100644 --- a/src/agents/tool-loop-detection.ts +++ b/src/agents/tool-loop-detection.ts @@ -660,7 +660,7 @@ export function recordToolCall( }); if (state.toolCallHistory.length > resolvedConfig.historySize) { - state.toolCallHistory.shift(); + state.toolCallHistory.splice(0, state.toolCallHistory.length - resolvedConfig.historySize); } } From 468c6a0101e9d1b260e01f77e92c4263113d58fb Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 11:36:17 -0700 Subject: [PATCH 018/215] perf(core): trim reply and agent allocation churn --- src/agents/anthropic-transport-stream.ts | 43 +++++++------ src/agents/auth-health.ts | 46 +++++++++----- .../command/attempt-execution.helpers.ts | 3 +- src/agents/compaction.ts | 7 ++- src/agents/model-selection-shared.ts | 5 +- src/agents/openclaw-tools.ts | 6 +- .../compaction-successor-transcript.ts | 25 ++++++-- src/agents/pi-embedded-runner/run/images.ts | 9 ++- src/agents/pi-embedded-subscribe.tools.ts | 9 ++- src/agents/pi-tools.params.ts | 20 +++--- src/agents/session-transcript-repair.ts | 8 ++- src/auto-reply/reply/agent-runner-payloads.ts | 61 ++++++++++++------- src/auto-reply/reply/followup-delivery.ts | 12 ++-- src/auto-reply/reply/followup-runner.ts | 32 +++------- src/auto-reply/reply/queue/drain.ts | 42 ++++++++++--- src/auto-reply/reply/session-system-events.ts | 52 ++++++++++------ 16 files changed, 245 insertions(+), 135 deletions(-) diff --git a/src/agents/anthropic-transport-stream.ts b/src/agents/anthropic-transport-stream.ts index 5df9491d6af..3828cce7a83 100644 --- a/src/agents/anthropic-transport-stream.ts +++ b/src/agents/anthropic-transport-stream.ts @@ -249,11 +249,13 @@ function convertContentBlocks( source: { type: "base64"; media_type: string; data: string }; } > = []; + let hasTextBlock = false; for (const block of content) { if (block.type === "text") { const text = sanitizeTransportPayloadText(block.text); if (text.trim().length > 0) { blocks.push({ type: "text", text }); + hasTextBlock = true; } } else { blocks.push({ @@ -266,11 +268,8 @@ function convertContentBlocks( }); } } - if (!blocks.some((block) => block.type === "text")) { - blocks.unshift({ - type: "text", - text: "(see attached image)", - }); + if (!hasTextBlock) { + return [{ type: "text", text: "(see attached image)" }, ...blocks]; } return blocks; } @@ -426,7 +425,16 @@ function convertAnthropicTools(tools: Context["tools"], isOAuthToken: boolean) { if (!tools) { return []; } - return tools.flatMap((tool) => { + const converted: Array<{ + name: string; + description?: string; + input_schema: { + type: "object"; + properties: unknown; + required: unknown; + }; + }> = []; + for (const tool of tools) { // Main quarantine happens when plugin tools materialize; this keeps Anthropic // safe for direct/custom tool arrays that bypass the plugin registry. const parameters = @@ -434,20 +442,19 @@ function convertAnthropicTools(tools: Context["tools"], isOAuthToken: boolean) { ? (tool.parameters as Record) : undefined; if (!parameters) { - return []; + continue; } - return [ - { - name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name, - description: tool.description, - input_schema: { - type: "object", - properties: parameters.properties || {}, - required: parameters.required || [], - }, + converted.push({ + name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name, + description: tool.description, + input_schema: { + type: "object", + properties: parameters.properties || {}, + required: parameters.required || [], }, - ]; - }); + }); + } + return converted; } function mapStopReason(reason: string | undefined): string { diff --git a/src/agents/auth-health.ts b/src/agents/auth-health.ts index e76664eb920..ddfc2f68892 100644 --- a/src/agents/auth-health.ts +++ b/src/agents/auth-health.ts @@ -262,28 +262,46 @@ export function buildAuthHealthSummary(params: { continue; } - const oauthProfiles = provider.profiles.filter((p) => p.type === "oauth"); - const tokenProfiles = provider.profiles.filter((p) => p.type === "token"); - const apiKeyProfiles = provider.profiles.filter((p) => p.type === "api_key"); + let hasApiKeyProfile = false; + let hasExpirableProfile = false; + let hasExpiredOrMissing = false; + let hasExpiring = false; + let earliestExpiry: number | undefined; + for (const profile of provider.profiles) { + if (profile.type === "api_key") { + hasApiKeyProfile = true; + continue; + } + if (profile.type !== "oauth" && profile.type !== "token") { + continue; + } + hasExpirableProfile = true; + if (typeof profile.expiresAt === "number" && Number.isFinite(profile.expiresAt)) { + earliestExpiry = + earliestExpiry === undefined + ? profile.expiresAt + : Math.min(earliestExpiry, profile.expiresAt); + } + if (profile.status === "expired" || profile.status === "missing") { + hasExpiredOrMissing = true; + } else if (profile.status === "expiring") { + hasExpiring = true; + } + } - const expirable = [...oauthProfiles, ...tokenProfiles]; - if (expirable.length === 0) { - provider.status = apiKeyProfiles.length > 0 ? "static" : "missing"; + if (!hasExpirableProfile) { + provider.status = hasApiKeyProfile ? "static" : "missing"; continue; } - const expiryCandidates = expirable - .map((p) => p.expiresAt) - .filter((v): v is number => typeof v === "number" && Number.isFinite(v)); - if (expiryCandidates.length > 0) { - provider.expiresAt = Math.min(...expiryCandidates); + if (earliestExpiry !== undefined) { + provider.expiresAt = earliestExpiry; provider.remainingMs = provider.expiresAt - now; } - const statuses = new Set(expirable.map((p) => p.status)); - if (statuses.has("expired") || statuses.has("missing")) { + if (hasExpiredOrMissing) { provider.status = "expired"; - } else if (statuses.has("expiring")) { + } else if (hasExpiring) { provider.status = "expiring"; } else { provider.status = "ok"; diff --git a/src/agents/command/attempt-execution.helpers.ts b/src/agents/command/attempt-execution.helpers.ts index 523615fbef3..13bb29f97ae 100644 --- a/src/agents/command/attempt-execution.helpers.ts +++ b/src/agents/command/attempt-execution.helpers.ts @@ -200,9 +200,10 @@ function formatFallbackTurns( if (consumed + line.length + 1 > remainingBudget) { break; } - lines.unshift(line); + lines.push(line); consumed += line.length + 1; } + lines.reverse(); return { text: lines.join("\n"), consumed }; } diff --git a/src/agents/compaction.ts b/src/agents/compaction.ts index a745e192c21..fe64bfc8f9e 100644 --- a/src/agents/compaction.ts +++ b/src/agents/compaction.ts @@ -177,7 +177,12 @@ export function splitMessagesByTokenShare( const stopReason = (message as { stopReason?: unknown }).stopReason; const keepsPending = stopReason !== "aborted" && stopReason !== "error" && toolCalls.length > 0; - pendingToolCallIds = keepsPending ? new Set(toolCalls.map((t) => t.id)) : new Set(); + pendingToolCallIds = new Set(); + if (keepsPending) { + for (const toolCall of toolCalls) { + pendingToolCallIds.add(toolCall.id); + } + } pendingChunkStartIndex = keepsPending ? current.length - 1 : null; } else if (message.role === "toolResult" && pendingToolCallIds.size > 0) { const resultId = extractToolResultId(message); diff --git a/src/agents/model-selection-shared.ts b/src/agents/model-selection-shared.ts index 520fd5c2791..b8e702f4fa2 100644 --- a/src/agents/model-selection-shared.ts +++ b/src/agents/model-selection-shared.ts @@ -631,7 +631,10 @@ export function buildAllowedModelSetWithFallbacks(params: { }) : null; const defaultKey = defaultRef ? modelKey(defaultRef.provider, defaultRef.model) : undefined; - const catalogKeys = new Set(catalog.map((entry) => modelKey(entry.provider, entry.id))); + const catalogKeys = new Set(); + for (const entry of catalog) { + catalogKeys.add(modelKey(entry.provider, entry.id)); + } if (allowAny) { if (defaultKey) { diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index 648e84d2a52..911248cc8d4 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -420,10 +420,14 @@ export function createOpenClawTools( return tools; } + const existingToolNames = new Set(); + for (const tool of tools) { + existingToolNames.add(tool.name); + } const wrappedPluginTools = resolveOpenClawPluginToolsForOptions({ options, resolvedConfig, - existingToolNames: new Set(tools.map((tool) => tool.name)), + existingToolNames, }); options?.recordToolPrepStage?.("openclaw-tools:plugin-tools"); diff --git a/src/agents/pi-embedded-runner/compaction-successor-transcript.ts b/src/agents/pi-embedded-runner/compaction-successor-transcript.ts index 7d73fde215a..eebe56251ee 100644 --- a/src/agents/pi-embedded-runner/compaction-successor-transcript.ts +++ b/src/agents/pi-embedded-runner/compaction-successor-transcript.ts @@ -152,9 +152,17 @@ function buildSuccessorEntries(params: { } } - const entryById = new Map(allEntries.map((entry) => [entry.id, entry])); - const activeBranchIds = new Set(branch.map((entry) => entry.id)); - const originalIndexById = new Map(allEntries.map((entry, index) => [entry.id, index])); + const entryById = new Map(); + const originalIndexById = new Map(); + for (let index = 0; index < allEntries.length; index += 1) { + const entry = allEntries[index]; + entryById.set(entry.id, entry); + originalIndexById.set(entry.id, index); + } + const activeBranchIds = new Set(); + for (const entry of branch) { + activeBranchIds.add(entry.id); + } const keptEntries: SessionEntry[] = []; for (const entry of allEntries) { if (removedIds.has(entry.id)) { @@ -185,7 +193,11 @@ function collectLatestStateEntryIds(entries: SessionEntry[]): Set { latestByType.set(entry.type, entry); } } - return new Set(Array.from(latestByType.values(), (entry) => entry.id)); + const ids = new Set(); + for (const entry of latestByType.values()) { + ids.add(entry.id); + } + return ids; } function isDedupedStateEntry(entry: SessionEntry): boolean { @@ -202,7 +214,10 @@ function orderSuccessorEntries(params: { originalIndexById: Map; }): SessionEntry[] { const { entries, activeBranchIds, originalIndexById } = params; - const entryIds = new Set(entries.map((entry) => entry.id)); + const entryIds = new Set(); + for (const entry of entries) { + entryIds.add(entry.id); + } const childrenByParentId = new Map(); for (const entry of entries) { diff --git a/src/agents/pi-embedded-runner/run/images.ts b/src/agents/pi-embedded-runner/run/images.ts index 51fba6e9646..5389016fac6 100644 --- a/src/agents/pi-embedded-runner/run/images.ts +++ b/src/agents/pi-embedded-runner/run/images.ts @@ -30,7 +30,10 @@ const IMAGE_EXTENSION_NAMES = [ "heic", "heif", ] as const; -const IMAGE_EXTENSIONS = new Set(IMAGE_EXTENSION_NAMES.map((ext) => `.${ext}`)); +const IMAGE_EXTENSIONS = new Set(); +for (const ext of IMAGE_EXTENSION_NAMES) { + IMAGE_EXTENSIONS.add(`.${ext}`); +} const IMAGE_EXTENSION_PATTERN = IMAGE_EXTENSION_NAMES.join("|"); const MEDIA_ATTACHED_PATH_REGEX_SOURCE = "^\\s*(.+?\\.(?:" + IMAGE_EXTENSION_PATTERN + "))\\s*(?:\\(|$|\\|)"; @@ -159,9 +162,9 @@ function extractTrailingAttachmentMediaUris(prompt: string, count: number): stri if (!match?.[1]) { break; } - uris.unshift(match[1]); + uris.push(match[1]); } - return uris; + return uris.reverse(); } export function splitPromptAndAttachmentRefs(params: { diff --git a/src/agents/pi-embedded-subscribe.tools.ts b/src/agents/pi-embedded-subscribe.tools.ts index 9e35cf09b15..ca0e87d42f8 100644 --- a/src/agents/pi-embedded-subscribe.tools.ts +++ b/src/agents/pi-embedded-subscribe.tools.ts @@ -249,9 +249,12 @@ const TRUSTED_TOOL_RESULT_MEDIA = new Set([ "x_search", "write", ]); -const TRUSTED_BUNDLED_PLUGIN_MEDIA_TOOLS = new Set( - pluginRegistrationContractRegistry.flatMap((entry) => entry.toolNames), -); +const TRUSTED_BUNDLED_PLUGIN_MEDIA_TOOLS = new Set(); +for (const entry of pluginRegistrationContractRegistry) { + for (const toolName of entry.toolNames) { + TRUSTED_BUNDLED_PLUGIN_MEDIA_TOOLS.add(toolName); + } +} const HTTP_URL_RE = /^https?:\/\//i; function readToolResultDetails(result: unknown): Record | undefined { diff --git a/src/agents/pi-tools.params.ts b/src/agents/pi-tools.params.ts index 0520621ba1a..bf34a878b90 100644 --- a/src/agents/pi-tools.params.ts +++ b/src/agents/pi-tools.params.ts @@ -33,16 +33,22 @@ function formatReceivedParamHint( record: Record, groups: readonly RequiredParamGroup[], ): string { - const allowEmptyKeys = new Set( - groups.filter((group) => group.allowEmpty).flatMap((group) => group.keys), - ); - const received = Object.keys(record).flatMap((key) => { + const allowEmptyKeys = new Set(); + for (const group of groups) { + if (group.allowEmpty) { + for (const key of group.keys) { + allowEmptyKeys.add(key); + } + } + } + const received: string[] = []; + for (const key of Object.keys(record)) { const detail = describeReceivedParamValue(record[key], allowEmptyKeys.has(key)); if (record[key] === undefined || record[key] === null) { - return []; + continue; } - return [detail ? `${key}=${detail}` : key]; - }); + received.push(detail ? `${key}=${detail}` : key); + } return received.length > 0 ? ` (received: ${received.join(", ")})` : ""; } diff --git a/src/agents/session-transcript-repair.ts b/src/agents/session-transcript-repair.ts index 79e2ea0a71d..d22ba1a5f5a 100644 --- a/src/agents/session-transcript-repair.ts +++ b/src/agents/session-transcript-repair.ts @@ -502,8 +502,12 @@ export function repairToolUseResultPairing( continue; } - const toolCallIds = new Set(toolCalls.map((t) => t.id)); - const toolCallNamesById = new Map(toolCalls.map((t) => [t.id, t.name] as const)); + const toolCallIds = new Set(); + const toolCallNamesById = new Map(); + for (const toolCall of toolCalls) { + toolCallIds.add(toolCall.id); + toolCallNamesById.set(toolCall.id, toolCall.name); + } const spanResultsById = new Map>(); const remainder: AgentMessage[] = []; diff --git a/src/auto-reply/reply/agent-runner-payloads.ts b/src/auto-reply/reply/agent-runner-payloads.ts index 6f87cf6afd5..2326d392cdd 100644 --- a/src/auto-reply/reply/agent-runner-payloads.ts +++ b/src/auto-reply/reply/agent-runner-payloads.ts @@ -131,29 +131,37 @@ export async function buildReplyPayloads(params: { normalizeMediaPaths?: (payload: ReplyPayload) => Promise; }): Promise<{ replyPayloads: ReplyPayload[]; didLogHeartbeatStrip: boolean }> { let didLogHeartbeatStrip = params.didLogHeartbeatStrip; - const sanitizedPayloads = params.isHeartbeat - ? params.payloads.map((payload) => sanitizeHeartbeatPayload(payload)) - : params.payloads.flatMap((payload) => { - let text = payload.text; + const sanitizedPayloads: ReplyPayload[] = []; + if (params.isHeartbeat) { + for (const payload of params.payloads) { + sanitizedPayloads.push(sanitizeHeartbeatPayload(payload)); + } + } else { + for (const payload of params.payloads) { + let text = payload.text; - if (payload.isError && text && isBunFetchSocketError(text)) { - text = formatBunFetchSocketError(text); - } + if (payload.isError && text && isBunFetchSocketError(text)) { + text = formatBunFetchSocketError(text); + } - if (!text || !text.includes("HEARTBEAT_OK")) { - return [copyReplyPayloadMetadata(payload, { ...payload, text })]; - } - const stripped = stripHeartbeatToken(text, { mode: "message" }); - if (stripped.didStrip && !didLogHeartbeatStrip) { - didLogHeartbeatStrip = true; - logVerbose("Stripped stray HEARTBEAT_OK token from reply"); - } - const hasMedia = resolveSendableOutboundReplyParts(payload).hasMedia; - if (stripped.shouldSkip && !hasMedia) { - return []; - } - return [copyReplyPayloadMetadata(payload, { ...payload, text: stripped.text })]; - }); + if (!text || !text.includes("HEARTBEAT_OK")) { + sanitizedPayloads.push(copyReplyPayloadMetadata(payload, { ...payload, text })); + continue; + } + const stripped = stripHeartbeatToken(text, { mode: "message" }); + if (stripped.didStrip && !didLogHeartbeatStrip) { + didLogHeartbeatStrip = true; + logVerbose("Stripped stray HEARTBEAT_OK token from reply"); + } + const hasMedia = resolveSendableOutboundReplyParts(payload).hasMedia; + if (stripped.shouldSkip && !hasMedia) { + continue; + } + sanitizedPayloads.push( + copyReplyPayloadMetadata(payload, { ...payload, text: stripped.text }), + ); + } + } const replyTaggedPayloads = ( await Promise.all( @@ -293,7 +301,16 @@ export async function buildReplyPayloads(params: { }); }; const contentSuppressedPayloads = shouldDropFinalPayloads - ? dedupedPayloads.flatMap((payload) => preserveUnsentMediaAfterBlockStream(payload) ?? []) + ? (() => { + const preserved: ReplyPayload[] = []; + for (const payload of dedupedPayloads) { + const next = preserveUnsentMediaAfterBlockStream(payload); + if (next) { + preserved.push(next); + } + } + return preserved; + })() : params.blockStreamingEnabled ? dedupedPayloads.filter( (payload) => diff --git a/src/auto-reply/reply/followup-delivery.ts b/src/auto-reply/reply/followup-delivery.ts index 1fde4532aa4..a2ecc1d192b 100644 --- a/src/auto-reply/reply/followup-delivery.ts +++ b/src/auto-reply/reply/followup-delivery.ts @@ -46,18 +46,20 @@ export function resolveFollowupDeliveryPayloads(params: { params.originatingAccountId, params.originatingChatType, ); - const sanitizedPayloads = params.payloads.flatMap((payload) => { + const sanitizedPayloads: ReplyPayload[] = []; + for (const payload of params.payloads) { const text = payload.text; if (!text || !text.includes("HEARTBEAT_OK")) { - return [payload]; + sanitizedPayloads.push(payload); + continue; } const stripped = stripHeartbeatToken(text, { mode: "message" }); const hasMedia = hasReplyPayloadMedia(payload); if (stripped.shouldSkip && !hasMedia) { - return []; + continue; } - return [{ ...payload, text: stripped.text }]; - }); + sanitizedPayloads.push({ ...payload, text: stripped.text }); + } const replyTaggedPayloads = applyReplyThreading({ payloads: sanitizedPayloads, replyToMode, diff --git a/src/auto-reply/reply/followup-runner.ts b/src/auto-reply/reply/followup-runner.ts index cc75197816e..8d3319809dd 100644 --- a/src/auto-reply/reply/followup-runner.ts +++ b/src/auto-reply/reply/followup-runner.ts @@ -1,8 +1,5 @@ import crypto from "node:crypto"; -import { - hasOutboundReplyContent, - resolveSendableOutboundReplyParts, -} from "openclaw/plugin-sdk/reply-payload"; +import { hasOutboundReplyContent } from "openclaw/plugin-sdk/reply-payload"; import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js"; import { resolveContextTokensForModel } from "../../agents/context.js"; import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js"; @@ -19,7 +16,6 @@ import { registerAgentRunContext } from "../../infra/agent-events.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { defaultRuntime } from "../../runtime.js"; import { isInternalMessageChannel } from "../../utils/message-channel.js"; -import { stripHeartbeatToken } from "../heartbeat.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; import { runPreflightCompactionIfNeeded } from "./agent-runner-memory.js"; import { @@ -402,21 +398,9 @@ export function createFollowupRunner(params: { if (payloadArray.length === 0) { return; } - const sanitizedPayloads = payloadArray.flatMap((payload) => { - const text = payload.text; - if (!text || !text.includes("HEARTBEAT_OK")) { - return [payload]; - } - const stripped = stripHeartbeatToken(text, { mode: "message" }); - const hasMedia = resolveSendableOutboundReplyParts(payload).hasMedia; - if (stripped.shouldSkip && !hasMedia) { - return []; - } - return [{ ...payload, text: stripped.text }]; - }); const finalPayloads = resolveFollowupDeliveryPayloads({ cfg: runtimeConfig, - payloads: sanitizedPayloads, + payloads: payloadArray, messageProvider: run.messageProvider, originatingAccountId: queued.originatingAccountId ?? run.agentAccountId, originatingChannel: queued.originatingChannel, @@ -431,6 +415,7 @@ export function createFollowupRunner(params: { return; } + let deliveryPayloads = finalPayloads; if (autoCompactionCount > 0) { const previousSessionId = run.sessionId; const count = await incrementRunCompactionCount({ @@ -461,9 +446,12 @@ export function createFollowupRunner(params: { } if (run.verboseLevel && run.verboseLevel !== "off") { const suffix = typeof count === "number" ? ` (count ${count})` : ""; - finalPayloads.unshift({ - text: `🧹 Auto-compaction complete${suffix}.`, - }); + deliveryPayloads = [ + { + text: `🧹 Auto-compaction complete${suffix}.`, + }, + ...finalPayloads, + ]; } } @@ -474,7 +462,7 @@ export function createFollowupRunner(params: { return; } - await sendFollowupPayloads(finalPayloads, effectiveQueued, { + await sendFollowupPayloads(deliveryPayloads, effectiveQueued, { provider: providerUsed, modelId: modelUsed, }); diff --git a/src/auto-reply/reply/queue/drain.ts b/src/auto-reply/reply/queue/drain.ts index 23528c5a4e2..2bc1dc72f65 100644 --- a/src/auto-reply/reply/queue/drain.ts +++ b/src/auto-reply/reply/queue/drain.ts @@ -49,15 +49,29 @@ type OriginRoutingMetadata = Pick< >; function resolveOriginRoutingMetadata(items: FollowupRun[]): OriginRoutingMetadata { - return { - originatingChannel: items.find((item) => item.originatingChannel)?.originatingChannel, - originatingTo: items.find((item) => item.originatingTo)?.originatingTo, - originatingAccountId: items.find((item) => item.originatingAccountId)?.originatingAccountId, + const metadata: OriginRoutingMetadata = {}; + for (const item of items) { + metadata.originatingChannel ??= item.originatingChannel; + metadata.originatingTo ??= item.originatingTo; + metadata.originatingAccountId ??= item.originatingAccountId; // Support both number (Telegram topic) and string (Slack thread_ts) thread IDs. - originatingThreadId: items.find( - (item) => item.originatingThreadId != null && item.originatingThreadId !== "", - )?.originatingThreadId, - }; + if ( + metadata.originatingThreadId == null && + item.originatingThreadId != null && + item.originatingThreadId !== "" + ) { + metadata.originatingThreadId = item.originatingThreadId; + } + if ( + metadata.originatingChannel && + metadata.originatingTo && + metadata.originatingAccountId && + metadata.originatingThreadId != null + ) { + break; + } + } + return metadata; } // Keep this key aligned with the fields that affect per-message authorization or @@ -118,8 +132,16 @@ function renderCollectItem(item: FollowupRun, idx: number): string { } function collectQueuedImages(items: FollowupRun[]): Pick { - const images = items.flatMap((item) => item.images ?? []); - const imageOrder = items.flatMap((item) => item.imageOrder ?? []); + const images: NonNullable = []; + const imageOrder: NonNullable = []; + for (const item of items) { + if (item.images) { + images.push(...item.images); + } + if (item.imageOrder) { + imageOrder.push(...item.imageOrder); + } + } return { ...(images.length > 0 ? { images } : {}), ...(imageOrder.length > 0 ? { imageOrder } : {}), diff --git a/src/auto-reply/reply/session-system-events.ts b/src/auto-reply/reply/session-system-events.ts index 92ea0dae305..cea9f3a2c80 100644 --- a/src/auto-reply/reply/session-system-events.ts +++ b/src/auto-reply/reply/session-system-events.ts @@ -17,8 +17,15 @@ import { normalizeOptionalString, } from "../../shared/string-coerce.js"; -const selectGenericSystemEvents = (events: readonly SystemEvent[]): SystemEvent[] => - events.filter((event) => !isExecCompletionEvent(event.text)); +const selectGenericSystemEvents = (events: readonly SystemEvent[]): SystemEvent[] => { + const selected: SystemEvent[] = []; + for (const event of events) { + if (!isExecCompletionEvent(event.text)) { + selected.push(event); + } + } + return selected; +}; /** Drain queued system events, format as `System:` lines, return the block (or undefined). */ export async function drainFormattedSystemEvents(params: { @@ -90,6 +97,7 @@ export async function drainFormattedSystemEvents(params: { ); }; + const summaryLines: string[] = []; const systemLines: string[] = []; // Exec completions have a dedicated heartbeat prompt; leave those entries queued // so the heartbeat path can consume and deliver them. @@ -97,32 +105,36 @@ export async function drainFormattedSystemEvents(params: { params.sessionKey, selectGenericSystemEvents(peekSystemEventEntries(params.sessionKey)), ); - systemLines.push( - ...queued.flatMap((event) => { - const compacted = compactSystemEvent(event.text); - if (!compacted) { - return []; - } - const prefix = event.trusted === false ? "System (untrusted)" : "System"; - const timestamp = `[${formatSystemEventTimestamp(event.ts, params.cfg)}]`; - return compacted - .split("\n") - .map((subline, index) => `${prefix}: ${index === 0 ? `${timestamp} ` : ""}${subline}`); - }), - ); + for (const event of queued) { + const compacted = compactSystemEvent(event.text); + if (!compacted) { + continue; + } + const prefix = event.trusted === false ? "System (untrusted)" : "System"; + const timestamp = `[${formatSystemEventTimestamp(event.ts, params.cfg)}]`; + let index = 0; + for (const subline of compacted.split("\n")) { + systemLines.push(`${prefix}: ${index === 0 ? `${timestamp} ` : ""}${subline}`); + index += 1; + } + } if (params.isMainSession && params.isNewSession) { const summary = await buildChannelSummary(params.cfg); if (summary.length > 0) { - systemLines.unshift( - ...summary.flatMap((line) => line.split("\n").map((subline) => `System: ${subline}`)), - ); + for (const line of summary) { + for (const subline of line.split("\n")) { + summaryLines.push(`System: ${subline}`); + } + } } } - if (systemLines.length === 0) { + if (summaryLines.length === 0 && systemLines.length === 0) { return undefined; } // Each sub-line gets its own prefix so continuation lines can't be mistaken // for regular user content. - return systemLines.join("\n"); + return summaryLines.length > 0 + ? [...summaryLines, ...systemLines].join("\n") + : systemLines.join("\n"); } From eee730789107cbcabc5dc27ab769c91a3e1a93a0 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 13:57:39 -0700 Subject: [PATCH 019/215] perf(core): trim reply helper churn --- CHANGELOG.md | 1 + .../slack/src/approval-handler.runtime.ts | 39 ++++-- src/agents/pi-tools.ts | 128 +++++++++--------- src/agents/tool-display-common.ts | 82 +++++++---- src/auto-reply/commands-registry.shared.ts | 12 +- src/auto-reply/heartbeat-filter.ts | 35 +++-- src/auto-reply/reply/agent-runner-payloads.ts | 110 +++++++++------ src/auto-reply/reply/commands-tasks.ts | 5 +- src/auto-reply/reply/route-reply.ts | 14 +- src/auto-reply/status.ts | 40 +++--- .../plugins/directory-config-helpers.ts | 78 +++++++---- 11 files changed, 329 insertions(+), 215 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad2231ae107..5f766ad26e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -521,6 +521,7 @@ Docs: https://docs.openclaw.ai - Channels/streaming: add unified `streaming.mode: "progress"` drafts with auto single-word status labels and shared progress configuration across Discord, Telegram, Matrix, Slack, and Microsoft Teams. - Agents/commands: add `/steer ` for queue-independent steering of the active current-session run without starting a new turn when the session is idle. (#76934) +- Core/performance: trim reply payload routing, heartbeat filtering, tool display, core tool assembly, channel directory, task status, and Slack approval formatting helper chains with direct bounded scans. Thanks @vincentkoc. - Tools/BTW: add `/side` as a text and native slash-command alias for `/btw` side questions. - Doctor/config: `doctor --fix` now commits safe legacy migrations even when unrelated validation issues (e.g. a missing plugin) prevent full validation from passing, so `agents.defaults.llm` and other known-legacy keys are always cleaned up by `doctor --fix` regardless of other config problems. Fixes #76798. (#76800) Thanks @hclsys. - Agents/tools: skip optional media and PDF tool factories when the effective tool denylist already blocks them, avoiding unnecessary hot-path setup for tools that will be filtered out before model use. (#76773) Thanks @dorukardahan. diff --git a/extensions/slack/src/approval-handler.runtime.ts b/extensions/slack/src/approval-handler.runtime.ts index 116a6fa492f..72799817020 100644 --- a/extensions/slack/src/approval-handler.runtime.ts +++ b/extensions/slack/src/approval-handler.runtime.ts @@ -82,22 +82,35 @@ function formatSlackMetadataLine(label: string, value: string): string { } function buildSlackMetadataLines(metadata: readonly { label: string; value: string }[]): string[] { - return metadata.map((item) => formatSlackMetadataLine(item.label, item.value)); + const lines: string[] = []; + for (const item of metadata) { + lines.push(formatSlackMetadataLine(item.label, item.value)); + } + return lines; } function buildSlackMetadataContextElements(metadata: readonly { label: string; value: string }[]) { const lines = buildSlackMetadataLines(metadata); - const visibleLines = - lines.length > SLACK_CONTEXT_ELEMENTS_MAX - ? [ - ...lines.slice(0, SLACK_CONTEXT_ELEMENTS_MAX - 1), - `…+${lines.length - (SLACK_CONTEXT_ELEMENTS_MAX - 1)} more`, - ] - : lines; - return visibleLines.map((line) => ({ - type: "mrkdwn" as const, - text: truncateSlackMrkdwn(line, SLACK_TEXT_OBJECT_MAX), - })); + const visibleLineCount = + lines.length > SLACK_CONTEXT_ELEMENTS_MAX ? SLACK_CONTEXT_ELEMENTS_MAX - 1 : lines.length; + const elements: Array<{ type: "mrkdwn"; text: string }> = []; + for (let index = 0; index < visibleLineCount; index += 1) { + const line = lines[index]; + if (line === undefined) { + continue; + } + elements.push({ + type: "mrkdwn", + text: truncateSlackMrkdwn(line, SLACK_TEXT_OBJECT_MAX), + }); + } + if (lines.length > SLACK_CONTEXT_ELEMENTS_MAX) { + elements.push({ + type: "mrkdwn", + text: `…+${lines.length - visibleLineCount} more`, + }); + } + return elements; } function resolveSlackApprovalDecisionLabel( @@ -120,7 +133,7 @@ function buildSlackPendingApprovalText(view: ExecApprovalPendingView): string { buildSlackCodeBlock(view.commandText), ...metadataLines, ]; - return lines.filter(Boolean).join("\n"); + return lines.join("\n"); } function buildSlackPendingApprovalBlocks(view: ExecApprovalPendingView): SlackBlock[] { diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 633a4f5e30a..445444e1a4a 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -508,51 +508,56 @@ export function createOpenClawCodingTools(options?: { const imageSanitization = resolveImageSanitizationLimits(options?.config); options?.recordToolPrepStage?.("workspace-policy"); - const base = includeBaseCodingTools - ? (createCodingTools(workspaceRoot) as unknown as AnyAgentTool[]).flatMap((tool) => { - if (tool.name === "read") { - if (sandboxRoot) { - const sandboxed = createSandboxedReadTool({ - root: sandboxRoot, - bridge: sandboxFsBridge!, - modelContextWindowTokens: options?.modelContextWindowTokens, - imageSanitization, - }); - return [ - workspaceOnly - ? wrapToolWorkspaceRootGuardWithOptions(sandboxed, sandboxRoot, { - containerWorkdir: sandbox.containerWorkdir, - }) - : sandboxed, - ]; - } - const freshReadTool = createReadTool(workspaceRoot); - const wrapped = createOpenClawReadTool(freshReadTool, { + const base: AnyAgentTool[] = []; + if (includeBaseCodingTools) { + for (const tool of createCodingTools(workspaceRoot) as unknown as AnyAgentTool[]) { + if (tool.name === "read") { + if (sandboxRoot) { + const sandboxed = createSandboxedReadTool({ + root: sandboxRoot, + bridge: sandboxFsBridge!, modelContextWindowTokens: options?.modelContextWindowTokens, imageSanitization, }); - return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped]; + base.push( + workspaceOnly + ? wrapToolWorkspaceRootGuardWithOptions(sandboxed, sandboxRoot, { + containerWorkdir: sandbox.containerWorkdir, + }) + : sandboxed, + ); + continue; } - if (tool.name === "bash" || tool.name === execToolName) { - return []; + const freshReadTool = createReadTool(workspaceRoot); + const wrapped = createOpenClawReadTool(freshReadTool, { + modelContextWindowTokens: options?.modelContextWindowTokens, + imageSanitization, + }); + base.push(workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped); + continue; + } + if (tool.name === "bash" || tool.name === execToolName) { + continue; + } + if (tool.name === "write") { + if (sandboxRoot) { + continue; } - if (tool.name === "write") { - if (sandboxRoot) { - return []; - } - const wrapped = createHostWorkspaceWriteTool(workspaceRoot, { workspaceOnly }); - return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped]; + const wrapped = createHostWorkspaceWriteTool(workspaceRoot, { workspaceOnly }); + base.push(workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped); + continue; + } + if (tool.name === "edit") { + if (sandboxRoot) { + continue; } - if (tool.name === "edit") { - if (sandboxRoot) { - return []; - } - const wrapped = createHostWorkspaceEditTool(workspaceRoot, { workspaceOnly }); - return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped]; - } - return [tool]; - }) - : []; + const wrapped = createHostWorkspaceEditTool(workspaceRoot, { workspaceOnly }); + base.push(workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped); + continue; + } + base.push(tool); + } + } options?.recordToolPrepStage?.("base-coding-tools"); const { cleanupMs: cleanupMsOverride, ...execDefaults } = options?.exec ?? {}; const execTool = includeShellTools @@ -754,28 +759,29 @@ export function createOpenClawCodingTools(options?: { : pluginToolsOnly), ]; options?.recordToolPrepStage?.("openclaw-tools"); - const toolsForMemoryFlush = - isMemoryFlushRun && memoryFlushWritePath - ? tools.flatMap((tool) => { - if (!MEMORY_FLUSH_ALLOWED_TOOL_NAMES.has(tool.name)) { - return []; - } - if (tool.name === "write") { - return [ - wrapToolMemoryFlushAppendOnlyWrite(tool, { - root: sandboxRoot ?? workspaceRoot, - relativePath: memoryFlushWritePath, - containerWorkdir: sandbox?.containerWorkdir, - sandbox: - sandboxRoot && sandboxFsBridge - ? { root: sandboxRoot, bridge: sandboxFsBridge } - : undefined, - }), - ]; - } - return [tool]; - }) - : tools; + const toolsForMemoryFlush: AnyAgentTool[] = isMemoryFlushRun && memoryFlushWritePath ? [] : tools; + if (isMemoryFlushRun && memoryFlushWritePath) { + for (const tool of tools) { + if (!MEMORY_FLUSH_ALLOWED_TOOL_NAMES.has(tool.name)) { + continue; + } + if (tool.name === "write") { + toolsForMemoryFlush.push( + wrapToolMemoryFlushAppendOnlyWrite(tool, { + root: sandboxRoot ?? workspaceRoot, + relativePath: memoryFlushWritePath, + containerWorkdir: sandbox?.containerWorkdir, + sandbox: + sandboxRoot && sandboxFsBridge + ? { root: sandboxRoot, bridge: sandboxFsBridge } + : undefined, + }), + ); + continue; + } + toolsForMemoryFlush.push(tool); + } + } const toolsForMessageProvider = filterToolsByMessageProvider( toolsForMemoryFlush, options?.messageProvider, diff --git a/src/agents/tool-display-common.ts b/src/agents/tool-display-common.ts index 702dce06ce4..ed3d29b783c 100644 --- a/src/agents/tool-display-common.ts +++ b/src/agents/tool-display-common.ts @@ -34,14 +34,15 @@ export function defaultTitle(name: string): string { if (!cleaned) { return "Tool"; } - return cleaned - .split(/\s+/) - .map((part) => + const parts: string[] = []; + for (const part of cleaned.split(/\s+/)) { + parts.push( part.length <= 2 && part.toUpperCase() === part ? part : `${part.at(0)?.toUpperCase() ?? ""}${part.slice(1)}`, - ) - .join(" "); + ); + } + return parts.join(" "); } function normalizeVerb(value?: string): string | undefined { @@ -131,14 +132,23 @@ function coerceDisplayValue( return String(value); } if (Array.isArray(value)) { - const values = value - .map((item) => coerceDisplayValue(item, opts)) - .filter((item): item is string => Boolean(item)); - if (values.length === 0) { + const values: string[] = []; + let displayValueCount = 0; + for (const item of value) { + const display = coerceDisplayValue(item, opts); + if (!display) { + continue; + } + displayValueCount += 1; + if (values.length < maxArrayEntries) { + values.push(display); + } + } + if (displayValueCount === 0) { return undefined; } - const preview = values.slice(0, maxArrayEntries).join(", "); - return values.length > maxArrayEntries ? `${preview}…` : preview; + const preview = values.join(", "); + return displayValueCount > maxArrayEntries ? `${preview}…` : preview; } return undefined; } @@ -162,8 +172,13 @@ function lookupValueByPath(args: unknown, path: string): unknown { } export function formatDetailKey(raw: string, overrides: Record = {}): string { - const segments = raw.split(".").filter(Boolean); - const last = segments.at(-1) ?? raw; + let last = ""; + for (const segment of raw.split(".")) { + if (segment) { + last = segment; + } + } + last ||= raw; const override = overrides[last]; if (override) { return override; @@ -295,12 +310,13 @@ function resolveWebFetchDetail(args: unknown): string | undefined { ? Math.floor(record.maxChars) : undefined; - const suffix = [ - mode ? `mode ${mode}` : undefined, - maxChars !== undefined ? `max ${maxChars} chars` : undefined, - ] - .filter((value): value is string => Boolean(value)) - .join(", "); + let suffix = ""; + if (mode) { + suffix = `mode ${mode}`; + } + if (maxChars !== undefined) { + suffix = suffix ? `${suffix}, max ${maxChars} chars` : `max ${maxChars} chars`; + } return suffix ? `from ${url} (${suffix})` : `from ${url}`; } @@ -366,10 +382,15 @@ function resolveDetailFromKeys( return undefined; } - return unique - .slice(0, opts.maxEntries ?? 8) - .map((entry) => `${entry.label} ${entry.value}`) - .join(" · "); + const maxEntries = opts.maxEntries ?? 8; + const parts: string[] = []; + for (let index = 0; index < unique.length && index < maxEntries; index += 1) { + const entry = unique[index]; + if (entry) { + parts.push(`${entry.label} ${entry.value}`); + } + } + return parts.join(" · "); } function resolveToolVerbAndDetail(params: { @@ -438,11 +459,16 @@ export function formatToolDetailText( return undefined; } const normalized = detail.includes(" · ") - ? detail - .split(" · ") - .map((part) => part.trim()) - .filter((part) => part.length > 0) - .join(", ") + ? (() => { + const parts: string[] = []; + for (const part of detail.split(" · ")) { + const trimmed = part.trim(); + if (trimmed) { + parts.push(trimmed); + } + } + return parts.join(", "); + })() : detail; if (!normalized) { return undefined; diff --git a/src/auto-reply/commands-registry.shared.ts b/src/auto-reply/commands-registry.shared.ts index 886ad1102d9..79810656727 100644 --- a/src/auto-reply/commands-registry.shared.ts +++ b/src/auto-reply/commands-registry.shared.ts @@ -70,11 +70,13 @@ function registerAlias(commands: ChatCommandDefinition[], key: string, ...aliase if (!command) { throw new Error(`registerAlias: unknown command key: ${key}`); } - const existing = new Set( - command.textAliases - .map((alias) => normalizeOptionalLowercaseString(alias)) - .filter((alias): alias is string => Boolean(alias)), - ); + const existing = new Set(); + for (const alias of command.textAliases) { + const lowered = normalizeOptionalLowercaseString(alias); + if (lowered) { + existing.add(lowered); + } + } for (const alias of aliases) { const trimmed = alias.trim(); if (!trimmed) { diff --git a/src/auto-reply/heartbeat-filter.ts b/src/auto-reply/heartbeat-filter.ts index 057389db76b..a848a2ec7c4 100644 --- a/src/auto-reply/heartbeat-filter.ts +++ b/src/auto-reply/heartbeat-filter.ts @@ -13,24 +13,23 @@ function resolveMessageText(content: unknown): { text: string; hasNonTextContent return { text: "", hasNonTextContent: content != null }; } let hasNonTextContent = false; - const text = content - .filter((block): block is { type: "text"; text: string } => { - if (typeof block !== "object" || block === null || !("type" in block)) { - hasNonTextContent = true; - return false; - } - if (block.type !== "text") { - hasNonTextContent = true; - return false; - } - if (typeof (block as { text?: unknown }).text !== "string") { - hasNonTextContent = true; - return false; - } - return true; - }) - .map((block) => block.text) - .join(""); + let text = ""; + for (const block of content) { + if (typeof block !== "object" || block === null || !("type" in block)) { + hasNonTextContent = true; + continue; + } + if (block.type !== "text") { + hasNonTextContent = true; + continue; + } + const blockText = (block as { text?: unknown }).text; + if (typeof blockText !== "string") { + hasNonTextContent = true; + continue; + } + text += blockText; + } return { text, hasNonTextContent }; } diff --git a/src/auto-reply/reply/agent-runner-payloads.ts b/src/auto-reply/reply/agent-runner-payloads.ts index 2326d392cdd..b611344b93f 100644 --- a/src/auto-reply/reply/agent-runner-payloads.ts +++ b/src/auto-reply/reply/agent-runner-payloads.ts @@ -163,39 +163,47 @@ export async function buildReplyPayloads(params: { } } - const replyTaggedPayloads = ( - await Promise.all( - applyReplyThreading({ - payloads: sanitizedPayloads, - replyToMode: params.replyToMode, - replyToChannel: params.replyToChannel, + const replyTaggedPayloadCandidates = await Promise.all( + applyReplyThreading({ + payloads: sanitizedPayloads, + replyToMode: params.replyToMode, + replyToChannel: params.replyToChannel, + currentMessageId: params.currentMessageId, + replyThreading: params.replyThreading, + }).map(async (payload) => { + const parsed = normalizeReplyPayloadDirectives({ + payload, currentMessageId: params.currentMessageId, - replyThreading: params.replyThreading, - }).map(async (payload) => { - const parsed = normalizeReplyPayloadDirectives({ - payload, - currentMessageId: params.currentMessageId, - silentToken: SILENT_REPLY_TOKEN, - parseMode: "always", - extractMarkdownImages: params.extractMarkdownImages, - }); - const mediaNormalizedPayload = await normalizeReplyPayloadMedia({ - payload: parsed.payload, - normalizeMediaPaths: params.normalizeMediaPaths, - }); - if ( - parsed.isSilent && - !resolveSendableOutboundReplyParts(mediaNormalizedPayload).hasMedia - ) { - mediaNormalizedPayload.text = undefined; - } - return mediaNormalizedPayload; - }), - ) - ).filter(isRenderablePayload); - const silentFilteredPayloads = params.silentExpected - ? replyTaggedPayloads.filter(shouldKeepPayloadDuringSilentTurn) - : replyTaggedPayloads; + silentToken: SILENT_REPLY_TOKEN, + parseMode: "always", + extractMarkdownImages: params.extractMarkdownImages, + }); + const mediaNormalizedPayload = await normalizeReplyPayloadMedia({ + payload: parsed.payload, + normalizeMediaPaths: params.normalizeMediaPaths, + }); + if (parsed.isSilent && !resolveSendableOutboundReplyParts(mediaNormalizedPayload).hasMedia) { + mediaNormalizedPayload.text = undefined; + } + return mediaNormalizedPayload; + }), + ); + const replyTaggedPayloads: ReplyPayload[] = []; + for (const payload of replyTaggedPayloadCandidates) { + if (isRenderablePayload(payload)) { + replyTaggedPayloads.push(payload); + } + } + const silentFilteredPayloads: ReplyPayload[] = []; + if (params.silentExpected) { + for (const payload of replyTaggedPayloads) { + if (shouldKeepPayloadDuringSilentTurn(payload)) { + silentFilteredPayloads.push(payload); + } + } + } else { + silentFilteredPayloads.push(...replyTaggedPayloads); + } // Drop final payloads only when block streaming succeeded end-to-end. // If streaming aborted (e.g., timeout), fall back to final payloads. @@ -312,15 +320,28 @@ export async function buildReplyPayloads(params: { return preserved; })() : params.blockStreamingEnabled - ? dedupedPayloads.filter( - (payload) => - !params.blockReplyPipeline?.hasSentPayload(payload) && - !isDirectlySentBlockPayload(payload), - ) + ? (() => { + const unsent: ReplyPayload[] = []; + for (const payload of dedupedPayloads) { + if ( + !params.blockReplyPipeline?.hasSentPayload(payload) && + !isDirectlySentBlockPayload(payload) + ) { + unsent.push(payload); + } + } + return unsent; + })() : params.directlySentBlockKeys?.size - ? dedupedPayloads.filter( - (payload) => !params.directlySentBlockKeys!.has(createBlockReplyContentKey(payload)), - ) + ? (() => { + const unsent: ReplyPayload[] = []; + for (const payload of dedupedPayloads) { + if (!params.directlySentBlockKeys.has(createBlockReplyContentKey(payload))) { + unsent.push(payload); + } + } + return unsent; + })() : dedupedPayloads; const blockSentMediaUrls = params.blockStreamingEnabled ? await normalizeSentMediaUrlsForDedupe({ @@ -337,7 +358,14 @@ export async function buildReplyPayloads(params: { sentMediaUrls: blockSentMediaUrls, }) : contentSuppressedPayloads; - const replyPayloads = filteredPayloads.filter(isRenderablePayload); + const replyPayloads: ReplyPayload[] = []; + if (!messagingToolPayloadDedupe.suppressReplies) { + for (const payload of filteredPayloads) { + if (isRenderablePayload(payload)) { + replyPayloads.push(payload); + } + } + } return { replyPayloads, diff --git a/src/auto-reply/reply/commands-tasks.ts b/src/auto-reply/reply/commands-tasks.ts index 05076d2a0b9..c64f72acce6 100644 --- a/src/auto-reply/reply/commands-tasks.ts +++ b/src/auto-reply/reply/commands-tasks.ts @@ -70,7 +70,10 @@ function formatVisibleTask(task: TaskRecord, index: number): string { const status = task.status.replaceAll("_", " "); const timing = formatTaskTiming(task); const detail = formatTaskDetail(task); - const meta = [TASK_RUNTIME_LABELS[task.runtime], status, timing].filter(Boolean).join(" · "); + let meta = `${TASK_RUNTIME_LABELS[task.runtime]} · ${status}`; + if (timing) { + meta += ` · ${timing}`; + } const lines = [`${index + 1}. ${TASK_STATUS_ICONS[task.status]} ${title}`, ` ${meta}`]; if (detail) { lines.push(` ${detail}`); diff --git a/src/auto-reply/reply/route-reply.ts b/src/auto-reply/reply/route-reply.ts index 5cf6899d0b9..1c0dd5c7de8 100644 --- a/src/auto-reply/reply/route-reply.ts +++ b/src/auto-reply/reply/route-reply.ts @@ -155,11 +155,15 @@ export async function routeReply(params: RouteReplyParams): Promise ({ - label: group.label, - tools: sortToolsMessageItems( - group.tools.map((tool) => ({ - id: normalizeToolName(tool.id), - name: tool.label, - description: tool.description || "Tool", - rawDescription: tool.rawDescription || tool.description || "Tool", - source: tool.source, - pluginId: tool.pluginId, - channelId: tool.channelId, - })), - ), - })) - .filter((group) => group.tools.length > 0); + const groups: Array<{ label: string; tools: ToolsMessageItem[] }> = []; + for (const group of result.groups) { + const tools: ToolsMessageItem[] = []; + for (const tool of group.tools) { + tools.push({ + id: normalizeToolName(tool.id), + name: tool.label, + description: tool.description || "Tool", + rawDescription: tool.rawDescription || tool.description || "Tool", + source: tool.source, + pluginId: tool.pluginId, + channelId: tool.channelId, + }); + } + if (tools.length > 0) { + groups.push({ label: group.label, tools: sortToolsMessageItems(tools) }); + } + } if (groups.length === 0) { const lines = [ @@ -89,7 +91,11 @@ export function buildToolsMessage( } continue; } - lines.push(` ${group.tools.map((tool) => formatCompactToolEntry(tool)).join(", ")}`); + const compactTools: string[] = []; + for (const tool of group.tools) { + compactTools.push(formatCompactToolEntry(tool)); + } + lines.push(` ${compactTools.join(", ")}`); } if (verbose) { diff --git a/src/channels/plugins/directory-config-helpers.ts b/src/channels/plugins/directory-config-helpers.ts index 96f98ff5e44..537f5be3f57 100644 --- a/src/channels/plugins/directory-config-helpers.ts +++ b/src/channels/plugins/directory-config-helpers.ts @@ -20,50 +20,76 @@ export function applyDirectoryQueryAndLimit( ): string[] { const q = resolveDirectoryQuery(params.query); const limit = resolveDirectoryLimit(params.limit); - const filtered = ids.filter((id) => (q ? normalizeLowercaseStringOrEmpty(id).includes(q) : true)); - return typeof limit === "number" ? filtered.slice(0, limit) : filtered; + const filtered: string[] = []; + for (const id of ids) { + if (q && !normalizeLowercaseStringOrEmpty(id).includes(q)) { + continue; + } + filtered.push(id); + if (typeof limit === "number" && filtered.length >= limit) { + break; + } + } + return filtered; } export function toDirectoryEntries(kind: "user" | "group", ids: string[]): ChannelDirectoryEntry[] { - return ids.map((id) => ({ kind, id }) as const); -} - -function normalizeDirectoryIds(params: { - rawIds: readonly string[]; - normalizeId?: (entry: string) => string | null | undefined; -}): string[] { - return params.rawIds - .map((entry) => normalizeOptionalString(entry) ?? "") - .filter((entry) => Boolean(entry) && entry !== "*") - .map((entry) => { - const normalized = params.normalizeId ? params.normalizeId(entry) : entry; - return normalizeOptionalString(normalized) ?? ""; - }) - .filter(Boolean); + const entries: ChannelDirectoryEntry[] = []; + for (const id of ids) { + entries.push({ kind, id }); + } + return entries; } function collectDirectoryIdsFromEntries(params: { entries?: readonly unknown[]; normalizeId?: (entry: string) => string | null | undefined; }): string[] { - return normalizeDirectoryIds({ - rawIds: (params.entries ?? []).map((entry) => String(entry)), - normalizeId: params.normalizeId, - }); + const ids: string[] = []; + for (const value of params.entries ?? []) { + const entry = normalizeOptionalString(String(value)) ?? ""; + if (!entry || entry === "*") { + continue; + } + const normalized = params.normalizeId ? params.normalizeId(entry) : entry; + const id = normalizeOptionalString(normalized) ?? ""; + if (id) { + ids.push(id); + } + } + return ids; } function collectDirectoryIdsFromMapKeys(params: { groups?: Record; normalizeId?: (entry: string) => string | null | undefined; }): string[] { - return normalizeDirectoryIds({ - rawIds: Object.keys(params.groups ?? {}), - normalizeId: params.normalizeId, - }); + const ids: string[] = []; + for (const key of Object.keys(params.groups ?? {})) { + const entry = normalizeOptionalString(key) ?? ""; + if (!entry || entry === "*") { + continue; + } + const normalized = params.normalizeId ? params.normalizeId(entry) : entry; + const id = normalizeOptionalString(normalized) ?? ""; + if (id) { + ids.push(id); + } + } + return ids; } function dedupeDirectoryIds(ids: string[]): string[] { - return Array.from(new Set(ids)); + const deduped: string[] = []; + const seen = new Set(); + for (const id of ids) { + if (seen.has(id)) { + continue; + } + seen.add(id); + deduped.push(id); + } + return deduped; } export function collectNormalizedDirectoryIds(params: { From 8a2348547251e7f96a2581381c656cacc264d1f3 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 20:29:33 -0700 Subject: [PATCH 020/215] fix(reply): preserve queue metadata after perf cherry-picks --- src/auto-reply/reply/agent-runner-payloads.ts | 8 +++----- src/auto-reply/reply/queue/drain.ts | 12 +++++++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/auto-reply/reply/agent-runner-payloads.ts b/src/auto-reply/reply/agent-runner-payloads.ts index b611344b93f..ac688407b0b 100644 --- a/src/auto-reply/reply/agent-runner-payloads.ts +++ b/src/auto-reply/reply/agent-runner-payloads.ts @@ -359,11 +359,9 @@ export async function buildReplyPayloads(params: { }) : contentSuppressedPayloads; const replyPayloads: ReplyPayload[] = []; - if (!messagingToolPayloadDedupe.suppressReplies) { - for (const payload of filteredPayloads) { - if (isRenderablePayload(payload)) { - replyPayloads.push(payload); - } + for (const payload of filteredPayloads) { + if (isRenderablePayload(payload)) { + replyPayloads.push(payload); } } diff --git a/src/auto-reply/reply/queue/drain.ts b/src/auto-reply/reply/queue/drain.ts index 2bc1dc72f65..9c15e8d2601 100644 --- a/src/auto-reply/reply/queue/drain.ts +++ b/src/auto-reply/reply/queue/drain.ts @@ -51,9 +51,15 @@ type OriginRoutingMetadata = Pick< function resolveOriginRoutingMetadata(items: FollowupRun[]): OriginRoutingMetadata { const metadata: OriginRoutingMetadata = {}; for (const item of items) { - metadata.originatingChannel ??= item.originatingChannel; - metadata.originatingTo ??= item.originatingTo; - metadata.originatingAccountId ??= item.originatingAccountId; + if (!metadata.originatingChannel && item.originatingChannel) { + metadata.originatingChannel = item.originatingChannel; + } + if (!metadata.originatingTo && item.originatingTo) { + metadata.originatingTo = item.originatingTo; + } + if (!metadata.originatingAccountId && item.originatingAccountId) { + metadata.originatingAccountId = item.originatingAccountId; + } // Support both number (Telegram topic) and string (Slack thread_ts) thread IDs. if ( metadata.originatingThreadId == null && From 42cddcae0ab3ed8addb5866bc6db318c7f73170f Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 20:38:49 -0700 Subject: [PATCH 021/215] fix(agents): keep transcript repair tool names typed --- src/agents/session-transcript-repair.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/agents/session-transcript-repair.ts b/src/agents/session-transcript-repair.ts index d22ba1a5f5a..d30128ebfab 100644 --- a/src/agents/session-transcript-repair.ts +++ b/src/agents/session-transcript-repair.ts @@ -506,7 +506,9 @@ export function repairToolUseResultPairing( const toolCallNamesById = new Map(); for (const toolCall of toolCalls) { toolCallIds.add(toolCall.id); - toolCallNamesById.set(toolCall.id, toolCall.name); + if (typeof toolCall.name === "string") { + toolCallNamesById.set(toolCall.id, toolCall.name); + } } const spanResultsById = new Map>(); From f0a7b8a6a8a9d7567a9c193209b572a3162e67db Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 20:42:38 -0700 Subject: [PATCH 022/215] fix(core): satisfy perf bucket lint --- src/agents/pi-embedded-runner/run/images.ts | 7 ++++++- src/auto-reply/reply/reply-payloads-dedupe.ts | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/agents/pi-embedded-runner/run/images.ts b/src/agents/pi-embedded-runner/run/images.ts index 5389016fac6..b77993713f0 100644 --- a/src/agents/pi-embedded-runner/run/images.ts +++ b/src/agents/pi-embedded-runner/run/images.ts @@ -164,7 +164,12 @@ function extractTrailingAttachmentMediaUris(prompt: string, count: number): stri } uris.push(match[1]); } - return uris.reverse(); + for (let left = 0, right = uris.length - 1; left < right; left += 1, right -= 1) { + const uri = uris[left]; + uris[left] = uris[right]; + uris[right] = uri; + } + return uris; } export function splitPromptAndAttachmentRefs(params: { diff --git a/src/auto-reply/reply/reply-payloads-dedupe.ts b/src/auto-reply/reply/reply-payloads-dedupe.ts index d370549d609..2d33c4ef44e 100644 --- a/src/auto-reply/reply/reply-payloads-dedupe.ts +++ b/src/auto-reply/reply/reply-payloads-dedupe.ts @@ -52,7 +52,7 @@ export function filterMessagingToolMediaDuplicates(params: { let nextPayloads: ReplyPayload[] | undefined; for (let index = 0; index < payloads.length; index++) { - const payload = payloads[index]!; + const payload = payloads[index]; const mediaUrl = payload.mediaUrl; const mediaUrls = payload.mediaUrls; const stripSingle = mediaUrl && sentSet.has(normalizeMediaForDedupe(mediaUrl)); @@ -61,7 +61,7 @@ export function filterMessagingToolMediaDuplicates(params: { let strippedMediaUrls = false; if (mediaUrls?.length) { for (let mediaIndex = 0; mediaIndex < mediaUrls.length; mediaIndex++) { - const url = mediaUrls[mediaIndex]!; + const url = mediaUrls[mediaIndex]; if (sentSet.has(normalizeMediaForDedupe(url))) { strippedMediaUrls = true; if (!filteredUrls) { From f05f9f69d79af9749378da5355a57be55f3a7386 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 20:53:06 -0700 Subject: [PATCH 023/215] fix(agents): leave trusted media guard out of perf churn --- src/agents/pi-embedded-subscribe.tools.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/agents/pi-embedded-subscribe.tools.ts b/src/agents/pi-embedded-subscribe.tools.ts index ca0e87d42f8..9e35cf09b15 100644 --- a/src/agents/pi-embedded-subscribe.tools.ts +++ b/src/agents/pi-embedded-subscribe.tools.ts @@ -249,12 +249,9 @@ const TRUSTED_TOOL_RESULT_MEDIA = new Set([ "x_search", "write", ]); -const TRUSTED_BUNDLED_PLUGIN_MEDIA_TOOLS = new Set(); -for (const entry of pluginRegistrationContractRegistry) { - for (const toolName of entry.toolNames) { - TRUSTED_BUNDLED_PLUGIN_MEDIA_TOOLS.add(toolName); - } -} +const TRUSTED_BUNDLED_PLUGIN_MEDIA_TOOLS = new Set( + pluginRegistrationContractRegistry.flatMap((entry) => entry.toolNames), +); const HTTP_URL_RE = /^https?:\/\//i; function readToolResultDetails(result: unknown): Record | undefined { From 60313069bad244fed7952c82d8b27a39477e529f Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 21:56:33 -0700 Subject: [PATCH 024/215] docs(changelog): move reply queue note to unreleased --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f766ad26e3..27fbab5b0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,7 @@ Docs: https://docs.openclaw.ai - Providers/OpenRouter: expand app-attribution categories so OpenClaw advertises coding, programming, writing, chat, and personal-agent usage on verified OpenRouter routes. Thanks @vincentkoc. - Agents/performance: pass the resolved workspace through BTW, compaction, embedded-run model generation, and PDF model setup so explicit agent-dir model refreshes can reuse the current workspace-scoped plugin metadata snapshot instead of falling back to cold plugin metadata scans. (#77519, #77532) - Plugins/performance: let unscoped model catalog and manifest-contract readers reuse the current workspace-compatible plugin metadata snapshot, avoiding repeated cold plugin metadata scans on hot control-plane paths while preserving env/config/workspace compatibility checks. (#77519, #77532) +- Core/performance: trim reply payload routing, heartbeat filtering, tool display, core tool assembly, channel directory, task status, and Slack approval formatting helper chains with direct bounded scans. Thanks @vincentkoc. - Agents/sandbox: store sandbox container and browser registry entries as per-runtime shard files, reducing unrelated session lock contention while `openclaw doctor --fix` migrates legacy monolithic registry files. (#74831) Thanks @luckylhb90. - Plugins/runtime state: add `registerIfAbsent` for atomic keyed-store dedupe claims that return whether a plugin successfully claimed a key without overwriting an existing live value. Thanks @amknight. - Exec approvals: add a tree-sitter-backed shell command explainer for future approval and command-review surfaces. (#75004) Thanks @jesse-merhi. @@ -521,7 +522,6 @@ Docs: https://docs.openclaw.ai - Channels/streaming: add unified `streaming.mode: "progress"` drafts with auto single-word status labels and shared progress configuration across Discord, Telegram, Matrix, Slack, and Microsoft Teams. - Agents/commands: add `/steer ` for queue-independent steering of the active current-session run without starting a new turn when the session is idle. (#76934) -- Core/performance: trim reply payload routing, heartbeat filtering, tool display, core tool assembly, channel directory, task status, and Slack approval formatting helper chains with direct bounded scans. Thanks @vincentkoc. - Tools/BTW: add `/side` as a text and native slash-command alias for `/btw` side questions. - Doctor/config: `doctor --fix` now commits safe legacy migrations even when unrelated validation issues (e.g. a missing plugin) prevent full validation from passing, so `agents.defaults.llm` and other known-legacy keys are always cleaned up by `doctor --fix` regardless of other config problems. Fixes #76798. (#76800) Thanks @hclsys. - Agents/tools: skip optional media and PDF tool factories when the effective tool denylist already blocks them, avoiding unnecessary hot-path setup for tools that will be filtered out before model use. (#76773) Thanks @dorukardahan. From 70defcc04607ca0be293193e1f97fdeeadce0167 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 22:05:44 -0700 Subject: [PATCH 025/215] fix(commands): audit explicit task records --- src/commands/tasks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/tasks.ts b/src/commands/tasks.ts index 8152875a500..c306d13311c 100644 --- a/src/commands/tasks.ts +++ b/src/commands/tasks.ts @@ -221,7 +221,7 @@ function toSystemAuditFindings(params: { severityFilter?: TaskSystemAuditSeverity; codeFilter?: TaskSystemAuditCode; }) { - const taskFindings = listTaskAuditFindings(); + const taskFindings = listTaskAuditFindings({ tasks: reconcileInspectableTasks() }); const flowFindings = listTaskFlowAuditFindings(); const allFindings: TaskSystemAuditFinding[] = [ ...taskFindings.map((finding) => ({ From c0302512d4dd168e0063023a8002dc8afb102f17 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 10:30:07 -0700 Subject: [PATCH 026/215] perf(slack): reduce message hot-path overhead (cherry picked from commit 9962328b7cf00e94e5d52abb725b1682a71c8d7c) --- .../src/monitor/message-handler/dispatch.ts | 8 +++++- .../message-handler/prepare-content.ts | 28 +++++++++---------- .../src/monitor/message-handler/prepare.ts | 25 ++++++++++------- 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index 9f6c757659e..e3bf3d546a7 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -109,7 +109,13 @@ const UNICODE_TO_SLACK: Record = { }; function toSlackEmojiName(emoji: string): string { - const trimmed = emoji.trim().replace(/^:+|:+$/g, ""); + let trimmed = emoji.trim(); + while (trimmed.startsWith(":")) { + trimmed = trimmed.slice(1); + } + while (trimmed.endsWith(":")) { + trimmed = trimmed.slice(0, -1); + } return UNICODE_TO_SLACK[trimmed] ?? trimmed; } diff --git a/extensions/slack/src/monitor/message-handler/prepare-content.ts b/extensions/slack/src/monitor/message-handler/prepare-content.ts index da130e73fac..52adc6017e3 100644 --- a/extensions/slack/src/monitor/message-handler/prepare-content.ts +++ b/extensions/slack/src/monitor/message-handler/prepare-content.ts @@ -262,29 +262,29 @@ export async function resolveSlackMessageContent(params: { threadStarter: params.threadStarter, }); - const media = + const mediaPromise = ownFiles && ownFiles.length > 0 - ? await (async () => { - const { resolveSlackMedia } = await loadSlackMediaModule(); - return resolveSlackMedia({ + ? loadSlackMediaModule().then(({ resolveSlackMedia }) => + resolveSlackMedia({ files: ownFiles, token: params.botToken, maxBytes: params.mediaMaxBytes, - }); - })() - : null; + }), + ) + : Promise.resolve(null); - const attachmentContent = + const attachmentContentPromise = params.message.attachments && params.message.attachments.length > 0 - ? await (async () => { - const { resolveSlackAttachmentContent } = await loadSlackMediaModule(); - return resolveSlackAttachmentContent({ + ? loadSlackMediaModule().then(({ resolveSlackAttachmentContent }) => + resolveSlackAttachmentContent({ attachments: params.message.attachments, token: params.botToken, maxBytes: params.mediaMaxBytes, - }); - })() - : null; + }), + ) + : Promise.resolve(null); + + const [media, attachmentContent] = await Promise.all([mediaPromise, attachmentContentPromise]); const mergedMedia = [...(media ?? []), ...(attachmentContent?.media ?? [])]; const effectiveDirectMedia = mergedMedia.length > 0 ? mergedMedia : null; diff --git a/extensions/slack/src/monitor/message-handler/prepare.ts b/extensions/slack/src/monitor/message-handler/prepare.ts index 42c82bbaafa..551f9b45617 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.ts @@ -67,6 +67,8 @@ import { isSlackSubteamMentionForBot } from "./subteam-mentions.js"; import type { PreparedSlackMessage } from "./types.js"; const mentionRegexCache = new WeakMap>(); +const SLACK_ANY_MENTION_RE = /<@[^>]+>|]+>/; +const SLACK_SUBTEAM_MENTION_MARKER = "]+>|]+>/.test(message.text ?? ""); + const messageText = message.text ?? ""; + const hasAnyMention = SLACK_ANY_MENTION_RE.test(messageText); + const hasSubteamMention = messageText.includes(SLACK_SUBTEAM_MENTION_MARKER); const explicitlyMentioned = Boolean( ctx.botUserId && - (message.text?.includes(`<@${ctx.botUserId}>`) || - (await isSlackSubteamMentionForBot({ - client: ctx.app.client, - text: message.text, - botUserId: ctx.botUserId, - teamId: ctx.teamId, - log: logVerbose, - }))), + (messageText.includes(`<@${ctx.botUserId}>`) || + (hasSubteamMention && + (await isSlackSubteamMentionForBot({ + client: ctx.app.client, + text: messageText, + botUserId: ctx.botUserId, + teamId: ctx.teamId, + log: logVerbose, + })))), ); const seedTopLevelRoomThreadBySource = opts.source === "app_mention" || opts.wasMentioned === true || explicitlyMentioned; @@ -315,7 +320,7 @@ export async function prepareSlackMessage(params: { opts.wasMentioned ?? (!isDirectMessage && matchesMentionWithExplicit({ - text: message.text ?? "", + text: messageText, mentionRegexes, explicit: { hasAnyMention, From b09033e587c29db667cecfbd53ed29f15706ea6d Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 10:35:10 -0700 Subject: [PATCH 027/215] perf(slack): cache stream recipient team lookup (cherry picked from commit 8ce7cc8aae5b11d45993e82d2e0db0c7fbe52fa2) --- .../dispatch.streaming.test.ts | 27 +++++++++- .../src/monitor/message-handler/dispatch.ts | 50 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/extensions/slack/src/monitor/message-handler/dispatch.streaming.test.ts b/extensions/slack/src/monitor/message-handler/dispatch.streaming.test.ts index 86666f5d3cc..5c3897d6f13 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.streaming.test.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.streaming.test.ts @@ -1,7 +1,8 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { createSlackTurnDeliveryTracker, isSlackStreamingEnabled, + resetSlackStreamRecipientTeamCacheForTests, resolveSlackDisableBlockStreaming, resolveSlackStreamRecipientTeamId, resolveSlackStreamingThreadHint, @@ -9,6 +10,10 @@ import { shouldInitializeSlackDraftStream, } from "./dispatch.js"; +afterEach(() => { + resetSlackStreamRecipientTeamCacheForTests(); +}); + describe("slack native streaming defaults", () => { it("is enabled for partial mode when native streaming is on", () => { expect(isSlackStreamingEnabled({ mode: "partial", nativeStreaming: true })).toBe(true); @@ -93,6 +98,26 @@ describe("slack native streaming recipient team", () => { }), ).toBe("T_LOCAL"); }); + + it("caches resolved user teams for repeated stream starts", async () => { + const usersInfo = vi.fn(async () => ({ + user: { team_id: "T_LOOKUP" }, + })); + const params = { + client: { + users: { + info: usersInfo, + }, + } as never, + token: "xoxb-test", + userId: "U_REMOTE", + fallbackTeamId: "T_LOCAL", + }; + + await expect(resolveSlackStreamRecipientTeamId(params)).resolves.toBe("T_LOOKUP"); + await expect(resolveSlackStreamRecipientTeamId(params)).resolves.toBe("T_LOOKUP"); + expect(usersInfo).toHaveBeenCalledTimes(1); + }); }); describe("slack turn delivery tracker", () => { diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index e3bf3d546a7..5e607e4f71b 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -177,6 +177,9 @@ type SlackTurnDeliveryAttempt = { textOverride?: string; }; +const SLACK_STREAM_RECIPIENT_TEAM_CACHE_MAX = 2000; +const slackStreamRecipientTeamCache = new Map(); + function buildSlackTurnDeliveryKey(params: SlackTurnDeliveryAttempt): string | null { const reply = resolveSendableOutboundReplyParts(params.payload, { text: params.textOverride, @@ -195,6 +198,48 @@ function buildSlackTurnDeliveryKey(params: SlackTurnDeliveryAttempt): string | n }); } +function readSlackStreamRecipientTeamCache(params: { + fallbackTeamId?: string; + userId?: string; +}): string | undefined { + if (!params.fallbackTeamId || !params.userId) { + return undefined; + } + const cacheKey = `${params.fallbackTeamId}:${params.userId}`; + const cached = slackStreamRecipientTeamCache.get(cacheKey); + if (!cached) { + return undefined; + } + slackStreamRecipientTeamCache.delete(cacheKey); + slackStreamRecipientTeamCache.set(cacheKey, cached); + return cached; +} + +function rememberSlackStreamRecipientTeam(params: { + fallbackTeamId?: string; + userId?: string; + teamId: string; +}): void { + if (!params.fallbackTeamId || !params.userId) { + return; + } + const cacheKey = `${params.fallbackTeamId}:${params.userId}`; + if (slackStreamRecipientTeamCache.has(cacheKey)) { + slackStreamRecipientTeamCache.delete(cacheKey); + } + slackStreamRecipientTeamCache.set(cacheKey, params.teamId); + if (slackStreamRecipientTeamCache.size > SLACK_STREAM_RECIPIENT_TEAM_CACHE_MAX) { + const oldest = slackStreamRecipientTeamCache.keys().next().value; + if (oldest) { + slackStreamRecipientTeamCache.delete(oldest); + } + } +} + +export function resetSlackStreamRecipientTeamCacheForTests(): void { + slackStreamRecipientTeamCache.clear(); +} + export function createSlackTurnDeliveryTracker() { const deliveredKeys = new Set(); return { @@ -231,6 +276,10 @@ export async function resolveSlackStreamRecipientTeamId(params: { userId?: PreparedSlackMessage["message"]["user"]; fallbackTeamId?: string; }): Promise { + const cachedTeamId = readSlackStreamRecipientTeamCache(params); + if (cachedTeamId) { + return cachedTeamId; + } if (params.userId) { try { const info = await params.client.users.info({ @@ -239,6 +288,7 @@ export async function resolveSlackStreamRecipientTeamId(params: { }); const teamId = info.user?.team_id ?? info.user?.profile?.team; if (teamId) { + rememberSlackStreamRecipientTeam({ ...params, teamId }); return teamId; } } catch (err) { From ac74a928456dcebb12a5c508392303083d6ae95b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 10:36:15 -0700 Subject: [PATCH 028/215] perf(slack): avoid redundant thread participation lookups (cherry picked from commit 098a8b34b9aad6e1314330876e17d970faa19960) --- .../src/monitor/message-handler/prepare.ts | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/extensions/slack/src/monitor/message-handler/prepare.ts b/extensions/slack/src/monitor/message-handler/prepare.ts index 551f9b45617..59ea569c5f2 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.ts @@ -369,20 +369,30 @@ export async function prepareSlackMessage(params: { `slack: routed via bound conversation ${runtimeBinding.conversation.conversationId} -> ${runtimeBinding.targetSessionKey}`, ); } - const implicitMentionKinds = - isDirectMessage || !ctx.botUserId || !message.thread_ts - ? [] - : [ - ...implicitMentionKindWhen("reply_to_bot", message.parent_user_id === ctx.botUserId), - ...implicitMentionKindWhen( + let implicitMentionKinds: ReturnType = []; + if ( + !isDirectMessage && + ctx.botUserId && + message.thread_ts && + !ctx.threadRequireExplicitMention && + !wasMentioned + ) { + const replyToBotKinds = implicitMentionKindWhen( + "reply_to_bot", + message.parent_user_id === ctx.botUserId, + ); + implicitMentionKinds = + replyToBotKinds.length > 0 + ? replyToBotKinds + : implicitMentionKindWhen( "bot_thread_participant", await hasSlackThreadParticipationWithPersistence({ accountId: account.accountId, channelId: message.channel, threadTs: message.thread_ts, }), - ), - ]; + ); + } let resolvedSenderName = normalizeOptionalString(message.username); const resolveSenderName = async (): Promise => { From 7eaabc0b3bd36edd1c7fa8ef9a880411f417bbf4 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 10:47:13 -0700 Subject: [PATCH 029/215] perf(slack): trim thread context allocation (cherry picked from commit 0caa419f76bdc3bd5eefc24edef1422da78a7d74) --- .../message-handler/prepare-content.ts | 78 ++++++++++----- .../message-handler/prepare-thread-context.ts | 97 +++++++++++++------ extensions/slack/src/monitor/thread.ts | 6 +- 3 files changed, 120 insertions(+), 61 deletions(-) diff --git a/extensions/slack/src/monitor/message-handler/prepare-content.ts b/extensions/slack/src/monitor/message-handler/prepare-content.ts index 52adc6017e3..36f0dc36698 100644 --- a/extensions/slack/src/monitor/message-handler/prepare-content.ts +++ b/extensions/slack/src/monitor/message-handler/prepare-content.ts @@ -13,6 +13,7 @@ type SlackResolvedMessageContent = { const SLACK_MENTION_RESOLUTION_CONCURRENCY = 4; const SLACK_MENTION_RESOLUTION_MAX_LOOKUPS_PER_MESSAGE = 20; +const SLACK_USER_MENTION_RE = /<@([A-Z0-9]+)(?:\|[^>]+)?>/gi; type SlackTextObject = { text?: unknown; @@ -54,7 +55,8 @@ function collectUniqueSlackMentionIds(texts: Array): string[ if (!text) { continue; } - for (const match of text.matchAll(/<@([A-Z0-9]+)(?:\|[^>]+)?>/gi)) { + SLACK_USER_MENTION_RE.lastIndex = 0; + for (const match of text.matchAll(SLACK_USER_MENTION_RE)) { const userId = match[1]; if (!userId || seen.has(userId)) { continue; @@ -73,7 +75,8 @@ function renderSlackUserMentions( if (!text || renderedMentions.size === 0) { return text; } - return text.replace(/<@([A-Z0-9]+)(?:\|[^>]+)?>/gi, (full, userId: string) => { + SLACK_USER_MENTION_RE.lastIndex = 0; + return text.replace(SLACK_USER_MENTION_RE, (full, userId: string) => { const rendered = renderedMentions.get(userId); return rendered ?? full; }); @@ -139,16 +142,19 @@ function renderSlackRichTextElements(elements: unknown): string { break; } case "rich_text_list": { - const listText = Array.isArray(element.elements) - ? element.elements - .map((child) => - child && typeof child === "object" - ? renderSlackRichTextElements((child as SlackRichTextElement).elements) - : "", - ) - .filter(Boolean) - .join("\n") - : ""; + const listParts: string[] = []; + if (Array.isArray(element.elements)) { + for (const child of element.elements) { + if (!child || typeof child !== "object") { + continue; + } + const rendered = renderSlackRichTextElements((child as SlackRichTextElement).elements); + if (rendered) { + listParts.push(rendered); + } + } + } + const listText = listParts.join("\n"); parts.push(listText); break; } @@ -174,7 +180,13 @@ function readSlackBlockText(block: unknown): string | undefined { return text; } if (Array.isArray(blockLike.fields)) { - const fields = blockLike.fields.map(readTextObject).filter(Boolean); + const fields: string[] = []; + for (const field of blockLike.fields) { + const fieldText = readTextObject(field); + if (fieldText) { + fields.push(fieldText); + } + } return fields.length > 0 ? fields.join("\n") : undefined; } return undefined; @@ -185,7 +197,13 @@ function readSlackBlockText(block: unknown): string | undefined { if (!Array.isArray(blockLike.elements)) { return undefined; } - const parts = blockLike.elements.map(readTextObject).filter(Boolean); + const parts: string[] = []; + for (const element of blockLike.elements) { + const text = readTextObject(element); + if (text) { + parts.push(text); + } + } return parts.length > 0 ? parts.join(" ") : undefined; } case "image": @@ -205,7 +223,13 @@ function resolveSlackBlocksText(blocks: unknown[] | undefined): string | undefin if (!blocks?.length) { return undefined; } - const parts = blocks.map(readSlackBlockText).filter(Boolean); + const parts: string[] = []; + for (const block of blocks) { + const text = readSlackBlockText(block); + if (text) { + parts.push(text); + } + } return parts.length > 0 ? parts.join("\n") : undefined; } @@ -302,17 +326,19 @@ export async function resolveSlackMessageContent(params: { : undefined; const fileOnlyPlaceholder = fileOnlyFallback ? `[Slack file: ${fileOnlyFallback}]` : undefined; - const botAttachmentText = - params.isBotMessage && !attachmentContent?.text - ? (params.message.attachments ?? []) - .map( - (attachment) => - normalizeOptionalString(attachment.text) ?? - normalizeOptionalString(attachment.fallback), - ) - .filter(Boolean) - .join("\n") - : undefined; + let botAttachmentText: string | undefined; + if (params.isBotMessage && !attachmentContent?.text) { + const botAttachmentTextParts: string[] = []; + for (const attachment of params.message.attachments ?? []) { + const text = + normalizeOptionalString(attachment.text) ?? normalizeOptionalString(attachment.fallback); + if (text) { + botAttachmentTextParts.push(text); + } + } + botAttachmentText = + botAttachmentTextParts.length > 0 ? botAttachmentTextParts.join("\n") : undefined; + } const blocksText = resolveSlackBlocksText(params.message.blocks); const primaryText = chooseSlackPrimaryText({ diff --git a/extensions/slack/src/monitor/message-handler/prepare-thread-context.ts b/extensions/slack/src/monitor/message-handler/prepare-thread-context.ts index 22bed6ef7d4..be2e418815c 100644 --- a/extensions/slack/src/monitor/message-handler/prepare-thread-context.ts +++ b/extensions/slack/src/monitor/message-handler/prepare-thread-context.ts @@ -1,4 +1,5 @@ import { formatInboundEnvelope } from "openclaw/plugin-sdk/channel-inbound"; +import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime"; import type { ContextVisibilityMode } from "openclaw/plugin-sdk/config-types"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { @@ -29,6 +30,8 @@ type SlackThreadContextData = { threadStarterMedia: SlackMediaResult[] | null; }; +const SLACK_THREAD_CONTEXT_USER_LOOKUP_CONCURRENCY = 4; + function isSlackThreadContextSenderAllowed(params: { allowFromLower: string[]; allowNameMatching: boolean; @@ -50,6 +53,38 @@ function isSlackThreadContextSenderAllowed(params: { }).allowed; } +async function resolveSlackThreadUserMap(params: { + ctx: SlackMonitorContext; + messages: SlackThreadStarter[]; +}): Promise> { + const uniqueUserIds: string[] = []; + const seen = new Set(); + for (const item of params.messages) { + if (!item.userId || seen.has(item.userId)) { + continue; + } + seen.add(item.userId); + uniqueUserIds.push(item.userId); + } + const userMap = new Map(); + if (uniqueUserIds.length === 0) { + return userMap; + } + const { results } = await runTasksWithConcurrency({ + tasks: uniqueUserIds.map((id) => async () => { + const user = await params.ctx.resolveUserName(id); + return user ? { id, user } : null; + }), + limit: SLACK_THREAD_CONTEXT_USER_LOOKUP_CONCURRENCY, + }); + for (const result of results) { + if (result) { + userMap.set(result.id, result.user); + } + } + return userMap; +} + export async function resolveSlackThreadContextData(params: { ctx: SlackMonitorContext; account: ResolvedSlackAccount; @@ -92,7 +127,7 @@ export async function resolveSlackThreadContextData(params: { const starter = params.threadStarter; const starterSenderName = - params.allowNameMatching && starter?.userId + params.allowNameMatching && params.allowFromLower.length > 0 && starter?.userId ? (await params.ctx.resolveUserName(starter.userId))?.name : undefined; const starterIsCurrentBot = Boolean( @@ -174,39 +209,37 @@ export async function resolveSlackThreadContextData(params: { const omittedCurrentBotHistoryCount = threadHistory.length - threadHistoryWithoutCurrentBot.length; - const uniqueUserIds = [ - ...new Set( - threadHistoryWithoutCurrentBot - .map((item) => item.userId) - .filter((id): id is string => Boolean(id)), - ), - ]; - const userMap = new Map(); - await Promise.all( - uniqueUserIds.map(async (id) => { - const user = await params.ctx.resolveUserName(id); - if (user) { - userMap.set(id, user); - } - }), - ); - + const userMapForFilter = + params.contextVisibilityMode !== "all" && + params.allowNameMatching && + params.allowFromLower.length > 0 + ? await resolveSlackThreadUserMap({ + ctx: params.ctx, + messages: threadHistoryWithoutCurrentBot, + }) + : new Map(); const { items: filteredThreadHistory, omitted: omittedHistoryCount } = - filterSupplementalContextItems({ - items: threadHistoryWithoutCurrentBot, - mode: params.contextVisibilityMode, - kind: "thread", - isSenderAllowed: (historyMsg) => { - const msgUser = historyMsg.userId ? userMap.get(historyMsg.userId) : null; - return isSlackThreadContextSenderAllowed({ - allowFromLower: params.allowFromLower, - allowNameMatching: params.allowNameMatching, - userId: historyMsg.userId, - userName: msgUser?.name, - botId: historyMsg.botId, + params.contextVisibilityMode === "all" + ? { items: threadHistoryWithoutCurrentBot, omitted: 0 } + : filterSupplementalContextItems({ + items: threadHistoryWithoutCurrentBot, + mode: params.contextVisibilityMode, + kind: "thread", + isSenderAllowed: (historyMsg) => { + const msgUser = historyMsg.userId ? userMapForFilter.get(historyMsg.userId) : null; + return isSlackThreadContextSenderAllowed({ + allowFromLower: params.allowFromLower, + allowNameMatching: params.allowNameMatching, + userId: historyMsg.userId, + userName: msgUser?.name, + botId: historyMsg.botId, + }); + }, }); - }, - }); + const userMap = await resolveSlackThreadUserMap({ + ctx: params.ctx, + messages: filteredThreadHistory, + }); if (omittedHistoryCount > 0 || omittedCurrentBotHistoryCount > 0) { logVerbose( `slack: omitted ${omittedHistoryCount + omittedCurrentBotHistoryCount} thread message(s) from context (mode=${params.contextVisibilityMode})`, diff --git a/extensions/slack/src/monitor/thread.ts b/extensions/slack/src/monitor/thread.ts index b2fdee74991..9425078cb35 100644 --- a/extensions/slack/src/monitor/thread.ts +++ b/extensions/slack/src/monitor/thread.ts @@ -163,9 +163,9 @@ export async function resolveSlackThreadHistory(params: { continue; } retained.push(msg); - if (retained.length > maxMessages) { - retained.shift(); - } + } + if (retained.length > maxMessages) { + retained.splice(0, retained.length - maxMessages); } const next = response.response_metadata?.next_cursor; From 5a67b57b4b53ed1ee0b166043729029e6001f535 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 20:09:24 -0700 Subject: [PATCH 030/215] chore(changelog): note slack hot-path perf --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27fbab5b0e6..a09575d0902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai - Gateway/Windows: bind the default loopback gateway listener only to `127.0.0.1` on Windows so libuv's dual-stack `::1` behavior cannot wedge localhost HTTP requests. (#69701, fixes #69674) Thanks @SARAMALI15792. - Slack/streaming: add `streaming.progress.render: "rich"` for Block Kit progress drafts backed by structured progress line data. - Slack/streaming: keep the newest rich progress lines when Block Kit limits trim long progress drafts. Thanks @vincentkoc. +- Slack/performance: reduce message preparation, stream recipient lookup, and thread-context allocation overhead on Slack reply hot paths. Thanks @vincentkoc. - Channels/streaming: cap progress-draft tool lines by default so edited progress boxes avoid jumpy reflow from long wrapped lines. - Control UI/chat: add an agent-first filter to the chat session picker, keep chat controls/composer responsive across phone/tablet/desktop widths, keep desktop chat controls on one row, avoid duplicate avatar refreshes during initial chat load, and hide that row while scrolling down the transcript. Thanks @BunsDev. - Control UI/chat: collapse consecutive duplicate text messages into one bubble with a count so repeated text-only messages stay compact without hiding nearby context. From 93747f69553957cbd4fec4922d369419b3b58837 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 20:48:01 -0700 Subject: [PATCH 031/215] test(qa): add discord voice autojoin smoke --- docs/concepts/qa-e2e-automation.md | 10 + .../discord/discord-live.runtime.test.ts | 108 +++++++++ .../discord/discord-live.runtime.ts | 221 +++++++++++++++++- .../convex/payload-validation.ts | 11 + ...nvex-credential-payload-validation.test.ts | 15 ++ 5 files changed, 363 insertions(+), 2 deletions(-) diff --git a/docs/concepts/qa-e2e-automation.md b/docs/concepts/qa-e2e-automation.md index 4ddb7586f9c..11e2432e735 100644 --- a/docs/concepts/qa-e2e-automation.md +++ b/docs/concepts/qa-e2e-automation.md @@ -316,14 +316,24 @@ Required env when `--credential-source env`: Optional: - `OPENCLAW_QA_DISCORD_CAPTURE_CONTENT=1` keeps message bodies in observed-message artifacts. +- `OPENCLAW_QA_DISCORD_VOICE_CHANNEL_ID` selects the voice/stage channel for `discord-voice-autojoin`; without it, the scenario picks the first visible voice/stage channel for the SUT bot. Scenarios (`extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts:36`): - `discord-canary` - `discord-mention-gating` - `discord-native-help-command-registration` +- `discord-voice-autojoin` - opt-in voice scenario. Runs by itself, enables `channels.discord.voice.autoJoin`, and verifies the SUT bot's current Discord voice state is the target voice/stage channel. Convex Discord credentials may include optional `voiceChannelId`; otherwise the runner discovers the first visible voice/stage channel in the guild. - `discord-status-reactions-tool-only` - opt-in Mantis scenario. Runs by itself because it switches the SUT to always-on, tool-only guild replies with `messages.statusReactions.enabled=true`, then captures a REST reaction timeline plus HTML/PNG visual artifacts. Mantis before/after reports also preserve scenario-provided MP4 artifacts as `baseline.mp4` and `candidate.mp4`. +Run the Discord voice auto-join scenario explicitly: + +```bash +pnpm openclaw qa discord \ + --scenario discord-voice-autojoin \ + --provider-mode mock-openai +``` + Run the Mantis status-reaction scenario explicitly: ```bash diff --git a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.test.ts b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.test.ts index f68fd9945a8..6e44f937486 100644 --- a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.test.ts +++ b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.test.ts @@ -30,6 +30,26 @@ describe("discord live qa runtime", () => { }); }); + it("resolves optional Discord QA voice channel env var", () => { + expect( + __testing.resolveDiscordQaRuntimeEnv({ + OPENCLAW_QA_DISCORD_GUILD_ID: "123456789012345678", + OPENCLAW_QA_DISCORD_CHANNEL_ID: "223456789012345678", + OPENCLAW_QA_DISCORD_VOICE_CHANNEL_ID: "523456789012345678", + OPENCLAW_QA_DISCORD_DRIVER_BOT_TOKEN: "driver", + OPENCLAW_QA_DISCORD_SUT_BOT_TOKEN: "sut", + OPENCLAW_QA_DISCORD_SUT_APPLICATION_ID: "323456789012345678", + }), + ).toEqual({ + guildId: "123456789012345678", + channelId: "223456789012345678", + voiceChannelId: "523456789012345678", + driverBotToken: "driver", + sutBotToken: "sut", + sutApplicationId: "323456789012345678", + }); + }); + it("fails when a required Discord QA env var is missing", () => { expect(() => __testing.resolveDiscordQaRuntimeEnv({ @@ -58,6 +78,7 @@ describe("discord live qa runtime", () => { __testing.parseDiscordQaCredentialPayload({ guildId: "123456789012345678", channelId: "223456789012345678", + voiceChannelId: "523456789012345678", driverBotToken: "driver", sutBotToken: "sut", sutApplicationId: "323456789012345678", @@ -65,6 +86,7 @@ describe("discord live qa runtime", () => { ).toEqual({ guildId: "123456789012345678", channelId: "223456789012345678", + voiceChannelId: "523456789012345678", driverBotToken: "driver", sutBotToken: "sut", sutApplicationId: "323456789012345678", @@ -141,6 +163,35 @@ describe("discord live qa runtime", () => { }); }); + it("injects Discord voice auto-join config for the voice smoke", () => { + const next = __testing.buildDiscordQaConfig( + {}, + { + guildId: "123456789012345678", + channelId: "223456789012345678", + driverBotId: "423456789012345678", + sutAccountId: "sut", + sutBotToken: "sut-token", + }, + { + voiceAutoJoin: { + guildId: "123456789012345678", + channelId: "523456789012345678", + }, + }, + ); + + expect(next.channels?.discord?.voice).toEqual({ + enabled: true, + autoJoin: [ + { + guildId: "123456789012345678", + channelId: "523456789012345678", + }, + ], + }); + }); + it("injects tool-only Discord status reaction config for the Mantis scenario", () => { const next = __testing.buildDiscordQaConfig( {}, @@ -250,6 +301,9 @@ describe("discord live qa runtime", () => { expect( __testing.findScenario(["discord-status-reactions-tool-only"]).map((scenario) => scenario.id), ).toEqual(["discord-status-reactions-tool-only"]); + expect( + __testing.findScenario(["discord-voice-autojoin"]).map((scenario) => scenario.id), + ).toEqual(["discord-voice-autojoin"]); expect( __testing .findScenario(["discord-thread-reply-filepath-attachment"]) @@ -464,6 +518,60 @@ describe("discord live qa runtime", () => { ]); }); + it("discovers the first visible Discord voice channel for the voice smoke", async () => { + vi.stubGlobal( + "fetch", + vi.fn( + async () => + new Response( + JSON.stringify([ + { id: "123456789012345678", name: "general", position: 0, type: 0 }, + { id: "523456789012345678", name: "qa-voice", position: 1, type: 2 }, + { id: "623456789012345678", name: "stage", position: 2, type: 13 }, + ]), + { + status: 200, + headers: { + "content-type": "application/json", + }, + }, + ), + ), + ); + + await expect( + __testing.resolveDiscordQaVoiceChannel({ + token: "token", + guildId: "123456789012345678", + }), + ).resolves.toMatchObject({ + id: "523456789012345678", + name: "qa-voice", + }); + }); + + it("normalizes missing current Discord voice state to null", async () => { + vi.stubGlobal( + "fetch", + vi.fn( + async () => + new Response(JSON.stringify({ message: "Unknown Voice State" }), { + status: 404, + headers: { + "content-type": "application/json", + }, + }), + ), + ); + + await expect( + __testing.getCurrentDiscordVoiceState({ + token: "token", + guildId: "123456789012345678", + }), + ).resolves.toBeNull(); + }); + it("waits for required Discord application commands to be registered", async () => { vi.useFakeTimers(); try { diff --git a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts index e838d858df8..a8a521c64b5 100644 --- a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts +++ b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts @@ -2,7 +2,11 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; -import { handleDiscordMessageAction, requestDiscord } from "@openclaw/discord/api.js"; +import { + DiscordApiError, + handleDiscordMessageAction, + requestDiscord, +} from "@openclaw/discord/api.js"; import { DEFAULT_EMOJIS } from "openclaw/plugin-sdk/channel-feedback"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; @@ -35,12 +39,14 @@ type DiscordQaRuntimeEnv = { driverBotToken: string; sutBotToken: string; sutApplicationId: string; + voiceChannelId?: string; }; type DiscordQaScenarioId = | "discord-canary" | "discord-mention-gating" | "discord-native-help-command-registration" + | "discord-voice-autojoin" | "discord-thread-reply-filepath-attachment" | "discord-status-reactions-tool-only"; @@ -56,6 +62,9 @@ type DiscordQaScenarioRun = kind: "application-command-registration"; expectedCommandNames: string[]; } + | { + kind: "voice-autojoin"; + } | { kind: "status-reactions-tool-only"; expectedSequence: string[]; @@ -117,6 +126,21 @@ type DiscordApplicationCommand = { name?: string; }; +type DiscordChannel = { + id: string; + guild_id?: string; + name?: string; + parent_id?: string | null; + position?: number; + type: number; +}; + +type DiscordVoiceState = { + channel_id?: string | null; + guild_id?: string; + user_id?: string; +}; + type DiscordObservedMessage = { messageId: string; channelId: string; @@ -286,6 +310,14 @@ const DISCORD_QA_SCENARIOS: DiscordQaScenarioDefinition[] = [ expectedCommandNames: ["help"], }), }, + { + id: "discord-voice-autojoin", + title: "Discord voice auto-join connects", + timeoutMs: 60_000, + buildRun: () => ({ + kind: "voice-autojoin", + }), + }, { id: "discord-status-reactions-tool-only", title: "Discord explicit status reactions run in tool-only reply mode", @@ -322,6 +354,7 @@ const DISCORD_QA_SCENARIOS: DiscordQaScenarioDefinition[] = [ const DISCORD_QA_DEFAULT_SCENARIOS = DISCORD_QA_SCENARIOS.filter( (scenario) => scenario.id !== "discord-status-reactions-tool-only" && + scenario.id !== "discord-voice-autojoin" && scenario.id !== "discord-thread-reply-filepath-attachment", ); @@ -335,6 +368,7 @@ const discordQaCredentialPayloadSchema = z.object({ driverBotToken: z.string().trim().min(1), sutBotToken: z.string().trim().min(1), sutApplicationId: z.string().trim().min(1), + voiceChannelId: z.string().trim().min(1).optional(), }); function isDiscordSnowflake(value: string) { @@ -361,12 +395,14 @@ function isTruthyOptIn(value: string | undefined) { } function resolveDiscordQaRuntimeEnv(env: NodeJS.ProcessEnv = process.env): DiscordQaRuntimeEnv { + const voiceChannelId = env.OPENCLAW_QA_DISCORD_VOICE_CHANNEL_ID?.trim(); const runtimeEnv = { guildId: resolveEnvValue(env, "OPENCLAW_QA_DISCORD_GUILD_ID"), channelId: resolveEnvValue(env, "OPENCLAW_QA_DISCORD_CHANNEL_ID"), driverBotToken: resolveEnvValue(env, "OPENCLAW_QA_DISCORD_DRIVER_BOT_TOKEN"), sutBotToken: resolveEnvValue(env, "OPENCLAW_QA_DISCORD_SUT_BOT_TOKEN"), sutApplicationId: resolveEnvValue(env, "OPENCLAW_QA_DISCORD_SUT_APPLICATION_ID"), + ...(voiceChannelId ? { voiceChannelId } : {}), }; validateDiscordQaRuntimeEnv(runtimeEnv, "OPENCLAW_QA_DISCORD"); return runtimeEnv; @@ -376,6 +412,9 @@ function validateDiscordQaRuntimeEnv(runtimeEnv: DiscordQaRuntimeEnv, prefix: st assertDiscordSnowflake(runtimeEnv.guildId, `${prefix}_GUILD_ID`); assertDiscordSnowflake(runtimeEnv.channelId, `${prefix}_CHANNEL_ID`); assertDiscordSnowflake(runtimeEnv.sutApplicationId, `${prefix}_SUT_APPLICATION_ID`); + if (runtimeEnv.voiceChannelId) { + assertDiscordSnowflake(runtimeEnv.voiceChannelId, `${prefix}_VOICE_CHANNEL_ID`); + } } function parseDiscordQaCredentialPayload(payload: unknown): DiscordQaRuntimeEnv { @@ -386,6 +425,7 @@ function parseDiscordQaCredentialPayload(payload: unknown): DiscordQaRuntimeEnv driverBotToken: parsed.driverBotToken, sutBotToken: parsed.sutBotToken, sutApplicationId: parsed.sutApplicationId, + ...(parsed.voiceChannelId ? { voiceChannelId: parsed.voiceChannelId } : {}), }; validateDiscordQaRuntimeEnv(runtimeEnv, "Discord credential payload"); return runtimeEnv; @@ -402,6 +442,10 @@ function buildDiscordQaConfig( }, options: { statusReactionsToolOnly?: boolean; + voiceAutoJoin?: { + channelId: string; + guildId: string; + }; } = {}, ): OpenClawConfig { const pluginAllow = [...new Set([...(baseCfg.plugins?.allow ?? []), "discord"])]; @@ -435,6 +479,13 @@ function buildDiscordQaConfig( visibleReplies: "automatic" as const, }, }; + const voiceConfig = options.voiceAutoJoin + ? { + ...baseCfg.channels?.discord?.voice, + enabled: true, + autoJoin: [options.voiceAutoJoin], + } + : undefined; return { ...baseCfg, plugins: { @@ -448,6 +499,7 @@ function buildDiscordQaConfig( discord: { enabled: true, defaultAccount: params.sutAccountId, + ...(voiceConfig ? { voice: voiceConfig } : {}), accounts: { [params.sutAccountId]: { enabled: true, @@ -480,6 +532,125 @@ async function getCurrentDiscordUser(token: string) { }); } +async function listGuildChannels(params: { token: string; guildId: string }) { + return await requestDiscord( + `/guilds/${params.guildId}/channels`, + params.token, + { + timeoutMs: 15_000, + }, + ); +} + +async function getDiscordChannel(params: { token: string; channelId: string }) { + return await requestDiscord(`/channels/${params.channelId}`, params.token, { + timeoutMs: 15_000, + }); +} + +function isDiscordVoiceChannel(channel: DiscordChannel) { + return channel.type === 2 || channel.type === 13; +} + +function formatDiscordChannelLabel(channel: DiscordChannel) { + return channel.name?.trim() ? `${channel.name} (${channel.id})` : channel.id; +} + +async function resolveDiscordQaVoiceChannel(params: { + guildId: string; + token: string; + voiceChannelId?: string; +}) { + if (params.voiceChannelId) { + const channel = await getDiscordChannel({ + token: params.token, + channelId: params.voiceChannelId, + }); + if (!isDiscordVoiceChannel(channel)) { + throw new Error(`Discord voiceChannelId ${params.voiceChannelId} is not a voice channel.`); + } + if (channel.guild_id && channel.guild_id !== params.guildId) { + throw new Error( + `Discord voiceChannelId ${params.voiceChannelId} belongs to guild ${channel.guild_id}, not ${params.guildId}.`, + ); + } + return channel; + } + + const channels = await listGuildChannels({ token: params.token, guildId: params.guildId }); + const voiceChannels = channels + .filter(isDiscordVoiceChannel) + .toSorted( + (a, b) => + (a.position ?? Number.MAX_SAFE_INTEGER) - (b.position ?? Number.MAX_SAFE_INTEGER) || + (a.name ?? "").localeCompare(b.name ?? "") || + a.id.localeCompare(b.id), + ); + const first = voiceChannels[0]; + if (!first) { + throw new Error( + "Discord voice auto-join scenario could not find a visible voice/stage channel for the SUT bot. Add voiceChannelId to the Convex discord credential payload or set OPENCLAW_QA_DISCORD_VOICE_CHANNEL_ID.", + ); + } + return first; +} + +async function getCurrentDiscordVoiceState(params: { token: string; guildId: string }) { + try { + return await requestDiscord( + `/guilds/${params.guildId}/voice-states/@me`, + params.token, + { + timeoutMs: 15_000, + }, + ); + } catch (error) { + if (error instanceof DiscordApiError && error.status === 404) { + return null; + } + throw error; + } +} + +async function waitForDiscordVoiceState(params: { + channelId: string; + guildId: string; + sutBotId: string; + timeoutMs: number; + token: string; +}) { + const startedAt = Date.now(); + let lastState: DiscordVoiceState | null = null; + let lastError: string | undefined; + while (Date.now() - startedAt < params.timeoutMs) { + try { + const state = await getCurrentDiscordVoiceState({ + token: params.token, + guildId: params.guildId, + }); + lastState = state; + lastError = undefined; + if ( + state?.channel_id === params.channelId && + (!state.user_id || state.user_id === params.sutBotId) + ) { + return state; + } + } catch (error) { + lastError = formatErrorMessage(error); + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + const stateDetails = lastState + ? `last voice state channel=${lastState.channel_id ?? "none"} user=${lastState.user_id ?? "unknown"}` + : "no current voice state"; + throw new Error( + `SUT bot did not join Discord voice channel ${params.channelId} (${stateDetails}${ + lastError ? `; last error: ${lastError}` : "" + })`, + ); +} + async function sendChannelMessage(token: string, channelId: string, content: string) { return await requestDiscord(`/channels/${channelId}/messages`, token, { body: { @@ -1362,11 +1533,19 @@ export async function runDiscordQaLive(params: { const statusReactionScenarioRequested = scenarios.some( (scenario) => scenario.id === "discord-status-reactions-tool-only", ); + const voiceAutoJoinScenarioRequested = scenarios.some( + (scenario) => scenario.id === "discord-voice-autojoin", + ); if (statusReactionScenarioRequested && scenarios.length > 1) { throw new Error( "discord-status-reactions-tool-only must run by itself because it changes Discord tool-only reply config.", ); } + if (voiceAutoJoinScenarioRequested && scenarios.length > 1) { + throw new Error( + "discord-voice-autojoin must run by itself because it changes Discord voice auto-join config.", + ); + } const credentialLease = await acquireQaCredentialLease({ kind: "discord", @@ -1403,6 +1582,13 @@ export async function runDiscordQaLive(params: { "Discord QA SUT application id must match the SUT bot user id returned by Discord.", ); } + const voiceChannel = voiceAutoJoinScenarioRequested + ? await resolveDiscordQaVoiceChannel({ + guildId: runtimeEnv.guildId, + token: runtimeEnv.sutBotToken, + voiceChannelId: runtimeEnv.voiceChannelId, + }) + : undefined; const gatewayHarness = await startQaLiveLaneGateway({ repoRoot, @@ -1426,7 +1612,15 @@ export async function runDiscordQaLive(params: { sutAccountId, sutBotToken: runtimeEnv.sutBotToken, }, - { statusReactionsToolOnly: statusReactionScenarioRequested }, + voiceChannel + ? { + voiceAutoJoin: { + guildId: runtimeEnv.guildId, + channelId: voiceChannel.id, + }, + statusReactionsToolOnly: statusReactionScenarioRequested, + } + : { statusReactionsToolOnly: statusReactionScenarioRequested }, ), }); try { @@ -1453,6 +1647,27 @@ export async function runDiscordQaLive(params: { }); continue; } + if (scenarioRun.kind === "voice-autojoin") { + if (!voiceChannel) { + throw new Error("Discord voice auto-join scenario did not resolve a voice channel."); + } + await waitForDiscordVoiceState({ + token: runtimeEnv.sutBotToken, + guildId: runtimeEnv.guildId, + channelId: voiceChannel.id, + sutBotId: sutIdentity.id, + timeoutMs: scenario.timeoutMs, + }); + scenarioResults.push({ + id: scenario.id, + title: scenario.title, + status: "pass", + details: redactPublicMetadata + ? "SUT bot joined voice channel" + : `SUT bot joined voice channel ${formatDiscordChannelLabel(voiceChannel)}`, + }); + continue; + } if (scenarioRun.kind === "thread-reply-filepath-attachment") { const result = await runDiscordThreadReplyFilePathAttachmentScenario({ cfg: buildDiscordQaConfig( @@ -1705,7 +1920,9 @@ export const __testing = { findScenario, getCurrentDiscordUser, getChannelMessage, + getCurrentDiscordVoiceState, listApplicationCommands, + resolveDiscordQaVoiceChannel, matchesDiscordScenarioReply, normalizeDiscordReactionSnapshot, normalizeDiscordObservedMessage, diff --git a/qa/convex-credential-broker/convex/payload-validation.ts b/qa/convex-credential-broker/convex/payload-validation.ts index 778fef019a9..91104a63f79 100644 --- a/qa/convex-credential-broker/convex/payload-validation.ts +++ b/qa/convex-credential-broker/convex/payload-validation.ts @@ -95,6 +95,16 @@ function normalizeDiscordCredentialPayload( "sutApplicationId", createFailure, ); + const voiceChannelId = + typeof payload.voiceChannelId === "string" && payload.voiceChannelId.trim() + ? payload.voiceChannelId.trim() + : undefined; + if (voiceChannelId && !DISCORD_SNOWFLAKE_RE.test(voiceChannelId)) { + throwPayloadError( + createFailure, + 'Credential payload for kind "discord" must include "voiceChannelId" as a Discord snowflake string when set.', + ); + } const driverBotToken = requirePayloadString(payload, "driverBotToken", "discord", createFailure); const sutBotToken = requirePayloadString(payload, "sutBotToken", "discord", createFailure); @@ -104,6 +114,7 @@ function normalizeDiscordCredentialPayload( driverBotToken, sutBotToken, sutApplicationId, + ...(voiceChannelId ? { voiceChannelId } : {}), } satisfies Record; } diff --git a/test/qa-convex-credential-payload-validation.test.ts b/test/qa-convex-credential-payload-validation.test.ts index e046dd0071f..68693ae53d9 100644 --- a/test/qa-convex-credential-payload-validation.test.ts +++ b/test/qa-convex-credential-payload-validation.test.ts @@ -10,6 +10,7 @@ describe("QA Convex credential payload validation", () => { normalizeCredentialPayloadForKind("discord", { guildId: " 1496962067029299350 ", channelId: "1496962068027281447", + voiceChannelId: "1496962069025263624", driverBotToken: " driver-token ", sutBotToken: "sut-token", sutApplicationId: "1496963665587601428", @@ -18,6 +19,7 @@ describe("QA Convex credential payload validation", () => { ).toEqual({ guildId: "1496962067029299350", channelId: "1496962068027281447", + voiceChannelId: "1496962069025263624", driverBotToken: "driver-token", sutBotToken: "sut-token", sutApplicationId: "1496963665587601428", @@ -48,6 +50,19 @@ describe("QA Convex credential payload validation", () => { ).toThrow(/driverBotToken/u); }); + it("rejects malformed optional Discord voice channel ids", () => { + expect(() => + normalizeCredentialPayloadForKind("discord", { + guildId: "1496962067029299350", + channelId: "1496962068027281447", + voiceChannelId: "voice-channel", + driverBotToken: "driver-token", + sutBotToken: "sut-token", + sutApplicationId: "1496963665587601428", + }), + ).toThrow(/voiceChannelId/u); + }); + it("keeps unknown credential kinds pass-through-compatible", () => { const payload = { anything: true }; From 6e5ba8b0471f2d7e3e534a71d5bde432921f7c4f Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 21:45:59 -0700 Subject: [PATCH 032/215] fix(discord): smooth voice capture prompts --- CHANGELOG.md | 1 + docs/channels/discord.md | 1 + extensions/discord/src/config-schema.test.ts | 4 ++ extensions/discord/src/config-ui-hints.ts | 4 ++ .../discord/src/voice/manager.e2e.test.ts | 38 ++++++++++++++++++- extensions/discord/src/voice/manager.ts | 8 +++- extensions/discord/src/voice/prompt.test.ts | 4 ++ extensions/discord/src/voice/prompt.ts | 7 +++- extensions/discord/src/voice/session.ts | 2 +- src/config/types.discord.ts | 2 + src/config/zod-schema.providers-core.ts | 1 + 11 files changed, 67 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a09575d0902..19e4f0988a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai - Gateway/tasks: reconcile stale CLI run-context tasks whose live run context disappeared even when a child session row remains, and apply the default bounded reload deferral timeout to channel hot reloads so stale task records cannot block Discord/Slack/Telegram reloads forever. - Discord/voice: make `openclaw channels capabilities --channel discord --target channel:` and `channels status --probe` audit voice-channel permissions, including auto-join targets, so missing Connect/Speak/Read Message History permissions show up before `/vc join`. - Docs/iMessage: deprecate BlueBubbles for new OpenClaw setups, document the upstream server-release rationale, and point new iMessage deployments toward the native `imsg` path while keeping BlueBubbles as a supported legacy fallback. +- Discord/voice: make voice capture less choppy by extending the default post-speech silence grace to 2.5s, add `voice.captureSilenceGraceMs` for noisy Discord sessions, and tighten the spoken-output prompt around live STT fragments. Thanks @vincentkoc. - Discord/streaming: default Discord replies to progress draft previews so tool/work activity appears in one edited Discord message unless `channels.discord.streaming.mode` is set to `off`. - Plugins/install: add `npm-pack:` installs so local npm pack artifacts run through the same managed npm-root install, lockfile verification, dependency scan, and install-record path as registry npm plugins. - Codex app-server: disarm the short post-tool completion watchdog after current-turn activity, expose `appServer.turnCompletionIdleTimeoutMs`, and include raw assistant item context in idle-timeout diagnostics so status-only post-tool stalls stop failing as idle. Fixes #77984. Thanks @roseware-dev and @rubencu. diff --git a/docs/channels/discord.md b/docs/channels/discord.md index b6606d54114..6cf6dbd2b0b 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -1206,6 +1206,7 @@ Notes: - `voice.connectTimeoutMs` controls the initial `@discordjs/voice` Ready wait for `/vc join` and auto-join attempts. Default: `30000`. - `voice.reconnectGraceMs` controls how long OpenClaw waits for a disconnected voice session to begin reconnecting before destroying it. Default: `15000`. - Voice playback does not stop just because another user starts speaking. To avoid feedback loops, OpenClaw ignores new voice capture while TTS is playing; speak after playback finishes for the next turn. +- `voice.captureSilenceGraceMs` controls how long OpenClaw waits after Discord reports a speaker has stopped before finalizing that audio segment for STT. Default: `2500`; raise this if Discord splits normal pauses into choppy partial transcripts. - OpenClaw also watches receive decrypt failures and auto-recovers by leaving/rejoining the voice channel after repeated failures in a short window. - If receive logs repeatedly show `DecryptionFailed(UnencryptedWhenPassthroughDisabled)` after updating, collect a dependency report and logs. The bundled `@discordjs/voice` line includes the upstream padding fix from discord.js PR #11449, which closed discord.js issue #11419. - `The operation was aborted` receive events are expected when OpenClaw finalizes a captured speaker segment; they are verbose diagnostics, not warnings. diff --git a/extensions/discord/src/config-schema.test.ts b/extensions/discord/src/config-schema.test.ts index ce8c62b2517..3373878bfa0 100644 --- a/extensions/discord/src/config-schema.test.ts +++ b/extensions/discord/src/config-schema.test.ts @@ -152,11 +152,13 @@ describe("discord config schema", () => { voice: { connectTimeoutMs: 45_000, reconnectGraceMs: 20_000, + captureSilenceGraceMs: 3_500, }, }); expect(cfg.voice?.connectTimeoutMs).toBe(45_000); expect(cfg.voice?.reconnectGraceMs).toBe(20_000); + expect(cfg.voice?.captureSilenceGraceMs).toBe(3_500); }); it("rejects invalid Discord voice timing overrides", () => { @@ -165,6 +167,8 @@ describe("discord config schema", () => { { connectTimeoutMs: 120_001 }, { reconnectGraceMs: -1 }, { reconnectGraceMs: 1.5 }, + { captureSilenceGraceMs: 0 }, + { captureSilenceGraceMs: 30_001 }, ]) { expectInvalidDiscordConfig({ voice }); } diff --git a/extensions/discord/src/config-ui-hints.ts b/extensions/discord/src/config-ui-hints.ts index b6c8e57d7a5..fee04e9cffc 100644 --- a/extensions/discord/src/config-ui-hints.ts +++ b/extensions/discord/src/config-ui-hints.ts @@ -201,6 +201,10 @@ export const discordChannelConfigUiHints = { label: "Discord Voice Reconnect Grace (ms)", help: "Grace period for a disconnected Discord voice session to enter Signalling or Connecting before OpenClaw destroys it. Default: 15000.", }, + "voice.captureSilenceGraceMs": { + label: "Discord Voice Capture Silence Grace (ms)", + help: "Silence window after Discord reports a speaker ended before OpenClaw finalizes the audio segment for transcription. Default: 2500.", + }, "voice.tts": { label: "Discord Voice Text-to-Speech", help: "Optional TTS overrides for Discord voice playback (merged with messages.tts).", diff --git a/extensions/discord/src/voice/manager.e2e.test.ts b/extensions/discord/src/voice/manager.e2e.test.ts index 4a5f08a7eac..c566690029c 100644 --- a/extensions/discord/src/voice/manager.e2e.test.ts +++ b/extensions/discord/src/voice/manager.e2e.test.ts @@ -573,7 +573,7 @@ describe("DiscordVoiceManager", () => { } ).scheduleCaptureFinalize(entry, "u1", "test"); - await vi.advanceTimersByTimeAsync(1_200); + await vi.advanceTimersByTimeAsync(2_500); expect(firstStream.destroy).toHaveBeenCalledTimes(1); expect(entry?.capture.activeSpeakers.has("u1")).toBe(false); @@ -600,6 +600,41 @@ describe("DiscordVoiceManager", () => { } }); + it("uses configured silence grace before finalizing voice capture", async () => { + vi.useFakeTimers(); + try { + const manager = createManager({ + voice: { + enabled: true, + captureSilenceGraceMs: 4_000, + }, + }); + const stream = { destroy: vi.fn() }; + const entry = { + guildId: "g1", + channelId: "1001", + capture: createVoiceCaptureState(), + }; + entry.capture.activeSpeakers.add("u1"); + entry.capture.captureGenerations.set("u1", 1); + entry.capture.activeCaptureStreams.set("u1", { generation: 1, stream }); + + ( + manager as unknown as { + scheduleCaptureFinalize: (entry: unknown, userId: string, reason: string) => void; + } + ).scheduleCaptureFinalize(entry, "u1", "test"); + + await vi.advanceTimersByTimeAsync(3_999); + expect(stream.destroy).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(1); + expect(stream.destroy).toHaveBeenCalledTimes(1); + } finally { + vi.useRealTimers(); + } + }); + it("passes senderIsOwner=true for allowlisted voice speakers", async () => { const client = createClient(); client.fetchMember.mockResolvedValue({ @@ -702,6 +737,7 @@ describe("DiscordVoiceManager", () => { expect(commandArgs?.messageChannel).toBe("discord"); expect(commandArgs?.messageProvider).toBe("discord-voice"); expect(commandArgs?.message).toContain("Do not call the tts tool"); + expect(commandArgs?.message).toContain("repair obvious transcription artifacts"); expect(textToSpeechMock).toHaveBeenCalledWith( expect.objectContaining({ channel: "discord", diff --git a/extensions/discord/src/voice/manager.ts b/extensions/discord/src/voice/manager.ts index 1c03aac44aa..f429bf38807 100644 --- a/extensions/discord/src/voice/manager.ts +++ b/extensions/discord/src/voice/manager.ts @@ -462,13 +462,17 @@ export class DiscordVoiceManager { } private scheduleCaptureFinalize(entry: VoiceSessionEntry, userId: string, reason: string) { + const graceMs = resolveVoiceTimeoutMs( + this.params.discordConfig.voice?.captureSilenceGraceMs, + CAPTURE_FINALIZE_GRACE_MS, + ); scheduleVoiceCaptureFinalize({ state: entry.capture, userId, - delayMs: CAPTURE_FINALIZE_GRACE_MS, + delayMs: graceMs, onFinalize: () => { logVoiceVerbose( - `capture finalize: guild ${entry.guildId} channel ${entry.channelId} user ${userId} reason=${reason} grace=${CAPTURE_FINALIZE_GRACE_MS}ms`, + `capture finalize: guild ${entry.guildId} channel ${entry.channelId} user ${userId} reason=${reason} grace=${graceMs}ms`, ); }, }); diff --git a/extensions/discord/src/voice/prompt.test.ts b/extensions/discord/src/voice/prompt.test.ts index f747f42ac6c..07b84f25cb9 100644 --- a/extensions/discord/src/voice/prompt.test.ts +++ b/extensions/discord/src/voice/prompt.test.ts @@ -3,6 +3,10 @@ import { DISCORD_VOICE_SPOKEN_OUTPUT_CONTRACT, formatVoiceIngressPrompt } from " describe("formatVoiceIngressPrompt", () => { it("formats speaker-labeled voice input with the spoken-output contract", () => { + expect(DISCORD_VOICE_SPOKEN_OUTPUT_CONTRACT).toContain("OpenClaw's Discord voice interface"); + expect(DISCORD_VOICE_SPOKEN_OUTPUT_CONTRACT).toContain( + "repair obvious transcription artifacts", + ); expect(formatVoiceIngressPrompt("hello there", "speaker-1")).toBe( `${DISCORD_VOICE_SPOKEN_OUTPUT_CONTRACT}\n\nVoice transcript from speaker "speaker-1":\nhello there`, ); diff --git a/extensions/discord/src/voice/prompt.ts b/extensions/discord/src/voice/prompt.ts index bc49e896646..91346640eff 100644 --- a/extensions/discord/src/voice/prompt.ts +++ b/extensions/discord/src/voice/prompt.ts @@ -1,9 +1,14 @@ export const DISCORD_VOICE_SPOKEN_OUTPUT_CONTRACT = [ + "You are OpenClaw's Discord voice interface in a live voice channel.", "Discord voice reply requirements:", "- Return only the concise text that should be spoken aloud in the voice channel.", + "- Treat the transcript as speech-to-text from a live conversation; repair obvious transcription artifacts and ignore repeated partial fragments caused by voice buffering.", + "- If the transcript is garbled, incomplete, or missing the user's intent, ask one brief clarifying question instead of guessing.", + "- If the request needs deeper reasoning, current information, or tools, use the available tools before answering.", "- Do not call the tts tool; Discord voice will synthesize and play the returned text.", "- Do not reply with NO_REPLY unless no spoken response is appropriate.", - "- Keep the response brief and conversational.", + "- Keep the response brief, natural, and conversational. Prefer one to three short sentences.", + "- Avoid markdown tables, code fences, citations, and visual formatting unless the user explicitly asks for something that cannot be spoken naturally.", ].join("\n"); export function formatVoiceIngressPrompt(transcript: string, speakerLabel?: string): string { diff --git a/extensions/discord/src/voice/session.ts b/extensions/discord/src/voice/session.ts index 4ed98b7f946..9960deb194a 100644 --- a/extensions/discord/src/voice/session.ts +++ b/extensions/discord/src/voice/session.ts @@ -5,7 +5,7 @@ import type { VoiceCaptureState } from "./capture-state.js"; import type { VoiceReceiveRecoveryState } from "./receive-recovery.js"; export const MIN_SEGMENT_SECONDS = 0.35; -export const CAPTURE_FINALIZE_GRACE_MS = 1_200; +export const CAPTURE_FINALIZE_GRACE_MS = 2_500; export const VOICE_CONNECT_READY_TIMEOUT_MS = 30_000; export const VOICE_RECONNECT_GRACE_MS = 15_000; export const PLAYBACK_READY_TIMEOUT_MS = 60_000; diff --git a/src/config/types.discord.ts b/src/config/types.discord.ts index 46a22b181b4..8a7a0039a02 100644 --- a/src/config/types.discord.ts +++ b/src/config/types.discord.ts @@ -144,6 +144,8 @@ export type DiscordVoiceConfig = { connectTimeoutMs?: number; /** Grace period for Discord voice reconnect signalling after a disconnect (default: 15000). */ reconnectGraceMs?: number; + /** Silence grace after Discord reports a speaker ended before finalizing STT capture (default: 2500). */ + captureSilenceGraceMs?: number; /** Optional TTS overrides for Discord voice output. */ tts?: TtsConfig; }; diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 1454dcbbace..ee5eeb6df34 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -549,6 +549,7 @@ const DiscordVoiceSchema = z decryptionFailureTolerance: z.number().int().min(0).optional(), connectTimeoutMs: z.number().int().positive().max(120_000).optional(), reconnectGraceMs: z.number().int().positive().max(120_000).optional(), + captureSilenceGraceMs: z.number().int().positive().max(30_000).optional(), tts: TtsConfigSchema.optional(), }) .strict() From 0a3c7d34e6c173accb670180989d503ff8815a01 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 21:49:32 -0700 Subject: [PATCH 033/215] test(discord): type voice capture stream mock --- extensions/discord/src/voice/manager.e2e.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions/discord/src/voice/manager.e2e.test.ts b/extensions/discord/src/voice/manager.e2e.test.ts index c566690029c..77b617423db 100644 --- a/extensions/discord/src/voice/manager.e2e.test.ts +++ b/extensions/discord/src/voice/manager.e2e.test.ts @@ -1,3 +1,4 @@ +import type { Readable } from "node:stream"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { ChannelType } from "../internal/discord.js"; import { createVoiceCaptureState } from "./capture-state.js"; @@ -617,7 +618,10 @@ describe("DiscordVoiceManager", () => { }; entry.capture.activeSpeakers.add("u1"); entry.capture.captureGenerations.set("u1", 1); - entry.capture.activeCaptureStreams.set("u1", { generation: 1, stream }); + entry.capture.activeCaptureStreams.set("u1", { + generation: 1, + stream: stream as unknown as Readable, + }); ( manager as unknown as { From a8d8d49ab818a7d2e7b4e0c22f2d091ffa24a7bb Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 7 May 2026 00:31:41 -0500 Subject: [PATCH 034/215] fix(ui): label inherited thinking overrides Closes #77581. ## Summary - Add a shared thinking-label formatter for inherited vs explicit reasoning values. - Show inherited thinking controls as `Inherited (Default: )` in chat and Sessions selectors. - Preserve provider/model labels for explicit thinking overrides and document the inherited/default wording. ## Verification - `pnpm docs:list` - `pnpm tsgo:core` on current `origin/main` (`70defcc046`) -> passes - `git diff --check` - `pnpm exec oxfmt --check --threads=1 ui/src/ui/thinking-labels.ts ui/src/ui/chat/session-controls.ts ui/src/ui/views/chat.test.ts ui/src/ui/views/sessions.ts ui/src/ui/views/sessions.test.ts docs/tools/thinking.md CHANGELOG.md` - `pnpm changed:lanes --json` -> `core`, `coreTests`, `docs` - `pnpm test ui/src/ui/views/chat.test.ts ui/src/ui/views/sessions.test.ts src/gateway/server.sessions.list-changed.test.ts` -> 3 Vitest shards, 58 tests - Testbox `pnpm check:changed` on `a906cb75ce` -> passes - GitHub PR checks for #78176 on `a906cb75ce` -> no pending or failed jobs --- CHANGELOG.md | 1 + docs/tools/thinking.md | 3 +- ui/src/ui/chat/session-controls.ts | 23 ++++++------- ui/src/ui/thinking-labels.ts | 23 +++++++++++++ ui/src/ui/views/chat.test.ts | 24 +++++++------- ui/src/ui/views/sessions.test.ts | 53 +++++++++++++++++++++++++++--- ui/src/ui/views/sessions.ts | 51 +++++++++++++++++++--------- 7 files changed, 133 insertions(+), 45 deletions(-) create mode 100644 ui/src/ui/thinking-labels.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 19e4f0988a2..48f4ec4386a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai - Channels/streaming: cap progress-draft tool lines by default so edited progress boxes avoid jumpy reflow from long wrapped lines. - Control UI/chat: add an agent-first filter to the chat session picker, keep chat controls/composer responsive across phone/tablet/desktop widths, keep desktop chat controls on one row, avoid duplicate avatar refreshes during initial chat load, and hide that row while scrolling down the transcript. Thanks @BunsDev. - Control UI/chat: collapse consecutive duplicate text messages into one bubble with a count so repeated text-only messages stay compact without hiding nearby context. +- Control UI/chat and Sessions: label inherited thinking defaults separately from explicit overrides while preserving provider-supplied option labels. Fixes #77581. Thanks @BunsDev and @Beandon13. - Agents/subagents: preserve every grouped child result when direct completion fallback has to bypass the requester-agent announce turn. Thanks @vincentkoc. - TTS/telephony: honor provider voice/model overrides in telephony synthesis providers so Google Meet agent speech logs match the backend that actually produced the audio. Thanks @vincentkoc. - Voice Call/realtime: bound the paced Twilio audio queue and close overloaded realtime streams before provider audio can pile up behind the websocket backpressure guard. Thanks @vincentkoc. diff --git a/docs/tools/thinking.md b/docs/tools/thinking.md index b870eb85cb8..1ad5e01eac3 100644 --- a/docs/tools/thinking.md +++ b/docs/tools/thinking.md @@ -123,7 +123,8 @@ Malformed local-model reasoning tags are handled conservatively. Closed ` - The web chat thinking selector mirrors the session's stored level from the inbound session store/config when the page loads. - Picking another level writes the session override immediately via `sessions.patch`; it does not wait for the next send and it is not a one-shot `thinkingOnce` override. -- The first option is always `Default ()`, where the resolved default comes from the active session model's provider thinking profile plus the same fallback logic that `/status` and `session_status` use. +- The first option is always the clear-override choice. It shows `Inherited: ` when the session is inheriting a non-off effective default, or `Off` when inherited thinking is disabled. +- Explicit picker choices are labeled as overrides, while preserving provider labels when present (for example `Override: maximum` for a provider-labeled `max` option). - The picker uses `thinkingLevels` returned by the gateway session row/defaults, with `thinkingOptions` kept as a legacy label list. The browser UI does not keep its own provider regex list; plugins own model-specific level sets. - `/think:` still works and updates the same stored session level, so chat directives and the picker stay in sync. diff --git a/ui/src/ui/chat/session-controls.ts b/ui/src/ui/chat/session-controls.ts index a10f032cba0..e567749a1c8 100644 --- a/ui/src/ui/chat/session-controls.ts +++ b/ui/src/ui/chat/session-controls.ts @@ -15,6 +15,11 @@ import { parseAgentSessionKey, } from "../session-key.ts"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../string-coerce.ts"; +import { + formatInheritedThinkingLabel, + formatThinkingOverrideLabel, + normalizeThinkingOptionValue, +} from "../thinking-labels.ts"; import { listThinkingLevelLabels, normalizeThinkLevel, @@ -211,18 +216,14 @@ function buildThinkingOptions( const options: ChatThinkingSelectOption[] = []; const addOption = (value: string, label?: string) => { - const resolvedLabel = - label ?? - value - .split(/[-_]/g) - .map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part)) - .join(" "); - pushUniqueTrimmedSelectOption(options, seen, value, () => resolvedLabel); + const normalizedValue = normalizeThinkingOptionValue(value); + pushUniqueTrimmedSelectOption(options, seen, normalizedValue, () => + formatThinkingOverrideLabel(normalizedValue, label), + ); }; for (const level of levels) { - const normalized = normalizeThinkLevel(level.id) ?? normalizeLowercaseStringOrEmpty(level.id); - addOption(normalized, level.label); + addOption(level.id, level.label); } if (currentOverride) { addOption(currentOverride); @@ -257,7 +258,7 @@ function resolveThinkingLevelOptions( })); } -function resolveChatThinkingSelectState(state: AppViewState): ChatThinkingSelectState { +export function resolveChatThinkingSelectState(state: AppViewState): ChatThinkingSelectState { const activeRow = state.sessionsResult?.sessions?.find((row) => row.key === state.sessionKey); const persisted = activeRow?.thinkingLevel; const currentOverride = @@ -283,7 +284,7 @@ function resolveChatThinkingSelectState(state: AppViewState): ChatThinkingSelect : "off"); return { currentOverride, - defaultLabel: `Default (${defaultLevel})`, + defaultLabel: formatInheritedThinkingLabel(defaultLevel), options: buildThinkingOptions(levels, currentOverride), }; } diff --git a/ui/src/ui/thinking-labels.ts b/ui/src/ui/thinking-labels.ts new file mode 100644 index 00000000000..ac141f9497b --- /dev/null +++ b/ui/src/ui/thinking-labels.ts @@ -0,0 +1,23 @@ +import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts"; +import { normalizeThinkLevel } from "./thinking.ts"; + +export function normalizeThinkingOptionValue(raw: string): string { + return normalizeThinkLevel(raw) ?? normalizeLowercaseStringOrEmpty(raw); +} + +export function formatInheritedThinkingLabel(effectiveLevel: string | null | undefined): string { + const normalized = effectiveLevel ? normalizeThinkingOptionValue(effectiveLevel) : "off"; + if (!normalized || normalized === "off") { + return "Off"; + } + return `Inherited: ${normalized}`; +} + +export function formatThinkingOverrideLabel(value: string, label?: string | null): string { + const normalized = normalizeThinkingOptionValue(value); + if (!normalized || normalized === "off") { + return "Off"; + } + const displayLabel = label?.trim() || normalized; + return `Override: ${displayLabel}`; +} diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index d522fc4f98e..286ca14c9fd 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -1041,7 +1041,7 @@ describe("chat session controls", () => { [...(thinkingSelect?.options ?? [])] .find((option) => option.value === "max") ?.textContent?.trim(), - ).toBe("maximum"); + ).toBe("Override: maximum"); }); it("labels chat thinking default from the active session row", () => { @@ -1058,8 +1058,8 @@ describe("chat session controls", () => { ); expect(thinkingSelect?.value).toBe(""); - expect(thinkingSelect?.options[0]?.textContent?.trim()).toBe("Default (adaptive)"); - expect(thinkingSelect?.title).toBe("Default (adaptive)"); + expect(thinkingSelect?.options[0]?.textContent?.trim()).toBe("Inherited: adaptive"); + expect(thinkingSelect?.title).toBe("Inherited: adaptive"); }); it("always renders full thinking labels", () => { @@ -1089,14 +1089,14 @@ describe("chat session controls", () => { expect(container.querySelector('select[data-chat-thinking-select-compact="true"]')).toBeNull(); expect(thinkingSelect?.value).toBe(""); - expect(thinkingSelect?.title).toBe("Default (high)"); + expect(thinkingSelect?.title).toBe("Inherited: high"); expect([...thinkingSelect!.options].map((option) => option.textContent?.trim())).toEqual([ - "Default (high)", - "off", - "low", - "medium", - "high", - "xhigh", + "Inherited: high", + "Off", + "Override: low", + "Override: medium", + "Override: high", + "Override: xhigh", ]); }); @@ -1113,7 +1113,7 @@ describe("chat session controls", () => { ); expect(thinkingSelect?.value).toBe(""); - expect(thinkingSelect?.options[0]?.textContent?.trim()).toBe("Default (adaptive)"); - expect(thinkingSelect?.title).toBe("Default (adaptive)"); + expect(thinkingSelect?.options[0]?.textContent?.trim()).toBe("Inherited: adaptive"); + expect(thinkingSelect?.title).toBe("Inherited: adaptive"); }); }); diff --git a/ui/src/ui/views/sessions.test.ts b/ui/src/ui/views/sessions.test.ts index 9612d666340..99a5f0b531b 100644 --- a/ui/src/ui/views/sessions.test.ts +++ b/ui/src/ui/views/sessions.test.ts @@ -5,12 +5,15 @@ import { describe, expect, it, vi } from "vitest"; import type { SessionsListResult } from "../types.ts"; import { renderSessions, type SessionsProps } from "./sessions.ts"; -function buildResult(session: SessionsListResult["sessions"][number]): SessionsListResult { +function buildResult( + session: SessionsListResult["sessions"][number], + defaults?: Partial, +): SessionsListResult { return { ts: Date.now(), path: "(multiple)", count: 1, - defaults: { modelProvider: null, model: null, contextTokens: null }, + defaults: { modelProvider: null, model: null, contextTokens: null, ...defaults }, sessions: [session], }; } @@ -229,7 +232,7 @@ describe("sessions view", () => { Array.from(thinking?.options ?? []) .find((option) => option.value === "max") ?.textContent?.trim(), - ).toBe("maximum"); + ).toBe("Override: maximum"); thinking!.value = "max"; thinking!.dispatchEvent(new Event("change", { bubbles: true })); @@ -260,7 +263,47 @@ describe("sessions view", () => { const thinking = container.querySelector("tbody select") as HTMLSelectElement | null; expect(thinking?.value).toBe(""); - expect(thinking?.options[0]?.textContent?.trim()).toBe("Default (adaptive)"); + expect(thinking?.options[0]?.textContent?.trim()).toBe("Inherited: adaptive"); + expect( + Array.from(thinking?.options ?? []) + .find((option) => option.value === "adaptive") + ?.textContent?.trim(), + ).toBe("Override: adaptive"); + }); + + it("labels inherited thinking from list defaults when lightweight rows omit row defaults", async () => { + const container = document.createElement("div"); + render( + renderSessions( + buildProps( + buildResult( + { + key: "agent:main:main", + kind: "direct", + updatedAt: Date.now(), + }, + { + modelProvider: "openai-codex", + model: "gpt-5.5", + thinkingDefault: "high", + thinkingLevels: [ + { id: "off", label: "off" }, + { id: "high", label: "high" }, + ], + }, + ), + ), + ), + container, + ); + await Promise.resolve(); + + const thinking = container.querySelector("tbody select") as HTMLSelectElement | null; + expect(thinking?.value).toBe(""); + expect(thinking?.options[0]?.textContent?.trim()).toBe("Inherited: high"); + expect(Array.from(thinking?.options ?? []).map((option) => option.textContent?.trim())).toEqual( + ["Inherited: high", "Off", "Override: high"], + ); }); it("keeps legacy binary thinking labels patching canonical ids", async () => { @@ -289,7 +332,7 @@ describe("sessions view", () => { Array.from(thinking?.options ?? []) .find((option) => option.value === "low") ?.textContent?.trim(), - ).toBe("on"); + ).toBe("Override: on"); thinking!.value = "low"; thinking!.dispatchEvent(new Event("change", { bubbles: true })); diff --git a/ui/src/ui/views/sessions.ts b/ui/src/ui/views/sessions.ts index 80a27bdb5ca..85a39664203 100644 --- a/ui/src/ui/views/sessions.ts +++ b/ui/src/ui/views/sessions.ts @@ -5,7 +5,11 @@ import { icons } from "../icons.ts"; import { pathForTab } from "../navigation.ts"; import { formatSessionTokens } from "../presenter.ts"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../string-coerce.ts"; -import { normalizeThinkLevel } from "../thinking.ts"; +import { + formatInheritedThinkingLabel, + formatThinkingOverrideLabel, + normalizeThinkingOptionValue, +} from "../thinking-labels.ts"; import type { AgentIdentityResult, GatewaySessionRow, @@ -88,27 +92,42 @@ function getAgentIdentity( : null; } -function normalizeThinkingOptionValue(raw: string): string { - return normalizeThinkLevel(raw) ?? normalizeLowercaseStringOrEmpty(raw); +function rowMatchesSessionDefaults( + row: GatewaySessionRow, + defaults: SessionsListResult["defaults"] | undefined, +): boolean { + return ( + (!row.modelProvider || row.modelProvider === defaults?.modelProvider) && + (!row.model || row.model === defaults?.model) + ); } function resolveThinkLevelOptions( row: GatewaySessionRow, + defaults?: SessionsListResult["defaults"], ): readonly { value: string; label: string }[] { - const defaultLabel = row.thinkingDefault - ? t("sessionsView.defaultOption", { value: row.thinkingDefault }) - : t("sessionsView.inherit"); + const sessionModelMatchesDefaults = rowMatchesSessionDefaults(row, defaults); + const defaultLabel = formatInheritedThinkingLabel( + row.thinkingDefault ?? (sessionModelMatchesDefaults ? defaults?.thinkingDefault : undefined), + ); const options: readonly GatewayThinkingLevelOption[] = row.thinkingLevels?.length ? row.thinkingLevels - : (row.thinkingOptions?.length ? row.thinkingOptions : DEFAULT_THINK_LEVELS).map((label) => ({ - id: normalizeThinkingOptionValue(label), - label, - })); + : sessionModelMatchesDefaults && defaults?.thinkingLevels?.length + ? defaults.thinkingLevels + : (row.thinkingOptions?.length + ? row.thinkingOptions + : sessionModelMatchesDefaults && defaults?.thinkingOptions?.length + ? defaults.thinkingOptions + : DEFAULT_THINK_LEVELS + ).map((label) => ({ + id: normalizeThinkingOptionValue(label), + label, + })); return [ { value: "", label: defaultLabel }, ...options.map((option) => ({ value: normalizeThinkingOptionValue(option.id), - label: option.label, + label: formatThinkingOverrideLabel(option.id, option.label), })), ]; } @@ -133,10 +152,7 @@ function withCurrentLabeledOption( if (options.some((option) => option.value === current)) { return [...options]; } - return [ - ...options, - { value: current, label: t("sessionsView.customOption", { value: current }) }, - ]; + return [...options, { value: current, label: formatThinkingOverrideLabel(current) }]; } function buildVerboseLevelOptions(): Array<{ value: string; label: string }> { @@ -689,7 +705,10 @@ function renderRows(row: GatewaySessionRow, props: SessionsProps) { const updated = row.updatedAt ? formatRelativeTimestamp(row.updatedAt) : t("common.na"); const rawThinking = row.thinkingLevel ?? ""; const thinking = rawThinking ? normalizeThinkingOptionValue(rawThinking) : ""; - const thinkLevels = withCurrentLabeledOption(resolveThinkLevelOptions(row), thinking); + const thinkLevels = withCurrentLabeledOption( + resolveThinkLevelOptions(row, props.result?.defaults), + thinking, + ); const fastMode = row.fastMode === true ? "on" : row.fastMode === false ? "off" : ""; const fastLevels = withCurrentLabeledOption(buildFastLevelOptions(), fastMode); const verbose = row.verboseLevel ?? ""; From 85b914a4e1863c1bcd6acba9c4fb942887969561 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 06:41:28 +0100 Subject: [PATCH 035/215] fix(model): repair provider replay edge cases --- CHANGELOG.md | 2 + extensions/google/transport-stream.test.ts | 38 +++++++++++++++++++ extensions/google/transport-stream.ts | 21 +++++++--- extensions/telegram/src/model-buttons.test.ts | 1 + extensions/telegram/src/model-buttons.ts | 2 +- .../src/host/embeddings-remote-client.test.ts | 23 +++++++++++ src/agents/model-auth.test.ts | 30 +++++++++++++++ src/agents/openai-transport-stream.test.ts | 16 +++++--- src/agents/openai-transport-stream.ts | 25 +++++++----- src/agents/session-transcript-repair.test.ts | 32 +++++++++++++++- src/agents/session-transcript-repair.ts | 34 ++++++++++++----- src/agents/transport-message-transform.ts | 7 +++- src/config/types.secrets.resolution.test.ts | 5 +++ src/config/types.secrets.ts | 14 ++++++- src/media/mime.test.ts | 1 + src/media/mime.ts | 5 ++- 16 files changed, 219 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48f4ec4386a..16ae95af94a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -137,6 +137,8 @@ Docs: https://docs.openclaw.ai ### Fixes - CLI backends: keep versioned OAuth identity matches reusable when auth profile ids rotate, so Claude CLI sessions do not reset and lose continuity during same-account OAuth refresh/profile alias changes. Fixes #78541. +- Model providers: normalize APNG sniffed PNG uploads, preserve Gemini 3 tool-call thought-signature replay with documented fallback signatures, accept legacy `__env__:VAR` custom-provider keys, and repair snake_case tool-call transcript sanitization. Fixes #51881, #48915, #77566, and #42858. +- Telegram/models: parse provider ids containing dots in `/models` callback buttons so `hf.co` model lists render as inline keyboard buttons. Fixes #38745. - Anthropic: reject uppercase provider-prefixed forward-compat model ids locally instead of sending malformed dynamic ids upstream. Fixes #73715. - OpenAI/embeddings: pass configured output dimensionality through single and batched embedding requests so memory embedding indexes can request smaller vectors. Fixes #55126. - CLI/infer: normalize HEIC/HEIF image files to JPEG before model-run requests, avoiding providers that reject Apple image container formats. Fixes #50081. diff --git a/extensions/google/transport-stream.test.ts b/extensions/google/transport-stream.test.ts index e8f1d3199bd..cfa81b9b4ca 100644 --- a/extensions/google/transport-stream.test.ts +++ b/extensions/google/transport-stream.test.ts @@ -504,6 +504,44 @@ describe("google transport stream", () => { }); }); + it("uses Gemini skip-validator thought signatures for cross-provider tool-call replay", () => { + const model = buildGeminiModel({ + id: "gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro Preview", + }); + + const params = buildGoogleGenerativeAiParams(model, { + messages: [ + { + role: "assistant", + provider: "anthropic", + api: "anthropic-messages", + model: "claude-opus-4-7", + stopReason: "toolUse", + timestamp: 0, + content: [ + { + type: "toolCall", + id: "call_1", + name: "lookup", + arguments: { q: "hello" }, + }, + ], + }, + ], + } as never); + + expect(params.contents[0]).toMatchObject({ + role: "model", + parts: [ + { + thoughtSignature: "skip_thought_signature_validator", + functionCall: { name: "lookup", args: { q: "hello" } }, + }, + ], + }); + }); + it("builds direct Gemini payloads without negative fallback thinking budgets", () => { const model = { id: "custom-gemini-model", diff --git a/extensions/google/transport-stream.ts b/extensions/google/transport-stream.ts index bd2874ec97d..9fed6faaa8c 100644 --- a/extensions/google/transport-stream.ts +++ b/extensions/google/transport-stream.ts @@ -134,6 +134,7 @@ type GoogleSseChunk = { }; let toolCallCounter = 0; +const GEMINI_THOUGHT_SIGNATURE_VALIDATOR_SKIP = "skip_thought_signature_validator"; function normalizeOptionalString(value: unknown): string | undefined { return typeof value === "string" && value.trim() ? value.trim() : undefined; @@ -143,6 +144,10 @@ function requiresToolCallId(modelId: string): boolean { return modelId.startsWith("claude-") || modelId.startsWith("gpt-oss-"); } +function requiresToolCallThoughtSignature(modelId: string): boolean { + return normalizeLowercaseStringOrEmpty(modelId).includes("gemini-3"); +} + function supportsMultimodalFunctionResponse(modelId: string): boolean { const match = normalizeLowercaseStringOrEmpty(modelId).match(/^gemini(?:-live)?-(\d+)/); if (!match) { @@ -377,8 +382,11 @@ function normalizeGoogleThinkingConfig( function convertGoogleMessages(model: GoogleTransportModel, context: Context) { const contents: Array> = []; - const transformedMessages = transformTransportMessages(context.messages, model, (id) => - requiresToolCallId(model.id) ? normalizeToolCallId(id) : id, + const transformedMessages = transformTransportMessages( + context.messages, + model, + (id) => (requiresToolCallId(model.id) ? normalizeToolCallId(id) : id), + { preserveCrossModelToolCallThoughtSignature: true }, ); for (const msg of transformedMessages) { if (msg.role === "user") { @@ -440,15 +448,18 @@ function convertGoogleMessages(model: GoogleTransportModel, context: Context) { continue; } if (block.type === "toolCall") { + const thoughtSignature = + (isSameProviderAndModel ? block.thoughtSignature : undefined) ?? + (requiresToolCallThoughtSignature(model.id) + ? GEMINI_THOUGHT_SIGNATURE_VALIDATOR_SKIP + : undefined); parts.push({ functionCall: { name: block.name, args: coerceTransportToolCallArguments(block.arguments), ...(requiresToolCallId(model.id) ? { id: block.id } : {}), }, - ...(isSameProviderAndModel && block.thoughtSignature - ? { thoughtSignature: block.thoughtSignature } - : {}), + ...(thoughtSignature ? { thoughtSignature } : {}), }); } } diff --git a/extensions/telegram/src/model-buttons.test.ts b/extensions/telegram/src/model-buttons.test.ts index f03f228d84d..8b9bfe62a54 100644 --- a/extensions/telegram/src/model-buttons.test.ts +++ b/extensions/telegram/src/model-buttons.test.ts @@ -18,6 +18,7 @@ describe("parseModelCallbackData", () => { ["mdl_back", { type: "back" }], ["mdl_list_anthropic_2", { type: "list", provider: "anthropic", page: 2 }], ["mdl_list_open-ai_1", { type: "list", provider: "open-ai", page: 1 }], + ["mdl_list_hf.co_1", { type: "list", provider: "hf.co", page: 1 }], [ "mdl_sel_anthropic/claude-sonnet-4-5", { type: "select", provider: "anthropic", model: "claude-sonnet-4-5" }, diff --git a/extensions/telegram/src/model-buttons.ts b/extensions/telegram/src/model-buttons.ts index 8f421f2fc11..bc92fd98b5c 100644 --- a/extensions/telegram/src/model-buttons.ts +++ b/extensions/telegram/src/model-buttons.ts @@ -63,7 +63,7 @@ export function parseModelCallbackData(data: string): ParsedModelCallback | null } // mdl_list_{provider}_{page} - const listMatch = trimmed.match(/^mdl_list_([a-z0-9_-]+)_(\d+)$/i); + const listMatch = trimmed.match(/^mdl_list_([a-z0-9_.-]+)_(\d+)$/i); if (listMatch) { const [, provider, pageStr] = listMatch; const page = Number.parseInt(pageStr ?? "1", 10); diff --git a/packages/memory-host-sdk/src/host/embeddings-remote-client.test.ts b/packages/memory-host-sdk/src/host/embeddings-remote-client.test.ts index 9cf1fc96e6a..b6cee74b4cb 100644 --- a/packages/memory-host-sdk/src/host/embeddings-remote-client.test.ts +++ b/packages/memory-host-sdk/src/host/embeddings-remote-client.test.ts @@ -2,6 +2,29 @@ import { describe, expect, it, vi } from "vitest"; import { resolveRemoteEmbeddingBearerClient } from "./embeddings-remote-client.js"; describe("resolveRemoteEmbeddingBearerClient", () => { + it("uses configured OpenAI provider baseUrl for memory embeddings", async () => { + const client = await resolveRemoteEmbeddingBearerClient({ + provider: "openai", + defaultBaseUrl: "https://api.openai.com/v1", + options: { + agentDir: "/tmp/openclaw-agent", + config: { + models: { + providers: { + openai: { + apiKey: "sk-config", + baseUrl: "https://proxy.example.test/openai/v1", + }, + }, + }, + } as never, + model: "text-embedding-3-small", + }, + }); + + expect(client.baseUrl).toBe("https://proxy.example.test/openai/v1"); + }); + it("adds OpenClaw attribution to native OpenAI embedding requests", async () => { vi.stubEnv("OPENCLAW_VERSION", "2026.3.22"); const client = await resolveRemoteEmbeddingBearerClient({ diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index 22119367c5c..89a9b21a59f 100644 --- a/src/agents/model-auth.test.ts +++ b/src/agents/model-auth.test.ts @@ -518,6 +518,36 @@ describe("resolveUsableCustomProviderApiKey", () => { } }); + it("resolves legacy __env__ markers from process env for custom providers", () => { + const previous = process.env.BAILIAN_API_KEY; + process.env.BAILIAN_API_KEY = "sk-bailian-env"; // pragma: allowlist secret + try { + const resolved = resolveUsableCustomProviderApiKey({ + cfg: { + models: { + providers: { + bailian: { + baseUrl: "https://coding.dashscope.aliyuncs.com/v1", + api: "openai-completions", + apiKey: "__env__:BAILIAN_API_KEY", // pragma: allowlist secret + models: [], + }, + }, + }, + }, + provider: "bailian", + }); + expect(resolved?.apiKey).toBe("sk-bailian-env"); + expect(resolved?.source).toContain("BAILIAN_API_KEY"); + } finally { + if (previous === undefined) { + delete process.env.BAILIAN_API_KEY; + } else { + process.env.BAILIAN_API_KEY = previous; + } + } + }); + it("does not resolve env SecretRefs when provider allowlist excludes the env id", () => { const previous = process.env.MY_CUSTOM_KEY; process.env.MY_CUSTOM_KEY = "sk-custom-secretref-env"; // pragma: allowlist secret diff --git a/src/agents/openai-transport-stream.test.ts b/src/agents/openai-transport-stream.test.ts index 59828d7d281..a59d0cd48b2 100644 --- a/src/agents/openai-transport-stream.test.ts +++ b/src/agents/openai-transport-stream.test.ts @@ -2901,7 +2901,7 @@ describe("openai transport stream", () => { ); }); - it("does not replay thought_signature across a different API surface", () => { + it("uses the Gemini skip-validator signature across a different API surface", () => { const params = buildOpenAICompletionsParams( geminiModel, { @@ -2938,12 +2938,14 @@ describe("openai transport stream", () => { ) as { messages: Array> }; const assistant = params.messages.find((message) => message.role === "assistant") as - | { tool_calls?: Array<{ extra_content?: unknown }> } + | { tool_calls?: Array<{ extra_content?: { google?: { thought_signature?: string } } }> } | undefined; - expect(assistant?.tool_calls?.[0]?.extra_content).toBeUndefined(); + expect(assistant?.tool_calls?.[0]?.extra_content?.google?.thought_signature).toBe( + "skip_thought_signature_validator", + ); }); - it("does not emit extra_content when no thought_signature was captured", () => { + it("uses the Gemini skip-validator signature when no thought_signature was captured", () => { const params = buildOpenAICompletionsParams( geminiModel, { @@ -2972,9 +2974,11 @@ describe("openai transport stream", () => { ) as { messages: Array> }; const assistant = params.messages.find((message) => message.role === "assistant") as - | { tool_calls?: Array<{ extra_content?: unknown }> } + | { tool_calls?: Array<{ extra_content?: { google?: { thought_signature?: string } } }> } | undefined; - expect(assistant?.tool_calls?.[0]?.extra_content).toBeUndefined(); + expect(assistant?.tool_calls?.[0]?.extra_content?.google?.thought_signature).toBe( + "skip_thought_signature_validator", + ); }); }); diff --git a/src/agents/openai-transport-stream.ts b/src/agents/openai-transport-stream.ts index f83fdb36bec..ec9b0a5966a 100644 --- a/src/agents/openai-transport-stream.ts +++ b/src/agents/openai-transport-stream.ts @@ -58,6 +58,7 @@ import { mergeTransportMetadata, sanitizeTransportPayloadText } from "./transpor const DEFAULT_AZURE_OPENAI_API_VERSION = "2024-12-01-preview"; const OPENAI_CODEX_RESPONSES_EMPTY_INPUT_TEXT = " "; +const GEMINI_THOUGHT_SIGNATURE_VALIDATOR_SKIP = "skip_thought_signature_validator"; const log = createSubsystemLogger("openai-transport"); type ReplayableResponseOutputMessage = Omit & { id?: string }; @@ -1800,6 +1801,10 @@ function isGoogleOpenAICompatModel(model: OpenAIModeModel): boolean { ); } +function requiresGoogleCompatToolCallThoughtSignature(model: OpenAIModeModel): boolean { + return model.id.toLowerCase().includes("gemini-3"); +} + function injectToolCallThoughtSignatures( outgoingMessages: unknown[], context: Context, @@ -1809,18 +1814,14 @@ function injectToolCallThoughtSignatures( return; } const sigById = new Map(); + const fallbackSig = requiresGoogleCompatToolCallThoughtSignature(model) + ? GEMINI_THOUGHT_SIGNATURE_VALIDATOR_SKIP + : undefined; for (const msg of context.messages ?? []) { if ((msg as { role?: string }).role !== "assistant") { continue; } const source = msg as { api?: string; provider?: string; model?: string; content?: unknown }; - if ( - source.api !== model.api || - source.provider !== model.provider || - source.model !== model.id - ) { - continue; - } if (!Array.isArray(source.content)) { continue; } @@ -1831,11 +1832,15 @@ function injectToolCallThoughtSignatures( const id = block.id; const sig = block.thoughtSignature; if (typeof id === "string" && typeof sig === "string" && sig.length > 0) { - sigById.set(id, sig); + const isSameRoute = + source.api === model.api && + source.provider === model.provider && + source.model === model.id; + sigById.set(id, isSameRoute ? sig : (fallbackSig ?? sig)); } } } - if (sigById.size === 0) { + if (sigById.size === 0 && !fallbackSig) { return; } for (const message of outgoingMessages) { @@ -1848,7 +1853,7 @@ function injectToolCallThoughtSignatures( if (typeof id !== "string") { continue; } - const sig = sigById.get(id); + const sig = sigById.get(id) ?? fallbackSig; if (!sig) { continue; } diff --git a/src/agents/session-transcript-repair.test.ts b/src/agents/session-transcript-repair.test.ts index ecab4cc2485..6d6548f103f 100644 --- a/src/agents/session-transcript-repair.test.ts +++ b/src/agents/session-transcript-repair.test.ts @@ -8,7 +8,14 @@ import { } from "./session-transcript-repair.js"; import { castAgentMessage, castAgentMessages } from "./test-helpers/agent-message-fixtures.js"; -const TOOL_CALL_BLOCK_TYPES = new Set(["toolCall", "toolUse", "functionCall"]); +const TOOL_CALL_BLOCK_TYPES = new Set([ + "toolCall", + "toolUse", + "functionCall", + "tool_call", + "tool_use", + "function_call", +]); function getAssistantToolCallBlocks(messages: AgentMessage[]) { const assistant = messages[0] as Extract | undefined; @@ -316,6 +323,29 @@ describe("sanitizeToolUseResultPairing", () => { }); }); +describe("sanitizeToolCallInputs", () => { + it("drops malformed snake_case tool call blocks", () => { + const input = castAgentMessages([ + { + role: "assistant", + content: [ + { type: "text", text: "before" }, + { type: "tool_use", id: "tool_1", name: "read" }, + { type: "tool_call", tool_call_id: "tool_2", name: "write", arguments: {} }, + { type: "function_call", call_id: "tool_3", name: "exec", arguments: "{}" }, + ], + }, + ]); + + const out = sanitizeToolCallInputs(input, { allowedToolNames: ["write", "exec"] }); + + expect(getAssistantToolCallBlocks(out)).toMatchObject([ + { type: "tool_call", name: "write" }, + { type: "function_call", name: "exec" }, + ]); + }); +}); + describe("sanitizeToolCallInputs", () => { function sanitizeAssistantContent( content: unknown[], diff --git a/src/agents/session-transcript-repair.ts b/src/agents/session-transcript-repair.ts index d30128ebfab..96ee67f38f8 100644 --- a/src/agents/session-transcript-repair.ts +++ b/src/agents/session-transcript-repair.ts @@ -16,11 +16,25 @@ import { type RawToolCallBlock = { type?: unknown; id?: unknown; + call_id?: unknown; + toolCallId?: unknown; + toolUseId?: unknown; + tool_call_id?: unknown; + tool_use_id?: unknown; name?: unknown; input?: unknown; arguments?: unknown; }; +const RAW_TOOL_CALL_BLOCK_TYPES = new Set([ + "toolCall", + "toolUse", + "functionCall", + "tool_call", + "tool_use", + "function_call", +]); + function isThinkingLikeBlock(block: unknown): boolean { if (!block || typeof block !== "object") { return false; @@ -34,10 +48,7 @@ function isRawToolCallBlock(block: unknown): block is RawToolCallBlock { return false; } const type = (block as { type?: unknown }).type; - return ( - typeof type === "string" && - (type === "toolCall" || type === "toolUse" || type === "functionCall") - ); + return typeof type === "string" && RAW_TOOL_CALL_BLOCK_TYPES.has(type); } function hasToolCallInput(block: RawToolCallBlock): boolean { @@ -52,7 +63,14 @@ function hasNonEmptyStringField(value: unknown): boolean { } function hasToolCallId(block: RawToolCallBlock): boolean { - return hasNonEmptyStringField(block.id); + return ( + hasNonEmptyStringField(block.id) || + hasNonEmptyStringField(block.call_id) || + hasNonEmptyStringField(block.toolCallId) || + hasNonEmptyStringField(block.toolUseId) || + hasNonEmptyStringField(block.tool_call_id) || + hasNonEmptyStringField(block.tool_use_id) + ); } function redactSessionsSpawnAttachmentsArgs(value: unknown): unknown { @@ -350,11 +368,7 @@ function repairToolCallInputs( continue; } if (isRawToolCallBlock(block)) { - if ( - (block as { type?: unknown }).type === "toolCall" || - (block as { type?: unknown }).type === "toolUse" || - (block as { type?: unknown }).type === "functionCall" - ) { + if (RAW_TOOL_CALL_BLOCK_TYPES.has((block as { type?: string }).type ?? "")) { // Only sanitize (redact) sessions_spawn blocks; all others are passed through // unchanged to preserve provider-specific shapes (e.g. toolUse.input for Anthropic). const blockName = diff --git a/src/agents/transport-message-transform.ts b/src/agents/transport-message-transform.ts index 20d47f7bc70..deebf68da9e 100644 --- a/src/agents/transport-message-transform.ts +++ b/src/agents/transport-message-transform.ts @@ -45,6 +45,7 @@ export function transformTransportMessages( targetModel: Model, source: { provider: string; api: Api; model: string }, ) => string, + options?: { preserveCrossModelToolCallThoughtSignature?: boolean }, ): Context["messages"] { const allowSyntheticToolResults = defaultAllowSyntheticToolResults(model.api); const syntheticToolResultText = CODEX_STYLE_ABORTED_OUTPUT_APIS.has(model.api) @@ -94,7 +95,11 @@ export function transformTransportMessages( continue; } let normalizedToolCall = block; - if (!isSameModel && block.thoughtSignature) { + if ( + !isSameModel && + block.thoughtSignature && + options?.preserveCrossModelToolCallThoughtSignature !== true + ) { normalizedToolCall = { ...normalizedToolCall }; delete normalizedToolCall.thoughtSignature; } diff --git a/src/config/types.secrets.resolution.test.ts b/src/config/types.secrets.resolution.test.ts index cb7bbc8477d..efb64a6e9bc 100644 --- a/src/config/types.secrets.resolution.test.ts +++ b/src/config/types.secrets.resolution.test.ts @@ -90,6 +90,11 @@ describe("parseLegacySecretRefEnvMarker", () => { provider: "default", id: "OPENAI_API_KEY", }); + expect(parseLegacySecretRefEnvMarker("__env__:BAILIAN_API_KEY")).toEqual({ + source: "env", + provider: "default", + id: "BAILIAN_API_KEY", + }); expect(parseLegacySecretRefEnvMarker("secretref-env:not-valid")).toBeNull(); expect( resolveSecretInputString({ diff --git a/src/config/types.secrets.ts b/src/config/types.secrets.ts index 5cb58778718..2faaecb4b79 100644 --- a/src/config/types.secrets.ts +++ b/src/config/types.secrets.ts @@ -19,6 +19,7 @@ export type SecretInput = string | SecretRef; export const DEFAULT_SECRET_PROVIDER_ALIAS = "default"; // pragma: allowlist secret export const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/; export const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:"; // pragma: allowlist secret +export const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:"; // pragma: allowlist secret const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/; export type SecretInputStringResolutionMode = "strict" | "inspect"; export type SecretInputStringResolution = @@ -91,10 +92,15 @@ export function parseLegacySecretRefEnvMarker( return null; } const trimmed = value.trim(); - if (!trimmed.startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX)) { + const prefix = trimmed.startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX) + ? LEGACY_SECRETREF_ENV_MARKER_PREFIX + : trimmed.startsWith(LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX) + ? LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX + : undefined; + if (!prefix) { return null; } - const id = trimmed.slice(LEGACY_SECRETREF_ENV_MARKER_PREFIX.length); + const id = trimmed.slice(prefix.length); if (!ENV_SECRET_REF_ID_RE.test(id)) { return null; } @@ -109,6 +115,10 @@ export function coerceSecretRef(value: unknown, defaults?: SecretDefaults): Secr if (isSecretRef(value)) { return value; } + const legacyEnvMarker = parseLegacySecretRefEnvMarker(value, defaults?.env); + if (legacyEnvMarker) { + return legacyEnvMarker; + } if (isLegacySecretRefWithoutProvider(value)) { const provider = value.source === "env" diff --git a/src/media/mime.test.ts b/src/media/mime.test.ts index 192abff1727..0e045110b4d 100644 --- a/src/media/mime.test.ts +++ b/src/media/mime.test.ts @@ -207,6 +207,7 @@ describe("normalizeMimeType", () => { it.each([ { input: "Audio/MP4; codecs=mp4a.40.2", expected: "audio/mp4" }, + { input: "image/apng", expected: "image/png" }, { input: " ", expected: undefined }, { input: null, expected: undefined }, { input: undefined, expected: undefined }, diff --git a/src/media/mime.ts b/src/media/mime.ts index 9b1a8da7d13..65acbd91733 100644 --- a/src/media/mime.ts +++ b/src/media/mime.ts @@ -74,6 +74,9 @@ export function normalizeMimeType(mime?: string | null): string | undefined { return undefined; } const cleaned = mime.split(";")[0]?.trim().toLowerCase(); + if (cleaned === "image/apng") { + return "image/png"; + } return cleaned || undefined; } @@ -93,7 +96,7 @@ async function sniffMime(buffer?: Buffer): Promise { const { fileTypeFromBuffer } = await fileTypeModuleLoader.load(); const type = await fileTypeFromBuffer(sliceMimeSniffBuffer(buffer)); if (type?.mime) { - return type.mime; + return normalizeMimeType(type.mime); } } catch { // fall through to manual magic-byte sniffs From 5aefe6abd6bec1d737029c4f83b03a996bf00f09 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 06:47:23 +0100 Subject: [PATCH 036/215] feat: stream elevenlabs tts into discord voice --- CHANGELOG.md | 1 + .../.generated/plugin-sdk-api-baseline.sha256 | 4 +- docs/channels/discord.md | 3 +- docs/providers/elevenlabs.md | 7 + docs/providers/google.md | 5 + docs/tools/tts.md | 34 +-- .../discord/src/voice/manager.e2e.test.ts | 55 ++++- extensions/discord/src/voice/segment.ts | 40 ++-- extensions/discord/src/voice/tts.ts | 30 ++- extensions/elevenlabs/speech-provider.ts | 41 +++- extensions/elevenlabs/tts.test.ts | 64 +++++- extensions/elevenlabs/tts.ts | 114 ++++++++-- extensions/speech-core/api.ts | 2 + extensions/speech-core/runtime-api.ts | 4 + extensions/speech-core/src/tts.ts | 200 ++++++++++++++++++ src/plugin-sdk/speech-core.ts | 2 + src/plugin-sdk/speech.ts | 2 + .../test-helpers/plugin-runtime-mock.ts | 1 + src/plugin-sdk/tts-runtime.ts | 12 ++ src/plugin-sdk/tts-runtime.types.ts | 23 ++ src/plugins/runtime/index.ts | 1 + src/plugins/runtime/types-core.ts | 2 + src/plugins/types.ts | 3 + src/tts/provider-types.ts | 10 + src/tts/tts.ts | 4 + 25 files changed, 607 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16ae95af94a..189788a3be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Discord/voice: stream ElevenLabs TTS directly into Discord playback and send ElevenLabs latency optimization as the documented query parameter so spoken replies can start sooner. - Discord/voice: keep TTS playback running when another user starts speaking, ignore new capture during playback to avoid feedback loops, and downgrade expected receive-stream aborts to verbose diagnostics. - Telegram: treat successful same-chat `message` tool outbound sends during an inbound telegram turn as delivered when deciding whether to emit the rewritten silent reply fallback (#78685). Thanks @neeravmakwana. - Gateway/tasks: reconcile stale CLI run-context tasks whose live run context disappeared even when a child session row remains, and apply the default bounded reload deferral timeout to channel hot reloads so stale task records cannot block Discord/Slack/Telegram reloads forever. diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 8d07cef5f88..ffad13cc882 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -bf73d3d6b83410753ee782289e4748c96d97bc76459b116e5e03c678996da360 plugin-sdk-api-baseline.json -f6a9f57d7b632391061c5bac78366bcb01318e0fde26a437e48606bdb70fe9fa plugin-sdk-api-baseline.jsonl +e26753b5aaa10cd98cb0e07fca4034c091471cf434239cc3597b62b5a62b082b plugin-sdk-api-baseline.json +7b998abde706a1afe4d1e4475a87069c31f673c3c90b8a7f23f7ba8cff6d1c85 plugin-sdk-api-baseline.jsonl diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 6cf6dbd2b0b..7ea444b865f 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -1207,6 +1207,7 @@ Notes: - `voice.reconnectGraceMs` controls how long OpenClaw waits for a disconnected voice session to begin reconnecting before destroying it. Default: `15000`. - Voice playback does not stop just because another user starts speaking. To avoid feedback loops, OpenClaw ignores new voice capture while TTS is playing; speak after playback finishes for the next turn. - `voice.captureSilenceGraceMs` controls how long OpenClaw waits after Discord reports a speaker has stopped before finalizing that audio segment for STT. Default: `2500`; raise this if Discord splits normal pauses into choppy partial transcripts. +- When ElevenLabs is the selected TTS provider, Discord voice playback uses streaming TTS and starts from the provider response stream. Providers without streaming support fall back to the synthesized temp-file path. - OpenClaw also watches receive decrypt failures and auto-recovers by leaving/rejoining the voice channel after repeated failures in a short window. - If receive logs repeatedly show `DecryptionFailed(UnencryptedWhenPassthroughDisabled)` after updating, collect a dependency report and logs. The bundled `@discordjs/voice` line includes the upstream padding fix from discord.js PR #11449, which closed discord.js issue #11419. - `The operation was aborted` receive events are expected when OpenClaw finalizes a captured speaker segment; they are verbose diagnostics, not warnings. @@ -1217,7 +1218,7 @@ Voice channel pipeline: - `tools.media.audio` handles STT, for example `openai/gpt-4o-mini-transcribe`. - The transcript is sent through Discord ingress and routing while the response LLM runs with a voice-output policy that hides the agent `tts` tool and asks for returned text, because Discord voice owns final TTS playback. - `voice.model`, when set, overrides only the response LLM for this voice-channel turn. -- `voice.tts` is merged over `messages.tts`; the resulting audio is played in the joined channel. +- `voice.tts` is merged over `messages.tts`; streaming-capable providers feed the player directly, otherwise the resulting audio file is played in the joined channel. Credentials are resolved per component: LLM route auth for `voice.model`, STT auth for `tools.media.audio`, and TTS auth for `messages.tts`/`voice.tts`. diff --git a/docs/providers/elevenlabs.md b/docs/providers/elevenlabs.md index 1157bf72405..9030a856f55 100644 --- a/docs/providers/elevenlabs.md +++ b/docs/providers/elevenlabs.md @@ -46,6 +46,13 @@ export ELEVENLABS_API_KEY="..." Set `modelId` to `eleven_v3` to use ElevenLabs v3 TTS. OpenClaw keeps `eleven_multilingual_v2` as the default for existing installs. +Discord voice channels use ElevenLabs' streaming TTS endpoint when ElevenLabs is +the selected `voice.tts`/`messages.tts` provider. Playback starts from the +returned audio stream instead of waiting for OpenClaw to download and write the +whole audio file first. `latencyTier` maps to ElevenLabs' +`optimize_streaming_latency` query parameter for models that accept it; OpenClaw +omits that parameter for `eleven_v3`, which rejects it. + ## Speech-to-text Use Scribe v2 for inbound audio attachments and short recorded voice segments: diff --git a/docs/providers/google.md b/docs/providers/google.md index c1f6b21de7e..d4b826ff1ba 100644 --- a/docs/providers/google.md +++ b/docs/providers/google.md @@ -287,6 +287,11 @@ The bundled `google` speech provider uses the Gemini API TTS path with - Output: WAV for regular TTS attachments, Opus for voice-note targets, PCM for Talk/telephony - Voice-note output: Google PCM is wrapped as WAV and transcoded to 48 kHz Opus with `ffmpeg` +Google's batch Gemini TTS path returns generated audio in the completed +`generateContent` response. For lowest-latency spoken conversations, use the +Google realtime voice provider backed by the Gemini Live API instead of batch +TTS. + To use Google as the default TTS provider: ```json5 diff --git a/docs/tools/tts.md b/docs/tools/tts.md index 1aafb2ebeac..353f3d4b15a 100644 --- a/docs/tools/tts.md +++ b/docs/tools/tts.md @@ -60,23 +60,23 @@ speech. ## Supported providers -| Provider | Auth | Notes | -| ----------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -| **Azure Speech** | `AZURE_SPEECH_KEY` + `AZURE_SPEECH_REGION` (also `AZURE_SPEECH_API_KEY`, `SPEECH_KEY`, `SPEECH_REGION`) | Native Ogg/Opus voice-note output and telephony. | -| **DeepInfra** | `DEEPINFRA_API_KEY` | OpenAI-compatible TTS. Defaults to `hexgrad/Kokoro-82M`. | -| **ElevenLabs** | `ELEVENLABS_API_KEY` or `XI_API_KEY` | Voice cloning, multilingual, deterministic via `seed`. | -| **Google Gemini** | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | Gemini API TTS; persona-aware via `promptTemplate: "audio-profile-v1"`. | -| **Gradium** | `GRADIUM_API_KEY` | Voice-note and telephony output. | -| **Inworld** | `INWORLD_API_KEY` | Streaming TTS API. Native Opus voice-note and PCM telephony. | -| **Local CLI** | none | Runs a configured local TTS command. | -| **Microsoft** | none | Public Edge neural TTS via `node-edge-tts`. Best-effort, no SLA. | -| **MiniMax** | `MINIMAX_API_KEY` (or Token Plan: `MINIMAX_OAUTH_TOKEN`, `MINIMAX_CODE_PLAN_KEY`, `MINIMAX_CODING_API_KEY`) | T2A v2 API. Defaults to `speech-2.8-hd`. | -| **OpenAI** | `OPENAI_API_KEY` | Also used for auto-summary; supports persona `instructions`. | -| **OpenRouter** | `OPENROUTER_API_KEY` (can reuse `models.providers.openrouter.apiKey`) | Default model `hexgrad/kokoro-82m`. | -| **Volcengine** | `VOLCENGINE_TTS_API_KEY` or `BYTEPLUS_SEED_SPEECH_API_KEY` (legacy AppID/token: `VOLCENGINE_TTS_APPID`/`_TOKEN`) | BytePlus Seed Speech HTTP API. | -| **Vydra** | `VYDRA_API_KEY` | Shared image, video, and speech provider. | -| **xAI** | `XAI_API_KEY` | xAI batch TTS. Native Opus voice-note is **not** supported. | -| **Xiaomi MiMo** | `XIAOMI_API_KEY` | MiMo TTS through Xiaomi chat completions. | +| Provider | Auth | Notes | +| ----------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| **Azure Speech** | `AZURE_SPEECH_KEY` + `AZURE_SPEECH_REGION` (also `AZURE_SPEECH_API_KEY`, `SPEECH_KEY`, `SPEECH_REGION`) | Native Ogg/Opus voice-note output and telephony. | +| **DeepInfra** | `DEEPINFRA_API_KEY` | OpenAI-compatible TTS. Defaults to `hexgrad/Kokoro-82M`. | +| **ElevenLabs** | `ELEVENLABS_API_KEY` or `XI_API_KEY` | Voice cloning, multilingual, deterministic via `seed`; streamed for Discord voice playback. | +| **Google Gemini** | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | Gemini API batch TTS; persona-aware via `promptTemplate: "audio-profile-v1"`. | +| **Gradium** | `GRADIUM_API_KEY` | Voice-note and telephony output. | +| **Inworld** | `INWORLD_API_KEY` | Streaming TTS API. Native Opus voice-note and PCM telephony. | +| **Local CLI** | none | Runs a configured local TTS command. | +| **Microsoft** | none | Public Edge neural TTS via `node-edge-tts`. Best-effort, no SLA. | +| **MiniMax** | `MINIMAX_API_KEY` (or Token Plan: `MINIMAX_OAUTH_TOKEN`, `MINIMAX_CODE_PLAN_KEY`, `MINIMAX_CODING_API_KEY`) | T2A v2 API. Defaults to `speech-2.8-hd`. | +| **OpenAI** | `OPENAI_API_KEY` | Also used for auto-summary; supports persona `instructions`. | +| **OpenRouter** | `OPENROUTER_API_KEY` (can reuse `models.providers.openrouter.apiKey`) | Default model `hexgrad/kokoro-82m`. | +| **Volcengine** | `VOLCENGINE_TTS_API_KEY` or `BYTEPLUS_SEED_SPEECH_API_KEY` (legacy AppID/token: `VOLCENGINE_TTS_APPID`/`_TOKEN`) | BytePlus Seed Speech HTTP API. | +| **Vydra** | `VYDRA_API_KEY` | Shared image, video, and speech provider. | +| **xAI** | `XAI_API_KEY` | xAI batch TTS. Native Opus voice-note is **not** supported. | +| **Xiaomi MiMo** | `XIAOMI_API_KEY` | MiMo TTS through Xiaomi chat completions. | If multiple providers are configured, the selected one is used first and the others are fallback options. Auto-summary uses `summaryModel` (or diff --git a/extensions/discord/src/voice/manager.e2e.test.ts b/extensions/discord/src/voice/manager.e2e.test.ts index 77b617423db..18ef6ff3980 100644 --- a/extensions/discord/src/voice/manager.e2e.test.ts +++ b/extensions/discord/src/voice/manager.e2e.test.ts @@ -10,9 +10,11 @@ const { joinVoiceChannelMock, entersStateMock, createAudioPlayerMock, + createAudioResourceMock, resolveAgentRouteMock, agentCommandMock, transcribeAudioFileMock, + textToSpeechStreamMock, textToSpeechMock, } = vi.hoisted(() => { type EventHandler = (...args: unknown[]) => unknown; @@ -94,6 +96,7 @@ const { entersStateMock: vi.fn(async (_target?: unknown, _state?: string, _timeoutMs?: number) => { return undefined; }), + createAudioResourceMock: vi.fn(), createAudioPlayerMock: vi.fn(() => ({ on: vi.fn(), off: vi.fn(), @@ -104,6 +107,9 @@ const { resolveAgentRouteMock: vi.fn(() => ({ agentId: "agent-1", sessionKey: "discord:g1:c1" })), agentCommandMock: vi.fn(async (_opts?: unknown, _runtime?: unknown) => ({ payloads: [] })), transcribeAudioFileMock: vi.fn(async () => ({ text: "hello from voice" })), + textToSpeechStreamMock: vi.fn( + async (): Promise => ({ success: false, error: "stream unavailable" }), + ), textToSpeechMock: vi.fn(async () => ({ success: true, audioPath: "/tmp/voice.mp3" })), }; }); @@ -121,7 +127,7 @@ vi.mock("./sdk-runtime.js", () => ({ Connecting: "connecting", }, createAudioPlayer: createAudioPlayerMock, - createAudioResource: vi.fn(), + createAudioResource: createAudioResourceMock, entersState: entersStateMock, getVoiceConnection: getVoiceConnectionMock, joinVoiceChannel: joinVoiceChannelMock, @@ -154,6 +160,7 @@ vi.mock("../runtime.js", () => ({ transcribeAudioFile: transcribeAudioFileMock, }, tts: { + textToSpeechStream: textToSpeechStreamMock, textToSpeech: textToSpeechMock, }, }), @@ -207,8 +214,11 @@ describe("DiscordVoiceManager", () => { agentCommandMock.mockResolvedValue({ payloads: [] }); transcribeAudioFileMock.mockReset(); transcribeAudioFileMock.mockResolvedValue({ text: "hello from voice" }); + textToSpeechStreamMock.mockReset(); + textToSpeechStreamMock.mockResolvedValue({ success: false, error: "stream unavailable" }); textToSpeechMock.mockReset(); textToSpeechMock.mockResolvedValue({ success: true, audioPath: "/tmp/voice.mp3" }); + createAudioResourceMock.mockClear(); }); const createManager = ( @@ -750,6 +760,49 @@ describe("DiscordVoiceManager", () => { ); }); + it("plays streaming TTS audio before falling back to a synthesized file", async () => { + const release = vi.fn(async () => undefined); + textToSpeechStreamMock.mockResolvedValue({ + success: true, + audioStream: new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array([1, 2, 3])); + controller.close(); + }, + }), + release, + }); + agentCommandMock.mockResolvedValueOnce({ + payloads: [{ text: "hello back" }], + } as never); + + const client = createClient(); + client.fetchMember.mockResolvedValue({ + nickname: "Guest Nick", + user: { + id: "u-guest", + username: "guest", + globalName: "Guest", + discriminator: "4321", + }, + }); + const manager = createManager({ groupPolicy: "open" }, client, { + commands: { useAccessGroups: false }, + }); + await processVoiceSegment(manager, "u-guest"); + + expect(textToSpeechStreamMock).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "discord", + disableFallback: true, + text: "hello back", + }), + ); + expect(textToSpeechMock).not.toHaveBeenCalled(); + expect(createAudioResourceMock).toHaveBeenCalledWith(expect.anything()); + await vi.waitFor(() => expect(release).toHaveBeenCalledTimes(1)); + }); + it("passes per-channel system prompt overrides to voice agent runs", async () => { const client = createClient(); client.fetchMember.mockResolvedValue({ diff --git a/extensions/discord/src/voice/segment.ts b/extensions/discord/src/voice/segment.ts index 68ee775231a..d1e61dd1464 100644 --- a/extensions/discord/src/voice/segment.ts +++ b/extensions/discord/src/voice/segment.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { Readable } from "node:stream"; import { agentCommandFromIngress } from "openclaw/plugin-sdk/agent-runtime"; import type { DiscordAccountConfig, OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; @@ -139,18 +140,33 @@ export async function processDiscordVoiceSegment(params: { ); params.enqueuePlayback(entry, async () => { - logVoiceVerbose( - `playback start: guild ${entry.guildId} channel ${entry.channelId} file ${path.basename(voiceReplyAudio.audioPath)}`, - ); const voiceSdk = loadDiscordVoiceSdk(); - const resource = voiceSdk.createAudioResource(voiceReplyAudio.audioPath); - entry.player.play(resource); - await voiceSdk - .entersState(entry.player, voiceSdk.AudioPlayerStatus.Playing, PLAYBACK_READY_TIMEOUT_MS) - .catch(() => undefined); - await voiceSdk - .entersState(entry.player, voiceSdk.AudioPlayerStatus.Idle, SPEAKING_READY_TIMEOUT_MS) - .catch(() => undefined); - logVoiceVerbose(`playback done: guild ${entry.guildId} channel ${entry.channelId}`); + const releaseAudioStream = + voiceReplyAudio.mode === "stream" ? voiceReplyAudio.release : undefined; + try { + if (voiceReplyAudio.mode === "stream") { + logVoiceVerbose(`playback start: guild ${entry.guildId} channel ${entry.channelId} stream`); + const nodeStream = Readable.fromWeb( + voiceReplyAudio.audioStream as import("node:stream/web").ReadableStream, + ); + const resource = voiceSdk.createAudioResource(nodeStream); + entry.player.play(resource); + } else { + logVoiceVerbose( + `playback start: guild ${entry.guildId} channel ${entry.channelId} file ${path.basename(voiceReplyAudio.audioPath)}`, + ); + const resource = voiceSdk.createAudioResource(voiceReplyAudio.audioPath); + entry.player.play(resource); + } + await voiceSdk + .entersState(entry.player, voiceSdk.AudioPlayerStatus.Playing, PLAYBACK_READY_TIMEOUT_MS) + .catch(() => undefined); + await voiceSdk + .entersState(entry.player, voiceSdk.AudioPlayerStatus.Idle, SPEAKING_READY_TIMEOUT_MS) + .catch(() => undefined); + logVoiceVerbose(`playback done: guild ${entry.guildId} channel ${entry.channelId}`); + } finally { + await releaseAudioStream?.(); + } }); } diff --git a/extensions/discord/src/voice/tts.ts b/extensions/discord/src/voice/tts.ts index 8c0e245ff9d..b1c2aaa36ab 100644 --- a/extensions/discord/src/voice/tts.ts +++ b/extensions/discord/src/voice/tts.ts @@ -14,9 +14,17 @@ import { sanitizeVoiceReplyTextForSpeech } from "./sanitize.js"; type VoiceReplyAudioResult = | { status: "ok"; + mode: "file"; audioPath: string; speakText: string; } + | { + status: "ok"; + mode: "stream"; + audioStream: ReadableStream; + release?: () => Promise; + speakText: string; + } | { status: "empty"; } @@ -112,7 +120,25 @@ export async function synthesizeVoiceReplyAudio(params: { return { status: "empty" }; } - const result = await getDiscordRuntime().tts.textToSpeech({ + const runtime = getDiscordRuntime(); + const streamResult = await runtime.tts.textToSpeechStream?.({ + text: speakText, + cfg: ttsCfg, + channel: "discord", + overrides: directive.overrides, + disableFallback: true, + }); + if (streamResult?.success && streamResult.audioStream) { + return { + status: "ok", + mode: "stream", + audioStream: streamResult.audioStream, + release: streamResult.release, + speakText, + }; + } + + const result = await runtime.tts.textToSpeech({ text: speakText, cfg: ttsCfg, channel: "discord", @@ -121,5 +147,5 @@ export async function synthesizeVoiceReplyAudio(params: { if (!result.success || !result.audioPath) { return { status: "failed", error: result.error ?? "unknown error" }; } - return { status: "ok", audioPath: result.audioPath, speakText }; + return { status: "ok", mode: "file", audioPath: result.audioPath, speakText }; } diff --git a/extensions/elevenlabs/speech-provider.ts b/extensions/elevenlabs/speech-provider.ts index 2aac85929d2..2aba6d58736 100644 --- a/extensions/elevenlabs/speech-provider.ts +++ b/extensions/elevenlabs/speech-provider.ts @@ -25,7 +25,7 @@ import { import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { resolveElevenLabsApiKeyWithProfileFallback } from "./config-api.js"; import { isValidElevenLabsVoiceId, normalizeElevenLabsBaseUrl } from "./shared.js"; -import { elevenLabsTTS } from "./tts.js"; +import { elevenLabsTTS, elevenLabsTTSStream } from "./tts.js"; const DEFAULT_ELEVENLABS_VOICE_ID = "pMsXgVXv3BLzUgSXRplE"; const DEFAULT_ELEVENLABS_MODEL_ID = "eleven_multilingual_v2"; const DEFAULT_ELEVENLABS_VOICE_SETTINGS = { @@ -521,6 +521,45 @@ export function buildElevenLabsSpeechProvider(): SpeechProviderPlugin { voiceCompatible: req.target === "voice-note", }; }, + streamSynthesize: async (req) => { + const config = readElevenLabsProviderConfig(req.providerConfig); + const overrides = req.providerOverrides ?? {}; + const apiKey = + config.apiKey || resolveElevenLabsApiKeyWithProfileFallback() || process.env.XI_API_KEY; + if (!apiKey) { + throw new Error("ElevenLabs API key missing"); + } + const outputFormat = + trimToUndefined(overrides.outputFormat) ?? + (req.target === "voice-note" ? "opus_48000_64" : "mp3_44100_128"); + const latencyTier = asFiniteNumber(overrides.latencyTier); + const stream = await elevenLabsTTSStream({ + text: req.text, + apiKey, + baseUrl: config.baseUrl, + voiceId: trimToUndefined(overrides.voiceId) ?? config.voiceId, + modelId: trimToUndefined(overrides.modelId) ?? config.modelId, + outputFormat, + seed: asFiniteNumber(overrides.seed) ?? config.seed, + applyTextNormalization: + (trimToUndefined(overrides.applyTextNormalization) as + | "auto" + | "on" + | "off" + | undefined) ?? config.applyTextNormalization, + languageCode: trimToUndefined(overrides.languageCode) ?? config.languageCode, + latencyTier, + voiceSettings: resolveVoiceSettingsOverride(config.voiceSettings, overrides.voiceSettings), + timeoutMs: req.timeoutMs, + }); + return { + audioStream: stream.audioStream, + outputFormat, + fileExtension: req.target === "voice-note" ? ".opus" : ".mp3", + voiceCompatible: req.target === "voice-note", + release: stream.release, + }; + }, synthesizeTelephony: async (req) => { const config = readElevenLabsProviderConfig(req.providerConfig); const overrides = req.providerOverrides ?? {}; diff --git a/extensions/elevenlabs/tts.test.ts b/extensions/elevenlabs/tts.test.ts index 11042cfd321..536fdf9e284 100644 --- a/extensions/elevenlabs/tts.test.ts +++ b/extensions/elevenlabs/tts.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { createStreamingErrorResponse } from "../test-support/streaming-error-response.js"; -import { elevenLabsTTS } from "./tts.js"; +import { elevenLabsTTS, elevenLabsTTSStream } from "./tts.js"; describe("elevenlabs tts diagnostics", () => { const originalFetch = globalThis.fetch; @@ -29,6 +29,15 @@ describe("elevenlabs tts diagnostics", () => { return new Headers(init?.headers); } + function getInitFromFirstFetchCall(fetchMock: ReturnType): RequestInit { + return (fetchMock.mock.calls[0] as unknown[])[1] as RequestInit; + } + + function getUrlFromFirstFetchCall(fetchMock: ReturnType): URL { + const url = fetchMock.mock.calls[0]?.[0] as string | URL; + return new URL(url.toString()); + } + async function expectDefaultTtsRequestToThrow(message: string | RegExp) { await expect(elevenLabsTTS(createDefaultTtsRequest())).rejects.toThrow(message); } @@ -106,4 +115,57 @@ describe("elevenlabs tts diagnostics", () => { expect(getHeadersFromFirstFetchCall(fetchMock).has("accept")).toBe(false); }); + + it("sends latency optimization as an ElevenLabs query parameter", async () => { + const fetchMock = vi.fn(async () => new Response(Buffer.from("mp3"))); + globalThis.fetch = fetchMock as unknown as typeof fetch; + + await elevenLabsTTS({ + ...createDefaultTtsRequest(), + latencyTier: 3, + }); + + const url = getUrlFromFirstFetchCall(fetchMock); + expect(url.searchParams.get("optimize_streaming_latency")).toBe("3"); + const body = JSON.parse(getInitFromFirstFetchCall(fetchMock).body as string) as { + latency_optimization_level?: number; + }; + expect(body.latency_optimization_level).toBeUndefined(); + }); + + it("omits latency optimization for eleven_v3 because the API rejects it", async () => { + const fetchMock = vi.fn(async () => new Response(Buffer.from("mp3"))); + globalThis.fetch = fetchMock as unknown as typeof fetch; + + await elevenLabsTTS({ + ...createDefaultTtsRequest(), + modelId: "eleven_v3", + latencyTier: 3, + }); + + const url = getUrlFromFirstFetchCall(fetchMock); + expect(url.searchParams.has("optimize_streaming_latency")).toBe(false); + }); + + it("uses the streaming endpoint without buffering the audio body", async () => { + const audioStream = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array([1, 2, 3])); + controller.close(); + }, + }); + const fetchMock = vi.fn(async () => new Response(audioStream)); + globalThis.fetch = fetchMock as unknown as typeof fetch; + + const result = await elevenLabsTTSStream({ + ...createDefaultTtsRequest(), + latencyTier: 2, + }); + + const url = getUrlFromFirstFetchCall(fetchMock); + expect(url.pathname).toBe("/v1/text-to-speech/pMsXgVXv3BLzUgSXRplE/stream"); + expect(url.searchParams.get("optimize_streaming_latency")).toBe("2"); + expect(result.audioStream).toBeInstanceOf(ReadableStream); + await result.release(); + }); }); diff --git a/extensions/elevenlabs/tts.ts b/extensions/elevenlabs/tts.ts index 4e81cc1ef93..ea20cea088f 100644 --- a/extensions/elevenlabs/tts.ts +++ b/extensions/elevenlabs/tts.ts @@ -32,7 +32,7 @@ function resolveElevenLabsAcceptHeader(outputFormat: string): string | undefined return undefined; } -export async function elevenLabsTTS(params: { +type ElevenLabsTtsRequestParams = { text: string; apiKey: string; baseUrl: string; @@ -51,10 +51,16 @@ export async function elevenLabsTTS(params: { speed: number; }; timeoutMs: number; -}): Promise { +}; + +function prepareElevenLabsTtsRequest(params: ElevenLabsTtsRequestParams & { stream: boolean }): { + url: URL; + normalizedBaseUrl: string; + acceptHeader?: string; + body: string; +} { const { text, - apiKey, baseUrl, voiceId, modelId, @@ -64,7 +70,6 @@ export async function elevenLabsTTS(params: { languageCode, latencyTier, voiceSettings, - timeoutMs, } = params; if (!isValidElevenLabsVoiceId(voiceId)) { throw new Error("Invalid voiceId format"); @@ -74,11 +79,51 @@ export async function elevenLabsTTS(params: { const normalizedNormalization = normalizeApplyTextNormalization(applyTextNormalization); const normalizedSeed = normalizeSeed(seed); const normalizedBaseUrl = normalizeElevenLabsBaseUrl(baseUrl); - const url = new URL(`${normalizedBaseUrl}/v1/text-to-speech/${voiceId}`); + const normalizedLatencyTier = + typeof latencyTier === "number" && Number.isFinite(latencyTier) + ? Math.trunc(latencyTier) + : undefined; + if (normalizedLatencyTier !== undefined) { + requireInRange(normalizedLatencyTier, 0, 4, "latencyTier"); + } + const url = new URL( + `${normalizedBaseUrl}/v1/text-to-speech/${voiceId}${params.stream ? "/stream" : ""}`, + ); if (outputFormat) { url.searchParams.set("output_format", outputFormat); } + const supportsStreamingLatency = modelId.trim().toLowerCase() !== "eleven_v3"; + if (normalizedLatencyTier !== undefined && supportsStreamingLatency) { + url.searchParams.set("optimize_streaming_latency", normalizedLatencyTier.toString()); + } const acceptHeader = resolveElevenLabsAcceptHeader(outputFormat); + return { + url, + normalizedBaseUrl, + acceptHeader, + body: JSON.stringify({ + text, + model_id: modelId, + seed: normalizedSeed, + apply_text_normalization: normalizedNormalization, + language_code: normalizedLanguage, + voice_settings: { + stability: voiceSettings.stability, + similarity_boost: voiceSettings.similarityBoost, + style: voiceSettings.style, + use_speaker_boost: voiceSettings.useSpeakerBoost, + speed: voiceSettings.speed, + }, + }), + }; +} + +export async function elevenLabsTTS(params: ElevenLabsTtsRequestParams): Promise { + const { apiKey, timeoutMs } = params; + const { url, normalizedBaseUrl, acceptHeader, body } = prepareElevenLabsTtsRequest({ + ...params, + stream: false, + }); const { response, release } = await fetchWithSsrFGuard({ url: url.toString(), @@ -89,21 +134,7 @@ export async function elevenLabsTTS(params: { "Content-Type": "application/json", ...(acceptHeader ? { Accept: acceptHeader } : {}), }, - body: JSON.stringify({ - text, - model_id: modelId, - seed: normalizedSeed, - apply_text_normalization: normalizedNormalization, - language_code: normalizedLanguage, - latency_optimization_level: latencyTier, - voice_settings: { - stability: voiceSettings.stability, - similarity_boost: voiceSettings.similarityBoost, - style: voiceSettings.style, - use_speaker_boost: voiceSettings.useSpeakerBoost, - speed: voiceSettings.speed, - }, - }), + body, }, timeoutMs, policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(normalizedBaseUrl), @@ -117,3 +148,46 @@ export async function elevenLabsTTS(params: { await release(); } } + +export async function elevenLabsTTSStream(params: ElevenLabsTtsRequestParams): Promise<{ + audioStream: ReadableStream; + release: () => Promise; +}> { + const { apiKey, timeoutMs } = params; + const { url, normalizedBaseUrl, acceptHeader, body } = prepareElevenLabsTtsRequest({ + ...params, + stream: true, + }); + + const { response, release } = await fetchWithSsrFGuard({ + url: url.toString(), + init: { + method: "POST", + headers: { + "xi-api-key": apiKey, + "Content-Type": "application/json", + ...(acceptHeader ? { Accept: acceptHeader } : {}), + }, + body, + }, + timeoutMs, + policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(normalizedBaseUrl), + auditContext: "elevenlabs.tts.stream", + }); + let handedOff = false; + try { + await assertOkOrThrowProviderError(response, "ElevenLabs API error"); + if (!response.body) { + throw new Error("ElevenLabs API response missing audio stream"); + } + handedOff = true; + return { + audioStream: response.body, + release, + }; + } finally { + if (!handedOff) { + await release(); + } + } +} diff --git a/extensions/speech-core/api.ts b/extensions/speech-core/api.ts index eec3ab13492..8e95e8efcf3 100644 --- a/extensions/speech-core/api.ts +++ b/extensions/speech-core/api.ts @@ -43,6 +43,8 @@ export type { SpeechProviderResolveTalkConfigContext, SpeechProviderResolveTalkOverridesContext, SpeechSynthesisRequest, + SpeechSynthesisStreamRequest, + SpeechSynthesisStreamResult, SpeechSynthesisTarget, SpeechTelephonySynthesisRequest, SpeechVoiceOption, diff --git a/extensions/speech-core/runtime-api.ts b/extensions/speech-core/runtime-api.ts index 2959109108e..586c573e4e2 100644 --- a/extensions/speech-core/runtime-api.ts +++ b/extensions/speech-core/runtime-api.ts @@ -24,7 +24,9 @@ export { setTtsPersona, setTtsProvider, synthesizeSpeech, + streamSpeech, textToSpeech, + textToSpeechStream, textToSpeechTelephony, _test, type ResolvedTtsConfig, @@ -33,5 +35,7 @@ export { type TtsDirectiveParseResult, type TtsResult, type TtsSynthesisResult, + type TtsSynthesisStreamResult, + type TtsStreamResult, type TtsTelephonyResult, } from "./src/tts.js"; diff --git a/extensions/speech-core/src/tts.ts b/extensions/speech-core/src/tts.ts index 53ff0f13df1..0d92871e546 100644 --- a/extensions/speech-core/src/tts.ts +++ b/extensions/speech-core/src/tts.ts @@ -79,6 +79,7 @@ export type TtsAttemptReasonCode = | "success" | "no_provider_registered" | "not_configured" + | "unsupported_for_streaming" | "unsupported_for_telephony" | "timeout" | "provider_error"; @@ -127,6 +128,27 @@ export type TtsSynthesisResult = { target?: "audio-file" | "voice-note"; }; +export type TtsStreamResult = { + success: boolean; + audioStream?: ReadableStream; + error?: string; + latencyMs?: number; + provider?: string; + providerModel?: string; + providerVoice?: string; + persona?: string; + fallbackFrom?: string; + attemptedProviders?: string[]; + attempts?: TtsProviderAttempt[]; + outputFormat?: string; + voiceCompatible?: boolean; + fileExtension?: string; + target?: "audio-file" | "voice-note"; + release?: () => Promise; +}; + +export type TtsSynthesisStreamResult = TtsStreamResult; + export type TtsTelephonyResult = { success: boolean; audioBuffer?: Buffer; @@ -1330,6 +1352,184 @@ export async function synthesizeSpeech(params: { return buildTtsFailureResult(errors, attemptedProviders, attempts, persona?.id); } +export async function streamSpeech(params: { + text: string; + cfg: OpenClawConfig; + prefsPath?: string; + channel?: string; + overrides?: TtsDirectiveOverrides; + disableFallback?: boolean; + timeoutMs?: number; + agentId?: string; + accountId?: string; +}): Promise { + const setup = resolveTtsRequestSetup({ + text: params.text, + cfg: params.cfg, + prefsPath: params.prefsPath, + providerOverride: params.overrides?.provider, + disableFallback: params.disableFallback, + agentId: params.agentId, + channelId: params.channel, + accountId: params.accountId, + }); + if ("error" in setup) { + return { success: false, error: setup.error }; + } + + const { cfg, config, persona, providers } = setup; + const timeoutMs = params.timeoutMs ?? config.timeoutMs; + const target = resolveTtsSynthesisTarget(params.channel); + const errors: string[] = []; + const attemptedProviders: string[] = []; + const attempts: TtsProviderAttempt[] = []; + const primaryProvider = providers[0]; + logVerbose( + `TTS stream: starting with provider ${primaryProvider}, fallbacks: ${providers.slice(1).join(", ") || "none"}`, + ); + + for (const provider of providers) { + attemptedProviders.push(provider); + const providerStart = Date.now(); + try { + const resolvedProvider = resolveReadySpeechProvider({ + provider, + cfg, + config, + persona, + }); + if (resolvedProvider.kind === "skip") { + errors.push(resolvedProvider.message); + attempts.push({ + provider, + outcome: "skipped", + reasonCode: resolvedProvider.reasonCode, + persona: persona?.id, + ...(resolvedProvider.personaBinding + ? { personaBinding: resolvedProvider.personaBinding } + : {}), + error: resolvedProvider.message, + }); + logVerbose(`TTS stream: provider ${provider} skipped (${resolvedProvider.message})`); + continue; + } + if (!resolvedProvider.provider.streamSynthesize) { + const message = `${provider} does not support streaming TTS`; + errors.push(message); + attempts.push({ + provider, + outcome: "skipped", + reasonCode: "unsupported_for_streaming", + persona: persona?.id, + personaBinding: resolvedProvider.personaBinding, + error: message, + }); + logVerbose(`TTS stream: provider ${provider} skipped (${message})`); + continue; + } + const prepared = await prepareSpeechSynthesis({ + provider: resolvedProvider.provider, + text: params.text, + cfg, + providerConfig: resolvedProvider.providerConfig, + providerOverrides: params.overrides?.providerOverrides?.[resolvedProvider.provider.id], + persona: resolvedProvider.synthesisPersona, + personaProviderConfig: resolvedProvider.personaProviderConfig, + target, + timeoutMs, + }); + const synthesis = await resolvedProvider.provider.streamSynthesize({ + text: prepared.text, + cfg, + providerConfig: prepared.providerConfig, + target, + providerOverrides: prepared.providerOverrides, + timeoutMs, + }); + const latencyMs = Date.now() - providerStart; + attempts.push({ + provider, + outcome: "success", + reasonCode: "success", + persona: persona?.id, + personaBinding: resolvedProvider.personaBinding, + latencyMs, + }); + return { + success: true, + audioStream: synthesis.audioStream, + latencyMs, + provider, + providerModel: resolveTtsResultModel(prepared.providerConfig, prepared.providerOverrides), + providerVoice: resolveTtsResultVoice(prepared.providerConfig, prepared.providerOverrides), + persona: persona?.id, + fallbackFrom: provider !== primaryProvider ? primaryProvider : undefined, + attemptedProviders, + attempts, + outputFormat: synthesis.outputFormat, + voiceCompatible: synthesis.voiceCompatible, + fileExtension: synthesis.fileExtension, + target, + release: synthesis.release, + }; + } catch (err) { + const errorMsg = formatTtsProviderError(provider, err); + const latencyMs = Date.now() - providerStart; + errors.push(errorMsg); + attempts.push({ + provider, + outcome: "failed", + reasonCode: + err instanceof Error && err.name === "AbortError" ? "timeout" : "provider_error", + latencyMs, + persona: persona?.id, + personaBinding: + resolvePersonaProviderConfig(persona, provider) != null + ? "applied" + : persona + ? "missing" + : "none", + error: errorMsg, + }); + const rawError = sanitizeTtsErrorForLog(err); + if (provider === primaryProvider) { + const hasFallbacks = providers.length > 1; + logVerbose( + `TTS stream: primary provider ${provider} failed (${rawError})${hasFallbacks ? "; trying fallback providers." : "; no fallback providers configured."}`, + ); + } else { + logVerbose(`TTS stream: ${provider} failed (${rawError}); trying next provider.`); + } + } + } + + return buildTtsFailureResult(errors, attemptedProviders, attempts, persona?.id); +} + +export async function textToSpeechStream(params: { + text: string; + cfg: OpenClawConfig; + prefsPath?: string; + channel?: string; + overrides?: TtsDirectiveOverrides; + disableFallback?: boolean; + timeoutMs?: number; + agentId?: string; + accountId?: string; +}): Promise { + const synthesis = await streamSpeech(params); + if (!synthesis.success || !synthesis.audioStream || !synthesis.fileExtension) { + return { + success: false, + error: synthesis.error ?? "Streaming TTS conversion failed", + persona: synthesis.persona, + attemptedProviders: synthesis.attemptedProviders, + attempts: synthesis.attempts, + }; + } + return synthesis; +} + export async function textToSpeechTelephony(params: { text: string; cfg: OpenClawConfig; diff --git a/src/plugin-sdk/speech-core.ts b/src/plugin-sdk/speech-core.ts index 76de5dfe624..370c92d19fe 100644 --- a/src/plugin-sdk/speech-core.ts +++ b/src/plugin-sdk/speech-core.ts @@ -16,6 +16,8 @@ export type { SpeechProviderResolveTalkOverridesContext, SpeechProviderOverrides, SpeechSynthesisRequest, + SpeechSynthesisStreamRequest, + SpeechSynthesisStreamResult, SpeechSynthesisTarget, SpeechTelephonySynthesisRequest, SpeechVoiceOption, diff --git a/src/plugin-sdk/speech.ts b/src/plugin-sdk/speech.ts index 72660896334..d9382daa859 100644 --- a/src/plugin-sdk/speech.ts +++ b/src/plugin-sdk/speech.ts @@ -19,6 +19,8 @@ export type { SpeechProviderResolveTalkOverridesContext, SpeechProviderOverrides, SpeechSynthesisRequest, + SpeechSynthesisStreamRequest, + SpeechSynthesisStreamResult, SpeechSynthesisTarget, SpeechTelephonySynthesisRequest, SpeechVoiceOption, diff --git a/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts b/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts index 23560b1ac07..c5c23046563 100644 --- a/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts +++ b/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts @@ -392,6 +392,7 @@ export function createPluginRuntimeMock(overrides: DeepPartial = }, tts: { textToSpeech: vi.fn() as unknown as PluginRuntime["tts"]["textToSpeech"], + textToSpeechStream: vi.fn() as unknown as PluginRuntime["tts"]["textToSpeechStream"], textToSpeechTelephony: vi.fn() as unknown as PluginRuntime["tts"]["textToSpeechTelephony"], listVoices: vi.fn() as unknown as PluginRuntime["tts"]["listVoices"], }, diff --git a/src/plugin-sdk/tts-runtime.ts b/src/plugin-sdk/tts-runtime.ts index 57c10f98dae..460abe7273e 100644 --- a/src/plugin-sdk/tts-runtime.ts +++ b/src/plugin-sdk/tts-runtime.ts @@ -11,6 +11,8 @@ import type { TtsResult, TtsRuntimeFacade, TtsSynthesisResult, + TtsSynthesisStreamResult, + TtsStreamResult, TtsTelephonyResult, } from "./tts-runtime.types.js"; export { @@ -116,10 +118,18 @@ export const synthesizeSpeech: FacadeModule["synthesizeSpeech"] = createLazyFaca loadFacadeModule, "synthesizeSpeech", ); +export const streamSpeech: FacadeModule["streamSpeech"] = createLazyFacadeRuntimeValue( + loadFacadeModule, + "streamSpeech", +); export const textToSpeech: FacadeModule["textToSpeech"] = createLazyFacadeRuntimeValue( loadFacadeModule, "textToSpeech", ); +export const textToSpeechStream: FacadeModule["textToSpeechStream"] = createLazyFacadeRuntimeValue( + loadFacadeModule, + "textToSpeechStream", +); export const textToSpeechTelephony: FacadeModule["textToSpeechTelephony"] = createLazyFacadeRuntimeValue(loadFacadeModule, "textToSpeechTelephony"); @@ -130,5 +140,7 @@ export type { TtsDirectiveParseResult, TtsResult, TtsSynthesisResult, + TtsSynthesisStreamResult, + TtsStreamResult, TtsTelephonyResult, } from "./tts-runtime.types.js"; diff --git a/src/plugin-sdk/tts-runtime.types.ts b/src/plugin-sdk/tts-runtime.types.ts index 26fd82e01e4..d9bbf835a42 100644 --- a/src/plugin-sdk/tts-runtime.types.ts +++ b/src/plugin-sdk/tts-runtime.types.ts @@ -18,6 +18,7 @@ export type TtsAttemptReasonCode = | "success" | "no_provider_registered" | "not_configured" + | "unsupported_for_streaming" | "unsupported_for_telephony" | "timeout" | "provider_error"; @@ -166,6 +167,25 @@ export type TtsSynthesisResult = { target?: TtsSpeechTarget; }; +export type TtsStreamResult = { + success: boolean; + audioStream?: ReadableStream; + error?: string; + latencyMs?: number; + provider?: string; + persona?: string; + fallbackFrom?: string; + attemptedProviders?: string[]; + attempts?: TtsProviderAttempt[]; + outputFormat?: string; + voiceCompatible?: boolean; + fileExtension?: string; + target?: TtsSpeechTarget; + release?: () => Promise; +}; + +export type TtsSynthesisStreamResult = TtsStreamResult; + export type TtsTelephonyResult = { success: boolean; audioBuffer?: Buffer; @@ -183,6 +203,7 @@ export type TtsTelephonyResult = { }; export type TextToSpeech = (params: TtsRequestParams) => Promise; +export type TextToSpeechStream = (params: TtsRequestParams) => Promise; export type TextToSpeechTelephony = ( params: TtsTelephonyRequestParams, ) => Promise; @@ -226,6 +247,8 @@ export type TtsRuntimeFacade = { setTtsPersona: (prefsPath: string, persona: string | null | undefined) => void; setTtsProvider: (prefsPath: string, provider: TtsProvider) => void; synthesizeSpeech: (params: TtsRequestParams) => Promise; + streamSpeech: (params: TtsRequestParams) => Promise; textToSpeech: TextToSpeech; + textToSpeechStream: TextToSpeechStream; textToSpeechTelephony: TextToSpeechTelephony; }; diff --git a/src/plugins/runtime/index.ts b/src/plugins/runtime/index.ts index 10fef42e22e..011fcfb71ac 100644 --- a/src/plugins/runtime/index.ts +++ b/src/plugins/runtime/index.ts @@ -56,6 +56,7 @@ function createRuntimeTts(): PluginRuntime["tts"] { const bindTtsRuntime = createLazyRuntimeMethodBinder(loadTtsRuntime); return { textToSpeech: bindTtsRuntime((runtime) => runtime.textToSpeech), + textToSpeechStream: bindTtsRuntime((runtime) => runtime.textToSpeechStream), textToSpeechTelephony: bindTtsRuntime((runtime) => runtime.textToSpeechTelephony), listVoices: bindTtsRuntime((runtime) => runtime.listSpeechVoices), }; diff --git a/src/plugins/runtime/types-core.ts b/src/plugins/runtime/types-core.ts index c318ef78c6b..87c9969c01c 100644 --- a/src/plugins/runtime/types-core.ts +++ b/src/plugins/runtime/types-core.ts @@ -4,6 +4,7 @@ import type { MediaUnderstandingRuntime } from "../../media-understanding/runtim import type { ListSpeechVoices, TextToSpeech, + TextToSpeechStream, TextToSpeechTelephony, } from "../../plugin-sdk/tts-runtime.types.js"; import type { PluginRuntimeTaskFlows, PluginRuntimeTaskRuns } from "./runtime-tasks.types.js"; @@ -190,6 +191,7 @@ export type PluginRuntimeCore = { }; tts: { textToSpeech: TextToSpeech; + textToSpeechStream: TextToSpeechStream; textToSpeechTelephony: TextToSpeechTelephony; listVoices: ListSpeechVoices; }; diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 27c5c973f7c..f7b228c8399 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -70,6 +70,8 @@ import type { SpeechProviderId, SpeechSynthesisRequest, SpeechSynthesisResult, + SpeechSynthesisStreamRequest, + SpeechSynthesisStreamResult, SpeechTelephonySynthesisRequest, SpeechTelephonySynthesisResult, SpeechVoiceOption, @@ -1807,6 +1809,7 @@ export type SpeechProviderPlugin = { | Promise; isConfigured: (ctx: SpeechProviderConfiguredContext) => boolean; synthesize: (req: SpeechSynthesisRequest) => Promise; + streamSynthesize?: (req: SpeechSynthesisStreamRequest) => Promise; synthesizeTelephony?: ( req: SpeechTelephonySynthesisRequest, ) => Promise; diff --git a/src/tts/provider-types.ts b/src/tts/provider-types.ts index f972d795af8..fefa4d86937 100644 --- a/src/tts/provider-types.ts +++ b/src/tts/provider-types.ts @@ -57,6 +57,16 @@ export type SpeechSynthesisResult = { voiceCompatible: boolean; }; +export type SpeechSynthesisStreamRequest = SpeechSynthesisRequest; + +export type SpeechSynthesisStreamResult = { + audioStream: ReadableStream; + outputFormat: string; + fileExtension: string; + voiceCompatible: boolean; + release?: () => Promise; +}; + export type SpeechTelephonySynthesisRequest = { text: string; cfg: OpenClawConfig; diff --git a/src/tts/tts.ts b/src/tts/tts.ts index dc962a8c8b8..48edf1caece 100644 --- a/src/tts/tts.ts +++ b/src/tts/tts.ts @@ -25,7 +25,9 @@ export { setTtsPersona, setTtsProvider, synthesizeSpeech, + streamSpeech, textToSpeech, + textToSpeechStream, textToSpeechTelephony, type ResolvedTtsConfig, type ResolvedTtsModelOverrides, @@ -33,5 +35,7 @@ export { type TtsDirectiveParseResult, type TtsResult, type TtsSynthesisResult, + type TtsSynthesisStreamResult, + type TtsStreamResult, type TtsTelephonyResult, } from "../plugin-sdk/tts-runtime.js"; From 156068a3cfe5dc418d61656396d3c97545fd935b Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 06:13:20 +0100 Subject: [PATCH 037/215] fix: keep secret target cache unscoped --- ...get-registry-data.current-snapshot.test.ts | 42 +++++++++++++++++++ src/secrets/target-registry-data.ts | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/secrets/target-registry-data.current-snapshot.test.ts diff --git a/src/secrets/target-registry-data.current-snapshot.test.ts b/src/secrets/target-registry-data.current-snapshot.test.ts new file mode 100644 index 00000000000..dae45831260 --- /dev/null +++ b/src/secrets/target-registry-data.current-snapshot.test.ts @@ -0,0 +1,42 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const metadataMocks = vi.hoisted(() => ({ + getCurrentPluginMetadataSnapshot: vi.fn(() => undefined), + loadPluginMetadataSnapshot: vi.fn(() => ({ plugins: [] })), +})); + +vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({ + getCurrentPluginMetadataSnapshot: metadataMocks.getCurrentPluginMetadataSnapshot, +})); + +vi.mock("../plugins/plugin-metadata-snapshot.js", () => ({ + loadPluginMetadataSnapshot: metadataMocks.loadPluginMetadataSnapshot, +})); + +describe("getSecretTargetRegistry metadata reuse", () => { + beforeEach(() => { + vi.resetModules(); + metadataMocks.getCurrentPluginMetadataSnapshot.mockClear(); + metadataMocks.getCurrentPluginMetadataSnapshot.mockReturnValue(undefined); + metadataMocks.loadPluginMetadataSnapshot.mockClear(); + metadataMocks.loadPluginMetadataSnapshot.mockReturnValue({ plugins: [] }); + }); + + it("does not request workspace-scoped current metadata for the configless global cache", async () => { + const { getSecretTargetRegistry } = await import("./target-registry-data.js"); + + getSecretTargetRegistry(); + + expect(metadataMocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({ + config: {}, + env: process.env, + }); + expect(metadataMocks.getCurrentPluginMetadataSnapshot).not.toHaveBeenCalledWith( + expect.objectContaining({ allowWorkspaceScopedSnapshot: true }), + ); + expect(metadataMocks.loadPluginMetadataSnapshot).toHaveBeenCalledWith({ + config: {}, + env: process.env, + }); + }); +}); diff --git a/src/secrets/target-registry-data.ts b/src/secrets/target-registry-data.ts index 218a20cc4d8..eda2f683548 100644 --- a/src/secrets/target-registry-data.ts +++ b/src/secrets/target-registry-data.ts @@ -450,8 +450,8 @@ function loadSecretTargetRegistryFromPluginMetadata(params: { (params.preferPersisted === false ? undefined : getCurrentPluginMetadataSnapshot({ + config: {}, env: params.env, - allowWorkspaceScopedSnapshot: true, }) )?.plugins ?? loadPluginMetadataSnapshot({ From 0caa8e22d717faef53505174616537ff2c0530b2 Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 06:13:23 +0100 Subject: [PATCH 038/215] fix: thread registry model workspace --- src/agents/pi-embedded-runner/model.test.ts | 61 ++++++++++++++++++++- src/agents/pi-embedded-runner/model.ts | 23 ++++---- 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/agents/pi-embedded-runner/model.test.ts b/src/agents/pi-embedded-runner/model.test.ts index c256d1c970f..7e947c5f813 100644 --- a/src/agents/pi-embedded-runner/model.test.ts +++ b/src/agents/pi-embedded-runner/model.test.ts @@ -157,7 +157,12 @@ vi.mock("./openrouter-model-capabilities.js", () => ({ import type { OpenClawConfig } from "../../config/config.js"; import { getModelProviderRequestTransport } from "../provider-request-config.js"; import { buildForwardCompatTemplate } from "./model.forward-compat.test-support.js"; -import { buildInlineProviderModels, resolveModel, resolveModelAsync } from "./model.js"; +import { + buildInlineProviderModels, + resolveModel, + resolveModelAsync, + resolveModelWithRegistry, +} from "./model.js"; import { buildOpenAICodexForwardCompatExpectation, makeModel, @@ -1952,6 +1957,60 @@ describe("resolveModel", () => { }); }); + it("passes configured workspaceDir through direct registry dynamic hooks", () => { + const runProviderDynamicModel = vi.fn( + (params: { + workspaceDir?: string; + context: { workspaceDir?: string; provider: string; modelId: string }; + }) => + params.workspaceDir === "/tmp/workspace" && + params.context.workspaceDir === "/tmp/workspace" && + params.context.provider === "openai-codex" && + params.context.modelId === "gpt-5.4" + ? ({ + ...buildOpenAICodexForwardCompatExpectation("gpt-5.4"), + name: "GPT-5.4", + } as ReturnType) + : undefined, + ); + const runtimeHooks = { + ...createRuntimeHooks(), + runProviderDynamicModel, + }; + const cfg = { + agents: { + defaults: { + workspace: "/tmp/workspace", + }, + }, + } as OpenClawConfig; + + const result = resolveModelWithRegistry({ + provider: "openai-codex", + modelId: "gpt-5.4", + agentDir: "/tmp/agent-state", + cfg, + modelRegistry: discoverModels({ mocked: true } as never, "/tmp/agent-state"), + runtimeHooks, + }); + + expect(runProviderDynamicModel).toHaveBeenCalledWith( + expect.objectContaining({ + workspaceDir: "/tmp/workspace", + context: expect.objectContaining({ + workspaceDir: "/tmp/workspace", + agentDir: "/tmp/agent-state", + modelId: "gpt-5.4", + provider: "openai-codex", + }), + }), + ); + expect(result).toMatchObject({ + provider: "openai-codex", + id: "gpt-5.4", + }); + }); + it("resolves discovered openai-codex gpt-5.4-mini rows", () => { mockDiscoveredModel(discoverModels, { provider: "openai-codex", diff --git a/src/agents/pi-embedded-runner/model.ts b/src/agents/pi-embedded-runner/model.ts index 08a31b40f45..cdc9d85a867 100644 --- a/src/agents/pi-embedded-runner/model.ts +++ b/src/agents/pi-embedded-runner/model.ts @@ -965,38 +965,39 @@ export function resolveModelWithRegistry(params: { const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS; const workspaceDir = normalizedParams.workspaceDir ?? normalizedParams.cfg?.agents?.defaults?.workspace; - const explicitModel = resolveExplicitModelWithRegistry(normalizedParams); + const scopedParams = { + ...normalizedParams, + ...(workspaceDir !== undefined ? { workspaceDir } : {}), + }; + const explicitModel = resolveExplicitModelWithRegistry(scopedParams); if (explicitModel?.kind === "suppressed") { return undefined; } if (explicitModel?.kind === "resolved") { if ( !shouldCompareProviderRuntimeResolvedModel({ - provider: normalizedParams.provider, - modelId: normalizedParams.modelId, - cfg: normalizedParams.cfg, - agentDir: normalizedParams.agentDir, + provider: scopedParams.provider, + modelId: scopedParams.modelId, + cfg: scopedParams.cfg, + agentDir: scopedParams.agentDir, workspaceDir, runtimeHooks, }) ) { return explicitModel.model; } - const pluginDynamicModel = resolvePluginDynamicModelWithRegistry({ - ...normalizedParams, - workspaceDir, - }); + const pluginDynamicModel = resolvePluginDynamicModelWithRegistry(scopedParams); return preferProviderRuntimeResolvedModel({ explicitModel: explicitModel.model, runtimeResolvedModel: pluginDynamicModel, }); } - const pluginDynamicModel = resolvePluginDynamicModelWithRegistry(normalizedParams); + const pluginDynamicModel = resolvePluginDynamicModelWithRegistry(scopedParams); if (pluginDynamicModel) { return pluginDynamicModel; } - return resolveConfiguredFallbackModel(normalizedParams); + return resolveConfiguredFallbackModel(scopedParams); } export function resolveModel( From fb2f3fbb08a9bf56645288c5d2bf4f11c27a3b0a Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 06:13:42 +0100 Subject: [PATCH 039/215] docs: clarify metadata reuse changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 189788a3be7..3fbd3c6b766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,7 @@ Docs: https://docs.openclaw.ai - Docs: clarify that IRC uses raw TCP/TLS sockets outside operator-managed forward proxy routing, so direct IRC egress should be explicitly approved before enabling IRC. Thanks @jesse-merhi. - Gateway/performance: defer non-readiness sidecars until after the ready signal, avoid hot-path channel plugin barrel imports, and fast-path trusted bundled plugin metadata during Gateway startup. - Gateway/performance: reuse the compatible plugin metadata snapshot across dashboard and channel agent turns so auto-enabled runtime config does not repeatedly rescan plugin metadata before provider calls. Thanks @shakkernerd. -- Gateway/performance: reuse current plugin metadata for provider activation, auth/env lookup, secret-target compilation, and bundle settings during dashboard and channel agent turns, eliminating Gateway-process metadata scans before provider calls. Thanks @shakkernerd. +- Gateway/performance: reuse current plugin metadata for provider activation, auth/env lookup, and bundle settings during dashboard and channel agent turns while keeping the configless secret-target cache unscoped. Thanks @shakkernerd. - Gateway/performance: avoid resolving plugin auto-enable metadata twice in one runtime config pass, reducing repeated dashboard turn metadata scans. Thanks @shakkernerd. - Auth/providers: pass `config` and `workspaceDir` lookup context through to provider-id resolution so workspace-scoped auth aliases resolve correctly when no explicit alias map is supplied. Thanks @shakkernerd. - Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed. From ee7da913462b9fc3f1134f506fd13b9dca77692e Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 06:17:14 +0100 Subject: [PATCH 040/215] fix: guard metadata reuse on load paths --- src/agents/pi-project-settings-snapshot.ts | 16 +++++--- src/agents/pi-project-settings.bundle.test.ts | 39 +++++++++++++++++++ src/config/plugin-auto-enable.core.test.ts | 34 ++++++++++++++++ src/config/plugin-auto-enable.shared.ts | 8 ++++ 4 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/agents/pi-project-settings-snapshot.ts b/src/agents/pi-project-settings-snapshot.ts index d4d06b27374..cb715aed972 100644 --- a/src/agents/pi-project-settings-snapshot.ts +++ b/src/agents/pi-project-settings-snapshot.ts @@ -41,6 +41,10 @@ function sanitizeProjectSettings(settings: PiSettingsSnapshot): PiSettingsSnapsh return sanitizePiSettingsSnapshot(settings); } +function canReuseUnscopedCurrentPluginMetadataSnapshot(config: OpenClawConfig): boolean { + return normalizePluginsConfigWithResolver(config.plugins).loadPaths.length === 0; +} + function loadBundleSettingsFile(params: { rootDir: string; relativePath: string; @@ -90,11 +94,13 @@ export function loadEnabledBundlePiSettingsSnapshot(params: { env, workspaceDir, }) ?? - getCurrentPluginMetadataSnapshot({ - env, - workspaceDir, - allowWorkspaceScopedSnapshot: true, - }) ?? + (canReuseUnscopedCurrentPluginMetadataSnapshot(config) + ? getCurrentPluginMetadataSnapshot({ + env, + workspaceDir, + allowWorkspaceScopedSnapshot: true, + }) + : undefined) ?? loadPluginMetadataSnapshot({ workspaceDir, config, diff --git a/src/agents/pi-project-settings.bundle.test.ts b/src/agents/pi-project-settings.bundle.test.ts index 7c0e3d61257..43abf6ba349 100644 --- a/src/agents/pi-project-settings.bundle.test.ts +++ b/src/agents/pi-project-settings.bundle.test.ts @@ -325,6 +325,45 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled(); }); + it("does not reuse an unscoped current snapshot when plugin load paths change", async () => { + const workspaceDir = await tempDirs.make("openclaw-workspace-"); + const pluginRoot = await createWorkspaceBundle({ workspaceDir }); + const resolvedPluginRoot = await fs.realpath(pluginRoot); + await fs.writeFile( + path.join(pluginRoot, "settings.json"), + JSON.stringify({ hideThinkingBlock: true }), + "utf-8", + ); + + pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot.mockReturnValueOnce(undefined); + pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear(); + + const snapshot = loadEnabledBundlePiSettingsSnapshot({ + cwd: workspaceDir, + cfg: { + plugins: { + load: { paths: ["/tmp/changed-plugin-root"] }, + entries: { + "claude-bundle": { enabled: true }, + }, + }, + }, + }); + + expect(snapshot.hideThinkingBlock).toBe(true); + expect(pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledOnce(); + expect(pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({ + config: expect.objectContaining({ + plugins: expect.objectContaining({ + load: { paths: ["/tmp/changed-plugin-root"] }, + }), + }), + env: process.env, + workspaceDir, + }); + expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce(); + }); + it("loads sanitized settings and MCP defaults from enabled bundle plugins", async () => { const workspaceDir = await tempDirs.make("openclaw-workspace-"); const pluginRoot = await createWorkspaceBundle({ workspaceDir }); diff --git a/src/config/plugin-auto-enable.core.test.ts b/src/config/plugin-auto-enable.core.test.ts index 3af4eac8cc8..1226136ccbf 100644 --- a/src/config/plugin-auto-enable.core.test.ts +++ b/src/config/plugin-auto-enable.core.test.ts @@ -168,6 +168,40 @@ describe("applyPluginAutoEnable core", () => { ); }); + it("does not reuse an unscoped current manifest registry when plugin load paths change", () => { + const manifestRegistry = makeRegistry([{ id: "load-path-chat", channels: ["load-path-chat"] }]); + const snapshotConfig: OpenClawConfig = { plugins: { allow: ["existing"] } }; + setCurrentPluginMetadataSnapshot( + createPluginMetadataSnapshot({ + config: snapshotConfig, + manifestRegistry, + workspaceDir: "/tmp/workspace", + }), + { + config: snapshotConfig, + workspaceDir: "/tmp/workspace", + }, + ); + + const result = applyPluginAutoEnable({ + config: { + plugins: { + allow: ["existing"], + load: { paths: ["/tmp/changed-plugin-root"] }, + entries: { + "load-path-chat": { config: { token: "x" } }, + }, + }, + }, + env, + }); + + expect(result.config.plugins?.allow).toEqual(["existing"]); + expect(result.changes).not.toContain( + "load-path-chat plugin config present, added to plugin allowlist.", + ); + }); + it("formats typed provider-auth candidates into stable reasons", () => { expect( resolvePluginAutoEnableCandidateReason({ diff --git a/src/config/plugin-auto-enable.shared.ts b/src/config/plugin-auto-enable.shared.ts index 5aa0e85d4b8..2fd7fc73873 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -9,6 +9,7 @@ import { listBundledChannelIdsWithConfiguredState, } from "../channels/plugins/configured-state.js"; import { getChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js"; +import { normalizePluginsConfig } from "../plugins/config-state.js"; import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; import { resolveInstalledPluginIndexPolicyHash } from "../plugins/installed-plugin-index-policy.js"; import { @@ -54,6 +55,10 @@ function resolveAutoEnableProviderPluginIds( return Object.fromEntries(entries); } +function canReuseUnscopedCurrentPluginMetadataSnapshot(config: OpenClawConfig): boolean { + return normalizePluginsConfig(config.plugins).loadPaths.length === 0; +} + function extractProviderFromModelRef(value: string): string | null { const trimmed = value.trim(); const slash = trimmed.indexOf("/"); @@ -955,6 +960,9 @@ export function resolvePluginAutoEnableManifestRegistry(params: { const policyCompatibleCurrentSnapshot = currentSnapshot ?? (() => { + if (!canReuseUnscopedCurrentPluginMetadataSnapshot(params.config)) { + return undefined; + } const snapshot = getCurrentPluginMetadataSnapshot({ env: params.env, allowWorkspaceScopedSnapshot: true, From 917ccde7bf0a2ac83478a5050dafbf1fada8a793 Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 06:17:34 +0100 Subject: [PATCH 041/215] docs: clarify load path metadata reuse --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fbd3c6b766..56b76702be0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,7 @@ Docs: https://docs.openclaw.ai - Docs: clarify that IRC uses raw TCP/TLS sockets outside operator-managed forward proxy routing, so direct IRC egress should be explicitly approved before enabling IRC. Thanks @jesse-merhi. - Gateway/performance: defer non-readiness sidecars until after the ready signal, avoid hot-path channel plugin barrel imports, and fast-path trusted bundled plugin metadata during Gateway startup. - Gateway/performance: reuse the compatible plugin metadata snapshot across dashboard and channel agent turns so auto-enabled runtime config does not repeatedly rescan plugin metadata before provider calls. Thanks @shakkernerd. -- Gateway/performance: reuse current plugin metadata for provider activation, auth/env lookup, and bundle settings during dashboard and channel agent turns while keeping the configless secret-target cache unscoped. Thanks @shakkernerd. +- Gateway/performance: reuse current plugin metadata for provider activation, auth/env lookup, and bundle settings during dashboard and channel agent turns while keeping the configless secret-target cache unscoped and refusing stale unscoped reuse when plugin load paths change. Thanks @shakkernerd. - Gateway/performance: avoid resolving plugin auto-enable metadata twice in one runtime config pass, reducing repeated dashboard turn metadata scans. Thanks @shakkernerd. - Auth/providers: pass `config` and `workspaceDir` lookup context through to provider-id resolution so workspace-scoped auth aliases resolve correctly when no explicit alias map is supplied. Thanks @shakkernerd. - Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed. From a7cc9e8a56fe7bcfac2d905ff4c3e36b698dd9e0 Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 06:35:36 +0100 Subject: [PATCH 042/215] fix: require default discovery for metadata reuse --- src/agents/pi-project-settings-snapshot.ts | 28 ++++++--- src/agents/pi-project-settings.bundle.test.ts | 58 +++++++++++++++++++ src/agents/provider-auth-aliases.ts | 19 +++--- src/config/plugin-auto-enable.core.test.ts | 36 ++++++++++++ src/config/plugin-auto-enable.shared.ts | 1 + src/plugins/activation-context.ts | 12 ++-- .../current-plugin-metadata-snapshot.test.ts | 26 +++++++++ .../current-plugin-metadata-snapshot.ts | 20 +++++++ 8 files changed, 181 insertions(+), 19 deletions(-) diff --git a/src/agents/pi-project-settings-snapshot.ts b/src/agents/pi-project-settings-snapshot.ts index cb715aed972..f3acc01da5b 100644 --- a/src/agents/pi-project-settings-snapshot.ts +++ b/src/agents/pi-project-settings-snapshot.ts @@ -45,6 +45,22 @@ function canReuseUnscopedCurrentPluginMetadataSnapshot(config: OpenClawConfig): return normalizePluginsConfigWithResolver(config.plugins).loadPaths.length === 0; } +function resolveUnscopedCurrentPluginMetadataSnapshot(params: { + config: OpenClawConfig; + env: NodeJS.ProcessEnv; + workspaceDir?: string; +}): PluginMetadataSnapshot | undefined { + if (!canReuseUnscopedCurrentPluginMetadataSnapshot(params.config)) { + return undefined; + } + return getCurrentPluginMetadataSnapshot({ + env: params.env, + workspaceDir: params.workspaceDir, + allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, + }); +} + function loadBundleSettingsFile(params: { rootDir: string; relativePath: string; @@ -94,13 +110,11 @@ export function loadEnabledBundlePiSettingsSnapshot(params: { env, workspaceDir, }) ?? - (canReuseUnscopedCurrentPluginMetadataSnapshot(config) - ? getCurrentPluginMetadataSnapshot({ - env, - workspaceDir, - allowWorkspaceScopedSnapshot: true, - }) - : undefined) ?? + resolveUnscopedCurrentPluginMetadataSnapshot({ + config, + env, + workspaceDir, + }) ?? loadPluginMetadataSnapshot({ workspaceDir, config, diff --git a/src/agents/pi-project-settings.bundle.test.ts b/src/agents/pi-project-settings.bundle.test.ts index 43abf6ba349..766468745fa 100644 --- a/src/agents/pi-project-settings.bundle.test.ts +++ b/src/agents/pi-project-settings.bundle.test.ts @@ -364,6 +364,64 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce(); }); + it("does not reuse a load-path current snapshot for a config with default load paths", async () => { + const workspaceDir = await tempDirs.make("openclaw-workspace-"); + const pluginRoot = await createWorkspaceBundle({ workspaceDir }); + const resolvedPluginRoot = await fs.realpath(pluginRoot); + await fs.writeFile( + path.join(pluginRoot, "settings.json"), + JSON.stringify({ hideThinkingBlock: true }), + "utf-8", + ); + const staleSnapshot = { + policyHash: "policy", + manifestRegistry: { + diagnostics: [], + plugins: [ + { + id: "claude-bundle", + origin: "workspace", + format: "bundle", + bundleFormat: "claude", + settingsFiles: ["settings.json"], + rootDir: resolvedPluginRoot, + }, + ], + }, + normalizePluginId: (id: string) => id.trim(), + }; + pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot.mockImplementation( + (params: { config?: unknown; requireDefaultDiscoveryContext?: boolean }) => { + if (params.config || params.requireDefaultDiscoveryContext) { + return undefined; + } + return staleSnapshot; + }, + ); + pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear(); + + const snapshot = loadEnabledBundlePiSettingsSnapshot({ + cwd: workspaceDir, + cfg: { + plugins: { + entries: { + "claude-bundle": { enabled: true }, + }, + }, + }, + }); + + expect(snapshot.hideThinkingBlock).toBe(true); + expect(pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledTimes(2); + expect(pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot).toHaveBeenLastCalledWith({ + env: process.env, + workspaceDir, + allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, + }); + expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce(); + }); + it("loads sanitized settings and MCP defaults from enabled bundle plugins", async () => { const workspaceDir = await tempDirs.make("openclaw-workspace-"); const pluginRoot = await createWorkspaceBundle({ workspaceDir }); diff --git a/src/agents/provider-auth-aliases.ts b/src/agents/provider-auth-aliases.ts index e025b503e4f..0242fad67f1 100644 --- a/src/agents/provider-auth-aliases.ts +++ b/src/agents/provider-auth-aliases.ts @@ -128,13 +128,18 @@ export function resolveProviderAuthAliasMap( env, allowWorkspaceScopedSnapshot: true, }) ?? - (normalizePluginsConfig(config.plugins).loadPaths.length === 0 - ? getCurrentPluginMetadataSnapshot({ - ...(params?.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), - env, - allowWorkspaceScopedSnapshot: true, - }) - : undefined) ?? + (() => { + if (normalizePluginsConfig(config.plugins).loadPaths.length !== 0) { + return undefined; + } + const currentSnapshot = getCurrentPluginMetadataSnapshot({ + ...(params?.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), + env, + allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, + }); + return currentSnapshot; + })() ?? loadPluginMetadataSnapshot({ config, ...(params?.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), diff --git a/src/config/plugin-auto-enable.core.test.ts b/src/config/plugin-auto-enable.core.test.ts index 1226136ccbf..f19cc0e0cdd 100644 --- a/src/config/plugin-auto-enable.core.test.ts +++ b/src/config/plugin-auto-enable.core.test.ts @@ -146,6 +146,7 @@ describe("applyPluginAutoEnable core", () => { }), { config: snapshotConfig, + env, workspaceDir: "/tmp/workspace", }, ); @@ -179,6 +180,7 @@ describe("applyPluginAutoEnable core", () => { }), { config: snapshotConfig, + env, workspaceDir: "/tmp/workspace", }, ); @@ -202,6 +204,40 @@ describe("applyPluginAutoEnable core", () => { ); }); + it("does not reuse a load-path current manifest registry for a config with default load paths", () => { + const manifestRegistry = makeRegistry([{ id: "load-path-chat", channels: ["load-path-chat"] }]); + const snapshotConfig: OpenClawConfig = { + plugins: { + allow: ["existing"], + load: { paths: ["/tmp/custom-plugin-root"] }, + }, + }; + setCurrentPluginMetadataSnapshot( + createPluginMetadataSnapshot({ + config: snapshotConfig, + manifestRegistry, + }), + { config: snapshotConfig, env }, + ); + + const result = applyPluginAutoEnable({ + config: { + plugins: { + allow: ["existing"], + entries: { + "load-path-chat": { config: { token: "x" } }, + }, + }, + }, + env, + }); + + expect(result.config.plugins?.allow).toEqual(["existing"]); + expect(result.changes).not.toContain( + "load-path-chat plugin config present, added to plugin allowlist.", + ); + }); + it("formats typed provider-auth candidates into stable reasons", () => { expect( resolvePluginAutoEnableCandidateReason({ diff --git a/src/config/plugin-auto-enable.shared.ts b/src/config/plugin-auto-enable.shared.ts index 2fd7fc73873..99a8b2fa4c0 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -966,6 +966,7 @@ export function resolvePluginAutoEnableManifestRegistry(params: { const snapshot = getCurrentPluginMetadataSnapshot({ env: params.env, allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, }); return snapshot?.policyHash === resolveInstalledPluginIndexPolicyHash(params.config) ? snapshot diff --git a/src/plugins/activation-context.ts b/src/plugins/activation-context.ts index 266270a623e..68dd7f3fe5e 100644 --- a/src/plugins/activation-context.ts +++ b/src/plugins/activation-context.ts @@ -182,15 +182,17 @@ function applyPluginAutoEnableForActivation(params: { workspaceDir: params.workspaceDir, allowWorkspaceScopedSnapshot: true, }); - const currentManifestRegistry = - currentSnapshot?.manifestRegistry ?? - (normalizePluginsConfig(params.config.plugins).loadPaths.length === 0 + const defaultDiscoverySnapshot = + normalizePluginsConfig(params.config.plugins).loadPaths.length === 0 ? getCurrentPluginMetadataSnapshot({ env: params.env, workspaceDir: params.workspaceDir, allowWorkspaceScopedSnapshot: true, - })?.manifestRegistry - : undefined); + requireDefaultDiscoveryContext: true, + }) + : undefined; + const currentManifestRegistry = + currentSnapshot?.manifestRegistry ?? defaultDiscoverySnapshot?.manifestRegistry; return applyPluginAutoEnable({ config: params.config, env: params.env, diff --git a/src/plugins/current-plugin-metadata-snapshot.test.ts b/src/plugins/current-plugin-metadata-snapshot.test.ts index 37e7f817f39..64b7a2423ae 100644 --- a/src/plugins/current-plugin-metadata-snapshot.test.ts +++ b/src/plugins/current-plugin-metadata-snapshot.test.ts @@ -113,6 +113,32 @@ describe("current plugin metadata snapshot", () => { ).toBeUndefined(); }); + it("rejects configless default-discovery reuse for snapshots created with load paths", () => { + const config = { plugins: { allow: ["demo"], load: { paths: ["/plugins/one"] } } }; + const snapshot = createSnapshot({ config }); + setCurrentPluginMetadataSnapshot(snapshot, { config }); + + expect( + getCurrentPluginMetadataSnapshot({ + allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, + }), + ).toBeUndefined(); + }); + + it("accepts configless default-discovery reuse for snapshots created without load paths", () => { + const config = { plugins: { allow: ["demo"] } }; + const snapshot = createSnapshot({ config }); + setCurrentPluginMetadataSnapshot(snapshot, { config }); + + expect( + getCurrentPluginMetadataSnapshot({ + allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, + }), + ).toBe(snapshot); + }); + it("rejects a current snapshot when env-resolved plugin load paths change", () => { const config = { plugins: { load: { paths: ["~/plugins"] } } }; const snapshot = createSnapshot({ config }); diff --git a/src/plugins/current-plugin-metadata-snapshot.ts b/src/plugins/current-plugin-metadata-snapshot.ts index aadbc96ad1b..900b183d458 100644 --- a/src/plugins/current-plugin-metadata-snapshot.ts +++ b/src/plugins/current-plugin-metadata-snapshot.ts @@ -70,6 +70,7 @@ export function getCurrentPluginMetadataSnapshot( env?: NodeJS.ProcessEnv; workspaceDir?: string; allowWorkspaceScopedSnapshot?: boolean; + requireDefaultDiscoveryContext?: boolean; } = {}, ): PluginMetadataSnapshot | undefined { const { @@ -110,6 +111,25 @@ export function getCurrentPluginMetadataSnapshot( return undefined; } } + if (params.requireDefaultDiscoveryContext === true) { + const defaultDiscoveryConfigFingerprint = resolvePluginMetadataControlPlaneFingerprint( + {}, + { + env: params.env, + index: snapshot.index, + policyHash: snapshot.policyHash, + workspaceDir: requestedWorkspaceDir, + }, + ); + const compatibleFingerprints = new Set(compatibleConfigFingerprints ?? []); + const fingerprintMatches = + configFingerprint === defaultDiscoveryConfigFingerprint || + snapshot.configFingerprint === defaultDiscoveryConfigFingerprint || + compatibleFingerprints.has(defaultDiscoveryConfigFingerprint); + if (!fingerprintMatches) { + return undefined; + } + } if (snapshot.workspaceDir !== undefined && requestedWorkspaceDir === undefined) { return undefined; } From 3a718ed4916ed751cd335199f38cf00fc4a0c42a Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 06:35:47 +0100 Subject: [PATCH 043/215] docs: clarify metadata discovery reuse --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56b76702be0..ce452ca4aa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,7 @@ Docs: https://docs.openclaw.ai - Docs: clarify that IRC uses raw TCP/TLS sockets outside operator-managed forward proxy routing, so direct IRC egress should be explicitly approved before enabling IRC. Thanks @jesse-merhi. - Gateway/performance: defer non-readiness sidecars until after the ready signal, avoid hot-path channel plugin barrel imports, and fast-path trusted bundled plugin metadata during Gateway startup. - Gateway/performance: reuse the compatible plugin metadata snapshot across dashboard and channel agent turns so auto-enabled runtime config does not repeatedly rescan plugin metadata before provider calls. Thanks @shakkernerd. -- Gateway/performance: reuse current plugin metadata for provider activation, auth/env lookup, and bundle settings during dashboard and channel agent turns while keeping the configless secret-target cache unscoped and refusing stale unscoped reuse when plugin load paths change. Thanks @shakkernerd. +- Gateway/performance: reuse current plugin metadata for provider activation, auth/env lookup, and bundle settings during dashboard and channel agent turns while keeping the configless secret-target cache unscoped and refusing stale unscoped reuse when plugin discovery roots differ. Thanks @shakkernerd. - Gateway/performance: avoid resolving plugin auto-enable metadata twice in one runtime config pass, reducing repeated dashboard turn metadata scans. Thanks @shakkernerd. - Auth/providers: pass `config` and `workspaceDir` lookup context through to provider-id resolution so workspace-scoped auth aliases resolve correctly when no explicit alias map is supplied. Thanks @shakkernerd. - Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed. From 835b88460600f265fabc8544ae1f65cb17867b13 Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 06:41:36 +0100 Subject: [PATCH 044/215] fix: guard provider env metadata reuse --- src/secrets/provider-env-vars.dynamic.test.ts | 43 +++++++++++++++++++ src/secrets/provider-env-vars.ts | 1 + 2 files changed, 44 insertions(+) diff --git a/src/secrets/provider-env-vars.dynamic.test.ts b/src/secrets/provider-env-vars.dynamic.test.ts index 5199802d19b..eca7ef42956 100644 --- a/src/secrets/provider-env-vars.dynamic.test.ts +++ b/src/secrets/provider-env-vars.dynamic.test.ts @@ -6,6 +6,7 @@ import { listKnownSecretEnvVarNames, PROVIDER_AUTH_ENV_VAR_CANDIDATES, PROVIDER_ENV_VARS, + resolveProviderAuthEnvVarCandidates, resolveProviderAuthEvidence, } from "./provider-env-vars.js"; @@ -233,6 +234,48 @@ describe("provider env vars dynamic manifest metadata", () => { expect(pluginRegistryMocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled(); }); + it("does not reuse a load-path current snapshot for default provider env lookups", async () => { + const staleSnapshot = { + index: { + plugins: [ + { + pluginId: "load-path-provider", + origin: "global", + enabled: true, + enabledByDefault: true, + }, + ], + }, + plugins: [ + { + id: "load-path-provider", + origin: "global", + providerAuthEnvVars: { + "load-path-provider": ["LOAD_PATH_PROVIDER_API_KEY"], + }, + }, + ], + }; + pluginRegistryMocks.getCurrentPluginMetadataSnapshot.mockImplementation( + (params: { config?: unknown; requireDefaultDiscoveryContext?: boolean }) => { + if (params.config || params.requireDefaultDiscoveryContext) { + return undefined; + } + return staleSnapshot; + }, + ); + + expect( + resolveProviderAuthEnvVarCandidates({ config: {} })["load-path-provider"], + ).toBeUndefined(); + expect(pluginRegistryMocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({ + env: process.env, + allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, + }); + expect(pluginRegistryMocks.loadPluginMetadataSnapshot).toHaveBeenCalled(); + }); + it("excludes untrusted workspace plugin auth evidence by default", async () => { pluginRegistryMocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({ plugins: [ diff --git a/src/secrets/provider-env-vars.ts b/src/secrets/provider-env-vars.ts index ef6e2d9524a..4e5bb43ba41 100644 --- a/src/secrets/provider-env-vars.ts +++ b/src/secrets/provider-env-vars.ts @@ -139,6 +139,7 @@ function resolveProviderMetadataSnapshot( env, ...(params?.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, }); if (unscopedCurrent) { return unscopedCurrent; From c233e813a5fa3924989ad6b8cf9be72147863004 Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 06:41:48 +0100 Subject: [PATCH 045/215] docs: clarify provider env metadata reuse --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce452ca4aa1..15306230d11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,7 @@ Docs: https://docs.openclaw.ai - Docs: clarify that IRC uses raw TCP/TLS sockets outside operator-managed forward proxy routing, so direct IRC egress should be explicitly approved before enabling IRC. Thanks @jesse-merhi. - Gateway/performance: defer non-readiness sidecars until after the ready signal, avoid hot-path channel plugin barrel imports, and fast-path trusted bundled plugin metadata during Gateway startup. - Gateway/performance: reuse the compatible plugin metadata snapshot across dashboard and channel agent turns so auto-enabled runtime config does not repeatedly rescan plugin metadata before provider calls. Thanks @shakkernerd. -- Gateway/performance: reuse current plugin metadata for provider activation, auth/env lookup, and bundle settings during dashboard and channel agent turns while keeping the configless secret-target cache unscoped and refusing stale unscoped reuse when plugin discovery roots differ. Thanks @shakkernerd. +- Gateway/performance: reuse current plugin metadata for provider activation, auth/env candidate lookup, and bundle settings during dashboard and channel agent turns while keeping the configless secret-target cache unscoped and refusing stale unscoped reuse when plugin discovery roots differ. Thanks @shakkernerd. - Gateway/performance: avoid resolving plugin auto-enable metadata twice in one runtime config pass, reducing repeated dashboard turn metadata scans. Thanks @shakkernerd. - Auth/providers: pass `config` and `workspaceDir` lookup context through to provider-id resolution so workspace-scoped auth aliases resolve correctly when no explicit alias map is supplied. Thanks @shakkernerd. - Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed. From 5b34805895e88a2bca280949b5ae020965c85bd8 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 22:52:32 -0700 Subject: [PATCH 046/215] test(agents): remove unused bundle snapshot variable --- src/agents/pi-project-settings.bundle.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/agents/pi-project-settings.bundle.test.ts b/src/agents/pi-project-settings.bundle.test.ts index 766468745fa..1d4af916c8b 100644 --- a/src/agents/pi-project-settings.bundle.test.ts +++ b/src/agents/pi-project-settings.bundle.test.ts @@ -328,7 +328,6 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { it("does not reuse an unscoped current snapshot when plugin load paths change", async () => { const workspaceDir = await tempDirs.make("openclaw-workspace-"); const pluginRoot = await createWorkspaceBundle({ workspaceDir }); - const resolvedPluginRoot = await fs.realpath(pluginRoot); await fs.writeFile( path.join(pluginRoot, "settings.json"), JSON.stringify({ hideThinkingBlock: true }), From a428568157c3c27df3be9c43e6c89450ff219a46 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 07:08:48 +0100 Subject: [PATCH 047/215] fix(gemini): gate thought-signature replay trust --- extensions/google/transport-stream.test.ts | 36 +++++++++++++++++ extensions/google/transport-stream.ts | 4 +- src/agents/openai-transport-stream.test.ts | 47 ++++++++++++++++++++++ src/agents/openai-transport-stream.ts | 3 ++ 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/extensions/google/transport-stream.test.ts b/extensions/google/transport-stream.test.ts index cfa81b9b4ca..2c0d930ea99 100644 --- a/extensions/google/transport-stream.test.ts +++ b/extensions/google/transport-stream.test.ts @@ -542,6 +542,42 @@ describe("google transport stream", () => { }); }); + it("does not trust cross-provider tool-call thought signatures for non-Gemini-3 models", () => { + const model = buildGeminiModel({ + id: "gemini-2.5-pro", + name: "Gemini 2.5 Pro", + }); + + const params = buildGoogleGenerativeAiParams(model, { + messages: [ + { + role: "assistant", + provider: "anthropic", + api: "anthropic-messages", + model: "claude-opus-4-7", + stopReason: "toolUse", + timestamp: 0, + content: [ + { + type: "toolCall", + id: "call_1", + name: "lookup", + arguments: { q: "hello" }, + thoughtSignature: "foreign_sig", + }, + ], + }, + ], + } as never); + + expect(params.contents[0]).toMatchObject({ + role: "model", + parts: [{ functionCall: { name: "lookup", args: { q: "hello" } } }], + }); + expect(JSON.stringify(params.contents)).not.toContain("foreign_sig"); + expect(JSON.stringify(params.contents)).not.toContain("skip_thought_signature_validator"); + }); + it("builds direct Gemini payloads without negative fallback thinking budgets", () => { const model = { id: "custom-gemini-model", diff --git a/extensions/google/transport-stream.ts b/extensions/google/transport-stream.ts index 9fed6faaa8c..5b6350c283f 100644 --- a/extensions/google/transport-stream.ts +++ b/extensions/google/transport-stream.ts @@ -386,7 +386,9 @@ function convertGoogleMessages(model: GoogleTransportModel, context: Context) { context.messages, model, (id) => (requiresToolCallId(model.id) ? normalizeToolCallId(id) : id), - { preserveCrossModelToolCallThoughtSignature: true }, + { + preserveCrossModelToolCallThoughtSignature: requiresToolCallThoughtSignature(model.id), + }, ); for (const msg of transformedMessages) { if (msg.role === "user") { diff --git a/src/agents/openai-transport-stream.test.ts b/src/agents/openai-transport-stream.test.ts index a59d0cd48b2..b891e421cd9 100644 --- a/src/agents/openai-transport-stream.test.ts +++ b/src/agents/openai-transport-stream.test.ts @@ -2980,6 +2980,53 @@ describe("openai transport stream", () => { "skip_thought_signature_validator", ); }); + + it("does not trust cross-route thought_signature for non-Gemini-3 Google compat models", () => { + const nonGemini3Model = { + ...geminiModel, + id: "gemini-2.5-pro", + name: "Gemini 2.5 Pro", + }; + const params = buildOpenAICompletionsParams( + nonGemini3Model, + { + messages: [ + { + role: "assistant", + api: "google-generative-ai", + provider: nonGemini3Model.provider, + model: nonGemini3Model.id, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "toolUse", + timestamp: 1, + content: [ + { + type: "toolCall", + id: "call_abc", + name: "echo_value", + arguments: { value: "repro" }, + thoughtSignature: "SIG-OPAQUE-ABC==", + }, + ], + }, + ], + tools: [], + } as never, + undefined, + ) as { messages: Array> }; + + const assistant = params.messages.find((message) => message.role === "assistant") as + | { tool_calls?: Array<{ extra_content?: unknown }> } + | undefined; + expect(assistant?.tool_calls?.[0]?.extra_content).toBeUndefined(); + }); }); it("uses Mistral compat defaults for direct Mistral completions providers", () => { diff --git a/src/agents/openai-transport-stream.ts b/src/agents/openai-transport-stream.ts index ec9b0a5966a..920661f4766 100644 --- a/src/agents/openai-transport-stream.ts +++ b/src/agents/openai-transport-stream.ts @@ -1836,6 +1836,9 @@ function injectToolCallThoughtSignatures( source.api === model.api && source.provider === model.provider && source.model === model.id; + if (!isSameRoute && !fallbackSig) { + continue; + } sigById.set(id, isSameRoute ? sig : (fallbackSig ?? sig)); } } From 7dc597b92176c253fcc85ab349d59d718e38293f Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 23:10:26 -0700 Subject: [PATCH 048/215] docs: refresh config baseline hash --- docs/.generated/config-baseline.sha256 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index b50dae930ce..d2cd21d62ee 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -da2ba9afd1062db1fafe81fb42e39db4ad65995a5e56caef4057a9954c2c386b config-baseline.json +b14178c6945e0d9da9b35a12cc1fecd1406165bb440619c33bfec59fe5e0287d config-baseline.json f860a7d43d3bd15379d8c3dfccbc6fcbf47b9bec8d8b67b29dd7313946905645 config-baseline.core.json -cd7c0c7fb1435bc7e59099e9ac334462d5ad444016e9ab4512aae63a238f78dc config-baseline.channel.json -2fee9c16a60d074fac428b4ad14c38ad3ca7febefacfad819f741a820101326e config-baseline.plugin.json +463c45a79d02598184caccbc6f316692df962fe6b0e84d1a3e3cc1809f862b15 config-baseline.channel.json +3094eba68b507a852a73952179e5f6decddfbb1ec377a2bc65409f92aff647a3 config-baseline.plugin.json From a2efabf4c93baf23b9be0d161d8672f71cccdb21 Mon Sep 17 00:00:00 2001 From: NVIDIAN Date: Wed, 6 May 2026 23:15:19 -0700 Subject: [PATCH 049/215] fix(whatsapp): dedupe captioned MEDIA auto-replies (#78770) * fix(whatsapp): dedupe captioned MEDIA auto-replies * docs: note whatsapp media directive dedupe --------- Co-authored-by: Marcus Castro --- CHANGELOG.md | 1 + .../monitor/inbound-dispatch.test.ts | 22 +- .../auto-reply/monitor/inbound-dispatch.ts | 205 ++++++++++++++---- 3 files changed, 173 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15306230d11..892fe70b237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -506,6 +506,7 @@ Docs: https://docs.openclaw.ai - Agents/subagents: have completed session-mode subagent registry rows honor `agents.defaults.subagents.archiveAfterMinutes` (default 60 minutes; same knob run-mode already uses for `archiveAtMs`) instead of a hardcoded 5-minute TTL, so `subagents list` and other registry-backed surfaces still show recently-completed runs and operators have one consistent retention knob across spawn modes. (#78263) Thanks @arniesaha. - Plugins/channel setup: fix `setChannelRuntime` being silently dropped from non-bundled external plugin setup entries — external channel plugins that export `{ plugin, setChannelRuntime }` from their setup entry now have the runtime setter invoked, so the runtime initializer the provider polls for is set before the channel starts, preventing a poll timeout and gateway crash loop when the plugin opts into deferred startup loading. Fixes #77779. (#77799) Thanks @openperf. - WhatsApp: route proactive phone-number sends through Baileys LID forward mappings when available, so LID-addressed contacts receive agent messages instead of creating sender-only ghost chats. Fixes #67378. (#74925) Thanks @edenfunf. +- WhatsApp: send captioned `MEDIA:` directive auto-replies once instead of emitting an empty media message before the captioned media reply. (#78770) Thanks @ai-hpc. ## 2026.5.3-1 diff --git a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts index fa32513fae8..0a26be224b5 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts @@ -487,7 +487,7 @@ describe("whatsapp inbound dispatch", () => { expect(groupHistories.get("whatsapp:default:group:123@g.us") ?? []).toHaveLength(0); }); - it("delivers block and final WhatsApp payloads; suppresses text-only tool payloads but delivers media", async () => { + it("replaces duplicate media-only interim payloads with the final captioned WhatsApp media", async () => { const deliverReply = vi.fn(async () => acceptedDeliveryResult()); const rememberSentText = vi.fn(); @@ -509,16 +509,8 @@ describe("whatsapp inbound dispatch", () => { kind: "tool", }, ); - expect(deliverReply).toHaveBeenCalledTimes(1); - expect(rememberSentText).toHaveBeenCalledTimes(1); - expect(deliverReply).toHaveBeenLastCalledWith( - expect.objectContaining({ - replyResult: expect.objectContaining({ - mediaUrls: ["/tmp/generated.jpg"], - text: undefined, - }), - }), - ); + expect(deliverReply).not.toHaveBeenCalled(); + expect(rememberSentText).not.toHaveBeenCalled(); await deliver?.( { text: "generated image", mediaUrls: ["/tmp/generated.jpg"] }, @@ -526,8 +518,8 @@ describe("whatsapp inbound dispatch", () => { kind: "block", }, ); - expect(deliverReply).toHaveBeenCalledTimes(2); - expect(rememberSentText).toHaveBeenCalledTimes(2); + expect(deliverReply).toHaveBeenCalledTimes(1); + expect(rememberSentText).toHaveBeenCalledTimes(1); expect(deliverReply).toHaveBeenLastCalledWith( expect.objectContaining({ replyResult: expect.objectContaining({ @@ -539,8 +531,8 @@ describe("whatsapp inbound dispatch", () => { await deliver?.({ text: "block payload" }, { kind: "block" }); await deliver?.({ text: "final payload" }, { kind: "final" }); - expect(deliverReply).toHaveBeenCalledTimes(4); - expect(rememberSentText).toHaveBeenCalledTimes(4); + expect(deliverReply).toHaveBeenCalledTimes(3); + expect(rememberSentText).toHaveBeenCalledTimes(3); }); it("queues final WhatsApp payloads through durable outbound delivery", async () => { diff --git a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts index 7c74baca19d..9118d8414c7 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts @@ -60,9 +60,22 @@ type SenderContext = { e164?: string; }; +type ReplyDeliveryInfo = { kind: ReplyLifecycleKind }; + +type PendingWhatsAppMediaOnlyPayload = { + info: ReplyDeliveryInfo; + mediaUrls: Set; + payload: DeliverableWhatsAppOutboundPayload; +}; + +type WhatsAppMediaOnlyFlushResult = { + delivered: number; + droppedDuplicateMedia: number; +}; + function logWhatsAppReplyDeliveryError(params: { err: unknown; - info: { kind: ReplyLifecycleKind }; + info: ReplyDeliveryInfo; connectionId: string; conversationId: string; msg: WebInboundMsg; @@ -109,6 +122,85 @@ function resolveWhatsAppDeliverablePayload( return payload; } +function getWhatsAppPayloadMediaUrls(payload: ReplyPayload): Set { + return new Set( + [ + ...(Array.isArray(payload.mediaUrls) ? payload.mediaUrls : []), + ...(typeof payload.mediaUrl === "string" ? [payload.mediaUrl] : []), + ] + .map((url) => url.trim()) + .filter(Boolean), + ); +} + +function hasWhatsAppMediaUrlOverlap(left: Set, right: Set): boolean { + for (const url of left) { + if (right.has(url)) { + return true; + } + } + return false; +} + +function shouldDeferWhatsAppMediaOnlyPayload(params: { + info: ReplyDeliveryInfo; + mediaUrls: Set; + reply: ReturnType; +}): boolean { + return ( + params.info.kind !== "final" && + params.reply.hasMedia && + !params.reply.text.trim() && + params.mediaUrls.size > 0 + ); +} + +function createWhatsAppMediaOnlyReplyCoalescer(params: { + deliver: (pending: PendingWhatsAppMediaOnlyPayload) => Promise; +}) { + const pendingMediaOnlyPayloads: PendingWhatsAppMediaOnlyPayload[] = []; + const flushExceptDuplicateMedia = async ( + mediaUrls?: Set, + ): Promise => { + const flushResult: WhatsAppMediaOnlyFlushResult = { + delivered: 0, + droppedDuplicateMedia: 0, + }; + const pending = pendingMediaOnlyPayloads.splice(0); + for (const candidate of pending) { + if (mediaUrls && hasWhatsAppMediaUrlOverlap(candidate.mediaUrls, mediaUrls)) { + flushResult.droppedDuplicateMedia += 1; + continue; + } + await params.deliver(candidate); + flushResult.delivered += 1; + } + return flushResult; + }; + + return { + defer(pending: PendingWhatsAppMediaOnlyPayload) { + pendingMediaOnlyPayloads.push(pending); + }, + flushExceptDuplicateMedia, + flushAll: () => flushExceptDuplicateMedia(), + }; +} + +function logWhatsAppMediaOnlyFlushResult(result: WhatsAppMediaOnlyFlushResult) { + if (!shouldLogVerbose()) { + return; + } + if (result.droppedDuplicateMedia > 0) { + logVerbose( + `Dropped ${result.droppedDuplicateMedia} deferred media-only WhatsApp reply payload(s) superseded by captioned media`, + ); + } + if (result.delivered > 0) { + logVerbose(`Flushed ${result.delivered} deferred media-only WhatsApp reply payload(s)`); + } +} + export function resolveWhatsAppResponsePrefix(params: { cfg: ReturnType; agentId: string; @@ -335,6 +427,63 @@ export async function dispatchWhatsAppBufferedReply(params: { let didSendReply = false; let didLogHeartbeatStrip = false; + const deliverNormalizedPayload = async ( + normalizedDeliveryPayload: DeliverableWhatsAppOutboundPayload, + info: ReplyDeliveryInfo, + ) => { + const reply = resolveSendableOutboundReplyParts(normalizedDeliveryPayload); + if (!reply.hasMedia && !reply.text.trim()) { + return; + } + const delivery = await params.deliverReply({ + replyResult: normalizedDeliveryPayload, + normalizedReplyResult: normalizedDeliveryPayload, + msg: params.msg, + mediaLocalRoots, + maxMediaBytes: params.maxMediaBytes, + textLimit, + chunkMode, + replyLogger: params.replyLogger, + connectionId: params.connectionId, + skipLog: false, + tableMode, + }); + if (!delivery.providerAccepted) { + params.replyLogger.warn( + { + correlationId: params.msg.id ?? null, + connectionId: params.connectionId, + conversationId: params.conversationId, + chatId: params.msg.chatId, + to: params.msg.from, + from: params.msg.to, + replyKind: info.kind, + }, + "auto-reply was not accepted by WhatsApp provider", + ); + return; + } + didSendReply = true; + const shouldLog = normalizedDeliveryPayload.text ? true : undefined; + params.rememberSentText(normalizedDeliveryPayload.text, { + combinedBody: params.context.Body as string | undefined, + combinedBodySessionKey: params.route.sessionKey, + logVerboseMessage: shouldLog, + }); + const fromDisplay = + params.msg.chatType === "group" ? params.conversationId : (params.msg.from ?? "unknown"); + if (shouldLogVerbose()) { + const preview = normalizedDeliveryPayload.text != null ? reply.text : ""; + logVerbose(`Reply body: ${preview}${reply.hasMedia ? " (media)" : ""} -> ${fromDisplay}`); + } + }; + + const mediaOnlyCoalescer = createWhatsAppMediaOnlyReplyCoalescer({ + deliver: async (pending) => { + await deliverNormalizedPayload(pending.payload, pending.info); + }, + }); + const { queuedFinal, counts } = await dispatchReplyWithBufferedBlockDispatcher({ ctx: params.context, cfg: params.cfg, @@ -364,6 +513,7 @@ export async function dispatchWhatsAppBufferedReply(params: { return; } if (!reply.hasMedia) { + logWhatsAppMediaOnlyFlushResult(await mediaOnlyCoalescer.flushAll()); const durable = await deliverInboundReplyWithMessageSendContext({ cfg: params.cfg, channel: "whatsapp", @@ -395,48 +545,22 @@ export async function dispatchWhatsAppBufferedReply(params: { if (durable.status === "handled_no_send") { return; } - } - const delivery = await params.deliverReply({ - replyResult: normalizedDeliveryPayload, - normalizedReplyResult: normalizedDeliveryPayload, - msg: params.msg, - mediaLocalRoots, - maxMediaBytes: params.maxMediaBytes, - textLimit, - chunkMode, - replyLogger: params.replyLogger, - connectionId: params.connectionId, - skipLog: false, - tableMode, - }); - if (!delivery.providerAccepted) { - params.replyLogger.warn( - { - correlationId: params.msg.id ?? null, - connectionId: params.connectionId, - conversationId: params.conversationId, - chatId: params.msg.chatId, - to: params.msg.from, - from: params.msg.to, - replyKind: info.kind, - }, - "auto-reply was not accepted by WhatsApp provider", - ); + await deliverNormalizedPayload(normalizedDeliveryPayload, info); return; } - didSendReply = true; - const shouldLog = normalizedDeliveryPayload.text ? true : undefined; - params.rememberSentText(normalizedDeliveryPayload.text, { - combinedBody: params.context.Body as string | undefined, - combinedBodySessionKey: params.route.sessionKey, - logVerboseMessage: shouldLog, - }); - const fromDisplay = - params.msg.chatType === "group" ? params.conversationId : (params.msg.from ?? "unknown"); - if (shouldLogVerbose()) { - const preview = normalizedDeliveryPayload.text != null ? reply.text : ""; - logVerbose(`Reply body: ${preview}${reply.hasMedia ? " (media)" : ""} -> ${fromDisplay}`); + const mediaUrls = getWhatsAppPayloadMediaUrls(normalizedDeliveryPayload); + if (shouldDeferWhatsAppMediaOnlyPayload({ info, mediaUrls, reply })) { + mediaOnlyCoalescer.defer({ + info, + mediaUrls, + payload: normalizedDeliveryPayload, + }); + return; } + logWhatsAppMediaOnlyFlushResult( + await mediaOnlyCoalescer.flushExceptDuplicateMedia(mediaUrls), + ); + await deliverNormalizedPayload(normalizedDeliveryPayload, info); }, onReplyStart: params.msg.sendComposing, onError: (err, info) => { @@ -456,6 +580,7 @@ export async function dispatchWhatsAppBufferedReply(params: { onModelSelected: params.onModelSelected, }, }); + logWhatsAppMediaOnlyFlushResult(await mediaOnlyCoalescer.flushAll()); const didQueueVisibleReply = hasVisibleInboundReplyDispatch({ queuedFinal, counts }); if (!didQueueVisibleReply) { From a4b8cc307cd43e2ba945b0826188c43592495192 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 23:16:06 -0700 Subject: [PATCH 050/215] docs: refresh plugin sdk api baseline hash --- docs/.generated/plugin-sdk-api-baseline.sha256 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index ffad13cc882..b730e543e49 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -e26753b5aaa10cd98cb0e07fca4034c091471cf434239cc3597b62b5a62b082b plugin-sdk-api-baseline.json -7b998abde706a1afe4d1e4475a87069c31f673c3c90b8a7f23f7ba8cff6d1c85 plugin-sdk-api-baseline.jsonl +c6b4de2d3df663777ef44f4f24b7bd6d935be6f4c9a1206ce95b1fa1b64993fd plugin-sdk-api-baseline.json +d4f6a5bd7500dc0640f7c5c53f715131de8de50927c557057eeb7b7a63197fb4 plugin-sdk-api-baseline.jsonl From 5b9672b4bbfb5698418847941cacea19752f7f1b Mon Sep 17 00:00:00 2001 From: Kevin Lin Date: Wed, 6 May 2026 23:28:02 -0700 Subject: [PATCH 051/215] fix: surface cron model rejection diagnostics Fixes #78597 --- CHANGELOG.md | 1 + .../run.cron-model-override.test.ts | 10 ++ src/cron/service/timer.regression.test.ts | 14 ++- src/cron/service/timer.ts | 11 ++ src/gateway/server/hooks.agent-trust.test.ts | 110 ++++++++++++++++++ src/gateway/server/hooks.ts | 66 ++++++++++- 6 files changed, 206 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 892fe70b237..f6210640f80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -507,6 +507,7 @@ Docs: https://docs.openclaw.ai - Plugins/channel setup: fix `setChannelRuntime` being silently dropped from non-bundled external plugin setup entries — external channel plugins that export `{ plugin, setChannelRuntime }` from their setup entry now have the runtime setter invoked, so the runtime initializer the provider polls for is set before the channel starts, preventing a poll timeout and gateway crash loop when the plugin opts into deferred startup loading. Fixes #77779. (#77799) Thanks @openperf. - WhatsApp: route proactive phone-number sends through Baileys LID forward mappings when available, so LID-addressed contacts receive agent messages instead of creating sender-only ghost chats. Fixes #67378. (#74925) Thanks @edenfunf. - WhatsApp: send captioned `MEDIA:` directive auto-replies once instead of emitting an empty media message before the captioned media reply. (#78770) Thanks @ai-hpc. +- Hooks/cron: log returned `/hooks/agent` isolated-run errors and failed cron jobs with cron diagnostic summaries, so rejected `payload.model` values are visible instead of looking like accepted-but-missing runs. Fixes #78597. (#78655) Thanks @kevinslin. ## 2026.5.3-1 diff --git a/src/cron/isolated-agent/run.cron-model-override.test.ts b/src/cron/isolated-agent/run.cron-model-override.test.ts index 1454af39cab..f4ffe1afe09 100644 --- a/src/cron/isolated-agent/run.cron-model-override.test.ts +++ b/src/cron/isolated-agent/run.cron-model-override.test.ts @@ -175,6 +175,16 @@ describe("runCronIsolatedAgentTurn — cron model override (#21057)", () => { expect(result.status).toBe("error"); expect(result.error).toContain("Model not allowed"); + expect(result.diagnostics).toMatchObject({ + summary: expect.stringContaining("Model not allowed"), + entries: [ + expect.objectContaining({ + source: "cron-preflight", + severity: "error", + message: expect.stringContaining("Model not allowed"), + }), + ], + }); // Model should remain undefined — the early return happens before the // pre-run persist block, so neither the session entry nor the store // should be touched with a rejected model. diff --git a/src/cron/service/timer.regression.test.ts b/src/cron/service/timer.regression.test.ts index 6ad0757dd10..284918f1fa2 100644 --- a/src/cron/service/timer.regression.test.ts +++ b/src/cron/service/timer.regression.test.ts @@ -1487,7 +1487,7 @@ describe("cron service timer regressions", () => { expect(job.state.nextRunAtMs).toBe(expectedNextMs); }); - it("persists last cron run diagnostics on job state", () => { + it("persists and warns with last cron run diagnostics", () => { const startedAt = Date.parse("2026-04-14T12:00:00.000Z"); const endedAt = startedAt + 500; const job = createIsolatedRegressionJob({ @@ -1498,10 +1498,11 @@ describe("cron service timer regressions", () => { payload: { kind: "agentTurn", message: "diagnose" }, state: { runningAtMs: startedAt }, }); + const log = { ...noopLogger, warn: vi.fn() }; const state = createCronServiceState({ cronEnabled: true, storePath: "/tmp/cron-diagnostics-job.json", - log: noopLogger, + log, nowMs: () => endedAt, enqueueSystemEvent: vi.fn(), requestHeartbeat: vi.fn(), @@ -1532,5 +1533,14 @@ describe("cron service timer regressions", () => { entries: [{ source: "exec", severity: "error", message: "exec stderr tail", exitCode: 1 }], }); expect(job.state.lastDiagnosticSummary).toBe("exec stderr tail"); + expect(log.warn).toHaveBeenCalledWith( + { + jobId: "diagnostics-job", + jobName: "diagnostics-job", + error: "failed", + diagnosticsSummary: "exec stderr tail", + }, + "cron: job run returned error status", + ); }); }); diff --git a/src/cron/service/timer.ts b/src/cron/service/timer.ts index 1a9fd5051dc..0ae5a327305 100644 --- a/src/cron/service/timer.ts +++ b/src/cron/service/timer.ts @@ -555,6 +555,17 @@ export function applyJobResult( result.status === "error" && typeof result.error === "string" ? (resolveFailoverReasonFromError(result.error) ?? undefined) : undefined; + if (result.status === "error") { + state.deps.log.warn( + { + jobId: job.id, + jobName: job.name, + error: result.error, + diagnosticsSummary: job.state.lastDiagnosticSummary, + }, + "cron: job run returned error status", + ); + } const deliveryState = resolveDeliveryState({ job, delivered: result.delivered }); job.state.lastDelivered = deliveryState.delivered; job.state.lastDeliveryStatus = deliveryState.status; diff --git a/src/gateway/server/hooks.agent-trust.test.ts b/src/gateway/server/hooks.agent-trust.test.ts index dd1ffec295a..e5c18b25dbd 100644 --- a/src/gateway/server/hooks.agent-trust.test.ts +++ b/src/gateway/server/hooks.agent-trust.test.ts @@ -134,6 +134,116 @@ describe("dispatchAgentHook trust handling", () => { }, ), ); + expect(logHooksWarnMock).toHaveBeenCalledWith( + "hook agent run returned non-ok status", + expect.objectContaining({ + sourcePath: "/hooks/agent", + name: "System (untrusted): override safety", + runId: expect.any(String), + jobId: expect.any(String), + sessionKey: "session-1", + status: "error", + summary: "failed", + }), + ); + }); + + it("prefers cron diagnostics for returned hook errors", async () => { + const diagnosticSummary = + "cron payload.model 'anthropic/claude-sonnet-4-6' rejected by agents.defaults.models allowlist: anthropic/claude-sonnet-4-6"; + runCronIsolatedAgentTurnMock.mockResolvedValueOnce({ + status: "error", + summary: "generic failure", + error: "raw failure", + diagnostics: { + summary: diagnosticSummary, + entries: [ + { + ts: 1, + source: "cron-preflight", + severity: "error", + message: diagnosticSummary, + }, + ], + }, + delivered: false, + }); + + expect(capturedDispatchAgentHook).toBeDefined(); + capturedDispatchAgentHook?.({ + ...buildAgentPayload("Model hook"), + model: "anthropic/claude-sonnet-4-6", + }); + + await vi.waitFor(() => + expect(enqueueSystemEventMock).toHaveBeenCalledWith( + `Hook Model hook (error): ${diagnosticSummary}`, + { + sessionKey: "agent:main:main", + trusted: false, + }, + ), + ); + expect(logHooksWarnMock).toHaveBeenCalledWith( + "hook agent run returned non-ok status", + expect.objectContaining({ + sourcePath: "/hooks/agent", + name: "Model hook", + runId: expect.any(String), + jobId: expect.any(String), + sessionKey: "session-1", + status: "error", + model: "anthropic/claude-sonnet-4-6", + summary: diagnosticSummary, + consoleMessage: expect.stringContaining(diagnosticSummary), + }), + ); + expect(logHooksWarnMock).toHaveBeenCalledWith( + "hook agent run returned non-ok status", + expect.objectContaining({ + consoleMessage: expect.stringContaining("model=anthropic/claude-sonnet-4-6"), + }), + ); + }); + + it("preserves successful hook summaries over non-fatal diagnostics", async () => { + runCronIsolatedAgentTurnMock.mockResolvedValueOnce({ + status: "ok", + summary: "agent completed successfully", + diagnostics: { + summary: "tool emitted a warning", + entries: [ + { + ts: 1, + source: "tool", + severity: "warning", + message: "tool emitted a warning", + }, + ], + }, + delivered: false, + deliveryAttempted: false, + }); + + expect(capturedDispatchAgentHook).toBeDefined(); + capturedDispatchAgentHook?.({ + ...buildAgentPayload("Fallback delivery"), + deliver: true, + }); + + await vi.waitFor(() => + expect(enqueueSystemEventMock).toHaveBeenCalledWith( + "Hook Fallback delivery: agent completed successfully", + { + sessionKey: "agent:main:main", + trusted: false, + }, + ), + ); + expect(enqueueSystemEventMock).not.toHaveBeenCalledWith( + expect.stringContaining("tool emitted a warning"), + expect.anything(), + ); }); it("announces skipped deliver:false hook results as non-ok status events", async () => { diff --git a/src/gateway/server/hooks.ts b/src/gateway/server/hooks.ts index 396c8aa968c..6ab654a2faa 100644 --- a/src/gateway/server/hooks.ts +++ b/src/gateway/server/hooks.ts @@ -37,6 +37,49 @@ function shouldAnnounceHookRunResult(params: { ); } +function resolveHookRunSummary(result: RunCronAgentTurnResult): string { + const diagnosticsSummary = + result.status !== "ok" ? normalizeOptionalString(result.diagnostics?.summary) : undefined; + return ( + diagnosticsSummary || + normalizeOptionalString(result.summary) || + normalizeOptionalString(result.error) || + result.status + ); +} + +function sanitizeHookConsoleValue(value: string | undefined): string | undefined { + const normalized = normalizeOptionalString(value); + if (!normalized) { + return undefined; + } + const withoutControlChars = Array.from(normalized, (char) => { + const code = char.charCodeAt(0); + return code < 32 || code === 127 ? " " : char; + }).join(""); + return withoutControlChars.replace(/\s+/gu, " ").trim().slice(0, 500); +} + +function formatHookRunWarningConsoleMessage(params: { + status: string; + model: string | undefined; + summary: string; +}): string { + const parts = [ + "hook agent run returned non-ok status", + `status=${sanitizeHookConsoleValue(params.status) ?? "unknown"}`, + ]; + const model = sanitizeHookConsoleValue(params.model); + if (model) { + parts.push(`model=${model}`); + } + const summary = sanitizeHookConsoleValue(params.summary); + if (summary) { + parts.push(`summary=${summary}`); + } + return parts.join(" "); +} + export function createGatewayHooksRequestHandler(params: { deps: CliDeps; getHooksConfig: () => HooksConfigResolved | null; @@ -108,13 +151,28 @@ export function createGatewayHooksRequestHandler(params: { sessionKey, lane: "cron", }); - const summary = - normalizeOptionalString(result.summary) || - normalizeOptionalString(result.error) || - result.status; + const summary = resolveHookRunSummary(result); const prefix = result.status === "ok" ? `Hook ${safeName}` : `Hook ${safeName} (${result.status})`; const shouldAnnounce = shouldAnnounceHookRunResult({ deliver: value.deliver, result }); + if (result.status !== "ok") { + logHooks.warn("hook agent run returned non-ok status", { + sourcePath: value.sourcePath, + name: safeName, + runId, + jobId, + agentId: value.agentId, + sessionKey, + status: result.status, + model: value.model, + summary, + consoleMessage: formatHookRunWarningConsoleMessage({ + status: result.status, + model: value.model, + summary, + }), + }); + } if (shouldAnnounce) { const eventSessionKey = hookEventSessionKey ?? resolveMainSessionKeyFromConfig(); enqueueSystemEvent(`${prefix}: ${summary}`.trim(), { From 42ecd5d95eae8b9fbe155a2e5bb20f32080826e3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 07:30:37 +0100 Subject: [PATCH 052/215] fix(acpx): harden session lifecycle cleanup Harden ACPX process cleanup with lease-backed ownership verification, startup orphan reaping, reusable cancel semantics, and spawned-session visibility fixes. --- CHANGELOG.md | 3 + docs/.i18n/glossary.zh-CN.json | 4 + docs/docs.json | 1 + docs/gateway/cli-backends.md | 6 + docs/plugins/building-plugins.md | 8 +- docs/plugins/cli-backend-plugins.md | 310 +++++++++++ docs/plugins/sdk-overview.md | 5 +- docs/refactor/acp.md | 298 ++++++++++ docs/tools/acp-agents.md | 4 +- extensions/acpx/src/codex-auth-bridge.test.ts | 90 ++- extensions/acpx/src/codex-auth-bridge.ts | 114 +++- extensions/acpx/src/codex-trust-config.ts | 181 ++++++ extensions/acpx/src/process-lease.test.ts | 36 ++ extensions/acpx/src/process-lease.ts | 169 ++++++ extensions/acpx/src/process-reaper.test.ts | 262 +++++++++ extensions/acpx/src/process-reaper.ts | 381 +++++++++++++ extensions/acpx/src/runtime.test.ts | 523 +++++++++++++++++- extensions/acpx/src/runtime.ts | 424 ++++++++++++-- extensions/acpx/src/service.test.ts | 148 +++++ extensions/acpx/src/service.ts | 102 ++++ src/agents/tools/cron-tool.ts | 17 +- src/agents/tools/sessions-access.test.ts | 170 ++++++ src/agents/tools/sessions-access.ts | 2 + src/agents/tools/sessions-helpers.ts | 1 + src/agents/tools/sessions-list-tool.ts | 16 +- src/agents/tools/sessions-send-tool.ts | 18 +- src/agents/tools/sessions.test.ts | 86 ++- src/plugin-sdk/session-visibility.ts | 69 ++- 28 files changed, 3353 insertions(+), 95 deletions(-) create mode 100644 docs/plugins/cli-backend-plugins.md create mode 100644 docs/refactor/acp.md create mode 100644 extensions/acpx/src/codex-trust-config.ts create mode 100644 extensions/acpx/src/process-lease.test.ts create mode 100644 extensions/acpx/src/process-lease.ts create mode 100644 extensions/acpx/src/process-reaper.test.ts create mode 100644 extensions/acpx/src/process-reaper.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f6210640f80..09ef18320e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ Docs: https://docs.openclaw.ai - Codex/approvals: in Codex approval modes, stop installing the pre-guardian native `PermissionRequest` hook by default so Codex's reviewer can approve safe commands before OpenClaw surfaces an approval, remember `allow-always` decisions for identical Codex native `PermissionRequest` payloads within the active session window, and make plugin approval requests validate/render their actual allowed decisions so Telegram and other native approval UIs cannot offer stale actions. Thanks @shakkernerd. - PR triage: mark external pull requests with `proof: supplied` when Barnacle finds structured real behavior proof, keep stale negative proof labels in sync across CRLF-edited PR bodies, and let ClawSweeper own the stronger `proof: sufficient` judgement. - Sessions CLI: show the selected agent runtime in the `openclaw sessions` table so terminal output matches the runtime visibility already present in JSON/status surfaces. Thanks @vincentkoc. +- ACPX/Codex: preserve trusted Codex project declarations when launching isolated Codex ACP sessions, avoiding interactive trust prompts in headless runs. Thanks @Stedyclaw. +- ACPX/Codex: reap stale OpenClaw-owned ACPX/Codex ACP process trees on startup and after ACP session close, preventing orphaned harness processes from slowing the Gateway. Thanks @91wan. +- ACP sessions: allow parent agents to inspect and message their own spawned cross-agent ACP sessions without enabling broad agent-to-agent visibility. Thanks @barronlroth. - Talk/voice: unify realtime relay, transcription relay, managed-room handoff, Voice Call, Google Meet, VoiceClaw, and native clients around a shared Talk session controller and add the Gateway-managed `talk.session.*` RPC surface. - Diagnostics/Talk: export bounded Talk lifecycle/audio metrics and session recovery metrics through OpenTelemetry and Prometheus without exposing transcripts, audio payloads, room ids, turn ids, or session ids. - Logging/Talk: route shared Talk lifecycle events into bounded file and OTLP log records while keeping transcript text, audio payloads, turn ids, call ids, and provider item ids out of logs. diff --git a/docs/.i18n/glossary.zh-CN.json b/docs/.i18n/glossary.zh-CN.json index 98af41ede60..91a1508c089 100644 --- a/docs/.i18n/glossary.zh-CN.json +++ b/docs/.i18n/glossary.zh-CN.json @@ -31,6 +31,10 @@ "source": "Message lifecycle refactor", "target": "消息生命周期重构" }, + { + "source": "ACP lifecycle refactor", + "target": "ACP 生命周期重构" + }, { "source": "Channel message API", "target": "频道消息 API" diff --git a/docs/docs.json b/docs/docs.json index 6dd32dfe62a..59679e162d0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1208,6 +1208,7 @@ "plugins/sdk-channel-plugins", "plugins/sdk-channel-message", "plugins/sdk-provider-plugins", + "plugins/cli-backend-plugins", "plugins/adding-capabilities", "plugins/compatibility", "plugins/sdk-migration" diff --git a/docs/gateway/cli-backends.md b/docs/gateway/cli-backends.md index c754064b33f..9b7056a7afa 100644 --- a/docs/gateway/cli-backends.md +++ b/docs/gateway/cli-backends.md @@ -23,6 +23,12 @@ If you want a full harness runtime with ACP session controls, background tasks, thread/conversation binding, and persistent external coding sessions, use [ACP Agents](/tools/acp-agents) instead. CLI backends are not ACP. + + Building a new backend plugin? Use + [CLI backend plugins](/plugins/cli-backend-plugins). This page is for users + configuring and operating an already registered backend. + + ## Beginner-friendly quick start You can use Codex CLI **without any config** (the bundled OpenAI plugin diff --git a/docs/plugins/building-plugins.md b/docs/plugins/building-plugins.md index ab35a819640..efd88963ffc 100644 --- a/docs/plugins/building-plugins.md +++ b/docs/plugins/building-plugins.md @@ -35,6 +35,9 @@ install from npm during the launch cutover. Add a model provider (LLM, proxy, or custom endpoint) + + Map a local AI CLI into OpenClaw's text fallback runner + Register agent tools, event hooks, or services - continue below @@ -160,7 +163,7 @@ A single plugin can register any number of capabilities via the `api` object: | Capability | Registration method | Detailed guide | | ---------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------- | | Text inference (LLM) | `api.registerProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins) | -| CLI inference backend | `api.registerCliBackend(...)` | [CLI Backends](/gateway/cli-backends) | +| CLI inference backend | `api.registerCliBackend(...)` | [CLI Backend Plugins](/plugins/cli-backend-plugins) | | Channel / messaging | `api.registerChannel(...)` | [Channel Plugins](/plugins/sdk-channel-plugins) | | Speech (TTS/STT) | `api.registerSpeechProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) | | Realtime transcription | `api.registerRealtimeTranscriptionProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) | @@ -382,6 +385,9 @@ reserved surfaces, not as the default pattern for new third-party plugins. Build a model provider plugin + + Register a local AI CLI backend + Import map and registration API reference diff --git a/docs/plugins/cli-backend-plugins.md b/docs/plugins/cli-backend-plugins.md new file mode 100644 index 00000000000..43a2c1929f8 --- /dev/null +++ b/docs/plugins/cli-backend-plugins.md @@ -0,0 +1,310 @@ +--- +summary: "Build a plugin that registers a local AI CLI backend" +title: "Building CLI backend plugins" +sidebarTitle: "CLI backend plugins" +read_when: + - You are building a local AI CLI backend plugin + - You want to register a backend for model refs such as acme-cli/model + - You need to map a third-party CLI into OpenClaw's text fallback runner +--- + +CLI backend plugins let OpenClaw call a local AI CLI as a text inference +backend. The backend appears as a provider prefix in model refs: + +```text +acme-cli/acme-large +``` + +Use a CLI backend when the upstream integration is already exposed as a local +command, when the CLI owns local login state, or when the CLI is a useful +fallback if API providers are unavailable. + + + If the upstream service exposes a normal HTTP model API, write a + [provider plugin](/plugins/sdk-provider-plugins) instead. If the upstream + runtime owns complete agent sessions, tool events, compaction, or background + task state, use an [agent harness](/plugins/sdk-agent-harness). + + +## What the plugin owns + +A CLI backend plugin has three contracts: + +| Contract | File | Purpose | +| -------------------- | ---------------------- | --------------------------------------------------------- | +| Package entry | `package.json` | Points OpenClaw at the plugin runtime module | +| Manifest ownership | `openclaw.plugin.json` | Declares the backend id before runtime loads | +| Runtime registration | `index.ts` | Calls `api.registerCliBackend(...)` with command defaults | + +The manifest is discovery metadata. It does not execute the CLI and does not +register runtime behavior. Runtime behavior starts when the plugin entry calls +`api.registerCliBackend(...)`. + +## Minimal backend plugin + + + + ```json package.json + { + "name": "@acme/openclaw-acme-cli", + "version": "1.0.0", + "type": "module", + "openclaw": { + "extensions": ["./index.ts"], + "compat": { + "pluginApi": ">=2026.3.24-beta.2", + "minGatewayVersion": "2026.3.24-beta.2" + }, + "build": { + "openclawVersion": "2026.3.24-beta.2", + "pluginSdkVersion": "2026.3.24-beta.2" + } + }, + "dependencies": { + "openclaw": "^2026.3.24" + }, + "devDependencies": { + "typescript": "^5.9.0" + } + } + ``` + + Published packages must ship built JavaScript runtime files. If your source + entry is `./src/index.ts`, add `openclaw.runtimeExtensions` that points at + the built JavaScript peer. See [Entry points](/plugins/sdk-entrypoints). + + + + + ```json openclaw.plugin.json + { + "id": "acme-cli", + "name": "Acme CLI", + "description": "Run Acme's local AI CLI through OpenClaw", + "cliBackends": ["acme-cli"], + "setup": { + "cliBackends": ["acme-cli"], + "requiresRuntime": false + }, + "activation": { + "onStartup": false + }, + "configSchema": { + "type": "object", + "additionalProperties": false + } + } + ``` + + `cliBackends` is the runtime ownership list. It lets OpenClaw auto-load the + plugin when config or model selection mentions `acme-cli/...`. + + `setup.cliBackends` is the descriptor-first setup surface. Add it when + model discovery, onboarding, or status should recognize the backend without + loading plugin runtime. Use `requiresRuntime: false` only when those static + descriptors are enough for setup. + + + + + ```typescript index.ts + import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; + import { + CLI_FRESH_WATCHDOG_DEFAULTS, + CLI_RESUME_WATCHDOG_DEFAULTS, + type CliBackendPlugin, + } from "openclaw/plugin-sdk/cli-backend"; + + function buildAcmeCliBackend(): CliBackendPlugin { + return { + id: "acme-cli", + liveTest: { + defaultModelRef: "acme-cli/acme-large", + defaultImageProbe: false, + defaultMcpProbe: false, + docker: { + npmPackage: "@acme/acme-cli", + binaryName: "acme", + }, + }, + config: { + command: "acme", + args: ["chat", "--json"], + output: "json", + input: "stdin", + modelArg: "--model", + sessionArg: "--session", + sessionMode: "existing", + sessionIdFields: ["session_id", "conversation_id"], + systemPromptFileArg: "--system-file", + systemPromptWhen: "first", + imageArg: "--image", + imageMode: "repeat", + reliability: { + watchdog: { + fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS }, + resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS }, + }, + }, + serialize: true, + }, + }; + } + + export default definePluginEntry({ + id: "acme-cli", + name: "Acme CLI", + description: "Run Acme's local AI CLI through OpenClaw", + register(api) { + api.registerCliBackend(buildAcmeCliBackend()); + }, + }); + ``` + + The backend id must match the manifest `cliBackends` entry. The registered + `config` is only the default; user config under + `agents.defaults.cliBackends.acme-cli` is merged over it at runtime. + + + + +## Config shape + +`CliBackendConfig` describes how OpenClaw should launch and parse the CLI: + +| Field | Use | +| ----------------------------------------- | ----------------------------------------------------------- | +| `command` | Binary name or absolute command path | +| `args` | Base argv for fresh runs | +| `resumeArgs` | Alternate argv for resumed sessions; supports `{sessionId}` | +| `output` / `resumeOutput` | Parser: `json`, `jsonl`, or `text` | +| `input` | Prompt transport: `arg` or `stdin` | +| `modelArg` | Flag used before the model id | +| `modelAliases` | Map OpenClaw model ids to CLI-native ids | +| `sessionArg` / `sessionArgs` | How to pass a session id | +| `sessionMode` | `always`, `existing`, or `none` | +| `sessionIdFields` | JSON fields OpenClaw reads from CLI output | +| `systemPromptArg` / `systemPromptFileArg` | System prompt transport | +| `systemPromptWhen` | `first`, `always`, or `never` | +| `imageArg` / `imageMode` | Image path support | +| `serialize` | Keep same-backend runs ordered | +| `reliability.watchdog` | No-output timeout tuning | + +Prefer the smallest static config that matches the CLI. Add plugin callbacks +only for behavior that really belongs to the backend. + +## Advanced backend hooks + +`CliBackendPlugin` can also define: + +| Hook | Use | +| ---------------------------------- | ------------------------------------------------------ | +| `normalizeConfig(config, context)` | Rewrite legacy user config after merge | +| `resolveExecutionArgs(ctx)` | Add request-scoped flags such as thinking effort | +| `prepareExecution(ctx)` | Create temporary auth or config bridges before launch | +| `transformSystemPrompt(ctx)` | Apply a final CLI-specific system prompt transform | +| `textTransforms` | Bidirectional prompt/output replacements | +| `defaultAuthProfileId` | Prefer a specific OpenClaw auth profile | +| `authEpochMode` | Decide how auth changes invalidate stored CLI sessions | +| `nativeToolMode` | Declare whether the CLI has always-on native tools | +| `bundleMcp` / `bundleMcpMode` | Opt into OpenClaw's loopback MCP tool bridge | + +Keep these hooks provider-owned. Do not add CLI-specific branches to core when a +backend hook can express the behavior. + +## MCP tool bridge + +CLI backends do not receive OpenClaw tools by default. If the CLI can consume an +MCP configuration, opt in explicitly: + +```typescript +return { + id: "acme-cli", + bundleMcp: true, + bundleMcpMode: "codex-config-overrides", + config: { + command: "acme", + args: ["chat", "--json"], + output: "json", + }, +}; +``` + +Supported bridge modes are: + +| Mode | Use | +| ------------------------ | ---------------------------------------------------------------- | +| `claude-config-file` | CLIs that accept an MCP config file | +| `codex-config-overrides` | CLIs that accept config overrides on argv | +| `gemini-system-settings` | CLIs that read MCP settings from their system settings directory | + +Only enable the bridge when the CLI can actually consume it. If the CLI has its +own built-in tool layer that cannot be disabled, set `nativeToolMode: +"always-on"` so OpenClaw can fail closed when a caller requires no native tools. + +## User configuration + +Users can override any backend default: + +```json5 +{ + agents: { + defaults: { + cliBackends: { + "acme-cli": { + command: "/opt/acme/bin/acme", + args: ["chat", "--json", "--profile", "work"], + modelAliases: { + large: "acme-large-2026", + }, + }, + }, + model: { + primary: "openai/gpt-5.5", + fallbacks: ["acme-cli/large"], + }, + }, + }, +} +``` + +Document the minimum override users are likely to need. Usually that is only +`command` when the binary is outside `PATH`. + +## Verification + +For bundled plugins, add a focused test around the builder and setup +registration, then run the plugin's targeted test lane: + +```bash +pnpm test extensions/acme-cli +``` + +For local or installed plugins, verify discovery and one real model run: + +```bash +openclaw plugins inspect acme-cli --runtime --json +openclaw agent --message "reply exactly: backend ok" --model acme-cli/acme-large +``` + +If the backend supports images or MCP, add a live smoke that proves those paths +with the real CLI. Do not rely on static inspection for prompt, image, MCP, or +session-resume behavior. + +## Checklist + +`package.json` has `openclaw.extensions` and built runtime entries for published packages +`openclaw.plugin.json` declares `cliBackends` and intentional `activation.onStartup` +`setup.cliBackends` is present when setup/model discovery should see the backend cold +`api.registerCliBackend(...)` uses the same backend id as the manifest +User overrides under `agents.defaults.cliBackends.` still win +Session, system prompt, image, and output parser settings match the real CLI contract +Targeted tests and at least one live CLI smoke prove the backend path + +## Related + +- [CLI backends](/gateway/cli-backends) - user configuration and runtime behavior +- [Building plugins](/plugins/building-plugins) - package and manifest basics +- [Plugin SDK overview](/plugins/sdk-overview) - registration API reference +- [Plugin manifest](/plugins/manifest) - `cliBackends` and setup descriptors +- [Agent harness](/plugins/sdk-agent-harness) - full external agent runtimes diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index b3a253d1b3c..5c237ba6a98 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -20,7 +20,7 @@ reference for **what to import** and **what you can register**. -Looking for a how-to guide instead? Start with [Building plugins](/plugins/building-plugins), use [Channel plugins](/plugins/sdk-channel-plugins) for channel plugins, [Provider plugins](/plugins/sdk-provider-plugins) for provider plugins, and [Plugin hooks](/plugins/hooks) for tool or lifecycle hook plugins. +Looking for a how-to guide instead? Start with [Building plugins](/plugins/building-plugins), use [Channel plugins](/plugins/sdk-channel-plugins) for channel plugins, [Provider plugins](/plugins/sdk-provider-plugins) for provider plugins, [CLI backend plugins](/plugins/cli-backend-plugins) for local AI CLI backends, and [Plugin hooks](/plugins/hooks) for tool or lifecycle hook plugins. ## Import convention @@ -261,6 +261,9 @@ AI CLI backend such as `codex-cli`. the CLI dialect, such as mapping OpenClaw thinking levels to a native effort flag. +For an end-to-end authoring guide, see +[CLI backend plugins](/plugins/cli-backend-plugins). + ### Exclusive slots | Method | What it registers | diff --git a/docs/refactor/acp.md b/docs/refactor/acp.md new file mode 100644 index 00000000000..2c9fa2e15b5 --- /dev/null +++ b/docs/refactor/acp.md @@ -0,0 +1,298 @@ +--- +summary: "Migration plan for making ACP session and ACPX process ownership explicit" +read_when: + - Refactoring ACP session lifecycle or ACPX process cleanup + - Debugging ACPX orphan processes, PID reuse, or multi-gateway cleanup safety + - Changing sessions_list visibility for spawned ACP or subagent sessions + - Designing ownership metadata for background tasks, ACP sessions, or process leases +title: "ACP lifecycle refactor" +sidebarTitle: "ACP lifecycle refactor" +--- + +ACP lifecycle currently works, but too much of it is inferred after the fact. +Process cleanup reconstructs ownership from PIDs, command strings, wrapper +paths, and the live process table. Session visibility reconstructs ownership +from session-key strings plus secondary `sessions.list({ spawnedBy })` lookups. +That makes narrow fixes possible, but it also makes edge cases easy to miss: +PID reuse, quoted commands, adapter grandchildren, multi-gateway state roots, +`cancel` versus `close`, and `tree` versus `all` visibility all become separate +places to rediscover the same ownership rules. + +This refactor makes ownership first-class. The goal is not a new ACP product +surface; it is a safer internal contract for the existing ACP and ACPX behavior. + +## Goals + +- Cleanup never signals a process unless current live evidence matches an + OpenClaw-owned lease. +- `cancel`, `close`, and startup reaping have distinct lifecycle intents. +- `sessions_list`, `sessions_history`, `sessions_send`, and status checks use + the same requester-owned session model. +- Multi-gateway installs cannot reap each other's ACPX wrappers. +- Old ACPX session records keep working during migration. +- The runtime remains plugin-owned; core does not learn ACPX package details. + +## Non-goals + +- Replacing ACPX or changing the public `/acp` command surface. +- Moving vendor-specific ACP adapter behavior into core. +- Requiring users to manually clean state before upgrading. +- Making `cancel` close reusable ACP sessions. + +## Target Model + +### Gateway Instance Identity + +Each Gateway process should have a stable runtime instance id: + +```ts +type GatewayInstanceId = string; +``` + +It can be generated on Gateway startup and persisted in state for the life of +that install. It is not a security secret; it is an ownership discriminator used +to avoid confusing one Gateway's ACP processes with another Gateway's processes. + +### ACP Session Ownership + +Every spawned ACP session should have normalized ownership metadata: + +```ts +type AcpSessionOwner = { + sessionKey: string; + spawnedBy?: string; + parentSessionKey?: string; + ownerSessionKey: string; + agentId: string; + backend: "acpx"; + gatewayInstanceId: GatewayInstanceId; + createdAt: number; +}; +``` + +The Gateway should return these fields on session rows where they are known. +Visibility filtering should be a pure check over row metadata: + +```ts +canSeeSessionRow({ + row, + requesterSessionKey, + visibility, + a2aPolicy, +}); +``` + +That removes hidden secondary `sessions.list({ spawnedBy })` calls from +visibility checks. A spawned cross-agent ACP child is requester-owned because +the row says so, not because a second query happens to find it. + +### ACPX Process Leases + +Every generated wrapper launch should create a lease record: + +```ts +type AcpxProcessLease = { + leaseId: string; + gatewayInstanceId: GatewayInstanceId; + sessionKey: string; + wrapperRoot: string; + wrapperPath: string; + rootPid: number; + processGroupId?: number; + commandHash: string; + startedAt: number; + state: "open" | "closing" | "closed" | "lost"; +}; +``` + +The wrapper process should receive the lease id and gateway instance id in its +environment: + +```sh +OPENCLAW_ACPX_LEASE_ID=... +OPENCLAW_GATEWAY_INSTANCE_ID=... +``` + +When the platform allows it, verification should prefer live process metadata +that cannot be confused by command quoting: + +- root PID still exists +- live wrapper path is under `wrapperRoot` +- process group matches the lease when available +- environment contains the expected lease id when readable +- command hash or executable path matches the lease + +If the live process cannot be verified, cleanup fails closed. + +## Lifecycle Controller + +Introduce one ACPX lifecycle controller that owns process leases and cleanup +policy: + +```ts +interface AcpxLifecycleController { + ensureSession(input: AcpRuntimeEnsureInput): Promise; + cancelTurn(handle: AcpRuntimeHandle): Promise; + closeSession(input: { + handle: AcpRuntimeHandle; + discardPersistentState?: boolean; + reason?: string; + }): Promise; + reapStartupOrphans(): Promise; + verifyOwnedTree(lease: AcpxProcessLease): Promise; +} +``` + +`cancelTurn` requests turn cancellation only. It must not reap reusable wrapper +or adapter processes. + +`closeSession` is allowed to reap, but only after loading the session record, +loading the lease, and verifying the live process tree still belongs to that +lease. + +`reapStartupOrphans` starts from open leases in state. It may use the process +table to find descendants, but it should not scan arbitrary ACP-looking +commands first and then decide they are probably ours. + +## Wrapper Contract + +Generated wrappers should stay small. They should: + +- start the adapter in a process group where supported +- forward normal termination signals to the process group +- detect parent death +- on parent death, send SIGTERM, then keep the wrapper alive until the SIGKILL + fallback runs +- report root PID and process group id back to the lifecycle controller when + that is available + +Wrappers should not decide session policy. They only enforce local process-tree +cleanup for their own adapter group. + +## Session Visibility Contract + +Visibility should use normalized row ownership: + +```ts +type SessionVisibilityInput = { + requesterSessionKey: string; + row: { + key: string; + agentId: string; + ownerSessionKey?: string; + spawnedBy?: string; + parentSessionKey?: string; + }; + visibility: "self" | "tree" | "agent" | "all"; + a2aPolicy: AgentToAgentPolicy; +}; +``` + +Rules: + +- `self`: only the requester session. +- `tree`: requester session plus rows owned by or spawned from the requester. +- `all`: all same-agent rows, a2a-allowed cross-agent rows, and requester-owned + spawned cross-agent rows even when general a2a is disabled. +- `agent`: same agent only, unless an explicit owner relationship says the row + belongs to the requester. + +This makes `tree` and `all` monotonic: `all` must not hide an owned child that +`tree` would show. + +## Migration Plan + +### Phase 1: Add Identity And Leases + +- Add `gatewayInstanceId` to Gateway state. +- Add an ACPX lease store under the ACPX state directory. +- Write a lease before spawning a generated wrapper. +- Store `leaseId` on new ACPX session records. +- Keep existing PID and command fields for old records. + +### Phase 2: Lease-First Cleanup + +- Change close cleanup to load `leaseId` first. +- Verify live process ownership against the lease before signaling. +- Keep the current root PID and wrapper-root fallback only for legacy records. +- Mark leases `closed` after verified cleanup. +- Mark leases `lost` when the process is gone before cleanup. + +### Phase 3: Lease-First Startup Reaping + +- Startup reaping scans open leases. +- For each lease, verify the root process and collect descendants. +- Reap verified trees children-first. +- Expire old `closed` and `lost` leases with a bounded retention window. +- Keep command-marker scanning only as a temporary legacy fallback, guarded by + wrapper root and Gateway instance where possible. + +### Phase 4: Session Ownership Rows + +- Add ownership metadata to Gateway session rows. +- Teach ACPX, subagent, background-task, and session-store writers to populate + `ownerSessionKey` or `spawnedBy`. +- Convert session visibility checks to use row metadata. +- Remove visibility-time secondary `sessions.list({ spawnedBy })` lookups. + +### Phase 5: Remove Legacy Heuristics + +After one release window: + +- stop relying on stored root command strings for non-legacy ACPX cleanup +- remove command-marker startup scans +- remove visibility fallback list lookups +- keep defensive fail-closed behavior for missing or unverifiable leases + +## Tests + +Add two table-driven suites. + +Process lifecycle simulator: + +- PID reused by unrelated process +- PID reused by another Gateway's wrapper root +- stored wrapper command is shell-quoted, live `ps` command is not +- adapter child exits, grandchild remains in the process group +- parent death SIGTERM fallback reaches SIGKILL +- process listing unavailable +- stale lease with missing process +- startup orphan with wrapper, adapter child, and grandchild + +Session visibility matrix: + +- `self`, `tree`, `agent`, `all` +- a2a enabled and disabled +- same-agent row +- cross-agent row +- requester-owned spawned cross-agent ACP row +- sandboxed requester clamped to `tree` +- list, history, send, and status actions + +The important invariant: a requester-owned spawned child is visible wherever +the configured visibility includes the requester session tree, and `all` is not +less capable than `tree`. + +## Compatibility Notes + +Old session records may not have `leaseId`. They should use the legacy +fail-closed cleanup path: + +- require a live root process +- require wrapper-root ownership when a generated wrapper is expected +- require command agreement for non-wrapper roots +- never signal based only on stale stored PID metadata + +If a legacy record cannot be verified, leave it alone. Startup lease cleanup and +the next release window should eventually retire the fallback. + +## Success Criteria + +- Closing an old or stale ACPX session cannot kill another Gateway's process. +- Parent death does not leave stubborn adapter grandchildren running. +- `cancel` aborts the active turn without closing reusable sessions. +- `sessions_list` can show requester-owned cross-agent ACP children under both + `tree` and `all`. +- Startup cleanup is driven by leases, not broad command-string scans. +- The focused process and visibility matrix tests cover every edge case that + previously required one-off review fixes. diff --git a/docs/tools/acp-agents.md b/docs/tools/acp-agents.md index 8726b636c06..bec9d0b618d 100644 --- a/docs/tools/acp-agents.md +++ b/docs/tools/acp-agents.md @@ -60,6 +60,7 @@ an unavailable backend. - If `plugins.allow` is set, it is a restrictive plugin inventory and **must** include `acpx`; otherwise the installed ACP backend is intentionally blocked and `/acp doctor` reports the missing allowlist entry. - The Codex ACP adapter is staged with the `acpx` plugin and launched locally when possible. + - Codex ACP runs with an isolated `CODEX_HOME`; OpenClaw copies only trusted project entries from the host Codex config and trusts the active workspace, leaving auth, notifications, and hooks on the host config. - Other target harness adapters may still be fetched on demand with `npx` the first time you use them. - Vendor auth still has to exist on the host for that harness. - If the host has no npm or network access, first-run adapter fetches fail until caches are pre-warmed or the adapter is installed another way. @@ -154,6 +155,7 @@ Quick `/acp` flow from chat: - Gateway commands stay local. `/acp ...`, `/status`, and `/unfocus` are never sent as normal prompt text to a bound ACP harness. - `cancel` aborts the active turn when the backend supports cancellation; it does not delete the binding or session metadata. - `close` ends the ACP session from OpenClaw's point of view and removes the binding. A harness may still keep its own upstream history if it supports resume. + - The acpx plugin cleans up OpenClaw-owned wrapper and adapter process trees after `close`, and reaps stale OpenClaw-owned ACPX orphans during Gateway startup. - Idle runtime workers are eligible for cleanup after `acp.runtime.ttlMinutes`; stored session metadata remains available for `/acp sessions`. @@ -830,7 +832,7 @@ permission modes, see | Missing ACP metadata for bound session | Stale/deleted ACP session metadata. | Recreate with `/acp spawn`, then rebind/focus thread. | | `AcpRuntimeError: Permission prompt unavailable in non-interactive mode` | `permissionMode` blocks writes/exec in non-interactive ACP session. | Set `plugins.entries.acpx.config.permissionMode` to `approve-all` and restart gateway. See [Permission configuration](/tools/acp-agents-setup#permission-configuration). | | ACP session fails early with little output | Permission prompts are blocked by `permissionMode`/`nonInteractivePermissions`. | Check gateway logs for `AcpRuntimeError`. For full permissions, set `permissionMode=approve-all`; for graceful degradation, set `nonInteractivePermissions=deny`. | -| ACP session stalls indefinitely after completing work | Harness process finished but ACP session did not report completion. | Monitor with `ps aux \| grep acpx`; kill stale processes manually. | +| ACP session stalls indefinitely after completing work | Harness process finished but ACP session did not report completion. | Update OpenClaw; current acpx cleanup reaps OpenClaw-owned stale wrapper and adapter processes on close and Gateway startup. | | Harness sees `<<>>` | Internal event envelope leaked across the ACP boundary. | Update OpenClaw and rerun the completion flow; external harnesses should receive plain completion prompts only. | ## Related diff --git a/extensions/acpx/src/codex-auth-bridge.test.ts b/extensions/acpx/src/codex-auth-bridge.test.ts index 3dfee4a1401..18176d39259 100644 --- a/extensions/acpx/src/codex-auth-bridge.test.ts +++ b/extensions/acpx/src/codex-auth-bridge.test.ts @@ -210,6 +210,34 @@ describe("prepareAcpxCodexAuthConfig", () => { expect(wrapper).toContain("defaultArgs = [installedBinPath]"); }); + it("keeps the orphaned wrapper alive long enough to force-kill the child process group", async () => { + const root = await makeTempDir(); + const stateDir = path.join(root, "state"); + const generated = generatedCodexPaths(stateDir); + const pluginConfig = resolveAcpxPluginConfig({ + rawConfig: {}, + workspaceDir: root, + }); + + await prepareAcpxCodexAuthConfig({ + pluginConfig, + stateDir, + }); + + const wrapper = await fs.readFile(generated.wrapperPath, "utf8"); + expect(wrapper).toContain('killChildTree("SIGTERM")'); + expect(wrapper).toContain('killChildTree("SIGKILL", { force: true })'); + expect(wrapper).toMatch( + /forceKillTimer = setTimeout\(\(\) => \{\s*killChildTree\("SIGKILL", \{ force: true \}\);\s*process\.exit\(1\);/s, + ); + expect(wrapper).toMatch( + /child\.on\("exit", \(code, signal\) => \{\s*if \(parentWatcher\) \{\s*clearInterval\(parentWatcher\);\s*\}\s*if \(orphanCleanupStarted\) \{\s*return;\s*\}/s, + ); + expect(wrapper).not.toMatch( + /forceKillTimer = setTimeout\(\(\) => killChildTree\("SIGKILL"\), 1_500\);\s*forceKillTimer\.unref\?\.\(\);\s*process\.exit\(1\);/s, + ); + }); + it("uses the bundled Claude ACP dependency by default when it is installed", async () => { const root = await makeTempDir(); const stateDir = path.join(root, "state"); @@ -251,9 +279,19 @@ describe("prepareAcpxCodexAuthConfig", () => { resolveInstalledCodexAcpBinPath: async () => installedBinPath, }); - const { stdout } = await execFileAsync(process.execPath, [generated.wrapperPath], { - cwd: root, - }); + const { stdout } = await execFileAsync( + process.execPath, + [ + generated.wrapperPath, + "--openclaw-acpx-lease-id", + "lease-1", + "--openclaw-gateway-instance-id", + "gateway-1", + ], + { + cwd: root, + }, + ); const launched = JSON.parse(stdout.trim()) as { argv?: unknown; codexHome?: unknown }; expect(launched.argv).toEqual([]); const expectedCodexHome = await fs.realpath(path.join(stateDir, "acpx", "codex-home")); @@ -326,6 +364,8 @@ describe("prepareAcpxCodexAuthConfig", () => { const isolatedConfig = await fs.readFile(generated.configPath, "utf8"); expect(isolatedConfig).not.toContain("notify"); expect(isolatedConfig).not.toContain("SkyComputerUseClient"); + expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(root))}]`); + expect(isolatedConfig).toContain('trust_level = "trusted"'); const wrapper = await fs.readFile(generated.wrapperPath, "utf8"); expect(wrapper).toContain("CODEX_HOME: codexHome"); expect(wrapper).not.toContain(sourceCodexHome); @@ -337,6 +377,50 @@ describe("prepareAcpxCodexAuthConfig", () => { ).rejects.toMatchObject({ code: "ENOENT" }); }); + it("copies only trusted Codex project declarations into the isolated Codex home", async () => { + const root = await makeTempDir(); + const sourceCodexHome = path.join(root, "source-codex"); + const stateDir = path.join(root, "state"); + const explicitProject = path.join(root, "explicit project"); + const inlineProject = path.join(root, "inline-project"); + const mapProject = path.join(root, "map-project"); + const untrustedProject = path.join(root, "untrusted-project"); + const generated = generatedCodexPaths(stateDir); + await fs.mkdir(sourceCodexHome, { recursive: true }); + await fs.writeFile( + path.join(sourceCodexHome, "config.toml"), + [ + 'notify = ["SkyComputerUseClient", "turn-ended"]', + `projects = { ${JSON.stringify(mapProject)} = { trust_level = "trusted" }, ${JSON.stringify(untrustedProject)} = { trust_level = "untrusted" } }`, + "[projects]", + `${JSON.stringify(inlineProject)} = { trust_level = "trusted" }`, + `[projects.${JSON.stringify(explicitProject)}]`, + 'trust_level = "trusted"', + "", + ].join("\n"), + ); + process.env.CODEX_HOME = sourceCodexHome; + const pluginConfig = resolveAcpxPluginConfig({ + rawConfig: {}, + workspaceDir: root, + }); + + await prepareAcpxCodexAuthConfig({ + pluginConfig, + stateDir, + resolveInstalledCodexAcpBinPath: async () => undefined, + }); + + const isolatedConfig = await fs.readFile(generated.configPath, "utf8"); + expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(root))}]`); + expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(explicitProject))}]`); + expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(inlineProject))}]`); + expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(mapProject))}]`); + expect(isolatedConfig).not.toContain(untrustedProject); + expect(isolatedConfig).not.toContain("notify"); + expect(isolatedConfig).not.toContain("SkyComputerUseClient"); + }); + it("normalizes an explicitly configured Codex ACP command to the local wrapper", async () => { const root = await makeTempDir(); const sourceCodexHome = path.join(root, "source-codex"); diff --git a/extensions/acpx/src/codex-auth-bridge.ts b/extensions/acpx/src/codex-auth-bridge.ts index 9b322fa3466..4f76e661037 100644 --- a/extensions/acpx/src/codex-auth-bridge.ts +++ b/extensions/acpx/src/codex-auth-bridge.ts @@ -1,10 +1,16 @@ import fsSync from "node:fs"; import fs from "node:fs/promises"; import { createRequire } from "node:module"; +import os from "node:os"; import path from "node:path"; import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store"; +import { + extractTrustedCodexProjectPaths, + renderIsolatedCodexProjectTrustConfig, +} from "./codex-trust-config.js"; import { resolveAcpxPluginRoot } from "./config.js"; import type { ResolvedAcpxPluginConfig } from "./config.js"; +import { OPENCLAW_ACPX_LEASE_ID_ARG, OPENCLAW_GATEWAY_INSTANCE_ID_ARG } from "./process-lease.js"; const CODEX_ACP_PACKAGE = "@zed-industries/codex-acp"; const CODEX_ACP_BIN = "codex-acp"; @@ -156,7 +162,25 @@ import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; ${params.envSetup} -const configuredArgs = process.argv.slice(2); +const openClawWrapperArgs = new Set([ + ${quoteCommandPart(OPENCLAW_ACPX_LEASE_ID_ARG)}, + ${quoteCommandPart(OPENCLAW_GATEWAY_INSTANCE_ID_ARG)}, +]); + +function stripOpenClawWrapperArgs(args) { + const stripped = []; + for (let index = 0; index < args.length; index += 1) { + const value = args[index]; + if (openClawWrapperArgs.has(value)) { + index += 1; + continue; + } + stripped.push(value); + } + return stripped; +} + +const configuredArgs = stripOpenClawWrapperArgs(process.argv.slice(2)); function resolveNpmCliPath() { const candidate = path.resolve( @@ -198,23 +222,78 @@ if (!command) { } const child = spawn(command, args, { + detached: process.platform !== "win32", env, stdio: "inherit", windowsHide: true, }); +let forceKillTimer; +let orphanCleanupStarted = false; + +function killChildTree(signal, options = {}) { + if (!child.pid || (!options.force && child.killed)) { + return; + } + if (process.platform !== "win32") { + try { + // The adapter can spawn grandchildren; signaling the process group keeps + // the generated wrapper from leaving an ACP tree behind. + process.kill(-child.pid, signal); + return; + } catch { + // Fall back to direct child signaling below. + } + } + child.kill(signal); +} + for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { process.once(signal, () => { - child.kill(signal); + killChildTree(signal); }); } +const originalParentPid = process.ppid; +const parentWatcher = + process.platform === "win32" + ? undefined + : setInterval(() => { + if (process.ppid === originalParentPid || process.ppid !== 1) { + return; + } + if (orphanCleanupStarted) { + return; + } + orphanCleanupStarted = true; + if (parentWatcher) { + clearInterval(parentWatcher); + } + killChildTree("SIGTERM"); + // Keep the wrapper alive long enough for stubborn adapters to receive + // a forced fallback signal after SIGTERM. + forceKillTimer = setTimeout(() => { + killChildTree("SIGKILL", { force: true }); + process.exit(1); + }, 1_500); + }, 1_000); +parentWatcher?.unref?.(); + child.on("error", (error) => { console.error(\`[openclaw] failed to launch ${params.displayName} ACP wrapper: \${error.message}\`); process.exit(1); }); child.on("exit", (code, signal) => { + if (parentWatcher) { + clearInterval(parentWatcher); + } + if (orphanCleanupStarted) { + return; + } + if (forceKillTimer) { + clearTimeout(forceKillTimer); + } if (code !== null) { process.exit(code); } @@ -250,12 +329,32 @@ function buildClaudeAcpWrapperScript(installedBinPath?: string): string { }); } -async function prepareIsolatedCodexHome(baseDir: string): Promise { - const codexHome = path.join(baseDir, "codex-home"); +async function readSourceCodexConfig(codexHome: string): Promise { + try { + return await fs.readFile(path.join(codexHome, "config.toml"), "utf8"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return undefined; + } + throw error; + } +} + +async function prepareIsolatedCodexHome(params: { + baseDir: string; + workspaceDir: string; +}): Promise { + const sourceCodexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex"); + const sourceConfig = await readSourceCodexConfig(sourceCodexHome); + const trustedProjectPaths = [ + ...(sourceConfig ? extractTrustedCodexProjectPaths(sourceConfig) : []), + params.workspaceDir, + ]; + const codexHome = path.join(params.baseDir, "codex-home"); await fs.mkdir(codexHome, { recursive: true }); await fs.writeFile( path.join(codexHome, "config.toml"), - "# Generated by OpenClaw for Codex ACP sessions.\n", + renderIsolatedCodexProjectTrustConfig(trustedProjectPaths), "utf8", ); return codexHome; @@ -383,7 +482,10 @@ export async function prepareAcpxCodexAuthConfig(params: { }): Promise { void params.logger; const codexBaseDir = path.join(params.stateDir, "acpx"); - await prepareIsolatedCodexHome(codexBaseDir); + await prepareIsolatedCodexHome({ + baseDir: codexBaseDir, + workspaceDir: params.pluginConfig.cwd, + }); const installedCodexBinPath = await ( params.resolveInstalledCodexAcpBinPath ?? resolveInstalledCodexAcpBinPath )(); diff --git a/extensions/acpx/src/codex-trust-config.ts b/extensions/acpx/src/codex-trust-config.ts new file mode 100644 index 00000000000..386eee640a0 --- /dev/null +++ b/extensions/acpx/src/codex-trust-config.ts @@ -0,0 +1,181 @@ +import path from "node:path"; + +function stripTomlComment(line: string): string { + let quote: "'" | '"' | null = null; + let escaping = false; + for (let index = 0; index < line.length; index += 1) { + const ch = line[index]; + if (escaping) { + escaping = false; + continue; + } + if (quote === '"' && ch === "\\") { + escaping = true; + continue; + } + if (quote) { + if (ch === quote) { + quote = null; + } + continue; + } + if (ch === "'" || ch === '"') { + quote = ch; + continue; + } + if (ch === "#") { + return line.slice(0, index); + } + } + return line; +} + +function parseTomlString(value: string): string | undefined { + const trimmed = value.trim(); + if (trimmed.startsWith('"') && trimmed.endsWith('"')) { + try { + return JSON.parse(trimmed) as string; + } catch { + return undefined; + } + } + if (trimmed.startsWith("'") && trimmed.endsWith("'")) { + return trimmed.slice(1, -1); + } + return undefined; +} + +function parseTomlDottedKey(value: string): string[] { + const parts: string[] = []; + let current = ""; + let quote: "'" | '"' | null = null; + let escaping = false; + + for (const ch of value.trim()) { + if (escaping) { + current += ch; + escaping = false; + continue; + } + if (quote === '"' && ch === "\\") { + current += ch; + escaping = true; + continue; + } + if (quote) { + current += ch; + if (ch === quote) { + quote = null; + } + continue; + } + if (ch === "'" || ch === '"') { + quote = ch; + current += ch; + continue; + } + if (ch === ".") { + parts.push(current.trim()); + current = ""; + continue; + } + current += ch; + } + if (current.trim()) { + parts.push(current.trim()); + } + return parts.map((part) => parseTomlString(part) ?? part); +} + +function parseProjectHeader(line: string): string | undefined { + const trimmed = line.trim(); + if (!trimmed.startsWith("[") || !trimmed.endsWith("]") || trimmed.startsWith("[[")) { + return undefined; + } + const parts = parseTomlDottedKey(trimmed.slice(1, -1)); + return parts.length === 2 && parts[0] === "projects" ? parts[1] : undefined; +} + +function parseTrustedInlineProjectEntries(value: string): string[] { + const trusted: string[] = []; + const entryPattern = + /(?"(?:\\.|[^"\\])*"|'[^']*'|[A-Za-z0-9_\-/.~:]+)\s*=\s*\{(?[^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g; + for (const match of value.matchAll(entryPattern)) { + const key = match.groups?.key; + const body = match.groups?.body; + if (!key || !body || !/\btrust_level\s*=\s*["']trusted["']/.test(body)) { + continue; + } + const projectPath = parseTomlString(key) ?? key.trim(); + if (projectPath) { + trusted.push(projectPath); + } + } + return trusted; +} + +export function extractTrustedCodexProjectPaths(configToml: string): string[] { + const trusted = new Set(); + let currentProjectPath: string | undefined; + let inProjectsTable = false; + + for (const rawLine of configToml.split(/\r?\n/)) { + const line = stripTomlComment(rawLine).trim(); + if (!line) { + continue; + } + if (line.startsWith("[")) { + currentProjectPath = parseProjectHeader(line); + inProjectsTable = line === "[projects]"; + continue; + } + + if (currentProjectPath && /^trust_level\s*=\s*["']trusted["']\s*$/.test(line)) { + trusted.add(currentProjectPath); + continue; + } + + const assignment = + /^(?"(?:\\.|[^"\\])*"|'[^']*'|[A-Za-z0-9_\-/.~:]+)\s*=\s*(?.+)$/.exec(line); + if (!assignment?.groups) { + continue; + } + + const key = parseTomlString(assignment.groups.key) ?? assignment.groups.key; + const value = assignment.groups.value.trim(); + if (inProjectsTable && /^\{.*\}$/.test(value)) { + if (/\btrust_level\s*=\s*["']trusted["']/.test(value) && key) { + trusted.add(key); + } + continue; + } + if (key === "projects" || inProjectsTable) { + for (const projectPath of parseTrustedInlineProjectEntries(value)) { + trusted.add(projectPath); + } + } + } + + return Array.from(trusted); +} + +export function renderIsolatedCodexProjectTrustConfig(projectPaths: string[]): string { + const normalized = Array.from( + new Set( + projectPaths + .map((projectPath) => projectPath.trim()) + .filter(Boolean) + .map((projectPath) => path.resolve(projectPath)), + ), + ).toSorted((left, right) => left.localeCompare(right)); + + return [ + "# Generated by OpenClaw for Codex ACP sessions.", + ...normalized.flatMap((projectPath) => [ + "", + `[projects.${JSON.stringify(projectPath)}]`, + 'trust_level = "trusted"', + ]), + "", + ].join("\n"); +} diff --git a/extensions/acpx/src/process-lease.test.ts b/extensions/acpx/src/process-lease.test.ts new file mode 100644 index 00000000000..e33e8ac2553 --- /dev/null +++ b/extensions/acpx/src/process-lease.test.ts @@ -0,0 +1,36 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { createAcpxProcessLeaseStore, type AcpxProcessLease } from "./process-lease.js"; + +function makeLease(index: number): AcpxProcessLease { + return { + leaseId: `lease-${index}`, + gatewayInstanceId: "gateway-test", + sessionKey: `agent:codex:acp:${index}`, + wrapperRoot: "/tmp/openclaw/acpx", + wrapperPath: "/tmp/openclaw/acpx/codex-acp-wrapper.mjs", + rootPid: 1000 + index, + commandHash: `hash-${index}`, + startedAt: index, + state: "open", + }; +} + +describe("createAcpxProcessLeaseStore", () => { + it("serializes concurrent lease saves without dropping records", async () => { + const stateDir = await mkdtemp(path.join(tmpdir(), "openclaw-acpx-leases-")); + try { + const store = createAcpxProcessLeaseStore({ stateDir }); + await Promise.all(Array.from({ length: 25 }, (_, index) => store.save(makeLease(index)))); + + const leases = await store.listOpen("gateway-test"); + expect(leases.map((lease) => lease.leaseId).toSorted()).toEqual( + Array.from({ length: 25 }, (_, index) => `lease-${index}`).toSorted(), + ); + } finally { + await rm(stateDir, { recursive: true, force: true }); + } + }); +}); diff --git a/extensions/acpx/src/process-lease.ts b/extensions/acpx/src/process-lease.ts new file mode 100644 index 00000000000..bed260e7add --- /dev/null +++ b/extensions/acpx/src/process-lease.ts @@ -0,0 +1,169 @@ +import { randomUUID, createHash } from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store"; + +export const OPENCLAW_ACPX_LEASE_ID_ENV = "OPENCLAW_ACPX_LEASE_ID"; +export const OPENCLAW_GATEWAY_INSTANCE_ID_ENV = "OPENCLAW_GATEWAY_INSTANCE_ID"; +export const OPENCLAW_ACPX_LEASE_ID_ARG = "--openclaw-acpx-lease-id"; +export const OPENCLAW_GATEWAY_INSTANCE_ID_ARG = "--openclaw-gateway-instance-id"; + +export type AcpxProcessLeaseState = "open" | "closing" | "closed" | "lost"; + +export type AcpxProcessLease = { + leaseId: string; + gatewayInstanceId: string; + sessionKey: string; + wrapperRoot: string; + wrapperPath: string; + rootPid: number; + processGroupId?: number; + commandHash: string; + startedAt: number; + state: AcpxProcessLeaseState; +}; + +export type AcpxProcessLeaseStore = { + load(leaseId: string): Promise; + listOpen(gatewayInstanceId?: string): Promise; + save(lease: AcpxProcessLease): Promise; + markState(leaseId: string, state: AcpxProcessLeaseState): Promise; +}; + +type LeaseFile = { + version: 1; + leases: AcpxProcessLease[]; +}; + +const LEASE_FILE = "process-leases.json"; + +function normalizeLease(value: unknown): AcpxProcessLease | undefined { + if (typeof value !== "object" || value === null) { + return undefined; + } + const record = value as Record; + if ( + typeof record.leaseId !== "string" || + typeof record.gatewayInstanceId !== "string" || + typeof record.sessionKey !== "string" || + typeof record.wrapperRoot !== "string" || + typeof record.wrapperPath !== "string" || + typeof record.rootPid !== "number" || + typeof record.commandHash !== "string" || + typeof record.startedAt !== "number" || + !["open", "closing", "closed", "lost"].includes(String(record.state)) + ) { + return undefined; + } + return { + leaseId: record.leaseId, + gatewayInstanceId: record.gatewayInstanceId, + sessionKey: record.sessionKey, + wrapperRoot: record.wrapperRoot, + wrapperPath: record.wrapperPath, + rootPid: record.rootPid, + ...(typeof record.processGroupId === "number" ? { processGroupId: record.processGroupId } : {}), + commandHash: record.commandHash, + startedAt: record.startedAt, + state: record.state as AcpxProcessLeaseState, + }; +} + +async function readLeaseFile(filePath: string): Promise { + const { value } = await readJsonFileWithFallback>(filePath, { + version: 1, + leases: [], + }); + const leases = Array.isArray(value.leases) + ? value.leases.map(normalizeLease).filter((lease): lease is AcpxProcessLease => !!lease) + : []; + return { version: 1, leases }; +} + +function writeLeaseFile(filePath: string, value: LeaseFile): Promise { + return writeJsonFileAtomically(filePath, value); +} + +export function createAcpxProcessLeaseStore(params: { stateDir: string }): AcpxProcessLeaseStore { + const filePath = path.join(params.stateDir, LEASE_FILE); + let updateQueue: Promise = Promise.resolve(); + + async function update( + mutator: (leases: AcpxProcessLease[]) => AcpxProcessLease[], + ): Promise { + const run = updateQueue.then(async () => { + await fs.mkdir(params.stateDir, { recursive: true }); + const current = await readLeaseFile(filePath); + await writeLeaseFile(filePath, { + version: 1, + leases: mutator(current.leases), + }); + }); + updateQueue = run.catch(() => {}); + await run; + } + + async function readCurrent(): Promise { + await updateQueue; + return await readLeaseFile(filePath); + } + + return { + async load(leaseId) { + const current = await readCurrent(); + return current.leases.find((lease) => lease.leaseId === leaseId); + }, + async listOpen(gatewayInstanceId) { + const current = await readCurrent(); + return current.leases.filter( + (lease) => + (lease.state === "open" || lease.state === "closing") && + (!gatewayInstanceId || lease.gatewayInstanceId === gatewayInstanceId), + ); + }, + async save(lease) { + await update((leases) => [ + ...leases.filter((entry) => entry.leaseId !== lease.leaseId), + lease, + ]); + }, + async markState(leaseId, state) { + await update((leases) => + leases.map((lease) => (lease.leaseId === leaseId ? { ...lease, state } : lease)), + ); + }, + }; +} + +export function createAcpxProcessLeaseId(): string { + return randomUUID(); +} + +export function hashAcpxProcessCommand(command: string): string { + return createHash("sha256").update(command).digest("hex"); +} + +function quoteEnvValue(value: string): string { + return /^[A-Za-z0-9_./:=@+-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`; +} + +export function withAcpxLeaseEnvironment(params: { + command: string; + leaseId: string; + gatewayInstanceId: string; + platform?: NodeJS.Platform; +}): string { + if ((params.platform ?? process.platform) === "win32") { + return params.command; + } + return [ + "env", + `${OPENCLAW_ACPX_LEASE_ID_ENV}=${quoteEnvValue(params.leaseId)}`, + `${OPENCLAW_GATEWAY_INSTANCE_ID_ENV}=${quoteEnvValue(params.gatewayInstanceId)}`, + params.command, + OPENCLAW_ACPX_LEASE_ID_ARG, + quoteEnvValue(params.leaseId), + OPENCLAW_GATEWAY_INSTANCE_ID_ARG, + quoteEnvValue(params.gatewayInstanceId), + ].join(" "); +} diff --git a/extensions/acpx/src/process-reaper.test.ts b/extensions/acpx/src/process-reaper.test.ts new file mode 100644 index 00000000000..80e6775e1be --- /dev/null +++ b/extensions/acpx/src/process-reaper.test.ts @@ -0,0 +1,262 @@ +import { describe, expect, it, vi } from "vitest"; +import { OPENCLAW_ACPX_LEASE_ID_ARG, OPENCLAW_GATEWAY_INSTANCE_ID_ARG } from "./process-lease.js"; +import { + cleanupOpenClawOwnedAcpxProcessTree, + isOpenClawOwnedAcpxProcessCommand, + reapStaleOpenClawOwnedAcpxOrphans, + type AcpxProcessInfo, +} from "./process-reaper.js"; + +const WRAPPER_ROOT = "/tmp/openclaw-state/acpx"; +const CODEX_WRAPPER_COMMAND = `node ${WRAPPER_ROOT}/codex-acp-wrapper.mjs`; +const CODEX_WRAPPER_COMMAND_WITH_LEASE = `${CODEX_WRAPPER_COMMAND} ${OPENCLAW_ACPX_LEASE_ID_ARG} lease-1 ${OPENCLAW_GATEWAY_INSTANCE_ID_ARG} gateway-1`; +const CLAUDE_WRAPPER_COMMAND = `node ${WRAPPER_ROOT}/claude-agent-acp-wrapper.mjs`; +const PLUGIN_DEPS_CODEX_COMMAND = + "node /tmp/openclaw/plugin-runtime-deps/node_modules/@zed-industries/codex-acp/bin/codex-acp.js"; + +function cleanupDeps(processes: AcpxProcessInfo[]) { + const killed: Array<{ pid: number; signal: NodeJS.Signals }> = []; + return { + killed, + deps: { + listProcesses: vi.fn(async () => processes), + killProcess: vi.fn((pid: number, signal: NodeJS.Signals) => { + killed.push({ pid, signal }); + }), + sleep: vi.fn(async () => {}), + }, + }; +} + +describe("process reaper", () => { + it("recognizes generated Codex and Claude wrappers only under the configured root", () => { + expect( + isOpenClawOwnedAcpxProcessCommand({ + command: CODEX_WRAPPER_COMMAND, + wrapperRoot: WRAPPER_ROOT, + }), + ).toBe(true); + expect( + isOpenClawOwnedAcpxProcessCommand({ + command: CLAUDE_WRAPPER_COMMAND, + wrapperRoot: WRAPPER_ROOT, + }), + ).toBe(true); + expect( + isOpenClawOwnedAcpxProcessCommand({ + command: "node /tmp/other/codex-acp-wrapper.mjs", + wrapperRoot: WRAPPER_ROOT, + }), + ).toBe(false); + }); + + it("recognizes OpenClaw plugin-runtime-deps ACP adapter children", () => { + expect(isOpenClawOwnedAcpxProcessCommand({ command: PLUGIN_DEPS_CODEX_COMMAND })).toBe(true); + expect(isOpenClawOwnedAcpxProcessCommand({ command: "npx @zed-industries/codex-acp" })).toBe( + false, + ); + }); + + it("kills an owned recorded process tree children first", async () => { + const { deps, killed } = cleanupDeps([ + { pid: 100, ppid: 1, command: CODEX_WRAPPER_COMMAND }, + { pid: 101, ppid: 100, command: PLUGIN_DEPS_CODEX_COMMAND }, + { pid: 102, ppid: 101, command: "node child.js" }, + ]); + + const result = await cleanupOpenClawOwnedAcpxProcessTree({ + rootPid: 100, + rootCommand: CODEX_WRAPPER_COMMAND, + wrapperRoot: WRAPPER_ROOT, + deps, + }); + + expect(result.skippedReason).toBeUndefined(); + expect(result.inspectedPids).toEqual([100, 101, 102]); + expect(killed.slice(0, 3)).toEqual([ + { pid: 102, signal: "SIGTERM" }, + { pid: 101, signal: "SIGTERM" }, + { pid: 100, signal: "SIGTERM" }, + ]); + }); + + it("allows wrapper-root verification when stored wrapper commands are shell-quoted", async () => { + const { deps, killed } = cleanupDeps([{ pid: 110, ppid: 1, command: CODEX_WRAPPER_COMMAND }]); + + const result = await cleanupOpenClawOwnedAcpxProcessTree({ + rootPid: 110, + rootCommand: `"/usr/local/bin/node" "${WRAPPER_ROOT}/codex-acp-wrapper.mjs"`, + wrapperRoot: WRAPPER_ROOT, + deps, + }); + + expect(result.skippedReason).toBeUndefined(); + expect(killed[0]).toEqual({ pid: 110, signal: "SIGTERM" }); + }); + + it("requires matching lease identity before killing a leased process tree", async () => { + const { deps, killed } = cleanupDeps([ + { pid: 112, ppid: 1, command: CODEX_WRAPPER_COMMAND_WITH_LEASE }, + ]); + + const result = await cleanupOpenClawOwnedAcpxProcessTree({ + rootPid: 112, + rootCommand: CODEX_WRAPPER_COMMAND, + expectedLeaseId: "lease-1", + expectedGatewayInstanceId: "gateway-1", + wrapperRoot: WRAPPER_ROOT, + deps, + }); + + expect(result.skippedReason).toBeUndefined(); + expect(killed[0]).toEqual({ pid: 112, signal: "SIGTERM" }); + }); + + it("does not kill a reused same-root wrapper pid with a different lease identity", async () => { + const { deps, killed } = cleanupDeps([ + { + pid: 113, + ppid: 1, + command: `${CODEX_WRAPPER_COMMAND} ${OPENCLAW_ACPX_LEASE_ID_ARG} other-lease ${OPENCLAW_GATEWAY_INSTANCE_ID_ARG} gateway-1`, + }, + ]); + + const result = await cleanupOpenClawOwnedAcpxProcessTree({ + rootPid: 113, + rootCommand: CODEX_WRAPPER_COMMAND, + expectedLeaseId: "lease-1", + expectedGatewayInstanceId: "gateway-1", + wrapperRoot: WRAPPER_ROOT, + deps, + }); + + expect(result).toEqual({ + inspectedPids: [113], + terminatedPids: [], + skippedReason: "not-openclaw-owned", + }); + expect(killed).toEqual([]); + }); + + it("skips recorded pid cleanup when process listing is unavailable", async () => { + const killed: Array<{ pid: number; signal: NodeJS.Signals }> = []; + const result = await cleanupOpenClawOwnedAcpxProcessTree({ + rootPid: 200, + rootCommand: CODEX_WRAPPER_COMMAND, + wrapperRoot: WRAPPER_ROOT, + deps: { + listProcesses: vi.fn(async () => { + throw new Error("ps unavailable"); + }), + killProcess: vi.fn((pid, signal) => { + killed.push({ pid, signal }); + }), + sleep: vi.fn(async () => {}), + }, + }); + + expect(result).toEqual({ + inspectedPids: [], + terminatedPids: [], + skippedReason: "unverified-root", + }); + expect(killed).toEqual([]); + }); + + it("does not kill a reused pid when the live command is not OpenClaw-owned", async () => { + const { deps, killed } = cleanupDeps([{ pid: 250, ppid: 1, command: "node unrelated.js" }]); + + const result = await cleanupOpenClawOwnedAcpxProcessTree({ + rootPid: 250, + rootCommand: CODEX_WRAPPER_COMMAND, + wrapperRoot: WRAPPER_ROOT, + deps, + }); + + expect(result).toEqual({ + inspectedPids: [250], + terminatedPids: [], + skippedReason: "not-openclaw-owned", + }); + expect(killed).toEqual([]); + }); + + it("does not kill a reused adapter pid when the stored root was a generated wrapper", async () => { + const { deps, killed } = cleanupDeps([ + { + pid: 260, + ppid: 1, + command: PLUGIN_DEPS_CODEX_COMMAND, + }, + ]); + + const result = await cleanupOpenClawOwnedAcpxProcessTree({ + rootPid: 260, + rootCommand: CODEX_WRAPPER_COMMAND, + wrapperRoot: WRAPPER_ROOT, + deps, + }); + + expect(result).toEqual({ + inspectedPids: [260], + terminatedPids: [], + skippedReason: "not-openclaw-owned", + }); + expect(killed).toEqual([]); + }); + + it("skips non-owned recorded process trees", async () => { + const { deps, killed } = cleanupDeps([{ pid: 300, ppid: 1, command: "node server.js" }]); + + const result = await cleanupOpenClawOwnedAcpxProcessTree({ + rootPid: 300, + rootCommand: "node server.js", + wrapperRoot: WRAPPER_ROOT, + deps, + }); + + expect(result.skippedReason).toBe("not-openclaw-owned"); + expect(killed).toEqual([]); + }); + + it("reaps stale OpenClaw-owned wrapper and adapter orphans on startup", async () => { + const { deps, killed } = cleanupDeps([ + { pid: 400, ppid: 1, command: CODEX_WRAPPER_COMMAND }, + { pid: 401, ppid: 400, command: PLUGIN_DEPS_CODEX_COMMAND }, + { pid: 402, ppid: 401, command: "node child.js" }, + { pid: 403, ppid: 1, command: CLAUDE_WRAPPER_COMMAND }, + { pid: 404, ppid: 403, command: "node claude-child.js" }, + { pid: 405, ppid: 1, command: PLUGIN_DEPS_CODEX_COMMAND }, + { pid: 406, ppid: 1, command: "node /tmp/other/codex-acp-wrapper.mjs" }, + ]); + + const result = await reapStaleOpenClawOwnedAcpxOrphans({ + wrapperRoot: WRAPPER_ROOT, + deps, + }); + + expect(result.skippedReason).toBeUndefined(); + expect(result.inspectedPids).toEqual([400, 401, 402, 403, 404, 405]); + expect(killed.filter((entry) => entry.signal === "SIGTERM").map((entry) => entry.pid)).toEqual([ + 402, 401, 400, 404, 403, 405, + ]); + }); + + it("keeps startup scans quiet when process listing is unavailable", async () => { + const result = await reapStaleOpenClawOwnedAcpxOrphans({ + wrapperRoot: WRAPPER_ROOT, + deps: { + listProcesses: vi.fn(async () => { + throw new Error("ps unavailable"); + }), + sleep: vi.fn(async () => {}), + }, + }); + + expect(result).toEqual({ + inspectedPids: [], + terminatedPids: [], + skippedReason: "process-list-unavailable", + }); + }); +}); diff --git a/extensions/acpx/src/process-reaper.ts b/extensions/acpx/src/process-reaper.ts new file mode 100644 index 00000000000..f7c3b7aad7f --- /dev/null +++ b/extensions/acpx/src/process-reaper.ts @@ -0,0 +1,381 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { OPENCLAW_ACPX_LEASE_ID_ARG, OPENCLAW_GATEWAY_INSTANCE_ID_ARG } from "./process-lease.js"; + +const execFileAsync = promisify(execFile); +const GENERATED_WRAPPER_BASENAMES = new Set([ + "codex-acp-wrapper.mjs", + "claude-agent-acp-wrapper.mjs", +]); +const OPENCLAW_PLUGIN_DEPS_MARKER = "/plugin-runtime-deps/"; +const ACP_PACKAGE_MARKERS = [ + "/@zed-industries/codex-acp/", + "/@agentclientprotocol/claude-agent-acp/", + "/acpx/dist/", +]; + +export type AcpxProcessInfo = { + pid: number; + ppid: number; + command: string; +}; + +export type AcpxProcessCleanupDeps = { + listProcesses?: () => Promise; + killProcess?: (pid: number, signal: NodeJS.Signals) => void; + sleep?: (ms: number) => Promise; +}; + +export type AcpxProcessCleanupResult = { + inspectedPids: number[]; + terminatedPids: number[]; + skippedReason?: "missing-root" | "not-openclaw-owned" | "unverified-root"; +}; + +export type AcpxStartupReapResult = { + inspectedPids: number[]; + terminatedPids: number[]; + skippedReason?: "unsupported-platform" | "process-list-unavailable"; +}; + +function normalizePathLike(value: string): string { + return value.replaceAll("\\", "/"); +} + +function commandMentionsGeneratedWrapper(command: string): boolean { + return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) => command.includes(basename)); +} + +function commandWrapperBelongsToRoot(command: string, wrapperRoot: string | undefined): boolean { + if (!wrapperRoot) { + return true; + } + const normalizedCommand = normalizePathLike(command); + const normalizedRoot = normalizePathLike(wrapperRoot).replace(/\/+$/, ""); + return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) => + normalizedCommand.includes(`${normalizedRoot}/${basename}`), + ); +} + +function commandsReferToSameRootCommand(liveCommand: string, storedCommand: string | undefined) { + if (!storedCommand?.trim()) { + return true; + } + return normalizePathLike(liveCommand).trim() === normalizePathLike(storedCommand).trim(); +} + +function splitCommandParts(value: string): string[] { + const parts: string[] = []; + let current = ""; + let quote: "'" | '"' | null = null; + let escaping = false; + + for (const ch of value) { + if (escaping) { + current += ch; + escaping = false; + continue; + } + if (ch === "\\" && quote !== "'") { + escaping = true; + continue; + } + if (quote) { + if (ch === quote) { + quote = null; + } else { + current += ch; + } + continue; + } + if (ch === "'" || ch === '"') { + quote = ch; + continue; + } + if (/\s/.test(ch)) { + if (current) { + parts.push(current); + current = ""; + } + continue; + } + current += ch; + } + + if (escaping) { + current += "\\"; + } + if (current) { + parts.push(current); + } + return parts; +} + +function commandOptionEquals( + parts: string[], + option: string, + expected: string | undefined, +): boolean { + if (!expected) { + return true; + } + const index = parts.indexOf(option); + return index >= 0 && parts[index + 1] === expected; +} + +function liveCommandMatchesLeaseIdentity(params: { + command: string | undefined; + expectedLeaseId?: string; + expectedGatewayInstanceId?: string; +}): boolean { + if (!params.expectedLeaseId && !params.expectedGatewayInstanceId) { + return true; + } + const parts = splitCommandParts(params.command ?? ""); + return ( + commandOptionEquals(parts, OPENCLAW_ACPX_LEASE_ID_ARG, params.expectedLeaseId) && + commandOptionEquals(parts, OPENCLAW_GATEWAY_INSTANCE_ID_ARG, params.expectedGatewayInstanceId) + ); +} + +export function isOpenClawOwnedAcpxProcessCommand(params: { + command: string | undefined; + wrapperRoot?: string; +}): boolean { + const command = params.command?.trim(); + if (!command) { + return false; + } + const normalized = normalizePathLike(command); + if (commandMentionsGeneratedWrapper(normalized)) { + return commandWrapperBelongsToRoot(normalized, params.wrapperRoot); + } + if (!normalized.includes(OPENCLAW_PLUGIN_DEPS_MARKER)) { + return false; + } + return ACP_PACKAGE_MARKERS.some((marker) => normalized.includes(marker)); +} + +function parseProcessList(stdout: string): AcpxProcessInfo[] { + const processes: AcpxProcessInfo[] = []; + for (const line of stdout.split(/\r?\n/)) { + const match = /^\s*(?\d+)\s+(?\d+)\s+(?.+?)\s*$/.exec(line); + if (!match?.groups) { + continue; + } + processes.push({ + pid: Number.parseInt(match.groups.pid, 10), + ppid: Number.parseInt(match.groups.ppid, 10), + command: match.groups.command, + }); + } + return processes; +} + +export async function listPlatformProcesses(): Promise { + if (process.platform === "win32") { + return []; + } + const { stdout } = await execFileAsync("ps", ["-axo", "pid=,ppid=,command="], { + maxBuffer: 8 * 1024 * 1024, + }); + return parseProcessList(stdout); +} + +function collectProcessTree(processes: AcpxProcessInfo[], rootPid: number): AcpxProcessInfo[] { + const childrenByParent = new Map(); + for (const processInfo of processes) { + const children = childrenByParent.get(processInfo.ppid) ?? []; + children.push(processInfo); + childrenByParent.set(processInfo.ppid, children); + } + + const byPid = new Map(processes.map((processInfo) => [processInfo.pid, processInfo])); + const root = byPid.get(rootPid); + const collected: AcpxProcessInfo[] = []; + if (root) { + collected.push(root); + } + + const queue = [...(childrenByParent.get(rootPid) ?? [])]; + while (queue.length > 0) { + const next = queue.shift(); + if (!next || collected.some((processInfo) => processInfo.pid === next.pid)) { + continue; + } + collected.push(next); + queue.push(...(childrenByParent.get(next.pid) ?? [])); + } + + return collected; +} + +function uniquePids(processes: AcpxProcessInfo[]): number[] { + return Array.from( + new Set( + processes + .map((processInfo) => processInfo.pid) + .filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid), + ), + ); +} + +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +async function terminatePids( + pids: number[], + deps: AcpxProcessCleanupDeps | undefined, +): Promise { + const killProcess = deps?.killProcess ?? ((pid, signal) => process.kill(pid, signal)); + const sleep = deps?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))); + const terminated: number[] = []; + + for (const pid of pids) { + try { + killProcess(pid, "SIGTERM"); + terminated.push(pid); + } catch { + // The process may already be gone. + } + } + if (terminated.length === 0) { + return terminated; + } + await sleep(750); + for (const pid of terminated) { + if (deps?.killProcess || isProcessAlive(pid)) { + try { + killProcess(pid, "SIGKILL"); + } catch { + // Best-effort cleanup only. + } + } + } + return terminated; +} + +export async function cleanupOpenClawOwnedAcpxProcessTree(params: { + rootPid?: number; + rootCommand?: string; + expectedLeaseId?: string; + expectedGatewayInstanceId?: string; + wrapperRoot?: string; + deps?: AcpxProcessCleanupDeps; +}): Promise { + const rootPid = params.rootPid; + if (!rootPid || rootPid <= 0 || rootPid === process.pid) { + return { inspectedPids: [], terminatedPids: [], skippedReason: "missing-root" }; + } + + let processes: AcpxProcessInfo[] = []; + try { + processes = await (params.deps?.listProcesses ?? listPlatformProcesses)(); + } catch { + processes = []; + } + + const listedTree = collectProcessTree(processes, rootPid); + // Session-store PIDs are stale data. If the live process table cannot prove + // that this PID still belongs to an OpenClaw-owned wrapper, fail closed to + // avoid killing an unrelated process after PID reuse. + if (listedTree.length === 0) { + return { inspectedPids: [], terminatedPids: [], skippedReason: "unverified-root" }; + } + const rootCommand = listedTree[0]?.command ?? params.rootCommand; + const liveCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper( + normalizePathLike(rootCommand ?? ""), + ); + const storedCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper( + normalizePathLike(params.rootCommand ?? ""), + ); + if (!liveCommandWasGeneratedWrapper && storedCommandWasGeneratedWrapper) { + return { + inspectedPids: listedTree.map((processInfo) => processInfo.pid), + terminatedPids: [], + skippedReason: "not-openclaw-owned", + }; + } + if ( + !liveCommandWasGeneratedWrapper && + !commandsReferToSameRootCommand(rootCommand ?? "", params.rootCommand) + ) { + return { + inspectedPids: listedTree.map((processInfo) => processInfo.pid), + terminatedPids: [], + skippedReason: "not-openclaw-owned", + }; + } + if ( + !isOpenClawOwnedAcpxProcessCommand({ + command: rootCommand, + wrapperRoot: params.wrapperRoot, + }) + ) { + return { + inspectedPids: listedTree.map((processInfo) => processInfo.pid), + terminatedPids: [], + skippedReason: "not-openclaw-owned", + }; + } + if ( + !liveCommandMatchesLeaseIdentity({ + command: rootCommand, + expectedLeaseId: params.expectedLeaseId, + expectedGatewayInstanceId: params.expectedGatewayInstanceId, + }) + ) { + return { + inspectedPids: listedTree.map((processInfo) => processInfo.pid), + terminatedPids: [], + skippedReason: "not-openclaw-owned", + }; + } + + const pids = uniquePids(listedTree.toReversed()); + return { + inspectedPids: uniquePids(listedTree), + terminatedPids: await terminatePids(pids, params.deps), + }; +} + +export async function reapStaleOpenClawOwnedAcpxOrphans(params: { + wrapperRoot: string; + deps?: AcpxProcessCleanupDeps; +}): Promise { + if (process.platform === "win32") { + return { inspectedPids: [], terminatedPids: [], skippedReason: "unsupported-platform" }; + } + + let processes: AcpxProcessInfo[]; + try { + processes = await (params.deps?.listProcesses ?? listPlatformProcesses)(); + } catch { + return { inspectedPids: [], terminatedPids: [], skippedReason: "process-list-unavailable" }; + } + + const orphans = processes.filter( + (processInfo) => + processInfo.ppid === 1 && + isOpenClawOwnedAcpxProcessCommand({ + command: processInfo.command, + wrapperRoot: params.wrapperRoot, + }), + ); + // Startup reaping starts from currently visible orphan roots and then expands + // each tree, so adapter grandchildren do not survive as fresh orphans after + // the wrapper root exits. + const orphanTrees = orphans.map((orphan) => collectProcessTree(processes, orphan.pid)); + const inspectedPids = uniquePids(orphanTrees.flat()); + const pids = uniquePids(orphanTrees.flatMap((tree) => tree.toReversed())); + return { + inspectedPids, + terminatedPids: await terminatePids(pids, params.deps), + }; +} diff --git a/extensions/acpx/src/runtime.test.ts b/extensions/acpx/src/runtime.test.ts index ca7e1ccda40..ec5e37d9429 100644 --- a/extensions/acpx/src/runtime.test.ts +++ b/extensions/acpx/src/runtime.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { AcpRuntimeError, type AcpRuntime } from "../runtime-api.js"; +import { OPENCLAW_ACPX_LEASE_ID_ARG, OPENCLAW_GATEWAY_INSTANCE_ID_ARG } from "./process-lease.js"; import { AcpxRuntime, __testing } from "./runtime.js"; type TestSessionStore = { @@ -11,14 +12,17 @@ const DOCUMENTED_OPENCLAW_BRIDGE_COMMAND = "env OPENCLAW_HIDE_BANNER=1 OPENCLAW_SUPPRESS_NOTES=1 openclaw acp --url ws://127.0.0.1:18789 --token-file ~/.openclaw/gateway.token --session agent:main:main"; const CODEX_ACP_COMMAND = "npx @zed-industries/codex-acp@0.13.0"; const CODEX_ACP_WRAPPER_COMMAND = `node "/tmp/openclaw/acpx/codex-acp-wrapper.mjs"`; +const CODEX_ACP_WRAPPER_COMMAND_WITH_LEASE = `${CODEX_ACP_WRAPPER_COMMAND} ${OPENCLAW_ACPX_LEASE_ID_ARG} lease-close ${OPENCLAW_GATEWAY_INSTANCE_ID_ARG} gateway-test`; function makeRuntime( baseStore: TestSessionStore, options: Partial[0]> = {}, + testOptions?: ConstructorParameters[1], ): { runtime: AcpxRuntime; wrappedStore: TestSessionStore & { markFresh: (sessionKey: string) => void }; delegate: { + cancel: AcpRuntime["cancel"]; close: AcpRuntime["close"]; ensureSession: AcpRuntime["ensureSession"]; getStatus: NonNullable; @@ -35,16 +39,19 @@ function makeRuntime( probeAvailability(): Promise; }; } { - const runtime = new AcpxRuntime({ - cwd: "/tmp", - sessionStore: baseStore, - agentRegistry: { - resolve: (agentName: string) => (agentName === "openclaw" ? "openclaw acp" : agentName), - list: () => ["codex", "openclaw"], + const runtime = new AcpxRuntime( + { + cwd: "/tmp", + sessionStore: baseStore, + agentRegistry: { + resolve: (agentName: string) => (agentName === "openclaw" ? "openclaw acp" : agentName), + list: () => ["codex", "openclaw"], + }, + permissionMode: "approve-reads", + ...options, }, - permissionMode: "approve-reads", - ...options, - }); + testOptions, + ); return { runtime, @@ -56,6 +63,7 @@ function makeRuntime( delegate: ( runtime as unknown as { delegate: { + cancel: AcpRuntime["cancel"]; close: AcpRuntime["close"]; ensureSession: AcpRuntime["ensureSession"]; getStatus: NonNullable; @@ -80,6 +88,26 @@ function makeRuntime( }; } +function makeLeaseStore() { + const leases = new Map>(); + return { + leases, + store: { + load: vi.fn(async (leaseId: string) => leases.get(leaseId) as never), + listOpen: vi.fn(async () => Array.from(leases.values()) as never), + save: vi.fn(async (lease: Record) => { + leases.set(String(lease.leaseId), lease); + }), + markState: vi.fn(async (leaseId: string, state: string) => { + const lease = leases.get(leaseId); + if (lease) { + lease.state = state; + } + }), + }, + }; +} + describe("AcpxRuntime fresh reset wrapper", () => { beforeEach(() => { vi.restoreAllMocks(); @@ -517,6 +545,483 @@ describe("AcpxRuntime fresh reset wrapper", () => { expect(baseStore.load).toHaveBeenCalledOnce(); }); + it("cleans up OpenClaw-owned ACPX process trees after close", async () => { + const baseStore: TestSessionStore = { + load: vi.fn(async () => ({ + acpxRecordId: "agent:codex:acp:binding:test", + agentCommand: 'node "/tmp/openclaw/acpx/codex-acp-wrapper.mjs"', + pid: 900, + })), + save: vi.fn(async () => {}), + }; + const killed: Array<{ pid: number; signal: NodeJS.Signals }> = []; + const { runtime, delegate } = makeRuntime( + baseStore, + { + openclawWrapperRoot: "/tmp/openclaw/acpx", + }, + { + openclawProcessCleanup: { + listProcesses: vi.fn(async () => [ + { + pid: 900, + ppid: 1, + command: 'node "/tmp/openclaw/acpx/codex-acp-wrapper.mjs"', + }, + { + pid: 901, + ppid: 900, + command: + "node /tmp/openclaw/plugin-runtime-deps/node_modules/@zed-industries/codex-acp/bin/codex-acp.js", + }, + ]), + killProcess: vi.fn((pid, signal) => { + killed.push({ pid, signal }); + }), + sleep: vi.fn(async () => {}), + }, + }, + ); + vi.spyOn(delegate, "close").mockResolvedValue(undefined); + + await runtime.close({ + handle: { + sessionKey: "agent:codex:acp:binding:test", + backend: "acpx", + runtimeSessionName: "agent:codex:acp:binding:test", + }, + reason: "user-close", + }); + + expect(killed.slice(0, 2)).toEqual([ + { pid: 901, signal: "SIGTERM" }, + { pid: 900, signal: "SIGTERM" }, + ]); + }); + + it("records ACPX process leases without persisting lease-specific agent commands", async () => { + const savedRecords: Record[] = []; + const launchCommands: string[] = []; + const baseStore: TestSessionStore = { + load: vi.fn(async () => undefined), + save: vi.fn(async (record) => { + savedRecords.push(record); + }), + }; + const leaseStore = makeLeaseStore(); + const { runtime, delegate, wrappedStore } = makeRuntime(baseStore, { + openclawGatewayInstanceId: "gateway-test", + openclawProcessLeaseStore: leaseStore.store, + openclawWrapperRoot: "/tmp/openclaw/acpx", + agentRegistry: { + resolve: (agentName: string) => + agentName === "codex" ? CODEX_ACP_WRAPPER_COMMAND : agentName, + list: () => ["codex"], + }, + }); + vi.spyOn(delegate, "ensureSession").mockImplementation(async (input) => { + const command = ( + runtime as unknown as { scopedAgentRegistry: { resolve(agent: string): string } } + ).scopedAgentRegistry.resolve("codex"); + launchCommands.push(command); + await wrappedStore.save({ + name: input.sessionKey, + agentCommand: command, + pid: 777, + }); + return { + sessionKey: input.sessionKey, + backend: "acpx", + runtimeSessionName: input.sessionKey, + }; + }); + + await runtime.ensureSession({ + sessionKey: "agent:codex:acp:binding:test", + agent: "codex", + mode: "persistent", + }); + + expect(leaseStore.store.save).toHaveBeenCalledTimes(2); + const leases = Array.from(leaseStore.leases.values()); + expect(leases).toHaveLength(1); + expect(leases[0]).toMatchObject({ + gatewayInstanceId: "gateway-test", + sessionKey: "agent:codex:acp:binding:test", + rootPid: 777, + state: "open", + wrapperPath: "/tmp/openclaw/acpx/codex-acp-wrapper.mjs", + }); + expect(launchCommands[0]).toContain("OPENCLAW_ACPX_LEASE_ID="); + expect(launchCommands[0]).toContain("OPENCLAW_GATEWAY_INSTANCE_ID=gateway-test"); + expect(savedRecords[0]?.agentCommand).toBe(CODEX_ACP_WRAPPER_COMMAND); + expect(savedRecords[0]).toMatchObject({ + openclawGatewayInstanceId: "gateway-test", + openclawLeaseId: leases[0]?.leaseId, + }); + }); + + it("keeps reusable persistent ACP launch commands stable across ensures", async () => { + const baseStore: TestSessionStore = { + load: vi.fn(async () => ({ + name: "agent:codex:acp:binding:test", + acpxRecordId: "record-1", + acpSessionId: "session-1", + agentCommand: CODEX_ACP_WRAPPER_COMMAND, + cwd: "/tmp", + closed: false, + })), + save: vi.fn(async () => {}), + }; + const leaseStore = makeLeaseStore(); + const { runtime, delegate } = makeRuntime(baseStore, { + openclawGatewayInstanceId: "gateway-test", + openclawProcessLeaseStore: leaseStore.store, + openclawWrapperRoot: "/tmp/openclaw/acpx", + agentRegistry: { + resolve: (agentName: string) => + agentName === "codex" ? CODEX_ACP_WRAPPER_COMMAND : agentName, + list: () => ["codex"], + }, + }); + const resolvedCommands: string[] = []; + vi.spyOn(delegate, "ensureSession").mockImplementation(async (input) => { + resolvedCommands.push( + ( + runtime as unknown as { scopedAgentRegistry: { resolve(agent: string): string } } + ).scopedAgentRegistry.resolve("codex"), + ); + return { + sessionKey: input.sessionKey, + backend: "acpx", + runtimeSessionName: input.sessionKey, + }; + }); + + await runtime.ensureSession({ + sessionKey: "agent:codex:acp:binding:test", + agent: "codex", + mode: "persistent", + }); + + expect(resolvedCommands).toEqual([CODEX_ACP_WRAPPER_COMMAND]); + expect(leaseStore.store.save).not.toHaveBeenCalled(); + }); + + it("merges sidecar lease ids into loaded ACPX session records", async () => { + const leaseStore = makeLeaseStore(); + leaseStore.leases.set("lease-loaded", { + leaseId: "lease-loaded", + gatewayInstanceId: "gateway-test", + sessionKey: "agent:codex:acp:binding:test", + wrapperRoot: "/tmp/openclaw/acpx", + wrapperPath: "/tmp/openclaw/acpx/codex-acp-wrapper.mjs", + rootPid: 777, + commandHash: "hash", + startedAt: 1, + state: "open", + }); + const baseStore: TestSessionStore = { + load: vi.fn(async () => ({ + name: "agent:codex:acp:binding:test", + agentCommand: 'node "/tmp/openclaw/acpx/codex-acp-wrapper.mjs"', + pid: 777, + })), + save: vi.fn(async () => {}), + }; + const { wrappedStore } = makeRuntime(baseStore, { + openclawGatewayInstanceId: "gateway-test", + openclawProcessLeaseStore: leaseStore.store, + openclawWrapperRoot: "/tmp/openclaw/acpx", + }); + + await expect(wrappedStore.load("agent:codex:acp:binding:test")).resolves.toMatchObject({ + openclawGatewayInstanceId: "gateway-test", + openclawLeaseId: "lease-loaded", + }); + }); + + it("merges the lease for the current ACPX session process when old leases exist", async () => { + const leaseStore = makeLeaseStore(); + leaseStore.leases.set("lease-old", { + leaseId: "lease-old", + gatewayInstanceId: "gateway-test", + sessionKey: "agent:codex:acp:binding:test", + wrapperRoot: "/tmp/openclaw/acpx", + wrapperPath: "/tmp/openclaw/acpx/codex-acp-wrapper.mjs", + rootPid: 700, + commandHash: "hash", + startedAt: 1, + state: "open", + }); + leaseStore.leases.set("lease-current", { + leaseId: "lease-current", + gatewayInstanceId: "gateway-test", + sessionKey: "agent:codex:acp:binding:test", + wrapperRoot: "/tmp/openclaw/acpx", + wrapperPath: "/tmp/openclaw/acpx/codex-acp-wrapper.mjs", + rootPid: 777, + commandHash: "hash", + startedAt: 2, + state: "open", + }); + const baseStore: TestSessionStore = { + load: vi.fn(async () => ({ + name: "agent:codex:acp:binding:test", + agentCommand: 'node "/tmp/openclaw/acpx/codex-acp-wrapper.mjs"', + pid: 777, + })), + save: vi.fn(async () => {}), + }; + const { wrappedStore } = makeRuntime(baseStore, { + openclawGatewayInstanceId: "gateway-test", + openclawProcessLeaseStore: leaseStore.store, + openclawWrapperRoot: "/tmp/openclaw/acpx", + }); + + await expect(wrappedStore.load("agent:codex:acp:binding:test")).resolves.toMatchObject({ + openclawGatewayInstanceId: "gateway-test", + openclawLeaseId: "lease-current", + }); + }); + + it("uses matching leases before legacy pid cleanup on close", async () => { + const leaseStore = makeLeaseStore(); + leaseStore.leases.set("lease-close", { + leaseId: "lease-close", + gatewayInstanceId: "gateway-test", + sessionKey: "agent:codex:acp:binding:test", + wrapperRoot: "/tmp/openclaw/acpx", + wrapperPath: "/tmp/openclaw/acpx/codex-acp-wrapper.mjs", + rootPid: 930, + commandHash: "hash", + startedAt: 1, + state: "open", + }); + const baseStore: TestSessionStore = { + load: vi.fn(async () => ({ + acpxRecordId: "agent:codex:acp:binding:test", + agentCommand: 'node "/tmp/openclaw/acpx/codex-acp-wrapper.mjs"', + openclawLeaseId: "lease-close", + pid: 930, + })), + save: vi.fn(async () => {}), + }; + const killed: Array<{ pid: number; signal: NodeJS.Signals }> = []; + const { runtime, delegate } = makeRuntime( + baseStore, + { + openclawGatewayInstanceId: "gateway-test", + openclawProcessLeaseStore: leaseStore.store, + openclawWrapperRoot: "/tmp/openclaw/acpx", + }, + { + openclawProcessCleanup: { + listProcesses: vi.fn(async () => [ + { + pid: 930, + ppid: 1, + command: CODEX_ACP_WRAPPER_COMMAND_WITH_LEASE, + }, + { pid: 931, ppid: 930, command: "node child.js" }, + ]), + killProcess: vi.fn((pid, signal) => { + killed.push({ pid, signal }); + }), + sleep: vi.fn(async () => {}), + }, + }, + ); + vi.spyOn(delegate, "close").mockResolvedValue(undefined); + + await runtime.close({ + handle: { + sessionKey: "agent:codex:acp:binding:test", + backend: "acpx", + runtimeSessionName: "agent:codex:acp:binding:test", + }, + reason: "user-close", + }); + + expect(killed.slice(0, 2)).toEqual([ + { pid: 931, signal: "SIGTERM" }, + { pid: 930, signal: "SIGTERM" }, + ]); + expect(leaseStore.store.markState).toHaveBeenCalledWith("lease-close", "closing"); + expect(leaseStore.store.markState).toHaveBeenLastCalledWith("lease-close", "closed"); + }); + + it("closes the current process lease when the saved lease id is stale", async () => { + const leaseStore = makeLeaseStore(); + leaseStore.leases.set("lease-old", { + leaseId: "lease-old", + gatewayInstanceId: "gateway-test", + sessionKey: "agent:codex:acp:binding:test", + wrapperRoot: "/tmp/openclaw/acpx", + wrapperPath: "/tmp/openclaw/acpx/codex-acp-wrapper.mjs", + rootPid: 930, + commandHash: "hash", + startedAt: 1, + state: "open", + }); + leaseStore.leases.set("lease-current", { + leaseId: "lease-current", + gatewayInstanceId: "gateway-test", + sessionKey: "agent:codex:acp:binding:test", + wrapperRoot: "/tmp/openclaw/acpx", + wrapperPath: "/tmp/openclaw/acpx/codex-acp-wrapper.mjs", + rootPid: 940, + commandHash: "hash", + startedAt: 2, + state: "open", + }); + const baseStore: TestSessionStore = { + load: vi.fn(async () => ({ + acpxRecordId: "agent:codex:acp:binding:test", + agentCommand: 'node "/tmp/openclaw/acpx/codex-acp-wrapper.mjs"', + openclawLeaseId: "lease-old", + pid: 940, + })), + save: vi.fn(async () => {}), + }; + const killed: Array<{ pid: number; signal: NodeJS.Signals }> = []; + const { runtime, delegate } = makeRuntime( + baseStore, + { + openclawGatewayInstanceId: "gateway-test", + openclawProcessLeaseStore: leaseStore.store, + openclawWrapperRoot: "/tmp/openclaw/acpx", + }, + { + openclawProcessCleanup: { + listProcesses: vi.fn(async () => [ + { + pid: 930, + ppid: 1, + command: `${CODEX_ACP_WRAPPER_COMMAND} ${OPENCLAW_ACPX_LEASE_ID_ARG} lease-old ${OPENCLAW_GATEWAY_INSTANCE_ID_ARG} gateway-test`, + }, + { + pid: 940, + ppid: 1, + command: `${CODEX_ACP_WRAPPER_COMMAND} ${OPENCLAW_ACPX_LEASE_ID_ARG} lease-current ${OPENCLAW_GATEWAY_INSTANCE_ID_ARG} gateway-test`, + }, + { pid: 941, ppid: 940, command: "node child.js" }, + ]), + killProcess: vi.fn((pid, signal) => { + killed.push({ pid, signal }); + }), + sleep: vi.fn(async () => {}), + }, + }, + ); + vi.spyOn(delegate, "close").mockResolvedValue(undefined); + + await runtime.close({ + handle: { + sessionKey: "agent:codex:acp:binding:test", + backend: "acpx", + runtimeSessionName: "agent:codex:acp:binding:test", + }, + reason: "user-close", + }); + + expect(killed.slice(0, 2)).toEqual([ + { pid: 941, signal: "SIGTERM" }, + { pid: 940, signal: "SIGTERM" }, + ]); + expect(leaseStore.store.markState).not.toHaveBeenCalledWith("lease-old", expect.any(String)); + expect(leaseStore.store.markState).toHaveBeenCalledWith("lease-current", "closing"); + expect(leaseStore.store.markState).toHaveBeenLastCalledWith("lease-current", "closed"); + }); + + it("does not clean up a stale close pid reused by another wrapper root", async () => { + const baseStore: TestSessionStore = { + load: vi.fn(async () => ({ + acpxRecordId: "agent:codex:acp:binding:test", + agentCommand: 'node "/tmp/openclaw/acpx/codex-acp-wrapper.mjs"', + pid: 920, + })), + save: vi.fn(async () => {}), + }; + const killed: Array<{ pid: number; signal: NodeJS.Signals }> = []; + const { runtime, delegate } = makeRuntime( + baseStore, + { + openclawWrapperRoot: "/tmp/openclaw/acpx", + }, + { + openclawProcessCleanup: { + listProcesses: vi.fn(async () => [ + { + pid: 920, + ppid: 1, + command: 'node "/tmp/other-gateway/acpx/codex-acp-wrapper.mjs"', + }, + ]), + killProcess: vi.fn((pid, signal) => { + killed.push({ pid, signal }); + }), + sleep: vi.fn(async () => {}), + }, + }, + ); + vi.spyOn(delegate, "close").mockResolvedValue(undefined); + + await runtime.close({ + handle: { + sessionKey: "agent:codex:acp:binding:test", + backend: "acpx", + runtimeSessionName: "agent:codex:acp:binding:test", + }, + reason: "user-close", + }); + + expect(killed).toEqual([]); + }); + + it("does not tear down reusable ACPX sessions after cancel", async () => { + const baseStore: TestSessionStore = { + load: vi.fn(async () => ({ + acpxRecordId: "agent:codex:acp:binding:test", + agentCommand: 'node "/tmp/openclaw/acpx/codex-acp-wrapper.mjs"', + processId: "910", + })), + save: vi.fn(async () => {}), + }; + const killed: Array<{ pid: number; signal: NodeJS.Signals }> = []; + const listProcesses = vi.fn(async () => { + throw new Error("process listing should not run on cancel"); + }); + const { runtime, delegate } = makeRuntime( + baseStore, + {}, + { + openclawProcessCleanup: { + listProcesses, + killProcess: vi.fn((pid, signal) => { + killed.push({ pid, signal }); + }), + sleep: vi.fn(async () => {}), + }, + }, + ); + const cancel = vi.spyOn(delegate, "cancel").mockResolvedValue(undefined); + + const input = { + handle: { + sessionKey: "agent:codex:acp:binding:test", + backend: "acpx", + runtimeSessionName: "agent:codex:acp:binding:test", + }, + } satisfies Parameters[0]; + + await runtime.cancel(input); + + expect(cancel).toHaveBeenCalledWith(input); + expect(listProcesses).not.toHaveBeenCalled(); + expect(killed).toEqual([]); + }); + it("routes openclaw ensureSession through the bridge-safe delegate when MCP servers are configured", async () => { const baseStore: TestSessionStore = { load: vi.fn(async () => undefined), diff --git a/extensions/acpx/src/runtime.ts b/extensions/acpx/src/runtime.ts index 252eef132ea..0b8e37e23c9 100644 --- a/extensions/acpx/src/runtime.ts +++ b/extensions/acpx/src/runtime.ts @@ -1,4 +1,5 @@ import { AsyncLocalStorage } from "node:async_hooks"; +import { resolve as resolvePath } from "node:path"; import { ACPX_BACKEND_ID, AcpxRuntime as BaseAcpxRuntime, @@ -15,16 +16,45 @@ import { type AcpRuntimeStatus, } from "acpx/runtime"; import { AcpRuntimeError, type AcpRuntime } from "../runtime-api.js"; +import { + createAcpxProcessLeaseId, + hashAcpxProcessCommand, + withAcpxLeaseEnvironment, + type AcpxProcessLease, + type AcpxProcessLeaseStore, +} from "./process-lease.js"; +import { + cleanupOpenClawOwnedAcpxProcessTree, + isOpenClawOwnedAcpxProcessCommand, + type AcpxProcessCleanupDeps, +} from "./process-reaper.js"; type AcpSessionStore = AcpRuntimeOptions["sessionStore"]; type AcpSessionRecord = Parameters[0]; type AcpLoadedSessionRecord = Awaited>; +type BaseAcpxRuntimeTestOptions = ConstructorParameters[1]; +type OpenClawAcpxRuntimeOptions = AcpRuntimeOptions & { + openclawWrapperRoot?: string; + openclawGatewayInstanceId?: string; + openclawProcessLeaseStore?: AcpxProcessLeaseStore; +}; +type AcpxRuntimeTestOptions = Record & { + openclawProcessCleanup?: AcpxProcessCleanupDeps; +}; type ResetAwareSessionStore = AcpSessionStore & { markFresh: (sessionKey: string) => void; }; -function readSessionRecordName(record: AcpSessionRecord): string { +type AcpxLaunchLeaseContext = { + leaseId: string; + gatewayInstanceId: string; + sessionKey: string; + wrapperRoot: string; + stableCommand?: string; +}; + +function readSessionRecordName(record: unknown): string { if (typeof record !== "object" || record === null) { return ""; } @@ -32,7 +62,88 @@ function readSessionRecordName(record: AcpSessionRecord): string { return typeof name === "string" ? name.trim() : ""; } -function createResetAwareSessionStore(baseStore: AcpSessionStore): ResetAwareSessionStore { +function readRecordAgentCommand(record: unknown): string | undefined { + if (typeof record !== "object" || record === null) { + return undefined; + } + const { agentCommand } = record as { agentCommand?: unknown }; + return typeof agentCommand === "string" ? agentCommand.trim() || undefined : undefined; +} + +function readRecordCwd(record: unknown): string | undefined { + if (typeof record !== "object" || record === null) { + return undefined; + } + const { cwd } = record as { cwd?: unknown }; + return typeof cwd === "string" ? cwd.trim() || undefined : undefined; +} + +function readRecordResetOnNextEnsure(record: unknown): boolean { + if (typeof record !== "object" || record === null) { + return false; + } + const { acpx } = record as { acpx?: unknown }; + if (typeof acpx !== "object" || acpx === null) { + return false; + } + return (acpx as { reset_on_next_ensure?: unknown }).reset_on_next_ensure === true; +} + +function readRecordAgentPid(record: unknown): number | undefined { + if (typeof record !== "object" || record === null) { + return undefined; + } + const { pid, processId } = record as { pid?: unknown; processId?: unknown }; + const rawPid = pid ?? processId; + const numericPid = + typeof rawPid === "number" + ? rawPid + : typeof rawPid === "string" + ? Number.parseInt(rawPid, 10) + : undefined; + return numericPid && Number.isInteger(numericPid) && numericPid > 0 ? numericPid : undefined; +} + +function readOpenClawLeaseIdFromRecord(record: AcpLoadedSessionRecord): string | undefined { + if (typeof record !== "object" || record === null) { + return undefined; + } + const { openclawLeaseId } = record as { openclawLeaseId?: unknown }; + return typeof openclawLeaseId === "string" ? openclawLeaseId.trim() || undefined : undefined; +} + +function extractGeneratedWrapperPath(command: string | undefined): string { + const parts = splitCommandParts(command ?? ""); + return ( + parts.find( + (part) => + basename(part) === "codex-acp-wrapper.mjs" || + basename(part) === "claude-agent-acp-wrapper.mjs", + ) ?? "" + ); +} + +function selectCurrentSessionLease(params: { + leases: AcpxProcessLease[]; + sessionKeys: string[]; + rootPid?: number; +}): AcpxProcessLease | undefined { + const sessionKeys = new Set(params.sessionKeys.map((entry) => entry.trim()).filter(Boolean)); + const candidates = params.leases.filter((lease) => sessionKeys.has(lease.sessionKey)); + if (params.rootPid) { + return candidates.find((lease) => lease.rootPid === params.rootPid); + } + return candidates.toSorted((a, b) => b.startedAt - a.startedAt)[0]; +} + +function createResetAwareSessionStore( + baseStore: AcpSessionStore, + params?: { + gatewayInstanceId?: string; + leaseStore?: AcpxProcessLeaseStore; + launchScope?: AsyncLocalStorage; + }, +): ResetAwareSessionStore { const freshSessionKeys = new Set(); return { @@ -41,11 +152,61 @@ function createResetAwareSessionStore(baseStore: AcpSessionStore): ResetAwareSes if (normalized && freshSessionKeys.has(normalized)) { return undefined; } - return await baseStore.load(sessionId); + const record = await baseStore.load(sessionId); + if (!record || !params?.leaseStore || !params.gatewayInstanceId) { + return record; + } + const sessionName = readSessionRecordName(record) || normalized; + const lease = selectCurrentSessionLease({ + leases: await params.leaseStore.listOpen(params.gatewayInstanceId), + sessionKeys: [sessionName, normalized], + rootPid: readRecordAgentPid(record), + }); + if (!lease) { + return record; + } + return { + ...(record as Record), + openclawLeaseId: lease.leaseId, + openclawGatewayInstanceId: lease.gatewayInstanceId, + } as AcpLoadedSessionRecord; }, async save(record: AcpSessionRecord): Promise { - await baseStore.save(record); + let recordToSave = record; + const launch = params?.launchScope?.getStore(); const sessionName = readSessionRecordName(record); + const rootPid = readRecordAgentPid(record); + const agentCommand = readRecordAgentCommand(record); + const stableAgentCommand = launch?.stableCommand ?? agentCommand; + if ( + launch && + params?.leaseStore && + sessionName === launch.sessionKey && + rootPid && + stableAgentCommand + ) { + const lease: AcpxProcessLease = { + leaseId: launch.leaseId, + gatewayInstanceId: launch.gatewayInstanceId, + sessionKey: launch.sessionKey, + wrapperRoot: launch.wrapperRoot, + wrapperPath: extractGeneratedWrapperPath(stableAgentCommand), + rootPid, + commandHash: hashAcpxProcessCommand(stableAgentCommand), + startedAt: Date.now(), + state: "open", + }; + await params.leaseStore.save(lease); + recordToSave = { + ...(record as Record), + // ACPX uses agentCommand as reuse identity. Lease metadata belongs to + // our sidecar record, so keep the persisted command stable. + agentCommand: stableAgentCommand, + openclawLeaseId: launch.leaseId, + openclawGatewayInstanceId: launch.gatewayInstanceId, + } as AcpSessionRecord; + } + await baseStore.save(recordToSave); if (sessionName) { freshSessionKeys.delete(sessionName); } @@ -109,11 +270,11 @@ function readAgentFromHandle(handle: AcpRuntimeHandle): string | undefined { } function readAgentCommandFromRecord(record: AcpLoadedSessionRecord): string | undefined { - if (typeof record !== "object" || record === null) { - return undefined; - } - const { agentCommand } = record as { agentCommand?: unknown }; - return typeof agentCommand === "string" ? agentCommand.trim() || undefined : undefined; + return readRecordAgentCommand(record); +} + +function readAgentPidFromRecord(record: AcpLoadedSessionRecord): number | undefined { + return readRecordAgentPid(record); } function splitCommandParts(value: string): string[] { @@ -338,6 +499,7 @@ function appendCodexAcpConfigOverrides(command: string, override: CodexAcpModelO function createModelScopedAgentRegistry(params: { agentRegistry: AcpAgentRegistry; scope: AsyncLocalStorage; + leaseCommand: (command: string | undefined) => string | undefined; }): AcpAgentRegistry { return { resolve(agentName: string): string | undefined { @@ -349,9 +511,9 @@ function createModelScopedAgentRegistry(params: { typeof command !== "string" || !isCodexAcpCommand(command) ) { - return command; + return params.leaseCommand(command); } - return appendCodexAcpConfigOverrides(command, override); + return params.leaseCommand(appendCodexAcpConfigOverrides(command, override)); }, list(): string[] { return params.agentRegistry.list(); @@ -402,30 +564,47 @@ export class AcpxRuntime implements AcpRuntime { private readonly delegate: BaseAcpxRuntime; private readonly bridgeSafeDelegate: BaseAcpxRuntime; private readonly probeDelegate: BaseAcpxRuntime; + private readonly processCleanupDeps: AcpxProcessCleanupDeps | undefined; + private readonly wrapperRoot: string | undefined; + private readonly gatewayInstanceId: string | undefined; + private readonly processLeaseStore: AcpxProcessLeaseStore | undefined; + private readonly launchLeaseScope = new AsyncLocalStorage(); + private readonly cwd: string; - constructor( - options: AcpRuntimeOptions, - testOptions?: ConstructorParameters[1], - ) { - this.sessionStore = createResetAwareSessionStore(options.sessionStore); + constructor(options: OpenClawAcpxRuntimeOptions, testOptions?: AcpxRuntimeTestOptions) { + const { openclawProcessCleanup, ...delegateTestOptions } = testOptions ?? {}; + this.processCleanupDeps = openclawProcessCleanup; + this.wrapperRoot = options.openclawWrapperRoot; + this.gatewayInstanceId = options.openclawGatewayInstanceId; + this.processLeaseStore = options.openclawProcessLeaseStore; + this.cwd = options.cwd; + this.sessionStore = createResetAwareSessionStore(options.sessionStore, { + gatewayInstanceId: this.gatewayInstanceId, + leaseStore: this.processLeaseStore, + launchScope: this.launchLeaseScope, + }); this.agentRegistry = options.agentRegistry; this.scopedAgentRegistry = createModelScopedAgentRegistry({ agentRegistry: this.agentRegistry, scope: this.codexAcpModelOverrideScope, + leaseCommand: (command) => this.commandWithLaunchLease(command), }); const sharedOptions = { ...options, sessionStore: this.sessionStore, agentRegistry: this.scopedAgentRegistry, }; - this.delegate = new BaseAcpxRuntime(sharedOptions, testOptions); + this.delegate = new BaseAcpxRuntime( + sharedOptions, + delegateTestOptions as BaseAcpxRuntimeTestOptions, + ); this.bridgeSafeDelegate = shouldUseDistinctBridgeDelegate(options) ? new BaseAcpxRuntime( { ...sharedOptions, mcpServers: [], }, - testOptions, + delegateTestOptions as BaseAcpxRuntimeTestOptions, ) : this.delegate; this.probeDelegate = this.resolveDelegateForAgent(resolveProbeAgentName(options)); @@ -445,6 +624,13 @@ export class AcpxRuntime implements AcpRuntime { private async resolveDelegateForHandle(handle: AcpRuntimeHandle): Promise { const record = await this.sessionStore.load(handle.acpxRecordId ?? handle.sessionKey); + return this.resolveDelegateForLoadedRecord(handle, record); + } + + private resolveDelegateForLoadedRecord( + handle: AcpRuntimeHandle, + record: AcpLoadedSessionRecord, + ): BaseAcpxRuntime { const recordCommand = readAgentCommandFromRecord(record); if (recordCommand) { return this.resolveDelegateForCommand(recordCommand); @@ -464,6 +650,150 @@ export class AcpxRuntime implements AcpRuntime { }); } + private commandWithLaunchLease(command: string | undefined): string | undefined { + const launch = this.launchLeaseScope.getStore(); + if (!command || !launch) { + return command; + } + launch.stableCommand = command; + return withAcpxLeaseEnvironment({ + command, + leaseId: launch.leaseId, + gatewayInstanceId: launch.gatewayInstanceId, + }); + } + + private async canReuseStablePersistentSession(params: { + sessionKey: string; + mode: Parameters[0]["mode"]; + cwd: string | undefined; + command: string | undefined; + resumeSessionId: string | undefined; + }): Promise { + if (params.mode !== "persistent" || !params.command) { + return false; + } + const existing = await this.sessionStore.load(params.sessionKey); + if (!existing || readRecordResetOnNextEnsure(existing)) { + return false; + } + const recordCwd = readRecordCwd(existing); + if (!recordCwd || resolvePath(recordCwd) !== resolvePath(params.cwd?.trim() || this.cwd)) { + return false; + } + if (readRecordAgentCommand(existing) !== params.command) { + return false; + } + const existingSessionId = + typeof existing === "object" && existing !== null + ? (existing as { acpSessionId?: unknown }).acpSessionId + : undefined; + return !params.resumeSessionId || existingSessionId === params.resumeSessionId; + } + + private async runWithLaunchLease(params: { + sessionKey: string; + command: string | undefined; + enabled?: boolean; + run: () => Promise; + }): Promise { + if ( + params.enabled === false || + !params.command || + !this.wrapperRoot || + !this.gatewayInstanceId || + !this.processLeaseStore || + !isOpenClawOwnedAcpxProcessCommand({ + command: params.command, + wrapperRoot: this.wrapperRoot, + }) + ) { + return await params.run(); + } + const launch: AcpxLaunchLeaseContext = { + leaseId: createAcpxProcessLeaseId(), + gatewayInstanceId: this.gatewayInstanceId, + sessionKey: params.sessionKey, + wrapperRoot: this.wrapperRoot, + stableCommand: params.command, + }; + // The pending lease is written before acpx spawns. The session-store save + // path fills in the live PID after acpx connects and exposes the process. + await this.processLeaseStore.save({ + leaseId: launch.leaseId, + gatewayInstanceId: launch.gatewayInstanceId, + sessionKey: launch.sessionKey, + wrapperRoot: launch.wrapperRoot, + wrapperPath: extractGeneratedWrapperPath(params.command), + rootPid: 0, + commandHash: hashAcpxProcessCommand(params.command), + startedAt: Date.now(), + state: "open", + }); + return await this.launchLeaseScope.run(launch, params.run); + } + + private async cleanupProcessTreeForRecord( + handle: AcpRuntimeHandle, + record: AcpLoadedSessionRecord, + ): Promise { + const leaseId = readOpenClawLeaseIdFromRecord(record); + const rootPid = readAgentPidFromRecord(record); + const sessionKeys = [handle.sessionKey, readSessionRecordName(record)]; + const openLeases = + this.gatewayInstanceId && this.processLeaseStore + ? await this.processLeaseStore.listOpen(this.gatewayInstanceId) + : []; + const selectedLease = selectCurrentSessionLease({ + leases: openLeases, + sessionKeys, + rootPid, + }); + const loadedLease = leaseId ? await this.processLeaseStore?.load(leaseId) : undefined; + const lease = + selectedLease ?? + (loadedLease && + loadedLease.gatewayInstanceId === this.gatewayInstanceId && + (!rootPid || loadedLease.rootPid === rootPid) && + sessionKeys.includes(loadedLease.sessionKey) + ? loadedLease + : undefined); + if (lease && lease.gatewayInstanceId === this.gatewayInstanceId && lease.rootPid > 0) { + await this.processLeaseStore?.markState(lease.leaseId, "closing"); + const result = await cleanupOpenClawOwnedAcpxProcessTree({ + rootPid: lease.rootPid, + rootCommand: readAgentCommandFromRecord(record), + expectedLeaseId: lease.leaseId, + expectedGatewayInstanceId: lease.gatewayInstanceId, + wrapperRoot: lease.wrapperRoot, + deps: this.processCleanupDeps, + }); + await this.processLeaseStore?.markState( + lease.leaseId, + result.terminatedPids.length > 0 || result.skippedReason === "missing-root" + ? "closed" + : "lost", + ); + return; + } + + const rootCommand = + readAgentCommandFromRecord(record) ?? + resolveAgentCommandForName({ + agentName: readAgentFromHandle(handle), + agentRegistry: this.agentRegistry, + }); + if (!rootPid || !rootCommand) { + return; + } + await cleanupOpenClawOwnedAcpxProcessTree({ + rootPid, + rootCommand, + wrapperRoot: this.wrapperRoot, + deps: this.processCleanupDeps, + }); + } + isHealthy(): boolean { return this.probeDelegate.isHealthy(); } @@ -489,9 +819,25 @@ export class AcpxRuntime implements AcpRuntime { normalizeAgentName(input.agent) === CODEX_ACP_AGENT_ID && isCodexAcpCommand(command) ? normalizeCodexAcpModelOverride(input.model, input.thinking) : undefined; + const stableLaunchCommand = + codexModelOverride && command + ? appendCodexAcpConfigOverrides(command, codexModelOverride) + : command; + const shouldStartWithLease = !(await this.canReuseStablePersistentSession({ + sessionKey: input.sessionKey, + mode: input.mode, + cwd: input.cwd, + command: stableLaunchCommand, + resumeSessionId: input.resumeSessionId, + })); if (!codexModelOverride) { - return delegate.ensureSession(input); + return await this.runWithLaunchLease({ + sessionKey: input.sessionKey, + command: stableLaunchCommand, + enabled: shouldStartWithLease, + run: () => delegate.ensureSession(input), + }); } const normalizedInput = { @@ -500,9 +846,15 @@ export class AcpxRuntime implements AcpRuntime { ? { model: codexAcpSessionModelId(codexModelOverride) } : {}), }; - return this.codexAcpModelOverrideScope.run(codexModelOverride, () => - delegate.ensureSession(normalizedInput), - ); + return await this.runWithLaunchLease({ + sessionKey: input.sessionKey, + command: stableLaunchCommand, + enabled: shouldStartWithLease, + run: () => + this.codexAcpModelOverrideScope.run(codexModelOverride, () => + delegate.ensureSession(normalizedInput), + ), + }); } async *runTurn(input: Parameters[0]): AsyncIterable { @@ -571,7 +923,10 @@ export class AcpxRuntime implements AcpRuntime { } async cancel(input: Parameters[0]): Promise { - const delegate = await this.resolveDelegateForHandle(input.handle); + const record = await this.sessionStore.load( + input.handle.acpxRecordId ?? input.handle.sessionKey, + ); + const delegate = this.resolveDelegateForLoadedRecord(input.handle, record); await delegate.cancel(input); } @@ -580,14 +935,21 @@ export class AcpxRuntime implements AcpRuntime { } async close(input: Parameters[0]): Promise { - await ( - await this.resolveDelegateForHandle(input.handle) - ).close({ - handle: input.handle, - reason: input.reason, - discardPersistentState: input.discardPersistentState, - }); - if (input.discardPersistentState) { + const record = await this.sessionStore.load( + input.handle.acpxRecordId ?? input.handle.sessionKey, + ); + let closeSucceeded = false; + try { + await this.resolveDelegateForLoadedRecord(input.handle, record).close({ + handle: input.handle, + reason: input.reason, + discardPersistentState: input.discardPersistentState, + }); + closeSucceeded = true; + } finally { + await this.cleanupProcessTreeForRecord(input.handle, record); + } + if (closeSucceeded && input.discardPersistentState) { this.sessionStore.markFresh(input.handle.sessionKey); } } diff --git a/extensions/acpx/src/service.test.ts b/extensions/acpx/src/service.test.ts index 085b592375f..88c55d1c577 100644 --- a/extensions/acpx/src/service.test.ts +++ b/extensions/acpx/src/service.test.ts @@ -11,6 +11,30 @@ const { prepareAcpxCodexAuthConfigMock } = vi.hoisted(() => ({ async ({ pluginConfig }: { pluginConfig: unknown }) => pluginConfig, ), })); +const { cleanupOpenClawOwnedAcpxProcessTreeMock } = vi.hoisted(() => ({ + cleanupOpenClawOwnedAcpxProcessTreeMock: vi.fn( + async (): Promise<{ + inspectedPids: number[]; + terminatedPids: number[]; + skippedReason?: string; + }> => ({ + inspectedPids: [], + terminatedPids: [], + }), + ), +})); +const { reapStaleOpenClawOwnedAcpxOrphansMock } = vi.hoisted(() => ({ + reapStaleOpenClawOwnedAcpxOrphansMock: vi.fn( + async (): Promise<{ + inspectedPids: number[]; + terminatedPids: number[]; + skippedReason?: string; + }> => ({ + inspectedPids: [], + terminatedPids: [], + }), + ), +})); const { acpxRuntimeConstructorMock, createAgentRegistryMock, createFileSessionStoreMock } = vi.hoisted(() => ({ acpxRuntimeConstructorMock: vi.fn(function AcpxRuntime(options: unknown) { @@ -59,6 +83,11 @@ vi.mock("./codex-auth-bridge.js", () => ({ prepareAcpxCodexAuthConfig: prepareAcpxCodexAuthConfigMock, })); +vi.mock("./process-reaper.js", () => ({ + cleanupOpenClawOwnedAcpxProcessTree: cleanupOpenClawOwnedAcpxProcessTreeMock, + reapStaleOpenClawOwnedAcpxOrphans: reapStaleOpenClawOwnedAcpxOrphansMock, +})); + import { getAcpRuntimeBackend } from "../runtime-api.js"; import { createAcpxRuntimeService } from "./service.js"; @@ -73,6 +102,8 @@ async function makeTempDir(): Promise { afterEach(async () => { runtimeRegistry.clear(); prepareAcpxCodexAuthConfigMock.mockClear(); + cleanupOpenClawOwnedAcpxProcessTreeMock.mockClear(); + reapStaleOpenClawOwnedAcpxOrphansMock.mockClear(); acpxRuntimeConstructorMock.mockClear(); createAgentRegistryMock.mockClear(); createFileSessionStoreMock.mockClear(); @@ -155,6 +186,123 @@ describe("createAcpxRuntimeService", () => { await service.stop?.(ctx); }); + it("reaps stale ACPX process leases from the generated wrapper root at startup", async () => { + const workspaceDir = await makeTempDir(); + const ctx = createServiceContext(workspaceDir); + const runtime = createMockRuntime(); + const processCleanupDeps = { sleep: vi.fn(async () => {}) }; + await fs.mkdir(path.join(ctx.stateDir, "acpx"), { recursive: true }); + await fs.writeFile(path.join(ctx.stateDir, "gateway-instance-id"), "gw-test\n"); + await fs.writeFile( + path.join(ctx.stateDir, "acpx", "process-leases.json"), + JSON.stringify({ + version: 1, + leases: [ + { + leaseId: "lease-1", + gatewayInstanceId: "gw-test", + sessionKey: "agent:codex:acp:test", + wrapperRoot: path.join(ctx.stateDir, "acpx"), + wrapperPath: path.join(ctx.stateDir, "acpx", "codex-acp-wrapper.mjs"), + rootPid: 101, + commandHash: "hash", + startedAt: 1, + state: "open", + }, + ], + }), + ); + cleanupOpenClawOwnedAcpxProcessTreeMock.mockResolvedValueOnce({ + inspectedPids: [101, 102], + terminatedPids: [101, 102], + }); + const service = createAcpxRuntimeService({ + runtimeFactory: () => runtime as never, + processCleanupDeps, + }); + + await service.start(ctx); + + expect(cleanupOpenClawOwnedAcpxProcessTreeMock).toHaveBeenCalledWith({ + rootPid: 101, + expectedLeaseId: "lease-1", + expectedGatewayInstanceId: "gw-test", + wrapperRoot: path.join(ctx.stateDir, "acpx"), + deps: processCleanupDeps, + }); + expect(ctx.logger.info).toHaveBeenCalledWith("reaped 2 stale OpenClaw-owned ACPX processes"); + + await service.stop?.(ctx); + }); + + it("runs wrapper-root orphan cleanup before dropping pending ACPX leases", async () => { + const workspaceDir = await makeTempDir(); + const ctx = createServiceContext(workspaceDir); + const runtime = createMockRuntime(); + const processCleanupDeps = { sleep: vi.fn(async () => {}) }; + const wrapperRoot = path.join(ctx.stateDir, "acpx"); + await fs.mkdir(wrapperRoot, { recursive: true }); + await fs.writeFile(path.join(ctx.stateDir, "gateway-instance-id"), "gw-test\n"); + await fs.writeFile( + path.join(wrapperRoot, "process-leases.json"), + JSON.stringify({ + version: 1, + leases: [ + { + leaseId: "lease-pending", + gatewayInstanceId: "gw-test", + sessionKey: "agent:codex:acp:test", + wrapperRoot, + wrapperPath: path.join(wrapperRoot, "codex-acp-wrapper.mjs"), + rootPid: 0, + commandHash: "hash", + startedAt: 1, + state: "open", + }, + ], + }), + ); + reapStaleOpenClawOwnedAcpxOrphansMock.mockResolvedValueOnce({ + inspectedPids: [201, 202], + terminatedPids: [201, 202], + }); + const service = createAcpxRuntimeService({ + runtimeFactory: () => runtime as never, + processCleanupDeps, + }); + + await service.start(ctx); + + expect(cleanupOpenClawOwnedAcpxProcessTreeMock).not.toHaveBeenCalled(); + expect(reapStaleOpenClawOwnedAcpxOrphansMock).toHaveBeenCalledWith({ + wrapperRoot, + deps: processCleanupDeps, + }); + expect(ctx.logger.info).toHaveBeenCalledWith("reaped 2 stale OpenClaw-owned ACPX processes"); + const leaseFile = JSON.parse( + await fs.readFile(path.join(wrapperRoot, "process-leases.json"), "utf8"), + ); + expect(leaseFile.leases[0].state).toBe("closed"); + + await service.stop?.(ctx); + }); + + it("keeps startup quiet when no process leases are open", async () => { + const workspaceDir = await makeTempDir(); + const ctx = createServiceContext(workspaceDir); + const runtime = createMockRuntime(); + const service = createAcpxRuntimeService({ + runtimeFactory: () => runtime as never, + }); + + await service.start(ctx); + + expect(cleanupOpenClawOwnedAcpxProcessTreeMock).not.toHaveBeenCalled(); + expect(ctx.logger.warn).not.toHaveBeenCalled(); + + await service.stop?.(ctx); + }); + it("registers the default backend without importing ACPX runtime until first use", async () => { const workspaceDir = await makeTempDir(); const ctx = createServiceContext(workspaceDir); diff --git a/extensions/acpx/src/service.ts b/extensions/acpx/src/service.ts index 1c989bfce8a..2a06060f5d1 100644 --- a/extensions/acpx/src/service.ts +++ b/extensions/acpx/src/service.ts @@ -1,4 +1,6 @@ +import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; +import path from "node:path"; import { inspect } from "node:util"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { @@ -14,6 +16,12 @@ import { toAcpMcpServers, type ResolvedAcpxPluginConfig, } from "./config.js"; +import { createAcpxProcessLeaseStore, type AcpxProcessLeaseStore } from "./process-lease.js"; +import { + cleanupOpenClawOwnedAcpxProcessTree, + reapStaleOpenClawOwnedAcpxOrphans, + type AcpxProcessCleanupDeps, +} from "./process-reaper.js"; type AcpxRuntimeLike = AcpRuntime & { probeAvailability(): Promise; @@ -33,12 +41,16 @@ let runtimeModulePromise: Promise | null = null; type AcpxRuntimeFactoryParams = { pluginConfig: ResolvedAcpxPluginConfig; + gatewayInstanceId: string; + processLeaseStore: AcpxProcessLeaseStore; + wrapperRoot: string; logger?: PluginLogger; }; type CreateAcpxRuntimeServiceParams = { pluginConfig?: unknown; runtimeFactory?: (params: AcpxRuntimeFactoryParams) => AcpxRuntimeLike | Promise; + processCleanupDeps?: AcpxProcessCleanupDeps; }; function loadRuntimeModule(): Promise { @@ -57,6 +69,9 @@ function createLazyDefaultRuntime(params: AcpxRuntimeFactoryParams): AcpxRuntime runtimePromise ??= loadRuntimeModule().then((module) => { runtime = new module.AcpxRuntime({ cwd: params.pluginConfig.cwd, + openclawGatewayInstanceId: params.gatewayInstanceId, + openclawProcessLeaseStore: params.processLeaseStore, + openclawWrapperRoot: params.wrapperRoot, sessionStore: module.createFileSessionStore({ stateDir: params.pluginConfig.stateDir, }), @@ -188,6 +203,73 @@ function shouldRunStartupProbe(env: NodeJS.ProcessEnv = process.env): boolean { return env[ENABLE_STARTUP_PROBE_ENV] === "1"; } +async function resolveGatewayInstanceId(stateDir: string): Promise { + const filePath = path.join(stateDir, "gateway-instance-id"); + try { + const existing = (await fs.readFile(filePath, "utf8")).trim(); + if (existing) { + return existing; + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + } + const next = randomUUID(); + await fs.mkdir(stateDir, { recursive: true }); + await fs.writeFile(filePath, `${next}\n`, { mode: 0o600 }); + return next; +} + +async function reapOpenAcpxProcessLeases(params: { + gatewayInstanceId: string; + leaseStore: AcpxProcessLeaseStore; + deps?: AcpxProcessCleanupDeps; +}): Promise<{ inspectedPids: number[]; terminatedPids: number[] }> { + const leases = await params.leaseStore.listOpen(params.gatewayInstanceId); + const inspectedPids: number[] = []; + const terminatedPids: number[] = []; + const pendingLeaseRootResults = new Map< + string, + { inspectedPids: number[]; terminatedPids: number[] } + >(); + for (const lease of leases) { + if (lease.rootPid <= 0) { + await params.leaseStore.markState(lease.leaseId, "closing"); + let result = pendingLeaseRootResults.get(lease.wrapperRoot); + if (!result) { + result = await reapStaleOpenClawOwnedAcpxOrphans({ + wrapperRoot: lease.wrapperRoot, + deps: params.deps, + }); + pendingLeaseRootResults.set(lease.wrapperRoot, result); + inspectedPids.push(...result.inspectedPids); + terminatedPids.push(...result.terminatedPids); + } + await params.leaseStore.markState( + lease.leaseId, + result.terminatedPids.length > 0 ? "closed" : "lost", + ); + continue; + } + await params.leaseStore.markState(lease.leaseId, "closing"); + const result = await cleanupOpenClawOwnedAcpxProcessTree({ + rootPid: lease.rootPid, + expectedLeaseId: lease.leaseId, + expectedGatewayInstanceId: lease.gatewayInstanceId, + wrapperRoot: lease.wrapperRoot, + deps: params.deps, + }); + inspectedPids.push(...result.inspectedPids); + terminatedPids.push(...result.terminatedPids); + await params.leaseStore.markState( + lease.leaseId, + result.terminatedPids.length > 0 ? "closed" : "lost", + ); + } + return { inspectedPids, terminatedPids }; +} + export function createAcpxRuntimeService( params: CreateAcpxRuntimeServiceParams = {}, ): OpenClawPluginService { @@ -215,7 +297,21 @@ export function createAcpxRuntimeService( stateDir: ctx.stateDir, logger: ctx.logger, }); + const wrapperRoot = path.join(ctx.stateDir, "acpx"); await fs.mkdir(pluginConfig.stateDir, { recursive: true }); + await fs.mkdir(wrapperRoot, { recursive: true }); + const gatewayInstanceId = await resolveGatewayInstanceId(ctx.stateDir); + const processLeaseStore = createAcpxProcessLeaseStore({ stateDir: wrapperRoot }); + const startupReap = await reapOpenAcpxProcessLeases({ + gatewayInstanceId, + leaseStore: processLeaseStore, + deps: params.processCleanupDeps, + }); + if (startupReap.terminatedPids.length > 0) { + ctx.logger.info( + `reaped ${startupReap.terminatedPids.length} stale OpenClaw-owned ACPX process${startupReap.terminatedPids.length === 1 ? "" : "es"}`, + ); + } warnOnIgnoredLegacyCompatibilityConfig({ pluginConfig, logger: ctx.logger, @@ -224,10 +320,16 @@ export function createAcpxRuntimeService( runtime = params.runtimeFactory ? await params.runtimeFactory({ pluginConfig, + gatewayInstanceId, + processLeaseStore, + wrapperRoot, logger: ctx.logger, }) : createLazyDefaultRuntime({ pluginConfig, + gatewayInstanceId, + processLeaseStore, + wrapperRoot, logger: ctx.logger, }); diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index 445c949cc9a..58e6669e02d 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -735,20 +735,23 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con const includeDisabled = Boolean(params.includeDisabled); let offset = 0; let result: unknown; - for (;;) { + let shouldContinue = true; + while (shouldContinue) { result = await callGateway("cron.list", gatewayOpts, { includeDisabled, agentId: listAgentId, ...(selfRemoveOnlyJobId ? { limit: 200, offset } : {}), }); if (!selfRemoveOnlyJobId || cronListResultHasJob(result, selfRemoveOnlyJobId)) { - break; + shouldContinue = false; + } else { + const nextOffset = readCronListNextOffset(result, offset); + if (nextOffset === undefined) { + shouldContinue = false; + } else { + offset = nextOffset; + } } - const nextOffset = readCronListNextOffset(result, offset); - if (nextOffset === undefined) { - break; - } - offset = nextOffset; } return jsonResult( selfRemoveOnlyJobId ? filterCronListResultToJobId(result, selfRemoveOnlyJobId) : result, diff --git a/src/agents/tools/sessions-access.test.ts b/src/agents/tools/sessions-access.test.ts index 533c5b70b0b..79685beb722 100644 --- a/src/agents/tools/sessions-access.test.ts +++ b/src/agents/tools/sessions-access.test.ts @@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../../config/config.js"; import { createAgentToAgentPolicy, createSessionVisibilityGuard, + createSessionVisibilityRowChecker, resolveEffectiveSessionToolsVisibility, resolveSandboxSessionToolsVisibility, resolveSessionToolsVisibility, @@ -109,6 +110,175 @@ describe("createAgentToAgentPolicy", () => { }); describe("createSessionVisibilityGuard", () => { + it("allows cross-agent spawned child rows in list results with tree visibility", () => { + const guard = createSessionVisibilityRowChecker({ + action: "list", + requesterSessionKey: "agent:main:main", + visibility: "tree", + a2aPolicy: createAgentToAgentPolicy({} as unknown as OpenClawConfig), + }); + + expect( + guard.check({ + key: "agent:codex:acp:child-1", + spawnedBy: "agent:main:main", + }), + ).toEqual({ allowed: true }); + }); + + it("allows cross-agent spawned child rows in all-visibility list results when a2a is disabled", () => { + const guard = createSessionVisibilityRowChecker({ + action: "list", + requesterSessionKey: "agent:main:main", + visibility: "all", + a2aPolicy: createAgentToAgentPolicy({ + tools: { agentToAgent: { enabled: false } }, + } as unknown as OpenClawConfig), + }); + + expect( + guard.check({ + key: "agent:codex:acp:child-1", + spawnedBy: "agent:main:main", + }), + ).toEqual({ allowed: true }); + }); + + it("keeps agent visibility same-agent-only for cross-agent owned child rows", () => { + const guard = createSessionVisibilityRowChecker({ + action: "list", + requesterSessionKey: "agent:main:main", + visibility: "agent", + a2aPolicy: createAgentToAgentPolicy({ + tools: { agentToAgent: { enabled: true, allow: ["main", "codex"] } }, + } as unknown as OpenClawConfig), + }); + + expect( + guard.check({ + key: "agent:codex:acp:child-1", + spawnedBy: "agent:main:main", + }), + ).toEqual({ + allowed: false, + status: "forbidden", + error: + "Session list visibility is restricted. Set tools.sessions.visibility=all to allow cross-agent access.", + }); + }); + + it("does not do spawned lookup for list visibility without row metadata", async () => { + const callGateway = vi.fn(async () => ({ + sessions: [{ key: "agent:codex:acp:child-1" }], + })); + sessionsResolutionTesting.setDepsForTest({ + callGateway: callGateway as never, + }); + + const guard = await createSessionVisibilityGuard({ + action: "list", + requesterSessionKey: "agent:main:main", + visibility: "tree", + a2aPolicy: createAgentToAgentPolicy({} as unknown as OpenClawConfig), + }); + + expect(guard.check("agent:codex:acp:child-1")).toMatchObject({ allowed: false }); + expect(callGateway).not.toHaveBeenCalled(); + + sessionsResolutionTesting.setDepsForTest(); + }); + + it("allows cross-agent spawned child sessions with tree visibility", async () => { + sessionsResolutionTesting.setDepsForTest({ + callGateway: vi.fn(async (request: { method?: string; params?: { spawnedBy?: string } }) => { + if (request.method === "sessions.list") { + expect(request.params?.spawnedBy).toBe("agent:main:main"); + return { + sessions: [{ key: "agent:codex:acp:child-1" }], + }; + } + return {}; + }) as never, + }); + + const guard = await createSessionVisibilityGuard({ + action: "history", + requesterSessionKey: "agent:main:main", + visibility: "tree", + a2aPolicy: createAgentToAgentPolicy({} as unknown as OpenClawConfig), + }); + + expect(guard.check("agent:codex:acp:child-1")).toEqual({ allowed: true }); + + sessionsResolutionTesting.setDepsForTest(); + }); + + it("keeps self visibility restricted even for spawned child sessions", async () => { + const guard = await createSessionVisibilityGuard({ + action: "history", + requesterSessionKey: "agent:main:main", + visibility: "self", + a2aPolicy: createAgentToAgentPolicy({} as unknown as OpenClawConfig), + }); + + expect(guard.check("agent:codex:acp:child-1")).toEqual({ + allowed: false, + status: "forbidden", + error: + "Session history visibility is restricted. Set tools.sessions.visibility=all to allow cross-agent access.", + }); + }); + + it("allows cross-agent spawned child sessions before agent-to-agent checks with all visibility", async () => { + sessionsResolutionTesting.setDepsForTest({ + callGateway: vi.fn(async (request: { method?: string; params?: { spawnedBy?: string } }) => { + if (request.method === "sessions.list") { + expect(request.params?.spawnedBy).toBe("agent:main:main"); + return { + sessions: [{ key: "agent:codex:acp:child-1" }], + }; + } + return {}; + }) as never, + }); + + const guard = await createSessionVisibilityGuard({ + action: "send", + requesterSessionKey: "agent:main:main", + visibility: "all", + a2aPolicy: createAgentToAgentPolicy({} as unknown as OpenClawConfig), + }); + + expect(guard.check("agent:codex:acp:child-1")).toEqual({ allowed: true }); + + sessionsResolutionTesting.setDepsForTest(); + }); + + it("allows cross-agent spawned child status before agent-to-agent checks with all visibility", async () => { + sessionsResolutionTesting.setDepsForTest({ + callGateway: vi.fn(async (request: { method?: string; params?: { spawnedBy?: string } }) => { + if (request.method === "sessions.list") { + expect(request.params?.spawnedBy).toBe("agent:main:main"); + return { + sessions: [{ key: "agent:codex:acp:child-1" }], + }; + } + return {}; + }) as never, + }); + + const guard = await createSessionVisibilityGuard({ + action: "status", + requesterSessionKey: "agent:main:main", + visibility: "all", + a2aPolicy: createAgentToAgentPolicy({} as unknown as OpenClawConfig), + }); + + expect(guard.check("agent:codex:acp:child-1")).toEqual({ allowed: true }); + + sessionsResolutionTesting.setDepsForTest(); + }); + it("does not block exact same-agent spawned targets that fall past the spawned list cap", async () => { sessionsResolutionTesting.setDepsForTest({ callGateway: vi.fn(async (request: { method?: string; params?: { key?: string } }) => { diff --git a/src/agents/tools/sessions-access.ts b/src/agents/tools/sessions-access.ts index 63f6eeeecd5..46aa69ad0d0 100644 --- a/src/agents/tools/sessions-access.ts +++ b/src/agents/tools/sessions-access.ts @@ -3,6 +3,7 @@ import { createAgentToAgentPolicy, createSessionVisibilityChecker, createSessionVisibilityGuard, + createSessionVisibilityRowChecker, listSpawnedSessionKeys, resolveEffectiveSessionToolsVisibility, resolveSandboxSessionToolsVisibility, @@ -15,6 +16,7 @@ export { createAgentToAgentPolicy, createSessionVisibilityChecker, createSessionVisibilityGuard, + createSessionVisibilityRowChecker, listSpawnedSessionKeys, resolveEffectiveSessionToolsVisibility, } from "../../plugin-sdk/session-visibility.js"; diff --git a/src/agents/tools/sessions-helpers.ts b/src/agents/tools/sessions-helpers.ts index 2f7b96feb88..f05e5d4e5f7 100644 --- a/src/agents/tools/sessions-helpers.ts +++ b/src/agents/tools/sessions-helpers.ts @@ -1,6 +1,7 @@ export { createAgentToAgentPolicy, createSessionVisibilityGuard, + createSessionVisibilityRowChecker, resolveEffectiveSessionToolsVisibility, resolveSandboxedSessionToolContext, } from "./sessions-access.js"; diff --git a/src/agents/tools/sessions-list-tool.ts b/src/agents/tools/sessions-list-tool.ts index dc046dbbec2..02309a9e934 100644 --- a/src/agents/tools/sessions-list-tool.ts +++ b/src/agents/tools/sessions-list-tool.ts @@ -22,8 +22,8 @@ import { import type { AnyAgentTool } from "./common.js"; import { jsonResult, readStringArrayParam, readStringParam } from "./common.js"; import { - createSessionVisibilityGuard, createAgentToAgentPolicy, + createSessionVisibilityRowChecker, classifySessionKind, deriveChannel, resolveDisplaySessionKey, @@ -136,7 +136,7 @@ export function createSessionsListTool(opts?: { const sessions = Array.isArray(list?.sessions) ? list.sessions : []; const storePath = typeof list?.path === "string" ? list.path : undefined; - const visibilityGuard = await createSessionVisibilityGuard({ + const visibilityGuard = createSessionVisibilityRowChecker({ action: "list", requesterSessionKey: effectiveRequesterKey, visibility, @@ -160,7 +160,17 @@ export function createSessionsListTool(opts?: { if (!key) { continue; } - const access = visibilityGuard.check(key); + const access = visibilityGuard.check({ + key, + agentId: typeof entry.agentId === "string" ? entry.agentId : undefined, + ownerSessionKey: + typeof (entry as { ownerSessionKey?: unknown }).ownerSessionKey === "string" + ? (entry as { ownerSessionKey?: string }).ownerSessionKey + : undefined, + spawnedBy: typeof entry.spawnedBy === "string" ? entry.spawnedBy : undefined, + parentSessionKey: + typeof entry.parentSessionKey === "string" ? entry.parentSessionKey : undefined, + }); if (!access.allowed) { continue; } diff --git a/src/agents/tools/sessions-send-tool.ts b/src/agents/tools/sessions-send-tool.ts index 26ed967a54d..0f14d5b86da 100644 --- a/src/agents/tools/sessions-send-tool.ts +++ b/src/agents/tools/sessions-send-tool.ts @@ -269,6 +269,15 @@ export function createSessionsSendTool(opts?: { const announceTimeoutMs = timeoutSeconds === 0 ? 30_000 : timeoutMs; const idempotencyKey = crypto.randomUUID(); let runId: string = idempotencyKey; + if (parseSessionThreadInfoFast(resolvedKey).threadId) { + return jsonResult({ + runId: crypto.randomUUID(), + status: "error", + error: + "sessions_send cannot target a thread session for inter-agent coordination. Use the parent channel session key instead.", + sessionKey: displayKey, + }); + } const visibilityGuard = await createSessionVisibilityGuard({ action: "send", requesterSessionKey: effectiveRequesterKey, @@ -284,15 +293,6 @@ export function createSessionsSendTool(opts?: { sessionKey: displayKey, }); } - if (parseSessionThreadInfoFast(resolvedKey).threadId) { - return jsonResult({ - runId: crypto.randomUUID(), - status: "error", - error: - "sessions_send cannot target a thread session for inter-agent coordination. Use the parent channel session key instead.", - sessionKey: displayKey, - }); - } // Capture the pre-run assistant snapshot before starting the nested run. // Fast in-process test doubles and short-circuit agent paths can finish diff --git a/src/agents/tools/sessions.test.ts b/src/agents/tools/sessions.test.ts index bd0a0f142e0..01d33e1f340 100644 --- a/src/agents/tools/sessions.test.ts +++ b/src/agents/tools/sessions.test.ts @@ -14,7 +14,7 @@ type SessionsToolTestConfig = { session: { scope: "per-sender"; mainKey: string }; tools: { agentToAgent: { enabled: boolean }; - sessions?: { visibility: "all" | "own" }; + sessions?: { visibility: "self" | "tree" | "agent" | "all" }; }; }; @@ -417,13 +417,20 @@ describe("resolveAnnounceTarget", () => { describe("sessions_list gating", () => { beforeEach(() => { callGatewayMock.mockClear(); - callGatewayMock.mockResolvedValue({ - path: "/tmp/sessions.json", - sessions: [ - { key: "agent:main:main", kind: "direct" }, - { key: "agent:other:main", kind: "direct" }, - ], - }); + callGatewayMock.mockImplementation( + (request: { method?: string; params?: { spawnedBy?: string } }) => { + if (request.method === "sessions.list" && request.params?.spawnedBy) { + return Promise.resolve({ path: "/tmp/sessions.json", sessions: [] }); + } + return Promise.resolve({ + path: "/tmp/sessions.json", + sessions: [ + { key: "agent:main:main", kind: "direct" }, + { key: "agent:other:main", kind: "direct" }, + ], + }); + }, + ); }); it("filters out other agents when tools.agentToAgent.enabled is false", async () => { @@ -435,6 +442,62 @@ describe("sessions_list gating", () => { }); }); + it("keeps requester-owned cross-agent rows with tree visibility without a spawned lookup", async () => { + loadConfigMock.mockReturnValue({ + session: { scope: "per-sender", mainKey: "main" }, + tools: { + agentToAgent: { enabled: false }, + sessions: { visibility: "tree" }, + }, + }); + callGatewayMock.mockResolvedValueOnce({ + path: "/tmp/sessions.json", + sessions: [ + { + key: "agent:codex:acp:child-1", + kind: "direct", + spawnedBy: MAIN_AGENT_SESSION_KEY, + }, + ], + }); + + const result = await createMainSessionsListTool().execute("call1", {}); + + expect(result.details).toMatchObject({ + count: 1, + sessions: [{ key: "agent:codex:acp:child-1", spawnedBy: MAIN_AGENT_SESSION_KEY }], + }); + expect(callGatewayMock).toHaveBeenCalledTimes(1); + }); + + it("keeps requester-owned cross-agent rows with all visibility when a2a is disabled", async () => { + loadConfigMock.mockReturnValue({ + session: { scope: "per-sender", mainKey: "main" }, + tools: { + agentToAgent: { enabled: false }, + sessions: { visibility: "all" }, + }, + }); + callGatewayMock.mockResolvedValueOnce({ + path: "/tmp/sessions.json", + sessions: [ + { + key: "agent:codex:acp:child-1", + kind: "direct", + parentSessionKey: MAIN_AGENT_SESSION_KEY, + }, + ], + }); + + const result = await createMainSessionsListTool().execute("call1", {}); + + expect(result.details).toMatchObject({ + count: 1, + sessions: [{ key: "agent:codex:acp:child-1", parentSessionKey: MAIN_AGENT_SESSION_KEY }], + }); + expect(callGatewayMock).toHaveBeenCalledTimes(1); + }); + it("keeps literal current keys for message previews", async () => { callGatewayMock.mockReset(); callGatewayMock @@ -442,7 +505,6 @@ describe("sessions_list gating", () => { path: "/tmp/sessions.json", sessions: [{ key: "current", kind: "direct" }], }) - .mockResolvedValueOnce({ sessions: [{ key: "current" }] }) .mockResolvedValueOnce({ messages: [{ role: "assistant", content: [] }] }); await createMainSessionsListTool().execute("call1", { messageLimit: 1 }); @@ -478,7 +540,6 @@ describe("sessions_list transcriptPath resolution", () => { }, ], }); - const result = await executeMainSessionsList(); expectWorkerTranscriptPath(result, { containsPath: path.join("agents", "worker", "sessions"), @@ -498,7 +559,6 @@ describe("sessions_list transcriptPath resolution", () => { }, ], }); - const result = await executeMainSessionsList(); expectWorkerTranscriptPath(result, { containsPath: path.join("agents", "worker", "sessions"), @@ -519,7 +579,6 @@ describe("sessions_list transcriptPath resolution", () => { }, ], }); - const result = await executeMainSessionsList(); expectWorkerTranscriptPath(result, { containsPath: path.join("agents", "worker", "sessions"), @@ -540,7 +599,6 @@ describe("sessions_list transcriptPath resolution", () => { }, ], }); - const result = await executeMainSessionsList(); expectWorkerTranscriptPath(result, { containsPath: path.join(stateDir, "agents", "worker", "sessions"), @@ -562,7 +620,6 @@ describe("sessions_list transcriptPath resolution", () => { }, ], }); - const result = await executeMainSessionsList(); const expectedSessionsDir = path.dirname(templateStorePath.replace("{agentId}", "worker")); expectWorkerTranscriptPath(result, { @@ -595,7 +652,6 @@ describe("sessions_list channel derivation", () => { }, ], }); - const result = await executeMainSessionsList(); expect(result.details).toMatchObject({ diff --git a/src/plugin-sdk/session-visibility.ts b/src/plugin-sdk/session-visibility.ts index 6a6015ccec9..81b71ecce69 100644 --- a/src/plugin-sdk/session-visibility.ts +++ b/src/plugin-sdk/session-visibility.ts @@ -31,6 +31,14 @@ export type SessionAccessResult = | { allowed: true } | { allowed: false; error: string; status: "forbidden" }; +export type SessionVisibilityRow = { + key: string; + agentId?: string; + ownerSessionKey?: string; + spawnedBy?: string; + parentSessionKey?: string; +}; + export async function listSpawnedSessionKeys(params: { requesterSessionKey: string; limit?: number; @@ -191,11 +199,56 @@ export function createSessionVisibilityChecker(params: { a2aPolicy: AgentToAgentPolicy; spawnedKeys: Set | null; }): { check: (targetSessionKey: string) => SessionAccessResult } { - const requesterAgentId = resolveAgentIdFromSessionKey(params.requesterSessionKey); const spawnedKeys = params.spawnedKeys; + const rowChecker = createSessionVisibilityRowChecker({ + action: params.action, + requesterSessionKey: params.requesterSessionKey, + visibility: params.visibility, + a2aPolicy: params.a2aPolicy, + }); const check = (targetSessionKey: string): SessionAccessResult => { - const targetAgentId = resolveAgentIdFromSessionKey(targetSessionKey); + const isSpawnedSession = spawnedKeys?.has(targetSessionKey) === true; + return rowChecker.check({ + key: targetSessionKey, + spawnedBy: isSpawnedSession ? params.requesterSessionKey : undefined, + }); + }; + + return { check }; +} + +function rowOwnedByRequester(row: SessionVisibilityRow, requesterSessionKey: string): boolean { + return ( + row.ownerSessionKey === requesterSessionKey || + row.spawnedBy === requesterSessionKey || + row.parentSessionKey === requesterSessionKey + ); +} + +export function createSessionVisibilityRowChecker(params: { + action: SessionAccessAction; + requesterSessionKey: string; + visibility: SessionToolsVisibility; + a2aPolicy: AgentToAgentPolicy; +}): { check: (row: SessionVisibilityRow) => SessionAccessResult } { + const requesterAgentId = resolveAgentIdFromSessionKey(params.requesterSessionKey); + + const check = (row: SessionVisibilityRow): SessionAccessResult => { + const targetSessionKey = row.key; + const targetAgentId = row.agentId ?? resolveAgentIdFromSessionKey(targetSessionKey); + const isRequesterSession = + targetSessionKey === params.requesterSessionKey || targetSessionKey === "current"; + const isRequesterOwned = rowOwnedByRequester(row, params.requesterSessionKey); + // Row ownership is stronger than agent ids: ACP children may use a backend + // agent id while still belonging to the requester that spawned them. + if ( + !isRequesterSession && + isRequesterOwned && + (params.visibility === "tree" || params.visibility === "all") + ) { + return { allowed: true }; + } const isCrossAgent = targetAgentId !== requesterAgentId; if (isCrossAgent) { if (params.visibility !== "all") { @@ -222,7 +275,7 @@ export function createSessionVisibilityChecker(params: { return { allowed: true }; } - if (params.visibility === "self" && targetSessionKey !== params.requesterSessionKey) { + if (params.visibility === "self" && !isRequesterSession) { return { allowed: false, status: "forbidden", @@ -230,11 +283,7 @@ export function createSessionVisibilityChecker(params: { }; } - if ( - params.visibility === "tree" && - targetSessionKey !== params.requesterSessionKey && - !spawnedKeys?.has(targetSessionKey) - ) { + if (params.visibility === "tree" && !isRequesterSession && !isRequesterOwned) { return { allowed: false, status: "forbidden", @@ -256,8 +305,10 @@ export async function createSessionVisibilityGuard(params: { }): Promise<{ check: (targetSessionKey: string) => SessionAccessResult; }> { + // Listing already has row ownership metadata; direct key actions still need + // this lookup until every caller can pass a normalized session row. const spawnedKeys = - params.visibility === "tree" + params.action !== "list" && (params.visibility === "tree" || params.visibility === "all") ? await listSpawnedSessionKeys({ requesterSessionKey: params.requesterSessionKey }) : null; return createSessionVisibilityChecker({ From 10341c6158c7ca438e1add8116e70fe7f3297061 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 07:35:32 +0100 Subject: [PATCH 053/215] fix(llm-task): resolve model aliases before dispatch --- CHANGELOG.md | 1 + extensions/llm-task/src/llm-task-tool.test.ts | 26 ++++++++++ extensions/llm-task/src/llm-task-tool.ts | 48 ++++++++++++++++++- 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09ef18320e3..afea9ce8465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- llm-task: resolve configured model aliases before embedded dispatch so `model="gemini-flash"` and other aliases route to the intended provider instead of the agent default. Fixes #54166. - CLI backends: keep versioned OAuth identity matches reusable when auth profile ids rotate, so Claude CLI sessions do not reset and lose continuity during same-account OAuth refresh/profile alias changes. Fixes #78541. - Model providers: normalize APNG sniffed PNG uploads, preserve Gemini 3 tool-call thought-signature replay with documented fallback signatures, accept legacy `__env__:VAR` custom-provider keys, and repair snake_case tool-call transcript sanitization. Fixes #51881, #48915, #77566, and #42858. - Telegram/models: parse provider ids containing dots in `/models` callback buttons so `hf.co` model lists render as inline keyboard buttons. Fixes #38745. diff --git a/extensions/llm-task/src/llm-task-tool.test.ts b/extensions/llm-task/src/llm-task-tool.test.ts index 1f52eae3a5b..465040aa344 100644 --- a/extensions/llm-task/src/llm-task-tool.test.ts +++ b/extensions/llm-task/src/llm-task-tool.test.ts @@ -87,6 +87,7 @@ function fakeApi(overrides: any = {}) { runtime: { version: "test", agent: { + defaults: { provider: "openai-codex", model: "gpt-5.2" }, runEmbeddedPiAgent, resolveThinkingPolicy, normalizeThinkingLevel, @@ -191,6 +192,31 @@ describe("llm-task tool (json-only)", () => { expect(call.model).toBe("claude-4-sonnet"); }); + it("resolves configured model aliases before dispatching the embedded run", async () => { + mockEmbeddedRunJson({ ok: true }); + const tool = createLlmTaskTool( + fakeApi({ + config: { + agents: { + defaults: { + workspace: "/tmp", + model: { primary: "anthropic/claude-sonnet-4-6" }, + models: { + "google/gemini-3-flash-preview": { alias: "gemini-flash" }, + }, + }, + }, + }, + }), + ); + + await tool.execute("id", { prompt: "x", model: "gemini-flash" }); + + const call = (runEmbeddedPiAgent as any).mock.calls[0]?.[0]; + expect(call.provider).toBe("google"); + expect(call.model).toBe("gemini-3-flash-preview"); + }); + it("passes thinking override to embedded runner", async () => { mockEmbeddedRunJson({ ok: true }); const call = await executeEmbeddedRun({ prompt: "x", thinking: "high" }); diff --git a/extensions/llm-task/src/llm-task-tool.ts b/extensions/llm-task/src/llm-task-tool.ts index d01fec079c2..7ab86a71931 100644 --- a/extensions/llm-task/src/llm-task-tool.ts +++ b/extensions/llm-task/src/llm-task-tool.ts @@ -1,5 +1,6 @@ import path from "node:path"; import Ajv from "ajv"; +import { buildModelAliasIndex, resolveModelRefFromString } from "openclaw/plugin-sdk/agent-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { Type } from "typebox"; import { resolvePreferredOpenClawTmpDir, withTempWorkspace } from "../api.js"; @@ -42,6 +43,44 @@ function stripDuplicateProviderPrefix(provider: string | undefined, model: strin return m.startsWith(prefix) ? m.slice(prefix.length) : m; } +function resolveLlmTaskModelRef(params: { + api: OpenClawPluginApi; + provider?: string; + rawModel?: string; +}): { provider?: string; model?: string } { + const defaultProvider = + normalizeOptionalString(params.provider) ?? + normalizeOptionalString(params.api.runtime.agent.defaults.provider); + const rawModel = normalizeOptionalString(params.rawModel); + if (!rawModel || !defaultProvider) { + return { + provider: params.provider, + model: stripDuplicateProviderPrefix(params.provider, rawModel), + }; + } + + const cfg = params.api.config; + const aliasIndex = cfg + ? buildModelAliasIndex({ + cfg, + defaultProvider, + }) + : undefined; + const resolved = resolveModelRefFromString({ + cfg, + raw: rawModel, + defaultProvider, + aliasIndex, + }); + if (!resolved) { + return { + provider: params.provider, + model: stripDuplicateProviderPrefix(params.provider, rawModel), + }; + } + return resolved.ref; +} + type PluginCfg = { defaultProvider?: string; defaultModel?: string; @@ -117,7 +156,7 @@ export function createLlmTaskTool(api: OpenClawPluginApi) { const primaryModel = typeof primary === "string" ? primary.split("/").slice(1).join("/") : undefined; - const provider = + const requestedProvider = (typeof params.provider === "string" && params.provider.trim()) || (typeof pluginCfg.defaultProvider === "string" && pluginCfg.defaultProvider.trim()) || primaryProvider || @@ -128,7 +167,12 @@ export function createLlmTaskTool(api: OpenClawPluginApi) { (typeof pluginCfg.defaultModel === "string" && pluginCfg.defaultModel.trim()) || primaryModel || undefined; - const model = stripDuplicateProviderPrefix(provider, rawModel); + const { provider: resolvedProvider, model } = resolveLlmTaskModelRef({ + api, + provider: requestedProvider, + rawModel, + }); + const provider = resolvedProvider; const authProfileId = (typeof params.authProfileId === "string" && params.authProfileId.trim()) || From 64bbe96d88433c5e23565e3d68529982f641aab9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 07:35:50 +0100 Subject: [PATCH 054/215] fix(media): resolve slash-containing generation model overrides --- CHANGELOG.md | 1 + src/agents/tools/image-generate-tool.test.ts | 25 ++++++++ src/agents/tools/media-tool-shared.ts | 41 ++++++++++++- src/media-generation/runtime-shared.test.ts | 21 +++++++ src/media-generation/runtime-shared.ts | 63 ++++++++++++++++++-- 5 files changed, 144 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afea9ce8465..36fc49e2136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -141,6 +141,7 @@ Docs: https://docs.openclaw.ai ### Fixes - llm-task: resolve configured model aliases before embedded dispatch so `model="gemini-flash"` and other aliases route to the intended provider instead of the agent default. Fixes #54166. +- Media generation: resolve slash-containing model-only overrides like `fal-ai/flux/dev` through registered provider model metadata so FAL image/video models do not get misparsed as provider `fal-ai`. Fixes #77444. - CLI backends: keep versioned OAuth identity matches reusable when auth profile ids rotate, so Claude CLI sessions do not reset and lose continuity during same-account OAuth refresh/profile alias changes. Fixes #78541. - Model providers: normalize APNG sniffed PNG uploads, preserve Gemini 3 tool-call thought-signature replay with documented fallback signatures, accept legacy `__env__:VAR` custom-provider keys, and repair snake_case tool-call transcript sanitization. Fixes #51881, #48915, #77566, and #42858. - Telegram/models: parse provider ids containing dots in `/models` callback buttons so `hf.co` model lists render as inline keyboard buttons. Fixes #38745. diff --git a/src/agents/tools/image-generate-tool.test.ts b/src/agents/tools/image-generate-tool.test.ts index ca7a2ca9cc8..aad1375de28 100644 --- a/src/agents/tools/image-generate-tool.test.ts +++ b/src/agents/tools/image-generate-tool.test.ts @@ -1516,6 +1516,31 @@ describe("createImageGenerateTool", () => { expect(generateImage).not.toHaveBeenCalled(); }); + it("uses registered provider metadata for slash-containing model overrides", async () => { + vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([ + createFalEditProvider(), + ]); + const generateImage = vi.spyOn(imageGenerationRuntime, "generateImage"); + vi.spyOn(webMedia, "loadWebMedia").mockResolvedValue({ + kind: "image", + buffer: Buffer.from("input-image"), + contentType: "image/png", + }); + + const tool = createToolWithPrimaryImageModel("fal/fal-ai/flux/dev", { + workspaceDir: process.cwd(), + }); + + await expect( + tool.execute("call-fal-model-only-edit", { + prompt: "combine", + model: "fal-ai/flux/dev", + images: ["./fixtures/a.png", "./fixtures/b.png"], + }), + ).rejects.toThrow("fal edit supports at most 1 reference image"); + expect(generateImage).not.toHaveBeenCalled(); + }); + it("passes edit aspect ratio overrides through to runtime for provider-level handling", async () => { vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([ createFalEditProvider({ aspectRatios: ["1:1", "16:9"] }), diff --git a/src/agents/tools/media-tool-shared.ts b/src/agents/tools/media-tool-shared.ts index 299b738a0fc..d126bbfd9be 100644 --- a/src/agents/tools/media-tool-shared.ts +++ b/src/agents/tools/media-tool-shared.ts @@ -135,6 +135,7 @@ type CapabilityProvider = { id: string; aliases?: string[]; defaultModel?: string; + models?: readonly string[]; isConfigured?: (ctx: { cfg?: OpenClawConfig; agentDir?: string }) => boolean; }; @@ -157,6 +158,35 @@ function findCapabilityProviderById(params: { ); } +function parseCapabilityModelRefForProviders(params: { + providers: CapabilityProvider[]; + raw?: string; + parseModelRef: ParseGenerationModelRef; +}): GenerationModelRef | null { + const raw = normalizeOptionalString(params.raw); + if (!raw) { + return null; + } + const parsed = params.parseModelRef(raw); + if ( + parsed && + findCapabilityProviderById({ + providers: params.providers, + providerId: parsed.provider, + }) + ) { + return parsed; + } + const provider = params.providers.find((candidate) => { + const models = [candidate.defaultModel, ...(candidate.models ?? [])]; + return models.some((model) => normalizeOptionalString(model) === raw); + }); + if (provider) { + return { provider: provider.id, model: raw }; + } + return parsed; +} + export function isCapabilityProviderConfigured(params: { providers: T[]; provider?: T; @@ -200,7 +230,16 @@ export function resolveSelectedCapabilityProvider( parseModelRef: ParseGenerationModelRef; }): T | undefined { const selectedRef = - params.parseModelRef(params.modelOverride) ?? params.parseModelRef(params.modelConfig.primary); + parseCapabilityModelRefForProviders({ + providers: params.providers, + raw: params.modelOverride, + parseModelRef: params.parseModelRef, + }) ?? + parseCapabilityModelRefForProviders({ + providers: params.providers, + raw: params.modelConfig.primary, + parseModelRef: params.parseModelRef, + }); if (!selectedRef) { return undefined; } diff --git a/src/media-generation/runtime-shared.test.ts b/src/media-generation/runtime-shared.test.ts index 63d1c52c9c9..949434cf1eb 100644 --- a/src/media-generation/runtime-shared.test.ts +++ b/src/media-generation/runtime-shared.test.ts @@ -181,6 +181,27 @@ describe("media-generation runtime shared candidates", () => { expect(candidates).toEqual([{ provider: "openai", model: "gpt-image-2" }]); }); + + it("resolves slash-containing provider model IDs from registered provider models", () => { + const candidates = resolveCapabilityModelCandidates({ + cfg: {} as OpenClawConfig, + modelConfig: { + primary: "openai/gpt-image-2", + }, + modelOverride: "fal-ai/flux/dev", + parseModelRef, + listProviders: () => [ + { + id: "fal", + defaultModel: "fal-ai/flux/dev", + models: ["fal-ai/flux/dev", "fal-ai/flux/dev/image-to-image"], + isConfigured: () => true, + }, + ], + }); + + expect(candidates).toEqual([{ provider: "fal", model: "fal-ai/flux/dev" }]); + }); }); describe("media-generation runtime shared normalization", () => { diff --git a/src/media-generation/runtime-shared.ts b/src/media-generation/runtime-shared.ts index 5af8fa01f02..a556392eca2 100644 --- a/src/media-generation/runtime-shared.ts +++ b/src/media-generation/runtime-shared.ts @@ -64,6 +64,7 @@ type CapabilityProviderCandidate = { id: string; aliases?: readonly string[]; defaultModel?: string | null; + models?: readonly string[]; isConfigured?: (ctx: { cfg?: OpenClawConfig; agentDir?: string }) => boolean; }; @@ -162,6 +163,34 @@ function resolveAutoCapabilityFallbackRefs(params: { }); } +function providerMatchesId(provider: CapabilityProviderCandidate, providerId: string): boolean { + const normalizedProviderId = normalizeOptionalString(providerId); + if (!normalizedProviderId) { + return false; + } + return ( + normalizeOptionalString(provider.id) === normalizedProviderId || + (provider.aliases ?? []).some( + (alias) => normalizeOptionalString(alias) === normalizedProviderId, + ) + ); +} + +function resolveProviderModelOnlyRef(params: { + raw: string; + providers: CapabilityProviderCandidate[]; +}): ParsedProviderModelRef | null { + const model = normalizeOptionalString(params.raw); + if (!model) { + return null; + } + const provider = params.providers.find((candidate) => { + const models = [candidate.defaultModel, ...(candidate.models ?? [])]; + return models.some((entry) => normalizeOptionalString(entry) === model); + }); + return provider ? { provider: provider.id, model } : null; +} + export function resolveCapabilityModelCandidates(params: { cfg: OpenClawConfig; modelConfig: AgentModelConfig | undefined; @@ -173,20 +202,42 @@ export function resolveCapabilityModelCandidates(params: { }): ParsedProviderModelRef[] { const candidates: ParsedProviderModelRef[] = []; const seen = new Set(); + let providers: CapabilityProviderCandidate[] | undefined; + const getProviders = (): CapabilityProviderCandidate[] => { + providers ??= params.listProviders?.(params.cfg) ?? []; + return providers; + }; const add = (raw: string | undefined) => { - const parsed = params.parseModelRef(raw); - if (!parsed) { + const trimmed = normalizeOptionalString(raw); + if (!trimmed) { return; } - const key = `${parsed.provider}/${parsed.model}`; + const parsed = params.parseModelRef(raw); + const candidate = + parsed && getProviders().some((provider) => providerMatchesId(provider, parsed.provider)) + ? parsed + : (resolveProviderModelOnlyRef({ raw: trimmed, providers: getProviders() }) ?? parsed); + if (!candidate) { + return; + } + const key = `${candidate.provider}/${candidate.model}`; if (seen.has(key)) { return; } seen.add(key); - candidates.push(parsed); + candidates.push(candidate); }; - const override = params.parseModelRef(params.modelOverride); + const override = (() => { + const raw = normalizeOptionalString(params.modelOverride); + if (!raw) { + return null; + } + const parsed = params.parseModelRef(raw); + return parsed && getProviders().some((provider) => providerMatchesId(provider, parsed.provider)) + ? parsed + : (resolveProviderModelOnlyRef({ raw, providers: getProviders() }) ?? parsed); + })(); if (override) { return [override]; } @@ -203,7 +254,7 @@ export function resolveCapabilityModelCandidates(params: { for (const candidate of resolveAutoCapabilityFallbackRefs({ cfg: params.cfg, agentDir: params.agentDir, - listProviders: params.listProviders, + listProviders: () => getProviders(), })) { add(candidate); } From a35067f872499797446000a229c16e92847868df Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 23:44:34 -0700 Subject: [PATCH 055/215] fix(media): avoid provider listing for exact media defaults --- src/media-generation/runtime-shared.test.ts | 19 +++++--- src/media-generation/runtime-shared.ts | 51 ++++++++------------- 2 files changed, 30 insertions(+), 40 deletions(-) diff --git a/src/media-generation/runtime-shared.test.ts b/src/media-generation/runtime-shared.test.ts index 949434cf1eb..24f3a517caf 100644 --- a/src/media-generation/runtime-shared.test.ts +++ b/src/media-generation/runtime-shared.test.ts @@ -131,6 +131,7 @@ describe("media-generation runtime shared candidates", () => { }); it("disables implicit provider expansion when mediaGenerationAutoProviderFallback=false", () => { + let listProviderCalls = 0; const candidates = resolveCapabilityModelCandidates({ cfg: { agents: { @@ -143,16 +144,20 @@ describe("media-generation runtime shared candidates", () => { primary: "google/gemini-3.1-flash-image-preview", }, parseModelRef, - listProviders: () => [ - { - id: "openai", - defaultModel: "gpt-image-1", - isConfigured: () => true, - }, - ], + listProviders: () => { + listProviderCalls += 1; + return [ + { + id: "openai", + defaultModel: "gpt-image-1", + isConfigured: () => true, + }, + ]; + }, }); expect(candidates).toEqual([{ provider: "google", model: "gemini-3.1-flash-image-preview" }]); + expect(listProviderCalls).toBe(0); }); it("treats an explicit model override as exact-only", () => { diff --git a/src/media-generation/runtime-shared.ts b/src/media-generation/runtime-shared.ts index a556392eca2..27453014f9c 100644 --- a/src/media-generation/runtime-shared.ts +++ b/src/media-generation/runtime-shared.ts @@ -163,19 +163,6 @@ function resolveAutoCapabilityFallbackRefs(params: { }); } -function providerMatchesId(provider: CapabilityProviderCandidate, providerId: string): boolean { - const normalizedProviderId = normalizeOptionalString(providerId); - if (!normalizedProviderId) { - return false; - } - return ( - normalizeOptionalString(provider.id) === normalizedProviderId || - (provider.aliases ?? []).some( - (alias) => normalizeOptionalString(alias) === normalizedProviderId, - ) - ); -} - function resolveProviderModelOnlyRef(params: { raw: string; providers: CapabilityProviderCandidate[]; @@ -207,16 +194,19 @@ export function resolveCapabilityModelCandidates(params: { providers ??= params.listProviders?.(params.cfg) ?? []; return providers; }; - const add = (raw: string | undefined) => { + const resolveCandidate = (raw: string | undefined, options: { useProviderMetadata: boolean }) => { const trimmed = normalizeOptionalString(raw); if (!trimmed) { - return; + return null; } const parsed = params.parseModelRef(raw); - const candidate = - parsed && getProviders().some((provider) => providerMatchesId(provider, parsed.provider)) - ? parsed - : (resolveProviderModelOnlyRef({ raw: trimmed, providers: getProviders() }) ?? parsed); + if (!options.useProviderMetadata) { + return parsed; + } + return resolveProviderModelOnlyRef({ raw: trimmed, providers: getProviders() }) ?? parsed; + }; + const add = (raw: string | undefined, options: { useProviderMetadata: boolean }) => { + const candidate = resolveCandidate(raw, options); if (!candidate) { return; } @@ -229,34 +219,29 @@ export function resolveCapabilityModelCandidates(params: { }; const override = (() => { - const raw = normalizeOptionalString(params.modelOverride); - if (!raw) { - return null; - } - const parsed = params.parseModelRef(raw); - return parsed && getProviders().some((provider) => providerMatchesId(provider, parsed.provider)) - ? parsed - : (resolveProviderModelOnlyRef({ raw, providers: getProviders() }) ?? parsed); + return resolveCandidate(params.modelOverride, { useProviderMetadata: true }); })(); if (override) { return [override]; } - add(params.modelOverride); - add(resolveAgentModelPrimaryValue(params.modelConfig)); - for (const fallback of resolveAgentModelFallbackValues(params.modelConfig)) { - add(fallback); - } const autoProviderFallbackEnabled = params.autoProviderFallback ?? params.cfg.agents?.defaults?.mediaGenerationAutoProviderFallback !== false; + add(params.modelOverride, { useProviderMetadata: true }); + add(resolveAgentModelPrimaryValue(params.modelConfig), { + useProviderMetadata: autoProviderFallbackEnabled, + }); + for (const fallback of resolveAgentModelFallbackValues(params.modelConfig)) { + add(fallback, { useProviderMetadata: autoProviderFallbackEnabled }); + } if (autoProviderFallbackEnabled && params.listProviders) { for (const candidate of resolveAutoCapabilityFallbackRefs({ cfg: params.cfg, agentDir: params.agentDir, listProviders: () => getProviders(), })) { - add(candidate); + add(candidate, { useProviderMetadata: false }); } } return candidates; From c018d8405b5ed6633c7d61d8a5e48920c1a4252c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 07:57:24 +0100 Subject: [PATCH 056/215] fix: refresh Bedrock profile credentials live --- CHANGELOG.md | 1 + .../amazon-bedrock/aws-credential-refresh.ts | 42 +++++ extensions/amazon-bedrock/discovery.ts | 4 + .../amazon-bedrock/embedding-provider.ts | 26 +-- extensions/amazon-bedrock/index.test.ts | 159 ++++++++++++------ extensions/amazon-bedrock/package.json | 3 +- .../amazon-bedrock/register.sync.runtime.ts | 137 +++++++++------ 7 files changed, 256 insertions(+), 116 deletions(-) create mode 100644 extensions/amazon-bedrock/aws-credential-refresh.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 36fc49e2136..6f96c8ce54c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -145,6 +145,7 @@ Docs: https://docs.openclaw.ai - CLI backends: keep versioned OAuth identity matches reusable when auth profile ids rotate, so Claude CLI sessions do not reset and lose continuity during same-account OAuth refresh/profile alias changes. Fixes #78541. - Model providers: normalize APNG sniffed PNG uploads, preserve Gemini 3 tool-call thought-signature replay with documented fallback signatures, accept legacy `__env__:VAR` custom-provider keys, and repair snake_case tool-call transcript sanitization. Fixes #51881, #48915, #77566, and #42858. - Telegram/models: parse provider ids containing dots in `/models` callback buttons so `hf.co` model lists render as inline keyboard buttons. Fixes #38745. +- Amazon Bedrock: refresh shared AWS profile/config file credentials before Bedrock model, discovery, and embedding requests so long-running Gateway processes pick up renewed profile credentials without restart. Fixes #77551. - Anthropic: reject uppercase provider-prefixed forward-compat model ids locally instead of sending malformed dynamic ids upstream. Fixes #73715. - OpenAI/embeddings: pass configured output dimensionality through single and batched embedding requests so memory embedding indexes can request smaller vectors. Fixes #55126. - CLI/infer: normalize HEIC/HEIF image files to JPEG before model-run requests, avoiding providers that reject Apple image container formats. Fixes #50081. diff --git a/extensions/amazon-bedrock/aws-credential-refresh.ts b/extensions/amazon-bedrock/aws-credential-refresh.ts new file mode 100644 index 00000000000..13f8e6702e3 --- /dev/null +++ b/extensions/amazon-bedrock/aws-credential-refresh.ts @@ -0,0 +1,42 @@ +type SharedIniFileLoader = { + loadSharedConfigFiles(init?: { ignoreCache?: boolean }): Promise; +}; + +let sharedIniFileLoaderForTest: SharedIniFileLoader | null | undefined; + +function hasStaticAwsCredentialEnv(env: NodeJS.ProcessEnv): boolean { + return Boolean(env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY); +} + +export function shouldRefreshAwsSharedConfigCacheForBedrock(env: NodeJS.ProcessEnv): boolean { + if (env.AWS_BEDROCK_SKIP_AUTH === "1" || env.AWS_BEARER_TOKEN_BEDROCK) { + return false; + } + return !hasStaticAwsCredentialEnv(env); +} + +async function loadSharedIniFileLoader(): Promise { + if (sharedIniFileLoaderForTest !== undefined) { + if (!sharedIniFileLoaderForTest) { + throw new Error("AWS shared INI file loader unavailable"); + } + return sharedIniFileLoaderForTest; + } + return (await import("@smithy/shared-ini-file-loader")) as SharedIniFileLoader; +} + +export async function refreshAwsSharedConfigCacheForBedrock( + env: NodeJS.ProcessEnv = process.env, +): Promise { + if (!shouldRefreshAwsSharedConfigCacheForBedrock(env)) { + return; + } + const loader = await loadSharedIniFileLoader(); + await loader.loadSharedConfigFiles({ ignoreCache: true }); +} + +export function setAwsSharedIniFileLoaderForTest( + loader: SharedIniFileLoader | null | undefined, +): void { + sharedIniFileLoaderForTest = loader; +} diff --git a/extensions/amazon-bedrock/discovery.ts b/extensions/amazon-bedrock/discovery.ts index b71a8d6767a..81e7492ebb9 100644 --- a/extensions/amazon-bedrock/discovery.ts +++ b/extensions/amazon-bedrock/discovery.ts @@ -14,6 +14,7 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, } from "openclaw/plugin-sdk/text-runtime"; +import { refreshAwsSharedConfigCacheForBedrock } from "./aws-credential-refresh.js"; import { resolveBedrockConfigApiKey } from "./discovery-shared.js"; const log = createSubsystemLogger("bedrock-discovery"); @@ -481,6 +482,9 @@ export async function discoverBedrockModels(params: { ? createInjectedClientDiscoverySdk() : await loadBedrockDiscoverySdk(); const clientFactory = params.clientFactory ?? ((region: string) => sdk.createClient(region)); + if (!params.clientFactory) { + await refreshAwsSharedConfigCacheForBedrock(); + } const client = clientFactory(params.region); const discoveryPromise = (async () => { diff --git a/extensions/amazon-bedrock/embedding-provider.ts b/extensions/amazon-bedrock/embedding-provider.ts index 143506a94aa..d37bd1952e1 100644 --- a/extensions/amazon-bedrock/embedding-provider.ts +++ b/extensions/amazon-bedrock/embedding-provider.ts @@ -5,6 +5,7 @@ import { type MemoryEmbeddingProviderCreateOptions, } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { refreshAwsSharedConfigCacheForBedrock } from "./aws-credential-refresh.js"; // --------------------------------------------------------------------------- // Types & constants @@ -263,7 +264,6 @@ export async function createBedrockEmbeddingProvider( ): Promise<{ provider: MemoryEmbeddingProvider; client: BedrockEmbeddingClient }> { const client = resolveBedrockEmbeddingClient(options); const { BedrockRuntimeClient, InvokeModelCommand } = await loadSdk(); - const sdk = new BedrockRuntimeClient({ region: client.region }); const spec = resolveSpec(client.model); const family = spec?.family ?? inferFamily(client.model); @@ -275,15 +275,21 @@ export async function createBedrockEmbeddingProvider( }); const invoke = async (body: string): Promise => { - const res = await sdk.send( - new InvokeModelCommand({ - modelId: client.model, - body, - contentType: "application/json", - accept: "application/json", - }), - ); - return new TextDecoder().decode(res.body); + await refreshAwsSharedConfigCacheForBedrock(); + const sdk = new BedrockRuntimeClient({ region: client.region }); + try { + const res = await sdk.send( + new InvokeModelCommand({ + modelId: client.model, + body, + contentType: "application/json", + accept: "application/json", + }), + ); + return new TextDecoder().decode(res.body); + } finally { + sdk.destroy(); + } }; const isCohere = family === "cohere-v3" || family === "cohere-v4"; diff --git a/extensions/amazon-bedrock/index.test.ts b/extensions/amazon-bedrock/index.test.ts index 8420a5ddce2..f226c54bd0f 100644 --- a/extensions/amazon-bedrock/index.test.ts +++ b/extensions/amazon-bedrock/index.test.ts @@ -7,6 +7,7 @@ import { registerSingleProviderPlugin, } from "openclaw/plugin-sdk/plugin-test-runtime"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { setAwsSharedIniFileLoaderForTest } from "./aws-credential-refresh.js"; import { resetBedrockDiscoveryCacheForTest } from "./discovery.js"; import amazonBedrockPlugin from "./index.js"; import { @@ -26,6 +27,7 @@ const foundationModelResults: BedrockClientResult[] = []; const inferenceProfileListResults: BedrockClientResult[] = []; const inferenceProfileGetResults: BedrockClientResult[] = []; const bedrockClientConfigs: Array> = []; +const refreshSharedConfigCache = vi.fn(async () => {}); const sendBedrockCommand = vi.fn(async (command: unknown) => { const commandName = command?.constructor?.name; const queue = @@ -162,12 +164,12 @@ function makeAppInferenceProfileDescriptor(modelId: string): never { * Call wrapStreamFn and then invoke the returned stream function, capturing * the payload via the onPayload hook that streamWithPayloadPatch installs. */ -function callWrappedStream( +async function callWrappedStream( provider: RegisteredProviderPlugin, modelId: string, modelDescriptor: never, config?: OpenClawConfig, -): Record { +): Promise> { const wrapped = provider.wrapStreamFn?.({ provider: "amazon-bedrock", modelId, @@ -186,8 +188,13 @@ function callWrappedStream( // If onPayload was installed by streamWithPayloadPatch, call it to apply the patch. if (typeof result?.onPayload === "function") { const payload: Record = {}; - (result.onPayload as (p: Record) => void)(payload); - return { ...result, _capturedPayload: payload }; + await (result.onPayload as (p: Record, model: unknown) => Promise)( + payload, + modelDescriptor, + ); + if (Object.keys(payload).length > 0) { + return { ...result, _capturedPayload: payload }; + } } return result; @@ -213,6 +220,8 @@ describe("amazon-bedrock provider plugin", () => { inferenceProfileListResults.length = 0; inferenceProfileGetResults.length = 0; bedrockClientConfigs.length = 0; + refreshSharedConfigCache.mockClear(); + setAwsSharedIniFileLoaderForTest({ loadSharedConfigFiles: refreshSharedConfigCache }); sendBedrockCommand.mockClear(); resetBedrockDiscoveryCacheForTest(); resetBedrockAppProfileCacheEligibilityForTest(); @@ -229,6 +238,7 @@ describe("amazon-bedrock provider plugin", () => { afterEach(() => { setBedrockAppProfileControlPlaneForTest(undefined); + setAwsSharedIniFileLoaderForTest(undefined); }); it("marks Claude 4.6 Bedrock models as adaptive by default", async () => { @@ -326,6 +336,31 @@ describe("amazon-bedrock provider plugin", () => { }); }); + it("refreshes AWS shared config cache before Bedrock sends", async () => { + const order: string[] = []; + refreshSharedConfigCache.mockImplementationOnce(async () => { + order.push("refresh"); + }); + const provider = await registerSingleProviderPlugin(amazonBedrockPlugin); + const wrapped = provider.wrapStreamFn?.({ + provider: "amazon-bedrock", + modelId: ANTHROPIC_MODEL, + streamFn: spyStreamFn, + } as never); + const result = wrapped?.(ANTHROPIC_MODEL_DESCRIPTOR, { messages: [] } as never, { + onPayload: () => { + order.push("original"); + }, + }) as Record | undefined; + + await ( + result?.onPayload as ((p: Record, model: unknown) => unknown) | undefined + )?.({}, ANTHROPIC_MODEL_DESCRIPTOR); + + expect(refreshSharedConfigCache).toHaveBeenCalledWith({ ignoreCache: true }); + expect(order).toEqual(["refresh", "original"]); + }); + it("omits temperature for Bedrock Opus 4.7 model ids", async () => { const provider = await registerSingleProviderPlugin(amazonBedrockPlugin); const wrapped = provider.wrapStreamFn?.({ @@ -334,17 +369,18 @@ describe("amazon-bedrock provider plugin", () => { streamFn: spyStreamFn, } as never); - expect( - wrapped?.( - { - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - id: "us.anthropic.claude-opus-4-7", - } as never, - { messages: [] } as never, - { temperature: 0.2, maxTokens: 10 }, - ), - ).toEqual({ maxTokens: 10 }); + const result = wrapped?.( + { + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + id: "us.anthropic.claude-opus-4-7", + } as never, + { messages: [] } as never, + { temperature: 0.2, maxTokens: 10 }, + ) as Record | undefined; + + expect(result).toMatchObject({ maxTokens: 10 }); + expect(result).not.toHaveProperty("temperature"); }); it("omits temperature for dotted Bedrock Opus 4.7 model ids", async () => { @@ -355,17 +391,18 @@ describe("amazon-bedrock provider plugin", () => { streamFn: spyStreamFn, } as never); - expect( - wrapped?.( - { - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - id: "us.anthropic.claude-opus-4.7-v1:0", - } as never, - { messages: [] } as never, - { temperature: 0.2, maxTokens: 10 }, - ), - ).toEqual({ maxTokens: 10 }); + const result = wrapped?.( + { + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + id: "us.anthropic.claude-opus-4.7-v1:0", + } as never, + { messages: [] } as never, + { temperature: 0.2, maxTokens: 10 }, + ) as Record | undefined; + + expect(result).toMatchObject({ maxTokens: 10 }); + expect(result).not.toHaveProperty("temperature"); }); it("omits temperature for named Bedrock Opus 4.7 inference profile ARNs", async () => { @@ -378,17 +415,18 @@ describe("amazon-bedrock provider plugin", () => { streamFn: spyStreamFn, } as never); - expect( - wrapped?.( - { - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - id: modelId, - } as never, - { messages: [] } as never, - { temperature: 0, region: "us-west-2" } as never, - ), - ).toEqual({ region: "us-west-2" }); + const result = wrapped?.( + { + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + id: modelId, + } as never, + { messages: [] } as never, + { temperature: 0, region: "us-west-2" } as never, + ) as Record | undefined; + + expect(result).toMatchObject({ region: "us-west-2" }); + expect(result).not.toHaveProperty("temperature"); }); it("omits temperature for non-US Bedrock Opus 4.7 regional profiles", async () => { @@ -399,17 +437,18 @@ describe("amazon-bedrock provider plugin", () => { streamFn: spyStreamFn, } as never); - expect( - wrapped?.( - { - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - id: "eu.anthropic.claude-opus-4-7", - } as never, - { messages: [] } as never, - { temperature: 0.4, maxTokens: 12 }, - ), - ).toEqual({ maxTokens: 12 }); + const result = wrapped?.( + { + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + id: "eu.anthropic.claude-opus-4-7", + } as never, + { messages: [] } as never, + { temperature: 0.4, maxTokens: 12 }, + ) as Record | undefined; + + expect(result).toMatchObject({ maxTokens: 12 }); + expect(result).not.toHaveProperty("temperature"); }); it("preserves Bedrock Opus 4.7 max thinking in the final payload", async () => { @@ -461,7 +500,15 @@ describe("amazon-bedrock provider plugin", () => { { reasoning: "xhigh" } as never, ) as Record | undefined; - expect(result).not.toHaveProperty("onPayload"); + const payload = { + additionalModelRequestFields: { + output_config: { effort: "xhigh" }, + }, + }; + + await (result?.onPayload as ((p: Record) => unknown) | undefined)?.(payload); + + expect(payload.additionalModelRequestFields.output_config).toEqual({ effort: "xhigh" }); }); it("classifies nested Bedrock deprecated-temperature validation as format failover", async () => { @@ -533,7 +580,7 @@ describe("amazon-bedrock provider plugin", () => { describe("guardrail payload injection", () => { it("does not inject guardrailConfig when guardrail is absent from plugin config", async () => { const provider = await registerWithConfig(undefined); - const result = callWrappedStream(provider, NON_ANTHROPIC_MODEL, MODEL_DESCRIPTOR); + const result = await callWrappedStream(provider, NON_ANTHROPIC_MODEL, MODEL_DESCRIPTOR); expect(result).not.toHaveProperty("_capturedPayload"); // The onPayload hook should not exist when no guardrail is configured @@ -549,7 +596,7 @@ describe("amazon-bedrock provider plugin", () => { trace: "enabled", }, }); - const result = callWrappedStream(provider, NON_ANTHROPIC_MODEL, MODEL_DESCRIPTOR); + const result = await callWrappedStream(provider, NON_ANTHROPIC_MODEL, MODEL_DESCRIPTOR); expect(result._capturedPayload).toEqual({ guardrailConfig: { @@ -568,7 +615,7 @@ describe("amazon-bedrock provider plugin", () => { guardrailVersion: "DRAFT", }, }); - const result = callWrappedStream(provider, NON_ANTHROPIC_MODEL, MODEL_DESCRIPTOR); + const result = await callWrappedStream(provider, NON_ANTHROPIC_MODEL, MODEL_DESCRIPTOR); expect(result._capturedPayload).toEqual({ guardrailConfig: { @@ -587,7 +634,7 @@ describe("amazon-bedrock provider plugin", () => { trace: "disabled", }, }); - const result = callWrappedStream(provider, ANTHROPIC_MODEL, ANTHROPIC_MODEL_DESCRIPTOR); + const result = await callWrappedStream(provider, ANTHROPIC_MODEL, ANTHROPIC_MODEL_DESCRIPTOR); // Anthropic models should get guardrailConfig expect(result._capturedPayload).toEqual({ @@ -609,7 +656,7 @@ describe("amazon-bedrock provider plugin", () => { guardrailVersion: "3", }, }); - const result = callWrappedStream(provider, NON_ANTHROPIC_MODEL, MODEL_DESCRIPTOR); + const result = await callWrappedStream(provider, NON_ANTHROPIC_MODEL, MODEL_DESCRIPTOR); // Non-Anthropic models should get guardrailConfig expect(result._capturedPayload).toEqual({ @@ -624,7 +671,7 @@ describe("amazon-bedrock provider plugin", () => { it("uses live plugin config to inject guardrailConfig after startup disable", async () => { const provider = await registerWithConfig(undefined); - const result = callWrappedStream( + const result = await callWrappedStream( provider, NON_ANTHROPIC_MODEL, MODEL_DESCRIPTOR, @@ -651,7 +698,7 @@ describe("amazon-bedrock provider plugin", () => { guardrailVersion: "5", }, }); - const result = callWrappedStream( + const result = await callWrappedStream( provider, NON_ANTHROPIC_MODEL, MODEL_DESCRIPTOR, diff --git a/extensions/amazon-bedrock/package.json b/extensions/amazon-bedrock/package.json index e67e0c802f9..f3a69687aeb 100644 --- a/extensions/amazon-bedrock/package.json +++ b/extensions/amazon-bedrock/package.json @@ -7,7 +7,8 @@ "dependencies": { "@aws-sdk/client-bedrock": "3.1042.0", "@aws-sdk/client-bedrock-runtime": "3.1042.0", - "@aws-sdk/credential-provider-node": "3.972.39" + "@aws-sdk/credential-provider-node": "3.972.39", + "@smithy/shared-ini-file-loader": "4.4.9" }, "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" diff --git a/extensions/amazon-bedrock/register.sync.runtime.ts b/extensions/amazon-bedrock/register.sync.runtime.ts index 19a7e18fb6b..abef50b3578 100644 --- a/extensions/amazon-bedrock/register.sync.runtime.ts +++ b/extensions/amazon-bedrock/register.sync.runtime.ts @@ -11,6 +11,7 @@ import { isAnthropicBedrockModel, streamWithPayloadPatch, } from "openclaw/plugin-sdk/provider-stream-shared"; +import { refreshAwsSharedConfigCacheForBedrock } from "./aws-credential-refresh.js"; import { mergeImplicitBedrockProvider, resolveBedrockConfigApiKey } from "./discovery-shared.js"; import { bedrockMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js"; @@ -195,6 +196,7 @@ async function createBedrockControlPlane(region: string | undefined): Promise).temperature; } + function withAwsCredentialRefreshOnPayload( + options: TOptions, + ): TOptions & { onPayload: (payload: unknown, payloadModel: unknown) => Promise } { + const originalOnPayload = (options as { onPayload?: unknown }).onPayload as + | ((payload: unknown, model: unknown) => unknown) + | undefined; + return { + ...options, + onPayload: async (payload: unknown, payloadModel: unknown) => { + await refreshAwsSharedConfigCacheForBedrock(); + return originalOnPayload?.(payload, payloadModel); + }, + }; + } + + function createAwsCredentialRefreshStreamWrapper( + streamFn: StreamFn | null | undefined, + ): StreamFn | null | undefined { + if (!streamFn) { + return streamFn; + } + return (streamModel, context, options) => + streamFn(streamModel, context, withAwsCredentialRefreshOnPayload(Object.assign({}, options))); + } + /** Extract the AWS region from a bedrock-runtime baseUrl. */ function extractRegionFromBaseUrl(baseUrl: string | undefined): string | undefined { if (!baseUrl) { @@ -475,7 +502,7 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { const heuristicMatch = needsCachePointInjection(modelId); if (!region && !mayNeedCacheInjection && !shouldOmitTemperature && !shouldPatchMaxThinking) { - return wrapped; + return createAwsCredentialRefreshStreamWrapper(wrapped); } const underlying = wrapped ?? streamFn; @@ -493,19 +520,23 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { | undefined; if (!mayNeedCacheInjection) { - return underlying(streamModel, context, { - ...merged, - ...(shouldPatchMaxThinking - ? { - onPayload: (payload: unknown, payloadModel: unknown) => { - if (payload && typeof payload === "object") { - patchOpus47MaxThinkingEffort(payload as Record); - } - return originalOnPayload?.(payload, payloadModel); - }, - } - : {}), - }); + return underlying( + streamModel, + context, + withAwsCredentialRefreshOnPayload({ + ...merged, + ...(shouldPatchMaxThinking + ? { + onPayload: (payload: unknown, payloadModel: unknown) => { + if (payload && typeof payload === "object") { + patchOpus47MaxThinkingEffort(payload as Record); + } + return originalOnPayload?.(payload, payloadModel); + }, + } + : {}), + }), + ); } // Use the cacheRetention from options if explicitly set. @@ -522,48 +553,56 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { // Fast path: ARN heuristic already identified this as Claude, but the // concrete target may still need profile traits for Opus 4.7 payloads. const mayNeedTemperatureTrait = "temperature" in merged; - return underlying(streamModel, context, { - ...merged, - onPayload: async (payload: unknown, payloadModel: unknown) => { - if (payload && typeof payload === "object") { - const payloadRecord = payload as Record; - injectBedrockCachePoints(payloadRecord, cacheRetention); - if (shouldPatchMaxThinking) { - patchOpus47MaxThinkingEffort(payloadRecord); - } - if (mayNeedTemperatureTrait) { - const traits = await resolveAppProfileTraits(modelId, region); - if (traits.omitTemperature) { - omitDeprecatedOpus47PayloadTemperature(payloadRecord); + return underlying( + streamModel, + context, + withAwsCredentialRefreshOnPayload({ + ...merged, + onPayload: async (payload: unknown, payloadModel: unknown) => { + if (payload && typeof payload === "object") { + const payloadRecord = payload as Record; + injectBedrockCachePoints(payloadRecord, cacheRetention); + if (shouldPatchMaxThinking) { + patchOpus47MaxThinkingEffort(payloadRecord); + } + if (mayNeedTemperatureTrait) { + const traits = await resolveAppProfileTraits(modelId, region); + if (traits.omitTemperature) { + omitDeprecatedOpus47PayloadTemperature(payloadRecord); + } } } - } - return originalOnPayload?.(payload, payloadModel); - }, - }); + return originalOnPayload?.(payload, payloadModel); + }, + }), + ); } // Slow path: opaque profile ID — resolve underlying model via API (cached). // pi-ai's onPayload supports async, so we await the resolution inline. - return underlying(streamModel, context, { - ...merged, - onPayload: async (payload: unknown, payloadModel: unknown) => { - const traits = await resolveAppProfileTraits(modelId, region); - if (payload && typeof payload === "object") { - const payloadRecord = payload as Record; - if (traits.cacheEligible) { - injectBedrockCachePoints(payloadRecord, cacheRetention); + return underlying( + streamModel, + context, + withAwsCredentialRefreshOnPayload({ + ...merged, + onPayload: async (payload: unknown, payloadModel: unknown) => { + const traits = await resolveAppProfileTraits(modelId, region); + if (payload && typeof payload === "object") { + const payloadRecord = payload as Record; + if (traits.cacheEligible) { + injectBedrockCachePoints(payloadRecord, cacheRetention); + } + if (shouldPatchMaxThinking) { + patchOpus47MaxThinkingEffort(payloadRecord); + } + if (traits.omitTemperature) { + omitDeprecatedOpus47PayloadTemperature(payloadRecord); + } } - if (shouldPatchMaxThinking) { - patchOpus47MaxThinkingEffort(payloadRecord); - } - if (traits.omitTemperature) { - omitDeprecatedOpus47PayloadTemperature(payloadRecord); - } - } - return originalOnPayload?.(payload, payloadModel); - }, - }); + return originalOnPayload?.(payload, payloadModel); + }, + }), + ); }; }, matchesContextOverflowError: ({ errorMessage }) => From 4721ca8e459f83842a5f670a76bd2c20db29b884 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 07:57:29 +0100 Subject: [PATCH 057/215] chore: update Bedrock provider lockfile --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad6229d5872..7e4cd7d92ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -329,6 +329,9 @@ importers: '@aws-sdk/credential-provider-node': specifier: 3.972.39 version: 3.972.39 + '@smithy/shared-ini-file-loader': + specifier: 4.4.9 + version: 4.4.9 devDependencies: '@openclaw/plugin-sdk': specifier: workspace:* From 2e78fc57af0983aef12225021d076aae4836b64e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 08:09:55 +0100 Subject: [PATCH 058/215] fix: accept aws-sdk auth profiles --- CHANGELOG.md | 1 + src/agents/auth-health.ts | 13 ++- ...th-profiles.ensureauthprofilestore.test.ts | 19 ++++ src/agents/auth-profiles/credential-state.ts | 4 + src/agents/auth-profiles/oauth.ts | 5 +- src/agents/auth-profiles/persisted.ts | 15 ++- src/agents/auth-profiles/policy.ts | 2 +- src/agents/auth-profiles/types.ts | 17 +++- src/agents/cli-auth-epoch.ts | 8 ++ src/agents/model-auth.profiles.test.ts | 55 +++++++++++ src/agents/model-auth.ts | 98 +++++++++++++++++-- .../models-config.providers.secret-helpers.ts | 2 +- .../reply/directive-handling.auth.ts | 10 ++ src/commands/auth-choice.test.ts | 2 +- src/commands/doctor-auth-flat-profiles.ts | 7 +- src/commands/doctor-auth-profile-config.ts | 7 +- src/commands/models/auth-list.ts | 4 +- src/commands/models/auth.test.ts | 2 +- src/commands/models/auth.ts | 5 +- src/commands/models/list.auth-overview.ts | 5 + src/commands/models/list.status-command.ts | 2 +- src/commands/models/list.types.ts | 1 + src/config/types.auth.ts | 3 +- src/config/zod-schema.ts | 7 +- .../server-methods/models-auth-status.ts | 2 +- src/infra/provider-usage.auth.ts | 7 +- .../provider-discovery-contract.ts | 2 +- src/plugins/provider-auth-helpers.ts | 2 +- src/plugins/provider-discovery.ts | 2 +- src/plugins/types.ts | 2 +- ...runtime-auth-profiles-oauth-policy.test.ts | 2 +- 31 files changed, 285 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f96c8ce54c..f12cb90a1c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -145,6 +145,7 @@ Docs: https://docs.openclaw.ai - CLI backends: keep versioned OAuth identity matches reusable when auth profile ids rotate, so Claude CLI sessions do not reset and lose continuity during same-account OAuth refresh/profile alias changes. Fixes #78541. - Model providers: normalize APNG sniffed PNG uploads, preserve Gemini 3 tool-call thought-signature replay with documented fallback signatures, accept legacy `__env__:VAR` custom-provider keys, and repair snake_case tool-call transcript sanitization. Fixes #51881, #48915, #77566, and #42858. - Telegram/models: parse provider ids containing dots in `/models` callback buttons so `hf.co` model lists render as inline keyboard buttons. Fixes #38745. +- Auth profiles/Bedrock: accept persisted `type: "aws-sdk"` auth profiles so EC2/IMDS and shared AWS credential-chain Bedrock setups are not dropped as `invalid_type`. Fixes #69708. - Amazon Bedrock: refresh shared AWS profile/config file credentials before Bedrock model, discovery, and embedding requests so long-running Gateway processes pick up renewed profile credentials without restart. Fixes #77551. - Anthropic: reject uppercase provider-prefixed forward-compat model ids locally instead of sending malformed dynamic ids upstream. Fixes #73715. - OpenAI/embeddings: pass configured output dimensionality through single and batched embedding requests so memory embedding indexes can request smaller vectors. Fixes #55126. diff --git a/src/agents/auth-health.ts b/src/agents/auth-health.ts index ddfc2f68892..4dc938942c3 100644 --- a/src/agents/auth-health.ts +++ b/src/agents/auth-health.ts @@ -17,7 +17,7 @@ export type AuthProfileHealthStatus = "ok" | "expiring" | "expired" | "missing" type AuthProfileHealth = { profileId: string; provider: string; - type: "oauth" | "token" | "api_key"; + type: "oauth" | "token" | "api_key" | "aws-sdk"; status: AuthProfileHealthStatus; reasonCode?: AuthCredentialReasonCode; expiresAt?: number; @@ -127,6 +127,17 @@ function buildProfileHealth(params: { }; } + if (healthCredential.type === "aws-sdk") { + return { + profileId, + provider, + type: "aws-sdk", + status: "static", + source, + label, + }; + } + if (healthCredential.type === "token") { const eligibility = evaluateStoredCredentialEligibility({ credential: healthCredential, diff --git a/src/agents/auth-profiles.ensureauthprofilestore.test.ts b/src/agents/auth-profiles.ensureauthprofilestore.test.ts index 0f5b42c47a5..a00be87324d 100644 --- a/src/agents/auth-profiles.ensureauthprofilestore.test.ts +++ b/src/agents/auth-profiles.ensureauthprofilestore.test.ts @@ -996,6 +996,25 @@ describe("ensureAuthProfileStore", () => { } }); + it("accepts aws-sdk auth profiles without static credential material (#69708)", () => { + withTempAgentDir("openclaw-auth-aws-sdk-", (agentDir) => { + writeAuthProfileStore(agentDir, { + "amazon-bedrock:default": { + type: "aws-sdk", + provider: "amazon-bedrock", + createdAt: "2026-03-15T10:00:00.000Z", + }, + }); + + const profile = loadAuthProfile(agentDir, "amazon-bedrock:default"); + + expect(profile).toMatchObject({ + type: "aws-sdk", + provider: "amazon-bedrock", + }); + }); + }); + it.each([ { name: "migrates SecretRef object in `key` to `keyRef` and clears `key`", diff --git a/src/agents/auth-profiles/credential-state.ts b/src/agents/auth-profiles/credential-state.ts index 4aeb8c7e6ed..c6511e8a52d 100644 --- a/src/agents/auth-profiles/credential-state.ts +++ b/src/agents/auth-profiles/credential-state.ts @@ -85,6 +85,10 @@ export function evaluateStoredCredentialEligibility(params: { return { eligible: true, reasonCode: "ok" }; } + if (credential.type === "aws-sdk") { + return { eligible: true, reasonCode: "ok" }; + } + if (credential.type === "token") { const hasToken = hasConfiguredSecretString(credential.token); const hasTokenRef = hasConfiguredSecretRef(credential.tokenRef); diff --git a/src/agents/auth-profiles/oauth.ts b/src/agents/auth-profiles/oauth.ts index 9ce4157542d..b7511ec66e8 100644 --- a/src/agents/auth-profiles/oauth.ts +++ b/src/agents/auth-profiles/oauth.ts @@ -80,7 +80,7 @@ function isProfileConfigCompatible(params: { cfg?: OpenClawConfig; profileId: string; provider: string; - mode: "api_key" | "token" | "oauth"; + mode: "api_key" | "aws-sdk" | "token" | "oauth"; allowOAuthTokenCompatibility?: boolean; }): boolean { const profileConfig = params.cfg?.auth?.profiles?.[params.profileId]; @@ -311,6 +311,9 @@ export async function resolveApiKeyForProfile( } return buildApiKeyProfileResult({ apiKey: key, provider: cred.provider, email: cred.email }); } + if (cred.type === "aws-sdk") { + return null; + } if (cred.type === "token") { const expiryState = resolveTokenExpiryState(cred.expires); if (expiryState === "expired" || expiryState === "invalid_expires") { diff --git a/src/agents/auth-profiles/persisted.ts b/src/agents/auth-profiles/persisted.ts index 1276a5aa725..6bbf41a1420 100644 --- a/src/agents/auth-profiles/persisted.ts +++ b/src/agents/auth-profiles/persisted.ts @@ -31,7 +31,12 @@ export type LegacyAuthStore = Record; type CredentialRejectReason = "non_object" | "invalid_type" | "missing_provider"; type RejectedCredentialEntry = { key: string; reason: CredentialRejectReason }; -const AUTH_PROFILE_TYPES = new Set(["api_key", "oauth", "token"]); +const AUTH_PROFILE_TYPES = new Set([ + "api_key", + "aws-sdk", + "oauth", + "token", +]); function normalizeSecretBackedField(params: { entry: Record; @@ -539,6 +544,14 @@ export function applyLegacyAuthStore(store: AuthProfileStore, legacy: LegacyAuth }; continue; } + if (cred.type === "aws-sdk") { + store.profiles[profileId] = { + type: "aws-sdk", + provider: credentialProvider, + ...(cred.email ? { email: cred.email } : {}), + }; + continue; + } if (cred.type === "token") { store.profiles[profileId] = { type: "token", diff --git a/src/agents/auth-profiles/policy.ts b/src/agents/auth-profiles/policy.ts index ff899206651..26a8e88377d 100644 --- a/src/agents/auth-profiles/policy.ts +++ b/src/agents/auth-profiles/policy.ts @@ -61,7 +61,7 @@ function collectOAuthModeSecretRefViolations(params: { profileId: string; credential: AuthProfileCredential; defaults: SecretDefaults | undefined; - configuredMode?: "api_key" | "oauth" | "token"; + configuredMode?: "api_key" | "aws-sdk" | "oauth" | "token"; violations: OAuthSecretRefPolicyViolation[]; }): void { if (params.configuredMode !== "oauth") { diff --git a/src/agents/auth-profiles/types.ts b/src/agents/auth-profiles/types.ts index a9a735f5748..474efb8e0f7 100644 --- a/src/agents/auth-profiles/types.ts +++ b/src/agents/auth-profiles/types.ts @@ -29,6 +29,17 @@ export type ApiKeyCredential = { metadata?: Record; }; +export type AwsSdkCredential = { + type: "aws-sdk"; + provider: string; + /** Explicit opt-out for copying this profile when creating another agent. */ + copyToAgents?: boolean; + email?: string; + displayName?: string; + /** Optional provider-specific metadata (e.g., account IDs, regions). */ + metadata?: Record; +}; + export type TokenCredential = { /** * Static bearer-style token (often OAuth access token / PAT). @@ -59,7 +70,11 @@ export type OAuthCredential = OAuthCredentials & { displayName?: string; }; -export type AuthProfileCredential = ApiKeyCredential | TokenCredential | OAuthCredential; +export type AuthProfileCredential = + | ApiKeyCredential + | AwsSdkCredential + | TokenCredential + | OAuthCredential; export type AuthProfileFailureReason = | "auth" diff --git a/src/agents/cli-auth-epoch.ts b/src/agents/cli-auth-epoch.ts index d67aea8c33e..995fe0312ef 100644 --- a/src/agents/cli-auth-epoch.ts +++ b/src/agents/cli-auth-epoch.ts @@ -99,6 +99,14 @@ function encodeAuthProfileCredential(credential: AuthProfileCredential): string credential.displayName ?? null, encodeUnknown(credential.metadata), ]); + case "aws-sdk": + return JSON.stringify([ + "aws-sdk", + credential.provider, + credential.email ?? null, + credential.displayName ?? null, + encodeUnknown(credential.metadata), + ]); case "token": return JSON.stringify([ "token", diff --git a/src/agents/model-auth.profiles.test.ts b/src/agents/model-auth.profiles.test.ts index 299891c1d82..91b3392008f 100644 --- a/src/agents/model-auth.profiles.test.ts +++ b/src/agents/model-auth.profiles.test.ts @@ -290,6 +290,61 @@ async function expectBedrockAuthSource(params: { }); } +it("resolves persisted aws-sdk auth profiles without static keys (#69708)", async () => { + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-bedrock-auth-profile-")); + try { + await fs.writeFile( + path.join(agentDir, "auth-profiles.json"), + `${JSON.stringify( + { + version: 1, + profiles: { + "amazon-bedrock:default": { + type: "aws-sdk", + provider: "amazon-bedrock", + createdAt: "2026-03-15T10:00:00.000Z", + }, + }, + }, + null, + 2, + )}\n`, + "utf8", + ); + clearRuntimeAuthProfileStoreSnapshots(); + const store = ensureAuthProfileStore(agentDir); + + const resolved = await resolveApiKeyForProvider({ + provider: "amazon-bedrock", + profileId: "amazon-bedrock:default", + cfg: BEDROCK_PROVIDER_CFG as never, + store, + agentDir, + }); + + expect(resolved).toMatchObject({ + mode: "aws-sdk", + profileId: "amazon-bedrock:default", + source: "profile:amazon-bedrock:default", + }); + expect(resolved.apiKey).toBeUndefined(); + await expect( + hasAvailableAuthForProvider({ + provider: "amazon-bedrock", + cfg: BEDROCK_PROVIDER_CFG as never, + store, + agentDir, + }), + ).resolves.toBe(true); + expect(resolveModelAuthMode("amazon-bedrock", BEDROCK_PROVIDER_CFG as never, store)).toBe( + "aws-sdk", + ); + } finally { + await fs.rm(agentDir, { recursive: true, force: true }); + clearRuntimeAuthProfileStoreSnapshots(); + } +}); + function buildDemoLocalStore(keys: string[]) { return { version: 1 as const, diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index babb80f1c70..7522989f6d0 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -20,6 +20,7 @@ import { } from "../shared/string-coerce.js"; import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js"; import { + type AuthProfileCredential, type AuthProfileStore, externalCliDiscoveryForProviderAuth, ensureAuthProfileStore, @@ -43,6 +44,7 @@ import { type ResolvedProviderAuth, } from "./model-auth-runtime-shared.js"; import { normalizeProviderId } from "./model-selection.js"; +import { resolveProviderIdForAuth } from "./provider-auth-aliases.js"; export { ensureAuthProfileStore, @@ -234,6 +236,58 @@ function resolveProviderAuthOverride( return undefined; } +function profileTypeToAuthMode(type: AuthProfileCredential["type"]): ResolvedProviderAuth["mode"] { + return type === "oauth" + ? "oauth" + : type === "token" + ? "token" + : type === "aws-sdk" + ? "aws-sdk" + : "api-key"; +} + +function isProfileForProvider(params: { + cfg?: OpenClawConfig; + credential: AuthProfileCredential; + provider: string; +}): boolean { + return ( + resolveProviderIdForAuth(params.credential.provider, { config: params.cfg }) === + resolveProviderIdForAuth(params.provider, { config: params.cfg }) + ); +} + +function resolveAwsSdkProfileAuth(params: { + cfg?: OpenClawConfig; + provider: string; + profileId: string; + credential: AuthProfileCredential | undefined; +}): ResolvedProviderAuth | null { + if (params.credential?.type !== "aws-sdk") { + return null; + } + if ( + !isProfileForProvider({ + cfg: params.cfg, + credential: params.credential, + provider: params.provider, + }) + ) { + return null; + } + const profileConfig = params.cfg?.auth?.profiles?.[params.profileId]; + if (profileConfig) { + if (profileConfig.provider !== params.credential.provider || profileConfig.mode !== "aws-sdk") { + return null; + } + } + return { + ...resolveAwsSdkAuthInfo(), + profileId: params.profileId, + source: `profile:${params.profileId}`, + }; +} + function isLocalBaseUrl(baseUrl: string): boolean { try { let host = normalizeLowercaseStringOrEmpty(new URL(baseUrl).hostname); @@ -539,6 +593,15 @@ export async function resolveApiKeyForProvider(params: { profileId, preferredProfile, }); + const awsSdkProfileAuth = resolveAwsSdkProfileAuth({ + cfg, + provider, + profileId, + credential: store.profiles[profileId], + }); + if (awsSdkProfileAuth) { + return awsSdkProfileAuth; + } const resolved = await resolveApiKeyForProfile({ cfg, store, @@ -553,7 +616,7 @@ export async function resolveApiKeyForProvider(params: { apiKey: resolved.apiKey, profileId, source: `profile:${profileId}`, - mode: mode === "oauth" ? "oauth" : mode === "token" ? "token" : "api-key", + mode: mode ? profileTypeToAuthMode(mode) : "api-key", }; // When the resolved key is a provider-owned synthetic profile marker and // the caller has not locked this profile, fall through to env/config @@ -641,6 +704,15 @@ export async function resolveApiKeyForProvider(params: { let deferredAuthProfileResult: ResolvedProviderAuth | null = null; for (const candidate of order) { try { + const awsSdkProfileAuth = resolveAwsSdkProfileAuth({ + cfg, + provider, + profileId: candidate, + credential: store.profiles[candidate], + }); + if (awsSdkProfileAuth) { + return awsSdkProfileAuth; + } const resolved = await resolveApiKeyForProfile({ cfg, store, @@ -649,8 +721,9 @@ export async function resolveApiKeyForProvider(params: { }); if (resolved) { const mode = store.profiles[candidate]?.type; - const resolvedMode: ResolvedProviderAuth["mode"] = - mode === "oauth" ? "oauth" : mode === "token" ? "token" : "api-key"; + const resolvedMode: ResolvedProviderAuth["mode"] = mode + ? profileTypeToAuthMode(mode) + : "api-key"; const result: ResolvedProviderAuth = { apiKey: resolved.apiKey, profileId: candidate, @@ -770,10 +843,10 @@ export function resolveModelAuthMode( const modes = new Set( profiles .map((id) => authStore.profiles[id]?.type) - .filter((mode): mode is "api_key" | "oauth" | "token" => Boolean(mode)), + .filter((mode): mode is "api_key" | "aws-sdk" | "oauth" | "token" => Boolean(mode)), ); - const distinct = ["oauth", "token", "api_key"].filter((k) => - modes.has(k as "oauth" | "token" | "api_key"), + const distinct = ["oauth", "token", "api_key", "aws-sdk"].filter((k) => + modes.has(k as "oauth" | "token" | "api_key" | "aws-sdk"), ); if (distinct.length >= 2) { return "mixed"; @@ -787,6 +860,9 @@ export function resolveModelAuthMode( if (modes.has("api_key")) { return "api-key"; } + if (modes.has("aws-sdk")) { + return "aws-sdk"; + } } if (authOverride === undefined && normalizeProviderId(resolved) === "amazon-bedrock") { @@ -855,6 +931,16 @@ export async function hasAvailableAuthForProvider(params: { }); for (const candidate of order) { try { + if ( + resolveAwsSdkProfileAuth({ + cfg, + provider, + profileId: candidate, + credential: store.profiles[candidate], + }) + ) { + return true; + } const resolved = await resolveApiKeyForProfile({ cfg, store, diff --git a/src/agents/models-config.providers.secret-helpers.ts b/src/agents/models-config.providers.secret-helpers.ts index 9735df227e4..19f137799f8 100644 --- a/src/agents/models-config.providers.secret-helpers.ts +++ b/src/agents/models-config.providers.secret-helpers.ts @@ -39,7 +39,7 @@ export type ProviderAuthResolver = ( ) => { apiKey: string | undefined; discoveryApiKey?: string; - mode: "api_key" | "oauth" | "token" | "none"; + mode: "api_key" | "aws-sdk" | "oauth" | "token" | "none"; source: "env" | "profile" | "none"; profileId?: string; }; diff --git a/src/auto-reply/reply/directive-handling.auth.ts b/src/auto-reply/reply/directive-handling.auth.ts index 320db4b1c28..2dbb1190810 100644 --- a/src/auto-reply/reply/directive-handling.auth.ts +++ b/src/auto-reply/reply/directive-handling.auth.ts @@ -113,6 +113,12 @@ export const resolveAuthLabel = async ( source: "", }; } + if (profile.type === "aws-sdk") { + return { + label: `${profileId} aws-sdk${more}`, + source: "", + }; + } const display = resolveAuthProfileDisplayLabel({ cfg, store, profileId }); const label = display === profileId ? profileId : display; const exp = formatExpirationLabel(profile.expires, now, formatUntil); @@ -169,6 +175,10 @@ export const resolveAuthLabel = async ( const suffix = formatFlagsSuffix(flags); return `${profileId}=token:${tokenLabel}${suffix}`; } + if (profile.type === "aws-sdk") { + const suffix = formatFlagsSuffix(flags); + return `${profileId}=aws-sdk${suffix}`; + } const display = resolveAuthProfileDisplayLabel({ cfg, store, diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index dfe2a3b5550..a5ce3135d08 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -98,7 +98,7 @@ vi.mock("../plugins/provider-auth-helpers.js", () => ({ params: { profileId: string; provider: string; - mode: "api_key" | "oauth" | "token"; + mode: "api_key" | "aws-sdk" | "oauth" | "token"; email?: string; displayName?: string; }, diff --git a/src/commands/doctor-auth-flat-profiles.ts b/src/commands/doctor-auth-flat-profiles.ts index 8fcf9ed4bb1..a8f988e8dcc 100644 --- a/src/commands/doctor-auth-flat-profiles.ts +++ b/src/commands/doctor-auth-flat-profiles.ts @@ -51,7 +51,12 @@ function inferLegacyCredentialType( record: Record, ): AuthProfileCredential["type"] | undefined { const explicit = readNonEmptyString(record.type) ?? readNonEmptyString(record.mode); - if (explicit === "api_key" || explicit === "token" || explicit === "oauth") { + if ( + explicit === "api_key" || + explicit === "aws-sdk" || + explicit === "token" || + explicit === "oauth" + ) { return explicit; } if (readNonEmptyString(record.key) ?? readNonEmptyString(record.apiKey)) { diff --git a/src/commands/doctor-auth-profile-config.ts b/src/commands/doctor-auth-profile-config.ts index bd95da733fd..268e165d764 100644 --- a/src/commands/doctor-auth-profile-config.ts +++ b/src/commands/doctor-auth-profile-config.ts @@ -8,7 +8,12 @@ import { } from "../shared/string-coerce.js"; import { isRecord } from "../utils.js"; -const AUTH_PROFILE_MODES = new Set(["api_key", "oauth", "token"]); +const AUTH_PROFILE_MODES = new Set([ + "api_key", + "aws-sdk", + "oauth", + "token", +]); export type AuthProfileConfigProtectionResult = { config: OpenClawConfig; diff --git a/src/commands/models/auth-list.ts b/src/commands/models/auth-list.ts index 907dd5a90d9..41d9631c488 100644 --- a/src/commands/models/auth-list.ts +++ b/src/commands/models/auth-list.ts @@ -46,7 +46,9 @@ function formatTimestamp(value: number | undefined): string | undefined { } function resolveProfileExpiry(profile: AuthProfileCredential): string | undefined { - return profile.type === "api_key" ? undefined : formatTimestamp(profile.expires); + return profile.type === "oauth" || profile.type === "token" + ? formatTimestamp(profile.expires) + : undefined; } function summarizeProfile(params: { diff --git a/src/commands/models/auth.test.ts b/src/commands/models/auth.test.ts index 5c5dccba2ce..cb09062cced 100644 --- a/src/commands/models/auth.test.ts +++ b/src/commands/models/auth.test.ts @@ -47,7 +47,7 @@ vi.mock("../../plugins/provider-auth-helpers.js", () => ({ params: { profileId: string; provider: string; - mode: "api_key" | "oauth" | "token"; + mode: "api_key" | "aws-sdk" | "oauth" | "token"; email?: string; displayName?: string; }, diff --git a/src/commands/models/auth.ts b/src/commands/models/auth.ts index 9968591613f..033815c0816 100644 --- a/src/commands/models/auth.ts +++ b/src/commands/models/auth.ts @@ -588,10 +588,13 @@ export function resolveRequestedLoginProviderOrThrow( return resolveRequestedProviderOrThrow(providers, rawProvider); } -function credentialMode(credential: AuthProfileCredential): "api_key" | "oauth" | "token" { +function credentialMode(credential: AuthProfileCredential): AuthProfileCredential["type"] { if (credential.type === "api_key") { return "api_key"; } + if (credential.type === "aws-sdk") { + return "aws-sdk"; + } if (credential.type === "token") { return "token"; } diff --git a/src/commands/models/list.auth-overview.ts b/src/commands/models/list.auth-overview.ts index 86442763fdf..19d8235cd41 100644 --- a/src/commands/models/list.auth-overview.ts +++ b/src/commands/models/list.auth-overview.ts @@ -110,6 +110,9 @@ export function resolveProviderAuthOverview(params: { profileId, ); } + if (profile.type === "aws-sdk") { + return withUnusableSuffix(`${profileId}=AWS SDK`, profileId); + } const display = resolveAuthProfileDisplayLabel({ cfg, store, profileId }); const suffix = display === profileId @@ -123,6 +126,7 @@ export function resolveProviderAuthOverview(params: { const oauthCount = profiles.filter((id) => store.profiles[id]?.type === "oauth").length; const tokenCount = profiles.filter((id) => store.profiles[id]?.type === "token").length; const apiKeyCount = profiles.filter((id) => store.profiles[id]?.type === "api_key").length; + const awsSdkCount = profiles.filter((id) => store.profiles[id]?.type === "aws-sdk").length; const envKey = resolveEnvApiKey(provider, process.env, { config: cfg, @@ -171,6 +175,7 @@ export function resolveProviderAuthOverview(params: { oauth: oauthCount, token: tokenCount, apiKey: apiKeyCount, + awsSdk: awsSdkCount, labels, }, ...(envKey diff --git a/src/commands/models/list.status-command.ts b/src/commands/models/list.status-command.ts index 6517e099a55..e2494817029 100644 --- a/src/commands/models/list.status-command.ts +++ b/src/commands/models/list.status-command.ts @@ -671,7 +671,7 @@ export async function modelsStatusCommand( bits.push( formatKeyValue( "profiles", - `${entry.profiles.count} (oauth=${entry.profiles.oauth}, token=${entry.profiles.token}, api_key=${entry.profiles.apiKey})`, + `${entry.profiles.count} (oauth=${entry.profiles.oauth}, token=${entry.profiles.token}, api_key=${entry.profiles.apiKey}, aws-sdk=${entry.profiles.awsSdk})`, ), ); if (entry.profiles.labels.length > 0) { diff --git a/src/commands/models/list.types.ts b/src/commands/models/list.types.ts index 060286a888d..e0acaefdc12 100644 --- a/src/commands/models/list.types.ts +++ b/src/commands/models/list.types.ts @@ -28,6 +28,7 @@ export type ProviderAuthOverview = { oauth: number; token: number; apiKey: number; + awsSdk: number; labels: string[]; }; env?: { value: string; source: string }; diff --git a/src/config/types.auth.ts b/src/config/types.auth.ts index ca9340dd6b9..8cfad18d663 100644 --- a/src/config/types.auth.ts +++ b/src/config/types.auth.ts @@ -5,8 +5,9 @@ export type AuthProfileConfig = { * - api_key: static provider API key * - oauth: refreshable OAuth credentials (access+refresh+expires) * - token: static bearer-style token (optionally expiring; no refresh) + * - aws-sdk: AWS SDK default credential chain (no secret in auth-profiles.json) */ - mode: "api_key" | "oauth" | "token"; + mode: "api_key" | "aws-sdk" | "oauth" | "token"; email?: string; displayName?: string; }; diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 9a848290b20..73760e8c4ec 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -560,7 +560,12 @@ export const OpenClawSchema = z z .object({ provider: z.string(), - mode: z.union([z.literal("api_key"), z.literal("oauth"), z.literal("token")]), + mode: z.union([ + z.literal("api_key"), + z.literal("aws-sdk"), + z.literal("oauth"), + z.literal("token"), + ]), email: z.string().optional(), displayName: z.string().optional(), }) diff --git a/src/gateway/server-methods/models-auth-status.ts b/src/gateway/server-methods/models-auth-status.ts index 6f7d637046c..5ba6df52a9f 100644 --- a/src/gateway/server-methods/models-auth-status.ts +++ b/src/gateway/server-methods/models-auth-status.ts @@ -42,7 +42,7 @@ export type ModelAuthExpiry = { export type ModelAuthStatusProfile = { profileId: string; - type: "oauth" | "token" | "api_key"; + type: "oauth" | "token" | "api_key" | "aws-sdk"; status: AuthProfileHealthStatus; expiry?: ModelAuthExpiry; }; diff --git a/src/infra/provider-usage.auth.ts b/src/infra/provider-usage.auth.ts index 16e5b65563e..c9ac81379bf 100644 --- a/src/infra/provider-usage.auth.ts +++ b/src/infra/provider-usage.auth.ts @@ -337,7 +337,12 @@ function hasAuthProfileCredentialSource(params: { if ( dedupeProfileIds(order).some((profileId) => { const cred = store.profiles[profileId]; - return cred?.type === "api_key" || cred?.type === "oauth" || cred?.type === "token"; + return ( + cred?.type === "api_key" || + cred?.type === "aws-sdk" || + cred?.type === "oauth" || + cred?.type === "token" + ); }) ) { return true; diff --git a/src/plugin-sdk/test-helpers/provider-discovery-contract.ts b/src/plugin-sdk/test-helpers/provider-discovery-contract.ts index 6b49cd1f3a2..1a83175003e 100644 --- a/src/plugin-sdk/test-helpers/provider-discovery-contract.ts +++ b/src/plugin-sdk/test-helpers/provider-discovery-contract.ts @@ -90,7 +90,7 @@ function runCatalog( ) => { apiKey: string | undefined; discoveryApiKey?: string; - mode: "api_key" | "oauth" | "token" | "none"; + mode: "api_key" | "aws-sdk" | "oauth" | "token" | "none"; source: "env" | "profile" | "none"; profileId?: string; }; diff --git a/src/plugins/provider-auth-helpers.ts b/src/plugins/provider-auth-helpers.ts index b8e56335c3a..ce97bf4162a 100644 --- a/src/plugins/provider-auth-helpers.ts +++ b/src/plugins/provider-auth-helpers.ts @@ -138,7 +138,7 @@ export function applyAuthProfileConfig( params: { profileId: string; provider: string; - mode: "api_key" | "oauth" | "token"; + mode: "api_key" | "aws-sdk" | "oauth" | "token"; email?: string; displayName?: string; preferProfileFirst?: boolean; diff --git a/src/plugins/provider-discovery.ts b/src/plugins/provider-discovery.ts index 3b0723cda8b..6e30ac74ab8 100644 --- a/src/plugins/provider-discovery.ts +++ b/src/plugins/provider-discovery.ts @@ -155,7 +155,7 @@ export function runProviderCatalog(params: { ) => { apiKey: string | undefined; discoveryApiKey?: string; - mode: "api_key" | "oauth" | "token" | "none"; + mode: "api_key" | "aws-sdk" | "oauth" | "token" | "none"; source: "env" | "profile" | "none"; profileId?: string; }; diff --git a/src/plugins/types.ts b/src/plugins/types.ts index f7b228c8399..ab7fb2f39f5 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -441,7 +441,7 @@ export type ProviderCatalogContext = { ) => { apiKey: string | undefined; discoveryApiKey?: string; - mode: "api_key" | "oauth" | "token" | "none"; + mode: "api_key" | "aws-sdk" | "oauth" | "token" | "none"; source: "env" | "profile" | "none"; profileId?: string; }; diff --git a/src/secrets/runtime-auth-profiles-oauth-policy.test.ts b/src/secrets/runtime-auth-profiles-oauth-policy.test.ts index 0d718717eb7..f4dd3886bb2 100644 --- a/src/secrets/runtime-auth-profiles-oauth-policy.test.ts +++ b/src/secrets/runtime-auth-profiles-oauth-policy.test.ts @@ -7,7 +7,7 @@ import { const { prepareSecretsRuntimeSnapshot } = setupSecretsRuntimeSnapshotTestHooks(); -function withAuthProfileMode(mode: "api_key" | "oauth" | "token"): OpenClawConfig { +function withAuthProfileMode(mode: "api_key" | "aws-sdk" | "oauth" | "token"): OpenClawConfig { return { auth: { profiles: { From d4e04f33a65f555adb7ac41a617b6d9ff8ff1926 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 7 May 2026 02:16:46 -0500 Subject: [PATCH 059/215] fix(sessions): retire stale direct dm rows after dmscope changes Summary: - Add explicit sessions cleanup --fix-dm-scope handling for stale direct-DM rows after session.dmScope returns to main. - Preserve removed-row transcripts as deleted archives and expose the option through CLI, Gateway RPC, protocol schema, generated Swift mirrors, docs, tests, and changelog. - Fixes #47561 and #45554. Verification: - pnpm exec oxfmt --check --threads=1 CHANGELOG.md docs/cli/sessions.md docs/concepts/session.md src/config/sessions/cleanup-service.ts src/commands/sessions-cleanup.ts src/cli/program/register.status-health-sessions.ts src/gateway/protocol/schema/sessions.ts src/gateway/server-methods/sessions.ts src/config/sessions/store.pruning.integration.test.ts src/commands/sessions-cleanup.test.ts src/cli/program/register.status-health-sessions.test.ts - git diff --check origin/main...HEAD - pnpm protocol:check - pnpm exec oxlint src/config/sessions/cleanup-service.ts src/commands/sessions-cleanup.ts src/cli/program/register.status-health-sessions.ts src/gateway/protocol/schema/sessions.ts src/gateway/server-methods/sessions.ts src/config/sessions/store.pruning.integration.test.ts src/commands/sessions-cleanup.test.ts src/cli/program/register.status-health-sessions.test.ts - pnpm test src/config/sessions/store.pruning.integration.test.ts src/commands/sessions-cleanup.test.ts src/cli/program/register.status-health-sessions.test.ts src/gateway/server.sessions.store-rpc.test.ts - pnpm changed:lanes --json Security: - No new network, credential, process execution, dependency, or permission surface. Cleanup is explicit operator-invoked local session-store repair. CI note: - Exact-head CI failures match current main at 2e78fc57af in unrelated extensions/codex and extensions/microsoft-foundry type checks, outside this PR diff. No required checks are reported for this branch. --- CHANGELOG.md | 1 + .../OpenClawProtocol/GatewayModels.swift | 6 +- .../OpenClawProtocol/GatewayModels.swift | 6 +- docs/cli/sessions.md | 6 + docs/concepts/session.md | 6 + .../register.status-health-sessions.test.ts | 2 + .../register.status-health-sessions.ts | 10 ++ src/commands/sessions-cleanup.test.ts | 16 ++ src/commands/sessions-cleanup.ts | 9 +- src/config/sessions/cleanup-service.ts | 145 ++++++++++++++++-- .../store.pruning.integration.test.ts | 86 +++++++++++ src/gateway/protocol/schema/sessions.ts | 1 + src/gateway/server-methods/sessions.ts | 1 + 13 files changed, 283 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f12cb90a1c4..c486b9b75dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -228,6 +228,7 @@ Docs: https://docs.openclaw.ai - WebChat/Codex media: stage Codex app-server generated local images into managed media before Gateway display, so Codex-home image paths no longer hit `LocalMediaAccessError` while keeping Codex home out of the display allowlist. Thanks @frankekn. - Plugins/update: repair plugin-local `openclaw` peer links for all recorded npm plugins after any npm update mutates the shared managed npm tree, so targeted or batch updates cannot leave Codex, Discord, or Brave with pruned SDK imports. (#77787) Thanks @ProspectOre. - Codex harness: honor `models.providers.openai-codex.models[].contextTokens` for native `openai/*` Codex runtime runs and `/status` context reporting, so subscription-backed Codex agents use the configured OAuth context cap without inflating past the runtime model window. Fixes #77858. Thanks @lilesjtu. +- Sessions cleanup: add `openclaw sessions cleanup --fix-dm-scope` so operators who return `session.dmScope` to `main` can dry-run and retire stale direct-DM session rows while preserving transcripts as deleted archives. Fixes #47561 and #45554. Thanks @BunsDev. - TUI/sessions: bound the session picker to recent rows and use exact lookup-style refreshes for the active session, so dusty stores no longer make TUI hydrate weeks-old transcripts before becoming responsive. Thanks @vincentkoc. - Doctor/gateway: report recent supervisor restart handoffs in `openclaw doctor --deep`, using the installed service environment when available so service-managed clean exits are visible in guided diagnostics. Thanks @shakkernerd. - Gateway/status: show recent supervisor restart handoffs in `openclaw gateway status --deep`, including JSON details, so clean service-managed restarts are reported as restart handoffs instead of opaque stopped-service diagnostics. Thanks @shakkernerd. diff --git a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift index 806412bce9b..ccef9eb5dd0 100644 --- a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift @@ -1568,19 +1568,22 @@ public struct SessionsCleanupParams: Codable, Sendable { public let enforce: Bool? public let activekey: String? public let fixmissing: Bool? + public let fixdmscope: Bool? public init( agent: String?, allagents: Bool?, enforce: Bool?, activekey: String?, - fixmissing: Bool?) + fixmissing: Bool?, + fixdmscope: Bool?) { self.agent = agent self.allagents = allagents self.enforce = enforce self.activekey = activekey self.fixmissing = fixmissing + self.fixdmscope = fixdmscope } private enum CodingKeys: String, CodingKey { @@ -1589,6 +1592,7 @@ public struct SessionsCleanupParams: Codable, Sendable { case enforce case activekey = "activeKey" case fixmissing = "fixMissing" + case fixdmscope = "fixDmScope" } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index 806412bce9b..ccef9eb5dd0 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -1568,19 +1568,22 @@ public struct SessionsCleanupParams: Codable, Sendable { public let enforce: Bool? public let activekey: String? public let fixmissing: Bool? + public let fixdmscope: Bool? public init( agent: String?, allagents: Bool?, enforce: Bool?, activekey: String?, - fixmissing: Bool?) + fixmissing: Bool?, + fixdmscope: Bool?) { self.agent = agent self.allagents = allagents self.enforce = enforce self.activekey = activekey self.fixmissing = fixmissing + self.fixdmscope = fixdmscope } private enum CodingKeys: String, CodingKey { @@ -1589,6 +1592,7 @@ public struct SessionsCleanupParams: Codable, Sendable { case enforce case activekey = "activeKey" case fixmissing = "fixMissing" + case fixdmscope = "fixDmScope" } } diff --git a/docs/cli/sessions.md b/docs/cli/sessions.md index 6a510b3e129..66dee565935 100644 --- a/docs/cli/sessions.md +++ b/docs/cli/sessions.md @@ -93,6 +93,7 @@ openclaw sessions cleanup --agent work --dry-run openclaw sessions cleanup --all-agents --dry-run openclaw sessions cleanup --enforce openclaw sessions cleanup --enforce --active-key "agent:main:telegram:direct:123" +openclaw sessions cleanup --dry-run --fix-dm-scope openclaw sessions cleanup --json ``` @@ -105,6 +106,7 @@ openclaw sessions cleanup --json - In text mode, dry-run prints a per-session action table (`Action`, `Key`, `Age`, `Model`, `Flags`) so you can see what would be kept vs removed. - `--enforce`: apply maintenance even when `session.maintenance.mode` is `warn`. - `--fix-missing`: remove entries whose transcript files are missing, even if they would not normally age/count out yet. +- `--fix-dm-scope`: when `session.dmScope` is `main`, retire stale peer-keyed direct-DM rows left behind by earlier `per-peer`, `per-channel-peer`, or `per-account-channel-peer` routing. Use `--dry-run` first; applying the cleanup removes those rows from `sessions.json` and preserves their transcripts as deleted archives. - `--active-key `: protect a specific active key from disk-budget eviction. Durable external conversation pointers, such as group sessions and thread-scoped chat sessions, are also kept by age/count/disk-budget maintenance. - `--agent `: run cleanup for one configured agent store. - `--all-agents`: run cleanup for all configured agent stores. @@ -128,6 +130,8 @@ traffic. Use `--store ` for explicit offline repair of a store file. "storePath": "/home/user/.openclaw/agents/main/sessions/sessions.json", "beforeCount": 120, "afterCount": 80, + "missing": 0, + "dmScopeRetired": 0, "pruned": 40, "capped": 0 }, @@ -136,6 +140,8 @@ traffic. Use `--store ` for explicit offline repair of a store file. "storePath": "/home/user/.openclaw/agents/work/sessions/sessions.json", "beforeCount": 18, "afterCount": 18, + "missing": 0, + "dmScopeRetired": 0, "pruned": 0, "capped": 0 } diff --git a/docs/concepts/session.md b/docs/concepts/session.md index 0ac9d1a9a7a..c831d72238c 100644 --- a/docs/concepts/session.md +++ b/docs/concepts/session.md @@ -131,6 +131,12 @@ Maintenance preserves durable external conversation pointers, including group sessions and thread-scoped chat sessions, while still allowing synthetic cron, hook, heartbeat, ACP, and sub-agent entries to age out. +If you previously used direct-message isolation and later returned +`session.dmScope` to `main`, preview stale peer-keyed DM rows with +`openclaw sessions cleanup --dry-run --fix-dm-scope`. Applying the same flag +retires those old direct-DM rows and keeps their transcripts as deleted +archives. + Preview with `openclaw sessions cleanup --dry-run`. ## Inspecting sessions diff --git a/src/cli/program/register.status-health-sessions.test.ts b/src/cli/program/register.status-health-sessions.test.ts index a7f0764f8f9..d172074ac3c 100644 --- a/src/cli/program/register.status-health-sessions.test.ts +++ b/src/cli/program/register.status-health-sessions.test.ts @@ -239,6 +239,7 @@ describe("registerStatusHealthSessionsCommands", () => { "--dry-run", "--enforce", "--fix-missing", + "--fix-dm-scope", "--active-key", "agent:main:main", "--json", @@ -252,6 +253,7 @@ describe("registerStatusHealthSessionsCommands", () => { dryRun: true, enforce: true, fixMissing: true, + fixDmScope: true, activeKey: "agent:main:main", json: true, }), diff --git a/src/cli/program/register.status-health-sessions.ts b/src/cli/program/register.status-health-sessions.ts index 4d39987135e..c4c21e086f7 100644 --- a/src/cli/program/register.status-health-sessions.ts +++ b/src/cli/program/register.status-health-sessions.ts @@ -182,6 +182,11 @@ export function registerStatusHealthSessionsCommands(program: Command) { "Remove store entries whose transcript files are missing (bypasses age/count retention)", false, ) + .option( + "--fix-dm-scope", + "Retire stale direct-DM session rows that no longer match session.dmScope=main", + false, + ) .option("--active-key ", "Protect this session key from budget-eviction") .option("--json", "Output JSON", false) .addHelpText( @@ -193,6 +198,10 @@ export function registerStatusHealthSessionsCommands(program: Command) { "openclaw sessions cleanup --dry-run --fix-missing", "Also preview pruning entries with missing transcript files.", ], + [ + "openclaw sessions cleanup --dry-run --fix-dm-scope", + "Preview stale direct-DM rows after returning dmScope to main.", + ], ["openclaw sessions cleanup --enforce", "Apply maintenance now."], ["openclaw sessions cleanup --agent work --dry-run", "Preview one agent store."], ["openclaw sessions cleanup --all-agents --dry-run", "Preview all agent stores."], @@ -220,6 +229,7 @@ export function registerStatusHealthSessionsCommands(program: Command) { dryRun: Boolean(opts.dryRun), enforce: Boolean(opts.enforce), fixMissing: Boolean(opts.fixMissing), + fixDmScope: Boolean(opts.fixDmScope), activeKey: opts.activeKey as string | undefined, json: Boolean(opts.json || parentOpts?.json), }, diff --git a/src/commands/sessions-cleanup.test.ts b/src/commands/sessions-cleanup.test.ts index 323a23a2f73..64d3a580716 100644 --- a/src/commands/sessions-cleanup.test.ts +++ b/src/commands/sessions-cleanup.test.ts @@ -119,7 +119,11 @@ describe("sessionsCleanupCommand", () => { staleKeys: Set; cappedKeys: Set; budgetEvictedKeys: Set; + dmScopeRetiredKeys: Set; }) => { + if (params.dmScopeRetiredKeys.has(params.key)) { + return "retire-dm-scope"; + } if (params.missingKeys.has(params.key)) { return "prune-missing"; } @@ -181,6 +185,7 @@ describe("sessionsCleanupCommand", () => { beforeCount: 3, afterCount: 1, missing: 0, + dmScopeRetired: 0, pruned: 0, capped: 2, diskBudget: { @@ -245,6 +250,7 @@ describe("sessionsCleanupCommand", () => { beforeCount: 3, afterCount: 1, missing: 0, + dmScopeRetired: 0, pruned: 2, capped: 0, diskBudget: null, @@ -286,6 +292,7 @@ describe("sessionsCleanupCommand", () => { beforeCount: 2, afterCount: 1, missing: 0, + dmScopeRetired: 0, pruned: 1, capped: 0, diskBudget: { @@ -305,6 +312,7 @@ describe("sessionsCleanupCommand", () => { staleKeys: new Set(), cappedKeys: new Set(), budgetEvictedKeys: new Set(), + dmScopeRetiredKeys: new Set(), }, ], appliedSummaries: [], @@ -347,6 +355,7 @@ describe("sessionsCleanupCommand", () => { beforeCount: 1, afterCount: 0, missing: 1, + dmScopeRetired: 0, pruned: 0, capped: 0, diskBudget: null, @@ -357,6 +366,7 @@ describe("sessionsCleanupCommand", () => { staleKeys: new Set(), cappedKeys: new Set(), budgetEvictedKeys: new Set(), + dmScopeRetiredKeys: new Set(), }, ], appliedSummaries: [], @@ -393,6 +403,7 @@ describe("sessionsCleanupCommand", () => { beforeCount: 2, afterCount: 1, missing: 0, + dmScopeRetired: 0, pruned: 1, capped: 0, unreferencedArtifacts: { @@ -412,6 +423,7 @@ describe("sessionsCleanupCommand", () => { staleKeys: new Set(["stale"]), cappedKeys: new Set(), budgetEvictedKeys: new Set(), + dmScopeRetiredKeys: new Set(), }, ], appliedSummaries: [], @@ -450,6 +462,7 @@ describe("sessionsCleanupCommand", () => { beforeCount: 1, afterCount: 0, missing: 0, + dmScopeRetired: 0, pruned: 1, capped: 0, diskBudget: null, @@ -460,6 +473,7 @@ describe("sessionsCleanupCommand", () => { staleKeys: new Set(["stale"]), cappedKeys: new Set(), budgetEvictedKeys: new Set(), + dmScopeRetiredKeys: new Set(), }, { summary: { @@ -470,6 +484,7 @@ describe("sessionsCleanupCommand", () => { beforeCount: 1, afterCount: 0, missing: 0, + dmScopeRetired: 0, pruned: 1, capped: 0, diskBudget: null, @@ -480,6 +495,7 @@ describe("sessionsCleanupCommand", () => { staleKeys: new Set(["stale"]), cappedKeys: new Set(), budgetEvictedKeys: new Set(), + dmScopeRetiredKeys: new Set(), }, ], appliedSummaries: [], diff --git a/src/commands/sessions-cleanup.ts b/src/commands/sessions-cleanup.ts index 80366e97a9b..b9e9d7b7e39 100644 --- a/src/commands/sessions-cleanup.ts +++ b/src/commands/sessions-cleanup.ts @@ -25,7 +25,7 @@ import { toSessionDisplayRows, } from "./sessions-table.js"; -const ACTION_PAD = 12; +const ACTION_PAD = 16; type SessionCleanupActionRow = ReturnType[number] & { action: ReturnType; @@ -48,6 +48,9 @@ function formatCleanupActionCell( if (action === "prune-stale") { return theme.warn(label); } + if (action === "retire-dm-scope") { + return theme.warn(label); + } if (action === "cap-overflow") { return theme.accentBright(label); } @@ -60,6 +63,7 @@ function buildActionRows(params: { staleKeys: Set; cappedKeys: Set; budgetEvictedKeys: Set; + dmScopeRetiredKeys: Set; }): SessionCleanupActionRow[] { return toSessionDisplayRows(params.beforeStore).map((row) => Object.assign({}, row, { @@ -69,6 +73,7 @@ function buildActionRows(params: { staleKeys: params.staleKeys, cappedKeys: params.cappedKeys, budgetEvictedKeys: params.budgetEvictedKeys, + dmScopeRetiredKeys: params.dmScopeRetiredKeys, }), }), ); @@ -91,6 +96,7 @@ function renderStoreDryRunPlan(params: { `Entries: ${params.summary.beforeCount} -> ${params.summary.afterCount} (remove ${params.summary.beforeCount - params.summary.afterCount})`, ); params.runtime.log(`Would prune missing transcripts: ${params.summary.missing}`); + params.runtime.log(`Would retire stale direct DM sessions: ${params.summary.dmScopeRetired}`); params.runtime.log(`Would prune stale: ${params.summary.pruned}`); params.runtime.log(`Would cap overflow: ${params.summary.capped}`); if (params.summary.unreferencedArtifacts?.scannedFiles) { @@ -169,6 +175,7 @@ async function maybeRunGatewayCleanup( enforce: opts.enforce, activeKey: opts.activeKey, fixMissing: opts.fixMissing, + fixDmScope: opts.fixDmScope, }, mode: GATEWAY_CLIENT_MODES.CLI, clientName: GATEWAY_CLIENT_NAMES.CLI, diff --git a/src/config/sessions/cleanup-service.ts b/src/config/sessions/cleanup-service.ts index 1da353be7c1..75d46a93f71 100644 --- a/src/config/sessions/cleanup-service.ts +++ b/src/config/sessions/cleanup-service.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { resolveStoredSessionOwnerAgentId } from "../../gateway/session-store-key.js"; import { getLogger } from "../../logging/logger.js"; -import { normalizeAgentId } from "../../routing/session-key.js"; +import { normalizeAgentId, parseAgentSessionKey } from "../../routing/session-key.js"; import type { OpenClawConfig } from "../types.openclaw.js"; import { enforceSessionDiskBudget, @@ -24,6 +24,7 @@ import { type ResolvedSessionMaintenanceConfig, } from "./store-maintenance.js"; import { + archiveRemovedSessionTranscripts, loadSessionStore, updateSessionStore, type SessionMaintenanceApplyReport, @@ -41,6 +42,7 @@ export type SessionsCleanupOptions = SessionStoreSelectionOptions & { activeKey?: string; json?: boolean; fixMissing?: boolean; + fixDmScope?: boolean; }; export type SessionCleanupAction = @@ -48,7 +50,8 @@ export type SessionCleanupAction = | "prune-missing" | "prune-stale" | "cap-overflow" - | "evict-budget"; + | "evict-budget" + | "retire-dm-scope"; export type SessionCleanupSummary = { agentId: string; @@ -58,6 +61,7 @@ export type SessionCleanupSummary = { beforeCount: number; afterCount: number; missing: number; + dmScopeRetired: number; pruned: number; capped: number; unreferencedArtifacts: SessionUnreferencedArtifactSweepResult; @@ -85,6 +89,7 @@ export type SessionsCleanupRunResult = { staleKeys: Set; cappedKeys: Set; budgetEvictedKeys: Set; + dmScopeRetiredKeys: Set; }>; appliedSummaries: SessionCleanupSummary[]; }; @@ -95,7 +100,11 @@ export function resolveSessionCleanupAction(params: { staleKeys: Set; cappedKeys: Set; budgetEvictedKeys: Set; + dmScopeRetiredKeys: Set; }): SessionCleanupAction { + if (params.dmScopeRetiredKeys.has(params.key)) { + return "retire-dm-scope"; + } if (params.missingKeys.has(params.key)) { return "prune-missing"; } @@ -111,6 +120,64 @@ export function resolveSessionCleanupAction(params: { return "keep"; } +function isMainScopeStaleDirectSessionKey(params: { + cfg: OpenClawConfig; + targetAgentId: string; + key: string; + activeKey?: string; +}): boolean { + if ((params.cfg.session?.dmScope ?? "main") !== "main") { + return false; + } + if (params.activeKey && params.key === params.activeKey) { + return false; + } + const parsed = parseAgentSessionKey(params.key); + if (!parsed || normalizeAgentId(parsed.agentId) !== normalizeAgentId(params.targetAgentId)) { + return false; + } + const parts = parsed.rest.split(":").filter(Boolean); + return ( + (parts.length === 2 && parts[0] === "direct") || + (parts.length === 3 && parts[1] === "direct") || + (parts.length === 4 && parts[2] === "direct") + ); +} + +function rememberRemovedSessionFile( + removedSessionFiles: Map, + entry: SessionEntry | undefined, +): void { + if (entry?.sessionId) { + removedSessionFiles.set(entry.sessionId, entry.sessionFile); + } +} + +function retireMainScopeDirectSessionEntries(params: { + cfg: OpenClawConfig; + store: Record; + targetAgentId: string; + activeKey?: string; + onRetired?: (key: string, entry: SessionEntry) => void; +}): number { + let retired = 0; + for (const [key, entry] of Object.entries(params.store)) { + if ( + isMainScopeStaleDirectSessionKey({ + cfg: params.cfg, + targetAgentId: params.targetAgentId, + key, + activeKey: params.activeKey, + }) + ) { + params.onRetired?.(key, entry); + delete params.store[key]; + retired += 1; + } + } + return retired; +} + export function serializeSessionCleanupResult(params: { mode: ResolvedSessionMaintenanceConfig["mode"]; dryRun: boolean; @@ -172,18 +239,21 @@ function addEntryArtifactPathsToSet(params: { } async function previewStoreCleanup(params: { + cfg: OpenClawConfig; target: SessionStoreTarget; maintenance: ResolvedSessionMaintenanceConfig; mode: ResolvedSessionMaintenanceConfig["mode"]; dryRun: boolean; activeKey?: string; fixMissing?: boolean; + fixDmScope?: boolean; }) { const beforeStore = loadSessionStore(params.target.storePath, { skipCache: true }); const previewStore = cloneSessionStoreRecord(beforeStore); const staleKeys = new Set(); const cappedKeys = new Set(); const missingKeys = new Set(); + const dmScopeRetiredKeys = new Set(); const missing = params.fixMissing === true ? pruneMissingTranscriptEntries({ @@ -194,6 +264,18 @@ async function previewStoreCleanup(params: { }, }) : 0; + const dmScopeRetired = + params.fixDmScope === true + ? retireMainScopeDirectSessionEntries({ + cfg: params.cfg, + store: previewStore, + targetAgentId: params.target.agentId, + activeKey: params.activeKey, + onRetired: (key) => { + dmScopeRetiredKeys.add(key); + }, + }) + : 0; const pruned = pruneStaleEntries(previewStore, params.maintenance.pruneAfterMs, { log: false, onPruned: ({ key }) => { @@ -219,6 +301,12 @@ async function previewStoreCleanup(params: { storePath: params.target.storePath, keys: cappedKeys, }); + addEntryArtifactPathsToSet({ + paths: entryCleanupArtifactPaths, + store: beforeStore, + storePath: params.target.storePath, + keys: dmScopeRetiredKeys, + }); const beforeBudgetStore = cloneSessionStoreRecord(previewStore); const budgetRemovedFilePaths = new Set(); const diskBudget = await enforceSessionDiskBudget({ @@ -249,6 +337,7 @@ async function previewStoreCleanup(params: { const afterPreviewCount = Object.keys(previewStore).length; const wouldMutate = missing > 0 || + dmScopeRetired > 0 || pruned > 0 || capped > 0 || unreferencedArtifacts.removedFiles > 0 || @@ -263,6 +352,7 @@ async function previewStoreCleanup(params: { beforeCount, afterCount: afterPreviewCount, missing, + dmScopeRetired, pruned, capped, unreferencedArtifacts, @@ -277,6 +367,7 @@ async function previewStoreCleanup(params: { staleKeys, cappedKeys, budgetEvictedKeys, + dmScopeRetiredKeys, }; } @@ -299,12 +390,14 @@ export async function runSessionsCleanup(params: { const previewResults: SessionsCleanupRunResult["previewResults"] = []; for (const target of targets) { const result = await previewStoreCleanup({ + cfg, target, maintenance, mode, dryRun: Boolean(opts.dryRun), activeKey: opts.activeKey, fixMissing: Boolean(opts.fixMissing), + fixDmScope: Boolean(opts.fixDmScope), }); previewResults.push(result); } @@ -315,16 +408,33 @@ export async function runSessionsCleanup(params: { const appliedReportRef: { current: SessionMaintenanceApplyReport | null } = { current: null, }; - const missingApplied = await updateSessionStore( + const dmScopeRemovedSessionFiles = new Map(); + let missingApplied = 0; + let dmScopeRetiredApplied = 0; + await updateSessionStore( target.storePath, async (store) => { - if (!opts.fixMissing) { - return 0; + let removed = 0; + if (opts.fixMissing) { + missingApplied = pruneMissingTranscriptEntries({ + store, + storePath: target.storePath, + }); + removed += missingApplied; } - return pruneMissingTranscriptEntries({ - store, - storePath: target.storePath, - }); + if (opts.fixDmScope) { + dmScopeRetiredApplied = retireMainScopeDirectSessionEntries({ + cfg, + store, + targetAgentId: target.agentId, + activeKey: opts.activeKey, + onRetired: (_key, entry) => { + rememberRemovedSessionFile(dmScopeRemovedSessionFiles, entry); + }, + }); + removed += dmScopeRetiredApplied; + } + return removed; }, { activeSessionKey: opts.activeKey, @@ -336,6 +446,20 @@ export async function runSessionsCleanup(params: { }, }, ); + if (dmScopeRemovedSessionFiles.size > 0) { + const storeAfterDmScopeRetire = loadSessionStore(target.storePath, { skipCache: true }); + await archiveRemovedSessionTranscripts({ + removedSessionFiles: dmScopeRemovedSessionFiles, + referencedSessionIds: new Set( + Object.values(storeAfterDmScopeRetire) + .map((entry) => entry?.sessionId) + .filter((id): id is string => Boolean(id)), + ), + storePath: target.storePath, + reason: "deleted", + restrictToStoreDir: true, + }); + } const afterStore = loadSessionStore(target.storePath, { skipCache: true }); const unreferencedArtifacts = mode === "warn" @@ -366,6 +490,7 @@ export async function runSessionsCleanup(params: { beforeCount: 0, afterCount: 0, missing: 0, + dmScopeRetired: 0, pruned: 0, capped: 0, unreferencedArtifacts, @@ -387,12 +512,14 @@ export async function runSessionsCleanup(params: { beforeCount: appliedReport.beforeCount, afterCount: appliedReport.afterCount, missing: missingApplied, + dmScopeRetired: dmScopeRetiredApplied, pruned: appliedReport.pruned, capped: appliedReport.capped, unreferencedArtifacts, diskBudget: appliedReport.diskBudget, wouldMutate: missingApplied > 0 || + dmScopeRetiredApplied > 0 || appliedReport.pruned > 0 || appliedReport.capped > 0 || unreferencedArtifacts.removedFiles > 0 || diff --git a/src/config/sessions/store.pruning.integration.test.ts b/src/config/sessions/store.pruning.integration.test.ts index e6e2699c34e..3f42e4dddeb 100644 --- a/src/config/sessions/store.pruning.integration.test.ts +++ b/src/config/sessions/store.pruning.integration.test.ts @@ -307,6 +307,92 @@ describe("Integration: saveSessionStore with pruning", () => { await expect(fs.stat(freshOrphanTranscript)).resolves.toBeDefined(); }); + it("sessions cleanup previews stale direct DM rows after dmScope returns to main", async () => { + applyEnforcedMaintenanceConfig(mockLoadConfig); + + const now = Date.now(); + const directTranscript = path.join(testDir, "direct-session.jsonl"); + await fs.writeFile( + storePath, + JSON.stringify( + { + "agent:main:main": { + sessionId: "main-session", + updatedAt: now, + }, + "agent:main:telegram:direct:6101296751": { + sessionId: "direct-session", + updatedAt: now, + lastChannel: "telegram", + lastTo: "6101296751", + }, + } satisfies Record, + null, + 2, + ), + "utf-8", + ); + await fs.writeFile(path.join(testDir, "main-session.jsonl"), "main", "utf-8"); + await fs.writeFile(directTranscript, "direct", "utf-8"); + + const dryRun = await runSessionsCleanup({ + cfg: { session: { dmScope: "main" } }, + opts: { store: storePath, dryRun: true, enforce: true, fixDmScope: true }, + targets: [{ agentId: "main", storePath }], + }); + + const preview = dryRun.previewResults[0]; + expect(preview?.summary.dmScopeRetired).toBe(1); + expect(preview?.summary.afterCount).toBe(1); + expect(preview?.dmScopeRetiredKeys.has("agent:main:telegram:direct:6101296751")).toBe(true); + expect(preview?.summary.unreferencedArtifacts.removedFiles).toBe(0); + await expect(fs.stat(directTranscript)).resolves.toBeDefined(); + }); + + it("sessions cleanup retires stale direct DM rows and archives their transcripts", async () => { + applyEnforcedMaintenanceConfig(mockLoadConfig); + + const now = Date.now(); + const directTranscript = path.join(testDir, "direct-session.jsonl"); + await fs.writeFile( + storePath, + JSON.stringify( + { + "agent:main:main": { + sessionId: "main-session", + updatedAt: now, + }, + "agent:main:telegram:direct:6101296751": { + sessionId: "direct-session", + updatedAt: now, + sessionFile: directTranscript, + lastChannel: "telegram", + lastTo: "6101296751", + }, + } satisfies Record, + null, + 2, + ), + "utf-8", + ); + await fs.writeFile(path.join(testDir, "main-session.jsonl"), "main", "utf-8"); + await fs.writeFile(directTranscript, "direct", "utf-8"); + + const applied = await runSessionsCleanup({ + cfg: { session: { dmScope: "main" } }, + opts: { store: storePath, enforce: true, fixDmScope: true }, + targets: [{ agentId: "main", storePath }], + }); + + expect(applied.appliedSummaries[0]?.dmScopeRetired).toBe(1); + const persisted = loadSessionStore(storePath, { skipCache: true }); + expect(persisted["agent:main:main"]).toBeDefined(); + expect(persisted["agent:main:telegram:direct:6101296751"]).toBeUndefined(); + await expect(fs.stat(directTranscript)).rejects.toThrow(); + const files = await fs.readdir(testDir); + expect(files.some((name) => name.startsWith("direct-session.jsonl.deleted."))).toBe(true); + }); + it("sessions cleanup dry-run does not double-count artifacts already covered by disk budget", async () => { mockLoadConfig.mockReturnValue({ session: { diff --git a/src/gateway/protocol/schema/sessions.ts b/src/gateway/protocol/schema/sessions.ts index 6fa6de273a0..bb7a690a8b2 100644 --- a/src/gateway/protocol/schema/sessions.ts +++ b/src/gateway/protocol/schema/sessions.ts @@ -71,6 +71,7 @@ export const SessionsCleanupParamsSchema = Type.Object( enforce: Type.Optional(Type.Boolean()), activeKey: Type.Optional(NonEmptyString), fixMissing: Type.Optional(Type.Boolean()), + fixDmScope: Type.Optional(Type.Boolean()), }, { additionalProperties: false }, ); diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 02c5008c48e..ec17d72e0da 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -708,6 +708,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { enforce: params.enforce, activeKey: params.activeKey, fixMissing: params.fixMissing, + fixDmScope: params.fixDmScope, }, }); const result = serializeSessionCleanupResult({ From 62ccd8b644ebeade4c12091980b60686cc92e660 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 7 May 2026 02:29:28 -0500 Subject: [PATCH 060/215] Fix model and tool normalization regressions Summary: - Fix model and tool normalization regressions, including explicit tool-policy grants for messaging profile warnings. - Keep Codex and Microsoft Foundry auth handling compatible with aws-sdk auth profile modes after rebasing onto current main. Verification: - pnpm test src/agents/pi-tools.policy.test.ts - pnpm tsgo:extensions - pnpm tsgo:extensions:test - pnpm test extensions/codex/src/app-server/auth-bridge.test.ts extensions/microsoft-foundry/index.test.ts - pnpm test:extensions:package-boundary - pnpm lint --threads=8 - git diff --check - GitHub PR checks green on 4ad136106bd6485422050f7efe0b12ebf9951873 --- .gitignore | 1 + CHANGELOG.md | 6 ++ .../codex/src/app-server/auth-bridge.test.ts | 32 +++++++ .../codex/src/app-server/auth-bridge.ts | 3 + .../src/app-server/dynamic-tool-profile.ts | 16 +++- .../codex/src/app-server/run-attempt.test.ts | 10 ++ .../codex/src/app-server/run-attempt.ts | 24 ++++- .../src/dreaming-narrative.test.ts | 10 ++ .../memory-core/src/dreaming-narrative.ts | 3 +- extensions/microsoft-foundry/index.test.ts | 34 +++++++ extensions/microsoft-foundry/shared.ts | 2 +- .../bash-tools.exec-host-node-phases.ts | 5 + src/agents/bash-tools.exec-host-node.test.ts | 30 ++++++ src/agents/model-fallback.test.ts | 15 +++ src/agents/model-selection-cli.test.ts | 4 + src/agents/model-selection.test.ts | 19 ++++ src/agents/pi-tools.policy.test.ts | 92 +++++++++++++++++++ src/agents/pi-tools.policy.ts | 56 +++++++---- src/agents/provider-id.ts | 3 + src/agents/skills/plugin-skills.test.ts | 19 ++++ src/agents/skills/plugin-skills.ts | 3 + 21 files changed, 362 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index ac9d57de4ec..efb6b5e820f 100644 --- a/.gitignore +++ b/.gitignore @@ -220,3 +220,4 @@ extensions/**/.openclaw-runtime-deps-stamp.json # Output dir for scripts/run-opengrep.sh (local opengrep scans) /.opengrep-out/ /.crabbox-artifacts +.comux* diff --git a/CHANGELOG.md b/CHANGELOG.md index c486b9b75dd..a62b4ac412d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,12 @@ Docs: https://docs.openclaw.ai ### Fixes +- Agents/tools: fail `exec host=node` before `system.run` when the selected node is known to be disconnected, with an actionable reconnect message instead of a raw node invoke failure. Thanks @BunsDev. +- Agents/models: accept legacy `anthropic-cli/*` model refs as Claude CLI runtime refs instead of failing model resolution with `Unknown model`. Thanks @BunsDev. +- Agents/tools: keep restrictive-profile tool-section warnings scoped to the configured sections whose tools are still missing from `alsoAllow`, so already re-allowed filesystem tools do not make exec-only fixes look broader than they are. Thanks @BunsDev. +- Agents/tools: avoid warning messaging-only agents about inherited global `tools.exec` or `tools.fs` sections when the agent profile did not configure those tool sections itself. Thanks @BunsDev. +- Codex dynamic tools: normalize runtime `toolsAllow` entries the same way as Pi tool policy, so aliases like `bash` and `apply-patch` still expose the intended OpenClaw tools. Thanks @BunsDev. +- Memory/dreaming: read OpenAI-style `output_text` assistant parts from narrative subagent transcripts, so light-phase Dream Diary entries are not dropped as empty. Thanks @BunsDev. - llm-task: resolve configured model aliases before embedded dispatch so `model="gemini-flash"` and other aliases route to the intended provider instead of the agent default. Fixes #54166. - Media generation: resolve slash-containing model-only overrides like `fal-ai/flux/dev` through registered provider model metadata so FAL image/video models do not get misparsed as provider `fal-ai`. Fixes #77444. - CLI backends: keep versioned OAuth identity matches reusable when auth profile ids rotate, so Claude CLI sessions do not reset and lose continuity during same-account OAuth refresh/profile alias changes. Fixes #78541. diff --git a/extensions/codex/src/app-server/auth-bridge.test.ts b/extensions/codex/src/app-server/auth-bridge.test.ts index 8093dcfc05d..160557c51e0 100644 --- a/extensions/codex/src/app-server/auth-bridge.test.ts +++ b/extensions/codex/src/app-server/auth-bridge.test.ts @@ -69,6 +69,9 @@ vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => { : ""); return apiKey ? { apiKey, provider: credential.provider, email: credential.email } : null; } + if (credential.type !== "oauth") { + return null; + } let oauthCredential = credential; if ((oauthCredential.expires ?? 0) <= Date.now()) { const refreshed = await providerRuntimeMocks.refreshProviderOAuthCredentialWithPlugin({ @@ -545,6 +548,35 @@ describe("bridgeCodexAppServerStartOptions", () => { } }); + it("rejects unsupported Codex auth profile credential types before OAuth refresh", async () => { + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); + const request = vi.fn(async () => ({ type: "chatgptAuthTokens" })); + try { + upsertAuthProfile({ + agentDir, + profileId: "openai-codex:aws", + credential: { + type: "aws-sdk", + provider: "openai-codex", + }, + }); + + await expect( + applyCodexAppServerAuthProfile({ + client: { request } as never, + agentDir, + authProfileId: "openai-codex:aws", + }), + ).rejects.toThrow( + 'Codex app-server auth profile "openai-codex:aws" does not contain usable credentials.', + ); + expect(oauthMocks.refreshOpenAICodexToken).not.toHaveBeenCalled(); + expect(request).not.toHaveBeenCalled(); + } finally { + await fs.rm(agentDir, { recursive: true, force: true }); + } + }); + it("falls back to CODEX_API_KEY when no auth profile and no Codex account is available", async () => { const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); const request = vi.fn(async (method: string) => { diff --git a/extensions/codex/src/app-server/auth-bridge.ts b/extensions/codex/src/app-server/auth-bridge.ts index 641e7ff04e0..fba62d2d39c 100644 --- a/extensions/codex/src/app-server/auth-bridge.ts +++ b/extensions/codex/src/app-server/auth-bridge.ts @@ -272,6 +272,9 @@ async function resolveLoginParamsForCredential( ? buildChatgptAuthTokensParams(profileId, credential, accessToken) : undefined; } + if (credential.type !== "oauth") { + return undefined; + } const resolvedCredential = await resolveOAuthCredentialForCodexAppServer(profileId, credential, { agentDir: params.agentDir, forceRefresh: params.forceOAuthRefresh, diff --git a/extensions/codex/src/app-server/dynamic-tool-profile.ts b/extensions/codex/src/app-server/dynamic-tool-profile.ts index f6b28a7e8f3..e6dc0797759 100644 --- a/extensions/codex/src/app-server/dynamic-tool-profile.ts +++ b/extensions/codex/src/app-server/dynamic-tool-profile.ts @@ -10,6 +10,16 @@ export const CODEX_NATIVE_FIRST_DYNAMIC_TOOL_EXCLUDES = [ "update_plan", ] as const; +const DYNAMIC_TOOL_NAME_ALIASES: Record = { + bash: "exec", + "apply-patch": "apply_patch", +}; + +export function normalizeCodexDynamicToolName(name: string): string { + const normalized = name.trim().toLowerCase(); + return DYNAMIC_TOOL_NAME_ALIASES[normalized] ?? normalized; +} + export function applyCodexDynamicToolProfile( tools: T[], config: Pick, @@ -22,10 +32,12 @@ export function applyCodexDynamicToolProfile( } } for (const name of config.codexDynamicToolsExclude ?? []) { - const trimmed = name.trim(); + const trimmed = normalizeCodexDynamicToolName(name); if (trimmed) { excludes.add(trimmed); } } - return excludes.size === 0 ? tools : tools.filter((tool) => !excludes.has(tool.name)); + return excludes.size === 0 + ? tools + : tools.filter((tool) => !excludes.has(normalizeCodexDynamicToolName(tool.name))); } diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index e05f95520fc..1d7a2f0917d 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -504,6 +504,16 @@ describe("runCodexAppServerAttempt", () => { ); }); + it("normalizes Codex dynamic toolsAllow entries before filtering", () => { + const tools = ["exec", "apply_patch", "read", "message"].map((name) => ({ name })); + + expect( + __testing + .filterCodexDynamicToolsForAllowlist(tools, [" BASH ", "apply-patch", "READ"]) + .map((tool) => tool.name), + ).toEqual(["exec", "apply_patch", "read"]); + }); + it("forces the message dynamic tool for message-tool-only source replies", () => { const workspaceDir = path.join(tempDir, "workspace"); const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir); diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index b72a7af686b..298b1702c03 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -64,7 +64,10 @@ import { type CodexPluginConfig, } from "./config.js"; import { projectContextEngineAssemblyForCodex } from "./context-engine-projection.js"; -import { applyCodexDynamicToolProfile } from "./dynamic-tool-profile.js"; +import { + applyCodexDynamicToolProfile, + normalizeCodexDynamicToolName, +} from "./dynamic-tool-profile.js"; import { createCodexDynamicToolBridge, type CodexDynamicToolBridge } from "./dynamic-tools.js"; import { handleCodexAppServerElicitationRequest } from "./elicitation-bridge.js"; import { CodexAppServerEventProjector } from "./event-projector.js"; @@ -1678,10 +1681,7 @@ async function buildDynamicTools(input: DynamicToolBuildParams) { modelHasVision, hasInboundImages: (params.images?.length ?? 0) > 0, }); - const filteredTools = - params.toolsAllow && params.toolsAllow.length > 0 - ? visionFilteredTools.filter((tool) => params.toolsAllow?.includes(tool.name)) - : visionFilteredTools; + const filteredTools = filterCodexDynamicToolsForAllowlist(visionFilteredTools, params.toolsAllow); return normalizeAgentRuntimeTools({ runtimePlan: params.runtimePlan, tools: filteredTools, @@ -1695,6 +1695,19 @@ async function buildDynamicTools(input: DynamicToolBuildParams) { }); } +function filterCodexDynamicToolsForAllowlist( + tools: T[], + toolsAllow?: string[], +): T[] { + if (!toolsAllow || toolsAllow.length === 0) { + return tools; + } + const allowSet = new Set( + toolsAllow.map((name) => normalizeCodexDynamicToolName(name)).filter(Boolean), + ); + return tools.filter((tool) => allowSet.has(normalizeCodexDynamicToolName(tool.name))); +} + function shouldForceMessageTool(params: EmbeddedRunAttemptParams): boolean { return params.sourceReplyDeliveryMode === "message_tool_only"; } @@ -2117,6 +2130,7 @@ export const __testing = { buildCodexNativeHookRelayId, applyCodexDynamicToolProfile, buildDynamicTools, + filterCodexDynamicToolsForAllowlist, filterToolsForVisionInputs, handleDynamicToolCallWithTimeout, resolveOpenClawCodingToolsSessionKeys, diff --git a/extensions/memory-core/src/dreaming-narrative.test.ts b/extensions/memory-core/src/dreaming-narrative.test.ts index 1ce74ca1651..bf662fe53b8 100644 --- a/extensions/memory-core/src/dreaming-narrative.test.ts +++ b/extensions/memory-core/src/dreaming-narrative.test.ts @@ -101,6 +101,16 @@ describe("extractNarrativeText", () => { expect(extractNarrativeText(messages)).toBe("First paragraph.\nSecond paragraph."); }); + it("extracts from OpenAI output_text assistant parts", () => { + const messages = [ + { + role: "assistant", + content: [{ type: "output_text", text: "The light phase found a diary thread." }], + }, + ]; + expect(extractNarrativeText(messages)).toBe("The light phase found a diary thread."); + }); + it("returns null when no assistant message exists", () => { const messages = [{ role: "user", content: "hello" }]; expect(extractNarrativeText(messages)).toBeNull(); diff --git a/extensions/memory-core/src/dreaming-narrative.ts b/extensions/memory-core/src/dreaming-narrative.ts index 4a1bf197c1c..909552433e7 100644 --- a/extensions/memory-core/src/dreaming-narrative.ts +++ b/extensions/memory-core/src/dreaming-narrative.ts @@ -304,7 +304,8 @@ export function extractNarrativeText(messages: unknown[]): string | null { part && typeof part === "object" && !Array.isArray(part) && - (part as Record).type === "text" && + ((part as Record).type === "text" || + (part as Record).type === "output_text") && typeof (part as Record).text === "string", ) .map((part) => (part as { text: string }).text) diff --git a/extensions/microsoft-foundry/index.test.ts b/extensions/microsoft-foundry/index.test.ts index fb96bfc8b02..86961f3108a 100644 --- a/extensions/microsoft-foundry/index.test.ts +++ b/extensions/microsoft-foundry/index.test.ts @@ -698,6 +698,40 @@ describe("microsoft-foundry plugin", () => { ]); }); + it("keeps Foundry profile selection compatible with unrelated AWS SDK profile modes", async () => { + const provider = registerProvider(); + const config: OpenClawConfig = { + ...buildFoundryConfig({ + profileIds: ["microsoft-foundry:entra"], + orderedProfileIds: ["microsoft-foundry:entra"], + }), + auth: { + profiles: { + "amazon-bedrock:default": { + provider: "amazon-bedrock", + mode: "aws-sdk", + }, + "microsoft-foundry:entra": { + provider: "microsoft-foundry", + mode: "api_key", + }, + }, + order: { + "microsoft-foundry": ["microsoft-foundry:entra"], + }, + }, + }; + + await provider.onModelSelected?.({ + config, + model: "microsoft-foundry/gpt-5.4", + prompter: {} as never, + agentDir: defaultFoundryAgentDir, + }); + + expect(config.auth?.order?.["microsoft-foundry"]).toEqual(["microsoft-foundry:entra"]); + }); + it("persists discovered deployments alongside the selected default model", () => { const result = buildFoundryAuthResult({ profileId: "microsoft-foundry:entra", diff --git a/extensions/microsoft-foundry/shared.ts b/extensions/microsoft-foundry/shared.ts index 58868bd580f..ff9c0d2f749 100644 --- a/extensions/microsoft-foundry/shared.ts +++ b/extensions/microsoft-foundry/shared.ts @@ -100,7 +100,7 @@ type FoundryModelCompat = { type FoundryAuthProfileConfig = { provider: string; - mode: "api_key" | "oauth" | "token"; + mode: "api_key" | "aws-sdk" | "oauth" | "token"; email?: string; }; diff --git a/src/agents/bash-tools.exec-host-node-phases.ts b/src/agents/bash-tools.exec-host-node-phases.ts index cabbcb26a2a..7b7654a0928 100644 --- a/src/agents/bash-tools.exec-host-node-phases.ts +++ b/src/agents/bash-tools.exec-host-node-phases.ts @@ -119,6 +119,11 @@ export async function resolveNodeExecutionTarget( throw err; } const nodeInfo = nodes.find((entry) => entry.nodeId === nodeId); + if (nodeInfo?.connected === false) { + throw new Error( + `exec host=node requires a connected node (${nodeId} is currently disconnected). Start or reconnect the companion app or node host, or select a connected node.`, + ); + } const declaredCommands = Array.isArray(nodeInfo?.commands) ? nodeInfo.commands : []; const supportsSystemRun = declaredCommands.includes("system.run"); if (!supportsSystemRun) { diff --git a/src/agents/bash-tools.exec-host-node.test.ts b/src/agents/bash-tools.exec-host-node.test.ts index 08be30eb17e..b55fd997010 100644 --- a/src/agents/bash-tools.exec-host-node.test.ts +++ b/src/agents/bash-tools.exec-host-node.test.ts @@ -401,6 +401,36 @@ describe("executeNodeHostCommand", () => { ); }); + it("rejects disconnected node targets before invoking system.run", async () => { + listNodesMock.mockResolvedValueOnce([ + { + nodeId: "node-1", + commands: ["system.run", "system.run.prepare"], + connected: false, + platform: process.platform, + }, + ]); + + await expect( + executeNodeHostCommand({ + command: "git log --oneline -5", + workdir: "/tmp/work", + env: {}, + security: "allowlist", + ask: "off", + requestedNode: "node-1", + defaultTimeoutSec: 30, + approvalRunningNoticeMs: 0, + warnings: [], + agentId: "requested-agent", + sessionKey: "requested-session", + }), + ).rejects.toThrow( + "exec host=node requires a connected node (node-1 is currently disconnected)", + ); + expect(callGatewayToolMock).not.toHaveBeenCalled(); + }); + it("returns a non-empty placeholder for silent node exec results", async () => { callGatewayToolMock.mockImplementationOnce( async (method: string, _options: unknown, params: MockNodeInvokeParams | undefined) => { diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index 4b2f049fb65..a588a1e8c43 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -362,6 +362,21 @@ const INSUFFICIENT_QUOTA_PAYLOAD = '{"type":"error","error":{"type":"insufficient_quota","message":"Your account has insufficient quota balance to run this request."}}'; describe("runWithModelFallback", () => { + it("normalizes anthropic-cli refs to the Claude CLI provider before execution", async () => { + const run = vi.fn().mockResolvedValue("ok"); + + const result = await runWithModelFallback({ + cfg: {} as OpenClawConfig, + provider: "anthropic-cli", + model: "claude-opus-4-7", + run, + }); + + expect(run).toHaveBeenCalledWith("claude-cli", "claude-opus-4-7"); + expect(result.provider).toBe("claude-cli"); + expect(result.model).toBe("claude-opus-4-7"); + }); + it("skips auth store bootstrap when no auth profile sources exist", async () => { authSourceCheckMock.hasAnyAuthProfileStoreSource.mockReturnValue(false); const run = vi.fn().mockResolvedValueOnce("ok"); diff --git a/src/agents/model-selection-cli.test.ts b/src/agents/model-selection-cli.test.ts index 56d67df1786..37444d0787d 100644 --- a/src/agents/model-selection-cli.test.ts +++ b/src/agents/model-selection-cli.test.ts @@ -25,6 +25,10 @@ describe("isCliProvider", () => { expect(isCliProvider("claude-cli", {} as OpenClawConfig)).toBe(true); }); + it("accepts the anthropic-cli auth-choice id as a Claude CLI provider alias", () => { + expect(isCliProvider("anthropic-cli", {} as OpenClawConfig)).toBe(true); + }); + it("returns false for provider ids", () => { expect(isCliProvider("example-cli", {} as OpenClawConfig)).toBe(false); }); diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index 7a8ac14c91b..cc34850de9d 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.js"; import { resetLogger, setLoggerOverride } from "../logging/logger.js"; import { createWarnLogCapture } from "../logging/test-helpers/warn-log-capture.js"; +import { migrateLegacyRuntimeModelRef } from "./model-runtime-aliases.js"; import { buildAllowedModelSet, inferUniqueProviderFromConfiguredModels, @@ -220,6 +221,7 @@ describe("model-selection", () => { expect(normalizeProviderId("qwen")).toBe("qwen"); expect(normalizeProviderId("kimi-code")).toBe("kimi"); expect(normalizeProviderId("kimi-coding")).toBe("kimi"); + expect(normalizeProviderId("anthropic-cli")).toBe("claude-cli"); expect(normalizeProviderId("bedrock")).toBe("amazon-bedrock"); expect(normalizeProviderId("aws-bedrock")).toBe("amazon-bedrock"); expect(normalizeProviderId("amazon-bedrock")).toBe("amazon-bedrock"); @@ -390,6 +392,12 @@ describe("model-selection", () => { defaultProvider: "google-vertex", expected: { provider: "google-vertex", model: "gemini-3.1-flash-lite-preview" }, }, + { + name: "normalizes anthropic-cli refs to the Claude CLI provider alias", + variants: ["anthropic-cli/claude-opus-4-7"], + defaultProvider: "openai", + expected: { provider: "claude-cli", model: "claude-opus-4-7" }, + }, ]; it("parses and normalizes provider/model refs", () => { @@ -398,6 +406,17 @@ describe("model-selection", () => { } }); + it("migrates anthropic-cli legacy runtime refs to canonical Anthropic refs", () => { + expect(migrateLegacyRuntimeModelRef("anthropic-cli/claude-opus-4-7")).toEqual({ + ref: "anthropic/claude-opus-4-7", + legacyProvider: "claude-cli", + provider: "anthropic", + model: "claude-opus-4-7", + runtime: "claude-cli", + cli: true, + }); + }); + it("round-trips normalized refs through modelKey", () => { const parsed = parseModelRef(" opus-4.6 ", "anthropic", { allowPluginNormalization: false, diff --git a/src/agents/pi-tools.policy.test.ts b/src/agents/pi-tools.policy.test.ts index 8658b3004e3..bc995ad183e 100644 --- a/src/agents/pi-tools.policy.test.ts +++ b/src/agents/pi-tools.policy.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; +import { createWarnLogCapture } from "../logging/test-helpers/warn-log-capture.js"; import { filterToolsByPolicy, isToolAllowedByPolicyName, @@ -590,6 +591,97 @@ describe("resolveEffectiveToolPolicy", () => { expect(result.profileAlsoAllow).not.toContain("process"); }); + it("does not warn an agent profile about inherited global tool sections (#47487)", async () => { + const warnLogs = createWarnLogCapture("openclaw-pi-tools-policy-test"); + try { + const cfg = { + tools: { + exec: { security: "allowlist" }, + fs: { workspaceOnly: true }, + }, + agents: { + list: [ + { + id: "sage", + tools: { + profile: "messaging", + alsoAllow: ["image"], + }, + }, + ], + }, + } as OpenClawConfig; + + resolveEffectiveToolPolicy({ config: cfg, agentId: "sage" }); + + expect(await warnLogs.findText('tools policy: profile "messaging"')).toBeUndefined(); + } finally { + warnLogs.cleanup(); + } + }); + + it("still warns when an agent profile has its own configured exec section (#47487)", async () => { + const warnLogs = createWarnLogCapture("openclaw-pi-tools-policy-test"); + try { + const cfg = { + agents: { + list: [ + { + id: "sage", + tools: { + profile: "messaging", + exec: { security: "allowlist" }, + }, + }, + ], + }, + } as OpenClawConfig; + + resolveEffectiveToolPolicy({ config: cfg, agentId: "sage" }); + + const warning = await warnLogs.findText('tools policy: profile "messaging"'); + expect(warning).toContain('(agent "sage")'); + expect(warning).toContain("configured tool sections (tools.exec)"); + expect(warning).toContain('Add alsoAllow: ["exec", "process"]'); + } finally { + warnLogs.cleanup(); + } + }); + + it("only lists configured sections whose grants are still missing (#47487)", async () => { + const warnLogs = createWarnLogCapture("openclaw-pi-tools-policy-test"); + try { + const cfg = { + agents: { + list: [ + { + id: "echo", + tools: { + profile: "messaging", + alsoAllow: ["read", "write", "edit"], + exec: { security: "allowlist" }, + fs: { workspaceOnly: true }, + }, + }, + ], + }, + } as OpenClawConfig; + + resolveEffectiveToolPolicy({ config: cfg, agentId: "echo" }); + + const warning = await warnLogs.findText('tools policy: profile "messaging"'); + expect(warning).toContain('(agent "echo")'); + expect(warning).toContain("configured tool sections (tools.exec)"); + expect(warning).not.toContain("tools.exec / tools.fs"); + expect(warning).toContain('Add alsoAllow: ["exec", "process"]'); + expect(warning).not.toContain('"read"'); + expect(warning).not.toContain('"write"'); + expect(warning).not.toContain('"edit"'); + } finally { + warnLogs.cleanup(); + } + }); + it("explicit alsoAllow with exec still grants exec under messaging profile", () => { const cfg = { tools: { diff --git a/src/agents/pi-tools.policy.ts b/src/agents/pi-tools.policy.ts index 63a1a9d11a6..d2039c56353 100644 --- a/src/agents/pi-tools.policy.ts +++ b/src/agents/pi-tools.policy.ts @@ -372,27 +372,40 @@ function hasExplicitToolSection(section: unknown): boolean { /** Detect tool config sections that previously widened profiles implicitly. * Used only for migration warnings — not merged into profileAlsoAllow. #47487 */ +type ImplicitProfileGrantDetection = { + entries: Array<{ section: string; grants: string[] }>; +}; + function detectImplicitProfileGrants(params: { globalTools?: OpenClawConfig["tools"]; agentTools?: AgentToolsConfig; -}): string[] | undefined { - const implicit = new Set(); + includeGlobalSections: boolean; +}): ImplicitProfileGrantDetection | undefined { + const entries: ImplicitProfileGrantDetection["entries"] = []; if ( hasExplicitToolSection(params.agentTools?.exec) || - hasExplicitToolSection(params.globalTools?.exec) + (params.includeGlobalSections && hasExplicitToolSection(params.globalTools?.exec)) ) { - implicit.add("exec"); - implicit.add("process"); + entries.push({ section: "tools.exec", grants: ["exec", "process"] }); } if ( hasExplicitToolSection(params.agentTools?.fs) || - hasExplicitToolSection(params.globalTools?.fs) + (params.includeGlobalSections && hasExplicitToolSection(params.globalTools?.fs)) ) { - implicit.add("read"); - implicit.add("write"); - implicit.add("edit"); + entries.push({ section: "tools.fs", grants: ["read", "write", "edit"] }); } - return implicit.size > 0 ? Array.from(implicit) : undefined; + if (entries.length === 0) { + return undefined; + } + return { entries }; +} + +function formatImplicitToolSections(sections: string[]): string { + return sections.join(" / "); +} + +function formatToolListForWarning(toolNames: string[]): string { + return toolNames.map((toolName) => `"${toolName}"`).join(", "); } export function resolveEffectiveToolPolicy(params: { @@ -415,6 +428,7 @@ export function resolveEffectiveToolPolicy(params: { const globalTools = params.config?.tools; const profile = agentTools?.profile ?? globalTools?.profile; + const profileSource = agentTools?.profile ? "agent" : globalTools?.profile ? "global" : undefined; const providerPolicy = resolveProviderToolPolicy({ byProvider: globalTools?.byProvider, modelProvider: params.modelProvider, @@ -431,20 +445,30 @@ export function resolveEffectiveToolPolicy(params: { // Warn affected users about removed implicit grants (#47487), but only when // the active profile/explicit alsoAllow do not already grant those tools. if (profile) { - const implicitGrants = detectImplicitProfileGrants({ globalTools, agentTools }); + const implicitGrants = detectImplicitProfileGrants({ + globalTools, + agentTools, + includeGlobalSections: profileSource === "global", + }); if (implicitGrants) { const profilePolicy = mergeAlsoAllowPolicy( resolveToolProfilePolicy(profile), explicitProfileAlsoAllow, ); - const uncovered = implicitGrants.filter( - (toolName) => !isToolAllowedByPolicyName(toolName, profilePolicy), - ); + const uncoveredEntries = implicitGrants.entries + .map((entry) => ({ + section: entry.section, + grants: entry.grants.filter( + (toolName) => !isToolAllowedByPolicyName(toolName, profilePolicy), + ), + })) + .filter((entry) => entry.grants.length > 0); + const uncovered = uncoveredEntries.flatMap((entry) => entry.grants); if (uncovered.length > 0) { logWarn( `tools policy: profile "${profile}"${agentId ? ` (agent "${agentId}")` : ""} has ` + - `configured tool sections (tools.exec / tools.fs) that no longer implicitly widen ` + - `the profile. Add alsoAllow: [${uncovered.map((t) => `"${t}"`).join(", ")}] ` + + `configured tool sections (${formatImplicitToolSections(uncoveredEntries.map((entry) => entry.section))}) that no longer implicitly widen ` + + `the profile. Add alsoAllow: [${formatToolListForWarning(uncovered)}] ` + `explicitly if these tools should be available. See #47487.`, ); } diff --git a/src/agents/provider-id.ts b/src/agents/provider-id.ts index 6b8d0a2939b..dd13825aeed 100644 --- a/src/agents/provider-id.ts +++ b/src/agents/provider-id.ts @@ -14,6 +14,9 @@ export function normalizeProviderId(provider: string): string { if (normalized === "opencode-go-auth") { return "opencode-go"; } + if (normalized === "anthropic-cli") { + return "claude-cli"; + } if (normalized === "kimi" || normalized === "kimi-code" || normalized === "kimi-coding") { return "kimi"; } diff --git a/src/agents/skills/plugin-skills.test.ts b/src/agents/skills/plugin-skills.test.ts index b9269dbddd1..c921d2b60c7 100644 --- a/src/agents/skills/plugin-skills.test.ts +++ b/src/agents/skills/plugin-skills.test.ts @@ -306,6 +306,25 @@ describe("resolvePluginSkillDirs", () => { }); }); + it("cleans up generated plugin skill links when no workspace is active", async () => { + const pluginSkillsDir = await tempDirs.make("managed-plugin-skills-"); + const staleRoot = await tempDirs.make("stale-plugin-skills-"); + const staleSkill = path.join(staleRoot, "stale-skill"); + await fs.mkdir(staleSkill, { recursive: true }); + fsSync.symlinkSync(staleSkill, path.join(pluginSkillsDir, "stale-skill"), "dir"); + + const dirs = resolvePluginSkillDirs({ + workspaceDir: undefined, + config: {} as OpenClawConfig, + pluginSkillsDir, + }); + + expect(dirs).toEqual([]); + await expect(fs.lstat(path.join(pluginSkillsDir, "stale-skill"))).rejects.toMatchObject({ + code: "ENOENT", + }); + }); + it("resolves Claude bundle command roots through the normal plugin skill path", async () => { const workspaceDir = await tempDirs.make("openclaw-"); const pluginRoot = await tempDirs.make("openclaw-claude-bundle-"); diff --git a/src/agents/skills/plugin-skills.ts b/src/agents/skills/plugin-skills.ts index 757c926df5d..0a172de8c40 100644 --- a/src/agents/skills/plugin-skills.ts +++ b/src/agents/skills/plugin-skills.ts @@ -27,6 +27,9 @@ export function resolvePluginSkillDirs(params: { }): string[] { const workspaceDir = (params.workspaceDir ?? "").trim(); if (!workspaceDir) { + publishPluginSkills([], { + pluginSkillsDir: params.pluginSkillsDir, + }); return []; } const config = params.config ?? {}; From 9ffe290a170b4ae5aefefa01d53f07aaa47aca24 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 7 May 2026 02:39:01 -0500 Subject: [PATCH 061/215] fix(chat): decode native thinking metadata Decode gateway-provided thinking metadata for native iOS/macOS chat picker options, preserving extended and legacy thinking levels without leaking default-model options across sessions.\n\nVerification:\n- swift test --package-path apps/shared/OpenClawKit --filter ChatViewModelTests --no-parallel\n- swift test --package-path apps/macos --filter WebChatSwiftUISmokeTests --no-parallel\n- pnpm lint:swift\n- pnpm check:changed\n\nFollow-up maintainer fix for #40878 review feedback. --- CHANGELOG.md | 1 + .../Sources/OpenClaw/WebChatSwiftUI.swift | 4 + .../Sources/OpenClawChatUI/ChatComposer.swift | 10 +- .../Sources/OpenClawChatUI/ChatSessions.swift | 79 ++++- .../OpenClawChatUI/ChatViewModel.swift | 174 ++++++++++- .../OpenClawKitTests/ChatViewModelTests.swift | 270 ++++++++++++++++++ 6 files changed, 525 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a62b4ac412d..929772b6f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Native chat: decode gateway-provided thinking metadata for the iOS/macOS picker so provider-specific levels such as `adaptive`, `xhigh`, and `max` appear without leaking unsupported default-model options. Thanks @BunsDev. - Agents/tools: fail `exec host=node` before `system.run` when the selected node is known to be disconnected, with an actionable reconnect message instead of a raw node invoke failure. Thanks @BunsDev. - Agents/models: accept legacy `anthropic-cli/*` model refs as Claude CLI runtime refs instead of failing model resolution with `Unknown model`. Thanks @BunsDev. - Agents/tools: keep restrictive-profile tool-section warnings scoped to the configured sections whose tools are still missing from `alsoAllow`, so already re-allowed filesystem tools do not make exec-only fixes look broader than they are. Thanks @BunsDev. diff --git a/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift b/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift index bf8ebbc7ed4..a47d5a1393f 100644 --- a/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift +++ b/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift @@ -63,8 +63,12 @@ struct MacGatewayChatTransport: OpenClawChatTransport { let mainSessionKey = await GatewayConnection.shared.cachedMainSessionKey() let defaults = decoded.defaults.map { OpenClawChatSessionsDefaults( + modelProvider: $0.modelProvider, model: $0.model, contextTokens: $0.contextTokens, + thinkingLevels: $0.thinkingLevels, + thinkingOptions: $0.thinkingOptions, + thinkingDefault: $0.thinkingDefault, mainSessionKey: mainSessionKey) } ?? OpenClawChatSessionsDefaults( model: nil, diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift index a85f922defe..28835f02d0e 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift @@ -9,8 +9,6 @@ import UniformTypeIdentifiers @MainActor struct OpenClawChatComposer: View { - private static let menuThinkingLevels = ["off", "low", "medium", "high"] - @Bindable var viewModel: OpenClawChatViewModel let style: OpenClawChatView.Style let showsSessionSwitcher: Bool @@ -95,12 +93,8 @@ struct OpenClawChatComposer: View { get: { self.viewModel.thinkingLevel }, set: { next in self.viewModel.selectThinkingLevel(next) })) { - Text("Off").tag("off") - Text("Low").tag("low") - Text("Medium").tag("medium") - Text("High").tag("high") - if !Self.menuThinkingLevels.contains(self.viewModel.thinkingLevel) { - Text(self.viewModel.thinkingLevel.capitalized).tag(self.viewModel.thinkingLevel) + ForEach(self.viewModel.thinkingLevelOptions) { option in + Text(option.label).tag(option.id) } } .labelsHidden() diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift index 381829f428f..6733a55c757 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift @@ -1,5 +1,15 @@ import Foundation +public struct OpenClawChatThinkingLevelOption: Codable, Identifiable, Sendable, Hashable { + public let id: String + public let label: String + + public init(id: String, label: String) { + self.id = id + self.label = label + } +} + public struct OpenClawChatModelChoice: Identifiable, Codable, Sendable, Hashable { public var id: String { self.selectionID @@ -34,13 +44,29 @@ public struct OpenClawChatModelChoice: Identifiable, Codable, Sendable, Hashable } public struct OpenClawChatSessionsDefaults: Codable, Sendable { + public let modelProvider: String? public let model: String? public let contextTokens: Int? + public let thinkingLevels: [OpenClawChatThinkingLevelOption]? + public let thinkingOptions: [String]? + public let thinkingDefault: String? public let mainSessionKey: String? - public init(model: String?, contextTokens: Int?, mainSessionKey: String? = nil) { + public init( + modelProvider: String? = nil, + model: String?, + contextTokens: Int?, + thinkingLevels: [OpenClawChatThinkingLevelOption]? = nil, + thinkingOptions: [String]? = nil, + thinkingDefault: String? = nil, + mainSessionKey: String? = nil) + { + self.modelProvider = modelProvider self.model = model self.contextTokens = contextTokens + self.thinkingLevels = thinkingLevels + self.thinkingOptions = thinkingOptions + self.thinkingDefault = thinkingDefault self.mainSessionKey = mainSessionKey } } @@ -72,6 +98,57 @@ public struct OpenClawChatSessionEntry: Codable, Identifiable, Sendable, Hashabl public let modelProvider: String? public let model: String? public let contextTokens: Int? + public let thinkingLevels: [OpenClawChatThinkingLevelOption]? + public let thinkingOptions: [String]? + public let thinkingDefault: String? + + public init( + key: String, + kind: String?, + displayName: String?, + surface: String?, + subject: String?, + room: String?, + space: String?, + updatedAt: Double?, + sessionId: String?, + systemSent: Bool?, + abortedLastRun: Bool?, + thinkingLevel: String?, + verboseLevel: String?, + inputTokens: Int?, + outputTokens: Int?, + totalTokens: Int?, + modelProvider: String?, + model: String?, + contextTokens: Int?, + thinkingLevels: [OpenClawChatThinkingLevelOption]? = nil, + thinkingOptions: [String]? = nil, + thinkingDefault: String? = nil) + { + self.key = key + self.kind = kind + self.displayName = displayName + self.surface = surface + self.subject = subject + self.room = room + self.space = space + self.updatedAt = updatedAt + self.sessionId = sessionId + self.systemSent = systemSent + self.abortedLastRun = abortedLastRun + self.thinkingLevel = thinkingLevel + self.verboseLevel = verboseLevel + self.inputTokens = inputTokens + self.outputTokens = outputTokens + self.totalTokens = totalTokens + self.modelProvider = modelProvider + self.model = model + self.contextTokens = contextTokens + self.thinkingLevels = thinkingLevels + self.thinkingOptions = thinkingOptions + self.thinkingDefault = thinkingDefault + } } public struct OpenClawChatSessionsListResponse: Codable, Sendable { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift index e647435008f..d5601c86415 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift @@ -21,6 +21,7 @@ public final class OpenClawChatViewModel { public private(set) var messages: [OpenClawChatMessage] = [] public var input: String = "" public private(set) var thinkingLevel: String + public private(set) var thinkingLevelOptions: [OpenClawChatThinkingLevelOption] public private(set) var modelSelectionID: String = "__default__" public private(set) var modelChoices: [OpenClawChatModelChoice] = [] public private(set) var isLoading = false @@ -83,7 +84,11 @@ public final class OpenClawChatViewModel { self.sessionKey = sessionKey self.transport = transport let normalizedThinkingLevel = Self.normalizedThinkingLevel(initialThinkingLevel) - self.thinkingLevel = normalizedThinkingLevel ?? "off" + let initialResolvedThinkingLevel = normalizedThinkingLevel ?? "off" + self.thinkingLevel = initialResolvedThinkingLevel + self.thinkingLevelOptions = Self.withCurrentThinkingOption( + Self.baseThinkingLevelOptions, + current: initialResolvedThinkingLevel) self.prefersExplicitThinkingLevel = normalizedThinkingLevel != nil self.onThinkingLevelChanged = onThinkingLevelChanged @@ -198,6 +203,14 @@ public final class OpenClawChatViewModel { return "Default: \(self.modelLabel(for: defaultModelID))" } + private static let baseThinkingLevelOptions: [OpenClawChatThinkingLevelOption] = [ + OpenClawChatThinkingLevelOption(id: "off", label: "off"), + OpenClawChatThinkingLevelOption(id: "minimal", label: "minimal"), + OpenClawChatThinkingLevelOption(id: "low", label: "low"), + OpenClawChatThinkingLevelOption(id: "medium", label: "medium"), + OpenClawChatThinkingLevelOption(id: "high", label: "high"), + ] + public func addAttachments(urls: [URL]) { Task { await self.loadAttachments(urls: urls) } } @@ -243,6 +256,7 @@ public final class OpenClawChatViewModel { { self.thinkingLevel = level } + self.syncThinkingLevelOptions() await self.pollHealthIfNeeded(force: true) await self.fetchSessions(limit: 50) await self.fetchModels() @@ -594,6 +608,7 @@ public final class OpenClawChatViewModel { self.sessions = res.sessions self.sessionDefaults = res.defaults self.syncSelectedModel() + self.syncThinkingLevelOptions() } catch { // Best-effort. } @@ -675,6 +690,8 @@ public final class OpenClawChatViewModel { let sessionKey = self.sessionKey self.thinkingLevel = next + self.syncThinkingLevelOptions() + self.updateCurrentSessionThinkingLevel(next, sessionKey: sessionKey) self.onThinkingLevelChanged?(next) self.nextThinkingSelectionRequestID &+= 1 let requestID = self.nextThinkingSelectionRequestID @@ -770,6 +787,99 @@ public final class OpenClawChatViewModel { } } + private func syncThinkingLevelOptions() { + let currentSession = self.sessions.first(where: { $0.key == self.sessionKey }) + var options = self.resolvedThinkingLevelOptions(for: currentSession) + if let current = Self.normalizedThinkingLevel(self.thinkingLevel) { + options = Self.withCurrentThinkingOption(options, current: current) + } + self.thinkingLevelOptions = options + } + + private func resolvedThinkingLevelOptions( + for currentSession: OpenClawChatSessionEntry?) -> [OpenClawChatThinkingLevelOption] + { + if let levels = Self.normalizedThinkingLevelOptions(currentSession?.thinkingLevels), !levels.isEmpty { + return levels + } + + let defaultsMatch = currentSession.map { + Self.sessionModelMatchesDefaults($0, defaults: self.sessionDefaults) + } ?? true + + if defaultsMatch, + let levels = Self.normalizedThinkingLevelOptions(self.sessionDefaults?.thinkingLevels), + !levels.isEmpty + { + return levels + } + + if let options = Self.thinkingOptions(from: currentSession?.thinkingOptions), !options.isEmpty { + return options + } + + if defaultsMatch, + let options = Self.thinkingOptions(from: self.sessionDefaults?.thinkingOptions), + !options.isEmpty + { + return options + } + + return Self.baseThinkingLevelOptions + } + + private static func sessionModelMatchesDefaults( + _ session: OpenClawChatSessionEntry, + defaults: OpenClawChatSessionsDefaults?) -> Bool + { + let providerMatches = session.modelProvider == nil || session.modelProvider == defaults?.modelProvider + let modelMatches = session.model == nil || session.model == defaults?.model + return providerMatches && modelMatches + } + + private static func normalizedThinkingLevelOptions( + _ levels: [OpenClawChatThinkingLevelOption]?) -> [OpenClawChatThinkingLevelOption]? + { + guard let levels else { return nil } + return Self.dedupedThinkingOptions( + levels.compactMap { level in + guard let id = Self.normalizedThinkingLevel(level.id) else { return nil } + let label = level.label.trimmingCharacters(in: .whitespacesAndNewlines) + return OpenClawChatThinkingLevelOption(id: id, label: label.isEmpty ? id : label) + }) + } + + private static func thinkingOptions(from labels: [String]?) -> [OpenClawChatThinkingLevelOption]? { + guard let labels else { return nil } + return Self.dedupedThinkingOptions( + labels.compactMap { label in + guard let id = Self.normalizedThinkingLevel(label) else { return nil } + let trimmed = label.trimmingCharacters(in: .whitespacesAndNewlines) + return OpenClawChatThinkingLevelOption(id: id, label: trimmed.isEmpty ? id : trimmed) + }) + } + + private static func withCurrentThinkingOption( + _ options: [OpenClawChatThinkingLevelOption], + current: String) -> [OpenClawChatThinkingLevelOption] + { + guard !options.contains(where: { $0.id == current }) else { return options } + return options + [OpenClawChatThinkingLevelOption(id: current, label: current)] + } + + private static func dedupedThinkingOptions( + _ options: [OpenClawChatThinkingLevelOption]) -> [OpenClawChatThinkingLevelOption] + { + var result: [OpenClawChatThinkingLevelOption] = [] + var seen = Set() + for option in options { + guard !option.id.isEmpty, !seen.contains(option.id) else { continue } + seen.insert(option.id) + result.append(option) + } + return result + } + private func placeholderSession(key: String) -> OpenClawChatSessionEntry { OpenClawChatSessionEntry( key: key, @@ -858,6 +968,9 @@ public final class OpenClawChatViewModel { modelProvider: resolved.modelProvider, sessionKey: sessionKey, syncSelection: syncSelection) + if sessionKey == self.sessionKey { + self.syncThinkingLevelOptions() + } } private func resolvedSessionModelIdentity(forSelectionID selectionID: String) @@ -885,6 +998,34 @@ public final class OpenClawChatViewModel { return "\(provider)/\(modelID)" } + private func updateCurrentSessionThinkingLevel(_ thinkingLevel: String?, sessionKey: String) { + guard let index = self.sessions.firstIndex(where: { $0.key == sessionKey }) else { return } + let current = self.sessions[index] + self.sessions[index] = OpenClawChatSessionEntry( + key: current.key, + kind: current.kind, + displayName: current.displayName, + surface: current.surface, + subject: current.subject, + room: current.room, + space: current.space, + updatedAt: current.updatedAt, + sessionId: current.sessionId, + systemSent: current.systemSent, + abortedLastRun: current.abortedLastRun, + thinkingLevel: thinkingLevel, + verboseLevel: current.verboseLevel, + inputTokens: current.inputTokens, + outputTokens: current.outputTokens, + totalTokens: current.totalTokens, + modelProvider: current.modelProvider, + model: current.model, + contextTokens: current.contextTokens, + thinkingLevels: current.thinkingLevels, + thinkingOptions: current.thinkingOptions, + thinkingDefault: current.thinkingDefault) + } + private func updateCurrentSessionModel( modelID: String?, modelProvider: String?, @@ -1084,6 +1225,7 @@ public final class OpenClawChatViewModel { let level = Self.normalizedThinkingLevel(payload.thinkingLevel) { self.thinkingLevel = level + self.syncThinkingLevelOptions() } } catch { chatUILogger.error("refresh history failed \(error.localizedDescription, privacy: .public)") @@ -1195,9 +1337,33 @@ public final class OpenClawChatViewModel { private static func normalizedThinkingLevel(_ level: String?) -> String? { guard let level else { return nil } let trimmed = level.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - guard ["off", "minimal", "low", "medium", "high", "xhigh", "adaptive"].contains(trimmed) else { - return nil + guard !trimmed.isEmpty else { return nil } + let collapsed = trimmed.replacingOccurrences( + of: "[\\s_-]+", + with: "", + options: .regularExpression) + + switch collapsed { + case "adaptive", "auto": + return "adaptive" + case "max": + return "max" + case "xhigh", "extrahigh": + return "xhigh" + case "off", "none": + return "off" + case "on", "enable", "enabled": + return "low" + case "min", "minimal", "think": + return "minimal" + case "low", "thinkhard": + return "low" + case "mid", "med", "medium", "thinkharder", "harder": + return "medium" + case "high", "ultra", "ultrathink", "thinkhardest", "highest": + return "high" + default: + return trimmed } - return trimmed } } diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift index e33c2890c39..278f0a76174 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift @@ -46,6 +46,10 @@ private func sessionEntry(key: String, updatedAt: Double) -> OpenClawChatSession contextTokens: nil) } +private func thinkingOption(_ id: String, label: String? = nil) -> OpenClawChatThinkingLevelOption { + OpenClawChatThinkingLevelOption(id: id, label: label ?? id) +} + private func sessionEntry( key: String, updatedAt: Double, @@ -1632,6 +1636,272 @@ extension TestChatTransportState { } } + @Test func decodesGatewayThinkingMetadataFromSessionList() throws { + let json = """ + { + "defaults": { + "modelProvider": "anthropic", + "model": "claude-opus-4-7", + "thinkingLevels": [ + { "id": "off", "label": "off" }, + { "id": "adaptive", "label": "adaptive" }, + { "id": "max", "label": "maximum" } + ], + "thinkingOptions": ["off", "adaptive", "maximum"], + "thinkingDefault": "adaptive" + }, + "sessions": [ + { + "key": "main", + "modelProvider": "openrouter", + "model": "deepseek/deepseek-v4", + "thinkingLevel": "max", + "thinkingLevels": [ + { "id": "off", "label": "off" }, + { "id": "xhigh", "label": "xhigh" }, + { "id": "max", "label": "max" } + ], + "thinkingOptions": ["off", "xhigh", "max"], + "thinkingDefault": "max" + } + ] + } + """ + + let decoded = try JSONDecoder().decode( + OpenClawChatSessionsListResponse.self, + from: Data(json.utf8)) + + #expect(decoded.defaults?.modelProvider == "anthropic") + #expect(decoded.defaults?.thinkingLevels?.map(\.id) == ["off", "adaptive", "max"]) + #expect(decoded.defaults?.thinkingLevels?.last?.label == "maximum") + #expect(decoded.defaults?.thinkingDefault == "adaptive") + #expect(decoded.sessions.first?.thinkingLevels?.map(\.id) == ["off", "xhigh", "max"]) + #expect(decoded.sessions.first?.thinkingDefault == "max") + } + + @Test func sessionThinkingLevelsDrivePickerOptions() async throws { + let history = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [], + thinkingLevel: "adaptive") + let sessions = OpenClawChatSessionsListResponse( + ts: 1, + path: nil, + count: 1, + defaults: OpenClawChatSessionsDefaults( + modelProvider: "openai-codex", + model: "gpt-5.5", + contextTokens: nil, + thinkingLevels: [ + thinkingOption("off"), + thinkingOption("low"), + thinkingOption("xhigh"), + thinkingOption("max", label: "maximum"), + ], + thinkingOptions: ["off", "low", "xhigh", "maximum"], + thinkingDefault: "xhigh"), + sessions: [ + OpenClawChatSessionEntry( + key: "main", + kind: nil, + displayName: nil, + surface: nil, + subject: nil, + room: nil, + space: nil, + updatedAt: 1, + sessionId: "sess-main", + systemSent: nil, + abortedLastRun: nil, + thinkingLevel: "adaptive", + verboseLevel: nil, + inputTokens: nil, + outputTokens: nil, + totalTokens: nil, + modelProvider: "anthropic", + model: "claude-opus-4-7", + contextTokens: nil, + thinkingLevels: [ + thinkingOption("off"), + thinkingOption("adaptive"), + thinkingOption("max", label: "maximum"), + ], + thinkingOptions: ["off", "adaptive", "maximum"], + thinkingDefault: "adaptive"), + ]) + + let (_, vm) = await makeViewModel( + historyResponses: [history], + sessionsResponses: [sessions]) + + try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main") + + #expect(await MainActor.run { vm.thinkingLevel } == "adaptive") + #expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == ["off", "adaptive", "max"]) + #expect(await MainActor.run { vm.thinkingLevelOptions.map(\.label) } == ["off", "adaptive", "maximum"]) + } + + @Test func thinkingOptionsFallbackAndCurrentUnsupportedLevelStayVisible() async throws { + let history = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [], + thinkingLevel: "xhigh") + let sessions = OpenClawChatSessionsListResponse( + ts: 1, + path: nil, + count: 1, + defaults: nil, + sessions: [ + OpenClawChatSessionEntry( + key: "main", + kind: nil, + displayName: nil, + surface: nil, + subject: nil, + room: nil, + space: nil, + updatedAt: 1, + sessionId: "sess-main", + systemSent: nil, + abortedLastRun: nil, + thinkingLevel: "xhigh", + verboseLevel: nil, + inputTokens: nil, + outputTokens: nil, + totalTokens: nil, + modelProvider: "openrouter", + model: "deepseek/deepseek-v4", + contextTokens: nil, + thinkingLevels: nil, + thinkingOptions: ["off", "max"], + thinkingDefault: "max"), + ]) + + let (_, vm) = await makeViewModel( + historyResponses: [history], + sessionsResponses: [sessions]) + + try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main") + + #expect(await MainActor.run { vm.thinkingLevel } == "xhigh") + #expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == ["off", "max", "xhigh"]) + #expect(await MainActor.run { vm.thinkingLevelOptions.map(\.label) } == ["off", "max", "xhigh"]) + } + + @Test func matchingDefaultThinkingLevelsBeatLegacyRowThinkingOptions() async throws { + let history = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [], + thinkingLevel: "adaptive") + let sessions = OpenClawChatSessionsListResponse( + ts: 1, + path: nil, + count: 1, + defaults: OpenClawChatSessionsDefaults( + modelProvider: "anthropic", + model: "claude-opus-4-7", + contextTokens: nil, + thinkingLevels: [ + thinkingOption("off"), + thinkingOption("adaptive"), + thinkingOption("max"), + ], + thinkingOptions: ["off", "adaptive", "max"], + thinkingDefault: "adaptive"), + sessions: [ + OpenClawChatSessionEntry( + key: "main", + kind: nil, + displayName: nil, + surface: nil, + subject: nil, + room: nil, + space: nil, + updatedAt: 1, + sessionId: "sess-main", + systemSent: nil, + abortedLastRun: nil, + thinkingLevel: "adaptive", + verboseLevel: nil, + inputTokens: nil, + outputTokens: nil, + totalTokens: nil, + modelProvider: "anthropic", + model: "claude-opus-4-7", + contextTokens: nil, + thinkingLevels: nil, + thinkingOptions: ["off"], + thinkingDefault: "off"), + ]) + + let (_, vm) = await makeViewModel( + historyResponses: [history], + sessionsResponses: [sessions]) + + try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main") + + #expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == ["off", "adaptive", "max"]) + } + + @Test func defaultThinkingLevelsDoNotLeakToDifferentSessionModel() async throws { + let history = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [], + thinkingLevel: "max") + let sessions = OpenClawChatSessionsListResponse( + ts: 1, + path: nil, + count: 1, + defaults: OpenClawChatSessionsDefaults( + modelProvider: "anthropic", + model: "claude-opus-4-7", + contextTokens: nil, + thinkingLevels: [ + thinkingOption("off"), + thinkingOption("adaptive"), + thinkingOption("max"), + ], + thinkingOptions: ["off", "adaptive", "max"], + thinkingDefault: "adaptive"), + sessions: [ + OpenClawChatSessionEntry( + key: "main", + kind: nil, + displayName: nil, + surface: nil, + subject: nil, + room: nil, + space: nil, + updatedAt: 1, + sessionId: "sess-main", + systemSent: nil, + abortedLastRun: nil, + thinkingLevel: "max", + verboseLevel: nil, + inputTokens: nil, + outputTokens: nil, + totalTokens: nil, + modelProvider: "openai", + model: "gpt-5.4", + contextTokens: nil), + ]) + + let (_, vm) = await makeViewModel( + historyResponses: [history], + sessionsResponses: [sessions]) + + try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main") + + #expect(await MainActor.run { vm.thinkingLevel } == "max") + #expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == + ["off", "minimal", "low", "medium", "high", "max"]) + } + @Test func staleThinkingPatchCompletionReappliesLatestSelection() async throws { let history = OpenClawChatHistoryPayload( sessionKey: "main", From 1ef85c7d4c30c05b7ef4191d00b50cdb7e4d8721 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 08:43:29 +0100 Subject: [PATCH 062/215] test: make suites safe without isolation (#78834) * test: make suites safe without isolation * fix: narrow auth profile credential types * test: inject channel module loader factory locally --- CHANGELOG.md | 1 + extensions/acpx/src/service.test.ts | 20 +- extensions/active-memory/index.test.ts | 1 + .../amazon-bedrock-mantle/discovery.test.ts | 3 + extensions/amazon-bedrock/discovery.test.ts | 6 +- extensions/amazon-bedrock/index.test.ts | 9 +- .../memory-embedding-adapter.test.ts | 7 +- extensions/anthropic-vertex/index.test.ts | 7 +- .../anthropic-vertex/region.adc.test.ts | 7 +- extensions/anthropic/cli-migration.test.ts | 12 +- extensions/anthropic/index.test.ts | 12 +- .../azure-speech/speech-provider.test.ts | 7 +- extensions/bluebubbles/src/actions.test.ts | 13 +- extensions/bluebubbles/src/catchup.test.ts | 8 +- .../src/channel.message-adapter.test.ts | 7 +- .../bluebubbles/src/channel.status.test.ts | 7 +- extensions/bluebubbles/src/media-send.test.ts | 9 +- extensions/bluebubbles/src/monitor.test.ts | 11 +- .../src/monitor.webhook-auth.test.ts | 12 +- extensions/bluebubbles/src/reactions.test.ts | 7 +- extensions/bonjour/index.test.ts | 8 +- extensions/bonjour/src/advertiser.test.ts | 7 +- .../src/brave-web-search-provider.test.ts | 7 +- extensions/chutes/implicit-provider.test.ts | 12 +- extensions/chutes/models.test.ts | 12 +- .../stream-wrappers.test.ts | 7 +- .../comfy/music-generation-provider.test.ts | 7 +- .../image-generation-provider.test.ts | 8 +- .../media-understanding-provider.test.ts | 7 +- extensions/deepinfra/speech-provider.test.ts | 7 +- extensions/device-pair/index.test.ts | 8 +- extensions/device-pair/notify.test.ts | 7 +- .../diagnostics-otel/src/service.test.ts | 16 +- extensions/diffs/src/browser.test.ts | 7 +- extensions/diffs/src/render-target.test.ts | 7 +- .../diffs/src/tool-render-output.test.ts | 7 +- .../document-extractor.test.ts | 8 +- .../src/ddg-search-provider.test.ts | 7 +- extensions/elevenlabs/speech-provider.test.ts | 7 +- extensions/feishu/setup-entry.test.ts | 7 +- extensions/feishu/src/bitable.test.ts | 7 +- extensions/feishu/src/bot-group-name.test.ts | 8 +- extensions/feishu/src/bot.broadcast.test.ts | 8 +- extensions/feishu/src/bot.card-action.test.ts | 14 +- extensions/feishu/src/bot.test.ts | 13 +- .../feishu/src/card-ux-launcher.test.ts | 7 +- extensions/feishu/src/channel.test.ts | 9 +- extensions/feishu/src/chat.test.ts | 7 +- extensions/feishu/src/client.test.ts | 17 +- .../feishu/src/comment-dispatcher.test.ts | 12 +- extensions/feishu/src/comment-handler.test.ts | 11 +- .../feishu/src/comment-reaction.test.ts | 8 +- extensions/feishu/src/directory.test.ts | 7 +- .../feishu/src/docx.account-selection.test.ts | 8 +- extensions/feishu/src/docx.test.ts | 7 +- extensions/feishu/src/drive.test.ts | 12 +- extensions/feishu/src/media.test.ts | 11 +- .../feishu/src/monitor.bot-menu.test.ts | 8 +- extensions/feishu/src/monitor.cleanup.test.ts | 7 +- extensions/feishu/src/monitor.comment.test.ts | 8 +- .../feishu/src/monitor.reaction.test.ts | 10 +- extensions/feishu/src/monitor.startup.test.ts | 9 +- .../feishu/src/monitor.webhook-e2e.test.ts | 9 +- .../src/monitor.webhook-security.test.ts | 11 +- extensions/feishu/src/outbound.test.ts | 12 +- extensions/feishu/src/probe.test.ts | 7 +- .../feishu/src/reasoning-preview.test.ts | 7 +- .../feishu/src/reply-dispatcher.test.ts | 14 +- extensions/feishu/src/send-target.test.ts | 8 +- .../feishu/src/send.reply-fallback.test.ts | 8 +- extensions/feishu/src/send.test.ts | 11 +- extensions/feishu/src/setup-surface.test.ts | 8 +- extensions/feishu/src/streaming-card.test.ts | 11 +- .../feishu/src/tool-account-routing.test.ts | 7 +- extensions/file-transfer/index.test.ts | 14 +- .../src/shared/node-invoke-policy.test.ts | 8 +- .../file-transfer/src/shared/policy.test.ts | 8 +- .../firecrawl/src/firecrawl-tools.test.ts | 8 +- extensions/github-copilot/auth.test.ts | 8 +- extensions/github-copilot/embeddings.test.ts | 10 +- extensions/github-copilot/index.test.ts | 8 +- extensions/github-copilot/models.test.ts | 10 +- extensions/google-meet/index.create.test.ts | 8 +- extensions/google-meet/index.test.ts | 9 +- extensions/google-meet/node-host.test.ts | 12 +- extensions/google-meet/src/cli.test.ts | 7 +- .../src/voice-call-gateway.test.ts | 11 +- extensions/google/google.live.test.ts | 12 +- .../google/image-generation-provider.test.ts | 1 + .../google/music-generation-provider.test.ts | 7 +- extensions/google/oauth.test.ts | 8 +- .../google/realtime-voice-provider.test.ts | 24 +- extensions/google/speech-provider.test.ts | 7 +- extensions/google/transport-stream.test.ts | 7 +- .../google/video-generation-provider.test.ts | 7 +- extensions/google/web-search-provider.test.ts | 3 +- extensions/googlechat/src/actions.test.ts | 10 +- extensions/googlechat/src/channel.test.ts | 8 +- .../src/google-auth.runtime.test.ts | 9 +- .../googlechat/src/monitor-access.test.ts | 9 +- .../googlechat/src/monitor-webhook.test.ts | 9 +- .../src/monitor.reply-delivery.test.ts | 7 +- .../src/monitor.webhook-routing.test.ts | 7 +- extensions/googlechat/src/setup.test.ts | 7 +- extensions/googlechat/src/targets.test.ts | 10 +- extensions/huggingface/index.test.ts | 8 +- extensions/huggingface/models.test.ts | 12 +- .../image-generation-core/src/runtime.test.ts | 7 +- .../src/monitor.watch-subscribe-retry.test.ts | 9 +- .../imessage/src/monitor/deliver.test.ts | 8 +- .../src/monitor/self-chat-cache.test.ts | 6 +- extensions/imessage/src/status.test.ts | 12 +- extensions/inworld/speech-provider.test.ts | 17 +- extensions/inworld/tts.test.ts | 11 +- extensions/irc/src/accounts.test.ts | 11 +- extensions/irc/src/inbound.behavior.test.ts | 20 +- extensions/irc/src/probe.test.ts | 9 +- extensions/irc/src/send.test.ts | 32 ++- extensions/irc/src/setup.test.ts | 7 +- extensions/kilocode/onboard.test.ts | 13 +- extensions/kilocode/provider-models.test.ts | 24 +- extensions/line/src/accounts.test.ts | 14 +- extensions/line/src/bot-handlers.test.ts | 18 +- extensions/line/src/download.test.ts | 9 +- extensions/line/src/monitor.lifecycle.test.ts | 21 +- extensions/line/src/outbound-media.test.ts | 7 +- extensions/line/src/rich-menu.test.ts | 7 +- extensions/line/src/send.test.ts | 13 +- extensions/line/src/setup-surface.test.ts | 7 +- .../litellm/image-generation-provider.test.ts | 8 +- extensions/llm-task/src/llm-task-tool.test.ts | 23 +- extensions/lmstudio/src/models.test.ts | 7 +- extensions/lmstudio/src/runtime.test.ts | 7 +- extensions/lmstudio/src/setup.test.ts | 9 +- extensions/lmstudio/src/stream.test.ts | 9 +- extensions/lobster/src/lobster-runner.test.ts | 9 +- .../memory-core/src/memory/index.test.ts | 6 +- extensions/memory-core/src/memory/manager.ts | 26 +- extensions/memory-lancedb/index.test.ts | 77 ++++-- extensions/microsoft-foundry/shared.ts | 12 +- .../src/minimax-web-search-provider.test.ts | 18 +- extensions/msteams/src/user-agent.test.ts | 1 + extensions/nextcloud-talk/src/core.test.ts | 52 ++-- .../src/inbound.behavior.test.ts | 32 ++- .../nostr/src/nostr-bus.integration.test.ts | 6 +- extensions/nostr/src/nostr-profile.test.ts | 8 +- extensions/openai/index.test.ts | 1 + extensions/openai/tts.test.ts | 9 +- .../openshell/src/openshell-core.test.ts | 1 + .../qa-lab/src/gateway-rpc-client.test.ts | 72 ++--- extensions/synology-chat/src/core.test.ts | 7 +- extensions/telegram/src/api-fetch.test.ts | 1 + .../src/bot.create-telegram-bot.test.ts | 6 +- ...t.media.stickers-and-fragments.e2e.test.ts | 46 +--- extensions/telegram/src/bot.test.ts | 6 +- .../telegram/src/thread-bindings.test.ts | 7 +- extensions/thread-ownership/index.test.ts | 14 +- extensions/twitch/src/token.test.ts | 8 +- extensions/volcengine/tts.test.ts | 54 ++-- extensions/whatsapp/src/inbound.media.test.ts | 6 + extensions/whatsapp/src/text-runtime.test.ts | 6 +- extensions/xiaomi/speech-provider.test.ts | 5 +- extensions/zalouser/src/accounts.test.ts | 17 +- ...i-embedded-runner-extraparams.live.test.ts | 115 -------- .../pi-embedded-runner.bundle-mcp.e2e.test.ts | 258 ------------------ src/channels/plugins/module-loader.test.ts | 20 +- src/channels/plugins/module-loader.ts | 12 + .../bundled-package-channel-metadata.test.ts | 1 + src/plugins/doctor-contract-registry.test.ts | 6 + src/plugins/doctor-contract-registry.ts | 10 + src/plugins/plugin-module-loader-cache.ts | 9 - src/plugins/setup-registry.test.ts | 6 + src/plugins/setup-registry.ts | 10 + .../test-helpers/registry-jiti-mocks.ts | 13 - test/vitest-ui-package-config.test.ts | 10 +- test/vitest/vitest.gateway-server.config.ts | 4 +- test/vitest/vitest.plugins.config.ts | 2 +- ui/src/ui/app-gateway-chat-load.node.test.ts | 17 +- ui/src/ui/app-gateway.sessions.node.test.ts | 18 +- ui/src/ui/custom-theme.test.ts | 6 +- ui/vitest.config.ts | 9 +- ui/vitest.node.config.ts | 8 +- 182 files changed, 1501 insertions(+), 834 deletions(-) delete mode 100644 src/agents/pi-embedded-runner.bundle-mcp.e2e.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 929772b6f09..50ac6bfcf3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -159,6 +159,7 @@ Docs: https://docs.openclaw.ai - CLI/infer: normalize HEIC/HEIF image files to JPEG before model-run requests, avoiding providers that reject Apple image container formats. Fixes #50081. - OpenRouter: keep the default `openrouter/auto` model ref canonical while preventing TUI and Control UI catalog pickers from displaying or submitting `openrouter/openrouter/auto`. Fixes #62655. - Status/Claude CLI: show `oauth (claude-cli)` for working Claude CLI OAuth runtime sessions instead of `unknown` when no local auth profile exists. Fixes #78632. Thanks @gorkem2020. +- Memory search: preserve keyword-only hybrid FTS matches when vector scoring is unavailable or below the configured minimum score, so exact lexical hits are not dropped by weighted min-score filtering. - Exec approvals/node: let trusted backend node invokes complete no-device Control UI approvals after the original request connection changes, while keeping node, command, cwd, env, and allow-once replay bindings enforced. Fixes #78569. Thanks @naturedogdog. - Agents/subagents: keep background completion delivery on the requester-agent handoff/queue-retry path instead of raw-sending child results directly, and strip child-result wrapper or OpenClaw runtime-context scaffolding from queued outbound retries. Fixes #78531. Thanks @EthanSK. - CLI/completion: guard the shell-profile source line written by `openclaw completion --install` with a file existence check (`[ -f ... ] && source ...` for bash/zsh, `test -f ...; and source ...` for fish) so uninstalling OpenClaw no longer makes new login shells error on a missing completion cache. (#78659) Thanks @sjf. diff --git a/extensions/acpx/src/service.test.ts b/extensions/acpx/src/service.test.ts index 88c55d1c577..62d0a445056 100644 --- a/extensions/acpx/src/service.test.ts +++ b/extensions/acpx/src/service.test.ts @@ -92,6 +92,20 @@ import { getAcpRuntimeBackend } from "../runtime-api.js"; import { createAcpxRuntimeService } from "./service.js"; const tempDirs: string[] = []; +const previousEnv = { + OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE: process.env.OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE, + OPENCLAW_SKIP_ACPX_RUNTIME: process.env.OPENCLAW_SKIP_ACPX_RUNTIME, + OPENCLAW_SKIP_ACPX_RUNTIME_PROBE: process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE, +}; + +function restoreEnv(name: keyof typeof previousEnv): void { + const value = previousEnv[name]; + if (value === undefined) { + delete process.env[name]; + } else { + process.env[name] = value; + } +} async function makeTempDir(): Promise { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-acpx-service-")); @@ -107,9 +121,9 @@ afterEach(async () => { acpxRuntimeConstructorMock.mockClear(); createAgentRegistryMock.mockClear(); createFileSessionStoreMock.mockClear(); - delete process.env.OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE; - delete process.env.OPENCLAW_SKIP_ACPX_RUNTIME; - delete process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE; + restoreEnv("OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE"); + restoreEnv("OPENCLAW_SKIP_ACPX_RUNTIME"); + restoreEnv("OPENCLAW_SKIP_ACPX_RUNTIME_PROBE"); for (const dir of tempDirs.splice(0)) { await fs.rm(dir, { recursive: true, force: true }); } diff --git a/extensions/active-memory/index.test.ts b/extensions/active-memory/index.test.ts index f5e34ddec69..1736b403ff9 100644 --- a/extensions/active-memory/index.test.ts +++ b/extensions/active-memory/index.test.ts @@ -213,6 +213,7 @@ describe("active-memory plugin", () => { afterEach(async () => { vi.useRealTimers(); vi.restoreAllMocks(); + __testing.resetActiveRecallCacheForTests(); if (stateDir) { await fs.rm(stateDir, { recursive: true, force: true }); stateDir = ""; diff --git a/extensions/amazon-bedrock-mantle/discovery.test.ts b/extensions/amazon-bedrock-mantle/discovery.test.ts index 49ee719945d..484028b2e30 100644 --- a/extensions/amazon-bedrock-mantle/discovery.test.ts +++ b/extensions/amazon-bedrock-mantle/discovery.test.ts @@ -28,6 +28,9 @@ describe("bedrock mantle discovery", () => { }); afterEach(() => { + vi.restoreAllMocks(); + resetMantleDiscoveryCacheForTest(); + resetIamTokenCacheForTest(); process.env = originalEnv; }); diff --git a/extensions/amazon-bedrock/discovery.test.ts b/extensions/amazon-bedrock/discovery.test.ts index e28b729c2c0..26e43495d9c 100644 --- a/extensions/amazon-bedrock/discovery.test.ts +++ b/extensions/amazon-bedrock/discovery.test.ts @@ -1,5 +1,5 @@ import type { BedrockClient } from "@aws-sdk/client-bedrock"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { discoverBedrockModels, mergeImplicitBedrockProvider, @@ -36,6 +36,10 @@ describe("bedrock discovery", () => { resetBedrockDiscoveryCacheForTest(); }); + afterEach(() => { + resetBedrockDiscoveryCacheForTest(); + }); + it("filters to active streaming text models and maps modalities", async () => { sendMock .mockResolvedValueOnce({ diff --git a/extensions/amazon-bedrock/index.test.ts b/extensions/amazon-bedrock/index.test.ts index f226c54bd0f..773e63bf04b 100644 --- a/extensions/amazon-bedrock/index.test.ts +++ b/extensions/amazon-bedrock/index.test.ts @@ -6,7 +6,7 @@ import { buildPluginApi, registerSingleProviderPlugin, } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { setAwsSharedIniFileLoaderForTest } from "./aws-credential-refresh.js"; import { resetBedrockDiscoveryCacheForTest } from "./discovery.js"; import amazonBedrockPlugin from "./index.js"; @@ -239,6 +239,13 @@ describe("amazon-bedrock provider plugin", () => { afterEach(() => { setBedrockAppProfileControlPlaneForTest(undefined); setAwsSharedIniFileLoaderForTest(undefined); + resetBedrockDiscoveryCacheForTest(); + resetBedrockAppProfileCacheEligibilityForTest(); + }); + + afterAll(() => { + vi.doUnmock("@aws-sdk/client-bedrock"); + vi.resetModules(); }); it("marks Claude 4.6 Bedrock models as adaptive by default", async () => { diff --git a/extensions/amazon-bedrock/memory-embedding-adapter.test.ts b/extensions/amazon-bedrock/memory-embedding-adapter.test.ts index 66003ae5dfc..6d01c8d5d44 100644 --- a/extensions/amazon-bedrock/memory-embedding-adapter.test.ts +++ b/extensions/amazon-bedrock/memory-embedding-adapter.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const hasAwsCredentialsMock = vi.hoisted(() => vi.fn()); const createBedrockEmbeddingProviderMock = vi.hoisted(() => vi.fn()); @@ -44,6 +44,11 @@ describe("bedrockMemoryEmbeddingProviderAdapter", () => { vi.restoreAllMocks(); }); + afterAll(() => { + vi.doUnmock("./embedding-provider.js"); + vi.resetModules(); + }); + it("registers the expected adapter metadata", () => { expect(bedrockMemoryEmbeddingProviderAdapter.id).toBe("bedrock"); expect(bedrockMemoryEmbeddingProviderAdapter.transport).toBe("remote"); diff --git a/extensions/anthropic-vertex/index.test.ts b/extensions/anthropic-vertex/index.test.ts index 6d737172407..138eb976f82 100644 --- a/extensions/anthropic-vertex/index.test.ts +++ b/extensions/anthropic-vertex/index.test.ts @@ -1,5 +1,5 @@ import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { hasAnthropicVertexAvailableAuthMock } = vi.hoisted(() => ({ hasAnthropicVertexAvailableAuthMock: vi.fn(), @@ -24,6 +24,11 @@ describe("anthropic-vertex provider plugin", () => { vi.clearAllMocks(); }); + afterAll(() => { + vi.doUnmock("./api.js"); + vi.resetModules(); + }); + it("resolves the ADC marker through the provider hook", async () => { const provider = await registerSingleProviderPlugin(anthropicVertexPlugin); diff --git a/extensions/anthropic-vertex/region.adc.test.ts b/extensions/anthropic-vertex/region.adc.test.ts index 1146f1491c8..dfa141d1082 100644 --- a/extensions/anthropic-vertex/region.adc.test.ts +++ b/extensions/anthropic-vertex/region.adc.test.ts @@ -1,6 +1,6 @@ import { platform } from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; const { existsSyncMock, readFileSyncMock } = vi.hoisted(() => ({ existsSyncMock: vi.fn(), @@ -35,6 +35,11 @@ describe("anthropic-vertex ADC reads", () => { readFileSyncMock.mockClear(); }); + afterAll(() => { + vi.doUnmock("node:fs"); + vi.resetModules(); + }); + it("reads explicit ADC credentials without an existsSync preflight", () => { const env = { GOOGLE_APPLICATION_CREDENTIALS: "/tmp/vertex-adc.json", diff --git a/extensions/anthropic/cli-migration.test.ts b/extensions/anthropic/cli-migration.test.ts index 4eeb2609971..a2a648a090b 100644 --- a/extensions/anthropic/cli-migration.test.ts +++ b/extensions/anthropic/cli-migration.test.ts @@ -2,7 +2,7 @@ import type { ProviderAuthContext, ProviderAuthMethodNonInteractiveContext, } from "openclaw/plugin-sdk/plugin-entry"; -import { describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; const { readClaudeCliCredentialsForSetup, readClaudeCliCredentialsForSetupNonInteractive } = vi.hoisted(() => ({ @@ -24,6 +24,16 @@ const { createTestWizardPrompter, registerSingleProviderPlugin } = await import("openclaw/plugin-sdk/plugin-test-runtime"); const { default: anthropicPlugin } = await import("./index.js"); +beforeEach(() => { + readClaudeCliCredentialsForSetup.mockReset(); + readClaudeCliCredentialsForSetupNonInteractive.mockReset(); +}); + +afterAll(() => { + vi.doUnmock("./cli-auth-seam.js"); + vi.resetModules(); +}); + async function resolveAnthropicCliAuthMethod() { const provider = await registerSingleProviderPlugin(anthropicPlugin); const method = provider.auth.find((entry) => entry.id === "cli"); diff --git a/extensions/anthropic/index.test.ts b/extensions/anthropic/index.test.ts index 49708879a27..96d0f1c1b01 100644 --- a/extensions/anthropic/index.test.ts +++ b/extensions/anthropic/index.test.ts @@ -6,7 +6,7 @@ import { capturePluginRegistration, registerSingleProviderPlugin, } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; const { readClaudeCliCredentialsForSetupMock, readClaudeCliCredentialsForRuntimeMock } = vi.hoisted( () => ({ @@ -24,6 +24,16 @@ vi.mock("./cli-auth-seam.js", () => { import anthropicPlugin from "./index.js"; +beforeEach(() => { + readClaudeCliCredentialsForSetupMock.mockReset(); + readClaudeCliCredentialsForRuntimeMock.mockReset(); +}); + +afterAll(() => { + vi.doUnmock("./cli-auth-seam.js"); + vi.resetModules(); +}); + function createModelRegistry(models: ProviderRuntimeModel[]) { return { find(providerId: string, modelId: string) { diff --git a/extensions/azure-speech/speech-provider.test.ts b/extensions/azure-speech/speech-provider.test.ts index c34fd652257..da5c68a9271 100644 --- a/extensions/azure-speech/speech-provider.test.ts +++ b/extensions/azure-speech/speech-provider.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; const { azureSpeechTTSMock, listAzureSpeechVoicesMock } = vi.hoisted(() => ({ azureSpeechTTSMock: vi.fn(async () => Buffer.from("audio-bytes")), @@ -39,6 +39,11 @@ describe("buildAzureSpeechProvider", () => { vi.restoreAllMocks(); }); + afterAll(() => { + vi.doUnmock("./tts.js"); + vi.resetModules(); + }); + it("reports configured only when key plus region or endpoint is available", () => { const provider = buildAzureSpeechProvider(); delete process.env.AZURE_SPEECH_KEY; diff --git a/extensions/bluebubbles/src/actions.test.ts b/extensions/bluebubbles/src/actions.test.ts index eead12620aa..d065a58df02 100644 --- a/extensions/bluebubbles/src/actions.test.ts +++ b/extensions/bluebubbles/src/actions.test.ts @@ -1,5 +1,5 @@ import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures"; -import { describe, expect, it, vi, beforeEach } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { sendBlueBubblesAttachment } from "./attachments.js"; import { editBlueBubblesMessage, setGroupIconBlueBubbles } from "./chat.js"; import { resolveBlueBubblesMessageId } from "./monitor-reply-cache.js"; @@ -92,6 +92,17 @@ describe("bluebubblesMessageActions", () => { vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValue(null); }); + afterAll(() => { + vi.doUnmock("./accounts.js"); + vi.doUnmock("./reactions.js"); + vi.doUnmock("./send.js"); + vi.doUnmock("./chat.js"); + vi.doUnmock("./attachments.js"); + vi.doUnmock("./monitor-reply-cache.js"); + vi.doUnmock("./probe.js"); + vi.resetModules(); + }); + describe("describeMessageTool", () => { it("returns empty array when account is not enabled", () => { const cfg: OpenClawConfig = { diff --git a/extensions/bluebubbles/src/catchup.test.ts b/extensions/bluebubbles/src/catchup.test.ts index 4116fcbb513..0309806587a 100644 --- a/extensions/bluebubbles/src/catchup.test.ts +++ b/extensions/bluebubbles/src/catchup.test.ts @@ -11,6 +11,8 @@ import { import type { NormalizedWebhookMessage } from "./monitor-normalize.js"; import type { WebhookTarget } from "./monitor-shared.js"; +const originalStateDir = process.env.OPENCLAW_STATE_DIR; + function makeStateDir(): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-catchup-test-")); process.env.OPENCLAW_STATE_DIR = dir; @@ -18,7 +20,11 @@ function makeStateDir(): string { } function clearStateDir(dir: string): void { - delete process.env.OPENCLAW_STATE_DIR; + if (originalStateDir === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = originalStateDir; + } fs.rmSync(dir, { recursive: true, force: true }); } diff --git a/extensions/bluebubbles/src/channel.message-adapter.test.ts b/extensions/bluebubbles/src/channel.message-adapter.test.ts index cf9e7209afa..e01779e8f03 100644 --- a/extensions/bluebubbles/src/channel.message-adapter.test.ts +++ b/extensions/bluebubbles/src/channel.message-adapter.test.ts @@ -2,7 +2,7 @@ import { createMessageReceiptFromOutboundResults, verifyChannelMessageAdapterCapabilityProofs, } from "openclaw/plugin-sdk/channel-message"; -import { describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; import { bluebubblesPlugin } from "./channel.js"; const sendMessageBlueBubblesMock = vi.hoisted(() => vi.fn()); @@ -17,6 +17,11 @@ vi.mock("./channel.runtime.js", () => ({ }, })); +afterAll(() => { + vi.doUnmock("./channel.runtime.js"); + vi.resetModules(); +}); + describe("bluebubbles message adapter", () => { it("declares durable text, media, and reply target capabilities with receipt proofs", async () => { sendMessageBlueBubblesMock.mockImplementation( diff --git a/extensions/bluebubbles/src/channel.status.test.ts b/extensions/bluebubbles/src/channel.status.test.ts index 92d80ae8710..d6c452f0f48 100644 --- a/extensions/bluebubbles/src/channel.status.test.ts +++ b/extensions/bluebubbles/src/channel.status.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "./runtime-api.js"; const probeBlueBubblesMock = vi.hoisted(() => vi.fn()); @@ -10,6 +10,11 @@ vi.mock("./channel.runtime.js", () => ({ }, })); +afterAll(() => { + vi.doUnmock("./channel.runtime.js"); + vi.resetModules(); +}); + let bluebubblesPlugin: typeof import("./channel.js").bluebubblesPlugin; describe("bluebubblesPlugin.status.probeAccount", () => { diff --git a/extensions/bluebubbles/src/media-send.test.ts b/extensions/bluebubbles/src/media-send.test.ts index 2f81d89231d..a1b0d59c30a 100644 --- a/extensions/bluebubbles/src/media-send.test.ts +++ b/extensions/bluebubbles/src/media-send.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { sendBlueBubblesMedia } from "./media-send.js"; import type { OpenClawConfig, PluginRuntime } from "./runtime-api.js"; import { setBlueBubblesRuntime } from "./runtime.js"; @@ -23,6 +23,13 @@ vi.mock("./monitor-reply-cache.js", () => ({ resolveBlueBubblesMessageId: resolveBlueBubblesMessageIdMock, })); +afterAll(() => { + vi.doUnmock("./attachments.js"); + vi.doUnmock("./send.js"); + vi.doUnmock("./monitor-reply-cache.js"); + vi.resetModules(); +}); + type RuntimeMocks = { detectMime: ReturnType; fetchRemoteMedia: ReturnType; diff --git a/extensions/bluebubbles/src/monitor.test.ts b/extensions/bluebubbles/src/monitor.test.ts index c89ca72d677..f02eb12b6f4 100644 --- a/extensions/bluebubbles/src/monitor.test.ts +++ b/extensions/bluebubbles/src/monitor.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ResolvedBlueBubblesAccount } from "./accounts.js"; import { fetchBlueBubblesHistory } from "./history.js"; import { createBlueBubblesDebounceRegistry } from "./monitor-debounce.js"; @@ -70,6 +70,15 @@ vi.mock("./history.js", () => ({ fetchBlueBubblesHistory: vi.fn().mockResolvedValue({ entries: [], resolved: true }), })); +afterAll(() => { + vi.doUnmock("./send.js"); + vi.doUnmock("./chat.js"); + vi.doUnmock("./attachments.js"); + vi.doUnmock("./reactions.js"); + vi.doUnmock("./history.js"); + vi.resetModules(); +}); + // Mock runtime const mockEnqueueSystemEvent = vi.fn(); const mockBuildPairingReply = vi.fn(() => "Pairing code: TESTCODE"); diff --git a/extensions/bluebubbles/src/monitor.webhook-auth.test.ts b/extensions/bluebubbles/src/monitor.webhook-auth.test.ts index e9e02d7d607..6da5389144e 100644 --- a/extensions/bluebubbles/src/monitor.webhook-auth.test.ts +++ b/extensions/bluebubbles/src/monitor.webhook-auth.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ResolvedBlueBubblesAccount } from "./accounts.js"; import { fetchBlueBubblesHistory } from "./history.js"; import { @@ -80,6 +80,16 @@ vi.mock("./webhook-ingress.js", async () => { }; }); +afterAll(() => { + vi.doUnmock("./send.js"); + vi.doUnmock("./chat.js"); + vi.doUnmock("./attachments.js"); + vi.doUnmock("./reactions.js"); + vi.doUnmock("./history.js"); + vi.doUnmock("./webhook-ingress.js"); + vi.resetModules(); +}); + // Mock runtime const mockEnqueueSystemEvent = vi.fn(); const mockBuildPairingReply = vi.fn(() => "Pairing code: TESTCODE"); diff --git a/extensions/bluebubbles/src/reactions.test.ts b/extensions/bluebubbles/src/reactions.test.ts index e1a52512a98..865707a311d 100644 --- a/extensions/bluebubbles/src/reactions.test.ts +++ b/extensions/bluebubbles/src/reactions.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; import { normalizeBlueBubblesReactionInput, normalizeBlueBubblesReactionInputStrict, @@ -21,6 +21,11 @@ installBlueBubblesFetchTestHooks({ privateApiStatusMock: noopPrivateApiStatusMock, }); +afterAll(() => { + vi.doUnmock("./accounts.js"); + vi.resetModules(); +}); + describe("reactions", () => { describe("sendBlueBubblesReaction", () => { async function expectRemovedReaction(emoji: string, expectedReaction = "-love") { diff --git a/extensions/bonjour/index.test.ts b/extensions/bonjour/index.test.ts index 78112e79f7f..c713840a80b 100644 --- a/extensions/bonjour/index.test.ts +++ b/extensions/bonjour/index.test.ts @@ -1,5 +1,5 @@ import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; -import { describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ advertiserModuleLoaded: vi.fn(), @@ -26,6 +26,12 @@ vi.mock("openclaw/plugin-sdk/runtime", () => { const { default: bonjourPlugin } = await import("./index.js"); +afterAll(() => { + vi.doUnmock("./src/advertiser.js"); + vi.doUnmock("openclaw/plugin-sdk/runtime"); + vi.resetModules(); +}); + describe("bonjour plugin entry", () => { it("lazy-loads advertiser runtime when gateway discovery advertises", async () => { let discoveryService: diff --git a/extensions/bonjour/src/advertiser.test.ts b/extensions/bonjour/src/advertiser.test.ts index 472dca4ed18..c96c4494055 100644 --- a/extensions/bonjour/src/advertiser.test.ts +++ b/extensions/bonjour/src/advertiser.test.ts @@ -2,7 +2,7 @@ import type { ChildProcess } from "node:child_process"; import fs from "node:fs"; import { createRequire } from "node:module"; import os from "node:os"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; const nodeRequire = createRequire(import.meta.url); const childProcessModule = nodeRequire("node:child_process") as { @@ -95,6 +95,11 @@ vi.mock("@homebridge/ciao", () => { const { startGatewayBonjourAdvertiser } = await import("./advertiser.js"); +afterAll(() => { + vi.doUnmock("@homebridge/ciao"); + vi.resetModules(); +}); + type StartGatewayBonjourAdvertiser = typeof startGatewayBonjourAdvertiser; const startAdvertiser = ( diff --git a/extensions/brave/src/brave-web-search-provider.test.ts b/extensions/brave/src/brave-web-search-provider.test.ts index 4528e615071..2028418a11e 100644 --- a/extensions/brave/src/brave-web-search-provider.test.ts +++ b/extensions/brave/src/brave-web-search-provider.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import { validateJsonSchemaValue } from "openclaw/plugin-sdk/config-schema"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { __testing } from "../test-api.js"; import { createBraveWebSearchProvider as createBraveWebSearchContractProvider } from "../web-search-contract-api.js"; import { createBraveWebSearchProvider } from "./brave-web-search-provider.js"; @@ -37,6 +37,11 @@ const braveManifest = JSON.parse( configSchema?: Record; }; +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/runtime-env"); + vi.resetModules(); +}); + function installBraveLlmContextFetch() { const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => { return { diff --git a/extensions/chutes/implicit-provider.test.ts b/extensions/chutes/implicit-provider.test.ts index fa091d68e2b..486c274c3b2 100644 --- a/extensions/chutes/implicit-provider.test.ts +++ b/extensions/chutes/implicit-provider.test.ts @@ -6,6 +6,14 @@ import { CHUTES_BASE_URL } from "./models.js"; const CHUTES_OAUTH_MARKER = resolveOAuthApiKeyMarker("chutes"); +function restoreEnvVar(name: string, value: string | undefined): void { + if (value === undefined) { + delete process.env[name]; + } else { + process.env[name] = value; + } +} + async function runChutesCatalog(params: { apiKey?: string; discoveryApiKey?: string }) { const provider = await registerSingleProviderPlugin(plugin); const result = await provider.catalog?.run({ @@ -44,8 +52,8 @@ async function withRealChutesDiscovery( try { return await run(fetchMock); } finally { - process.env.VITEST = originalVitest; - process.env.NODE_ENV = originalNodeEnv; + restoreEnvVar("VITEST", originalVitest); + restoreEnvVar("NODE_ENV", originalNodeEnv); globalThis.fetch = originalFetch; } } diff --git a/extensions/chutes/models.test.ts b/extensions/chutes/models.test.ts index a41542fcdc9..be80d1e76e1 100644 --- a/extensions/chutes/models.test.ts +++ b/extensions/chutes/models.test.ts @@ -6,6 +6,14 @@ import { discoverChutesModels, } from "./models.js"; +function restoreEnvVar(name: string, value: string | undefined): void { + if (value === undefined) { + delete process.env[name]; + } else { + process.env[name] = value; + } +} + async function withLiveChutesDiscovery( fetchMock: ReturnType, run: () => Promise, @@ -24,8 +32,8 @@ async function withLiveChutesDiscovery( try { return await run(); } finally { - process.env.NODE_ENV = oldNodeEnv; - process.env.VITEST = oldVitest; + restoreEnvVar("NODE_ENV", oldNodeEnv); + restoreEnvVar("VITEST", oldVitest); vi.unstubAllGlobals(); if (options?.now) { vi.useRealTimers(); diff --git a/extensions/cloudflare-ai-gateway/stream-wrappers.test.ts b/extensions/cloudflare-ai-gateway/stream-wrappers.test.ts index 2d3346cd361..e96f8714fa2 100644 --- a/extensions/cloudflare-ai-gateway/stream-wrappers.test.ts +++ b/extensions/cloudflare-ai-gateway/stream-wrappers.test.ts @@ -1,5 +1,5 @@ import type { StreamFn } from "@mariozechner/pi-agent-core"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { __testing, createCloudflareAiGatewayAnthropicThinkingPrefillWrapper, @@ -19,6 +19,11 @@ vi.mock("openclaw/plugin-sdk/runtime-env", () => ({ }), })); +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/runtime-env"); + vi.resetModules(); +}); + function createPayloadBaseStream(payload: Record): StreamFn { return ((model, _context, options) => { options?.onPayload?.(payload as never, model as never); diff --git a/extensions/comfy/music-generation-provider.test.ts b/extensions/comfy/music-generation-provider.test.ts index 6fdd77a5830..805337b06dd 100644 --- a/extensions/comfy/music-generation-provider.test.ts +++ b/extensions/comfy/music-generation-provider.test.ts @@ -1,5 +1,5 @@ import { expectExplicitMusicGenerationCapabilities } from "openclaw/plugin-sdk/provider-test-contracts"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { buildComfyMusicGenerationProvider } from "./music-generation-provider.js"; import { _setComfyFetchGuardForTesting } from "./workflow-runtime.js"; @@ -8,6 +8,11 @@ const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({ })); describe("comfy music-generation provider", () => { + afterEach(() => { + _setComfyFetchGuardForTesting(null); + vi.clearAllMocks(); + }); + it("registers the workflow model", () => { const provider = buildComfyMusicGenerationProvider(); diff --git a/extensions/deepinfra/image-generation-provider.test.ts b/extensions/deepinfra/image-generation-provider.test.ts index 1cd108eacbc..f485233c323 100644 --- a/extensions/deepinfra/image-generation-provider.test.ts +++ b/extensions/deepinfra/image-generation-provider.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { buildDeepInfraImageGenerationProvider } from "./image-generation-provider.js"; const { @@ -40,6 +40,12 @@ vi.mock("openclaw/plugin-sdk/provider-http", () => ({ sanitizeConfiguredModelProviderRequest: vi.fn((request) => request), })); +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/provider-auth-runtime"); + vi.doUnmock("openclaw/plugin-sdk/provider-http"); + vi.resetModules(); +}); + describe("deepinfra image generation provider", () => { afterEach(() => { assertOkOrThrowHttpErrorMock.mockClear(); diff --git a/extensions/deepinfra/media-understanding-provider.test.ts b/extensions/deepinfra/media-understanding-provider.test.ts index c684685fa77..ba81e5e04d5 100644 --- a/extensions/deepinfra/media-understanding-provider.test.ts +++ b/extensions/deepinfra/media-understanding-provider.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; import { deepinfraMediaUnderstandingProvider, transcribeDeepInfraAudio, @@ -18,6 +18,11 @@ vi.mock("openclaw/plugin-sdk/media-understanding", async () => { }; }); +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/media-understanding"); + vi.resetModules(); +}); + describe("deepinfra media understanding provider", () => { it("declares image and audio defaults", () => { expect(deepinfraMediaUnderstandingProvider).toMatchObject({ diff --git a/extensions/deepinfra/speech-provider.test.ts b/extensions/deepinfra/speech-provider.test.ts index 8a09235c39b..441c3b5c14c 100644 --- a/extensions/deepinfra/speech-provider.test.ts +++ b/extensions/deepinfra/speech-provider.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { buildDeepInfraSpeechProvider } from "./speech-provider.js"; const { assertOkOrThrowHttpErrorMock, postJsonRequestMock, resolveProviderHttpRequestConfigMock } = @@ -19,6 +19,11 @@ vi.mock("openclaw/plugin-sdk/provider-http", () => ({ resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock, })); +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/provider-http"); + vi.resetModules(); +}); + describe("deepinfra speech provider", () => { afterEach(() => { assertOkOrThrowHttpErrorMock.mockClear(); diff --git a/extensions/device-pair/index.test.ts b/extensions/device-pair/index.test.ts index 73c427b26f8..52fa36265d0 100644 --- a/extensions/device-pair/index.test.ts +++ b/extensions/device-pair/index.test.ts @@ -6,7 +6,7 @@ import type { PluginCommandContext, } from "openclaw/plugin-sdk/core"; import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawPluginApi } from "./api.js"; const pluginApiMocks = vi.hoisted(() => ({ @@ -64,6 +64,12 @@ import { } from "./api.js"; import registerDevicePair from "./index.js"; +afterAll(() => { + vi.doUnmock("./api.js"); + vi.doUnmock("./notify.js"); + vi.resetModules(); +}); + type ListedPendingPairingRequest = Awaited>["pending"][number]; type ApproveDevicePairingResolved = Awaited>; type ApprovedPairingResult = Extract< diff --git a/extensions/device-pair/notify.test.ts b/extensions/device-pair/notify.test.ts index 2265ed74622..07eee4822c8 100644 --- a/extensions/device-pair/notify.test.ts +++ b/extensions/device-pair/notify.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const listDevicePairingMock = vi.hoisted(() => vi.fn(async () => ({ pending: [] }))); @@ -12,6 +12,11 @@ vi.mock("./api.js", () => ({ import { handleNotifyCommand } from "./notify.js"; +afterAll(() => { + vi.doUnmock("./api.js"); + vi.resetModules(); +}); + describe("device-pair notify persistence", () => { let stateDir: string; diff --git a/extensions/diagnostics-otel/src/service.test.ts b/extensions/diagnostics-otel/src/service.test.ts index 46cd0d628cc..f4c05b3d73f 100644 --- a/extensions/diagnostics-otel/src/service.test.ts +++ b/extensions/diagnostics-otel/src/service.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, test, vi } from "vitest"; const telemetryState = vi.hoisted(() => { const counters = new Map }>(); @@ -228,6 +228,20 @@ function flushDiagnosticEvents() { return new Promise((resolve) => setImmediate(resolve)); } +afterAll(() => { + vi.doUnmock("@opentelemetry/api"); + vi.doUnmock("@opentelemetry/sdk-node"); + vi.doUnmock("@opentelemetry/exporter-metrics-otlp-proto"); + vi.doUnmock("@opentelemetry/exporter-trace-otlp-proto"); + vi.doUnmock("@opentelemetry/exporter-logs-otlp-proto"); + vi.doUnmock("@opentelemetry/sdk-logs"); + vi.doUnmock("@opentelemetry/sdk-metrics"); + vi.doUnmock("@opentelemetry/sdk-trace-base"); + vi.doUnmock("@opentelemetry/resources"); + vi.doUnmock("@opentelemetry/semantic-conventions"); + vi.resetModules(); +}); + describe("diagnostics-otel service", () => { beforeEach(() => { resetDiagnosticEventsForTest(); diff --git a/extensions/diffs/src/browser.test.ts b/extensions/diffs/src/browser.test.ts index d9012563286..f619e9e2935 100644 --- a/extensions/diffs/src/browser.test.ts +++ b/extensions/diffs/src/browser.test.ts @@ -3,7 +3,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import path from "node:path"; import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { createMockServerResponse } from "openclaw/plugin-sdk/test-env"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../api.js"; import type { OpenClawPluginApi, OpenClawPluginToolContext } from "../api.js"; import { registerDiffsPlugin } from "./plugin.js"; @@ -22,6 +22,11 @@ vi.mock("playwright-core", () => ({ }, })); +afterAll(() => { + vi.doUnmock("playwright-core"); + vi.resetModules(); +}); + describe("PlaywrightDiffScreenshotter", () => { let rootDir: string; let outputPath: string; diff --git a/extensions/diffs/src/render-target.test.ts b/extensions/diffs/src/render-target.test.ts index ac1bcae59cb..3a4991c1e87 100644 --- a/extensions/diffs/src/render-target.test.ts +++ b/extensions/diffs/src/render-target.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; const { preloadFileDiffMock, preloadMultiFileDiffMock } = vi.hoisted(() => ({ preloadFileDiffMock: vi.fn(async ({ fileDiff }: { fileDiff: unknown }) => ({ @@ -19,6 +19,11 @@ vi.mock("@pierre/diffs/ssr", () => ({ preloadMultiFileDiff: preloadMultiFileDiffMock, })); +afterAll(() => { + vi.doUnmock("@pierre/diffs/ssr"); + vi.resetModules(); +}); + import { DEFAULT_DIFFS_TOOL_DEFAULTS, resolveDiffImageRenderOptions } from "./config.js"; import { renderDiffDocument } from "./render.js"; import { parseViewerPayloadJson } from "./viewer-payload.js"; diff --git a/extensions/diffs/src/tool-render-output.test.ts b/extensions/diffs/src/tool-render-output.test.ts index a5956a63096..2849986a9d3 100644 --- a/extensions/diffs/src/tool-render-output.test.ts +++ b/extensions/diffs/src/tool-render-output.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawPluginApi } from "../api.js"; import type { DiffScreenshotter } from "./browser.js"; import { DEFAULT_DIFFS_TOOL_DEFAULTS } from "./config.js"; @@ -15,6 +15,11 @@ vi.mock("./render.js", () => ({ renderDiffDocument: renderDiffDocumentMock, })); +afterAll(() => { + vi.doUnmock("./render.js"); + vi.resetModules(); +}); + describe("diffs tool rendered output guards", () => { let createDiffsTool: typeof import("./tool.js").createDiffsTool; let cleanupRootDir: () => Promise; diff --git a/extensions/document-extract/document-extractor.test.ts b/extensions/document-extract/document-extractor.test.ts index d8b4e3d87a3..f7f5605bf6f 100644 --- a/extensions/document-extract/document-extractor.test.ts +++ b/extensions/document-extract/document-extractor.test.ts @@ -1,7 +1,7 @@ import { existsSync } from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; const { canvasSizes, getDocumentMock, pdfDocument } = vi.hoisted(() => ({ canvasSizes: [] as Array<{ width: number; height: number }>, @@ -37,6 +37,12 @@ import { createPdfDocumentExtractor } from "./document-extractor.js"; const require = createRequire(import.meta.url); describe("PDF document extractor", () => { + afterAll(() => { + vi.doUnmock("pdfjs-dist/legacy/build/pdf.mjs"); + vi.doUnmock("@napi-rs/canvas"); + vi.resetModules(); + }); + beforeEach(() => { canvasSizes.length = 0; getDocumentMock.mockReset(); diff --git a/extensions/duckduckgo/src/ddg-search-provider.test.ts b/extensions/duckduckgo/src/ddg-search-provider.test.ts index b1b8eb4a19b..6ce0880810b 100644 --- a/extensions/duckduckgo/src/ddg-search-provider.test.ts +++ b/extensions/duckduckgo/src/ddg-search-provider.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createDuckDuckGoWebSearchProvider as createDuckDuckGoWebSearchContractProvider } from "../web-search-contract-api.js"; import { DEFAULT_DDG_SAFE_SEARCH, resolveDdgRegion, resolveDdgSafeSearch } from "./config.js"; @@ -14,6 +14,11 @@ describe("duckduckgo web search provider", () => { let createDuckDuckGoWebSearchProvider: typeof import("./ddg-search-provider.js").createDuckDuckGoWebSearchProvider; let ddgClientTesting: typeof import("./ddg-client.js").__testing; + afterAll(() => { + vi.doUnmock("./ddg-client.js"); + vi.resetModules(); + }); + beforeAll(async () => { ({ createDuckDuckGoWebSearchProvider } = await import("./ddg-search-provider.js")); ({ __testing: ddgClientTesting } = diff --git a/extensions/elevenlabs/speech-provider.test.ts b/extensions/elevenlabs/speech-provider.test.ts index 40a4dc95a5d..b296ee53701 100644 --- a/extensions/elevenlabs/speech-provider.test.ts +++ b/extensions/elevenlabs/speech-provider.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { buildElevenLabsSpeechProvider, isValidVoiceId } from "./speech-provider.js"; vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({ @@ -29,6 +29,11 @@ function parseRequestBody(init: RequestInit | undefined): Record { const originalFetch = globalThis.fetch; + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.resetModules(); + }); + afterEach(() => { globalThis.fetch = originalFetch; vi.restoreAllMocks(); diff --git a/extensions/feishu/setup-entry.test.ts b/extensions/feishu/setup-entry.test.ts index aa792287d8c..14607633e79 100644 --- a/extensions/feishu/setup-entry.test.ts +++ b/extensions/feishu/setup-entry.test.ts @@ -1,10 +1,15 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; vi.mock("@larksuiteoapi/node-sdk", () => { throw new Error("setup entry must not load the Feishu SDK"); }); describe("feishu setup entry", () => { + afterAll(() => { + vi.doUnmock("@larksuiteoapi/node-sdk"); + vi.resetModules(); + }); + it("declares the setup entry without importing Feishu runtime dependencies", async () => { const { default: setupEntry } = await import("./setup-entry.js"); diff --git a/extensions/feishu/src/bitable.test.ts b/extensions/feishu/src/bitable.test.ts index 618cf081d60..6310d276ccf 100644 --- a/extensions/feishu/src/bitable.test.ts +++ b/extensions/feishu/src/bitable.test.ts @@ -1,5 +1,5 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawPluginApi } from "../runtime-api.js"; import { createToolFactoryHarness } from "./tool-factory-test-harness.js"; @@ -71,6 +71,11 @@ function createBitableClient(records: MockRecord[]) { } describe("feishu bitable create app cleanup", () => { + afterAll(() => { + vi.doUnmock("./client.js"); + vi.resetModules(); + }); + beforeEach(() => { createFeishuClientMock.mockReset(); }); diff --git a/extensions/feishu/src/bot-group-name.test.ts b/extensions/feishu/src/bot-group-name.test.ts index d5d53627c28..ee8d0499df1 100644 --- a/extensions/feishu/src/bot-group-name.test.ts +++ b/extensions/feishu/src/bot-group-name.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { afterAll, describe, it, expect, vi, beforeEach } from "vitest"; import { resolveGroupName, clearGroupNameCache } from "./bot.js"; import type { ResolvedFeishuAccount } from "./types.js"; @@ -40,6 +40,12 @@ describe("resolveGroupName", () => { const account = makeAccount(); const log = vi.fn(); + afterAll(() => { + vi.doUnmock("./chat.js"); + vi.doUnmock("./client.js"); + vi.resetModules(); + }); + beforeEach(() => { vi.clearAllMocks(); mockGetChatInfo.mockReset(); diff --git a/extensions/feishu/src/bot.broadcast.test.ts b/extensions/feishu/src/bot.broadcast.test.ts index c569fa32196..da4b2cd659f 100644 --- a/extensions/feishu/src/bot.broadcast.test.ts +++ b/extensions/feishu/src/bot.broadcast.test.ts @@ -1,5 +1,5 @@ import type { EnvelopeFormatOptions } from "openclaw/plugin-sdk/channel-inbound"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js"; import type { FeishuMessageEvent } from "./bot.js"; import { clearGroupNameCache, handleFeishuMessage } from "./bot.js"; @@ -176,6 +176,12 @@ describe("broadcast dispatch", () => { }, } as unknown as PluginRuntime; + afterAll(() => { + vi.doUnmock("./reply-dispatcher.js"); + vi.doUnmock("./client.js"); + vi.resetModules(); + }); + function createBroadcastConfig(): ClawdbotConfig { return { broadcast: { "oc-broadcast-group": ["susan", "main"] }, diff --git a/extensions/feishu/src/bot.card-action.test.ts b/extensions/feishu/src/bot.card-action.test.ts index e8e046be66e..0bc6404df4b 100644 --- a/extensions/feishu/src/bot.card-action.test.ts +++ b/extensions/feishu/src/bot.card-action.test.ts @@ -1,5 +1,5 @@ import { createRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { afterAll, afterEach, describe, it, expect, vi, beforeEach } from "vitest"; import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; import { FeishuRetryableCardActionError, @@ -48,6 +48,18 @@ describe("Feishu Card Action Handler", () => { const cfg: ClawdbotConfig = {}; const runtime: RuntimeEnv = createRuntimeEnv(); + afterAll(() => { + vi.doUnmock("./accounts.js"); + vi.doUnmock("./bot.js"); + vi.doUnmock("./client.js"); + vi.doUnmock("./send.js"); + vi.resetModules(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + function createCardActionEvent(params: { token: string; actionValue: Record; diff --git a/extensions/feishu/src/bot.test.ts b/extensions/feishu/src/bot.test.ts index 396c1e8dd3a..61ddacec34c 100644 --- a/extensions/feishu/src/bot.test.ts +++ b/extensions/feishu/src/bot.test.ts @@ -1,7 +1,7 @@ import type * as ConversationRuntime from "openclaw/plugin-sdk/conversation-runtime"; import { createRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime"; import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js"; import type { FeishuMessageEvent } from "./bot.js"; import { handleFeishuMessage } from "./bot.js"; @@ -353,6 +353,17 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => { }; }); +afterAll(() => { + vi.doUnmock("./reply-dispatcher.js"); + vi.doUnmock("./reasoning-preview.js"); + vi.doUnmock("./send.js"); + vi.doUnmock("./media.js"); + vi.doUnmock("./audio-preflight.runtime.js"); + vi.doUnmock("./client.js"); + vi.doUnmock("openclaw/plugin-sdk/conversation-runtime"); + vi.resetModules(); +}); + async function dispatchMessage(params: { cfg: ClawdbotConfig; event: FeishuMessageEvent }) { const runtime = createRuntimeEnv(); const feishuConfig = params.cfg.channels?.feishu; diff --git a/extensions/feishu/src/card-ux-launcher.test.ts b/extensions/feishu/src/card-ux-launcher.test.ts index 1ca9eb5ee66..7e4aea4ac60 100644 --- a/extensions/feishu/src/card-ux-launcher.test.ts +++ b/extensions/feishu/src/card-ux-launcher.test.ts @@ -1,5 +1,5 @@ import { createRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { describe, expect, it, vi, beforeEach } from "vitest"; +import { afterAll, describe, expect, it, vi, beforeEach } from "vitest"; import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; import { expectFirstSentCardUsesFillWidthOnly, @@ -20,6 +20,11 @@ vi.mock("./send.js", () => ({ describe("feishu quick-action launcher", () => { const cfg: ClawdbotConfig = {}; + afterAll(() => { + vi.doUnmock("./send.js"); + vi.resetModules(); + }); + beforeEach(() => { vi.clearAllMocks(); }); diff --git a/extensions/feishu/src/channel.test.ts b/extensions/feishu/src/channel.test.ts index 10c98c64bb6..93a4985f068 100644 --- a/extensions/feishu/src/channel.test.ts +++ b/extensions/feishu/src/channel.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../runtime-api.js"; import { feishuPlugin } from "./channel.js"; import { looksLikeFeishuId, normalizeFeishuTarget, resolveReceiveIdType } from "./targets.js"; @@ -59,6 +59,13 @@ function getDescribedActions(cfg: OpenClawConfig, accountId?: string): string[] return [...(feishuPlugin.actions?.describeMessageTool?.({ cfg, accountId })?.actions ?? [])]; } +afterAll(() => { + vi.doUnmock("./probe.js"); + vi.doUnmock("./client.js"); + vi.doUnmock("./channel.runtime.js"); + vi.resetModules(); +}); + describe("feishuPlugin.status.probeAccount", () => { it("uses current account credentials for multi-account config", async () => { const cfg = { diff --git a/extensions/feishu/src/chat.test.ts b/extensions/feishu/src/chat.test.ts index 9e58724f770..e0873d9d85e 100644 --- a/extensions/feishu/src/chat.test.ts +++ b/extensions/feishu/src/chat.test.ts @@ -1,5 +1,5 @@ import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawPluginApi, PluginRuntime } from "../runtime-api.js"; const createFeishuClientMock = vi.hoisted(() => vi.fn()); @@ -37,6 +37,11 @@ describe("registerFeishuChatTools", () => { ({ registerFeishuChatTools } = await import("./chat.js")); }); + afterAll(() => { + vi.doUnmock("./client.js"); + vi.resetModules(); + }); + beforeEach(() => { vi.clearAllMocks(); createFeishuClientMock.mockReturnValue({ diff --git a/extensions/feishu/src/client.test.ts b/extensions/feishu/src/client.test.ts index cd33482f4c6..5c212954d19 100644 --- a/extensions/feishu/src/client.test.ts +++ b/extensions/feishu/src/client.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { FeishuConfigSchema } from "./config-schema.js"; import type { ResolvedFeishuAccount } from "./types.js"; @@ -206,6 +206,21 @@ afterEach(() => { setFeishuClientRuntimeForTest(); }); +afterAll(() => { + vi.doUnmock("./channel.js"); + vi.doUnmock("./docx.js"); + vi.doUnmock("./chat.js"); + vi.doUnmock("./wiki.js"); + vi.doUnmock("./drive.js"); + vi.doUnmock("./perm.js"); + vi.doUnmock("./bitable.js"); + vi.doUnmock("./runtime.js"); + vi.doUnmock("./subagent-hooks.js"); + vi.doUnmock("@larksuiteoapi/node-sdk"); + vi.doUnmock("proxy-agent"); + vi.resetModules(); +}); + describe("createFeishuClient HTTP timeout", () => { const getLastClientHttpInstance = (): HttpInstanceLike | undefined => { const httpInstance = readCallOptions(clientCtorMock).httpInstance; diff --git a/extensions/feishu/src/comment-dispatcher.test.ts b/extensions/feishu/src/comment-dispatcher.test.ts index ad853c1970c..4b880a8e732 100644 --- a/extensions/feishu/src/comment-dispatcher.test.ts +++ b/extensions/feishu/src/comment-dispatcher.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; const resolveFeishuRuntimeAccountMock = vi.hoisted(() => vi.fn()); const createFeishuClientMock = vi.hoisted(() => vi.fn()); @@ -35,6 +35,16 @@ vi.mock("./runtime.js", () => ({ import { createFeishuCommentReplyDispatcher } from "./comment-dispatcher.js"; describe("createFeishuCommentReplyDispatcher", () => { + afterAll(() => { + vi.doUnmock("./accounts.js"); + vi.doUnmock("./client.js"); + vi.doUnmock("./comment-dispatcher-runtime-api.js"); + vi.doUnmock("./comment-reaction.js"); + vi.doUnmock("./drive.js"); + vi.doUnmock("./runtime.js"); + vi.resetModules(); + }); + function createTestCommentReplyDispatcher() { createFeishuCommentReplyDispatcher({ cfg: {} as never, diff --git a/extensions/feishu/src/comment-handler.test.ts b/extensions/feishu/src/comment-handler.test.ts index bfb20455c18..fce428116e0 100644 --- a/extensions/feishu/src/comment-handler.test.ts +++ b/extensions/feishu/src/comment-handler.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js"; import { handleFeishuCommentEvent } from "./comment-handler.js"; import { setFeishuRuntime } from "./runtime.js"; @@ -172,6 +172,15 @@ function createTestRuntime(overrides?: { } describe("handleFeishuCommentEvent", () => { + afterAll(() => { + vi.doUnmock("./monitor.comment.js"); + vi.doUnmock("./comment-dispatcher.js"); + vi.doUnmock("./dynamic-agent.js"); + vi.doUnmock("./client.js"); + vi.doUnmock("./drive.js"); + vi.resetModules(); + }); + beforeEach(() => { vi.clearAllMocks(); maybeCreateDynamicAgentMock.mockResolvedValue({ created: false }); diff --git a/extensions/feishu/src/comment-reaction.test.ts b/extensions/feishu/src/comment-reaction.test.ts index 3a3cd9f9ef3..4ecb39bc8fb 100644 --- a/extensions/feishu/src/comment-reaction.test.ts +++ b/extensions/feishu/src/comment-reaction.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../runtime-api.js"; import { cleanupAmbientCommentTypingReaction, @@ -19,6 +19,12 @@ vi.mock("./client.js", () => ({ describe("createCommentTypingReactionLifecycle", () => { const request = vi.fn(); + afterAll(() => { + vi.doUnmock("./accounts.js"); + vi.doUnmock("./client.js"); + vi.resetModules(); + }); + beforeEach(() => { vi.clearAllMocks(); resolveFeishuRuntimeAccountMock.mockReturnValue({ diff --git a/extensions/feishu/src/directory.test.ts b/extensions/feishu/src/directory.test.ts index 11c266c345b..d8a3432bcdc 100644 --- a/extensions/feishu/src/directory.test.ts +++ b/extensions/feishu/src/directory.test.ts @@ -1,5 +1,5 @@ import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../runtime-api.js"; const createFeishuClientMock = vi.hoisted(() => vi.fn()); @@ -45,6 +45,11 @@ function makeConfiguredCfg(): ClawdbotConfig { } describe("feishu directory (config-backed)", () => { + afterAll(() => { + vi.doUnmock("./client.js"); + vi.resetModules(); + }); + beforeEach(() => { createFeishuClientMock.mockReset(); }); diff --git a/extensions/feishu/src/docx.account-selection.test.ts b/extensions/feishu/src/docx.account-selection.test.ts index d4a2ff39470..36d483a72d8 100644 --- a/extensions/feishu/src/docx.account-selection.test.ts +++ b/extensions/feishu/src/docx.account-selection.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; import type { OpenClawPluginApi } from "../runtime-api.js"; import { createToolFactoryHarness } from "./tool-factory-test-harness.js"; @@ -26,6 +26,12 @@ describe("feishu_doc account selection", () => { ({ registerFeishuDocTools } = await import("./docx.js")); }); + afterAll(() => { + vi.doUnmock("./client.js"); + vi.doUnmock("@larksuiteoapi/node-sdk"); + vi.resetModules(); + }); + beforeEach(() => { vi.clearAllMocks(); }); diff --git a/extensions/feishu/src/docx.test.ts b/extensions/feishu/src/docx.test.ts index afe0fce13f1..cf965a14444 100644 --- a/extensions/feishu/src/docx.test.ts +++ b/extensions/feishu/src/docx.test.ts @@ -1,6 +1,6 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createToolFactoryHarness, type ToolLike } from "./tool-factory-test-harness.js"; const createFeishuClientMock = vi.hoisted(() => vi.fn()); @@ -64,6 +64,11 @@ type ToolResultWithDetails = { const WORKSPACE_ROOT = path.resolve("/workspace"); describe("feishu_doc image fetch hardening", () => { + afterAll(() => { + vi.restoreAllMocks(); + vi.resetModules(); + }); + beforeEach(() => { vi.clearAllMocks(); diff --git a/extensions/feishu/src/drive.test.ts b/extensions/feishu/src/drive.test.ts index 744cc8c8f6a..4e3345a2781 100644 --- a/extensions/feishu/src/drive.test.ts +++ b/extensions/feishu/src/drive.test.ts @@ -1,5 +1,5 @@ import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawPluginApi, PluginRuntime } from "../runtime-api.js"; const createFeishuToolClientMock = vi.hoisted(() => vi.fn()); @@ -43,6 +43,16 @@ describe("registerFeishuDriveTools", () => { ({ registerFeishuDriveTools } = await import("./drive.js")); }); + afterAll(() => { + vi.doUnmock("./tool-account.js"); + vi.doUnmock("./comment-reaction.js"); + vi.resetModules(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + beforeEach(() => { vi.clearAllMocks(); resolveAnyEnabledFeishuToolsConfigMock.mockReturnValue({ diff --git a/extensions/feishu/src/media.test.ts b/extensions/feishu/src/media.test.ts index 192b9a3def7..dad8f20673e 100644 --- a/extensions/feishu/src/media.test.ts +++ b/extensions/feishu/src/media.test.ts @@ -2,7 +2,7 @@ import { realpathSync } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../runtime-api.js"; const createFeishuClientMock = vi.hoisted(() => vi.fn()); @@ -98,6 +98,15 @@ describe("sendMediaFeishu msg_type routing", () => { } = await import("./media.js")); }); + afterAll(() => { + vi.doUnmock("./client.js"); + vi.doUnmock("./accounts.js"); + vi.doUnmock("./targets.js"); + vi.doUnmock("./runtime.js"); + vi.doUnmock("openclaw/plugin-sdk/media-runtime"); + vi.resetModules(); + }); + beforeEach(() => { vi.clearAllMocks(); mockResolvedFeishuAccount(); diff --git a/extensions/feishu/src/monitor.bot-menu.test.ts b/extensions/feishu/src/monitor.bot-menu.test.ts index ce2ea8411f9..f5691d71bc0 100644 --- a/extensions/feishu/src/monitor.bot-menu.test.ts +++ b/extensions/feishu/src/monitor.bot-menu.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../runtime-api.js"; import { expectFirstSentCardUsesFillWidthOnly } from "./card-test-helpers.js"; import { createFeishuBotMenuHandler } from "./monitor.bot-menu-handler.js"; @@ -55,6 +55,12 @@ async function registerHandlers() { } describe("Feishu bot menu handler", () => { + afterAll(() => { + vi.doUnmock("./bot.js"); + vi.doUnmock("./send.js"); + vi.resetModules(); + }); + beforeEach(() => { vi.clearAllMocks(); process.env.OPENCLAW_STATE_DIR = `/tmp/openclaw-feishu-bot-menu-test-${Date.now()}-${Math.random().toString(36).slice(2)}`; diff --git a/extensions/feishu/src/monitor.cleanup.test.ts b/extensions/feishu/src/monitor.cleanup.test.ts index fd81fd58cc7..115e59d8dad 100644 --- a/extensions/feishu/src/monitor.cleanup.test.ts +++ b/extensions/feishu/src/monitor.cleanup.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { botNames, botOpenIds, stopFeishuMonitorState, wsClients } from "./monitor.state.js"; import type { ResolvedFeishuAccount } from "./types.js"; @@ -43,6 +43,11 @@ afterEach(() => { vi.clearAllMocks(); }); +afterAll(() => { + vi.doUnmock("./client.js"); + vi.resetModules(); +}); + describe("feishu websocket cleanup", () => { it("closes the websocket client when the monitor aborts", async () => { const wsClient = createWsClient(); diff --git a/extensions/feishu/src/monitor.comment.test.ts b/extensions/feishu/src/monitor.comment.test.ts index 04b1135b547..a1ed3e9808d 100644 --- a/extensions/feishu/src/monitor.comment.test.ts +++ b/extensions/feishu/src/monitor.comment.test.ts @@ -1,5 +1,5 @@ import { createNonExitingRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../runtime-api.js"; import * as dedup from "./dedup.js"; import { createFeishuDriveCommentNoticeHandler } from "./monitor.comment-notice-handler.js"; @@ -23,6 +23,12 @@ vi.mock("./comment-handler.js", () => ({ handleFeishuCommentEvent: handleFeishuCommentEventMock, })); +afterAll(() => { + vi.doUnmock("./client.js"); + vi.doUnmock("./comment-handler.js"); + vi.resetModules(); +}); + function buildMonitorConfig(): ClawdbotConfig { return { channels: { diff --git a/extensions/feishu/src/monitor.reaction.test.ts b/extensions/feishu/src/monitor.reaction.test.ts index 7ef26d0c5d7..23815ad9726 100644 --- a/extensions/feishu/src/monitor.reaction.test.ts +++ b/extensions/feishu/src/monitor.reaction.test.ts @@ -4,7 +4,7 @@ import { } from "openclaw/plugin-sdk/channel-inbound-debounce"; import { hasControlCommand } from "openclaw/plugin-sdk/command-detection"; import { createNonExitingRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js"; import { parseFeishuMessageEvent, type FeishuMessageEvent } from "./bot.js"; import * as dedup from "./dedup.js"; @@ -45,6 +45,14 @@ vi.mock("./thread-bindings.js", () => ({ createFeishuThreadBindingManager: createFeishuThreadBindingManagerMock, })); +afterAll(() => { + vi.doUnmock("./client.js"); + vi.doUnmock("./bot.js"); + vi.doUnmock("./monitor.transport.js"); + vi.doUnmock("./thread-bindings.js"); + vi.resetModules(); +}); + const cfg = {} as ClawdbotConfig; function makeReactionEvent( diff --git a/extensions/feishu/src/monitor.startup.test.ts b/extensions/feishu/src/monitor.startup.test.ts index 7d1d4c96fff..c8f2bdc6b7b 100644 --- a/extensions/feishu/src/monitor.startup.test.ts +++ b/extensions/feishu/src/monitor.startup.test.ts @@ -1,5 +1,5 @@ import { createNonExitingRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../runtime-api.js"; import { monitorFeishuProvider, stopFeishuMonitor } from "./monitor.js"; @@ -52,6 +52,13 @@ afterEach(() => { stopFeishuMonitor(); }); +afterAll(() => { + vi.doUnmock("./probe.js"); + vi.doUnmock("./client.js"); + vi.doUnmock("./runtime.js"); + vi.resetModules(); +}); + describe("Feishu monitor startup preflight", () => { it("starts account probes sequentially to avoid startup bursts", async () => { let inFlight = 0; diff --git a/extensions/feishu/src/monitor.webhook-e2e.test.ts b/extensions/feishu/src/monitor.webhook-e2e.test.ts index 57170d9d25c..f3acd699213 100644 --- a/extensions/feishu/src/monitor.webhook-e2e.test.ts +++ b/extensions/feishu/src/monitor.webhook-e2e.test.ts @@ -1,5 +1,5 @@ import crypto from "node:crypto"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { createFeishuRuntimeMockModule } from "./monitor.test-mocks.js"; import { withRunningWebhookMonitor } from "./monitor.webhook.test-helpers.js"; @@ -63,6 +63,13 @@ afterEach(() => { stopFeishuMonitor(); }); +afterAll(() => { + vi.doUnmock("./probe.js"); + vi.doUnmock("./client.js"); + vi.doUnmock("./runtime.js"); + vi.resetModules(); +}); + describe("Feishu webhook signed-request e2e", () => { it("rejects invalid signatures with 401 instead of empty 200", async () => { probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" }); diff --git a/extensions/feishu/src/monitor.webhook-security.test.ts b/extensions/feishu/src/monitor.webhook-security.test.ts index f04b7144b10..bdbd16d5566 100644 --- a/extensions/feishu/src/monitor.webhook-security.test.ts +++ b/extensions/feishu/src/monitor.webhook-security.test.ts @@ -1,5 +1,5 @@ import { createConnection } from "node:net"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { createFeishuClientMockModule, createFeishuRuntimeMockModule, @@ -155,6 +155,15 @@ afterEach(() => { stopFeishuMonitor(); }); +afterAll(() => { + vi.doUnmock("./probe.js"); + vi.doUnmock("./client.js"); + vi.doUnmock("./runtime.js"); + vi.doUnmock("@larksuiteoapi/node-sdk"); + vi.doUnmock("./monitor.state.js"); + vi.resetModules(); +}); + describe("Feishu webhook security hardening", () => { it("rejects webhook mode without verificationToken", async () => { probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" }); diff --git a/extensions/feishu/src/outbound.test.ts b/extensions/feishu/src/outbound.test.ts index 3ef50e51973..25980f766b3 100644 --- a/extensions/feishu/src/outbound.test.ts +++ b/extensions/feishu/src/outbound.test.ts @@ -3,7 +3,7 @@ import os from "node:os"; import path from "node:path"; import { verifyChannelMessageAdapterCapabilityProofs } from "openclaw/plugin-sdk/channel-message"; import type { MessagePresentation } from "openclaw/plugin-sdk/interactive-runtime"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../runtime-api.js"; const sendMediaFeishuMock = vi.hoisted(() => vi.fn()); @@ -85,6 +85,16 @@ const cardRenderConfig: ClawdbotConfig = { }, }; +afterAll(() => { + vi.doUnmock("./media.js"); + vi.doUnmock("./send.js"); + vi.doUnmock("./runtime.js"); + vi.doUnmock("./client.js"); + vi.doUnmock("./drive.js"); + vi.doUnmock("./comment-reaction.js"); + vi.resetModules(); +}); + function resetOutboundMocks() { vi.clearAllMocks(); sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" }); diff --git a/extensions/feishu/src/probe.test.ts b/extensions/feishu/src/probe.test.ts index 4462e2f53e0..89ba57e8309 100644 --- a/extensions/feishu/src/probe.test.ts +++ b/extensions/feishu/src/probe.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { clearProbeCache, FEISHU_PROBE_REQUEST_TIMEOUT_MS, probeFeishu } from "./probe.js"; const createFeishuClientMock = vi.hoisted(() => vi.fn()); @@ -23,6 +23,11 @@ const BOT1_RESPONSE = { data: { pingBotInfo: { botName: "Bot1", botID: "ou_1" } }, } as const; +afterAll(() => { + vi.doUnmock("./client.js"); + vi.resetModules(); +}); + function makeRequestFn(response: Record) { return vi.fn().mockResolvedValue(response); } diff --git a/extensions/feishu/src/reasoning-preview.test.ts b/extensions/feishu/src/reasoning-preview.test.ts index ed7b8f2219e..c6bf99c9b2a 100644 --- a/extensions/feishu/src/reasoning-preview.test.ts +++ b/extensions/feishu/src/reasoning-preview.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { resolveFeishuReasoningPreviewEnabled } from "./reasoning-preview.js"; const { loadSessionStoreMock } = vi.hoisted(() => ({ @@ -14,6 +14,11 @@ vi.mock("./bot-runtime-api.js", async () => { }; }); +afterAll(() => { + vi.doUnmock("./bot-runtime-api.js"); + vi.resetModules(); +}); + describe("resolveFeishuReasoningPreviewEnabled", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/extensions/feishu/src/reply-dispatcher.test.ts b/extensions/feishu/src/reply-dispatcher.test.ts index b85e7ae2950..663ef6cab71 100644 --- a/extensions/feishu/src/reply-dispatcher.test.ts +++ b/extensions/feishu/src/reply-dispatcher.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; type StreamingSessionStub = { active: boolean; @@ -98,6 +98,18 @@ import { createFeishuReplyDispatcher, } from "./reply-dispatcher.js"; +afterAll(() => { + vi.doUnmock("./accounts.js"); + vi.doUnmock("./runtime.js"); + vi.doUnmock("./send.js"); + vi.doUnmock("./media.js"); + vi.doUnmock("./client.js"); + vi.doUnmock("./targets.js"); + vi.doUnmock("./typing.js"); + vi.doUnmock("./streaming-card.js"); + vi.resetModules(); +}); + describe("createFeishuReplyDispatcher streaming behavior", () => { type ReplyDispatcherArgs = Parameters[0]; diff --git a/extensions/feishu/src/send-target.test.ts b/extensions/feishu/src/send-target.test.ts index c82ac22e006..db00786d4f3 100644 --- a/extensions/feishu/src/send-target.test.ts +++ b/extensions/feishu/src/send-target.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../runtime-api.js"; const resolveFeishuAccountMock = vi.hoisted(() => vi.fn()); @@ -23,6 +23,12 @@ describe("resolveFeishuSendTarget", () => { ({ resolveFeishuSendTarget } = await import("./send-target.js")); }); + afterAll(() => { + vi.doUnmock("./accounts.js"); + vi.doUnmock("./client.js"); + vi.resetModules(); + }); + beforeEach(() => { resolveFeishuAccountMock.mockReset().mockReturnValue({ accountId: "default", diff --git a/extensions/feishu/src/send.reply-fallback.test.ts b/extensions/feishu/src/send.reply-fallback.test.ts index cd11d05cd61..2b444005853 100644 --- a/extensions/feishu/src/send.reply-fallback.test.ts +++ b/extensions/feishu/src/send.reply-fallback.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const resolveFeishuSendTargetMock = vi.hoisted(() => vi.fn()); const resolveMarkdownTableModeMock = vi.hoisted(() => vi.fn(() => "preserve")); @@ -41,6 +41,12 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => { ({ sendCardFeishu, sendMessageFeishu } = await import("./send.js")); }); + afterAll(() => { + vi.doUnmock("./send-target.js"); + vi.doUnmock("./runtime.js"); + vi.resetModules(); + }); + beforeEach(() => { vi.clearAllMocks(); resolveFeishuSendTargetMock.mockReturnValue({ diff --git a/extensions/feishu/src/send.test.ts b/extensions/feishu/src/send.test.ts index b881bff4810..cf4bcb159b3 100644 --- a/extensions/feishu/src/send.test.ts +++ b/extensions/feishu/src/send.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../runtime-api.js"; import { buildMarkdownCard } from "./send.js"; @@ -75,6 +75,15 @@ describe("getMessageFeishu", () => { } = await import("./send.js")); }); + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/markdown-table-runtime"); + vi.doUnmock("openclaw/plugin-sdk/text-runtime"); + vi.doUnmock("./client.js"); + vi.doUnmock("./accounts.js"); + vi.doUnmock("./runtime.js"); + vi.resetModules(); + }); + beforeEach(() => { vi.clearAllMocks(); mockResolveMarkdownTableMode.mockReturnValue("preserve"); diff --git a/extensions/feishu/src/setup-surface.test.ts b/extensions/feishu/src/setup-surface.test.ts index db60f8fada8..70cf5164556 100644 --- a/extensions/feishu/src/setup-surface.test.ts +++ b/extensions/feishu/src/setup-surface.test.ts @@ -5,7 +5,7 @@ import { createTestWizardPrompter, runSetupWizardConfigure, } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { FeishuProbeResult } from "./types.js"; const { probeFeishuMock } = vi.hoisted(() => ({ @@ -76,6 +76,12 @@ async function getStatusWithEnvRefs(params: { appIdKey: string; appSecretKey: st const feishuConfigure = createPluginSetupWizardConfigure(feishuPlugin); const feishuGetStatus = createPluginSetupWizardStatus(feishuPlugin); +afterAll(() => { + vi.doUnmock("./probe.js"); + vi.doUnmock("./app-registration.js"); + vi.resetModules(); +}); + describe("feishu setup wizard", () => { beforeEach(() => { probeFeishuMock.mockReset(); diff --git a/extensions/feishu/src/streaming-card.test.ts b/extensions/feishu/src/streaming-card.test.ts index f531e11fd1c..0903acf8b24 100644 --- a/extensions/feishu/src/streaming-card.test.ts +++ b/extensions/feishu/src/streaming-card.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn()); @@ -38,11 +38,20 @@ function setStreamingSessionInternals( } describe("FeishuStreamingSession", () => { + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.resetModules(); + }); + beforeEach(() => { vi.useRealTimers(); fetchWithSsrFGuardMock.mockReset(); }); + afterEach(() => { + vi.useRealTimers(); + }); + function mockFetches(updateBodies: string[]) { fetchWithSsrFGuardMock.mockImplementation( async ({ url, init }: { url: string; init?: { body?: string } }) => { diff --git a/extensions/feishu/src/tool-account-routing.test.ts b/extensions/feishu/src/tool-account-routing.test.ts index 581ba45f997..2e046f58896 100644 --- a/extensions/feishu/src/tool-account-routing.test.ts +++ b/extensions/feishu/src/tool-account-routing.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; import type { OpenClawPluginApi } from "../runtime-api.js"; import { createToolFactoryHarness } from "./tool-factory-test-harness.js"; @@ -62,6 +62,11 @@ describe("feishu tool account routing", () => { ({ registerFeishuWikiTools } = await import("./wiki.js")); }); + afterAll(() => { + vi.doUnmock("./client.js"); + vi.resetModules(); + }); + beforeEach(() => { vi.clearAllMocks(); }); diff --git a/extensions/file-transfer/index.test.ts b/extensions/file-transfer/index.test.ts index 83280f61182..cd0d7c5c252 100644 --- a/extensions/file-transfer/index.test.ts +++ b/extensions/file-transfer/index.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; import pluginEntry from "./index.js"; function rejectRuntimeImport(moduleName: string) { @@ -16,6 +16,18 @@ vi.mock("./src/tools/dir-list-tool.js", rejectRuntimeImport("tools/dir-list-tool vi.mock("./src/tools/dir-fetch-tool.js", rejectRuntimeImport("tools/dir-fetch-tool")); vi.mock("./src/tools/file-write-tool.js", rejectRuntimeImport("tools/file-write-tool")); +afterAll(() => { + vi.doUnmock("./src/node-host/file-fetch.js"); + vi.doUnmock("./src/node-host/dir-list.js"); + vi.doUnmock("./src/node-host/dir-fetch.js"); + vi.doUnmock("./src/node-host/file-write.js"); + vi.doUnmock("./src/tools/file-fetch-tool.js"); + vi.doUnmock("./src/tools/dir-list-tool.js"); + vi.doUnmock("./src/tools/dir-fetch-tool.js"); + vi.doUnmock("./src/tools/file-write-tool.js"); + vi.resetModules(); +}); + describe("file-transfer plugin entry", () => { it("registers static command and tool descriptors without importing runtime handlers", () => { const registerNodeInvokePolicy = vi.fn(); diff --git a/extensions/file-transfer/src/shared/node-invoke-policy.test.ts b/extensions/file-transfer/src/shared/node-invoke-policy.test.ts index aa89024f399..ab9705f803d 100644 --- a/extensions/file-transfer/src/shared/node-invoke-policy.test.ts +++ b/extensions/file-transfer/src/shared/node-invoke-policy.test.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { OpenClawPluginNodeInvokePolicyContext } from "openclaw/plugin-sdk/plugin-entry"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { createFileTransferNodeInvokePolicy } from "./node-invoke-policy.js"; vi.mock("./audit.js", () => ({ @@ -26,6 +26,12 @@ afterEach(async () => { tmpRoots.length = 0; }); +afterAll(() => { + vi.doUnmock("./audit.js"); + vi.doUnmock("./policy.js"); + vi.resetModules(); +}); + async function tarEntries(entries: Record): Promise { const tmpRoot = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "node-policy-tar-"))); tmpRoots.push(tmpRoot); diff --git a/extensions/file-transfer/src/shared/policy.test.ts b/extensions/file-transfer/src/shared/policy.test.ts index ebeee95053e..af541b28043 100644 --- a/extensions/file-transfer/src/shared/policy.test.ts +++ b/extensions/file-transfer/src/shared/policy.test.ts @@ -1,6 +1,6 @@ import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // Mock the plugin-sdk runtime-config surface so we can drive the policy // reader from the test without booting a gateway. mutateConfigFile is also @@ -28,6 +28,12 @@ afterEach(() => { vi.restoreAllMocks(); }); +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/runtime-config-snapshot"); + vi.doUnmock("openclaw/plugin-sdk/config-mutation"); + vi.resetModules(); +}); + function withConfig(fileTransfer: Record | undefined) { if (fileTransfer === undefined) { getRuntimeConfigMock.mockReturnValue({}); diff --git a/extensions/firecrawl/src/firecrawl-tools.test.ts b/extensions/firecrawl/src/firecrawl-tools.test.ts index 17b75e09088..2098c265693 100644 --- a/extensions/firecrawl/src/firecrawl-tools.test.ts +++ b/extensions/firecrawl/src/firecrawl-tools.test.ts @@ -1,6 +1,6 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import { mockPinnedHostnameResolution } from "openclaw/plugin-sdk/test-env"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_FIRECRAWL_BASE_URL, DEFAULT_FIRECRAWL_MAX_AGE_MS, @@ -65,6 +65,12 @@ describe("firecrawl tools", () => { ssrfMock?.mockRestore(); ssrfMock = undefined; global.fetch = priorFetch; + vi.unstubAllEnvs(); + }); + + afterAll(() => { + vi.doUnmock("./firecrawl-client.js"); + vi.resetModules(); }); it("exposes selection metadata and enables the plugin in config", () => { diff --git a/extensions/github-copilot/auth.test.ts b/extensions/github-copilot/auth.test.ts index 9e83b431b6f..4e57d49e1a7 100644 --- a/extensions/github-copilot/auth.test.ts +++ b/extensions/github-copilot/auth.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const ensureAuthProfileStoreMock = vi.hoisted(() => vi.fn()); const listProfilesForProviderMock = vi.hoisted(() => vi.fn()); @@ -17,6 +17,12 @@ vi.mock("openclaw/plugin-sdk/secret-input-runtime", () => ({ import { resolveFirstGithubToken } from "./auth.js"; +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/provider-auth"); + vi.doUnmock("openclaw/plugin-sdk/secret-input-runtime"); + vi.resetModules(); +}); + describe("resolveFirstGithubToken", () => { beforeEach(() => { ensureAuthProfileStoreMock.mockReturnValue({ diff --git a/extensions/github-copilot/embeddings.test.ts b/extensions/github-copilot/embeddings.test.ts index dfe104fa5dc..b8f3f7beb85 100644 --- a/extensions/github-copilot/embeddings.test.ts +++ b/extensions/github-copilot/embeddings.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const resolveFirstGithubTokenMock = vi.hoisted(() => vi.fn()); const resolveCopilotApiTokenMock = vi.hoisted(() => vi.fn()); @@ -24,6 +24,14 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({ import { githubCopilotMemoryEmbeddingProviderAdapter } from "./embeddings.js"; +afterAll(() => { + vi.doUnmock("./auth.js"); + vi.doUnmock("openclaw/plugin-sdk/secret-input-runtime"); + vi.doUnmock("./token.js"); + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.resetModules(); +}); + const TEST_BASE_URL = "https://api.githubcopilot.test"; function buildModelsResponse(models: Array<{ id: string; supported_endpoints?: unknown }>) { diff --git a/extensions/github-copilot/index.test.ts b/extensions/github-copilot/index.test.ts index a8033017b13..d59162b7fc1 100644 --- a/extensions/github-copilot/index.test.ts +++ b/extensions/github-copilot/index.test.ts @@ -6,7 +6,7 @@ import { ensureAuthProfileStore, } from "openclaw/plugin-sdk/agent-runtime"; import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ githubCopilotLoginCommand: vi.fn(), @@ -26,10 +26,16 @@ const tempDirs: string[] = []; afterEach(async () => { vi.clearAllMocks(); + vi.unstubAllGlobals(); clearRuntimeAuthProfileStoreSnapshots(); await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); }); +afterAll(() => { + vi.doUnmock("./register.runtime.js"); + vi.resetModules(); +}); + async function createAgentDir() { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-github-copilot-test-")); tempDirs.push(dir); diff --git a/extensions/github-copilot/models.test.ts b/extensions/github-copilot/models.test.ts index e503bd84910..9154ad38636 100644 --- a/extensions/github-copilot/models.test.ts +++ b/extensions/github-copilot/models.test.ts @@ -1,5 +1,5 @@ import { createProviderUsageFetch, makeResponse } from "openclaw/plugin-sdk/test-env"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { buildCopilotModelDefinition, getDefaultCopilotModelIds } from "./models-defaults.js"; import { fetchCopilotUsage } from "./usage.js"; @@ -40,6 +40,14 @@ vi.mock("openclaw/plugin-sdk/state-paths", () => ({ import type { ProviderResolveDynamicModelContext } from "openclaw/plugin-sdk/core"; import { resolveCopilotForwardCompatModel } from "./models.js"; +afterAll(() => { + vi.doUnmock("@mariozechner/pi-ai/oauth"); + vi.doUnmock("openclaw/plugin-sdk/provider-model-shared"); + vi.doUnmock("openclaw/plugin-sdk/json-store"); + vi.doUnmock("openclaw/plugin-sdk/state-paths"); + vi.resetModules(); +}); + let deriveCopilotApiBaseUrlFromToken: typeof import("./token.js").deriveCopilotApiBaseUrlFromToken; let resolveCopilotApiToken: typeof import("./token.js").resolveCopilotApiToken; diff --git a/extensions/google-meet/index.create.test.ts b/extensions/google-meet/index.create.test.ts index a9fa1cc6943..913771fcb91 100644 --- a/extensions/google-meet/index.create.test.ts +++ b/extensions/google-meet/index.create.test.ts @@ -1,5 +1,5 @@ import { Command } from "commander"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import plugin, { __testing as googleMeetPluginTesting } from "./index.js"; import { registerGoogleMeetCli } from "./src/cli.js"; import { resolveGoogleMeetConfig } from "./src/config.js"; @@ -110,6 +110,12 @@ describe("google-meet create flow", () => { googleMeetPluginTesting.setPlatformForTests(); }); + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.doUnmock("./src/voice-call-gateway.js"); + vi.resetModules(); + }); + it("CLI create can configure API-created space access", async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL, _init?: RequestInit) => { const url = input instanceof Request ? input.url : input.toString(); diff --git a/extensions/google-meet/index.test.ts b/extensions/google-meet/index.test.ts index def5a1a7da9..acbe87efa8e 100644 --- a/extensions/google-meet/index.test.ts +++ b/extensions/google-meet/index.test.ts @@ -7,7 +7,7 @@ import { createContext, Script } from "node:vm"; import { validateJsonSchemaValue, type JsonSchemaObject } from "openclaw/plugin-sdk/config-schema"; import type { RealtimeTranscriptionProviderPlugin } from "openclaw/plugin-sdk/realtime-transcription"; import type { RealtimeVoiceProviderPlugin } from "openclaw/plugin-sdk/realtime-voice"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import plugin, { __testing as googleMeetPluginTesting } from "./index.js"; import { extractGoogleMeetUriFromCalendarEvent, @@ -345,11 +345,18 @@ describe("google-meet plugin", () => { afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); + vi.unstubAllEnvs(); chromeTransportTesting.setDepsForTest(null); googleMeetPluginTesting.setCallGatewayFromCliForTests(); googleMeetPluginTesting.setPlatformForTests(); }); + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.doUnmock("./src/voice-call-gateway.js"); + vi.resetModules(); + }); + it("defaults to chrome agent mode with safe read-only tools", () => { expect(resolveGoogleMeetConfig({})).toMatchObject({ enabled: true, diff --git a/extensions/google-meet/node-host.test.ts b/extensions/google-meet/node-host.test.ts index 6bf66e40a6a..97f684b2482 100644 --- a/extensions/google-meet/node-host.test.ts +++ b/extensions/google-meet/node-host.test.ts @@ -1,6 +1,6 @@ import { spawnSync } from "node:child_process"; import { EventEmitter } from "node:events"; -import { describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; type MockChild = EventEmitter & { exitCode: number | null; @@ -41,6 +41,16 @@ vi.mock("node:child_process", async (importOriginal) => { }); describe("google-meet node host bridge sessions", () => { + afterEach(() => { + vi.useRealTimers(); + children.length = 0; + }); + + afterAll(() => { + vi.doUnmock("node:child_process"); + vi.resetModules(); + }); + it("starts observe-only Chrome without BlackHole or bridge processes", async () => { const { handleGoogleMeetNodeHostCommand } = await import("./src/node-host.js"); const originalPlatform = process.platform; diff --git a/extensions/google-meet/src/cli.test.ts b/extensions/google-meet/src/cli.test.ts index 2c1dae2738a..fc85b7d3bc5 100644 --- a/extensions/google-meet/src/cli.test.ts +++ b/extensions/google-meet/src/cli.test.ts @@ -2,7 +2,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { Command } from "commander"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { registerGoogleMeetCli } from "./cli.js"; import { resolveGoogleMeetConfig } from "./config.js"; import type { GoogleMeetRuntime } from "./runtime.js"; @@ -216,6 +216,11 @@ describe("google-meet CLI", () => { vi.unstubAllGlobals(); }); + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.resetModules(); + }); + it("prints setup checks as text and JSON", async () => { { const stdout = captureStdout(); diff --git a/extensions/google-meet/src/voice-call-gateway.test.ts b/extensions/google-meet/src/voice-call-gateway.test.ts index 7953babc5cc..065b6a032bb 100644 --- a/extensions/google-meet/src/voice-call-gateway.test.ts +++ b/extensions/google-meet/src/voice-call-gateway.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi, beforeEach } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { resolveGoogleMeetConfig } from "./config.js"; import { endMeetVoiceCallGatewayCall, @@ -32,6 +32,15 @@ describe("Google Meet voice-call gateway", () => { gatewayMocks.startGatewayClientWhenEventLoopReady.mockClear(); }); + afterEach(() => { + vi.useRealTimers(); + }); + + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/gateway-runtime"); + vi.resetModules(); + }); + it("starts Twilio Meet calls with pre-connect DTMF, then speaks the intro without TwiML fallback", async () => { const config = resolveGoogleMeetConfig({ voiceCall: { diff --git a/extensions/google/google.live.test.ts b/extensions/google/google.live.test.ts index 23805291cb4..6bf3c7f40c7 100644 --- a/extensions/google/google.live.test.ts +++ b/extensions/google/google.live.test.ts @@ -24,8 +24,16 @@ async function withGoogleApiEnvUnset(fn: () => Promise): Promise { try { return await fn(); } finally { - process.env.GEMINI_API_KEY = geminiApiKey; - process.env.GOOGLE_API_KEY = googleApiKey; + if (geminiApiKey === undefined) { + delete process.env.GEMINI_API_KEY; + } else { + process.env.GEMINI_API_KEY = geminiApiKey; + } + if (googleApiKey === undefined) { + delete process.env.GOOGLE_API_KEY; + } else { + process.env.GOOGLE_API_KEY = googleApiKey; + } } } diff --git a/extensions/google/image-generation-provider.test.ts b/extensions/google/image-generation-provider.test.ts index 7add6f21c66..cec51bf8735 100644 --- a/extensions/google/image-generation-provider.test.ts +++ b/extensions/google/image-generation-provider.test.ts @@ -46,6 +46,7 @@ function installGoogleFetchMock(params?: { describe("Google image-generation provider", () => { afterEach(() => { vi.restoreAllMocks(); + vi.unstubAllGlobals(); }); it("generates image buffers from the Gemini generateContent API", async () => { diff --git a/extensions/google/music-generation-provider.test.ts b/extensions/google/music-generation-provider.test.ts index 6d9282d808d..451ad3062f3 100644 --- a/extensions/google/music-generation-provider.test.ts +++ b/extensions/google/music-generation-provider.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; const { createGoogleGenAIMock, generateContentMock } = vi.hoisted(() => { const generateContentMock = vi.fn(); @@ -27,6 +27,11 @@ describe("google music generation provider", () => { createGoogleGenAIMock.mockClear(); }); + afterAll(() => { + vi.doUnmock("./google-genai-runtime.js"); + vi.resetModules(); + }); + it("declares explicit mode capabilities", () => { expectExplicitMusicGenerationCapabilities(buildGoogleMusicGenerationProvider()); }); diff --git a/extensions/google/oauth.test.ts b/extensions/google/oauth.test.ts index 0b2b6a81907..db18d493126 100644 --- a/extensions/google/oauth.test.ts +++ b/extensions/google/oauth.test.ts @@ -1,5 +1,5 @@ import { join, parse } from "node:path"; -import { describe, expect, it, vi, beforeAll, beforeEach, afterEach } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("openclaw/plugin-sdk/runtime-env", async () => { const actual = await vi.importActual( @@ -33,6 +33,12 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", async () => { }; }); +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/runtime-env"); + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.resetModules(); +}); + const mockExistsSync = vi.fn(); const mockReadFileSync = vi.fn(); const mockRealpathSync = vi.fn(); diff --git a/extensions/google/realtime-voice-provider.test.ts b/extensions/google/realtime-voice-provider.test.ts index 4c32f423fc8..876fda974e9 100644 --- a/extensions/google/realtime-voice-provider.test.ts +++ b/extensions/google/realtime-voice-provider.test.ts @@ -1,5 +1,5 @@ import { REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ } from "openclaw/plugin-sdk/realtime-voice"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { buildGoogleRealtimeVoiceProvider } from "./realtime-voice-provider.js"; type MockGoogleLiveSession = { @@ -45,6 +45,10 @@ vi.mock("./google-genai-runtime.js", () => ({ })), })); +const ENV_KEYS = ["GEMINI_API_KEY", "GOOGLE_API_KEY"] as const; + +let envSnapshot: Partial>; + function lastConnectParams(): MockGoogleLiveConnectParams { const params = connectMock.mock.calls.at(-1)?.[0]; if (!params) { @@ -55,6 +59,7 @@ function lastConnectParams(): MockGoogleLiveConnectParams { describe("buildGoogleRealtimeVoiceProvider", () => { beforeEach(() => { + envSnapshot = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]])); connectMock.mockClear(); createTokenMock.mockClear(); session.close.mockClear(); @@ -65,6 +70,23 @@ describe("buildGoogleRealtimeVoiceProvider", () => { delete process.env.GOOGLE_API_KEY; }); + afterEach(() => { + vi.useRealTimers(); + for (const key of ENV_KEYS) { + const value = envSnapshot[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + }); + + afterAll(() => { + vi.doUnmock("./google-genai-runtime.js"); + vi.resetModules(); + }); + it("declares realtime Talk capabilities for catalog selection", () => { const provider = buildGoogleRealtimeVoiceProvider(); diff --git a/extensions/google/speech-provider.test.ts b/extensions/google/speech-provider.test.ts index f1da219f99d..36df5b7566b 100644 --- a/extensions/google/speech-provider.test.ts +++ b/extensions/google/speech-provider.test.ts @@ -2,7 +2,7 @@ import { getProviderHttpMocks, installProviderHttpMockCleanup, } from "openclaw/plugin-sdk/provider-http-test-mocks"; -import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; const transcodeAudioBufferToOpusMock = vi.hoisted(() => vi.fn()); @@ -62,6 +62,11 @@ describe("Google speech provider", () => { transcodeAudioBufferToOpusMock.mockReset(); }); + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/media-runtime"); + vi.resetModules(); + }); + it("synthesizes Gemini PCM as WAV and preserves audio tags in the request text", async () => { const requestMock = installGoogleTtsRequestMock(); const provider = buildGoogleSpeechProvider(); diff --git a/extensions/google/transport-stream.test.ts b/extensions/google/transport-stream.test.ts index 2c0d930ea99..09e1b0cf0c1 100644 --- a/extensions/google/transport-stream.test.ts +++ b/extensions/google/transport-stream.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { Model } from "@mariozechner/pi-ai"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const { buildGuardedModelFetchMock, guardedFetchMock } = vi.hoisted(() => ({ buildGuardedModelFetchMock: vi.fn(), @@ -107,6 +107,11 @@ describe("google transport stream", () => { vi.unstubAllEnvs(); }); + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/provider-transport-runtime"); + vi.resetModules(); + }); + it("uses the guarded fetch transport and parses Gemini SSE output", async () => { guardedFetchMock.mockResolvedValueOnce( buildSseResponse([ diff --git a/extensions/google/video-generation-provider.test.ts b/extensions/google/video-generation-provider.test.ts index 9ea1489297c..4549351ce4f 100644 --- a/extensions/google/video-generation-provider.test.ts +++ b/extensions/google/video-generation-provider.test.ts @@ -1,6 +1,6 @@ import { writeFile } from "node:fs/promises"; import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; const { createGoogleGenAIMock, downloadMock, generateVideosMock, getVideosOperationMock } = vi.hoisted(() => { @@ -41,6 +41,11 @@ describe("google video generation provider", () => { createGoogleGenAIMock.mockClear(); }); + afterAll(() => { + vi.doUnmock("./google-genai-runtime.js"); + vi.resetModules(); + }); + it("declares explicit mode capabilities", () => { const provider = buildGoogleVideoGenerationProvider(); expectExplicitVideoGenerationCapabilities(provider); diff --git a/extensions/google/web-search-provider.test.ts b/extensions/google/web-search-provider.test.ts index a80034911bc..5cb04b4ec64 100644 --- a/extensions/google/web-search-provider.test.ts +++ b/extensions/google/web-search-provider.test.ts @@ -24,7 +24,7 @@ function installGeminiFetch() { }), } as Response), ); - global.fetch = withFetchPreconnect(mockFetch); + vi.stubGlobal("fetch", withFetchPreconnect(mockFetch)); return mockFetch; } @@ -69,6 +69,7 @@ function parseGeminiFetchBody(mockFetch: ReturnType): afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); + vi.unstubAllGlobals(); }); describe("google web search provider", () => { diff --git a/extensions/googlechat/src/actions.test.ts b/extensions/googlechat/src/actions.test.ts index a2e0eee1bea..42b1077a0df 100644 --- a/extensions/googlechat/src/actions.test.ts +++ b/extensions/googlechat/src/actions.test.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const listEnabledGoogleChatAccounts = vi.hoisted(() => vi.fn()); const resolveGoogleChatAccount = vi.hoisted(() => vi.fn()); @@ -43,6 +43,14 @@ describe("googlechat message actions", () => { vi.clearAllMocks(); }); + afterAll(() => { + vi.doUnmock("./accounts.js"); + vi.doUnmock("./api.js"); + vi.doUnmock("./runtime.js"); + vi.doUnmock("./targets.js"); + vi.resetModules(); + }); + it("describes send and reaction actions only when enabled accounts exist", async () => { listEnabledGoogleChatAccounts.mockReturnValueOnce([]); expect(googlechatMessageActions.describeMessageTool?.({ cfg: {} as never })).toBeNull(); diff --git a/extensions/googlechat/src/channel.test.ts b/extensions/googlechat/src/channel.test.ts index f97f8cb765b..77292ed9c40 100644 --- a/extensions/googlechat/src/channel.test.ts +++ b/extensions/googlechat/src/channel.test.ts @@ -3,7 +3,7 @@ import { createDirectoryTestRuntime, expectDirectorySurface, } from "openclaw/plugin-sdk/channel-test-helpers"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../runtime-api.js"; import { googlechatDirectoryAdapter, @@ -173,6 +173,12 @@ afterEach(() => { mockGoogleChatMediaLoaders(); }); +afterAll(() => { + vi.doUnmock("./channel.runtime.js"); + vi.doUnmock("./channel.deps.runtime.js"); + vi.resetModules(); +}); + function createGoogleChatCfg(): OpenClawConfig { return { channels: { diff --git a/extensions/googlechat/src/google-auth.runtime.test.ts b/extensions/googlechat/src/google-auth.runtime.test.ts index f7ef267292b..4c8f12c76b4 100644 --- a/extensions/googlechat/src/google-auth.runtime.test.ts +++ b/extensions/googlechat/src/google-auth.runtime.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ buildHostnameAllowlistPolicyFromSuffixAllowlist: vi.fn((hosts: string[]) => ({ @@ -62,6 +62,13 @@ beforeEach(() => { afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); + vi.unstubAllEnvs(); +}); + +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.doUnmock("gaxios"); + vi.resetModules(); }); describe("googlechat google auth runtime", () => { diff --git a/extensions/googlechat/src/monitor-access.test.ts b/extensions/googlechat/src/monitor-access.test.ts index a3902694f03..7642e4a5df0 100644 --- a/extensions/googlechat/src/monitor-access.test.ts +++ b/extensions/googlechat/src/monitor-access.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; const createChannelPairingController = vi.hoisted(() => vi.fn()); const evaluateGroupRouteAccessForPolicy = vi.hoisted(() => vi.fn()); @@ -118,6 +118,13 @@ describe("googlechat inbound access policy", () => { ({ applyGoogleChatInboundAccessPolicy } = await import("./monitor-access.js")); }); + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/channel-inbound"); + vi.doUnmock("../runtime-api.js"); + vi.doUnmock("./api.js"); + vi.resetModules(); + }); + it("issues a pairing challenge for unauthorized DMs in pairing mode", async () => { primeCommonDefaults(); const issueChallenge = vi.fn(async ({ onCreated, sendPairingReply }) => { diff --git a/extensions/googlechat/src/monitor-webhook.test.ts b/extensions/googlechat/src/monitor-webhook.test.ts index 77ddf41e5fe..5331e307655 100644 --- a/extensions/googlechat/src/monitor-webhook.test.ts +++ b/extensions/googlechat/src/monitor-webhook.test.ts @@ -1,5 +1,5 @@ import type { IncomingMessage, ServerResponse } from "node:http"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { WebhookTarget } from "./monitor-types.js"; import type { GoogleChatEvent } from "./types.js"; @@ -102,6 +102,13 @@ describe("googlechat monitor webhook", () => { vi.clearAllMocks(); }); + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/webhook-request-guards"); + vi.doUnmock("openclaw/plugin-sdk/webhook-targets"); + vi.doUnmock("./auth.js"); + vi.resetModules(); + }); + it("accepts add-on payloads that carry systemIdToken in the body", async () => { installSimplePipeline([ { diff --git a/extensions/googlechat/src/monitor.reply-delivery.test.ts b/extensions/googlechat/src/monitor.reply-delivery.test.ts index 49a38e73f1c..22ec9262b6f 100644 --- a/extensions/googlechat/src/monitor.reply-delivery.test.ts +++ b/extensions/googlechat/src/monitor.reply-delivery.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../runtime-api.js"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; import type { GoogleChatCoreRuntime, GoogleChatRuntimeEnv } from "./monitor-types.js"; @@ -57,6 +57,11 @@ beforeEach(async () => { ({ deliverGoogleChatReply } = await import("./monitor-reply-delivery.js")); }); +afterAll(() => { + vi.doUnmock("./api.js"); + vi.resetModules(); +}); + describe("Google Chat reply delivery", () => { it("resends the first text chunk as a new message when typing update fails", async () => { const core = createCore({ chunks: ["first chunk", "second chunk"] }); diff --git a/extensions/googlechat/src/monitor.webhook-routing.test.ts b/extensions/googlechat/src/monitor.webhook-routing.test.ts index 5f284c9120a..bed4a686d89 100644 --- a/extensions/googlechat/src/monitor.webhook-routing.test.ts +++ b/extensions/googlechat/src/monitor.webhook-routing.test.ts @@ -5,7 +5,7 @@ import { setActivePluginRegistry, } from "openclaw/plugin-sdk/plugin-test-runtime"; import { createMockServerResponse } from "openclaw/plugin-sdk/test-env"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; import { verifyGoogleChatRequest } from "./auth.js"; @@ -159,6 +159,11 @@ describe("Google Chat webhook routing", () => { setActivePluginRegistry(createEmptyPluginRegistry()); }); + afterAll(() => { + vi.doUnmock("./auth.js"); + vi.resetModules(); + }); + it("rejects ambiguous routing when multiple targets on the same path verify successfully", async () => { vi.mocked(verifyGoogleChatRequest).mockResolvedValue({ ok: true }); diff --git a/extensions/googlechat/src/setup.test.ts b/extensions/googlechat/src/setup.test.ts index 295eba73c71..bcadced27df 100644 --- a/extensions/googlechat/src/setup.test.ts +++ b/extensions/googlechat/src/setup.test.ts @@ -11,7 +11,7 @@ import { } from "openclaw/plugin-sdk/plugin-test-runtime"; import type { WizardPrompter } from "openclaw/plugin-sdk/plugin-test-runtime"; import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../runtime-api.js"; import { listGoogleChatAccountIds, @@ -84,6 +84,11 @@ describe("googlechat setup", () => { vi.unstubAllEnvs(); }); + afterAll(() => { + vi.doUnmock("./channel.runtime.js"); + vi.resetModules(); + }); + it("rejects env auth for non-default accounts", () => { if (!googlechatSetupAdapter.validateInput) { throw new Error("Expected googlechatSetupAdapter.validateInput to be defined"); diff --git a/extensions/googlechat/src/targets.test.ts b/extensions/googlechat/src/targets.test.ts index d0fd10a432e..5e86489fea5 100644 --- a/extensions/googlechat/src/targets.test.ts +++ b/extensions/googlechat/src/targets.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; import { downloadGoogleChatMedia, sendGoogleChatMessage } from "./api.js"; import { resolveGoogleChatGroupRequireMention } from "./group-policy.js"; @@ -80,6 +80,14 @@ vi.mock("./auth.js", async () => { const authActual = await vi.importActual("./auth.js"); const { __testing: authTesting, getGoogleChatAccessToken, verifyGoogleChatRequest } = authActual; +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.doUnmock("gaxios"); + vi.doUnmock("google-auth-library"); + vi.doUnmock("./auth.js"); + vi.resetModules(); +}); + const account = { accountId: "default", enabled: true, diff --git a/extensions/huggingface/index.test.ts b/extensions/huggingface/index.test.ts index 0ba5f22caf8..c6c89a8ad2d 100644 --- a/extensions/huggingface/index.test.ts +++ b/extensions/huggingface/index.test.ts @@ -1,5 +1,5 @@ import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; -import { describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; const buildHuggingfaceProviderMock = vi.hoisted(() => vi.fn(async () => ({ @@ -44,6 +44,12 @@ function registerProviderWithPluginConfig(pluginConfig: Record) } describe("huggingface plugin", () => { + afterAll(() => { + vi.doUnmock("./provider-catalog.js"); + vi.doUnmock("./onboard.js"); + vi.resetModules(); + }); + it("skips catalog discovery when plugin discovery is disabled", async () => { const provider = registerProvider(); diff --git a/extensions/huggingface/models.test.ts b/extensions/huggingface/models.test.ts index ed982d519e3..d16ef0ce85a 100644 --- a/extensions/huggingface/models.test.ts +++ b/extensions/huggingface/models.test.ts @@ -10,9 +10,17 @@ import { HUGGINGFACE_DISCOVERY_TIMEOUT_MS } from "./models.js"; const ORIGINAL_VITEST = process.env.VITEST; const ORIGINAL_NODE_ENV = process.env.NODE_ENV; +function restoreEnv(key: "VITEST" | "NODE_ENV", value: string | undefined) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } +} + afterEach(() => { - process.env.VITEST = ORIGINAL_VITEST; - process.env.NODE_ENV = ORIGINAL_NODE_ENV; + restoreEnv("VITEST", ORIGINAL_VITEST); + restoreEnv("NODE_ENV", ORIGINAL_NODE_ENV); vi.restoreAllMocks(); vi.unstubAllGlobals(); }); diff --git a/extensions/image-generation-core/src/runtime.test.ts b/extensions/image-generation-core/src/runtime.test.ts index 1d748980628..9fcc56e8852 100644 --- a/extensions/image-generation-core/src/runtime.test.ts +++ b/extensions/image-generation-core/src/runtime.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; const sdkExports = vi.hoisted(() => ({ generateImage: vi.fn(), @@ -14,6 +14,11 @@ import { import { generateImage, listRuntimeImageGenerationProviders } from "./runtime.js"; describe("image-generation-core runtime", () => { + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/image-generation-runtime"); + vi.resetModules(); + }); + it("re-exports generateImage from the plugin sdk runtime", () => { expect(generateImage).toBe(sdkGenerateImage); }); diff --git a/extensions/imessage/src/monitor.watch-subscribe-retry.test.ts b/extensions/imessage/src/monitor.watch-subscribe-retry.test.ts index 197819e19f5..f1a9bb75c7c 100644 --- a/extensions/imessage/src/monitor.watch-subscribe-retry.test.ts +++ b/extensions/imessage/src/monitor.watch-subscribe-retry.test.ts @@ -1,5 +1,5 @@ import type { waitForTransportReady } from "openclaw/plugin-sdk/transport-ready-runtime"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { createIMessageRpcClient, IMessageRpcClient } from "./client.js"; import { monitorIMessageProvider } from "./monitor.js"; import type { attachIMessageMonitorAbortHandler } from "./monitor/abort-handler.js"; @@ -71,6 +71,13 @@ describe("monitorIMessageProvider watch.subscribe startup retry", () => { vi.useRealTimers(); }); + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/transport-ready-runtime"); + vi.doUnmock("./client.js"); + vi.doUnmock("./monitor/abort-handler.js"); + vi.resetModules(); + }); + it("retries a transient watch.subscribe startup timeout without tearing down the monitor", async () => { const runtime = createRuntime(); const firstClient = createRpcClient({ diff --git a/extensions/imessage/src/monitor/deliver.test.ts b/extensions/imessage/src/monitor/deliver.test.ts index 611604338ba..456f78472f3 100644 --- a/extensions/imessage/src/monitor/deliver.test.ts +++ b/extensions/imessage/src/monitor/deliver.test.ts @@ -1,5 +1,5 @@ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const sendMessageIMessageMock = vi.hoisted(() => vi.fn().mockImplementation(async (_to: string, message: string) => ({ @@ -41,6 +41,12 @@ describe("deliverReplies", () => { chunkTextWithModeMock.mockImplementation((text: string) => [text]); }); + afterAll(() => { + vi.doUnmock("../send.js"); + vi.doUnmock("./deliver.runtime.js"); + vi.resetModules(); + }); + it("propagates payload replyToId through all text chunks", async () => { chunkTextWithModeMock.mockImplementation((text: string) => text.split("|")); diff --git a/extensions/imessage/src/monitor/self-chat-cache.test.ts b/extensions/imessage/src/monitor/self-chat-cache.test.ts index cf3a245ba30..3c0c3810e7e 100644 --- a/extensions/imessage/src/monitor/self-chat-cache.test.ts +++ b/extensions/imessage/src/monitor/self-chat-cache.test.ts @@ -1,7 +1,11 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { createSelfChatCache } from "./self-chat-cache.js"; describe("createSelfChatCache", () => { + afterEach(() => { + vi.useRealTimers(); + }); + const directLookup = { accountId: "default", sender: "+15555550123", diff --git a/extensions/imessage/src/status.test.ts b/extensions/imessage/src/status.test.ts index a7147e8cbd0..cbfa9615ef5 100644 --- a/extensions/imessage/src/status.test.ts +++ b/extensions/imessage/src/status.test.ts @@ -1,7 +1,7 @@ import { createPluginSetupWizardStatus } from "openclaw/plugin-sdk/plugin-test-runtime"; import * as processRuntime from "openclaw/plugin-sdk/process-runtime"; import * as setupRuntime from "openclaw/plugin-sdk/setup"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { resolveIMessageAccount } from "./accounts.js"; import * as channelRuntimeModule from "./channel.runtime.js"; import * as clientModule from "./client.js"; @@ -27,6 +27,16 @@ vi.mock("node:child_process", async () => { }; }); +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); +}); + +afterAll(() => { + vi.doUnmock("node:child_process"); + vi.resetModules(); +}); + describe("createIMessageRpcClient", () => { beforeEach(() => { spawnMock.mockClear(); diff --git a/extensions/inworld/speech-provider.test.ts b/extensions/inworld/speech-provider.test.ts index 5676a905d88..3fd8437fd65 100644 --- a/extensions/inworld/speech-provider.test.ts +++ b/extensions/inworld/speech-provider.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; const { inworldTTSMock, listInworldVoicesMock } = vi.hoisted(() => ({ inworldTTSMock: vi.fn(), @@ -16,18 +16,21 @@ vi.mock("./tts.js", async (importOriginal) => { import { buildInworldSpeechProvider } from "./speech-provider.js"; -describe("buildInworldSpeechProvider", () => { - const originalEnv = process.env.INWORLD_API_KEY; +afterAll(() => { + vi.doUnmock("./tts.js"); + vi.resetModules(); +}); +describe("buildInworldSpeechProvider", () => { afterEach(() => { - process.env.INWORLD_API_KEY = originalEnv; inworldTTSMock.mockReset(); listInworldVoicesMock.mockReset(); + vi.unstubAllEnvs(); vi.restoreAllMocks(); }); it("reports configured when INWORLD_API_KEY env var is set", () => { - process.env.INWORLD_API_KEY = "test-key"; + vi.stubEnv("INWORLD_API_KEY", "test-key"); const provider = buildInworldSpeechProvider(); expect( provider.isConfigured({ @@ -38,7 +41,7 @@ describe("buildInworldSpeechProvider", () => { }); it("reports configured when providerConfig apiKey is set", () => { - delete process.env.INWORLD_API_KEY; + vi.stubEnv("INWORLD_API_KEY", ""); const provider = buildInworldSpeechProvider(); expect( provider.isConfigured({ @@ -49,7 +52,7 @@ describe("buildInworldSpeechProvider", () => { }); it("reports not configured when no key is available", () => { - delete process.env.INWORLD_API_KEY; + vi.stubEnv("INWORLD_API_KEY", ""); const provider = buildInworldSpeechProvider(); expect( provider.isConfigured({ diff --git a/extensions/inworld/tts.test.ts b/extensions/inworld/tts.test.ts index f3fadeddfe7..18666ced963 100644 --- a/extensions/inworld/tts.test.ts +++ b/extensions/inworld/tts.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({ fetchWithSsrFGuardMock: vi.fn(), @@ -44,9 +44,14 @@ function readRequestBody(request: GuardRequest): string { return body; } +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.resetModules(); +}); + describe("listInworldVoices", () => { afterEach(() => { - fetchWithSsrFGuardMock.mockClear(); + fetchWithSsrFGuardMock.mockReset(); vi.restoreAllMocks(); }); @@ -157,7 +162,7 @@ describe("listInworldVoices", () => { describe("inworldTTS", () => { afterEach(() => { - fetchWithSsrFGuardMock.mockClear(); + fetchWithSsrFGuardMock.mockReset(); vi.restoreAllMocks(); }); diff --git a/extensions/irc/src/accounts.test.ts b/extensions/irc/src/accounts.test.ts index 676ead48181..8d018dcc013 100644 --- a/extensions/irc/src/accounts.test.ts +++ b/extensions/irc/src/accounts.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount } from "./accounts.js"; import type { CoreConfig } from "./types.js"; @@ -105,8 +105,7 @@ describe("resolveIrcAccount", () => { }); it("parses delimited IRC_CHANNELS env values for the default account", () => { - const previousChannels = process.env.IRC_CHANNELS; - process.env.IRC_CHANNELS = "alpha, beta\ngamma; delta"; + vi.stubEnv("IRC_CHANNELS", "alpha, beta\ngamma; delta"); try { const account = resolveIrcAccount({ @@ -122,11 +121,7 @@ describe("resolveIrcAccount", () => { expect(account.config.channels).toEqual(["alpha", "beta", "gamma", "delta"]); } finally { - if (previousChannels === undefined) { - delete process.env.IRC_CHANNELS; - } else { - process.env.IRC_CHANNELS = previousChannels; - } + vi.unstubAllEnvs(); } }); diff --git a/extensions/irc/src/inbound.behavior.test.ts b/extensions/irc/src/inbound.behavior.test.ts index 389dfde442b..aa6a82187a3 100644 --- a/extensions/irc/src/inbound.behavior.test.ts +++ b/extensions/irc/src/inbound.behavior.test.ts @@ -1,8 +1,8 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ResolvedIrcAccount } from "./accounts.js"; import { handleIrcInbound } from "./inbound.js"; import type { RuntimeEnv } from "./runtime-api.js"; -import { setIrcRuntime } from "./runtime.js"; +import { clearIrcRuntime, setIrcRuntime } from "./runtime.js"; import type { CoreConfig, IrcInboundMessage } from "./types.js"; const { @@ -81,11 +81,23 @@ function createMessage(overrides?: Partial): IrcInboundMessag }; } +function resetInboundMocks() { + buildMentionRegexesMock.mockReset().mockReturnValue([]); + hasControlCommandMock.mockReset().mockReturnValue(false); + matchesMentionPatternsMock.mockReset().mockReturnValue(false); + readAllowFromStoreMock.mockReset().mockResolvedValue([]); + shouldHandleTextCommandsMock.mockReset().mockReturnValue(false); + upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "CODE", created: true }); +} + describe("irc inbound behavior", () => { beforeEach(() => { - vi.clearAllMocks(); + resetInboundMocks(); installIrcRuntime(); - readAllowFromStoreMock.mockResolvedValue([]); + }); + + afterEach(() => { + clearIrcRuntime(); }); it("issues a DM pairing challenge and sends the reply to the sender nick", async () => { diff --git a/extensions/irc/src/probe.test.ts b/extensions/irc/src/probe.test.ts index 0aacc95aa6b..ba1fa6fff22 100644 --- a/extensions/irc/src/probe.test.ts +++ b/extensions/irc/src/probe.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { probeIrc } from "./probe.js"; const resolveIrcAccountMock = vi.hoisted(() => vi.fn()); @@ -17,6 +17,13 @@ vi.mock("./client.js", () => ({ connectIrcClient: connectIrcClientMock, })); +afterAll(() => { + vi.doUnmock("./accounts.js"); + vi.doUnmock("./connect-options.js"); + vi.doUnmock("./client.js"); + vi.resetModules(); +}); + describe("probeIrc", () => { beforeEach(() => { resolveIrcAccountMock.mockReset(); diff --git a/extensions/irc/src/send.test.ts b/extensions/irc/src/send.test.ts index e71d4fe0080..35002ae90ea 100644 --- a/extensions/irc/src/send.test.ts +++ b/extensions/irc/src/send.test.ts @@ -1,8 +1,8 @@ import { verifyChannelMessageAdapterCapabilityProofs } from "openclaw/plugin-sdk/channel-message"; import { createSendCfgThreadingRuntime } from "openclaw/plugin-sdk/channel-test-helpers"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { IrcClient } from "./client.js"; -import { setIrcRuntime } from "./runtime.js"; +import { clearIrcRuntime, setIrcRuntime } from "./runtime.js"; import type { CoreConfig } from "./types.js"; const hoisted = vi.hoisted(() => { @@ -66,12 +66,38 @@ vi.mock("openclaw/plugin-sdk/text-runtime", async () => { import { ircMessageAdapter } from "./message-adapter.js"; import { sendMessageIrc } from "./send.js"; +function resetHoistedMocks() { + hoisted.loadConfig.mockReset(); + hoisted.resolveMarkdownTableMode.mockReset().mockReturnValue("preserve"); + hoisted.convertMarkdownTables.mockReset().mockImplementation((text: string) => text); + hoisted.record.mockReset(); + hoisted.normalizeIrcMessagingTarget + .mockReset() + .mockImplementation((value: string) => value.trim()); + hoisted.connectIrcClient.mockReset(); + hoisted.buildIrcConnectOptions.mockReset().mockReturnValue({}); +} + +afterAll(() => { + vi.doUnmock("./normalize.js"); + vi.doUnmock("./client.js"); + vi.doUnmock("./connect-options.js"); + vi.doUnmock("./protocol.js"); + vi.doUnmock("openclaw/plugin-sdk/plugin-config-runtime"); + vi.doUnmock("openclaw/plugin-sdk/text-runtime"); + vi.resetModules(); +}); + describe("sendMessageIrc cfg threading", () => { beforeEach(() => { - vi.clearAllMocks(); + resetHoistedMocks(); setIrcRuntime(createSendCfgThreadingRuntime(hoisted) as never); }); + afterEach(() => { + clearIrcRuntime(); + }); + it("uses explicitly provided cfg without loading runtime config", async () => { const providedCfg = { channels: { diff --git a/extensions/irc/src/setup.test.ts b/extensions/irc/src/setup.test.ts index 2fa5784c93e..86d1a516637 100644 --- a/extensions/irc/src/setup.test.ts +++ b/extensions/irc/src/setup.test.ts @@ -11,7 +11,7 @@ import { runSetupWizardConfigure, } from "openclaw/plugin-sdk/plugin-test-runtime"; import type { WizardPrompter } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { listIrcAccountIds, resolveDefaultIrcAccountId, @@ -43,6 +43,11 @@ vi.mock("./channel-runtime.js", () => { }; }); +afterAll(() => { + vi.doUnmock("./channel-runtime.js"); + vi.resetModules(); +}); + const ircSetupPlugin = { id: "irc", meta: { diff --git a/extensions/kilocode/onboard.test.ts b/extensions/kilocode/onboard.test.ts index 951f25254bf..885c5ad811a 100644 --- a/extensions/kilocode/onboard.test.ts +++ b/extensions/kilocode/onboard.test.ts @@ -1,8 +1,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import { resolveEnvApiKey } from "openclaw/plugin-sdk/provider-auth-runtime"; import { resolveAgentModelPrimaryValue } from "openclaw/plugin-sdk/provider-onboard"; -import { captureEnv } from "openclaw/plugin-sdk/test-env"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { buildKilocodeModelDefinition, KILOCODE_DEFAULT_CONTEXT_WINDOW, @@ -142,8 +141,7 @@ describe("Kilo Gateway provider config", () => { describe("env var resolution", () => { it("resolves KILOCODE_API_KEY from env", () => { - const envSnapshot = captureEnv(["KILOCODE_API_KEY"]); - process.env.KILOCODE_API_KEY = "test-kilo-key"; + vi.stubEnv("KILOCODE_API_KEY", "test-kilo-key"); try { const result = resolveEnvApiKey("kilocode"); @@ -151,19 +149,18 @@ describe("Kilo Gateway provider config", () => { expect(result?.apiKey).toBe("test-kilo-key"); expect(result?.source).toContain("KILOCODE_API_KEY"); } finally { - envSnapshot.restore(); + vi.unstubAllEnvs(); } }); it("returns null when KILOCODE_API_KEY is not set", () => { - const envSnapshot = captureEnv(["KILOCODE_API_KEY"]); - delete process.env.KILOCODE_API_KEY; + vi.stubEnv("KILOCODE_API_KEY", ""); try { const result = resolveEnvApiKey("kilocode"); expect(result).toBeNull(); } finally { - envSnapshot.restore(); + vi.unstubAllEnvs(); } }); }); diff --git a/extensions/kilocode/provider-models.test.ts b/extensions/kilocode/provider-models.test.ts index 236e60c09d9..90910ddd989 100644 --- a/extensions/kilocode/provider-models.test.ts +++ b/extensions/kilocode/provider-models.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({ fetchWithSsrFGuardMock: vi.fn(), @@ -77,11 +77,9 @@ function makeAutoModel(overrides: Record = {}) { } async function withFetchPathTest(mockFetch: MockKilocodeFetch, runAssertions: () => Promise) { - const origNodeEnv = process.env.NODE_ENV; - const origVitest = process.env.VITEST; const release = vi.fn(async () => {}); - delete process.env.NODE_ENV; - delete process.env.VITEST; + vi.stubEnv("NODE_ENV", ""); + vi.stubEnv("VITEST", ""); fetchWithSsrFGuardMock.mockReset(); const callMockFetch = mockFetch as unknown as ( @@ -98,20 +96,16 @@ async function withFetchPathTest(mockFetch: MockKilocodeFetch, runAssertions: () try { await runAssertions(); } finally { - if (origNodeEnv === undefined) { - delete process.env.NODE_ENV; - } else { - process.env.NODE_ENV = origNodeEnv; - } - if (origVitest === undefined) { - delete process.env.VITEST; - } else { - process.env.VITEST = origVitest; - } + vi.unstubAllEnvs(); fetchWithSsrFGuardMock.mockReset(); } } +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.resetModules(); +}); + describe("discoverKilocodeModels", () => { it("returns static catalog in test environment", async () => { const models = await discoverKilocodeModels(); diff --git a/extensions/line/src/accounts.test.ts b/extensions/line/src/accounts.test.ts index 7b83486fc9a..696a1b98bfd 100644 --- a/extensions/line/src/accounts.test.ts +++ b/extensions/line/src/accounts.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { resolveLineAccount, resolveDefaultLineAccountId, @@ -11,7 +11,6 @@ import { } from "./accounts.js"; describe("LINE accounts", () => { - const originalEnv = { ...process.env }; const tempDirs: string[] = []; const createSecretFile = (fileName: string, contents: string) => { @@ -23,13 +22,12 @@ describe("LINE accounts", () => { }; beforeEach(() => { - process.env = { ...originalEnv }; - delete process.env.LINE_CHANNEL_ACCESS_TOKEN; - delete process.env.LINE_CHANNEL_SECRET; + vi.stubEnv("LINE_CHANNEL_ACCESS_TOKEN", ""); + vi.stubEnv("LINE_CHANNEL_SECRET", ""); }); afterEach(() => { - process.env = originalEnv; + vi.unstubAllEnvs(); for (const dir of tempDirs.splice(0)) { fs.rmSync(dir, { recursive: true, force: true }); } @@ -59,8 +57,8 @@ describe("LINE accounts", () => { }); it("resolves account from environment variables", () => { - process.env.LINE_CHANNEL_ACCESS_TOKEN = "env-token"; - process.env.LINE_CHANNEL_SECRET = "env-secret"; + vi.stubEnv("LINE_CHANNEL_ACCESS_TOKEN", "env-token"); + vi.stubEnv("LINE_CHANNEL_SECRET", "env-secret"); const cfg: OpenClawConfig = { channels: { diff --git a/extensions/line/src/bot-handlers.test.ts b/extensions/line/src/bot-handlers.test.ts index b14ce78d256..e821d792022 100644 --- a/extensions/line/src/bot-handlers.test.ts +++ b/extensions/line/src/bot-handlers.test.ts @@ -1,6 +1,6 @@ import type { webhook } from "@line/bot-sdk"; import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { LineAccountConfig } from "./types.js"; type MessageEvent = webhook.MessageEvent; @@ -373,6 +373,22 @@ describe("handleLineWebhookEvents", () => { await import("./bot-handlers.js")); }); + afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/channel-inbound"); + vi.doUnmock("openclaw/plugin-sdk/channel-pairing"); + vi.doUnmock("openclaw/plugin-sdk/command-auth"); + vi.doUnmock("openclaw/plugin-sdk/runtime-group-policy"); + vi.doUnmock("openclaw/plugin-sdk/runtime-env"); + vi.doUnmock("openclaw/plugin-sdk/group-access"); + vi.doUnmock("openclaw/plugin-sdk/reply-history"); + vi.doUnmock("openclaw/plugin-sdk/routing"); + vi.doUnmock("openclaw/plugin-sdk/conversation-runtime"); + vi.doUnmock("./download.js"); + vi.doUnmock("./send.js"); + vi.doUnmock("./bot-message-context.js"); + vi.resetModules(); + }); + beforeEach(() => { buildLineMessageContextMock.mockReset(); buildLineMessageContextMock.mockImplementation(async () => ({ diff --git a/extensions/line/src/download.test.ts b/extensions/line/src/download.test.ts index 93eb80c0632..0bad04dea8b 100644 --- a/extensions/line/src/download.test.ts +++ b/extensions/line/src/download.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const getMessageContentMock = vi.hoisted(() => vi.fn()); const saveMediaBufferMock = vi.hoisted(() => vi.fn()); @@ -44,6 +44,13 @@ describe("downloadLineMedia", () => { ({ downloadLineMedia } = await import("./download.js")); }); + afterAll(() => { + vi.doUnmock("@line/bot-sdk"); + vi.doUnmock("openclaw/plugin-sdk/runtime-env"); + vi.doUnmock("openclaw/plugin-sdk/media-store"); + vi.resetModules(); + }); + beforeEach(() => { vi.restoreAllMocks(); getMessageContentMock.mockReset(); diff --git a/extensions/line/src/monitor.lifecycle.test.ts b/extensions/line/src/monitor.lifecycle.test.ts index 8b80687611c..5e2ece16d65 100644 --- a/extensions/line/src/monitor.lifecycle.test.ts +++ b/extensions/line/src/monitor.lifecycle.test.ts @@ -5,7 +5,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { createMockIncomingRequest } from "openclaw/plugin-sdk/test-env"; import { WEBHOOK_IN_FLIGHT_DEFAULTS } from "openclaw/plugin-sdk/webhook-request-guards"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; type LineNodeWebhookHandler = (req: IncomingMessage, res: ServerResponse) => Promise; @@ -112,6 +112,21 @@ describe("monitorLineProvider lifecycle", () => { await import("./monitor.js")); }); + afterAll(() => { + vi.doUnmock("./bot.js"); + vi.doUnmock("openclaw/plugin-sdk/reply-runtime"); + vi.doUnmock("openclaw/plugin-sdk/runtime-env"); + vi.doUnmock("openclaw/plugin-sdk/channel-message"); + vi.doUnmock("openclaw/plugin-sdk/webhook-ingress"); + vi.doUnmock("./webhook-node.js"); + vi.doUnmock("./auto-reply-delivery.js"); + vi.doUnmock("./markdown-to-line.js"); + vi.doUnmock("./reply-chunks.js"); + vi.doUnmock("./send.js"); + vi.doUnmock("./template-messages.js"); + vi.resetModules(); + }); + beforeEach(() => { clearLineRuntimeStateForTests(); createLineBotMock.mockReset(); @@ -148,6 +163,10 @@ describe("monitorLineProvider lifecycle", () => { }); }); + afterEach(() => { + clearLineRuntimeStateForTests(); + }); + const createRouteResponse = () => { const resObj = { statusCode: 0, diff --git a/extensions/line/src/outbound-media.test.ts b/extensions/line/src/outbound-media.test.ts index ff86328d113..f79c62c08be 100644 --- a/extensions/line/src/outbound-media.test.ts +++ b/extensions/line/src/outbound-media.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; const ssrfMocks = vi.hoisted(() => ({ resolvePinnedHostnameWithPolicy: vi.fn(), @@ -8,6 +8,11 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({ resolvePinnedHostnameWithPolicy: ssrfMocks.resolvePinnedHostnameWithPolicy, })); +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.resetModules(); +}); + import { detectLineMediaKind, resolveLineOutboundMedia, diff --git a/extensions/line/src/rich-menu.test.ts b/extensions/line/src/rich-menu.test.ts index 9644af0cae3..0a981b1125e 100644 --- a/extensions/line/src/rich-menu.test.ts +++ b/extensions/line/src/rich-menu.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createDefaultMenuConfig, createGridLayout, @@ -25,6 +25,11 @@ vi.mock("@line/bot-sdk", () => ({ messagingApi: { MessagingApiBlobClient: MessagingApiBlobClientMock }, })); +afterAll(() => { + vi.doUnmock("@line/bot-sdk"); + vi.resetModules(); +}); + describe("messageAction", () => { it("creates message actions with explicit or default text", () => { const cases = [ diff --git a/extensions/line/src/send.test.ts b/extensions/line/src/send.test.ts index 2454235792f..587e9998756 100644 --- a/extensions/line/src/send.test.ts +++ b/extensions/line/src/send.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const { pushMessageMock, @@ -97,6 +97,17 @@ describe("LINE send helpers", () => { sendModule = await import("./send.js"); }); + afterAll(() => { + vi.doUnmock("@line/bot-sdk"); + vi.doUnmock("openclaw/plugin-sdk/plugin-config-runtime"); + vi.doUnmock("./accounts.js"); + vi.doUnmock("./channel-access-token.js"); + vi.doUnmock("openclaw/plugin-sdk/channel-activity-runtime"); + vi.doUnmock("openclaw/plugin-sdk/runtime-env"); + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.resetModules(); + }); + beforeEach(() => { pushMessageMock.mockReset(); replyMessageMock.mockReset(); diff --git a/extensions/line/src/setup-surface.test.ts b/extensions/line/src/setup-surface.test.ts index 953f6f8813b..490500701de 100644 --- a/extensions/line/src/setup-surface.test.ts +++ b/extensions/line/src/setup-surface.test.ts @@ -9,7 +9,7 @@ import { import type { WizardPrompter } from "openclaw/plugin-sdk/plugin-test-runtime"; import { bundledPluginRoot } from "openclaw/plugin-sdk/test-fixtures"; import ts from "typescript"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig, PluginRuntime, ResolvedLineAccount } from "../api.js"; import { linePlugin } from "./channel.js"; import { lineGatewayAdapter } from "./gateway.js"; @@ -30,6 +30,11 @@ vi.mock("@line/bot-sdk", () => ({ messagingApi: { MessagingApiClient: MessagingApiClientMock }, })); +afterAll(() => { + vi.doUnmock("@line/bot-sdk"); + vi.resetModules(); +}); + const lineConfigure = createPluginSetupWizardConfigure(linePlugin); const LINE_SRC_PREFIX = `../../${bundledPluginRoot("line")}/src/`; diff --git a/extensions/litellm/image-generation-provider.test.ts b/extensions/litellm/image-generation-provider.test.ts index 8ebb3ff18fe..6d6e0f0040e 100644 --- a/extensions/litellm/image-generation-provider.test.ts +++ b/extensions/litellm/image-generation-provider.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { buildLitellmImageGenerationProvider } from "./image-generation-provider.js"; const { @@ -42,6 +42,12 @@ vi.mock("openclaw/plugin-sdk/provider-http", () => ({ sanitizeConfiguredModelProviderRequest: sanitizeConfiguredModelProviderRequestMock, })); +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/provider-auth-runtime"); + vi.doUnmock("openclaw/plugin-sdk/provider-http"); + vi.resetModules(); +}); + function mockGeneratedPngResponse() { postJsonRequestMock.mockResolvedValue({ response: { diff --git a/extensions/llm-task/src/llm-task-tool.test.ts b/extensions/llm-task/src/llm-task-tool.test.ts index 465040aa344..644fdfbea49 100644 --- a/extensions/llm-task/src/llm-task-tool.test.ts +++ b/extensions/llm-task/src/llm-task-tool.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("typebox", () => ({ Type: { @@ -44,6 +44,13 @@ vi.mock("../api.js", async () => { }; }); +afterAll(() => { + vi.doUnmock("typebox"); + vi.doUnmock("ajv"); + vi.doUnmock("../api.js"); + vi.resetModules(); +}); + import { createLlmTaskTool } from "./llm-task-tool.js"; const runEmbeddedPiAgent = vi.fn(async () => ({ @@ -106,6 +113,16 @@ function mockEmbeddedRunJson(payload: unknown) { }); } +function resetRunnerMocks() { + runEmbeddedPiAgent.mockReset(); + runEmbeddedPiAgent.mockImplementation(async () => ({ + meta: { startedAt: Date.now() }, + payloads: [{ text: "{}" }], + })); + resolveThinkingPolicy.mockClear(); + normalizeThinkingLevel.mockClear(); +} + async function executeEmbeddedRun(input: Record) { const tool = createLlmTaskTool(fakeApi()); await tool.execute("id", input); @@ -113,7 +130,9 @@ async function executeEmbeddedRun(input: Record) { } describe("llm-task tool (json-only)", () => { - beforeEach(() => vi.clearAllMocks()); + beforeEach(() => { + resetRunnerMocks(); + }); it("returns parsed json", async () => { (runEmbeddedPiAgent as any).mockResolvedValueOnce({ diff --git a/extensions/lmstudio/src/models.test.ts b/extensions/lmstudio/src/models.test.ts index b3d8515bd7b..283ffc57784 100644 --- a/extensions/lmstudio/src/models.test.ts +++ b/extensions/lmstudio/src/models.test.ts @@ -2,7 +2,7 @@ import { SELF_HOSTED_DEFAULT_CONTEXT_WINDOW, SELF_HOSTED_DEFAULT_MAX_TOKENS, } from "openclaw/plugin-sdk/provider-setup"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { LMSTUDIO_DEFAULT_LOAD_CONTEXT_LENGTH } from "./defaults.js"; import { discoverLmstudioModels, ensureLmstudioModelLoaded } from "./models.fetch.js"; import { @@ -23,6 +23,11 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => { }; }); +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); + vi.resetModules(); +}); + describe("lmstudio-models", () => { const asFetch = (mock: unknown) => mock as typeof fetch; const parseJsonRequestBody = (init: RequestInit | undefined): unknown => { diff --git a/extensions/lmstudio/src/runtime.test.ts b/extensions/lmstudio/src/runtime.test.ts index a48c962c9fa..e2686102eef 100644 --- a/extensions/lmstudio/src/runtime.test.ts +++ b/extensions/lmstudio/src/runtime.test.ts @@ -1,6 +1,6 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-auth"; import { CUSTOM_LOCAL_AUTH_MARKER } from "openclaw/plugin-sdk/provider-auth"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER } from "./defaults.js"; import { buildLmstudioAuthHeaders, @@ -19,6 +19,11 @@ vi.mock("openclaw/plugin-sdk/provider-auth-runtime", async (importOriginal) => { }; }); +afterAll(() => { + vi.doUnmock("openclaw/plugin-sdk/provider-auth-runtime"); + vi.resetModules(); +}); + function buildLmstudioConfig(overrides?: { apiKey?: unknown; headers?: unknown; diff --git a/extensions/lmstudio/src/setup.test.ts b/extensions/lmstudio/src/setup.test.ts index 14b69e4db4d..f9725384104 100644 --- a/extensions/lmstudio/src/setup.test.ts +++ b/extensions/lmstudio/src/setup.test.ts @@ -8,7 +8,7 @@ import { type ProviderCatalogContext, } from "openclaw/plugin-sdk/provider-setup"; import type { WizardPrompter } from "openclaw/plugin-sdk/setup"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { LMSTUDIO_DEFAULT_API_KEY_ENV_VAR, LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER, @@ -48,6 +48,13 @@ vi.mock("openclaw/plugin-sdk/provider-setup", async (importOriginal) => { }; }); +afterAll(() => { + vi.doUnmock("./models.fetch.js"); + vi.doUnmock("openclaw/plugin-sdk/provider-auth"); + vi.doUnmock("openclaw/plugin-sdk/provider-setup"); + vi.resetModules(); +}); + function createModel(id: string, name = id): ModelDefinitionConfig { return { id, diff --git a/extensions/lmstudio/src/stream.test.ts b/extensions/lmstudio/src/stream.test.ts index 0ee0177594d..94dac48f742 100644 --- a/extensions/lmstudio/src/stream.test.ts +++ b/extensions/lmstudio/src/stream.test.ts @@ -1,6 +1,6 @@ import type { StreamFn } from "@mariozechner/pi-agent-core"; import { createAssistantMessageEventStream } from "@mariozechner/pi-ai"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { __resetLmstudioPreloadCooldownForTest, wrapLmstudioInferencePreload } from "./stream.js"; const ensureLmstudioModelLoadedMock = vi.hoisted(() => vi.fn()); @@ -28,6 +28,12 @@ vi.mock("./runtime.js", async (importOriginal) => { }; }); +afterAll(() => { + vi.doUnmock("./models.fetch.js"); + vi.doUnmock("./runtime.js"); + vi.resetModules(); +}); + type StreamEvent = { type: string } & Record; async function collectEvents(stream: ReturnType): Promise { @@ -108,6 +114,7 @@ describe("lmstudio stream wrapper", () => { }); afterEach(() => { + vi.restoreAllMocks(); ensureLmstudioModelLoadedMock.mockReset(); resolveLmstudioProviderHeadersMock.mockReset(); resolveLmstudioRuntimeApiKeyMock.mockReset(); diff --git a/extensions/lobster/src/lobster-runner.test.ts b/extensions/lobster/src/lobster-runner.test.ts index 6e93c9e9d85..69da1bc4ad1 100644 --- a/extensions/lobster/src/lobster-runner.test.ts +++ b/extensions/lobster/src/lobster-runner.test.ts @@ -543,13 +543,14 @@ describe("createEmbeddedLobsterRunner", () => { runToolRequest: vi.fn( async ({ ctx }: { ctx?: { signal?: AbortSignal } }) => await new Promise((resolve, reject) => { - ctx?.signal?.addEventListener("abort", () => { - reject(ctx.signal?.reason ?? new Error("aborted")); - }); - setTimeout( + const timeout = setTimeout( () => resolve({ ok: true, status: "ok", output: [], requiresApproval: null }), 500, ); + ctx?.signal?.addEventListener("abort", () => { + clearTimeout(timeout); + reject(ctx.signal?.reason ?? new Error("aborted")); + }); }), ), resumeToolRequest: vi.fn(), diff --git a/extensions/memory-core/src/memory/index.test.ts b/extensions/memory-core/src/memory/index.test.ts index 4995e88ee0e..298e013df32 100644 --- a/extensions/memory-core/src/memory/index.test.ts +++ b/extensions/memory-core/src/memory/index.test.ts @@ -330,7 +330,7 @@ describe("memory index", () => { return manager.status().fts?.available ? manager : null; } - it.skip("indexes memory files and searches", async () => { + it("indexes memory files and searches", async () => { const cfg = createCfg({ storePath: indexMainPath, hybrid: { enabled: true, vectorWeight: 0.5, textWeight: 0.5 }, @@ -381,7 +381,7 @@ describe("memory index", () => { expect(audioResults.some((result) => result.path.endsWith("meeting.wav"))).toBe(true); }); - it.skip("finds keyword matches via hybrid search when query embedding is zero", async () => { + it("finds keyword matches via hybrid search when query embedding is zero", async () => { await expectHybridKeywordSearchFindsMemory( createCfg({ storePath: indexMainPath, @@ -390,7 +390,7 @@ describe("memory index", () => { ); }); - it.skip("preserves keyword-only hybrid hits when minScore exceeds text weight", async () => { + it("preserves keyword-only hybrid hits when minScore exceeds text weight", async () => { await expectHybridKeywordSearchFindsMemory( createCfg({ storePath: indexMainPath, diff --git a/extensions/memory-core/src/memory/manager.ts b/extensions/memory-core/src/memory/manager.ts index 331090beff9..a92e9d280d0 100644 --- a/extensions/memory-core/src/memory/manager.ts +++ b/extensions/memory-core/src/memory/manager.ts @@ -436,14 +436,15 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem // If FTS isn't available, hybrid mode cannot use keyword search; degrade to vector-only. const keywordResults = hybrid.enabled && this.fts.enabled && this.fts.available - ? await this.searchKeyword(cleaned, candidates, undefined, sourceFilterList).catch( - (err) => { - log.warn( - `memory search: FTS hybrid keyword query failed: ${formatErrorMessage(err)}`, - ); - return []; - }, - ) + ? await this.searchKeyword( + cleaned, + candidates, + { boostFallbackRanking: true }, + sourceFilterList, + ).catch((err) => { + log.warn(`memory search: FTS hybrid keyword query failed: ${formatErrorMessage(err)}`); + return []; + }) : []; const queryVec = await this.embedQueryWithTimeout(cleaned); @@ -472,11 +473,10 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem return strict.slice(0, maxResults); } - // Hybrid defaults can produce keyword-only matches with max score equal to - // textWeight (for example 0.3). If minScore is higher (for example 0.35), - // these exact lexical hits get filtered out even when they are the only - // relevant results. - const relaxedMinScore = Math.min(minScore, hybrid.textWeight); + // Hybrid defaults can produce keyword-only matches below minScore after + // weighting. If strict vector+keyword results are empty, preserve the FTS + // matches; FTS already established lexical relevance. + const relaxedMinScore = 0; const keywordKeys = new Set( keywordResults.map( (entry) => `${entry.source}:${entry.path}:${entry.startLine}:${entry.endLine}`, diff --git a/extensions/memory-lancedb/index.test.ts b/extensions/memory-lancedb/index.test.ts index ccdaae7de5e..7beeeaf998b 100644 --- a/extensions/memory-lancedb/index.test.ts +++ b/extensions/memory-lancedb/index.test.ts @@ -95,19 +95,26 @@ describe("memory plugin e2e", () => { }); test("config schema resolves env vars", async () => { - // Set a test env var - process.env.TEST_MEMORY_API_KEY = "test-key-123"; + const previousApiKey = process.env.TEST_MEMORY_API_KEY; - const config = memoryPlugin.configSchema?.parse?.({ - embedding: { - apiKey: "${TEST_MEMORY_API_KEY}", - }, - dbPath: getDbPath(), - }) as MemoryPluginTestConfig | undefined; + try { + process.env.TEST_MEMORY_API_KEY = "test-key-123"; - expect(config?.embedding?.apiKey).toBe("test-key-123"); + const config = memoryPlugin.configSchema?.parse?.({ + embedding: { + apiKey: "${TEST_MEMORY_API_KEY}", + }, + dbPath: getDbPath(), + }) as MemoryPluginTestConfig | undefined; - delete process.env.TEST_MEMORY_API_KEY; + expect(config?.embedding?.apiKey).toBe("test-key-123"); + } finally { + if (previousApiKey === undefined) { + delete process.env.TEST_MEMORY_API_KEY; + } else { + process.env.TEST_MEMORY_API_KEY = previousApiKey; + } + } }); test("config schema accepts provider-backed embeddings without apiKey", async () => { @@ -1979,6 +1986,8 @@ describe("memory plugin e2e", () => { test("config schema resolves env vars in storageOptions", async () => { const { default: memoryPlugin } = await import("./index.js"); + const previousAccessKey = process.env.TEST_MEMORY_STORAGE_ACCESS_KEY; + const previousSecretKey = process.env.TEST_MEMORY_STORAGE_SECRET_KEY; process.env.TEST_MEMORY_STORAGE_ACCESS_KEY = "env-access"; process.env.TEST_MEMORY_STORAGE_SECRET_KEY = "env-secret"; @@ -2002,27 +2011,45 @@ describe("memory plugin e2e", () => { secret_key: "env-secret", }); } finally { - delete process.env.TEST_MEMORY_STORAGE_ACCESS_KEY; - delete process.env.TEST_MEMORY_STORAGE_SECRET_KEY; + if (previousAccessKey === undefined) { + delete process.env.TEST_MEMORY_STORAGE_ACCESS_KEY; + } else { + process.env.TEST_MEMORY_STORAGE_ACCESS_KEY = previousAccessKey; + } + if (previousSecretKey === undefined) { + delete process.env.TEST_MEMORY_STORAGE_SECRET_KEY; + } else { + process.env.TEST_MEMORY_STORAGE_SECRET_KEY = previousSecretKey; + } } }); test("config schema rejects missing env vars in storageOptions", async () => { const { default: memoryPlugin } = await import("./index.js"); - delete process.env.TEST_MEMORY_STORAGE_MISSING; + const previousMissing = process.env.TEST_MEMORY_STORAGE_MISSING; - expect(() => { - memoryPlugin.configSchema?.parse?.({ - embedding: { - apiKey: OPENAI_API_KEY, - model: "text-embedding-3-small", - }, - dbPath: getDbPath(), - storageOptions: { - secret_key: "${TEST_MEMORY_STORAGE_MISSING}", - }, - }); - }).toThrow("Environment variable TEST_MEMORY_STORAGE_MISSING is not set"); + try { + delete process.env.TEST_MEMORY_STORAGE_MISSING; + + expect(() => { + memoryPlugin.configSchema?.parse?.({ + embedding: { + apiKey: OPENAI_API_KEY, + model: "text-embedding-3-small", + }, + dbPath: getDbPath(), + storageOptions: { + secret_key: "${TEST_MEMORY_STORAGE_MISSING}", + }, + }); + }).toThrow("Environment variable TEST_MEMORY_STORAGE_MISSING is not set"); + } finally { + if (previousMissing === undefined) { + delete process.env.TEST_MEMORY_STORAGE_MISSING; + } else { + process.env.TEST_MEMORY_STORAGE_MISSING = previousMissing; + } + } }); test("config schema rejects storageOptions with non-string values", async () => { diff --git a/extensions/microsoft-foundry/shared.ts b/extensions/microsoft-foundry/shared.ts index ff9c0d2f749..eac2b5f726f 100644 --- a/extensions/microsoft-foundry/shared.ts +++ b/extensions/microsoft-foundry/shared.ts @@ -1,3 +1,4 @@ +import type { AuthConfig } from "openclaw/plugin-sdk/config-types"; import { applyAuthProfileConfig, buildApiKeyCredential, @@ -98,17 +99,8 @@ type FoundryModelCompat = { maxTokensField: "max_completion_tokens" | "max_tokens"; }; -type FoundryAuthProfileConfig = { - provider: string; - mode: "api_key" | "aws-sdk" | "oauth" | "token"; - email?: string; -}; - type FoundryConfigShape = { - auth?: { - profiles?: Record; - order?: Record; - }; + auth?: AuthConfig; models?: { providers?: Record; }; diff --git a/extensions/minimax/src/minimax-web-search-provider.test.ts b/extensions/minimax/src/minimax-web-search-provider.test.ts index d35b0910aff..3e3130f4ce8 100644 --- a/extensions/minimax/src/minimax-web-search-provider.test.ts +++ b/extensions/minimax/src/minimax-web-search-provider.test.ts @@ -9,6 +9,14 @@ const { resolveMiniMaxRegion, } = minimaxWebSearchTesting; +function restoreEnvValue(key: string, value: string | undefined) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } +} + describe("minimax web search provider", () => { const originalApiHost = process.env.MINIMAX_API_HOST; const originalCodePlanKey = process.env.MINIMAX_CODE_PLAN_KEY; @@ -25,11 +33,11 @@ describe("minimax web search provider", () => { }); afterEach(() => { - process.env.MINIMAX_API_HOST = originalApiHost; - process.env.MINIMAX_CODE_PLAN_KEY = originalCodePlanKey; - process.env.MINIMAX_CODING_API_KEY = originalCodingApiKey; - process.env.MINIMAX_OAUTH_TOKEN = originalOauthToken; - process.env.MINIMAX_API_KEY = originalApiKey; + restoreEnvValue("MINIMAX_API_HOST", originalApiHost); + restoreEnvValue("MINIMAX_CODE_PLAN_KEY", originalCodePlanKey); + restoreEnvValue("MINIMAX_CODING_API_KEY", originalCodingApiKey); + restoreEnvValue("MINIMAX_OAUTH_TOKEN", originalOauthToken); + restoreEnvValue("MINIMAX_API_KEY", originalApiKey); }); describe("resolveMiniMaxRegion", () => { diff --git a/extensions/msteams/src/user-agent.test.ts b/extensions/msteams/src/user-agent.test.ts index ef8c152a7ed..89a1462e108 100644 --- a/extensions/msteams/src/user-agent.test.ts +++ b/extensions/msteams/src/user-agent.test.ts @@ -20,6 +20,7 @@ describe("buildUserAgent", () => { }); afterEach(() => { + vi.unstubAllGlobals(); vi.restoreAllMocks(); }); diff --git a/extensions/nextcloud-talk/src/core.test.ts b/extensions/nextcloud-talk/src/core.test.ts index b6938e03d26..46254a17f18 100644 --- a/extensions/nextcloud-talk/src/core.test.ts +++ b/extensions/nextcloud-talk/src/core.test.ts @@ -193,35 +193,37 @@ describe("nextcloud talk core", () => { }; }); - const { generateNextcloudTalkSignature, verifyNextcloudTalkSignature } = - await import("./signature.js"); - const body = JSON.stringify({ hello: "world" }); - const generated = generateNextcloudTalkSignature({ - body, - secret: "secret-123", - }); - const shortSignature = generated.signature.slice(0, 12); - - expect( - verifyNextcloudTalkSignature({ - signature: shortSignature, - random: generated.random, + try { + const { generateNextcloudTalkSignature, verifyNextcloudTalkSignature } = + await import("./signature.js"); + const body = JSON.stringify({ hello: "world" }); + const generated = generateNextcloudTalkSignature({ body, secret: "secret-123", - }), - ).toBe(false); + }); + const shortSignature = generated.signature.slice(0, 12); - expect(timingSafeEqualMock).toHaveBeenCalledOnce(); - const [leftBuffer, rightBuffer] = timingSafeEqualMock.mock.calls[0] ?? []; - expect(Buffer.isBuffer(leftBuffer)).toBe(true); - expect(Buffer.isBuffer(rightBuffer)).toBe(true); - if (!Buffer.isBuffer(leftBuffer) || !Buffer.isBuffer(rightBuffer)) { - throw new TypeError("Expected timingSafeEqual to receive Buffer arguments"); + expect( + verifyNextcloudTalkSignature({ + signature: shortSignature, + random: generated.random, + body, + secret: "secret-123", + }), + ).toBe(false); + + expect(timingSafeEqualMock).toHaveBeenCalledOnce(); + const [leftBuffer, rightBuffer] = timingSafeEqualMock.mock.calls[0] ?? []; + expect(Buffer.isBuffer(leftBuffer)).toBe(true); + expect(Buffer.isBuffer(rightBuffer)).toBe(true); + if (!Buffer.isBuffer(leftBuffer) || !Buffer.isBuffer(rightBuffer)) { + throw new TypeError("Expected timingSafeEqual to receive Buffer arguments"); + } + expect(leftBuffer).toHaveLength(rightBuffer.length); + } finally { + vi.doUnmock("node:crypto"); + vi.resetModules(); } - expect(leftBuffer).toHaveLength(rightBuffer.length); - - vi.doUnmock("node:crypto"); - vi.resetModules(); }); it("persists replay decisions across guard instances and scopes account namespaces", async () => { diff --git a/extensions/nextcloud-talk/src/inbound.behavior.test.ts b/extensions/nextcloud-talk/src/inbound.behavior.test.ts index 473997dea73..922b9526dfd 100644 --- a/extensions/nextcloud-talk/src/inbound.behavior.test.ts +++ b/extensions/nextcloud-talk/src/inbound.behavior.test.ts @@ -135,12 +135,15 @@ describe("nextcloud-talk inbound behavior", () => { readStoreAllowFromForDmPolicyMock.mockResolvedValue([]); }); - // The DM pairing assertion currently depends on a mocked runtime barrel that Vitest - // does not bind reliably for this extension package. - it.skip("issues a DM pairing challenge and sends the challenge text", async () => { + it("issues a DM pairing challenge and sends the challenge text", async () => { + const issueChallenge = vi.fn( + async (params: { sendPairingReply: (text: string) => Promise }) => { + await params.sendPairingReply("Pair with code 123456"); + }, + ); createChannelPairingControllerMock.mockReturnValue({ readStoreForDmPolicy: vi.fn(), - issueChallenge: vi.fn(), + issueChallenge, }); resolveDmGroupAccessWithCommandGateMock.mockReturnValue({ decision: "pairing", @@ -152,12 +155,31 @@ describe("nextcloud-talk inbound behavior", () => { const statusSink = vi.fn(); await handleNextcloudTalkInbound({ - message: createMessage(), + message: createMessage({ timestamp: 1_736_380_800_000 }), account: createAccount(), config: { channels: { "nextcloud-talk": {} } } as CoreConfig, runtime: createRuntimeEnv(), statusSink, }); + + expect(issueChallenge).toHaveBeenCalledWith( + expect.objectContaining({ + senderId: "user-1", + senderIdLine: "Your Nextcloud user id: user-1", + meta: { name: "Alice" }, + }), + ); + expect(sendMessageNextcloudTalkMock).toHaveBeenCalledWith( + "room-1", + "Pair with code 123456", + expect.objectContaining({ + cfg: { channels: { "nextcloud-talk": {} } }, + accountId: "default", + }), + ); + expect(statusSink).toHaveBeenCalledWith({ lastInboundAt: 1_736_380_800_000 }); + expect(statusSink).toHaveBeenCalledWith({ lastOutboundAt: expect.any(Number) }); + expect(dispatchChannelMessageReplyWithBaseMock).not.toHaveBeenCalled(); }); it("drops unmentioned group traffic before dispatch", async () => { diff --git a/extensions/nostr/src/nostr-bus.integration.test.ts b/extensions/nostr/src/nostr-bus.integration.test.ts index 8f677d95912..790c79745d3 100644 --- a/extensions/nostr/src/nostr-bus.integration.test.ts +++ b/extensions/nostr/src/nostr-bus.integration.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { createMetrics, createNoopMetrics, type MetricEvent } from "./metrics.js"; import { createSeenTracker } from "./seen-tracker.js"; import { TEST_RELAY_URL } from "./test-fixtures.js"; @@ -9,6 +9,10 @@ const TEST_RELAY_URL_PRIMARY = "wss://relay.com"; const TEST_RELAY_URL_GOOD = "wss://good-relay.com"; const TEST_RELAY_URL_BAD = "wss://bad-relay.com"; +afterEach(() => { + vi.useRealTimers(); +}); + function createTracker(overrides?: Partial[0]>) { return createSeenTracker({ maxEntries: 100, diff --git a/extensions/nostr/src/nostr-profile.test.ts b/extensions/nostr/src/nostr-profile.test.ts index d34f29a06f9..5def17c09bf 100644 --- a/extensions/nostr/src/nostr-profile.test.ts +++ b/extensions/nostr/src/nostr-profile.test.ts @@ -1,5 +1,5 @@ import { verifyEvent, getPublicKey } from "nostr-tools"; -import { describe, expect, it, vi, beforeEach } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { NostrProfile } from "./config-schema.js"; import { createProfileEvent, @@ -119,6 +119,10 @@ describe("createProfileEvent", () => { vi.setSystemTime(new Date("2024-01-15T12:00:00Z")); }); + afterEach(() => { + vi.useRealTimers(); + }); + it("creates a valid kind:0 event", () => { const profile: NostrProfile = { name: "testbot", @@ -183,8 +187,6 @@ describe("createProfileEvent", () => { const expectedTimestamp = Math.floor(Date.now() / 1000); expect(event.created_at).toBe(expectedTimestamp); }); - - vi.useRealTimers(); }); // ============================================================================ diff --git a/extensions/openai/index.test.ts b/extensions/openai/index.test.ts index 1d94d89b1a7..b13958a8dfe 100644 --- a/extensions/openai/index.test.ts +++ b/extensions/openai/index.test.ts @@ -144,6 +144,7 @@ describe("openai plugin", () => { }); afterEach(() => { + vi.unstubAllGlobals(); vi.restoreAllMocks(); }); diff --git a/extensions/openai/tts.test.ts b/extensions/openai/tts.test.ts index 343503879a5..fb06ca8b5e4 100644 --- a/extensions/openai/tts.test.ts +++ b/extensions/openai/tts.test.ts @@ -6,7 +6,7 @@ import { getDebugProxyCaptureStore, initializeDebugProxyCapture, } from "openclaw/plugin-sdk/proxy-capture"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { installDebugProxyTestResetHooks } from "../test-support/debug-proxy-env-test-helpers.js"; import { createStreamingErrorResponse } from "../test-support/streaming-error-response.js"; import { @@ -34,6 +34,13 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({ describe("openai tts", () => { const proxyReset = installDebugProxyTestResetHooks(); + const originalFetch = globalThis.fetch; + + afterEach(() => { + globalThis.fetch = originalFetch; + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); describe("isValidOpenAIVoice", () => { it("accepts all valid OpenAI voices including newer additions", () => { diff --git a/extensions/openshell/src/openshell-core.test.ts b/extensions/openshell/src/openshell-core.test.ts index 342d39b3245..10d6f817ce0 100644 --- a/extensions/openshell/src/openshell-core.test.ts +++ b/extensions/openshell/src/openshell-core.test.ts @@ -85,6 +85,7 @@ describe("openshell backend manager", () => { afterAll(() => { vi.doUnmock("./cli.js"); + vi.resetModules(); }); beforeEach(() => { diff --git a/extensions/qa-lab/src/gateway-rpc-client.test.ts b/extensions/qa-lab/src/gateway-rpc-client.test.ts index 583b47b0ed1..1ac7bb4b8e2 100644 --- a/extensions/qa-lab/src/gateway-rpc-client.test.ts +++ b/extensions/qa-lab/src/gateway-rpc-client.test.ts @@ -25,40 +25,48 @@ describe("startQaGatewayRpcClient", () => { const originalHome = process.env.OPENCLAW_HOME; delete process.env.OPENCLAW_HOME; - gatewayRpcMock.callGatewayFromCli.mockImplementationOnce(async () => { - expect(process.env.OPENCLAW_HOME).toBeUndefined(); - return { ok: true }; - }); + try { + gatewayRpcMock.callGatewayFromCli.mockImplementationOnce(async () => { + expect(process.env.OPENCLAW_HOME).toBeUndefined(); + return { ok: true }; + }); - const client = await startQaGatewayRpcClient({ - wsUrl: "ws://127.0.0.1:18789", - token: "qa-token", - logs: () => "qa logs", - }); - - await expect( - client.request("agent.run", { prompt: "hi" }, { expectFinal: true, timeoutMs: 45_000 }), - ).resolves.toEqual({ ok: true }); - - expect(gatewayRpcMock.callGatewayFromCli).toHaveBeenCalledWith( - "agent.run", - { - url: "ws://127.0.0.1:18789", + const client = await startQaGatewayRpcClient({ + wsUrl: "ws://127.0.0.1:18789", token: "qa-token", - timeout: "45000", - expectFinal: true, - json: true, - }, - { prompt: "hi" }, - { - clientName: "gateway-client", - deviceIdentity: null, - expectFinal: true, - mode: "backend", - progress: false, - scopes: ["operator.admin"], - }, - ); + logs: () => "qa logs", + }); + + await expect( + client.request("agent.run", { prompt: "hi" }, { expectFinal: true, timeoutMs: 45_000 }), + ).resolves.toEqual({ ok: true }); + + expect(gatewayRpcMock.callGatewayFromCli).toHaveBeenCalledWith( + "agent.run", + { + url: "ws://127.0.0.1:18789", + token: "qa-token", + timeout: "45000", + expectFinal: true, + json: true, + }, + { prompt: "hi" }, + { + clientName: "gateway-client", + deviceIdentity: null, + expectFinal: true, + mode: "backend", + progress: false, + scopes: ["operator.admin"], + }, + ); + } finally { + if (originalHome === undefined) { + delete process.env.OPENCLAW_HOME; + } else { + process.env.OPENCLAW_HOME = originalHome; + } + } expect(process.env.OPENCLAW_HOME).toBe(originalHome); }); diff --git a/extensions/synology-chat/src/core.test.ts b/extensions/synology-chat/src/core.test.ts index 6eeec16e42e..64789d6902b 100644 --- a/extensions/synology-chat/src/core.test.ts +++ b/extensions/synology-chat/src/core.test.ts @@ -5,7 +5,7 @@ import { runSetupWizardConfigure, } from "openclaw/plugin-sdk/plugin-test-runtime"; import type { WizardPrompter } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { listAccountIds, resolveAccount } from "./accounts.js"; import { SynologyChatChannelConfigSchema } from "./config-schema.js"; import { @@ -54,6 +54,11 @@ function createSynologySetupPrompter(params: { allowedUserIds?: string } = {}) { } describe("synology-chat core", () => { + afterAll(() => { + vi.unstubAllEnvs(); + process.env = { ...originalEnv }; + }); + beforeEach(() => { vi.unstubAllEnvs(); process.env = { ...originalEnv }; diff --git a/extensions/telegram/src/api-fetch.test.ts b/extensions/telegram/src/api-fetch.test.ts index a18c33dd13d..290231d8986 100644 --- a/extensions/telegram/src/api-fetch.test.ts +++ b/extensions/telegram/src/api-fetch.test.ts @@ -48,6 +48,7 @@ function getOwnSymbolValue( } afterEach(() => { + vi.unstubAllGlobals(); vi.unstubAllEnvs(); }); diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index b1bdba57984..ae3a2ebc3ce 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -158,7 +158,11 @@ describe("createTelegramBot", () => { process.env.TZ = "UTC"; }); afterAll(() => { - process.env.TZ = ORIGINAL_TZ; + if (ORIGINAL_TZ === undefined) { + delete process.env.TZ; + } else { + process.env.TZ = ORIGINAL_TZ; + } }); beforeEach(() => { resetTelegramForumFlagCacheForTest(); diff --git a/extensions/telegram/src/bot.media.stickers-and-fragments.e2e.test.ts b/extensions/telegram/src/bot.media.stickers-and-fragments.e2e.test.ts index 535be168a92..3753880e9e8 100644 --- a/extensions/telegram/src/bot.media.stickers-and-fragments.e2e.test.ts +++ b/extensions/telegram/src/bot.media.stickers-and-fragments.e2e.test.ts @@ -32,51 +32,7 @@ describe("telegram stickers", () => { describeStickerImageSpy.mockReturnValue(undefined); }); - // Skipped pending #50185: deterministic static sticker fetch injection. - it.skip( - "downloads static sticker (WEBP) and includes sticker metadata", - async () => { - const { handler, proxyFetch, replySpy, runtimeError } = await createStaticStickerHarness(); - - await handler({ - message: { - message_id: 100, - chat: { id: 1234, type: "private" }, - from: { id: 777, is_bot: false, first_name: "Ada" }, - sticker: { - file_id: "sticker_file_id_123", - file_unique_id: "sticker_unique_123", - type: "regular", - width: 512, - height: 512, - is_animated: false, - is_video: false, - emoji: "🎉", - set_name: "TestStickerPack", - }, - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ file_path: "stickers/sticker.webp" }), - }); - - expect(runtimeError).not.toHaveBeenCalled(); - expect(proxyFetch).toHaveBeenCalledWith( - "https://api.telegram.org/file/bottok/stickers/sticker.webp", - expect.objectContaining({ redirect: "manual" }), - ); - expect(replySpy).toHaveBeenCalledTimes(1); - const payload = replySpy.mock.calls[0][0]; - expect(payload.Body).toContain(""); - expect(payload.Sticker?.emoji).toBe("🎉"); - expect(payload.Sticker?.setName).toBe("TestStickerPack"); - expect(payload.Sticker?.fileId).toBe("sticker_file_id_123"); - }, - STICKER_TEST_TIMEOUT_MS, - ); - - // Skipped pending #50185: deterministic cache-refresh assertions in CI. - it.skip( + it( "refreshes cached sticker metadata on cache hit", async () => { const { handler, proxyFetch, replySpy, runtimeError } = await createStaticStickerHarness(); diff --git a/extensions/telegram/src/bot.test.ts b/extensions/telegram/src/bot.test.ts index 6666167b17d..b939c36126c 100644 --- a/extensions/telegram/src/bot.test.ts +++ b/extensions/telegram/src/bot.test.ts @@ -89,7 +89,11 @@ describe("createTelegramBot", () => { process.env.TZ = "UTC"; }); afterAll(() => { - process.env.TZ = ORIGINAL_TZ; + if (ORIGINAL_TZ === undefined) { + delete process.env.TZ; + } else { + process.env.TZ = ORIGINAL_TZ; + } }); beforeEach(() => { diff --git a/extensions/telegram/src/thread-bindings.test.ts b/extensions/telegram/src/thread-bindings.test.ts index 20385bdf854..ad2b257340c 100644 --- a/extensions/telegram/src/thread-bindings.test.ts +++ b/extensions/telegram/src/thread-bindings.test.ts @@ -66,6 +66,7 @@ async function flushMicrotasks(): Promise { } describe("telegram thread bindings", () => { + const originalStateDir = process.env.OPENCLAW_STATE_DIR; let stateDirOverride: string | undefined; beforeEach(async () => { @@ -82,10 +83,14 @@ describe("telegram thread bindings", () => { vi.useRealTimers(); await __testing.resetTelegramThreadBindingsForTests(); if (stateDirOverride) { - delete process.env.OPENCLAW_STATE_DIR; fs.rmSync(stateDirOverride, { recursive: true, force: true }); stateDirOverride = undefined; } + if (originalStateDir === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = originalStateDir; + } }); it("registers a telegram binding adapter and binds current conversations", async () => { diff --git a/extensions/thread-ownership/index.test.ts b/extensions/thread-ownership/index.test.ts index 6de53c991b5..1db1c51ebe8 100644 --- a/extensions/thread-ownership/index.test.ts +++ b/extensions/thread-ownership/index.test.ts @@ -6,6 +6,8 @@ describe("thread-ownership plugin", () => { const hooks: Record = {}; const fetchMock = vi.fn() as unknown as typeof globalThis.fetch; let configFile: Record = {}; + const originalSlackForwarderUrl = process.env.SLACK_FORWARDER_URL; + const originalSlackBotUserId = process.env.SLACK_BOT_USER_ID; const api = { pluginConfig: {}, config: { @@ -44,8 +46,16 @@ describe("thread-ownership plugin", () => { afterEach(() => { vi.unstubAllGlobals(); - delete process.env.SLACK_FORWARDER_URL; - delete process.env.SLACK_BOT_USER_ID; + if (originalSlackForwarderUrl === undefined) { + delete process.env.SLACK_FORWARDER_URL; + } else { + process.env.SLACK_FORWARDER_URL = originalSlackForwarderUrl; + } + if (originalSlackBotUserId === undefined) { + delete process.env.SLACK_BOT_USER_ID; + } else { + process.env.SLACK_BOT_USER_ID = originalSlackBotUserId; + } vi.restoreAllMocks(); }); diff --git a/extensions/twitch/src/token.test.ts b/extensions/twitch/src/token.test.ts index 1e33dc6bdea..79e71bf345c 100644 --- a/extensions/twitch/src/token.test.ts +++ b/extensions/twitch/src/token.test.ts @@ -13,6 +13,8 @@ import type { OpenClawConfig } from "../api.js"; import { resolveTwitchToken, type TwitchTokenSource } from "./token.js"; describe("token", () => { + const originalAccessToken = process.env.OPENCLAW_TWITCH_ACCESS_TOKEN; + // Multi-account config for testing non-default accounts const mockMultiAccountConfig = { channels: { @@ -47,7 +49,11 @@ describe("token", () => { afterEach(() => { vi.restoreAllMocks(); - delete process.env.OPENCLAW_TWITCH_ACCESS_TOKEN; + if (originalAccessToken === undefined) { + delete process.env.OPENCLAW_TWITCH_ACCESS_TOKEN; + } else { + process.env.OPENCLAW_TWITCH_ACCESS_TOKEN = originalAccessToken; + } }); describe("resolveTwitchToken", () => { diff --git a/extensions/volcengine/tts.test.ts b/extensions/volcengine/tts.test.ts index dd08824e5b7..5412e90147e 100644 --- a/extensions/volcengine/tts.test.ts +++ b/extensions/volcengine/tts.test.ts @@ -36,6 +36,14 @@ function clearTtsEnv() { delete process.env.VOLCENGINE_TTS_TOKEN; } +function restoreOptionalEnv(key: string, value: string | undefined) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } +} + describe("Volcengine speech provider", () => { const provider = buildVolcengineSpeechProvider(); @@ -72,21 +80,11 @@ describe("Volcengine speech provider", () => { try { expect(provider.isConfigured({ providerConfig: {}, timeoutMs: 30000 })).toBe(false); } finally { - if (oldBytePlusKey) { - process.env.BYTEPLUS_API_KEY = oldBytePlusKey; - } - if (oldSeedKey) { - process.env.BYTEPLUS_SEED_SPEECH_API_KEY = oldSeedKey; - } - if (oldApiKey) { - process.env.VOLCENGINE_TTS_API_KEY = oldApiKey; - } - if (oldAppId) { - process.env.VOLCENGINE_TTS_APPID = oldAppId; - } - if (oldToken) { - process.env.VOLCENGINE_TTS_TOKEN = oldToken; - } + restoreOptionalEnv("BYTEPLUS_API_KEY", oldBytePlusKey); + restoreOptionalEnv("BYTEPLUS_SEED_SPEECH_API_KEY", oldSeedKey); + restoreOptionalEnv("VOLCENGINE_TTS_API_KEY", oldApiKey); + restoreOptionalEnv("VOLCENGINE_TTS_APPID", oldAppId); + restoreOptionalEnv("VOLCENGINE_TTS_TOKEN", oldToken); } }); @@ -101,27 +99,11 @@ describe("Volcengine speech provider", () => { try { expect(provider.isConfigured({ providerConfig: {}, timeoutMs: 30000 })).toBe(true); } finally { - if (oldBytePlusKey) { - process.env.BYTEPLUS_API_KEY = oldBytePlusKey; - } - if (oldSeedKey) { - process.env.BYTEPLUS_SEED_SPEECH_API_KEY = oldSeedKey; - } else { - delete process.env.BYTEPLUS_SEED_SPEECH_API_KEY; - } - if (oldApiKey) { - process.env.VOLCENGINE_TTS_API_KEY = oldApiKey; - } - if (oldAppId) { - process.env.VOLCENGINE_TTS_APPID = oldAppId; - } else { - delete process.env.VOLCENGINE_TTS_APPID; - } - if (oldToken) { - process.env.VOLCENGINE_TTS_TOKEN = oldToken; - } else { - delete process.env.VOLCENGINE_TTS_TOKEN; - } + restoreOptionalEnv("BYTEPLUS_API_KEY", oldBytePlusKey); + restoreOptionalEnv("BYTEPLUS_SEED_SPEECH_API_KEY", oldSeedKey); + restoreOptionalEnv("VOLCENGINE_TTS_API_KEY", oldApiKey); + restoreOptionalEnv("VOLCENGINE_TTS_APPID", oldAppId); + restoreOptionalEnv("VOLCENGINE_TTS_TOKEN", oldToken); } }); diff --git a/extensions/whatsapp/src/inbound.media.test.ts b/extensions/whatsapp/src/inbound.media.test.ts index 9ab47cca7ba..c7163860efc 100644 --- a/extensions/whatsapp/src/inbound.media.test.ts +++ b/extensions/whatsapp/src/inbound.media.test.ts @@ -90,6 +90,7 @@ vi.mock("openclaw/plugin-sdk/media-store", async () => { }); const HOME = path.join(os.tmpdir(), `openclaw-inbound-media-${crypto.randomUUID()}`); +const ORIGINAL_HOME = process.env.HOME; process.env.HOME = HOME; vi.mock("@whiskeysockets/baileys", async () => { @@ -178,6 +179,11 @@ describe("web inbound media saves with extension", () => { afterAll(async () => { await fs.rm(HOME, { recursive: true, force: true }); + if (ORIGINAL_HOME === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = ORIGINAL_HOME; + } }); it("stores image extension and keeps document filename", async () => { diff --git a/extensions/whatsapp/src/text-runtime.test.ts b/extensions/whatsapp/src/text-runtime.test.ts index d019a25047a..81749b4a747 100644 --- a/extensions/whatsapp/src/text-runtime.test.ts +++ b/extensions/whatsapp/src/text-runtime.test.ts @@ -95,10 +95,10 @@ describe("jidToE164", () => { const { jidToE164: freshJidToE164 } = await import("./text-runtime.js"); expect(freshJidToE164("123@lid")).toBe("+5551234"); } finally { - if (previousStateDir) { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } else { + if (previousStateDir === undefined) { delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = previousStateDir; } vi.resetModules(); } diff --git a/extensions/xiaomi/speech-provider.test.ts b/extensions/xiaomi/speech-provider.test.ts index 33ab1f73445..be74d985ce5 100644 --- a/extensions/xiaomi/speech-provider.test.ts +++ b/extensions/xiaomi/speech-provider.test.ts @@ -127,6 +127,7 @@ describe("buildXiaomiSpeechProvider", () => { }); afterEach(() => { + vi.unstubAllGlobals(); globalThis.fetch = savedFetch; vi.restoreAllMocks(); }); @@ -217,7 +218,9 @@ describe("buildXiaomiSpeechProvider", () => { }), ).rejects.toThrow("Xiaomi API key missing"); } finally { - if (savedKey) { + if (savedKey === undefined) { + delete process.env.XIAOMI_API_KEY; + } else { process.env.XIAOMI_API_KEY = savedKey; } } diff --git a/extensions/zalouser/src/accounts.test.ts b/extensions/zalouser/src/accounts.test.ts index bee3ec42efb..2a1fe2cc739 100644 --- a/extensions/zalouser/src/accounts.test.ts +++ b/extensions/zalouser/src/accounts.test.ts @@ -1,5 +1,5 @@ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../runtime-api.js"; import { getZcaUserInfo, @@ -18,6 +18,8 @@ vi.mock("./zalo-js.js", () => ({ const mockCheckAuthenticated = vi.mocked(checkZaloAuthenticated); const mockGetUserInfo = vi.mocked(getZaloUserInfo); +const originalZalouserProfile = process.env.ZALOUSER_PROFILE; +const originalZcaProfile = process.env.ZCA_PROFILE; function asConfig(value: unknown): OpenClawConfig { return value as OpenClawConfig; @@ -31,6 +33,19 @@ describe("zalouser account resolution", () => { delete process.env.ZCA_PROFILE; }); + afterEach(() => { + if (originalZalouserProfile === undefined) { + delete process.env.ZALOUSER_PROFILE; + } else { + process.env.ZALOUSER_PROFILE = originalZalouserProfile; + } + if (originalZcaProfile === undefined) { + delete process.env.ZCA_PROFILE; + } else { + process.env.ZCA_PROFILE = originalZcaProfile; + } + }); + it("returns default account id when no accounts are configured", () => { expect(listZalouserAccountIds(asConfig({}))).toEqual([DEFAULT_ACCOUNT_ID]); }); diff --git a/src/agents/pi-embedded-runner-extraparams.live.test.ts b/src/agents/pi-embedded-runner-extraparams.live.test.ts index be45f21f88e..ff5610bcbcb 100644 --- a/src/agents/pi-embedded-runner-extraparams.live.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.live.test.ts @@ -7,14 +7,11 @@ import { applyExtraParamsToAgent } from "./pi-embedded-runner.js"; const OPENAI_KEY = process.env.OPENAI_API_KEY ?? ""; const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY ?? ""; -const GEMINI_KEY = process.env.GEMINI_API_KEY ?? ""; const LIVE = isLiveTestEnabled(["OPENAI_LIVE_TEST"]); const ANTHROPIC_LIVE = isLiveTestEnabled(["ANTHROPIC_LIVE_TEST"]); -const GEMINI_LIVE = isLiveTestEnabled(["GEMINI_LIVE_TEST"]); const describeLive = LIVE && OPENAI_KEY ? describe : describe.skip; const describeAnthropicLive = ANTHROPIC_LIVE && ANTHROPIC_KEY ? describe : describe.skip; -const describeGeminiLive = GEMINI_LIVE && GEMINI_KEY ? describe : describe.skip; describeLive("pi embedded extra params (live)", () => { it("applies config maxTokens to openai streamFn", async () => { @@ -141,115 +138,3 @@ describeAnthropicLive("pi embedded extra params (anthropic live)", () => { expect(fast.stop_reason).toBe("end_turn"); }, 45_000); }); - -describeGeminiLive("pi embedded extra params (gemini live)", () => { - function buildGeminiPayloadThroughWrapper(params: { - model: Model<"google-generative-ai">; - oneByOneRedPngBase64: string; - includeImage?: boolean; - prompt: string; - }): Record { - const userContent: Array< - { type: "text"; text: string } | { type: "image"; mimeType: string; data: string } - > = [{ type: "text", text: params.prompt }]; - if (params.includeImage ?? true) { - userContent.push({ - type: "image", - mimeType: "image/png", - data: params.oneByOneRedPngBase64, - }); - } - - const payload: Record = { - model: params.model.id, - contents: [{ role: "user", parts: userContent.map(mapGeminiContentPart) }], - config: { - maxOutputTokens: 64, - thinkingConfig: { - includeThoughts: true, - thinkingBudget: 32768, - }, - }, - }; - - const baseStreamFn = ( - _model: Model<"google-generative-ai">, - _context: unknown, - options?: { - onPayload?: (payload: unknown) => unknown; - }, - ) => { - options?.onPayload?.(payload); - return {} as ReturnType; - }; - const agent = { streamFn: baseStreamFn as typeof streamSimple }; - applyExtraParamsToAgent(agent, undefined, "google", params.model.id, undefined, "high"); - void agent.streamFn( - params.model, - { messages: [] }, - { - reasoning: "high", - maxTokens: 64, - }, - ); - return payload; - } - - function mapGeminiContentPart( - part: { type: "text"; text: string } | { type: "image"; mimeType: string; data: string }, - ): { text: string } | { inlineData: { mimeType: string; data: string } } { - if (part.type === "text") { - return { text: part.text }; - } - return { - inlineData: { - mimeType: part.mimeType, - data: part.data, - }, - }; - } - - // Payload mutation is covered by extra-params.google.test.ts, and Gemini - // roundtrips are exercised by the dedicated live gateway/model suites. This - // direct live test currently flakes on Vitest timeout teardown without - // providing unique signal. - it.skip("sanitizes Gemini thinking payload and keeps image parts with reasoning enabled", async () => { - const model = getModel("google", "gemini-2.5-pro") as unknown as Model<"google-generative-ai">; - - const oneByOneRedPngBase64 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGP4zwAAAgIBAJBzWgkAAAAASUVORK5CYII="; - - const capturedPayload = buildGeminiPayloadThroughWrapper({ - model, - oneByOneRedPngBase64, - includeImage: true, - prompt: "What color is this image? Reply with one word.", - }); - - expect(capturedPayload).toBeDefined(); - const thinkingConfig = ( - capturedPayload?.config as { thinkingConfig?: Record } | undefined - )?.thinkingConfig; - const thinkingBudget = thinkingConfig?.thinkingBudget; - if (thinkingBudget !== undefined) { - expect(typeof thinkingBudget).toBe("number"); - expect(thinkingBudget).toBeGreaterThanOrEqual(0); - } - // Gemini 3.1-specific thinkingLevel fill is covered by - // extra-params.google.test.ts. The live probe uses the stable 2.5 model and - // only verifies that we never forward an invalid negative budget. - - const imagePart = ( - capturedPayload?.contents as - | Array<{ parts?: Array<{ inlineData?: { mimeType?: string; data?: string } }> }> - | undefined - )?.[0]?.parts?.find((part) => part.inlineData !== undefined)?.inlineData; - expect(imagePart).toEqual({ - mimeType: "image/png", - data: oneByOneRedPngBase64, - }); - - // End-to-end Gemini roundtrips are already covered elsewhere. This live - // check stays focused on the request payload we generate for those suites. - }, 60_000); -}); diff --git a/src/agents/pi-embedded-runner.bundle-mcp.e2e.test.ts b/src/agents/pi-embedded-runner.bundle-mcp.e2e.test.ts deleted file mode 100644 index 3602309c332..00000000000 --- a/src/agents/pi-embedded-runner.bundle-mcp.e2e.test.ts +++ /dev/null @@ -1,258 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import "./test-helpers/fast-coding-tools.js"; -import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; -import { setPluginToolMeta } from "../plugins/tools.js"; -import { - cleanupEmbeddedPiRunnerTestWorkspace, - createEmbeddedPiRunnerOpenAiConfig, - createEmbeddedPiRunnerTestWorkspace, - type EmbeddedPiRunnerTestWorkspace, - immediateEnqueue, -} from "./test-helpers/pi-embedded-runner-e2e-fixtures.js"; - -const E2E_TIMEOUT_MS = 40_000; - -function createMockUsage(input: number, output: number) { - return { - input, - output, - cacheRead: 0, - cacheWrite: 0, - totalTokens: input + output, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }; -} - -let streamCallCount = 0; -let observedContexts: Array> = []; - -vi.mock("./pi-bundle-mcp-tools.js", () => ({ - retireSessionMcpRuntime: vi.fn(async () => true), - getOrCreateSessionMcpRuntime: async () => ({ - sessionId: "bundle-mcp-runtime", - sessionKey: "agent:test:bundle-mcp-e2e", - workspaceDir: "/tmp", - configFingerprint: "test", - createdAt: Date.now(), - lastUsedAt: Date.now(), - markUsed: () => {}, - getCatalog: async () => ({ - version: 1, - generatedAt: Date.now(), - servers: {}, - tools: [], - }), - callTool: async () => ({ - content: [{ type: "text", text: "FROM-BUNDLE" }], - }), - dispose: async () => {}, - }), - materializeBundleMcpToolsForRun: async () => { - const tool = { - name: "bundleProbe__bundle_probe", - label: "bundle_probe", - description: "Bundle MCP probe", - parameters: { type: "object", properties: {} }, - execute: async () => ({ - content: [{ type: "text", text: "FROM-BUNDLE" }], - details: { - mcpServer: "bundleProbe", - mcpTool: "bundle_probe", - }, - }), - }; - setPluginToolMeta(tool as any, { pluginId: "bundle-mcp", optional: false }); - return { - tools: [tool], - dispose: async () => {}, - }; - }, -})); - -vi.mock("@mariozechner/pi-ai", async () => { - const actual = await vi.importActual("@mariozechner/pi-ai"); - - const buildToolUseMessage = (model: { api: string; provider: string; id: string }) => ({ - role: "assistant" as const, - content: [ - { - type: "toolCall" as const, - id: "tc-bundle-mcp-1", - name: "bundleProbe__bundle_probe", - arguments: {}, - }, - ], - stopReason: "toolUse" as const, - api: model.api, - provider: model.provider, - model: model.id, - usage: createMockUsage(1, 1), - timestamp: Date.now(), - }); - - const buildStopMessage = ( - model: { api: string; provider: string; id: string }, - text: string, - ) => ({ - role: "assistant" as const, - content: [{ type: "text" as const, text }], - stopReason: "stop" as const, - api: model.api, - provider: model.provider, - model: model.id, - usage: createMockUsage(1, 1), - timestamp: Date.now(), - }); - - return { - ...actual, - complete: async (model: { api: string; provider: string; id: string }) => { - streamCallCount += 1; - return streamCallCount === 1 - ? buildToolUseMessage(model) - : buildStopMessage(model, "BUNDLE MCP OK FROM-BUNDLE"); - }, - completeSimple: async (model: { api: string; provider: string; id: string }) => { - streamCallCount += 1; - return streamCallCount === 1 - ? buildToolUseMessage(model) - : buildStopMessage(model, "BUNDLE MCP OK FROM-BUNDLE"); - }, - streamSimple: ( - model: { api: string; provider: string; id: string }, - context: { messages?: Array<{ role?: string; content?: unknown }> }, - ) => { - streamCallCount += 1; - const messages = (context.messages ?? []).map((message) => Object.assign({}, message)); - observedContexts.push(messages); - const stream = actual.createAssistantMessageEventStream(); - queueMicrotask(() => { - if (streamCallCount === 1) { - stream.push({ - type: "done", - reason: "toolUse", - message: buildToolUseMessage(model), - }); - stream.end(); - return; - } - - const toolResultText = messages.flatMap((message) => - Array.isArray(message.content) - ? (message.content as Array<{ type?: string; text?: string }>) - .filter((entry) => entry.type === "text" && typeof entry.text === "string") - .map((entry) => entry.text ?? "") - : [], - ); - const sawBundleResult = toolResultText.some((text) => text.includes("FROM-BUNDLE")); - if (!sawBundleResult) { - stream.push({ - type: "done", - reason: "stop", - message: buildStopMessage(model, "bundle MCP tool result missing from context"), - }); - stream.end(); - return; - } - - stream.push({ - type: "done", - reason: "stop", - message: buildStopMessage(model, "BUNDLE MCP OK FROM-BUNDLE"), - }); - stream.end(); - }); - return stream; - }, - }; -}); - -let runEmbeddedPiAgent: typeof import("./pi-embedded-runner/run.js").runEmbeddedPiAgent; -let e2eWorkspace: EmbeddedPiRunnerTestWorkspace | undefined; -let agentDir: string; -let workspaceDir: string; - -beforeAll(async () => { - vi.useRealTimers(); - ({ runEmbeddedPiAgent } = await import("./pi-embedded-runner/run.js")); - e2eWorkspace = await createEmbeddedPiRunnerTestWorkspace("openclaw-bundle-mcp-pi-"); - ({ agentDir, workspaceDir } = e2eWorkspace); -}, 180_000); - -afterAll(async () => { - await cleanupEmbeddedPiRunnerTestWorkspace(e2eWorkspace); - e2eWorkspace = undefined; -}); - -const readSessionMessages = async (sessionFile: string) => { - const raw = await fs.readFile(sessionFile, "utf-8"); - return raw - .split(/\r?\n/) - .filter(Boolean) - .map( - (line) => - JSON.parse(line) as { type?: string; message?: { role?: string; content?: unknown } }, - ) - .filter((entry) => entry.type === "message") - .map((entry) => entry.message) as Array<{ role?: string; content?: unknown }>; -}; - -describe("runEmbeddedPiAgent bundle MCP e2e", () => { - it.skip( - "loads bundle MCP into Pi, executes the MCP tool, and includes the result in the follow-up turn", - { timeout: E2E_TIMEOUT_MS }, - async () => { - streamCallCount = 0; - observedContexts = []; - - const sessionFile = path.join(workspaceDir, "session-bundle-mcp-e2e.jsonl"); - const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-bundle-mcp"]); - - const result = await runEmbeddedPiAgent({ - sessionId: "bundle-mcp-e2e", - sessionKey: "agent:test:bundle-mcp-e2e", - sessionFile, - workspaceDir, - config: cfg, - prompt: "Use the bundle MCP tool and report its result.", - provider: "openai", - model: "mock-bundle-mcp", - timeoutMs: 30_000, - agentDir, - runId: "run-bundle-mcp-e2e", - enqueue: immediateEnqueue, - }); - - expect(result.payloads?.[0]?.text).toContain("BUNDLE MCP OK FROM-BUNDLE"); - expect(streamCallCount).toBe(2); - - const followUpContext = observedContexts[1] ?? []; - const followUpTexts = followUpContext.flatMap((message) => - Array.isArray(message.content) - ? (message.content as Array<{ type?: string; text?: string }>) - .filter((entry) => entry.type === "text" && typeof entry.text === "string") - .map((entry) => entry.text ?? "") - : [], - ); - expect(followUpTexts.some((text) => text.includes("FROM-BUNDLE"))).toBe(true); - - const messages = await readSessionMessages(sessionFile); - const toolResults = messages.filter((message) => message?.role === "toolResult"); - const toolResultText = toolResults.flatMap((message) => - Array.isArray(message.content) - ? (message.content as Array<{ type?: string; text?: string }>) - .filter((entry) => entry.type === "text" && typeof entry.text === "string") - .map((entry) => entry.text ?? "") - : [], - ); - expect(toolResultText.some((text) => text.includes("FROM-BUNDLE"))).toBe(true); - }, - ); -}); diff --git a/src/channels/plugins/module-loader.test.ts b/src/channels/plugins/module-loader.test.ts index 67876b8848d..19c632815bb 100644 --- a/src/channels/plugins/module-loader.test.ts +++ b/src/channels/plugins/module-loader.test.ts @@ -9,9 +9,6 @@ import type { PluginModuleLoaderFactory } from "../../plugins/plugin-module-load import { resolveExistingPluginModulePath } from "./module-loader.js"; const tempDirs: string[] = []; -const pluginModuleLoaderJitiFactoryOverrideKey = Symbol.for( - "openclaw.pluginModuleLoaderJitiFactoryOverride", -); const testRequire = createRequire(import.meta.url); afterEach(() => { @@ -21,21 +18,8 @@ afterEach(() => { vi.restoreAllMocks(); vi.resetModules(); vi.doUnmock("jiti"); - delete ( - globalThis as typeof globalThis & { - [pluginModuleLoaderJitiFactoryOverrideKey]?: PluginModuleLoaderFactory; - } - )[pluginModuleLoaderJitiFactoryOverrideKey]; }); -function stubPluginModuleLoaderJitiFactory(createJiti: PluginModuleLoaderFactory): void { - ( - globalThis as typeof globalThis & { - [pluginModuleLoaderJitiFactoryOverrideKey]?: PluginModuleLoaderFactory; - } - )[pluginModuleLoaderJitiFactoryOverrideKey] = createJiti; -} - function createTempDir(): string { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-channel-module-loader-")); tempDirs.push(tempDir); @@ -95,11 +79,13 @@ describe("channel plugin module loader helpers", () => { delete testRequire.extensions[extension]; } vi.resetModules(); - stubPluginModuleLoaderJitiFactory(createJiti as unknown as PluginModuleLoaderFactory); const loaderModule = await importFreshModule( import.meta.url, "./module-loader.js?scope=source-ts-jiti-fallback", ); + loaderModule.setChannelPluginModuleLoaderFactoryForTest( + createJiti as unknown as PluginModuleLoaderFactory, + ); const rootDir = createTempDir(); const modulePath = path.join(rootDir, "extensions", "demo", "index.ts"); fs.mkdirSync(path.dirname(modulePath), { recursive: true }); diff --git a/src/channels/plugins/module-loader.ts b/src/channels/plugins/module-loader.ts index 4498b6c9e34..f16a0c2994c 100644 --- a/src/channels/plugins/module-loader.ts +++ b/src/channels/plugins/module-loader.ts @@ -6,11 +6,20 @@ import { isJavaScriptModulePath } from "../../plugins/native-module-require.js"; import { getCachedPluginModuleLoader, type PluginModuleLoaderCache, + type PluginModuleLoaderFactory, } from "../../plugins/plugin-module-loader-cache.js"; const nodeRequire = createRequire(import.meta.url); const SOURCE_MODULE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"]); const jitiLoaders: PluginModuleLoaderCache = new Map(); +let channelPluginModuleLoaderFactoryForTest: PluginModuleLoaderFactory | undefined; + +export function setChannelPluginModuleLoaderFactoryForTest( + factory?: PluginModuleLoaderFactory, +): void { + channelPluginModuleLoaderFactoryForTest = factory; + jitiLoaders.clear(); +} function hasNativeSourceRequireHook(modulePath: string): boolean { const extension = path.extname(modulePath).toLowerCase(); @@ -32,6 +41,9 @@ function loadModuleWithJiti(modulePath: string): unknown { loaderFilename: import.meta.url, tryNative: false, cacheScopeKey: "channel-plugin-module-loader", + ...(channelPluginModuleLoaderFactoryForTest + ? { createLoader: channelPluginModuleLoaderFactoryForTest } + : {}), }); return loadWithJiti(modulePath); } diff --git a/src/plugins/bundled-package-channel-metadata.test.ts b/src/plugins/bundled-package-channel-metadata.test.ts index ace62af5c42..410dcc82755 100644 --- a/src/plugins/bundled-package-channel-metadata.test.ts +++ b/src/plugins/bundled-package-channel-metadata.test.ts @@ -5,6 +5,7 @@ import { cleanupTempDirs, makeTempRepoRoot, writeJsonFile } from "../../test/hel vi.mock("./bundled-dir.js", () => ({ resolveBundledPluginsDir: vi.fn(), + resolveSourceCheckoutDependencyDiagnostic: vi.fn(() => null), })); import { resolveBundledPluginsDir } from "./bundled-dir.js"; diff --git a/src/plugins/doctor-contract-registry.test.ts b/src/plugins/doctor-contract-registry.test.ts index f07ef1429df..3718d7ecc3c 100644 --- a/src/plugins/doctor-contract-registry.test.ts +++ b/src/plugins/doctor-contract-registry.test.ts @@ -15,12 +15,16 @@ let clearPluginDoctorContractRegistryCache: typeof import("./doctor-contract-reg let collectRelevantDoctorPluginIdsForTouchedPaths: typeof import("./doctor-contract-registry.js").collectRelevantDoctorPluginIdsForTouchedPaths; let listPluginDoctorLegacyConfigRules: typeof import("./doctor-contract-registry.js").listPluginDoctorLegacyConfigRules; let listPluginDoctorSessionRouteStateOwners: typeof import("./doctor-contract-registry.js").listPluginDoctorSessionRouteStateOwners; +let setPluginDoctorContractRegistryModuleLoaderFactoryForTest: + | typeof import("./doctor-contract-registry.js").setPluginDoctorContractRegistryModuleLoaderFactoryForTest + | undefined; function makeTempDir(): string { return makeTrackedTempDir("openclaw-doctor-contract-registry", tempDirs); } afterEach(() => { + setPluginDoctorContractRegistryModuleLoaderFactoryForTest?.(undefined); cleanupTrackedTempDirs(tempDirs); }); @@ -33,7 +37,9 @@ describe("doctor-contract-registry module loader", () => { collectRelevantDoctorPluginIdsForTouchedPaths, listPluginDoctorLegacyConfigRules, listPluginDoctorSessionRouteStateOwners, + setPluginDoctorContractRegistryModuleLoaderFactoryForTest, } = await import("./doctor-contract-registry.js")); + setPluginDoctorContractRegistryModuleLoaderFactoryForTest(mocks.createJiti); clearPluginDoctorContractRegistryCache(); }); diff --git a/src/plugins/doctor-contract-registry.ts b/src/plugins/doctor-contract-registry.ts index 1b3438bebb2..c4126ae83d3 100644 --- a/src/plugins/doctor-contract-registry.ts +++ b/src/plugins/doctor-contract-registry.ts @@ -9,6 +9,7 @@ import type { PluginManifestRegistry } from "./manifest-registry.js"; import { createPluginModuleLoaderCache, getCachedPluginModuleLoader, + type PluginModuleLoaderFactory, type PluginModuleLoaderCache, } from "./plugin-module-loader-cache.js"; import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; @@ -44,12 +45,14 @@ type PluginDoctorContractEntry = { type PluginManifestRegistryRecord = PluginManifestRegistry["plugins"][number]; const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache(); +let moduleLoaderFactoryForTest: PluginModuleLoaderFactory | undefined; function loadPluginDoctorContractModule(modulePath: string): PluginDoctorContractModule { return getCachedPluginModuleLoader({ cache: moduleLoaders, modulePath, importerUrl: import.meta.url, + ...(moduleLoaderFactoryForTest ? { createLoader: moduleLoaderFactoryForTest } : {}), })(modulePath) as PluginDoctorContractModule; } @@ -296,6 +299,13 @@ export function clearPluginDoctorContractRegistryCache(): void { moduleLoaders.clear(); } +export function setPluginDoctorContractRegistryModuleLoaderFactoryForTest( + factory: PluginModuleLoaderFactory | undefined, +): void { + moduleLoaderFactoryForTest = factory; + moduleLoaders.clear(); +} + export function listPluginDoctorLegacyConfigRules(params?: { config?: OpenClawConfig; workspaceDir?: string; diff --git a/src/plugins/plugin-module-loader-cache.ts b/src/plugins/plugin-module-loader-cache.ts index 76b6a991a72..4ec504828da 100644 --- a/src/plugins/plugin-module-loader-cache.ts +++ b/src/plugins/plugin-module-loader-cache.ts @@ -49,7 +49,6 @@ export type PluginModuleLoaderStatsSnapshot = { const DEFAULT_PLUGIN_MODULE_LOADER_CACHE_ENTRIES = 128; const MAX_TRACKED_SOURCE_TRANSFORM_TARGETS = 24; -const JITI_FACTORY_OVERRIDE_KEY = Symbol.for("openclaw.pluginModuleLoaderJitiFactoryOverride"); const PLUGIN_SDK_IMPORT_SPECIFIER_PATTERN = /(?:\bfrom\s*["']|\bimport\s*\(\s*["']|\brequire\s*\(\s*["'])(?:openclaw|@openclaw)\/plugin-sdk(?:\/[^"']*)?["']/u; const requireForJiti = createRequire(import.meta.url); @@ -106,14 +105,6 @@ export function resetPluginModuleLoaderStatsForTest(): void { } function loadCreateJitiLoaderFactory(): PluginModuleLoaderFactory { - const override = ( - globalThis as typeof globalThis & { - [JITI_FACTORY_OVERRIDE_KEY]?: PluginModuleLoaderFactory; - } - )[JITI_FACTORY_OVERRIDE_KEY]; - if (override) { - return override; - } if (createJitiLoaderFactory) { return createJitiLoaderFactory; } diff --git a/src/plugins/setup-registry.test.ts b/src/plugins/setup-registry.test.ts index 28a48e16fd7..ba0f497b3ca 100644 --- a/src/plugins/setup-registry.test.ts +++ b/src/plugins/setup-registry.test.ts @@ -25,6 +25,9 @@ let resolvePluginSetupRegistry: typeof import("./setup-registry.js").resolvePlug let resolvePluginSetupProvider: typeof import("./setup-registry.js").resolvePluginSetupProvider; let resolvePluginSetupCliBackend: typeof import("./setup-registry.js").resolvePluginSetupCliBackend; let runPluginSetupConfigMigrations: typeof import("./setup-registry.js").runPluginSetupConfigMigrations; +let setPluginSetupRegistryModuleLoaderFactoryForTest: + | typeof import("./setup-registry.js").setPluginSetupRegistryModuleLoaderFactoryForTest + | undefined; function forceNodeRuntimeVersionsForTest(): () => void { const originalVersions = process.versions; @@ -167,6 +170,7 @@ async function expectNoUnhandledRejection(run: () => void | Promise): Prom } afterEach(() => { + setPluginSetupRegistryModuleLoaderFactoryForTest?.(undefined); cleanupTrackedTempDirs(tempDirs); }); @@ -180,7 +184,9 @@ describe("setup-registry module loader", () => { resolvePluginSetupProvider, resolvePluginSetupCliBackend, runPluginSetupConfigMigrations, + setPluginSetupRegistryModuleLoaderFactoryForTest, } = await import("./setup-registry.js")); + setPluginSetupRegistryModuleLoaderFactoryForTest(mocks.createJiti); clearPluginSetupRegistryCache(); }); diff --git a/src/plugins/setup-registry.ts b/src/plugins/setup-registry.ts index 7f10d44e550..44963eaefbf 100644 --- a/src/plugins/setup-registry.ts +++ b/src/plugins/setup-registry.ts @@ -9,6 +9,7 @@ import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-re import { createPluginModuleLoaderCache, getCachedPluginModuleLoader, + type PluginModuleLoaderFactory, type PluginModuleLoaderCache, } from "./plugin-module-loader-cache.js"; import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; @@ -87,16 +88,25 @@ const NOOP_LOGGER: PluginLogger = { }; const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache(); +let moduleLoaderFactoryForTest: PluginModuleLoaderFactory | undefined; export function clearPluginSetupRegistryCache(): void { moduleLoaders.clear(); } +export function setPluginSetupRegistryModuleLoaderFactoryForTest( + factory: PluginModuleLoaderFactory | undefined, +): void { + moduleLoaderFactoryForTest = factory; + moduleLoaders.clear(); +} + function getModuleLoader(modulePath: string) { return getCachedPluginModuleLoader({ cache: moduleLoaders, modulePath, importerUrl: import.meta.url, + ...(moduleLoaderFactoryForTest ? { createLoader: moduleLoaderFactoryForTest } : {}), }); } diff --git a/src/plugins/test-helpers/registry-jiti-mocks.ts b/src/plugins/test-helpers/registry-jiti-mocks.ts index 07f0e5a4878..c2534f97eec 100644 --- a/src/plugins/test-helpers/registry-jiti-mocks.ts +++ b/src/plugins/test-helpers/registry-jiti-mocks.ts @@ -6,14 +6,6 @@ const registryJitiMocks = vi.hoisted(() => ({ loadPluginManifestRegistry: vi.fn(), loadPluginRegistrySnapshot: vi.fn(), })); -const pluginModuleLoaderJitiFactoryOverrideKey = Symbol.for( - "openclaw.pluginModuleLoaderJitiFactoryOverride", -); - -vi.mock("jiti", () => ({ - createJiti: (...args: Parameters) => - registryJitiMocks.createJiti(...args), -})); vi.mock("../discovery.js", () => ({ discoverOpenClawPlugins: ( @@ -46,11 +38,6 @@ vi.mock("../plugin-registry.js", async (importOriginal) => { }; }); export function resetRegistryJitiMocks(): void { - ( - globalThis as typeof globalThis & { - [pluginModuleLoaderJitiFactoryOverrideKey]?: typeof registryJitiMocks.createJiti; - } - )[pluginModuleLoaderJitiFactoryOverrideKey] = registryJitiMocks.createJiti; registryJitiMocks.createJiti.mockReset(); registryJitiMocks.discoverOpenClawPlugins.mockReset(); registryJitiMocks.loadPluginManifestRegistry.mockReset(); diff --git a/test/vitest-ui-package-config.test.ts b/test/vitest-ui-package-config.test.ts index a5d07923ad9..0051a3f3312 100644 --- a/test/vitest-ui-package-config.test.ts +++ b/test/vitest-ui-package-config.test.ts @@ -3,21 +3,21 @@ import uiConfig from "../ui/vitest.config.ts"; import uiNodeConfig from "../ui/vitest.node.config.ts"; describe("ui package vitest config", () => { - it("keeps the standalone ui package on thread workers with isolation enabled", () => { + it("keeps the standalone ui package on thread workers without isolation", () => { expect(uiConfig.test?.pool).toBe("threads"); - expect(uiConfig.test?.isolate).toBe(true); + expect(uiConfig.test?.isolate).toBe(false); expect(uiConfig.test?.projects).toHaveLength(3); for (const project of uiConfig.test?.projects ?? []) { expect(project.test?.pool).toBe("threads"); - expect(project.test?.isolate).toBe(true); + expect(project.test?.isolate).toBe(false); expect(project.test?.runner).toBeUndefined(); } }); - it("keeps the standalone ui node config on thread workers with isolation enabled", () => { + it("keeps the standalone ui node config on thread workers without isolation", () => { expect(uiNodeConfig.test?.pool).toBe("threads"); - expect(uiNodeConfig.test?.isolate).toBe(true); + expect(uiNodeConfig.test?.isolate).toBe(false); expect(uiNodeConfig.test?.runner).toBeUndefined(); }); }); diff --git a/test/vitest/vitest.gateway-server.config.ts b/test/vitest/vitest.gateway-server.config.ts index b26a8098d54..3e01e65abbf 100644 --- a/test/vitest/vitest.gateway-server.config.ts +++ b/test/vitest/vitest.gateway-server.config.ts @@ -20,9 +20,7 @@ export function createGatewayServerVitestConfig(env?: Record ({ subscribeSessions: subscribeSessionsMock, })); +afterAll(() => { + vi.doUnmock("./gateway.ts"); + vi.doUnmock("./app-chat.ts"); + vi.doUnmock("./app-settings.ts"); + vi.doUnmock("./controllers/agents.ts"); + vi.doUnmock("./controllers/assistant-identity.ts"); + vi.doUnmock("./controllers/control-ui-bootstrap.ts"); + vi.doUnmock("./controllers/devices.ts"); + vi.doUnmock("./controllers/exec-approval.ts"); + vi.doUnmock("./controllers/health.ts"); + vi.doUnmock("./controllers/nodes.ts"); + vi.doUnmock("./controllers/sessions.ts"); + vi.resetModules(); +}); + function createHost(tab: Tab) { return { settings: { diff --git a/ui/src/ui/app-gateway.sessions.node.test.ts b/ui/src/ui/app-gateway.sessions.node.test.ts index 79c72939410..54960b02370 100644 --- a/ui/src/ui/app-gateway.sessions.node.test.ts +++ b/ui/src/ui/app-gateway.sessions.node.test.ts @@ -1,5 +1,5 @@ // @vitest-environment node -import { describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; const loadSessionsMock = vi.fn(); const loadChatHistoryMock = vi.fn(); @@ -38,6 +38,7 @@ vi.mock("./controllers/exec-approval.ts", () => ({ addExecApproval: vi.fn(), parseExecApprovalRequested: vi.fn(() => null), parseExecApprovalResolved: vi.fn(() => null), + pruneExecApprovalQueue: vi.fn((queue) => queue), removeExecApproval: vi.fn(), })); vi.mock("./controllers/nodes.ts", () => ({ @@ -58,6 +59,21 @@ const { addExecApproval } = await vi.importActual { + vi.doUnmock("./app-chat.ts"); + vi.doUnmock("./app-settings.ts"); + vi.doUnmock("./app-tool-stream.ts"); + vi.doUnmock("./controllers/agents.ts"); + vi.doUnmock("./controllers/assistant-identity.ts"); + vi.doUnmock("./controllers/chat.ts"); + vi.doUnmock("./controllers/devices.ts"); + vi.doUnmock("./controllers/exec-approval.ts"); + vi.doUnmock("./controllers/nodes.ts"); + vi.doUnmock("./controllers/sessions.ts"); + vi.doUnmock("./gateway.ts"); + vi.resetModules(); +}); + function createHost() { return { settings: { diff --git a/ui/src/ui/custom-theme.test.ts b/ui/src/ui/custom-theme.test.ts index 34ce8f111d5..4cddb4d0fe5 100644 --- a/ui/src/ui/custom-theme.test.ts +++ b/ui/src/ui/custom-theme.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { buildCustomThemeStyles, importCustomThemeFromUrl, @@ -9,6 +9,10 @@ import { } from "./custom-theme.ts"; import type { ImportedCustomTheme } from "./custom-theme.ts"; +afterEach(() => { + vi.unstubAllGlobals(); +}); + function createTweakcnPayload() { return { name: "Light Green", diff --git a/ui/vitest.config.ts b/ui/vitest.config.ts index ef72b3cb28e..6a101a8d5fe 100644 --- a/ui/vitest.config.ts +++ b/ui/vitest.config.ts @@ -6,9 +6,13 @@ import { } from "../test/vitest/vitest.shared.config.ts"; const sharedUiTestConfig = { - isolate: true, + isolate: false, pool: resolveDefaultVitestPool(), } as const; +const nodeDrivenBrowserLayoutTests = [ + "src/ui/chat/chat-responsive.browser.test.ts", + "src/ui/views/sessions.browser.test.ts", +] as const; export default defineConfig({ test: { @@ -30,7 +34,7 @@ export default defineConfig({ ...sharedUiTestConfig, deps: jsdomOptimizedDeps, name: "unit-node", - include: ["src/**/*.node.test.ts"], + include: ["src/**/*.node.test.ts", ...nodeDrivenBrowserLayoutTests], environment: "jsdom", setupFiles: ["./src/test-helpers/lit-warnings.setup.ts"], }, @@ -40,6 +44,7 @@ export default defineConfig({ ...sharedUiTestConfig, name: "browser", include: ["src/**/*.browser.test.ts"], + exclude: [...nodeDrivenBrowserLayoutTests], setupFiles: ["./src/test-helpers/lit-warnings.setup.ts"], browser: { enabled: true, diff --git a/ui/vitest.node.config.ts b/ui/vitest.node.config.ts index e6895270c78..5c475d8c136 100644 --- a/ui/vitest.node.config.ts +++ b/ui/vitest.node.config.ts @@ -4,10 +4,14 @@ import { resolveDefaultVitestPool } from "../test/vitest/vitest.shared.config.ts // Node-only tests for pure logic (no Playwright/browser dependency). export default defineConfig({ test: { - isolate: true, + isolate: false, pool: resolveDefaultVitestPool(), testTimeout: 120_000, - include: ["src/**/*.node.test.ts"], + include: [ + "src/**/*.node.test.ts", + "src/ui/chat/chat-responsive.browser.test.ts", + "src/ui/views/sessions.browser.test.ts", + ], environment: "node", }, }); From 2d65ead914c7883491810c82fc3f959288095343 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Gondhi Date: Thu, 7 May 2026 13:27:32 +0530 Subject: [PATCH 063/215] fix(skill-workshop): honor pending approval for tool suggestions [AI] (#78516) * fix: honor pending skill workshop approvals * addressing review-skill * addressing codex review * addressing codex review * fix: require approval before skill workshop apply * docs: add changelog entry for PR merge --- CHANGELOG.md | 1 + docs/plugins/skill-workshop.md | 12 +++- extensions/skill-workshop/index.test.ts | 67 +++++++++++++++++++++ extensions/skill-workshop/index.ts | 24 ++++++++ extensions/skill-workshop/src/prompt.ts | 4 +- extensions/skill-workshop/src/tool.ts | 72 +++-------------------- extensions/skill-workshop/src/workshop.ts | 3 +- 7 files changed, 113 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50ac6bfcf3b..2004166d6ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- fix(skill-workshop): honor pending approval for tool suggestions [AI]. (#78516) Thanks @pgondhi987. - Native chat: decode gateway-provided thinking metadata for the iOS/macOS picker so provider-specific levels such as `adaptive`, `xhigh`, and `max` appear without leaking unsupported default-model options. Thanks @BunsDev. - Agents/tools: fail `exec host=node` before `system.run` when the selected node is known to be disconnected, with an actionable reconnect message instead of a raw node invoke failure. Thanks @BunsDev. - Agents/models: accept legacy `anthropic-cli/*` model refs as Claude CLI runtime refs instead of failing model resolution with `Unknown model`. Thanks @BunsDev. diff --git a/docs/plugins/skill-workshop.md b/docs/plugins/skill-workshop.md index 48e1ed54ec2..0351efa0f35 100644 --- a/docs/plugins/skill-workshop.md +++ b/docs/plugins/skill-workshop.md @@ -357,7 +357,7 @@ Create a proposal. With `approvalPolicy: "pending"` (default), this queues inste ``` - + ```json { @@ -369,6 +369,9 @@ Create a proposal. With `approvalPolicy: "pending"` (default), this queues inste } ``` +With `approvalPolicy: "pending"`, `apply: true` still queues the proposal. Review it, then use +the `apply` action after approval. + @@ -417,6 +420,9 @@ Create a proposal. With `approvalPolicy: "pending"` (default), this queues inste Apply a pending proposal. +With `approvalPolicy: "pending"`, this action asks for operator approval before writing the +workspace skill. + ```json { "action": "apply", @@ -551,8 +557,8 @@ The guidance emphasizes: The write mode text changes with `approvalPolicy`: -- pending mode: queue suggestions; apply only after explicit approval -- auto mode: apply safe workspace-skill updates when clearly reusable +- pending mode: queue suggestions; use `apply` after explicit approval +- auto mode: apply safe workspace-skill updates unless `apply: false` queues instead ## Costs and runtime behavior diff --git a/extensions/skill-workshop/index.test.ts b/extensions/skill-workshop/index.test.ts index 68fe1fc5d73..45b92650890 100644 --- a/extensions/skill-workshop/index.test.ts +++ b/extensions/skill-workshop/index.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { AnyAgentTool } from "openclaw/plugin-sdk/agent-runtime"; +import type { PluginTrustedToolPolicyRegistration } from "openclaw/plugin-sdk/core"; import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { afterEach, describe, expect, it, vi } from "vitest"; import plugin, { @@ -617,6 +618,72 @@ describe("skill-workshop", () => { expect(await store.list("pending")).toHaveLength(1); }); + it("queues apply true suggestions in pending mode before explicit apply", async () => { + const workspaceDir = await makeTempDir(); + const stateDir = await makeTempDir(); + let tool: AnyAgentTool | undefined; + const api = createTestPluginApi({ + pluginConfig: { approvalPolicy: "pending" }, + runtime: { + agent: { + resolveAgentWorkspaceDir: () => workspaceDir, + }, + state: { + resolveStateDir: () => stateDir, + }, + } as never, + registerTool(registered) { + const resolved = + typeof registered === "function" ? registered({ workspaceDir }) : registered; + tool = Array.isArray(resolved) ? resolved[0] : (resolved ?? undefined); + }, + }); + + plugin.register(api); + const result = await tool?.execute?.("call-1", { + action: "suggest", + apply: true, + skillName: "screenshot-asset-workflow", + description: "Screenshot asset workflow", + body: "Verify dimensions, optimize the PNG, and run the relevant gate.", + }); + + expect(result?.details).toMatchObject({ status: "pending" }); + const proposalId = + (result?.details as { proposal?: { id?: string } } | undefined)?.proposal?.id ?? ""; + expect(proposalId).toBeTruthy(); + await expect( + fs.access(path.join(workspaceDir, "skills", "screenshot-asset-workflow", "SKILL.md")), + ).rejects.toMatchObject({ code: "ENOENT" }); + const store = new SkillWorkshopStore({ stateDir, workspaceDir }); + expect(await store.list("pending")).toHaveLength(1); + expect(await store.list("applied")).toHaveLength(0); + }); + + it("requires operator approval before applying queued proposals in pending mode", async () => { + let trustedPolicy: PluginTrustedToolPolicyRegistration | undefined; + const api = createTestPluginApi({ + pluginConfig: { approvalPolicy: "pending" }, + registerTrustedToolPolicy(policy) { + trustedPolicy = policy; + }, + }); + + plugin.register(api); + + const result = await trustedPolicy?.evaluate( + { toolName: "skill_workshop", params: { action: "apply", id: "proposal-1" } }, + { toolName: "skill_workshop" }, + ); + + expect(result).toMatchObject({ + requireApproval: { + title: "Apply workspace skill proposal", + allowedDecisions: ["allow-once", "deny"], + }, + }); + }); + it("uses the reviewer to propose existing skill repairs", async () => { const workspaceDir = await makeTempDir(); const stateDir = await makeTempDir(); diff --git a/extensions/skill-workshop/index.ts b/extensions/skill-workshop/index.ts index 3b649661e02..52d9320d15d 100644 --- a/extensions/skill-workshop/index.ts +++ b/extensions/skill-workshop/index.ts @@ -38,6 +38,30 @@ export default definePluginEntry({ }, ); + api.registerTrustedToolPolicy({ + id: "skill-workshop-apply-approval", + description: "Require operator approval before applying queued workspace skill proposals.", + evaluate(event) { + const config = resolveCurrentConfig(); + if ( + !config.enabled || + config.approvalPolicy === "auto" || + event.toolName !== "skill_workshop" || + event.params.action !== "apply" + ) { + return undefined; + } + return { + requireApproval: { + title: "Apply workspace skill proposal", + description: "Apply a queued workspace skill proposal.", + severity: "warning", + allowedDecisions: ["allow-once", "deny"], + }, + }; + }, + }); + api.on("before_prompt_build", async () => { const config = resolveCurrentConfig(); if (!config.enabled) { diff --git a/extensions/skill-workshop/src/prompt.ts b/extensions/skill-workshop/src/prompt.ts index df1d3025444..ed7ef9c6c3f 100644 --- a/extensions/skill-workshop/src/prompt.ts +++ b/extensions/skill-workshop/src/prompt.ts @@ -3,8 +3,8 @@ import type { SkillWorkshopConfig } from "./config.js"; export function buildWorkshopGuidance(config: SkillWorkshopConfig): string { const writeMode = config.approvalPolicy === "auto" - ? "Auto mode: apply safe workspace-skill updates when clearly reusable." - : "Pending mode: queue suggestions; apply only after explicit approval."; + ? "Auto mode: apply safe workspace-skill updates; apply=false queues instead." + : "Pending mode: queue suggestions; use apply action after explicit approval."; return [ "", "Use for durable procedural memory, not facts/preferences.", diff --git a/extensions/skill-workshop/src/tool.ts b/extensions/skill-workshop/src/tool.ts index fe0c191abf3..1fc75724cf1 100644 --- a/extensions/skill-workshop/src/tool.ts +++ b/extensions/skill-workshop/src/tool.ts @@ -2,14 +2,9 @@ import { randomUUID } from "node:crypto"; import { Type } from "typebox"; import { jsonResult, type OpenClawPluginApi } from "../api.js"; import type { SkillWorkshopConfig } from "./config.js"; -import { - applyProposalToWorkspace, - normalizeSkillName, - prepareProposalWrite, - writeSupportFile, -} from "./skills.js"; +import { applyProposalToWorkspace, normalizeSkillName, writeSupportFile } from "./skills.js"; import type { SkillChange, SkillProposal, SkillWorkshopStatus } from "./types.js"; -import { createStoreForContext, resolveWorkspaceDir } from "./workshop.js"; +import { applyOrStoreProposal, createStoreForContext, resolveWorkspaceDir } from "./workshop.js"; type ToolParams = { action?: string; @@ -150,65 +145,14 @@ export function createSkillWorkshopTool(params: { } if (action === "suggest") { const proposal = buildProposal({ workspaceDir, raw, source: "tool" }); - const shouldApply = - raw.apply === true || (raw.apply !== false && params.config.approvalPolicy === "auto"); - if (shouldApply) { - const prepared = await prepareProposalWrite({ - proposal, - maxSkillBytes: params.config.maxSkillBytes, - }); - const critical = prepared.findings.find((finding) => finding.severity === "critical"); - if (critical) { - const stored = await store.add( - { - ...proposal, - status: "quarantined", - updatedAt: Date.now(), - scanFindings: prepared.findings, - quarantineReason: critical.message, - }, - params.config.maxPending, - ); - return jsonResult({ status: "quarantined", proposal: stored }); - } - const applied = await applyProposalToWorkspace({ - proposal, - maxSkillBytes: params.config.maxSkillBytes, - }); - const stored = await store.add( - { - ...proposal, - status: "applied", - updatedAt: Date.now(), - scanFindings: applied.findings, - }, - params.config.maxPending, - ); - return jsonResult({ status: "applied", skillPath: applied.skillPath, proposal: stored }); - } - const prepared = await prepareProposalWrite({ + const result = await applyOrStoreProposal({ proposal, - maxSkillBytes: params.config.maxSkillBytes, + store, + config: params.config, + workspaceDir, + skipAutoApply: raw.apply === false, }); - const critical = prepared.findings.find((finding) => finding.severity === "critical"); - if (critical) { - const stored = await store.add( - { - ...proposal, - status: "quarantined", - updatedAt: Date.now(), - scanFindings: prepared.findings, - quarantineReason: critical.message, - }, - params.config.maxPending, - ); - return jsonResult({ status: "quarantined", proposal: stored }); - } - const stored = await store.add( - { ...proposal, scanFindings: prepared.findings }, - params.config.maxPending, - ); - return jsonResult({ status: "pending", proposal: stored }); + return jsonResult(result); } if (action === "apply") { if (!raw.id) { diff --git a/extensions/skill-workshop/src/workshop.ts b/extensions/skill-workshop/src/workshop.ts index 92e6ee064a2..4926c9a3d95 100644 --- a/extensions/skill-workshop/src/workshop.ts +++ b/extensions/skill-workshop/src/workshop.ts @@ -37,6 +37,7 @@ export async function applyOrStoreProposal(params: { store: SkillWorkshopStore; config: SkillWorkshopConfig; workspaceDir: string; + skipAutoApply?: boolean; }): Promise<{ status: "pending" | "applied" | "quarantined"; skillPath?: string; @@ -60,7 +61,7 @@ export async function applyOrStoreProposal(params: { ); return { status: "quarantined", proposal: stored }; } - if (params.config.approvalPolicy === "auto") { + if (params.config.approvalPolicy === "auto" && !params.skipAutoApply) { const applied = await applyProposalToWorkspace({ proposal: params.proposal, maxSkillBytes: params.config.maxSkillBytes, From 5a90179e8fdfefc501d21a603513f640f045c587 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 7 May 2026 02:59:42 -0500 Subject: [PATCH 064/215] feat(ui): show persistent chat context usage Summary: - Show a persistent compact Control UI/WebChat context usage indicator whenever fresh session token/context data is available below the high-pressure threshold. - Preserve stale usage snapshot hiding plus the existing high-pressure warning and compact-session action thresholds. - Update Control UI docs and changelog attribution. Fixes #46398. Refs #73744, #45048, #50071, #32188, and #62167. Verification: - pnpm docs:list - pnpm format:docs:check - pnpm exec oxfmt --check --threads=1 CHANGELOG.md docs/web/control-ui.md ui/src/styles/chat/layout.css ui/src/ui/chat/context-notice.ts ui/src/ui/chat/run-controls.test.ts - pnpm test ui/src/ui/chat/run-controls.test.ts - pnpm changed:lanes --json selected core, coreTests, docs only - Blacksmith Testbox pnpm check:changed passed on tbx_01kr0pvxy0ssp70p3qe49j5dcb: https://github.com/openclaw/openclaw/actions/runs/25483307211 - GitHub PR checks for 04b8ad2e0969130b857ae8c5c67b68b67f9d9487 were clean before merge. --- CHANGELOG.md | 1 + docs/web/control-ui.md | 2 +- ui/src/styles/chat/layout.css | 24 +++++++++++- ui/src/ui/chat/context-notice.ts | 61 +++++++++++++++++++---------- ui/src/ui/chat/run-controls.test.ts | 39 +++++++++++------- 5 files changed, 90 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2004166d6ca..506ae25cdb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,6 +127,7 @@ Docs: https://docs.openclaw.ai - QA/Mantis: accept Blacksmith Testbox `tbx_...` lease ids from desktop smoke warmup, so provider overrides do not fail before inspect/run. Thanks @vincentkoc. - Plugins/SDK: add bounded `before_agent_finalize` retry instructions so workflow plugins can request one more model pass. Thanks @100yenadmin. - Plugin SDK: add plugin-owned `SessionEntry` slot projection and scoped trusted-policy session extension reads. (#75609; replaces part of #73384/#74483) Thanks @100yenadmin. +- Control UI/WebChat: show a persistent compact context usage indicator from fresh session token data before the high-pressure warning state, while keeping the existing compaction prompt threshold. Fixes #46398; refs #45048, #50071, and #73744. Thanks @walterwkchoy, @AxelrodAI, @Brissux, @vincentkoc, and @BunsDev. - Docs: clarify that IRC uses raw TCP/TLS sockets outside operator-managed forward proxy routing, so direct IRC egress should be explicitly approved before enabling IRC. Thanks @jesse-merhi. - Dependencies: refresh runtime and provider packages including Pi 0.73.0, ACPX adapters, OpenAI, Anthropic, Slack, and TypeScript native preview, while keeping the Bedrock runtime installer override pinned below the Windows ARM Node 24 npm resolver failure. - Contributor PRs: require external pull requests to include after-fix real behavior proof from a real OpenClaw setup, with terminal screenshots, console output, redacted runtime logs, linked artifacts, and copied live output treated as valid evidence while unit tests, mocks, lint, typechecks, snapshots, and CI remain supplemental only. diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index d6b3c4a4df1..980861de3f4 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -166,7 +166,7 @@ Imported themes are stored only in the current browser profile. They are not wri - The chat header model and thinking pickers patch the active session immediately through `sessions.patch`; they are persistent session overrides, not one-turn-only send options. - Typing `/new` in the Control UI creates and switches to the same fresh dashboard session as New Chat. Typing `/reset` keeps the Gateway's explicit in-place reset for the current session. - The chat model picker requests the Gateway's configured model view. If `agents.defaults.models` is present, that allowlist drives the picker. Otherwise the picker shows explicit `models.providers.*.models` entries plus providers with usable auth. The full catalog stays available through the debug `models.list` RPC with `view: "all"`. - - When fresh Gateway session usage reports show high context pressure, the chat composer area shows a context notice and, at recommended compaction levels, a compact button that runs the normal session compaction path. Stale token snapshots are hidden until the Gateway reports fresh usage again. + - When fresh Gateway session usage reports include current context tokens, the chat composer area shows a compact context usage indicator. It switches to warning styling at high context pressure and, at recommended compaction levels, shows a compact button that runs the normal session compaction path. Stale token snapshots are hidden until the Gateway reports fresh usage again. diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css index 1a14e6eb6a9..b0abb2f189f 100644 --- a/ui/src/styles/chat/layout.css +++ b/ui/src/styles/chat/layout.css @@ -146,7 +146,7 @@ flex-shrink: 0; } -/* Context usage warning pill */ +/* Context usage pill */ .context-notice { align-self: center; display: inline-flex; @@ -168,6 +168,10 @@ animation: fade-in 0.2s var(--ease-out); } +.context-notice--usage { + border-color: color-mix(in srgb, var(--border) 70%, transparent); +} + .context-notice__icon { width: 16px; height: 16px; @@ -175,6 +179,24 @@ stroke: currentColor; } +.context-notice__meter { + position: relative; + width: 46px; + height: 6px; + overflow: hidden; + flex-shrink: 0; + border-radius: var(--radius-full); + background: color-mix(in srgb, currentColor 16%, transparent); +} + +.context-notice__meter-fill { + position: absolute; + inset: 0 auto 0 0; + max-width: 100%; + border-radius: inherit; + background: currentColor; +} + .context-notice__detail { color: color-mix(in srgb, currentColor 72%, var(--muted)); font-variant-numeric: tabular-nums; diff --git a/ui/src/ui/chat/context-notice.ts b/ui/src/ui/chat/context-notice.ts index 42fed23018a..80d30b764f8 100644 --- a/ui/src/ui/chat/context-notice.ts +++ b/ui/src/ui/chat/context-notice.ts @@ -59,21 +59,30 @@ export function getContextNoticeViewModel( detail: string; color: string; bg: string; + warning: boolean; compactRecommended: boolean; } | null { if (session?.totalTokensFresh === false) { return null; } - const used = session?.totalTokens ?? 0; + const used = session?.totalTokens; const limit = session?.contextTokens ?? defaultContextTokens ?? 0; - if (!used || !limit) { + if (typeof used !== "number" || !Number.isFinite(used) || used < 0 || !limit) { return null; } const ratio = used / limit; - if (ratio < CONTEXT_NOTICE_RATIO) { - return null; - } const pct = Math.min(Math.round(ratio * 100), 100); + const warning = ratio >= CONTEXT_NOTICE_RATIO; + if (!warning) { + return { + pct, + detail: `${formatTokensCompact(used)} / ${formatTokensCompact(limit)}`, + color: "var(--muted)", + bg: "color-mix(in srgb, var(--muted) 8%, transparent)", + warning, + compactRecommended: false, + }; + } // Read theme semantic tokens so color tracks the active theme (Dash, dark, light ...). const { warnRgb, dangerRgb } = getThemeNoticeColors(); const [wr, wg, wb] = warnRgb; @@ -90,6 +99,7 @@ export function getContextNoticeViewModel( detail: `${formatTokensCompact(used)} / ${formatTokensCompact(limit)}`, color, bg, + warning, compactRecommended: ratio >= CONTEXT_COMPACT_RATIO, }; } @@ -107,25 +117,34 @@ export function renderContextNotice( const compactDisabled = options.compactDisabled === true || options.compactBusy === true; return html`
- - - - - + ${model.warning + ? html` + + + + + + ` + : html` + + `} ${model.pct}% context used ${model.detail} ${canRenderCompact diff --git a/ui/src/ui/chat/run-controls.test.ts b/ui/src/ui/chat/run-controls.test.ts index 320b62d34e3..539b900c457 100644 --- a/ui/src/ui/chat/run-controls.test.ts +++ b/ui/src/ui/chat/run-controls.test.ts @@ -234,7 +234,7 @@ describe("context notice", () => { resetContextNoticeThemeCacheForTest(); }); - it("renders only for fresh high current usage", () => { + it("renders persistent fresh context usage and keeps high-usage warning behavior", () => { const container = document.createElement("div"); vi.spyOn(window, "getComputedStyle").mockReturnValue({ getPropertyValue: (name: string) => @@ -242,19 +242,28 @@ describe("context notice", () => { } as CSSStyleDeclaration); resetContextNoticeThemeCacheForTest(); - expect( - getContextNoticeViewModel( - { - key: "main", - kind: "direct", - updatedAt: null, - inputTokens: 757_300, - totalTokens: 46_000, - contextTokens: 200_000, - }, - 200_000, - ), - ).toBeNull(); + const lowUsageSession: GatewaySessionRow = { + key: "main", + kind: "direct", + updatedAt: null, + inputTokens: 757_300, + totalTokens: 46_000, + contextTokens: 200_000, + }; + const lowUsage = getContextNoticeViewModel(lowUsageSession, 200_000); + expect(lowUsage).toMatchObject({ + pct: 23, + detail: "46k / 200k", + warning: false, + compactRecommended: false, + }); + render(renderContextNotice(lowUsageSession, 200_000), container); + expect(container.textContent).toContain("23% context used"); + expect(container.textContent).toContain("46k / 200k"); + expect(container.querySelector(".context-notice--usage")).not.toBeNull(); + expect(container.querySelector(".context-notice__meter")).not.toBeNull(); + expect(container.querySelector(".context-notice__icon")).toBeNull(); + expect(container.textContent).not.toContain("757.3k / 200k"); const session: GatewaySessionRow = { key: "main", @@ -272,6 +281,8 @@ describe("context notice", () => { expect(container.textContent).not.toContain("757.3k / 200k"); const notice = container.querySelector(".context-notice"); expect(notice).not.toBeNull(); + expect(notice?.classList.contains("context-notice--warning")).toBe(true); + expect(notice?.getAttribute("title")).toBe("Session context usage: 190k / 200k (95%)"); expect(notice?.style.getPropertyValue("--ctx-color")).toContain("rgb("); expect(notice?.style.getPropertyValue("--ctx-color")).toContain("4, 5, 6"); expect(notice?.style.getPropertyValue("--ctx-color")).not.toContain("NaN"); From 0003f3f755fdc177d070b9ed772b85c5f4572486 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Gondhi Date: Thu, 7 May 2026 13:30:05 +0530 Subject: [PATCH 065/215] feishu: honor config write policy for dynamic agents [AI] (#78520) * fix: honor Feishu config write policy for dynamic agents * docs: add changelog entry for PR merge --- CHANGELOG.md | 1 + extensions/feishu/src/bot.test.ts | 50 ++++++ extensions/feishu/src/bot.ts | 6 + extensions/feishu/src/comment-handler.test.ts | 41 +++++ extensions/feishu/src/comment-handler.ts | 6 + extensions/feishu/src/dynamic-agent.test.ts | 155 ++++++++++++++++++ extensions/feishu/src/dynamic-agent.ts | 8 +- 7 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 extensions/feishu/src/dynamic-agent.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 506ae25cdb4..6291f78cb1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -141,6 +141,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- feishu: honor config write policy for dynamic agents [AI]. (#78520) Thanks @pgondhi987. - fix(skill-workshop): honor pending approval for tool suggestions [AI]. (#78516) Thanks @pgondhi987. - Native chat: decode gateway-provided thinking metadata for the iOS/macOS picker so provider-specific levels such as `adaptive`, `xhigh`, and `max` appear without leaking unsupported default-model options. Thanks @BunsDev. - Agents/tools: fail `exec host=node` before `system.run` when the selected node is known to be disconnected, with an actionable reconnect message instead of a raw node invoke failure. Thanks @BunsDev. diff --git a/extensions/feishu/src/bot.test.ts b/extensions/feishu/src/bot.test.ts index 61ddacec34c..de52e7a3147 100644 --- a/extensions/feishu/src/bot.test.ts +++ b/extensions/feishu/src/bot.test.ts @@ -249,6 +249,7 @@ const { mockTouchBinding, mockResolveFeishuReasoningPreviewEnabled, mockTranscribeFirstAudio, + mockMaybeCreateDynamicAgent, } = vi.hoisted(() => ({ mockCreateFeishuReplyDispatcher: vi.fn(() => ({ dispatcher: createReplyDispatcher(), @@ -284,6 +285,7 @@ const { mockTouchBinding: vi.fn(), mockResolveFeishuReasoningPreviewEnabled: vi.fn(() => false), mockTranscribeFirstAudio: vi.fn(), + mockMaybeCreateDynamicAgent: vi.fn(), })); vi.mock("./reply-dispatcher.js", () => ({ @@ -312,6 +314,10 @@ vi.mock("./client.js", () => ({ createFeishuClient: mockCreateFeishuClient, })); +vi.mock("./dynamic-agent.js", () => ({ + maybeCreateDynamicAgent: mockMaybeCreateDynamicAgent, +})); + vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => { const actual = await vi.importActual( "openclaw/plugin-sdk/conversation-runtime", @@ -406,6 +412,7 @@ describe("handleFeishuMessage ACP routing", () => { mockTouchBinding.mockReset(); mockResolveFeishuReasoningPreviewEnabled.mockReset().mockReturnValue(false); mockTranscribeFirstAudio.mockReset().mockResolvedValue(undefined); + mockMaybeCreateDynamicAgent.mockReset().mockResolvedValue({ created: false }); mockResolveAgentRoute.mockReset().mockReturnValue({ ...buildDefaultResolveRoute(), sessionKey: "agent:main:feishu:direct:ou_sender_1", @@ -605,6 +612,7 @@ describe("handleFeishuMessage command authorization", () => { mockResolveBoundConversation.mockReset().mockReturnValue(null); mockTouchBinding.mockReset(); mockTranscribeFirstAudio.mockReset().mockResolvedValue(undefined); + mockMaybeCreateDynamicAgent.mockReset().mockResolvedValue({ created: false }); mockResolveAgentRoute.mockReturnValue(buildDefaultResolveRoute()); mockCreateFeishuClient.mockReturnValue({ contact: { @@ -679,6 +687,48 @@ describe("handleFeishuMessage command authorization", () => { expect(mockEnqueueSystemEvent).not.toHaveBeenCalled(); }); + it("passes disabled config-write policy to dynamic agent creation", async () => { + mockShouldComputeCommandAuthorized.mockReturnValue(false); + + const cfg: ClawdbotConfig = { + channels: { + feishu: { + dmPolicy: "open", + allowFrom: ["*"], + configWrites: false, + dynamicAgentCreation: { + enabled: true, + }, + }, + }, + } as ClawdbotConfig; + + const event: FeishuMessageEvent = { + sender: { + sender_id: { + open_id: "ou-attacker", + }, + }, + message: { + message_id: "msg-dynamic-config-writes-disabled", + chat_id: "oc-dm", + chat_type: "p2p", + message_type: "text", + content: JSON.stringify({ text: "hello" }), + }, + }; + + await dispatchMessage({ cfg, event }); + + expect(mockMaybeCreateDynamicAgent).toHaveBeenCalledWith( + expect.objectContaining({ + senderOpenId: "ou-attacker", + configWritesAllowed: false, + }), + ); + expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1); + }); + it("blocks open DMs when a restrictive allowlist does not match", async () => { const cfg: ClawdbotConfig = { commands: { useAccessGroups: true }, diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 1b1552907ac..4f7376cb55f 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -1,3 +1,4 @@ +import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-writes"; import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing"; import { ensureConfiguredBindingRouteReady, @@ -806,6 +807,11 @@ export async function handleFeishuMessage(params: { runtime, senderOpenId: ctx.senderOpenId, dynamicCfg, + configWritesAllowed: resolveChannelConfigWrites({ + cfg, + channelId: "feishu", + accountId: account.accountId, + }), log: (msg) => log(msg), }); if (result.created) { diff --git a/extensions/feishu/src/comment-handler.test.ts b/extensions/feishu/src/comment-handler.test.ts index fce428116e0..8245af9f383 100644 --- a/extensions/feishu/src/comment-handler.test.ts +++ b/extensions/feishu/src/comment-handler.test.ts @@ -297,6 +297,47 @@ describe("handleFeishuCommentEvent", () => { expect(deliverCommentThreadTextMock).not.toHaveBeenCalled(); }); + it("passes disabled config-write policy to dynamic agent creation", async () => { + const runtime = createTestRuntime({ + resolveAgentRoute: () => buildResolvedRoute("default"), + }); + setFeishuRuntime(runtime); + + await handleFeishuCommentEvent({ + cfg: buildConfig({ + channels: { + feishu: { + enabled: true, + dmPolicy: "open", + allowFrom: ["*"], + configWrites: false, + dynamicAgentCreation: { + enabled: true, + }, + }, + }, + }), + accountId: "default", + event: { event_id: "evt_1" }, + botOpenId: "ou_bot", + runtime: { + log: vi.fn(), + error: vi.fn(), + } as never, + }); + + expect(maybeCreateDynamicAgentMock).toHaveBeenCalledWith( + expect.objectContaining({ + senderOpenId: "ou_sender", + configWritesAllowed: false, + }), + ); + const dispatchReplyFromConfig = runtime.channel.reply.dispatchReplyFromConfig as ReturnType< + typeof vi.fn + >; + expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1); + }); + it("issues a pairing challenge in the comment thread when dmPolicy=pairing", async () => { const runtime = createTestRuntime(); setFeishuRuntime(runtime); diff --git a/extensions/feishu/src/comment-handler.ts b/extensions/feishu/src/comment-handler.ts index df1a21ba035..ae6e01d2a3b 100644 --- a/extensions/feishu/src/comment-handler.ts +++ b/extensions/feishu/src/comment-handler.ts @@ -1,3 +1,4 @@ +import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-writes"; import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing"; import { resolveOpenDmAllowlistAccess } from "openclaw/plugin-sdk/security-runtime"; import { resolveFeishuRuntimeAccount } from "./accounts.js"; @@ -163,6 +164,11 @@ export async function handleFeishuCommentEvent( runtime: core, senderOpenId: turn.senderId, dynamicCfg, + configWritesAllowed: resolveChannelConfigWrites({ + cfg: params.cfg, + channelId: "feishu", + accountId: account.accountId, + }), log: (message) => log(message), }); if (dynamicResult.created) { diff --git a/extensions/feishu/src/dynamic-agent.test.ts b/extensions/feishu/src/dynamic-agent.test.ts new file mode 100644 index 00000000000..ad2676639d2 --- /dev/null +++ b/extensions/feishu/src/dynamic-agent.test.ts @@ -0,0 +1,155 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js"; +import { maybeCreateDynamicAgent } from "./dynamic-agent.js"; + +let tempRoot: string; + +beforeEach(async () => { + tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openclaw-feishu-agent-")); +}); + +afterEach(async () => { + await fs.promises.rm(tempRoot, { recursive: true, force: true }); +}); + +function createRuntime() { + const replaceConfigFile = vi.fn(async () => {}); + return { + runtime: { + config: { + replaceConfigFile, + }, + } as unknown as PluginRuntime, + replaceConfigFile, + }; +} + +function createDynamicConfig() { + return { + enabled: true, + workspaceTemplate: path.join(tempRoot, "workspace-{agentId}"), + agentDirTemplate: path.join(tempRoot, "agent-{agentId}"), + }; +} + +async function pathExists(target: string): Promise { + return fs.promises + .stat(target) + .then(() => true) + .catch((err: unknown) => { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + return false; + } + throw err; + }); +} + +describe("maybeCreateDynamicAgent", () => { + it("does not persist dynamic agents when config writes are disabled", async () => { + const { runtime, replaceConfigFile } = createRuntime(); + const dynamicCfg = createDynamicConfig(); + + const result = await maybeCreateDynamicAgent({ + cfg: { + channels: { feishu: { configWrites: false } }, + agents: { list: [] }, + bindings: [], + } as OpenClawConfig, + runtime, + senderOpenId: "ou_sender", + dynamicCfg, + configWritesAllowed: false, + log: vi.fn(), + }); + + expect(result).toEqual({ + created: false, + updatedCfg: { + channels: { feishu: { configWrites: false } }, + agents: { list: [] }, + bindings: [], + }, + }); + expect(replaceConfigFile).not.toHaveBeenCalled(); + expect(await pathExists(path.join(tempRoot, "workspace-feishu-ou_sender"))).toBe(false); + expect(await pathExists(path.join(tempRoot, "agent-feishu-ou_sender"))).toBe(false); + }); + + it("persists a sender agent and direct binding when config writes are allowed", async () => { + const { runtime, replaceConfigFile } = createRuntime(); + + const result = await maybeCreateDynamicAgent({ + cfg: { + agents: { list: [] }, + bindings: [], + } as OpenClawConfig, + runtime, + senderOpenId: "ou_sender", + dynamicCfg: createDynamicConfig(), + configWritesAllowed: true, + log: vi.fn(), + }); + + expect(result.created).toBe(true); + expect(result.agentId).toBe("feishu-ou_sender"); + expect(replaceConfigFile).toHaveBeenCalledTimes(1); + expect(replaceConfigFile).toHaveBeenCalledWith({ + nextConfig: expect.objectContaining({ + agents: { + list: [ + { + id: "feishu-ou_sender", + workspace: path.join(tempRoot, "workspace-feishu-ou_sender"), + agentDir: path.join(tempRoot, "agent-feishu-ou_sender"), + }, + ], + }, + bindings: [ + { + agentId: "feishu-ou_sender", + match: { + channel: "feishu", + peer: { kind: "direct", id: "ou_sender" }, + }, + }, + ], + }), + afterWrite: { mode: "auto" }, + }); + expect(await pathExists(path.join(tempRoot, "workspace-feishu-ou_sender"))).toBe(true); + expect(await pathExists(path.join(tempRoot, "agent-feishu-ou_sender"))).toBe(true); + }); + + it("keeps the maxAgents limit before adding a missing binding", async () => { + const { runtime, replaceConfigFile } = createRuntime(); + + const result = await maybeCreateDynamicAgent({ + cfg: { + agents: { + list: [ + { + id: "feishu-ou_sender", + workspace: path.join(tempRoot, "existing-workspace"), + agentDir: path.join(tempRoot, "existing-agent"), + }, + ], + }, + bindings: [], + } as OpenClawConfig, + runtime, + senderOpenId: "ou_sender", + dynamicCfg: { + ...createDynamicConfig(), + maxAgents: 1, + }, + configWritesAllowed: true, + log: vi.fn(), + }); + + expect(result.created).toBe(false); + expect(replaceConfigFile).not.toHaveBeenCalled(); + }); +}); diff --git a/extensions/feishu/src/dynamic-agent.ts b/extensions/feishu/src/dynamic-agent.ts index 1e23079cb7f..76afb12990d 100644 --- a/extensions/feishu/src/dynamic-agent.ts +++ b/extensions/feishu/src/dynamic-agent.ts @@ -19,9 +19,15 @@ export async function maybeCreateDynamicAgent(params: { runtime: PluginRuntime; senderOpenId: string; dynamicCfg: DynamicAgentCreationConfig; + configWritesAllowed: boolean; log: (msg: string) => void; }): Promise { - const { cfg, runtime, senderOpenId, dynamicCfg, log } = params; + const { cfg, runtime, senderOpenId, dynamicCfg, configWritesAllowed, log } = params; + + if (!configWritesAllowed) { + log(`feishu: config writes disabled, not creating agent for ${senderOpenId}`); + return { created: false, updatedCfg: cfg }; + } // Check if there's already a binding for this user const existingBindings = cfg.bindings ?? []; From c6e6b31643ee3672778e5f83dfe8f27ed5100ee6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 09:00:26 +0100 Subject: [PATCH 066/215] docs: clarify legacy compatibility policy --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index c1a02f00248..71fac20f004 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work. - Owner boundary: fix owner-specific behavior in the owner module. Shared/core gets generic seams only; no owner ids, dependency strings, defaults, migrations, or recovery policy. If a bug names an extension or its dependency, start in that extension and add a generic core seam only when multiple owners need it. - Dependency ownership follows runtime ownership: extension-only deps stay plugin-local; root deps only for core imports or intentionally internalized bundled plugin runtime. - Legacy config repair: doctor/fix paths, not startup/load-time core migrations. +- No legacy compatibility in core/runtime paths. When old config/store shapes need support, add an `openclaw doctor --fix` rewrite/repair rule with tests and keep runtime code on the canonical contract. - Core test asserting extension-specific behavior: move to owner extension or generic contract test. - New seams: backwards-compatible, documented, versioned. Third-party plugins exist. - Channels: `src/channels/**` is implementation; plugin authors get SDK seams. From 330ba1fa319407315d5173cffd318cf123361fe3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 05:07:32 +0100 Subject: [PATCH 067/215] refactor: move canvas to plugin surfaces --- .agents/skills/crabbox/SKILL.md | 57 +- .crabbox.yaml | 7 +- .github/pull_request_template.md | 2 +- .github/workflows/ci.yml | 171 +- .gitignore | 2 + .oxfmtrc.jsonc | 1 + AGENTS.md | 2 +- CHANGELOG.md | 1 + CONTRIBUTING.md | 4 +- Dockerfile | 6 +- README.md | 7 +- apps/android/README.md | 2 +- .../main/java/ai/openclaw/app/NodeRuntime.kt | 2 +- .../openclaw/app/gateway/GatewayProtocol.kt | 2 +- .../ai/openclaw/app/gateway/GatewaySession.kt | 118 +- .../ai/openclaw/app/node/InvokeDispatcher.kt | 16 +- .../app/gateway/GatewaySessionInvokeTest.kt | 61 +- .../GatewaySessionInvokeTimeoutTest.kt | 22 - .../openclaw/app/node/InvokeDispatcherTest.kt | 2 +- .../Sources/Model/NodeAppModel+Canvas.swift | 23 +- .../Sources/OpenClaw/CanvasManager.swift | 12 +- .../Sources/OpenClaw/GatewayConnection.swift | 5 +- .../NodeMode/MacNodeModeCoordinator.swift | 12 +- .../OpenClaw/NodeMode/MacNodeRuntime.swift | 31 +- .../OpenClawProtocol/GatewayModels.swift | 5847 ------ .../MacGatewayChatTransportMappingTests.swift | 2 +- .../MacNodeRuntimeTests.swift | 24 + .../Sources/OpenClawKit/BridgeFrames.swift | 3 - .../OpenClawKit/GatewayNodeSession.swift | 112 +- .../OpenClawProtocol/GatewayModels.swift | 10 +- docs/automation/cron-jobs.md | 2 - docs/cli/cron.md | 2 - docs/cli/nodes.md | 2 +- docs/cli/plugins.md | 6 +- docs/cli/update.md | 5 - docs/concepts/system-prompt.md | 7 +- docs/concepts/typebox.md | 12 +- docs/gateway/bridge-protocol.md | 6 +- docs/gateway/configuration-examples.md | 2 +- docs/gateway/configuration-reference.md | 18 +- docs/gateway/configuration.md | 2 +- docs/gateway/doctor.md | 1 - docs/gateway/protocol.md | 27 +- docs/gateway/security/index.md | 63 - docs/help/faq.md | 2 +- docs/install/development-channels.md | 30 +- docs/install/updating.md | 4 - docs/platforms/ios.md | 2 +- docs/plugins/plugin-inventory.md | 1 + docs/plugins/reference.md | 1 + docs/plugins/reference/canvas.md | 19 + docs/plugins/sdk-entrypoints.md | 9 +- docs/plugins/sdk-overview.md | 35 +- docs/refactor/canvas.md | 131 + docs/reference/RELEASING.md | 29 +- docs/tools/index.md | 4 +- docs/tools/plugin.md | 4 - docs/tools/subagents.md | 5 - docs/tools/web.md | 13 +- extensions/canvas/cli-metadata.ts | 18 + extensions/canvas/index.ts | 98 + extensions/canvas/openclaw.plugin.json | 40 + extensions/canvas/package.json | 22 + extensions/canvas/runtime-api.ts | 42 + extensions/canvas/scripts/bundle-a2ui.mjs | 228 + extensions/canvas/scripts/copy-a2ui.d.mts | 4 + .../canvas/scripts/copy-a2ui.mjs | 13 +- extensions/canvas/setup-api.ts | 11 + .../canvas/src}/a2ui-jsonl.ts | 0 extensions/canvas/src/capability.ts | 25 + extensions/canvas/src/cli-helpers.ts | 42 + extensions/canvas/src/cli.test.ts | 75 + extensions/canvas/src/cli.ts | 428 + .../canvas/src/config-migration.test.ts | 75 + extensions/canvas/src/config-migration.ts | 54 + extensions/canvas/src/config.test.ts | 87 + extensions/canvas/src/config.ts | 126 + .../canvas/src/documents.test.ts | 2 +- .../canvas/src/documents.ts | 8 +- .../canvas/src/host-url.test.ts | 2 +- extensions/canvas/src/host-url.ts | 15 + .../canvas/src/host/a2ui-app}/bootstrap.js | 109 +- .../src/host/a2ui-app}/rolldown.config.mjs | 22 +- .../canvas/src/host}/a2ui-shared.ts | 2 +- .../canvas/src/host}/a2ui.ts | 19 +- extensions/canvas/src/host/a2ui/.bundle.hash | 1 + .../canvas/src/host}/a2ui/index.html | 0 .../canvas/src/host}/file-resolver.test.ts | 2 +- .../canvas/src/host}/file-resolver.ts | 6 +- .../canvas/src/host}/server.state-dir.test.ts | 4 +- .../canvas/src/host}/server.test.ts | 4 +- .../canvas/src/host}/server.ts | 24 +- extensions/canvas/src/http-route.ts | 72 + extensions/canvas/src/tool.test.ts | 92 + .../canvas/src/tool.ts | 153 +- .../codex/src/app-server/auth-bridge.ts | 22 +- .../codex/src/app-server/computer-use.ts | 65 +- .../codex/src/app-server/event-projector.ts | 4 +- extensions/codex/src/app-server/models.ts | 6 +- .../json/DynamicToolCallParams.json | 31 +- .../json/v2/ErrorNotification.json | 124 +- .../json/v2/GetAccountResponse.json | 61 +- .../json/v2/ModelListResponse.json | 197 +- .../json/v2/ThreadResumeResponse.json | 1984 +- .../json/v2/ThreadStartResponse.json | 1984 +- .../json/v2/TurnCompletedNotification.json | 1270 +- .../json/v2/TurnStartResponse.json | 1266 +- .../typescript/AbsolutePathBuf.ts | 14 - .../typescript/AgentPath.ts | 5 - .../typescript/ApplyPatchApprovalParams.ts | 24 - .../typescript/ApplyPatchApprovalResponse.ts | 6 - .../protocol-generated/typescript/AuthMode.ts | 8 - .../typescript/ClientInfo.ts | 5 - .../typescript/ClientNotification.ts | 5 - .../typescript/ClientRequest.ts | 244 - .../typescript/CollaborationMode.ts | 10 - .../typescript/ContentItem.ts | 9 - .../typescript/ConversationGitInfo.ts | 9 - .../typescript/ConversationSummary.ts | 19 - .../typescript/ExecCommandApprovalParams.ts | 22 - .../typescript/ExecCommandApprovalResponse.ts | 6 - .../typescript/ExecPolicyAmendment.ts | 12 - .../typescript/FileChange.ts | 8 - .../typescript/ForcedLoginMethod.ts | 5 - .../typescript/FunctionCallOutputBody.ts | 6 - .../FunctionCallOutputContentItem.ts | 12 - .../typescript/FuzzyFileSearchMatchType.ts | 5 - .../typescript/FuzzyFileSearchParams.ts | 9 - .../typescript/FuzzyFileSearchResponse.ts | 6 - .../typescript/FuzzyFileSearchResult.ts | 16 - ...yFileSearchSessionCompletedNotification.ts | 5 - .../FuzzyFileSearchSessionStartParams.ts | 5 - .../FuzzyFileSearchSessionStartResponse.ts | 5 - .../FuzzyFileSearchSessionStopParams.ts | 5 - .../FuzzyFileSearchSessionStopResponse.ts | 5 - .../FuzzyFileSearchSessionUpdateParams.ts | 5 - .../FuzzyFileSearchSessionUpdateResponse.ts | 5 - ...zzyFileSearchSessionUpdatedNotification.ts | 10 - .../typescript/GetAuthStatusParams.ts | 5 - .../typescript/GetAuthStatusResponse.ts | 10 - .../GetConversationSummaryParams.ts | 6 - .../GetConversationSummaryResponse.ts | 6 - .../typescript/GitDiffToRemoteParams.ts | 5 - .../typescript/GitDiffToRemoteResponse.ts | 6 - .../protocol-generated/typescript/GitSha.ts | 5 - .../typescript/ImageDetail.ts | 5 - .../typescript/InitializeCapabilities.ts | 18 - .../typescript/InitializeParams.ts | 10 - .../typescript/InitializeResponse.ts | 22 - .../typescript/InputModality.ts | 8 - .../typescript/InternalSessionSource.ts | 5 - .../typescript/LocalShellAction.ts | 6 - .../typescript/LocalShellExecAction.ts | 11 - .../typescript/LocalShellStatus.ts | 5 - .../typescript/MessagePhase.ts | 11 - .../protocol-generated/typescript/ModeKind.ts | 8 - .../typescript/NetworkPolicyAmendment.ts | 6 - .../typescript/NetworkPolicyRuleAction.ts | 5 - .../typescript/ParsedCommand.ts | 20 - .../typescript/Personality.ts | 5 - .../protocol-generated/typescript/PlanType.ts | 17 - .../typescript/RealtimeConversationVersion.ts | 5 - .../typescript/RealtimeOutputModality.ts | 5 - .../typescript/RealtimeVoice.ts | 24 - .../typescript/RealtimeVoicesList.ts | 11 - .../typescript/ReasoningEffort.ts | 8 - .../typescript/ReasoningItemContent.ts | 7 - .../ReasoningItemReasoningSummary.ts | 5 - .../typescript/ReasoningSummary.ts | 10 - .../typescript/RequestId.ts | 5 - .../protocol-generated/typescript/Resource.ts | 19 - .../typescript/ResourceContent.ts | 27 - .../typescript/ResourceTemplate.ts | 16 - .../typescript/ResponseItem.ts | 63 - .../typescript/ReviewDecision.ts | 17 - .../typescript/ServerNotification.ts | 150 - .../typescript/ServerRequest.ts | 47 - .../typescript/ServiceTier.ts | 5 - .../typescript/SessionSource.ts | 15 - .../protocol-generated/typescript/Settings.ts | 13 - .../typescript/SubAgentSource.ts | 20 - .../protocol-generated/typescript/ThreadId.ts | 5 - .../typescript/ThreadMemoryMode.ts | 5 - .../protocol-generated/typescript/Tool.ts | 18 - .../typescript/Verbosity.ts | 9 - .../typescript/WebSearchAction.ts | 9 - .../typescript/WebSearchContextSize.ts | 5 - .../typescript/WebSearchLocation.ts | 10 - .../typescript/WebSearchMode.ts | 5 - .../typescript/WebSearchToolConfig.ts | 11 - .../protocol-generated/typescript/index.ts | 86 - .../typescript/serde_json/JsonValue.ts | 11 - .../typescript/v2/Account.ts | 9 - .../v2/AccountLoginCompletedNotification.ts | 9 - .../AccountRateLimitsUpdatedNotification.ts | 6 - .../v2/AccountUpdatedNotification.ts | 7 - .../typescript/v2/ActivePermissionProfile.ts | 22 - .../v2/ActivePermissionProfileModification.ts | 9 - .../v2/AddCreditsNudgeCreditType.ts | 5 - .../v2/AddCreditsNudgeEmailStatus.ts | 5 - .../v2/AdditionalFileSystemPermissions.ts | 18 - .../v2/AdditionalNetworkPermissions.ts | 5 - .../v2/AdditionalPermissionProfile.ts | 13 - .../v2/AgentMessageDeltaNotification.ts | 10 - .../typescript/v2/AnalyticsConfig.ts | 14 - .../typescript/v2/AppBranding.ts | 15 - .../typescript/v2/AppInfo.ts | 32 - .../v2/AppListUpdatedNotification.ts | 9 - .../typescript/v2/AppMetadata.ts | 20 - .../typescript/v2/AppReview.ts | 5 - .../typescript/v2/AppScreenshot.ts | 5 - .../typescript/v2/AppSummary.ts | 14 - .../typescript/v2/AppToolApproval.ts | 5 - .../typescript/v2/AppToolsConfig.ts | 8 - .../typescript/v2/ApprovalsReviewer.ts | 12 - .../typescript/v2/AppsConfig.ts | 17 - .../typescript/v2/AppsDefaultConfig.ts | 9 - .../typescript/v2/AppsListParams.ts | 25 - .../typescript/v2/AppsListResponse.ts | 16 - .../typescript/v2/AskForApproval.ts | 18 - .../typescript/v2/AutoReviewDecisionSource.ts | 8 - .../typescript/v2/ByteRange.ts | 5 - .../typescript/v2/CancelLoginAccountParams.ts | 5 - .../v2/CancelLoginAccountResponse.ts | 6 - .../typescript/v2/CancelLoginAccountStatus.ts | 5 - .../v2/ChatgptAuthTokensRefreshParams.ts | 18 - .../v2/ChatgptAuthTokensRefreshReason.ts | 5 - .../v2/ChatgptAuthTokensRefreshResponse.ts | 9 - .../typescript/v2/CodexErrorInfo.ts | 27 - .../typescript/v2/CollabAgentState.ts | 6 - .../typescript/v2/CollabAgentStatus.ts | 12 - .../typescript/v2/CollabAgentTool.ts | 5 - .../v2/CollabAgentToolCallStatus.ts | 5 - .../v2/CollaborationModeListParams.ts | 8 - .../v2/CollaborationModeListResponse.ts | 9 - .../typescript/v2/CollaborationModeMask.ts | 15 - .../typescript/v2/CommandAction.ts | 10 - .../v2/CommandExecOutputDeltaNotification.ts | 31 - .../typescript/v2/CommandExecOutputStream.ts | 8 - .../typescript/v2/CommandExecParams.ts | 107 - .../typescript/v2/CommandExecResizeParams.ts | 19 - .../v2/CommandExecResizeResponse.ts | 8 - .../typescript/v2/CommandExecResponse.ts | 25 - .../typescript/v2/CommandExecTerminalSize.ts | 17 - .../v2/CommandExecTerminateParams.ts | 14 - .../v2/CommandExecTerminateResponse.ts | 8 - .../typescript/v2/CommandExecWriteParams.ts | 23 - .../typescript/v2/CommandExecWriteResponse.ts | 8 - .../v2/CommandExecutionApprovalDecision.ts | 13 - ...CommandExecutionOutputDeltaNotification.ts | 10 - .../CommandExecutionRequestApprovalParams.ts | 62 - ...CommandExecutionRequestApprovalResponse.ts | 8 - .../typescript/v2/CommandExecutionSource.ts | 9 - .../typescript/v2/CommandExecutionStatus.ts | 5 - .../typescript/v2/CommandMigration.ts | 5 - .../typescript/v2/Config.ts | 57 - .../typescript/v2/ConfigBatchWriteParams.ts | 17 - .../typescript/v2/ConfigEdit.ts | 7 - .../typescript/v2/ConfigLayer.ts | 12 - .../typescript/v2/ConfigLayerMetadata.ts | 6 - .../typescript/v2/ConfigLayerSource.ts | 27 - .../typescript/v2/ConfigReadParams.ts | 13 - .../typescript/v2/ConfigReadResponse.ts | 12 - .../typescript/v2/ConfigRequirements.ts | 21 - .../v2/ConfigRequirementsReadResponse.ts | 11 - .../typescript/v2/ConfigValueWriteParams.ts | 16 - .../v2/ConfigWarningNotification.ts | 23 - .../typescript/v2/ConfigWriteResponse.ts | 16 - .../typescript/v2/ConfiguredHookHandler.ts | 14 - .../v2/ConfiguredHookMatcherGroup.ts | 9 - .../v2/ContextCompactedNotification.ts | 8 - .../typescript/v2/CreditsSnapshot.ts | 5 - .../v2/DeprecationNoticeNotification.ts | 14 - .../typescript/v2/DeviceKeyAlgorithm.ts | 8 - .../typescript/v2/DeviceKeyCreateParams.ts | 16 - .../typescript/v2/DeviceKeyCreateResponse.ts | 18 - .../typescript/v2/DeviceKeyProtectionClass.ts | 11 - .../v2/DeviceKeyProtectionPolicy.ts | 8 - .../typescript/v2/DeviceKeyPublicParams.ts | 8 - .../typescript/v2/DeviceKeyPublicResponse.ts | 18 - .../typescript/v2/DeviceKeySignParams.ts | 9 - .../typescript/v2/DeviceKeySignPayload.ts | 68 - .../typescript/v2/DeviceKeySignResponse.ts | 20 - .../v2/DynamicToolCallOutputContentItem.ts | 7 - .../typescript/v2/DynamicToolCallParams.ts | 13 - .../typescript/v2/DynamicToolCallResponse.ts | 9 - .../typescript/v2/DynamicToolCallStatus.ts | 5 - .../typescript/v2/DynamicToolSpec.ts | 12 - .../typescript/v2/ErrorNotification.ts | 11 - .../typescript/v2/ExecPolicyAmendment.ts | 5 - .../typescript/v2/ExperimentalFeature.ts | 38 - .../ExperimentalFeatureEnablementSetParams.ts | 13 - ...xperimentalFeatureEnablementSetResponse.ts | 10 - .../v2/ExperimentalFeatureListParams.ts | 14 - .../v2/ExperimentalFeatureListResponse.ts | 13 - .../typescript/v2/ExperimentalFeatureStage.ts | 10 - .../v2/ExternalAgentConfigDetectParams.ts | 14 - .../v2/ExternalAgentConfigDetectResponse.ts | 6 - ...lAgentConfigImportCompletedNotification.ts | 5 - .../v2/ExternalAgentConfigImportParams.ts | 8 - .../v2/ExternalAgentConfigImportResponse.ts | 5 - .../v2/ExternalAgentConfigMigrationItem.ts | 15 - .../ExternalAgentConfigMigrationItemType.ts | 14 - .../typescript/v2/FeedbackUploadParams.ts | 12 - .../typescript/v2/FeedbackUploadResponse.ts | 5 - .../v2/FileChangeApprovalDecision.ts | 5 - .../v2/FileChangeOutputDeltaNotification.ts | 15 - .../v2/FileChangePatchUpdatedNotification.ts | 11 - .../v2/FileChangeRequestApprovalParams.ts | 18 - .../v2/FileChangeRequestApprovalResponse.ts | 6 - .../typescript/v2/FileSystemAccessMode.ts | 5 - .../typescript/v2/FileSystemPath.ts | 10 - .../typescript/v2/FileSystemSandboxEntry.ts | 7 - .../typescript/v2/FileSystemSpecialPath.ts | 11 - .../typescript/v2/FileUpdateChange.ts | 6 - .../typescript/v2/FsChangedNotification.ts | 18 - .../typescript/v2/FsCopyParams.ts | 22 - .../typescript/v2/FsCopyResponse.ts | 8 - .../typescript/v2/FsCreateDirectoryParams.ts | 18 - .../v2/FsCreateDirectoryResponse.ts | 8 - .../typescript/v2/FsGetMetadataParams.ts | 14 - .../typescript/v2/FsGetMetadataResponse.ts | 29 - .../typescript/v2/FsReadDirectoryEntry.ts | 21 - .../typescript/v2/FsReadDirectoryParams.ts | 14 - .../typescript/v2/FsReadDirectoryResponse.ts | 14 - .../typescript/v2/FsReadFileParams.ts | 14 - .../typescript/v2/FsReadFileResponse.ts | 13 - .../typescript/v2/FsRemoveParams.ts | 22 - .../typescript/v2/FsRemoveResponse.ts | 8 - .../typescript/v2/FsUnwatchParams.ts | 13 - .../typescript/v2/FsUnwatchResponse.ts | 8 - .../typescript/v2/FsWatchParams.ts | 18 - .../typescript/v2/FsWatchResponse.ts | 14 - .../typescript/v2/FsWriteFileParams.ts | 18 - .../typescript/v2/FsWriteFileResponse.ts | 8 - .../typescript/v2/GetAccountParams.ts | 14 - .../v2/GetAccountRateLimitsResponse.ts | 15 - .../typescript/v2/GetAccountResponse.ts | 6 - .../typescript/v2/GitInfo.ts | 5 - .../typescript/v2/GrantedPermissionProfile.ts | 10 - .../typescript/v2/GuardianApprovalReview.ts | 18 - .../v2/GuardianApprovalReviewAction.ts | 34 - .../v2/GuardianApprovalReviewStatus.ts | 13 - .../typescript/v2/GuardianCommandSource.ts | 5 - .../typescript/v2/GuardianRiskLevel.ts | 8 - .../v2/GuardianUserAuthorization.ts | 8 - .../v2/GuardianWarningNotification.ts | 14 - .../v2/HookCompletedNotification.ts | 10 - .../typescript/v2/HookErrorInfo.ts | 5 - .../typescript/v2/HookEventName.ts | 11 - .../typescript/v2/HookExecutionMode.ts | 5 - .../typescript/v2/HookHandlerType.ts | 5 - .../typescript/v2/HookMetadata.ts | 23 - .../typescript/v2/HookMigration.ts | 5 - .../typescript/v2/HookOutputEntry.ts | 6 - .../typescript/v2/HookOutputEntryKind.ts | 5 - .../typescript/v2/HookPromptFragment.ts | 5 - .../typescript/v2/HookRunStatus.ts | 5 - .../typescript/v2/HookRunSummary.ts | 28 - .../typescript/v2/HookScope.ts | 5 - .../typescript/v2/HookSource.ts | 15 - .../typescript/v2/HookStartedNotification.ts | 10 - .../typescript/v2/HooksListEntry.ts | 12 - .../typescript/v2/HooksListParams.ts | 10 - .../typescript/v2/HooksListResponse.ts | 6 - .../v2/ItemCompletedNotification.ts | 14 - ...dianApprovalReviewCompletedNotification.ts | 36 - ...ardianApprovalReviewStartedNotification.ts | 34 - .../typescript/v2/ItemStartedNotification.ts | 14 - .../v2/ListMcpServerStatusParams.ts | 20 - .../v2/ListMcpServerStatusResponse.ts | 13 - .../typescript/v2/LoginAccountParams.ts | 27 - .../typescript/v2/LoginAccountResponse.ts | 27 - .../typescript/v2/LogoutAccountResponse.ts | 5 - .../typescript/v2/ManagedHooksRequirements.ts | 15 - .../typescript/v2/MarketplaceAddParams.ts | 9 - .../typescript/v2/MarketplaceAddResponse.ts | 10 - .../typescript/v2/MarketplaceInterface.ts | 5 - .../typescript/v2/MarketplaceLoadErrorInfo.ts | 6 - .../typescript/v2/MarketplaceRemoveParams.ts | 5 - .../v2/MarketplaceRemoveResponse.ts | 9 - .../v2/MarketplaceUpgradeErrorInfo.ts | 5 - .../typescript/v2/MarketplaceUpgradeParams.ts | 5 - .../v2/MarketplaceUpgradeResponse.ts | 11 - .../typescript/v2/McpAuthStatus.ts | 5 - .../typescript/v2/McpElicitationArrayType.ts | 5 - .../v2/McpElicitationBooleanSchema.ts | 11 - .../v2/McpElicitationBooleanType.ts | 5 - .../v2/McpElicitationConstOption.ts | 5 - .../typescript/v2/McpElicitationEnumSchema.ts | 11 - .../McpElicitationLegacyTitledEnumSchema.ts | 13 - .../v2/McpElicitationMultiSelectEnumSchema.ts | 9 - .../v2/McpElicitationNumberSchema.ts | 13 - .../typescript/v2/McpElicitationNumberType.ts | 5 - .../typescript/v2/McpElicitationObjectType.ts | 5 - .../v2/McpElicitationPrimitiveSchema.ts | 13 - .../typescript/v2/McpElicitationSchema.ts | 18 - .../McpElicitationSingleSelectEnumSchema.ts | 9 - .../v2/McpElicitationStringFormat.ts | 5 - .../v2/McpElicitationStringSchema.ts | 15 - .../typescript/v2/McpElicitationStringType.ts | 5 - .../v2/McpElicitationTitledEnumItems.ts | 6 - ...pElicitationTitledMultiSelectEnumSchema.ts | 15 - ...ElicitationTitledSingleSelectEnumSchema.ts | 13 - .../v2/McpElicitationUntitledEnumItems.ts | 9 - ...licitationUntitledMultiSelectEnumSchema.ts | 15 - ...icitationUntitledSingleSelectEnumSchema.ts | 12 - .../typescript/v2/McpResourceReadParams.ts | 5 - .../typescript/v2/McpResourceReadResponse.ts | 6 - .../v2/McpServerElicitationAction.ts | 5 - .../v2/McpServerElicitationRequestParams.ts | 27 - .../v2/McpServerElicitationRequestResponse.ts | 19 - .../typescript/v2/McpServerMigration.ts | 5 - ...cpServerOauthLoginCompletedNotification.ts | 9 - .../v2/McpServerOauthLoginParams.ts | 9 - .../v2/McpServerOauthLoginResponse.ts | 5 - .../typescript/v2/McpServerRefreshResponse.ts | 5 - .../typescript/v2/McpServerStartupState.ts | 5 - .../typescript/v2/McpServerStatus.ts | 15 - .../typescript/v2/McpServerStatusDetail.ts | 5 - .../v2/McpServerStatusUpdatedNotification.ts | 10 - .../typescript/v2/McpServerToolCallParams.ts | 12 - .../v2/McpServerToolCallResponse.ts | 11 - .../typescript/v2/McpToolCallError.ts | 5 - .../v2/McpToolCallProgressNotification.ts | 10 - .../typescript/v2/McpToolCallResult.ts | 10 - .../typescript/v2/McpToolCallStatus.ts | 5 - .../typescript/v2/MemoryCitation.ts | 6 - .../typescript/v2/MemoryCitationEntry.ts | 10 - .../typescript/v2/MemoryResetResponse.ts | 5 - .../typescript/v2/MergeStrategy.ts | 5 - .../typescript/v2/MigrationDetails.ts | 18 - .../v2/MockExperimentalMethodParams.ts | 10 - .../v2/MockExperimentalMethodResponse.ts | 10 - .../protocol-generated/typescript/v2/Model.ts | 25 - .../typescript/v2/ModelAvailabilityNux.ts | 5 - .../typescript/v2/ModelListParams.ts | 18 - .../typescript/v2/ModelListResponse.ts | 13 - .../v2/ModelProviderCapabilitiesReadParams.ts | 5 - .../ModelProviderCapabilitiesReadResponse.ts | 9 - .../typescript/v2/ModelRerouteReason.ts | 5 - .../v2/ModelReroutedNotification.ts | 12 - .../typescript/v2/ModelUpgradeInfo.ts | 10 - .../typescript/v2/ModelVerification.ts | 5 - .../v2/ModelVerificationNotification.ts | 10 - .../typescript/v2/NetworkAccess.ts | 5 - .../typescript/v2/NetworkApprovalContext.ts | 6 - .../typescript/v2/NetworkApprovalProtocol.ts | 5 - .../typescript/v2/NetworkDomainPermission.ts | 5 - .../typescript/v2/NetworkPolicyAmendment.ts | 6 - .../typescript/v2/NetworkPolicyRuleAction.ts | 5 - .../typescript/v2/NetworkRequirements.ts | 40 - .../v2/NetworkUnixSocketPermission.ts | 5 - .../typescript/v2/NonSteerableTurnKind.ts | 5 - .../typescript/v2/OverriddenMetadata.ts | 11 - .../typescript/v2/PatchApplyStatus.ts | 5 - .../typescript/v2/PatchChangeKind.ts | 8 - .../typescript/v2/PermissionGrantScope.ts | 5 - .../typescript/v2/PermissionProfile.ts | 14 - .../PermissionProfileFileSystemPermissions.ts | 8 - .../v2/PermissionProfileModificationParams.ts | 9 - .../v2/PermissionProfileNetworkPermissions.ts | 5 - .../v2/PermissionProfileSelectionParams.ts | 10 - .../v2/PermissionsRequestApprovalParams.ts | 14 - .../v2/PermissionsRequestApprovalResponse.ts | 14 - .../typescript/v2/PlanDeltaNotification.ts | 14 - .../typescript/v2/PluginAuthPolicy.ts | 5 - .../typescript/v2/PluginAvailability.ts | 5 - .../typescript/v2/PluginDetail.ts | 17 - .../typescript/v2/PluginInstallParams.ts | 10 - .../typescript/v2/PluginInstallPolicy.ts | 5 - .../typescript/v2/PluginInstallResponse.ts | 10 - .../typescript/v2/PluginInterface.ts | 46 - .../typescript/v2/PluginListParams.ts | 12 - .../typescript/v2/PluginListResponse.ts | 11 - .../typescript/v2/PluginMarketplaceEntry.ts | 17 - .../typescript/v2/PluginReadParams.ts | 10 - .../typescript/v2/PluginReadResponse.ts | 6 - .../typescript/v2/PluginShareDeleteParams.ts | 5 - .../v2/PluginShareDeleteResponse.ts | 5 - .../typescript/v2/PluginShareListItem.ts | 11 - .../typescript/v2/PluginShareListParams.ts | 5 - .../typescript/v2/PluginShareListResponse.ts | 6 - .../typescript/v2/PluginShareSaveParams.ts | 6 - .../typescript/v2/PluginShareSaveResponse.ts | 5 - .../typescript/v2/PluginSkillReadParams.ts | 9 - .../typescript/v2/PluginSkillReadResponse.ts | 5 - .../typescript/v2/PluginSource.ts | 9 - .../typescript/v2/PluginSummary.ts | 23 - .../typescript/v2/PluginUninstallParams.ts | 5 - .../typescript/v2/PluginUninstallResponse.ts | 5 - .../typescript/v2/PluginsMigration.ts | 5 - .../typescript/v2/ProfileV2.ts | 39 - .../typescript/v2/RateLimitReachedType.ts | 10 - .../typescript/v2/RateLimitSnapshot.ts | 17 - .../typescript/v2/RateLimitWindow.ts | 9 - .../RawResponseItemCompletedNotification.ts | 10 - .../typescript/v2/ReasoningEffortOption.ts | 6 - .../ReasoningSummaryPartAddedNotification.ts | 10 - .../ReasoningSummaryTextDeltaNotification.ts | 11 - .../v2/ReasoningTextDeltaNotification.ts | 11 - .../RemoteControlClientConnectionAudience.ts | 8 - .../RemoteControlClientEnrollmentAudience.ts | 8 - .../v2/RemoteControlConnectionStatus.ts | 5 - .../RemoteControlStatusChangedNotification.ts | 12 - .../typescript/v2/RequestPermissionProfile.ts | 10 - .../typescript/v2/ResidencyRequirement.ts | 5 - .../typescript/v2/ReviewDelivery.ts | 5 - .../typescript/v2/ReviewStartParams.ts | 15 - .../typescript/v2/ReviewStartResponse.ts | 15 - .../typescript/v2/ReviewTarget.ts | 16 - .../typescript/v2/SandboxMode.ts | 5 - .../typescript/v2/SandboxPolicy.ts | 17 - .../typescript/v2/SandboxWorkspaceWrite.ts | 10 - .../v2/SendAddCreditsNudgeEmailParams.ts | 6 - .../v2/SendAddCreditsNudgeEmailResponse.ts | 6 - .../v2/ServerRequestResolvedNotification.ts | 6 - .../typescript/v2/SessionMigration.ts | 5 - .../typescript/v2/SessionSource.ts | 13 - .../typescript/v2/SkillDependencies.ts | 6 - .../typescript/v2/SkillErrorInfo.ts | 5 - .../typescript/v2/SkillInterface.ts | 13 - .../typescript/v2/SkillMetadata.ts | 21 - .../typescript/v2/SkillScope.ts | 5 - .../typescript/v2/SkillSummary.ts | 14 - .../typescript/v2/SkillToolDependency.ts | 12 - .../v2/SkillsChangedNotification.ts | 11 - .../typescript/v2/SkillsConfigWriteParams.ts | 16 - .../v2/SkillsConfigWriteResponse.ts | 5 - .../typescript/v2/SkillsListEntry.ts | 11 - .../v2/SkillsListExtraRootsForCwd.ts | 5 - .../typescript/v2/SkillsListParams.ts | 19 - .../typescript/v2/SkillsListResponse.ts | 6 - .../typescript/v2/SortDirection.ts | 5 - .../typescript/v2/SubagentMigration.ts | 5 - .../v2/TerminalInteractionNotification.ts | 11 - .../typescript/v2/TextElement.ts | 15 - .../typescript/v2/TextPosition.ts | 14 - .../typescript/v2/TextRange.ts | 6 - .../typescript/v2/Thread.ts | 79 - .../typescript/v2/ThreadActiveFlag.ts | 5 - ...ThreadApproveGuardianDeniedActionParams.ts | 12 - ...readApproveGuardianDeniedActionResponse.ts | 5 - .../typescript/v2/ThreadArchiveParams.ts | 5 - .../typescript/v2/ThreadArchiveResponse.ts | 5 - .../v2/ThreadArchivedNotification.ts | 5 - .../ThreadBackgroundTerminalsCleanParams.ts | 5 - .../ThreadBackgroundTerminalsCleanResponse.ts | 5 - .../typescript/v2/ThreadClosedNotification.ts | 5 - .../typescript/v2/ThreadCompactStartParams.ts | 5 - .../v2/ThreadCompactStartResponse.ts | 5 - .../v2/ThreadDecrementElicitationParams.ts | 13 - .../v2/ThreadDecrementElicitationResponse.ts | 17 - .../typescript/v2/ThreadForkParams.ts | 63 - .../typescript/v2/ThreadForkResponse.ts | 46 - .../typescript/v2/ThreadGoal.ts | 15 - .../typescript/v2/ThreadGoalClearParams.ts | 5 - .../typescript/v2/ThreadGoalClearResponse.ts | 5 - .../v2/ThreadGoalClearedNotification.ts | 5 - .../typescript/v2/ThreadGoalGetParams.ts | 5 - .../typescript/v2/ThreadGoalGetResponse.ts | 6 - .../typescript/v2/ThreadGoalSetParams.ts | 11 - .../typescript/v2/ThreadGoalSetResponse.ts | 6 - .../typescript/v2/ThreadGoalStatus.ts | 5 - .../v2/ThreadGoalUpdatedNotification.ts | 10 - .../v2/ThreadIncrementElicitationParams.ts | 13 - .../v2/ThreadIncrementElicitationResponse.ts | 17 - .../typescript/v2/ThreadInjectItemsParams.ts | 12 - .../v2/ThreadInjectItemsResponse.ts | 5 - .../typescript/v2/ThreadItem.ts | 156 - .../typescript/v2/ThreadListParams.ts | 55 - .../typescript/v2/ThreadListResponse.ts | 20 - .../typescript/v2/ThreadLoadedListParams.ts | 14 - .../typescript/v2/ThreadLoadedListResponse.ts | 15 - .../v2/ThreadMemoryModeSetParams.ts | 6 - .../v2/ThreadMemoryModeSetResponse.ts | 5 - .../v2/ThreadMetadataGitInfoUpdateParams.ts | 21 - .../v2/ThreadMetadataUpdateParams.ts | 14 - .../v2/ThreadMetadataUpdateResponse.ts | 6 - .../v2/ThreadNameUpdatedNotification.ts | 5 - .../typescript/v2/ThreadReadParams.ts | 11 - .../typescript/v2/ThreadReadResponse.ts | 6 - .../v2/ThreadRealtimeAppendAudioParams.ts | 9 - .../v2/ThreadRealtimeAppendAudioResponse.ts | 8 - .../v2/ThreadRealtimeAppendTextParams.ts | 8 - .../v2/ThreadRealtimeAppendTextResponse.ts | 8 - .../typescript/v2/ThreadRealtimeAudioChunk.ts | 14 - .../v2/ThreadRealtimeClosedNotification.ts | 8 - .../v2/ThreadRealtimeErrorNotification.ts | 8 - .../v2/ThreadRealtimeItemAddedNotification.ts | 9 - .../v2/ThreadRealtimeListVoicesParams.ts | 8 - .../v2/ThreadRealtimeListVoicesResponse.ts | 9 - ...eadRealtimeOutputAudioDeltaNotification.ts | 12 - .../v2/ThreadRealtimeSdpNotification.ts | 8 - .../v2/ThreadRealtimeStartParams.ts | 22 - .../v2/ThreadRealtimeStartResponse.ts | 8 - .../v2/ThreadRealtimeStartTransport.ts | 17 - .../v2/ThreadRealtimeStartedNotification.ts | 13 - .../typescript/v2/ThreadRealtimeStopParams.ts | 8 - .../v2/ThreadRealtimeStopResponse.ts | 8 - ...readRealtimeTranscriptDeltaNotification.ts | 16 - ...hreadRealtimeTranscriptDoneNotification.ts | 16 - .../typescript/v2/ThreadResumeParams.ts | 73 - .../typescript/v2/ThreadResumeResponse.ts | 46 - .../typescript/v2/ThreadRollbackParams.ts | 14 - .../typescript/v2/ThreadRollbackResponse.ts | 15 - .../typescript/v2/ThreadSetNameParams.ts | 5 - .../typescript/v2/ThreadSetNameResponse.ts | 5 - .../typescript/v2/ThreadShellCommandParams.ts | 14 - .../v2/ThreadShellCommandResponse.ts | 5 - .../typescript/v2/ThreadSortKey.ts | 5 - .../typescript/v2/ThreadSourceKind.ts | 15 - .../typescript/v2/ThreadStartParams.ts | 66 - .../typescript/v2/ThreadStartResponse.ts | 46 - .../typescript/v2/ThreadStartSource.ts | 5 - .../v2/ThreadStartedNotification.ts | 6 - .../typescript/v2/ThreadStatus.ts | 10 - .../v2/ThreadStatusChangedNotification.ts | 6 - .../typescript/v2/ThreadTokenUsage.ts | 10 - .../v2/ThreadTokenUsageUpdatedNotification.ts | 10 - .../typescript/v2/ThreadTurnsListParams.ts | 20 - .../typescript/v2/ThreadTurnsListResponse.ts | 20 - .../typescript/v2/ThreadUnarchiveParams.ts | 5 - .../typescript/v2/ThreadUnarchiveResponse.ts | 6 - .../v2/ThreadUnarchivedNotification.ts | 5 - .../typescript/v2/ThreadUnsubscribeParams.ts | 5 - .../v2/ThreadUnsubscribeResponse.ts | 6 - .../typescript/v2/ThreadUnsubscribeStatus.ts | 5 - .../typescript/v2/TokenUsageBreakdown.ts | 11 - .../v2/ToolRequestUserInputAnswer.ts | 8 - .../v2/ToolRequestUserInputOption.ts | 8 - .../v2/ToolRequestUserInputParams.ts | 14 - .../v2/ToolRequestUserInputQuestion.ts | 16 - .../v2/ToolRequestUserInputResponse.ts | 11 - .../typescript/v2/ToolsV2.ts | 6 - .../protocol-generated/typescript/v2/Turn.ts | 33 - .../v2/TurnCompletedNotification.ts | 6 - .../v2/TurnDiffUpdatedNotification.ts | 9 - .../typescript/v2/TurnEnvironmentParams.ts | 6 - .../typescript/v2/TurnError.ts | 10 - .../typescript/v2/TurnInterruptParams.ts | 5 - .../typescript/v2/TurnInterruptResponse.ts | 5 - .../typescript/v2/TurnPlanStep.ts | 6 - .../typescript/v2/TurnPlanStepStatus.ts | 5 - .../v2/TurnPlanUpdatedNotification.ts | 11 - .../typescript/v2/TurnStartParams.ts | 89 - .../typescript/v2/TurnStartResponse.ts | 6 - .../typescript/v2/TurnStartedNotification.ts | 6 - .../typescript/v2/TurnStatus.ts | 5 - .../typescript/v2/TurnSteerParams.ts | 18 - .../typescript/v2/TurnSteerResponse.ts | 5 - .../typescript/v2/UserInput.ts | 18 - .../typescript/v2/WarningNotification.ts | 14 - .../typescript/v2/WebSearchAction.ts | 9 - ...indowsSandboxSetupCompletedNotification.ts | 10 - .../typescript/v2/WindowsSandboxSetupMode.ts | 5 - .../v2/WindowsSandboxSetupStartParams.ts | 10 - .../v2/WindowsSandboxSetupStartResponse.ts | 5 - ...WindowsWorldWritableWarningNotification.ts | 9 - .../typescript/v2/WriteStatus.ts | 5 - .../protocol-generated/typescript/v2/index.ts | 470 - .../src/app-server/protocol-validators.ts | 16 +- extensions/codex/src/app-server/protocol.ts | 307 +- extensions/deepseek/index.test.ts | 217 +- extensions/discord/src/channel.test.ts | 27 - extensions/discord/src/channel.ts | 16 - extensions/discord/src/normalize.ts | 3 - .../discord/src/outbound-adapter.test.ts | 7 - .../src/outbound-session-route.test.ts | 32 - extensions/discord/src/target-parsing.ts | 17 - extensions/discord/src/targets.test.ts | 6 - .../src/memory/search-manager.test.ts | 182 +- .../qa-lab/src/mantis/crabbox-runtime.ts | 208 + .../mantis/desktop-browser-smoke.runtime.ts | 214 +- .../src/mantis/slack-desktop-smoke.runtime.ts | 219 +- .../qa-lab/src/mantis/visual-task.runtime.ts | 189 +- extensions/telegram/src/bot-access.ts | 38 - .../telegram/src/bot-handlers.runtime.ts | 19 +- .../telegram/src/bot-message-context.ts | 23 +- .../telegram/src/bot-message-dispatch.test.ts | 18 - .../telegram/src/bot-native-commands.ts | 20 +- .../src/bot.create-telegram-bot.test.ts | 79 - extensions/telegram/src/bot/helpers.ts | 18 +- .../telegram/src/polling-liveness.test.ts | 16 +- extensions/telegram/src/polling-liveness.ts | 51 +- .../telegram/src/polling-session.test.ts | 41 +- package.json | 9 +- pnpm-lock.yaml | 76 +- scripts/build-all.mjs | 16 +- scripts/bundle-a2ui.mjs | 221 +- scripts/bundled-plugin-assets.mjs | 177 + scripts/check-codex-app-server-protocol.ts | 50 +- scripts/ci-changed-scope.mjs | 23 +- scripts/ci-run-timings.mjs | 160 +- scripts/ci-runner-labels.mjs | 200 - scripts/e2e/lib/parallels-package-common.sh | 2 +- scripts/e2e/parallels/package-artifact.ts | 10 +- scripts/lib/ci-node-test-plan.mjs | 8 +- scripts/pre-commit/filter-staged-files.mjs | 6 +- scripts/prepush-ci.sh | 3 +- scripts/protocol-gen-swift.ts | 1 - scripts/restart-mac.sh | 4 +- scripts/run-additional-boundary-checks.mjs | 25 +- scripts/run-node-watch-paths.mjs | 10 +- scripts/sync-codex-app-server-protocol.ts | 5 +- scripts/test-projects.test-support.mjs | 15 +- src/agents/command/delivery.test.ts | 18 +- src/agents/command/delivery.ts | 4 +- ...compaction.identifier-preservation.test.ts | 1 - ...compaction.reserve-tokens-clamping.test.ts | 154 - src/agents/compaction.ts | 11 +- src/agents/failover-error.ts | 2 +- src/agents/model-fallback.test.ts | 30 - src/agents/model-fallback.ts | 4 - src/agents/openclaw-gateway-tool.test.ts | 1 - .../openclaw-tools.subagents.scope.test.ts | 1 - ...subagents.sessions-spawn.lifecycle.test.ts | 1 - ...s.steer-failure-clears-suppression.test.ts | 1 - src/agents/openclaw-tools.ts | 2 - src/agents/openclaw-tools.tts-config.test.ts | 4 - .../pi-embedded-runner/system-prompt.ts | 1 - .../tool-result-context-guard.test.ts | 45 - .../tool-result-context-guard.ts | 17 +- ...-embedded-subscribe.handlers.tools.test.ts | 411 - .../pi-embedded-subscribe.handlers.tools.ts | 111 +- ...s.before-tool-call.integration.e2e.test.ts | 3 +- src/agents/subagent-registry.test.ts | 8 - src/agents/subagent-registry.ts | 14 +- src/agents/system-prompt-params.test.ts | 9 - src/agents/system-prompt-params.ts | 21 - src/agents/system-prompt.test.ts | 4 +- src/agents/system-prompt.ts | 12 +- src/agents/test-helpers/fast-core-tools.ts | 6 - src/agents/tool-catalog.ts | 3 +- src/agents/tools/message-tool.test.ts | 3 - src/auto-reply/reply/session.test.ts | 30 - src/canvas-host/a2ui/.bundle.hash | 1 - src/canvas-host/a2ui/a2ui.bundle.js | 14908 ---------------- src/channels/draft-stream-controls.test.ts | 62 - src/cli/cli-utils.test.ts | 2 +- src/cli/completion-runtime.ts | 4 +- src/cli/nodes-canvas.ts | 24 - src/cli/nodes-cli.coverage.test.ts | 11 +- src/cli/nodes-cli/register.canvas.ts | 247 - src/cli/nodes-cli/register.ts | 10 +- src/cli/program.nodes-basic.e2e.test.ts | 6 +- src/cli/program.nodes-media.e2e.test.ts | 16 +- .../doctor-cron-store-migration.test.ts | 43 - src/commands/doctor-cron-store-migration.ts | 21 - src/commands/doctor-cron.test.ts | 61 - src/commands/doctor-cron.ts | 6 - src/config/config-misc.test.ts | 23 + src/config/config.plugin-validation.test.ts | 49 - src/config/config.web-search-provider.test.ts | 20 +- src/config/plugin-auto-enable.apply.ts | 29 +- src/config/schema-base.ts | 2 +- src/config/schema.base.generated.test.ts | 9 +- src/config/schema.help.quality.test.ts | 6 - src/config/schema.help.ts | 10 - src/config/schema.labels.ts | 5 - src/config/sessions/paths.ts | 68 - src/config/sessions/sessions.test.ts | 52 - src/config/types.gateway.ts | 10 - src/config/types.openclaw.ts | 8 +- src/config/validation.ts | 22 +- src/config/zod-schema.ts | 20 +- .../run.message-tool-policy.test.ts | 64 +- src/cron/isolated-agent/run.ts | 46 - src/gateway/canvas-capability.ts | 87 - src/gateway/config-reload-plan.ts | 1 - src/gateway/gateway-misc.test.ts | 4 +- src/gateway/hosted-plugin-surface-url.test.ts | 26 + .../hosted-plugin-surface-url.ts} | 41 +- src/gateway/method-scopes.test.ts | 1 + src/gateway/method-scopes.ts | 2 +- src/gateway/node-command-policy.test.ts | 57 +- src/gateway/node-command-policy.ts | 54 +- src/gateway/plugin-node-capability.test.ts | 182 + src/gateway/plugin-node-capability.ts | 255 + src/gateway/protocol/schema/frames.ts | 2 +- src/gateway/protocol/version.ts | 2 +- src/gateway/role-policy.test.ts | 2 + src/gateway/server-close.test.ts | 11 +- src/gateway/server-close.ts | 9 - src/gateway/server-http.request-trace.test.ts | 1 - src/gateway/server-http.test-harness.ts | 1 - src/gateway/server-http.ts | 154 +- src/gateway/server-import-boundary.test.ts | 2 +- src/gateway/server-methods-list.test.ts | 4 + src/gateway/server-methods-list.ts | 2 +- src/gateway/server-methods/agent.test.ts | 132 - src/gateway/server-methods/agent.ts | 13 - .../nodes.canvas-capability-refresh.test.ts | 63 - .../server-methods/nodes.invoke-wake.test.ts | 49 + src/gateway/server-methods/nodes.ts | 112 +- src/gateway/server-methods/shared-types.ts | 7 +- src/gateway/server-runtime-config.ts | 5 - src/gateway/server-runtime-state.test.ts | 45 +- src/gateway/server-runtime-state.ts | 73 +- .../server-startup-config.recovery.test.ts | 42 - src/gateway/server-startup-config.ts | 5 - src/gateway/server-ws-runtime.ts | 5 +- src/gateway/server.impl.ts | 33 +- .../server.models-voicewake-misc.test.ts | 19 - ...erver.plugin-node-capability-auth.test.ts} | 61 +- src/gateway/server.preauth-hardening.test.ts | 6 +- ...auth.ts => plugin-node-capability-auth.ts} | 56 +- src/gateway/server/plugins-http.ts | 98 + .../plugins-http/route-capability.test.ts | 20 + .../server/plugins-http/route-capability.ts | 56 + .../server/ws-connection.startup.test.ts | 1 - src/gateway/server/ws-connection.test.ts | 338 +- src/gateway/server/ws-connection.ts | 40 +- .../server/ws-connection/message-handler.ts | 68 +- src/gateway/server/ws-types.ts | 7 +- src/gateway/talk-realtime-relay.test.ts | 188 +- src/gateway/test-helpers.config-runtime.ts | 12 - src/gateway/test-helpers.runtime-state.ts | 2 - src/gateway/test-helpers.server.ts | 2 - .../heartbeat-runner.tool-response.test.ts | 262 +- src/infra/outbound/outbound-send-service.ts | 112 +- src/infra/run-node.test.ts | 8 +- src/infra/vitest-e2e-config.test.ts | 10 +- src/media/web-media.test.ts | 22 +- src/media/web-media.ts | 15 +- src/plugin-sdk/gateway-runtime.ts | 15 + src/plugin-sdk/plugin-test-api.ts | 2 + src/plugins/api-builder.ts | 12 +- src/plugins/captured-registration.ts | 5 + src/plugins/cli-registry-loader.ts | 16 +- src/plugins/cli.test.ts | 69 + .../installed-plugin-index-records.test.ts | 56 +- src/plugins/loader-channel-setup.ts | 6 - src/plugins/loader.test.ts | 152 +- src/plugins/plugin-lookup-table.test.ts | 196 +- src/plugins/plugin-registry-snapshot.test.ts | 56 +- .../register-plugin-cli-command-groups.ts | 48 +- src/plugins/registry-empty.ts | 1 + src/plugins/registry-types.ts | 17 + src/plugins/registry.ts | 61 +- src/plugins/setup-registry.canvas.test.ts | 39 + src/plugins/status.registry-snapshot.test.ts | 58 +- .../test-helpers/managed-npm-plugin.ts | 59 + src/plugins/types.ts | 74 +- src/scripts/canvas-a2ui-copy.test.ts | 2 +- src/scripts/ci-changed-scope.test.ts | 87 +- src/secrets/target-registry.docs.test.ts | 2 +- .../discord-group-codex-message-tool.md | 14 +- test/scripts/build-all.test.ts | 8 +- test/scripts/bundle-a2ui.test.ts | 39 +- test/scripts/bundled-plugin-assets.test.ts | 73 + test/scripts/changed-lanes.test.ts | 24 +- test/scripts/ci-node-test-plan.test.ts | 9 +- test/scripts/ci-runner-labels.test.ts | 42 - test/scripts/prompt-snapshots.test.ts | 30 +- .../run-additional-boundary-checks.test.ts | 13 +- test/vitest/vitest.e2e.config.ts | 23 +- test/vitest/vitest.unit-fast-paths.mjs | 4 +- ui/src/ui/app-gateway-chat-load.node.test.ts | 2 +- ui/src/ui/app-gateway.node.test.ts | 8 +- ui/src/ui/app-render.ts | 2 +- ui/src/ui/canvas-url.ts | 6 +- ui/src/ui/chat/grouped-render.test.ts | 2 +- ui/src/ui/chat/grouped-render.ts | 18 +- ui/src/ui/chat/tool-cards.ts | 16 +- ui/src/ui/controllers/dreaming.test.ts | 8 +- ui/src/ui/gateway.node.test.ts | 4 +- ui/src/ui/gateway.ts | 10 +- ui/src/ui/views/chat.test.ts | 2 +- ui/src/ui/views/chat.ts | 6 +- ui/src/ui/views/markdown-sidebar.ts | 4 +- vendor/a2ui/.gemini/GEMINI.md | 94 - vendor/a2ui/.github/workflows/docs.yml | 79 - .../a2ui/.github/workflows/editor_build.yml | 55 - .../.github/workflows/inspector_build.yml | 56 - .../.github/workflows/java_build_and_test.yml | 48 - .../.github/workflows/lit_samples_build.yml | 54 - .../.github/workflows/ng_build_and_test.yml | 72 - .../workflows/python_samples_build.yml | 62 - .../.github/workflows/web_build_and_test.yml | 50 - vendor/a2ui/.gitignore | 16 - vendor/a2ui/CONTRIBUTING.md | 49 - vendor/a2ui/LICENSE | 203 - vendor/a2ui/README.md | 162 - vendor/a2ui/mkdocs.yaml | 184 - vendor/a2ui/renderers/angular/.npmrc | 2 - vendor/a2ui/renderers/angular/README.md | 9 - vendor/a2ui/renderers/angular/angular.json | 35 - vendor/a2ui/renderers/angular/ng-package.json | 8 - .../a2ui/renderers/angular/package-lock.json | 14264 --------------- vendor/a2ui/renderers/angular/package.json | 59 - .../angular/src/lib/catalog/audio.ts | 50 - .../angular/src/lib/catalog/button.ts | 56 - .../renderers/angular/src/lib/catalog/card.ts | 57 - .../angular/src/lib/catalog/checkbox.ts | 73 - .../angular/src/lib/catalog/column.ts | 96 - .../angular/src/lib/catalog/datetime-input.ts | 127 - .../angular/src/lib/catalog/default.ts | 185 - .../angular/src/lib/catalog/divider.ts | 37 - .../renderers/angular/src/lib/catalog/icon.ts | 44 - .../angular/src/lib/catalog/image.ts | 62 - .../renderers/angular/src/lib/catalog/list.ts | 63 - .../angular/src/lib/catalog/modal.ts | 113 - .../src/lib/catalog/multiple-choice.ts | 77 - .../renderers/angular/src/lib/catalog/row.ts | 100 - .../angular/src/lib/catalog/slider.ts | 73 - .../angular/src/lib/catalog/surface.ts | 99 - .../renderers/angular/src/lib/catalog/tabs.ts | 72 - .../angular/src/lib/catalog/text-field.ts | 86 - .../renderers/angular/src/lib/catalog/text.ts | 137 - .../angular/src/lib/catalog/video.ts | 50 - .../a2ui/renderers/angular/src/lib/config.ts | 25 - .../renderers/angular/src/lib/data/index.ts | 18 - .../angular/src/lib/data/markdown.ts | 114 - .../angular/src/lib/data/processor.ts | 47 - .../renderers/angular/src/lib/data/types.ts | 29 - .../angular/src/lib/rendering/catalog.ts | 36 - .../src/lib/rendering/dynamic-component.ts | 100 - .../angular/src/lib/rendering/index.ts | 20 - .../angular/src/lib/rendering/renderer.ts | 109 - .../angular/src/lib/rendering/theming.ts | 22 - .../a2ui/renderers/angular/src/public-api.ts | 21 - vendor/a2ui/renderers/angular/tsconfig.json | 23 - .../a2ui/renderers/angular/tsconfig.lib.json | 16 - .../renderers/angular/tsconfig.lib.prod.json | 9 - .../a2ui/renderers/angular/tsconfig.spec.json | 12 - vendor/a2ui/renderers/lit/.npmrc | 2 - vendor/a2ui/renderers/lit/README | 9 - vendor/a2ui/renderers/lit/README.md | 9 - vendor/a2ui/renderers/lit/package-lock.json | 1196 -- vendor/a2ui/renderers/lit/package.json | 108 - vendor/a2ui/renderers/lit/src/0.8/core.ts | 35 - .../a2ui/renderers/lit/src/0.8/data/guards.ts | 236 - .../lit/src/0.8/data/model-processor.ts | 867 - .../src/0.8/data/signal-model-processor.ts | 31 - .../a2ui/renderers/lit/src/0.8/events/a2ui.ts | 28 - .../a2ui/renderers/lit/src/0.8/events/base.ts | 19 - .../renderers/lit/src/0.8/events/events.ts | 53 - vendor/a2ui/renderers/lit/src/0.8/index.ts | 18 - .../a2ui/renderers/lit/src/0.8/model.test.ts | 1376 -- .../renderers/lit/src/0.8/schemas/.gitignore | 4 - ...erver_to_client_with_standard_catalog.json | 827 - .../renderers/lit/src/0.8/styles/behavior.ts | 55 - .../renderers/lit/src/0.8/styles/border.ts | 42 - .../renderers/lit/src/0.8/styles/colors.ts | 100 - .../renderers/lit/src/0.8/styles/icons.ts | 60 - .../renderers/lit/src/0.8/styles/index.ts | 37 - .../renderers/lit/src/0.8/styles/layout.ts | 235 - .../renderers/lit/src/0.8/styles/opacity.ts | 24 - .../renderers/lit/src/0.8/styles/shared.ts | 17 - .../a2ui/renderers/lit/src/0.8/styles/type.ts | 156 - .../renderers/lit/src/0.8/styles/utils.ts | 104 - .../lit/src/0.8/types/client-event.ts | 80 - .../renderers/lit/src/0.8/types/colors.ts | 66 - .../renderers/lit/src/0.8/types/components.ts | 211 - .../renderers/lit/src/0.8/types/primitives.ts | 60 - .../a2ui/renderers/lit/src/0.8/types/types.ts | 474 - vendor/a2ui/renderers/lit/src/0.8/ui/audio.ts | 96 - .../a2ui/renderers/lit/src/0.8/ui/button.ts | 65 - vendor/a2ui/renderers/lit/src/0.8/ui/card.ts | 64 - .../a2ui/renderers/lit/src/0.8/ui/checkbox.ts | 139 - .../a2ui/renderers/lit/src/0.8/ui/column.ts | 104 - .../lit/src/0.8/ui/component-registry.ts | 58 - .../renderers/lit/src/0.8/ui/context/theme.ts | 20 - .../lit/src/0.8/ui/custom-components/index.ts | 22 - .../lit/src/0.8/ui/datetime-input.ts | 197 - .../lit/src/0.8/ui/directives/directives.ts | 17 - .../lit/src/0.8/ui/directives/markdown.ts | 152 - .../lit/src/0.8/ui/directives/sanitizer.ts | 40 - .../a2ui/renderers/lit/src/0.8/ui/divider.ts | 51 - vendor/a2ui/renderers/lit/src/0.8/ui/icon.ts | 98 - vendor/a2ui/renderers/lit/src/0.8/ui/image.ts | 118 - vendor/a2ui/renderers/lit/src/0.8/ui/list.ts | 72 - vendor/a2ui/renderers/lit/src/0.8/ui/modal.ts | 131 - .../lit/src/0.8/ui/multiple-choice.ts | 142 - vendor/a2ui/renderers/lit/src/0.8/ui/root.ts | 475 - vendor/a2ui/renderers/lit/src/0.8/ui/row.ts | 104 - .../a2ui/renderers/lit/src/0.8/ui/slider.ts | 159 - .../a2ui/renderers/lit/src/0.8/ui/styles.ts | 20 - .../a2ui/renderers/lit/src/0.8/ui/surface.ts | 134 - vendor/a2ui/renderers/lit/src/0.8/ui/tabs.ts | 132 - .../renderers/lit/src/0.8/ui/text-field.ts | 131 - vendor/a2ui/renderers/lit/src/0.8/ui/text.ts | 164 - vendor/a2ui/renderers/lit/src/0.8/ui/ui.ts | 119 - .../renderers/lit/src/0.8/ui/utils/utils.ts | 92 - .../renderers/lit/src/0.8/ui/utils/youtube.ts | 77 - vendor/a2ui/renderers/lit/src/0.8/ui/video.ts | 96 - vendor/a2ui/renderers/lit/src/index.ts | 17 - vendor/a2ui/renderers/lit/tsconfig.json | 36 - vendor/a2ui/requirements-docs.txt | 9 - vendor/a2ui/specification/0.8/eval/.gitignore | 26 - vendor/a2ui/specification/0.8/eval/GEMINI.md | 76 - vendor/a2ui/specification/0.8/eval/README.md | 61 - .../specification/0.8/eval/genkit.conf.js | 24 - .../a2ui/specification/0.8/eval/package.json | 36 - .../specification/0.8/eval/pnpm-lock.yaml | 4885 ----- .../0.8/eval/pnpm-workspace.yaml | 18 - .../0.8/eval/src/basic_schema_matcher.ts | 65 - vendor/a2ui/specification/0.8/eval/src/dev.ts | 17 - .../a2ui/specification/0.8/eval/src/flows.ts | 71 - .../a2ui/specification/0.8/eval/src/index.ts | 363 - .../0.8/eval/src/message_type_matcher.ts | 50 - .../a2ui/specification/0.8/eval/src/models.ts | 68 - .../specification/0.8/eval/src/prompts.ts | 493 - .../0.8/eval/src/schema_matcher.ts | 24 - .../eval/src/surface_update_schema_matcher.ts | 207 - .../specification/0.8/eval/src/validator.ts | 496 - .../a2ui/specification/0.8/eval/tsconfig.json | 12 - vendor/a2ui/specification/0.8/json/README.md | 15 - .../json/a2ui_client_capabilities_schema.json | 23 - .../0.8/json/catalog_description_schema.json | 34 - .../0.8/json/client_to_server.json | 53 - .../0.8/json/server_to_client.json | 148 - ...erver_to_client_with_standard_catalog.json | 827 - .../0.8/json/standard_catalog_definition.json | 685 - vendor/a2ui/specification/0.9/eval/.gitignore | 26 - vendor/a2ui/specification/0.9/eval/README.md | 79 - .../specification/0.9/eval/genkit.conf.js | 24 - .../a2ui/specification/0.9/eval/package.json | 47 - .../specification/0.9/eval/pnpm-lock.yaml | 4727 ----- .../0.9/eval/pnpm-workspace.yaml | 18 - vendor/a2ui/specification/0.9/eval/src/ai.ts | 46 - .../0.9/eval/src/analysis_flow.ts | 118 - vendor/a2ui/specification/0.9/eval/src/dev.ts | 18 - .../0.9/eval/src/evaluation_flow.ts | 193 - .../specification/0.9/eval/src/evaluator.ts | 205 - .../0.9/eval/src/generation_flow.ts | 148 - .../specification/0.9/eval/src/generator.ts | 211 - .../a2ui/specification/0.9/eval/src/index.ts | 493 - .../a2ui/specification/0.9/eval/src/logger.ts | 70 - .../a2ui/specification/0.9/eval/src/models.ts | 93 - .../specification/0.9/eval/src/prompts.ts | 373 - .../specification/0.9/eval/src/rateLimiter.ts | 205 - .../a2ui/specification/0.9/eval/src/types.ts | 47 - .../a2ui/specification/0.9/eval/src/utils.ts | 44 - .../specification/0.9/eval/src/validator.ts | 365 - .../a2ui/specification/0.9/eval/tsconfig.json | 12 - .../0.9/json/client_to_server.json | 97 - .../specification/0.9/json/common_types.json | 120 - .../0.9/json/contact_form_example.jsonl | 3 - .../0.9/json/server_to_client.json | 114 - .../0.9/json/standard_catalog_definition.json | 638 - .../0.9/json/standard_catalog_rules.txt | 5 - vendor/a2ui/specification/0.9/validate.sh | 38 - 1044 files changed, 11532 insertions(+), 81283 deletions(-) delete mode 100644 apps/macos/Sources/OpenClawProtocol/GatewayModels.swift create mode 100644 docs/plugins/reference/canvas.md create mode 100644 docs/refactor/canvas.md create mode 100644 extensions/canvas/cli-metadata.ts create mode 100644 extensions/canvas/index.ts create mode 100644 extensions/canvas/openclaw.plugin.json create mode 100644 extensions/canvas/package.json create mode 100644 extensions/canvas/runtime-api.ts create mode 100644 extensions/canvas/scripts/bundle-a2ui.mjs create mode 100644 extensions/canvas/scripts/copy-a2ui.d.mts rename scripts/canvas-a2ui-copy.ts => extensions/canvas/scripts/copy-a2ui.mjs (72%) create mode 100644 extensions/canvas/setup-api.ts rename {src/cli/nodes-cli => extensions/canvas/src}/a2ui-jsonl.ts (100%) create mode 100644 extensions/canvas/src/capability.ts create mode 100644 extensions/canvas/src/cli-helpers.ts create mode 100644 extensions/canvas/src/cli.test.ts create mode 100644 extensions/canvas/src/cli.ts create mode 100644 extensions/canvas/src/config-migration.test.ts create mode 100644 extensions/canvas/src/config-migration.ts create mode 100644 extensions/canvas/src/config.test.ts create mode 100644 extensions/canvas/src/config.ts rename src/gateway/canvas-documents.test.ts => extensions/canvas/src/documents.test.ts (99%) rename src/gateway/canvas-documents.ts => extensions/canvas/src/documents.ts (97%) rename src/infra/canvas-host-url.test.ts => extensions/canvas/src/host-url.test.ts (97%) create mode 100644 extensions/canvas/src/host-url.ts rename {apps/shared/OpenClawKit/Tools/CanvasA2UI => extensions/canvas/src/host/a2ui-app}/bootstrap.js (87%) rename {apps/shared/OpenClawKit/Tools/CanvasA2UI => extensions/canvas/src/host/a2ui-app}/rolldown.config.mjs (80%) rename {src/canvas-host => extensions/canvas/src/host}/a2ui-shared.ts (96%) rename {src/canvas-host => extensions/canvas/src/host}/a2ui.ts (88%) create mode 100644 extensions/canvas/src/host/a2ui/.bundle.hash rename {src/canvas-host => extensions/canvas/src/host}/a2ui/index.html (100%) rename {src/canvas-host => extensions/canvas/src/host}/file-resolver.test.ts (95%) rename {src/canvas-host => extensions/canvas/src/host}/file-resolver.ts (84%) rename {src/canvas-host => extensions/canvas/src/host}/server.state-dir.test.ts (88%) rename {src/canvas-host => extensions/canvas/src/host}/server.test.ts (98%) rename {src/canvas-host => extensions/canvas/src/host}/server.ts (95%) create mode 100644 extensions/canvas/src/http-route.ts create mode 100644 extensions/canvas/src/tool.test.ts rename src/agents/tools/canvas-tool.ts => extensions/canvas/src/tool.ts (59%) delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/AbsolutePathBuf.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/AgentPath.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ApplyPatchApprovalParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ApplyPatchApprovalResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/AuthMode.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ClientInfo.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ClientNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ClientRequest.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/CollaborationMode.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ContentItem.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ConversationGitInfo.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ConversationSummary.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ExecCommandApprovalParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ExecCommandApprovalResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ExecPolicyAmendment.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FileChange.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ForcedLoginMethod.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FunctionCallOutputBody.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FunctionCallOutputContentItem.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchMatchType.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchResult.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionCompletedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStartParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStartResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStopParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStopResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionUpdateParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionUpdateResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionUpdatedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/GetAuthStatusParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/GetAuthStatusResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/GetConversationSummaryParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/GetConversationSummaryResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/GitDiffToRemoteParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/GitDiffToRemoteResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/GitSha.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ImageDetail.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/InitializeCapabilities.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/InitializeParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/InitializeResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/InputModality.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/InternalSessionSource.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/LocalShellAction.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/LocalShellExecAction.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/LocalShellStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/MessagePhase.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ModeKind.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/NetworkPolicyAmendment.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/NetworkPolicyRuleAction.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ParsedCommand.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/Personality.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/PlanType.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/RealtimeConversationVersion.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/RealtimeOutputModality.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/RealtimeVoice.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/RealtimeVoicesList.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ReasoningEffort.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ReasoningItemContent.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ReasoningItemReasoningSummary.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ReasoningSummary.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/RequestId.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/Resource.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ResourceContent.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ResourceTemplate.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ResponseItem.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ReviewDecision.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ServerNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ServerRequest.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ServiceTier.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/SessionSource.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/Settings.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/SubAgentSource.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ThreadId.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/ThreadMemoryMode.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/Tool.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/Verbosity.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/WebSearchAction.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/WebSearchContextSize.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/WebSearchLocation.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/WebSearchMode.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/WebSearchToolConfig.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/index.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/serde_json/JsonValue.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/Account.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AccountLoginCompletedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AccountRateLimitsUpdatedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AccountUpdatedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ActivePermissionProfile.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ActivePermissionProfileModification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AddCreditsNudgeCreditType.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AddCreditsNudgeEmailStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AdditionalFileSystemPermissions.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AdditionalNetworkPermissions.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AdditionalPermissionProfile.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AgentMessageDeltaNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AnalyticsConfig.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AppBranding.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AppInfo.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AppListUpdatedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AppMetadata.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AppReview.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AppScreenshot.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AppSummary.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AppToolApproval.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AppToolsConfig.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ApprovalsReviewer.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsConfig.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsDefaultConfig.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsListParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsListResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AskForApproval.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/AutoReviewDecisionSource.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ByteRange.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CancelLoginAccountParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CancelLoginAccountResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CancelLoginAccountStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ChatgptAuthTokensRefreshParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ChatgptAuthTokensRefreshReason.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ChatgptAuthTokensRefreshResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CodexErrorInfo.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentState.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentTool.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentToolCallStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CollaborationModeListParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CollaborationModeListResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CollaborationModeMask.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandAction.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecOutputDeltaNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecOutputStream.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecResizeParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecResizeResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecTerminalSize.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecTerminateParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecTerminateResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecWriteParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecWriteResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionApprovalDecision.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionOutputDeltaNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionRequestApprovalParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionRequestApprovalResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionSource.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandMigration.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/Config.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigBatchWriteParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigEdit.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigLayer.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigLayerMetadata.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigLayerSource.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigReadParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigReadResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigRequirements.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigRequirementsReadResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigValueWriteParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigWarningNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigWriteResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfiguredHookHandler.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfiguredHookMatcherGroup.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ContextCompactedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/CreditsSnapshot.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DeprecationNoticeNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyAlgorithm.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyCreateParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyCreateResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyProtectionClass.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyProtectionPolicy.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyPublicParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyPublicResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeySignParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeySignPayload.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeySignResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallOutputContentItem.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolSpec.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ErrorNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExecPolicyAmendment.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeature.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureEnablementSetParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureEnablementSetResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureListParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureListResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureStage.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigDetectParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigDetectResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigImportCompletedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigImportParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigImportResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigMigrationItem.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigMigrationItemType.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FeedbackUploadParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FeedbackUploadResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeApprovalDecision.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeOutputDeltaNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangePatchUpdatedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeRequestApprovalParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeRequestApprovalResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemAccessMode.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemPath.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemSandboxEntry.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemSpecialPath.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FileUpdateChange.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsChangedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCopyParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCopyResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCreateDirectoryParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCreateDirectoryResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsGetMetadataParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsGetMetadataResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadDirectoryEntry.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadDirectoryParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadDirectoryResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadFileParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadFileResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsRemoveParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsRemoveResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsUnwatchParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsUnwatchResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWatchParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWatchResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWriteFileParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWriteFileResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/GetAccountParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/GetAccountRateLimitsResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/GetAccountResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/GitInfo.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/GrantedPermissionProfile.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianApprovalReview.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianApprovalReviewAction.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianApprovalReviewStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianCommandSource.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianRiskLevel.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianUserAuthorization.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianWarningNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookCompletedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookErrorInfo.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookEventName.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookExecutionMode.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookHandlerType.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookMetadata.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookMigration.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookOutputEntry.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookOutputEntryKind.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookPromptFragment.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookRunStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookRunSummary.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookScope.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookSource.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HookStartedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HooksListEntry.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HooksListParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/HooksListResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemCompletedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemGuardianApprovalReviewCompletedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemGuardianApprovalReviewStartedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemStartedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ListMcpServerStatusParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ListMcpServerStatusResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/LoginAccountParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/LoginAccountResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/LogoutAccountResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ManagedHooksRequirements.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceAddParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceAddResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceInterface.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceLoadErrorInfo.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceRemoveParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceRemoveResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceUpgradeErrorInfo.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceUpgradeParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceUpgradeResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpAuthStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationArrayType.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationBooleanSchema.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationBooleanType.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationConstOption.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationEnumSchema.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationLegacyTitledEnumSchema.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationMultiSelectEnumSchema.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationNumberSchema.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationNumberType.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationObjectType.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationPrimitiveSchema.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationSchema.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationSingleSelectEnumSchema.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationStringFormat.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationStringSchema.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationStringType.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationTitledEnumItems.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationTitledMultiSelectEnumSchema.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationTitledSingleSelectEnumSchema.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationUntitledEnumItems.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationUntitledMultiSelectEnumSchema.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationUntitledSingleSelectEnumSchema.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpResourceReadParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpResourceReadResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerElicitationAction.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerElicitationRequestParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerElicitationRequestResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerMigration.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerOauthLoginCompletedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerOauthLoginParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerOauthLoginResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerRefreshResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStartupState.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStatusDetail.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStatusUpdatedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerToolCallParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerToolCallResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallError.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallProgressNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallResult.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MemoryCitation.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MemoryCitationEntry.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MemoryResetResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MergeStrategy.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MigrationDetails.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MockExperimentalMethodParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/MockExperimentalMethodResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/Model.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelAvailabilityNux.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelListParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelListResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelProviderCapabilitiesReadParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelProviderCapabilitiesReadResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelRerouteReason.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelReroutedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelUpgradeInfo.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelVerification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelVerificationNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkAccess.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkApprovalContext.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkApprovalProtocol.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkDomainPermission.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkPolicyAmendment.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkPolicyRuleAction.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkRequirements.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkUnixSocketPermission.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/NonSteerableTurnKind.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/OverriddenMetadata.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PatchApplyStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PatchChangeKind.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionGrantScope.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfile.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileFileSystemPermissions.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileModificationParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileNetworkPermissions.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileSelectionParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionsRequestApprovalParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionsRequestApprovalResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PlanDeltaNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginAuthPolicy.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginAvailability.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginDetail.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInstallParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInstallPolicy.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInstallResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInterface.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginListParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginListResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginMarketplaceEntry.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginReadParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginReadResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareDeleteParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareDeleteResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareListItem.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareListParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareListResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareSaveParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareSaveResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSkillReadParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSkillReadResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSource.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSummary.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginUninstallParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginUninstallResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginsMigration.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ProfileV2.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/RateLimitReachedType.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/RateLimitSnapshot.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/RateLimitWindow.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/RawResponseItemCompletedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningEffortOption.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningSummaryPartAddedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningSummaryTextDeltaNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningTextDeltaNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlClientConnectionAudience.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlClientEnrollmentAudience.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlConnectionStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlStatusChangedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/RequestPermissionProfile.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ResidencyRequirement.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewDelivery.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewStartParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewStartResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewTarget.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SandboxMode.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SandboxPolicy.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SandboxWorkspaceWrite.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SendAddCreditsNudgeEmailParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SendAddCreditsNudgeEmailResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ServerRequestResolvedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SessionMigration.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SessionSource.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillDependencies.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillErrorInfo.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillInterface.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillMetadata.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillScope.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillSummary.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillToolDependency.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsChangedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsConfigWriteParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsConfigWriteResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListEntry.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListExtraRootsForCwd.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SortDirection.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/SubagentMigration.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TerminalInteractionNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TextElement.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TextPosition.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TextRange.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/Thread.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadActiveFlag.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadApproveGuardianDeniedActionParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadApproveGuardianDeniedActionResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadArchiveParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadArchiveResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadArchivedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadBackgroundTerminalsCleanParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadBackgroundTerminalsCleanResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadClosedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadCompactStartParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadCompactStartResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadDecrementElicitationParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadDecrementElicitationResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadForkParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadForkResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoal.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalClearParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalClearResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalClearedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalGetParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalGetResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalSetParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalSetResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalUpdatedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadIncrementElicitationParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadIncrementElicitationResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadInjectItemsParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadInjectItemsResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadItem.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadListParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadListResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadLoadedListParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadLoadedListResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMemoryModeSetParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMemoryModeSetResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMetadataGitInfoUpdateParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMetadataUpdateParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMetadataUpdateResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadNameUpdatedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadReadParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadReadResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendAudioParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendAudioResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendTextParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendTextResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAudioChunk.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeClosedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeErrorNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeItemAddedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeListVoicesParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeListVoicesResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeOutputAudioDeltaNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeSdpNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartTransport.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStopParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStopResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeTranscriptDeltaNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeTranscriptDoneNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadResumeParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadResumeResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRollbackParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRollbackResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSetNameParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSetNameResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadShellCommandParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadShellCommandResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSortKey.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSourceKind.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartSource.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStatusChangedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTokenUsage.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTokenUsageUpdatedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTurnsListParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTurnsListResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnarchiveParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnarchiveResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnarchivedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnsubscribeParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnsubscribeResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnsubscribeStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TokenUsageBreakdown.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputAnswer.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputOption.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputQuestion.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolsV2.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/Turn.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnCompletedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnDiffUpdatedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnEnvironmentParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnError.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnInterruptParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnInterruptResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnPlanStep.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnPlanStepStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnPlanUpdatedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStartParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStartResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStartedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnSteerParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnSteerResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/UserInput.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/WarningNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/WebSearchAction.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupCompletedNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupMode.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupStartParams.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupStartResponse.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsWorldWritableWarningNotification.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/WriteStatus.ts delete mode 100644 extensions/codex/src/app-server/protocol-generated/typescript/v2/index.ts create mode 100644 extensions/qa-lab/src/mantis/crabbox-runtime.ts create mode 100644 scripts/bundled-plugin-assets.mjs delete mode 100644 scripts/ci-runner-labels.mjs delete mode 100644 src/agents/compaction.reserve-tokens-clamping.test.ts delete mode 100644 src/agents/test-helpers/fast-core-tools.ts delete mode 100644 src/canvas-host/a2ui/.bundle.hash delete mode 100644 src/canvas-host/a2ui/a2ui.bundle.js delete mode 100644 src/cli/nodes-canvas.ts delete mode 100644 src/cli/nodes-cli/register.canvas.ts delete mode 100644 src/gateway/canvas-capability.ts create mode 100644 src/gateway/hosted-plugin-surface-url.test.ts rename src/{infra/canvas-host-url.ts => gateway/hosted-plugin-surface-url.ts} (57%) create mode 100644 src/gateway/plugin-node-capability.test.ts create mode 100644 src/gateway/plugin-node-capability.ts delete mode 100644 src/gateway/server-methods/nodes.canvas-capability-refresh.test.ts rename src/gateway/{server.canvas-auth.test.ts => server.plugin-node-capability-auth.test.ts} (90%) rename src/gateway/server/{http-auth.ts => plugin-node-capability-auth.ts} (53%) create mode 100644 src/gateway/server/plugins-http/route-capability.test.ts create mode 100644 src/gateway/server/plugins-http/route-capability.ts create mode 100644 src/plugins/setup-registry.canvas.test.ts create mode 100644 src/plugins/test-helpers/managed-npm-plugin.ts create mode 100644 test/scripts/bundled-plugin-assets.test.ts delete mode 100644 test/scripts/ci-runner-labels.test.ts delete mode 100644 vendor/a2ui/.gemini/GEMINI.md delete mode 100644 vendor/a2ui/.github/workflows/docs.yml delete mode 100644 vendor/a2ui/.github/workflows/editor_build.yml delete mode 100644 vendor/a2ui/.github/workflows/inspector_build.yml delete mode 100644 vendor/a2ui/.github/workflows/java_build_and_test.yml delete mode 100644 vendor/a2ui/.github/workflows/lit_samples_build.yml delete mode 100644 vendor/a2ui/.github/workflows/ng_build_and_test.yml delete mode 100644 vendor/a2ui/.github/workflows/python_samples_build.yml delete mode 100644 vendor/a2ui/.github/workflows/web_build_and_test.yml delete mode 100644 vendor/a2ui/.gitignore delete mode 100644 vendor/a2ui/CONTRIBUTING.md delete mode 100644 vendor/a2ui/LICENSE delete mode 100644 vendor/a2ui/README.md delete mode 100644 vendor/a2ui/mkdocs.yaml delete mode 100644 vendor/a2ui/renderers/angular/.npmrc delete mode 100644 vendor/a2ui/renderers/angular/README.md delete mode 100644 vendor/a2ui/renderers/angular/angular.json delete mode 100644 vendor/a2ui/renderers/angular/ng-package.json delete mode 100644 vendor/a2ui/renderers/angular/package-lock.json delete mode 100644 vendor/a2ui/renderers/angular/package.json delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/audio.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/button.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/card.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/checkbox.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/column.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/datetime-input.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/default.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/divider.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/icon.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/image.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/list.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/modal.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/multiple-choice.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/row.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/slider.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/surface.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/tabs.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/text-field.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/text.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/catalog/video.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/config.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/data/index.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/data/markdown.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/data/processor.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/data/types.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/rendering/catalog.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/rendering/dynamic-component.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/rendering/index.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/rendering/renderer.ts delete mode 100644 vendor/a2ui/renderers/angular/src/lib/rendering/theming.ts delete mode 100644 vendor/a2ui/renderers/angular/src/public-api.ts delete mode 100644 vendor/a2ui/renderers/angular/tsconfig.json delete mode 100644 vendor/a2ui/renderers/angular/tsconfig.lib.json delete mode 100644 vendor/a2ui/renderers/angular/tsconfig.lib.prod.json delete mode 100644 vendor/a2ui/renderers/angular/tsconfig.spec.json delete mode 100644 vendor/a2ui/renderers/lit/.npmrc delete mode 100644 vendor/a2ui/renderers/lit/README delete mode 100644 vendor/a2ui/renderers/lit/README.md delete mode 100644 vendor/a2ui/renderers/lit/package-lock.json delete mode 100644 vendor/a2ui/renderers/lit/package.json delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/core.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/data/guards.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/data/model-processor.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/data/signal-model-processor.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/events/a2ui.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/events/base.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/events/events.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/index.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/model.test.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/schemas/.gitignore delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/schemas/server_to_client_with_standard_catalog.json delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/styles/behavior.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/styles/border.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/styles/colors.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/styles/icons.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/styles/index.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/styles/layout.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/styles/opacity.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/styles/shared.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/styles/type.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/styles/utils.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/types/client-event.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/types/colors.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/types/components.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/types/primitives.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/types/types.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/audio.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/button.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/card.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/checkbox.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/column.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/component-registry.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/context/theme.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/custom-components/index.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/datetime-input.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/directives/directives.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/directives/markdown.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/directives/sanitizer.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/divider.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/icon.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/image.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/list.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/modal.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/multiple-choice.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/root.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/row.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/slider.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/styles.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/surface.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/tabs.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/text-field.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/text.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/ui.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/utils/utils.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/utils/youtube.ts delete mode 100644 vendor/a2ui/renderers/lit/src/0.8/ui/video.ts delete mode 100644 vendor/a2ui/renderers/lit/src/index.ts delete mode 100644 vendor/a2ui/renderers/lit/tsconfig.json delete mode 100644 vendor/a2ui/requirements-docs.txt delete mode 100644 vendor/a2ui/specification/0.8/eval/.gitignore delete mode 100644 vendor/a2ui/specification/0.8/eval/GEMINI.md delete mode 100644 vendor/a2ui/specification/0.8/eval/README.md delete mode 100644 vendor/a2ui/specification/0.8/eval/genkit.conf.js delete mode 100644 vendor/a2ui/specification/0.8/eval/package.json delete mode 100644 vendor/a2ui/specification/0.8/eval/pnpm-lock.yaml delete mode 100644 vendor/a2ui/specification/0.8/eval/pnpm-workspace.yaml delete mode 100644 vendor/a2ui/specification/0.8/eval/src/basic_schema_matcher.ts delete mode 100644 vendor/a2ui/specification/0.8/eval/src/dev.ts delete mode 100644 vendor/a2ui/specification/0.8/eval/src/flows.ts delete mode 100644 vendor/a2ui/specification/0.8/eval/src/index.ts delete mode 100644 vendor/a2ui/specification/0.8/eval/src/message_type_matcher.ts delete mode 100644 vendor/a2ui/specification/0.8/eval/src/models.ts delete mode 100644 vendor/a2ui/specification/0.8/eval/src/prompts.ts delete mode 100644 vendor/a2ui/specification/0.8/eval/src/schema_matcher.ts delete mode 100644 vendor/a2ui/specification/0.8/eval/src/surface_update_schema_matcher.ts delete mode 100644 vendor/a2ui/specification/0.8/eval/src/validator.ts delete mode 100644 vendor/a2ui/specification/0.8/eval/tsconfig.json delete mode 100644 vendor/a2ui/specification/0.8/json/README.md delete mode 100644 vendor/a2ui/specification/0.8/json/a2ui_client_capabilities_schema.json delete mode 100644 vendor/a2ui/specification/0.8/json/catalog_description_schema.json delete mode 100644 vendor/a2ui/specification/0.8/json/client_to_server.json delete mode 100644 vendor/a2ui/specification/0.8/json/server_to_client.json delete mode 100644 vendor/a2ui/specification/0.8/json/server_to_client_with_standard_catalog.json delete mode 100644 vendor/a2ui/specification/0.8/json/standard_catalog_definition.json delete mode 100644 vendor/a2ui/specification/0.9/eval/.gitignore delete mode 100644 vendor/a2ui/specification/0.9/eval/README.md delete mode 100644 vendor/a2ui/specification/0.9/eval/genkit.conf.js delete mode 100644 vendor/a2ui/specification/0.9/eval/package.json delete mode 100644 vendor/a2ui/specification/0.9/eval/pnpm-lock.yaml delete mode 100644 vendor/a2ui/specification/0.9/eval/pnpm-workspace.yaml delete mode 100644 vendor/a2ui/specification/0.9/eval/src/ai.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/analysis_flow.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/dev.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/evaluation_flow.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/evaluator.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/generation_flow.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/generator.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/index.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/logger.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/models.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/prompts.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/rateLimiter.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/types.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/utils.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/src/validator.ts delete mode 100644 vendor/a2ui/specification/0.9/eval/tsconfig.json delete mode 100644 vendor/a2ui/specification/0.9/json/client_to_server.json delete mode 100644 vendor/a2ui/specification/0.9/json/common_types.json delete mode 100644 vendor/a2ui/specification/0.9/json/contact_form_example.jsonl delete mode 100644 vendor/a2ui/specification/0.9/json/server_to_client.json delete mode 100644 vendor/a2ui/specification/0.9/json/standard_catalog_definition.json delete mode 100644 vendor/a2ui/specification/0.9/json/standard_catalog_rules.txt delete mode 100755 vendor/a2ui/specification/0.9/validate.sh diff --git a/.agents/skills/crabbox/SKILL.md b/.agents/skills/crabbox/SKILL.md index 95eda7ae25d..1e52d71a87c 100644 --- a/.agents/skills/crabbox/SKILL.md +++ b/.agents/skills/crabbox/SKILL.md @@ -229,21 +229,6 @@ Raw Blacksmith footguns: - Treat `blacksmith testbox list` as cleanup diagnostics, not a shared reusable queue. -Blacksmith queue/outage mode: - -```sh -blacksmith --version -blacksmith testbox list --all -blacksmith testbox status --id -``` - -If the CLI can list/status boxes but new warmups stay `queued` with no IP or -Actions run URL after a couple of minutes, treat it as Blacksmith provider, -org-limit, billing, or queue pressure. Stop the queued ids you created and do -not warm more boxes into the same stalled queue. Check the Blacksmith dashboard, -billing, and org limits out-of-band, then use Owned Cloud Fallback below for -maintainer proof. - Escalate to owned AWS/Hetzner only when Blacksmith is down, quota-limited, missing the needed environment, or owned capacity is the explicit goal. Use the Owned Cloud Fallback section below. @@ -277,9 +262,6 @@ Important Blacksmith footguns: - Always run from repo root. The CLI syncs the current directory. - Raw commit SHAs are not reliable `warmup --ref` refs; use a branch or tag. -- If `blacksmith testbox list --all` works but warmups stay `queued`, this is - not a Crabbox bug. Stop the queued ids and switch to owned AWS/Hetzner instead - of retrying. - If auth is missing and browser auth is acceptable: ```sh @@ -291,45 +273,8 @@ blacksmith auth login --non-interactive --organization openclaw Use AWS/Hetzner only when Blacksmith is down, quota-limited, missing the needed environment, or owned capacity is explicitly the goal. -When AWS capacity is under pressure, do not start with `class=beast`. -`beast` begins at 48xlarge instances and can burn 192 vCPU quota per request. -OpenClaw's owned-cloud default is `standard`; escalate to `fast`, then `large`, -and only use `beast` when the work is explicitly CPU-bound and the smaller class -already failed the goal. -Keep capacity hints enabled so brokered AWS leases print selected region/market, -quota pressure, Spot fallback, and high-pressure class warnings. The OpenClaw -repo config sets `capacity.hints: true`; use `CRABBOX_CAPACITY_HINTS=0` only -when debugging hint rendering itself. - -Use `beast` only for exceptional lanes: - -- full-suite or all-plugin Docker matrices where wall time is dominated by CPU, - not dependency install or network; -- release/blocker validation where a maintainer explicitly asks for the largest - owned AWS class; -- performance profiling where the point is to compare high-core behavior. - -Do not use `beast` for `pnpm check:changed`, focused tests, docs-only work, -ordinary lint/typecheck, small E2E repros, or Blacksmith outage triage. Those -should use `standard` first and `fast` only when the extra cores materially help. - -Preferred AWS pressure-relief flow: - ```sh -CRABBOX_CAPACITY_REGIONS=eu-west-1,eu-west-2,eu-central-1,us-east-1,us-west-2 \ - pnpm crabbox:warmup -- --provider aws --class standard --market on-demand --idle-timeout 90m -pnpm crabbox:hydrate -- --id -pnpm crabbox:run -- --id --timing-json --shell -- "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm check:changed" -pnpm crabbox:stop -- -``` - -Use `--market spot` only when testing Spot behavior or saving cost matters more -than launch reliability. Use `--market on-demand` when diagnosing quota/capacity -because it removes Spot market churn from the failure. - -```sh -CRABBOX_CAPACITY_REGIONS=eu-west-1,eu-west-2,eu-central-1,us-east-1,us-west-2 \ - pnpm crabbox:warmup -- --provider aws --class fast --market on-demand --idle-timeout 90m +pnpm crabbox:warmup -- --provider aws --class beast --market on-demand --idle-timeout 90m pnpm crabbox:hydrate -- --id pnpm crabbox:run -- --id --timing-json --shell -- "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test:changed" pnpm crabbox:stop -- diff --git a/.crabbox.yaml b/.crabbox.yaml index d745f9d837a..ab0046d8ce9 100644 --- a/.crabbox.yaml +++ b/.crabbox.yaml @@ -1,17 +1,12 @@ profile: openclaw-check provider: aws -class: standard +class: beast capacity: market: spot strategy: most-available fallback: on-demand-after-120s - hints: true regions: - eu-west-1 - - eu-west-2 - - eu-central-1 - - us-east-1 - - us-west-2 actions: workflow: .github/workflows/crabbox-hydrate.yml job: hydrate diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e5f0a311bf4..d659077dc34 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -37,7 +37,7 @@ If this PR fixes a plugin beta-release blocker, title it `fix(): beta ## Real behavior proof (required for external PRs) -External contributors must show after-fix evidence from a real OpenClaw setup. Unit tests, mocks, lint, typechecks, snapshots, and CI are supplemental only. Screenshots are encouraged even for CLI, console, text, or log changes; terminal screenshots and copied live output count. Be mindful of private information like IP addresses, API keys, phone numbers, non-public endpoints, or other private details when providing evidence. +External contributors must show after-fix evidence from a real OpenClaw setup. Unit tests, mocks, lint, typechecks, snapshots, and CI are supplemental only. Screenshots are encouraged even for CLI, console, text, or log changes; terminal screenshots and copied live output count. - Behavior or issue addressed: - Real environment tested: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39c8756ea12..b5e84058296 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,6 @@ jobs: # work fan out from a single source of truth. preflight: permissions: - actions: read contents: read if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: ubuntu-24.04 @@ -66,11 +65,9 @@ jobs: checks_node_core_dist_matrix: ${{ steps.manifest.outputs.checks_node_core_dist_matrix }} run_check: ${{ steps.manifest.outputs.run_check }} run_check_additional: ${{ steps.manifest.outputs.run_check_additional }} - additional_matrix: ${{ steps.manifest.outputs.additional_matrix }} run_build_smoke: ${{ steps.manifest.outputs.run_build_smoke }} run_check_docs: ${{ steps.manifest.outputs.run_check_docs }} run_control_ui_i18n: ${{ steps.manifest.outputs.run_control_ui_i18n }} - run_prompt_snapshots: ${{ steps.manifest.outputs.run_prompt_snapshots }} run_checks_windows: ${{ steps.manifest.outputs.run_checks_windows }} checks_windows_matrix: ${{ steps.manifest.outputs.checks_windows_matrix }} run_macos_node: ${{ steps.manifest.outputs.run_macos_node }} @@ -78,12 +75,6 @@ jobs: run_macos_swift: ${{ steps.manifest.outputs.run_macos_swift }} run_android_job: ${{ steps.manifest.outputs.run_android_job }} android_matrix: ${{ steps.manifest.outputs.android_matrix }} - runner_4vcpu_ubuntu: ${{ steps.runner_labels.outputs.runner_4vcpu_ubuntu }} - runner_8vcpu_ubuntu: ${{ steps.runner_labels.outputs.runner_8vcpu_ubuntu }} - runner_16vcpu_ubuntu: ${{ steps.runner_labels.outputs.runner_16vcpu_ubuntu }} - runner_16vcpu_windows: ${{ steps.runner_labels.outputs.runner_16vcpu_windows }} - runner_6vcpu_macos: ${{ steps.runner_labels.outputs.runner_6vcpu_macos }} - runner_12vcpu_macos: ${{ steps.runner_labels.outputs.runner_12vcpu_macos }} steps: - name: Checkout uses: actions/checkout@v6 @@ -139,7 +130,6 @@ jobs: OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }} OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_skills_python || 'false' }} OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_control_ui_i18n || 'false' }} - OPENCLAW_CI_RUN_PROMPT_SNAPSHOTS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_prompt_snapshots || 'false' }} OPENCLAW_CI_CHECKOUT_REVISION: ${{ steps.checkout_ref.outputs.sha }} OPENCLAW_CI_REPOSITORY: ${{ github.repository }} run: | @@ -204,46 +194,6 @@ jobs: const runSkillsPython = parseBoolean(process.env.OPENCLAW_CI_RUN_SKILLS_PYTHON) && !docsOnly; const runControlUiI18n = parseBoolean(process.env.OPENCLAW_CI_RUN_CONTROL_UI_I18N) && !docsOnly; - const runPromptSnapshots = - parseBoolean(process.env.OPENCLAW_CI_RUN_PROMPT_SNAPSHOTS) && !docsOnly; - const additionalCheckTasks = [ - { - check_name: "check-additional-boundaries-a", - group: "boundaries", - boundary_shard: "1/4", - }, - { - check_name: "check-additional-boundaries-b", - group: "boundaries", - boundary_shard: "2/4", - }, - { - check_name: "check-additional-boundaries-c", - group: "boundaries", - boundary_shard: "3/4", - }, - { - check_name: "check-additional-boundaries-d", - group: "boundaries", - boundary_shard: "4/4", - }, - { check_name: "check-additional-extension-channels", group: "extension-channels" }, - { check_name: "check-additional-extension-bundled", group: "extension-bundled" }, - { - check_name: "check-additional-extension-package-boundary", - group: "extension-package-boundary", - }, - { - check_name: "check-additional-runtime-topology-architecture", - group: "runtime-topology-architecture", - }, - ]; - if (runPromptSnapshots) { - additionalCheckTasks.push({ - check_name: "check-additional-prompt-snapshots", - group: "prompt-snapshots", - }); - } const checksFastCoreTasks = []; if (runNodeFull) { checksFastCoreTasks.push( @@ -309,11 +259,9 @@ jobs: checks_node_core_dist_matrix: createMatrix(nodeTestDistShards), run_check: runNodeFull, run_check_additional: runNodeFull, - additional_matrix: createMatrix(runNodeFull ? additionalCheckTasks : []), run_build_smoke: runNodeFull, run_check_docs: docsChanged, run_control_ui_i18n: runControlUiI18n, - run_prompt_snapshots: runPromptSnapshots, run_skills_python_job: runSkillsPython, run_checks_windows: runWindows, checks_windows_matrix: createMatrix( @@ -347,13 +295,6 @@ jobs: } EOF - - name: Select runner labels - id: runner_labels - env: - GITHUB_TOKEN: ${{ github.token }} - OPENCLAW_CI_BLACKSMITH_FALLBACK: "true" - run: node scripts/ci-runner-labels.mjs - # Run the fast security/SCM checks in parallel with scope detection so the # main Node jobs do not have to wait for Python/pre-commit setup. security-scm-fast: @@ -511,7 +452,7 @@ jobs: contents: read needs: [preflight] if: needs.preflight.outputs.run_build_artifacts == 'true' - runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_8vcpu_ubuntu || 'ubuntu-24.04' }} + runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 20 outputs: channels-result: ${{ steps.built_artifact_checks.outputs['channels-result'] }} @@ -606,11 +547,13 @@ jobs: path: dist-runtime-build.tar.zst retention-days: 1 - - name: Upload A2UI bundle artifact + - name: Upload bundled plugin asset artifacts uses: actions/upload-artifact@v7 with: - name: canvas-a2ui-bundle - path: src/canvas-host/a2ui/ + name: bundled-plugin-assets + path: | + extensions/*/src/host/**/.bundle.hash + extensions/*/src/host/**/*.bundle.js include-hidden-files: true retention-days: 1 @@ -633,7 +576,6 @@ jobs: RUN_CHANNELS: ${{ needs.preflight.outputs.run_checks }} RUN_CORE_SUPPORT_BOUNDARY: ${{ needs.preflight.outputs.run_checks_node_core_dist }} RUN_GATEWAY_WATCH: ${{ needs.preflight.outputs.run_check_additional }} - OPENCLAW_RUN_PROMPT_SNAPSHOTS: ${{ needs.preflight.outputs.run_prompt_snapshots }} shell: bash run: | set -uo pipefail @@ -711,7 +653,7 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: needs.preflight.outputs.run_checks_fast_core == 'true' - runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_4vcpu_ubuntu || 'ubuntu-24.04' }} + runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 60 strategy: fail-fast: false @@ -800,67 +742,13 @@ jobs: ;; esac - ci-timings-summary: - permissions: - actions: read - contents: read - name: ci-timings-summary - needs: - - preflight - - security-fast - - build-artifacts - - checks-fast-core - - checks-fast-plugin-contracts - - checks-fast-channel-contracts - - checks-fast-protocol - - checks - - checks-node-compat - - checks-node-core-test - - check - - check-additional - - build-smoke - - check-docs - - skills-python - - checks-windows - - macos-node - - macos-swift - - android - if: ${{ !cancelled() && always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} - runs-on: ubuntu-24.04 - timeout-minutes: 5 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.preflight.outputs.checkout_revision || github.sha }} - fetch-depth: 1 - fetch-tags: false - persist-credentials: false - submodules: false - - - name: Write CI timing summary - env: - GITHUB_REPOSITORY: ${{ github.repository }} - GH_TOKEN: ${{ github.token }} - RUN_ID: ${{ github.run_id }} - run: | - node scripts/ci-run-timings.mjs "$RUN_ID" --limit 25 > ci-timings-summary.txt - cat ci-timings-summary.txt >> "$GITHUB_STEP_SUMMARY" - - - name: Upload CI timing summary - uses: actions/upload-artifact@v7 - with: - name: ci-timings-summary - path: ci-timings-summary.txt - retention-days: 14 - checks-fast-plugin-contracts-shard: permissions: contents: read name: ${{ matrix.checkName }} needs: [preflight] if: needs.preflight.outputs.run_plugin_contracts_shards == 'true' - runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_4vcpu_ubuntu || 'ubuntu-24.04' }} + runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 60 strategy: fail-fast: false @@ -1169,7 +1057,7 @@ jobs: name: checks-node-compat-node22 needs: [preflight] if: needs.preflight.outputs.run_build_artifacts == 'true' && github.event_name == 'workflow_dispatch' - runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_4vcpu_ubuntu || 'ubuntu-24.04' }} + runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 60 steps: - name: Checkout @@ -1246,7 +1134,7 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: needs.preflight.outputs.run_checks_node_core_nondist == 'true' - runs-on: ${{ github.repository != 'openclaw/openclaw' && 'ubuntu-24.04' || matrix.runner == 'blacksmith-4vcpu-ubuntu-2404' && needs.preflight.outputs.runner_4vcpu_ubuntu || matrix.runner == 'blacksmith-8vcpu-ubuntu-2404' && needs.preflight.outputs.runner_8vcpu_ubuntu || matrix.runner == 'blacksmith-16vcpu-ubuntu-2404' && needs.preflight.outputs.runner_16vcpu_ubuntu || matrix.runner || 'ubuntu-24.04' }} + runs-on: ${{ github.repository == 'openclaw/openclaw' && (matrix.runner || 'ubuntu-24.04') || 'ubuntu-24.04' }} timeout-minutes: 60 strategy: fail-fast: false @@ -1414,7 +1302,7 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check == 'true' }} - runs-on: ${{ github.repository != 'openclaw/openclaw' && 'ubuntu-24.04' || matrix.runner == 'blacksmith-4vcpu-ubuntu-2404' && needs.preflight.outputs.runner_4vcpu_ubuntu || matrix.runner == 'blacksmith-8vcpu-ubuntu-2404' && needs.preflight.outputs.runner_8vcpu_ubuntu || matrix.runner == 'blacksmith-16vcpu-ubuntu-2404' && needs.preflight.outputs.runner_16vcpu_ubuntu || matrix.runner || 'ubuntu-24.04' }} + runs-on: ${{ github.repository == 'openclaw/openclaw' && matrix.runner || 'ubuntu-24.04' }} timeout-minutes: 20 strategy: fail-fast: false @@ -1575,11 +1463,32 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' }} - runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_8vcpu_ubuntu || 'ubuntu-24.04' }} + runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 20 strategy: fail-fast: false - matrix: ${{ fromJson(needs.preflight.outputs.additional_matrix) }} + matrix: + include: + - check_name: check-additional-boundaries-a + group: boundaries + boundary_shard: 1/4 + - check_name: check-additional-boundaries-b + group: boundaries + boundary_shard: 2/4 + - check_name: check-additional-boundaries-c + group: boundaries + boundary_shard: 3/4 + - check_name: check-additional-boundaries-d + group: boundaries + boundary_shard: 4/4 + - check_name: check-additional-extension-channels + group: extension-channels + - check_name: check-additional-extension-bundled + group: extension-bundled + - check_name: check-additional-extension-package-boundary + group: extension-package-boundary + - check_name: check-additional-runtime-topology-architecture + group: runtime-topology-architecture steps: - name: Checkout shell: bash @@ -1677,7 +1586,6 @@ jobs: env: ADDITIONAL_CHECK_GROUP: ${{ matrix.group }} OPENCLAW_ADDITIONAL_BOUNDARY_SHARD: ${{ matrix.boundary_shard || '' }} - OPENCLAW_RUN_PROMPT_SNAPSHOTS: ${{ needs.preflight.outputs.run_prompt_snapshots }} RUN_CONTROL_UI_I18N: ${{ needs.preflight.outputs.run_control_ui_i18n }} OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY: 4 OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY: 6 @@ -1705,9 +1613,6 @@ jobs: boundaries) node scripts/run-additional-boundary-checks.mjs ;; - prompt-snapshots) - run_check "prompt:snapshots:check" pnpm prompt:snapshots:check - ;; extension-channels) run_check "lint:extensions:channels" pnpm run lint:extensions:channels ;; @@ -1877,7 +1782,7 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: needs.preflight.outputs.run_checks_windows == 'true' - runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_16vcpu_windows || 'windows-2025' }} + runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-windows-2025' || 'windows-2025' }} timeout-minutes: 60 env: NODE_OPTIONS: --max-old-space-size=6144 @@ -1990,7 +1895,7 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_macos_node == 'true' }} - runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_6vcpu_macos || 'macos-latest' }} + runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-6vcpu-macos-latest' || 'macos-latest' }} timeout-minutes: 20 strategy: fail-fast: false @@ -2034,7 +1939,7 @@ jobs: name: "macos-swift" needs: [preflight] if: needs.preflight.outputs.run_macos_swift == 'true' - runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_12vcpu_macos || 'macos-latest' }} + runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-latest' || 'macos-latest' }} timeout-minutes: 20 steps: - name: Checkout @@ -2131,7 +2036,7 @@ jobs: name: ${{ matrix.check_name }} needs: [preflight] if: needs.preflight.outputs.run_android_job == 'true' - runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_8vcpu_ubuntu || 'ubuntu-24.04' }} + runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 20 strategy: fail-fast: false diff --git a/.gitignore b/.gitignore index efb6b5e820f..7d420cdcc0e 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,8 @@ apps/ios/*.xcfilelist vendor/a2ui/renderers/lit/dist/ src/canvas-host/a2ui/*.bundle.js src/canvas-host/a2ui/*.map +extensions/canvas/src/host/a2ui/*.bundle.js +extensions/canvas/src/host/a2ui/*.map .bundle.hash # fastlane (iOS) diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc index 2def267522f..221e1cd7172 100644 --- a/.oxfmtrc.jsonc +++ b/.oxfmtrc.jsonc @@ -14,6 +14,7 @@ "docker-compose.yml", "dist/", "docs/_layouts/", + "**/*.json", "node_modules/", "patches/", "pnpm-lock.yaml/", diff --git a/AGENTS.md b/AGENTS.md index 71fac20f004..f41103d56b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,7 +190,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work. - Mac gateway: dev watch = `pnpm gateway:watch` (tmux `openclaw-gateway-watch-main`, auto-attach). Noninteractive: `OPENCLAW_GATEWAY_WATCH_ATTACH=0 pnpm gateway:watch`; attach/stop: `tmux attach -t openclaw-gateway-watch-main` / `tmux kill-session -t openclaw-gateway-watch-main`. Managed installs: `openclaw gateway restart/status --deep`. No launchd/ad-hoc tmux. Logs: `./scripts/clawlog.sh`. - Version bump touches: `package.json`, `apps/android/app/build.gradle.kts`, `apps/ios/version.json` + `pnpm ios:version:sync`, macOS `Info.plist`, `docs/install/updating.md`. Appcast only for Sparkle release. - Mobile LAN pairing: plaintext `ws://` loopback-only. Private-network `ws://` needs `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; Tailscale/public use `wss://` or tunnel. -- A2UI hash `src/canvas-host/a2ui/.bundle.hash`: generated; ignore unless running `pnpm canvas:a2ui:bundle`; commit separately. +- A2UI hash `extensions/canvas/src/host/a2ui/.bundle.hash`: generated; ignore unless running `pnpm canvas:a2ui:bundle`; commit separately. ## Ops / Footguns diff --git a/CHANGELOG.md b/CHANGELOG.md index 6291f78cb1e..7260d01dcfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -141,6 +141,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Canvas plugin: keep legacy root `canvasHost` configs valid until `openclaw doctor --fix` migrates them into `plugins.entries.canvas.config.host`, move Canvas/A2UI clients to gateway protocol v4 plugin surfaces, and refresh the generated A2UI bundle hash so normal builds stay clean. - feishu: honor config write policy for dynamic agents [AI]. (#78520) Thanks @pgondhi987. - fix(skill-workshop): honor pending approval for tool suggestions [AI]. (#78516) Thanks @pgondhi987. - Native chat: decode gateway-provided thinking metadata for the iOS/macOS picker so provider-specific levels such as `adaptive`, `xhigh`, and `max` appear without leaking unsupported default-model options. Thanks @BunsDev. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4a2908bec36..c5b441bac09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -103,7 +103,7 @@ For coordinated change sets that genuinely need more than 20 PRs, join the **#cl ## Before You PR - Test locally with your OpenClaw instance -- External PRs must include a filled **Real behavior proof** section in the PR body. Show the real setup you tested, the exact command or steps you ran after the patch, after-fix evidence, the observed result, and anything you did not test. Screenshots, recordings, terminal screenshots, console output, copied live output, linked artifacts, and redacted runtime logs all count. Be mindful of private information like IP addresses, API keys, phone numbers, non-public endpoints, or other private details when providing evidence. Unit tests, mocks, snapshots, lint, typechecks, and CI are useful but do not satisfy this requirement by themselves. Maintainers may apply `proof: override` only when the proof gate should not apply. +- External PRs must include a filled **Real behavior proof** section in the PR body. Show the real setup you tested, the exact command or steps you ran after the patch, after-fix evidence, the observed result, and anything you did not test. Screenshots, recordings, terminal screenshots, console output, copied live output, linked artifacts, and redacted runtime logs all count. Unit tests, mocks, snapshots, lint, typechecks, and CI are useful but do not satisfy this requirement by themselves. Maintainers may apply `proof: override` only when the proof gate should not apply. - Run tests: `pnpm build && pnpm check && pnpm test` - For iterative local commits, `scripts/committer --fast "message" ` passes `FAST_COMMIT=1` through to the pre-commit hook so it skips the repo-wide `pnpm check`. Only use it when you've already run equivalent targeted validation for the touched surface. - For extension/plugin changes, run the fast local lane first: @@ -164,7 +164,7 @@ Built with Codex, Claude, or other AI tools? **Awesome - just mark it!** Please include in your PR: - [ ] Mark as AI-assisted in the PR title or description -- [ ] Include human-run real behavior proof from your own setup. Redact private information like IP addresses, API keys, phone numbers, or non-public endpoints before posting evidence. AI-generated tests, mocks, lint, typechecks, and CI output are supplemental only; they do not prove the fix works for users. +- [ ] Include human-run real behavior proof from your own setup. AI-generated tests, mocks, lint, typechecks, and CI output are supplemental only; they do not prove the fix works for users. - [ ] Include prompts or session logs if possible (super helpful!) - [ ] Confirm you understand what the code does - [ ] If you have access to Codex, run `codex review --base origin/main` locally and address the findings before asking for review diff --git a/Dockerfile b/Dockerfile index 081e0cfbb1b..3e9213bb882 100644 --- a/Dockerfile +++ b/Dockerfile @@ -97,9 +97,9 @@ RUN for dir in /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} /app/.agent /app/.agents; do # Stub it so local cross-arch builds still succeed. RUN pnpm canvas:a2ui:bundle || \ (echo "A2UI bundle: creating stub (non-fatal)" && \ - mkdir -p src/canvas-host/a2ui && \ - echo "/* A2UI bundle unavailable in this build */" > src/canvas-host/a2ui/a2ui.bundle.js && \ - echo "stub" > src/canvas-host/a2ui/.bundle.hash && \ + mkdir -p extensions/canvas/src/host/a2ui && \ + echo "/* A2UI bundle unavailable in this build */" > extensions/canvas/src/host/a2ui/a2ui.bundle.js && \ + echo "stub" > extensions/canvas/src/host/a2ui/.bundle.hash && \ rm -rf vendor/a2ui apps/shared/OpenClawKit/Tools/CanvasA2UI) RUN pnpm build:docker # Force pnpm for UI build (Bun may fail on ARM/Synology architectures) diff --git a/README.md b/README.md index 1ae806d9ea8..3bee5fbba4d 100644 --- a/README.md +++ b/README.md @@ -246,18 +246,13 @@ Note: `pnpm openclaw ...` runs TypeScript directly (via `tsx`). `pnpm build` pro ## Development channels -- **stable**: tagged releases (`vYYYY.M.D` today), npm dist-tag `latest`. +- **stable**: tagged releases (`vYYYY.M.D` or `vYYYY.M.D-`), npm dist-tag `latest`. - **beta**: prerelease tags (`vYYYY.M.D-beta.N`), npm dist-tag `beta` (macOS app may be missing). - **dev**: moving head of `main`, npm dist-tag `dev` (when published). Switch channels (git + npm): `openclaw update --channel stable|beta|dev`. Details: [Development channels](https://docs.openclaw.ai/install/development-channels). -We are planning SemVer-compatible monthly support lines using `YYYY.M.PATCH` -versions, but they are not available yet. Legacy `vYYYY.M.D-` correction -tags may still be recognized for older releases; new release work should not use -that format as the long-term support model. - ## Agent workspace + skills - Workspace root: `~/.openclaw/workspace` (configurable via `agents.defaults.workspace`). diff --git a/apps/android/README.md b/apps/android/README.md index d9e622ea6a6..3eb5d9eb763 100644 --- a/apps/android/README.md +++ b/apps/android/README.md @@ -285,7 +285,7 @@ Common failure quick-fixes: - `pairing required` before tests start: - approve pending device pairing (`openclaw devices approve --latest`) and rerun. - `A2UI host not reachable` / `A2UI_HOST_NOT_CONFIGURED`: - - ensure gateway canvas host is running and reachable, keep the app on the **Screen** tab. The app will auto-refresh canvas capability once; if it still fails, reconnect app and rerun. + - ensure the Canvas plugin host is running and reachable, keep the app on the **Screen** tab. The app refreshes the Canvas plugin surface URL once before failing; if it still fails, reconnect app and rerun. - `NODE_BACKGROUND_UNAVAILABLE: canvas unavailable`: - app is not effectively ready for canvas commands; keep app foregrounded and **Screen** tab active. diff --git a/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt b/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt index 379bb0f5908..42e7ab614d9 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt @@ -233,13 +233,13 @@ class NodeRuntime( smsTelephonyAvailable = { sms.hasTelephonyFeature() }, callLogAvailable = { SensitiveFeatureConfig.callLogEnabled }, debugBuild = { BuildConfig.DEBUG }, - refreshNodeCanvasCapability = { nodeSession.refreshNodeCanvasCapability() }, onCanvasA2uiPush = { _canvasA2uiHydrated.value = true _canvasRehydratePending.value = false _canvasRehydrateErrorText.value = null }, onCanvasA2uiReset = { _canvasA2uiHydrated.value = false }, + refreshCanvasHostUrl = { nodeSession.refreshCanvasHostUrl() }, motionActivityAvailable = { motionHandler.isActivityAvailable() }, motionPedometerAvailable = { motionHandler.isPedometerAvailable() }, ) diff --git a/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt b/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt index 27b4566ac93..ddf33c60702 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt @@ -1,3 +1,3 @@ package ai.openclaw.app.gateway -const val GATEWAY_PROTOCOL_VERSION = 3 +const val GATEWAY_PROTOCOL_VERSION = 4 diff --git a/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt b/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt index 73c9b1e1cdb..1cf13a43c3e 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt @@ -135,7 +135,7 @@ class GatewaySession( private val writeLock = Mutex() private val pending = ConcurrentHashMap>() - @Volatile private var canvasHostUrl: String? = null + @Volatile private var pluginSurfaceUrls: Map = emptyMap() @Volatile private var mainSessionKey: String? = null @@ -185,7 +185,7 @@ class GatewaySession( scope.launch(Dispatchers.IO) { job?.cancelAndJoin() job = null - canvasHostUrl = null + pluginSurfaceUrls = emptyMap() mainSessionKey = null onDisconnected("Offline") } @@ -196,7 +196,20 @@ class GatewaySession( currentConnection?.closeQuietly() } - fun currentCanvasHostUrl(): String? = canvasHostUrl + fun currentCanvasHostUrl(): String? = pluginSurfaceUrls["canvas"] + + suspend fun refreshCanvasHostUrl(timeoutMs: Long = 8_000): String? { + val refreshed = + refreshPluginSurfaceUrl( + method = "node.pluginSurface.refresh", + params = buildJsonObject { put("surface", JsonPrimitive("canvas")) }, + timeoutMs = timeoutMs, + ) + if (!refreshed.isNullOrBlank()) { + pluginSurfaceUrls = pluginSurfaceUrls + ("canvas" to refreshed) + } + return refreshed + } fun currentMainSessionKey(): String? = mainSessionKey @@ -218,6 +231,28 @@ class GatewaySession( } } + private suspend fun refreshPluginSurfaceUrl( + method: String, + params: JsonElement?, + timeoutMs: Long, + ): String? { + val conn = currentConnection ?: return null + return try { + val res = conn.request(method, params, timeoutMs) + if (!res.ok) return null + val obj = res.payloadJson?.let { json.parseToJsonElement(it).asObjectOrNull() } ?: return null + val raw = + obj["pluginSurfaceUrls"] + .asObjectOrNull() + ?.get("canvas") + .asStringOrNull() + normalizeCanvasHostUrl(raw, conn.endpoint, isTlsConnection = conn.tls != null) + } catch (err: Throwable) { + Log.d("OpenClawGateway", "$method failed: ${err.message ?: err::class.java.simpleName}") + null + } + } + suspend fun sendNodeEventDetailed( event: String, payloadJson: String?, @@ -280,52 +315,6 @@ class GatewaySession( return RpcResult(ok = res.ok, payloadJson = res.payloadJson, error = res.error) } - suspend fun refreshNodeCanvasCapability(timeoutMs: Long = 8_000): Boolean { - val conn = currentConnection ?: return false - val response = - try { - conn.request( - "node.canvas.capability.refresh", - params = buildJsonObject {}, - timeoutMs = timeoutMs, - ) - } catch (err: Throwable) { - Log.w("OpenClawGateway", "node.canvas.capability.refresh failed: ${err.message ?: err::class.java.simpleName}") - return false - } - if (!response.ok) { - val err = response.error - Log.w( - "OpenClawGateway", - "node.canvas.capability.refresh rejected: ${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}", - ) - return false - } - val payloadObj = response.payloadJson?.let(::parseJsonOrNull)?.asObjectOrNull() - val refreshedCapability = - payloadObj - ?.get("canvasCapability") - .asStringOrNull() - ?.trim() - .orEmpty() - if (refreshedCapability.isEmpty()) { - Log.w("OpenClawGateway", "node.canvas.capability.refresh missing canvasCapability") - return false - } - val scopedCanvasHostUrl = canvasHostUrl?.trim().orEmpty() - if (scopedCanvasHostUrl.isEmpty()) { - Log.w("OpenClawGateway", "node.canvas.capability.refresh missing local canvasHostUrl") - return false - } - val refreshedUrl = replaceCanvasCapabilityInScopedHostUrl(scopedCanvasHostUrl, refreshedCapability) - if (refreshedUrl == null) { - Log.w("OpenClawGateway", "node.canvas.capability.refresh unable to rewrite scoped canvas URL") - return false - } - canvasHostUrl = refreshedUrl - return true - } - private data class RpcResponse( val id: String, val ok: Boolean, @@ -334,12 +323,12 @@ class GatewaySession( ) private inner class Connection( - private val endpoint: GatewayEndpoint, + val endpoint: GatewayEndpoint, private val token: String?, private val bootstrapToken: String?, private val password: String?, private val options: GatewayConnectOptions, - private val tls: GatewayTlsParams?, + val tls: GatewayTlsParams?, ) { private val connectDeferred = CompletableDeferred() private val closedDeferred = CompletableDeferred() @@ -615,8 +604,13 @@ class GatewaySession( } } } - val rawCanvas = obj["canvasHostUrl"].asStringOrNull() - canvasHostUrl = normalizeCanvasHostUrl(rawCanvas, endpoint, isTlsConnection = tls != null) + val rawPluginSurfaceUrls = obj["pluginSurfaceUrls"].asObjectOrNull() + val normalizedPluginSurfaceUrls = + rawPluginSurfaceUrls?.mapNotNull { (surface, value) -> + normalizeCanvasHostUrl(value.asStringOrNull(), endpoint, isTlsConnection = tls != null) + ?.let { normalized -> surface to normalized } + } ?: emptyList() + pluginSurfaceUrls = normalizedPluginSurfaceUrls.toMap() val sessionDefaults = obj["snapshot"] .asObjectOrNull() @@ -910,7 +904,7 @@ class GatewaySession( conn.awaitClose() } finally { currentConnection = null - canvasHostUrl = null + pluginSurfaceUrls = emptyMap() mainSessionKey = null } } @@ -1133,22 +1127,6 @@ private fun parseJsonOrNull(payload: String): JsonElement? { } } -internal fun replaceCanvasCapabilityInScopedHostUrl( - scopedUrl: String, - capability: String, -): String? { - val marker = "/__openclaw__/cap/" - val markerStart = scopedUrl.indexOf(marker) - if (markerStart < 0) return null - val capabilityStart = markerStart + marker.length - val slashEnd = scopedUrl.indexOf("/", capabilityStart).takeIf { it >= 0 } - val queryEnd = scopedUrl.indexOf("?", capabilityStart).takeIf { it >= 0 } - val fragmentEnd = scopedUrl.indexOf("#", capabilityStart).takeIf { it >= 0 } - val capabilityEnd = listOfNotNull(slashEnd, queryEnd, fragmentEnd).minOrNull() ?: scopedUrl.length - if (capabilityEnd <= capabilityStart) return null - return scopedUrl.substring(0, capabilityStart) + capability + scopedUrl.substring(capabilityEnd) -} - internal fun resolveInvokeResultAckTimeoutMs(invokeTimeoutMs: Long?): Long { val normalized = invokeTimeoutMs?.takeIf { it > 0L } ?: 15_000L return normalized.coerceIn(15_000L, 120_000L) diff --git a/apps/android/app/src/main/java/ai/openclaw/app/node/InvokeDispatcher.kt b/apps/android/app/src/main/java/ai/openclaw/app/node/InvokeDispatcher.kt index 10d610ef8a4..b6afaf8256a 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/node/InvokeDispatcher.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/node/InvokeDispatcher.kt @@ -78,9 +78,9 @@ class InvokeDispatcher( private val smsTelephonyAvailable: () -> Boolean, private val callLogAvailable: () -> Boolean, private val debugBuild: () -> Boolean, - private val refreshNodeCanvasCapability: suspend () -> Boolean, private val onCanvasA2uiPush: () -> Unit, private val onCanvasA2uiReset: () -> Unit, + private val refreshCanvasHostUrl: suspend () -> String?, private val motionActivityAvailable: () -> Boolean, private val motionPedometerAvailable: () -> Boolean, ) { @@ -231,23 +231,15 @@ class InvokeDispatcher( private suspend fun withReadyA2ui(block: suspend () -> GatewaySession.InvokeResult): GatewaySession.InvokeResult { var a2uiUrl = a2uiHandler.resolveA2uiHostUrl() + ?: refreshCanvasHostUrl().let { a2uiHandler.resolveA2uiHostUrl() } ?: return GatewaySession.InvokeResult.error( code = "A2UI_HOST_NOT_CONFIGURED", message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host", ) val readyOnFirstCheck = a2uiHandler.ensureA2uiReady(a2uiUrl) if (!readyOnFirstCheck) { - if (!refreshNodeCanvasCapability()) { - return GatewaySession.InvokeResult.error( - code = "A2UI_HOST_UNAVAILABLE", - message = "A2UI_HOST_UNAVAILABLE: A2UI host not reachable", - ) - } - a2uiUrl = a2uiHandler.resolveA2uiHostUrl() - ?: return GatewaySession.InvokeResult.error( - code = "A2UI_HOST_NOT_CONFIGURED", - message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host", - ) + refreshCanvasHostUrl() + a2uiUrl = a2uiHandler.resolveA2uiHostUrl() ?: a2uiUrl if (!a2uiHandler.ensureA2uiReady(a2uiUrl)) { return GatewaySession.InvokeResult.error( code = "A2UI_HOST_UNAVAILABLE", diff --git a/apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewaySessionInvokeTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewaySessionInvokeTest.kt index 81f33937842..7437adc6e0c 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewaySessionInvokeTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewaySessionInvokeTest.kt @@ -476,56 +476,6 @@ class GatewaySessionInvokeTest { ) } - @Test - fun refreshNodeCanvasCapability_sendsObjectParamsAndUpdatesScopedUrl() = - runBlocking { - val json = testJson() - val connected = CompletableDeferred() - val refreshRequestParams = CompletableDeferred() - val lastDisconnect = AtomicReference("") - - val server = - startGatewayServer(json) { webSocket, id, method, frame -> - when (method) { - "connect" -> { - webSocket.send(connectResponseFrame(id, canvasHostUrl = "http://127.0.0.1/__openclaw__/cap/old-cap")) - } - "node.canvas.capability.refresh" -> { - if (!refreshRequestParams.isCompleted) { - refreshRequestParams.complete(frame["params"]?.toString()) - } - webSocket.send( - """{"type":"res","id":"$id","ok":true,"payload":{"canvasCapability":"new-cap"}}""", - ) - webSocket.close(1000, "done") - } - } - } - - val harness = - createNodeHarness( - connected = connected, - lastDisconnect = lastDisconnect, - ) { GatewaySession.InvokeResult.ok("""{"handled":true}""") } - - try { - connectNodeSession(harness.session, server.port) - awaitConnectedOrThrow(connected, lastDisconnect, server) - - val refreshed = harness.session.refreshNodeCanvasCapability(timeoutMs = TEST_TIMEOUT_MS) - val refreshParamsJson = withTimeout(TEST_TIMEOUT_MS) { refreshRequestParams.await() } - - assertEquals(true, refreshed) - assertEquals("{}", refreshParamsJson) - assertEquals( - "http://127.0.0.1:${server.port}/__openclaw__/cap/new-cap", - harness.session.currentCanvasHostUrl(), - ) - } finally { - shutdownHarness(harness, server) - } - } - @Test fun sendNodeEventDetailed_sendsPresenceAlivePayloadAndReturnsStructuredResponse() = runBlocking { @@ -778,12 +728,17 @@ class GatewaySessionInvokeTest { private fun connectResponseFrame( id: String, - canvasHostUrl: String? = null, + pluginSurfaceUrls: Map = emptyMap(), authJson: String? = null, ): String { - val canvas = canvasHostUrl?.let { "\"canvasHostUrl\":\"$it\"," } ?: "" + val surfaces = + pluginSurfaceUrls.entries + .joinToString(",") { (key, value) -> """"$key":"$value"""" } + .takeIf { it.isNotEmpty() } + ?.let { """"pluginSurfaceUrls":{$it},""" } + ?: "" val auth = authJson?.let { "\"auth\":$it," } ?: "" - return """{"type":"res","id":"$id","ok":true,"payload":{$canvas$auth"snapshot":{"sessionDefaults":{"mainSessionKey":"main"}}}}""" + return """{"type":"res","id":"$id","ok":true,"payload":{$surfaces$auth"snapshot":{"sessionDefaults":{"mainSessionKey":"main"}}}}""" } private fun startGatewayServer( diff --git a/apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewaySessionInvokeTimeoutTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewaySessionInvokeTimeoutTest.kt index 0f7b072722b..0b79cbbd454 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewaySessionInvokeTimeoutTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewaySessionInvokeTimeoutTest.kt @@ -39,26 +39,4 @@ class GatewaySessionInvokeTimeoutTest { assertEquals(120_000L, resolveInvokeResultAckTimeoutMs(121_000L)) assertEquals(120_000L, resolveInvokeResultAckTimeoutMs(Long.MAX_VALUE)) } - - @Test - fun replaceCanvasCapabilityInScopedHostUrl_rewritesTerminalCapabilitySegment() { - assertEquals( - "http://127.0.0.1:18789/__openclaw__/cap/new-token", - replaceCanvasCapabilityInScopedHostUrl( - "http://127.0.0.1:18789/__openclaw__/cap/old-token", - "new-token", - ), - ) - } - - @Test - fun replaceCanvasCapabilityInScopedHostUrl_rewritesWhenQueryAndFragmentPresent() { - assertEquals( - "http://127.0.0.1:18789/__openclaw__/cap/new-token?a=1#frag", - replaceCanvasCapabilityInScopedHostUrl( - "http://127.0.0.1:18789/__openclaw__/cap/old-token?a=1#frag", - "new-token", - ), - ) - } } diff --git a/apps/android/app/src/test/java/ai/openclaw/app/node/InvokeDispatcherTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/node/InvokeDispatcherTest.kt index cad08b1f689..80bacc6efe5 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/node/InvokeDispatcherTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/node/InvokeDispatcherTest.kt @@ -286,9 +286,9 @@ class InvokeDispatcherTest { smsTelephonyAvailable = { smsTelephonyAvailable }, callLogAvailable = { callLogAvailable }, debugBuild = { debugBuild }, - refreshNodeCanvasCapability = { false }, onCanvasA2uiPush = {}, onCanvasA2uiReset = {}, + refreshCanvasHostUrl = { null }, motionActivityAvailable = { motionActivityAvailable }, motionPedometerAvailable = { motionPedometerAvailable }, ) diff --git a/apps/ios/Sources/Model/NodeAppModel+Canvas.swift b/apps/ios/Sources/Model/NodeAppModel+Canvas.swift index 668cd3a8245..9259fe96459 100644 --- a/apps/ios/Sources/Model/NodeAppModel+Canvas.swift +++ b/apps/ios/Sources/Model/NodeAppModel+Canvas.swift @@ -63,10 +63,9 @@ extension NodeAppModel { if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) { return .ready(initialUrl) } - - // First render can fail when scoped capability rotates between reconnects. - guard await self.gatewaySession.refreshNodeCanvasCapability() else { return .hostUnavailable } - guard let refreshedUrl = await self.resolveA2UIHostURL() else { return .hostUnavailable } + guard let refreshedUrl = await self.resolveA2UIHostURLWithCapabilityRefresh(forceRefresh: true) else { + return .hostUnavailable + } self.screen.navigate(to: refreshedUrl, trustA2UIActions: true) if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) { return .ready(refreshedUrl) @@ -79,19 +78,19 @@ extension NodeAppModel { self.screen.showDefaultCanvas() } - private func resolveA2UIHostURLWithCapabilityRefresh() async -> String? { - if let url = await self.resolveA2UIHostURL() { - return url + private func resolveA2UIHostURLWithCapabilityRefresh(forceRefresh: Bool = false) async -> String? { + if !forceRefresh, let current = await self.resolveA2UIHostURL() { + return current } - guard await self.gatewaySession.refreshNodeCanvasCapability() else { return nil } + _ = await self.gatewaySession.refreshCanvasHostUrl() return await self.resolveA2UIHostURL() } - private func resolveCanvasHostURLWithCapabilityRefresh() async -> String? { - if let url = await self.resolveCanvasHostURL() { - return url + private func resolveCanvasHostURLWithCapabilityRefresh(forceRefresh: Bool = false) async -> String? { + if !forceRefresh, let current = await self.resolveCanvasHostURL() { + return current } - guard await self.gatewaySession.refreshNodeCanvasCapability() else { return nil } + _ = await self.gatewaySession.refreshCanvasHostUrl() return await self.resolveCanvasHostURL() } diff --git a/apps/macos/Sources/OpenClaw/CanvasManager.swift b/apps/macos/Sources/OpenClaw/CanvasManager.swift index 4b2c3120e6e..dfd690111af 100644 --- a/apps/macos/Sources/OpenClaw/CanvasManager.swift +++ b/apps/macos/Sources/OpenClaw/CanvasManager.swift @@ -152,15 +152,17 @@ final class CanvasManager { private func handleGatewayPush(_ push: GatewayPush) { guard case let .snapshot(snapshot) = push else { return } - let raw = snapshot.canvashosturl?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let raw = + (snapshot.pluginsurfaceurls?["canvas"]?.value as? String)? + .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) ?? "" if raw.isEmpty { - Self.logger.debug("canvas host url missing in gateway snapshot") + Self.logger.debug("canvas plugin surface URL missing in gateway snapshot") } else { - Self.logger.debug("canvas host url snapshot=\(raw, privacy: .public)") + Self.logger.debug("canvas plugin surface URL snapshot=\(raw, privacy: .public)") } let a2uiUrl = Self.resolveA2UIHostUrl(from: raw) if a2uiUrl == nil, !raw.isEmpty { - Self.logger.debug("canvas host url invalid; cannot resolve A2UI") + Self.logger.debug("canvas plugin surface URL invalid; cannot resolve A2UI") } guard let controller = self.panelController else { if a2uiUrl != nil { @@ -197,7 +199,7 @@ final class CanvasManager { } private func resolveA2UIHostUrl() async -> String? { - let raw = await GatewayConnection.shared.canvasHostUrl() + let raw = await GatewayConnection.shared.canvasPluginSurfaceUrl() return Self.resolveA2UIHostUrl(from: raw) } diff --git a/apps/macos/Sources/OpenClaw/GatewayConnection.swift b/apps/macos/Sources/OpenClaw/GatewayConnection.swift index 261b94b02c4..1ec076b1e6e 100644 --- a/apps/macos/Sources/OpenClaw/GatewayConnection.swift +++ b/apps/macos/Sources/OpenClaw/GatewayConnection.swift @@ -311,9 +311,10 @@ actor GatewayConnection { self.lastSnapshot = nil } - func canvasHostUrl() async -> String? { + func canvasPluginSurfaceUrl() async -> String? { guard let snapshot = self.lastSnapshot else { return nil } - let trimmed = snapshot.canvashosturl?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) ?? "" + let raw = snapshot.pluginsurfaceurls?["canvas"]?.value as? String + let trimmed = raw?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) ?? "" return trimmed.isEmpty ? nil : trimmed } diff --git a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeModeCoordinator.swift b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeModeCoordinator.swift index 20ede5febc9..7cf8938bcca 100644 --- a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeModeCoordinator.swift +++ b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeModeCoordinator.swift @@ -8,10 +8,18 @@ final class MacNodeModeCoordinator { private let logger = Logger(subsystem: "ai.openclaw", category: "mac-node") private var task: Task? - private let runtime = MacNodeRuntime() - private let session = GatewayNodeSession() + private let runtime: MacNodeRuntime + private let session: GatewayNodeSession private var autoRepairedTLSFingerprintsByStoreKey: [String: String] = [:] + private init() { + let session = GatewayNodeSession() + self.session = session + self.runtime = MacNodeRuntime( + canvasSurfaceUrl: { await session.currentCanvasHostUrl() }, + refreshCanvasSurfaceUrl: { await session.refreshCanvasHostUrl() }) + } + func start() { guard self.task == nil else { return } self.task = Task { [weak self] in diff --git a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntime.swift b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntime.swift index fa156895f1d..077f1868fd7 100644 --- a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntime.swift +++ b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntime.swift @@ -7,6 +7,8 @@ actor MacNodeRuntime { private let cameraCapture = CameraCaptureService() private let makeMainActorServices: () async -> any MacNodeRuntimeMainActorServices private let browserProxyRequest: @Sendable (String?) async throws -> String + private let canvasSurfaceUrl: @Sendable () async -> String? + private let refreshCanvasSurfaceUrl: @Sendable () async -> String? private var cachedMainActorServices: (any MacNodeRuntimeMainActorServices)? private var mainSessionKey: String = "main" private var eventSender: (@Sendable (String, String?) async -> Void)? @@ -17,10 +19,16 @@ actor MacNodeRuntime { }, browserProxyRequest: @escaping @Sendable (String?) async throws -> String = { paramsJSON in try await MacNodeBrowserProxy.shared.request(paramsJSON: paramsJSON) - }) + }, + canvasSurfaceUrl: @escaping @Sendable () async -> String? = { + await GatewayConnection.shared.canvasPluginSurfaceUrl() + }, + refreshCanvasSurfaceUrl: @escaping @Sendable () async -> String? = { nil }) { self.makeMainActorServices = makeMainActorServices self.browserProxyRequest = browserProxyRequest + self.canvasSurfaceUrl = canvasSurfaceUrl + self.refreshCanvasSurfaceUrl = refreshCanvasSurfaceUrl } func updateMainSessionKey(_ sessionKey: String) { @@ -441,7 +449,7 @@ actor MacNodeRuntime { private func ensureA2UIHost() async throws { if await self.isA2UIReady() { return } - guard let a2uiUrl = await self.resolveA2UIHostUrl() else { + guard let a2uiUrl = await self.resolveA2UIHostUrlWithCapabilityRefresh() else { throw NSError(domain: "Canvas", code: 30, userInfo: [ NSLocalizedDescriptionKey: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host", ]) @@ -451,18 +459,35 @@ actor MacNodeRuntime { try CanvasManager.shared.show(sessionKey: sessionKey, path: a2uiUrl) } if await self.isA2UIReady(poll: true) { return } + if let refreshedUrl = await self.resolveA2UIHostUrlWithCapabilityRefresh(forceRefresh: true) { + _ = try await MainActor.run { + try CanvasManager.shared.show(sessionKey: sessionKey, path: refreshedUrl) + } + if await self.isA2UIReady(poll: true) { return } + } throw NSError(domain: "Canvas", code: 31, userInfo: [ NSLocalizedDescriptionKey: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable", ]) } private func resolveA2UIHostUrl() async -> String? { - guard let raw = await GatewayConnection.shared.canvasHostUrl() else { return nil } + Self.resolveA2UIHostUrl(from: await self.canvasSurfaceUrl()) + } + + private static func resolveA2UIHostUrl(from raw: String?) -> String? { + guard let raw else { return nil } let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty, let baseUrl = URL(string: trimmed) else { return nil } return baseUrl.appendingPathComponent("__openclaw__/a2ui/").absoluteString + "?platform=macos" } + func resolveA2UIHostUrlWithCapabilityRefresh(forceRefresh: Bool = false) async -> String? { + if !forceRefresh, let current = await self.resolveA2UIHostUrl() { + return current + } + return Self.resolveA2UIHostUrl(from: await self.refreshCanvasSurfaceUrl()) + } + private func isA2UIReady(poll: Bool = false) async -> Bool { let deadline = poll ? Date().addingTimeInterval(6.0) : Date() while true { diff --git a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift deleted file mode 100644 index ccef9eb5dd0..00000000000 --- a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift +++ /dev/null @@ -1,5847 +0,0 @@ -// Generated by scripts/protocol-gen-swift.ts — do not edit by hand -// swiftlint:disable file_length -import Foundation - -public let GATEWAY_PROTOCOL_VERSION = 3 - -public enum ErrorCode: String, Codable, Sendable { - case notLinked = "NOT_LINKED" - case notPaired = "NOT_PAIRED" - case agentTimeout = "AGENT_TIMEOUT" - case invalidRequest = "INVALID_REQUEST" - case approvalNotFound = "APPROVAL_NOT_FOUND" - case unavailable = "UNAVAILABLE" -} - -public enum EnvironmentStatus: String, Codable, Sendable { - case available = "available" - case unavailable = "unavailable" - case starting = "starting" - case stopping = "stopping" - case error = "error" -} - -public enum NodePresenceAliveReason: String, Codable, Sendable { - case background = "background" - case silentPush = "silent_push" - case bgAppRefresh = "bg_app_refresh" - case significantLocation = "significant_location" - case manual = "manual" - case connect = "connect" -} - -public struct ConnectParams: Codable, Sendable { - public let minprotocol: Int - public let maxprotocol: Int - public let client: [String: AnyCodable] - public let caps: [String]? - public let commands: [String]? - public let permissions: [String: AnyCodable]? - public let pathenv: String? - public let role: String? - public let scopes: [String]? - public let device: [String: AnyCodable]? - public let auth: [String: AnyCodable]? - public let locale: String? - public let useragent: String? - - public init( - minprotocol: Int, - maxprotocol: Int, - client: [String: AnyCodable], - caps: [String]?, - commands: [String]?, - permissions: [String: AnyCodable]?, - pathenv: String?, - role: String?, - scopes: [String]?, - device: [String: AnyCodable]?, - auth: [String: AnyCodable]?, - locale: String?, - useragent: String?) - { - self.minprotocol = minprotocol - self.maxprotocol = maxprotocol - self.client = client - self.caps = caps - self.commands = commands - self.permissions = permissions - self.pathenv = pathenv - self.role = role - self.scopes = scopes - self.device = device - self.auth = auth - self.locale = locale - self.useragent = useragent - } - - private enum CodingKeys: String, CodingKey { - case minprotocol = "minProtocol" - case maxprotocol = "maxProtocol" - case client - case caps - case commands - case permissions - case pathenv = "pathEnv" - case role - case scopes - case device - case auth - case locale - case useragent = "userAgent" - } -} - -public struct HelloOk: Codable, Sendable { - public let type: String - public let _protocol: Int - public let server: [String: AnyCodable] - public let features: [String: AnyCodable] - public let snapshot: Snapshot - public let canvashosturl: String? - public let auth: [String: AnyCodable] - public let policy: [String: AnyCodable] - - public init( - type: String, - _protocol: Int, - server: [String: AnyCodable], - features: [String: AnyCodable], - snapshot: Snapshot, - canvashosturl: String?, - auth: [String: AnyCodable], - policy: [String: AnyCodable]) - { - self.type = type - self._protocol = _protocol - self.server = server - self.features = features - self.snapshot = snapshot - self.canvashosturl = canvashosturl - self.auth = auth - self.policy = policy - } - - private enum CodingKeys: String, CodingKey { - case type - case _protocol = "protocol" - case server - case features - case snapshot - case canvashosturl = "canvasHostUrl" - case auth - case policy - } -} - -public struct RequestFrame: Codable, Sendable { - public let type: String - public let id: String - public let method: String - public let params: AnyCodable? - - public init( - type: String, - id: String, - method: String, - params: AnyCodable?) - { - self.type = type - self.id = id - self.method = method - self.params = params - } - - private enum CodingKeys: String, CodingKey { - case type - case id - case method - case params - } -} - -public struct ResponseFrame: Codable, Sendable { - public let type: String - public let id: String - public let ok: Bool - public let payload: AnyCodable? - public let error: [String: AnyCodable]? - - public init( - type: String, - id: String, - ok: Bool, - payload: AnyCodable?, - error: [String: AnyCodable]?) - { - self.type = type - self.id = id - self.ok = ok - self.payload = payload - self.error = error - } - - private enum CodingKeys: String, CodingKey { - case type - case id - case ok - case payload - case error - } -} - -public struct EventFrame: Codable, Sendable { - public let type: String - public let event: String - public let payload: AnyCodable? - public let seq: Int? - public let stateversion: [String: AnyCodable]? - - public init( - type: String, - event: String, - payload: AnyCodable?, - seq: Int?, - stateversion: [String: AnyCodable]?) - { - self.type = type - self.event = event - self.payload = payload - self.seq = seq - self.stateversion = stateversion - } - - private enum CodingKeys: String, CodingKey { - case type - case event - case payload - case seq - case stateversion = "stateVersion" - } -} - -public struct PresenceEntry: Codable, Sendable { - public let host: String? - public let ip: String? - public let version: String? - public let platform: String? - public let devicefamily: String? - public let modelidentifier: String? - public let mode: String? - public let lastinputseconds: Int? - public let reason: String? - public let tags: [String]? - public let text: String? - public let ts: Int - public let deviceid: String? - public let roles: [String]? - public let scopes: [String]? - public let instanceid: String? - - public init( - host: String?, - ip: String?, - version: String?, - platform: String?, - devicefamily: String?, - modelidentifier: String?, - mode: String?, - lastinputseconds: Int?, - reason: String?, - tags: [String]?, - text: String?, - ts: Int, - deviceid: String?, - roles: [String]?, - scopes: [String]?, - instanceid: String?) - { - self.host = host - self.ip = ip - self.version = version - self.platform = platform - self.devicefamily = devicefamily - self.modelidentifier = modelidentifier - self.mode = mode - self.lastinputseconds = lastinputseconds - self.reason = reason - self.tags = tags - self.text = text - self.ts = ts - self.deviceid = deviceid - self.roles = roles - self.scopes = scopes - self.instanceid = instanceid - } - - private enum CodingKeys: String, CodingKey { - case host - case ip - case version - case platform - case devicefamily = "deviceFamily" - case modelidentifier = "modelIdentifier" - case mode - case lastinputseconds = "lastInputSeconds" - case reason - case tags - case text - case ts - case deviceid = "deviceId" - case roles - case scopes - case instanceid = "instanceId" - } -} - -public struct StateVersion: Codable, Sendable { - public let presence: Int - public let health: Int - - public init( - presence: Int, - health: Int) - { - self.presence = presence - self.health = health - } - - private enum CodingKeys: String, CodingKey { - case presence - case health - } -} - -public struct Snapshot: Codable, Sendable { - public let presence: [PresenceEntry] - public let health: AnyCodable - public let stateversion: StateVersion - public let uptimems: Int - public let configpath: String? - public let statedir: String? - public let sessiondefaults: [String: AnyCodable]? - public let authmode: AnyCodable? - public let updateavailable: [String: AnyCodable]? - - public init( - presence: [PresenceEntry], - health: AnyCodable, - stateversion: StateVersion, - uptimems: Int, - configpath: String?, - statedir: String?, - sessiondefaults: [String: AnyCodable]?, - authmode: AnyCodable?, - updateavailable: [String: AnyCodable]?) - { - self.presence = presence - self.health = health - self.stateversion = stateversion - self.uptimems = uptimems - self.configpath = configpath - self.statedir = statedir - self.sessiondefaults = sessiondefaults - self.authmode = authmode - self.updateavailable = updateavailable - } - - private enum CodingKeys: String, CodingKey { - case presence - case health - case stateversion = "stateVersion" - case uptimems = "uptimeMs" - case configpath = "configPath" - case statedir = "stateDir" - case sessiondefaults = "sessionDefaults" - case authmode = "authMode" - case updateavailable = "updateAvailable" - } -} - -public struct ErrorShape: Codable, Sendable { - public let code: String - public let message: String - public let details: AnyCodable? - public let retryable: Bool? - public let retryafterms: Int? - - public init( - code: String, - message: String, - details: AnyCodable?, - retryable: Bool?, - retryafterms: Int?) - { - self.code = code - self.message = message - self.details = details - self.retryable = retryable - self.retryafterms = retryafterms - } - - private enum CodingKeys: String, CodingKey { - case code - case message - case details - case retryable - case retryafterms = "retryAfterMs" - } -} - -public struct EnvironmentSummary: Codable, Sendable { - public let id: String - public let type: String - public let label: String? - public let status: EnvironmentStatus - public let capabilities: [String]? - - public init( - id: String, - type: String, - label: String?, - status: EnvironmentStatus, - capabilities: [String]?) - { - self.id = id - self.type = type - self.label = label - self.status = status - self.capabilities = capabilities - } - - private enum CodingKeys: String, CodingKey { - case id - case type - case label - case status - case capabilities - } -} - -public struct EnvironmentsListParams: Codable, Sendable {} - -public struct EnvironmentsListResult: Codable, Sendable { - public let environments: [EnvironmentSummary] - - public init( - environments: [EnvironmentSummary]) - { - self.environments = environments - } - - private enum CodingKeys: String, CodingKey { - case environments - } -} - -public struct EnvironmentsStatusParams: Codable, Sendable { - public let environmentid: String - - public init( - environmentid: String) - { - self.environmentid = environmentid - } - - private enum CodingKeys: String, CodingKey { - case environmentid = "environmentId" - } -} - -public struct EnvironmentsStatusResult: Codable, Sendable { - public let id: String - public let type: String - public let label: String? - public let status: EnvironmentStatus - public let capabilities: [String]? - - public init( - id: String, - type: String, - label: String?, - status: EnvironmentStatus, - capabilities: [String]?) - { - self.id = id - self.type = type - self.label = label - self.status = status - self.capabilities = capabilities - } - - private enum CodingKeys: String, CodingKey { - case id - case type - case label - case status - case capabilities - } -} - -public struct AgentEvent: Codable, Sendable { - public let runid: String - public let seq: Int - public let stream: String - public let ts: Int - public let spawnedby: String? - public let data: [String: AnyCodable] - - public init( - runid: String, - seq: Int, - stream: String, - ts: Int, - spawnedby: String?, - data: [String: AnyCodable]) - { - self.runid = runid - self.seq = seq - self.stream = stream - self.ts = ts - self.spawnedby = spawnedby - self.data = data - } - - private enum CodingKeys: String, CodingKey { - case runid = "runId" - case seq - case stream - case ts - case spawnedby = "spawnedBy" - case data - } -} - -public struct MessageActionParams: Codable, Sendable { - public let channel: String - public let action: String - public let params: [String: AnyCodable] - public let accountid: String? - public let requestersenderid: String? - public let senderisowner: Bool? - public let sessionkey: String? - public let sessionid: String? - public let agentid: String? - public let toolcontext: [String: AnyCodable]? - public let idempotencykey: String - - public init( - channel: String, - action: String, - params: [String: AnyCodable], - accountid: String?, - requestersenderid: String?, - senderisowner: Bool?, - sessionkey: String?, - sessionid: String?, - agentid: String?, - toolcontext: [String: AnyCodable]?, - idempotencykey: String) - { - self.channel = channel - self.action = action - self.params = params - self.accountid = accountid - self.requestersenderid = requestersenderid - self.senderisowner = senderisowner - self.sessionkey = sessionkey - self.sessionid = sessionid - self.agentid = agentid - self.toolcontext = toolcontext - self.idempotencykey = idempotencykey - } - - private enum CodingKeys: String, CodingKey { - case channel - case action - case params - case accountid = "accountId" - case requestersenderid = "requesterSenderId" - case senderisowner = "senderIsOwner" - case sessionkey = "sessionKey" - case sessionid = "sessionId" - case agentid = "agentId" - case toolcontext = "toolContext" - case idempotencykey = "idempotencyKey" - } -} - -public struct SendParams: Codable, Sendable { - public let to: String - public let message: String? - public let mediaurl: String? - public let mediaurls: [String]? - public let asvoice: Bool? - public let gifplayback: Bool? - public let channel: String? - public let accountid: String? - public let agentid: String? - public let replytoid: String? - public let threadid: String? - public let sessionkey: String? - public let idempotencykey: String - - public init( - to: String, - message: String?, - mediaurl: String?, - mediaurls: [String]?, - asvoice: Bool?, - gifplayback: Bool?, - channel: String?, - accountid: String?, - agentid: String?, - replytoid: String?, - threadid: String?, - sessionkey: String?, - idempotencykey: String) - { - self.to = to - self.message = message - self.mediaurl = mediaurl - self.mediaurls = mediaurls - self.asvoice = asvoice - self.gifplayback = gifplayback - self.channel = channel - self.accountid = accountid - self.agentid = agentid - self.replytoid = replytoid - self.threadid = threadid - self.sessionkey = sessionkey - self.idempotencykey = idempotencykey - } - - private enum CodingKeys: String, CodingKey { - case to - case message - case mediaurl = "mediaUrl" - case mediaurls = "mediaUrls" - case asvoice = "asVoice" - case gifplayback = "gifPlayback" - case channel - case accountid = "accountId" - case agentid = "agentId" - case replytoid = "replyToId" - case threadid = "threadId" - case sessionkey = "sessionKey" - case idempotencykey = "idempotencyKey" - } -} - -public struct PollParams: Codable, Sendable { - public let to: String - public let question: String - public let options: [String] - public let maxselections: Int? - public let durationseconds: Int? - public let durationhours: Int? - public let silent: Bool? - public let isanonymous: Bool? - public let threadid: String? - public let channel: String? - public let accountid: String? - public let idempotencykey: String - - public init( - to: String, - question: String, - options: [String], - maxselections: Int?, - durationseconds: Int?, - durationhours: Int?, - silent: Bool?, - isanonymous: Bool?, - threadid: String?, - channel: String?, - accountid: String?, - idempotencykey: String) - { - self.to = to - self.question = question - self.options = options - self.maxselections = maxselections - self.durationseconds = durationseconds - self.durationhours = durationhours - self.silent = silent - self.isanonymous = isanonymous - self.threadid = threadid - self.channel = channel - self.accountid = accountid - self.idempotencykey = idempotencykey - } - - private enum CodingKeys: String, CodingKey { - case to - case question - case options - case maxselections = "maxSelections" - case durationseconds = "durationSeconds" - case durationhours = "durationHours" - case silent - case isanonymous = "isAnonymous" - case threadid = "threadId" - case channel - case accountid = "accountId" - case idempotencykey = "idempotencyKey" - } -} - -public struct AgentParams: Codable, Sendable { - public let message: String - public let agentid: String? - public let provider: String? - public let model: String? - public let to: String? - public let replyto: String? - public let sessionid: String? - public let sessionkey: String? - public let thinking: String? - public let deliver: Bool? - public let attachments: [AnyCodable]? - public let channel: String? - public let replychannel: String? - public let accountid: String? - public let replyaccountid: String? - public let threadid: String? - public let groupid: String? - public let groupchannel: String? - public let groupspace: String? - public let timeout: Int? - public let besteffortdeliver: Bool? - public let lane: String? - public let cleanupbundlemcponrunend: Bool? - public let modelrun: Bool? - public let promptmode: AnyCodable? - public let extrasystemprompt: String? - public let bootstrapcontextmode: AnyCodable? - public let bootstrapcontextrunkind: AnyCodable? - public let acpturnsource: String? - public let internalevents: [[String: AnyCodable]]? - public let inputprovenance: [String: AnyCodable]? - public let voicewaketrigger: String? - public let idempotencykey: String - public let label: String? - - public init( - message: String, - agentid: String?, - provider: String?, - model: String?, - to: String?, - replyto: String?, - sessionid: String?, - sessionkey: String?, - thinking: String?, - deliver: Bool?, - attachments: [AnyCodable]?, - channel: String?, - replychannel: String?, - accountid: String?, - replyaccountid: String?, - threadid: String?, - groupid: String?, - groupchannel: String?, - groupspace: String?, - timeout: Int?, - besteffortdeliver: Bool?, - lane: String?, - cleanupbundlemcponrunend: Bool?, - modelrun: Bool?, - promptmode: AnyCodable?, - extrasystemprompt: String?, - bootstrapcontextmode: AnyCodable?, - bootstrapcontextrunkind: AnyCodable?, - acpturnsource: String?, - internalevents: [[String: AnyCodable]]?, - inputprovenance: [String: AnyCodable]?, - voicewaketrigger: String?, - idempotencykey: String, - label: String?) - { - self.message = message - self.agentid = agentid - self.provider = provider - self.model = model - self.to = to - self.replyto = replyto - self.sessionid = sessionid - self.sessionkey = sessionkey - self.thinking = thinking - self.deliver = deliver - self.attachments = attachments - self.channel = channel - self.replychannel = replychannel - self.accountid = accountid - self.replyaccountid = replyaccountid - self.threadid = threadid - self.groupid = groupid - self.groupchannel = groupchannel - self.groupspace = groupspace - self.timeout = timeout - self.besteffortdeliver = besteffortdeliver - self.lane = lane - self.cleanupbundlemcponrunend = cleanupbundlemcponrunend - self.modelrun = modelrun - self.promptmode = promptmode - self.extrasystemprompt = extrasystemprompt - self.bootstrapcontextmode = bootstrapcontextmode - self.bootstrapcontextrunkind = bootstrapcontextrunkind - self.acpturnsource = acpturnsource - self.internalevents = internalevents - self.inputprovenance = inputprovenance - self.voicewaketrigger = voicewaketrigger - self.idempotencykey = idempotencykey - self.label = label - } - - private enum CodingKeys: String, CodingKey { - case message - case agentid = "agentId" - case provider - case model - case to - case replyto = "replyTo" - case sessionid = "sessionId" - case sessionkey = "sessionKey" - case thinking - case deliver - case attachments - case channel - case replychannel = "replyChannel" - case accountid = "accountId" - case replyaccountid = "replyAccountId" - case threadid = "threadId" - case groupid = "groupId" - case groupchannel = "groupChannel" - case groupspace = "groupSpace" - case timeout - case besteffortdeliver = "bestEffortDeliver" - case lane - case cleanupbundlemcponrunend = "cleanupBundleMcpOnRunEnd" - case modelrun = "modelRun" - case promptmode = "promptMode" - case extrasystemprompt = "extraSystemPrompt" - case bootstrapcontextmode = "bootstrapContextMode" - case bootstrapcontextrunkind = "bootstrapContextRunKind" - case acpturnsource = "acpTurnSource" - case internalevents = "internalEvents" - case inputprovenance = "inputProvenance" - case voicewaketrigger = "voiceWakeTrigger" - case idempotencykey = "idempotencyKey" - case label - } -} - -public struct AgentIdentityParams: Codable, Sendable { - public let agentid: String? - public let sessionkey: String? - - public init( - agentid: String?, - sessionkey: String?) - { - self.agentid = agentid - self.sessionkey = sessionkey - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - case sessionkey = "sessionKey" - } -} - -public struct AgentIdentityResult: Codable, Sendable { - public let agentid: String - public let name: String? - public let avatar: String? - public let avatarsource: String? - public let avatarstatus: String? - public let avatarreason: String? - public let emoji: String? - - public init( - agentid: String, - name: String?, - avatar: String?, - avatarsource: String?, - avatarstatus: String?, - avatarreason: String?, - emoji: String?) - { - self.agentid = agentid - self.name = name - self.avatar = avatar - self.avatarsource = avatarsource - self.avatarstatus = avatarstatus - self.avatarreason = avatarreason - self.emoji = emoji - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - case name - case avatar - case avatarsource = "avatarSource" - case avatarstatus = "avatarStatus" - case avatarreason = "avatarReason" - case emoji - } -} - -public struct AgentWaitParams: Codable, Sendable { - public let runid: String - public let timeoutms: Int? - - public init( - runid: String, - timeoutms: Int?) - { - self.runid = runid - self.timeoutms = timeoutms - } - - private enum CodingKeys: String, CodingKey { - case runid = "runId" - case timeoutms = "timeoutMs" - } -} - -public struct WakeParams: Codable, Sendable { - public let mode: AnyCodable - public let text: String - - public init( - mode: AnyCodable, - text: String) - { - self.mode = mode - self.text = text - } - - private enum CodingKeys: String, CodingKey { - case mode - case text - } -} - -public struct NodePairRequestParams: Codable, Sendable { - public let nodeid: String - public let displayname: String? - public let platform: String? - public let version: String? - public let coreversion: String? - public let uiversion: String? - public let devicefamily: String? - public let modelidentifier: String? - public let caps: [String]? - public let commands: [String]? - public let remoteip: String? - public let silent: Bool? - - public init( - nodeid: String, - displayname: String?, - platform: String?, - version: String?, - coreversion: String?, - uiversion: String?, - devicefamily: String?, - modelidentifier: String?, - caps: [String]?, - commands: [String]?, - remoteip: String?, - silent: Bool?) - { - self.nodeid = nodeid - self.displayname = displayname - self.platform = platform - self.version = version - self.coreversion = coreversion - self.uiversion = uiversion - self.devicefamily = devicefamily - self.modelidentifier = modelidentifier - self.caps = caps - self.commands = commands - self.remoteip = remoteip - self.silent = silent - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - case displayname = "displayName" - case platform - case version - case coreversion = "coreVersion" - case uiversion = "uiVersion" - case devicefamily = "deviceFamily" - case modelidentifier = "modelIdentifier" - case caps - case commands - case remoteip = "remoteIp" - case silent - } -} - -public struct NodePairListParams: Codable, Sendable {} - -public struct NodePairApproveParams: Codable, Sendable { - public let requestid: String - - public init( - requestid: String) - { - self.requestid = requestid - } - - private enum CodingKeys: String, CodingKey { - case requestid = "requestId" - } -} - -public struct NodePairRejectParams: Codable, Sendable { - public let requestid: String - - public init( - requestid: String) - { - self.requestid = requestid - } - - private enum CodingKeys: String, CodingKey { - case requestid = "requestId" - } -} - -public struct NodePairRemoveParams: Codable, Sendable { - public let nodeid: String - - public init( - nodeid: String) - { - self.nodeid = nodeid - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - } -} - -public struct NodePairVerifyParams: Codable, Sendable { - public let nodeid: String - public let token: String - - public init( - nodeid: String, - token: String) - { - self.nodeid = nodeid - self.token = token - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - case token - } -} - -public struct NodeRenameParams: Codable, Sendable { - public let nodeid: String - public let displayname: String - - public init( - nodeid: String, - displayname: String) - { - self.nodeid = nodeid - self.displayname = displayname - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - case displayname = "displayName" - } -} - -public struct NodeListParams: Codable, Sendable {} - -public struct NodePendingAckParams: Codable, Sendable { - public let ids: [String] - - public init( - ids: [String]) - { - self.ids = ids - } - - private enum CodingKeys: String, CodingKey { - case ids - } -} - -public struct NodeDescribeParams: Codable, Sendable { - public let nodeid: String - - public init( - nodeid: String) - { - self.nodeid = nodeid - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - } -} - -public struct NodeInvokeParams: Codable, Sendable { - public let nodeid: String - public let command: String - public let params: AnyCodable? - public let timeoutms: Int? - public let idempotencykey: String - - public init( - nodeid: String, - command: String, - params: AnyCodable?, - timeoutms: Int?, - idempotencykey: String) - { - self.nodeid = nodeid - self.command = command - self.params = params - self.timeoutms = timeoutms - self.idempotencykey = idempotencykey - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - case command - case params - case timeoutms = "timeoutMs" - case idempotencykey = "idempotencyKey" - } -} - -public struct NodeInvokeResultParams: Codable, Sendable { - public let id: String - public let nodeid: String - public let ok: Bool - public let payload: AnyCodable? - public let payloadjson: String? - public let error: [String: AnyCodable]? - - public init( - id: String, - nodeid: String, - ok: Bool, - payload: AnyCodable?, - payloadjson: String?, - error: [String: AnyCodable]?) - { - self.id = id - self.nodeid = nodeid - self.ok = ok - self.payload = payload - self.payloadjson = payloadjson - self.error = error - } - - private enum CodingKeys: String, CodingKey { - case id - case nodeid = "nodeId" - case ok - case payload - case payloadjson = "payloadJSON" - case error - } -} - -public struct NodeEventParams: Codable, Sendable { - public let event: String - public let payload: AnyCodable? - public let payloadjson: String? - - public init( - event: String, - payload: AnyCodable?, - payloadjson: String?) - { - self.event = event - self.payload = payload - self.payloadjson = payloadjson - } - - private enum CodingKeys: String, CodingKey { - case event - case payload - case payloadjson = "payloadJSON" - } -} - -public struct NodeEventResult: Codable, Sendable { - public let ok: Bool - public let event: String - public let handled: Bool - public let reason: String? - - public init( - ok: Bool, - event: String, - handled: Bool, - reason: String?) - { - self.ok = ok - self.event = event - self.handled = handled - self.reason = reason - } - - private enum CodingKeys: String, CodingKey { - case ok - case event - case handled - case reason - } -} - -public struct NodePresenceAlivePayload: Codable, Sendable { - public let trigger: NodePresenceAliveReason - public let sentatms: Int? - public let displayname: String? - public let version: String? - public let platform: String? - public let devicefamily: String? - public let modelidentifier: String? - public let pushtransport: String? - - public init( - trigger: NodePresenceAliveReason, - sentatms: Int?, - displayname: String?, - version: String?, - platform: String?, - devicefamily: String?, - modelidentifier: String?, - pushtransport: String?) - { - self.trigger = trigger - self.sentatms = sentatms - self.displayname = displayname - self.version = version - self.platform = platform - self.devicefamily = devicefamily - self.modelidentifier = modelidentifier - self.pushtransport = pushtransport - } - - private enum CodingKeys: String, CodingKey { - case trigger - case sentatms = "sentAtMs" - case displayname = "displayName" - case version - case platform - case devicefamily = "deviceFamily" - case modelidentifier = "modelIdentifier" - case pushtransport = "pushTransport" - } -} - -public struct NodePendingDrainParams: Codable, Sendable { - public let maxitems: Int? - - public init( - maxitems: Int?) - { - self.maxitems = maxitems - } - - private enum CodingKeys: String, CodingKey { - case maxitems = "maxItems" - } -} - -public struct NodePendingDrainResult: Codable, Sendable { - public let nodeid: String - public let revision: Int - public let items: [[String: AnyCodable]] - public let hasmore: Bool - - public init( - nodeid: String, - revision: Int, - items: [[String: AnyCodable]], - hasmore: Bool) - { - self.nodeid = nodeid - self.revision = revision - self.items = items - self.hasmore = hasmore - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - case revision - case items - case hasmore = "hasMore" - } -} - -public struct NodePendingEnqueueParams: Codable, Sendable { - public let nodeid: String - public let type: String - public let priority: String? - public let expiresinms: Int? - public let wake: Bool? - - public init( - nodeid: String, - type: String, - priority: String?, - expiresinms: Int?, - wake: Bool?) - { - self.nodeid = nodeid - self.type = type - self.priority = priority - self.expiresinms = expiresinms - self.wake = wake - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - case type - case priority - case expiresinms = "expiresInMs" - case wake - } -} - -public struct NodePendingEnqueueResult: Codable, Sendable { - public let nodeid: String - public let revision: Int - public let queued: [String: AnyCodable] - public let waketriggered: Bool - - public init( - nodeid: String, - revision: Int, - queued: [String: AnyCodable], - waketriggered: Bool) - { - self.nodeid = nodeid - self.revision = revision - self.queued = queued - self.waketriggered = waketriggered - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - case revision - case queued - case waketriggered = "wakeTriggered" - } -} - -public struct NodeInvokeRequestEvent: Codable, Sendable { - public let id: String - public let nodeid: String - public let command: String - public let paramsjson: String? - public let timeoutms: Int? - public let idempotencykey: String? - - public init( - id: String, - nodeid: String, - command: String, - paramsjson: String?, - timeoutms: Int?, - idempotencykey: String?) - { - self.id = id - self.nodeid = nodeid - self.command = command - self.paramsjson = paramsjson - self.timeoutms = timeoutms - self.idempotencykey = idempotencykey - } - - private enum CodingKeys: String, CodingKey { - case id - case nodeid = "nodeId" - case command - case paramsjson = "paramsJSON" - case timeoutms = "timeoutMs" - case idempotencykey = "idempotencyKey" - } -} - -public struct PushTestParams: Codable, Sendable { - public let nodeid: String - public let title: String? - public let body: String? - public let environment: String? - - public init( - nodeid: String, - title: String?, - body: String?, - environment: String?) - { - self.nodeid = nodeid - self.title = title - self.body = body - self.environment = environment - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - case title - case body - case environment - } -} - -public struct PushTestResult: Codable, Sendable { - public let ok: Bool - public let status: Int - public let apnsid: String? - public let reason: String? - public let tokensuffix: String - public let topic: String - public let environment: String - public let transport: String - - public init( - ok: Bool, - status: Int, - apnsid: String?, - reason: String?, - tokensuffix: String, - topic: String, - environment: String, - transport: String) - { - self.ok = ok - self.status = status - self.apnsid = apnsid - self.reason = reason - self.tokensuffix = tokensuffix - self.topic = topic - self.environment = environment - self.transport = transport - } - - private enum CodingKeys: String, CodingKey { - case ok - case status - case apnsid = "apnsId" - case reason - case tokensuffix = "tokenSuffix" - case topic - case environment - case transport - } -} - -public struct SecretsReloadParams: Codable, Sendable {} - -public struct SecretsResolveParams: Codable, Sendable { - public let commandname: String - public let targetids: [String] - - public init( - commandname: String, - targetids: [String]) - { - self.commandname = commandname - self.targetids = targetids - } - - private enum CodingKeys: String, CodingKey { - case commandname = "commandName" - case targetids = "targetIds" - } -} - -public struct SecretsResolveAssignment: Codable, Sendable { - public let path: String? - public let pathsegments: [String] - public let value: AnyCodable - - public init( - path: String?, - pathsegments: [String], - value: AnyCodable) - { - self.path = path - self.pathsegments = pathsegments - self.value = value - } - - private enum CodingKeys: String, CodingKey { - case path - case pathsegments = "pathSegments" - case value - } -} - -public struct SecretsResolveResult: Codable, Sendable { - public let ok: Bool? - public let assignments: [SecretsResolveAssignment]? - public let diagnostics: [String]? - public let inactiverefpaths: [String]? - - public init( - ok: Bool?, - assignments: [SecretsResolveAssignment]?, - diagnostics: [String]?, - inactiverefpaths: [String]?) - { - self.ok = ok - self.assignments = assignments - self.diagnostics = diagnostics - self.inactiverefpaths = inactiverefpaths - } - - private enum CodingKeys: String, CodingKey { - case ok - case assignments - case diagnostics - case inactiverefpaths = "inactiveRefPaths" - } -} - -public struct SessionsListParams: Codable, Sendable { - public let limit: Int? - public let activeminutes: Int? - public let includeglobal: Bool? - public let includeunknown: Bool? - public let includederivedtitles: Bool? - public let includelastmessage: Bool? - public let label: String? - public let spawnedby: String? - public let agentid: String? - public let search: String? - - public init( - limit: Int?, - activeminutes: Int?, - includeglobal: Bool?, - includeunknown: Bool?, - includederivedtitles: Bool?, - includelastmessage: Bool?, - label: String?, - spawnedby: String?, - agentid: String?, - search: String?) - { - self.limit = limit - self.activeminutes = activeminutes - self.includeglobal = includeglobal - self.includeunknown = includeunknown - self.includederivedtitles = includederivedtitles - self.includelastmessage = includelastmessage - self.label = label - self.spawnedby = spawnedby - self.agentid = agentid - self.search = search - } - - private enum CodingKeys: String, CodingKey { - case limit - case activeminutes = "activeMinutes" - case includeglobal = "includeGlobal" - case includeunknown = "includeUnknown" - case includederivedtitles = "includeDerivedTitles" - case includelastmessage = "includeLastMessage" - case label - case spawnedby = "spawnedBy" - case agentid = "agentId" - case search - } -} - -public struct SessionsCleanupParams: Codable, Sendable { - public let agent: String? - public let allagents: Bool? - public let enforce: Bool? - public let activekey: String? - public let fixmissing: Bool? - public let fixdmscope: Bool? - - public init( - agent: String?, - allagents: Bool?, - enforce: Bool?, - activekey: String?, - fixmissing: Bool?, - fixdmscope: Bool?) - { - self.agent = agent - self.allagents = allagents - self.enforce = enforce - self.activekey = activekey - self.fixmissing = fixmissing - self.fixdmscope = fixdmscope - } - - private enum CodingKeys: String, CodingKey { - case agent - case allagents = "allAgents" - case enforce - case activekey = "activeKey" - case fixmissing = "fixMissing" - case fixdmscope = "fixDmScope" - } -} - -public struct SessionsPreviewParams: Codable, Sendable { - public let keys: [String] - public let limit: Int? - public let maxchars: Int? - - public init( - keys: [String], - limit: Int?, - maxchars: Int?) - { - self.keys = keys - self.limit = limit - self.maxchars = maxchars - } - - private enum CodingKeys: String, CodingKey { - case keys - case limit - case maxchars = "maxChars" - } -} - -public struct SessionsDescribeParams: Codable, Sendable { - public let key: String - public let includederivedtitles: Bool? - public let includelastmessage: Bool? - - public init( - key: String, - includederivedtitles: Bool?, - includelastmessage: Bool?) - { - self.key = key - self.includederivedtitles = includederivedtitles - self.includelastmessage = includelastmessage - } - - private enum CodingKeys: String, CodingKey { - case key - case includederivedtitles = "includeDerivedTitles" - case includelastmessage = "includeLastMessage" - } -} - -public struct SessionsResolveParams: Codable, Sendable { - public let key: String? - public let sessionid: String? - public let label: String? - public let agentid: String? - public let spawnedby: String? - public let includeglobal: Bool? - public let includeunknown: Bool? - - public init( - key: String?, - sessionid: String?, - label: String?, - agentid: String?, - spawnedby: String?, - includeglobal: Bool?, - includeunknown: Bool?) - { - self.key = key - self.sessionid = sessionid - self.label = label - self.agentid = agentid - self.spawnedby = spawnedby - self.includeglobal = includeglobal - self.includeunknown = includeunknown - } - - private enum CodingKeys: String, CodingKey { - case key - case sessionid = "sessionId" - case label - case agentid = "agentId" - case spawnedby = "spawnedBy" - case includeglobal = "includeGlobal" - case includeunknown = "includeUnknown" - } -} - -public struct SessionCompactionCheckpoint: Codable, Sendable { - public let checkpointid: String - public let sessionkey: String - public let sessionid: String - public let createdat: Int - public let reason: AnyCodable - public let tokensbefore: Int? - public let tokensafter: Int? - public let summary: String? - public let firstkeptentryid: String? - public let precompaction: [String: AnyCodable] - public let postcompaction: [String: AnyCodable] - - public init( - checkpointid: String, - sessionkey: String, - sessionid: String, - createdat: Int, - reason: AnyCodable, - tokensbefore: Int?, - tokensafter: Int?, - summary: String?, - firstkeptentryid: String?, - precompaction: [String: AnyCodable], - postcompaction: [String: AnyCodable]) - { - self.checkpointid = checkpointid - self.sessionkey = sessionkey - self.sessionid = sessionid - self.createdat = createdat - self.reason = reason - self.tokensbefore = tokensbefore - self.tokensafter = tokensafter - self.summary = summary - self.firstkeptentryid = firstkeptentryid - self.precompaction = precompaction - self.postcompaction = postcompaction - } - - private enum CodingKeys: String, CodingKey { - case checkpointid = "checkpointId" - case sessionkey = "sessionKey" - case sessionid = "sessionId" - case createdat = "createdAt" - case reason - case tokensbefore = "tokensBefore" - case tokensafter = "tokensAfter" - case summary - case firstkeptentryid = "firstKeptEntryId" - case precompaction = "preCompaction" - case postcompaction = "postCompaction" - } -} - -public struct SessionsCompactionListParams: Codable, Sendable { - public let key: String - - public init( - key: String) - { - self.key = key - } - - private enum CodingKeys: String, CodingKey { - case key - } -} - -public struct SessionsCompactionGetParams: Codable, Sendable { - public let key: String - public let checkpointid: String - - public init( - key: String, - checkpointid: String) - { - self.key = key - self.checkpointid = checkpointid - } - - private enum CodingKeys: String, CodingKey { - case key - case checkpointid = "checkpointId" - } -} - -public struct SessionsCompactionBranchParams: Codable, Sendable { - public let key: String - public let checkpointid: String - - public init( - key: String, - checkpointid: String) - { - self.key = key - self.checkpointid = checkpointid - } - - private enum CodingKeys: String, CodingKey { - case key - case checkpointid = "checkpointId" - } -} - -public struct SessionsCompactionRestoreParams: Codable, Sendable { - public let key: String - public let checkpointid: String - - public init( - key: String, - checkpointid: String) - { - self.key = key - self.checkpointid = checkpointid - } - - private enum CodingKeys: String, CodingKey { - case key - case checkpointid = "checkpointId" - } -} - -public struct SessionsCompactionListResult: Codable, Sendable { - public let ok: Bool - public let key: String - public let checkpoints: [SessionCompactionCheckpoint] - - public init( - ok: Bool, - key: String, - checkpoints: [SessionCompactionCheckpoint]) - { - self.ok = ok - self.key = key - self.checkpoints = checkpoints - } - - private enum CodingKeys: String, CodingKey { - case ok - case key - case checkpoints - } -} - -public struct SessionsCompactionGetResult: Codable, Sendable { - public let ok: Bool - public let key: String - public let checkpoint: SessionCompactionCheckpoint - - public init( - ok: Bool, - key: String, - checkpoint: SessionCompactionCheckpoint) - { - self.ok = ok - self.key = key - self.checkpoint = checkpoint - } - - private enum CodingKeys: String, CodingKey { - case ok - case key - case checkpoint - } -} - -public struct SessionsCompactionBranchResult: Codable, Sendable { - public let ok: Bool - public let sourcekey: String - public let key: String - public let sessionid: String - public let checkpoint: SessionCompactionCheckpoint - public let entry: [String: AnyCodable] - - public init( - ok: Bool, - sourcekey: String, - key: String, - sessionid: String, - checkpoint: SessionCompactionCheckpoint, - entry: [String: AnyCodable]) - { - self.ok = ok - self.sourcekey = sourcekey - self.key = key - self.sessionid = sessionid - self.checkpoint = checkpoint - self.entry = entry - } - - private enum CodingKeys: String, CodingKey { - case ok - case sourcekey = "sourceKey" - case key - case sessionid = "sessionId" - case checkpoint - case entry - } -} - -public struct SessionsCompactionRestoreResult: Codable, Sendable { - public let ok: Bool - public let key: String - public let sessionid: String - public let checkpoint: SessionCompactionCheckpoint - public let entry: [String: AnyCodable] - - public init( - ok: Bool, - key: String, - sessionid: String, - checkpoint: SessionCompactionCheckpoint, - entry: [String: AnyCodable]) - { - self.ok = ok - self.key = key - self.sessionid = sessionid - self.checkpoint = checkpoint - self.entry = entry - } - - private enum CodingKeys: String, CodingKey { - case ok - case key - case sessionid = "sessionId" - case checkpoint - case entry - } -} - -public struct SessionsCreateParams: Codable, Sendable { - public let key: String? - public let agentid: String? - public let label: String? - public let model: String? - public let parentsessionkey: String? - public let emitcommandhooks: Bool? - public let task: String? - public let message: String? - - public init( - key: String?, - agentid: String?, - label: String?, - model: String?, - parentsessionkey: String?, - emitcommandhooks: Bool?, - task: String?, - message: String?) - { - self.key = key - self.agentid = agentid - self.label = label - self.model = model - self.parentsessionkey = parentsessionkey - self.emitcommandhooks = emitcommandhooks - self.task = task - self.message = message - } - - private enum CodingKeys: String, CodingKey { - case key - case agentid = "agentId" - case label - case model - case parentsessionkey = "parentSessionKey" - case emitcommandhooks = "emitCommandHooks" - case task - case message - } -} - -public struct SessionsSendParams: Codable, Sendable { - public let key: String - public let message: String - public let thinking: String? - public let attachments: [AnyCodable]? - public let timeoutms: Int? - public let idempotencykey: String? - - public init( - key: String, - message: String, - thinking: String?, - attachments: [AnyCodable]?, - timeoutms: Int?, - idempotencykey: String?) - { - self.key = key - self.message = message - self.thinking = thinking - self.attachments = attachments - self.timeoutms = timeoutms - self.idempotencykey = idempotencykey - } - - private enum CodingKeys: String, CodingKey { - case key - case message - case thinking - case attachments - case timeoutms = "timeoutMs" - case idempotencykey = "idempotencyKey" - } -} - -public struct SessionsMessagesSubscribeParams: Codable, Sendable { - public let key: String - - public init( - key: String) - { - self.key = key - } - - private enum CodingKeys: String, CodingKey { - case key - } -} - -public struct SessionsMessagesUnsubscribeParams: Codable, Sendable { - public let key: String - - public init( - key: String) - { - self.key = key - } - - private enum CodingKeys: String, CodingKey { - case key - } -} - -public struct SessionsAbortParams: Codable, Sendable { - public let key: String? - public let runid: String? - - public init( - key: String?, - runid: String?) - { - self.key = key - self.runid = runid - } - - private enum CodingKeys: String, CodingKey { - case key - case runid = "runId" - } -} - -public struct SessionsPatchParams: Codable, Sendable { - public let key: String - public let label: AnyCodable? - public let thinkinglevel: AnyCodable? - public let fastmode: AnyCodable? - public let verboselevel: AnyCodable? - public let tracelevel: AnyCodable? - public let reasoninglevel: AnyCodable? - public let responseusage: AnyCodable? - public let elevatedlevel: AnyCodable? - public let exechost: AnyCodable? - public let execsecurity: AnyCodable? - public let execask: AnyCodable? - public let execnode: AnyCodable? - public let model: AnyCodable? - public let spawnedby: AnyCodable? - public let spawnedworkspacedir: AnyCodable? - public let spawndepth: AnyCodable? - public let subagentrole: AnyCodable? - public let subagentcontrolscope: AnyCodable? - public let sendpolicy: AnyCodable? - public let groupactivation: AnyCodable? - - public init( - key: String, - label: AnyCodable?, - thinkinglevel: AnyCodable?, - fastmode: AnyCodable?, - verboselevel: AnyCodable?, - tracelevel: AnyCodable?, - reasoninglevel: AnyCodable?, - responseusage: AnyCodable?, - elevatedlevel: AnyCodable?, - exechost: AnyCodable?, - execsecurity: AnyCodable?, - execask: AnyCodable?, - execnode: AnyCodable?, - model: AnyCodable?, - spawnedby: AnyCodable?, - spawnedworkspacedir: AnyCodable?, - spawndepth: AnyCodable?, - subagentrole: AnyCodable?, - subagentcontrolscope: AnyCodable?, - sendpolicy: AnyCodable?, - groupactivation: AnyCodable?) - { - self.key = key - self.label = label - self.thinkinglevel = thinkinglevel - self.fastmode = fastmode - self.verboselevel = verboselevel - self.tracelevel = tracelevel - self.reasoninglevel = reasoninglevel - self.responseusage = responseusage - self.elevatedlevel = elevatedlevel - self.exechost = exechost - self.execsecurity = execsecurity - self.execask = execask - self.execnode = execnode - self.model = model - self.spawnedby = spawnedby - self.spawnedworkspacedir = spawnedworkspacedir - self.spawndepth = spawndepth - self.subagentrole = subagentrole - self.subagentcontrolscope = subagentcontrolscope - self.sendpolicy = sendpolicy - self.groupactivation = groupactivation - } - - private enum CodingKeys: String, CodingKey { - case key - case label - case thinkinglevel = "thinkingLevel" - case fastmode = "fastMode" - case verboselevel = "verboseLevel" - case tracelevel = "traceLevel" - case reasoninglevel = "reasoningLevel" - case responseusage = "responseUsage" - case elevatedlevel = "elevatedLevel" - case exechost = "execHost" - case execsecurity = "execSecurity" - case execask = "execAsk" - case execnode = "execNode" - case model - case spawnedby = "spawnedBy" - case spawnedworkspacedir = "spawnedWorkspaceDir" - case spawndepth = "spawnDepth" - case subagentrole = "subagentRole" - case subagentcontrolscope = "subagentControlScope" - case sendpolicy = "sendPolicy" - case groupactivation = "groupActivation" - } -} - -public struct SessionsPluginPatchParams: Codable, Sendable { - public let key: String - public let pluginid: String - public let namespace: String - public let value: AnyCodable? - public let unset: Bool? - - public init( - key: String, - pluginid: String, - namespace: String, - value: AnyCodable?, - unset: Bool?) - { - self.key = key - self.pluginid = pluginid - self.namespace = namespace - self.value = value - self.unset = unset - } - - private enum CodingKeys: String, CodingKey { - case key - case pluginid = "pluginId" - case namespace - case value - case unset - } -} - -public struct SessionsPluginPatchResult: Codable, Sendable { - public let ok: Bool - public let key: String - public let value: AnyCodable? - - public init( - ok: Bool, - key: String, - value: AnyCodable?) - { - self.ok = ok - self.key = key - self.value = value - } - - private enum CodingKeys: String, CodingKey { - case ok - case key - case value - } -} - -public struct SessionsResetParams: Codable, Sendable { - public let key: String - public let reason: AnyCodable? - - public init( - key: String, - reason: AnyCodable?) - { - self.key = key - self.reason = reason - } - - private enum CodingKeys: String, CodingKey { - case key - case reason - } -} - -public struct SessionsDeleteParams: Codable, Sendable { - public let key: String - public let deletetranscript: Bool? - public let emitlifecyclehooks: Bool? - - public init( - key: String, - deletetranscript: Bool?, - emitlifecyclehooks: Bool?) - { - self.key = key - self.deletetranscript = deletetranscript - self.emitlifecyclehooks = emitlifecyclehooks - } - - private enum CodingKeys: String, CodingKey { - case key - case deletetranscript = "deleteTranscript" - case emitlifecyclehooks = "emitLifecycleHooks" - } -} - -public struct SessionsCompactParams: Codable, Sendable { - public let key: String - public let maxlines: Int? - - public init( - key: String, - maxlines: Int?) - { - self.key = key - self.maxlines = maxlines - } - - private enum CodingKeys: String, CodingKey { - case key - case maxlines = "maxLines" - } -} - -public struct SessionsUsageParams: Codable, Sendable { - public let key: String? - public let startdate: String? - public let enddate: String? - public let mode: AnyCodable? - public let utcoffset: String? - public let limit: Int? - public let includecontextweight: Bool? - - public init( - key: String?, - startdate: String?, - enddate: String?, - mode: AnyCodable?, - utcoffset: String?, - limit: Int?, - includecontextweight: Bool?) - { - self.key = key - self.startdate = startdate - self.enddate = enddate - self.mode = mode - self.utcoffset = utcoffset - self.limit = limit - self.includecontextweight = includecontextweight - } - - private enum CodingKeys: String, CodingKey { - case key - case startdate = "startDate" - case enddate = "endDate" - case mode - case utcoffset = "utcOffset" - case limit - case includecontextweight = "includeContextWeight" - } -} - -public struct ConfigGetParams: Codable, Sendable {} - -public struct ConfigSetParams: Codable, Sendable { - public let raw: String - public let basehash: String? - - public init( - raw: String, - basehash: String?) - { - self.raw = raw - self.basehash = basehash - } - - private enum CodingKeys: String, CodingKey { - case raw - case basehash = "baseHash" - } -} - -public struct ConfigApplyParams: Codable, Sendable { - public let raw: String - public let basehash: String? - public let sessionkey: String? - public let deliverycontext: [String: AnyCodable]? - public let note: String? - public let restartdelayms: Int? - - public init( - raw: String, - basehash: String?, - sessionkey: String?, - deliverycontext: [String: AnyCodable]?, - note: String?, - restartdelayms: Int?) - { - self.raw = raw - self.basehash = basehash - self.sessionkey = sessionkey - self.deliverycontext = deliverycontext - self.note = note - self.restartdelayms = restartdelayms - } - - private enum CodingKeys: String, CodingKey { - case raw - case basehash = "baseHash" - case sessionkey = "sessionKey" - case deliverycontext = "deliveryContext" - case note - case restartdelayms = "restartDelayMs" - } -} - -public struct ConfigPatchParams: Codable, Sendable { - public let raw: String - public let basehash: String? - public let sessionkey: String? - public let deliverycontext: [String: AnyCodable]? - public let note: String? - public let restartdelayms: Int? - - public init( - raw: String, - basehash: String?, - sessionkey: String?, - deliverycontext: [String: AnyCodable]?, - note: String?, - restartdelayms: Int?) - { - self.raw = raw - self.basehash = basehash - self.sessionkey = sessionkey - self.deliverycontext = deliverycontext - self.note = note - self.restartdelayms = restartdelayms - } - - private enum CodingKeys: String, CodingKey { - case raw - case basehash = "baseHash" - case sessionkey = "sessionKey" - case deliverycontext = "deliveryContext" - case note - case restartdelayms = "restartDelayMs" - } -} - -public struct ConfigSchemaParams: Codable, Sendable {} - -public struct ConfigSchemaLookupParams: Codable, Sendable { - public let path: String - - public init( - path: String) - { - self.path = path - } - - private enum CodingKeys: String, CodingKey { - case path - } -} - -public struct ConfigSchemaResponse: Codable, Sendable { - public let schema: AnyCodable - public let uihints: [String: AnyCodable] - public let version: String - public let generatedat: String - - public init( - schema: AnyCodable, - uihints: [String: AnyCodable], - version: String, - generatedat: String) - { - self.schema = schema - self.uihints = uihints - self.version = version - self.generatedat = generatedat - } - - private enum CodingKeys: String, CodingKey { - case schema - case uihints = "uiHints" - case version - case generatedat = "generatedAt" - } -} - -public struct ConfigSchemaLookupResult: Codable, Sendable { - public let path: String - public let schema: AnyCodable - public let hint: [String: AnyCodable]? - public let hintpath: String? - public let children: [[String: AnyCodable]] - - public init( - path: String, - schema: AnyCodable, - hint: [String: AnyCodable]?, - hintpath: String?, - children: [[String: AnyCodable]]) - { - self.path = path - self.schema = schema - self.hint = hint - self.hintpath = hintpath - self.children = children - } - - private enum CodingKeys: String, CodingKey { - case path - case schema - case hint - case hintpath = "hintPath" - case children - } -} - -public struct WizardStartParams: Codable, Sendable { - public let mode: AnyCodable? - public let workspace: String? - - public init( - mode: AnyCodable?, - workspace: String?) - { - self.mode = mode - self.workspace = workspace - } - - private enum CodingKeys: String, CodingKey { - case mode - case workspace - } -} - -public struct WizardNextParams: Codable, Sendable { - public let sessionid: String - public let answer: [String: AnyCodable]? - - public init( - sessionid: String, - answer: [String: AnyCodable]?) - { - self.sessionid = sessionid - self.answer = answer - } - - private enum CodingKeys: String, CodingKey { - case sessionid = "sessionId" - case answer - } -} - -public struct WizardCancelParams: Codable, Sendable { - public let sessionid: String - - public init( - sessionid: String) - { - self.sessionid = sessionid - } - - private enum CodingKeys: String, CodingKey { - case sessionid = "sessionId" - } -} - -public struct WizardStatusParams: Codable, Sendable { - public let sessionid: String - - public init( - sessionid: String) - { - self.sessionid = sessionid - } - - private enum CodingKeys: String, CodingKey { - case sessionid = "sessionId" - } -} - -public struct WizardStep: Codable, Sendable { - public let id: String - public let type: AnyCodable - public let title: String? - public let message: String? - public let format: AnyCodable? - public let options: [[String: AnyCodable]]? - public let initialvalue: AnyCodable? - public let placeholder: String? - public let sensitive: Bool? - public let executor: AnyCodable? - - public init( - id: String, - type: AnyCodable, - title: String?, - message: String?, - format: AnyCodable?, - options: [[String: AnyCodable]]?, - initialvalue: AnyCodable?, - placeholder: String?, - sensitive: Bool?, - executor: AnyCodable?) - { - self.id = id - self.type = type - self.title = title - self.message = message - self.format = format - self.options = options - self.initialvalue = initialvalue - self.placeholder = placeholder - self.sensitive = sensitive - self.executor = executor - } - - private enum CodingKeys: String, CodingKey { - case id - case type - case title - case message - case format - case options - case initialvalue = "initialValue" - case placeholder - case sensitive - case executor - } -} - -public struct WizardNextResult: Codable, Sendable { - public let done: Bool - public let step: [String: AnyCodable]? - public let status: AnyCodable? - public let error: String? - - public init( - done: Bool, - step: [String: AnyCodable]?, - status: AnyCodable?, - error: String?) - { - self.done = done - self.step = step - self.status = status - self.error = error - } - - private enum CodingKeys: String, CodingKey { - case done - case step - case status - case error - } -} - -public struct WizardStartResult: Codable, Sendable { - public let sessionid: String - public let done: Bool - public let step: [String: AnyCodable]? - public let status: AnyCodable? - public let error: String? - - public init( - sessionid: String, - done: Bool, - step: [String: AnyCodable]?, - status: AnyCodable?, - error: String?) - { - self.sessionid = sessionid - self.done = done - self.step = step - self.status = status - self.error = error - } - - private enum CodingKeys: String, CodingKey { - case sessionid = "sessionId" - case done - case step - case status - case error - } -} - -public struct WizardStatusResult: Codable, Sendable { - public let status: AnyCodable - public let error: String? - - public init( - status: AnyCodable, - error: String?) - { - self.status = status - self.error = error - } - - private enum CodingKeys: String, CodingKey { - case status - case error - } -} - -public struct TalkModeParams: Codable, Sendable { - public let enabled: Bool - public let phase: String? - - public init( - enabled: Bool, - phase: String?) - { - self.enabled = enabled - self.phase = phase - } - - private enum CodingKeys: String, CodingKey { - case enabled - case phase - } -} - -public struct TalkEvent: Codable, Sendable { - public let id: String - public let type: AnyCodable - public let sessionid: String - public let turnid: String? - public let captureid: String? - public let seq: Int - public let timestamp: String - public let mode: AnyCodable - public let transport: AnyCodable - public let brain: AnyCodable - public let provider: String? - public let final: Bool? - public let callid: String? - public let itemid: String? - public let parentid: String? - public let payload: AnyCodable - - public init( - id: String, - type: AnyCodable, - sessionid: String, - turnid: String?, - captureid: String?, - seq: Int, - timestamp: String, - mode: AnyCodable, - transport: AnyCodable, - brain: AnyCodable, - provider: String?, - final: Bool?, - callid: String?, - itemid: String?, - parentid: String?, - payload: AnyCodable) - { - self.id = id - self.type = type - self.sessionid = sessionid - self.turnid = turnid - self.captureid = captureid - self.seq = seq - self.timestamp = timestamp - self.mode = mode - self.transport = transport - self.brain = brain - self.provider = provider - self.final = final - self.callid = callid - self.itemid = itemid - self.parentid = parentid - self.payload = payload - } - - private enum CodingKeys: String, CodingKey { - case id - case type - case sessionid = "sessionId" - case turnid = "turnId" - case captureid = "captureId" - case seq - case timestamp - case mode - case transport - case brain - case provider - case final - case callid = "callId" - case itemid = "itemId" - case parentid = "parentId" - case payload - } -} - -public struct TalkCatalogParams: Codable, Sendable {} - -public struct TalkCatalogResult: Codable, Sendable { - public let modes: [AnyCodable] - public let transports: [AnyCodable] - public let brains: [AnyCodable] - public let speech: [String: AnyCodable] - public let transcription: [String: AnyCodable] - public let realtime: [String: AnyCodable] - - public init( - modes: [AnyCodable], - transports: [AnyCodable], - brains: [AnyCodable], - speech: [String: AnyCodable], - transcription: [String: AnyCodable], - realtime: [String: AnyCodable]) - { - self.modes = modes - self.transports = transports - self.brains = brains - self.speech = speech - self.transcription = transcription - self.realtime = realtime - } - - private enum CodingKeys: String, CodingKey { - case modes - case transports - case brains - case speech - case transcription - case realtime - } -} - -public struct TalkClientCreateParams: Codable, Sendable { - public let sessionkey: String? - public let provider: String? - public let model: String? - public let voice: String? - public let mode: AnyCodable? - public let transport: AnyCodable? - public let brain: AnyCodable? - - public init( - sessionkey: String?, - provider: String?, - model: String?, - voice: String?, - mode: AnyCodable?, - transport: AnyCodable?, - brain: AnyCodable?) - { - self.sessionkey = sessionkey - self.provider = provider - self.model = model - self.voice = voice - self.mode = mode - self.transport = transport - self.brain = brain - } - - private enum CodingKeys: String, CodingKey { - case sessionkey = "sessionKey" - case provider - case model - case voice - case mode - case transport - case brain - } -} - -public struct TalkClientToolCallParams: Codable, Sendable { - public let sessionkey: String - public let callid: String - public let name: String - public let args: AnyCodable? - public let relaysessionid: String? - - public init( - sessionkey: String, - callid: String, - name: String, - args: AnyCodable?, - relaysessionid: String?) - { - self.sessionkey = sessionkey - self.callid = callid - self.name = name - self.args = args - self.relaysessionid = relaysessionid - } - - private enum CodingKeys: String, CodingKey { - case sessionkey = "sessionKey" - case callid = "callId" - case name - case args - case relaysessionid = "relaySessionId" - } -} - -public struct TalkClientToolCallResult: Codable, Sendable { - public let runid: String - public let idempotencykey: String - - public init( - runid: String, - idempotencykey: String) - { - self.runid = runid - self.idempotencykey = idempotencykey - } - - private enum CodingKeys: String, CodingKey { - case runid = "runId" - case idempotencykey = "idempotencyKey" - } -} - -public struct TalkConfigParams: Codable, Sendable { - public let includesecrets: Bool? - - public init( - includesecrets: Bool?) - { - self.includesecrets = includesecrets - } - - private enum CodingKeys: String, CodingKey { - case includesecrets = "includeSecrets" - } -} - -public struct TalkConfigResult: Codable, Sendable { - public let config: [String: AnyCodable] - - public init( - config: [String: AnyCodable]) - { - self.config = config - } - - private enum CodingKeys: String, CodingKey { - case config - } -} - -public struct TalkSessionAppendAudioParams: Codable, Sendable { - public let sessionid: String - public let audiobase64: String - public let timestamp: Double? - - public init( - sessionid: String, - audiobase64: String, - timestamp: Double?) - { - self.sessionid = sessionid - self.audiobase64 = audiobase64 - self.timestamp = timestamp - } - - private enum CodingKeys: String, CodingKey { - case sessionid = "sessionId" - case audiobase64 = "audioBase64" - case timestamp - } -} - -public struct TalkSessionCancelOutputParams: Codable, Sendable { - public let sessionid: String - public let turnid: String? - public let reason: String? - - public init( - sessionid: String, - turnid: String?, - reason: String?) - { - self.sessionid = sessionid - self.turnid = turnid - self.reason = reason - } - - private enum CodingKeys: String, CodingKey { - case sessionid = "sessionId" - case turnid = "turnId" - case reason - } -} - -public struct TalkSessionCancelTurnParams: Codable, Sendable { - public let sessionid: String - public let turnid: String? - public let reason: String? - - public init( - sessionid: String, - turnid: String?, - reason: String?) - { - self.sessionid = sessionid - self.turnid = turnid - self.reason = reason - } - - private enum CodingKeys: String, CodingKey { - case sessionid = "sessionId" - case turnid = "turnId" - case reason - } -} - -public struct TalkSessionCreateParams: Codable, Sendable { - public let sessionkey: String? - public let provider: String? - public let model: String? - public let voice: String? - public let mode: AnyCodable? - public let transport: AnyCodable? - public let brain: AnyCodable? - public let ttlms: Int? - - public init( - sessionkey: String?, - provider: String?, - model: String?, - voice: String?, - mode: AnyCodable?, - transport: AnyCodable?, - brain: AnyCodable?, - ttlms: Int?) - { - self.sessionkey = sessionkey - self.provider = provider - self.model = model - self.voice = voice - self.mode = mode - self.transport = transport - self.brain = brain - self.ttlms = ttlms - } - - private enum CodingKeys: String, CodingKey { - case sessionkey = "sessionKey" - case provider - case model - case voice - case mode - case transport - case brain - case ttlms = "ttlMs" - } -} - -public struct TalkSessionCreateResult: Codable, Sendable { - public let sessionid: String - public let provider: String? - public let mode: AnyCodable - public let transport: AnyCodable - public let brain: AnyCodable - public let relaysessionid: String? - public let transcriptionsessionid: String? - public let handoffid: String? - public let roomid: String? - public let roomurl: String? - public let token: String? - public let audio: AnyCodable? - public let model: String? - public let voice: String? - public let expiresat: Double? - - public init( - sessionid: String, - provider: String?, - mode: AnyCodable, - transport: AnyCodable, - brain: AnyCodable, - relaysessionid: String?, - transcriptionsessionid: String?, - handoffid: String?, - roomid: String?, - roomurl: String?, - token: String?, - audio: AnyCodable?, - model: String?, - voice: String?, - expiresat: Double?) - { - self.sessionid = sessionid - self.provider = provider - self.mode = mode - self.transport = transport - self.brain = brain - self.relaysessionid = relaysessionid - self.transcriptionsessionid = transcriptionsessionid - self.handoffid = handoffid - self.roomid = roomid - self.roomurl = roomurl - self.token = token - self.audio = audio - self.model = model - self.voice = voice - self.expiresat = expiresat - } - - private enum CodingKeys: String, CodingKey { - case sessionid = "sessionId" - case provider - case mode - case transport - case brain - case relaysessionid = "relaySessionId" - case transcriptionsessionid = "transcriptionSessionId" - case handoffid = "handoffId" - case roomid = "roomId" - case roomurl = "roomUrl" - case token - case audio - case model - case voice - case expiresat = "expiresAt" - } -} - -public struct TalkSessionJoinParams: Codable, Sendable { - public let sessionid: String - public let token: String - - public init( - sessionid: String, - token: String) - { - self.sessionid = sessionid - self.token = token - } - - private enum CodingKeys: String, CodingKey { - case sessionid = "sessionId" - case token - } -} - -public struct TalkSessionJoinResult: Codable, Sendable { - public let id: String - public let roomid: String - public let roomurl: String - public let sessionkey: String - public let sessionid: String? - public let channel: String? - public let target: String? - public let provider: String? - public let model: String? - public let voice: String? - public let mode: AnyCodable - public let transport: AnyCodable - public let brain: AnyCodable - public let createdat: Double - public let expiresat: Double - public let room: [String: AnyCodable] - - public init( - id: String, - roomid: String, - roomurl: String, - sessionkey: String, - sessionid: String?, - channel: String?, - target: String?, - provider: String?, - model: String?, - voice: String?, - mode: AnyCodable, - transport: AnyCodable, - brain: AnyCodable, - createdat: Double, - expiresat: Double, - room: [String: AnyCodable]) - { - self.id = id - self.roomid = roomid - self.roomurl = roomurl - self.sessionkey = sessionkey - self.sessionid = sessionid - self.channel = channel - self.target = target - self.provider = provider - self.model = model - self.voice = voice - self.mode = mode - self.transport = transport - self.brain = brain - self.createdat = createdat - self.expiresat = expiresat - self.room = room - } - - private enum CodingKeys: String, CodingKey { - case id - case roomid = "roomId" - case roomurl = "roomUrl" - case sessionkey = "sessionKey" - case sessionid = "sessionId" - case channel - case target - case provider - case model - case voice - case mode - case transport - case brain - case createdat = "createdAt" - case expiresat = "expiresAt" - case room - } -} - -public struct TalkSessionTurnParams: Codable, Sendable { - public let sessionid: String - public let turnid: String? - - public init( - sessionid: String, - turnid: String?) - { - self.sessionid = sessionid - self.turnid = turnid - } - - private enum CodingKeys: String, CodingKey { - case sessionid = "sessionId" - case turnid = "turnId" - } -} - -public struct TalkSessionTurnResult: Codable, Sendable { - public let ok: Bool - public let turnid: String? - public let events: [TalkEvent]? - - public init( - ok: Bool, - turnid: String?, - events: [TalkEvent]?) - { - self.ok = ok - self.turnid = turnid - self.events = events - } - - private enum CodingKeys: String, CodingKey { - case ok - case turnid = "turnId" - case events - } -} - -public struct TalkSessionSubmitToolResultParams: Codable, Sendable { - public let sessionid: String - public let callid: String - public let result: AnyCodable - - public init( - sessionid: String, - callid: String, - result: AnyCodable) - { - self.sessionid = sessionid - self.callid = callid - self.result = result - } - - private enum CodingKeys: String, CodingKey { - case sessionid = "sessionId" - case callid = "callId" - case result - } -} - -public struct TalkSessionCloseParams: Codable, Sendable { - public let sessionid: String - - public init( - sessionid: String) - { - self.sessionid = sessionid - } - - private enum CodingKeys: String, CodingKey { - case sessionid = "sessionId" - } -} - -public struct TalkSessionOkResult: Codable, Sendable { - public let ok: Bool - - public init( - ok: Bool) - { - self.ok = ok - } - - private enum CodingKeys: String, CodingKey { - case ok - } -} - -public struct TalkSpeakParams: Codable, Sendable { - public let text: String - public let voiceid: String? - public let modelid: String? - public let outputformat: String? - public let speed: Double? - public let ratewpm: Int? - public let stability: Double? - public let similarity: Double? - public let style: Double? - public let speakerboost: Bool? - public let seed: Int? - public let normalize: String? - public let language: String? - public let latencytier: Int? - - public init( - text: String, - voiceid: String?, - modelid: String?, - outputformat: String?, - speed: Double?, - ratewpm: Int?, - stability: Double?, - similarity: Double?, - style: Double?, - speakerboost: Bool?, - seed: Int?, - normalize: String?, - language: String?, - latencytier: Int?) - { - self.text = text - self.voiceid = voiceid - self.modelid = modelid - self.outputformat = outputformat - self.speed = speed - self.ratewpm = ratewpm - self.stability = stability - self.similarity = similarity - self.style = style - self.speakerboost = speakerboost - self.seed = seed - self.normalize = normalize - self.language = language - self.latencytier = latencytier - } - - private enum CodingKeys: String, CodingKey { - case text - case voiceid = "voiceId" - case modelid = "modelId" - case outputformat = "outputFormat" - case speed - case ratewpm = "rateWpm" - case stability - case similarity - case style - case speakerboost = "speakerBoost" - case seed - case normalize - case language - case latencytier = "latencyTier" - } -} - -public struct TalkSpeakResult: Codable, Sendable { - public let audiobase64: String - public let provider: String - public let outputformat: String? - public let voicecompatible: Bool? - public let mimetype: String? - public let fileextension: String? - - public init( - audiobase64: String, - provider: String, - outputformat: String?, - voicecompatible: Bool?, - mimetype: String?, - fileextension: String?) - { - self.audiobase64 = audiobase64 - self.provider = provider - self.outputformat = outputformat - self.voicecompatible = voicecompatible - self.mimetype = mimetype - self.fileextension = fileextension - } - - private enum CodingKeys: String, CodingKey { - case audiobase64 = "audioBase64" - case provider - case outputformat = "outputFormat" - case voicecompatible = "voiceCompatible" - case mimetype = "mimeType" - case fileextension = "fileExtension" - } -} - -public struct ChannelsStatusParams: Codable, Sendable { - public let probe: Bool? - public let timeoutms: Int? - - public init( - probe: Bool?, - timeoutms: Int?) - { - self.probe = probe - self.timeoutms = timeoutms - } - - private enum CodingKeys: String, CodingKey { - case probe - case timeoutms = "timeoutMs" - } -} - -public struct ChannelsStatusResult: Codable, Sendable { - public let ts: Int - public let channelorder: [String] - public let channellabels: [String: AnyCodable] - public let channeldetaillabels: [String: AnyCodable]? - public let channelsystemimages: [String: AnyCodable]? - public let channelmeta: [[String: AnyCodable]]? - public let channels: [String: AnyCodable] - public let channelaccounts: [String: AnyCodable] - public let channeldefaultaccountid: [String: AnyCodable] - public let eventloop: [String: AnyCodable]? - public let partial: Bool? - public let warnings: [String]? - - public init( - ts: Int, - channelorder: [String], - channellabels: [String: AnyCodable], - channeldetaillabels: [String: AnyCodable]?, - channelsystemimages: [String: AnyCodable]?, - channelmeta: [[String: AnyCodable]]?, - channels: [String: AnyCodable], - channelaccounts: [String: AnyCodable], - channeldefaultaccountid: [String: AnyCodable], - eventloop: [String: AnyCodable]?, - partial: Bool?, - warnings: [String]?) - { - self.ts = ts - self.channelorder = channelorder - self.channellabels = channellabels - self.channeldetaillabels = channeldetaillabels - self.channelsystemimages = channelsystemimages - self.channelmeta = channelmeta - self.channels = channels - self.channelaccounts = channelaccounts - self.channeldefaultaccountid = channeldefaultaccountid - self.eventloop = eventloop - self.partial = partial - self.warnings = warnings - } - - private enum CodingKeys: String, CodingKey { - case ts - case channelorder = "channelOrder" - case channellabels = "channelLabels" - case channeldetaillabels = "channelDetailLabels" - case channelsystemimages = "channelSystemImages" - case channelmeta = "channelMeta" - case channels - case channelaccounts = "channelAccounts" - case channeldefaultaccountid = "channelDefaultAccountId" - case eventloop = "eventLoop" - case partial - case warnings - } -} - -public struct ChannelsStartParams: Codable, Sendable { - public let channel: String - public let accountid: String? - - public init( - channel: String, - accountid: String?) - { - self.channel = channel - self.accountid = accountid - } - - private enum CodingKeys: String, CodingKey { - case channel - case accountid = "accountId" - } -} - -public struct ChannelsStopParams: Codable, Sendable { - public let channel: String - public let accountid: String? - - public init( - channel: String, - accountid: String?) - { - self.channel = channel - self.accountid = accountid - } - - private enum CodingKeys: String, CodingKey { - case channel - case accountid = "accountId" - } -} - -public struct ChannelsLogoutParams: Codable, Sendable { - public let channel: String - public let accountid: String? - - public init( - channel: String, - accountid: String?) - { - self.channel = channel - self.accountid = accountid - } - - private enum CodingKeys: String, CodingKey { - case channel - case accountid = "accountId" - } -} - -public struct WebLoginStartParams: Codable, Sendable { - public let force: Bool? - public let timeoutms: Int? - public let verbose: Bool? - public let accountid: String? - - public init( - force: Bool?, - timeoutms: Int?, - verbose: Bool?, - accountid: String?) - { - self.force = force - self.timeoutms = timeoutms - self.verbose = verbose - self.accountid = accountid - } - - private enum CodingKeys: String, CodingKey { - case force - case timeoutms = "timeoutMs" - case verbose - case accountid = "accountId" - } -} - -public struct WebLoginWaitParams: Codable, Sendable { - public let timeoutms: Int? - public let accountid: String? - public let currentqrdataurl: String? - - public init( - timeoutms: Int?, - accountid: String?, - currentqrdataurl: String?) - { - self.timeoutms = timeoutms - self.accountid = accountid - self.currentqrdataurl = currentqrdataurl - } - - private enum CodingKeys: String, CodingKey { - case timeoutms = "timeoutMs" - case accountid = "accountId" - case currentqrdataurl = "currentQrDataUrl" - } -} - -public struct AgentSummary: Codable, Sendable { - public let id: String - public let name: String? - public let identity: [String: AnyCodable]? - public let workspace: String? - public let model: [String: AnyCodable]? - public let agentruntime: [String: AnyCodable]? - - public init( - id: String, - name: String?, - identity: [String: AnyCodable]?, - workspace: String?, - model: [String: AnyCodable]?, - agentruntime: [String: AnyCodable]?) - { - self.id = id - self.name = name - self.identity = identity - self.workspace = workspace - self.model = model - self.agentruntime = agentruntime - } - - private enum CodingKeys: String, CodingKey { - case id - case name - case identity - case workspace - case model - case agentruntime = "agentRuntime" - } -} - -public struct AgentsCreateParams: Codable, Sendable { - public let name: String - public let workspace: String - public let model: String? - public let emoji: String? - public let avatar: String? - - public init( - name: String, - workspace: String, - model: String?, - emoji: String?, - avatar: String?) - { - self.name = name - self.workspace = workspace - self.model = model - self.emoji = emoji - self.avatar = avatar - } - - private enum CodingKeys: String, CodingKey { - case name - case workspace - case model - case emoji - case avatar - } -} - -public struct AgentsCreateResult: Codable, Sendable { - public let ok: Bool - public let agentid: String - public let name: String - public let workspace: String - public let model: String? - - public init( - ok: Bool, - agentid: String, - name: String, - workspace: String, - model: String?) - { - self.ok = ok - self.agentid = agentid - self.name = name - self.workspace = workspace - self.model = model - } - - private enum CodingKeys: String, CodingKey { - case ok - case agentid = "agentId" - case name - case workspace - case model - } -} - -public struct AgentsUpdateParams: Codable, Sendable { - public let agentid: String - public let name: String? - public let workspace: String? - public let model: String? - public let emoji: String? - public let avatar: String? - - public init( - agentid: String, - name: String?, - workspace: String?, - model: String?, - emoji: String?, - avatar: String?) - { - self.agentid = agentid - self.name = name - self.workspace = workspace - self.model = model - self.emoji = emoji - self.avatar = avatar - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - case name - case workspace - case model - case emoji - case avatar - } -} - -public struct AgentsUpdateResult: Codable, Sendable { - public let ok: Bool - public let agentid: String - - public init( - ok: Bool, - agentid: String) - { - self.ok = ok - self.agentid = agentid - } - - private enum CodingKeys: String, CodingKey { - case ok - case agentid = "agentId" - } -} - -public struct AgentsDeleteParams: Codable, Sendable { - public let agentid: String - public let deletefiles: Bool? - - public init( - agentid: String, - deletefiles: Bool?) - { - self.agentid = agentid - self.deletefiles = deletefiles - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - case deletefiles = "deleteFiles" - } -} - -public struct AgentsDeleteResult: Codable, Sendable { - public let ok: Bool - public let agentid: String - public let removedbindings: Int - - public init( - ok: Bool, - agentid: String, - removedbindings: Int) - { - self.ok = ok - self.agentid = agentid - self.removedbindings = removedbindings - } - - private enum CodingKeys: String, CodingKey { - case ok - case agentid = "agentId" - case removedbindings = "removedBindings" - } -} - -public struct AgentsFileEntry: Codable, Sendable { - public let name: String - public let path: String - public let missing: Bool - public let size: Int? - public let updatedatms: Int? - public let content: String? - - public init( - name: String, - path: String, - missing: Bool, - size: Int?, - updatedatms: Int?, - content: String?) - { - self.name = name - self.path = path - self.missing = missing - self.size = size - self.updatedatms = updatedatms - self.content = content - } - - private enum CodingKeys: String, CodingKey { - case name - case path - case missing - case size - case updatedatms = "updatedAtMs" - case content - } -} - -public struct AgentsFilesListParams: Codable, Sendable { - public let agentid: String - - public init( - agentid: String) - { - self.agentid = agentid - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - } -} - -public struct AgentsFilesListResult: Codable, Sendable { - public let agentid: String - public let workspace: String - public let files: [AgentsFileEntry] - - public init( - agentid: String, - workspace: String, - files: [AgentsFileEntry]) - { - self.agentid = agentid - self.workspace = workspace - self.files = files - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - case workspace - case files - } -} - -public struct AgentsFilesGetParams: Codable, Sendable { - public let agentid: String - public let name: String - - public init( - agentid: String, - name: String) - { - self.agentid = agentid - self.name = name - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - case name - } -} - -public struct AgentsFilesGetResult: Codable, Sendable { - public let agentid: String - public let workspace: String - public let file: AgentsFileEntry - - public init( - agentid: String, - workspace: String, - file: AgentsFileEntry) - { - self.agentid = agentid - self.workspace = workspace - self.file = file - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - case workspace - case file - } -} - -public struct AgentsFilesSetParams: Codable, Sendable { - public let agentid: String - public let name: String - public let content: String - - public init( - agentid: String, - name: String, - content: String) - { - self.agentid = agentid - self.name = name - self.content = content - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - case name - case content - } -} - -public struct AgentsFilesSetResult: Codable, Sendable { - public let ok: Bool - public let agentid: String - public let workspace: String - public let file: AgentsFileEntry - - public init( - ok: Bool, - agentid: String, - workspace: String, - file: AgentsFileEntry) - { - self.ok = ok - self.agentid = agentid - self.workspace = workspace - self.file = file - } - - private enum CodingKeys: String, CodingKey { - case ok - case agentid = "agentId" - case workspace - case file - } -} - -public struct ArtifactSummary: Codable, Sendable { - public let id: String - public let type: String - public let title: String - public let mimetype: String? - public let sizebytes: Int? - public let sessionkey: String? - public let runid: String? - public let taskid: String? - public let messageseq: Int? - public let source: String? - public let download: [String: AnyCodable] - - public init( - id: String, - type: String, - title: String, - mimetype: String?, - sizebytes: Int?, - sessionkey: String?, - runid: String?, - taskid: String?, - messageseq: Int?, - source: String?, - download: [String: AnyCodable]) - { - self.id = id - self.type = type - self.title = title - self.mimetype = mimetype - self.sizebytes = sizebytes - self.sessionkey = sessionkey - self.runid = runid - self.taskid = taskid - self.messageseq = messageseq - self.source = source - self.download = download - } - - private enum CodingKeys: String, CodingKey { - case id - case type - case title - case mimetype = "mimeType" - case sizebytes = "sizeBytes" - case sessionkey = "sessionKey" - case runid = "runId" - case taskid = "taskId" - case messageseq = "messageSeq" - case source - case download - } -} - -public struct ArtifactsListParams: Codable, Sendable { - public let sessionkey: String? - public let runid: String? - public let taskid: String? - - public init( - sessionkey: String?, - runid: String?, - taskid: String?) - { - self.sessionkey = sessionkey - self.runid = runid - self.taskid = taskid - } - - private enum CodingKeys: String, CodingKey { - case sessionkey = "sessionKey" - case runid = "runId" - case taskid = "taskId" - } -} - -public struct ArtifactsListResult: Codable, Sendable { - public let artifacts: [ArtifactSummary] - - public init( - artifacts: [ArtifactSummary]) - { - self.artifacts = artifacts - } - - private enum CodingKeys: String, CodingKey { - case artifacts - } -} - -public struct ArtifactsGetParams: Codable, Sendable { - public let sessionkey: String? - public let runid: String? - public let taskid: String? - public let artifactid: String - - public init( - sessionkey: String?, - runid: String?, - taskid: String?, - artifactid: String) - { - self.sessionkey = sessionkey - self.runid = runid - self.taskid = taskid - self.artifactid = artifactid - } - - private enum CodingKeys: String, CodingKey { - case sessionkey = "sessionKey" - case runid = "runId" - case taskid = "taskId" - case artifactid = "artifactId" - } -} - -public struct ArtifactsGetResult: Codable, Sendable { - public let artifact: ArtifactSummary - - public init( - artifact: ArtifactSummary) - { - self.artifact = artifact - } - - private enum CodingKeys: String, CodingKey { - case artifact - } -} - -public struct ArtifactsDownloadParams: Codable, Sendable { - public let sessionkey: String? - public let runid: String? - public let taskid: String? - public let artifactid: String - - public init( - sessionkey: String?, - runid: String?, - taskid: String?, - artifactid: String) - { - self.sessionkey = sessionkey - self.runid = runid - self.taskid = taskid - self.artifactid = artifactid - } - - private enum CodingKeys: String, CodingKey { - case sessionkey = "sessionKey" - case runid = "runId" - case taskid = "taskId" - case artifactid = "artifactId" - } -} - -public struct ArtifactsDownloadResult: Codable, Sendable { - public let artifact: ArtifactSummary - public let encoding: String? - public let data: String? - public let url: String? - - public init( - artifact: ArtifactSummary, - encoding: String?, - data: String?, - url: String?) - { - self.artifact = artifact - self.encoding = encoding - self.data = data - self.url = url - } - - private enum CodingKeys: String, CodingKey { - case artifact - case encoding - case data - case url - } -} - -public struct AgentsListParams: Codable, Sendable {} - -public struct AgentsListResult: Codable, Sendable { - public let defaultid: String - public let mainkey: String - public let scope: AnyCodable - public let agents: [AgentSummary] - - public init( - defaultid: String, - mainkey: String, - scope: AnyCodable, - agents: [AgentSummary]) - { - self.defaultid = defaultid - self.mainkey = mainkey - self.scope = scope - self.agents = agents - } - - private enum CodingKeys: String, CodingKey { - case defaultid = "defaultId" - case mainkey = "mainKey" - case scope - case agents - } -} - -public struct ModelChoice: Codable, Sendable { - public let id: String - public let name: String - public let provider: String - public let alias: String? - public let contextwindow: Int? - public let reasoning: Bool? - - public init( - id: String, - name: String, - provider: String, - alias: String?, - contextwindow: Int?, - reasoning: Bool?) - { - self.id = id - self.name = name - self.provider = provider - self.alias = alias - self.contextwindow = contextwindow - self.reasoning = reasoning - } - - private enum CodingKeys: String, CodingKey { - case id - case name - case provider - case alias - case contextwindow = "contextWindow" - case reasoning - } -} - -public struct ModelsListParams: Codable, Sendable { - public let view: AnyCodable? - - public init( - view: AnyCodable?) - { - self.view = view - } - - private enum CodingKeys: String, CodingKey { - case view - } -} - -public struct ModelsListResult: Codable, Sendable { - public let models: [ModelChoice] - - public init( - models: [ModelChoice]) - { - self.models = models - } - - private enum CodingKeys: String, CodingKey { - case models - } -} - -public struct CommandEntry: Codable, Sendable { - public let name: String - public let nativename: String? - public let textaliases: [String]? - public let description: String - public let category: AnyCodable? - public let source: AnyCodable - public let scope: AnyCodable - public let acceptsargs: Bool - public let args: [[String: AnyCodable]]? - - public init( - name: String, - nativename: String?, - textaliases: [String]?, - description: String, - category: AnyCodable?, - source: AnyCodable, - scope: AnyCodable, - acceptsargs: Bool, - args: [[String: AnyCodable]]?) - { - self.name = name - self.nativename = nativename - self.textaliases = textaliases - self.description = description - self.category = category - self.source = source - self.scope = scope - self.acceptsargs = acceptsargs - self.args = args - } - - private enum CodingKeys: String, CodingKey { - case name - case nativename = "nativeName" - case textaliases = "textAliases" - case description - case category - case source - case scope - case acceptsargs = "acceptsArgs" - case args - } -} - -public struct CommandsListParams: Codable, Sendable { - public let agentid: String? - public let provider: String? - public let scope: AnyCodable? - public let includeargs: Bool? - - public init( - agentid: String?, - provider: String?, - scope: AnyCodable?, - includeargs: Bool?) - { - self.agentid = agentid - self.provider = provider - self.scope = scope - self.includeargs = includeargs - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - case provider - case scope - case includeargs = "includeArgs" - } -} - -public struct CommandsListResult: Codable, Sendable { - public let commands: [CommandEntry] - - public init( - commands: [CommandEntry]) - { - self.commands = commands - } - - private enum CodingKeys: String, CodingKey { - case commands - } -} - -public struct SkillsStatusParams: Codable, Sendable { - public let agentid: String? - - public init( - agentid: String?) - { - self.agentid = agentid - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - } -} - -public struct ToolsCatalogParams: Codable, Sendable { - public let agentid: String? - public let includeplugins: Bool? - - public init( - agentid: String?, - includeplugins: Bool?) - { - self.agentid = agentid - self.includeplugins = includeplugins - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - case includeplugins = "includePlugins" - } -} - -public struct ToolCatalogProfile: Codable, Sendable { - public let id: AnyCodable - public let label: String - - public init( - id: AnyCodable, - label: String) - { - self.id = id - self.label = label - } - - private enum CodingKeys: String, CodingKey { - case id - case label - } -} - -public struct ToolCatalogEntry: Codable, Sendable { - public let id: String - public let label: String - public let description: String - public let source: AnyCodable - public let pluginid: String? - public let optional: Bool? - public let risk: AnyCodable? - public let tags: [String]? - public let defaultprofiles: [AnyCodable] - - public init( - id: String, - label: String, - description: String, - source: AnyCodable, - pluginid: String?, - optional: Bool?, - risk: AnyCodable?, - tags: [String]?, - defaultprofiles: [AnyCodable]) - { - self.id = id - self.label = label - self.description = description - self.source = source - self.pluginid = pluginid - self.optional = optional - self.risk = risk - self.tags = tags - self.defaultprofiles = defaultprofiles - } - - private enum CodingKeys: String, CodingKey { - case id - case label - case description - case source - case pluginid = "pluginId" - case optional - case risk - case tags - case defaultprofiles = "defaultProfiles" - } -} - -public struct ToolCatalogGroup: Codable, Sendable { - public let id: String - public let label: String - public let source: AnyCodable - public let pluginid: String? - public let tools: [ToolCatalogEntry] - - public init( - id: String, - label: String, - source: AnyCodable, - pluginid: String?, - tools: [ToolCatalogEntry]) - { - self.id = id - self.label = label - self.source = source - self.pluginid = pluginid - self.tools = tools - } - - private enum CodingKeys: String, CodingKey { - case id - case label - case source - case pluginid = "pluginId" - case tools - } -} - -public struct ToolsCatalogResult: Codable, Sendable { - public let agentid: String - public let profiles: [ToolCatalogProfile] - public let groups: [ToolCatalogGroup] - - public init( - agentid: String, - profiles: [ToolCatalogProfile], - groups: [ToolCatalogGroup]) - { - self.agentid = agentid - self.profiles = profiles - self.groups = groups - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - case profiles - case groups - } -} - -public struct ToolsEffectiveParams: Codable, Sendable { - public let agentid: String? - public let sessionkey: String - - public init( - agentid: String?, - sessionkey: String) - { - self.agentid = agentid - self.sessionkey = sessionkey - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - case sessionkey = "sessionKey" - } -} - -public struct ToolsEffectiveEntry: Codable, Sendable { - public let id: String - public let label: String - public let description: String - public let rawdescription: String - public let source: AnyCodable - public let pluginid: String? - public let channelid: String? - public let risk: AnyCodable? - public let tags: [String]? - - public init( - id: String, - label: String, - description: String, - rawdescription: String, - source: AnyCodable, - pluginid: String?, - channelid: String?, - risk: AnyCodable?, - tags: [String]?) - { - self.id = id - self.label = label - self.description = description - self.rawdescription = rawdescription - self.source = source - self.pluginid = pluginid - self.channelid = channelid - self.risk = risk - self.tags = tags - } - - private enum CodingKeys: String, CodingKey { - case id - case label - case description - case rawdescription = "rawDescription" - case source - case pluginid = "pluginId" - case channelid = "channelId" - case risk - case tags - } -} - -public struct ToolsEffectiveGroup: Codable, Sendable { - public let id: AnyCodable - public let label: String - public let source: AnyCodable - public let tools: [ToolsEffectiveEntry] - - public init( - id: AnyCodable, - label: String, - source: AnyCodable, - tools: [ToolsEffectiveEntry]) - { - self.id = id - self.label = label - self.source = source - self.tools = tools - } - - private enum CodingKeys: String, CodingKey { - case id - case label - case source - case tools - } -} - -public struct ToolsEffectiveResult: Codable, Sendable { - public let agentid: String - public let profile: String - public let groups: [ToolsEffectiveGroup] - - public init( - agentid: String, - profile: String, - groups: [ToolsEffectiveGroup]) - { - self.agentid = agentid - self.profile = profile - self.groups = groups - } - - private enum CodingKeys: String, CodingKey { - case agentid = "agentId" - case profile - case groups - } -} - -public struct ToolsInvokeParams: Codable, Sendable { - public let name: String - public let args: [String: AnyCodable]? - public let sessionkey: String? - public let agentid: String? - public let confirm: Bool? - public let idempotencykey: String? - - public init( - name: String, - args: [String: AnyCodable]?, - sessionkey: String?, - agentid: String?, - confirm: Bool?, - idempotencykey: String?) - { - self.name = name - self.args = args - self.sessionkey = sessionkey - self.agentid = agentid - self.confirm = confirm - self.idempotencykey = idempotencykey - } - - private enum CodingKeys: String, CodingKey { - case name - case args - case sessionkey = "sessionKey" - case agentid = "agentId" - case confirm - case idempotencykey = "idempotencyKey" - } -} - -public struct ToolsInvokeError: Codable, Sendable { - public let code: String - public let message: String - public let details: AnyCodable? - - public init( - code: String, - message: String, - details: AnyCodable?) - { - self.code = code - self.message = message - self.details = details - } - - private enum CodingKeys: String, CodingKey { - case code - case message - case details - } -} - -public struct ToolsInvokeResult: Codable, Sendable { - public let ok: Bool - public let toolname: String - public let output: AnyCodable? - public let requiresapproval: Bool? - public let approvalid: String? - public let source: AnyCodable? - public let error: [String: AnyCodable]? - - public init( - ok: Bool, - toolname: String, - output: AnyCodable?, - requiresapproval: Bool?, - approvalid: String?, - source: AnyCodable?, - error: [String: AnyCodable]?) - { - self.ok = ok - self.toolname = toolname - self.output = output - self.requiresapproval = requiresapproval - self.approvalid = approvalid - self.source = source - self.error = error - } - - private enum CodingKeys: String, CodingKey { - case ok - case toolname = "toolName" - case output - case requiresapproval = "requiresApproval" - case approvalid = "approvalId" - case source - case error - } -} - -public struct SkillsBinsParams: Codable, Sendable {} - -public struct SkillsBinsResult: Codable, Sendable { - public let bins: [String] - - public init( - bins: [String]) - { - self.bins = bins - } - - private enum CodingKeys: String, CodingKey { - case bins - } -} - -public struct SkillsSearchParams: Codable, Sendable { - public let query: String? - public let limit: Int? - - public init( - query: String?, - limit: Int?) - { - self.query = query - self.limit = limit - } - - private enum CodingKeys: String, CodingKey { - case query - case limit - } -} - -public struct SkillsSearchResult: Codable, Sendable { - public let results: [[String: AnyCodable]] - - public init( - results: [[String: AnyCodable]]) - { - self.results = results - } - - private enum CodingKeys: String, CodingKey { - case results - } -} - -public struct SkillsDetailParams: Codable, Sendable { - public let slug: String - - public init( - slug: String) - { - self.slug = slug - } - - private enum CodingKeys: String, CodingKey { - case slug - } -} - -public struct SkillsDetailResult: Codable, Sendable { - public let skill: AnyCodable - public let latestversion: AnyCodable? - public let metadata: AnyCodable? - public let owner: AnyCodable? - - public init( - skill: AnyCodable, - latestversion: AnyCodable?, - metadata: AnyCodable?, - owner: AnyCodable?) - { - self.skill = skill - self.latestversion = latestversion - self.metadata = metadata - self.owner = owner - } - - private enum CodingKeys: String, CodingKey { - case skill - case latestversion = "latestVersion" - case metadata - case owner - } -} - -public struct CronJob: Codable, Sendable { - public let id: String - public let agentid: String? - public let sessionkey: String? - public let name: String - public let description: String? - public let enabled: Bool - public let deleteafterrun: Bool? - public let createdatms: Int - public let updatedatms: Int - public let schedule: AnyCodable - public let sessiontarget: AnyCodable - public let wakemode: AnyCodable - public let payload: AnyCodable - public let delivery: AnyCodable? - public let failurealert: AnyCodable? - public let state: [String: AnyCodable] - - public init( - id: String, - agentid: String?, - sessionkey: String?, - name: String, - description: String?, - enabled: Bool, - deleteafterrun: Bool?, - createdatms: Int, - updatedatms: Int, - schedule: AnyCodable, - sessiontarget: AnyCodable, - wakemode: AnyCodable, - payload: AnyCodable, - delivery: AnyCodable?, - failurealert: AnyCodable?, - state: [String: AnyCodable]) - { - self.id = id - self.agentid = agentid - self.sessionkey = sessionkey - self.name = name - self.description = description - self.enabled = enabled - self.deleteafterrun = deleteafterrun - self.createdatms = createdatms - self.updatedatms = updatedatms - self.schedule = schedule - self.sessiontarget = sessiontarget - self.wakemode = wakemode - self.payload = payload - self.delivery = delivery - self.failurealert = failurealert - self.state = state - } - - private enum CodingKeys: String, CodingKey { - case id - case agentid = "agentId" - case sessionkey = "sessionKey" - case name - case description - case enabled - case deleteafterrun = "deleteAfterRun" - case createdatms = "createdAtMs" - case updatedatms = "updatedAtMs" - case schedule - case sessiontarget = "sessionTarget" - case wakemode = "wakeMode" - case payload - case delivery - case failurealert = "failureAlert" - case state - } -} - -public struct CronListParams: Codable, Sendable { - public let includedisabled: Bool? - public let limit: Int? - public let offset: Int? - public let query: String? - public let enabled: AnyCodable? - public let sortby: AnyCodable? - public let sortdir: AnyCodable? - public let agentid: String? - - public init( - includedisabled: Bool?, - limit: Int?, - offset: Int?, - query: String?, - enabled: AnyCodable?, - sortby: AnyCodable?, - sortdir: AnyCodable?, - agentid: String?) - { - self.includedisabled = includedisabled - self.limit = limit - self.offset = offset - self.query = query - self.enabled = enabled - self.sortby = sortby - self.sortdir = sortdir - self.agentid = agentid - } - - private enum CodingKeys: String, CodingKey { - case includedisabled = "includeDisabled" - case limit - case offset - case query - case enabled - case sortby = "sortBy" - case sortdir = "sortDir" - case agentid = "agentId" - } -} - -public struct CronStatusParams: Codable, Sendable {} - -public struct CronAddParams: Codable, Sendable { - public let name: String - public let agentid: AnyCodable? - public let sessionkey: AnyCodable? - public let description: String? - public let enabled: Bool? - public let deleteafterrun: Bool? - public let schedule: AnyCodable - public let sessiontarget: AnyCodable - public let wakemode: AnyCodable - public let payload: AnyCodable - public let delivery: AnyCodable? - public let failurealert: AnyCodable? - - public init( - name: String, - agentid: AnyCodable?, - sessionkey: AnyCodable?, - description: String?, - enabled: Bool?, - deleteafterrun: Bool?, - schedule: AnyCodable, - sessiontarget: AnyCodable, - wakemode: AnyCodable, - payload: AnyCodable, - delivery: AnyCodable?, - failurealert: AnyCodable?) - { - self.name = name - self.agentid = agentid - self.sessionkey = sessionkey - self.description = description - self.enabled = enabled - self.deleteafterrun = deleteafterrun - self.schedule = schedule - self.sessiontarget = sessiontarget - self.wakemode = wakemode - self.payload = payload - self.delivery = delivery - self.failurealert = failurealert - } - - private enum CodingKeys: String, CodingKey { - case name - case agentid = "agentId" - case sessionkey = "sessionKey" - case description - case enabled - case deleteafterrun = "deleteAfterRun" - case schedule - case sessiontarget = "sessionTarget" - case wakemode = "wakeMode" - case payload - case delivery - case failurealert = "failureAlert" - } -} - -public struct CronRunsParams: Codable, Sendable { - public let scope: AnyCodable? - public let id: String? - public let jobid: String? - public let limit: Int? - public let offset: Int? - public let statuses: [AnyCodable]? - public let status: AnyCodable? - public let deliverystatuses: [AnyCodable]? - public let deliverystatus: AnyCodable? - public let query: String? - public let sortdir: AnyCodable? - - public init( - scope: AnyCodable?, - id: String?, - jobid: String?, - limit: Int?, - offset: Int?, - statuses: [AnyCodable]?, - status: AnyCodable?, - deliverystatuses: [AnyCodable]?, - deliverystatus: AnyCodable?, - query: String?, - sortdir: AnyCodable?) - { - self.scope = scope - self.id = id - self.jobid = jobid - self.limit = limit - self.offset = offset - self.statuses = statuses - self.status = status - self.deliverystatuses = deliverystatuses - self.deliverystatus = deliverystatus - self.query = query - self.sortdir = sortdir - } - - private enum CodingKeys: String, CodingKey { - case scope - case id - case jobid = "jobId" - case limit - case offset - case statuses - case status - case deliverystatuses = "deliveryStatuses" - case deliverystatus = "deliveryStatus" - case query - case sortdir = "sortDir" - } -} - -public struct CronRunLogEntry: Codable, Sendable { - public let ts: Int - public let jobid: String - public let action: String - public let status: AnyCodable? - public let error: String? - public let summary: String? - public let diagnostics: [String: AnyCodable]? - public let delivered: Bool? - public let deliverystatus: AnyCodable? - public let deliveryerror: String? - public let sessionid: String? - public let sessionkey: String? - public let runid: String? - public let runatms: Int? - public let durationms: Int? - public let nextrunatms: Int? - public let model: String? - public let provider: String? - public let usage: [String: AnyCodable]? - public let jobname: String? - - public init( - ts: Int, - jobid: String, - action: String, - status: AnyCodable?, - error: String?, - summary: String?, - diagnostics: [String: AnyCodable]?, - delivered: Bool?, - deliverystatus: AnyCodable?, - deliveryerror: String?, - sessionid: String?, - sessionkey: String?, - runid: String?, - runatms: Int?, - durationms: Int?, - nextrunatms: Int?, - model: String?, - provider: String?, - usage: [String: AnyCodable]?, - jobname: String?) - { - self.ts = ts - self.jobid = jobid - self.action = action - self.status = status - self.error = error - self.summary = summary - self.diagnostics = diagnostics - self.delivered = delivered - self.deliverystatus = deliverystatus - self.deliveryerror = deliveryerror - self.sessionid = sessionid - self.sessionkey = sessionkey - self.runid = runid - self.runatms = runatms - self.durationms = durationms - self.nextrunatms = nextrunatms - self.model = model - self.provider = provider - self.usage = usage - self.jobname = jobname - } - - private enum CodingKeys: String, CodingKey { - case ts - case jobid = "jobId" - case action - case status - case error - case summary - case diagnostics - case delivered - case deliverystatus = "deliveryStatus" - case deliveryerror = "deliveryError" - case sessionid = "sessionId" - case sessionkey = "sessionKey" - case runid = "runId" - case runatms = "runAtMs" - case durationms = "durationMs" - case nextrunatms = "nextRunAtMs" - case model - case provider - case usage - case jobname = "jobName" - } -} - -public struct LogsTailParams: Codable, Sendable { - public let cursor: Int? - public let limit: Int? - public let maxbytes: Int? - - public init( - cursor: Int?, - limit: Int?, - maxbytes: Int?) - { - self.cursor = cursor - self.limit = limit - self.maxbytes = maxbytes - } - - private enum CodingKeys: String, CodingKey { - case cursor - case limit - case maxbytes = "maxBytes" - } -} - -public struct LogsTailResult: Codable, Sendable { - public let file: String - public let cursor: Int - public let size: Int - public let lines: [String] - public let truncated: Bool? - public let reset: Bool? - - public init( - file: String, - cursor: Int, - size: Int, - lines: [String], - truncated: Bool?, - reset: Bool?) - { - self.file = file - self.cursor = cursor - self.size = size - self.lines = lines - self.truncated = truncated - self.reset = reset - } - - private enum CodingKeys: String, CodingKey { - case file - case cursor - case size - case lines - case truncated - case reset - } -} - -public struct ExecApprovalsGetParams: Codable, Sendable {} - -public struct ExecApprovalsSetParams: Codable, Sendable { - public let file: [String: AnyCodable] - public let basehash: String? - - public init( - file: [String: AnyCodable], - basehash: String?) - { - self.file = file - self.basehash = basehash - } - - private enum CodingKeys: String, CodingKey { - case file - case basehash = "baseHash" - } -} - -public struct ExecApprovalsNodeGetParams: Codable, Sendable { - public let nodeid: String - - public init( - nodeid: String) - { - self.nodeid = nodeid - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - } -} - -public struct ExecApprovalsNodeSetParams: Codable, Sendable { - public let nodeid: String - public let file: [String: AnyCodable] - public let basehash: String? - - public init( - nodeid: String, - file: [String: AnyCodable], - basehash: String?) - { - self.nodeid = nodeid - self.file = file - self.basehash = basehash - } - - private enum CodingKeys: String, CodingKey { - case nodeid = "nodeId" - case file - case basehash = "baseHash" - } -} - -public struct ExecApprovalsSnapshot: Codable, Sendable { - public let path: String - public let exists: Bool - public let hash: String - public let file: [String: AnyCodable] - - public init( - path: String, - exists: Bool, - hash: String, - file: [String: AnyCodable]) - { - self.path = path - self.exists = exists - self.hash = hash - self.file = file - } - - private enum CodingKeys: String, CodingKey { - case path - case exists - case hash - case file - } -} - -public struct ExecApprovalGetParams: Codable, Sendable { - public let id: String - - public init( - id: String) - { - self.id = id - } - - private enum CodingKeys: String, CodingKey { - case id - } -} - -public struct ExecApprovalRequestParams: Codable, Sendable { - public let id: String? - public let command: String? - public let commandargv: [String]? - public let systemrunplan: [String: AnyCodable]? - public let env: [String: AnyCodable]? - public let cwd: AnyCodable? - public let nodeid: AnyCodable? - public let host: AnyCodable? - public let security: AnyCodable? - public let ask: AnyCodable? - public let warningtext: AnyCodable? - public let agentid: AnyCodable? - public let resolvedpath: AnyCodable? - public let sessionkey: AnyCodable? - public let turnsourcechannel: AnyCodable? - public let turnsourceto: AnyCodable? - public let turnsourceaccountid: AnyCodable? - public let turnsourcethreadid: AnyCodable? - public let timeoutms: Int? - public let twophase: Bool? - - public init( - id: String?, - command: String?, - commandargv: [String]?, - systemrunplan: [String: AnyCodable]?, - env: [String: AnyCodable]?, - cwd: AnyCodable?, - nodeid: AnyCodable?, - host: AnyCodable?, - security: AnyCodable?, - ask: AnyCodable?, - warningtext: AnyCodable?, - agentid: AnyCodable?, - resolvedpath: AnyCodable?, - sessionkey: AnyCodable?, - turnsourcechannel: AnyCodable?, - turnsourceto: AnyCodable?, - turnsourceaccountid: AnyCodable?, - turnsourcethreadid: AnyCodable?, - timeoutms: Int?, - twophase: Bool?) - { - self.id = id - self.command = command - self.commandargv = commandargv - self.systemrunplan = systemrunplan - self.env = env - self.cwd = cwd - self.nodeid = nodeid - self.host = host - self.security = security - self.ask = ask - self.warningtext = warningtext - self.agentid = agentid - self.resolvedpath = resolvedpath - self.sessionkey = sessionkey - self.turnsourcechannel = turnsourcechannel - self.turnsourceto = turnsourceto - self.turnsourceaccountid = turnsourceaccountid - self.turnsourcethreadid = turnsourcethreadid - self.timeoutms = timeoutms - self.twophase = twophase - } - - private enum CodingKeys: String, CodingKey { - case id - case command - case commandargv = "commandArgv" - case systemrunplan = "systemRunPlan" - case env - case cwd - case nodeid = "nodeId" - case host - case security - case ask - case warningtext = "warningText" - case agentid = "agentId" - case resolvedpath = "resolvedPath" - case sessionkey = "sessionKey" - case turnsourcechannel = "turnSourceChannel" - case turnsourceto = "turnSourceTo" - case turnsourceaccountid = "turnSourceAccountId" - case turnsourcethreadid = "turnSourceThreadId" - case timeoutms = "timeoutMs" - case twophase = "twoPhase" - } -} - -public struct ExecApprovalResolveParams: Codable, Sendable { - public let id: String - public let decision: String - - public init( - id: String, - decision: String) - { - self.id = id - self.decision = decision - } - - private enum CodingKeys: String, CodingKey { - case id - case decision - } -} - -public struct PluginApprovalRequestParams: Codable, Sendable { - public let pluginid: String? - public let title: String - public let description: String - public let severity: String? - public let toolname: String? - public let toolcallid: String? - public let alloweddecisions: [String]? - public let agentid: String? - public let sessionkey: String? - public let turnsourcechannel: String? - public let turnsourceto: String? - public let turnsourceaccountid: String? - public let turnsourcethreadid: AnyCodable? - public let timeoutms: Int? - public let twophase: Bool? - - public init( - pluginid: String?, - title: String, - description: String, - severity: String?, - toolname: String?, - toolcallid: String?, - alloweddecisions: [String]?, - agentid: String?, - sessionkey: String?, - turnsourcechannel: String?, - turnsourceto: String?, - turnsourceaccountid: String?, - turnsourcethreadid: AnyCodable?, - timeoutms: Int?, - twophase: Bool?) - { - self.pluginid = pluginid - self.title = title - self.description = description - self.severity = severity - self.toolname = toolname - self.toolcallid = toolcallid - self.alloweddecisions = alloweddecisions - self.agentid = agentid - self.sessionkey = sessionkey - self.turnsourcechannel = turnsourcechannel - self.turnsourceto = turnsourceto - self.turnsourceaccountid = turnsourceaccountid - self.turnsourcethreadid = turnsourcethreadid - self.timeoutms = timeoutms - self.twophase = twophase - } - - private enum CodingKeys: String, CodingKey { - case pluginid = "pluginId" - case title - case description - case severity - case toolname = "toolName" - case toolcallid = "toolCallId" - case alloweddecisions = "allowedDecisions" - case agentid = "agentId" - case sessionkey = "sessionKey" - case turnsourcechannel = "turnSourceChannel" - case turnsourceto = "turnSourceTo" - case turnsourceaccountid = "turnSourceAccountId" - case turnsourcethreadid = "turnSourceThreadId" - case timeoutms = "timeoutMs" - case twophase = "twoPhase" - } -} - -public struct PluginApprovalResolveParams: Codable, Sendable { - public let id: String - public let decision: String - - public init( - id: String, - decision: String) - { - self.id = id - self.decision = decision - } - - private enum CodingKeys: String, CodingKey { - case id - case decision - } -} - -public struct PluginControlUiDescriptor: Codable, Sendable { - public let id: String - public let pluginid: String - public let pluginname: String? - public let surface: AnyCodable - public let label: String - public let description: String? - public let placement: String? - public let schema: AnyCodable? - public let requiredscopes: [String]? - - public init( - id: String, - pluginid: String, - pluginname: String?, - surface: AnyCodable, - label: String, - description: String?, - placement: String?, - schema: AnyCodable?, - requiredscopes: [String]?) - { - self.id = id - self.pluginid = pluginid - self.pluginname = pluginname - self.surface = surface - self.label = label - self.description = description - self.placement = placement - self.schema = schema - self.requiredscopes = requiredscopes - } - - private enum CodingKeys: String, CodingKey { - case id - case pluginid = "pluginId" - case pluginname = "pluginName" - case surface - case label - case description - case placement - case schema - case requiredscopes = "requiredScopes" - } -} - -public struct PluginsUiDescriptorsParams: Codable, Sendable {} - -public struct PluginsUiDescriptorsResult: Codable, Sendable { - public let ok: Bool - public let descriptors: [PluginControlUiDescriptor] - - public init( - ok: Bool, - descriptors: [PluginControlUiDescriptor]) - { - self.ok = ok - self.descriptors = descriptors - } - - private enum CodingKeys: String, CodingKey { - case ok - case descriptors - } -} - -public struct DevicePairListParams: Codable, Sendable {} - -public struct DevicePairApproveParams: Codable, Sendable { - public let requestid: String - - public init( - requestid: String) - { - self.requestid = requestid - } - - private enum CodingKeys: String, CodingKey { - case requestid = "requestId" - } -} - -public struct DevicePairRejectParams: Codable, Sendable { - public let requestid: String - - public init( - requestid: String) - { - self.requestid = requestid - } - - private enum CodingKeys: String, CodingKey { - case requestid = "requestId" - } -} - -public struct DevicePairRemoveParams: Codable, Sendable { - public let deviceid: String - - public init( - deviceid: String) - { - self.deviceid = deviceid - } - - private enum CodingKeys: String, CodingKey { - case deviceid = "deviceId" - } -} - -public struct DeviceTokenRotateParams: Codable, Sendable { - public let deviceid: String - public let role: String - public let scopes: [String]? - - public init( - deviceid: String, - role: String, - scopes: [String]?) - { - self.deviceid = deviceid - self.role = role - self.scopes = scopes - } - - private enum CodingKeys: String, CodingKey { - case deviceid = "deviceId" - case role - case scopes - } -} - -public struct DeviceTokenRevokeParams: Codable, Sendable { - public let deviceid: String - public let role: String - - public init( - deviceid: String, - role: String) - { - self.deviceid = deviceid - self.role = role - } - - private enum CodingKeys: String, CodingKey { - case deviceid = "deviceId" - case role - } -} - -public struct DevicePairRequestedEvent: Codable, Sendable { - public let requestid: String - public let deviceid: String - public let publickey: String - public let displayname: String? - public let platform: String? - public let devicefamily: String? - public let clientid: String? - public let clientmode: String? - public let role: String? - public let roles: [String]? - public let scopes: [String]? - public let remoteip: String? - public let silent: Bool? - public let isrepair: Bool? - public let ts: Int - - public init( - requestid: String, - deviceid: String, - publickey: String, - displayname: String?, - platform: String?, - devicefamily: String?, - clientid: String?, - clientmode: String?, - role: String?, - roles: [String]?, - scopes: [String]?, - remoteip: String?, - silent: Bool?, - isrepair: Bool?, - ts: Int) - { - self.requestid = requestid - self.deviceid = deviceid - self.publickey = publickey - self.displayname = displayname - self.platform = platform - self.devicefamily = devicefamily - self.clientid = clientid - self.clientmode = clientmode - self.role = role - self.roles = roles - self.scopes = scopes - self.remoteip = remoteip - self.silent = silent - self.isrepair = isrepair - self.ts = ts - } - - private enum CodingKeys: String, CodingKey { - case requestid = "requestId" - case deviceid = "deviceId" - case publickey = "publicKey" - case displayname = "displayName" - case platform - case devicefamily = "deviceFamily" - case clientid = "clientId" - case clientmode = "clientMode" - case role - case roles - case scopes - case remoteip = "remoteIp" - case silent - case isrepair = "isRepair" - case ts - } -} - -public struct DevicePairResolvedEvent: Codable, Sendable { - public let requestid: String - public let deviceid: String - public let decision: String - public let ts: Int - - public init( - requestid: String, - deviceid: String, - decision: String, - ts: Int) - { - self.requestid = requestid - self.deviceid = deviceid - self.decision = decision - self.ts = ts - } - - private enum CodingKeys: String, CodingKey { - case requestid = "requestId" - case deviceid = "deviceId" - case decision - case ts - } -} - -public struct ChatHistoryParams: Codable, Sendable { - public let sessionkey: String - public let limit: Int? - public let maxchars: Int? - - public init( - sessionkey: String, - limit: Int?, - maxchars: Int?) - { - self.sessionkey = sessionkey - self.limit = limit - self.maxchars = maxchars - } - - private enum CodingKeys: String, CodingKey { - case sessionkey = "sessionKey" - case limit - case maxchars = "maxChars" - } -} - -public struct ChatSendParams: Codable, Sendable { - public let sessionkey: String - public let sessionid: String? - public let message: String - public let thinking: String? - public let deliver: Bool? - public let originatingchannel: String? - public let originatingto: String? - public let originatingaccountid: String? - public let originatingthreadid: String? - public let attachments: [AnyCodable]? - public let timeoutms: Int? - public let systeminputprovenance: [String: AnyCodable]? - public let systemprovenancereceipt: String? - public let idempotencykey: String - - public init( - sessionkey: String, - sessionid: String?, - message: String, - thinking: String?, - deliver: Bool?, - originatingchannel: String?, - originatingto: String?, - originatingaccountid: String?, - originatingthreadid: String?, - attachments: [AnyCodable]?, - timeoutms: Int?, - systeminputprovenance: [String: AnyCodable]?, - systemprovenancereceipt: String?, - idempotencykey: String) - { - self.sessionkey = sessionkey - self.sessionid = sessionid - self.message = message - self.thinking = thinking - self.deliver = deliver - self.originatingchannel = originatingchannel - self.originatingto = originatingto - self.originatingaccountid = originatingaccountid - self.originatingthreadid = originatingthreadid - self.attachments = attachments - self.timeoutms = timeoutms - self.systeminputprovenance = systeminputprovenance - self.systemprovenancereceipt = systemprovenancereceipt - self.idempotencykey = idempotencykey - } - - private enum CodingKeys: String, CodingKey { - case sessionkey = "sessionKey" - case sessionid = "sessionId" - case message - case thinking - case deliver - case originatingchannel = "originatingChannel" - case originatingto = "originatingTo" - case originatingaccountid = "originatingAccountId" - case originatingthreadid = "originatingThreadId" - case attachments - case timeoutms = "timeoutMs" - case systeminputprovenance = "systemInputProvenance" - case systemprovenancereceipt = "systemProvenanceReceipt" - case idempotencykey = "idempotencyKey" - } -} - -public struct ChatAbortParams: Codable, Sendable { - public let sessionkey: String - public let runid: String? - - public init( - sessionkey: String, - runid: String?) - { - self.sessionkey = sessionkey - self.runid = runid - } - - private enum CodingKeys: String, CodingKey { - case sessionkey = "sessionKey" - case runid = "runId" - } -} - -public struct ChatInjectParams: Codable, Sendable { - public let sessionkey: String - public let message: String - public let label: String? - - public init( - sessionkey: String, - message: String, - label: String?) - { - self.sessionkey = sessionkey - self.message = message - self.label = label - } - - private enum CodingKeys: String, CodingKey { - case sessionkey = "sessionKey" - case message - case label - } -} - -public struct ChatEvent: Codable, Sendable { - public let runid: String - public let sessionkey: String - public let spawnedby: String? - public let seq: Int - public let state: AnyCodable - public let message: AnyCodable? - public let errormessage: String? - public let errorkind: AnyCodable? - public let usage: AnyCodable? - public let stopreason: String? - - public init( - runid: String, - sessionkey: String, - spawnedby: String?, - seq: Int, - state: AnyCodable, - message: AnyCodable?, - errormessage: String?, - errorkind: AnyCodable?, - usage: AnyCodable?, - stopreason: String?) - { - self.runid = runid - self.sessionkey = sessionkey - self.spawnedby = spawnedby - self.seq = seq - self.state = state - self.message = message - self.errormessage = errormessage - self.errorkind = errorkind - self.usage = usage - self.stopreason = stopreason - } - - private enum CodingKeys: String, CodingKey { - case runid = "runId" - case sessionkey = "sessionKey" - case spawnedby = "spawnedBy" - case seq - case state - case message - case errormessage = "errorMessage" - case errorkind = "errorKind" - case usage - case stopreason = "stopReason" - } -} - -public struct UpdateStatusParams: Codable, Sendable {} - -public struct UpdateRunParams: Codable, Sendable { - public let sessionkey: String? - public let deliverycontext: [String: AnyCodable]? - public let note: String? - public let continuationmessage: String? - public let restartdelayms: Int? - public let timeoutms: Int? - - public init( - sessionkey: String?, - deliverycontext: [String: AnyCodable]?, - note: String?, - continuationmessage: String?, - restartdelayms: Int?, - timeoutms: Int?) - { - self.sessionkey = sessionkey - self.deliverycontext = deliverycontext - self.note = note - self.continuationmessage = continuationmessage - self.restartdelayms = restartdelayms - self.timeoutms = timeoutms - } - - private enum CodingKeys: String, CodingKey { - case sessionkey = "sessionKey" - case deliverycontext = "deliveryContext" - case note - case continuationmessage = "continuationMessage" - case restartdelayms = "restartDelayMs" - case timeoutms = "timeoutMs" - } -} - -public struct TickEvent: Codable, Sendable { - public let ts: Int - - public init( - ts: Int) - { - self.ts = ts - } - - private enum CodingKeys: String, CodingKey { - case ts - } -} - -public struct ShutdownEvent: Codable, Sendable { - public let reason: String - public let restartexpectedms: Int? - - public init( - reason: String, - restartexpectedms: Int?) - { - self.reason = reason - self.restartexpectedms = restartexpectedms - } - - private enum CodingKeys: String, CodingKey { - case reason - case restartexpectedms = "restartExpectedMs" - } -} - -public enum GatewayFrame: Codable, Sendable { - case req(RequestFrame) - case res(ResponseFrame) - case event(EventFrame) - case unknown(type: String, raw: [String: AnyCodable]) - - private enum CodingKeys: String, CodingKey { - case type - } - - public init(from decoder: Decoder) throws { - let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - let type = try typeContainer.decode(String.self, forKey: .type) - switch type { - case "req": - self = try .req(RequestFrame(from: decoder)) - case "res": - self = try .res(ResponseFrame(from: decoder)) - case "event": - self = try .event(EventFrame(from: decoder)) - default: - let container = try decoder.singleValueContainer() - let raw = try container.decode([String: AnyCodable].self) - self = .unknown(type: type, raw: raw) - } - } - - public func encode(to encoder: Encoder) throws { - switch self { - case let .req(v): - try v.encode(to: encoder) - case let .res(v): - try v.encode(to: encoder) - case let .event(v): - try v.encode(to: encoder) - case let .unknown(_, raw): - var container = encoder.singleValueContainer() - try container.encode(raw) - } - } -} diff --git a/apps/macos/Tests/OpenClawIPCTests/MacGatewayChatTransportMappingTests.swift b/apps/macos/Tests/OpenClawIPCTests/MacGatewayChatTransportMappingTests.swift index 3f6254fcf30..bf8d4d40a13 100644 --- a/apps/macos/Tests/OpenClawIPCTests/MacGatewayChatTransportMappingTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/MacGatewayChatTransportMappingTests.swift @@ -22,7 +22,7 @@ struct MacGatewayChatTransportMappingTests { server: [:], features: [:], snapshot: snapshot, - canvashosturl: nil, + pluginsurfaceurls: nil, auth: [:], policy: [:]) diff --git a/apps/macos/Tests/OpenClawIPCTests/MacNodeRuntimeTests.swift b/apps/macos/Tests/OpenClawIPCTests/MacNodeRuntimeTests.swift index d6eae4d866f..f24e288b2a7 100644 --- a/apps/macos/Tests/OpenClawIPCTests/MacNodeRuntimeTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/MacNodeRuntimeTests.swift @@ -5,6 +5,15 @@ import Testing @testable import OpenClaw struct MacNodeRuntimeTests { + actor CanvasRefreshProbe { + private(set) var calls = 0 + + func refresh() -> String? { + self.calls += 1 + return "http://127.0.0.1:18789/refreshed" + } + } + @Test func `handle invoke rejects unknown command`() async { let runtime = MacNodeRuntime() let response = await runtime.handleInvoke( @@ -12,6 +21,21 @@ struct MacNodeRuntimeTests { #expect(response.ok == false) } + @Test func `A2UI host capability refresh uses injected node session refresher`() async { + let probe = CanvasRefreshProbe() + let runtime = MacNodeRuntime( + canvasSurfaceUrl: { "http://127.0.0.1:18789/current" }, + refreshCanvasSurfaceUrl: { await probe.refresh() }) + + let current = await runtime.resolveA2UIHostUrlWithCapabilityRefresh() + #expect(current == "http://127.0.0.1:18789/current/__openclaw__/a2ui/?platform=macos") + #expect(await probe.calls == 0) + + let refreshed = await runtime.resolveA2UIHostUrlWithCapabilityRefresh(forceRefresh: true) + #expect(refreshed == "http://127.0.0.1:18789/refreshed/__openclaw__/a2ui/?platform=macos") + #expect(await probe.calls == 1) + } + @Test func `handle invoke rejects empty system run`() async throws { let runtime = MacNodeRuntime() let params = OpenClawSystemRunParams(command: []) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/BridgeFrames.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/BridgeFrames.swift index 648b257bbb4..debcec3ae87 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/BridgeFrames.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/BridgeFrames.swift @@ -105,18 +105,15 @@ public struct BridgeHello: Codable, Sendable { public struct BridgeHelloOk: Codable, Sendable { public let type: String public let serverName: String - public let canvasHostUrl: String? public let mainSessionKey: String? public init( type: String = "hello-ok", serverName: String, - canvasHostUrl: String? = nil, mainSessionKey: String? = nil) { self.type = type self.serverName = serverName - self.canvasHostUrl = canvasHostUrl self.mainSessionKey = mainSessionKey } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift index 58d437ce1bf..4e497ae2039 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift @@ -11,19 +11,6 @@ private struct NodeInvokeRequestPayload: Codable { var idempotencyKey: String? } -private func replaceCanvasCapabilityInScopedHostUrl(scopedUrl: String, capability: String) -> String? { - let marker = "/__openclaw__/cap/" - guard let markerRange = scopedUrl.range(of: marker) else { return nil } - let capabilityStart = markerRange.upperBound - let suffix = scopedUrl[capabilityStart...] - let nextSlash = suffix.firstIndex(of: "/") - let nextQuery = suffix.firstIndex(of: "?") - let nextFragment = suffix.firstIndex(of: "#") - let capabilityEnd = [nextSlash, nextQuery, nextFragment].compactMap(\.self).min() ?? scopedUrl.endIndex - guard capabilityStart < capabilityEnd else { return nil } - return String(scopedUrl[.. String? { let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard !trimmed.isEmpty else { return nil } @@ -152,7 +139,11 @@ public actor GatewayNodeSession { } private var serverEventSubscribers: [UUID: AsyncStream.Continuation] = [:] - private var canvasHostUrl: String? + private var pluginSurfaceUrls: [String: String] = [:] + + private struct PluginSurfaceRefreshResponse: Decodable { + let pluginSurfaceUrls: [String: AnyCodable]? + } public init() {} @@ -270,47 +261,26 @@ public actor GatewayNodeSession { } public func currentCanvasHostUrl() -> String? { - self.canvasHostUrl + self.pluginSurfaceUrls["canvas"] } - public func refreshNodeCanvasCapability(timeoutMs: Int = 8000) async -> Bool { - guard let channel = self.channel else { return false } - do { - let data = try await channel.request( - method: "node.canvas.capability.refresh", - params: [:], - timeoutMs: Double(max(timeoutMs, 1))) - guard - let payload = try JSONSerialization.jsonObject(with: data) as? [String: Any], - let rawCapability = payload["canvasCapability"] as? String - else { - self.logger.warning("node.canvas.capability.refresh missing canvasCapability") - return false - } - let capability = rawCapability.trimmingCharacters(in: .whitespacesAndNewlines) - guard !capability.isEmpty else { - self.logger.warning("node.canvas.capability.refresh returned empty capability") - return false - } - let scopedUrl = self.canvasHostUrl?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - guard !scopedUrl.isEmpty else { - self.logger.warning("node.canvas.capability.refresh missing local canvasHostUrl") - return false - } - guard let refreshed = replaceCanvasCapabilityInScopedHostUrl( - scopedUrl: scopedUrl, - capability: capability) - else { - self.logger.warning("node.canvas.capability.refresh could not rewrite scoped canvas URL") - return false - } - self.canvasHostUrl = refreshed - return true - } catch { - self.logger.warning( - "node.canvas.capability.refresh failed: \(error.localizedDescription, privacy: .public)") - return false - } + @discardableResult + public func refreshPluginSurfaceUrl(surface: String, timeoutSeconds: Int = 8) async -> String? { + guard let channel = self.channel else { return nil } + let trimmedSurface = surface.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedSurface.isEmpty else { return nil } + + return await self.requestPluginSurfaceRefresh( + channel: channel, + method: "node.pluginSurface.refresh", + params: ["surface": AnyCodable(trimmedSurface)], + surface: trimmedSurface, + timeoutSeconds: timeoutSeconds) + } + + @discardableResult + public func refreshCanvasHostUrl(timeoutSeconds: Int = 8) async -> String? { + await self.refreshPluginSurfaceUrl(surface: "canvas", timeoutSeconds: timeoutSeconds) } public func currentRemoteAddress() -> String? { @@ -364,8 +334,7 @@ public actor GatewayNodeSession { private func handlePush(_ push: GatewayPush) async { switch push { case let .snapshot(ok): - let raw = ok.canvashosturl?.trimmingCharacters(in: .whitespacesAndNewlines) - self.canvasHostUrl = self.normalizeCanvasHostUrl(raw) + self.pluginSurfaceUrls = self.normalizePluginSurfaceUrls(ok.pluginsurfaceurls) if self.hasEverConnected { self.broadcastServerEvent( EventFrame(type: "event", event: "seqGap", payload: nil, seq: nil, stateversion: nil)) @@ -436,6 +405,39 @@ public actor GatewayNodeSession { canonicalizeCanvasHostUrl(raw: raw, activeURL: self.activeURL) } + private func normalizePluginSurfaceUrls(_ raw: [String: AnyCodable]?) -> [String: String] { + var normalized: [String: String] = [:] + if let raw { + normalized = raw.compactMapValues { value in + self.normalizeCanvasHostUrl(value.value as? String) + } + } + return normalized + } + + private func requestPluginSurfaceRefresh( + channel: GatewayChannelActor, + method: String, + params: [String: AnyCodable]?, + surface: String, + timeoutSeconds: Int) async -> String? + { + do { + let data = try await channel.request( + method: method, + params: params, + timeoutMs: Double(timeoutSeconds * 1000)) + let decoded = try self.decoder.decode(PluginSurfaceRefreshResponse.self, from: data) + let urls = self.normalizePluginSurfaceUrls(decoded.pluginSurfaceUrls) + guard let refreshed = urls[surface] else { return nil } + self.pluginSurfaceUrls[surface] = refreshed + return refreshed + } catch { + self.logger.debug("\(method, privacy: .public) failed: \(error.localizedDescription, privacy: .public)") + return nil + } + } + private func handleEvent(_ evt: EventFrame) async { self.broadcastServerEvent(evt) guard evt.event == "node.invoke.request" else { return } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index ccef9eb5dd0..d8081b2d68a 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -2,7 +2,7 @@ // swiftlint:disable file_length import Foundation -public let GATEWAY_PROTOCOL_VERSION = 3 +public let GATEWAY_PROTOCOL_VERSION = 4 public enum ErrorCode: String, Codable, Sendable { case notLinked = "NOT_LINKED" @@ -98,7 +98,7 @@ public struct HelloOk: Codable, Sendable { public let server: [String: AnyCodable] public let features: [String: AnyCodable] public let snapshot: Snapshot - public let canvashosturl: String? + public let pluginsurfaceurls: [String: AnyCodable]? public let auth: [String: AnyCodable] public let policy: [String: AnyCodable] @@ -108,7 +108,7 @@ public struct HelloOk: Codable, Sendable { server: [String: AnyCodable], features: [String: AnyCodable], snapshot: Snapshot, - canvashosturl: String?, + pluginsurfaceurls: [String: AnyCodable]?, auth: [String: AnyCodable], policy: [String: AnyCodable]) { @@ -117,7 +117,7 @@ public struct HelloOk: Codable, Sendable { self.server = server self.features = features self.snapshot = snapshot - self.canvashosturl = canvashosturl + self.pluginsurfaceurls = pluginsurfaceurls self.auth = auth self.policy = policy } @@ -128,7 +128,7 @@ public struct HelloOk: Codable, Sendable { case server case features case snapshot - case canvashosturl = "canvasHostUrl" + case pluginsurfaceurls = "pluginSurfaceUrls" case auth case policy } diff --git a/docs/automation/cron-jobs.md b/docs/automation/cron-jobs.md index 93917097880..b4a9459bdfe 100644 --- a/docs/automation/cron-jobs.md +++ b/docs/automation/cron-jobs.md @@ -134,8 +134,6 @@ This fires ~5–6 times per month instead of 0–1 times per month. OpenClaw use `--model` uses the selected allowed model as that job's primary model. It is not the same as a chat-session `/model` override: configured fallback chains still apply when the job primary fails. If the requested model is not allowed or cannot be resolved, cron fails the run with an explicit validation error instead of silently falling back to the job's agent/default model selection. -If older or hand-edited `jobs.json` entries store `payload.model` as `"default"`, `"null"`, a blank string, or JSON `null`, run `openclaw doctor --fix`. Doctor removes those invalid persisted override sentinels; runtime does not support them as fallback aliases. Omit the model field to use the normal agent/default model selection. - Cron jobs can also carry payload-level `fallbacks`. When present, that list replaces the configured fallback chain for the job. Use `fallbacks: []` in the job payload/API when you want a strict cron run that tries only the selected model. If a job has `--model` but neither payload nor configured fallbacks, OpenClaw passes an explicit empty fallback override so the agent primary is not appended as a hidden extra retry target. Model-selection precedence for isolated jobs is: diff --git a/docs/cli/cron.md b/docs/cli/cron.md index c3f9b22a677..f836b6520c1 100644 --- a/docs/cli/cron.md +++ b/docs/cli/cron.md @@ -157,8 +157,6 @@ Retention and pruning are controlled in config: If you have cron jobs from before the current delivery and store format, run `openclaw doctor --fix`. Doctor normalizes legacy cron fields (`jobId`, `schedule.cron`, top-level delivery fields including legacy `threadId`, payload `provider` delivery aliases) and migrates simple `notify: true` webhook fallback jobs to explicit webhook delivery when `cron.webhook` is configured. - -Doctor also removes persisted cron `payload.model` sentinels such as `"default"`, `"null"`, blank strings, and JSON `null`. Cron runtime still treats any non-empty `payload.model` string as an explicit model override and validates it against `agents.defaults.models`; omit the model key when a job should use the agent/default model selection. ## Common edits diff --git a/docs/cli/nodes.md b/docs/cli/nodes.md index d225d37bac1..1567ceb7dd0 100644 --- a/docs/cli/nodes.md +++ b/docs/cli/nodes.md @@ -68,7 +68,7 @@ Invoke flags: For shell execution on a node, use the `exec` tool with `host=node` instead of `openclaw nodes run`. The `nodes` CLI is now capability-focused: direct RPC via `nodes invoke`, plus pairing, camera, -screen, location, canvas, and notifications. +screen, location, Canvas, and notifications. Canvas commands are implemented by the bundled experimental Canvas plugin; core keeps a compatibility hook so they remain under `openclaw nodes canvas`. ## Related diff --git a/docs/cli/plugins.md b/docs/cli/plugins.md index 6457fbbe0bf..217eaa34e22 100644 --- a/docs/cli/plugins.md +++ b/docs/cli/plugins.md @@ -139,7 +139,7 @@ is available, then fall back to `latest`. Use `npm:` when you want to make npm resolution explicit. Bare package specs also install directly from npm during the launch cutover. - Bare specs and `@latest` stay on the stable track. Legacy OpenClaw correction versions such as `2026.5.3-1` are still treated as stable releases for this check so older packages keep updating safely. New monthly support-line work is planned to use normal SemVer patch numbers instead of hyphen correction suffixes. If npm resolves a default-line spec to a prerelease, OpenClaw stops and asks you to opt in explicitly with a prerelease tag such as `@beta`/`@rc` or an exact prerelease version such as `@1.2.3-beta.4`. + Bare specs and `@latest` stay on the stable track. OpenClaw date-stamped correction versions such as `2026.5.3-1` are stable releases for this check. If npm resolves either of those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a prerelease tag such as `@beta`/`@rc` or an exact prerelease version such as `@1.2.3-beta.4`. If a bare install spec matches an official plugin id (for example `diffs`), OpenClaw installs the catalog entry directly. To install an npm package with the same name, use an explicit scoped spec (for example `@scope/diffs`). @@ -337,8 +337,6 @@ Updates apply to tracked plugin installs in the managed plugin index and tracked `openclaw plugins update` reuses the tracked plugin spec unless you pass a new spec. `openclaw update` additionally knows the active OpenClaw update channel: on the beta channel, default-line npm and ClawHub plugin records try `@beta` first, then fall back to the recorded default/latest spec if no plugin beta release exists. Exact versions and explicit tags stay pinned to that selector. - OpenClaw does not yet expose LTS or monthly support plugin channels. Planned support-line work will need plugin package and ClawHub tags to follow the same support line as the core package. - Before a live npm update, OpenClaw checks the installed package version against the npm registry metadata. If the installed version and recorded artifact identity already match the resolved target, the update is skipped without downloading, reinstalling, or rewriting `openclaw.json`. @@ -361,7 +359,7 @@ openclaw plugins inspect --json Inspect shows identity, load status, source, manifest capabilities, policy flags, diagnostics, install metadata, bundle capabilities, and any detected MCP or LSP server support without importing plugin runtime by default. Add `--runtime` to load the plugin module and include registered hooks, tools, commands, services, gateway methods, and HTTP routes. Runtime inspection reports missing plugin dependencies directly; installs and repairs stay in `openclaw plugins install`, `openclaw plugins update`, and `openclaw doctor --fix`. -Plugin-owned CLI commands are installed as root `openclaw` command groups. After `inspect --runtime` shows a command under `cliCommands`, run it as `openclaw ...`; for example a plugin that registers `demo-git` can be verified with `openclaw demo-git ping`. +Plugin-owned CLI commands are usually installed as root `openclaw` command groups, but plugins may also register nested commands under a core parent such as `openclaw nodes`. After `inspect --runtime` shows a command under `cliCommands`, run it at the listed path; for example a plugin that registers `demo-git` can be verified with `openclaw demo-git ping`. Each plugin is classified by what it actually registers at runtime: diff --git a/docs/cli/update.md b/docs/cli/update.md index dbe7e4afbc9..47780f80cb2 100644 --- a/docs/cli/update.md +++ b/docs/cli/update.md @@ -96,11 +96,6 @@ install method aligned: - `beta` → prefers npm dist-tag `beta`, but falls back to `latest` when beta is missing or older than the current stable release. -OpenClaw does not yet have an LTS or monthly support channel. We are working -toward monthly support lines, but `--channel` currently accepts only -`stable`, `beta`, and `dev`. Use `--tag ` for a one-off -target when you need a specific package artifact. - The Gateway core auto-updater (when enabled via config) launches the CLI update path outside the live Gateway request handler. Control-plane `update.run` package-manager updates force a non-deferred, no-cooldown update restart after the package swap, diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index b914f4a6778..921103289dd 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -137,10 +137,9 @@ collaboration-mode instructions inside the Codex runtime after OpenClaw sends thread and turn params. Regenerate them with `pnpm prompt:snapshots:gen` and verify drift with -`pnpm prompt:snapshots:check`. CI runs the drift check as a dedicated -additional check for manual CI and prompt-affecting changes so prompt changes -and snapshot updates stay attached to the same PR without slowing unrelated -boundary shards. +`pnpm prompt:snapshots:check`. CI runs the drift check in the additional +boundary shard so prompt changes and snapshot updates stay attached to the same +PR. ## Workspace bootstrap injection diff --git a/docs/concepts/typebox.md b/docs/concepts/typebox.md index 3878fac1bcc..df6734bebc5 100644 --- a/docs/concepts/typebox.md +++ b/docs/concepts/typebox.md @@ -94,8 +94,8 @@ Connect (first message): "id": "c1", "method": "connect", "params": { - "minProtocol": 3, - "maxProtocol": 3, + "minProtocol": 4, + "maxProtocol": 4, "client": { "id": "openclaw-macos", "displayName": "macos", @@ -117,7 +117,7 @@ Hello-ok response: "ok": true, "payload": { "type": "hello-ok", - "protocol": 3, + "protocol": 4, "server": { "version": "dev", "connId": "ws-1" }, "features": { "methods": ["health"], "events": ["tick"] }, "snapshot": { @@ -163,8 +163,8 @@ ws.on("open", () => { id: "c1", method: "connect", params: { - minProtocol: 3, - maxProtocol: 3, + minProtocol: 4, + maxProtocol: 4, client: { id: "cli", displayName: "example", @@ -272,7 +272,7 @@ Unknown frame types are preserved as raw payloads for forward compatibility. ## Versioning + compatibility -- `PROTOCOL_VERSION` lives in `src/gateway/protocol/schema.ts`. +- `PROTOCOL_VERSION` lives in `src/gateway/protocol/version.ts`. - Clients send `minProtocol` + `maxProtocol`; the server rejects mismatches. - The Swift models keep unknown frame types to avoid breaking older clients. diff --git a/docs/gateway/bridge-protocol.md b/docs/gateway/bridge-protocol.md index 025303bd73f..077dd24d265 100644 --- a/docs/gateway/bridge-protocol.md +++ b/docs/gateway/bridge-protocol.md @@ -40,8 +40,10 @@ authoritative pin without explicit user intent or other out-of-band verification 3. Client sends `pair-request`. 4. Gateway waits for approval, then sends `pair-ok` and `hello-ok`. -Historically, `hello-ok` returned `serverName` and could include -`canvasHostUrl`. +Historically, `hello-ok` returned `serverName`; hosted plugin surfaces are now +advertised through `pluginSurfaceUrls`. Canvas/A2UI uses +`pluginSurfaceUrls.canvas`; the deprecated `canvasHostUrl` alias is not part of +the refactored protocol. ## Frames diff --git a/docs/gateway/configuration-examples.md b/docs/gateway/configuration-examples.md index 98bbccd911a..e81c96a2834 100644 --- a/docs/gateway/configuration-examples.md +++ b/docs/gateway/configuration-examples.md @@ -654,7 +654,7 @@ Only enable direct mutable name/email/nick matching with each channel's `dangero - If you set `dmPolicy: "open"`, the matching `allowFrom` list must include `"*"`. - Provider IDs differ (phone numbers, user IDs, channel IDs). Use the provider docs to confirm the format. -- Optional sections to add later: `web`, `browser`, `ui`, `discovery`, `canvasHost`, `talk`, `signal`, `imessage`. +- Optional sections to add later: `web`, `browser`, `ui`, `discovery`, `plugins`, `talk`, `signal`, `imessage`. - See [Providers](/providers) and [Troubleshooting](/gateway/troubleshooting) for deeper setup notes. ## Related diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 772f25f336d..3eada9e6c54 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -651,14 +651,22 @@ Validation and safety notes: --- -## Canvas host +## Canvas plugin host ```json5 { - canvasHost: { - root: "~/.openclaw/workspace/canvas", - liveReload: true, - // enabled: false, // or OPENCLAW_SKIP_CANVAS_HOST=1 + plugins: { + entries: { + canvas: { + config: { + host: { + root: "~/.openclaw/workspace/canvas", + liveReload: true, + // enabled: false, // or OPENCLAW_SKIP_CANVAS_HOST=1 + }, + }, + }, + }, }, } ``` diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 51f4b260238..9b9ecc7c3b8 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -575,7 +575,7 @@ Most fields hot-apply without downtime. In `hybrid` mode, restart-required chang | Tools & media | `tools`, `browser`, `skills`, `mcp`, `audio`, `talk` | No | | UI & misc | `ui`, `logging`, `identity`, `bindings` | No | | Gateway server | `gateway.*` (port, bind, auth, tailscale, TLS, HTTP) | **Yes** | -| Infrastructure | `discovery`, `canvasHost`, `plugins` | **Yes** | +| Infrastructure | `discovery`, `plugins` | **Yes** | `gateway.reload` and `gateway.remote` are exceptions - changing them does **not** trigger a restart. diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index 75b1d7e1d3b..c43e5fd5651 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -310,7 +310,6 @@ That stages grounded durable candidates into the short-term dreaming store while - top-level payload fields (`message`, `model`, `thinking`, ...) → `payload` - top-level delivery fields (`deliver`, `channel`, `to`, `provider`, ...) → `delivery` - payload `provider` delivery aliases → explicit `delivery.channel` - - invalid persisted cron `payload.model` sentinels (`"default"`, `"null"`, blank strings, JSON `null`) → removed model override - simple legacy `notify: true` webhook fallback jobs → explicit `delivery.mode="webhook"` with `delivery.to=cron.webhook` Doctor only auto-migrates `notify: true` jobs when it can do so without changing behavior. If a job combines legacy notify fallback with an existing non-webhook delivery mode, doctor warns and leaves that job for manual review. diff --git a/docs/gateway/protocol.md b/docs/gateway/protocol.md index 342d3e4bb4f..e1d67c51d19 100644 --- a/docs/gateway/protocol.md +++ b/docs/gateway/protocol.md @@ -44,8 +44,8 @@ Client → Gateway: "id": "…", "method": "connect", "params": { - "minProtocol": 3, - "maxProtocol": 3, + "minProtocol": 4, + "maxProtocol": 4, "client": { "id": "cli", "version": "1.2.3", @@ -80,7 +80,7 @@ Gateway → Client: "ok": true, "payload": { "type": "hello-ok", - "protocol": 3, + "protocol": 4, "server": { "version": "…", "connId": "…" }, "features": { "methods": ["…"], "events": ["…"] }, "snapshot": { "…": "…" }, @@ -105,7 +105,15 @@ handshake failure. `server`, `features`, `snapshot`, and `policy` are all required by the schema (`src/gateway/protocol/schema/frames.ts`). `auth` is also required and reports -the negotiated role/scopes. `canvasHostUrl` is optional. +the negotiated role/scopes. `pluginSurfaceUrls` is optional and maps plugin +surface names, such as `canvas`, to scoped hosted URLs. + +Scoped plugin surface URLs may expire. Nodes can call +`node.pluginSurface.refresh` with `{ "surface": "canvas" }` to receive a fresh +entry in `pluginSurfaceUrls`. The experimental Canvas plugin refactor does not +support the deprecated `canvasHostUrl`, `canvasCapability`, or +`node.canvas.capability.refresh` compatibility path; current native clients and +gateways must use plugin surfaces. When no device token is issued, `hello-ok.auth` reports the negotiated permissions without token fields: @@ -174,8 +182,8 @@ roles still need scopes under their own role prefix. "id": "…", "method": "connect", "params": { - "minProtocol": 3, - "maxProtocol": 3, + "minProtocol": 4, + "maxProtocol": 4, "client": { "id": "ios-node", "version": "1.2.3", @@ -443,7 +451,6 @@ enumeration of `src/gateway/server-methods/*.ts`. - `node.invoke` forwards a command to a connected node. - `node.invoke.result` returns the result for an invoke request. - `node.event` carries node-originated events back into the gateway. - - `node.canvas.capability.refresh` refreshes scoped canvas-capability tokens. - `node.pending.pull` and `node.pending.ack` are the connected-node queue APIs. - `node.pending.enqueue` and `node.pending.drain` manage durable pending work for offline/disconnected nodes. @@ -572,7 +579,7 @@ enumeration of `src/gateway/server-methods/*.ts`. ## Versioning -- `PROTOCOL_VERSION` lives in `src/gateway/protocol/schema/protocol-schemas.ts`. +- `PROTOCOL_VERSION` lives in `src/gateway/protocol/version.ts`. - Clients send `minProtocol` + `maxProtocol`; the server rejects mismatches. - Schemas + models are generated from TypeBox definitions: - `pnpm protocol:gen` @@ -582,11 +589,11 @@ enumeration of `src/gateway/server-methods/*.ts`. ### Client constants The reference client in `src/gateway/client.ts` uses these defaults. Values are -stable across protocol v3 and are the expected baseline for third-party clients. +stable across protocol v4 and are the expected baseline for third-party clients. | Constant | Default | Source | | ----------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ | -| `PROTOCOL_VERSION` | `3` | `src/gateway/protocol/schema/protocol-schemas.ts` | +| `PROTOCOL_VERSION` | `4` | `src/gateway/protocol/version.ts` | | Request timeout (per RPC) | `30_000` ms | `src/gateway/client.ts` (`requestTimeoutMs`) | | Preauth / connect-challenge timeout | `15_000` ms | `src/gateway/handshake-timeouts.ts` (config/env can raise the paired server/client budget) | | Initial reconnect backoff | `1_000` ms | `src/gateway/client.ts` (`backoffMs`) | diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index f762e44ae62..55cb0ba1ffa 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -126,65 +126,6 @@ Use this as the quick model when triaging risk: | Node pairing and node commands | Operator-level remote execution on paired devices | "Remote device control should be treated as untrusted user access by default" | | `gateway.nodes.pairing.autoApproveCidrs` | Opt-in trusted-network node enrollment policy | "A disabled-by-default allowlist is an automatic pairing vulnerability" | -## Multi-agent and sub-agent boundaries - -OpenClaw can run many agents inside one Gateway, but those agents still sit -inside the same trusted-operator boundary unless you split the deployment by -Gateway, OS user, host, or sandbox. Treat sub-agent delegation as a tool-policy -and sandboxing decision, not as a hostile multi-tenant authorization layer. - -Expected behavior inside one trusted Gateway: - -- An authenticated operator can route work to sessions and agents they are - allowed to use by config. -- `sessionKey`, session id, labels, and sub-agent session keys select - conversation context. They are not bearer credentials and are not per-user - authorization boundaries. -- Sub-agents have separate sessions by default. Native `sessions_spawn` uses - isolated context unless the caller explicitly asks for `context: "fork"`; - thread-bound follow-up sessions use forked context because they continue the - conversation thread. -- A forked sub-agent can see the transcript context it was deliberately given. - That is expected. It becomes a security issue only if it receives context that - policy said it must not receive. -- Tool access comes from the effective profile, channel/group/provider policy, - sandbox policy, per-agent policy, and the sub-agent restriction layer. A broad - tool profile intentionally gives broad capability. -- Sub-agent auth profiles are resolved by target agent id. Main-agent auth can - be available as fallback unless you split credentials/deployments; do not rely - on sub-agent identity alone for strong secret isolation. - -What counts as a real boundary bypass: - -- `sessions_spawn` works even though the effective tool policy denied it. -- A child runs unsandboxed even though the requester is sandboxed or the call - required `sandbox: "require"`. -- A child receives session tools, system tools, or target-agent access that the - resolved config denied. -- A leaf sub-agent controls, kills, steers, or messages sibling sessions that it - did not spawn. -- A sub-agent sees transcript, memory, credentials, or files that were excluded - by an explicit policy or sandbox boundary. -- A Gateway/API caller without the required Gateway auth or trusted-proxy/device - identity can trigger agent or tool execution. - -Hardening knobs: - -- Keep `sessions_spawn` denied unless an agent truly needs delegation. -- Prefer `tools.profile: "messaging"` or another narrow profile for agents that - talk to external channels. -- Set `agents.list[].subagents.requireAgentId: true` for agents that may spawn - work, so target selection is explicit. -- Keep `agents.defaults.subagents.allowAgents` and - `agents.list[].subagents.allowAgents` narrow; avoid `["*"]` for agents that - receive untrusted input. -- Use `tools.subagents.tools.allow` to make sub-agent tools allow-only instead - of inheriting a broad parent profile. -- For workflows that must remain sandboxed, use `sessions_spawn` with - `sandbox: "require"`. -- Use separate gateways, OS users, hosts, browser profiles, and credentials when - agents or users are mutually untrusted. - ## Not vulnerabilities by design @@ -198,10 +139,6 @@ a real boundary bypass is demonstrated: - Claims that classify normal operator read-path access (for example `sessions.list` / `sessions.preview` / `chat.history`) as IDOR in a shared-gateway setup. -- Claims that treat expected `context: "fork"` transcript inheritance as a - boundary bypass when the requester explicitly forked that context. -- Claims that treat broad sub-agent tool access as a bypass when the configured - profile or allowlist intentionally granted those tools. - Localhost-only deployment findings (for example HSTS on a loopback-only gateway). - Discord inbound webhook signature findings for inbound paths that do not diff --git a/docs/help/faq.md b/docs/help/faq.md index 97ccea1412a..63f56c25f1a 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -988,7 +988,7 @@ lives on the [First-run FAQ](/help/faq-first-run). to the gateway (iOS/Android nodes, or macOS "node mode" in the menubar app). For headless node hosts and CLI control, see [Node host CLI](/cli/node). - A full restart is required for `gateway`, `discovery`, and `canvasHost` changes. + A full restart is required for `gateway`, `discovery`, and hosted plugin surface changes. diff --git a/docs/install/development-channels.md b/docs/install/development-channels.md index 026be7d3e37..05c8efccd3d 100644 --- a/docs/install/development-channels.md +++ b/docs/install/development-channels.md @@ -23,28 +23,6 @@ changing the version number. Maintainers can also publish a stable release directly to `latest` when needed. Dist-tags are the source of truth for npm installs. -## Planned monthly support lines - -OpenClaw does not yet ship an LTS or monthly support channel. We are working -toward SemVer-compatible monthly support lines so users can stay on a quieter -line while `latest` keeps moving quickly. - -The planned version shape is `YYYY.M.PATCH`: - -- `YYYY` is the year. -- `M` is the monthly release line, without a leading zero. -- `PATCH` increments within that monthly line and can grow past 100 if needed. - -Example future tags: - -- `v2026.6.0`, `v2026.6.1`, `v2026.6.2` for the June line. -- `v2026.6.3-beta.1` for a prerelease on the fast/latest train. -- A future support-line dist-tag such as `stable-2026-6` or `lts-2026-6` may - point at a monthly line, but no such channel is available today. - -Until that migration lands, the public update channels remain `stable`, `beta`, -and `dev`. - ## Switching channels ```bash @@ -134,12 +112,10 @@ source (config, git tag, git branch, or default). ## Tagging best practices -- Tag releases you want git checkouts to land on (`vYYYY.M.D` for current - stable releases, `vYYYY.M.D-beta.N` for current beta releases). +- Tag releases you want git checkouts to land on (`vYYYY.M.D` for stable, + `vYYYY.M.D-beta.N` for beta). - `vYYYY.M.D.beta.N` is also recognized for compatibility, but prefer `-beta.N`. -- Legacy `vYYYY.M.D-` tags are still recognized as stable (non-beta), - but the planned monthly support model will use normal patch numbers - (`vYYYY.M.PATCH`) instead of a hyphen correction suffix. +- Legacy `vYYYY.M.D-` tags are still recognized as stable (non-beta). - Keep tags immutable: never move or reuse a tag. - npm dist-tags remain the source of truth for npm installs: - `latest` -> stable diff --git a/docs/install/updating.md b/docs/install/updating.md index 39dd57c346e..c609c5c2bc0 100644 --- a/docs/install/updating.md +++ b/docs/install/updating.md @@ -35,10 +35,6 @@ installer has its own `--verbose` flag, but that flag is not part of the beta tag is missing or older than the latest stable release. Use `--tag beta` if you want the raw npm beta dist-tag for a one-off package update. -OpenClaw does not yet expose an LTS or monthly support update channel. We are -working toward SemVer-compatible monthly support lines, but today the supported -channels are still `stable`, `beta`, and `dev`. - See [Development channels](/install/development-channels) for channel semantics. ## Switch between npm and git installs diff --git a/docs/platforms/ios.md b/docs/platforms/ios.md index 2b8d01c86a3..d1dadfb1ede 100644 --- a/docs/platforms/ios.md +++ b/docs/platforms/ios.md @@ -272,7 +272,7 @@ openclaw nodes invoke --node "iOS Node" --command canvas.snapshot --params '{"ma ## Common errors - `NODE_BACKGROUND_UNAVAILABLE`: bring the iOS app to the foreground (canvas/camera/screen commands require it). -- `A2UI_HOST_NOT_CONFIGURED`: the Gateway did not advertise a canvas host URL; check `canvasHost` in [Gateway configuration](/gateway/configuration). +- `A2UI_HOST_NOT_CONFIGURED`: the Gateway did not advertise the Canvas plugin surface URL; check `plugins.entries.canvas.config.host` in [Gateway configuration](/gateway/configuration). - Pairing prompt never appears: run `openclaw devices list` and approve manually. - Reconnect fails after reinstall: the Keychain pairing token was cleared; re-pair the node. diff --git a/docs/plugins/plugin-inventory.md b/docs/plugins/plugin-inventory.md index db1490b66e6..1677c7f320b 100644 --- a/docs/plugins/plugin-inventory.md +++ b/docs/plugins/plugin-inventory.md @@ -60,6 +60,7 @@ uninstall, and publishing commands. | [bonjour](/plugins/reference/bonjour) | Advertise the local OpenClaw gateway over Bonjour/mDNS. | `@openclaw/bonjour`
included in OpenClaw | plugin | | [browser](/plugins/reference/browser) | Adds agent-callable tools. | `@openclaw/browser-plugin`
included in OpenClaw | contracts: tools; skills | | [byteplus](/plugins/reference/byteplus) | Adds BytePlus, BytePlus Plan model provider support to OpenClaw. | `@openclaw/byteplus-provider`
included in OpenClaw | providers: byteplus, byteplus-plan; contracts: videoGenerationProviders | +| [canvas](/plugins/reference/canvas) | Experimental Canvas control and A2UI rendering surfaces for paired nodes. | `@openclaw/canvas-plugin`
included in OpenClaw | contracts: tools | | [cerebras](/plugins/reference/cerebras) | Adds Cerebras model provider support to OpenClaw. | `@openclaw/cerebras-provider`
included in OpenClaw | providers: cerebras | | [chutes](/plugins/reference/chutes) | Adds Chutes model provider support to OpenClaw. | `@openclaw/chutes-provider`
included in OpenClaw | providers: chutes | | [cloudflare-ai-gateway](/plugins/reference/cloudflare-ai-gateway) | Adds Cloudflare AI Gateway model provider support to OpenClaw. | `@openclaw/cloudflare-ai-gateway-provider`
included in OpenClaw | providers: cloudflare-ai-gateway | diff --git a/docs/plugins/reference.md b/docs/plugins/reference.md index 250354fac5e..0f63719d65c 100644 --- a/docs/plugins/reference.md +++ b/docs/plugins/reference.md @@ -30,6 +30,7 @@ pnpm plugins:inventory:gen | [brave](/plugins/reference/brave) | Adds web search provider support. | `@openclaw/brave-plugin`
npm; ClawHub | contracts: webSearchProviders | | [browser](/plugins/reference/browser) | Adds agent-callable tools. | `@openclaw/browser-plugin`
included in OpenClaw | contracts: tools; skills | | [byteplus](/plugins/reference/byteplus) | Adds BytePlus, BytePlus Plan model provider support to OpenClaw. | `@openclaw/byteplus-provider`
included in OpenClaw | providers: byteplus, byteplus-plan; contracts: videoGenerationProviders | +| [canvas](/plugins/reference/canvas) | Experimental Canvas control and A2UI rendering surfaces for paired nodes. | `@openclaw/canvas-plugin`
included in OpenClaw | contracts: tools | | [cerebras](/plugins/reference/cerebras) | Adds Cerebras model provider support to OpenClaw. | `@openclaw/cerebras-provider`
included in OpenClaw | providers: cerebras | | [chutes](/plugins/reference/chutes) | Adds Chutes model provider support to OpenClaw. | `@openclaw/chutes-provider`
included in OpenClaw | providers: chutes | | [cloudflare-ai-gateway](/plugins/reference/cloudflare-ai-gateway) | Adds Cloudflare AI Gateway model provider support to OpenClaw. | `@openclaw/cloudflare-ai-gateway-provider`
included in OpenClaw | providers: cloudflare-ai-gateway | diff --git a/docs/plugins/reference/canvas.md b/docs/plugins/reference/canvas.md new file mode 100644 index 00000000000..1c1f490eee0 --- /dev/null +++ b/docs/plugins/reference/canvas.md @@ -0,0 +1,19 @@ +--- +summary: "Experimental Canvas control and A2UI rendering surfaces for paired nodes." +read_when: + - You are installing, configuring, or auditing the canvas plugin +title: "Canvas plugin" +--- + +# Canvas plugin + +Experimental Canvas control and A2UI rendering surfaces for paired nodes. + +## Distribution + +- Package: `@openclaw/canvas-plugin` +- Install route: included in OpenClaw + +## Surface + +contracts: tools diff --git a/docs/plugins/sdk-entrypoints.md b/docs/plugins/sdk-entrypoints.md index 69052d62097..568e72c32d3 100644 --- a/docs/plugins/sdk-entrypoints.md +++ b/docs/plugins/sdk-entrypoints.md @@ -140,8 +140,13 @@ export default defineChannelPluginEntry({ memoizes the resolved schema on first access. - For plugin-owned root CLI commands, prefer `api.registerCli(..., { descriptors: [...] })` when you want the command to stay lazy-loaded without disappearing from the - root CLI parse tree. For channel plugins, prefer registering those descriptors - from `registerCliMetadata(...)` and keep `registerFull(...)` focused on runtime-only work. + root CLI parse tree. For paired-node feature commands, prefer + `api.registerNodeCliFeature(...)` so the command lands under `openclaw nodes`. + For other nested plugin commands, add `parentPath` and register commands on + the `program` object passed to the registrar; OpenClaw resolves it to the + parent command before calling the plugin. For channel plugins, prefer + registering those descriptors from `registerCliMetadata(...)` and keep + `registerFull(...)` focused on runtime-only work. - If `registerFull(...)` also registers gateway RPC methods, keep them on a plugin-specific prefix. Reserved core admin namespaces (`config.*`, `exec.approvals.*`, `wizard.*`, `update.*`) are always coerced to diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index 5c237ba6a98..d689a117878 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -117,6 +117,7 @@ provider- or plugin-specific policy to core prompt builders. | `api.registerGatewayMethod(name, handler)` | Gateway RPC method | | `api.registerGatewayDiscoveryService(service)` | Local Gateway discovery advertiser | | `api.registerCli(registrar, opts?)` | CLI subcommand | +| `api.registerNodeCliFeature(registrar, opts?)` | Node feature CLI under `openclaw nodes` | | `api.registerService(service)` | Background service | | `api.registerInteractiveHandler(registration)` | Interactive handler | | `api.registerAgentToolResultMiddleware(...)` | Runtime tool-result middleware | @@ -214,11 +215,18 @@ own trust. ### CLI registration metadata -`api.registerCli(registrar, opts?)` accepts two kinds of top-level metadata: +`api.registerCli(registrar, opts?)` accepts two kinds of command metadata: -- `commands`: explicit command roots owned by the registrar -- `descriptors`: parse-time command descriptors used for root CLI help, +- `commands`: explicit command names owned by the registrar +- `descriptors`: parse-time command descriptors used for CLI help, routing, and lazy plugin CLI registration +- `parentPath`: optional parent command path for nested command groups, such as + `["nodes"]` + +For paired-node features, prefer +`api.registerNodeCliFeature(registrar, opts?)`. It is a small wrapper around +`api.registerCli(..., { parentPath: ["nodes"] })` and makes commands such as +`openclaw nodes canvas` explicit plugin-owned node features. If you want a plugin command to stay lazy-loaded in the normal root CLI path, provide `descriptors` that cover every top-level command root exposed by that @@ -242,6 +250,27 @@ api.registerCli( ); ``` +Nested commands receive the resolved parent command as `program`: + +```typescript +api.registerCli( + async ({ program }) => { + const { registerNodesCanvasCommands } = await import("./src/cli.js"); + registerNodesCanvasCommands(program); + }, + { + parentPath: ["nodes"], + descriptors: [ + { + name: "canvas", + description: "Capture or render canvas content from a paired node", + hasSubcommands: true, + }, + ], + }, +); +``` + Use `commands` by itself only when you do not need lazy root CLI registration. That eager compatibility path remains supported, but it does not install descriptor-backed placeholders for parse-time lazy loading. diff --git a/docs/refactor/canvas.md b/docs/refactor/canvas.md new file mode 100644 index 00000000000..d90c00fdfbf --- /dev/null +++ b/docs/refactor/canvas.md @@ -0,0 +1,131 @@ +--- +summary: "Plan and audit checklist for moving Canvas out of core and into a bundled experimental plugin." +read_when: + - Moving Canvas host, tools, commands, docs, or protocol ownership + - Auditing whether Canvas is still core-owned + - Preparing or reviewing the experimental Canvas plugin PR +title: "Canvas plugin refactor" +--- + +# Canvas plugin refactor + +Canvas is low-use and experimental. Treat it as a bundled plugin, not a core feature. Core may keep generic gateway, node, HTTP, auth, config, and native-client plumbing, but Canvas-specific behavior should live under `extensions/canvas`. + +## Goal + +Move Canvas ownership to `extensions/canvas` while preserving the current paired-node behavior: + +- the agent-facing `canvas` tool is registered by the Canvas plugin +- Canvas node commands are allowed only when the Canvas plugin registers them +- A2UI host/source files live under the Canvas plugin +- Canvas document materialization lives under the Canvas plugin +- CLI command implementation lives under the Canvas plugin, or delegates through a plugin-owned runtime barrel +- docs and plugin inventory describe Canvas as experimental and plugin-backed + +## Non-goals + +- Do not redesign the native app Canvas UI in this refactor. +- Do not remove Canvas protocol/client support from iOS, Android, or macOS unless a separate product decision says Canvas should be deleted. +- Do not build a broad plugin service framework only for Canvas unless at least one other bundled plugin needs the same seam. + +## Current branch state + +Done: + +- Added bundled plugin package in `extensions/canvas`. +- Added `extensions/canvas/openclaw.plugin.json`. +- Moved the agent `canvas` tool from `src/agents/tools/canvas-tool.ts` to `extensions/canvas/src/tool.ts`. +- Removed core registration of `createCanvasTool` from `src/agents/openclaw-tools.ts`. +- Moved Canvas host implementation from `src/canvas-host` to `extensions/canvas/src/host`. +- Kept `extensions/canvas/runtime-api.ts` as the plugin-owned compatibility barrel for tests, packaging, and external public Canvas helpers. +- Moved Canvas document materialization from `src/gateway/canvas-documents.ts` to `extensions/canvas/src/documents.ts`. +- Moved Canvas CLI implementation and A2UI JSONL helpers into `extensions/canvas/src/cli.ts`. +- Moved Canvas host URL and scoped capability helpers into `extensions/canvas/src`. +- Moved Canvas node command defaults out of hardcoded core lists and into plugin `nodeInvokePolicies`. +- Added plugin-owned Canvas host config at `plugins.entries.canvas.config.host`. +- Moved Canvas and A2UI HTTP serving behind Canvas plugin HTTP route registration. +- Added generic plugin WebSocket upgrade dispatch for plugin-owned HTTP routes. +- Replaced Canvas-specific gateway host URL and node capability auth with generic hosted plugin surface and node capability helpers. +- Added plugin-owned hosted media resolvers so Canvas document URLs resolve through the Canvas plugin instead of core importing Canvas document internals. +- Added `api.registerNodeCliFeature(...)` so Canvas can declare `openclaw nodes canvas` as a plugin-owned node feature without manually spelling the parent command path. +- Removed production `src/**` imports of `extensions/canvas/runtime-api.js`. +- Moved the A2UI bundle source from `apps/shared/OpenClawKit/Tools/CanvasA2UI` to `extensions/canvas/src/host/a2ui-app`. +- Moved A2UI build/copy implementation under `extensions/canvas/scripts` and replaced root build wiring with generic bundled-plugin asset hooks. +- Removed the runtime legacy top-level `canvasHost` config alias. +- Kept the Canvas doctor migration so `openclaw doctor --fix` rewrites old `canvasHost` configs into `plugins.entries.canvas.config.host`. +- Removed old-agent Canvas protocol compatibility behind gateway protocol v4. Native clients and gateways now use only `pluginSurfaceUrls.canvas` plus `node.pluginSurface.refresh`; the deprecated `canvasHostUrl`, `canvasCapability`, and `node.canvas.capability.refresh` path is intentionally unsupported in this experimental refactor. +- Updated generated plugin inventory to include Canvas. +- Added plugin reference docs at `docs/plugins/reference/canvas.md`. + +Known remaining core-owned Canvas surfaces: + +- Native app Canvas handlers under `apps/` still intentionally consume the Canvas plugin surface +- native app Canvas protocol/client handlers under `apps/` +- published artifact output still uses `dist/canvas-host/a2ui` for backwards-compatible runtime lookup, but the copy step is now plugin-owned + +## Target shape + +`extensions/canvas` should own: + +- plugin manifest and package metadata +- agent tool registration +- node invoke command policy +- Canvas host and A2UI runtime +- Canvas A2UI bundle source and asset build/copy scripts +- Canvas document creation and asset resolution +- Canvas CLI implementation +- Canvas docs page and plugin inventory entry + +Core should own only generic seams: + +- plugin discovery and registration +- generic agent tool registry +- generic node invoke policy registry +- generic gateway HTTP/auth and WebSocket upgrade dispatch +- generic hosted plugin surface URL resolution +- generic hosted media resolver registration +- generic node capability transport +- generic config plumbing +- generic bundled-plugin asset hook discovery + +Native apps may keep Canvas command handlers as clients of the protocol. They are not the plugin runtime owner. + +## Migration steps + +1. Treat `plugins.entries.canvas.config.host` as the plugin-owned config surface. +2. Update docs so Canvas is described as an experimental bundled plugin. +3. Run focused Canvas tests, plugin inventory checks, plugin SDK API checks, and build/type gates affected by runtime boundaries. + +## Audit checklist + +Before calling the refactor complete: + +- `rg "src/canvas-host|../canvas-host"` returns no live source imports. +- `rg "canvas-tool|createCanvasTool" src` finds no core-owned Canvas tool implementation. +- `rg "canvas.present|canvas.snapshot|canvas.a2ui" src/gateway` finds no hardcoded allowlist defaults outside generic plugin policy tests. +- `rg "extensions/canvas/runtime-api" src --glob '!**/*.test.ts'` is empty. +- `rg "canvas-documents" src` is empty. +- `rg "registerNodesCanvasCommands|nodes-canvas" src` is empty; the Canvas plugin registers `openclaw nodes canvas` through nested plugin CLI metadata. +- `rg "createCanvasHostHandler|handleA2uiHttpRequest" src/gateway` returns no gateway runtime ownership. +- `rg "apps/shared/OpenClawKit/Tools/CanvasA2UI|canvas-a2ui-copy|extensions/canvas/src/host/a2ui" scripts .github package.json` finds only compatibility wrappers or plugin-owned paths. +- `pnpm plugins:inventory:check` passes. +- `pnpm plugin-sdk:api:check` passes, or generated API baselines are intentionally updated and reviewed. +- Targeted Canvas tests pass. +- Changed-lanes tests pass for Canvas host/A2UI paths. +- PR body explicitly says Canvas is experimental and plugin-backed. + +## Verification commands + +Use targeted local checks while iterating: + +```sh +pnpm test extensions/canvas/src/host/server.test.ts extensions/canvas/src/host/server.state-dir.test.ts extensions/canvas/src/host/file-resolver.test.ts +pnpm test src/gateway/server.plugin-node-capability-auth.test.ts src/gateway/server-import-boundary.test.ts +pnpm test extensions/canvas/src/config-migration.test.ts src/commands/doctor-legacy-config.migrations.test.ts +pnpm test test/scripts/changed-lanes.test.ts test/scripts/build-all.test.ts test/scripts/bundle-a2ui.test.ts test/scripts/bundled-plugin-assets.test.ts src/scripts/canvas-a2ui-copy.test.ts src/infra/run-node.test.ts +pnpm tsgo:extensions +pnpm plugins:inventory:check +pnpm plugin-sdk:api:check +``` + +Run `pnpm build` before push if runtime barrel, lazy import, packaging, or published plugin surfaces change. diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index 85d6d03fa8e..d25f072d2b0 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -1,11 +1,10 @@ --- -summary: "Release lanes, operator checklist, validation boxes, version naming, planned monthly support lines, and cadence" +summary: "Release lanes, operator checklist, validation boxes, version naming, and cadence" title: "Release policy" read_when: - Looking for public release channel definitions - Running release validation or package acceptance - Looking for version naming and cadence - - Planning monthly support or LTS release lines --- OpenClaw has three public release lanes: @@ -18,38 +17,18 @@ OpenClaw has three public release lanes: - Stable release version: `YYYY.M.D` - Git tag: `vYYYY.M.D` -- Legacy stable correction release version: `YYYY.M.D-N` +- Stable correction release version: `YYYY.M.D-N` - Git tag: `vYYYY.M.D-N` - Beta prerelease version: `YYYY.M.D-beta.N` - Git tag: `vYYYY.M.D-beta.N` - Do not zero-pad month or day - `latest` means the current promoted stable npm release - `beta` means the current beta install target -- Stable and legacy correction releases publish to npm `beta` by default; release operators can target `latest` explicitly, or promote a vetted beta build later +- Stable and stable correction releases publish to npm `beta` by default; release operators can target `latest` explicitly, or promote a vetted beta build later - Every stable OpenClaw release ships the npm package and macOS app together; beta releases normally validate and publish the npm/package path first, with mac app build/sign/notarize reserved for stable unless explicitly requested -### Planned monthly support versioning - -OpenClaw does not yet have an LTS or monthly support channel. Maintainers are -working toward SemVer-compatible monthly support lines, but the shipped update -channels today are still `stable`, `beta`, and `dev`. - -The planned version shape is `YYYY.M.PATCH`: - -- `YYYY` is the year. -- `M` is the monthly release line, without a leading zero. -- `PATCH` increments within that monthly line and can grow as high as needed. - -For example, `2026.6.0`, `2026.6.1`, and `2026.6.2` would all be on the June -2026 line. A future monthly support dist-tag such as `stable-2026-6` or -`lts-2026-6` may point at that line, while `latest` continues to move quickly. - -This future model replaces the need for new `YYYY.M.D-N` correction releases. -Existing legacy correction versions remain recognized so older packages and -upgrade paths keep working. - ## Release cadence - Releases move beta-first @@ -260,7 +239,7 @@ Validation` or from the `main`/release workflow ref so workflow logic and `preflight_run_id` and `validate_run_id` - the real publish paths promote prepared artifacts instead of rebuilding them again -- For legacy stable correction releases like `YYYY.M.D-N`, the post-publish verifier +- For stable correction releases like `YYYY.M.D-N`, the post-publish verifier also checks the same temp-prefix upgrade path from `YYYY.M.D` to `YYYY.M.D-N` so release corrections cannot silently leave older global installs on the base stable payload diff --git a/docs/tools/index.md b/docs/tools/index.md index 956918a4240..f38b3e9a302 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -60,7 +60,6 @@ These tools ship with OpenClaw and are available without installing any plugins: | `read` / `write` / `edit` | File I/O in the workspace | | | `apply_patch` | Multi-hunk file patches | [Apply Patch](/tools/apply-patch) | | `message` | Send messages across all channels | [Agent Send](/tools/agent-send) | -| `canvas` | Drive node Canvas (present, eval, snapshot) | | | `nodes` | Discover and target paired devices | | | `cron` / `gateway` | Manage scheduled jobs; inspect, patch, restart, or update the gateway | | | `image` / `image_generate` | Analyze or generate images | [Image Generation](/tools/image-generation) | @@ -104,6 +103,7 @@ legacy `tools.bash.*` aliases normalize to the same protected exec paths. Plugins can register additional tools. Some examples: +- [Canvas](/plugins/reference/canvas) — experimental bundled plugin for node Canvas control and A2UI rendering - [Diffs](/tools/diffs) — diff viewer and renderer - [LLM Task](/tools/llm-task) — JSON-only LLM step for structured output - [Lobster](/tools/lobster) — typed workflow runtime with resumable approvals @@ -195,7 +195,7 @@ Use `group:*` shorthands in allow/deny lists: | `group:sessions` | sessions_list, sessions_history, sessions_send, sessions_spawn, sessions_yield, subagents, session_status | | `group:memory` | memory_search, memory_get | | `group:web` | web_search, x_search, web_fetch | -| `group:ui` | browser, canvas | +| `group:ui` | browser, canvas when the bundled Canvas plugin is enabled | | `group:automation` | heartbeat_respond, cron, gateway | | `group:messaging` | message | | `group:nodes` | nodes | diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index e9dd139968b..c13a43c5dd4 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -594,10 +594,6 @@ When `openclaw update` runs on the beta channel, default-line npm and ClawHub plugin records try `@beta` first and fall back to default/latest when no plugin beta release exists. Exact versions and explicit tags stay pinned. -OpenClaw does not yet expose LTS or monthly support plugin channels. Planned -monthly support-line work will need plugin npm and ClawHub tags to follow the -same support line as the core package instead of silently using `latest`. - `--pin` is npm-only. It is not supported with `--marketplace`, because marketplace installs persist marketplace source metadata instead of an npm spec. diff --git a/docs/tools/subagents.md b/docs/tools/subagents.md index 9a2ab0a08d3..99584b9c13b 100644 --- a/docs/tools/subagents.md +++ b/docs/tools/subagents.md @@ -14,11 +14,6 @@ when finished, **announce** their result back to the requester chat channel. Each sub-agent run is tracked as a [background task](/automation/tasks). -For the security model behind delegation, see -[Multi-agent and sub-agent boundaries](/gateway/security#multi-agent-and-sub-agent-boundaries). -Sub-agents are useful isolation and workflow units, but they are not a hostile -multi-tenant authorization boundary inside one shared Gateway. - Primary goals: - Parallelize "research / long task / slow tool" work without blocking the main run. diff --git a/docs/tools/web.md b/docs/tools/web.md index 02024e16c58..208074028ae 100644 --- a/docs/tools/web.md +++ b/docs/tools/web.md @@ -231,13 +231,12 @@ fallbacks after its dedicated web-search config and `GEMINI_API_KEY`. See the provider pages for examples. `tools.web.search.provider` is validated against the web-search provider ids -declared by bundled and installed plugin manifests, plus known installable -provider plugins. A typo such as `"brvae"` fails config validation instead of -silently falling back to auto-detection. If the configured provider is known but -the owning plugin is unavailable, OpenClaw keeps startup resilient and reports a -warning so you can run `openclaw doctor --fix` to install or enable the plugin. -The same warning behavior applies to stale plugin evidence, such as a leftover -`plugins.entries.` block after uninstalling a third-party plugin. +declared by bundled and installed plugin manifests. A typo such as `"brvae"` +fails config validation instead of silently falling back to auto-detection. If a +configured provider only has stale plugin evidence, such as a leftover +`plugins.entries.` block after uninstalling a third-party plugin, +OpenClaw keeps startup resilient and reports a warning so you can reinstall the +plugin or run `openclaw doctor --fix` to clean up the stale config. `web_fetch` fallback provider selection is separate: diff --git a/extensions/canvas/cli-metadata.ts b/extensions/canvas/cli-metadata.ts new file mode 100644 index 00000000000..0af33a762db --- /dev/null +++ b/extensions/canvas/cli-metadata.ts @@ -0,0 +1,18 @@ +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; + +export default definePluginEntry({ + id: "canvas", + name: "Canvas", + description: "Experimental Canvas control and A2UI rendering surfaces for paired nodes.", + register(api) { + api.registerNodeCliFeature(() => {}, { + descriptors: [ + { + name: "canvas", + description: "Capture or render canvas content from a paired node", + hasSubcommands: true, + }, + ], + }); + }, +}); diff --git a/extensions/canvas/index.ts b/extensions/canvas/index.ts new file mode 100644 index 00000000000..f99d4165eef --- /dev/null +++ b/extensions/canvas/index.ts @@ -0,0 +1,98 @@ +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import { createDefaultCanvasCliDependencies, registerNodesCanvasCommands } from "./src/cli.js"; +import { canvasConfigSchema, isCanvasHostEnabled } from "./src/config.js"; +import { resolveCanvasHttpPathToLocalPath } from "./src/documents.js"; +import { A2UI_PATH, CANVAS_HOST_PATH, CANVAS_WS_PATH } from "./src/host/a2ui.js"; +import { createCanvasHttpRouteHandler } from "./src/http-route.js"; +import { createCanvasTool } from "./src/tool.js"; + +const CANVAS_NODE_COMMANDS = [ + "canvas.present", + "canvas.hide", + "canvas.navigate", + "canvas.eval", + "canvas.snapshot", + "canvas.a2ui.push", + "canvas.a2ui.pushJSONL", + "canvas.a2ui.reset", +]; + +export default definePluginEntry({ + id: "canvas", + name: "Canvas", + description: "Experimental Canvas control and A2UI rendering surfaces for paired nodes.", + configSchema: canvasConfigSchema, + reload: { + restartPrefixes: ["plugins.enabled", "plugins.allow", "plugins.deny", "plugins.entries.canvas"], + }, + register(api) { + if (isCanvasHostEnabled(api.config)) { + const httpRouteHandler = createCanvasHttpRouteHandler({ + config: api.config, + pluginConfig: api.pluginConfig, + runtime: { + log: (...args) => api.logger.info(args.map(String).join(" ")), + error: (...args) => api.logger.error(args.map(String).join(" ")), + exit: (code) => { + throw new Error(`canvas host requested process exit ${code}`); + }, + }, + }); + const nodeCapability = { surface: "canvas" }; + api.registerHttpRoute({ + path: A2UI_PATH, + auth: "plugin", + match: "prefix", + nodeCapability, + handler: httpRouteHandler.handleHttpRequest, + }); + api.registerHttpRoute({ + path: CANVAS_HOST_PATH, + auth: "plugin", + match: "prefix", + nodeCapability, + handler: httpRouteHandler.handleHttpRequest, + }); + api.registerHttpRoute({ + path: CANVAS_WS_PATH, + auth: "plugin", + match: "exact", + nodeCapability, + handler: httpRouteHandler.handleHttpRequest, + handleUpgrade: httpRouteHandler.handleUpgrade, + }); + api.registerService({ + id: "canvas-host", + start: () => {}, + stop: () => httpRouteHandler.close(), + }); + api.registerHostedMediaResolver((mediaUrl) => resolveCanvasHttpPathToLocalPath(mediaUrl)); + } + api.registerNodeInvokePolicy({ + commands: CANVAS_NODE_COMMANDS, + defaultPlatforms: ["ios", "android", "macos", "windows", "unknown"], + foregroundRestrictedOnIos: true, + handle: (ctx) => ctx.invokeNode(), + }); + api.registerTool((ctx) => + createCanvasTool({ + config: ctx.runtimeConfig ?? ctx.config, + workspaceDir: ctx.workspaceDir, + }), + ); + api.registerNodeCliFeature( + ({ program }) => { + registerNodesCanvasCommands(program, createDefaultCanvasCliDependencies()); + }, + { + descriptors: [ + { + name: "canvas", + description: "Capture or render canvas content from a paired node", + hasSubcommands: true, + }, + ], + }, + ); + }, +}); diff --git a/extensions/canvas/openclaw.plugin.json b/extensions/canvas/openclaw.plugin.json new file mode 100644 index 00000000000..656f70695f8 --- /dev/null +++ b/extensions/canvas/openclaw.plugin.json @@ -0,0 +1,40 @@ +{ + "id": "canvas", + "activation": { + "onStartup": true + }, + "enabledByDefault": true, + "name": "Canvas", + "description": "Experimental Canvas control and A2UI rendering surfaces for paired nodes.", + "contracts": { + "tools": ["canvas"] + }, + "configContracts": { + "compatibilityMigrationPaths": ["canvasHost"] + }, + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "host": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "root": { + "type": "string" + }, + "port": { + "type": "integer", + "minimum": 1 + }, + "liveReload": { + "type": "boolean" + } + } + } + } + } +} diff --git a/extensions/canvas/package.json b/extensions/canvas/package.json new file mode 100644 index 00000000000..fded840fbaf --- /dev/null +++ b/extensions/canvas/package.json @@ -0,0 +1,22 @@ +{ + "name": "@openclaw/canvas-plugin", + "version": "2026.5.6", + "private": true, + "description": "OpenClaw Canvas plugin", + "type": "module", + "devDependencies": { + "@openclaw/plugin-sdk": "workspace:*" + }, + "dependencies": { + "typebox": "^1.0.58" + }, + "openclaw": { + "extensions": [ + "./index.ts" + ], + "assetScripts": { + "build": "node scripts/bundle-a2ui.mjs", + "copy": "node scripts/copy-a2ui.mjs" + } + } +} diff --git a/extensions/canvas/runtime-api.ts b/extensions/canvas/runtime-api.ts new file mode 100644 index 00000000000..9962ec980b5 --- /dev/null +++ b/extensions/canvas/runtime-api.ts @@ -0,0 +1,42 @@ +export { + canvasConfigSchema, + isCanvasHostEnabled, + isCanvasPluginEnabled, + parseCanvasPluginConfig, + resolveCanvasHostConfig, + type CanvasHostConfig, + type CanvasPluginConfig, +} from "./src/config.js"; +export { + A2UI_PATH, + CANVAS_HOST_PATH, + CANVAS_WS_PATH, + handleA2uiHttpRequest, +} from "./src/host/a2ui.js"; +export { + createCanvasHostHandler, + startCanvasHost, + type CanvasHostHandler, + type CanvasHostServer, +} from "./src/host/server.js"; +export { + buildCanvasDocumentEntryUrl, + createCanvasDocument, + resolveCanvasDocumentAssets, + resolveCanvasDocumentDir, + resolveCanvasHttpPathToLocalPath, +} from "./src/documents.js"; +export { + registerNodesCanvasCommands, + type CanvasCliDependencies, + type CanvasNodesRpcOpts, +} from "./src/cli.js"; +export { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "./src/cli-helpers.js"; +export { + buildCanvasScopedHostUrl, + CANVAS_CAPABILITY_PATH_PREFIX, + CANVAS_CAPABILITY_TTL_MS, + mintCanvasCapabilityToken, + normalizeCanvasScopedUrl, +} from "./src/capability.js"; +export { resolveCanvasHostUrl } from "./src/host-url.js"; diff --git a/extensions/canvas/scripts/bundle-a2ui.mjs b/extensions/canvas/scripts/bundle-a2ui.mjs new file mode 100644 index 00000000000..1407bf23647 --- /dev/null +++ b/extensions/canvas/scripts/bundle-a2ui.mjs @@ -0,0 +1,228 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { resolvePnpmRunner } from "../../../scripts/pnpm-runner.mjs"; + +const pluginDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const rootDir = path.resolve(pluginDir, "../.."); +const require = createRequire(import.meta.url); +const hashFile = path.join(pluginDir, "src", "host", "a2ui", ".bundle.hash"); +const outputFile = path.join(pluginDir, "src", "host", "a2ui", "a2ui.bundle.js"); +const a2uiAppDir = path.join(pluginDir, "src", "host", "a2ui-app"); +const rootPackageFile = path.join(rootDir, "package.json"); +const lockFile = path.join(rootDir, "pnpm-lock.yaml"); +const repoInputPaths = [rootPackageFile, lockFile, a2uiAppDir]; +const relativeRepoInputPaths = repoInputPaths.map((inputPath) => + normalizePath(path.relative(rootDir, inputPath)), +); + +function fail(message) { + console.error(message); + console.error("A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle"); + console.error("If this persists, verify pnpm deps and try again."); + process.exit(1); +} + +async function pathExists(targetPath) { + try { + await fs.stat(targetPath); + return true; + } catch { + return false; + } +} + +function normalizePath(filePath) { + return filePath.split(path.sep).join("/"); +} + +export function isBundleHashInputPath(filePath, repoRoot = rootDir) { + return Boolean(filePath && repoRoot); +} + +export function getLocalRolldownCliCandidates(repoRoot = rootDir) { + return [ + path.join(repoRoot, "node_modules", "rolldown", "bin", "cli.mjs"), + path.join(repoRoot, "node_modules", ".pnpm", "node_modules", "rolldown", "bin", "cli.mjs"), + path.join( + repoRoot, + "node_modules", + ".pnpm", + "rolldown@1.0.0-rc.12", + "node_modules", + "rolldown", + "bin", + "cli.mjs", + ), + ]; +} + +export function getBundleHashRepoInputPaths(repoRoot = rootDir) { + return [ + path.join(repoRoot, "package.json"), + path.join(repoRoot, "pnpm-lock.yaml"), + path.join(repoRoot, "extensions", "canvas", "src", "host", "a2ui-app"), + ]; +} + +export function getBundleHashInputPaths(repoRoot = rootDir) { + return getBundleHashRepoInputPaths(repoRoot); +} + +export function compareNormalizedPaths(left, right) { + const normalizedLeft = normalizePath(left); + const normalizedRight = normalizePath(right); + if (normalizedLeft < normalizedRight) { + return -1; + } + if (normalizedLeft > normalizedRight) { + return 1; + } + return 0; +} + +async function walkFiles(entryPath, files) { + if (!isBundleHashInputPath(entryPath)) { + return; + } + const stat = await fs.stat(entryPath); + if (!stat.isDirectory()) { + files.push(entryPath); + return; + } + const entries = await fs.readdir(entryPath); + for (const entry of entries) { + await walkFiles(path.join(entryPath, entry), files); + } +} + +function listTrackedInputFiles() { + const result = spawnSync("git", ["ls-files", "--", ...relativeRepoInputPaths], { + cwd: rootDir, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.status !== 0) { + return null; + } + const trackedFiles = result.stdout + .split("\n") + .filter(Boolean) + .map((filePath) => path.join(rootDir, filePath)) + .filter((filePath) => existsSync(filePath)) + .filter((filePath) => isBundleHashInputPath(filePath)); + return trackedFiles; +} + +async function computeHash() { + let files = listTrackedInputFiles(); + if (!files) { + files = []; + for (const inputPath of getBundleHashRepoInputPaths(rootDir)) { + await walkFiles(inputPath, files); + } + } + files = [...new Set(files)].toSorted(compareNormalizedPaths); + + const hash = createHash("sha256"); + for (const filePath of files) { + hash.update(normalizePath(path.relative(rootDir, filePath))); + hash.update("\0"); + hash.update(await fs.readFile(filePath)); + hash.update("\0"); + } + return hash.digest("hex"); +} + +function runStep(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: rootDir, + env: process.env, + stdio: "inherit", + ...options, + }); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function runPnpm(pnpmArgs) { + const runner = resolvePnpmRunner({ + pnpmArgs, + nodeExecPath: process.execPath, + npmExecPath: process.env.npm_execpath, + comSpec: process.env.ComSpec, + platform: process.platform, + }); + runStep(runner.command, runner.args, { + shell: runner.shell, + windowsVerbatimArguments: runner.windowsVerbatimArguments, + }); +} + +async function main() { + const hasAppDir = await pathExists(a2uiAppDir); + const hasOutputFile = await pathExists(outputFile); + let hasA2uiPackage = true; + try { + require.resolve("@a2ui/lit"); + require.resolve("@a2ui/lit/ui"); + } catch { + hasA2uiPackage = false; + } + if (!hasA2uiPackage || !hasAppDir) { + if (hasOutputFile) { + console.log("A2UI package missing; keeping prebuilt bundle."); + return; + } + if (process.env.OPENCLAW_SPARSE_PROFILE || process.env.OPENCLAW_A2UI_SKIP_MISSING === "1") { + console.error( + "A2UI package missing; skipping bundle because OPENCLAW_A2UI_SKIP_MISSING=1 or OPENCLAW_SPARSE_PROFILE is set.", + ); + return; + } + fail(`A2UI package missing and no prebuilt bundle found at: ${outputFile}`); + } + + const currentHash = await computeHash(); + if (await pathExists(hashFile)) { + const previousHash = (await fs.readFile(hashFile, "utf8")).trim(); + if (previousHash === currentHash && hasOutputFile) { + console.log("A2UI bundle up to date; skipping."); + return; + } + } + + const localRolldownCliCandidates = getLocalRolldownCliCandidates(rootDir); + const localRolldownCli = ( + await Promise.all( + localRolldownCliCandidates.map(async (candidate) => + (await pathExists(candidate)) ? candidate : null, + ), + ) + ).find(Boolean); + + if (localRolldownCli) { + runStep(process.execPath, [ + localRolldownCli, + "-c", + path.join(a2uiAppDir, "rolldown.config.mjs"), + ]); + } else { + runPnpm(["-s", "exec", "rolldown", "-c", path.join(a2uiAppDir, "rolldown.config.mjs")]); + } + + await fs.writeFile(hashFile, `${currentHash}\n`, "utf8"); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + await main().catch((error) => { + fail(error instanceof Error ? error.message : String(error)); + }); +} diff --git a/extensions/canvas/scripts/copy-a2ui.d.mts b/extensions/canvas/scripts/copy-a2ui.d.mts new file mode 100644 index 00000000000..76f35c91c15 --- /dev/null +++ b/extensions/canvas/scripts/copy-a2ui.d.mts @@ -0,0 +1,4 @@ +export declare function copyA2uiAssets(params: { + srcDir: string; + outDir: string; +}): Promise; diff --git a/scripts/canvas-a2ui-copy.ts b/extensions/canvas/scripts/copy-a2ui.mjs similarity index 72% rename from scripts/canvas-a2ui-copy.ts rename to extensions/canvas/scripts/copy-a2ui.mjs index 36ed4fcaeec..441953fd844 100644 --- a/scripts/canvas-a2ui-copy.ts +++ b/extensions/canvas/scripts/copy-a2ui.mjs @@ -1,20 +1,23 @@ +#!/usr/bin/env node + import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const pluginDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const rootDir = path.resolve(pluginDir, "../.."); function getA2uiPaths(env = process.env) { - const srcDir = env.OPENCLAW_A2UI_SRC_DIR ?? path.join(repoRoot, "src", "canvas-host", "a2ui"); - const outDir = env.OPENCLAW_A2UI_OUT_DIR ?? path.join(repoRoot, "dist", "canvas-host", "a2ui"); + const srcDir = env.OPENCLAW_A2UI_SRC_DIR ?? path.join(pluginDir, "src", "host", "a2ui"); + const outDir = env.OPENCLAW_A2UI_OUT_DIR ?? path.join(rootDir, "dist", "canvas-host", "a2ui"); return { srcDir, outDir }; } -function shouldSkipMissingA2uiAssets(env = process.env): boolean { +function shouldSkipMissingA2uiAssets(env = process.env) { return env.OPENCLAW_A2UI_SKIP_MISSING === "1" || Boolean(env.OPENCLAW_SPARSE_PROFILE); } -export async function copyA2uiAssets({ srcDir, outDir }: { srcDir: string; outDir: string }) { +export async function copyA2uiAssets({ srcDir, outDir }) { const skipMissing = shouldSkipMissingA2uiAssets(process.env); try { await fs.stat(path.join(srcDir, "index.html")); diff --git a/extensions/canvas/setup-api.ts b/extensions/canvas/setup-api.ts new file mode 100644 index 00000000000..56abb3b0b03 --- /dev/null +++ b/extensions/canvas/setup-api.ts @@ -0,0 +1,11 @@ +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import { migrateLegacyCanvasHostConfig } from "./src/config-migration.js"; + +export default definePluginEntry({ + id: "canvas", + name: "Canvas Setup", + description: "Lightweight Canvas setup hooks", + register(api) { + api.registerConfigMigration((config) => migrateLegacyCanvasHostConfig(config)); + }, +}); diff --git a/src/cli/nodes-cli/a2ui-jsonl.ts b/extensions/canvas/src/a2ui-jsonl.ts similarity index 100% rename from src/cli/nodes-cli/a2ui-jsonl.ts rename to extensions/canvas/src/a2ui-jsonl.ts diff --git a/extensions/canvas/src/capability.ts b/extensions/canvas/src/capability.ts new file mode 100644 index 00000000000..bcf2472d9b0 --- /dev/null +++ b/extensions/canvas/src/capability.ts @@ -0,0 +1,25 @@ +import { + buildPluginNodeCapabilityScopedHostUrl, + DEFAULT_PLUGIN_NODE_CAPABILITY_TTL_MS, + mintPluginNodeCapabilityToken, + normalizePluginNodeCapabilityScopedUrl, + PLUGIN_NODE_CAPABILITY_PATH_PREFIX, + type NormalizedPluginNodeCapabilityUrl, +} from "openclaw/plugin-sdk/gateway-runtime"; + +export const CANVAS_CAPABILITY_PATH_PREFIX = PLUGIN_NODE_CAPABILITY_PATH_PREFIX; +export const CANVAS_CAPABILITY_TTL_MS = DEFAULT_PLUGIN_NODE_CAPABILITY_TTL_MS; + +export type NormalizedCanvasScopedUrl = NormalizedPluginNodeCapabilityUrl; + +export function mintCanvasCapabilityToken(): string { + return mintPluginNodeCapabilityToken(); +} + +export function buildCanvasScopedHostUrl(baseUrl: string, capability: string): string | undefined { + return buildPluginNodeCapabilityScopedHostUrl(baseUrl, capability); +} + +export function normalizeCanvasScopedUrl(rawUrl: string): NormalizedCanvasScopedUrl { + return normalizePluginNodeCapabilityScopedUrl(rawUrl); +} diff --git a/extensions/canvas/src/cli-helpers.ts b/extensions/canvas/src/cli-helpers.ts new file mode 100644 index 00000000000..fb2c3b120ab --- /dev/null +++ b/extensions/canvas/src/cli-helpers.ts @@ -0,0 +1,42 @@ +import { randomUUID } from "node:crypto"; +import fs from "node:fs"; +import * as path from "node:path"; +import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/security-runtime"; +import { asRecord, readStringValue } from "openclaw/plugin-sdk/text-runtime"; + +type CanvasSnapshotPayload = { + format: string; + base64: string; +}; + +export function parseCanvasSnapshotPayload(value: unknown): CanvasSnapshotPayload { + const obj = asRecord(value); + const format = readStringValue(obj.format); + const base64 = readStringValue(obj.base64); + if (!format || !base64) { + throw new Error("invalid canvas.snapshot payload"); + } + return { format, base64 }; +} + +function resolveCliName(): string { + return "openclaw"; +} + +function resolveTempPathParts(opts: { ext: string; tmpDir?: string; id?: string }) { + const tmpDir = opts.tmpDir ?? resolvePreferredOpenClawTmpDir(); + if (!opts.tmpDir) { + fs.mkdirSync(tmpDir, { recursive: true, mode: 0o700 }); + } + return { + tmpDir, + id: opts.id ?? randomUUID(), + ext: opts.ext.startsWith(".") ? opts.ext : `.${opts.ext}`, + }; +} + +export function canvasSnapshotTempPath(opts: { ext: string; tmpDir?: string; id?: string }) { + const { tmpDir, id, ext } = resolveTempPathParts(opts); + const cliName = resolveCliName(); + return path.join(tmpDir, `${cliName}-canvas-snapshot-${id}${ext}`); +} diff --git a/extensions/canvas/src/cli.test.ts b/extensions/canvas/src/cli.test.ts new file mode 100644 index 00000000000..2dd089a91a1 --- /dev/null +++ b/extensions/canvas/src/cli.test.ts @@ -0,0 +1,75 @@ +import { Command } from "commander"; +import { describe, expect, it, vi } from "vitest"; +import { registerNodesCanvasCommands, type CanvasCliDependencies } from "./cli.js"; + +function createCanvasCliDeps() { + const writtenFiles: Array<{ filePath: string; base64: string }> = []; + const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn((code: number) => { + throw new Error(`exit ${code}`); + }), + writeJson: vi.fn(), + }; + const deps: CanvasCliDependencies = { + defaultRuntime: runtime, + nodesCallOpts: (cmd) => + cmd + .option("--url ", "Gateway WebSocket URL") + .option("--token ", "Gateway token") + .option("--timeout ", "Timeout in ms", "10000") + .option("--json", "Output JSON", false), + runNodesCommand: async (_label, action) => { + await action(); + }, + getNodesTheme: () => ({ ok: (value) => value }), + parseTimeoutMs: (raw) => (typeof raw === "string" ? Number.parseInt(raw, 10) : undefined), + resolveNodeId: async (opts) => opts.node ?? "ios-node", + buildNodeInvokeParams: ({ nodeId, command, params, timeoutMs }) => ({ + nodeId, + command, + params, + ...(typeof timeoutMs === "number" ? { timeoutMs } : {}), + }), + callGatewayCli: vi.fn(async () => ({ + payload: { + format: "png", + base64: "aGk=", + }, + })), + writeBase64ToFile: async (filePath, base64) => { + writtenFiles.push({ filePath, base64 }); + }, + shortenHomePath: (filePath) => filePath, + }; + return { deps, runtime, writtenFiles }; +} + +describe("canvas CLI", () => { + it("registers under nodes and captures a snapshot media path", async () => { + const program = new Command(); + program.exitOverride(); + const nodes = program.command("nodes"); + const { deps, runtime, writtenFiles } = createCanvasCliDeps(); + + registerNodesCanvasCommands(nodes, deps); + await program.parseAsync(["nodes", "canvas", "snapshot", "--node", "ios-node"], { + from: "user", + }); + + expect(deps.callGatewayCli).toHaveBeenCalledWith( + "node.invoke", + expect.objectContaining({ node: "ios-node" }), + expect.objectContaining({ + nodeId: "ios-node", + command: "canvas.snapshot", + params: expect.objectContaining({ format: "jpeg" }), + }), + ); + expect(writtenFiles).toHaveLength(1); + expect(writtenFiles[0]?.filePath).toMatch(/openclaw-canvas-snapshot-.*\.png$/); + expect(writtenFiles[0]?.base64).toBe("aGk="); + expect(runtime.log).toHaveBeenCalledWith(expect.stringMatching(/^MEDIA:.*\.png$/)); + }); +}); diff --git a/extensions/canvas/src/cli.ts b/extensions/canvas/src/cli.ts new file mode 100644 index 00000000000..b75369c455f --- /dev/null +++ b/extensions/canvas/src/cli.ts @@ -0,0 +1,428 @@ +import { randomUUID } from "node:crypto"; +import fs from "node:fs/promises"; +import type { Command } from "commander"; +import { runCommandWithRuntime, theme } from "openclaw/plugin-sdk/cli-runtime"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { + callGatewayFromCli, + resolveNodeFromNodeList, + type NodeMatchCandidate, +} from "openclaw/plugin-sdk/gateway-runtime"; +import { defaultRuntime } from "openclaw/plugin-sdk/runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, + shortenHomePath, +} from "openclaw/plugin-sdk/text-runtime"; +import { buildA2UITextJsonl, validateA2UIJsonl } from "./a2ui-jsonl.js"; +import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "./cli-helpers.js"; + +export type CanvasCliRuntime = { + log: (message: string) => void; + error: (message: string) => void; + exit: (code: number) => void; + writeJson: (value: unknown) => void; +}; + +export type CanvasNodesRpcOpts = { + url?: string; + token?: string; + timeout?: string; + json?: boolean; + node?: string; + invokeTimeout?: string; + target?: string; + x?: string; + y?: string; + width?: string; + height?: string; + js?: string; + jsonl?: string; + text?: string; + format?: string; + maxWidth?: string; + quality?: string; +}; + +export type CanvasCliDependencies = { + defaultRuntime: CanvasCliRuntime; + nodesCallOpts: (cmd: Command, defaults?: { timeoutMs?: number }) => Command; + runNodesCommand: (label: string, action: () => Promise) => Promise | void; + getNodesTheme: () => { ok: (value: string) => string }; + parseTimeoutMs: (raw: unknown) => number | undefined; + resolveNodeId: (opts: CanvasNodesRpcOpts, query: string) => Promise; + buildNodeInvokeParams: (params: { + nodeId: string; + command: string; + params?: Record; + timeoutMs?: number; + }) => Record; + callGatewayCli: ( + method: string, + opts: CanvasNodesRpcOpts, + params?: unknown, + callOpts?: { transportTimeoutMs?: number }, + ) => Promise; + writeBase64ToFile: (filePath: string, base64: string) => Promise; + shortenHomePath: (filePath: string) => string; +}; + +type CanvasNodeCandidate = NodeMatchCandidate; + +function parseTimeoutMs(raw: unknown): number | undefined { + if (raw === undefined || raw === null) { + return undefined; + } + const value = + typeof raw === "number" || typeof raw === "bigint" + ? Number(raw) + : typeof raw === "string" && raw.trim() + ? Number.parseInt(raw.trim(), 10) + : Number.NaN; + return Number.isFinite(value) ? value : undefined; +} + +function parseNodeCandidates(raw: unknown): CanvasNodeCandidate[] { + const payload = + raw && typeof raw === "object" ? (raw as { nodes?: unknown; paired?: unknown }) : {}; + const list = Array.isArray(payload.nodes) + ? payload.nodes + : Array.isArray(payload.paired) + ? payload.paired + : []; + return list + .map((entry) => { + if (!entry || typeof entry !== "object") { + return null; + } + const node = entry as { + nodeId?: unknown; + displayName?: unknown; + remoteIp?: unknown; + connected?: unknown; + clientId?: unknown; + }; + return typeof node.nodeId === "string" + ? { + nodeId: node.nodeId, + ...(typeof node.displayName === "string" && { displayName: node.displayName }), + ...(typeof node.remoteIp === "string" && { remoteIp: node.remoteIp }), + ...(typeof node.connected === "boolean" && { connected: node.connected }), + ...(typeof node.clientId === "string" && { clientId: node.clientId }), + } + : null; + }) + .filter((entry): entry is CanvasNodeCandidate => entry !== null); +} + +function unauthorizedHintForMessage(message: string): string | null { + const haystack = normalizeLowercaseStringOrEmpty(message); + if ( + haystack.includes("unauthorizedclient") || + haystack.includes("bridge client is not authorized") || + haystack.includes("unsigned bridge clients are not allowed") + ) { + return [ + "peekaboo bridge rejected the client.", + "sign the peekaboo CLI (TeamID Y5PE65HELJ) or launch the host with", + "PEEKABOO_ALLOW_UNSIGNED_SOCKET_CLIENTS=1 for local dev.", + ].join(" "); + } + return null; +} + +export function createDefaultCanvasCliDependencies(): CanvasCliDependencies { + const nodesCallOpts = (cmd: Command, defaults?: { timeoutMs?: number }) => + cmd + .option( + "--url ", + "Gateway WebSocket URL (defaults to gateway.remote.url when configured)", + ) + .option("--token ", "Gateway token (if required)") + .option("--timeout ", "Timeout in ms", String(defaults?.timeoutMs ?? 10_000)) + .option("--json", "Output JSON", false); + const callGatewayCli: CanvasCliDependencies["callGatewayCli"] = async ( + method, + opts, + params, + callOpts, + ) => { + const timeout = String(callOpts?.transportTimeoutMs ?? opts.timeout ?? 10_000); + return await callGatewayFromCli(method, { ...opts, timeout }, params, { + progress: opts.json !== true, + }); + }; + return { + defaultRuntime, + nodesCallOpts, + runNodesCommand: (label, action) => + runCommandWithRuntime(defaultRuntime, action, (err) => { + const message = formatErrorMessage(err); + defaultRuntime.error(theme.error(`nodes ${label} failed: ${message}`)); + const hint = unauthorizedHintForMessage(message); + if (hint) { + defaultRuntime.error(theme.warn(hint)); + } + defaultRuntime.exit(1); + }), + getNodesTheme: () => ({ ok: theme.success }), + parseTimeoutMs, + resolveNodeId: async (opts, query) => { + let raw: unknown; + try { + raw = await callGatewayCli("node.list", opts, {}); + } catch { + raw = await callGatewayCli("node.pair.list", opts, {}); + } + return resolveNodeFromNodeList(parseNodeCandidates(raw), query).nodeId; + }, + buildNodeInvokeParams: ({ nodeId, command, params, timeoutMs }) => ({ + nodeId, + command, + params, + idempotencyKey: randomUUID(), + ...(typeof timeoutMs === "number" && Number.isFinite(timeoutMs) ? { timeoutMs } : {}), + }), + callGatewayCli, + writeBase64ToFile: async (filePath, base64) => + await fs.writeFile(filePath, Buffer.from(base64, "base64")), + shortenHomePath, + }; +} + +async function invokeCanvas( + deps: CanvasCliDependencies, + opts: CanvasNodesRpcOpts, + command: string, + params?: Record, +) { + const nodeId = await deps.resolveNodeId(opts, normalizeOptionalString(opts.node) ?? ""); + const timeoutMs = deps.parseTimeoutMs(opts.invokeTimeout); + return await deps.callGatewayCli( + "node.invoke", + opts, + deps.buildNodeInvokeParams({ + nodeId, + command, + params, + timeoutMs: typeof timeoutMs === "number" ? timeoutMs : undefined, + }), + ); +} + +export function registerNodesCanvasCommands(nodes: Command, deps: CanvasCliDependencies) { + const canvas = nodes + .command("canvas") + .description("Capture or render canvas content from a paired node"); + + deps.nodesCallOpts( + canvas + .command("snapshot") + .description("Capture a canvas snapshot (prints MEDIA:)") + .requiredOption("--node ", "Node id, name, or IP") + .option("--format ", "Image format", "jpg") + .option("--max-width ", "Max width in px (optional)") + .option("--quality <0-1>", "JPEG quality (optional)") + .option("--invoke-timeout ", "Node invoke timeout in ms (default 20000)", "20000") + .action(async (opts: CanvasNodesRpcOpts) => { + await deps.runNodesCommand("canvas snapshot", async () => { + const formatOpt = normalizeLowercaseStringOrEmpty( + normalizeOptionalString(opts.format) ?? "jpg", + ); + const formatForParams = + formatOpt === "jpg" ? "jpeg" : formatOpt === "jpeg" ? "jpeg" : "png"; + if (formatForParams !== "png" && formatForParams !== "jpeg") { + throw new Error(`invalid format: ${String(opts.format)} (expected png|jpg|jpeg)`); + } + + const maxWidth = opts.maxWidth ? Number.parseInt(opts.maxWidth, 10) : undefined; + const quality = opts.quality ? Number.parseFloat(opts.quality) : undefined; + const raw = await invokeCanvas(deps, opts, "canvas.snapshot", { + format: formatForParams, + maxWidth: Number.isFinite(maxWidth) ? maxWidth : undefined, + quality: Number.isFinite(quality) ? quality : undefined, + }); + const res = typeof raw === "object" && raw !== null ? (raw as { payload?: unknown }) : {}; + const payload = parseCanvasSnapshotPayload(res.payload); + const filePath = canvasSnapshotTempPath({ + ext: payload.format === "jpeg" ? "jpg" : payload.format, + }); + await deps.writeBase64ToFile(filePath, payload.base64); + + if (opts.json) { + deps.defaultRuntime.writeJson({ file: { path: filePath, format: payload.format } }); + return; + } + deps.defaultRuntime.log(`MEDIA:${deps.shortenHomePath(filePath)}`); + }); + }), + { timeoutMs: 60_000 }, + ); + + deps.nodesCallOpts( + canvas + .command("present") + .description("Show the canvas (optionally with a target URL/path)") + .requiredOption("--node ", "Node id, name, or IP") + .option("--target ", "Target URL/path (optional)") + .option("--x ", "Placement x coordinate") + .option("--y ", "Placement y coordinate") + .option("--width ", "Placement width") + .option("--height ", "Placement height") + .option("--invoke-timeout ", "Node invoke timeout in ms") + .action(async (opts: CanvasNodesRpcOpts) => { + await deps.runNodesCommand("canvas present", async () => { + const placement = { + x: opts.x ? Number.parseFloat(opts.x) : undefined, + y: opts.y ? Number.parseFloat(opts.y) : undefined, + width: opts.width ? Number.parseFloat(opts.width) : undefined, + height: opts.height ? Number.parseFloat(opts.height) : undefined, + }; + const params: Record = {}; + if (opts.target) { + params.url = opts.target; + } + if ( + Number.isFinite(placement.x) || + Number.isFinite(placement.y) || + Number.isFinite(placement.width) || + Number.isFinite(placement.height) + ) { + params.placement = placement; + } + await invokeCanvas(deps, opts, "canvas.present", params); + if (!opts.json) { + const { ok } = deps.getNodesTheme(); + deps.defaultRuntime.log(ok("canvas present ok")); + } + }); + }), + ); + + deps.nodesCallOpts( + canvas + .command("hide") + .description("Hide the canvas") + .requiredOption("--node ", "Node id, name, or IP") + .option("--invoke-timeout ", "Node invoke timeout in ms") + .action(async (opts: CanvasNodesRpcOpts) => { + await deps.runNodesCommand("canvas hide", async () => { + await invokeCanvas(deps, opts, "canvas.hide", undefined); + if (!opts.json) { + const { ok } = deps.getNodesTheme(); + deps.defaultRuntime.log(ok("canvas hide ok")); + } + }); + }), + ); + + deps.nodesCallOpts( + canvas + .command("navigate") + .description("Navigate the canvas to a URL") + .argument("", "Target URL/path") + .requiredOption("--node ", "Node id, name, or IP") + .option("--invoke-timeout ", "Node invoke timeout in ms") + .action(async (url: string, opts: CanvasNodesRpcOpts) => { + await deps.runNodesCommand("canvas navigate", async () => { + await invokeCanvas(deps, opts, "canvas.navigate", { url }); + if (!opts.json) { + const { ok } = deps.getNodesTheme(); + deps.defaultRuntime.log(ok("canvas navigate ok")); + } + }); + }), + ); + + deps.nodesCallOpts( + canvas + .command("eval") + .description("Evaluate JavaScript in the canvas") + .argument("[js]", "JavaScript to evaluate") + .option("--js ", "JavaScript to evaluate") + .requiredOption("--node ", "Node id, name, or IP") + .option("--invoke-timeout ", "Node invoke timeout in ms") + .action(async (jsArg: string | undefined, opts: CanvasNodesRpcOpts) => { + await deps.runNodesCommand("canvas eval", async () => { + const js = opts.js ?? jsArg; + if (!js) { + throw new Error("missing --js or "); + } + const raw = await invokeCanvas(deps, opts, "canvas.eval", { + javaScript: js, + }); + if (opts.json) { + deps.defaultRuntime.writeJson(raw); + return; + } + const payload = + typeof raw === "object" && raw !== null + ? (raw as { payload?: { result?: string } }).payload + : undefined; + if (payload?.result) { + deps.defaultRuntime.log(payload.result); + } else { + const { ok } = deps.getNodesTheme(); + deps.defaultRuntime.log(ok("canvas eval ok")); + } + }); + }), + ); + + const a2ui = canvas.command("a2ui").description("Render A2UI content on the canvas"); + + deps.nodesCallOpts( + a2ui + .command("push") + .description("Push A2UI JSONL to the canvas") + .option("--jsonl ", "Path to JSONL payload") + .option("--text ", "Render a quick A2UI text payload") + .requiredOption("--node ", "Node id, name, or IP") + .option("--invoke-timeout ", "Node invoke timeout in ms") + .action(async (opts: CanvasNodesRpcOpts) => { + await deps.runNodesCommand("canvas a2ui push", async () => { + const hasJsonl = Boolean(opts.jsonl); + const hasText = typeof opts.text === "string"; + if (hasJsonl === hasText) { + throw new Error("provide exactly one of --jsonl or --text"); + } + + const jsonl = hasText + ? buildA2UITextJsonl(opts.text ?? "") + : await fs.readFile(String(opts.jsonl), "utf8"); + const { version, messageCount } = validateA2UIJsonl(jsonl); + if (version === "v0.9") { + throw new Error( + "Detected A2UI v0.9 JSONL (createSurface). OpenClaw currently supports v0.8 only.", + ); + } + await invokeCanvas(deps, opts, "canvas.a2ui.pushJSONL", { jsonl }); + if (!opts.json) { + const { ok } = deps.getNodesTheme(); + deps.defaultRuntime.log( + ok( + `canvas a2ui push ok (v0.8, ${messageCount} message${messageCount === 1 ? "" : "s"})`, + ), + ); + } + }); + }), + ); + + deps.nodesCallOpts( + a2ui + .command("reset") + .description("Reset A2UI renderer state") + .requiredOption("--node ", "Node id, name, or IP") + .option("--invoke-timeout ", "Node invoke timeout in ms") + .action(async (opts: CanvasNodesRpcOpts) => { + await deps.runNodesCommand("canvas a2ui reset", async () => { + await invokeCanvas(deps, opts, "canvas.a2ui.reset", undefined); + if (!opts.json) { + const { ok } = deps.getNodesTheme(); + deps.defaultRuntime.log(ok("canvas a2ui reset ok")); + } + }); + }), + ); +} diff --git a/extensions/canvas/src/config-migration.test.ts b/extensions/canvas/src/config-migration.test.ts new file mode 100644 index 00000000000..7040eb04af5 --- /dev/null +++ b/extensions/canvas/src/config-migration.test.ts @@ -0,0 +1,75 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; +import { describe, expect, test } from "vitest"; +import { migrateLegacyCanvasHostConfig } from "./config-migration.js"; + +describe("migrateLegacyCanvasHostConfig", () => { + test("moves legacy canvasHost into the Canvas plugin config", () => { + const result = migrateLegacyCanvasHostConfig({ + canvasHost: { + enabled: false, + root: "~/canvas", + liveReload: false, + }, + } as OpenClawConfig); + + expect(result?.changes).toEqual(["migrated canvasHost to plugins.entries.canvas.config.host"]); + expect(result?.config).toEqual({ + plugins: { + entries: { + canvas: { + config: { + host: { + enabled: false, + root: "~/canvas", + liveReload: false, + }, + }, + }, + }, + }, + }); + }); + + test("preserves plugin-owned Canvas host values when both shapes exist", () => { + const result = migrateLegacyCanvasHostConfig({ + canvasHost: { + enabled: false, + root: "~/legacy-canvas", + liveReload: false, + }, + plugins: { + entries: { + canvas: { + enabled: true, + config: { + host: { + root: "~/plugin-canvas", + }, + }, + }, + }, + }, + } as OpenClawConfig); + + expect(result?.config).toEqual({ + plugins: { + entries: { + canvas: { + enabled: true, + config: { + host: { + enabled: false, + root: "~/plugin-canvas", + liveReload: false, + }, + }, + }, + }, + }, + }); + }); + + test("ignores configs without legacy canvasHost", () => { + expect(migrateLegacyCanvasHostConfig({} as OpenClawConfig)).toBeNull(); + }); +}); diff --git a/extensions/canvas/src/config-migration.ts b/extensions/canvas/src/config-migration.ts new file mode 100644 index 00000000000..fe7180edf73 --- /dev/null +++ b/extensions/canvas/src/config-migration.ts @@ -0,0 +1,54 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; +import { isRecord } from "openclaw/plugin-sdk/text-runtime"; + +type MutableRecord = Record; + +function readRecord(value: unknown): MutableRecord | undefined { + return isRecord(value) ? (value as MutableRecord) : undefined; +} + +function mergeHostConfig(params: { + legacyHost: MutableRecord; + existingHost: MutableRecord | undefined; +}): MutableRecord { + return { + ...params.legacyHost, + ...(params.existingHost ?? {}), + }; +} + +export function migrateLegacyCanvasHostConfig(config: OpenClawConfig): { + config: OpenClawConfig; + changes: string[]; +} | null { + const legacyHost = readRecord((config as { canvasHost?: unknown }).canvasHost); + if (!legacyHost) { + return null; + } + + const plugins = structuredClone(readRecord(config.plugins) ?? {}); + const entries = readRecord(plugins.entries) ?? {}; + const canvasEntry = readRecord(entries.canvas) ?? {}; + const canvasConfig = readRecord(canvasEntry.config) ?? {}; + const existingHost = readRecord(canvasConfig.host); + + entries.canvas = { + ...canvasEntry, + config: { + ...canvasConfig, + host: mergeHostConfig({ + legacyHost, + existingHost, + }), + }, + }; + plugins.entries = entries; + + const next = { ...config, plugins } as OpenClawConfig & { canvasHost?: unknown }; + delete next.canvasHost; + + return { + config: next, + changes: ["migrated canvasHost to plugins.entries.canvas.config.host"], + }; +} diff --git a/extensions/canvas/src/config.test.ts b/extensions/canvas/src/config.test.ts new file mode 100644 index 00000000000..69786174c04 --- /dev/null +++ b/extensions/canvas/src/config.test.ts @@ -0,0 +1,87 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { + isCanvasHostEnabled, + isCanvasPluginEnabled, + parseCanvasPluginConfig, + resolveCanvasHostConfig, +} from "./config.js"; + +describe("Canvas plugin config", () => { + const originalSkipCanvasHost = process.env.OPENCLAW_SKIP_CANVAS_HOST; + + afterEach(() => { + if (originalSkipCanvasHost === undefined) { + delete process.env.OPENCLAW_SKIP_CANVAS_HOST; + } else { + process.env.OPENCLAW_SKIP_CANVAS_HOST = originalSkipCanvasHost; + } + }); + + it("parses host config from the plugin entry", () => { + expect( + parseCanvasPluginConfig({ + host: { + enabled: false, + root: "~/canvas", + port: 18793, + liveReload: false, + ignored: true, + }, + }), + ).toEqual({ + host: { + enabled: false, + root: "~/canvas", + port: 18793, + liveReload: false, + }, + }); + }); + + it("resolves host config from the plugin entry only", () => { + expect( + resolveCanvasHostConfig({ + config: { + plugins: { + entries: { + canvas: { + config: { + host: { + enabled: false, + root: "/plugin", + liveReload: false, + }, + }, + }, + }, + }, + }, + }), + ).toEqual({ + enabled: false, + root: "/plugin", + liveReload: false, + }); + }); + + it("disables the host when the bundled Canvas plugin is disabled", () => { + const config = { + plugins: { + entries: { + canvas: { + enabled: false, + }, + }, + }, + }; + expect(isCanvasPluginEnabled(config)).toBe(false); + expect(isCanvasHostEnabled(config)).toBe(false); + }); + + it("honors truthy skip-canvas env values before host registration", () => { + for (const value of ["1", "true", " yes ", "ON"]) { + process.env.OPENCLAW_SKIP_CANVAS_HOST = value; + expect(isCanvasHostEnabled()).toBe(false); + } + }); +}); diff --git a/extensions/canvas/src/config.ts b/extensions/canvas/src/config.ts new file mode 100644 index 00000000000..4176b72e172 --- /dev/null +++ b/extensions/canvas/src/config.ts @@ -0,0 +1,126 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; +import { + normalizePluginsConfig, + resolveEffectiveEnableState, + resolvePluginConfigObject, +} from "openclaw/plugin-sdk/plugin-config-runtime"; +import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env"; + +export type CanvasHostConfig = { + enabled?: boolean; + root?: string; + port?: number; + liveReload?: boolean; +}; + +export type CanvasPluginConfig = { + host?: CanvasHostConfig; +}; + +type CanvasPluginConfigSchema = { + parse: (value: unknown) => CanvasPluginConfig; + uiHints: Record; +}; + +function isRecord(value: unknown): value is Record { + return Boolean(value && typeof value === "object" && !Array.isArray(value)); +} + +function readBoolean(value: unknown): boolean | undefined { + return typeof value === "boolean" ? value : undefined; +} + +function readString(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined; +} + +function readPositiveInteger(value: unknown): number | undefined { + return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined; +} + +function parseCanvasHostConfig(value: unknown): CanvasHostConfig | undefined { + if (!isRecord(value)) { + return undefined; + } + return { + ...(readBoolean(value.enabled) !== undefined ? { enabled: readBoolean(value.enabled) } : {}), + ...(readString(value.root) !== undefined ? { root: readString(value.root) } : {}), + ...(readPositiveInteger(value.port) !== undefined + ? { port: readPositiveInteger(value.port) } + : {}), + ...(readBoolean(value.liveReload) !== undefined + ? { liveReload: readBoolean(value.liveReload) } + : {}), + }; +} + +export function parseCanvasPluginConfig(value: unknown): CanvasPluginConfig { + if (!isRecord(value)) { + return {}; + } + const host = parseCanvasHostConfig(value.host); + return { + ...(host ? { host } : {}), + }; +} + +export function isCanvasPluginEnabled(config?: OpenClawConfig): boolean { + if (!config) { + return true; + } + return resolveEffectiveEnableState({ + id: "canvas", + origin: "bundled", + config: normalizePluginsConfig(config.plugins), + rootConfig: config, + enabledByDefault: true, + }).enabled; +} + +export function resolveCanvasHostConfig(params: { + config?: OpenClawConfig; + pluginConfig?: Record; +}): CanvasHostConfig { + const pluginConfig = + params.pluginConfig ?? resolvePluginConfigObject(params.config, "canvas") ?? {}; + const parsedPluginConfig = parseCanvasPluginConfig(pluginConfig); + return parsedPluginConfig.host ?? {}; +} + +export function isCanvasHostEnabled(config?: OpenClawConfig): boolean { + if (isTruthyEnvValue(process.env.OPENCLAW_SKIP_CANVAS_HOST)) { + return false; + } + if (!isCanvasPluginEnabled(config)) { + return false; + } + return resolveCanvasHostConfig({ config }).enabled !== false; +} + +export const canvasConfigSchema: CanvasPluginConfigSchema = { + parse: parseCanvasPluginConfig, + uiHints: { + host: { + label: "Canvas Host", + help: "Serves local Canvas and A2UI files for paired nodes.", + advanced: true, + }, + "host.enabled": { + label: "Canvas Host Enabled", + advanced: true, + }, + "host.root": { + label: "Canvas Host Root Directory", + help: "Directory to serve. Defaults to the OpenClaw state canvas directory.", + advanced: true, + }, + "host.port": { + label: "Canvas Host Port", + advanced: true, + }, + "host.liveReload": { + label: "Canvas Host Live Reload", + advanced: true, + }, + }, +}; diff --git a/src/gateway/canvas-documents.test.ts b/extensions/canvas/src/documents.test.ts similarity index 99% rename from src/gateway/canvas-documents.test.ts rename to extensions/canvas/src/documents.test.ts index d01b0bb18de..addd6aabc3f 100644 --- a/src/gateway/canvas-documents.test.ts +++ b/extensions/canvas/src/documents.test.ts @@ -8,7 +8,7 @@ import { resolveCanvasDocumentAssets, resolveCanvasDocumentDir, resolveCanvasHttpPathToLocalPath, -} from "./canvas-documents.js"; +} from "./documents.js"; const tempDirs: string[] = []; diff --git a/src/gateway/canvas-documents.ts b/extensions/canvas/src/documents.ts similarity index 97% rename from src/gateway/canvas-documents.ts rename to extensions/canvas/src/documents.ts index 16c084c06da..8c4a24185fc 100644 --- a/src/gateway/canvas-documents.ts +++ b/extensions/canvas/src/documents.ts @@ -1,10 +1,10 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; -import { CANVAS_HOST_PATH } from "../canvas-host/a2ui.js"; -import { resolveStateDir } from "../config/paths.js"; -import { root as fsRoot, sanitizeUntrustedFileName } from "../infra/fs-safe.js"; -import { resolveUserPath } from "../utils.js"; +import { root as fsRoot, sanitizeUntrustedFileName } from "openclaw/plugin-sdk/security-runtime"; +import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; +import { resolveUserPath } from "openclaw/plugin-sdk/text-runtime"; +import { CANVAS_HOST_PATH } from "./host/a2ui.js"; type CanvasDocumentKind = "html_bundle" | "url_embed" | "document" | "image" | "video_asset"; diff --git a/src/infra/canvas-host-url.test.ts b/extensions/canvas/src/host-url.test.ts similarity index 97% rename from src/infra/canvas-host-url.test.ts rename to extensions/canvas/src/host-url.test.ts index 26ae9720e07..360060e3590 100644 --- a/src/infra/canvas-host-url.test.ts +++ b/extensions/canvas/src/host-url.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { resolveCanvasHostUrl } from "./canvas-host-url.js"; +import { resolveCanvasHostUrl } from "./host-url.js"; describe("resolveCanvasHostUrl", () => { it.each([ diff --git a/extensions/canvas/src/host-url.ts b/extensions/canvas/src/host-url.ts new file mode 100644 index 00000000000..028a798b0aa --- /dev/null +++ b/extensions/canvas/src/host-url.ts @@ -0,0 +1,15 @@ +import { + resolveHostedPluginSurfaceUrl, + type HostedPluginSurfaceUrlParams, +} from "openclaw/plugin-sdk/gateway-runtime"; + +type CanvasHostUrlParams = Omit & { + canvasPort?: number; +}; + +export function resolveCanvasHostUrl(params: CanvasHostUrlParams) { + return resolveHostedPluginSurfaceUrl({ + ...params, + port: params.canvasPort, + }); +} diff --git a/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js b/extensions/canvas/src/host/a2ui-app/bootstrap.js similarity index 87% rename from apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js rename to extensions/canvas/src/host/a2ui-app/bootstrap.js index b2d03165aa3..dfebeaed277 100644 --- a/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js +++ b/extensions/canvas/src/host/a2ui-app/bootstrap.js @@ -1,10 +1,9 @@ -import { html, css, LitElement, unsafeCSS } from "lit"; -import { repeat } from "lit/directives/repeat.js"; -import { ContextProvider } from "@lit/context"; - import { v0_8 } from "@a2ui/lit"; -import "@a2ui/lit/ui"; +import { ContextProvider } from "@lit/context"; import { themeContext } from "@openclaw/a2ui-theme-context"; +import { html, css, LitElement, unsafeCSS } from "lit"; +import "@a2ui/lit/ui"; +import { repeat } from "lit/directives/repeat.js"; const modalStyles = css` dialog { @@ -97,8 +96,12 @@ const textHintStyles = () => ({ h1: {}, h2: {}, h3: {}, h4: {}, h5: {}, body: {} const isAndroid = /Android/i.test(globalThis.navigator?.userAgent ?? ""); const cardShadow = isAndroid ? "0 2px 10px rgba(0,0,0,.18)" : "0 10px 30px rgba(0,0,0,.35)"; -const buttonShadow = isAndroid ? "0 2px 10px rgba(6, 182, 212, 0.14)" : "0 10px 25px rgba(6, 182, 212, 0.18)"; -const statusShadow = isAndroid ? "0 2px 10px rgba(0, 0, 0, 0.18)" : "0 10px 24px rgba(0, 0, 0, 0.25)"; +const buttonShadow = isAndroid + ? "0 2px 10px rgba(6, 182, 212, 0.14)" + : "0 10px 25px rgba(6, 182, 212, 0.18)"; +const statusShadow = isAndroid + ? "0 2px 10px rgba(0, 0, 0, 0.18)" + : "0 10px 24px rgba(0, 0, 0, 0.25)"; const statusBlur = isAndroid ? "10px" : "14px"; const openclawTheme = { @@ -125,7 +128,11 @@ const openclawTheme = { MultipleChoice: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() }, Row: emptyClasses(), Slider: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() }, - Tabs: { container: emptyClasses(), element: emptyClasses(), controls: { all: emptyClasses(), selected: emptyClasses() } }, + Tabs: { + container: emptyClasses(), + element: emptyClasses(), + controls: { all: emptyClasses(), selected: emptyClasses() }, + }, Text: { all: emptyClasses(), h1: emptyClasses(), @@ -235,11 +242,8 @@ class OpenClawA2UIHost extends LitElement { height: 100%; position: relative; box-sizing: border-box; - padding: - var(--openclaw-a2ui-inset-top, 0px) - var(--openclaw-a2ui-inset-right, 0px) - var(--openclaw-a2ui-inset-bottom, 0px) - var(--openclaw-a2ui-inset-left, 0px); + padding: var(--openclaw-a2ui-inset-top, 0px) var(--openclaw-a2ui-inset-right, 0px) + var(--openclaw-a2ui-inset-bottom, 0px) var(--openclaw-a2ui-inset-left, 0px); } #surfaces { @@ -264,7 +268,12 @@ class OpenClawA2UIHost extends LitElement { background: rgba(0, 0, 0, 0.45); border: 1px solid rgba(255, 255, 255, 0.18); color: rgba(255, 255, 255, 0.92); - font: 13px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Roboto", sans-serif; + font: + 13px/1.2 system-ui, + -apple-system, + BlinkMacSystemFont, + "Roboto", + sans-serif; pointer-events: none; backdrop-filter: blur(${unsafeCSS(statusBlur)}); -webkit-backdrop-filter: blur(${unsafeCSS(statusBlur)}); @@ -285,7 +294,12 @@ class OpenClawA2UIHost extends LitElement { background: rgba(0, 0, 0, 0.45); border: 1px solid rgba(255, 255, 255, 0.18); color: rgba(255, 255, 255, 0.92); - font: 13px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Roboto", sans-serif; + font: + 13px/1.2 system-ui, + -apple-system, + BlinkMacSystemFont, + "Roboto", + sans-serif; pointer-events: none; backdrop-filter: blur(${unsafeCSS(statusBlur)}); -webkit-backdrop-filter: blur(${unsafeCSS(statusBlur)}); @@ -360,7 +374,10 @@ class OpenClawA2UIHost extends LitElement { } #makeActionId() { - return globalThis.crypto?.randomUUID?.() ?? `a2ui_${Date.now()}_${Math.random().toString(16).slice(2)}`; + return ( + globalThis.crypto?.randomUUID?.() ?? + `a2ui_${Date.now()}_${Math.random().toString(16).slice(2)}` + ); } #setToast(text, kind = "ok", timeoutMs = 1400) { @@ -377,8 +394,12 @@ class OpenClawA2UIHost extends LitElement { #handleActionStatus(evt) { const detail = evt?.detail ?? null; - if (!detail || typeof detail.id !== "string") {return;} - if (!this.pendingAction || this.pendingAction.id !== detail.id) {return;} + if (!detail || typeof detail.id !== "string") { + return; + } + if (!this.pendingAction || this.pendingAction.id !== detail.id) { + return; + } if (detail.ok) { this.pendingAction = { ...this.pendingAction, phase: "sent", sentAt: Date.now() }; @@ -421,7 +442,9 @@ class OpenClawA2UIHost extends LitElement { for (const item of ctxItems) { const key = item?.key; const value = item?.value ?? null; - if (!key || !value) {continue;} + if (!key || !value) { + continue; + } if (typeof value.path === "string") { const resolved = sourceNode @@ -474,11 +497,23 @@ class OpenClawA2UIHost extends LitElement { } } catch (e) { const msg = String(e?.message ?? e); - this.pendingAction = { id: actionId, name, phase: "error", startedAt: Date.now(), error: msg }; + this.pendingAction = { + id: actionId, + name, + phase: "error", + startedAt: Date.now(), + error: msg, + }; this.#setToast(`Failed: ${msg}`, "error", 4500); } } else { - this.pendingAction = { id: actionId, name, phase: "error", startedAt: Date.now(), error: "missing native bridge" }; + this.pendingAction = { + id: actionId, + name, + phase: "error", + startedAt: Date.now(), + error: "missing native bridge", + }; this.#setToast("Failed: missing native bridge", "error", 4500); } } @@ -525,24 +560,28 @@ class OpenClawA2UIHost extends LitElement { ? `Failed: ${this.pendingAction.name}` : ""; - return html` - ${this.pendingAction && this.pendingAction.phase !== "error" - ? html`
${statusText}
` + return html` ${this.pendingAction && this.pendingAction.phase !== "error" + ? html`
+
+
${statusText}
+
` : ""} ${this.toast - ? html`
${this.toast.text}
` + ? html`
+ ${this.toast.text} +
` : ""}
- ${repeat( - this.surfaces, - ([surfaceId]) => surfaceId, - ([surfaceId, surface]) => html`` - )} -
`; + ${repeat( + this.surfaces, + ([surfaceId]) => surfaceId, + ([surfaceId, surface]) => html``, + )} + `; } } diff --git a/apps/shared/OpenClawKit/Tools/CanvasA2UI/rolldown.config.mjs b/extensions/canvas/src/host/a2ui-app/rolldown.config.mjs similarity index 80% rename from apps/shared/OpenClawKit/Tools/CanvasA2UI/rolldown.config.mjs rename to extensions/canvas/src/host/a2ui-app/rolldown.config.mjs index ccf1683d565..ab9cbfa64cc 100644 --- a/apps/shared/OpenClawKit/Tools/CanvasA2UI/rolldown.config.mjs +++ b/extensions/canvas/src/host/a2ui-app/rolldown.config.mjs @@ -1,22 +1,18 @@ -import path from "node:path"; import { existsSync } from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; import { fileURLToPath } from "node:url"; const here = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(here, "../../../../.."); +const require = createRequire(import.meta.url); const uiRoot = path.resolve(repoRoot, "ui"); const fromHere = (p) => path.resolve(here, p); -const outputFile = path.resolve( - here, - "../../../../..", - "src", - "canvas-host", - "a2ui", - "a2ui.bundle.js", -); +const outputFile = path.resolve(here, "..", "a2ui", "a2ui.bundle.js"); -const a2uiLitDist = path.resolve(repoRoot, "vendor/a2ui/renderers/lit/dist/src"); -const a2uiThemeContext = path.resolve(a2uiLitDist, "0.8/ui/context/theme.js"); +const a2uiLitIndex = require.resolve("@a2ui/lit"); +const a2uiLitUi = require.resolve("@a2ui/lit/ui"); +const a2uiThemeContext = path.resolve(path.dirname(a2uiLitUi), "context/theme.js"); const uiNodeModules = path.resolve(uiRoot, "node_modules"); const repoNodeModules = path.resolve(repoRoot, "node_modules"); @@ -46,8 +42,8 @@ export default { treeshake: false, resolve: { alias: { - "@a2ui/lit": path.resolve(a2uiLitDist, "index.js"), - "@a2ui/lit/ui": path.resolve(a2uiLitDist, "0.8/ui/ui.js"), + "@a2ui/lit": a2uiLitIndex, + "@a2ui/lit/ui": a2uiLitUi, "@openclaw/a2ui-theme-context": a2uiThemeContext, "@lit/context": resolveUiDependency("@lit/context"), "@lit/context/": resolveUiDependency("@lit/context/"), diff --git a/src/canvas-host/a2ui-shared.ts b/extensions/canvas/src/host/a2ui-shared.ts similarity index 96% rename from src/canvas-host/a2ui-shared.ts rename to extensions/canvas/src/host/a2ui-shared.ts index ee1897292ef..a1f8e063bd2 100644 --- a/src/canvas-host/a2ui-shared.ts +++ b/extensions/canvas/src/host/a2ui-shared.ts @@ -1,4 +1,4 @@ -import { lowercasePreservingWhitespace } from "../shared/string-coerce.js"; +import { lowercasePreservingWhitespace } from "openclaw/plugin-sdk/text-runtime"; export const A2UI_PATH = "/__openclaw__/a2ui"; diff --git a/src/canvas-host/a2ui.ts b/extensions/canvas/src/host/a2ui.ts similarity index 88% rename from src/canvas-host/a2ui.ts rename to extensions/canvas/src/host/a2ui.ts index 4b5bb5d3c53..6e2f6dbd64a 100644 --- a/src/canvas-host/a2ui.ts +++ b/extensions/canvas/src/host/a2ui.ts @@ -2,8 +2,8 @@ import fs from "node:fs/promises"; import type { IncomingMessage, ServerResponse } from "node:http"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { detectMime } from "../media/mime.js"; -import { lowercasePreservingWhitespace } from "../shared/string-coerce.js"; +import { detectMime } from "openclaw/plugin-sdk/media-mime"; +import { lowercasePreservingWhitespace } from "openclaw/plugin-sdk/text-runtime"; import { A2UI_PATH, injectCanvasLiveReload, isA2uiPath } from "./a2ui-shared.js"; import { resolveFileWithinRoot } from "./file-resolver.js"; @@ -24,24 +24,19 @@ async function resolveA2uiRoot(): Promise { const here = path.dirname(fileURLToPath(import.meta.url)); const entryDir = process.argv[1] ? path.dirname(path.resolve(process.argv[1])) : null; const candidates = [ - // Running from source (bun) or dist/canvas-host chunk. + // Running from source (bun) or a copied dist asset chunk. path.resolve(here, "a2ui"), // Running from dist root chunk (common launchd path). path.resolve(here, "canvas-host/a2ui"), - path.resolve(here, "../canvas-host/a2ui"), // Entry path fallbacks (helps when cwd is not the repo root). ...(entryDir - ? [ - path.resolve(entryDir, "a2ui"), - path.resolve(entryDir, "canvas-host/a2ui"), - path.resolve(entryDir, "../canvas-host/a2ui"), - ] + ? [path.resolve(entryDir, "a2ui"), path.resolve(entryDir, "canvas-host/a2ui")] : []), // Running from dist without copied assets (fallback to source). - path.resolve(here, "../../src/canvas-host/a2ui"), - path.resolve(here, "../src/canvas-host/a2ui"), + path.resolve(here, "../../extensions/canvas/src/host/a2ui"), + path.resolve(here, "../extensions/canvas/src/host/a2ui"), // Running from repo root. - path.resolve(process.cwd(), "src/canvas-host/a2ui"), + path.resolve(process.cwd(), "extensions/canvas/src/host/a2ui"), path.resolve(process.cwd(), "dist/canvas-host/a2ui"), ]; if (process.execPath) { diff --git a/extensions/canvas/src/host/a2ui/.bundle.hash b/extensions/canvas/src/host/a2ui/.bundle.hash new file mode 100644 index 00000000000..1d36360ff0e --- /dev/null +++ b/extensions/canvas/src/host/a2ui/.bundle.hash @@ -0,0 +1 @@ +9010c06425882ffb9300677df1255b4b2018edec1f03eafe78fda6bec84a0406 diff --git a/src/canvas-host/a2ui/index.html b/extensions/canvas/src/host/a2ui/index.html similarity index 100% rename from src/canvas-host/a2ui/index.html rename to extensions/canvas/src/host/a2ui/index.html diff --git a/src/canvas-host/file-resolver.test.ts b/extensions/canvas/src/host/file-resolver.test.ts similarity index 95% rename from src/canvas-host/file-resolver.test.ts rename to extensions/canvas/src/host/file-resolver.test.ts index f8877faf3d7..4ed34cfc2ba 100644 --- a/src/canvas-host/file-resolver.test.ts +++ b/extensions/canvas/src/host/file-resolver.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { createTrackedTempDirs } from "../test-utils/tracked-temp-dirs.js"; +import { createTrackedTempDirs } from "../../../../src/test-utils/tracked-temp-dirs.js"; import { normalizeUrlPath, resolveFileWithinRoot } from "./file-resolver.js"; const tempDirs = createTrackedTempDirs(); diff --git a/src/canvas-host/file-resolver.ts b/extensions/canvas/src/host/file-resolver.ts similarity index 84% rename from src/canvas-host/file-resolver.ts rename to extensions/canvas/src/host/file-resolver.ts index 6f5f1a2e758..2dace0fa2c8 100644 --- a/src/canvas-host/file-resolver.ts +++ b/extensions/canvas/src/host/file-resolver.ts @@ -1,5 +1,7 @@ import path from "node:path"; -import { root as fsRoot, FsSafeError, type OpenResult } from "../infra/fs-safe.js"; +import { root as fsRoot, FsSafeError } from "openclaw/plugin-sdk/security-runtime"; + +type CanvasOpenResult = Awaited>["open"]>>; export function normalizeUrlPath(rawPath: string): string { const decoded = decodeURIComponent(rawPath || "/"); @@ -10,7 +12,7 @@ export function normalizeUrlPath(rawPath: string): string { export async function resolveFileWithinRoot( rootReal: string, urlPath: string, -): Promise { +): Promise { const normalized = normalizeUrlPath(urlPath); const rel = normalized.replace(/^\/+/, ""); if (rel.split("/").some((p) => p === "..")) { diff --git a/src/canvas-host/server.state-dir.test.ts b/extensions/canvas/src/host/server.state-dir.test.ts similarity index 88% rename from src/canvas-host/server.state-dir.test.ts rename to extensions/canvas/src/host/server.state-dir.test.ts index d274b9826ed..7749331cd4f 100644 --- a/src/canvas-host/server.state-dir.test.ts +++ b/extensions/canvas/src/host/server.state-dir.test.ts @@ -1,8 +1,8 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { defaultRuntime } from "openclaw/plugin-sdk/runtime-env"; import { beforeAll, describe, expect, it } from "vitest"; -import { defaultRuntime } from "../runtime.js"; -import { withStateDirEnv } from "../test-helpers/state-dir-env.js"; +import { withStateDirEnv } from "../../../../src/test-helpers/state-dir-env.js"; describe("canvas host state dir defaults", () => { let createCanvasHostHandler: typeof import("./server.js").createCanvasHostHandler; diff --git a/src/canvas-host/server.test.ts b/extensions/canvas/src/host/server.test.ts similarity index 98% rename from src/canvas-host/server.test.ts rename to extensions/canvas/src/host/server.test.ts index 8f6e2ff8ba4..238906a33dd 100644 --- a/src/canvas-host/server.test.ts +++ b/extensions/canvas/src/host/server.test.ts @@ -3,8 +3,8 @@ import type { IncomingMessage } from "node:http"; import os from "node:os"; import path from "node:path"; import type { Duplex } from "node:stream"; +import { defaultRuntime } from "openclaw/plugin-sdk/runtime-env"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { defaultRuntime } from "../runtime.js"; import { A2UI_PATH, CANVAS_HOST_PATH, @@ -354,7 +354,7 @@ describe("canvas host", () => { }); it("serves A2UI scaffold and blocks traversal/symlink escapes", async () => { - const a2uiRoot = path.resolve(process.cwd(), "src/canvas-host/a2ui"); + const a2uiRoot = path.resolve(process.cwd(), "extensions/canvas/src/host/a2ui"); const bundlePath = path.join(a2uiRoot, "a2ui.bundle.js"); const linkName = `test-link-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`; const linkPath = path.join(a2uiRoot, linkName); diff --git a/src/canvas-host/server.ts b/extensions/canvas/src/host/server.ts similarity index 95% rename from src/canvas-host/server.ts rename to extensions/canvas/src/host/server.ts index ac69eed01f4..ba4c8744bc2 100644 --- a/src/canvas-host/server.ts +++ b/extensions/canvas/src/host/server.ts @@ -10,13 +10,16 @@ import { setTimeout as scheduleNativeTimeout, } from "node:timers"; import chokidar from "chokidar"; +import { detectMime } from "openclaw/plugin-sdk/media-mime"; +import { isTruthyEnvValue, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; +import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; +import { + ensureDir, + lowercasePreservingWhitespace, + normalizeOptionalString, + resolveUserPath, +} from "openclaw/plugin-sdk/text-runtime"; import { type WebSocket, WebSocketServer } from "ws"; -import { resolveStateDir } from "../config/paths.js"; -import { isTruthyEnvValue } from "../infra/env.js"; -import { detectMime } from "../media/mime.js"; -import type { RuntimeEnv } from "../runtime.js"; -import { lowercasePreservingWhitespace, normalizeOptionalString } from "../shared/string-coerce.js"; -import { ensureDir, resolveUserPath } from "../utils.js"; import { CANVAS_HOST_PATH, CANVAS_WS_PATH, @@ -169,9 +172,6 @@ function defaultIndexHTML() { } function isDisabledByEnv() { - if (isTruthyEnvValue(process.env.OPENCLAW_SKIP_CANVAS_HOST)) { - return true; - } if (isTruthyEnvValue(process.env.OPENCLAW_SKIP_CANVAS_HOST)) { return true; } @@ -321,7 +321,7 @@ export async function createCanvasHostHandler( } watcherClosed = true; opts.runtime.error( - `canvasHost watcher error: ${String(err)} (live reload disabled; consider canvasHost.liveReload=false or a smaller canvasHost.root)`, + `Canvas host watcher error: ${String(err)} (live reload disabled; consider plugins.entries.canvas.config.host.liveReload=false or a smaller plugins.entries.canvas.config.host.root)`, ); void watcher.close().catch(() => {}); }); @@ -412,7 +412,7 @@ export async function createCanvasHostHandler( res.end(data); return true; } catch (err) { - opts.runtime.error(`canvasHost request failed: ${String(err)}`); + opts.runtime.error(`Canvas host request failed: ${String(err)}`); res.statusCode = 500; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end("error"); @@ -482,7 +482,7 @@ export async function startCanvasHost(opts: CanvasHostServerOpts): Promise { - opts.runtime.error(`canvasHost request failed: ${String(err)}`); + opts.runtime.error(`Canvas host request failed: ${String(err)}`); res.statusCode = 500; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end("error"); diff --git a/extensions/canvas/src/http-route.ts b/extensions/canvas/src/http-route.ts new file mode 100644 index 00000000000..697744bdaca --- /dev/null +++ b/extensions/canvas/src/http-route.ts @@ -0,0 +1,72 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; +import type { Duplex } from "node:stream"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; +import { isCanvasHostEnabled, resolveCanvasHostConfig } from "./config.js"; +import { A2UI_PATH, CANVAS_HOST_PATH, CANVAS_WS_PATH, handleA2uiHttpRequest } from "./host/a2ui.js"; +import { createCanvasHostHandler, type CanvasHostHandler } from "./host/server.js"; + +export type CanvasHttpRouteHandler = { + handleHttpRequest: (req: IncomingMessage, res: ServerResponse) => Promise; + handleUpgrade: (req: IncomingMessage, socket: Duplex, head: Buffer) => Promise; + close: () => Promise; +}; + +export function createCanvasHttpRouteHandler(params: { + config: OpenClawConfig; + pluginConfig?: Record; + runtime: RuntimeEnv; + allowInTests?: boolean; +}): CanvasHttpRouteHandler { + let hostHandlerPromise: Promise | null = null; + const loadHostHandler = async (): Promise => { + if (!isCanvasHostEnabled(params.config)) { + return null; + } + hostHandlerPromise ??= (async () => { + const hostConfig = resolveCanvasHostConfig({ + config: params.config, + pluginConfig: params.pluginConfig, + }); + const handler = await createCanvasHostHandler({ + runtime: params.runtime, + rootDir: hostConfig.root, + basePath: CANVAS_HOST_PATH, + allowInTests: params.allowInTests, + liveReload: hostConfig.liveReload, + }); + return handler.rootDir ? handler : null; + })(); + return hostHandlerPromise; + }; + + return { + async handleHttpRequest(req, res) { + const handler = await loadHostHandler(); + if (!handler) { + return false; + } + const url = new URL(req.url ?? "/", "http://localhost"); + if (url.pathname === A2UI_PATH || url.pathname.startsWith(`${A2UI_PATH}/`)) { + return handleA2uiHttpRequest(req, res); + } + return handler.handleHttpRequest(req, res); + }, + async handleUpgrade(req, socket, head) { + const handler = await loadHostHandler(); + if (!handler) { + return false; + } + const url = new URL(req.url ?? "/", "http://localhost"); + if (url.pathname !== CANVAS_WS_PATH) { + return false; + } + return handler.handleUpgrade(req, socket, head); + }, + async close() { + const handler = hostHandlerPromise ? await hostHandlerPromise : null; + await handler?.close(); + hostHandlerPromise = null; + }, + }; +} diff --git a/extensions/canvas/src/tool.test.ts b/extensions/canvas/src/tool.test.ts new file mode 100644 index 00000000000..fc3b974bcc0 --- /dev/null +++ b/extensions/canvas/src/tool.test.ts @@ -0,0 +1,92 @@ +import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createCanvasTool } from "./tool.js"; + +const mocks = vi.hoisted(() => ({ + callGatewayTool: vi.fn(), + imageResultFromFile: vi.fn(async (params) => ({ content: [], details: params })), + listNodes: vi.fn(async () => []), + resolveNodeIdFromList: vi.fn(() => "node-1"), +})); + +vi.mock("openclaw/plugin-sdk/agent-harness-runtime", () => ({ + callGatewayTool: mocks.callGatewayTool, + listNodes: mocks.listNodes, + resolveNodeIdFromList: mocks.resolveNodeIdFromList, +})); + +vi.mock("openclaw/plugin-sdk/channel-actions", async (importOriginal) => ({ + ...(await importOriginal()), + imageResultFromFile: mocks.imageResultFromFile, +})); + +describe("Canvas tool", () => { + let tempRoot: string | undefined; + + beforeEach(() => { + mocks.callGatewayTool.mockReset(); + mocks.imageResultFromFile.mockClear(); + mocks.listNodes.mockClear(); + mocks.listNodes.mockResolvedValue([]); + mocks.resolveNodeIdFromList.mockClear(); + mocks.resolveNodeIdFromList.mockReturnValue("node-1"); + }); + + afterEach(async () => { + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + it.skipIf(process.platform === "win32")( + "rejects jsonlPath symlinks that resolve outside the workspace", + async () => { + tempRoot = await mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-tool-")); + const workspaceDir = path.join(tempRoot, "workspace"); + await mkdir(workspaceDir); + const outsidePath = path.join(tempRoot, "outside.jsonl"); + await writeFile(outsidePath, '{"secret":true}\n'); + await symlink(outsidePath, path.join(workspaceDir, "events.jsonl")); + + const tool = createCanvasTool({ workspaceDir }); + + await expect( + tool.execute("tool-call-1", { + action: "a2ui_push", + jsonlPath: "events.jsonl", + }), + ).rejects.toThrow("jsonlPath outside workspace"); + expect(mocks.callGatewayTool).not.toHaveBeenCalled(); + }, + ); + + it("applies configured image limits to canvas snapshots", async () => { + mocks.callGatewayTool.mockResolvedValue({ + payload: { + format: "png", + base64: Buffer.from("not-a-real-png").toString("base64"), + }, + }); + const tool = createCanvasTool({ + config: { + agents: { + defaults: { + imageMaxDimensionPx: 1600.9, + }, + }, + }, + }); + + await tool.execute("tool-call-1", { action: "snapshot" }); + + expect(mocks.imageResultFromFile).toHaveBeenCalledWith( + expect.objectContaining({ + label: "canvas:snapshot", + imageSanitization: { maxDimensionPx: 1600 }, + }), + ); + }); +}); diff --git a/src/agents/tools/canvas-tool.ts b/extensions/canvas/src/tool.ts similarity index 59% rename from src/agents/tools/canvas-tool.ts rename to extensions/canvas/src/tool.ts index 41c56e70678..cd216da536f 100644 --- a/src/agents/tools/canvas-tool.ts +++ b/extensions/canvas/src/tool.ts @@ -1,18 +1,21 @@ -import crypto from "node:crypto"; +import { randomUUID } from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { + callGatewayTool, + listNodes, + resolveNodeIdFromList, +} from "openclaw/plugin-sdk/agent-harness-runtime"; +import { + imageResultFromFile, + jsonResult, + optionalStringEnum, + readStringParam, + stringEnum, +} from "openclaw/plugin-sdk/channel-actions"; +import type { AnyAgentTool, OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry"; import { Type } from "typebox"; -import { writeBase64ToFile } from "../../cli/nodes-camera.js"; -import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "../../cli/nodes-canvas.js"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { logVerbose, shouldLogVerbose } from "../../globals.js"; -import { readLocalFileFromRoots } from "../../infra/fs-safe.js"; -import { getDefaultMediaLocalRoots } from "../../media/local-roots.js"; -import { imageMimeFromFormat } from "../../media/mime.js"; -import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; -import { resolveImageSanitizationLimits } from "../image-sanitization.js"; -import { optionalStringEnum, stringEnum } from "../schema/typebox.js"; -import { type AnyAgentTool, imageResult, jsonResult, readStringParam } from "./common.js"; -import { callGatewayTool, readGatewayCallOptions } from "./gateway.js"; -import { resolveNodeId } from "./nodes-utils.js"; const CANVAS_ACTIONS = [ "present", @@ -26,24 +29,90 @@ const CANVAS_ACTIONS = [ const CANVAS_SNAPSHOT_FORMATS = ["png", "jpg", "jpeg"] as const; -async function readJsonlFromPath(jsonlPath: string): Promise { +type CanvasToolOptions = { + config?: OpenClawConfig; + workspaceDir?: string; +}; + +type CanvasSnapshotPayload = { + format: string; + base64: string; +}; + +type CanvasImageSanitizationLimits = { + maxDimensionPx?: number; +}; + +function readGatewayCallOptions(params: Record) { + return { + gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }), + gatewayToken: readStringParam(params, "gatewayToken", { trim: false }), + timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined, + }; +} + +async function resolveNodeId( + opts: ReturnType, + query?: string, + allowDefault = false, +): Promise { + return resolveNodeIdFromList(await listNodes(opts), query, allowDefault); +} + +function parseCanvasSnapshotPayload(value: unknown): CanvasSnapshotPayload { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error("invalid canvas.snapshot payload"); + } + const record = value as Record; + const format = typeof record.format === "string" ? record.format : ""; + const base64 = typeof record.base64 === "string" ? record.base64 : ""; + if (!format || !base64) { + throw new Error("invalid canvas.snapshot payload"); + } + return { format, base64 }; +} + +async function writeBase64ToTempFile(params: { base64: string; ext: string }): Promise { + const dir = path.join(os.tmpdir(), "openclaw"); + await fs.mkdir(dir, { recursive: true, mode: 0o700 }); + const ext = params.ext.startsWith(".") ? params.ext : `.${params.ext}`; + const filePath = path.join(dir, `openclaw-canvas-snapshot-${randomUUID()}${ext}`); + await fs.writeFile(filePath, Buffer.from(params.base64, "base64")); + return filePath; +} + +function isPathInsideRoot(root: string, candidate: string): boolean { + const relative = path.relative(root, candidate); + return ( + relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative)) + ); +} + +async function readJsonlFromPath(jsonlPath: string, workspaceDir?: string): Promise { const trimmed = jsonlPath.trim(); if (!trimmed) { return ""; } - const roots = getDefaultMediaLocalRoots(); - const result = await readLocalFileFromRoots({ - filePath: trimmed, - roots, - label: "canvas jsonlPath", - }); - if (!result) { - if (shouldLogVerbose()) { - logVerbose(`Blocked canvas jsonlPath outside allowed roots: ${trimmed}`); - } - throw new Error("jsonlPath outside allowed roots"); + const workspaceRoot = path.resolve(workspaceDir ?? process.cwd()); + const resolved = path.resolve(workspaceRoot, trimmed); + const [workspaceReal, resolvedReal] = await Promise.all([ + fs.realpath(workspaceRoot), + fs.realpath(resolved), + ]); + if (!isPathInsideRoot(workspaceReal, resolvedReal)) { + throw new Error("jsonlPath outside workspace"); } - return result.buffer.toString("utf8"); + return await fs.readFile(resolvedReal, "utf8"); +} + +function resolveCanvasImageSanitizationLimits( + config?: OpenClawConfig, +): CanvasImageSanitizationLimits { + const configured = config?.agents?.defaults?.imageMaxDimensionPx; + if (typeof configured !== "number" || !Number.isFinite(configured)) { + return {}; + } + return { maxDimensionPx: Math.max(1, Math.floor(configured)) }; } // Flattened schema: runtime validates per-action requirements. @@ -53,28 +122,23 @@ const CanvasToolSchema = Type.Object({ gatewayToken: Type.Optional(Type.String()), timeoutMs: Type.Optional(Type.Number()), node: Type.Optional(Type.String()), - // present target: Type.Optional(Type.String()), x: Type.Optional(Type.Number()), y: Type.Optional(Type.Number()), width: Type.Optional(Type.Number()), height: Type.Optional(Type.Number()), - // navigate url: Type.Optional(Type.String()), - // eval javaScript: Type.Optional(Type.String()), - // snapshot outputFormat: optionalStringEnum(CANVAS_SNAPSHOT_FORMATS), maxWidth: Type.Optional(Type.Number()), quality: Type.Optional(Type.Number()), delayMs: Type.Optional(Type.Number()), - // a2ui_push jsonl: Type.Optional(Type.String()), jsonlPath: Type.Optional(Type.String()), }); -export function createCanvasTool(options?: { config?: OpenClawConfig }): AnyAgentTool { - const imageSanitization = resolveImageSanitizationLimits(options?.config); +export function createCanvasTool(options?: CanvasToolOptions): AnyAgentTool { + const imageSanitization = resolveCanvasImageSanitizationLimits(options?.config); return { label: "Canvas", name: "canvas", @@ -97,7 +161,7 @@ export function createCanvasTool(options?: { config?: OpenClawConfig }): AnyAgen nodeId, command, params: invokeParams, - idempotencyKey: crypto.randomUUID(), + idempotencyKey: randomUUID(), }); switch (action) { @@ -109,8 +173,6 @@ export function createCanvasTool(options?: { config?: OpenClawConfig }): AnyAgen height: typeof params.height === "number" ? params.height : undefined, }; const invokeParams: Record = {}; - // Accept both `target` and `url` for present to match common caller expectations. - // `target` remains the canonical field for CLI compatibility. const presentTarget = readStringParam(params, "target", { trim: true }) ?? readStringParam(params, "url", { trim: true }); @@ -132,7 +194,6 @@ export function createCanvasTool(options?: { config?: OpenClawConfig }): AnyAgen await invoke("canvas.hide", undefined); return jsonResult({ ok: true }); case "navigate": { - // Support `target` as an alias so callers can reuse the same field across present/navigate. const url = readStringParam(params, "url", { trim: true }) ?? readStringParam(params, "target", { required: true, trim: true, label: "url" }); @@ -156,7 +217,10 @@ export function createCanvasTool(options?: { config?: OpenClawConfig }): AnyAgen return jsonResult({ ok: true }); } case "snapshot": { - const formatRaw = normalizeLowercaseStringOrEmpty(params.outputFormat) || "png"; + const formatRaw = + typeof params.outputFormat === "string" && params.outputFormat.trim() + ? params.outputFormat.trim().toLowerCase() + : "png"; const format = formatRaw === "jpg" || formatRaw === "jpeg" ? "jpeg" : "png"; const maxWidth = typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth) @@ -172,16 +236,13 @@ export function createCanvasTool(options?: { config?: OpenClawConfig }): AnyAgen quality, })) as { payload?: unknown }; const payload = parseCanvasSnapshotPayload(raw?.payload); - const filePath = canvasSnapshotTempPath({ + const filePath = await writeBase64ToTempFile({ + base64: payload.base64, ext: payload.format === "jpeg" ? "jpg" : payload.format, }); - await writeBase64ToFile(filePath, payload.base64); - const mimeType = imageMimeFromFormat(payload.format) ?? "image/png"; - return await imageResult({ + return await imageResultFromFile({ label: "canvas:snapshot", path: filePath, - base64: payload.base64, - mimeType, details: { format: payload.format }, imageSanitization, }); @@ -191,7 +252,7 @@ export function createCanvasTool(options?: { config?: OpenClawConfig }): AnyAgen typeof params.jsonl === "string" && params.jsonl.trim() ? params.jsonl : typeof params.jsonlPath === "string" && params.jsonlPath.trim() - ? await readJsonlFromPath(params.jsonlPath) + ? await readJsonlFromPath(params.jsonlPath, options?.workspaceDir) : ""; if (!jsonl.trim()) { throw new Error("jsonl or jsonlPath required"); diff --git a/extensions/codex/src/app-server/auth-bridge.ts b/extensions/codex/src/app-server/auth-bridge.ts index fba62d2d39c..9bef9470dd1 100644 --- a/extensions/codex/src/app-server/auth-bridge.ts +++ b/extensions/codex/src/app-server/auth-bridge.ts @@ -14,9 +14,11 @@ import { } from "openclaw/plugin-sdk/agent-runtime"; import type { CodexAppServerClient } from "./client.js"; import type { CodexAppServerStartOptions } from "./config.js"; -import type { ChatgptAuthTokensRefreshResponse } from "./protocol-generated/typescript/v2/ChatgptAuthTokensRefreshResponse.js"; -import type { GetAccountResponse } from "./protocol-generated/typescript/v2/GetAccountResponse.js"; -import type { LoginAccountParams } from "./protocol-generated/typescript/v2/LoginAccountParams.js"; +import type { + CodexChatgptAuthTokensRefreshResponse, + CodexGetAccountResponse, + CodexLoginAccountParams, +} from "./protocol.js"; import { resolveCodexAppServerSpawnEnv } from "./transport-stdio.js"; const CODEX_APP_SERVER_AUTH_PROVIDER = "openai-codex"; @@ -170,7 +172,7 @@ function resolveCodexAppServerAuthProfileLoginParams(params: { agentDir: string; authProfileId?: string; config?: AuthProfileOrderConfig; -}): Promise { +}): Promise { return resolveCodexAppServerAuthProfileLoginParamsInternal(params); } @@ -178,7 +180,7 @@ export async function refreshCodexAppServerAuthTokens(params: { agentDir: string; authProfileId?: string; config?: AuthProfileOrderConfig; -}): Promise { +}): Promise { const loginParams = await resolveCodexAppServerAuthProfileLoginParamsInternal({ ...params, forceOAuthRefresh: true, @@ -198,7 +200,7 @@ async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: { authProfileId?: string; forceOAuthRefresh?: boolean; config?: AuthProfileOrderConfig; -}): Promise { +}): Promise { const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false }); const profileId = resolveCodexAppServerAuthProfileId({ authProfileId: params.authProfileId, @@ -233,12 +235,12 @@ async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: { async function resolveCodexAppServerEnvApiKeyLoginParams(params: { client: CodexAppServerClient; env: NodeJS.ProcessEnv; -}): Promise { +}): Promise { const apiKey = readFirstNonEmptyEnv(params.env, CODEX_APP_SERVER_API_KEY_ENV_VARS); if (!apiKey) { return undefined; } - const response = await params.client.request("account/read", { + const response = await params.client.request("account/read", { refreshToken: false, }); if (response.account || !response.requiresOpenaiAuth) { @@ -251,7 +253,7 @@ async function resolveLoginParamsForCredential( profileId: string, credential: AuthProfileCredential, params: { agentDir: string; forceOAuthRefresh: boolean; config?: AuthProfileOrderConfig }, -): Promise { +): Promise { if (credential.type === "api_key") { const resolved = await resolveApiKeyForProfile({ store: ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false }), @@ -378,7 +380,7 @@ function buildChatgptAuthTokensParams( profileId: string, credential: AuthProfileCredential, accessToken: string, -): LoginAccountParams { +): CodexLoginAccountParams { return { type: "chatgptAuthTokens", accessToken, diff --git a/extensions/codex/src/app-server/computer-use.ts b/extensions/codex/src/app-server/computer-use.ts index 895ba584018..ff01ce0d5bf 100644 --- a/extensions/codex/src/app-server/computer-use.ts +++ b/extensions/codex/src/app-server/computer-use.ts @@ -7,8 +7,15 @@ import { type CodexComputerUseConfig, type ResolvedCodexComputerUseConfig, } from "./config.js"; -import type { v2 } from "./protocol-generated/typescript/index.js"; -import type { JsonValue } from "./protocol.js"; +import type { + CodexListMcpServerStatusResponse, + CodexMcpServerStatus, + CodexPluginDetail, + CodexPluginListResponse, + CodexPluginReadResponse, + CodexRequestObject, + JsonValue, +} from "./protocol.js"; import { requestCodexAppServerJson } from "./request.js"; export type CodexComputerUseRequest = ( @@ -83,7 +90,7 @@ type MarketplaceResolution = { type PluginInspection = | { ok: true; - plugin: v2.PluginDetail; + plugin: CodexPluginDetail; } | { ok: false; @@ -184,12 +191,9 @@ async function inspectCodexComputerUse(params: { }): Promise { const request = createComputerUseRequest(params); if (params.installPlugin) { - await request( - "experimentalFeature/enablement/set", - { - enablement: { plugins: true }, - } satisfies v2.ExperimentalFeatureEnablementSetParams, - ); + await request("experimentalFeature/enablement/set", { + enablement: { plugins: true }, + } satisfies CodexRequestObject); } const marketplace = await resolveMarketplaceRef({ @@ -262,12 +266,9 @@ async function ensureComputerUsePlugin(params: { }), }; } - await params.request( + await params.request( "plugin/install", - pluginRequestParams( - params.marketplace, - params.config.pluginName, - ) satisfies v2.PluginInstallParams, + pluginRequestParams(params.marketplace, params.config.pluginName), ); await reloadMcpServers(params.request); plugin = await readComputerUsePlugin( @@ -294,7 +295,7 @@ async function ensureComputerUsePlugin(params: { async function readComputerUseTools(params: { request: CodexComputerUseRequest; config: ResolvedCodexComputerUseConfig; - plugin: v2.PluginDetail; + plugin: CodexPluginDetail; installPlugin: boolean; }): Promise { let server = await readMcpServerStatus(params.request, params.config.mcpServerName); @@ -330,9 +331,9 @@ async function resolveMarketplaceRef(params: { }): Promise { let preferredMarketplaceName = params.config.marketplaceName; if (params.config.marketplaceSource && params.allowAdd) { - const added = await params.request("marketplace/add", { + const added = await params.request<{ marketplaceName?: string }>("marketplace/add", { source: params.config.marketplaceSource, - } satisfies v2.MarketplaceAddParams); + } satisfies CodexRequestObject); preferredMarketplaceName ??= added.marketplaceName; } @@ -347,9 +348,9 @@ async function resolveMarketplaceRef(params: { if (candidates.length === 0 && shouldAddBundledComputerUseMarketplace(params)) { const bundledMarketplacePath = params.defaultBundledMarketplacePath ?? DEFAULT_CODEX_BUNDLED_MARKETPLACE_PATH; - const added = await params.request("marketplace/add", { + const added = await params.request<{ marketplaceName?: string }>("marketplace/add", { source: bundledMarketplacePath, - } satisfies v2.MarketplaceAddParams); + } satisfies CodexRequestObject); preferredMarketplaceName ??= added.marketplaceName; candidates = await listComputerUseMarketplaceCandidates(params.request, params.config); } @@ -398,9 +399,9 @@ async function listComputerUseMarketplaceCandidates( request: CodexComputerUseRequest, config: ResolvedCodexComputerUseConfig, ): Promise { - const listed = await request("plugin/list", { + const listed = await request("plugin/list", { cwds: [], - } satisfies v2.PluginListParams); + } satisfies CodexRequestObject); return findComputerUseMarketplaces(listed, config.pluginName); } @@ -434,7 +435,7 @@ function shouldAddBundledComputerUseMarketplace(params: { } function findComputerUseMarketplaces( - listed: v2.PluginListResponse, + listed: CodexPluginListResponse, pluginName: string, ): MarketplaceRef[] { return listed.marketplaces @@ -509,10 +510,10 @@ async function readComputerUsePlugin( request: CodexComputerUseRequest, marketplace: MarketplaceRef, pluginName: string, -): Promise { - const response = await request( +): Promise { + const response = await request( "plugin/read", - pluginRequestParams(marketplace, pluginName) satisfies v2.PluginReadParams, + pluginRequestParams(marketplace, pluginName), ); return response.plugin; } @@ -520,14 +521,14 @@ async function readComputerUsePlugin( async function readMcpServerStatus( request: CodexComputerUseRequest, serverName: string, -): Promise { +): Promise { let cursor: string | null | undefined; do { - const response = await request("mcpServerStatus/list", { + const response = await request("mcpServerStatus/list", { cursor, limit: 100, detail: "toolsAndAuthOnly", - } satisfies v2.ListMcpServerStatusParams); + } satisfies CodexRequestObject); const found = response.data.find((server) => server.name === serverName); if (found) { return found; @@ -552,7 +553,7 @@ function pluginRequestParams(marketplace: MarketplaceRef, pluginName: string) { } function pluginSetupReason( - plugin: v2.PluginDetail, + plugin: CodexPluginDetail, marketplace: MarketplaceRef, ): CodexComputerUseStatusReason { if (marketplace.kind === "remote") { @@ -563,7 +564,7 @@ function pluginSetupReason( function pluginSetupMessage( config: ResolvedCodexComputerUseConfig, - plugin: v2.PluginDetail, + plugin: CodexPluginDetail, marketplace: MarketplaceRef, ): string { if (marketplace.kind === "remote") { @@ -576,7 +577,7 @@ function pluginSetupMessage( } function remoteInstallUnsupportedMessage( - plugin: v2.PluginDetail, + plugin: CodexPluginDetail, marketplace: MarketplaceRef, ): string { const marketplaceName = marketplace.name ?? plugin.marketplaceName; @@ -586,7 +587,7 @@ function remoteInstallUnsupportedMessage( function statusFromPlugin(params: { config: ResolvedCodexComputerUseConfig; - plugin: v2.PluginDetail; + plugin: CodexPluginDetail; tools: string[]; reason: CodexComputerUseStatusReason; message: string; diff --git a/extensions/codex/src/app-server/event-projector.ts b/extensions/codex/src/app-server/event-projector.ts index e21efe51604..be29bd098cd 100644 --- a/extensions/codex/src/app-server/event-projector.ts +++ b/extensions/codex/src/app-server/event-projector.ts @@ -1268,9 +1268,7 @@ function itemOutputText(item: CodexThreadItem): string | undefined { return undefined; } -function collectDynamicToolContentText( - contentItems: Extract["contentItems"], -): string { +function collectDynamicToolContentText(contentItems: CodexThreadItem["contentItems"]): string { if (!Array.isArray(contentItems)) { return ""; } diff --git a/extensions/codex/src/app-server/models.ts b/extensions/codex/src/app-server/models.ts index 4cd84436914..911677d0fff 100644 --- a/extensions/codex/src/app-server/models.ts +++ b/extensions/codex/src/app-server/models.ts @@ -1,8 +1,8 @@ import type { resolveCodexAppServerAuthProfileIdForAgent } from "./auth-bridge.js"; import type { CodexAppServerClient } from "./client.js"; import type { CodexAppServerStartOptions } from "./config.js"; -import type { v2 } from "./protocol-generated/typescript/index.js"; import { readCodexModelListResponse } from "./protocol-validators.js"; +import type { CodexModel, CodexReasoningEffortOption } from "./protocol.js"; export type CodexAppServerModel = { id: string; @@ -127,7 +127,7 @@ export function readModelListResult(value: unknown): CodexAppServerModelListResu return { models, ...(nextCursor ? { nextCursor } : {}) }; } -function readCodexModel(value: v2.Model): CodexAppServerModel | undefined { +function readCodexModel(value: CodexModel): CodexAppServerModel | undefined { const id = readNonEmptyString(value.id); const model = readNonEmptyString(value.model) ?? id; if (!id || !model) { @@ -152,7 +152,7 @@ function readCodexModel(value: v2.Model): CodexAppServerModel | undefined { }; } -function readReasoningEfforts(value: v2.ReasoningEffortOption[]): string[] { +function readReasoningEfforts(value: CodexReasoningEffortOption[]): string[] { const efforts = value .map((entry) => readNonEmptyString(entry.reasoningEffort)) .filter((entry): entry is string => entry !== undefined); diff --git a/extensions/codex/src/app-server/protocol-generated/json/DynamicToolCallParams.json b/extensions/codex/src/app-server/protocol-generated/json/DynamicToolCallParams.json index 812682bdce3..d002d9b6e88 100644 --- a/extensions/codex/src/app-server/protocol-generated/json/DynamicToolCallParams.json +++ b/extensions/codex/src/app-server/protocol-generated/json/DynamicToolCallParams.json @@ -2,13 +2,32 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "DynamicToolCallParams", "type": "object", - "required": ["arguments", "callId", "threadId", "tool", "turnId"], + "required": [ + "arguments", + "callId", + "threadId", + "tool", + "turnId" + ], "properties": { "arguments": true, - "callId": { "type": "string" }, - "namespace": { "type": ["string", "null"] }, - "threadId": { "type": "string" }, - "tool": { "type": "string" }, - "turnId": { "type": "string" } + "callId": { + "type": "string" + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "tool": { + "type": "string" + }, + "turnId": { + "type": "string" + } } } diff --git a/extensions/codex/src/app-server/protocol-generated/json/v2/ErrorNotification.json b/extensions/codex/src/app-server/protocol-generated/json/v2/ErrorNotification.json index 1f02701b0e7..d96b9e472a0 100644 --- a/extensions/codex/src/app-server/protocol-generated/json/v2/ErrorNotification.json +++ b/extensions/codex/src/app-server/protocol-generated/json/v2/ErrorNotification.json @@ -2,12 +2,25 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "ErrorNotification", "type": "object", - "required": ["error", "threadId", "turnId", "willRetry"], + "required": [ + "error", + "threadId", + "turnId", + "willRetry" + ], "properties": { - "error": { "$ref": "#/definitions/TurnError" }, - "threadId": { "type": "string" }, - "turnId": { "type": "string" }, - "willRetry": { "type": "boolean" } + "error": { + "$ref": "#/definitions/TurnError" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + }, + "willRetry": { + "type": "boolean" + } }, "definitions": { "CodexErrorInfo": { @@ -30,12 +43,21 @@ }, { "type": "object", - "required": ["httpConnectionFailed"], + "required": [ + "httpConnectionFailed" + ], "properties": { "httpConnectionFailed": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -45,12 +67,21 @@ { "description": "Failed to connect to the response SSE stream.", "type": "object", - "required": ["responseStreamConnectionFailed"], + "required": [ + "responseStreamConnectionFailed" + ], "properties": { "responseStreamConnectionFailed": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -60,12 +91,21 @@ { "description": "The response SSE stream disconnected in the middle of a turn before completion.", "type": "object", - "required": ["responseStreamDisconnected"], + "required": [ + "responseStreamDisconnected" + ], "properties": { "responseStreamDisconnected": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -75,12 +115,21 @@ { "description": "Reached the retry limit for responses.", "type": "object", - "required": ["responseTooManyFailedAttempts"], + "required": [ + "responseTooManyFailedAttempts" + ], "properties": { "responseTooManyFailedAttempts": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -90,12 +139,20 @@ { "description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", "type": "object", - "required": ["activeTurnNotSteerable"], + "required": [ + "activeTurnNotSteerable" + ], "properties": { "activeTurnNotSteerable": { "type": "object", - "required": ["turnKind"], - "properties": { "turnKind": { "$ref": "#/definitions/NonSteerableTurnKind" } } + "required": [ + "turnKind" + ], + "properties": { + "turnKind": { + "$ref": "#/definitions/NonSteerableTurnKind" + } + } } }, "additionalProperties": false, @@ -103,16 +160,39 @@ } ] }, - "NonSteerableTurnKind": { "type": "string", "enum": ["review", "compact"] }, + "NonSteerableTurnKind": { + "type": "string", + "enum": [ + "review", + "compact" + ] + }, "TurnError": { "type": "object", - "required": ["message"], + "required": [ + "message" + ], "properties": { - "additionalDetails": { "default": null, "type": ["string", "null"] }, - "codexErrorInfo": { - "anyOf": [{ "$ref": "#/definitions/CodexErrorInfo" }, { "type": "null" }] + "additionalDetails": { + "default": null, + "type": [ + "string", + "null" + ] }, - "message": { "type": "string" } + "codexErrorInfo": { + "anyOf": [ + { + "$ref": "#/definitions/CodexErrorInfo" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": "string" + } } } } diff --git a/extensions/codex/src/app-server/protocol-generated/json/v2/GetAccountResponse.json b/extensions/codex/src/app-server/protocol-generated/json/v2/GetAccountResponse.json index e5e26fe695c..8b9f7729e1a 100644 --- a/extensions/codex/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +++ b/extensions/codex/src/app-server/protocol-generated/json/v2/GetAccountResponse.json @@ -2,39 +2,78 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "GetAccountResponse", "type": "object", - "required": ["requiresOpenaiAuth"], + "required": [ + "requiresOpenaiAuth" + ], "properties": { - "account": { "anyOf": [{ "$ref": "#/definitions/Account" }, { "type": "null" }] }, - "requiresOpenaiAuth": { "type": "boolean" } + "account": { + "anyOf": [ + { + "$ref": "#/definitions/Account" + }, + { + "type": "null" + } + ] + }, + "requiresOpenaiAuth": { + "type": "boolean" + } }, "definitions": { "Account": { "oneOf": [ { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["apiKey"], "title": "ApiKeyAccountType" } + "type": { + "type": "string", + "enum": [ + "apiKey" + ], + "title": "ApiKeyAccountType" + } }, "title": "ApiKeyAccount" }, { "type": "object", - "required": ["email", "planType", "type"], + "required": [ + "email", + "planType", + "type" + ], "properties": { - "email": { "type": "string" }, - "planType": { "$ref": "#/definitions/PlanType" }, - "type": { "type": "string", "enum": ["chatgpt"], "title": "ChatgptAccountType" } + "email": { + "type": "string" + }, + "planType": { + "$ref": "#/definitions/PlanType" + }, + "type": { + "type": "string", + "enum": [ + "chatgpt" + ], + "title": "ChatgptAccountType" + } }, "title": "ChatgptAccount" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["amazonBedrock"], + "enum": [ + "amazonBedrock" + ], "title": "AmazonBedrockAccountType" } }, diff --git a/extensions/codex/src/app-server/protocol-generated/json/v2/ModelListResponse.json b/extensions/codex/src/app-server/protocol-generated/json/v2/ModelListResponse.json index 7f4c9ca9444..26453e005fa 100644 --- a/extensions/codex/src/app-server/protocol-generated/json/v2/ModelListResponse.json +++ b/extensions/codex/src/app-server/protocol-generated/json/v2/ModelListResponse.json @@ -2,12 +2,22 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "ModelListResponse", "type": "object", - "required": ["data"], + "required": [ + "data" + ], "properties": { - "data": { "type": "array", "items": { "$ref": "#/definitions/Model" } }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/Model" + } + }, "nextCursor": { "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] } }, "definitions": { @@ -17,12 +27,16 @@ { "description": "Plain text turns and tool payloads.", "type": "string", - "enum": ["text"] + "enum": [ + "text" + ] }, { "description": "Image attachments included in user turns.", "type": "string", - "enum": ["image"] + "enum": [ + "image" + ] } ] }, @@ -39,59 +53,174 @@ "supportedReasoningEfforts" ], "properties": { - "additionalSpeedTiers": { "default": [], "type": "array", "items": { "type": "string" } }, - "availabilityNux": { - "anyOf": [{ "$ref": "#/definitions/ModelAvailabilityNux" }, { "type": "null" }] - }, - "defaultReasoningEffort": { "$ref": "#/definitions/ReasoningEffort" }, - "description": { "type": "string" }, - "displayName": { "type": "string" }, - "hidden": { "type": "boolean" }, - "id": { "type": "string" }, - "inputModalities": { - "default": ["text", "image"], + "additionalSpeedTiers": { + "description": "Deprecated: use `serviceTiers` instead.", + "default": [], "type": "array", - "items": { "$ref": "#/definitions/InputModality" } + "items": { + "type": "string" + } + }, + "availabilityNux": { + "anyOf": [ + { + "$ref": "#/definitions/ModelAvailabilityNux" + }, + { + "type": "null" + } + ] + }, + "defaultReasoningEffort": { + "$ref": "#/definitions/ReasoningEffort" + }, + "description": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "hidden": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "inputModalities": { + "default": [ + "text", + "image" + ], + "type": "array", + "items": { + "$ref": "#/definitions/InputModality" + } + }, + "isDefault": { + "type": "boolean" + }, + "model": { + "type": "string" + }, + "serviceTiers": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/ModelServiceTier" + } }, - "isDefault": { "type": "boolean" }, - "model": { "type": "string" }, "supportedReasoningEfforts": { "type": "array", - "items": { "$ref": "#/definitions/ReasoningEffortOption" } + "items": { + "$ref": "#/definitions/ReasoningEffortOption" + } + }, + "supportsPersonality": { + "default": false, + "type": "boolean" + }, + "upgrade": { + "type": [ + "string", + "null" + ] }, - "supportsPersonality": { "default": false, "type": "boolean" }, - "upgrade": { "type": ["string", "null"] }, "upgradeInfo": { - "anyOf": [{ "$ref": "#/definitions/ModelUpgradeInfo" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/ModelUpgradeInfo" + }, + { + "type": "null" + } + ] } } }, "ModelAvailabilityNux": { "type": "object", - "required": ["message"], - "properties": { "message": { "type": "string" } } + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + }, + "ModelServiceTier": { + "type": "object", + "required": [ + "description", + "id", + "name" + ], + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } }, "ModelUpgradeInfo": { "type": "object", - "required": ["model"], + "required": [ + "model" + ], "properties": { - "migrationMarkdown": { "type": ["string", "null"] }, - "model": { "type": "string" }, - "modelLink": { "type": ["string", "null"] }, - "upgradeCopy": { "type": ["string", "null"] } + "migrationMarkdown": { + "type": [ + "string", + "null" + ] + }, + "model": { + "type": "string" + }, + "modelLink": { + "type": [ + "string", + "null" + ] + }, + "upgradeCopy": { + "type": [ + "string", + "null" + ] + } } }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "type": "string", - "enum": ["none", "minimal", "low", "medium", "high", "xhigh"] + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] }, "ReasoningEffortOption": { "type": "object", - "required": ["description", "reasoningEffort"], + "required": [ + "description", + "reasoningEffort" + ], "properties": { - "description": { "type": "string" }, - "reasoningEffort": { "$ref": "#/definitions/ReasoningEffort" } + "description": { + "type": "string" + }, + "reasoningEffort": { + "$ref": "#/definitions/ReasoningEffort" + } } } } diff --git a/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json b/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json index eac156d5773..d4800805328 100644 --- a/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +++ b/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json @@ -15,36 +15,86 @@ "activePermissionProfile": { "description": "Named or implicit built-in profile that produced the active permissions, when known.", "default": null, - "anyOf": [{ "$ref": "#/definitions/ActivePermissionProfile" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/ActivePermissionProfile" + }, + { + "type": "null" + } + ] + }, + "approvalPolicy": { + "$ref": "#/definitions/AskForApproval" }, - "approvalPolicy": { "$ref": "#/definitions/AskForApproval" }, "approvalsReviewer": { "description": "Reviewer currently used for approval requests on this thread.", - "allOf": [{ "$ref": "#/definitions/ApprovalsReviewer" }] + "allOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + } + ] + }, + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" }, - "cwd": { "$ref": "#/definitions/AbsolutePathBuf" }, "instructionSources": { "description": "Instruction source files currently loaded for this thread.", "default": [], "type": "array", - "items": { "$ref": "#/definitions/AbsolutePathBuf" } + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + } + }, + "model": { + "type": "string" + }, + "modelProvider": { + "type": "string" }, - "model": { "type": "string" }, - "modelProvider": { "type": "string" }, "permissionProfile": { "description": "Full active permissions for this thread. `activePermissionProfile` carries display/provenance metadata for this runtime profile.", "default": null, - "anyOf": [{ "$ref": "#/definitions/PermissionProfile" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ] }, "reasoningEffort": { - "anyOf": [{ "$ref": "#/definitions/ReasoningEffort" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] }, "sandbox": { "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions.", - "allOf": [{ "$ref": "#/definitions/SandboxPolicy" }] + "allOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + } + ] }, - "serviceTier": { "anyOf": [{ "$ref": "#/definitions/ServiceTier" }, { "type": "null" }] }, - "thread": { "$ref": "#/definitions/Thread" } + "serviceTier": { + "anyOf": [ + { + "$ref": "#/definitions/ServiceTier" + }, + { + "type": "null" + } + ] + }, + "thread": { + "$ref": "#/definitions/Thread" + } }, "definitions": { "AbsolutePathBuf": { @@ -53,12 +103,17 @@ }, "ActivePermissionProfile": { "type": "object", - "required": ["id"], + "required": [ + "id" + ], "properties": { "extends": { "description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.", "default": null, - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "id": { "description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.]` profile.", @@ -68,7 +123,9 @@ "description": "Bounded user-requested modifications applied on top of the named profile, if any.", "default": [], "type": "array", - "items": { "$ref": "#/definitions/ActivePermissionProfileModification" } + "items": { + "$ref": "#/definitions/ActivePermissionProfileModification" + } } } }, @@ -77,12 +134,19 @@ { "description": "Additional concrete directory that should be writable.", "type": "object", - "required": ["path", "type"], + "required": [ + "path", + "type" + ], "properties": { - "path": { "$ref": "#/definitions/AbsolutePathBuf" }, + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, "type": { "type": "string", - "enum": ["additionalWritableRoot"], + "enum": [ + "additionalWritableRoot" + ], "title": "AdditionalWritableRootActivePermissionProfileModificationType" } }, @@ -90,28 +154,60 @@ } ] }, - "AgentPath": { "type": "string" }, + "AgentPath": { + "type": "string" + }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", "type": "string", - "enum": ["user", "auto_review", "guardian_subagent"] + "enum": [ + "user", + "auto_review", + "guardian_subagent" + ] }, "AskForApproval": { "oneOf": [ - { "type": "string", "enum": ["untrusted", "on-failure", "on-request", "never"] }, + { + "type": "string", + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ] + }, { "type": "object", - "required": ["granular"], + "required": [ + "granular" + ], "properties": { "granular": { "type": "object", - "required": ["mcp_elicitations", "rules", "sandbox_approval"], + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], "properties": { - "mcp_elicitations": { "type": "boolean" }, - "request_permissions": { "default": false, "type": "boolean" }, - "rules": { "type": "boolean" }, - "sandbox_approval": { "type": "boolean" }, - "skill_approval": { "default": false, "type": "boolean" } + "mcp_elicitations": { + "type": "boolean" + }, + "request_permissions": { + "default": false, + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + }, + "skill_approval": { + "default": false, + "type": "boolean" + } } } }, @@ -122,10 +218,21 @@ }, "ByteRange": { "type": "object", - "required": ["end", "start"], + "required": [ + "end", + "start" + ], "properties": { - "end": { "type": "integer", "format": "uint", "minimum": 0 }, - "start": { "type": "integer", "format": "uint", "minimum": 0 } + "end": { + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0 + } } }, "CodexErrorInfo": { @@ -148,12 +255,21 @@ }, { "type": "object", - "required": ["httpConnectionFailed"], + "required": [ + "httpConnectionFailed" + ], "properties": { "httpConnectionFailed": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -163,12 +279,21 @@ { "description": "Failed to connect to the response SSE stream.", "type": "object", - "required": ["responseStreamConnectionFailed"], + "required": [ + "responseStreamConnectionFailed" + ], "properties": { "responseStreamConnectionFailed": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -178,12 +303,21 @@ { "description": "The response SSE stream disconnected in the middle of a turn before completion.", "type": "object", - "required": ["responseStreamDisconnected"], + "required": [ + "responseStreamDisconnected" + ], "properties": { "responseStreamDisconnected": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -193,12 +327,21 @@ { "description": "Reached the retry limit for responses.", "type": "object", - "required": ["responseTooManyFailedAttempts"], + "required": [ + "responseTooManyFailedAttempts" + ], "properties": { "responseTooManyFailedAttempts": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -208,12 +351,20 @@ { "description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", "type": "object", - "required": ["activeTurnNotSteerable"], + "required": [ + "activeTurnNotSteerable" + ], "properties": { "activeTurnNotSteerable": { "type": "object", - "required": ["turnKind"], - "properties": { "turnKind": { "$ref": "#/definitions/NonSteerableTurnKind" } } + "required": [ + "turnKind" + ], + "properties": { + "turnKind": { + "$ref": "#/definitions/NonSteerableTurnKind" + } + } } }, "additionalProperties": false, @@ -223,10 +374,19 @@ }, "CollabAgentState": { "type": "object", - "required": ["status"], + "required": [ + "status" + ], "properties": { - "message": { "type": ["string", "null"] }, - "status": { "$ref": "#/definitions/CollabAgentStatus" } + "message": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/definitions/CollabAgentStatus" + } } }, "CollabAgentStatus": { @@ -243,34 +403,73 @@ }, "CollabAgentTool": { "type": "string", - "enum": ["spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent"] + "enum": [ + "spawnAgent", + "sendInput", + "resumeAgent", + "wait", + "closeAgent" + ] }, "CollabAgentToolCallStatus": { "type": "string", - "enum": ["inProgress", "completed", "failed"] + "enum": [ + "inProgress", + "completed", + "failed" + ] }, "CommandAction": { "oneOf": [ { "type": "object", - "required": ["command", "name", "path", "type"], + "required": [ + "command", + "name", + "path", + "type" + ], "properties": { - "command": { "type": "string" }, - "name": { "type": "string" }, - "path": { "$ref": "#/definitions/AbsolutePathBuf" }, - "type": { "type": "string", "enum": ["read"], "title": "ReadCommandActionType" } + "command": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "type": "string", + "enum": [ + "read" + ], + "title": "ReadCommandActionType" + } }, "title": "ReadCommandAction" }, { "type": "object", - "required": ["command", "type"], + "required": [ + "command", + "type" + ], "properties": { - "command": { "type": "string" }, - "path": { "type": ["string", "null"] }, + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, "type": { "type": "string", - "enum": ["listFiles"], + "enum": [ + "listFiles" + ], "title": "ListFilesCommandActionType" } }, @@ -278,21 +477,53 @@ }, { "type": "object", - "required": ["command", "type"], + "required": [ + "command", + "type" + ], "properties": { - "command": { "type": "string" }, - "path": { "type": ["string", "null"] }, - "query": { "type": ["string", "null"] }, - "type": { "type": "string", "enum": ["search"], "title": "SearchCommandActionType" } + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "search" + ], + "title": "SearchCommandActionType" + } }, "title": "SearchCommandAction" }, { "type": "object", - "required": ["command", "type"], + "required": [ + "command", + "type" + ], "properties": { - "command": { "type": "string" }, - "type": { "type": "string", "enum": ["unknown"], "title": "UnknownCommandActionType" } + "command": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "unknown" + ], + "title": "UnknownCommandActionType" + } }, "title": "UnknownCommandAction" } @@ -300,22 +531,39 @@ }, "CommandExecutionSource": { "type": "string", - "enum": ["agent", "userShell", "unifiedExecStartup", "unifiedExecInteraction"] + "enum": [ + "agent", + "userShell", + "unifiedExecStartup", + "unifiedExecInteraction" + ] }, "CommandExecutionStatus": { "type": "string", - "enum": ["inProgress", "completed", "failed", "declined"] + "enum": [ + "inProgress", + "completed", + "failed", + "declined" + ] }, "DynamicToolCallOutputContentItem": { "oneOf": [ { "type": "object", - "required": ["text", "type"], + "required": [ + "text", + "type" + ], "properties": { - "text": { "type": "string" }, + "text": { + "type": "string" + }, "type": { "type": "string", - "enum": ["inputText"], + "enum": [ + "inputText" + ], "title": "InputTextDynamicToolCallOutputContentItemType" } }, @@ -323,12 +571,19 @@ }, { "type": "object", - "required": ["imageUrl", "type"], + "required": [ + "imageUrl", + "type" + ], "properties": { - "imageUrl": { "type": "string" }, + "imageUrl": { + "type": "string" + }, "type": { "type": "string", - "enum": ["inputImage"], + "enum": [ + "inputImage" + ], "title": "InputImageDynamicToolCallOutputContentItemType" } }, @@ -336,27 +591,59 @@ } ] }, - "DynamicToolCallStatus": { "type": "string", "enum": ["inProgress", "completed", "failed"] }, - "FileSystemAccessMode": { "type": "string", "enum": ["read", "write", "none"] }, + "DynamicToolCallStatus": { + "type": "string", + "enum": [ + "inProgress", + "completed", + "failed" + ] + }, + "FileSystemAccessMode": { + "type": "string", + "enum": [ + "read", + "write", + "none" + ] + }, "FileSystemPath": { "oneOf": [ { "type": "object", - "required": ["path", "type"], + "required": [ + "path", + "type" + ], "properties": { - "path": { "$ref": "#/definitions/AbsolutePathBuf" }, - "type": { "type": "string", "enum": ["path"], "title": "PathFileSystemPathType" } + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "type": "string", + "enum": [ + "path" + ], + "title": "PathFileSystemPathType" + } }, "title": "PathFileSystemPath" }, { "type": "object", - "required": ["pattern", "type"], + "required": [ + "pattern", + "type" + ], "properties": { - "pattern": { "type": "string" }, + "pattern": { + "type": "string" + }, "type": { "type": "string", - "enum": ["glob_pattern"], + "enum": [ + "glob_pattern" + ], "title": "GlobPatternFileSystemPathType" } }, @@ -364,10 +651,21 @@ }, { "type": "object", - "required": ["type", "value"], + "required": [ + "type", + "value" + ], "properties": { - "type": { "type": "string", "enum": ["special"], "title": "SpecialFileSystemPathType" }, - "value": { "$ref": "#/definitions/FileSystemSpecialPath" } + "type": { + "type": "string", + "enum": [ + "special" + ], + "title": "SpecialFileSystemPathType" + }, + "value": { + "$ref": "#/definitions/FileSystemSpecialPath" + } }, "title": "SpecialFileSystemPath" } @@ -375,111 +673,264 @@ }, "FileSystemSandboxEntry": { "type": "object", - "required": ["access", "path"], + "required": [ + "access", + "path" + ], "properties": { - "access": { "$ref": "#/definitions/FileSystemAccessMode" }, - "path": { "$ref": "#/definitions/FileSystemPath" } + "access": { + "$ref": "#/definitions/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/FileSystemPath" + } } }, "FileSystemSpecialPath": { "oneOf": [ { "type": "object", - "required": ["kind"], - "properties": { "kind": { "type": "string", "enum": ["root"] } }, + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "root" + ] + } + }, "title": "RootFileSystemSpecialPath" }, { "type": "object", - "required": ["kind"], - "properties": { "kind": { "type": "string", "enum": ["minimal"] } }, + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "minimal" + ] + } + }, "title": "MinimalFileSystemSpecialPath" }, { "type": "object", - "required": ["kind"], + "required": [ + "kind" + ], "properties": { - "kind": { "type": "string", "enum": ["project_roots"] }, - "subpath": { "type": ["string", "null"] } + "kind": { + "type": "string", + "enum": [ + "project_roots" + ] + }, + "subpath": { + "type": [ + "string", + "null" + ] + } }, "title": "KindFileSystemSpecialPath" }, { "type": "object", - "required": ["kind"], - "properties": { "kind": { "type": "string", "enum": ["tmpdir"] } }, + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "tmpdir" + ] + } + }, "title": "TmpdirFileSystemSpecialPath" }, { "type": "object", - "required": ["kind"], - "properties": { "kind": { "type": "string", "enum": ["slash_tmp"] } }, + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "slash_tmp" + ] + } + }, "title": "SlashTmpFileSystemSpecialPath" }, { "type": "object", - "required": ["kind", "path"], + "required": [ + "kind", + "path" + ], "properties": { - "kind": { "type": "string", "enum": ["unknown"] }, - "path": { "type": "string" }, - "subpath": { "type": ["string", "null"] } + "kind": { + "type": "string", + "enum": [ + "unknown" + ] + }, + "path": { + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } } } ] }, "FileUpdateChange": { "type": "object", - "required": ["diff", "kind", "path"], + "required": [ + "diff", + "kind", + "path" + ], "properties": { - "diff": { "type": "string" }, - "kind": { "$ref": "#/definitions/PatchChangeKind" }, - "path": { "type": "string" } + "diff": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/PatchChangeKind" + }, + "path": { + "type": "string" + } } }, "GitInfo": { "type": "object", "properties": { - "branch": { "type": ["string", "null"] }, - "originUrl": { "type": ["string", "null"] }, - "sha": { "type": ["string", "null"] } + "branch": { + "type": [ + "string", + "null" + ] + }, + "originUrl": { + "type": [ + "string", + "null" + ] + }, + "sha": { + "type": [ + "string", + "null" + ] + } } }, "HookPromptFragment": { "type": "object", - "required": ["hookRunId", "text"], - "properties": { "hookRunId": { "type": "string" }, "text": { "type": "string" } } + "required": [ + "hookRunId", + "text" + ], + "properties": { + "hookRunId": { + "type": "string" + }, + "text": { + "type": "string" + } + } }, "McpToolCallError": { "type": "object", - "required": ["message"], - "properties": { "message": { "type": "string" } } + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } }, "McpToolCallResult": { "type": "object", - "required": ["content"], + "required": [ + "content" + ], "properties": { "_meta": true, - "content": { "type": "array", "items": true }, + "content": { + "type": "array", + "items": true + }, "structuredContent": true } }, - "McpToolCallStatus": { "type": "string", "enum": ["inProgress", "completed", "failed"] }, + "McpToolCallStatus": { + "type": "string", + "enum": [ + "inProgress", + "completed", + "failed" + ] + }, "MemoryCitation": { "type": "object", - "required": ["entries", "threadIds"], + "required": [ + "entries", + "threadIds" + ], "properties": { - "entries": { "type": "array", "items": { "$ref": "#/definitions/MemoryCitationEntry" } }, - "threadIds": { "type": "array", "items": { "type": "string" } } + "entries": { + "type": "array", + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + } + }, + "threadIds": { + "type": "array", + "items": { + "type": "string" + } + } } }, "MemoryCitationEntry": { "type": "object", - "required": ["lineEnd", "lineStart", "note", "path"], + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], "properties": { - "lineEnd": { "type": "integer", "format": "uint32", "minimum": 0 }, - "lineStart": { "type": "integer", "format": "uint32", "minimum": 0 }, - "note": { "type": "string" }, - "path": { "type": "string" } + "lineEnd": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "lineStart": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } } }, "MessagePhase": { @@ -488,45 +939,95 @@ { "description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.", "type": "string", - "enum": ["commentary"] + "enum": [ + "commentary" + ] }, { "description": "The assistant's terminal answer text for the current turn.", "type": "string", - "enum": ["final_answer"] + "enum": [ + "final_answer" + ] } ] }, - "NetworkAccess": { "type": "string", "enum": ["restricted", "enabled"] }, - "NonSteerableTurnKind": { "type": "string", "enum": ["review", "compact"] }, + "NetworkAccess": { + "type": "string", + "enum": [ + "restricted", + "enabled" + ] + }, + "NonSteerableTurnKind": { + "type": "string", + "enum": [ + "review", + "compact" + ] + }, "PatchApplyStatus": { "type": "string", - "enum": ["inProgress", "completed", "failed", "declined"] + "enum": [ + "inProgress", + "completed", + "failed", + "declined" + ] }, "PatchChangeKind": { "oneOf": [ { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["add"], "title": "AddPatchChangeKindType" } + "type": { + "type": "string", + "enum": [ + "add" + ], + "title": "AddPatchChangeKindType" + } }, "title": "AddPatchChangeKind" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["delete"], "title": "DeletePatchChangeKindType" } + "type": { + "type": "string", + "enum": [ + "delete" + ], + "title": "DeletePatchChangeKindType" + } }, "title": "DeletePatchChangeKind" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "move_path": { "type": ["string", "null"] }, - "type": { "type": "string", "enum": ["update"], "title": "UpdatePatchChangeKindType" } + "move_path": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "update" + ], + "title": "UpdatePatchChangeKindType" + } }, "title": "UpdatePatchChangeKind" } @@ -537,13 +1038,23 @@ { "description": "Codex owns sandbox construction for this profile.", "type": "object", - "required": ["fileSystem", "network", "type"], + "required": [ + "fileSystem", + "network", + "type" + ], "properties": { - "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, + "fileSystem": { + "$ref": "#/definitions/PermissionProfileFileSystemPermissions" + }, + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, "type": { "type": "string", - "enum": ["managed"], + "enum": [ + "managed" + ], "title": "ManagedPermissionProfileType" } }, @@ -552,11 +1063,15 @@ { "description": "Do not apply an outer sandbox.", "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["disabled"], + "enum": [ + "disabled" + ], "title": "DisabledPermissionProfileType" } }, @@ -565,12 +1080,19 @@ { "description": "Filesystem isolation is enforced by an external caller.", "type": "object", - "required": ["network", "type"], + "required": [ + "network", + "type" + ], "properties": { - "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, "type": { "type": "string", - "enum": ["external"], + "enum": [ + "external" + ], "title": "ExternalPermissionProfileType" } }, @@ -582,16 +1104,30 @@ "oneOf": [ { "type": "object", - "required": ["entries", "type"], + "required": [ + "entries", + "type" + ], "properties": { "entries": { "type": "array", - "items": { "$ref": "#/definitions/FileSystemSandboxEntry" } + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + } + }, + "globScanMaxDepth": { + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 1 }, - "globScanMaxDepth": { "type": ["integer", "null"], "format": "uint", "minimum": 1 }, "type": { "type": "string", - "enum": ["restricted"], + "enum": [ + "restricted" + ], "title": "RestrictedPermissionProfileFileSystemPermissionsType" } }, @@ -599,11 +1135,15 @@ }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["unrestricted"], + "enum": [ + "unrestricted" + ], "title": "UnrestrictedPermissionProfileFileSystemPermissionsType" } }, @@ -613,23 +1153,40 @@ }, "PermissionProfileNetworkPermissions": { "type": "object", - "required": ["enabled"], - "properties": { "enabled": { "type": "boolean" } } + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "type": "string", - "enum": ["none", "minimal", "low", "medium", "high", "xhigh"] + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] }, "SandboxPolicy": { "oneOf": [ { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["dangerFullAccess"], + "enum": [ + "dangerFullAccess" + ], "title": "DangerFullAccessSandboxPolicyType" } }, @@ -637,24 +1194,43 @@ }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "networkAccess": { "default": false, "type": "boolean" }, - "type": { "type": "string", "enum": ["readOnly"], "title": "ReadOnlySandboxPolicyType" } + "networkAccess": { + "default": false, + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "readOnly" + ], + "title": "ReadOnlySandboxPolicyType" + } }, "title": "ReadOnlySandboxPolicy" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "networkAccess": { "default": "restricted", - "allOf": [{ "$ref": "#/definitions/NetworkAccess" }] + "allOf": [ + { + "$ref": "#/definitions/NetworkAccess" + } + ] }, "type": { "type": "string", - "enum": ["externalSandbox"], + "enum": [ + "externalSandbox" + ], "title": "ExternalSandboxSandboxPolicyType" } }, @@ -662,41 +1238,83 @@ }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "excludeSlashTmp": { "default": false, "type": "boolean" }, - "excludeTmpdirEnvVar": { "default": false, "type": "boolean" }, - "networkAccess": { "default": false, "type": "boolean" }, + "excludeSlashTmp": { + "default": false, + "type": "boolean" + }, + "excludeTmpdirEnvVar": { + "default": false, + "type": "boolean" + }, + "networkAccess": { + "default": false, + "type": "boolean" + }, "type": { "type": "string", - "enum": ["workspaceWrite"], + "enum": [ + "workspaceWrite" + ], "title": "WorkspaceWriteSandboxPolicyType" }, "writableRoots": { "default": [], "type": "array", - "items": { "$ref": "#/definitions/AbsolutePathBuf" } + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + } } }, "title": "WorkspaceWriteSandboxPolicy" } ] }, - "ServiceTier": { "type": "string", "enum": ["fast", "flex"] }, + "ServiceTier": { + "type": "string", + "enum": [ + "fast", + "flex" + ] + }, "SessionSource": { "oneOf": [ - { "type": "string", "enum": ["cli", "vscode", "exec", "appServer", "unknown"] }, + { + "type": "string", + "enum": [ + "cli", + "vscode", + "exec", + "appServer", + "unknown" + ] + }, { "type": "object", - "required": ["custom"], - "properties": { "custom": { "type": "string" } }, + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, "additionalProperties": false, "title": "CustomSessionSource" }, { "type": "object", - "required": ["subAgent"], - "properties": { "subAgent": { "$ref": "#/definitions/SubAgentSource" } }, + "required": [ + "subAgent" + ], + "properties": { + "subAgent": { + "$ref": "#/definitions/SubAgentSource" + } + }, "additionalProperties": false, "title": "SubAgentSessionSource" } @@ -704,23 +1322,59 @@ }, "SubAgentSource": { "oneOf": [ - { "type": "string", "enum": ["review", "compact", "memory_consolidation"] }, + { + "type": "string", + "enum": [ + "review", + "compact", + "memory_consolidation" + ] + }, { "type": "object", - "required": ["thread_spawn"], + "required": [ + "thread_spawn" + ], "properties": { "thread_spawn": { "type": "object", - "required": ["depth", "parent_thread_id"], + "required": [ + "depth", + "parent_thread_id" + ], "properties": { - "agent_nickname": { "default": null, "type": ["string", "null"] }, + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, "agent_path": { "default": null, - "anyOf": [{ "$ref": "#/definitions/AgentPath" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/AgentPath" + }, + { + "type": "null" + } + ] }, - "agent_role": { "default": null, "type": ["string", "null"] }, - "depth": { "type": "integer", "format": "int32" }, - "parent_thread_id": { "$ref": "#/definitions/ThreadId" } + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "depth": { + "type": "integer", + "format": "int32" + }, + "parent_thread_id": { + "$ref": "#/definitions/ThreadId" + } } } }, @@ -729,8 +1383,14 @@ }, { "type": "object", - "required": ["other"], - "properties": { "other": { "type": "string" } }, + "required": [ + "other" + ], + "properties": { + "other": { + "type": "string" + } + }, "additionalProperties": false, "title": "OtherSubAgentSource" } @@ -738,15 +1398,24 @@ }, "TextElement": { "type": "object", - "required": ["byteRange"], + "required": [ + "byteRange" + ], "properties": { "byteRange": { "description": "Byte range in the parent `text` buffer that this element occupies.", - "allOf": [{ "$ref": "#/definitions/ByteRange" }] + "allOf": [ + { + "$ref": "#/definitions/ByteRange" + } + ] }, "placeholder": { "description": "Optional human-readable placeholder for the element, displayed in the UI.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] } } }, @@ -768,11 +1437,17 @@ "properties": { "agentNickname": { "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "agentRole": { "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "cliVersion": { "description": "Version of the CLI that created the thread.", @@ -785,7 +1460,11 @@ }, "cwd": { "description": "Working directory captured for the thread.", - "allOf": [{ "$ref": "#/definitions/AbsolutePathBuf" }] + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -793,21 +1472,42 @@ }, "forkedFromId": { "description": "Source thread id when this thread was created by forking another thread.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "gitInfo": { "description": "Optional Git metadata captured when the thread was created.", - "anyOf": [{ "$ref": "#/definitions/GitInfo" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/GitInfo" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" }, - "id": { "type": "string" }, "modelProvider": { "description": "Model provider used for this thread (for example, 'openai').", "type": "string" }, - "name": { "description": "Optional user-facing thread title.", "type": ["string", "null"] }, + "name": { + "description": "Optional user-facing thread title.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "preview": { "description": "Usually the first user message in the thread, if available.", @@ -815,16 +1515,37 @@ }, "source": { "description": "Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.).", - "allOf": [{ "$ref": "#/definitions/SessionSource" }] + "allOf": [ + { + "$ref": "#/definitions/SessionSource" + } + ] }, "status": { "description": "Current runtime status for the thread.", - "allOf": [{ "$ref": "#/definitions/ThreadStatus" }] + "allOf": [ + { + "$ref": "#/definitions/ThreadStatus" + } + ] + }, + "threadSource": { + "description": "Optional analytics source classification for this thread.", + "anyOf": [ + { + "$ref": "#/definitions/ThreadSource" + }, + { + "type": "null" + } + ] }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "type": "array", - "items": { "$ref": "#/definitions/Turn" } + "items": { + "$ref": "#/definitions/Turn" + } }, "updatedAt": { "description": "Unix timestamp (in seconds) when the thread was last updated.", @@ -833,19 +1554,40 @@ } } }, - "ThreadActiveFlag": { "type": "string", "enum": ["waitingOnApproval", "waitingOnUserInput"] }, - "ThreadId": { "type": "string" }, + "ThreadActiveFlag": { + "type": "string", + "enum": [ + "waitingOnApproval", + "waitingOnUserInput" + ] + }, + "ThreadId": { + "type": "string" + }, "ThreadItem": { "oneOf": [ { "type": "object", - "required": ["content", "id", "type"], + "required": [ + "content", + "id", + "type" + ], "properties": { - "content": { "type": "array", "items": { "$ref": "#/definitions/UserInput" } }, - "id": { "type": "string" }, + "content": { + "type": "array", + "items": { + "$ref": "#/definitions/UserInput" + } + }, + "id": { + "type": "string" + }, "type": { "type": "string", - "enum": ["userMessage"], + "enum": [ + "userMessage" + ], "title": "UserMessageThreadItemType" } }, @@ -853,16 +1595,26 @@ }, { "type": "object", - "required": ["fragments", "id", "type"], + "required": [ + "fragments", + "id", + "type" + ], "properties": { "fragments": { "type": "array", - "items": { "$ref": "#/definitions/HookPromptFragment" } + "items": { + "$ref": "#/definitions/HookPromptFragment" + } + }, + "id": { + "type": "string" }, - "id": { "type": "string" }, "type": { "type": "string", - "enum": ["hookPrompt"], + "enum": [ + "hookPrompt" + ], "title": "HookPromptThreadItemType" } }, @@ -870,21 +1622,45 @@ }, { "type": "object", - "required": ["id", "text", "type"], + "required": [ + "id", + "text", + "type" + ], "properties": { - "id": { "type": "string" }, + "id": { + "type": "string" + }, "memoryCitation": { "default": null, - "anyOf": [{ "$ref": "#/definitions/MemoryCitation" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ] }, "phase": { "default": null, - "anyOf": [{ "$ref": "#/definitions/MessagePhase" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, + "text": { + "type": "string" }, - "text": { "type": "string" }, "type": { "type": "string", - "enum": ["agentMessage"], + "enum": [ + "agentMessage" + ], "title": "AgentMessageThreadItemType" } }, @@ -893,66 +1669,141 @@ { "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", "type": "object", - "required": ["id", "text", "type"], + "required": [ + "id", + "text", + "type" + ], "properties": { - "id": { "type": "string" }, - "text": { "type": "string" }, - "type": { "type": "string", "enum": ["plan"], "title": "PlanThreadItemType" } + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "plan" + ], + "title": "PlanThreadItemType" + } }, "title": "PlanThreadItem" }, { "type": "object", - "required": ["id", "type"], + "required": [ + "id", + "type" + ], "properties": { - "content": { "default": [], "type": "array", "items": { "type": "string" } }, - "id": { "type": "string" }, - "summary": { "default": [], "type": "array", "items": { "type": "string" } }, - "type": { "type": "string", "enum": ["reasoning"], "title": "ReasoningThreadItemType" } + "content": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "summary": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string", + "enum": [ + "reasoning" + ], + "title": "ReasoningThreadItemType" + } }, "title": "ReasoningThreadItem" }, { "type": "object", - "required": ["command", "commandActions", "cwd", "id", "status", "type"], + "required": [ + "command", + "commandActions", + "cwd", + "id", + "status", + "type" + ], "properties": { "aggregatedOutput": { "description": "The command's output, aggregated from stdout and stderr.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] + }, + "command": { + "description": "The command to be executed.", + "type": "string" }, - "command": { "description": "The command to be executed.", "type": "string" }, "commandActions": { "description": "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", "type": "array", - "items": { "$ref": "#/definitions/CommandAction" } + "items": { + "$ref": "#/definitions/CommandAction" + } }, "cwd": { "description": "The command's working directory.", - "allOf": [{ "$ref": "#/definitions/AbsolutePathBuf" }] + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] }, "durationMs": { "description": "The duration of the command execution in milliseconds.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "exitCode": { "description": "The command's exit code.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int32" }, - "id": { "type": "string" }, + "id": { + "type": "string" + }, "processId": { "description": "Identifier for the underlying PTY process (when available).", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "source": { "default": "agent", - "allOf": [{ "$ref": "#/definitions/CommandExecutionSource" }] + "allOf": [ + { + "$ref": "#/definitions/CommandExecutionSource" + } + ] + }, + "status": { + "$ref": "#/definitions/CommandExecutionStatus" }, - "status": { "$ref": "#/definitions/CommandExecutionStatus" }, "type": { "type": "string", - "enum": ["commandExecution"], + "enum": [ + "commandExecution" + ], "title": "CommandExecutionThreadItemType" } }, @@ -960,14 +1811,30 @@ }, { "type": "object", - "required": ["changes", "id", "status", "type"], + "required": [ + "changes", + "id", + "status", + "type" + ], "properties": { - "changes": { "type": "array", "items": { "$ref": "#/definitions/FileUpdateChange" } }, - "id": { "type": "string" }, - "status": { "$ref": "#/definitions/PatchApplyStatus" }, + "changes": { + "type": "array", + "items": { + "$ref": "#/definitions/FileUpdateChange" + } + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/PatchApplyStatus" + }, "type": { "type": "string", - "enum": ["fileChange"], + "enum": [ + "fileChange" + ], "title": "FileChangeThreadItemType" } }, @@ -975,28 +1842,67 @@ }, { "type": "object", - "required": ["arguments", "id", "server", "status", "tool", "type"], + "required": [ + "arguments", + "id", + "server", + "status", + "tool", + "type" + ], "properties": { "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "error": { - "anyOf": [{ "$ref": "#/definitions/McpToolCallError" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "mcpAppResourceUri": { + "type": [ + "string", + "null" + ] }, - "id": { "type": "string" }, - "mcpAppResourceUri": { "type": ["string", "null"] }, "result": { - "anyOf": [{ "$ref": "#/definitions/McpToolCallResult" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallResult" + }, + { + "type": "null" + } + ] + }, + "server": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpToolCallStatus" + }, + "tool": { + "type": "string" }, - "server": { "type": "string" }, - "status": { "$ref": "#/definitions/McpToolCallStatus" }, - "tool": { "type": "string" }, "type": { "type": "string", - "enum": ["mcpToolCall"], + "enum": [ + "mcpToolCall" + ], "title": "McpToolCallThreadItemType" } }, @@ -1004,26 +1910,58 @@ }, { "type": "object", - "required": ["arguments", "id", "status", "tool", "type"], + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], "properties": { "arguments": true, "contentItems": { - "type": ["array", "null"], - "items": { "$ref": "#/definitions/DynamicToolCallOutputContentItem" } + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + } }, "durationMs": { "description": "The duration of the dynamic tool call in milliseconds.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, - "id": { "type": "string" }, - "namespace": { "type": ["string", "null"] }, - "status": { "$ref": "#/definitions/DynamicToolCallStatus" }, - "success": { "type": ["boolean", "null"] }, - "tool": { "type": "string" }, + "id": { + "type": "string" + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, "type": { "type": "string", - "enum": ["dynamicToolCall"], + "enum": [ + "dynamicToolCall" + ], "title": "DynamicToolCallThreadItemType" } }, @@ -1044,7 +1982,9 @@ "agentsStates": { "description": "Last known status of the target agents, when available.", "type": "object", - "additionalProperties": { "$ref": "#/definitions/CollabAgentState" } + "additionalProperties": { + "$ref": "#/definitions/CollabAgentState" + } }, "id": { "description": "Unique identifier for this collab tool call.", @@ -1052,20 +1992,35 @@ }, "model": { "description": "Model requested for the spawned agent, when applicable.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "prompt": { "description": "Prompt text sent as part of the collab tool call, when available.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "reasoningEffort": { "description": "Reasoning effort requested for the spawned agent, when applicable.", - "anyOf": [{ "$ref": "#/definitions/ReasoningEffort" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] }, "receiverThreadIds": { "description": "Thread ID of the receiving agent, when applicable. In case of spawn operation, this corresponds to the newly spawned agent.", "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, "senderThreadId": { "description": "Thread ID of the agent issuing the collab request.", @@ -1073,15 +2028,25 @@ }, "status": { "description": "Current status of the collab tool call.", - "allOf": [{ "$ref": "#/definitions/CollabAgentToolCallStatus" }] + "allOf": [ + { + "$ref": "#/definitions/CollabAgentToolCallStatus" + } + ] }, "tool": { "description": "Name of the collab tool that was invoked.", - "allOf": [{ "$ref": "#/definitions/CollabAgentTool" }] + "allOf": [ + { + "$ref": "#/definitions/CollabAgentTool" + } + ] }, "type": { "type": "string", - "enum": ["collabAgentToolCall"], + "enum": [ + "collabAgentToolCall" + ], "title": "CollabAgentToolCallThreadItemType" } }, @@ -1089,41 +2054,101 @@ }, { "type": "object", - "required": ["id", "query", "type"], + "required": [ + "id", + "query", + "type" + ], "properties": { "action": { - "anyOf": [{ "$ref": "#/definitions/WebSearchAction" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/WebSearchAction" + }, + { + "type": "null" + } + ] }, - "id": { "type": "string" }, - "query": { "type": "string" }, - "type": { "type": "string", "enum": ["webSearch"], "title": "WebSearchThreadItemType" } + "id": { + "type": "string" + }, + "query": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "webSearch" + ], + "title": "WebSearchThreadItemType" + } }, "title": "WebSearchThreadItem" }, { "type": "object", - "required": ["id", "path", "type"], + "required": [ + "id", + "path", + "type" + ], "properties": { - "id": { "type": "string" }, - "path": { "$ref": "#/definitions/AbsolutePathBuf" }, - "type": { "type": "string", "enum": ["imageView"], "title": "ImageViewThreadItemType" } + "id": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "type": "string", + "enum": [ + "imageView" + ], + "title": "ImageViewThreadItemType" + } }, "title": "ImageViewThreadItem" }, { "type": "object", - "required": ["id", "result", "status", "type"], + "required": [ + "id", + "result", + "status", + "type" + ], "properties": { - "id": { "type": "string" }, - "result": { "type": "string" }, - "revisedPrompt": { "type": ["string", "null"] }, - "savedPath": { - "anyOf": [{ "$ref": "#/definitions/AbsolutePathBuf" }, { "type": "null" }] + "id": { + "type": "string" + }, + "result": { + "type": "string" + }, + "revisedPrompt": { + "type": [ + "string", + "null" + ] + }, + "savedPath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string" }, - "status": { "type": "string" }, "type": { "type": "string", - "enum": ["imageGeneration"], + "enum": [ + "imageGeneration" + ], "title": "ImageGenerationThreadItemType" } }, @@ -1131,13 +2156,23 @@ }, { "type": "object", - "required": ["id", "review", "type"], + "required": [ + "id", + "review", + "type" + ], "properties": { - "id": { "type": "string" }, - "review": { "type": "string" }, + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, "type": { "type": "string", - "enum": ["enteredReviewMode"], + "enum": [ + "enteredReviewMode" + ], "title": "EnteredReviewModeThreadItemType" } }, @@ -1145,13 +2180,23 @@ }, { "type": "object", - "required": ["id", "review", "type"], + "required": [ + "id", + "review", + "type" + ], "properties": { - "id": { "type": "string" }, - "review": { "type": "string" }, + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, "type": { "type": "string", - "enum": ["exitedReviewMode"], + "enum": [ + "exitedReviewMode" + ], "title": "ExitedReviewModeThreadItemType" } }, @@ -1159,12 +2204,19 @@ }, { "type": "object", - "required": ["id", "type"], + "required": [ + "id", + "type" + ], "properties": { - "id": { "type": "string" }, + "id": { + "type": "string" + }, "type": { "type": "string", - "enum": ["contextCompaction"], + "enum": [ + "contextCompaction" + ], "title": "ContextCompactionThreadItemType" } }, @@ -1172,15 +2224,27 @@ } ] }, + "ThreadSource": { + "type": "string", + "enum": [ + "user", + "subagent", + "memory_consolidation" + ] + }, "ThreadStatus": { "oneOf": [ { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["notLoaded"], + "enum": [ + "notLoaded" + ], "title": "NotLoadedThreadStatusType" } }, @@ -1188,19 +2252,31 @@ }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["idle"], "title": "IdleThreadStatusType" } + "type": { + "type": "string", + "enum": [ + "idle" + ], + "title": "IdleThreadStatusType" + } }, "title": "IdleThreadStatus" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["systemError"], + "enum": [ + "systemError" + ], "title": "SystemErrorThreadStatusType" } }, @@ -1208,13 +2284,24 @@ }, { "type": "object", - "required": ["activeFlags", "type"], + "required": [ + "activeFlags", + "type" + ], "properties": { "activeFlags": { "type": "array", - "items": { "$ref": "#/definitions/ThreadActiveFlag" } + "items": { + "$ref": "#/definitions/ThreadActiveFlag" + } }, - "type": { "type": "string", "enum": ["active"], "title": "ActiveThreadStatusType" } + "type": { + "type": "string", + "enum": [ + "active" + ], + "title": "ActiveThreadStatusType" + } }, "title": "ActiveThreadStatus" } @@ -1222,103 +2309,248 @@ }, "Turn": { "type": "object", - "required": ["id", "items", "status"], + "required": [ + "id", + "items", + "status" + ], "properties": { "completedAt": { "description": "Unix timestamp (in seconds) when the turn completed.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "durationMs": { "description": "Duration between turn start and completion in milliseconds, if known.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "error": { "description": "Only populated when the Turn's status is failed.", - "anyOf": [{ "$ref": "#/definitions/TurnError" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/TurnError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" }, - "id": { "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "type": "array", - "items": { "$ref": "#/definitions/ThreadItem" } + "items": { + "$ref": "#/definitions/ThreadItem" + } + }, + "itemsView": { + "description": "Describes how much of `items` has been loaded for this turn.", + "default": "full", + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ] }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, - "status": { "$ref": "#/definitions/TurnStatus" } + "status": { + "$ref": "#/definitions/TurnStatus" + } } }, "TurnError": { "type": "object", - "required": ["message"], + "required": [ + "message" + ], "properties": { - "additionalDetails": { "default": null, "type": ["string", "null"] }, - "codexErrorInfo": { - "anyOf": [{ "$ref": "#/definitions/CodexErrorInfo" }, { "type": "null" }] + "additionalDetails": { + "default": null, + "type": [ + "string", + "null" + ] }, - "message": { "type": "string" } + "codexErrorInfo": { + "anyOf": [ + { + "$ref": "#/definitions/CodexErrorInfo" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": "string" + } } }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "type": "string", + "enum": [ + "notLoaded" + ] + }, + { + "description": "`items` contains only a display summary for this turn.", + "type": "string", + "enum": [ + "summary" + ] + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "type": "string", + "enum": [ + "full" + ] + } + ] + }, "TurnStatus": { "type": "string", - "enum": ["completed", "interrupted", "failed", "inProgress"] + "enum": [ + "completed", + "interrupted", + "failed", + "inProgress" + ] }, "UserInput": { "oneOf": [ { "type": "object", - "required": ["text", "type"], + "required": [ + "text", + "type" + ], "properties": { - "text": { "type": "string" }, + "text": { + "type": "string" + }, "text_elements": { "description": "UI-defined spans within `text` used to render or persist special elements.", "default": [], "type": "array", - "items": { "$ref": "#/definitions/TextElement" } + "items": { + "$ref": "#/definitions/TextElement" + } }, - "type": { "type": "string", "enum": ["text"], "title": "TextUserInputType" } + "type": { + "type": "string", + "enum": [ + "text" + ], + "title": "TextUserInputType" + } }, "title": "TextUserInput" }, { "type": "object", - "required": ["type", "url"], + "required": [ + "type", + "url" + ], "properties": { - "type": { "type": "string", "enum": ["image"], "title": "ImageUserInputType" }, - "url": { "type": "string" } + "type": { + "type": "string", + "enum": [ + "image" + ], + "title": "ImageUserInputType" + }, + "url": { + "type": "string" + } }, "title": "ImageUserInput" }, { "type": "object", - "required": ["path", "type"], + "required": [ + "path", + "type" + ], "properties": { - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["localImage"], "title": "LocalImageUserInputType" } + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "localImage" + ], + "title": "LocalImageUserInputType" + } }, "title": "LocalImageUserInput" }, { "type": "object", - "required": ["name", "path", "type"], + "required": [ + "name", + "path", + "type" + ], "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["skill"], "title": "SkillUserInputType" } + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "skill" + ], + "title": "SkillUserInputType" + } }, "title": "SkillUserInput" }, { "type": "object", - "required": ["name", "path", "type"], + "required": [ + "name", + "path", + "type" + ], "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["mention"], "title": "MentionUserInputType" } + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mention" + ], + "title": "MentionUserInputType" + } }, "title": "MentionUserInput" } @@ -1328,46 +2560,98 @@ "oneOf": [ { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "queries": { "type": ["array", "null"], "items": { "type": "string" } }, - "query": { "type": ["string", "null"] }, - "type": { "type": "string", "enum": ["search"], "title": "SearchWebSearchActionType" } + "queries": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "search" + ], + "title": "SearchWebSearchActionType" + } }, "title": "SearchWebSearchAction" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["openPage"], + "enum": [ + "openPage" + ], "title": "OpenPageWebSearchActionType" }, - "url": { "type": ["string", "null"] } + "url": { + "type": [ + "string", + "null" + ] + } }, "title": "OpenPageWebSearchAction" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "pattern": { "type": ["string", "null"] }, + "pattern": { + "type": [ + "string", + "null" + ] + }, "type": { "type": "string", - "enum": ["findInPage"], + "enum": [ + "findInPage" + ], "title": "FindInPageWebSearchActionType" }, - "url": { "type": ["string", "null"] } + "url": { + "type": [ + "string", + "null" + ] + } }, "title": "FindInPageWebSearchAction" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["other"], "title": "OtherWebSearchActionType" } + "type": { + "type": "string", + "enum": [ + "other" + ], + "title": "OtherWebSearchActionType" + } }, "title": "OtherWebSearchAction" } diff --git a/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json b/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json index 6315e981dd5..f052ccbe76d 100644 --- a/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +++ b/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json @@ -15,36 +15,86 @@ "activePermissionProfile": { "description": "Named or implicit built-in profile that produced the active permissions, when known.", "default": null, - "anyOf": [{ "$ref": "#/definitions/ActivePermissionProfile" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/ActivePermissionProfile" + }, + { + "type": "null" + } + ] + }, + "approvalPolicy": { + "$ref": "#/definitions/AskForApproval" }, - "approvalPolicy": { "$ref": "#/definitions/AskForApproval" }, "approvalsReviewer": { "description": "Reviewer currently used for approval requests on this thread.", - "allOf": [{ "$ref": "#/definitions/ApprovalsReviewer" }] + "allOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + } + ] + }, + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" }, - "cwd": { "$ref": "#/definitions/AbsolutePathBuf" }, "instructionSources": { "description": "Instruction source files currently loaded for this thread.", "default": [], "type": "array", - "items": { "$ref": "#/definitions/AbsolutePathBuf" } + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + } + }, + "model": { + "type": "string" + }, + "modelProvider": { + "type": "string" }, - "model": { "type": "string" }, - "modelProvider": { "type": "string" }, "permissionProfile": { "description": "Full active permissions for this thread. `activePermissionProfile` carries display/provenance metadata for this runtime profile.", "default": null, - "anyOf": [{ "$ref": "#/definitions/PermissionProfile" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/PermissionProfile" + }, + { + "type": "null" + } + ] }, "reasoningEffort": { - "anyOf": [{ "$ref": "#/definitions/ReasoningEffort" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] }, "sandbox": { "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions.", - "allOf": [{ "$ref": "#/definitions/SandboxPolicy" }] + "allOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + } + ] }, - "serviceTier": { "anyOf": [{ "$ref": "#/definitions/ServiceTier" }, { "type": "null" }] }, - "thread": { "$ref": "#/definitions/Thread" } + "serviceTier": { + "anyOf": [ + { + "$ref": "#/definitions/ServiceTier" + }, + { + "type": "null" + } + ] + }, + "thread": { + "$ref": "#/definitions/Thread" + } }, "definitions": { "AbsolutePathBuf": { @@ -53,12 +103,17 @@ }, "ActivePermissionProfile": { "type": "object", - "required": ["id"], + "required": [ + "id" + ], "properties": { "extends": { "description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.", "default": null, - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "id": { "description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.]` profile.", @@ -68,7 +123,9 @@ "description": "Bounded user-requested modifications applied on top of the named profile, if any.", "default": [], "type": "array", - "items": { "$ref": "#/definitions/ActivePermissionProfileModification" } + "items": { + "$ref": "#/definitions/ActivePermissionProfileModification" + } } } }, @@ -77,12 +134,19 @@ { "description": "Additional concrete directory that should be writable.", "type": "object", - "required": ["path", "type"], + "required": [ + "path", + "type" + ], "properties": { - "path": { "$ref": "#/definitions/AbsolutePathBuf" }, + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, "type": { "type": "string", - "enum": ["additionalWritableRoot"], + "enum": [ + "additionalWritableRoot" + ], "title": "AdditionalWritableRootActivePermissionProfileModificationType" } }, @@ -90,28 +154,60 @@ } ] }, - "AgentPath": { "type": "string" }, + "AgentPath": { + "type": "string" + }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", "type": "string", - "enum": ["user", "auto_review", "guardian_subagent"] + "enum": [ + "user", + "auto_review", + "guardian_subagent" + ] }, "AskForApproval": { "oneOf": [ - { "type": "string", "enum": ["untrusted", "on-failure", "on-request", "never"] }, + { + "type": "string", + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ] + }, { "type": "object", - "required": ["granular"], + "required": [ + "granular" + ], "properties": { "granular": { "type": "object", - "required": ["mcp_elicitations", "rules", "sandbox_approval"], + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], "properties": { - "mcp_elicitations": { "type": "boolean" }, - "request_permissions": { "default": false, "type": "boolean" }, - "rules": { "type": "boolean" }, - "sandbox_approval": { "type": "boolean" }, - "skill_approval": { "default": false, "type": "boolean" } + "mcp_elicitations": { + "type": "boolean" + }, + "request_permissions": { + "default": false, + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + }, + "skill_approval": { + "default": false, + "type": "boolean" + } } } }, @@ -122,10 +218,21 @@ }, "ByteRange": { "type": "object", - "required": ["end", "start"], + "required": [ + "end", + "start" + ], "properties": { - "end": { "type": "integer", "format": "uint", "minimum": 0 }, - "start": { "type": "integer", "format": "uint", "minimum": 0 } + "end": { + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0 + } } }, "CodexErrorInfo": { @@ -148,12 +255,21 @@ }, { "type": "object", - "required": ["httpConnectionFailed"], + "required": [ + "httpConnectionFailed" + ], "properties": { "httpConnectionFailed": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -163,12 +279,21 @@ { "description": "Failed to connect to the response SSE stream.", "type": "object", - "required": ["responseStreamConnectionFailed"], + "required": [ + "responseStreamConnectionFailed" + ], "properties": { "responseStreamConnectionFailed": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -178,12 +303,21 @@ { "description": "The response SSE stream disconnected in the middle of a turn before completion.", "type": "object", - "required": ["responseStreamDisconnected"], + "required": [ + "responseStreamDisconnected" + ], "properties": { "responseStreamDisconnected": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -193,12 +327,21 @@ { "description": "Reached the retry limit for responses.", "type": "object", - "required": ["responseTooManyFailedAttempts"], + "required": [ + "responseTooManyFailedAttempts" + ], "properties": { "responseTooManyFailedAttempts": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -208,12 +351,20 @@ { "description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", "type": "object", - "required": ["activeTurnNotSteerable"], + "required": [ + "activeTurnNotSteerable" + ], "properties": { "activeTurnNotSteerable": { "type": "object", - "required": ["turnKind"], - "properties": { "turnKind": { "$ref": "#/definitions/NonSteerableTurnKind" } } + "required": [ + "turnKind" + ], + "properties": { + "turnKind": { + "$ref": "#/definitions/NonSteerableTurnKind" + } + } } }, "additionalProperties": false, @@ -223,10 +374,19 @@ }, "CollabAgentState": { "type": "object", - "required": ["status"], + "required": [ + "status" + ], "properties": { - "message": { "type": ["string", "null"] }, - "status": { "$ref": "#/definitions/CollabAgentStatus" } + "message": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/definitions/CollabAgentStatus" + } } }, "CollabAgentStatus": { @@ -243,34 +403,73 @@ }, "CollabAgentTool": { "type": "string", - "enum": ["spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent"] + "enum": [ + "spawnAgent", + "sendInput", + "resumeAgent", + "wait", + "closeAgent" + ] }, "CollabAgentToolCallStatus": { "type": "string", - "enum": ["inProgress", "completed", "failed"] + "enum": [ + "inProgress", + "completed", + "failed" + ] }, "CommandAction": { "oneOf": [ { "type": "object", - "required": ["command", "name", "path", "type"], + "required": [ + "command", + "name", + "path", + "type" + ], "properties": { - "command": { "type": "string" }, - "name": { "type": "string" }, - "path": { "$ref": "#/definitions/AbsolutePathBuf" }, - "type": { "type": "string", "enum": ["read"], "title": "ReadCommandActionType" } + "command": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "type": "string", + "enum": [ + "read" + ], + "title": "ReadCommandActionType" + } }, "title": "ReadCommandAction" }, { "type": "object", - "required": ["command", "type"], + "required": [ + "command", + "type" + ], "properties": { - "command": { "type": "string" }, - "path": { "type": ["string", "null"] }, + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, "type": { "type": "string", - "enum": ["listFiles"], + "enum": [ + "listFiles" + ], "title": "ListFilesCommandActionType" } }, @@ -278,21 +477,53 @@ }, { "type": "object", - "required": ["command", "type"], + "required": [ + "command", + "type" + ], "properties": { - "command": { "type": "string" }, - "path": { "type": ["string", "null"] }, - "query": { "type": ["string", "null"] }, - "type": { "type": "string", "enum": ["search"], "title": "SearchCommandActionType" } + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "search" + ], + "title": "SearchCommandActionType" + } }, "title": "SearchCommandAction" }, { "type": "object", - "required": ["command", "type"], + "required": [ + "command", + "type" + ], "properties": { - "command": { "type": "string" }, - "type": { "type": "string", "enum": ["unknown"], "title": "UnknownCommandActionType" } + "command": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "unknown" + ], + "title": "UnknownCommandActionType" + } }, "title": "UnknownCommandAction" } @@ -300,22 +531,39 @@ }, "CommandExecutionSource": { "type": "string", - "enum": ["agent", "userShell", "unifiedExecStartup", "unifiedExecInteraction"] + "enum": [ + "agent", + "userShell", + "unifiedExecStartup", + "unifiedExecInteraction" + ] }, "CommandExecutionStatus": { "type": "string", - "enum": ["inProgress", "completed", "failed", "declined"] + "enum": [ + "inProgress", + "completed", + "failed", + "declined" + ] }, "DynamicToolCallOutputContentItem": { "oneOf": [ { "type": "object", - "required": ["text", "type"], + "required": [ + "text", + "type" + ], "properties": { - "text": { "type": "string" }, + "text": { + "type": "string" + }, "type": { "type": "string", - "enum": ["inputText"], + "enum": [ + "inputText" + ], "title": "InputTextDynamicToolCallOutputContentItemType" } }, @@ -323,12 +571,19 @@ }, { "type": "object", - "required": ["imageUrl", "type"], + "required": [ + "imageUrl", + "type" + ], "properties": { - "imageUrl": { "type": "string" }, + "imageUrl": { + "type": "string" + }, "type": { "type": "string", - "enum": ["inputImage"], + "enum": [ + "inputImage" + ], "title": "InputImageDynamicToolCallOutputContentItemType" } }, @@ -336,27 +591,59 @@ } ] }, - "DynamicToolCallStatus": { "type": "string", "enum": ["inProgress", "completed", "failed"] }, - "FileSystemAccessMode": { "type": "string", "enum": ["read", "write", "none"] }, + "DynamicToolCallStatus": { + "type": "string", + "enum": [ + "inProgress", + "completed", + "failed" + ] + }, + "FileSystemAccessMode": { + "type": "string", + "enum": [ + "read", + "write", + "none" + ] + }, "FileSystemPath": { "oneOf": [ { "type": "object", - "required": ["path", "type"], + "required": [ + "path", + "type" + ], "properties": { - "path": { "$ref": "#/definitions/AbsolutePathBuf" }, - "type": { "type": "string", "enum": ["path"], "title": "PathFileSystemPathType" } + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "type": "string", + "enum": [ + "path" + ], + "title": "PathFileSystemPathType" + } }, "title": "PathFileSystemPath" }, { "type": "object", - "required": ["pattern", "type"], + "required": [ + "pattern", + "type" + ], "properties": { - "pattern": { "type": "string" }, + "pattern": { + "type": "string" + }, "type": { "type": "string", - "enum": ["glob_pattern"], + "enum": [ + "glob_pattern" + ], "title": "GlobPatternFileSystemPathType" } }, @@ -364,10 +651,21 @@ }, { "type": "object", - "required": ["type", "value"], + "required": [ + "type", + "value" + ], "properties": { - "type": { "type": "string", "enum": ["special"], "title": "SpecialFileSystemPathType" }, - "value": { "$ref": "#/definitions/FileSystemSpecialPath" } + "type": { + "type": "string", + "enum": [ + "special" + ], + "title": "SpecialFileSystemPathType" + }, + "value": { + "$ref": "#/definitions/FileSystemSpecialPath" + } }, "title": "SpecialFileSystemPath" } @@ -375,111 +673,264 @@ }, "FileSystemSandboxEntry": { "type": "object", - "required": ["access", "path"], + "required": [ + "access", + "path" + ], "properties": { - "access": { "$ref": "#/definitions/FileSystemAccessMode" }, - "path": { "$ref": "#/definitions/FileSystemPath" } + "access": { + "$ref": "#/definitions/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/FileSystemPath" + } } }, "FileSystemSpecialPath": { "oneOf": [ { "type": "object", - "required": ["kind"], - "properties": { "kind": { "type": "string", "enum": ["root"] } }, + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "root" + ] + } + }, "title": "RootFileSystemSpecialPath" }, { "type": "object", - "required": ["kind"], - "properties": { "kind": { "type": "string", "enum": ["minimal"] } }, + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "minimal" + ] + } + }, "title": "MinimalFileSystemSpecialPath" }, { "type": "object", - "required": ["kind"], + "required": [ + "kind" + ], "properties": { - "kind": { "type": "string", "enum": ["project_roots"] }, - "subpath": { "type": ["string", "null"] } + "kind": { + "type": "string", + "enum": [ + "project_roots" + ] + }, + "subpath": { + "type": [ + "string", + "null" + ] + } }, "title": "KindFileSystemSpecialPath" }, { "type": "object", - "required": ["kind"], - "properties": { "kind": { "type": "string", "enum": ["tmpdir"] } }, + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "tmpdir" + ] + } + }, "title": "TmpdirFileSystemSpecialPath" }, { "type": "object", - "required": ["kind"], - "properties": { "kind": { "type": "string", "enum": ["slash_tmp"] } }, + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "slash_tmp" + ] + } + }, "title": "SlashTmpFileSystemSpecialPath" }, { "type": "object", - "required": ["kind", "path"], + "required": [ + "kind", + "path" + ], "properties": { - "kind": { "type": "string", "enum": ["unknown"] }, - "path": { "type": "string" }, - "subpath": { "type": ["string", "null"] } + "kind": { + "type": "string", + "enum": [ + "unknown" + ] + }, + "path": { + "type": "string" + }, + "subpath": { + "type": [ + "string", + "null" + ] + } } } ] }, "FileUpdateChange": { "type": "object", - "required": ["diff", "kind", "path"], + "required": [ + "diff", + "kind", + "path" + ], "properties": { - "diff": { "type": "string" }, - "kind": { "$ref": "#/definitions/PatchChangeKind" }, - "path": { "type": "string" } + "diff": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/PatchChangeKind" + }, + "path": { + "type": "string" + } } }, "GitInfo": { "type": "object", "properties": { - "branch": { "type": ["string", "null"] }, - "originUrl": { "type": ["string", "null"] }, - "sha": { "type": ["string", "null"] } + "branch": { + "type": [ + "string", + "null" + ] + }, + "originUrl": { + "type": [ + "string", + "null" + ] + }, + "sha": { + "type": [ + "string", + "null" + ] + } } }, "HookPromptFragment": { "type": "object", - "required": ["hookRunId", "text"], - "properties": { "hookRunId": { "type": "string" }, "text": { "type": "string" } } + "required": [ + "hookRunId", + "text" + ], + "properties": { + "hookRunId": { + "type": "string" + }, + "text": { + "type": "string" + } + } }, "McpToolCallError": { "type": "object", - "required": ["message"], - "properties": { "message": { "type": "string" } } + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } }, "McpToolCallResult": { "type": "object", - "required": ["content"], + "required": [ + "content" + ], "properties": { "_meta": true, - "content": { "type": "array", "items": true }, + "content": { + "type": "array", + "items": true + }, "structuredContent": true } }, - "McpToolCallStatus": { "type": "string", "enum": ["inProgress", "completed", "failed"] }, + "McpToolCallStatus": { + "type": "string", + "enum": [ + "inProgress", + "completed", + "failed" + ] + }, "MemoryCitation": { "type": "object", - "required": ["entries", "threadIds"], + "required": [ + "entries", + "threadIds" + ], "properties": { - "entries": { "type": "array", "items": { "$ref": "#/definitions/MemoryCitationEntry" } }, - "threadIds": { "type": "array", "items": { "type": "string" } } + "entries": { + "type": "array", + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + } + }, + "threadIds": { + "type": "array", + "items": { + "type": "string" + } + } } }, "MemoryCitationEntry": { "type": "object", - "required": ["lineEnd", "lineStart", "note", "path"], + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], "properties": { - "lineEnd": { "type": "integer", "format": "uint32", "minimum": 0 }, - "lineStart": { "type": "integer", "format": "uint32", "minimum": 0 }, - "note": { "type": "string" }, - "path": { "type": "string" } + "lineEnd": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "lineStart": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } } }, "MessagePhase": { @@ -488,45 +939,95 @@ { "description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.", "type": "string", - "enum": ["commentary"] + "enum": [ + "commentary" + ] }, { "description": "The assistant's terminal answer text for the current turn.", "type": "string", - "enum": ["final_answer"] + "enum": [ + "final_answer" + ] } ] }, - "NetworkAccess": { "type": "string", "enum": ["restricted", "enabled"] }, - "NonSteerableTurnKind": { "type": "string", "enum": ["review", "compact"] }, + "NetworkAccess": { + "type": "string", + "enum": [ + "restricted", + "enabled" + ] + }, + "NonSteerableTurnKind": { + "type": "string", + "enum": [ + "review", + "compact" + ] + }, "PatchApplyStatus": { "type": "string", - "enum": ["inProgress", "completed", "failed", "declined"] + "enum": [ + "inProgress", + "completed", + "failed", + "declined" + ] }, "PatchChangeKind": { "oneOf": [ { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["add"], "title": "AddPatchChangeKindType" } + "type": { + "type": "string", + "enum": [ + "add" + ], + "title": "AddPatchChangeKindType" + } }, "title": "AddPatchChangeKind" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["delete"], "title": "DeletePatchChangeKindType" } + "type": { + "type": "string", + "enum": [ + "delete" + ], + "title": "DeletePatchChangeKindType" + } }, "title": "DeletePatchChangeKind" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "move_path": { "type": ["string", "null"] }, - "type": { "type": "string", "enum": ["update"], "title": "UpdatePatchChangeKindType" } + "move_path": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "update" + ], + "title": "UpdatePatchChangeKindType" + } }, "title": "UpdatePatchChangeKind" } @@ -537,13 +1038,23 @@ { "description": "Codex owns sandbox construction for this profile.", "type": "object", - "required": ["fileSystem", "network", "type"], + "required": [ + "fileSystem", + "network", + "type" + ], "properties": { - "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, + "fileSystem": { + "$ref": "#/definitions/PermissionProfileFileSystemPermissions" + }, + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, "type": { "type": "string", - "enum": ["managed"], + "enum": [ + "managed" + ], "title": "ManagedPermissionProfileType" } }, @@ -552,11 +1063,15 @@ { "description": "Do not apply an outer sandbox.", "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["disabled"], + "enum": [ + "disabled" + ], "title": "DisabledPermissionProfileType" } }, @@ -565,12 +1080,19 @@ { "description": "Filesystem isolation is enforced by an external caller.", "type": "object", - "required": ["network", "type"], + "required": [ + "network", + "type" + ], "properties": { - "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, "type": { "type": "string", - "enum": ["external"], + "enum": [ + "external" + ], "title": "ExternalPermissionProfileType" } }, @@ -582,16 +1104,30 @@ "oneOf": [ { "type": "object", - "required": ["entries", "type"], + "required": [ + "entries", + "type" + ], "properties": { "entries": { "type": "array", - "items": { "$ref": "#/definitions/FileSystemSandboxEntry" } + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + } + }, + "globScanMaxDepth": { + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 1 }, - "globScanMaxDepth": { "type": ["integer", "null"], "format": "uint", "minimum": 1 }, "type": { "type": "string", - "enum": ["restricted"], + "enum": [ + "restricted" + ], "title": "RestrictedPermissionProfileFileSystemPermissionsType" } }, @@ -599,11 +1135,15 @@ }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["unrestricted"], + "enum": [ + "unrestricted" + ], "title": "UnrestrictedPermissionProfileFileSystemPermissionsType" } }, @@ -613,23 +1153,40 @@ }, "PermissionProfileNetworkPermissions": { "type": "object", - "required": ["enabled"], - "properties": { "enabled": { "type": "boolean" } } + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "type": "string", - "enum": ["none", "minimal", "low", "medium", "high", "xhigh"] + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] }, "SandboxPolicy": { "oneOf": [ { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["dangerFullAccess"], + "enum": [ + "dangerFullAccess" + ], "title": "DangerFullAccessSandboxPolicyType" } }, @@ -637,24 +1194,43 @@ }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "networkAccess": { "default": false, "type": "boolean" }, - "type": { "type": "string", "enum": ["readOnly"], "title": "ReadOnlySandboxPolicyType" } + "networkAccess": { + "default": false, + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "readOnly" + ], + "title": "ReadOnlySandboxPolicyType" + } }, "title": "ReadOnlySandboxPolicy" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "networkAccess": { "default": "restricted", - "allOf": [{ "$ref": "#/definitions/NetworkAccess" }] + "allOf": [ + { + "$ref": "#/definitions/NetworkAccess" + } + ] }, "type": { "type": "string", - "enum": ["externalSandbox"], + "enum": [ + "externalSandbox" + ], "title": "ExternalSandboxSandboxPolicyType" } }, @@ -662,41 +1238,83 @@ }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "excludeSlashTmp": { "default": false, "type": "boolean" }, - "excludeTmpdirEnvVar": { "default": false, "type": "boolean" }, - "networkAccess": { "default": false, "type": "boolean" }, + "excludeSlashTmp": { + "default": false, + "type": "boolean" + }, + "excludeTmpdirEnvVar": { + "default": false, + "type": "boolean" + }, + "networkAccess": { + "default": false, + "type": "boolean" + }, "type": { "type": "string", - "enum": ["workspaceWrite"], + "enum": [ + "workspaceWrite" + ], "title": "WorkspaceWriteSandboxPolicyType" }, "writableRoots": { "default": [], "type": "array", - "items": { "$ref": "#/definitions/AbsolutePathBuf" } + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + } } }, "title": "WorkspaceWriteSandboxPolicy" } ] }, - "ServiceTier": { "type": "string", "enum": ["fast", "flex"] }, + "ServiceTier": { + "type": "string", + "enum": [ + "fast", + "flex" + ] + }, "SessionSource": { "oneOf": [ - { "type": "string", "enum": ["cli", "vscode", "exec", "appServer", "unknown"] }, + { + "type": "string", + "enum": [ + "cli", + "vscode", + "exec", + "appServer", + "unknown" + ] + }, { "type": "object", - "required": ["custom"], - "properties": { "custom": { "type": "string" } }, + "required": [ + "custom" + ], + "properties": { + "custom": { + "type": "string" + } + }, "additionalProperties": false, "title": "CustomSessionSource" }, { "type": "object", - "required": ["subAgent"], - "properties": { "subAgent": { "$ref": "#/definitions/SubAgentSource" } }, + "required": [ + "subAgent" + ], + "properties": { + "subAgent": { + "$ref": "#/definitions/SubAgentSource" + } + }, "additionalProperties": false, "title": "SubAgentSessionSource" } @@ -704,23 +1322,59 @@ }, "SubAgentSource": { "oneOf": [ - { "type": "string", "enum": ["review", "compact", "memory_consolidation"] }, + { + "type": "string", + "enum": [ + "review", + "compact", + "memory_consolidation" + ] + }, { "type": "object", - "required": ["thread_spawn"], + "required": [ + "thread_spawn" + ], "properties": { "thread_spawn": { "type": "object", - "required": ["depth", "parent_thread_id"], + "required": [ + "depth", + "parent_thread_id" + ], "properties": { - "agent_nickname": { "default": null, "type": ["string", "null"] }, + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, "agent_path": { "default": null, - "anyOf": [{ "$ref": "#/definitions/AgentPath" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/AgentPath" + }, + { + "type": "null" + } + ] }, - "agent_role": { "default": null, "type": ["string", "null"] }, - "depth": { "type": "integer", "format": "int32" }, - "parent_thread_id": { "$ref": "#/definitions/ThreadId" } + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "depth": { + "type": "integer", + "format": "int32" + }, + "parent_thread_id": { + "$ref": "#/definitions/ThreadId" + } } } }, @@ -729,8 +1383,14 @@ }, { "type": "object", - "required": ["other"], - "properties": { "other": { "type": "string" } }, + "required": [ + "other" + ], + "properties": { + "other": { + "type": "string" + } + }, "additionalProperties": false, "title": "OtherSubAgentSource" } @@ -738,15 +1398,24 @@ }, "TextElement": { "type": "object", - "required": ["byteRange"], + "required": [ + "byteRange" + ], "properties": { "byteRange": { "description": "Byte range in the parent `text` buffer that this element occupies.", - "allOf": [{ "$ref": "#/definitions/ByteRange" }] + "allOf": [ + { + "$ref": "#/definitions/ByteRange" + } + ] }, "placeholder": { "description": "Optional human-readable placeholder for the element, displayed in the UI.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] } } }, @@ -768,11 +1437,17 @@ "properties": { "agentNickname": { "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "agentRole": { "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "cliVersion": { "description": "Version of the CLI that created the thread.", @@ -785,7 +1460,11 @@ }, "cwd": { "description": "Working directory captured for the thread.", - "allOf": [{ "$ref": "#/definitions/AbsolutePathBuf" }] + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] }, "ephemeral": { "description": "Whether the thread is ephemeral and should not be materialized on disk.", @@ -793,21 +1472,42 @@ }, "forkedFromId": { "description": "Source thread id when this thread was created by forking another thread.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "gitInfo": { "description": "Optional Git metadata captured when the thread was created.", - "anyOf": [{ "$ref": "#/definitions/GitInfo" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/GitInfo" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" }, - "id": { "type": "string" }, "modelProvider": { "description": "Model provider used for this thread (for example, 'openai').", "type": "string" }, - "name": { "description": "Optional user-facing thread title.", "type": ["string", "null"] }, + "name": { + "description": "Optional user-facing thread title.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "preview": { "description": "Usually the first user message in the thread, if available.", @@ -815,16 +1515,37 @@ }, "source": { "description": "Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.).", - "allOf": [{ "$ref": "#/definitions/SessionSource" }] + "allOf": [ + { + "$ref": "#/definitions/SessionSource" + } + ] }, "status": { "description": "Current runtime status for the thread.", - "allOf": [{ "$ref": "#/definitions/ThreadStatus" }] + "allOf": [ + { + "$ref": "#/definitions/ThreadStatus" + } + ] + }, + "threadSource": { + "description": "Optional analytics source classification for this thread.", + "anyOf": [ + { + "$ref": "#/definitions/ThreadSource" + }, + { + "type": "null" + } + ] }, "turns": { "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "type": "array", - "items": { "$ref": "#/definitions/Turn" } + "items": { + "$ref": "#/definitions/Turn" + } }, "updatedAt": { "description": "Unix timestamp (in seconds) when the thread was last updated.", @@ -833,19 +1554,40 @@ } } }, - "ThreadActiveFlag": { "type": "string", "enum": ["waitingOnApproval", "waitingOnUserInput"] }, - "ThreadId": { "type": "string" }, + "ThreadActiveFlag": { + "type": "string", + "enum": [ + "waitingOnApproval", + "waitingOnUserInput" + ] + }, + "ThreadId": { + "type": "string" + }, "ThreadItem": { "oneOf": [ { "type": "object", - "required": ["content", "id", "type"], + "required": [ + "content", + "id", + "type" + ], "properties": { - "content": { "type": "array", "items": { "$ref": "#/definitions/UserInput" } }, - "id": { "type": "string" }, + "content": { + "type": "array", + "items": { + "$ref": "#/definitions/UserInput" + } + }, + "id": { + "type": "string" + }, "type": { "type": "string", - "enum": ["userMessage"], + "enum": [ + "userMessage" + ], "title": "UserMessageThreadItemType" } }, @@ -853,16 +1595,26 @@ }, { "type": "object", - "required": ["fragments", "id", "type"], + "required": [ + "fragments", + "id", + "type" + ], "properties": { "fragments": { "type": "array", - "items": { "$ref": "#/definitions/HookPromptFragment" } + "items": { + "$ref": "#/definitions/HookPromptFragment" + } + }, + "id": { + "type": "string" }, - "id": { "type": "string" }, "type": { "type": "string", - "enum": ["hookPrompt"], + "enum": [ + "hookPrompt" + ], "title": "HookPromptThreadItemType" } }, @@ -870,21 +1622,45 @@ }, { "type": "object", - "required": ["id", "text", "type"], + "required": [ + "id", + "text", + "type" + ], "properties": { - "id": { "type": "string" }, + "id": { + "type": "string" + }, "memoryCitation": { "default": null, - "anyOf": [{ "$ref": "#/definitions/MemoryCitation" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ] }, "phase": { "default": null, - "anyOf": [{ "$ref": "#/definitions/MessagePhase" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, + "text": { + "type": "string" }, - "text": { "type": "string" }, "type": { "type": "string", - "enum": ["agentMessage"], + "enum": [ + "agentMessage" + ], "title": "AgentMessageThreadItemType" } }, @@ -893,66 +1669,141 @@ { "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", "type": "object", - "required": ["id", "text", "type"], + "required": [ + "id", + "text", + "type" + ], "properties": { - "id": { "type": "string" }, - "text": { "type": "string" }, - "type": { "type": "string", "enum": ["plan"], "title": "PlanThreadItemType" } + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "plan" + ], + "title": "PlanThreadItemType" + } }, "title": "PlanThreadItem" }, { "type": "object", - "required": ["id", "type"], + "required": [ + "id", + "type" + ], "properties": { - "content": { "default": [], "type": "array", "items": { "type": "string" } }, - "id": { "type": "string" }, - "summary": { "default": [], "type": "array", "items": { "type": "string" } }, - "type": { "type": "string", "enum": ["reasoning"], "title": "ReasoningThreadItemType" } + "content": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "summary": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string", + "enum": [ + "reasoning" + ], + "title": "ReasoningThreadItemType" + } }, "title": "ReasoningThreadItem" }, { "type": "object", - "required": ["command", "commandActions", "cwd", "id", "status", "type"], + "required": [ + "command", + "commandActions", + "cwd", + "id", + "status", + "type" + ], "properties": { "aggregatedOutput": { "description": "The command's output, aggregated from stdout and stderr.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] + }, + "command": { + "description": "The command to be executed.", + "type": "string" }, - "command": { "description": "The command to be executed.", "type": "string" }, "commandActions": { "description": "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", "type": "array", - "items": { "$ref": "#/definitions/CommandAction" } + "items": { + "$ref": "#/definitions/CommandAction" + } }, "cwd": { "description": "The command's working directory.", - "allOf": [{ "$ref": "#/definitions/AbsolutePathBuf" }] + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] }, "durationMs": { "description": "The duration of the command execution in milliseconds.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "exitCode": { "description": "The command's exit code.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int32" }, - "id": { "type": "string" }, + "id": { + "type": "string" + }, "processId": { "description": "Identifier for the underlying PTY process (when available).", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "source": { "default": "agent", - "allOf": [{ "$ref": "#/definitions/CommandExecutionSource" }] + "allOf": [ + { + "$ref": "#/definitions/CommandExecutionSource" + } + ] + }, + "status": { + "$ref": "#/definitions/CommandExecutionStatus" }, - "status": { "$ref": "#/definitions/CommandExecutionStatus" }, "type": { "type": "string", - "enum": ["commandExecution"], + "enum": [ + "commandExecution" + ], "title": "CommandExecutionThreadItemType" } }, @@ -960,14 +1811,30 @@ }, { "type": "object", - "required": ["changes", "id", "status", "type"], + "required": [ + "changes", + "id", + "status", + "type" + ], "properties": { - "changes": { "type": "array", "items": { "$ref": "#/definitions/FileUpdateChange" } }, - "id": { "type": "string" }, - "status": { "$ref": "#/definitions/PatchApplyStatus" }, + "changes": { + "type": "array", + "items": { + "$ref": "#/definitions/FileUpdateChange" + } + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/PatchApplyStatus" + }, "type": { "type": "string", - "enum": ["fileChange"], + "enum": [ + "fileChange" + ], "title": "FileChangeThreadItemType" } }, @@ -975,28 +1842,67 @@ }, { "type": "object", - "required": ["arguments", "id", "server", "status", "tool", "type"], + "required": [ + "arguments", + "id", + "server", + "status", + "tool", + "type" + ], "properties": { "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "error": { - "anyOf": [{ "$ref": "#/definitions/McpToolCallError" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "mcpAppResourceUri": { + "type": [ + "string", + "null" + ] }, - "id": { "type": "string" }, - "mcpAppResourceUri": { "type": ["string", "null"] }, "result": { - "anyOf": [{ "$ref": "#/definitions/McpToolCallResult" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallResult" + }, + { + "type": "null" + } + ] + }, + "server": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpToolCallStatus" + }, + "tool": { + "type": "string" }, - "server": { "type": "string" }, - "status": { "$ref": "#/definitions/McpToolCallStatus" }, - "tool": { "type": "string" }, "type": { "type": "string", - "enum": ["mcpToolCall"], + "enum": [ + "mcpToolCall" + ], "title": "McpToolCallThreadItemType" } }, @@ -1004,26 +1910,58 @@ }, { "type": "object", - "required": ["arguments", "id", "status", "tool", "type"], + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], "properties": { "arguments": true, "contentItems": { - "type": ["array", "null"], - "items": { "$ref": "#/definitions/DynamicToolCallOutputContentItem" } + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + } }, "durationMs": { "description": "The duration of the dynamic tool call in milliseconds.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, - "id": { "type": "string" }, - "namespace": { "type": ["string", "null"] }, - "status": { "$ref": "#/definitions/DynamicToolCallStatus" }, - "success": { "type": ["boolean", "null"] }, - "tool": { "type": "string" }, + "id": { + "type": "string" + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, "type": { "type": "string", - "enum": ["dynamicToolCall"], + "enum": [ + "dynamicToolCall" + ], "title": "DynamicToolCallThreadItemType" } }, @@ -1044,7 +1982,9 @@ "agentsStates": { "description": "Last known status of the target agents, when available.", "type": "object", - "additionalProperties": { "$ref": "#/definitions/CollabAgentState" } + "additionalProperties": { + "$ref": "#/definitions/CollabAgentState" + } }, "id": { "description": "Unique identifier for this collab tool call.", @@ -1052,20 +1992,35 @@ }, "model": { "description": "Model requested for the spawned agent, when applicable.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "prompt": { "description": "Prompt text sent as part of the collab tool call, when available.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "reasoningEffort": { "description": "Reasoning effort requested for the spawned agent, when applicable.", - "anyOf": [{ "$ref": "#/definitions/ReasoningEffort" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] }, "receiverThreadIds": { "description": "Thread ID of the receiving agent, when applicable. In case of spawn operation, this corresponds to the newly spawned agent.", "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, "senderThreadId": { "description": "Thread ID of the agent issuing the collab request.", @@ -1073,15 +2028,25 @@ }, "status": { "description": "Current status of the collab tool call.", - "allOf": [{ "$ref": "#/definitions/CollabAgentToolCallStatus" }] + "allOf": [ + { + "$ref": "#/definitions/CollabAgentToolCallStatus" + } + ] }, "tool": { "description": "Name of the collab tool that was invoked.", - "allOf": [{ "$ref": "#/definitions/CollabAgentTool" }] + "allOf": [ + { + "$ref": "#/definitions/CollabAgentTool" + } + ] }, "type": { "type": "string", - "enum": ["collabAgentToolCall"], + "enum": [ + "collabAgentToolCall" + ], "title": "CollabAgentToolCallThreadItemType" } }, @@ -1089,41 +2054,101 @@ }, { "type": "object", - "required": ["id", "query", "type"], + "required": [ + "id", + "query", + "type" + ], "properties": { "action": { - "anyOf": [{ "$ref": "#/definitions/WebSearchAction" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/WebSearchAction" + }, + { + "type": "null" + } + ] }, - "id": { "type": "string" }, - "query": { "type": "string" }, - "type": { "type": "string", "enum": ["webSearch"], "title": "WebSearchThreadItemType" } + "id": { + "type": "string" + }, + "query": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "webSearch" + ], + "title": "WebSearchThreadItemType" + } }, "title": "WebSearchThreadItem" }, { "type": "object", - "required": ["id", "path", "type"], + "required": [ + "id", + "path", + "type" + ], "properties": { - "id": { "type": "string" }, - "path": { "$ref": "#/definitions/AbsolutePathBuf" }, - "type": { "type": "string", "enum": ["imageView"], "title": "ImageViewThreadItemType" } + "id": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "type": "string", + "enum": [ + "imageView" + ], + "title": "ImageViewThreadItemType" + } }, "title": "ImageViewThreadItem" }, { "type": "object", - "required": ["id", "result", "status", "type"], + "required": [ + "id", + "result", + "status", + "type" + ], "properties": { - "id": { "type": "string" }, - "result": { "type": "string" }, - "revisedPrompt": { "type": ["string", "null"] }, - "savedPath": { - "anyOf": [{ "$ref": "#/definitions/AbsolutePathBuf" }, { "type": "null" }] + "id": { + "type": "string" + }, + "result": { + "type": "string" + }, + "revisedPrompt": { + "type": [ + "string", + "null" + ] + }, + "savedPath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string" }, - "status": { "type": "string" }, "type": { "type": "string", - "enum": ["imageGeneration"], + "enum": [ + "imageGeneration" + ], "title": "ImageGenerationThreadItemType" } }, @@ -1131,13 +2156,23 @@ }, { "type": "object", - "required": ["id", "review", "type"], + "required": [ + "id", + "review", + "type" + ], "properties": { - "id": { "type": "string" }, - "review": { "type": "string" }, + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, "type": { "type": "string", - "enum": ["enteredReviewMode"], + "enum": [ + "enteredReviewMode" + ], "title": "EnteredReviewModeThreadItemType" } }, @@ -1145,13 +2180,23 @@ }, { "type": "object", - "required": ["id", "review", "type"], + "required": [ + "id", + "review", + "type" + ], "properties": { - "id": { "type": "string" }, - "review": { "type": "string" }, + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, "type": { "type": "string", - "enum": ["exitedReviewMode"], + "enum": [ + "exitedReviewMode" + ], "title": "ExitedReviewModeThreadItemType" } }, @@ -1159,12 +2204,19 @@ }, { "type": "object", - "required": ["id", "type"], + "required": [ + "id", + "type" + ], "properties": { - "id": { "type": "string" }, + "id": { + "type": "string" + }, "type": { "type": "string", - "enum": ["contextCompaction"], + "enum": [ + "contextCompaction" + ], "title": "ContextCompactionThreadItemType" } }, @@ -1172,15 +2224,27 @@ } ] }, + "ThreadSource": { + "type": "string", + "enum": [ + "user", + "subagent", + "memory_consolidation" + ] + }, "ThreadStatus": { "oneOf": [ { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["notLoaded"], + "enum": [ + "notLoaded" + ], "title": "NotLoadedThreadStatusType" } }, @@ -1188,19 +2252,31 @@ }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["idle"], "title": "IdleThreadStatusType" } + "type": { + "type": "string", + "enum": [ + "idle" + ], + "title": "IdleThreadStatusType" + } }, "title": "IdleThreadStatus" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["systemError"], + "enum": [ + "systemError" + ], "title": "SystemErrorThreadStatusType" } }, @@ -1208,13 +2284,24 @@ }, { "type": "object", - "required": ["activeFlags", "type"], + "required": [ + "activeFlags", + "type" + ], "properties": { "activeFlags": { "type": "array", - "items": { "$ref": "#/definitions/ThreadActiveFlag" } + "items": { + "$ref": "#/definitions/ThreadActiveFlag" + } }, - "type": { "type": "string", "enum": ["active"], "title": "ActiveThreadStatusType" } + "type": { + "type": "string", + "enum": [ + "active" + ], + "title": "ActiveThreadStatusType" + } }, "title": "ActiveThreadStatus" } @@ -1222,103 +2309,248 @@ }, "Turn": { "type": "object", - "required": ["id", "items", "status"], + "required": [ + "id", + "items", + "status" + ], "properties": { "completedAt": { "description": "Unix timestamp (in seconds) when the turn completed.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "durationMs": { "description": "Duration between turn start and completion in milliseconds, if known.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "error": { "description": "Only populated when the Turn's status is failed.", - "anyOf": [{ "$ref": "#/definitions/TurnError" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/TurnError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" }, - "id": { "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "type": "array", - "items": { "$ref": "#/definitions/ThreadItem" } + "items": { + "$ref": "#/definitions/ThreadItem" + } + }, + "itemsView": { + "description": "Describes how much of `items` has been loaded for this turn.", + "default": "full", + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ] }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, - "status": { "$ref": "#/definitions/TurnStatus" } + "status": { + "$ref": "#/definitions/TurnStatus" + } } }, "TurnError": { "type": "object", - "required": ["message"], + "required": [ + "message" + ], "properties": { - "additionalDetails": { "default": null, "type": ["string", "null"] }, - "codexErrorInfo": { - "anyOf": [{ "$ref": "#/definitions/CodexErrorInfo" }, { "type": "null" }] + "additionalDetails": { + "default": null, + "type": [ + "string", + "null" + ] }, - "message": { "type": "string" } + "codexErrorInfo": { + "anyOf": [ + { + "$ref": "#/definitions/CodexErrorInfo" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": "string" + } } }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "type": "string", + "enum": [ + "notLoaded" + ] + }, + { + "description": "`items` contains only a display summary for this turn.", + "type": "string", + "enum": [ + "summary" + ] + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "type": "string", + "enum": [ + "full" + ] + } + ] + }, "TurnStatus": { "type": "string", - "enum": ["completed", "interrupted", "failed", "inProgress"] + "enum": [ + "completed", + "interrupted", + "failed", + "inProgress" + ] }, "UserInput": { "oneOf": [ { "type": "object", - "required": ["text", "type"], + "required": [ + "text", + "type" + ], "properties": { - "text": { "type": "string" }, + "text": { + "type": "string" + }, "text_elements": { "description": "UI-defined spans within `text` used to render or persist special elements.", "default": [], "type": "array", - "items": { "$ref": "#/definitions/TextElement" } + "items": { + "$ref": "#/definitions/TextElement" + } }, - "type": { "type": "string", "enum": ["text"], "title": "TextUserInputType" } + "type": { + "type": "string", + "enum": [ + "text" + ], + "title": "TextUserInputType" + } }, "title": "TextUserInput" }, { "type": "object", - "required": ["type", "url"], + "required": [ + "type", + "url" + ], "properties": { - "type": { "type": "string", "enum": ["image"], "title": "ImageUserInputType" }, - "url": { "type": "string" } + "type": { + "type": "string", + "enum": [ + "image" + ], + "title": "ImageUserInputType" + }, + "url": { + "type": "string" + } }, "title": "ImageUserInput" }, { "type": "object", - "required": ["path", "type"], + "required": [ + "path", + "type" + ], "properties": { - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["localImage"], "title": "LocalImageUserInputType" } + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "localImage" + ], + "title": "LocalImageUserInputType" + } }, "title": "LocalImageUserInput" }, { "type": "object", - "required": ["name", "path", "type"], + "required": [ + "name", + "path", + "type" + ], "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["skill"], "title": "SkillUserInputType" } + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "skill" + ], + "title": "SkillUserInputType" + } }, "title": "SkillUserInput" }, { "type": "object", - "required": ["name", "path", "type"], + "required": [ + "name", + "path", + "type" + ], "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["mention"], "title": "MentionUserInputType" } + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mention" + ], + "title": "MentionUserInputType" + } }, "title": "MentionUserInput" } @@ -1328,46 +2560,98 @@ "oneOf": [ { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "queries": { "type": ["array", "null"], "items": { "type": "string" } }, - "query": { "type": ["string", "null"] }, - "type": { "type": "string", "enum": ["search"], "title": "SearchWebSearchActionType" } + "queries": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "search" + ], + "title": "SearchWebSearchActionType" + } }, "title": "SearchWebSearchAction" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["openPage"], + "enum": [ + "openPage" + ], "title": "OpenPageWebSearchActionType" }, - "url": { "type": ["string", "null"] } + "url": { + "type": [ + "string", + "null" + ] + } }, "title": "OpenPageWebSearchAction" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "pattern": { "type": ["string", "null"] }, + "pattern": { + "type": [ + "string", + "null" + ] + }, "type": { "type": "string", - "enum": ["findInPage"], + "enum": [ + "findInPage" + ], "title": "FindInPageWebSearchActionType" }, - "url": { "type": ["string", "null"] } + "url": { + "type": [ + "string", + "null" + ] + } }, "title": "FindInPageWebSearchAction" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["other"], "title": "OtherWebSearchActionType" } + "type": { + "type": "string", + "enum": [ + "other" + ], + "title": "OtherWebSearchActionType" + } }, "title": "OtherWebSearchAction" } diff --git a/extensions/codex/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json b/extensions/codex/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json index 80bef4b7275..792e82593ec 100644 --- a/extensions/codex/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +++ b/extensions/codex/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json @@ -2,8 +2,18 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "TurnCompletedNotification", "type": "object", - "required": ["threadId", "turn"], - "properties": { "threadId": { "type": "string" }, "turn": { "$ref": "#/definitions/Turn" } }, + "required": [ + "threadId", + "turn" + ], + "properties": { + "threadId": { + "type": "string" + }, + "turn": { + "$ref": "#/definitions/Turn" + } + }, "definitions": { "AbsolutePathBuf": { "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", @@ -11,10 +21,21 @@ }, "ByteRange": { "type": "object", - "required": ["end", "start"], + "required": [ + "end", + "start" + ], "properties": { - "end": { "type": "integer", "format": "uint", "minimum": 0 }, - "start": { "type": "integer", "format": "uint", "minimum": 0 } + "end": { + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0 + } } }, "CodexErrorInfo": { @@ -37,12 +58,21 @@ }, { "type": "object", - "required": ["httpConnectionFailed"], + "required": [ + "httpConnectionFailed" + ], "properties": { "httpConnectionFailed": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -52,12 +82,21 @@ { "description": "Failed to connect to the response SSE stream.", "type": "object", - "required": ["responseStreamConnectionFailed"], + "required": [ + "responseStreamConnectionFailed" + ], "properties": { "responseStreamConnectionFailed": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -67,12 +106,21 @@ { "description": "The response SSE stream disconnected in the middle of a turn before completion.", "type": "object", - "required": ["responseStreamDisconnected"], + "required": [ + "responseStreamDisconnected" + ], "properties": { "responseStreamDisconnected": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -82,12 +130,21 @@ { "description": "Reached the retry limit for responses.", "type": "object", - "required": ["responseTooManyFailedAttempts"], + "required": [ + "responseTooManyFailedAttempts" + ], "properties": { "responseTooManyFailedAttempts": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -97,12 +154,20 @@ { "description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", "type": "object", - "required": ["activeTurnNotSteerable"], + "required": [ + "activeTurnNotSteerable" + ], "properties": { "activeTurnNotSteerable": { "type": "object", - "required": ["turnKind"], - "properties": { "turnKind": { "$ref": "#/definitions/NonSteerableTurnKind" } } + "required": [ + "turnKind" + ], + "properties": { + "turnKind": { + "$ref": "#/definitions/NonSteerableTurnKind" + } + } } }, "additionalProperties": false, @@ -112,10 +177,19 @@ }, "CollabAgentState": { "type": "object", - "required": ["status"], + "required": [ + "status" + ], "properties": { - "message": { "type": ["string", "null"] }, - "status": { "$ref": "#/definitions/CollabAgentStatus" } + "message": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/definitions/CollabAgentStatus" + } } }, "CollabAgentStatus": { @@ -132,34 +206,73 @@ }, "CollabAgentTool": { "type": "string", - "enum": ["spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent"] + "enum": [ + "spawnAgent", + "sendInput", + "resumeAgent", + "wait", + "closeAgent" + ] }, "CollabAgentToolCallStatus": { "type": "string", - "enum": ["inProgress", "completed", "failed"] + "enum": [ + "inProgress", + "completed", + "failed" + ] }, "CommandAction": { "oneOf": [ { "type": "object", - "required": ["command", "name", "path", "type"], + "required": [ + "command", + "name", + "path", + "type" + ], "properties": { - "command": { "type": "string" }, - "name": { "type": "string" }, - "path": { "$ref": "#/definitions/AbsolutePathBuf" }, - "type": { "type": "string", "enum": ["read"], "title": "ReadCommandActionType" } + "command": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "type": "string", + "enum": [ + "read" + ], + "title": "ReadCommandActionType" + } }, "title": "ReadCommandAction" }, { "type": "object", - "required": ["command", "type"], + "required": [ + "command", + "type" + ], "properties": { - "command": { "type": "string" }, - "path": { "type": ["string", "null"] }, + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, "type": { "type": "string", - "enum": ["listFiles"], + "enum": [ + "listFiles" + ], "title": "ListFilesCommandActionType" } }, @@ -167,21 +280,53 @@ }, { "type": "object", - "required": ["command", "type"], + "required": [ + "command", + "type" + ], "properties": { - "command": { "type": "string" }, - "path": { "type": ["string", "null"] }, - "query": { "type": ["string", "null"] }, - "type": { "type": "string", "enum": ["search"], "title": "SearchCommandActionType" } + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "search" + ], + "title": "SearchCommandActionType" + } }, "title": "SearchCommandAction" }, { "type": "object", - "required": ["command", "type"], + "required": [ + "command", + "type" + ], "properties": { - "command": { "type": "string" }, - "type": { "type": "string", "enum": ["unknown"], "title": "UnknownCommandActionType" } + "command": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "unknown" + ], + "title": "UnknownCommandActionType" + } }, "title": "UnknownCommandAction" } @@ -189,22 +334,39 @@ }, "CommandExecutionSource": { "type": "string", - "enum": ["agent", "userShell", "unifiedExecStartup", "unifiedExecInteraction"] + "enum": [ + "agent", + "userShell", + "unifiedExecStartup", + "unifiedExecInteraction" + ] }, "CommandExecutionStatus": { "type": "string", - "enum": ["inProgress", "completed", "failed", "declined"] + "enum": [ + "inProgress", + "completed", + "failed", + "declined" + ] }, "DynamicToolCallOutputContentItem": { "oneOf": [ { "type": "object", - "required": ["text", "type"], + "required": [ + "text", + "type" + ], "properties": { - "text": { "type": "string" }, + "text": { + "type": "string" + }, "type": { "type": "string", - "enum": ["inputText"], + "enum": [ + "inputText" + ], "title": "InputTextDynamicToolCallOutputContentItemType" } }, @@ -212,12 +374,19 @@ }, { "type": "object", - "required": ["imageUrl", "type"], + "required": [ + "imageUrl", + "type" + ], "properties": { - "imageUrl": { "type": "string" }, + "imageUrl": { + "type": "string" + }, "type": { "type": "string", - "enum": ["inputImage"], + "enum": [ + "inputImage" + ], "title": "InputImageDynamicToolCallOutputContentItemType" } }, @@ -225,52 +394,127 @@ } ] }, - "DynamicToolCallStatus": { "type": "string", "enum": ["inProgress", "completed", "failed"] }, + "DynamicToolCallStatus": { + "type": "string", + "enum": [ + "inProgress", + "completed", + "failed" + ] + }, "FileUpdateChange": { "type": "object", - "required": ["diff", "kind", "path"], + "required": [ + "diff", + "kind", + "path" + ], "properties": { - "diff": { "type": "string" }, - "kind": { "$ref": "#/definitions/PatchChangeKind" }, - "path": { "type": "string" } + "diff": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/PatchChangeKind" + }, + "path": { + "type": "string" + } } }, "HookPromptFragment": { "type": "object", - "required": ["hookRunId", "text"], - "properties": { "hookRunId": { "type": "string" }, "text": { "type": "string" } } + "required": [ + "hookRunId", + "text" + ], + "properties": { + "hookRunId": { + "type": "string" + }, + "text": { + "type": "string" + } + } }, "McpToolCallError": { "type": "object", - "required": ["message"], - "properties": { "message": { "type": "string" } } + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } }, "McpToolCallResult": { "type": "object", - "required": ["content"], + "required": [ + "content" + ], "properties": { "_meta": true, - "content": { "type": "array", "items": true }, + "content": { + "type": "array", + "items": true + }, "structuredContent": true } }, - "McpToolCallStatus": { "type": "string", "enum": ["inProgress", "completed", "failed"] }, + "McpToolCallStatus": { + "type": "string", + "enum": [ + "inProgress", + "completed", + "failed" + ] + }, "MemoryCitation": { "type": "object", - "required": ["entries", "threadIds"], + "required": [ + "entries", + "threadIds" + ], "properties": { - "entries": { "type": "array", "items": { "$ref": "#/definitions/MemoryCitationEntry" } }, - "threadIds": { "type": "array", "items": { "type": "string" } } + "entries": { + "type": "array", + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + } + }, + "threadIds": { + "type": "array", + "items": { + "type": "string" + } + } } }, "MemoryCitationEntry": { "type": "object", - "required": ["lineEnd", "lineStart", "note", "path"], + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], "properties": { - "lineEnd": { "type": "integer", "format": "uint32", "minimum": 0 }, - "lineStart": { "type": "integer", "format": "uint32", "minimum": 0 }, - "note": { "type": "string" }, - "path": { "type": "string" } + "lineEnd": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "lineStart": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } } }, "MessagePhase": { @@ -279,44 +523,88 @@ { "description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.", "type": "string", - "enum": ["commentary"] + "enum": [ + "commentary" + ] }, { "description": "The assistant's terminal answer text for the current turn.", "type": "string", - "enum": ["final_answer"] + "enum": [ + "final_answer" + ] } ] }, - "NonSteerableTurnKind": { "type": "string", "enum": ["review", "compact"] }, + "NonSteerableTurnKind": { + "type": "string", + "enum": [ + "review", + "compact" + ] + }, "PatchApplyStatus": { "type": "string", - "enum": ["inProgress", "completed", "failed", "declined"] + "enum": [ + "inProgress", + "completed", + "failed", + "declined" + ] }, "PatchChangeKind": { "oneOf": [ { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["add"], "title": "AddPatchChangeKindType" } + "type": { + "type": "string", + "enum": [ + "add" + ], + "title": "AddPatchChangeKindType" + } }, "title": "AddPatchChangeKind" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["delete"], "title": "DeletePatchChangeKindType" } + "type": { + "type": "string", + "enum": [ + "delete" + ], + "title": "DeletePatchChangeKindType" + } }, "title": "DeletePatchChangeKind" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "move_path": { "type": ["string", "null"] }, - "type": { "type": "string", "enum": ["update"], "title": "UpdatePatchChangeKindType" } + "move_path": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "update" + ], + "title": "UpdatePatchChangeKindType" + } }, "title": "UpdatePatchChangeKind" } @@ -325,19 +613,35 @@ "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "type": "string", - "enum": ["none", "minimal", "low", "medium", "high", "xhigh"] + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] }, "TextElement": { "type": "object", - "required": ["byteRange"], + "required": [ + "byteRange" + ], "properties": { "byteRange": { "description": "Byte range in the parent `text` buffer that this element occupies.", - "allOf": [{ "$ref": "#/definitions/ByteRange" }] + "allOf": [ + { + "$ref": "#/definitions/ByteRange" + } + ] }, "placeholder": { "description": "Optional human-readable placeholder for the element, displayed in the UI.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] } } }, @@ -345,13 +649,26 @@ "oneOf": [ { "type": "object", - "required": ["content", "id", "type"], + "required": [ + "content", + "id", + "type" + ], "properties": { - "content": { "type": "array", "items": { "$ref": "#/definitions/UserInput" } }, - "id": { "type": "string" }, + "content": { + "type": "array", + "items": { + "$ref": "#/definitions/UserInput" + } + }, + "id": { + "type": "string" + }, "type": { "type": "string", - "enum": ["userMessage"], + "enum": [ + "userMessage" + ], "title": "UserMessageThreadItemType" } }, @@ -359,16 +676,26 @@ }, { "type": "object", - "required": ["fragments", "id", "type"], + "required": [ + "fragments", + "id", + "type" + ], "properties": { "fragments": { "type": "array", - "items": { "$ref": "#/definitions/HookPromptFragment" } + "items": { + "$ref": "#/definitions/HookPromptFragment" + } + }, + "id": { + "type": "string" }, - "id": { "type": "string" }, "type": { "type": "string", - "enum": ["hookPrompt"], + "enum": [ + "hookPrompt" + ], "title": "HookPromptThreadItemType" } }, @@ -376,21 +703,45 @@ }, { "type": "object", - "required": ["id", "text", "type"], + "required": [ + "id", + "text", + "type" + ], "properties": { - "id": { "type": "string" }, + "id": { + "type": "string" + }, "memoryCitation": { "default": null, - "anyOf": [{ "$ref": "#/definitions/MemoryCitation" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ] }, "phase": { "default": null, - "anyOf": [{ "$ref": "#/definitions/MessagePhase" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, + "text": { + "type": "string" }, - "text": { "type": "string" }, "type": { "type": "string", - "enum": ["agentMessage"], + "enum": [ + "agentMessage" + ], "title": "AgentMessageThreadItemType" } }, @@ -399,66 +750,141 @@ { "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", "type": "object", - "required": ["id", "text", "type"], + "required": [ + "id", + "text", + "type" + ], "properties": { - "id": { "type": "string" }, - "text": { "type": "string" }, - "type": { "type": "string", "enum": ["plan"], "title": "PlanThreadItemType" } + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "plan" + ], + "title": "PlanThreadItemType" + } }, "title": "PlanThreadItem" }, { "type": "object", - "required": ["id", "type"], + "required": [ + "id", + "type" + ], "properties": { - "content": { "default": [], "type": "array", "items": { "type": "string" } }, - "id": { "type": "string" }, - "summary": { "default": [], "type": "array", "items": { "type": "string" } }, - "type": { "type": "string", "enum": ["reasoning"], "title": "ReasoningThreadItemType" } + "content": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "summary": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string", + "enum": [ + "reasoning" + ], + "title": "ReasoningThreadItemType" + } }, "title": "ReasoningThreadItem" }, { "type": "object", - "required": ["command", "commandActions", "cwd", "id", "status", "type"], + "required": [ + "command", + "commandActions", + "cwd", + "id", + "status", + "type" + ], "properties": { "aggregatedOutput": { "description": "The command's output, aggregated from stdout and stderr.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] + }, + "command": { + "description": "The command to be executed.", + "type": "string" }, - "command": { "description": "The command to be executed.", "type": "string" }, "commandActions": { "description": "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", "type": "array", - "items": { "$ref": "#/definitions/CommandAction" } + "items": { + "$ref": "#/definitions/CommandAction" + } }, "cwd": { "description": "The command's working directory.", - "allOf": [{ "$ref": "#/definitions/AbsolutePathBuf" }] + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] }, "durationMs": { "description": "The duration of the command execution in milliseconds.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "exitCode": { "description": "The command's exit code.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int32" }, - "id": { "type": "string" }, + "id": { + "type": "string" + }, "processId": { "description": "Identifier for the underlying PTY process (when available).", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "source": { "default": "agent", - "allOf": [{ "$ref": "#/definitions/CommandExecutionSource" }] + "allOf": [ + { + "$ref": "#/definitions/CommandExecutionSource" + } + ] + }, + "status": { + "$ref": "#/definitions/CommandExecutionStatus" }, - "status": { "$ref": "#/definitions/CommandExecutionStatus" }, "type": { "type": "string", - "enum": ["commandExecution"], + "enum": [ + "commandExecution" + ], "title": "CommandExecutionThreadItemType" } }, @@ -466,14 +892,30 @@ }, { "type": "object", - "required": ["changes", "id", "status", "type"], + "required": [ + "changes", + "id", + "status", + "type" + ], "properties": { - "changes": { "type": "array", "items": { "$ref": "#/definitions/FileUpdateChange" } }, - "id": { "type": "string" }, - "status": { "$ref": "#/definitions/PatchApplyStatus" }, + "changes": { + "type": "array", + "items": { + "$ref": "#/definitions/FileUpdateChange" + } + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/PatchApplyStatus" + }, "type": { "type": "string", - "enum": ["fileChange"], + "enum": [ + "fileChange" + ], "title": "FileChangeThreadItemType" } }, @@ -481,28 +923,67 @@ }, { "type": "object", - "required": ["arguments", "id", "server", "status", "tool", "type"], + "required": [ + "arguments", + "id", + "server", + "status", + "tool", + "type" + ], "properties": { "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "error": { - "anyOf": [{ "$ref": "#/definitions/McpToolCallError" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "mcpAppResourceUri": { + "type": [ + "string", + "null" + ] }, - "id": { "type": "string" }, - "mcpAppResourceUri": { "type": ["string", "null"] }, "result": { - "anyOf": [{ "$ref": "#/definitions/McpToolCallResult" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallResult" + }, + { + "type": "null" + } + ] + }, + "server": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpToolCallStatus" + }, + "tool": { + "type": "string" }, - "server": { "type": "string" }, - "status": { "$ref": "#/definitions/McpToolCallStatus" }, - "tool": { "type": "string" }, "type": { "type": "string", - "enum": ["mcpToolCall"], + "enum": [ + "mcpToolCall" + ], "title": "McpToolCallThreadItemType" } }, @@ -510,26 +991,58 @@ }, { "type": "object", - "required": ["arguments", "id", "status", "tool", "type"], + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], "properties": { "arguments": true, "contentItems": { - "type": ["array", "null"], - "items": { "$ref": "#/definitions/DynamicToolCallOutputContentItem" } + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + } }, "durationMs": { "description": "The duration of the dynamic tool call in milliseconds.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, - "id": { "type": "string" }, - "namespace": { "type": ["string", "null"] }, - "status": { "$ref": "#/definitions/DynamicToolCallStatus" }, - "success": { "type": ["boolean", "null"] }, - "tool": { "type": "string" }, + "id": { + "type": "string" + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, "type": { "type": "string", - "enum": ["dynamicToolCall"], + "enum": [ + "dynamicToolCall" + ], "title": "DynamicToolCallThreadItemType" } }, @@ -550,7 +1063,9 @@ "agentsStates": { "description": "Last known status of the target agents, when available.", "type": "object", - "additionalProperties": { "$ref": "#/definitions/CollabAgentState" } + "additionalProperties": { + "$ref": "#/definitions/CollabAgentState" + } }, "id": { "description": "Unique identifier for this collab tool call.", @@ -558,20 +1073,35 @@ }, "model": { "description": "Model requested for the spawned agent, when applicable.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "prompt": { "description": "Prompt text sent as part of the collab tool call, when available.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "reasoningEffort": { "description": "Reasoning effort requested for the spawned agent, when applicable.", - "anyOf": [{ "$ref": "#/definitions/ReasoningEffort" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] }, "receiverThreadIds": { "description": "Thread ID of the receiving agent, when applicable. In case of spawn operation, this corresponds to the newly spawned agent.", "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, "senderThreadId": { "description": "Thread ID of the agent issuing the collab request.", @@ -579,15 +1109,25 @@ }, "status": { "description": "Current status of the collab tool call.", - "allOf": [{ "$ref": "#/definitions/CollabAgentToolCallStatus" }] + "allOf": [ + { + "$ref": "#/definitions/CollabAgentToolCallStatus" + } + ] }, "tool": { "description": "Name of the collab tool that was invoked.", - "allOf": [{ "$ref": "#/definitions/CollabAgentTool" }] + "allOf": [ + { + "$ref": "#/definitions/CollabAgentTool" + } + ] }, "type": { "type": "string", - "enum": ["collabAgentToolCall"], + "enum": [ + "collabAgentToolCall" + ], "title": "CollabAgentToolCallThreadItemType" } }, @@ -595,41 +1135,101 @@ }, { "type": "object", - "required": ["id", "query", "type"], + "required": [ + "id", + "query", + "type" + ], "properties": { "action": { - "anyOf": [{ "$ref": "#/definitions/WebSearchAction" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/WebSearchAction" + }, + { + "type": "null" + } + ] }, - "id": { "type": "string" }, - "query": { "type": "string" }, - "type": { "type": "string", "enum": ["webSearch"], "title": "WebSearchThreadItemType" } + "id": { + "type": "string" + }, + "query": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "webSearch" + ], + "title": "WebSearchThreadItemType" + } }, "title": "WebSearchThreadItem" }, { "type": "object", - "required": ["id", "path", "type"], + "required": [ + "id", + "path", + "type" + ], "properties": { - "id": { "type": "string" }, - "path": { "$ref": "#/definitions/AbsolutePathBuf" }, - "type": { "type": "string", "enum": ["imageView"], "title": "ImageViewThreadItemType" } + "id": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "type": "string", + "enum": [ + "imageView" + ], + "title": "ImageViewThreadItemType" + } }, "title": "ImageViewThreadItem" }, { "type": "object", - "required": ["id", "result", "status", "type"], + "required": [ + "id", + "result", + "status", + "type" + ], "properties": { - "id": { "type": "string" }, - "result": { "type": "string" }, - "revisedPrompt": { "type": ["string", "null"] }, - "savedPath": { - "anyOf": [{ "$ref": "#/definitions/AbsolutePathBuf" }, { "type": "null" }] + "id": { + "type": "string" + }, + "result": { + "type": "string" + }, + "revisedPrompt": { + "type": [ + "string", + "null" + ] + }, + "savedPath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string" }, - "status": { "type": "string" }, "type": { "type": "string", - "enum": ["imageGeneration"], + "enum": [ + "imageGeneration" + ], "title": "ImageGenerationThreadItemType" } }, @@ -637,13 +1237,23 @@ }, { "type": "object", - "required": ["id", "review", "type"], + "required": [ + "id", + "review", + "type" + ], "properties": { - "id": { "type": "string" }, - "review": { "type": "string" }, + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, "type": { "type": "string", - "enum": ["enteredReviewMode"], + "enum": [ + "enteredReviewMode" + ], "title": "EnteredReviewModeThreadItemType" } }, @@ -651,13 +1261,23 @@ }, { "type": "object", - "required": ["id", "review", "type"], + "required": [ + "id", + "review", + "type" + ], "properties": { - "id": { "type": "string" }, - "review": { "type": "string" }, + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, "type": { "type": "string", - "enum": ["exitedReviewMode"], + "enum": [ + "exitedReviewMode" + ], "title": "ExitedReviewModeThreadItemType" } }, @@ -665,12 +1285,19 @@ }, { "type": "object", - "required": ["id", "type"], + "required": [ + "id", + "type" + ], "properties": { - "id": { "type": "string" }, + "id": { + "type": "string" + }, "type": { "type": "string", - "enum": ["contextCompaction"], + "enum": [ + "contextCompaction" + ], "title": "ContextCompactionThreadItemType" } }, @@ -680,103 +1307,248 @@ }, "Turn": { "type": "object", - "required": ["id", "items", "status"], + "required": [ + "id", + "items", + "status" + ], "properties": { "completedAt": { "description": "Unix timestamp (in seconds) when the turn completed.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "durationMs": { "description": "Duration between turn start and completion in milliseconds, if known.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "error": { "description": "Only populated when the Turn's status is failed.", - "anyOf": [{ "$ref": "#/definitions/TurnError" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/TurnError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" }, - "id": { "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "type": "array", - "items": { "$ref": "#/definitions/ThreadItem" } + "items": { + "$ref": "#/definitions/ThreadItem" + } + }, + "itemsView": { + "description": "Describes how much of `items` has been loaded for this turn.", + "default": "full", + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ] }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, - "status": { "$ref": "#/definitions/TurnStatus" } + "status": { + "$ref": "#/definitions/TurnStatus" + } } }, "TurnError": { "type": "object", - "required": ["message"], + "required": [ + "message" + ], "properties": { - "additionalDetails": { "default": null, "type": ["string", "null"] }, - "codexErrorInfo": { - "anyOf": [{ "$ref": "#/definitions/CodexErrorInfo" }, { "type": "null" }] + "additionalDetails": { + "default": null, + "type": [ + "string", + "null" + ] }, - "message": { "type": "string" } + "codexErrorInfo": { + "anyOf": [ + { + "$ref": "#/definitions/CodexErrorInfo" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": "string" + } } }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "type": "string", + "enum": [ + "notLoaded" + ] + }, + { + "description": "`items` contains only a display summary for this turn.", + "type": "string", + "enum": [ + "summary" + ] + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "type": "string", + "enum": [ + "full" + ] + } + ] + }, "TurnStatus": { "type": "string", - "enum": ["completed", "interrupted", "failed", "inProgress"] + "enum": [ + "completed", + "interrupted", + "failed", + "inProgress" + ] }, "UserInput": { "oneOf": [ { "type": "object", - "required": ["text", "type"], + "required": [ + "text", + "type" + ], "properties": { - "text": { "type": "string" }, + "text": { + "type": "string" + }, "text_elements": { "description": "UI-defined spans within `text` used to render or persist special elements.", "default": [], "type": "array", - "items": { "$ref": "#/definitions/TextElement" } + "items": { + "$ref": "#/definitions/TextElement" + } }, - "type": { "type": "string", "enum": ["text"], "title": "TextUserInputType" } + "type": { + "type": "string", + "enum": [ + "text" + ], + "title": "TextUserInputType" + } }, "title": "TextUserInput" }, { "type": "object", - "required": ["type", "url"], + "required": [ + "type", + "url" + ], "properties": { - "type": { "type": "string", "enum": ["image"], "title": "ImageUserInputType" }, - "url": { "type": "string" } + "type": { + "type": "string", + "enum": [ + "image" + ], + "title": "ImageUserInputType" + }, + "url": { + "type": "string" + } }, "title": "ImageUserInput" }, { "type": "object", - "required": ["path", "type"], + "required": [ + "path", + "type" + ], "properties": { - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["localImage"], "title": "LocalImageUserInputType" } + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "localImage" + ], + "title": "LocalImageUserInputType" + } }, "title": "LocalImageUserInput" }, { "type": "object", - "required": ["name", "path", "type"], + "required": [ + "name", + "path", + "type" + ], "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["skill"], "title": "SkillUserInputType" } + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "skill" + ], + "title": "SkillUserInputType" + } }, "title": "SkillUserInput" }, { "type": "object", - "required": ["name", "path", "type"], + "required": [ + "name", + "path", + "type" + ], "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["mention"], "title": "MentionUserInputType" } + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mention" + ], + "title": "MentionUserInputType" + } }, "title": "MentionUserInput" } @@ -786,46 +1558,98 @@ "oneOf": [ { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "queries": { "type": ["array", "null"], "items": { "type": "string" } }, - "query": { "type": ["string", "null"] }, - "type": { "type": "string", "enum": ["search"], "title": "SearchWebSearchActionType" } + "queries": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "search" + ], + "title": "SearchWebSearchActionType" + } }, "title": "SearchWebSearchAction" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["openPage"], + "enum": [ + "openPage" + ], "title": "OpenPageWebSearchActionType" }, - "url": { "type": ["string", "null"] } + "url": { + "type": [ + "string", + "null" + ] + } }, "title": "OpenPageWebSearchAction" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "pattern": { "type": ["string", "null"] }, + "pattern": { + "type": [ + "string", + "null" + ] + }, "type": { "type": "string", - "enum": ["findInPage"], + "enum": [ + "findInPage" + ], "title": "FindInPageWebSearchActionType" }, - "url": { "type": ["string", "null"] } + "url": { + "type": [ + "string", + "null" + ] + } }, "title": "FindInPageWebSearchAction" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["other"], "title": "OtherWebSearchActionType" } + "type": { + "type": "string", + "enum": [ + "other" + ], + "title": "OtherWebSearchActionType" + } }, "title": "OtherWebSearchAction" } diff --git a/extensions/codex/src/app-server/protocol-generated/json/v2/TurnStartResponse.json b/extensions/codex/src/app-server/protocol-generated/json/v2/TurnStartResponse.json index 8a88784e23b..1457a7c2456 100644 --- a/extensions/codex/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +++ b/extensions/codex/src/app-server/protocol-generated/json/v2/TurnStartResponse.json @@ -2,8 +2,14 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "TurnStartResponse", "type": "object", - "required": ["turn"], - "properties": { "turn": { "$ref": "#/definitions/Turn" } }, + "required": [ + "turn" + ], + "properties": { + "turn": { + "$ref": "#/definitions/Turn" + } + }, "definitions": { "AbsolutePathBuf": { "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", @@ -11,10 +17,21 @@ }, "ByteRange": { "type": "object", - "required": ["end", "start"], + "required": [ + "end", + "start" + ], "properties": { - "end": { "type": "integer", "format": "uint", "minimum": 0 }, - "start": { "type": "integer", "format": "uint", "minimum": 0 } + "end": { + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0 + } } }, "CodexErrorInfo": { @@ -37,12 +54,21 @@ }, { "type": "object", - "required": ["httpConnectionFailed"], + "required": [ + "httpConnectionFailed" + ], "properties": { "httpConnectionFailed": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -52,12 +78,21 @@ { "description": "Failed to connect to the response SSE stream.", "type": "object", - "required": ["responseStreamConnectionFailed"], + "required": [ + "responseStreamConnectionFailed" + ], "properties": { "responseStreamConnectionFailed": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -67,12 +102,21 @@ { "description": "The response SSE stream disconnected in the middle of a turn before completion.", "type": "object", - "required": ["responseStreamDisconnected"], + "required": [ + "responseStreamDisconnected" + ], "properties": { "responseStreamDisconnected": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -82,12 +126,21 @@ { "description": "Reached the retry limit for responses.", "type": "object", - "required": ["responseTooManyFailedAttempts"], + "required": [ + "responseTooManyFailedAttempts" + ], "properties": { "responseTooManyFailedAttempts": { "type": "object", "properties": { - "httpStatusCode": { "type": ["integer", "null"], "format": "uint16", "minimum": 0 } + "httpStatusCode": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0 + } } } }, @@ -97,12 +150,20 @@ { "description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", "type": "object", - "required": ["activeTurnNotSteerable"], + "required": [ + "activeTurnNotSteerable" + ], "properties": { "activeTurnNotSteerable": { "type": "object", - "required": ["turnKind"], - "properties": { "turnKind": { "$ref": "#/definitions/NonSteerableTurnKind" } } + "required": [ + "turnKind" + ], + "properties": { + "turnKind": { + "$ref": "#/definitions/NonSteerableTurnKind" + } + } } }, "additionalProperties": false, @@ -112,10 +173,19 @@ }, "CollabAgentState": { "type": "object", - "required": ["status"], + "required": [ + "status" + ], "properties": { - "message": { "type": ["string", "null"] }, - "status": { "$ref": "#/definitions/CollabAgentStatus" } + "message": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/definitions/CollabAgentStatus" + } } }, "CollabAgentStatus": { @@ -132,34 +202,73 @@ }, "CollabAgentTool": { "type": "string", - "enum": ["spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent"] + "enum": [ + "spawnAgent", + "sendInput", + "resumeAgent", + "wait", + "closeAgent" + ] }, "CollabAgentToolCallStatus": { "type": "string", - "enum": ["inProgress", "completed", "failed"] + "enum": [ + "inProgress", + "completed", + "failed" + ] }, "CommandAction": { "oneOf": [ { "type": "object", - "required": ["command", "name", "path", "type"], + "required": [ + "command", + "name", + "path", + "type" + ], "properties": { - "command": { "type": "string" }, - "name": { "type": "string" }, - "path": { "$ref": "#/definitions/AbsolutePathBuf" }, - "type": { "type": "string", "enum": ["read"], "title": "ReadCommandActionType" } + "command": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "type": "string", + "enum": [ + "read" + ], + "title": "ReadCommandActionType" + } }, "title": "ReadCommandAction" }, { "type": "object", - "required": ["command", "type"], + "required": [ + "command", + "type" + ], "properties": { - "command": { "type": "string" }, - "path": { "type": ["string", "null"] }, + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, "type": { "type": "string", - "enum": ["listFiles"], + "enum": [ + "listFiles" + ], "title": "ListFilesCommandActionType" } }, @@ -167,21 +276,53 @@ }, { "type": "object", - "required": ["command", "type"], + "required": [ + "command", + "type" + ], "properties": { - "command": { "type": "string" }, - "path": { "type": ["string", "null"] }, - "query": { "type": ["string", "null"] }, - "type": { "type": "string", "enum": ["search"], "title": "SearchCommandActionType" } + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "search" + ], + "title": "SearchCommandActionType" + } }, "title": "SearchCommandAction" }, { "type": "object", - "required": ["command", "type"], + "required": [ + "command", + "type" + ], "properties": { - "command": { "type": "string" }, - "type": { "type": "string", "enum": ["unknown"], "title": "UnknownCommandActionType" } + "command": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "unknown" + ], + "title": "UnknownCommandActionType" + } }, "title": "UnknownCommandAction" } @@ -189,22 +330,39 @@ }, "CommandExecutionSource": { "type": "string", - "enum": ["agent", "userShell", "unifiedExecStartup", "unifiedExecInteraction"] + "enum": [ + "agent", + "userShell", + "unifiedExecStartup", + "unifiedExecInteraction" + ] }, "CommandExecutionStatus": { "type": "string", - "enum": ["inProgress", "completed", "failed", "declined"] + "enum": [ + "inProgress", + "completed", + "failed", + "declined" + ] }, "DynamicToolCallOutputContentItem": { "oneOf": [ { "type": "object", - "required": ["text", "type"], + "required": [ + "text", + "type" + ], "properties": { - "text": { "type": "string" }, + "text": { + "type": "string" + }, "type": { "type": "string", - "enum": ["inputText"], + "enum": [ + "inputText" + ], "title": "InputTextDynamicToolCallOutputContentItemType" } }, @@ -212,12 +370,19 @@ }, { "type": "object", - "required": ["imageUrl", "type"], + "required": [ + "imageUrl", + "type" + ], "properties": { - "imageUrl": { "type": "string" }, + "imageUrl": { + "type": "string" + }, "type": { "type": "string", - "enum": ["inputImage"], + "enum": [ + "inputImage" + ], "title": "InputImageDynamicToolCallOutputContentItemType" } }, @@ -225,52 +390,127 @@ } ] }, - "DynamicToolCallStatus": { "type": "string", "enum": ["inProgress", "completed", "failed"] }, + "DynamicToolCallStatus": { + "type": "string", + "enum": [ + "inProgress", + "completed", + "failed" + ] + }, "FileUpdateChange": { "type": "object", - "required": ["diff", "kind", "path"], + "required": [ + "diff", + "kind", + "path" + ], "properties": { - "diff": { "type": "string" }, - "kind": { "$ref": "#/definitions/PatchChangeKind" }, - "path": { "type": "string" } + "diff": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/PatchChangeKind" + }, + "path": { + "type": "string" + } } }, "HookPromptFragment": { "type": "object", - "required": ["hookRunId", "text"], - "properties": { "hookRunId": { "type": "string" }, "text": { "type": "string" } } + "required": [ + "hookRunId", + "text" + ], + "properties": { + "hookRunId": { + "type": "string" + }, + "text": { + "type": "string" + } + } }, "McpToolCallError": { "type": "object", - "required": ["message"], - "properties": { "message": { "type": "string" } } + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } }, "McpToolCallResult": { "type": "object", - "required": ["content"], + "required": [ + "content" + ], "properties": { "_meta": true, - "content": { "type": "array", "items": true }, + "content": { + "type": "array", + "items": true + }, "structuredContent": true } }, - "McpToolCallStatus": { "type": "string", "enum": ["inProgress", "completed", "failed"] }, + "McpToolCallStatus": { + "type": "string", + "enum": [ + "inProgress", + "completed", + "failed" + ] + }, "MemoryCitation": { "type": "object", - "required": ["entries", "threadIds"], + "required": [ + "entries", + "threadIds" + ], "properties": { - "entries": { "type": "array", "items": { "$ref": "#/definitions/MemoryCitationEntry" } }, - "threadIds": { "type": "array", "items": { "type": "string" } } + "entries": { + "type": "array", + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + } + }, + "threadIds": { + "type": "array", + "items": { + "type": "string" + } + } } }, "MemoryCitationEntry": { "type": "object", - "required": ["lineEnd", "lineStart", "note", "path"], + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], "properties": { - "lineEnd": { "type": "integer", "format": "uint32", "minimum": 0 }, - "lineStart": { "type": "integer", "format": "uint32", "minimum": 0 }, - "note": { "type": "string" }, - "path": { "type": "string" } + "lineEnd": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "lineStart": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } } }, "MessagePhase": { @@ -279,44 +519,88 @@ { "description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.", "type": "string", - "enum": ["commentary"] + "enum": [ + "commentary" + ] }, { "description": "The assistant's terminal answer text for the current turn.", "type": "string", - "enum": ["final_answer"] + "enum": [ + "final_answer" + ] } ] }, - "NonSteerableTurnKind": { "type": "string", "enum": ["review", "compact"] }, + "NonSteerableTurnKind": { + "type": "string", + "enum": [ + "review", + "compact" + ] + }, "PatchApplyStatus": { "type": "string", - "enum": ["inProgress", "completed", "failed", "declined"] + "enum": [ + "inProgress", + "completed", + "failed", + "declined" + ] }, "PatchChangeKind": { "oneOf": [ { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["add"], "title": "AddPatchChangeKindType" } + "type": { + "type": "string", + "enum": [ + "add" + ], + "title": "AddPatchChangeKindType" + } }, "title": "AddPatchChangeKind" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["delete"], "title": "DeletePatchChangeKindType" } + "type": { + "type": "string", + "enum": [ + "delete" + ], + "title": "DeletePatchChangeKindType" + } }, "title": "DeletePatchChangeKind" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "move_path": { "type": ["string", "null"] }, - "type": { "type": "string", "enum": ["update"], "title": "UpdatePatchChangeKindType" } + "move_path": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "update" + ], + "title": "UpdatePatchChangeKindType" + } }, "title": "UpdatePatchChangeKind" } @@ -325,19 +609,35 @@ "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "type": "string", - "enum": ["none", "minimal", "low", "medium", "high", "xhigh"] + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] }, "TextElement": { "type": "object", - "required": ["byteRange"], + "required": [ + "byteRange" + ], "properties": { "byteRange": { "description": "Byte range in the parent `text` buffer that this element occupies.", - "allOf": [{ "$ref": "#/definitions/ByteRange" }] + "allOf": [ + { + "$ref": "#/definitions/ByteRange" + } + ] }, "placeholder": { "description": "Optional human-readable placeholder for the element, displayed in the UI.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] } } }, @@ -345,13 +645,26 @@ "oneOf": [ { "type": "object", - "required": ["content", "id", "type"], + "required": [ + "content", + "id", + "type" + ], "properties": { - "content": { "type": "array", "items": { "$ref": "#/definitions/UserInput" } }, - "id": { "type": "string" }, + "content": { + "type": "array", + "items": { + "$ref": "#/definitions/UserInput" + } + }, + "id": { + "type": "string" + }, "type": { "type": "string", - "enum": ["userMessage"], + "enum": [ + "userMessage" + ], "title": "UserMessageThreadItemType" } }, @@ -359,16 +672,26 @@ }, { "type": "object", - "required": ["fragments", "id", "type"], + "required": [ + "fragments", + "id", + "type" + ], "properties": { "fragments": { "type": "array", - "items": { "$ref": "#/definitions/HookPromptFragment" } + "items": { + "$ref": "#/definitions/HookPromptFragment" + } + }, + "id": { + "type": "string" }, - "id": { "type": "string" }, "type": { "type": "string", - "enum": ["hookPrompt"], + "enum": [ + "hookPrompt" + ], "title": "HookPromptThreadItemType" } }, @@ -376,21 +699,45 @@ }, { "type": "object", - "required": ["id", "text", "type"], + "required": [ + "id", + "text", + "type" + ], "properties": { - "id": { "type": "string" }, + "id": { + "type": "string" + }, "memoryCitation": { "default": null, - "anyOf": [{ "$ref": "#/definitions/MemoryCitation" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ] }, "phase": { "default": null, - "anyOf": [{ "$ref": "#/definitions/MessagePhase" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, + "text": { + "type": "string" }, - "text": { "type": "string" }, "type": { "type": "string", - "enum": ["agentMessage"], + "enum": [ + "agentMessage" + ], "title": "AgentMessageThreadItemType" } }, @@ -399,66 +746,141 @@ { "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", "type": "object", - "required": ["id", "text", "type"], + "required": [ + "id", + "text", + "type" + ], "properties": { - "id": { "type": "string" }, - "text": { "type": "string" }, - "type": { "type": "string", "enum": ["plan"], "title": "PlanThreadItemType" } + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "plan" + ], + "title": "PlanThreadItemType" + } }, "title": "PlanThreadItem" }, { "type": "object", - "required": ["id", "type"], + "required": [ + "id", + "type" + ], "properties": { - "content": { "default": [], "type": "array", "items": { "type": "string" } }, - "id": { "type": "string" }, - "summary": { "default": [], "type": "array", "items": { "type": "string" } }, - "type": { "type": "string", "enum": ["reasoning"], "title": "ReasoningThreadItemType" } + "content": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "summary": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string", + "enum": [ + "reasoning" + ], + "title": "ReasoningThreadItemType" + } }, "title": "ReasoningThreadItem" }, { "type": "object", - "required": ["command", "commandActions", "cwd", "id", "status", "type"], + "required": [ + "command", + "commandActions", + "cwd", + "id", + "status", + "type" + ], "properties": { "aggregatedOutput": { "description": "The command's output, aggregated from stdout and stderr.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] + }, + "command": { + "description": "The command to be executed.", + "type": "string" }, - "command": { "description": "The command to be executed.", "type": "string" }, "commandActions": { "description": "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", "type": "array", - "items": { "$ref": "#/definitions/CommandAction" } + "items": { + "$ref": "#/definitions/CommandAction" + } }, "cwd": { "description": "The command's working directory.", - "allOf": [{ "$ref": "#/definitions/AbsolutePathBuf" }] + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] }, "durationMs": { "description": "The duration of the command execution in milliseconds.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "exitCode": { "description": "The command's exit code.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int32" }, - "id": { "type": "string" }, + "id": { + "type": "string" + }, "processId": { "description": "Identifier for the underlying PTY process (when available).", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "source": { "default": "agent", - "allOf": [{ "$ref": "#/definitions/CommandExecutionSource" }] + "allOf": [ + { + "$ref": "#/definitions/CommandExecutionSource" + } + ] + }, + "status": { + "$ref": "#/definitions/CommandExecutionStatus" }, - "status": { "$ref": "#/definitions/CommandExecutionStatus" }, "type": { "type": "string", - "enum": ["commandExecution"], + "enum": [ + "commandExecution" + ], "title": "CommandExecutionThreadItemType" } }, @@ -466,14 +888,30 @@ }, { "type": "object", - "required": ["changes", "id", "status", "type"], + "required": [ + "changes", + "id", + "status", + "type" + ], "properties": { - "changes": { "type": "array", "items": { "$ref": "#/definitions/FileUpdateChange" } }, - "id": { "type": "string" }, - "status": { "$ref": "#/definitions/PatchApplyStatus" }, + "changes": { + "type": "array", + "items": { + "$ref": "#/definitions/FileUpdateChange" + } + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/PatchApplyStatus" + }, "type": { "type": "string", - "enum": ["fileChange"], + "enum": [ + "fileChange" + ], "title": "FileChangeThreadItemType" } }, @@ -481,28 +919,67 @@ }, { "type": "object", - "required": ["arguments", "id", "server", "status", "tool", "type"], + "required": [ + "arguments", + "id", + "server", + "status", + "tool", + "type" + ], "properties": { "arguments": true, "durationMs": { "description": "The duration of the MCP tool call in milliseconds.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "error": { - "anyOf": [{ "$ref": "#/definitions/McpToolCallError" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "mcpAppResourceUri": { + "type": [ + "string", + "null" + ] }, - "id": { "type": "string" }, - "mcpAppResourceUri": { "type": ["string", "null"] }, "result": { - "anyOf": [{ "$ref": "#/definitions/McpToolCallResult" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallResult" + }, + { + "type": "null" + } + ] + }, + "server": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpToolCallStatus" + }, + "tool": { + "type": "string" }, - "server": { "type": "string" }, - "status": { "$ref": "#/definitions/McpToolCallStatus" }, - "tool": { "type": "string" }, "type": { "type": "string", - "enum": ["mcpToolCall"], + "enum": [ + "mcpToolCall" + ], "title": "McpToolCallThreadItemType" } }, @@ -510,26 +987,58 @@ }, { "type": "object", - "required": ["arguments", "id", "status", "tool", "type"], + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], "properties": { "arguments": true, "contentItems": { - "type": ["array", "null"], - "items": { "$ref": "#/definitions/DynamicToolCallOutputContentItem" } + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + } }, "durationMs": { "description": "The duration of the dynamic tool call in milliseconds.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, - "id": { "type": "string" }, - "namespace": { "type": ["string", "null"] }, - "status": { "$ref": "#/definitions/DynamicToolCallStatus" }, - "success": { "type": ["boolean", "null"] }, - "tool": { "type": "string" }, + "id": { + "type": "string" + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, "type": { "type": "string", - "enum": ["dynamicToolCall"], + "enum": [ + "dynamicToolCall" + ], "title": "DynamicToolCallThreadItemType" } }, @@ -550,7 +1059,9 @@ "agentsStates": { "description": "Last known status of the target agents, when available.", "type": "object", - "additionalProperties": { "$ref": "#/definitions/CollabAgentState" } + "additionalProperties": { + "$ref": "#/definitions/CollabAgentState" + } }, "id": { "description": "Unique identifier for this collab tool call.", @@ -558,20 +1069,35 @@ }, "model": { "description": "Model requested for the spawned agent, when applicable.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "prompt": { "description": "Prompt text sent as part of the collab tool call, when available.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "reasoningEffort": { "description": "Reasoning effort requested for the spawned agent, when applicable.", - "anyOf": [{ "$ref": "#/definitions/ReasoningEffort" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] }, "receiverThreadIds": { "description": "Thread ID of the receiving agent, when applicable. In case of spawn operation, this corresponds to the newly spawned agent.", "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, "senderThreadId": { "description": "Thread ID of the agent issuing the collab request.", @@ -579,15 +1105,25 @@ }, "status": { "description": "Current status of the collab tool call.", - "allOf": [{ "$ref": "#/definitions/CollabAgentToolCallStatus" }] + "allOf": [ + { + "$ref": "#/definitions/CollabAgentToolCallStatus" + } + ] }, "tool": { "description": "Name of the collab tool that was invoked.", - "allOf": [{ "$ref": "#/definitions/CollabAgentTool" }] + "allOf": [ + { + "$ref": "#/definitions/CollabAgentTool" + } + ] }, "type": { "type": "string", - "enum": ["collabAgentToolCall"], + "enum": [ + "collabAgentToolCall" + ], "title": "CollabAgentToolCallThreadItemType" } }, @@ -595,41 +1131,101 @@ }, { "type": "object", - "required": ["id", "query", "type"], + "required": [ + "id", + "query", + "type" + ], "properties": { "action": { - "anyOf": [{ "$ref": "#/definitions/WebSearchAction" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/WebSearchAction" + }, + { + "type": "null" + } + ] }, - "id": { "type": "string" }, - "query": { "type": "string" }, - "type": { "type": "string", "enum": ["webSearch"], "title": "WebSearchThreadItemType" } + "id": { + "type": "string" + }, + "query": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "webSearch" + ], + "title": "WebSearchThreadItemType" + } }, "title": "WebSearchThreadItem" }, { "type": "object", - "required": ["id", "path", "type"], + "required": [ + "id", + "path", + "type" + ], "properties": { - "id": { "type": "string" }, - "path": { "$ref": "#/definitions/AbsolutePathBuf" }, - "type": { "type": "string", "enum": ["imageView"], "title": "ImageViewThreadItemType" } + "id": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "type": "string", + "enum": [ + "imageView" + ], + "title": "ImageViewThreadItemType" + } }, "title": "ImageViewThreadItem" }, { "type": "object", - "required": ["id", "result", "status", "type"], + "required": [ + "id", + "result", + "status", + "type" + ], "properties": { - "id": { "type": "string" }, - "result": { "type": "string" }, - "revisedPrompt": { "type": ["string", "null"] }, - "savedPath": { - "anyOf": [{ "$ref": "#/definitions/AbsolutePathBuf" }, { "type": "null" }] + "id": { + "type": "string" + }, + "result": { + "type": "string" + }, + "revisedPrompt": { + "type": [ + "string", + "null" + ] + }, + "savedPath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string" }, - "status": { "type": "string" }, "type": { "type": "string", - "enum": ["imageGeneration"], + "enum": [ + "imageGeneration" + ], "title": "ImageGenerationThreadItemType" } }, @@ -637,13 +1233,23 @@ }, { "type": "object", - "required": ["id", "review", "type"], + "required": [ + "id", + "review", + "type" + ], "properties": { - "id": { "type": "string" }, - "review": { "type": "string" }, + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, "type": { "type": "string", - "enum": ["enteredReviewMode"], + "enum": [ + "enteredReviewMode" + ], "title": "EnteredReviewModeThreadItemType" } }, @@ -651,13 +1257,23 @@ }, { "type": "object", - "required": ["id", "review", "type"], + "required": [ + "id", + "review", + "type" + ], "properties": { - "id": { "type": "string" }, - "review": { "type": "string" }, + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, "type": { "type": "string", - "enum": ["exitedReviewMode"], + "enum": [ + "exitedReviewMode" + ], "title": "ExitedReviewModeThreadItemType" } }, @@ -665,12 +1281,19 @@ }, { "type": "object", - "required": ["id", "type"], + "required": [ + "id", + "type" + ], "properties": { - "id": { "type": "string" }, + "id": { + "type": "string" + }, "type": { "type": "string", - "enum": ["contextCompaction"], + "enum": [ + "contextCompaction" + ], "title": "ContextCompactionThreadItemType" } }, @@ -680,103 +1303,248 @@ }, "Turn": { "type": "object", - "required": ["id", "items", "status"], + "required": [ + "id", + "items", + "status" + ], "properties": { "completedAt": { "description": "Unix timestamp (in seconds) when the turn completed.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "durationMs": { "description": "Duration between turn start and completion in milliseconds, if known.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, "error": { "description": "Only populated when the Turn's status is failed.", - "anyOf": [{ "$ref": "#/definitions/TurnError" }, { "type": "null" }] + "anyOf": [ + { + "$ref": "#/definitions/TurnError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" }, - "id": { "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "type": "array", - "items": { "$ref": "#/definitions/ThreadItem" } + "items": { + "$ref": "#/definitions/ThreadItem" + } + }, + "itemsView": { + "description": "Describes how much of `items` has been loaded for this turn.", + "default": "full", + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ] }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "int64" }, - "status": { "$ref": "#/definitions/TurnStatus" } + "status": { + "$ref": "#/definitions/TurnStatus" + } } }, "TurnError": { "type": "object", - "required": ["message"], + "required": [ + "message" + ], "properties": { - "additionalDetails": { "default": null, "type": ["string", "null"] }, - "codexErrorInfo": { - "anyOf": [{ "$ref": "#/definitions/CodexErrorInfo" }, { "type": "null" }] + "additionalDetails": { + "default": null, + "type": [ + "string", + "null" + ] }, - "message": { "type": "string" } + "codexErrorInfo": { + "anyOf": [ + { + "$ref": "#/definitions/CodexErrorInfo" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": "string" + } } }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "type": "string", + "enum": [ + "notLoaded" + ] + }, + { + "description": "`items` contains only a display summary for this turn.", + "type": "string", + "enum": [ + "summary" + ] + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "type": "string", + "enum": [ + "full" + ] + } + ] + }, "TurnStatus": { "type": "string", - "enum": ["completed", "interrupted", "failed", "inProgress"] + "enum": [ + "completed", + "interrupted", + "failed", + "inProgress" + ] }, "UserInput": { "oneOf": [ { "type": "object", - "required": ["text", "type"], + "required": [ + "text", + "type" + ], "properties": { - "text": { "type": "string" }, + "text": { + "type": "string" + }, "text_elements": { "description": "UI-defined spans within `text` used to render or persist special elements.", "default": [], "type": "array", - "items": { "$ref": "#/definitions/TextElement" } + "items": { + "$ref": "#/definitions/TextElement" + } }, - "type": { "type": "string", "enum": ["text"], "title": "TextUserInputType" } + "type": { + "type": "string", + "enum": [ + "text" + ], + "title": "TextUserInputType" + } }, "title": "TextUserInput" }, { "type": "object", - "required": ["type", "url"], + "required": [ + "type", + "url" + ], "properties": { - "type": { "type": "string", "enum": ["image"], "title": "ImageUserInputType" }, - "url": { "type": "string" } + "type": { + "type": "string", + "enum": [ + "image" + ], + "title": "ImageUserInputType" + }, + "url": { + "type": "string" + } }, "title": "ImageUserInput" }, { "type": "object", - "required": ["path", "type"], + "required": [ + "path", + "type" + ], "properties": { - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["localImage"], "title": "LocalImageUserInputType" } + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "localImage" + ], + "title": "LocalImageUserInputType" + } }, "title": "LocalImageUserInput" }, { "type": "object", - "required": ["name", "path", "type"], + "required": [ + "name", + "path", + "type" + ], "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["skill"], "title": "SkillUserInputType" } + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "skill" + ], + "title": "SkillUserInputType" + } }, "title": "SkillUserInput" }, { "type": "object", - "required": ["name", "path", "type"], + "required": [ + "name", + "path", + "type" + ], "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["mention"], "title": "MentionUserInputType" } + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mention" + ], + "title": "MentionUserInputType" + } }, "title": "MentionUserInput" } @@ -786,46 +1554,98 @@ "oneOf": [ { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "queries": { "type": ["array", "null"], "items": { "type": "string" } }, - "query": { "type": ["string", "null"] }, - "type": { "type": "string", "enum": ["search"], "title": "SearchWebSearchActionType" } + "queries": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "search" + ], + "title": "SearchWebSearchActionType" + } }, "title": "SearchWebSearchAction" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["openPage"], + "enum": [ + "openPage" + ], "title": "OpenPageWebSearchActionType" }, - "url": { "type": ["string", "null"] } + "url": { + "type": [ + "string", + "null" + ] + } }, "title": "OpenPageWebSearchAction" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "pattern": { "type": ["string", "null"] }, + "pattern": { + "type": [ + "string", + "null" + ] + }, "type": { "type": "string", - "enum": ["findInPage"], + "enum": [ + "findInPage" + ], "title": "FindInPageWebSearchActionType" }, - "url": { "type": ["string", "null"] } + "url": { + "type": [ + "string", + "null" + ] + } }, "title": "FindInPageWebSearchAction" }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { - "type": { "type": "string", "enum": ["other"], "title": "OtherWebSearchActionType" } + "type": { + "type": "string", + "enum": [ + "other" + ], + "title": "OtherWebSearchActionType" + } }, "title": "OtherWebSearchAction" } diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/AbsolutePathBuf.ts b/extensions/codex/src/app-server/protocol-generated/typescript/AbsolutePathBuf.ts deleted file mode 100644 index dc1cde12410..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/AbsolutePathBuf.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * A path that is guaranteed to be absolute and normalized (though it is not - * guaranteed to be canonicalized or exist on the filesystem). - * - * IMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set - * using [AbsolutePathBufGuard::new]. If no base path is set, the - * deserialization will fail unless the path being deserialized is already - * absolute. - */ -export type AbsolutePathBuf = string; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/AgentPath.ts b/extensions/codex/src/app-server/protocol-generated/typescript/AgentPath.ts deleted file mode 100644 index 6e55ce69e20..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/AgentPath.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AgentPath = string; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ApplyPatchApprovalParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ApplyPatchApprovalParams.ts deleted file mode 100644 index 0dd4959f95c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ApplyPatchApprovalParams.ts +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FileChange } from "./FileChange.js"; -import type { ThreadId } from "./ThreadId.js"; - -export type ApplyPatchApprovalParams = { - conversationId: ThreadId; - /** - * Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent] - * and [codex_protocol::protocol::PatchApplyEndEvent]. - */ - callId: string; - fileChanges: { [key in string]?: FileChange }; - /** - * Optional explanatory reason (e.g. request for extra write access). - */ - reason: string | null; - /** - * When set, the agent is asking the user to allow writes under this root - * for the remainder of the session (unclear if this is honored today). - */ - grantRoot: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ApplyPatchApprovalResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ApplyPatchApprovalResponse.ts deleted file mode 100644 index 4977876d966..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ApplyPatchApprovalResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReviewDecision } from "./ReviewDecision.js"; - -export type ApplyPatchApprovalResponse = { decision: ReviewDecision }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/AuthMode.ts b/extensions/codex/src/app-server/protocol-generated/typescript/AuthMode.ts deleted file mode 100644 index 210e54c4a5f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/AuthMode.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Authentication mode for OpenAI-backed providers. - */ -export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "agentIdentity"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ClientInfo.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ClientInfo.ts deleted file mode 100644 index b3871d69067..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ClientInfo.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ClientInfo = { name: string; title: string | null; version: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ClientNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ClientNotification.ts deleted file mode 100644 index 814c9de78da..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ClientNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ClientNotification = { method: "initialized" }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ClientRequest.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ClientRequest.ts deleted file mode 100644 index 1f2fc108e3c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ClientRequest.ts +++ /dev/null @@ -1,244 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FuzzyFileSearchParams } from "./FuzzyFileSearchParams.js"; -import type { FuzzyFileSearchSessionStartParams } from "./FuzzyFileSearchSessionStartParams.js"; -import type { FuzzyFileSearchSessionStopParams } from "./FuzzyFileSearchSessionStopParams.js"; -import type { FuzzyFileSearchSessionUpdateParams } from "./FuzzyFileSearchSessionUpdateParams.js"; -import type { GetAuthStatusParams } from "./GetAuthStatusParams.js"; -import type { GetConversationSummaryParams } from "./GetConversationSummaryParams.js"; -import type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams.js"; -import type { InitializeParams } from "./InitializeParams.js"; -import type { RequestId } from "./RequestId.js"; -import type { AppsListParams } from "./v2/AppsListParams.js"; -import type { CancelLoginAccountParams } from "./v2/CancelLoginAccountParams.js"; -import type { CollaborationModeListParams } from "./v2/CollaborationModeListParams.js"; -import type { CommandExecParams } from "./v2/CommandExecParams.js"; -import type { CommandExecResizeParams } from "./v2/CommandExecResizeParams.js"; -import type { CommandExecTerminateParams } from "./v2/CommandExecTerminateParams.js"; -import type { CommandExecWriteParams } from "./v2/CommandExecWriteParams.js"; -import type { ConfigBatchWriteParams } from "./v2/ConfigBatchWriteParams.js"; -import type { ConfigReadParams } from "./v2/ConfigReadParams.js"; -import type { ConfigValueWriteParams } from "./v2/ConfigValueWriteParams.js"; -import type { DeviceKeyCreateParams } from "./v2/DeviceKeyCreateParams.js"; -import type { DeviceKeyPublicParams } from "./v2/DeviceKeyPublicParams.js"; -import type { DeviceKeySignParams } from "./v2/DeviceKeySignParams.js"; -import type { ExperimentalFeatureEnablementSetParams } from "./v2/ExperimentalFeatureEnablementSetParams.js"; -import type { ExperimentalFeatureListParams } from "./v2/ExperimentalFeatureListParams.js"; -import type { ExternalAgentConfigDetectParams } from "./v2/ExternalAgentConfigDetectParams.js"; -import type { ExternalAgentConfigImportParams } from "./v2/ExternalAgentConfigImportParams.js"; -import type { FeedbackUploadParams } from "./v2/FeedbackUploadParams.js"; -import type { FsCopyParams } from "./v2/FsCopyParams.js"; -import type { FsCreateDirectoryParams } from "./v2/FsCreateDirectoryParams.js"; -import type { FsGetMetadataParams } from "./v2/FsGetMetadataParams.js"; -import type { FsReadDirectoryParams } from "./v2/FsReadDirectoryParams.js"; -import type { FsReadFileParams } from "./v2/FsReadFileParams.js"; -import type { FsRemoveParams } from "./v2/FsRemoveParams.js"; -import type { FsUnwatchParams } from "./v2/FsUnwatchParams.js"; -import type { FsWatchParams } from "./v2/FsWatchParams.js"; -import type { FsWriteFileParams } from "./v2/FsWriteFileParams.js"; -import type { GetAccountParams } from "./v2/GetAccountParams.js"; -import type { HooksListParams } from "./v2/HooksListParams.js"; -import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams.js"; -import type { LoginAccountParams } from "./v2/LoginAccountParams.js"; -import type { MarketplaceAddParams } from "./v2/MarketplaceAddParams.js"; -import type { MarketplaceRemoveParams } from "./v2/MarketplaceRemoveParams.js"; -import type { MarketplaceUpgradeParams } from "./v2/MarketplaceUpgradeParams.js"; -import type { McpResourceReadParams } from "./v2/McpResourceReadParams.js"; -import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams.js"; -import type { McpServerToolCallParams } from "./v2/McpServerToolCallParams.js"; -import type { MockExperimentalMethodParams } from "./v2/MockExperimentalMethodParams.js"; -import type { ModelListParams } from "./v2/ModelListParams.js"; -import type { ModelProviderCapabilitiesReadParams } from "./v2/ModelProviderCapabilitiesReadParams.js"; -import type { PluginInstallParams } from "./v2/PluginInstallParams.js"; -import type { PluginListParams } from "./v2/PluginListParams.js"; -import type { PluginReadParams } from "./v2/PluginReadParams.js"; -import type { PluginShareDeleteParams } from "./v2/PluginShareDeleteParams.js"; -import type { PluginShareListParams } from "./v2/PluginShareListParams.js"; -import type { PluginShareSaveParams } from "./v2/PluginShareSaveParams.js"; -import type { PluginSkillReadParams } from "./v2/PluginSkillReadParams.js"; -import type { PluginUninstallParams } from "./v2/PluginUninstallParams.js"; -import type { ReviewStartParams } from "./v2/ReviewStartParams.js"; -import type { SendAddCreditsNudgeEmailParams } from "./v2/SendAddCreditsNudgeEmailParams.js"; -import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams.js"; -import type { SkillsListParams } from "./v2/SkillsListParams.js"; -import type { ThreadApproveGuardianDeniedActionParams } from "./v2/ThreadApproveGuardianDeniedActionParams.js"; -import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams.js"; -import type { ThreadBackgroundTerminalsCleanParams } from "./v2/ThreadBackgroundTerminalsCleanParams.js"; -import type { ThreadCompactStartParams } from "./v2/ThreadCompactStartParams.js"; -import type { ThreadDecrementElicitationParams } from "./v2/ThreadDecrementElicitationParams.js"; -import type { ThreadForkParams } from "./v2/ThreadForkParams.js"; -import type { ThreadGoalClearParams } from "./v2/ThreadGoalClearParams.js"; -import type { ThreadGoalGetParams } from "./v2/ThreadGoalGetParams.js"; -import type { ThreadGoalSetParams } from "./v2/ThreadGoalSetParams.js"; -import type { ThreadIncrementElicitationParams } from "./v2/ThreadIncrementElicitationParams.js"; -import type { ThreadInjectItemsParams } from "./v2/ThreadInjectItemsParams.js"; -import type { ThreadListParams } from "./v2/ThreadListParams.js"; -import type { ThreadLoadedListParams } from "./v2/ThreadLoadedListParams.js"; -import type { ThreadMemoryModeSetParams } from "./v2/ThreadMemoryModeSetParams.js"; -import type { ThreadMetadataUpdateParams } from "./v2/ThreadMetadataUpdateParams.js"; -import type { ThreadReadParams } from "./v2/ThreadReadParams.js"; -import type { ThreadRealtimeAppendAudioParams } from "./v2/ThreadRealtimeAppendAudioParams.js"; -import type { ThreadRealtimeAppendTextParams } from "./v2/ThreadRealtimeAppendTextParams.js"; -import type { ThreadRealtimeListVoicesParams } from "./v2/ThreadRealtimeListVoicesParams.js"; -import type { ThreadRealtimeStartParams } from "./v2/ThreadRealtimeStartParams.js"; -import type { ThreadRealtimeStopParams } from "./v2/ThreadRealtimeStopParams.js"; -import type { ThreadResumeParams } from "./v2/ThreadResumeParams.js"; -import type { ThreadRollbackParams } from "./v2/ThreadRollbackParams.js"; -import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams.js"; -import type { ThreadShellCommandParams } from "./v2/ThreadShellCommandParams.js"; -import type { ThreadStartParams } from "./v2/ThreadStartParams.js"; -import type { ThreadTurnsListParams } from "./v2/ThreadTurnsListParams.js"; -import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams.js"; -import type { ThreadUnsubscribeParams } from "./v2/ThreadUnsubscribeParams.js"; -import type { TurnInterruptParams } from "./v2/TurnInterruptParams.js"; -import type { TurnStartParams } from "./v2/TurnStartParams.js"; -import type { TurnSteerParams } from "./v2/TurnSteerParams.js"; -import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupStartParams.js"; - -/** - * Request from the client to the server. - */ -export type ClientRequest = - | { method: "initialize"; id: RequestId; params: InitializeParams } - | { method: "thread/start"; id: RequestId; params: ThreadStartParams } - | { method: "thread/resume"; id: RequestId; params: ThreadResumeParams } - | { method: "thread/fork"; id: RequestId; params: ThreadForkParams } - | { method: "thread/archive"; id: RequestId; params: ThreadArchiveParams } - | { method: "thread/unsubscribe"; id: RequestId; params: ThreadUnsubscribeParams } - | { - method: "thread/increment_elicitation"; - id: RequestId; - params: ThreadIncrementElicitationParams; - } - | { - method: "thread/decrement_elicitation"; - id: RequestId; - params: ThreadDecrementElicitationParams; - } - | { method: "thread/name/set"; id: RequestId; params: ThreadSetNameParams } - | { method: "thread/goal/set"; id: RequestId; params: ThreadGoalSetParams } - | { method: "thread/goal/get"; id: RequestId; params: ThreadGoalGetParams } - | { method: "thread/goal/clear"; id: RequestId; params: ThreadGoalClearParams } - | { method: "thread/metadata/update"; id: RequestId; params: ThreadMetadataUpdateParams } - | { method: "thread/memoryMode/set"; id: RequestId; params: ThreadMemoryModeSetParams } - | { method: "memory/reset"; id: RequestId; params: undefined } - | { method: "thread/unarchive"; id: RequestId; params: ThreadUnarchiveParams } - | { method: "thread/compact/start"; id: RequestId; params: ThreadCompactStartParams } - | { method: "thread/shellCommand"; id: RequestId; params: ThreadShellCommandParams } - | { - method: "thread/approveGuardianDeniedAction"; - id: RequestId; - params: ThreadApproveGuardianDeniedActionParams; - } - | { - method: "thread/backgroundTerminals/clean"; - id: RequestId; - params: ThreadBackgroundTerminalsCleanParams; - } - | { method: "thread/rollback"; id: RequestId; params: ThreadRollbackParams } - | { method: "thread/list"; id: RequestId; params: ThreadListParams } - | { method: "thread/loaded/list"; id: RequestId; params: ThreadLoadedListParams } - | { method: "thread/read"; id: RequestId; params: ThreadReadParams } - | { method: "thread/turns/list"; id: RequestId; params: ThreadTurnsListParams } - | { method: "thread/inject_items"; id: RequestId; params: ThreadInjectItemsParams } - | { method: "skills/list"; id: RequestId; params: SkillsListParams } - | { method: "hooks/list"; id: RequestId; params: HooksListParams } - | { method: "marketplace/add"; id: RequestId; params: MarketplaceAddParams } - | { method: "marketplace/remove"; id: RequestId; params: MarketplaceRemoveParams } - | { method: "marketplace/upgrade"; id: RequestId; params: MarketplaceUpgradeParams } - | { method: "plugin/list"; id: RequestId; params: PluginListParams } - | { method: "plugin/read"; id: RequestId; params: PluginReadParams } - | { method: "plugin/skill/read"; id: RequestId; params: PluginSkillReadParams } - | { method: "plugin/share/save"; id: RequestId; params: PluginShareSaveParams } - | { method: "plugin/share/list"; id: RequestId; params: PluginShareListParams } - | { method: "plugin/share/delete"; id: RequestId; params: PluginShareDeleteParams } - | { method: "app/list"; id: RequestId; params: AppsListParams } - | { method: "device/key/create"; id: RequestId; params: DeviceKeyCreateParams } - | { method: "device/key/public"; id: RequestId; params: DeviceKeyPublicParams } - | { method: "device/key/sign"; id: RequestId; params: DeviceKeySignParams } - | { method: "fs/readFile"; id: RequestId; params: FsReadFileParams } - | { method: "fs/writeFile"; id: RequestId; params: FsWriteFileParams } - | { method: "fs/createDirectory"; id: RequestId; params: FsCreateDirectoryParams } - | { method: "fs/getMetadata"; id: RequestId; params: FsGetMetadataParams } - | { method: "fs/readDirectory"; id: RequestId; params: FsReadDirectoryParams } - | { method: "fs/remove"; id: RequestId; params: FsRemoveParams } - | { method: "fs/copy"; id: RequestId; params: FsCopyParams } - | { method: "fs/watch"; id: RequestId; params: FsWatchParams } - | { method: "fs/unwatch"; id: RequestId; params: FsUnwatchParams } - | { method: "skills/config/write"; id: RequestId; params: SkillsConfigWriteParams } - | { method: "plugin/install"; id: RequestId; params: PluginInstallParams } - | { method: "plugin/uninstall"; id: RequestId; params: PluginUninstallParams } - | { method: "turn/start"; id: RequestId; params: TurnStartParams } - | { method: "turn/steer"; id: RequestId; params: TurnSteerParams } - | { method: "turn/interrupt"; id: RequestId; params: TurnInterruptParams } - | { method: "thread/realtime/start"; id: RequestId; params: ThreadRealtimeStartParams } - | { - method: "thread/realtime/appendAudio"; - id: RequestId; - params: ThreadRealtimeAppendAudioParams; - } - | { method: "thread/realtime/appendText"; id: RequestId; params: ThreadRealtimeAppendTextParams } - | { method: "thread/realtime/stop"; id: RequestId; params: ThreadRealtimeStopParams } - | { method: "thread/realtime/listVoices"; id: RequestId; params: ThreadRealtimeListVoicesParams } - | { method: "review/start"; id: RequestId; params: ReviewStartParams } - | { method: "model/list"; id: RequestId; params: ModelListParams } - | { - method: "modelProvider/capabilities/read"; - id: RequestId; - params: ModelProviderCapabilitiesReadParams; - } - | { method: "experimentalFeature/list"; id: RequestId; params: ExperimentalFeatureListParams } - | { - method: "experimentalFeature/enablement/set"; - id: RequestId; - params: ExperimentalFeatureEnablementSetParams; - } - | { method: "collaborationMode/list"; id: RequestId; params: CollaborationModeListParams } - | { method: "mock/experimentalMethod"; id: RequestId; params: MockExperimentalMethodParams } - | { method: "mcpServer/oauth/login"; id: RequestId; params: McpServerOauthLoginParams } - | { method: "config/mcpServer/reload"; id: RequestId; params: undefined } - | { method: "mcpServerStatus/list"; id: RequestId; params: ListMcpServerStatusParams } - | { method: "mcpServer/resource/read"; id: RequestId; params: McpResourceReadParams } - | { method: "mcpServer/tool/call"; id: RequestId; params: McpServerToolCallParams } - | { method: "windowsSandbox/setupStart"; id: RequestId; params: WindowsSandboxSetupStartParams } - | { method: "account/login/start"; id: RequestId; params: LoginAccountParams } - | { method: "account/login/cancel"; id: RequestId; params: CancelLoginAccountParams } - | { method: "account/logout"; id: RequestId; params: undefined } - | { method: "account/rateLimits/read"; id: RequestId; params: undefined } - | { - method: "account/sendAddCreditsNudgeEmail"; - id: RequestId; - params: SendAddCreditsNudgeEmailParams; - } - | { method: "feedback/upload"; id: RequestId; params: FeedbackUploadParams } - | { method: "command/exec"; id: RequestId; params: CommandExecParams } - | { method: "command/exec/write"; id: RequestId; params: CommandExecWriteParams } - | { method: "command/exec/terminate"; id: RequestId; params: CommandExecTerminateParams } - | { method: "command/exec/resize"; id: RequestId; params: CommandExecResizeParams } - | { method: "config/read"; id: RequestId; params: ConfigReadParams } - | { method: "externalAgentConfig/detect"; id: RequestId; params: ExternalAgentConfigDetectParams } - | { method: "externalAgentConfig/import"; id: RequestId; params: ExternalAgentConfigImportParams } - | { method: "config/value/write"; id: RequestId; params: ConfigValueWriteParams } - | { method: "config/batchWrite"; id: RequestId; params: ConfigBatchWriteParams } - | { method: "configRequirements/read"; id: RequestId; params: undefined } - | { method: "account/read"; id: RequestId; params: GetAccountParams } - | { method: "getConversationSummary"; id: RequestId; params: GetConversationSummaryParams } - | { method: "gitDiffToRemote"; id: RequestId; params: GitDiffToRemoteParams } - | { method: "getAuthStatus"; id: RequestId; params: GetAuthStatusParams } - | { method: "fuzzyFileSearch"; id: RequestId; params: FuzzyFileSearchParams } - | { - method: "fuzzyFileSearch/sessionStart"; - id: RequestId; - params: FuzzyFileSearchSessionStartParams; - } - | { - method: "fuzzyFileSearch/sessionUpdate"; - id: RequestId; - params: FuzzyFileSearchSessionUpdateParams; - } - | { - method: "fuzzyFileSearch/sessionStop"; - id: RequestId; - params: FuzzyFileSearchSessionStopParams; - }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/CollaborationMode.ts b/extensions/codex/src/app-server/protocol-generated/typescript/CollaborationMode.ts deleted file mode 100644 index 42ed1899432..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/CollaborationMode.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ModeKind } from "./ModeKind.js"; -import type { Settings } from "./Settings.js"; - -/** - * Collaboration mode for a Codex session. - */ -export type CollaborationMode = { mode: ModeKind; settings: Settings }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ContentItem.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ContentItem.ts deleted file mode 100644 index 955e3a65264..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ContentItem.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ImageDetail } from "./ImageDetail.js"; - -export type ContentItem = - | { type: "input_text"; text: string } - | { type: "input_image"; image_url: string; detail?: ImageDetail } - | { type: "output_text"; text: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ConversationGitInfo.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ConversationGitInfo.ts deleted file mode 100644 index 87156451e03..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ConversationGitInfo.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ConversationGitInfo = { - sha: string | null; - branch: string | null; - origin_url: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ConversationSummary.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ConversationSummary.ts deleted file mode 100644 index 431f5e79420..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ConversationSummary.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConversationGitInfo } from "./ConversationGitInfo.js"; -import type { SessionSource } from "./SessionSource.js"; -import type { ThreadId } from "./ThreadId.js"; - -export type ConversationSummary = { - conversationId: ThreadId; - path: string; - preview: string; - timestamp: string | null; - updatedAt: string | null; - modelProvider: string; - cwd: string; - cliVersion: string; - source: SessionSource; - gitInfo: ConversationGitInfo | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ExecCommandApprovalParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ExecCommandApprovalParams.ts deleted file mode 100644 index f6153f68889..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ExecCommandApprovalParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ParsedCommand } from "./ParsedCommand.js"; -import type { ThreadId } from "./ThreadId.js"; - -export type ExecCommandApprovalParams = { - conversationId: ThreadId; - /** - * Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent] - * and [codex_protocol::protocol::ExecCommandEndEvent]. - */ - callId: string; - /** - * Identifier for this specific approval callback. - */ - approvalId: string | null; - command: Array; - cwd: string; - reason: string | null; - parsedCmd: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ExecCommandApprovalResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ExecCommandApprovalResponse.ts deleted file mode 100644 index b695e5f12e1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ExecCommandApprovalResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReviewDecision } from "./ReviewDecision.js"; - -export type ExecCommandApprovalResponse = { decision: ReviewDecision }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ExecPolicyAmendment.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ExecPolicyAmendment.ts deleted file mode 100644 index 98e2626c381..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ExecPolicyAmendment.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Proposed execpolicy change to allow commands starting with this prefix. - * - * The `command` tokens form the prefix that would be added as an execpolicy - * `prefix_rule(..., decision="allow")`, letting the agent bypass approval for - * commands that start with this token sequence. - */ -export type ExecPolicyAmendment = Array; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FileChange.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FileChange.ts deleted file mode 100644 index da6abdda285..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FileChange.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FileChange = - | { type: "add"; content: string } - | { type: "delete"; content: string } - | { type: "update"; unified_diff: string; move_path: string | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ForcedLoginMethod.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ForcedLoginMethod.ts deleted file mode 100644 index c695908866a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ForcedLoginMethod.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ForcedLoginMethod = "chatgpt" | "api"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FunctionCallOutputBody.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FunctionCallOutputBody.ts deleted file mode 100644 index 737f4cea9bf..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FunctionCallOutputBody.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentItem.js"; - -export type FunctionCallOutputBody = string | Array; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FunctionCallOutputContentItem.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FunctionCallOutputContentItem.ts deleted file mode 100644 index c60d1b72792..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FunctionCallOutputContentItem.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ImageDetail } from "./ImageDetail.js"; - -/** - * Responses API compatible content items that can be returned by a tool call. - * This is a subset of ContentItem with the types we support as function call outputs. - */ -export type FunctionCallOutputContentItem = - | { type: "input_text"; text: string } - | { type: "input_image"; image_url: string; detail?: ImageDetail }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchMatchType.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchMatchType.ts deleted file mode 100644 index 60e92f925ea..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchMatchType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchMatchType = "file" | "directory"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchParams.ts deleted file mode 100644 index 24e397c8478..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchParams = { - query: string; - roots: Array; - cancellationToken: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchResponse.ts deleted file mode 100644 index af93c1a2e42..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult.js"; - -export type FuzzyFileSearchResponse = { files: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchResult.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchResult.ts deleted file mode 100644 index bfd5992fd88..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchResult.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType.js"; - -/** - * Superset of [`codex_file_search::FileMatch`] - */ -export type FuzzyFileSearchResult = { - root: string; - path: string; - match_type: FuzzyFileSearchMatchType; - file_name: string; - score: number; - indices: Array | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionCompletedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionCompletedNotification.ts deleted file mode 100644 index e8f3e391fae..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionCompletedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionCompletedNotification = { sessionId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStartParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStartParams.ts deleted file mode 100644 index f746734f004..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStartParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionStartParams = { sessionId: string; roots: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStartResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStartResponse.ts deleted file mode 100644 index cfe1399b75c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStartResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionStartResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStopParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStopParams.ts deleted file mode 100644 index 72da3c89454..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStopParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionStopParams = { sessionId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStopResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStopResponse.ts deleted file mode 100644 index a3500fb00c6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionStopResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionStopResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionUpdateParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionUpdateParams.ts deleted file mode 100644 index 53542ea6c75..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionUpdateParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionUpdateParams = { sessionId: string; query: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionUpdateResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionUpdateResponse.ts deleted file mode 100644 index 54b8701656d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionUpdateResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FuzzyFileSearchSessionUpdateResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionUpdatedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionUpdatedNotification.ts deleted file mode 100644 index ccd8f5c36e0..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/FuzzyFileSearchSessionUpdatedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult.js"; - -export type FuzzyFileSearchSessionUpdatedNotification = { - sessionId: string; - query: string; - files: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/GetAuthStatusParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/GetAuthStatusParams.ts deleted file mode 100644 index c3f4c1a04f9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/GetAuthStatusParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GetAuthStatusParams = { includeToken: boolean | null; refreshToken: boolean | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/GetAuthStatusResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/GetAuthStatusResponse.ts deleted file mode 100644 index ec5e7783a31..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/GetAuthStatusResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AuthMode } from "./AuthMode.js"; - -export type GetAuthStatusResponse = { - authMethod: AuthMode | null; - authToken: string | null; - requiresOpenaiAuth: boolean | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/GetConversationSummaryParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/GetConversationSummaryParams.ts deleted file mode 100644 index acb7fba5d7b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/GetConversationSummaryParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadId } from "./ThreadId.js"; - -export type GetConversationSummaryParams = { rolloutPath: string } | { conversationId: ThreadId }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/GetConversationSummaryResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/GetConversationSummaryResponse.ts deleted file mode 100644 index 4b68fe805af..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/GetConversationSummaryResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConversationSummary } from "./ConversationSummary.js"; - -export type GetConversationSummaryResponse = { summary: ConversationSummary }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/GitDiffToRemoteParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/GitDiffToRemoteParams.ts deleted file mode 100644 index 31ff4f9ddd7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/GitDiffToRemoteParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GitDiffToRemoteParams = { cwd: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/GitDiffToRemoteResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/GitDiffToRemoteResponse.ts deleted file mode 100644 index e06a77c0a26..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/GitDiffToRemoteResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { GitSha } from "./GitSha.js"; - -export type GitDiffToRemoteResponse = { sha: GitSha; diff: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/GitSha.ts b/extensions/codex/src/app-server/protocol-generated/typescript/GitSha.ts deleted file mode 100644 index 701b75aa0bf..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/GitSha.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GitSha = string; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ImageDetail.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ImageDetail.ts deleted file mode 100644 index a48f07c0882..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ImageDetail.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ImageDetail = "auto" | "low" | "high" | "original"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/InitializeCapabilities.ts b/extensions/codex/src/app-server/protocol-generated/typescript/InitializeCapabilities.ts deleted file mode 100644 index eaac4b8da1f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/InitializeCapabilities.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Client-declared capabilities negotiated during initialize. - */ -export type InitializeCapabilities = { - /** - * Opt into receiving experimental API methods and fields. - */ - experimentalApi: boolean; - /** - * Exact notification method names that should be suppressed for this - * connection (for example `thread/started`). - */ - optOutNotificationMethods?: Array | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/InitializeParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/InitializeParams.ts deleted file mode 100644 index 9322cf9129b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/InitializeParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ClientInfo } from "./ClientInfo.js"; -import type { InitializeCapabilities } from "./InitializeCapabilities.js"; - -export type InitializeParams = { - clientInfo: ClientInfo; - capabilities: InitializeCapabilities | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/InitializeResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/InitializeResponse.ts deleted file mode 100644 index c6a9bc5377d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/InitializeResponse.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "./AbsolutePathBuf.js"; - -export type InitializeResponse = { - userAgent: string; - /** - * Absolute path to the server's $CODEX_HOME directory. - */ - codexHome: AbsolutePathBuf; - /** - * Platform family for the running app-server target, for example - * `"unix"` or `"windows"`. - */ - platformFamily: string; - /** - * Operating system for the running app-server target, for example - * `"macos"`, `"linux"`, or `"windows"`. - */ - platformOs: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/InputModality.ts b/extensions/codex/src/app-server/protocol-generated/typescript/InputModality.ts deleted file mode 100644 index 73661938b38..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/InputModality.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Canonical user-input modality tags advertised by a model. - */ -export type InputModality = "text" | "image"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/InternalSessionSource.ts b/extensions/codex/src/app-server/protocol-generated/typescript/InternalSessionSource.ts deleted file mode 100644 index 47417c51679..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/InternalSessionSource.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type InternalSessionSource = "memory_consolidation"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/LocalShellAction.ts b/extensions/codex/src/app-server/protocol-generated/typescript/LocalShellAction.ts deleted file mode 100644 index 89cd49e69b6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/LocalShellAction.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { LocalShellExecAction } from "./LocalShellExecAction.js"; - -export type LocalShellAction = { type: "exec" } & LocalShellExecAction; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/LocalShellExecAction.ts b/extensions/codex/src/app-server/protocol-generated/typescript/LocalShellExecAction.ts deleted file mode 100644 index 5faf9621ad9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/LocalShellExecAction.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type LocalShellExecAction = { - command: Array; - timeout_ms: bigint | null; - working_directory: string | null; - env: { [key in string]?: string } | null; - user: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/LocalShellStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/LocalShellStatus.ts deleted file mode 100644 index 00db484ad6d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/LocalShellStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type LocalShellStatus = "completed" | "in_progress" | "incomplete"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/MessagePhase.ts b/extensions/codex/src/app-server/protocol-generated/typescript/MessagePhase.ts deleted file mode 100644 index 9e16021b546..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/MessagePhase.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Classifies an assistant message as interim commentary or final answer text. - * - * Providers do not emit this consistently, so callers must treat `None` as - * "phase unknown" and keep compatibility behavior for legacy models. - */ -export type MessagePhase = "commentary" | "final_answer"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ModeKind.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ModeKind.ts deleted file mode 100644 index 7d2324add70..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ModeKind.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Initial collaboration mode to use when the TUI starts. - */ -export type ModeKind = "plan" | "default"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/NetworkPolicyAmendment.ts b/extensions/codex/src/app-server/protocol-generated/typescript/NetworkPolicyAmendment.ts deleted file mode 100644 index 5455621ece7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/NetworkPolicyAmendment.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction.js"; - -export type NetworkPolicyAmendment = { host: string; action: NetworkPolicyRuleAction }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/NetworkPolicyRuleAction.ts b/extensions/codex/src/app-server/protocol-generated/typescript/NetworkPolicyRuleAction.ts deleted file mode 100644 index 55ec70032a6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/NetworkPolicyRuleAction.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type NetworkPolicyRuleAction = "allow" | "deny"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ParsedCommand.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ParsedCommand.ts deleted file mode 100644 index 2302ea10d74..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ParsedCommand.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ParsedCommand = - | { - type: "read"; - cmd: string; - name: string; - /** - * (Best effort) Path to the file being read by the command. When - * possible, this is an absolute path, though when relative, it should - * be resolved against the `cwd`` that will be used to run the command - * to derive the absolute path. - */ - path: string; - } - | { type: "list_files"; cmd: string; path: string | null } - | { type: "search"; cmd: string; query: string | null; path: string | null } - | { type: "unknown"; cmd: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/Personality.ts b/extensions/codex/src/app-server/protocol-generated/typescript/Personality.ts deleted file mode 100644 index 45165f4e33d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/Personality.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type Personality = "none" | "friendly" | "pragmatic"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/PlanType.ts b/extensions/codex/src/app-server/protocol-generated/typescript/PlanType.ts deleted file mode 100644 index 54281532d13..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/PlanType.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PlanType = - | "free" - | "go" - | "plus" - | "pro" - | "prolite" - | "team" - | "self_serve_business_usage_based" - | "business" - | "enterprise_cbp_usage_based" - | "enterprise" - | "edu" - | "unknown"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/RealtimeConversationVersion.ts b/extensions/codex/src/app-server/protocol-generated/typescript/RealtimeConversationVersion.ts deleted file mode 100644 index cedc4bbe525..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/RealtimeConversationVersion.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RealtimeConversationVersion = "v1" | "v2"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/RealtimeOutputModality.ts b/extensions/codex/src/app-server/protocol-generated/typescript/RealtimeOutputModality.ts deleted file mode 100644 index 78e00e7143d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/RealtimeOutputModality.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RealtimeOutputModality = "text" | "audio"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/RealtimeVoice.ts b/extensions/codex/src/app-server/protocol-generated/typescript/RealtimeVoice.ts deleted file mode 100644 index bd40db79a1c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/RealtimeVoice.ts +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RealtimeVoice = - | "alloy" - | "arbor" - | "ash" - | "ballad" - | "breeze" - | "cedar" - | "coral" - | "cove" - | "echo" - | "ember" - | "juniper" - | "maple" - | "marin" - | "sage" - | "shimmer" - | "sol" - | "spruce" - | "vale" - | "verse"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/RealtimeVoicesList.ts b/extensions/codex/src/app-server/protocol-generated/typescript/RealtimeVoicesList.ts deleted file mode 100644 index f2d19ee1e30..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/RealtimeVoicesList.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RealtimeVoice } from "./RealtimeVoice.js"; - -export type RealtimeVoicesList = { - v1: Array; - v2: Array; - defaultV1: RealtimeVoice; - defaultV2: RealtimeVoice; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ReasoningEffort.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ReasoningEffort.ts deleted file mode 100644 index c0798f43a32..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ReasoningEffort.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning - */ -export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ReasoningItemContent.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ReasoningItemContent.ts deleted file mode 100644 index 1583fa45f00..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ReasoningItemContent.ts +++ /dev/null @@ -1,7 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReasoningItemContent = - | { type: "reasoning_text"; text: string } - | { type: "text"; text: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ReasoningItemReasoningSummary.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ReasoningItemReasoningSummary.ts deleted file mode 100644 index cd7cf0acd2b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ReasoningItemReasoningSummary.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReasoningItemReasoningSummary = { type: "summary_text"; text: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ReasoningSummary.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ReasoningSummary.ts deleted file mode 100644 index d246ac12ec7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ReasoningSummary.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * A summary of the reasoning performed by the model. This can be useful for - * debugging and understanding the model's reasoning process. - * See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries - */ -export type ReasoningSummary = "auto" | "concise" | "detailed" | "none"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/RequestId.ts b/extensions/codex/src/app-server/protocol-generated/typescript/RequestId.ts deleted file mode 100644 index 8a771bd0213..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/RequestId.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RequestId = string | number; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/Resource.ts b/extensions/codex/src/app-server/protocol-generated/typescript/Resource.ts deleted file mode 100644 index b519402c8e8..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/Resource.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "./serde_json/JsonValue.js"; - -/** - * A known resource that the server is capable of reading. - */ -export type Resource = { - annotations?: JsonValue; - description?: string; - mimeType?: string; - name: string; - size?: number; - title?: string; - uri: string; - icons?: Array; - _meta?: JsonValue; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ResourceContent.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ResourceContent.ts deleted file mode 100644 index d1d8316aa3b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ResourceContent.ts +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "./serde_json/JsonValue.js"; - -/** - * Contents returned when reading a resource from an MCP server. - */ -export type ResourceContent = - | { - /** - * The URI of this resource. - */ - uri: string; - mimeType?: string; - text: string; - _meta?: JsonValue; - } - | { - /** - * The URI of this resource. - */ - uri: string; - mimeType?: string; - blob: string; - _meta?: JsonValue; - }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ResourceTemplate.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ResourceTemplate.ts deleted file mode 100644 index 5ee2f13cd1b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ResourceTemplate.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "./serde_json/JsonValue.js"; - -/** - * A template description for resources available on the server. - */ -export type ResourceTemplate = { - annotations?: JsonValue; - uriTemplate: string; - name: string; - title?: string; - description?: string; - mimeType?: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ResponseItem.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ResponseItem.ts deleted file mode 100644 index fe3c211c75e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ResponseItem.ts +++ /dev/null @@ -1,63 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ContentItem } from "./ContentItem.js"; -import type { FunctionCallOutputBody } from "./FunctionCallOutputBody.js"; -import type { LocalShellAction } from "./LocalShellAction.js"; -import type { LocalShellStatus } from "./LocalShellStatus.js"; -import type { MessagePhase } from "./MessagePhase.js"; -import type { ReasoningItemContent } from "./ReasoningItemContent.js"; -import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary.js"; -import type { WebSearchAction } from "./WebSearchAction.js"; - -export type ResponseItem = - | { type: "message"; role: string; content: Array; phase?: MessagePhase } - | { - type: "reasoning"; - summary: Array; - content?: Array; - encrypted_content: string | null; - } - | { - type: "local_shell_call"; - /** - * Set when using the Responses API. - */ - call_id: string | null; - status: LocalShellStatus; - action: LocalShellAction; - } - | { type: "function_call"; name: string; namespace?: string; arguments: string; call_id: string } - | { - type: "tool_search_call"; - call_id: string | null; - status?: string; - execution: string; - arguments: unknown; - } - | { type: "function_call_output"; call_id: string; output: FunctionCallOutputBody } - | { type: "custom_tool_call"; status?: string; call_id: string; name: string; input: string } - | { - type: "custom_tool_call_output"; - call_id: string; - name?: string; - output: FunctionCallOutputBody; - } - | { - type: "tool_search_output"; - call_id: string | null; - status: string; - execution: string; - tools: unknown[]; - } - | { type: "web_search_call"; status?: string; action?: WebSearchAction } - | { - type: "image_generation_call"; - id: string; - status: string; - revised_prompt?: string; - result: string; - } - | { type: "compaction"; encrypted_content: string } - | { type: "context_compaction"; encrypted_content?: string } - | { type: "other" }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ReviewDecision.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ReviewDecision.ts deleted file mode 100644 index 23b8771936d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ReviewDecision.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExecPolicyAmendment } from "./ExecPolicyAmendment.js"; -import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment.js"; - -/** - * User's decision in response to an ExecApprovalRequest. - */ -export type ReviewDecision = - | "approved" - | { approved_execpolicy_amendment: { proposed_execpolicy_amendment: ExecPolicyAmendment } } - | "approved_for_session" - | { network_policy_amendment: { network_policy_amendment: NetworkPolicyAmendment } } - | "denied" - | "timed_out" - | "abort"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ServerNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ServerNotification.ts deleted file mode 100644 index 04d8301bcc3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ServerNotification.ts +++ /dev/null @@ -1,150 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FuzzyFileSearchSessionCompletedNotification } from "./FuzzyFileSearchSessionCompletedNotification.js"; -import type { FuzzyFileSearchSessionUpdatedNotification } from "./FuzzyFileSearchSessionUpdatedNotification.js"; -import type { AccountLoginCompletedNotification } from "./v2/AccountLoginCompletedNotification.js"; -import type { AccountRateLimitsUpdatedNotification } from "./v2/AccountRateLimitsUpdatedNotification.js"; -import type { AccountUpdatedNotification } from "./v2/AccountUpdatedNotification.js"; -import type { AgentMessageDeltaNotification } from "./v2/AgentMessageDeltaNotification.js"; -import type { AppListUpdatedNotification } from "./v2/AppListUpdatedNotification.js"; -import type { CommandExecOutputDeltaNotification } from "./v2/CommandExecOutputDeltaNotification.js"; -import type { CommandExecutionOutputDeltaNotification } from "./v2/CommandExecutionOutputDeltaNotification.js"; -import type { ConfigWarningNotification } from "./v2/ConfigWarningNotification.js"; -import type { ContextCompactedNotification } from "./v2/ContextCompactedNotification.js"; -import type { DeprecationNoticeNotification } from "./v2/DeprecationNoticeNotification.js"; -import type { ErrorNotification } from "./v2/ErrorNotification.js"; -import type { ExternalAgentConfigImportCompletedNotification } from "./v2/ExternalAgentConfigImportCompletedNotification.js"; -import type { FileChangeOutputDeltaNotification } from "./v2/FileChangeOutputDeltaNotification.js"; -import type { FileChangePatchUpdatedNotification } from "./v2/FileChangePatchUpdatedNotification.js"; -import type { FsChangedNotification } from "./v2/FsChangedNotification.js"; -import type { GuardianWarningNotification } from "./v2/GuardianWarningNotification.js"; -import type { HookCompletedNotification } from "./v2/HookCompletedNotification.js"; -import type { HookStartedNotification } from "./v2/HookStartedNotification.js"; -import type { ItemCompletedNotification } from "./v2/ItemCompletedNotification.js"; -import type { ItemGuardianApprovalReviewCompletedNotification } from "./v2/ItemGuardianApprovalReviewCompletedNotification.js"; -import type { ItemGuardianApprovalReviewStartedNotification } from "./v2/ItemGuardianApprovalReviewStartedNotification.js"; -import type { ItemStartedNotification } from "./v2/ItemStartedNotification.js"; -import type { McpServerOauthLoginCompletedNotification } from "./v2/McpServerOauthLoginCompletedNotification.js"; -import type { McpServerStatusUpdatedNotification } from "./v2/McpServerStatusUpdatedNotification.js"; -import type { McpToolCallProgressNotification } from "./v2/McpToolCallProgressNotification.js"; -import type { ModelReroutedNotification } from "./v2/ModelReroutedNotification.js"; -import type { ModelVerificationNotification } from "./v2/ModelVerificationNotification.js"; -import type { PlanDeltaNotification } from "./v2/PlanDeltaNotification.js"; -import type { RawResponseItemCompletedNotification } from "./v2/RawResponseItemCompletedNotification.js"; -import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummaryPartAddedNotification.js"; -import type { ReasoningSummaryTextDeltaNotification } from "./v2/ReasoningSummaryTextDeltaNotification.js"; -import type { ReasoningTextDeltaNotification } from "./v2/ReasoningTextDeltaNotification.js"; -import type { RemoteControlStatusChangedNotification } from "./v2/RemoteControlStatusChangedNotification.js"; -import type { ServerRequestResolvedNotification } from "./v2/ServerRequestResolvedNotification.js"; -import type { SkillsChangedNotification } from "./v2/SkillsChangedNotification.js"; -import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification.js"; -import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification.js"; -import type { ThreadClosedNotification } from "./v2/ThreadClosedNotification.js"; -import type { ThreadGoalClearedNotification } from "./v2/ThreadGoalClearedNotification.js"; -import type { ThreadGoalUpdatedNotification } from "./v2/ThreadGoalUpdatedNotification.js"; -import type { ThreadNameUpdatedNotification } from "./v2/ThreadNameUpdatedNotification.js"; -import type { ThreadRealtimeClosedNotification } from "./v2/ThreadRealtimeClosedNotification.js"; -import type { ThreadRealtimeErrorNotification } from "./v2/ThreadRealtimeErrorNotification.js"; -import type { ThreadRealtimeItemAddedNotification } from "./v2/ThreadRealtimeItemAddedNotification.js"; -import type { ThreadRealtimeOutputAudioDeltaNotification } from "./v2/ThreadRealtimeOutputAudioDeltaNotification.js"; -import type { ThreadRealtimeSdpNotification } from "./v2/ThreadRealtimeSdpNotification.js"; -import type { ThreadRealtimeStartedNotification } from "./v2/ThreadRealtimeStartedNotification.js"; -import type { ThreadRealtimeTranscriptDeltaNotification } from "./v2/ThreadRealtimeTranscriptDeltaNotification.js"; -import type { ThreadRealtimeTranscriptDoneNotification } from "./v2/ThreadRealtimeTranscriptDoneNotification.js"; -import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification.js"; -import type { ThreadStatusChangedNotification } from "./v2/ThreadStatusChangedNotification.js"; -import type { ThreadTokenUsageUpdatedNotification } from "./v2/ThreadTokenUsageUpdatedNotification.js"; -import type { ThreadUnarchivedNotification } from "./v2/ThreadUnarchivedNotification.js"; -import type { TurnCompletedNotification } from "./v2/TurnCompletedNotification.js"; -import type { TurnDiffUpdatedNotification } from "./v2/TurnDiffUpdatedNotification.js"; -import type { TurnPlanUpdatedNotification } from "./v2/TurnPlanUpdatedNotification.js"; -import type { TurnStartedNotification } from "./v2/TurnStartedNotification.js"; -import type { WarningNotification } from "./v2/WarningNotification.js"; -import type { WindowsSandboxSetupCompletedNotification } from "./v2/WindowsSandboxSetupCompletedNotification.js"; -import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldWritableWarningNotification.js"; - -/** - * Notification sent from the server to the client. - */ -export type ServerNotification = - | { method: "error"; params: ErrorNotification } - | { method: "thread/started"; params: ThreadStartedNotification } - | { method: "thread/status/changed"; params: ThreadStatusChangedNotification } - | { method: "thread/archived"; params: ThreadArchivedNotification } - | { method: "thread/unarchived"; params: ThreadUnarchivedNotification } - | { method: "thread/closed"; params: ThreadClosedNotification } - | { method: "skills/changed"; params: SkillsChangedNotification } - | { method: "thread/name/updated"; params: ThreadNameUpdatedNotification } - | { method: "thread/goal/updated"; params: ThreadGoalUpdatedNotification } - | { method: "thread/goal/cleared"; params: ThreadGoalClearedNotification } - | { method: "thread/tokenUsage/updated"; params: ThreadTokenUsageUpdatedNotification } - | { method: "turn/started"; params: TurnStartedNotification } - | { method: "hook/started"; params: HookStartedNotification } - | { method: "turn/completed"; params: TurnCompletedNotification } - | { method: "hook/completed"; params: HookCompletedNotification } - | { method: "turn/diff/updated"; params: TurnDiffUpdatedNotification } - | { method: "turn/plan/updated"; params: TurnPlanUpdatedNotification } - | { method: "item/started"; params: ItemStartedNotification } - | { - method: "item/autoApprovalReview/started"; - params: ItemGuardianApprovalReviewStartedNotification; - } - | { - method: "item/autoApprovalReview/completed"; - params: ItemGuardianApprovalReviewCompletedNotification; - } - | { method: "item/completed"; params: ItemCompletedNotification } - | { method: "rawResponseItem/completed"; params: RawResponseItemCompletedNotification } - | { method: "item/agentMessage/delta"; params: AgentMessageDeltaNotification } - | { method: "item/plan/delta"; params: PlanDeltaNotification } - | { method: "command/exec/outputDelta"; params: CommandExecOutputDeltaNotification } - | { method: "item/commandExecution/outputDelta"; params: CommandExecutionOutputDeltaNotification } - | { method: "item/commandExecution/terminalInteraction"; params: TerminalInteractionNotification } - | { method: "item/fileChange/outputDelta"; params: FileChangeOutputDeltaNotification } - | { method: "item/fileChange/patchUpdated"; params: FileChangePatchUpdatedNotification } - | { method: "serverRequest/resolved"; params: ServerRequestResolvedNotification } - | { method: "item/mcpToolCall/progress"; params: McpToolCallProgressNotification } - | { method: "mcpServer/oauthLogin/completed"; params: McpServerOauthLoginCompletedNotification } - | { method: "mcpServer/startupStatus/updated"; params: McpServerStatusUpdatedNotification } - | { method: "account/updated"; params: AccountUpdatedNotification } - | { method: "account/rateLimits/updated"; params: AccountRateLimitsUpdatedNotification } - | { method: "app/list/updated"; params: AppListUpdatedNotification } - | { method: "remoteControl/status/changed"; params: RemoteControlStatusChangedNotification } - | { - method: "externalAgentConfig/import/completed"; - params: ExternalAgentConfigImportCompletedNotification; - } - | { method: "fs/changed"; params: FsChangedNotification } - | { method: "item/reasoning/summaryTextDelta"; params: ReasoningSummaryTextDeltaNotification } - | { method: "item/reasoning/summaryPartAdded"; params: ReasoningSummaryPartAddedNotification } - | { method: "item/reasoning/textDelta"; params: ReasoningTextDeltaNotification } - | { method: "thread/compacted"; params: ContextCompactedNotification } - | { method: "model/rerouted"; params: ModelReroutedNotification } - | { method: "model/verification"; params: ModelVerificationNotification } - | { method: "warning"; params: WarningNotification } - | { method: "guardianWarning"; params: GuardianWarningNotification } - | { method: "deprecationNotice"; params: DeprecationNoticeNotification } - | { method: "configWarning"; params: ConfigWarningNotification } - | { method: "fuzzyFileSearch/sessionUpdated"; params: FuzzyFileSearchSessionUpdatedNotification } - | { - method: "fuzzyFileSearch/sessionCompleted"; - params: FuzzyFileSearchSessionCompletedNotification; - } - | { method: "thread/realtime/started"; params: ThreadRealtimeStartedNotification } - | { method: "thread/realtime/itemAdded"; params: ThreadRealtimeItemAddedNotification } - | { - method: "thread/realtime/transcript/delta"; - params: ThreadRealtimeTranscriptDeltaNotification; - } - | { method: "thread/realtime/transcript/done"; params: ThreadRealtimeTranscriptDoneNotification } - | { - method: "thread/realtime/outputAudio/delta"; - params: ThreadRealtimeOutputAudioDeltaNotification; - } - | { method: "thread/realtime/sdp"; params: ThreadRealtimeSdpNotification } - | { method: "thread/realtime/error"; params: ThreadRealtimeErrorNotification } - | { method: "thread/realtime/closed"; params: ThreadRealtimeClosedNotification } - | { method: "windows/worldWritableWarning"; params: WindowsWorldWritableWarningNotification } - | { method: "windowsSandbox/setupCompleted"; params: WindowsSandboxSetupCompletedNotification } - | { method: "account/login/completed"; params: AccountLoginCompletedNotification }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ServerRequest.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ServerRequest.ts deleted file mode 100644 index 356126c38ca..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ServerRequest.ts +++ /dev/null @@ -1,47 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams.js"; -import type { ExecCommandApprovalParams } from "./ExecCommandApprovalParams.js"; -import type { RequestId } from "./RequestId.js"; -import type { ChatgptAuthTokensRefreshParams } from "./v2/ChatgptAuthTokensRefreshParams.js"; -import type { CommandExecutionRequestApprovalParams } from "./v2/CommandExecutionRequestApprovalParams.js"; -import type { DynamicToolCallParams } from "./v2/DynamicToolCallParams.js"; -import type { FileChangeRequestApprovalParams } from "./v2/FileChangeRequestApprovalParams.js"; -import type { McpServerElicitationRequestParams } from "./v2/McpServerElicitationRequestParams.js"; -import type { PermissionsRequestApprovalParams } from "./v2/PermissionsRequestApprovalParams.js"; -import type { ToolRequestUserInputParams } from "./v2/ToolRequestUserInputParams.js"; - -/** - * Request initiated from the server and sent to the client. - */ -export type ServerRequest = - | { - method: "item/commandExecution/requestApproval"; - id: RequestId; - params: CommandExecutionRequestApprovalParams; - } - | { - method: "item/fileChange/requestApproval"; - id: RequestId; - params: FileChangeRequestApprovalParams; - } - | { method: "item/tool/requestUserInput"; id: RequestId; params: ToolRequestUserInputParams } - | { - method: "mcpServer/elicitation/request"; - id: RequestId; - params: McpServerElicitationRequestParams; - } - | { - method: "item/permissions/requestApproval"; - id: RequestId; - params: PermissionsRequestApprovalParams; - } - | { method: "item/tool/call"; id: RequestId; params: DynamicToolCallParams } - | { - method: "account/chatgptAuthTokens/refresh"; - id: RequestId; - params: ChatgptAuthTokensRefreshParams; - } - | { method: "applyPatchApproval"; id: RequestId; params: ApplyPatchApprovalParams } - | { method: "execCommandApproval"; id: RequestId; params: ExecCommandApprovalParams }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ServiceTier.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ServiceTier.ts deleted file mode 100644 index ce11286dbd1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ServiceTier.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ServiceTier = "fast" | "flex"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/SessionSource.ts b/extensions/codex/src/app-server/protocol-generated/typescript/SessionSource.ts deleted file mode 100644 index 4ff2e10411a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/SessionSource.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { InternalSessionSource } from "./InternalSessionSource.js"; -import type { SubAgentSource } from "./SubAgentSource.js"; - -export type SessionSource = - | "cli" - | "vscode" - | "exec" - | "mcp" - | { custom: string } - | { internal: InternalSessionSource } - | { subagent: SubAgentSource } - | "unknown"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/Settings.ts b/extensions/codex/src/app-server/protocol-generated/typescript/Settings.ts deleted file mode 100644 index 12132380a22..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/Settings.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReasoningEffort } from "./ReasoningEffort.js"; - -/** - * Settings for a collaboration mode. - */ -export type Settings = { - model: string; - reasoning_effort: ReasoningEffort | null; - developer_instructions: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/SubAgentSource.ts b/extensions/codex/src/app-server/protocol-generated/typescript/SubAgentSource.ts deleted file mode 100644 index 3c102517629..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/SubAgentSource.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AgentPath } from "./AgentPath.js"; -import type { ThreadId } from "./ThreadId.js"; - -export type SubAgentSource = - | "review" - | "compact" - | { - thread_spawn: { - parent_thread_id: ThreadId; - depth: number; - agent_path: AgentPath | null; - agent_nickname: string | null; - agent_role: string | null; - }; - } - | "memory_consolidation" - | { other: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ThreadId.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ThreadId.ts deleted file mode 100644 index bfb3b4b4d76..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ThreadId.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadId = string; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/ThreadMemoryMode.ts b/extensions/codex/src/app-server/protocol-generated/typescript/ThreadMemoryMode.ts deleted file mode 100644 index 74a7e759e73..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/ThreadMemoryMode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadMemoryMode = "enabled" | "disabled"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/Tool.ts b/extensions/codex/src/app-server/protocol-generated/typescript/Tool.ts deleted file mode 100644 index e0e7c0f9811..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/Tool.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "./serde_json/JsonValue.js"; - -/** - * Definition for a tool the client can call. - */ -export type Tool = { - name: string; - title?: string; - description?: string; - inputSchema: JsonValue; - outputSchema?: JsonValue; - annotations?: JsonValue; - icons?: Array; - _meta?: JsonValue; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/Verbosity.ts b/extensions/codex/src/app-server/protocol-generated/typescript/Verbosity.ts deleted file mode 100644 index 8fd97b0b89d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/Verbosity.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Controls output length/detail on GPT-5 models via the Responses API. - * Serialized with lowercase values to match the OpenAI API. - */ -export type Verbosity = "low" | "medium" | "high"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchAction.ts b/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchAction.ts deleted file mode 100644 index 3cae0b56f59..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchAction.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WebSearchAction = - | { type: "search"; query?: string; queries?: Array } - | { type: "open_page"; url?: string } - | { type: "find_in_page"; url?: string; pattern?: string } - | { type: "other" }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchContextSize.ts b/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchContextSize.ts deleted file mode 100644 index d6feedde849..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchContextSize.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WebSearchContextSize = "low" | "medium" | "high"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchLocation.ts b/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchLocation.ts deleted file mode 100644 index 4f257eeb418..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchLocation.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WebSearchLocation = { - country: string | null; - region: string | null; - city: string | null; - timezone: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchMode.ts b/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchMode.ts deleted file mode 100644 index 695c13e3f6f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchMode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WebSearchMode = "disabled" | "cached" | "live"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchToolConfig.ts b/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchToolConfig.ts deleted file mode 100644 index 31d47ae300d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/WebSearchToolConfig.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { WebSearchContextSize } from "./WebSearchContextSize.js"; -import type { WebSearchLocation } from "./WebSearchLocation.js"; - -export type WebSearchToolConfig = { - context_size: WebSearchContextSize | null; - allowed_domains: Array | null; - location: WebSearchLocation | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/index.ts b/extensions/codex/src/app-server/protocol-generated/typescript/index.ts deleted file mode 100644 index 55cb96c4993..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -export type { AbsolutePathBuf } from "./AbsolutePathBuf.js"; -export type { AgentPath } from "./AgentPath.js"; -export type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams.js"; -export type { ApplyPatchApprovalResponse } from "./ApplyPatchApprovalResponse.js"; -export type { AuthMode } from "./AuthMode.js"; -export type { ClientInfo } from "./ClientInfo.js"; -export type { ClientNotification } from "./ClientNotification.js"; -export type { ClientRequest } from "./ClientRequest.js"; -export type { CollaborationMode } from "./CollaborationMode.js"; -export type { ContentItem } from "./ContentItem.js"; -export type { ConversationGitInfo } from "./ConversationGitInfo.js"; -export type { ConversationSummary } from "./ConversationSummary.js"; -export type { ExecCommandApprovalParams } from "./ExecCommandApprovalParams.js"; -export type { ExecCommandApprovalResponse } from "./ExecCommandApprovalResponse.js"; -export type { ExecPolicyAmendment } from "./ExecPolicyAmendment.js"; -export type { FileChange } from "./FileChange.js"; -export type { ForcedLoginMethod } from "./ForcedLoginMethod.js"; -export type { FunctionCallOutputBody } from "./FunctionCallOutputBody.js"; -export type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentItem.js"; -export type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType.js"; -export type { FuzzyFileSearchParams } from "./FuzzyFileSearchParams.js"; -export type { FuzzyFileSearchResponse } from "./FuzzyFileSearchResponse.js"; -export type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult.js"; -export type { FuzzyFileSearchSessionCompletedNotification } from "./FuzzyFileSearchSessionCompletedNotification.js"; -export type { FuzzyFileSearchSessionStartParams } from "./FuzzyFileSearchSessionStartParams.js"; -export type { FuzzyFileSearchSessionStartResponse } from "./FuzzyFileSearchSessionStartResponse.js"; -export type { FuzzyFileSearchSessionStopParams } from "./FuzzyFileSearchSessionStopParams.js"; -export type { FuzzyFileSearchSessionStopResponse } from "./FuzzyFileSearchSessionStopResponse.js"; -export type { FuzzyFileSearchSessionUpdateParams } from "./FuzzyFileSearchSessionUpdateParams.js"; -export type { FuzzyFileSearchSessionUpdateResponse } from "./FuzzyFileSearchSessionUpdateResponse.js"; -export type { FuzzyFileSearchSessionUpdatedNotification } from "./FuzzyFileSearchSessionUpdatedNotification.js"; -export type { GetAuthStatusParams } from "./GetAuthStatusParams.js"; -export type { GetAuthStatusResponse } from "./GetAuthStatusResponse.js"; -export type { GetConversationSummaryParams } from "./GetConversationSummaryParams.js"; -export type { GetConversationSummaryResponse } from "./GetConversationSummaryResponse.js"; -export type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams.js"; -export type { GitDiffToRemoteResponse } from "./GitDiffToRemoteResponse.js"; -export type { GitSha } from "./GitSha.js"; -export type { ImageDetail } from "./ImageDetail.js"; -export type { InitializeCapabilities } from "./InitializeCapabilities.js"; -export type { InitializeParams } from "./InitializeParams.js"; -export type { InitializeResponse } from "./InitializeResponse.js"; -export type { InputModality } from "./InputModality.js"; -export type { InternalSessionSource } from "./InternalSessionSource.js"; -export type { LocalShellAction } from "./LocalShellAction.js"; -export type { LocalShellExecAction } from "./LocalShellExecAction.js"; -export type { LocalShellStatus } from "./LocalShellStatus.js"; -export type { MessagePhase } from "./MessagePhase.js"; -export type { ModeKind } from "./ModeKind.js"; -export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment.js"; -export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction.js"; -export type { ParsedCommand } from "./ParsedCommand.js"; -export type { Personality } from "./Personality.js"; -export type { PlanType } from "./PlanType.js"; -export type { RealtimeConversationVersion } from "./RealtimeConversationVersion.js"; -export type { RealtimeOutputModality } from "./RealtimeOutputModality.js"; -export type { RealtimeVoice } from "./RealtimeVoice.js"; -export type { RealtimeVoicesList } from "./RealtimeVoicesList.js"; -export type { ReasoningEffort } from "./ReasoningEffort.js"; -export type { ReasoningItemContent } from "./ReasoningItemContent.js"; -export type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary.js"; -export type { ReasoningSummary } from "./ReasoningSummary.js"; -export type { RequestId } from "./RequestId.js"; -export type { Resource } from "./Resource.js"; -export type { ResourceContent } from "./ResourceContent.js"; -export type { ResourceTemplate } from "./ResourceTemplate.js"; -export type { ResponseItem } from "./ResponseItem.js"; -export type { ReviewDecision } from "./ReviewDecision.js"; -export type { ServerNotification } from "./ServerNotification.js"; -export type { ServerRequest } from "./ServerRequest.js"; -export type { ServiceTier } from "./ServiceTier.js"; -export type { SessionSource } from "./SessionSource.js"; -export type { Settings } from "./Settings.js"; -export type { SubAgentSource } from "./SubAgentSource.js"; -export type { ThreadId } from "./ThreadId.js"; -export type { ThreadMemoryMode } from "./ThreadMemoryMode.js"; -export type { Tool } from "./Tool.js"; -export type { Verbosity } from "./Verbosity.js"; -export type { WebSearchAction } from "./WebSearchAction.js"; -export type { WebSearchContextSize } from "./WebSearchContextSize.js"; -export type { WebSearchLocation } from "./WebSearchLocation.js"; -export type { WebSearchMode } from "./WebSearchMode.js"; -export type { WebSearchToolConfig } from "./WebSearchToolConfig.js"; -export * as v2 from "./v2/index.js"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/serde_json/JsonValue.ts b/extensions/codex/src/app-server/protocol-generated/typescript/serde_json/JsonValue.ts deleted file mode 100644 index dbf7173b1dc..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/serde_json/JsonValue.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type JsonValue = - | number - | string - | boolean - | Array - | { [key in string]?: JsonValue } - | null; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/Account.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/Account.ts deleted file mode 100644 index ff69d7c4d5f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/Account.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PlanType } from "../PlanType.js"; - -export type Account = - | { type: "apiKey" } - | { type: "chatgpt"; email: string; planType: PlanType } - | { type: "amazonBedrock" }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AccountLoginCompletedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AccountLoginCompletedNotification.ts deleted file mode 100644 index b8960c6666e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AccountLoginCompletedNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AccountLoginCompletedNotification = { - loginId: string | null; - success: boolean; - error: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AccountRateLimitsUpdatedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AccountRateLimitsUpdatedNotification.ts deleted file mode 100644 index 4ae9a60207b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AccountRateLimitsUpdatedNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RateLimitSnapshot } from "./RateLimitSnapshot.js"; - -export type AccountRateLimitsUpdatedNotification = { rateLimits: RateLimitSnapshot }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AccountUpdatedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AccountUpdatedNotification.ts deleted file mode 100644 index 867778fac4c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AccountUpdatedNotification.ts +++ /dev/null @@ -1,7 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AuthMode } from "../AuthMode.js"; -import type { PlanType } from "../PlanType.js"; - -export type AccountUpdatedNotification = { authMode: AuthMode | null; planType: PlanType | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ActivePermissionProfile.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ActivePermissionProfile.ts deleted file mode 100644 index ec5b5af7d93..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ActivePermissionProfile.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ActivePermissionProfileModification } from "./ActivePermissionProfileModification.js"; - -export type ActivePermissionProfile = { - /** - * Identifier from `default_permissions` or the implicit built-in default, - * such as `:workspace` or a user-defined `[permissions.]` profile. - */ - id: string; - /** - * Parent profile identifier once permissions profiles support - * inheritance. This is currently always `null`. - */ - extends: string | null; - /** - * Bounded user-requested modifications applied on top of the named - * profile, if any. - */ - modifications: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ActivePermissionProfileModification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ActivePermissionProfileModification.ts deleted file mode 100644 index 69cf70ce53e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ActivePermissionProfileModification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type ActivePermissionProfileModification = { - type: "additionalWritableRoot"; - path: AbsolutePathBuf; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AddCreditsNudgeCreditType.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AddCreditsNudgeCreditType.ts deleted file mode 100644 index 70498d6a67a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AddCreditsNudgeCreditType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AddCreditsNudgeCreditType = "credits" | "usage_limit"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AddCreditsNudgeEmailStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AddCreditsNudgeEmailStatus.ts deleted file mode 100644 index 2b62da68eaf..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AddCreditsNudgeEmailStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AddCreditsNudgeEmailStatus = "sent" | "cooldown_active"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AdditionalFileSystemPermissions.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AdditionalFileSystemPermissions.ts deleted file mode 100644 index 7be8c2c956e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AdditionalFileSystemPermissions.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry.js"; - -export type AdditionalFileSystemPermissions = { - /** - * This will be removed in favor of `entries`. - */ - read: Array | null; - /** - * This will be removed in favor of `entries`. - */ - write: Array | null; - globScanMaxDepth?: number; - entries?: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AdditionalNetworkPermissions.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AdditionalNetworkPermissions.ts deleted file mode 100644 index 9349aa6628c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AdditionalNetworkPermissions.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AdditionalNetworkPermissions = { enabled: boolean | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AdditionalPermissionProfile.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AdditionalPermissionProfile.ts deleted file mode 100644 index 75c176bec81..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AdditionalPermissionProfile.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions.js"; -import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions.js"; - -export type AdditionalPermissionProfile = { - /** - * Partial overlay used for per-command permission requests. - */ - network: AdditionalNetworkPermissions | null; - fileSystem: AdditionalFileSystemPermissions | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AgentMessageDeltaNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AgentMessageDeltaNotification.ts deleted file mode 100644 index 1beb2e9026e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AgentMessageDeltaNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AgentMessageDeltaNotification = { - threadId: string; - turnId: string; - itemId: string; - delta: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AnalyticsConfig.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AnalyticsConfig.ts deleted file mode 100644 index b8203b0d0b5..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AnalyticsConfig.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; - -export type AnalyticsConfig = { enabled: boolean | null } & { - [key in string]?: - | number - | string - | boolean - | Array - | { [key in string]?: JsonValue } - | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppBranding.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppBranding.ts deleted file mode 100644 index 8a2999eaa5b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppBranding.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - app metadata returned by app-list APIs. - */ -export type AppBranding = { - category: string | null; - developer: string | null; - website: string | null; - privacyPolicy: string | null; - termsOfService: string | null; - isDiscoverableApp: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppInfo.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppInfo.ts deleted file mode 100644 index 0f444d7cf5b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppInfo.ts +++ /dev/null @@ -1,32 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppBranding } from "./AppBranding.js"; -import type { AppMetadata } from "./AppMetadata.js"; - -/** - * EXPERIMENTAL - app metadata returned by app-list APIs. - */ -export type AppInfo = { - id: string; - name: string; - description: string | null; - logoUrl: string | null; - logoUrlDark: string | null; - distributionChannel: string | null; - branding: AppBranding | null; - appMetadata: AppMetadata | null; - labels: { [key in string]?: string } | null; - installUrl: string | null; - isAccessible: boolean; - /** - * Whether this app is enabled in config.toml. - * Example: - * ```toml - * [apps.bad_app] - * enabled = false - * ``` - */ - isEnabled: boolean; - pluginDisplayNames: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppListUpdatedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppListUpdatedNotification.ts deleted file mode 100644 index 285273d98f5..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppListUpdatedNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppInfo } from "./AppInfo.js"; - -/** - * EXPERIMENTAL - notification emitted when the app list changes. - */ -export type AppListUpdatedNotification = { data: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppMetadata.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppMetadata.ts deleted file mode 100644 index 5aee5aaf427..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppMetadata.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppReview } from "./AppReview.js"; -import type { AppScreenshot } from "./AppScreenshot.js"; - -export type AppMetadata = { - review: AppReview | null; - categories: Array | null; - subCategories: Array | null; - seoDescription: string | null; - screenshots: Array | null; - developer: string | null; - version: string | null; - versionId: string | null; - versionNotes: string | null; - firstPartyType: string | null; - firstPartyRequiresInstall: boolean | null; - showInComposerWhenUnlinked: boolean | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppReview.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppReview.ts deleted file mode 100644 index 96e14b7e9c9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppReview.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AppReview = { status: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppScreenshot.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppScreenshot.ts deleted file mode 100644 index f538ccc9bd8..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppScreenshot.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AppScreenshot = { url: string | null; fileId: string | null; userPrompt: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppSummary.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppSummary.ts deleted file mode 100644 index b3160665060..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppSummary.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - app metadata summary for plugin responses. - */ -export type AppSummary = { - id: string; - name: string; - description: string | null; - installUrl: string | null; - needsAuth: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppToolApproval.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppToolApproval.ts deleted file mode 100644 index e92cd8e28b2..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppToolApproval.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AppToolApproval = "auto" | "prompt" | "approve"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppToolsConfig.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppToolsConfig.ts deleted file mode 100644 index af47194bc86..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppToolsConfig.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppToolApproval } from "./AppToolApproval.js"; - -export type AppToolsConfig = { - [key in string]?: { enabled: boolean | null; approval_mode: AppToolApproval | null }; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ApprovalsReviewer.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ApprovalsReviewer.ts deleted file mode 100644 index 1d932946cc5..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ApprovalsReviewer.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Configures who approval requests are routed to for review. Examples - * include sandbox escapes, blocked network access, MCP approval prompts, and - * ARC escalations. Defaults to `user`. `auto_review` uses a carefully - * prompted subagent to gather relevant context and apply a risk-based - * decision framework before approving or denying the request. - */ -export type ApprovalsReviewer = "user" | "auto_review" | "guardian_subagent"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsConfig.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsConfig.ts deleted file mode 100644 index 1bd048c7ddb..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsConfig.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -import type { AppsDefaultConfig } from "./AppsDefaultConfig.js"; -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppToolApproval } from "./AppToolApproval.js"; -import type { AppToolsConfig } from "./AppToolsConfig.js"; - -export type AppsConfig = { _default: AppsDefaultConfig | null } & { - [key in string]?: { - enabled: boolean; - destructive_enabled: boolean | null; - open_world_enabled: boolean | null; - default_tools_approval_mode: AppToolApproval | null; - default_tools_enabled: boolean | null; - tools: AppToolsConfig | null; - }; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsDefaultConfig.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsDefaultConfig.ts deleted file mode 100644 index 1fd8e81a0dc..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsDefaultConfig.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AppsDefaultConfig = { - enabled: boolean; - destructive_enabled: boolean; - open_world_enabled: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsListParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsListParams.ts deleted file mode 100644 index e85212c2827..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsListParams.ts +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - list available apps/connectors. - */ -export type AppsListParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to a reasonable server-side value. - */ - limit?: number | null; - /** - * Optional thread id used to evaluate app feature gating from that thread's config. - */ - threadId?: string | null; - /** - * When true, bypass app caches and fetch the latest data from sources. - */ - forceRefetch?: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsListResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsListResponse.ts deleted file mode 100644 index cd4cc6dc3e8..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AppsListResponse.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppInfo } from "./AppInfo.js"; - -/** - * EXPERIMENTAL - app list response. - */ -export type AppsListResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * If None, there are no more items to return. - */ - nextCursor: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AskForApproval.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AskForApproval.ts deleted file mode 100644 index 8c134c91e24..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AskForApproval.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AskForApproval = - | "untrusted" - | "on-failure" - | "on-request" - | { - granular: { - sandbox_approval: boolean; - rules: boolean; - skill_approval: boolean; - request_permissions: boolean; - mcp_elicitations: boolean; - }; - } - | "never"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AutoReviewDecisionSource.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/AutoReviewDecisionSource.ts deleted file mode 100644 index 8806981237f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/AutoReviewDecisionSource.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * [UNSTABLE] Source that produced a terminal approval auto-review decision. - */ -export type AutoReviewDecisionSource = "agent"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ByteRange.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ByteRange.ts deleted file mode 100644 index fae7a1ca5f7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ByteRange.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ByteRange = { start: number; end: number }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CancelLoginAccountParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CancelLoginAccountParams.ts deleted file mode 100644 index 8096042be44..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CancelLoginAccountParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CancelLoginAccountParams = { loginId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CancelLoginAccountResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CancelLoginAccountResponse.ts deleted file mode 100644 index 3a8b587da25..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CancelLoginAccountResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CancelLoginAccountStatus } from "./CancelLoginAccountStatus.js"; - -export type CancelLoginAccountResponse = { status: CancelLoginAccountStatus }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CancelLoginAccountStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CancelLoginAccountStatus.ts deleted file mode 100644 index bd851c6a39c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CancelLoginAccountStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CancelLoginAccountStatus = "canceled" | "notFound"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ChatgptAuthTokensRefreshParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ChatgptAuthTokensRefreshParams.ts deleted file mode 100644 index 548d26f9775..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ChatgptAuthTokensRefreshParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ChatgptAuthTokensRefreshReason } from "./ChatgptAuthTokensRefreshReason.js"; - -export type ChatgptAuthTokensRefreshParams = { - reason: ChatgptAuthTokensRefreshReason; - /** - * Workspace/account identifier that Codex was previously using. - * - * Clients that manage multiple accounts/workspaces can use this as a hint - * to refresh the token for the correct workspace. - * - * This may be `null` when the prior auth state did not include a workspace - * identifier (`chatgpt_account_id`). - */ - previousAccountId?: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ChatgptAuthTokensRefreshReason.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ChatgptAuthTokensRefreshReason.ts deleted file mode 100644 index ac4006ba6a9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ChatgptAuthTokensRefreshReason.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ChatgptAuthTokensRefreshReason = "unauthorized"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ChatgptAuthTokensRefreshResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ChatgptAuthTokensRefreshResponse.ts deleted file mode 100644 index 060598850e3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ChatgptAuthTokensRefreshResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ChatgptAuthTokensRefreshResponse = { - accessToken: string; - chatgptAccountId: string; - chatgptPlanType: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CodexErrorInfo.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CodexErrorInfo.ts deleted file mode 100644 index be64bb95465..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CodexErrorInfo.ts +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NonSteerableTurnKind } from "./NonSteerableTurnKind.js"; - -/** - * This translation layer make sure that we expose codex error code in camel case. - * - * When an upstream HTTP status is available (for example, from the Responses API or a provider), - * it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant. - */ -export type CodexErrorInfo = - | "contextWindowExceeded" - | "usageLimitExceeded" - | "serverOverloaded" - | "cyberPolicy" - | { httpConnectionFailed: { httpStatusCode: number | null } } - | { responseStreamConnectionFailed: { httpStatusCode: number | null } } - | "internalServerError" - | "unauthorized" - | "badRequest" - | "threadRollbackFailed" - | "sandboxError" - | { responseStreamDisconnected: { httpStatusCode: number | null } } - | { responseTooManyFailedAttempts: { httpStatusCode: number | null } } - | { activeTurnNotSteerable: { turnKind: NonSteerableTurnKind } } - | "other"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentState.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentState.ts deleted file mode 100644 index 0a3fb2c0490..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentState.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CollabAgentStatus } from "./CollabAgentStatus.js"; - -export type CollabAgentState = { status: CollabAgentStatus; message: string | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentStatus.ts deleted file mode 100644 index 5bff8e16bba..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentStatus.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CollabAgentStatus = - | "pendingInit" - | "running" - | "interrupted" - | "completed" - | "errored" - | "shutdown" - | "notFound"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentTool.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentTool.ts deleted file mode 100644 index 3637853a389..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentTool.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CollabAgentTool = "spawnAgent" | "sendInput" | "resumeAgent" | "wait" | "closeAgent"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentToolCallStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentToolCallStatus.ts deleted file mode 100644 index f21f7bd5d5f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollabAgentToolCallStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CollabAgentToolCallStatus = "inProgress" | "completed" | "failed"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollaborationModeListParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollaborationModeListParams.ts deleted file mode 100644 index 37e8f792d95..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollaborationModeListParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - list collaboration mode presets. - */ -export type CollaborationModeListParams = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollaborationModeListResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollaborationModeListResponse.ts deleted file mode 100644 index 479f76a8c20..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollaborationModeListResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CollaborationModeMask } from "./CollaborationModeMask.js"; - -/** - * EXPERIMENTAL - collaboration mode presets response. - */ -export type CollaborationModeListResponse = { data: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollaborationModeMask.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollaborationModeMask.ts deleted file mode 100644 index 3482bcc3af7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CollaborationModeMask.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ModeKind } from "../ModeKind.js"; -import type { ReasoningEffort } from "../ReasoningEffort.js"; - -/** - * EXPERIMENTAL - collaboration mode preset metadata for clients. - */ -export type CollaborationModeMask = { - name: string; - mode: ModeKind | null; - model: string | null; - reasoning_effort: ReasoningEffort | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandAction.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandAction.ts deleted file mode 100644 index aa2c5cbae31..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandAction.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type CommandAction = - | { type: "read"; command: string; name: string; path: AbsolutePathBuf } - | { type: "listFiles"; command: string; path: string | null } - | { type: "search"; command: string; query: string | null; path: string | null } - | { type: "unknown"; command: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecOutputDeltaNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecOutputDeltaNotification.ts deleted file mode 100644 index 669860d57a7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecOutputDeltaNotification.ts +++ /dev/null @@ -1,31 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CommandExecOutputStream } from "./CommandExecOutputStream.js"; - -/** - * Base64-encoded output chunk emitted for a streaming `command/exec` request. - * - * These notifications are connection-scoped. If the originating connection - * closes, the server terminates the process. - */ -export type CommandExecOutputDeltaNotification = { - /** - * Client-supplied, connection-scoped `processId` from the original - * `command/exec` request. - */ - processId: string; - /** - * Output stream for this chunk. - */ - stream: CommandExecOutputStream; - /** - * Base64-encoded output bytes. - */ - deltaBase64: string; - /** - * `true` on the final streamed chunk for a stream when `outputBytesCap` - * truncated later output on that stream. - */ - capReached: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecOutputStream.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecOutputStream.ts deleted file mode 100644 index a8c5b66711d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecOutputStream.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Stream label for `command/exec/outputDelta` notifications. - */ -export type CommandExecOutputStream = "stdout" | "stderr"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecParams.ts deleted file mode 100644 index ab97afd89f8..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecParams.ts +++ /dev/null @@ -1,107 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CommandExecTerminalSize } from "./CommandExecTerminalSize.js"; -import type { PermissionProfile } from "./PermissionProfile.js"; -import type { SandboxPolicy } from "./SandboxPolicy.js"; - -/** - * Run a standalone command (argv vector) in the server sandbox without - * creating a thread or turn. - * - * The final `command/exec` response is deferred until the process exits and is - * sent only after all `command/exec/outputDelta` notifications for that - * connection have been emitted. - */ -export type CommandExecParams = { - /** - * Command argv vector. Empty arrays are rejected. - */ - command: Array; - /** - * Optional client-supplied, connection-scoped process id. - * - * Required for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up - * `command/exec/write`, `command/exec/resize`, and - * `command/exec/terminate` calls. When omitted, buffered execution gets an - * internal id that is not exposed to the client. - */ - processId?: string | null; - /** - * Enable PTY mode. - * - * This implies `streamStdin` and `streamStdoutStderr`. - */ - tty?: boolean; - /** - * Allow follow-up `command/exec/write` requests to write stdin bytes. - * - * Requires a client-supplied `processId`. - */ - streamStdin?: boolean; - /** - * Stream stdout/stderr via `command/exec/outputDelta` notifications. - * - * Streamed bytes are not duplicated into the final response and require a - * client-supplied `processId`. - */ - streamStdoutStderr?: boolean; - /** - * Optional per-stream stdout/stderr capture cap in bytes. - * - * When omitted, the server default applies. Cannot be combined with - * `disableOutputCap`. - */ - outputBytesCap?: number | null; - /** - * Disable stdout/stderr capture truncation for this request. - * - * Cannot be combined with `outputBytesCap`. - */ - disableOutputCap?: boolean; - /** - * Disable the timeout entirely for this request. - * - * Cannot be combined with `timeoutMs`. - */ - disableTimeout?: boolean; - /** - * Optional timeout in milliseconds. - * - * When omitted, the server default applies. Cannot be combined with - * `disableTimeout`. - */ - timeoutMs?: number | null; - /** - * Optional working directory. Defaults to the server cwd. - */ - cwd?: string | null; - /** - * Optional environment overrides merged into the server-computed - * environment. - * - * Matching names override inherited values. Set a key to `null` to unset - * an inherited variable. - */ - env?: { [key in string]?: string | null } | null; - /** - * Optional initial PTY size in character cells. Only valid when `tty` is - * true. - */ - size?: CommandExecTerminalSize | null; - /** - * Optional sandbox policy for this command. - * - * Uses the same shape as thread/turn execution sandbox configuration and - * defaults to the user's configured policy when omitted. Cannot be - * combined with `permissionProfile`. - */ - sandboxPolicy?: SandboxPolicy | null; - /** - * Optional full permissions profile for this command. - * - * Defaults to the user's configured permissions when omitted. Cannot be - * combined with `sandboxPolicy`. - */ - permissionProfile?: PermissionProfile | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecResizeParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecResizeParams.ts deleted file mode 100644 index d2a2e4b5873..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecResizeParams.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CommandExecTerminalSize } from "./CommandExecTerminalSize.js"; - -/** - * Resize a running PTY-backed `command/exec` session. - */ -export type CommandExecResizeParams = { - /** - * Client-supplied, connection-scoped `processId` from the original - * `command/exec` request. - */ - processId: string; - /** - * New PTY size in character cells. - */ - size: CommandExecTerminalSize; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecResizeResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecResizeResponse.ts deleted file mode 100644 index 7b7f2be7006..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecResizeResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Empty success response for `command/exec/resize`. - */ -export type CommandExecResizeResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecResponse.ts deleted file mode 100644 index caa53bd18e1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecResponse.ts +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Final buffered result for `command/exec`. - */ -export type CommandExecResponse = { - /** - * Process exit code. - */ - exitCode: number; - /** - * Buffered stdout capture. - * - * Empty when stdout was streamed via `command/exec/outputDelta`. - */ - stdout: string; - /** - * Buffered stderr capture. - * - * Empty when stderr was streamed via `command/exec/outputDelta`. - */ - stderr: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecTerminalSize.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecTerminalSize.ts deleted file mode 100644 index 5d409e076d5..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecTerminalSize.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * PTY size in character cells for `command/exec` PTY sessions. - */ -export type CommandExecTerminalSize = { - /** - * Terminal height in character cells. - */ - rows: number; - /** - * Terminal width in character cells. - */ - cols: number; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecTerminateParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecTerminateParams.ts deleted file mode 100644 index 43cbaab71c5..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecTerminateParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Terminate a running `command/exec` session. - */ -export type CommandExecTerminateParams = { - /** - * Client-supplied, connection-scoped `processId` from the original - * `command/exec` request. - */ - processId: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecTerminateResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecTerminateResponse.ts deleted file mode 100644 index dc6371fbdd6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecTerminateResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Empty success response for `command/exec/terminate`. - */ -export type CommandExecTerminateResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecWriteParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecWriteParams.ts deleted file mode 100644 index 960fadcc872..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecWriteParams.ts +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Write stdin bytes to a running `command/exec` session, close stdin, or - * both. - */ -export type CommandExecWriteParams = { - /** - * Client-supplied, connection-scoped `processId` from the original - * `command/exec` request. - */ - processId: string; - /** - * Optional base64-encoded stdin bytes to write. - */ - deltaBase64?: string | null; - /** - * Close stdin after writing `deltaBase64`, if present. - */ - closeStdin?: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecWriteResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecWriteResponse.ts deleted file mode 100644 index 6dbbddf4dd2..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecWriteResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Empty success response for `command/exec/write`. - */ -export type CommandExecWriteResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionApprovalDecision.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionApprovalDecision.ts deleted file mode 100644 index d6404e44b03..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionApprovalDecision.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExecPolicyAmendment } from "./ExecPolicyAmendment.js"; -import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment.js"; - -export type CommandExecutionApprovalDecision = - | "accept" - | "acceptForSession" - | { acceptWithExecpolicyAmendment: { execpolicy_amendment: ExecPolicyAmendment } } - | { applyNetworkPolicyAmendment: { network_policy_amendment: NetworkPolicyAmendment } } - | "decline" - | "cancel"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionOutputDeltaNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionOutputDeltaNotification.ts deleted file mode 100644 index c476d448abe..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionOutputDeltaNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CommandExecutionOutputDeltaNotification = { - threadId: string; - turnId: string; - itemId: string; - delta: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionRequestApprovalParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionRequestApprovalParams.ts deleted file mode 100644 index 2b1d21b21d3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionRequestApprovalParams.ts +++ /dev/null @@ -1,62 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile.js"; -import type { CommandAction } from "./CommandAction.js"; -import type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision.js"; -import type { ExecPolicyAmendment } from "./ExecPolicyAmendment.js"; -import type { NetworkApprovalContext } from "./NetworkApprovalContext.js"; -import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment.js"; - -export type CommandExecutionRequestApprovalParams = { - threadId: string; - turnId: string; - itemId: string; - /** - * Unique identifier for this specific approval callback. - * - * For regular shell/unified_exec approvals, this is null. - * - * For zsh-exec-bridge subcommand approvals, multiple callbacks can belong to - * one parent `itemId`, so `approvalId` is a distinct opaque callback id - * (a UUID) used to disambiguate routing. - */ - approvalId?: string | null; - /** - * Optional explanatory reason (e.g. request for network access). - */ - reason?: string | null; - /** - * Optional context for a managed-network approval prompt. - */ - networkApprovalContext?: NetworkApprovalContext | null; - /** - * The command to be executed. - */ - command?: string | null; - /** - * The command's working directory. - */ - cwd?: AbsolutePathBuf | null; - /** - * Best-effort parsed command actions for friendly display. - */ - commandActions?: Array | null; - /** - * Optional additional permissions requested for this command. - */ - additionalPermissions?: AdditionalPermissionProfile | null; - /** - * Optional proposed execpolicy amendment to allow similar commands without prompting. - */ - proposedExecpolicyAmendment?: ExecPolicyAmendment | null; - /** - * Optional proposed network policy amendments (allow/deny host) for future requests. - */ - proposedNetworkPolicyAmendments?: Array | null; - /** - * Ordered list of decisions the client may present for this prompt. - */ - availableDecisions?: Array | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionRequestApprovalResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionRequestApprovalResponse.ts deleted file mode 100644 index 1a02bbf6a60..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionRequestApprovalResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision.js"; - -export type CommandExecutionRequestApprovalResponse = { - decision: CommandExecutionApprovalDecision; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionSource.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionSource.ts deleted file mode 100644 index 790b65b7ed1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionSource.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CommandExecutionSource = - | "agent" - | "userShell" - | "unifiedExecStartup" - | "unifiedExecInteraction"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionStatus.ts deleted file mode 100644 index c58b3cc7faa..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandExecutionStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CommandExecutionStatus = "inProgress" | "completed" | "failed" | "declined"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandMigration.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandMigration.ts deleted file mode 100644 index 8f2bee05871..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CommandMigration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CommandMigration = { name: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/Config.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/Config.ts deleted file mode 100644 index fff341ae2f9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/Config.ts +++ /dev/null @@ -1,57 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ForcedLoginMethod } from "../ForcedLoginMethod.js"; -import type { ReasoningEffort } from "../ReasoningEffort.js"; -import type { ReasoningSummary } from "../ReasoningSummary.js"; -import type { JsonValue } from "../serde_json/JsonValue.js"; -import type { ServiceTier } from "../ServiceTier.js"; -import type { Verbosity } from "../Verbosity.js"; -import type { WebSearchMode } from "../WebSearchMode.js"; -import type { AnalyticsConfig } from "./AnalyticsConfig.js"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer.js"; -import type { AppsConfig } from "./AppsConfig.js"; -import type { AskForApproval } from "./AskForApproval.js"; -import type { ProfileV2 } from "./ProfileV2.js"; -import type { SandboxMode } from "./SandboxMode.js"; -import type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite.js"; -import type { ToolsV2 } from "./ToolsV2.js"; - -export type Config = { - model: string | null; - review_model: string | null; - model_context_window: bigint | null; - model_auto_compact_token_limit: bigint | null; - model_provider: string | null; - approval_policy: AskForApproval | null; - /** - * [UNSTABLE] Optional default for where approval requests are routed for - * review. - */ - approvals_reviewer: ApprovalsReviewer | null; - sandbox_mode: SandboxMode | null; - sandbox_workspace_write: SandboxWorkspaceWrite | null; - forced_chatgpt_workspace_id: string | null; - forced_login_method: ForcedLoginMethod | null; - web_search: WebSearchMode | null; - tools: ToolsV2 | null; - profile: string | null; - profiles: { [key in string]?: ProfileV2 }; - instructions: string | null; - developer_instructions: string | null; - compact_prompt: string | null; - model_reasoning_effort: ReasoningEffort | null; - model_reasoning_summary: ReasoningSummary | null; - model_verbosity: Verbosity | null; - service_tier: ServiceTier | null; - analytics: AnalyticsConfig | null; - apps: AppsConfig | null; -} & { - [key in string]?: - | number - | string - | boolean - | Array - | { [key in string]?: JsonValue } - | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigBatchWriteParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigBatchWriteParams.ts deleted file mode 100644 index c763ac23a2f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigBatchWriteParams.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfigEdit } from "./ConfigEdit.js"; - -export type ConfigBatchWriteParams = { - edits: Array; - /** - * Path to the config file to write; defaults to the user's `config.toml` when omitted. - */ - filePath?: string | null; - expectedVersion?: string | null; - /** - * When true, hot-reload the updated user config into all loaded threads after writing. - */ - reloadUserConfig?: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigEdit.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigEdit.ts deleted file mode 100644 index 92a26a15271..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigEdit.ts +++ /dev/null @@ -1,7 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; -import type { MergeStrategy } from "./MergeStrategy.js"; - -export type ConfigEdit = { keyPath: string; value: JsonValue; mergeStrategy: MergeStrategy }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigLayer.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigLayer.ts deleted file mode 100644 index f48ad34a055..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigLayer.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; -import type { ConfigLayerSource } from "./ConfigLayerSource.js"; - -export type ConfigLayer = { - name: ConfigLayerSource; - version: string; - config: JsonValue; - disabledReason: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigLayerMetadata.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigLayerMetadata.ts deleted file mode 100644 index 26d4e976305..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigLayerMetadata.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfigLayerSource } from "./ConfigLayerSource.js"; - -export type ConfigLayerMetadata = { name: ConfigLayerSource; version: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigLayerSource.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigLayerSource.ts deleted file mode 100644 index 2941dfe7e41..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigLayerSource.ts +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type ConfigLayerSource = - | { type: "mdm"; domain: string; key: string } - | { - type: "system"; - /** - * This is the path to the system config.toml file, though it is not - * guaranteed to exist. - */ - file: AbsolutePathBuf; - } - | { - type: "user"; - /** - * This is the path to the user's config.toml file, though it is not - * guaranteed to exist. - */ - file: AbsolutePathBuf; - } - | { type: "project"; dotCodexFolder: AbsolutePathBuf } - | { type: "sessionFlags" } - | { type: "legacyManagedConfigTomlFromFile"; file: AbsolutePathBuf } - | { type: "legacyManagedConfigTomlFromMdm" }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigReadParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigReadParams.ts deleted file mode 100644 index 7e14e83f047..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigReadParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ConfigReadParams = { - includeLayers: boolean; - /** - * Optional working directory to resolve project config layers. If specified, - * return the effective config as seen from that directory (i.e., including any - * project layers between `cwd` and the project/repo root). - */ - cwd?: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigReadResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigReadResponse.ts deleted file mode 100644 index edf3436d977..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigReadResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Config } from "./Config.js"; -import type { ConfigLayer } from "./ConfigLayer.js"; -import type { ConfigLayerMetadata } from "./ConfigLayerMetadata.js"; - -export type ConfigReadResponse = { - config: Config; - origins: { [key in string]?: ConfigLayerMetadata }; - layers: Array | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigRequirements.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigRequirements.ts deleted file mode 100644 index 7b977d9bea9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigRequirements.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { WebSearchMode } from "../WebSearchMode.js"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer.js"; -import type { AskForApproval } from "./AskForApproval.js"; -import type { ManagedHooksRequirements } from "./ManagedHooksRequirements.js"; -import type { NetworkRequirements } from "./NetworkRequirements.js"; -import type { ResidencyRequirement } from "./ResidencyRequirement.js"; -import type { SandboxMode } from "./SandboxMode.js"; - -export type ConfigRequirements = { - allowedApprovalPolicies: Array | null; - allowedApprovalsReviewers: Array | null; - allowedSandboxModes: Array | null; - allowedWebSearchModes: Array | null; - featureRequirements: { [key in string]?: boolean } | null; - hooks: ManagedHooksRequirements | null; - enforceResidency: ResidencyRequirement | null; - network: NetworkRequirements | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigRequirementsReadResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigRequirementsReadResponse.ts deleted file mode 100644 index e030f512cd9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigRequirementsReadResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfigRequirements } from "./ConfigRequirements.js"; - -export type ConfigRequirementsReadResponse = { - /** - * Null if no requirements are configured (e.g. no requirements.toml/MDM entries). - */ - requirements: ConfigRequirements | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigValueWriteParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigValueWriteParams.ts deleted file mode 100644 index 37b2f44ffe2..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigValueWriteParams.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; -import type { MergeStrategy } from "./MergeStrategy.js"; - -export type ConfigValueWriteParams = { - keyPath: string; - value: JsonValue; - mergeStrategy: MergeStrategy; - /** - * Path to the config file to write; defaults to the user's `config.toml` when omitted. - */ - filePath?: string | null; - expectedVersion?: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigWarningNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigWarningNotification.ts deleted file mode 100644 index f6be1f97f6b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigWarningNotification.ts +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TextRange } from "./TextRange.js"; - -export type ConfigWarningNotification = { - /** - * Concise summary of the warning. - */ - summary: string; - /** - * Optional extra guidance or error details. - */ - details: string | null; - /** - * Optional path to the config file that triggered the warning. - */ - path?: string; - /** - * Optional range for the error location inside the config file. - */ - range?: TextRange; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigWriteResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigWriteResponse.ts deleted file mode 100644 index 43560042d03..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfigWriteResponse.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { OverriddenMetadata } from "./OverriddenMetadata.js"; -import type { WriteStatus } from "./WriteStatus.js"; - -export type ConfigWriteResponse = { - status: WriteStatus; - version: string; - /** - * Canonical path to the config file that was written. - */ - filePath: AbsolutePathBuf; - overriddenMetadata: OverriddenMetadata | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfiguredHookHandler.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfiguredHookHandler.ts deleted file mode 100644 index da81b015d96..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfiguredHookHandler.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ConfiguredHookHandler = - | { - type: "command"; - command: string; - timeoutSec: bigint | null; - async: boolean; - statusMessage: string | null; - } - | { type: "prompt" } - | { type: "agent" }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfiguredHookMatcherGroup.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfiguredHookMatcherGroup.ts deleted file mode 100644 index ee08ee7b1e0..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ConfiguredHookMatcherGroup.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfiguredHookHandler } from "./ConfiguredHookHandler.js"; - -export type ConfiguredHookMatcherGroup = { - matcher: string | null; - hooks: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ContextCompactedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ContextCompactedNotification.ts deleted file mode 100644 index bb6825365bb..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ContextCompactedNotification.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Deprecated: Use `ContextCompaction` item type instead. - */ -export type ContextCompactedNotification = { threadId: string; turnId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CreditsSnapshot.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/CreditsSnapshot.ts deleted file mode 100644 index dd5e746f6da..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/CreditsSnapshot.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CreditsSnapshot = { hasCredits: boolean; unlimited: boolean; balance: string | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeprecationNoticeNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeprecationNoticeNotification.ts deleted file mode 100644 index 54f3d085c08..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeprecationNoticeNotification.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type DeprecationNoticeNotification = { - /** - * Concise summary of what is deprecated. - */ - summary: string; - /** - * Optional extra guidance, such as migration steps or rationale. - */ - details: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyAlgorithm.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyAlgorithm.ts deleted file mode 100644 index 6809c41eb54..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyAlgorithm.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Device-key algorithm reported at enrollment and signing boundaries. - */ -export type DeviceKeyAlgorithm = "ecdsa_p256_sha256"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyCreateParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyCreateParams.ts deleted file mode 100644 index 485b6a7c9e3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyCreateParams.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DeviceKeyProtectionPolicy } from "./DeviceKeyProtectionPolicy.js"; - -/** - * Create a controller-local device key with a random key id. - */ -export type DeviceKeyCreateParams = { - /** - * Defaults to `hardware_only` when omitted. - */ - protectionPolicy?: DeviceKeyProtectionPolicy | null; - accountUserId: string; - clientId: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyCreateResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyCreateResponse.ts deleted file mode 100644 index a7b8ebcfd9a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyCreateResponse.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DeviceKeyAlgorithm } from "./DeviceKeyAlgorithm.js"; -import type { DeviceKeyProtectionClass } from "./DeviceKeyProtectionClass.js"; - -/** - * Device-key metadata and public key returned by create/public APIs. - */ -export type DeviceKeyCreateResponse = { - keyId: string; - /** - * SubjectPublicKeyInfo DER encoded as base64. - */ - publicKeySpkiDerBase64: string; - algorithm: DeviceKeyAlgorithm; - protectionClass: DeviceKeyProtectionClass; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyProtectionClass.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyProtectionClass.ts deleted file mode 100644 index fd3471b71c6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyProtectionClass.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Platform protection class for a controller-local device key. - */ -export type DeviceKeyProtectionClass = - | "hardware_secure_enclave" - | "hardware_tpm" - | "os_protected_nonextractable"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyProtectionPolicy.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyProtectionPolicy.ts deleted file mode 100644 index 66fceafb514..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyProtectionPolicy.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Protection policy for creating or loading a controller-local device key. - */ -export type DeviceKeyProtectionPolicy = "hardware_only" | "allow_os_protected_nonextractable"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyPublicParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyPublicParams.ts deleted file mode 100644 index 06442578a80..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyPublicParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Fetch a controller-local device key public key by id. - */ -export type DeviceKeyPublicParams = { keyId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyPublicResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyPublicResponse.ts deleted file mode 100644 index 46176860cc1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeyPublicResponse.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DeviceKeyAlgorithm } from "./DeviceKeyAlgorithm.js"; -import type { DeviceKeyProtectionClass } from "./DeviceKeyProtectionClass.js"; - -/** - * Device-key public metadata returned by `device/key/public`. - */ -export type DeviceKeyPublicResponse = { - keyId: string; - /** - * SubjectPublicKeyInfo DER encoded as base64. - */ - publicKeySpkiDerBase64: string; - algorithm: DeviceKeyAlgorithm; - protectionClass: DeviceKeyProtectionClass; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeySignParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeySignParams.ts deleted file mode 100644 index bbb37c9a2af..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeySignParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DeviceKeySignPayload } from "./DeviceKeySignPayload.js"; - -/** - * Sign an accepted structured payload with a controller-local device key. - */ -export type DeviceKeySignParams = { keyId: string; payload: DeviceKeySignPayload }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeySignPayload.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeySignPayload.ts deleted file mode 100644 index 269026544ff..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeySignPayload.ts +++ /dev/null @@ -1,68 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RemoteControlClientConnectionAudience } from "./RemoteControlClientConnectionAudience.js"; -import type { RemoteControlClientEnrollmentAudience } from "./RemoteControlClientEnrollmentAudience.js"; - -/** - * Structured payloads accepted by `device/key/sign`. - */ -export type DeviceKeySignPayload = - | { - type: "remoteControlClientConnection"; - nonce: string; - audience: RemoteControlClientConnectionAudience; - /** - * Backend-issued websocket session id that this proof authorizes. - */ - sessionId: string; - /** - * Origin of the backend endpoint that issued the challenge and will verify this proof. - */ - targetOrigin: string; - /** - * Websocket route path that this proof authorizes. - */ - targetPath: string; - accountUserId: string; - clientId: string; - /** - * Remote-control token expiration as Unix seconds. - */ - tokenExpiresAt: number; - /** - * SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url. - */ - tokenSha256Base64url: string; - /** - * Must contain exactly `remote_control_controller_websocket`. - */ - scopes: Array; - } - | { - type: "remoteControlClientEnrollment"; - nonce: string; - audience: RemoteControlClientEnrollmentAudience; - /** - * Backend-issued enrollment challenge id that this proof authorizes. - */ - challengeId: string; - /** - * Origin of the backend endpoint that issued the challenge and will verify this proof. - */ - targetOrigin: string; - /** - * HTTP route path that this proof authorizes. - */ - targetPath: string; - accountUserId: string; - clientId: string; - /** - * SHA-256 of the requested device identity operation, encoded as unpadded base64url. - */ - deviceIdentitySha256Base64url: string; - /** - * Enrollment challenge expiration as Unix seconds. - */ - challengeExpiresAt: number; - }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeySignResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeySignResponse.ts deleted file mode 100644 index b3882f463be..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DeviceKeySignResponse.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DeviceKeyAlgorithm } from "./DeviceKeyAlgorithm.js"; - -/** - * ASN.1 DER signature returned by `device/key/sign`. - */ -export type DeviceKeySignResponse = { - /** - * ECDSA signature DER encoded as base64. - */ - signatureDerBase64: string; - /** - * Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte - * string directly and must not reserialize `payload`. - */ - signedPayloadBase64: string; - algorithm: DeviceKeyAlgorithm; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallOutputContentItem.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallOutputContentItem.ts deleted file mode 100644 index c2fb2785582..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallOutputContentItem.ts +++ /dev/null @@ -1,7 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type DynamicToolCallOutputContentItem = - | { type: "inputText"; text: string } - | { type: "inputImage"; imageUrl: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallParams.ts deleted file mode 100644 index d35926a4f04..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; - -export type DynamicToolCallParams = { - threadId: string; - turnId: string; - callId: string; - namespace: string | null; - tool: string; - arguments: JsonValue; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallResponse.ts deleted file mode 100644 index 5e1e84b0956..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem.js"; - -export type DynamicToolCallResponse = { - contentItems: Array; - success: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallStatus.ts deleted file mode 100644 index 04f44ec0a8b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolCallStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type DynamicToolCallStatus = "inProgress" | "completed" | "failed"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolSpec.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolSpec.ts deleted file mode 100644 index 5f9176ac05d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/DynamicToolSpec.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; - -export type DynamicToolSpec = { - namespace?: string; - name: string; - description: string; - inputSchema: JsonValue; - deferLoading?: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ErrorNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ErrorNotification.ts deleted file mode 100644 index c0cacfc8794..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ErrorNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TurnError } from "./TurnError.js"; - -export type ErrorNotification = { - error: TurnError; - willRetry: boolean; - threadId: string; - turnId: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExecPolicyAmendment.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExecPolicyAmendment.ts deleted file mode 100644 index e893dd4477e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExecPolicyAmendment.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExecPolicyAmendment = Array; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeature.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeature.ts deleted file mode 100644 index 6b44cc29cd1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeature.ts +++ /dev/null @@ -1,38 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExperimentalFeatureStage } from "./ExperimentalFeatureStage.js"; - -export type ExperimentalFeature = { - /** - * Stable key used in config.toml and CLI flag toggles. - */ - name: string; - /** - * Lifecycle stage of this feature flag. - */ - stage: ExperimentalFeatureStage; - /** - * User-facing display name shown in the experimental features UI. - * Null when this feature is not in beta. - */ - displayName: string | null; - /** - * Short summary describing what the feature does. - * Null when this feature is not in beta. - */ - description: string | null; - /** - * Announcement copy shown to users when the feature is introduced. - * Null when this feature is not in beta. - */ - announcement: string | null; - /** - * Whether this feature is currently enabled in the loaded config. - */ - enabled: boolean; - /** - * Whether this feature is enabled by default. - */ - defaultEnabled: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureEnablementSetParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureEnablementSetParams.ts deleted file mode 100644 index de388e3dc62..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureEnablementSetParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExperimentalFeatureEnablementSetParams = { - /** - * Process-wide runtime feature enablement keyed by canonical feature name. - * - * Only named features are updated. Omitted features are left unchanged. - * Send an empty map for a no-op. - */ - enablement: { [key in string]?: boolean }; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureEnablementSetResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureEnablementSetResponse.ts deleted file mode 100644 index e3349d166d7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureEnablementSetResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExperimentalFeatureEnablementSetResponse = { - /** - * Feature enablement entries updated by this request. - */ - enablement: { [key in string]?: boolean }; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureListParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureListParams.ts deleted file mode 100644 index b17c8a637e4..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureListParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExperimentalFeatureListParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to a reasonable server-side value. - */ - limit?: number | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureListResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureListResponse.ts deleted file mode 100644 index c874344b568..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureListResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExperimentalFeature } from "./ExperimentalFeature.js"; - -export type ExperimentalFeatureListResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * If None, there are no more items to return. - */ - nextCursor: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureStage.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureStage.ts deleted file mode 100644 index b72828d4ca3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExperimentalFeatureStage.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExperimentalFeatureStage = - | "beta" - | "underDevelopment" - | "stable" - | "deprecated" - | "removed"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigDetectParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigDetectParams.ts deleted file mode 100644 index 7a081f254c6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigDetectParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExternalAgentConfigDetectParams = { - /** - * If true, include detection under the user's home (~/.claude, ~/.codex, etc.). - */ - includeHome?: boolean; - /** - * Zero or more working directories to include for repo-scoped detection. - */ - cwds?: Array | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigDetectResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigDetectResponse.ts deleted file mode 100644 index db6c9c81236..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigDetectResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem.js"; - -export type ExternalAgentConfigDetectResponse = { items: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigImportCompletedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigImportCompletedNotification.ts deleted file mode 100644 index edb8f191621..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigImportCompletedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExternalAgentConfigImportCompletedNotification = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigImportParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigImportParams.ts deleted file mode 100644 index d83473ed92f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigImportParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem.js"; - -export type ExternalAgentConfigImportParams = { - migrationItems: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigImportResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigImportResponse.ts deleted file mode 100644 index 2ceddade0e7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigImportResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExternalAgentConfigImportResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigMigrationItem.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigMigrationItem.ts deleted file mode 100644 index a4df7554331..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigMigrationItem.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType.js"; -import type { MigrationDetails } from "./MigrationDetails.js"; - -export type ExternalAgentConfigMigrationItem = { - itemType: ExternalAgentConfigMigrationItemType; - description: string; - /** - * Null or empty means home-scoped migration; non-empty means repo-scoped migration. - */ - cwd: string | null; - details: MigrationDetails | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigMigrationItemType.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigMigrationItemType.ts deleted file mode 100644 index 5a2ee5fc783..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ExternalAgentConfigMigrationItemType.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExternalAgentConfigMigrationItemType = - | "AGENTS_MD" - | "CONFIG" - | "SKILLS" - | "PLUGINS" - | "MCP_SERVER_CONFIG" - | "SUBAGENTS" - | "HOOKS" - | "COMMANDS" - | "SESSIONS"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FeedbackUploadParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FeedbackUploadParams.ts deleted file mode 100644 index 4313db5d87a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FeedbackUploadParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FeedbackUploadParams = { - classification: string; - reason?: string | null; - threadId?: string | null; - includeLogs: boolean; - extraLogFiles?: Array | null; - tags?: { [key in string]?: string } | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FeedbackUploadResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FeedbackUploadResponse.ts deleted file mode 100644 index 6b2db6462f1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FeedbackUploadResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FeedbackUploadResponse = { threadId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeApprovalDecision.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeApprovalDecision.ts deleted file mode 100644 index b74ba004b88..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeApprovalDecision.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FileChangeApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeOutputDeltaNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeOutputDeltaNotification.ts deleted file mode 100644 index abd645ca2f6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeOutputDeltaNotification.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Deprecated legacy notification for `apply_patch` textual output. - * - * The server no longer emits this notification. - */ -export type FileChangeOutputDeltaNotification = { - threadId: string; - turnId: string; - itemId: string; - delta: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangePatchUpdatedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangePatchUpdatedNotification.ts deleted file mode 100644 index 9015c2894ae..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangePatchUpdatedNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FileUpdateChange } from "./FileUpdateChange.js"; - -export type FileChangePatchUpdatedNotification = { - threadId: string; - turnId: string; - itemId: string; - changes: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeRequestApprovalParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeRequestApprovalParams.ts deleted file mode 100644 index c7855319252..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeRequestApprovalParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FileChangeRequestApprovalParams = { - threadId: string; - turnId: string; - itemId: string; - /** - * Optional explanatory reason (e.g. request for extra write access). - */ - reason?: string | null; - /** - * [UNSTABLE] When set, the agent is asking the user to allow writes under this root - * for the remainder of the session (unclear if this is honored today). - */ - grantRoot?: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeRequestApprovalResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeRequestApprovalResponse.ts deleted file mode 100644 index 6e528884ad4..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileChangeRequestApprovalResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision.js"; - -export type FileChangeRequestApprovalResponse = { decision: FileChangeApprovalDecision }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemAccessMode.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemAccessMode.ts deleted file mode 100644 index b1d801fe416..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemAccessMode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FileSystemAccessMode = "read" | "write" | "none"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemPath.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemPath.ts deleted file mode 100644 index 36dd69193c7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemPath.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { FileSystemSpecialPath } from "./FileSystemSpecialPath.js"; - -export type FileSystemPath = - | { type: "path"; path: AbsolutePathBuf } - | { type: "glob_pattern"; pattern: string } - | { type: "special"; value: FileSystemSpecialPath }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemSandboxEntry.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemSandboxEntry.ts deleted file mode 100644 index ff0a2dabd63..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemSandboxEntry.ts +++ /dev/null @@ -1,7 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FileSystemAccessMode } from "./FileSystemAccessMode.js"; -import type { FileSystemPath } from "./FileSystemPath.js"; - -export type FileSystemSandboxEntry = { path: FileSystemPath; access: FileSystemAccessMode }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemSpecialPath.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemSpecialPath.ts deleted file mode 100644 index 05827f26554..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileSystemSpecialPath.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type FileSystemSpecialPath = - | { kind: "root" } - | { kind: "minimal" } - | { kind: "project_roots"; subpath: string | null } - | { kind: "tmpdir" } - | { kind: "slash_tmp" } - | { kind: "unknown"; path: string; subpath: string | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileUpdateChange.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileUpdateChange.ts deleted file mode 100644 index 914add8a283..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FileUpdateChange.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PatchChangeKind } from "./PatchChangeKind.js"; - -export type FileUpdateChange = { path: string; kind: PatchChangeKind; diff: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsChangedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsChangedNotification.ts deleted file mode 100644 index 34572071185..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsChangedNotification.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -/** - * Filesystem watch notification emitted for `fs/watch` subscribers. - */ -export type FsChangedNotification = { - /** - * Watch identifier previously provided to `fs/watch`. - */ - watchId: string; - /** - * File or directory paths associated with this event. - */ - changedPaths: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCopyParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCopyParams.ts deleted file mode 100644 index 59b5906b4b1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCopyParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -/** - * Copy a file or directory tree on the host filesystem. - */ -export type FsCopyParams = { - /** - * Absolute source path. - */ - sourcePath: AbsolutePathBuf; - /** - * Absolute destination path. - */ - destinationPath: AbsolutePathBuf; - /** - * Required for directory copies; ignored for file copies. - */ - recursive?: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCopyResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCopyResponse.ts deleted file mode 100644 index 3e3061a8ab5..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCopyResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Successful response for `fs/copy`. - */ -export type FsCopyResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCreateDirectoryParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCreateDirectoryParams.ts deleted file mode 100644 index f606ca5fe72..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCreateDirectoryParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -/** - * Create a directory on the host filesystem. - */ -export type FsCreateDirectoryParams = { - /** - * Absolute directory path to create. - */ - path: AbsolutePathBuf; - /** - * Whether parent directories should also be created. Defaults to `true`. - */ - recursive?: boolean | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCreateDirectoryResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCreateDirectoryResponse.ts deleted file mode 100644 index 5d251b71564..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsCreateDirectoryResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Successful response for `fs/createDirectory`. - */ -export type FsCreateDirectoryResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsGetMetadataParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsGetMetadataParams.ts deleted file mode 100644 index e4d68d01b24..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsGetMetadataParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -/** - * Request metadata for an absolute path. - */ -export type FsGetMetadataParams = { - /** - * Absolute path to inspect. - */ - path: AbsolutePathBuf; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsGetMetadataResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsGetMetadataResponse.ts deleted file mode 100644 index 14352b90800..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsGetMetadataResponse.ts +++ /dev/null @@ -1,29 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Metadata returned by `fs/getMetadata`. - */ -export type FsGetMetadataResponse = { - /** - * Whether the path resolves to a directory. - */ - isDirectory: boolean; - /** - * Whether the path resolves to a regular file. - */ - isFile: boolean; - /** - * Whether the path itself is a symbolic link. - */ - isSymlink: boolean; - /** - * File creation time in Unix milliseconds when available, otherwise `0`. - */ - createdAtMs: number; - /** - * File modification time in Unix milliseconds when available, otherwise `0`. - */ - modifiedAtMs: number; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadDirectoryEntry.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadDirectoryEntry.ts deleted file mode 100644 index 0d44a75384f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadDirectoryEntry.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * A directory entry returned by `fs/readDirectory`. - */ -export type FsReadDirectoryEntry = { - /** - * Direct child entry name only, not an absolute or relative path. - */ - fileName: string; - /** - * Whether this entry resolves to a directory. - */ - isDirectory: boolean; - /** - * Whether this entry resolves to a regular file. - */ - isFile: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadDirectoryParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadDirectoryParams.ts deleted file mode 100644 index d025a33d754..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadDirectoryParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -/** - * List direct child names for a directory. - */ -export type FsReadDirectoryParams = { - /** - * Absolute directory path to read. - */ - path: AbsolutePathBuf; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadDirectoryResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadDirectoryResponse.ts deleted file mode 100644 index 3a235bbf35f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadDirectoryResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FsReadDirectoryEntry } from "./FsReadDirectoryEntry.js"; - -/** - * Directory entries returned by `fs/readDirectory`. - */ -export type FsReadDirectoryResponse = { - /** - * Direct child entries in the requested directory. - */ - entries: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadFileParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadFileParams.ts deleted file mode 100644 index 179c3a46567..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadFileParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -/** - * Read a file from the host filesystem. - */ -export type FsReadFileParams = { - /** - * Absolute path to read. - */ - path: AbsolutePathBuf; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadFileResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadFileResponse.ts deleted file mode 100644 index 15906af2bdd..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsReadFileResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Base64-encoded file contents returned by `fs/readFile`. - */ -export type FsReadFileResponse = { - /** - * File contents encoded as base64. - */ - dataBase64: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsRemoveParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsRemoveParams.ts deleted file mode 100644 index 779a9ef7eea..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsRemoveParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -/** - * Remove a file or directory tree from the host filesystem. - */ -export type FsRemoveParams = { - /** - * Absolute path to remove. - */ - path: AbsolutePathBuf; - /** - * Whether directory removal should recurse. Defaults to `true`. - */ - recursive?: boolean | null; - /** - * Whether missing paths should be ignored. Defaults to `true`. - */ - force?: boolean | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsRemoveResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsRemoveResponse.ts deleted file mode 100644 index 981c28fa1e4..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsRemoveResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Successful response for `fs/remove`. - */ -export type FsRemoveResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsUnwatchParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsUnwatchParams.ts deleted file mode 100644 index d3f6198b9a8..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsUnwatchParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Stop filesystem watch notifications for a prior `fs/watch`. - */ -export type FsUnwatchParams = { - /** - * Watch identifier previously provided to `fs/watch`. - */ - watchId: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsUnwatchResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsUnwatchResponse.ts deleted file mode 100644 index 02507d2c008..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsUnwatchResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Successful response for `fs/unwatch`. - */ -export type FsUnwatchResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWatchParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWatchParams.ts deleted file mode 100644 index a665c4c5cbb..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWatchParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -/** - * Start filesystem watch notifications for an absolute path. - */ -export type FsWatchParams = { - /** - * Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`. - */ - watchId: string; - /** - * Absolute file or directory path to watch. - */ - path: AbsolutePathBuf; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWatchResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWatchResponse.ts deleted file mode 100644 index 180484c7c24..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWatchResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -/** - * Successful response for `fs/watch`. - */ -export type FsWatchResponse = { - /** - * Canonicalized path associated with the watch. - */ - path: AbsolutePathBuf; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWriteFileParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWriteFileParams.ts deleted file mode 100644 index 97322921683..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWriteFileParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -/** - * Write a file on the host filesystem. - */ -export type FsWriteFileParams = { - /** - * Absolute path to write. - */ - path: AbsolutePathBuf; - /** - * File contents encoded as base64. - */ - dataBase64: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWriteFileResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWriteFileResponse.ts deleted file mode 100644 index ad0ce283801..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/FsWriteFileResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Successful response for `fs/writeFile`. - */ -export type FsWriteFileResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GetAccountParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/GetAccountParams.ts deleted file mode 100644 index c297c3e0d73..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GetAccountParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GetAccountParams = { - /** - * When `true`, requests a proactive token refresh before returning. - * - * In managed auth mode this triggers the normal refresh-token flow. In - * external auth mode this flag is ignored. Clients should refresh tokens - * themselves and call `account/login/start` with `chatgptAuthTokens`. - */ - refreshToken: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GetAccountRateLimitsResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/GetAccountRateLimitsResponse.ts deleted file mode 100644 index ffb594807db..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GetAccountRateLimitsResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RateLimitSnapshot } from "./RateLimitSnapshot.js"; - -export type GetAccountRateLimitsResponse = { - /** - * Backward-compatible single-bucket view; mirrors the historical payload. - */ - rateLimits: RateLimitSnapshot; - /** - * Multi-bucket view keyed by metered `limit_id` (for example, `codex`). - */ - rateLimitsByLimitId: { [key in string]?: RateLimitSnapshot } | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GetAccountResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/GetAccountResponse.ts deleted file mode 100644 index 77f00e83aea..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GetAccountResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Account } from "./Account.js"; - -export type GetAccountResponse = { account: Account | null; requiresOpenaiAuth: boolean }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GitInfo.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/GitInfo.ts deleted file mode 100644 index ea62ce27226..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GitInfo.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GitInfo = { sha: string | null; branch: string | null; originUrl: string | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GrantedPermissionProfile.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/GrantedPermissionProfile.ts deleted file mode 100644 index 70b804fa6fd..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GrantedPermissionProfile.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions.js"; -import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions.js"; - -export type GrantedPermissionProfile = { - network?: AdditionalNetworkPermissions; - fileSystem?: AdditionalFileSystemPermissions; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianApprovalReview.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianApprovalReview.ts deleted file mode 100644 index 11bc2010f57..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianApprovalReview.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { GuardianApprovalReviewStatus } from "./GuardianApprovalReviewStatus.js"; -import type { GuardianRiskLevel } from "./GuardianRiskLevel.js"; -import type { GuardianUserAuthorization } from "./GuardianUserAuthorization.js"; - -/** - * [UNSTABLE] Temporary approval auto-review payload used by - * `item/autoApprovalReview/*` notifications. This shape is expected to change - * soon. - */ -export type GuardianApprovalReview = { - status: GuardianApprovalReviewStatus; - riskLevel: GuardianRiskLevel | null; - userAuthorization: GuardianUserAuthorization | null; - rationale: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianApprovalReviewAction.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianApprovalReviewAction.ts deleted file mode 100644 index 5f9ddfa7df4..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianApprovalReviewAction.ts +++ /dev/null @@ -1,34 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { GuardianCommandSource } from "./GuardianCommandSource.js"; -import type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol.js"; -import type { RequestPermissionProfile } from "./RequestPermissionProfile.js"; - -export type GuardianApprovalReviewAction = - | { type: "command"; source: GuardianCommandSource; command: string; cwd: AbsolutePathBuf } - | { - type: "execve"; - source: GuardianCommandSource; - program: string; - argv: Array; - cwd: AbsolutePathBuf; - } - | { type: "applyPatch"; cwd: AbsolutePathBuf; files: Array } - | { - type: "networkAccess"; - target: string; - host: string; - protocol: NetworkApprovalProtocol; - port: number; - } - | { - type: "mcpToolCall"; - server: string; - toolName: string; - connectorId: string | null; - connectorName: string | null; - toolTitle: string | null; - } - | { type: "requestPermissions"; reason: string | null; permissions: RequestPermissionProfile }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianApprovalReviewStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianApprovalReviewStatus.ts deleted file mode 100644 index 7e84c40bffe..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianApprovalReviewStatus.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * [UNSTABLE] Lifecycle state for an approval auto-review. - */ -export type GuardianApprovalReviewStatus = - | "inProgress" - | "approved" - | "denied" - | "timedOut" - | "aborted"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianCommandSource.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianCommandSource.ts deleted file mode 100644 index b48e9b08261..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianCommandSource.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GuardianCommandSource = "shell" | "unifiedExec"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianRiskLevel.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianRiskLevel.ts deleted file mode 100644 index 7734016aa87..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianRiskLevel.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * [UNSTABLE] Risk level assigned by approval auto-review. - */ -export type GuardianRiskLevel = "low" | "medium" | "high" | "critical"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianUserAuthorization.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianUserAuthorization.ts deleted file mode 100644 index 936611f7849..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianUserAuthorization.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * [UNSTABLE] Authorization level assigned by approval auto-review. - */ -export type GuardianUserAuthorization = "unknown" | "low" | "medium" | "high"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianWarningNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianWarningNotification.ts deleted file mode 100644 index 763a0f6ef8a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/GuardianWarningNotification.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GuardianWarningNotification = { - /** - * Thread target for the guardian warning. - */ - threadId: string; - /** - * Concise guardian warning message for the user. - */ - message: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookCompletedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookCompletedNotification.ts deleted file mode 100644 index 0a9ed32f4bf..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookCompletedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HookRunSummary } from "./HookRunSummary.js"; - -export type HookCompletedNotification = { - threadId: string; - turnId: string | null; - run: HookRunSummary; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookErrorInfo.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookErrorInfo.ts deleted file mode 100644 index 9d6607387f1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookErrorInfo.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookErrorInfo = { path: string; message: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookEventName.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookEventName.ts deleted file mode 100644 index 6557e2247d9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookEventName.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookEventName = - | "preToolUse" - | "permissionRequest" - | "postToolUse" - | "sessionStart" - | "userPromptSubmit" - | "stop"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookExecutionMode.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookExecutionMode.ts deleted file mode 100644 index 61f98564cad..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookExecutionMode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookExecutionMode = "sync" | "async"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookHandlerType.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookHandlerType.ts deleted file mode 100644 index dc3f087bff9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookHandlerType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookHandlerType = "command" | "prompt" | "agent"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookMetadata.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookMetadata.ts deleted file mode 100644 index a57110fbb73..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookMetadata.ts +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { HookEventName } from "./HookEventName.js"; -import type { HookHandlerType } from "./HookHandlerType.js"; -import type { HookSource } from "./HookSource.js"; - -export type HookMetadata = { - key: string; - eventName: HookEventName; - handlerType: HookHandlerType; - matcher: string | null; - command: string | null; - timeoutSec: bigint; - statusMessage: string | null; - sourcePath: AbsolutePathBuf; - source: HookSource; - pluginId: string | null; - displayOrder: bigint; - enabled: boolean; - isManaged: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookMigration.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookMigration.ts deleted file mode 100644 index 4f4b3f34443..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookMigration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookMigration = { name: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookOutputEntry.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookOutputEntry.ts deleted file mode 100644 index 026a1b6579d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookOutputEntry.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HookOutputEntryKind } from "./HookOutputEntryKind.js"; - -export type HookOutputEntry = { kind: HookOutputEntryKind; text: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookOutputEntryKind.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookOutputEntryKind.ts deleted file mode 100644 index 090dfe38740..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookOutputEntryKind.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookOutputEntryKind = "warning" | "stop" | "feedback" | "context" | "error"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookPromptFragment.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookPromptFragment.ts deleted file mode 100644 index 74f24559ae3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookPromptFragment.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookPromptFragment = { text: string; hookRunId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookRunStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookRunStatus.ts deleted file mode 100644 index ffca7e0e2c9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookRunStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookRunStatus = "running" | "completed" | "failed" | "blocked" | "stopped"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookRunSummary.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookRunSummary.ts deleted file mode 100644 index 2198e4f6094..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookRunSummary.ts +++ /dev/null @@ -1,28 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { HookEventName } from "./HookEventName.js"; -import type { HookExecutionMode } from "./HookExecutionMode.js"; -import type { HookHandlerType } from "./HookHandlerType.js"; -import type { HookOutputEntry } from "./HookOutputEntry.js"; -import type { HookRunStatus } from "./HookRunStatus.js"; -import type { HookScope } from "./HookScope.js"; -import type { HookSource } from "./HookSource.js"; - -export type HookRunSummary = { - id: string; - eventName: HookEventName; - handlerType: HookHandlerType; - executionMode: HookExecutionMode; - scope: HookScope; - sourcePath: AbsolutePathBuf; - source: HookSource; - displayOrder: bigint; - status: HookRunStatus; - statusMessage: string | null; - startedAt: bigint; - completedAt: bigint | null; - durationMs: bigint | null; - entries: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookScope.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookScope.ts deleted file mode 100644 index ff6f8bfee44..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookScope.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookScope = "thread" | "turn"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookSource.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookSource.ts deleted file mode 100644 index c33572010d6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookSource.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HookSource = - | "system" - | "user" - | "project" - | "mdm" - | "sessionFlags" - | "plugin" - | "cloudRequirements" - | "legacyManagedConfigFile" - | "legacyManagedConfigMdm" - | "unknown"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookStartedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookStartedNotification.ts deleted file mode 100644 index 445fa4b4251..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HookStartedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HookRunSummary } from "./HookRunSummary.js"; - -export type HookStartedNotification = { - threadId: string; - turnId: string | null; - run: HookRunSummary; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HooksListEntry.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HooksListEntry.ts deleted file mode 100644 index 7fe1fc27e97..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HooksListEntry.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HookErrorInfo } from "./HookErrorInfo.js"; -import type { HookMetadata } from "./HookMetadata.js"; - -export type HooksListEntry = { - cwd: string; - hooks: Array; - warnings: Array; - errors: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HooksListParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HooksListParams.ts deleted file mode 100644 index 3f85dcb5a2c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HooksListParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HooksListParams = { - /** - * When empty, defaults to the current session working directory. - */ - cwds?: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HooksListResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/HooksListResponse.ts deleted file mode 100644 index 8371d011394..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/HooksListResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HooksListEntry } from "./HooksListEntry.js"; - -export type HooksListResponse = { data: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemCompletedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemCompletedNotification.ts deleted file mode 100644 index 91fbc4cdac7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemCompletedNotification.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadItem } from "./ThreadItem.js"; - -export type ItemCompletedNotification = { - item: ThreadItem; - threadId: string; - turnId: string; - /** - * Unix timestamp (in milliseconds) when this item lifecycle completed. - */ - completedAtMs: number; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemGuardianApprovalReviewCompletedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemGuardianApprovalReviewCompletedNotification.ts deleted file mode 100644 index 282594172db..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemGuardianApprovalReviewCompletedNotification.ts +++ /dev/null @@ -1,36 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AutoReviewDecisionSource } from "./AutoReviewDecisionSource.js"; -import type { GuardianApprovalReview } from "./GuardianApprovalReview.js"; -import type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction.js"; - -/** - * [UNSTABLE] Temporary notification payload for approval auto-review. This - * shape is expected to change soon. - */ -export type ItemGuardianApprovalReviewCompletedNotification = { - threadId: string; - turnId: string; - /** - * Stable identifier for this review. - */ - reviewId: string; - /** - * Identifier for the reviewed item or tool call when one exists. - * - * In most cases, one review maps to one target item. The exceptions are - * - execve reviews, where a single command may contain multiple execve - * calls to review (only possible when using the shell_zsh_fork feature) - * - network policy reviews, where there is no target item - * - * A network call is triggered by a CommandExecution item, so having a - * target_item_id set to the CommandExecution item would be misleading - * because the review is about the network call, not the command execution. - * Therefore, target_item_id is set to None for network policy reviews. - */ - targetItemId: string | null; - decisionSource: AutoReviewDecisionSource; - review: GuardianApprovalReview; - action: GuardianApprovalReviewAction; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemGuardianApprovalReviewStartedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemGuardianApprovalReviewStartedNotification.ts deleted file mode 100644 index 306084647ff..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemGuardianApprovalReviewStartedNotification.ts +++ /dev/null @@ -1,34 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { GuardianApprovalReview } from "./GuardianApprovalReview.js"; -import type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction.js"; - -/** - * [UNSTABLE] Temporary notification payload for approval auto-review. This - * shape is expected to change soon. - */ -export type ItemGuardianApprovalReviewStartedNotification = { - threadId: string; - turnId: string; - /** - * Stable identifier for this review. - */ - reviewId: string; - /** - * Identifier for the reviewed item or tool call when one exists. - * - * In most cases, one review maps to one target item. The exceptions are - * - execve reviews, where a single command may contain multiple execve - * calls to review (only possible when using the shell_zsh_fork feature) - * - network policy reviews, where there is no target item - * - * A network call is triggered by a CommandExecution item, so having a - * target_item_id set to the CommandExecution item would be misleading - * because the review is about the network call, not the command execution. - * Therefore, target_item_id is set to None for network policy reviews. - */ - targetItemId: string | null; - review: GuardianApprovalReview; - action: GuardianApprovalReviewAction; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemStartedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemStartedNotification.ts deleted file mode 100644 index a13ab1998c0..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ItemStartedNotification.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadItem } from "./ThreadItem.js"; - -export type ItemStartedNotification = { - item: ThreadItem; - threadId: string; - turnId: string; - /** - * Unix timestamp (in milliseconds) when this item lifecycle started. - */ - startedAtMs: number; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ListMcpServerStatusParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ListMcpServerStatusParams.ts deleted file mode 100644 index d230518a335..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ListMcpServerStatusParams.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpServerStatusDetail } from "./McpServerStatusDetail.js"; - -export type ListMcpServerStatusParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to a server-defined value. - */ - limit?: number | null; - /** - * Controls how much MCP inventory data to fetch for each server. - * Defaults to `Full` when omitted. - */ - detail?: McpServerStatusDetail | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ListMcpServerStatusResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ListMcpServerStatusResponse.ts deleted file mode 100644 index e5ece3822b3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ListMcpServerStatusResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpServerStatus } from "./McpServerStatus.js"; - -export type ListMcpServerStatusResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * If None, there are no more items to return. - */ - nextCursor: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/LoginAccountParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/LoginAccountParams.ts deleted file mode 100644 index daab1239b7b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/LoginAccountParams.ts +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type LoginAccountParams = - | { type: "apiKey"; apiKey: string } - | { type: "chatgpt"; codexStreamlinedLogin?: boolean } - | { type: "chatgptDeviceCode" } - | { - type: "chatgptAuthTokens"; - /** - * Access token (JWT) supplied by the client. - * This token is used for backend API requests and email extraction. - */ - accessToken: string; - /** - * Workspace/account identifier supplied by the client. - */ - chatgptAccountId: string; - /** - * Optional plan type supplied by the client. - * - * When `null`, Codex attempts to derive the plan type from access-token - * claims. If unavailable, the plan defaults to `unknown`. - */ - chatgptPlanType?: string | null; - }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/LoginAccountResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/LoginAccountResponse.ts deleted file mode 100644 index b961e2c14c3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/LoginAccountResponse.ts +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type LoginAccountResponse = - | { type: "apiKey" } - | { - type: "chatgpt"; - loginId: string; - /** - * URL the client should open in a browser to initiate the OAuth flow. - */ - authUrl: string; - } - | { - type: "chatgptDeviceCode"; - loginId: string; - /** - * URL the client should open in a browser to complete device code authorization. - */ - verificationUrl: string; - /** - * One-time code the user must enter after signing in. - */ - userCode: string; - } - | { type: "chatgptAuthTokens" }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/LogoutAccountResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/LogoutAccountResponse.ts deleted file mode 100644 index ec85cf0ff77..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/LogoutAccountResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type LogoutAccountResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ManagedHooksRequirements.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ManagedHooksRequirements.ts deleted file mode 100644 index 59d3d7c2e41..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ManagedHooksRequirements.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup.js"; - -export type ManagedHooksRequirements = { - managedDir: string | null; - windowsManagedDir: string | null; - PreToolUse: Array; - PermissionRequest: Array; - PostToolUse: Array; - SessionStart: Array; - UserPromptSubmit: Array; - Stop: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceAddParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceAddParams.ts deleted file mode 100644 index 40f5378dee5..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceAddParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MarketplaceAddParams = { - source: string; - refName?: string | null; - sparsePaths?: Array | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceAddResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceAddResponse.ts deleted file mode 100644 index f04abf50ed3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceAddResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type MarketplaceAddResponse = { - marketplaceName: string; - installedRoot: AbsolutePathBuf; - alreadyAdded: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceInterface.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceInterface.ts deleted file mode 100644 index 71f55ebcef0..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceInterface.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MarketplaceInterface = { displayName: string | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceLoadErrorInfo.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceLoadErrorInfo.ts deleted file mode 100644 index 5e755b46102..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceLoadErrorInfo.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type MarketplaceLoadErrorInfo = { marketplacePath: AbsolutePathBuf; message: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceRemoveParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceRemoveParams.ts deleted file mode 100644 index 88e6f3fe50c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceRemoveParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MarketplaceRemoveParams = { marketplaceName: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceRemoveResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceRemoveResponse.ts deleted file mode 100644 index b5fe4787a08..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceRemoveResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type MarketplaceRemoveResponse = { - marketplaceName: string; - installedRoot: AbsolutePathBuf | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceUpgradeErrorInfo.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceUpgradeErrorInfo.ts deleted file mode 100644 index 6f3dfc67eb0..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceUpgradeErrorInfo.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MarketplaceUpgradeErrorInfo = { marketplaceName: string; message: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceUpgradeParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceUpgradeParams.ts deleted file mode 100644 index 2061385580e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceUpgradeParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MarketplaceUpgradeParams = { marketplaceName?: string | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceUpgradeResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceUpgradeResponse.ts deleted file mode 100644 index 9c94c9bdcaa..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MarketplaceUpgradeResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { MarketplaceUpgradeErrorInfo } from "./MarketplaceUpgradeErrorInfo.js"; - -export type MarketplaceUpgradeResponse = { - selectedMarketplaces: Array; - upgradedRoots: Array; - errors: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpAuthStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpAuthStatus.ts deleted file mode 100644 index 6903a123210..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpAuthStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpAuthStatus = "unsupported" | "notLoggedIn" | "bearerToken" | "oAuth"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationArrayType.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationArrayType.ts deleted file mode 100644 index 066b44ea595..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationArrayType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationArrayType = "array"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationBooleanSchema.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationBooleanSchema.ts deleted file mode 100644 index eda80ed2415..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationBooleanSchema.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationBooleanType } from "./McpElicitationBooleanType.js"; - -export type McpElicitationBooleanSchema = { - type: McpElicitationBooleanType; - title?: string; - description?: string; - default?: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationBooleanType.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationBooleanType.ts deleted file mode 100644 index f2b9ed48df4..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationBooleanType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationBooleanType = "boolean"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationConstOption.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationConstOption.ts deleted file mode 100644 index eed92e21ac1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationConstOption.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationConstOption = { const: string; title: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationEnumSchema.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationEnumSchema.ts deleted file mode 100644 index d34200ef9e3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationEnumSchema.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationLegacyTitledEnumSchema } from "./McpElicitationLegacyTitledEnumSchema.js"; -import type { McpElicitationMultiSelectEnumSchema } from "./McpElicitationMultiSelectEnumSchema.js"; -import type { McpElicitationSingleSelectEnumSchema } from "./McpElicitationSingleSelectEnumSchema.js"; - -export type McpElicitationEnumSchema = - | McpElicitationSingleSelectEnumSchema - | McpElicitationMultiSelectEnumSchema - | McpElicitationLegacyTitledEnumSchema; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationLegacyTitledEnumSchema.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationLegacyTitledEnumSchema.ts deleted file mode 100644 index c00359f4b5e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationLegacyTitledEnumSchema.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationStringType } from "./McpElicitationStringType.js"; - -export type McpElicitationLegacyTitledEnumSchema = { - type: McpElicitationStringType; - title?: string; - description?: string; - enum: Array; - enumNames?: Array; - default?: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationMultiSelectEnumSchema.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationMultiSelectEnumSchema.ts deleted file mode 100644 index 662639ac508..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationMultiSelectEnumSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationTitledMultiSelectEnumSchema } from "./McpElicitationTitledMultiSelectEnumSchema.js"; -import type { McpElicitationUntitledMultiSelectEnumSchema } from "./McpElicitationUntitledMultiSelectEnumSchema.js"; - -export type McpElicitationMultiSelectEnumSchema = - | McpElicitationUntitledMultiSelectEnumSchema - | McpElicitationTitledMultiSelectEnumSchema; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationNumberSchema.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationNumberSchema.ts deleted file mode 100644 index c579eb68558..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationNumberSchema.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationNumberType } from "./McpElicitationNumberType.js"; - -export type McpElicitationNumberSchema = { - type: McpElicitationNumberType; - title?: string; - description?: string; - minimum?: number; - maximum?: number; - default?: number; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationNumberType.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationNumberType.ts deleted file mode 100644 index 96a9ded7607..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationNumberType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationNumberType = "number" | "integer"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationObjectType.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationObjectType.ts deleted file mode 100644 index 2449a0c1ed2..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationObjectType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationObjectType = "object"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationPrimitiveSchema.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationPrimitiveSchema.ts deleted file mode 100644 index dacf6cf21d6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationPrimitiveSchema.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema.js"; -import type { McpElicitationEnumSchema } from "./McpElicitationEnumSchema.js"; -import type { McpElicitationNumberSchema } from "./McpElicitationNumberSchema.js"; -import type { McpElicitationStringSchema } from "./McpElicitationStringSchema.js"; - -export type McpElicitationPrimitiveSchema = - | McpElicitationEnumSchema - | McpElicitationStringSchema - | McpElicitationNumberSchema - | McpElicitationBooleanSchema; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationSchema.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationSchema.ts deleted file mode 100644 index 1c377f886fb..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationSchema.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationObjectType } from "./McpElicitationObjectType.js"; -import type { McpElicitationPrimitiveSchema } from "./McpElicitationPrimitiveSchema.js"; - -/** - * Typed form schema for MCP `elicitation/create` requests. - * - * This matches the `requestedSchema` shape from the MCP 2025-11-25 - * `ElicitRequestFormParams` schema. - */ -export type McpElicitationSchema = { - $schema?: string; - type: McpElicitationObjectType; - properties: { [key in string]?: McpElicitationPrimitiveSchema }; - required?: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationSingleSelectEnumSchema.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationSingleSelectEnumSchema.ts deleted file mode 100644 index e9a3c8d67c7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationSingleSelectEnumSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationTitledSingleSelectEnumSchema } from "./McpElicitationTitledSingleSelectEnumSchema.js"; -import type { McpElicitationUntitledSingleSelectEnumSchema } from "./McpElicitationUntitledSingleSelectEnumSchema.js"; - -export type McpElicitationSingleSelectEnumSchema = - | McpElicitationUntitledSingleSelectEnumSchema - | McpElicitationTitledSingleSelectEnumSchema; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationStringFormat.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationStringFormat.ts deleted file mode 100644 index 9891d4c7ca7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationStringFormat.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationStringFormat = "email" | "uri" | "date" | "date-time"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationStringSchema.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationStringSchema.ts deleted file mode 100644 index f4d863d6f9b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationStringSchema.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationStringFormat } from "./McpElicitationStringFormat.js"; -import type { McpElicitationStringType } from "./McpElicitationStringType.js"; - -export type McpElicitationStringSchema = { - type: McpElicitationStringType; - title?: string; - description?: string; - minLength?: number; - maxLength?: number; - format?: McpElicitationStringFormat; - default?: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationStringType.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationStringType.ts deleted file mode 100644 index bf2ddfab91c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationStringType.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpElicitationStringType = "string"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationTitledEnumItems.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationTitledEnumItems.ts deleted file mode 100644 index f1f3b63ec5c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationTitledEnumItems.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationConstOption } from "./McpElicitationConstOption.js"; - -export type McpElicitationTitledEnumItems = { anyOf: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationTitledMultiSelectEnumSchema.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationTitledMultiSelectEnumSchema.ts deleted file mode 100644 index 4a6aeb7afff..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationTitledMultiSelectEnumSchema.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationArrayType } from "./McpElicitationArrayType.js"; -import type { McpElicitationTitledEnumItems } from "./McpElicitationTitledEnumItems.js"; - -export type McpElicitationTitledMultiSelectEnumSchema = { - type: McpElicitationArrayType; - title?: string; - description?: string; - minItems?: bigint; - maxItems?: bigint; - items: McpElicitationTitledEnumItems; - default?: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationTitledSingleSelectEnumSchema.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationTitledSingleSelectEnumSchema.ts deleted file mode 100644 index 2cc558616cd..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationTitledSingleSelectEnumSchema.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationConstOption } from "./McpElicitationConstOption.js"; -import type { McpElicitationStringType } from "./McpElicitationStringType.js"; - -export type McpElicitationTitledSingleSelectEnumSchema = { - type: McpElicitationStringType; - title?: string; - description?: string; - oneOf: Array; - default?: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationUntitledEnumItems.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationUntitledEnumItems.ts deleted file mode 100644 index 8419e36a4c9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationUntitledEnumItems.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationStringType } from "./McpElicitationStringType.js"; - -export type McpElicitationUntitledEnumItems = { - type: McpElicitationStringType; - enum: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationUntitledMultiSelectEnumSchema.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationUntitledMultiSelectEnumSchema.ts deleted file mode 100644 index 5bfbbbd0a48..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationUntitledMultiSelectEnumSchema.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationArrayType } from "./McpElicitationArrayType.js"; -import type { McpElicitationUntitledEnumItems } from "./McpElicitationUntitledEnumItems.js"; - -export type McpElicitationUntitledMultiSelectEnumSchema = { - type: McpElicitationArrayType; - title?: string; - description?: string; - minItems?: bigint; - maxItems?: bigint; - items: McpElicitationUntitledEnumItems; - default?: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationUntitledSingleSelectEnumSchema.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationUntitledSingleSelectEnumSchema.ts deleted file mode 100644 index 701c5d22e44..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpElicitationUntitledSingleSelectEnumSchema.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpElicitationStringType } from "./McpElicitationStringType.js"; - -export type McpElicitationUntitledSingleSelectEnumSchema = { - type: McpElicitationStringType; - title?: string; - description?: string; - enum: Array; - default?: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpResourceReadParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpResourceReadParams.ts deleted file mode 100644 index 1aa57b4471c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpResourceReadParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpResourceReadParams = { threadId?: string | null; server: string; uri: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpResourceReadResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpResourceReadResponse.ts deleted file mode 100644 index 337929783ae..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpResourceReadResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ResourceContent } from "../ResourceContent.js"; - -export type McpResourceReadResponse = { contents: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerElicitationAction.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerElicitationAction.ts deleted file mode 100644 index 7be134c0150..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerElicitationAction.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerElicitationAction = "accept" | "decline" | "cancel"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerElicitationRequestParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerElicitationRequestParams.ts deleted file mode 100644 index c0a3c915efd..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerElicitationRequestParams.ts +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; -import type { McpElicitationSchema } from "./McpElicitationSchema.js"; - -export type McpServerElicitationRequestParams = { - threadId: string; - /** - * Active Codex turn when this elicitation was observed, if app-server could correlate one. - * - * This is nullable because MCP models elicitation as a standalone server-to-client request - * identified by the MCP server request id. It may be triggered during a turn, but turn - * context is app-server correlation rather than part of the protocol identity of the - * elicitation itself. - */ - turnId: string | null; - serverName: string; -} & ( - | { - mode: "form"; - _meta: JsonValue | null; - message: string; - requestedSchema: McpElicitationSchema; - } - | { mode: "url"; _meta: JsonValue | null; message: string; url: string; elicitationId: string } -); diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerElicitationRequestResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerElicitationRequestResponse.ts deleted file mode 100644 index d70c55de7f9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerElicitationRequestResponse.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; -import type { McpServerElicitationAction } from "./McpServerElicitationAction.js"; - -export type McpServerElicitationRequestResponse = { - action: McpServerElicitationAction; - /** - * Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`. - * - * This is nullable because decline/cancel responses have no content. - */ - content: JsonValue | null; - /** - * Optional client metadata for form-mode action handling. - */ - _meta: JsonValue | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerMigration.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerMigration.ts deleted file mode 100644 index fe7f2009478..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerMigration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerMigration = { name: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerOauthLoginCompletedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerOauthLoginCompletedNotification.ts deleted file mode 100644 index 6303a53c92f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerOauthLoginCompletedNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerOauthLoginCompletedNotification = { - name: string; - success: boolean; - error?: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerOauthLoginParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerOauthLoginParams.ts deleted file mode 100644 index 4d0982c4730..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerOauthLoginParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerOauthLoginParams = { - name: string; - scopes?: Array | null; - timeoutSecs?: bigint | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerOauthLoginResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerOauthLoginResponse.ts deleted file mode 100644 index b119e07650c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerOauthLoginResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerOauthLoginResponse = { authorizationUrl: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerRefreshResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerRefreshResponse.ts deleted file mode 100644 index 48a25d2fec0..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerRefreshResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerRefreshResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStartupState.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStartupState.ts deleted file mode 100644 index c62babca66a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStartupState.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerStartupState = "starting" | "ready" | "failed" | "cancelled"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStatus.ts deleted file mode 100644 index 61fa8693130..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStatus.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Resource } from "../Resource.js"; -import type { ResourceTemplate } from "../ResourceTemplate.js"; -import type { Tool } from "../Tool.js"; -import type { McpAuthStatus } from "./McpAuthStatus.js"; - -export type McpServerStatus = { - name: string; - tools: { [key in string]?: Tool }; - resources: Array; - resourceTemplates: Array; - authStatus: McpAuthStatus; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStatusDetail.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStatusDetail.ts deleted file mode 100644 index ab97cc2f31d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStatusDetail.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpServerStatusDetail = "full" | "toolsAndAuthOnly"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStatusUpdatedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStatusUpdatedNotification.ts deleted file mode 100644 index 3b6c168b119..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerStatusUpdatedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { McpServerStartupState } from "./McpServerStartupState.js"; - -export type McpServerStatusUpdatedNotification = { - name: string; - status: McpServerStartupState; - error: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerToolCallParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerToolCallParams.ts deleted file mode 100644 index 6da04c52ac4..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerToolCallParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; - -export type McpServerToolCallParams = { - threadId: string; - server: string; - tool: string; - arguments?: JsonValue; - _meta?: JsonValue; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerToolCallResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerToolCallResponse.ts deleted file mode 100644 index 0314ef363d9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpServerToolCallResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; - -export type McpServerToolCallResponse = { - content: Array; - structuredContent?: JsonValue; - isError?: boolean; - _meta?: JsonValue; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallError.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallError.ts deleted file mode 100644 index f11054a8af9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallError.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpToolCallError = { message: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallProgressNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallProgressNotification.ts deleted file mode 100644 index 3f3137e100f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallProgressNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpToolCallProgressNotification = { - threadId: string; - turnId: string; - itemId: string; - message: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallResult.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallResult.ts deleted file mode 100644 index ac6804332e7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallResult.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; - -export type McpToolCallResult = { - content: Array; - structuredContent: JsonValue | null; - _meta: JsonValue | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallStatus.ts deleted file mode 100644 index f46bca07e84..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/McpToolCallStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type McpToolCallStatus = "inProgress" | "completed" | "failed"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MemoryCitation.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MemoryCitation.ts deleted file mode 100644 index 677d2791cb4..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MemoryCitation.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { MemoryCitationEntry } from "./MemoryCitationEntry.js"; - -export type MemoryCitation = { entries: Array; threadIds: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MemoryCitationEntry.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MemoryCitationEntry.ts deleted file mode 100644 index eb74521c268..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MemoryCitationEntry.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MemoryCitationEntry = { - path: string; - lineStart: number; - lineEnd: number; - note: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MemoryResetResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MemoryResetResponse.ts deleted file mode 100644 index d9507945a06..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MemoryResetResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MemoryResetResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MergeStrategy.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MergeStrategy.ts deleted file mode 100644 index 098677f2895..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MergeStrategy.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MergeStrategy = "replace" | "upsert"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MigrationDetails.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MigrationDetails.ts deleted file mode 100644 index 0f53c4d2a1d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MigrationDetails.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CommandMigration } from "./CommandMigration.js"; -import type { HookMigration } from "./HookMigration.js"; -import type { McpServerMigration } from "./McpServerMigration.js"; -import type { PluginsMigration } from "./PluginsMigration.js"; -import type { SessionMigration } from "./SessionMigration.js"; -import type { SubagentMigration } from "./SubagentMigration.js"; - -export type MigrationDetails = { - plugins: Array; - sessions: Array; - mcpServers: Array; - hooks: Array; - subagents: Array; - commands: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MockExperimentalMethodParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MockExperimentalMethodParams.ts deleted file mode 100644 index f48f7968476..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MockExperimentalMethodParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MockExperimentalMethodParams = { - /** - * Test-only payload field. - */ - value?: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MockExperimentalMethodResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/MockExperimentalMethodResponse.ts deleted file mode 100644 index 02ad93098f6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/MockExperimentalMethodResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MockExperimentalMethodResponse = { - /** - * Echoes the input `value`. - */ - echoed: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/Model.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/Model.ts deleted file mode 100644 index 88f0803e009..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/Model.ts +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { InputModality } from "../InputModality.js"; -import type { ReasoningEffort } from "../ReasoningEffort.js"; -import type { ModelAvailabilityNux } from "./ModelAvailabilityNux.js"; -import type { ModelUpgradeInfo } from "./ModelUpgradeInfo.js"; -import type { ReasoningEffortOption } from "./ReasoningEffortOption.js"; - -export type Model = { - id: string; - model: string; - upgrade: string | null; - upgradeInfo: ModelUpgradeInfo | null; - availabilityNux: ModelAvailabilityNux | null; - displayName: string; - description: string; - hidden: boolean; - supportedReasoningEfforts: Array; - defaultReasoningEffort: ReasoningEffort; - inputModalities: Array; - supportsPersonality: boolean; - additionalSpeedTiers: Array; - isDefault: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelAvailabilityNux.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelAvailabilityNux.ts deleted file mode 100644 index 8ea4b6ed1fd..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelAvailabilityNux.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelAvailabilityNux = { message: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelListParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelListParams.ts deleted file mode 100644 index 568c8ae9790..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelListParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelListParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to a reasonable server-side value. - */ - limit?: number | null; - /** - * When true, include models that are hidden from the default picker list. - */ - includeHidden?: boolean | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelListResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelListResponse.ts deleted file mode 100644 index 4de72f017e1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelListResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Model } from "./Model.js"; - -export type ModelListResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * If None, there are no more items to return. - */ - nextCursor: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelProviderCapabilitiesReadParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelProviderCapabilitiesReadParams.ts deleted file mode 100644 index 00cbe470b3c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelProviderCapabilitiesReadParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelProviderCapabilitiesReadParams = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelProviderCapabilitiesReadResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelProviderCapabilitiesReadResponse.ts deleted file mode 100644 index f831613025c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelProviderCapabilitiesReadResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelProviderCapabilitiesReadResponse = { - namespaceTools: boolean; - imageGeneration: boolean; - webSearch: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelRerouteReason.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelRerouteReason.ts deleted file mode 100644 index e780e7f95d7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelRerouteReason.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelRerouteReason = "highRiskCyberActivity"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelReroutedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelReroutedNotification.ts deleted file mode 100644 index 6d0e392660a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelReroutedNotification.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ModelRerouteReason } from "./ModelRerouteReason.js"; - -export type ModelReroutedNotification = { - threadId: string; - turnId: string; - fromModel: string; - toModel: string; - reason: ModelRerouteReason; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelUpgradeInfo.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelUpgradeInfo.ts deleted file mode 100644 index 2976b6d201d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelUpgradeInfo.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelUpgradeInfo = { - model: string; - upgradeCopy: string | null; - modelLink: string | null; - migrationMarkdown: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelVerification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelVerification.ts deleted file mode 100644 index 00538c090f0..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelVerification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ModelVerification = "trustedAccessForCyber"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelVerificationNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelVerificationNotification.ts deleted file mode 100644 index 24dcb7e7bcf..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ModelVerificationNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ModelVerification } from "./ModelVerification.js"; - -export type ModelVerificationNotification = { - threadId: string; - turnId: string; - verifications: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkAccess.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkAccess.ts deleted file mode 100644 index 7b697b23149..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkAccess.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type NetworkAccess = "restricted" | "enabled"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkApprovalContext.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkApprovalContext.ts deleted file mode 100644 index 20c32d6f445..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkApprovalContext.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol.js"; - -export type NetworkApprovalContext = { host: string; protocol: NetworkApprovalProtocol }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkApprovalProtocol.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkApprovalProtocol.ts deleted file mode 100644 index 9dd4066fd13..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkApprovalProtocol.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type NetworkApprovalProtocol = "http" | "https" | "socks5Tcp" | "socks5Udp"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkDomainPermission.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkDomainPermission.ts deleted file mode 100644 index 2ea44392de9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkDomainPermission.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type NetworkDomainPermission = "allow" | "deny"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkPolicyAmendment.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkPolicyAmendment.ts deleted file mode 100644 index 5455621ece7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkPolicyAmendment.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction.js"; - -export type NetworkPolicyAmendment = { host: string; action: NetworkPolicyRuleAction }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkPolicyRuleAction.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkPolicyRuleAction.ts deleted file mode 100644 index 55ec70032a6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkPolicyRuleAction.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type NetworkPolicyRuleAction = "allow" | "deny"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkRequirements.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkRequirements.ts deleted file mode 100644 index ab7475c9af3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkRequirements.ts +++ /dev/null @@ -1,40 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NetworkDomainPermission } from "./NetworkDomainPermission.js"; -import type { NetworkUnixSocketPermission } from "./NetworkUnixSocketPermission.js"; - -export type NetworkRequirements = { - enabled: boolean | null; - httpPort: number | null; - socksPort: number | null; - allowUpstreamProxy: boolean | null; - dangerouslyAllowNonLoopbackProxy: boolean | null; - dangerouslyAllowAllUnixSockets: boolean | null; - /** - * Canonical network permission map for `experimental_network`. - */ - domains: { [key in string]?: NetworkDomainPermission } | null; - /** - * When true, only managed allowlist entries are respected while managed - * network enforcement is active. - */ - managedAllowedDomainsOnly: boolean | null; - /** - * Legacy compatibility view derived from `domains`. - */ - allowedDomains: Array | null; - /** - * Legacy compatibility view derived from `domains`. - */ - deniedDomains: Array | null; - /** - * Canonical unix socket permission map for `experimental_network`. - */ - unixSockets: { [key in string]?: NetworkUnixSocketPermission } | null; - /** - * Legacy compatibility view derived from `unix_sockets`. - */ - allowUnixSockets: Array | null; - allowLocalBinding: boolean | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkUnixSocketPermission.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkUnixSocketPermission.ts deleted file mode 100644 index 466c6e5f8f9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NetworkUnixSocketPermission.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type NetworkUnixSocketPermission = "allow" | "none"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NonSteerableTurnKind.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/NonSteerableTurnKind.ts deleted file mode 100644 index 2624df2ba0d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/NonSteerableTurnKind.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type NonSteerableTurnKind = "review" | "compact"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/OverriddenMetadata.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/OverriddenMetadata.ts deleted file mode 100644 index bc6817aeed4..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/OverriddenMetadata.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; -import type { ConfigLayerMetadata } from "./ConfigLayerMetadata.js"; - -export type OverriddenMetadata = { - message: string; - overridingLayer: ConfigLayerMetadata; - effectiveValue: JsonValue; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PatchApplyStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PatchApplyStatus.ts deleted file mode 100644 index 620be789e49..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PatchApplyStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PatchApplyStatus = "inProgress" | "completed" | "failed" | "declined"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PatchChangeKind.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PatchChangeKind.ts deleted file mode 100644 index 41ef25c6a48..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PatchChangeKind.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PatchChangeKind = - | { type: "add" } - | { type: "delete" } - | { type: "update"; move_path: string | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionGrantScope.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionGrantScope.ts deleted file mode 100644 index 8ca127ebcb1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionGrantScope.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PermissionGrantScope = "turn" | "session"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfile.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfile.ts deleted file mode 100644 index 3a4ba02bd06..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfile.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PermissionProfileFileSystemPermissions } from "./PermissionProfileFileSystemPermissions.js"; -import type { PermissionProfileNetworkPermissions } from "./PermissionProfileNetworkPermissions.js"; - -export type PermissionProfile = - | { - type: "managed"; - network: PermissionProfileNetworkPermissions; - fileSystem: PermissionProfileFileSystemPermissions; - } - | { type: "disabled" } - | { type: "external"; network: PermissionProfileNetworkPermissions }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileFileSystemPermissions.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileFileSystemPermissions.ts deleted file mode 100644 index e528056b57e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileFileSystemPermissions.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry.js"; - -export type PermissionProfileFileSystemPermissions = - | { type: "restricted"; entries: Array; globScanMaxDepth?: number } - | { type: "unrestricted" }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileModificationParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileModificationParams.ts deleted file mode 100644 index ad980413f7f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileModificationParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type PermissionProfileModificationParams = { - type: "additionalWritableRoot"; - path: AbsolutePathBuf; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileNetworkPermissions.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileNetworkPermissions.ts deleted file mode 100644 index 12217e5374a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileNetworkPermissions.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PermissionProfileNetworkPermissions = { enabled: boolean }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileSelectionParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileSelectionParams.ts deleted file mode 100644 index fe6bff93b47..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionProfileSelectionParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PermissionProfileModificationParams } from "./PermissionProfileModificationParams.js"; - -export type PermissionProfileSelectionParams = { - type: "profile"; - id: string; - modifications?: Array | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionsRequestApprovalParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionsRequestApprovalParams.ts deleted file mode 100644 index 92d9d4b87c6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionsRequestApprovalParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { RequestPermissionProfile } from "./RequestPermissionProfile.js"; - -export type PermissionsRequestApprovalParams = { - threadId: string; - turnId: string; - itemId: string; - cwd: AbsolutePathBuf; - reason: string | null; - permissions: RequestPermissionProfile; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionsRequestApprovalResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionsRequestApprovalResponse.ts deleted file mode 100644 index 730f29c72e8..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PermissionsRequestApprovalResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { GrantedPermissionProfile } from "./GrantedPermissionProfile.js"; -import type { PermissionGrantScope } from "./PermissionGrantScope.js"; - -export type PermissionsRequestApprovalResponse = { - permissions: GrantedPermissionProfile; - scope: PermissionGrantScope; - /** - * Review every subsequent command in this turn before normal sandboxed execution. - */ - strictAutoReview?: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PlanDeltaNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PlanDeltaNotification.ts deleted file mode 100644 index 262f65204cf..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PlanDeltaNotification.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - proposed plan streaming deltas for plan items. Clients should - * not assume concatenated deltas match the completed plan item content. - */ -export type PlanDeltaNotification = { - threadId: string; - turnId: string; - itemId: string; - delta: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginAuthPolicy.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginAuthPolicy.ts deleted file mode 100644 index 5b90e9c3136..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginAuthPolicy.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginAuthPolicy = "ON_INSTALL" | "ON_USE"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginAvailability.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginAvailability.ts deleted file mode 100644 index bec0b88cc20..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginAvailability.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginAvailability = "AVAILABLE" | "DISABLED_BY_ADMIN"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginDetail.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginDetail.ts deleted file mode 100644 index 6b7b1b3c5c5..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginDetail.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { AppSummary } from "./AppSummary.js"; -import type { PluginSummary } from "./PluginSummary.js"; -import type { SkillSummary } from "./SkillSummary.js"; - -export type PluginDetail = { - marketplaceName: string; - marketplacePath: AbsolutePathBuf | null; - summary: PluginSummary; - description: string | null; - skills: Array; - apps: Array; - mcpServers: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInstallParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInstallParams.ts deleted file mode 100644 index 12584472d1a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInstallParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type PluginInstallParams = { - marketplacePath?: AbsolutePathBuf | null; - remoteMarketplaceName?: string | null; - pluginName: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInstallPolicy.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInstallPolicy.ts deleted file mode 100644 index d624f38ea3f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInstallPolicy.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginInstallPolicy = "NOT_AVAILABLE" | "AVAILABLE" | "INSTALLED_BY_DEFAULT"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInstallResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInstallResponse.ts deleted file mode 100644 index 08bf1ebc20e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInstallResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AppSummary } from "./AppSummary.js"; -import type { PluginAuthPolicy } from "./PluginAuthPolicy.js"; - -export type PluginInstallResponse = { - authPolicy: PluginAuthPolicy; - appsNeedingAuth: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInterface.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInterface.ts deleted file mode 100644 index 97596ce765e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginInterface.ts +++ /dev/null @@ -1,46 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type PluginInterface = { - displayName: string | null; - shortDescription: string | null; - longDescription: string | null; - developerName: string | null; - category: string | null; - capabilities: Array; - websiteUrl: string | null; - privacyPolicyUrl: string | null; - termsOfServiceUrl: string | null; - /** - * Starter prompts for the plugin. Capped at 3 entries with a maximum of - * 128 characters per entry. - */ - defaultPrompt: Array | null; - brandColor: string | null; - /** - * Local composer icon path, resolved from the installed plugin package. - */ - composerIcon: AbsolutePathBuf | null; - /** - * Remote composer icon URL from the plugin catalog. - */ - composerIconUrl: string | null; - /** - * Local logo path, resolved from the installed plugin package. - */ - logo: AbsolutePathBuf | null; - /** - * Remote logo URL from the plugin catalog. - */ - logoUrl: string | null; - /** - * Local screenshot paths, resolved from the installed plugin package. - */ - screenshots: Array; - /** - * Remote screenshot URLs from the plugin catalog. - */ - screenshotUrls: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginListParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginListParams.ts deleted file mode 100644 index 51e5652e91e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginListParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type PluginListParams = { - /** - * Optional working directories used to discover repo marketplaces. When omitted, - * only home-scoped marketplaces and the official curated marketplace are considered. - */ - cwds?: Array | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginListResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginListResponse.ts deleted file mode 100644 index c14b1306a68..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginListResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { MarketplaceLoadErrorInfo } from "./MarketplaceLoadErrorInfo.js"; -import type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry.js"; - -export type PluginListResponse = { - marketplaces: Array; - marketplaceLoadErrors: Array; - featuredPluginIds: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginMarketplaceEntry.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginMarketplaceEntry.ts deleted file mode 100644 index a7dbbd78c8e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginMarketplaceEntry.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { MarketplaceInterface } from "./MarketplaceInterface.js"; -import type { PluginSummary } from "./PluginSummary.js"; - -export type PluginMarketplaceEntry = { - name: string; - /** - * Local marketplace file path when the marketplace is backed by a local file. - * Remote-only catalog marketplaces do not have a local path. - */ - path: AbsolutePathBuf | null; - interface: MarketplaceInterface | null; - plugins: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginReadParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginReadParams.ts deleted file mode 100644 index 47125184bd6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginReadParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type PluginReadParams = { - marketplacePath?: AbsolutePathBuf | null; - remoteMarketplaceName?: string | null; - pluginName: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginReadResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginReadResponse.ts deleted file mode 100644 index a23191fb80a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginReadResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PluginDetail } from "./PluginDetail.js"; - -export type PluginReadResponse = { plugin: PluginDetail }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareDeleteParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareDeleteParams.ts deleted file mode 100644 index 092ac6c126e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareDeleteParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginShareDeleteParams = { remotePluginId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareDeleteResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareDeleteResponse.ts deleted file mode 100644 index 23102683645..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareDeleteResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginShareDeleteResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareListItem.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareListItem.ts deleted file mode 100644 index 3b5176c6da1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareListItem.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { PluginSummary } from "./PluginSummary.js"; - -export type PluginShareListItem = { - plugin: PluginSummary; - shareUrl: string; - localPluginPath: AbsolutePathBuf | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareListParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareListParams.ts deleted file mode 100644 index 167ace7ac2c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareListParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginShareListParams = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareListResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareListResponse.ts deleted file mode 100644 index eca6c2bbf7c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareListResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PluginShareListItem } from "./PluginShareListItem.js"; - -export type PluginShareListResponse = { data: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareSaveParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareSaveParams.ts deleted file mode 100644 index 1720cddfbf8..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareSaveParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type PluginShareSaveParams = { pluginPath: AbsolutePathBuf; remotePluginId?: string | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareSaveResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareSaveResponse.ts deleted file mode 100644 index 27abc82e06b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginShareSaveResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginShareSaveResponse = { remotePluginId: string; shareUrl: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSkillReadParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSkillReadParams.ts deleted file mode 100644 index 5145b2ceabc..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSkillReadParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginSkillReadParams = { - remoteMarketplaceName: string; - remotePluginId: string; - skillName: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSkillReadResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSkillReadResponse.ts deleted file mode 100644 index 98007bdb9ac..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSkillReadResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginSkillReadResponse = { contents: string | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSource.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSource.ts deleted file mode 100644 index d3f864c5f31..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSource.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type PluginSource = - | { type: "local"; path: AbsolutePathBuf } - | { type: "git"; url: string; path: string | null; refName: string | null; sha: string | null } - | { type: "remote" }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSummary.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSummary.ts deleted file mode 100644 index 9d724b10ae7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginSummary.ts +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PluginAuthPolicy } from "./PluginAuthPolicy.js"; -import type { PluginAvailability } from "./PluginAvailability.js"; -import type { PluginInstallPolicy } from "./PluginInstallPolicy.js"; -import type { PluginInterface } from "./PluginInterface.js"; -import type { PluginSource } from "./PluginSource.js"; - -export type PluginSummary = { - id: string; - name: string; - source: PluginSource; - installed: boolean; - enabled: boolean; - installPolicy: PluginInstallPolicy; - authPolicy: PluginAuthPolicy; - /** - * Availability state for installing and using the plugin. - */ - availability: PluginAvailability; - interface: PluginInterface | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginUninstallParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginUninstallParams.ts deleted file mode 100644 index 250ee7d3b90..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginUninstallParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginUninstallParams = { pluginId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginUninstallResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginUninstallResponse.ts deleted file mode 100644 index 5d02c2f7167..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginUninstallResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginUninstallResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginsMigration.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginsMigration.ts deleted file mode 100644 index 3d882cf1929..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/PluginsMigration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type PluginsMigration = { marketplaceName: string; pluginNames: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ProfileV2.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ProfileV2.ts deleted file mode 100644 index c9fbdf28c56..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ProfileV2.ts +++ /dev/null @@ -1,39 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReasoningEffort } from "../ReasoningEffort.js"; -import type { ReasoningSummary } from "../ReasoningSummary.js"; -import type { JsonValue } from "../serde_json/JsonValue.js"; -import type { ServiceTier } from "../ServiceTier.js"; -import type { Verbosity } from "../Verbosity.js"; -import type { WebSearchMode } from "../WebSearchMode.js"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer.js"; -import type { AskForApproval } from "./AskForApproval.js"; -import type { ToolsV2 } from "./ToolsV2.js"; - -export type ProfileV2 = { - model: string | null; - model_provider: string | null; - approval_policy: AskForApproval | null; - /** - * [UNSTABLE] Optional profile-level override for where approval requests - * are routed for review. If omitted, the enclosing config default is - * used. - */ - approvals_reviewer: ApprovalsReviewer | null; - service_tier: ServiceTier | null; - model_reasoning_effort: ReasoningEffort | null; - model_reasoning_summary: ReasoningSummary | null; - model_verbosity: Verbosity | null; - web_search: WebSearchMode | null; - tools: ToolsV2 | null; - chatgpt_base_url: string | null; -} & { - [key in string]?: - | number - | string - | boolean - | Array - | { [key in string]?: JsonValue } - | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RateLimitReachedType.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/RateLimitReachedType.ts deleted file mode 100644 index 84bb0b50979..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RateLimitReachedType.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RateLimitReachedType = - | "rate_limit_reached" - | "workspace_owner_credits_depleted" - | "workspace_member_credits_depleted" - | "workspace_owner_usage_limit_reached" - | "workspace_member_usage_limit_reached"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RateLimitSnapshot.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/RateLimitSnapshot.ts deleted file mode 100644 index a852413d239..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RateLimitSnapshot.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PlanType } from "../PlanType.js"; -import type { CreditsSnapshot } from "./CreditsSnapshot.js"; -import type { RateLimitReachedType } from "./RateLimitReachedType.js"; -import type { RateLimitWindow } from "./RateLimitWindow.js"; - -export type RateLimitSnapshot = { - limitId: string | null; - limitName: string | null; - primary: RateLimitWindow | null; - secondary: RateLimitWindow | null; - credits: CreditsSnapshot | null; - planType: PlanType | null; - rateLimitReachedType: RateLimitReachedType | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RateLimitWindow.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/RateLimitWindow.ts deleted file mode 100644 index c0ad59583b5..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RateLimitWindow.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RateLimitWindow = { - usedPercent: number; - windowDurationMins: number | null; - resetsAt: number | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RawResponseItemCompletedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/RawResponseItemCompletedNotification.ts deleted file mode 100644 index 531655936f6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RawResponseItemCompletedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ResponseItem } from "../ResponseItem.js"; - -export type RawResponseItemCompletedNotification = { - threadId: string; - turnId: string; - item: ResponseItem; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningEffortOption.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningEffortOption.ts deleted file mode 100644 index a9258c07079..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningEffortOption.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReasoningEffort } from "../ReasoningEffort.js"; - -export type ReasoningEffortOption = { reasoningEffort: ReasoningEffort; description: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningSummaryPartAddedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningSummaryPartAddedNotification.ts deleted file mode 100644 index 3ba8282e045..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningSummaryPartAddedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReasoningSummaryPartAddedNotification = { - threadId: string; - turnId: string; - itemId: string; - summaryIndex: number; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningSummaryTextDeltaNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningSummaryTextDeltaNotification.ts deleted file mode 100644 index 7ce59515b14..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningSummaryTextDeltaNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReasoningSummaryTextDeltaNotification = { - threadId: string; - turnId: string; - itemId: string; - delta: string; - summaryIndex: number; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningTextDeltaNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningTextDeltaNotification.ts deleted file mode 100644 index 23e9bc4cc12..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReasoningTextDeltaNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReasoningTextDeltaNotification = { - threadId: string; - turnId: string; - itemId: string; - delta: string; - contentIndex: number; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlClientConnectionAudience.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlClientConnectionAudience.ts deleted file mode 100644 index e4d41ff4c23..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlClientConnectionAudience.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Audience for a remote-control client connection device-key proof. - */ -export type RemoteControlClientConnectionAudience = "remote_control_client_websocket"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlClientEnrollmentAudience.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlClientEnrollmentAudience.ts deleted file mode 100644 index b65fb3d11ba..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlClientEnrollmentAudience.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Audience for a remote-control client enrollment device-key proof. - */ -export type RemoteControlClientEnrollmentAudience = "remote_control_client_enrollment"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlConnectionStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlConnectionStatus.ts deleted file mode 100644 index 3e6197f5b55..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlConnectionStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoteControlConnectionStatus = "disabled" | "connecting" | "connected" | "errored"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlStatusChangedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlStatusChangedNotification.ts deleted file mode 100644 index 923edb23434..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RemoteControlStatusChangedNotification.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus.js"; - -/** - * Current remote-control connection status and environment id exposed to clients. - */ -export type RemoteControlStatusChangedNotification = { - status: RemoteControlConnectionStatus; - environmentId: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RequestPermissionProfile.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/RequestPermissionProfile.ts deleted file mode 100644 index 3a5c34b243e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/RequestPermissionProfile.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions.js"; -import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions.js"; - -export type RequestPermissionProfile = { - network: AdditionalNetworkPermissions | null; - fileSystem: AdditionalFileSystemPermissions | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ResidencyRequirement.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ResidencyRequirement.ts deleted file mode 100644 index 1699c84e7cd..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ResidencyRequirement.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ResidencyRequirement = "us"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewDelivery.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewDelivery.ts deleted file mode 100644 index 8fbccd1050a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewDelivery.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReviewDelivery = "inline" | "detached"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewStartParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewStartParams.ts deleted file mode 100644 index b2af886bf18..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewStartParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReviewDelivery } from "./ReviewDelivery.js"; -import type { ReviewTarget } from "./ReviewTarget.js"; - -export type ReviewStartParams = { - threadId: string; - target: ReviewTarget; - /** - * Where to run the review: inline (default) on the current thread or - * detached on a new thread (returned in `reviewThreadId`). - */ - delivery?: ReviewDelivery | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewStartResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewStartResponse.ts deleted file mode 100644 index ea0ebeb4de0..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewStartResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Turn } from "./Turn.js"; - -export type ReviewStartResponse = { - turn: Turn; - /** - * Identifies the thread where the review runs. - * - * For inline reviews, this is the original thread id. - * For detached reviews, this is the id of the new review thread. - */ - reviewThreadId: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewTarget.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewTarget.ts deleted file mode 100644 index 640a5d8da8f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ReviewTarget.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReviewTarget = - | { type: "uncommittedChanges" } - | { type: "baseBranch"; branch: string } - | { - type: "commit"; - sha: string; - /** - * Optional human-readable label (e.g., commit subject) for UIs. - */ - title: string | null; - } - | { type: "custom"; instructions: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SandboxMode.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SandboxMode.ts deleted file mode 100644 index b8cf4326b98..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SandboxMode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SandboxPolicy.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SandboxPolicy.ts deleted file mode 100644 index 7afe44eb15a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SandboxPolicy.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { NetworkAccess } from "./NetworkAccess.js"; - -export type SandboxPolicy = - | { type: "dangerFullAccess" } - | { type: "readOnly"; networkAccess: boolean } - | { type: "externalSandbox"; networkAccess: NetworkAccess } - | { - type: "workspaceWrite"; - writableRoots: Array; - networkAccess: boolean; - excludeTmpdirEnvVar: boolean; - excludeSlashTmp: boolean; - }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SandboxWorkspaceWrite.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SandboxWorkspaceWrite.ts deleted file mode 100644 index c7e4cb6072f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SandboxWorkspaceWrite.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SandboxWorkspaceWrite = { - writable_roots: Array; - network_access: boolean; - exclude_tmpdir_env_var: boolean; - exclude_slash_tmp: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SendAddCreditsNudgeEmailParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SendAddCreditsNudgeEmailParams.ts deleted file mode 100644 index fa699d893e1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SendAddCreditsNudgeEmailParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType.js"; - -export type SendAddCreditsNudgeEmailParams = { creditType: AddCreditsNudgeCreditType }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SendAddCreditsNudgeEmailResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SendAddCreditsNudgeEmailResponse.ts deleted file mode 100644 index 2da8ace4c96..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SendAddCreditsNudgeEmailResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus.js"; - -export type SendAddCreditsNudgeEmailResponse = { status: AddCreditsNudgeEmailStatus }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ServerRequestResolvedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ServerRequestResolvedNotification.ts deleted file mode 100644 index 80fdbfbf7f8..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ServerRequestResolvedNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RequestId } from "../RequestId.js"; - -export type ServerRequestResolvedNotification = { threadId: string; requestId: RequestId }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SessionMigration.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SessionMigration.ts deleted file mode 100644 index 79a08cb8143..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SessionMigration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SessionMigration = { path: string; cwd: string; title: string | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SessionSource.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SessionSource.ts deleted file mode 100644 index 37ed283f396..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SessionSource.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SubAgentSource } from "../SubAgentSource.js"; - -export type SessionSource = - | "cli" - | "vscode" - | "exec" - | "appServer" - | { custom: string } - | { subAgent: SubAgentSource } - | "unknown"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillDependencies.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillDependencies.ts deleted file mode 100644 index 8d915861bf3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillDependencies.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SkillToolDependency } from "./SkillToolDependency.js"; - -export type SkillDependencies = { tools: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillErrorInfo.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillErrorInfo.ts deleted file mode 100644 index 5d7edccd0f1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillErrorInfo.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SkillErrorInfo = { path: string; message: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillInterface.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillInterface.ts deleted file mode 100644 index 7db9e8eaea5..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillInterface.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type SkillInterface = { - displayName?: string; - shortDescription?: string; - iconSmall?: AbsolutePathBuf; - iconLarge?: AbsolutePathBuf; - brandColor?: string; - defaultPrompt?: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillMetadata.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillMetadata.ts deleted file mode 100644 index 657b0a860c7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillMetadata.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { SkillDependencies } from "./SkillDependencies.js"; -import type { SkillInterface } from "./SkillInterface.js"; -import type { SkillScope } from "./SkillScope.js"; - -export type SkillMetadata = { - name: string; - description: string; - /** - * Legacy short_description from SKILL.md. Prefer SKILL.json interface.short_description. - */ - shortDescription?: string; - interface?: SkillInterface; - dependencies?: SkillDependencies; - path: AbsolutePathBuf; - scope: SkillScope; - enabled: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillScope.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillScope.ts deleted file mode 100644 index 997006f5b83..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillScope.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SkillScope = "user" | "repo" | "system" | "admin"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillSummary.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillSummary.ts deleted file mode 100644 index c4bb4c57707..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillSummary.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { SkillInterface } from "./SkillInterface.js"; - -export type SkillSummary = { - name: string; - description: string; - shortDescription: string | null; - interface: SkillInterface | null; - path: AbsolutePathBuf | null; - enabled: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillToolDependency.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillToolDependency.ts deleted file mode 100644 index 36a30a113b4..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillToolDependency.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SkillToolDependency = { - type: string; - value: string; - description?: string; - transport?: string; - command?: string; - url?: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsChangedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsChangedNotification.ts deleted file mode 100644 index 23ed93a5ece..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsChangedNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Notification emitted when watched local skill files change. - * - * Treat this as an invalidation signal and re-run `skills/list` with the - * client's current parameters when refreshed skill metadata is needed. - */ -export type SkillsChangedNotification = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsConfigWriteParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsConfigWriteParams.ts deleted file mode 100644 index 542eeec1c39..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsConfigWriteParams.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type SkillsConfigWriteParams = { - /** - * Path-based selector. - */ - path?: AbsolutePathBuf | null; - /** - * Name-based selector. - */ - name?: string | null; - enabled: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsConfigWriteResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsConfigWriteResponse.ts deleted file mode 100644 index ba5231919bc..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsConfigWriteResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SkillsConfigWriteResponse = { effectiveEnabled: boolean }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListEntry.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListEntry.ts deleted file mode 100644 index 4b1197a26e4..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListEntry.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SkillErrorInfo } from "./SkillErrorInfo.js"; -import type { SkillMetadata } from "./SkillMetadata.js"; - -export type SkillsListEntry = { - cwd: string; - skills: Array; - errors: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListExtraRootsForCwd.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListExtraRootsForCwd.ts deleted file mode 100644 index 00f6e857242..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListExtraRootsForCwd.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SkillsListExtraRootsForCwd = { cwd: string; extraUserRoots: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListParams.ts deleted file mode 100644 index a1d82190e44..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListParams.ts +++ /dev/null @@ -1,19 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SkillsListExtraRootsForCwd } from "./SkillsListExtraRootsForCwd.js"; - -export type SkillsListParams = { - /** - * When empty, defaults to the current session working directory. - */ - cwds?: Array; - /** - * When true, bypass the skills cache and re-scan skills from disk. - */ - forceReload?: boolean; - /** - * Optional per-cwd extra roots to scan as user-scoped skills. - */ - perCwdExtraUserRoots?: Array | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListResponse.ts deleted file mode 100644 index 61e4493b778..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SkillsListResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SkillsListEntry } from "./SkillsListEntry.js"; - -export type SkillsListResponse = { data: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SortDirection.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SortDirection.ts deleted file mode 100644 index d8597a46ea7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SortDirection.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SortDirection = "asc" | "desc"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SubagentMigration.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/SubagentMigration.ts deleted file mode 100644 index b361f8be921..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/SubagentMigration.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SubagentMigration = { name: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TerminalInteractionNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TerminalInteractionNotification.ts deleted file mode 100644 index 17f49787df3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TerminalInteractionNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TerminalInteractionNotification = { - threadId: string; - turnId: string; - itemId: string; - processId: string; - stdin: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TextElement.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TextElement.ts deleted file mode 100644 index 0a4abd273af..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TextElement.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ByteRange } from "./ByteRange.js"; - -export type TextElement = { - /** - * Byte range in the parent `text` buffer that this element occupies. - */ - byteRange: ByteRange; - /** - * Optional human-readable placeholder for the element, displayed in the UI. - */ - placeholder: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TextPosition.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TextPosition.ts deleted file mode 100644 index fb6c6457c7e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TextPosition.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TextPosition = { - /** - * 1-based line number. - */ - line: number; - /** - * 1-based column number (in Unicode scalar values). - */ - column: number; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TextRange.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TextRange.ts deleted file mode 100644 index 6e061b5efd0..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TextRange.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TextPosition } from "./TextPosition.js"; - -export type TextRange = { start: TextPosition; end: TextPosition }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/Thread.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/Thread.ts deleted file mode 100644 index a6c5fca1626..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/Thread.ts +++ /dev/null @@ -1,79 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { GitInfo } from "./GitInfo.js"; -import type { SessionSource } from "./SessionSource.js"; -import type { ThreadStatus } from "./ThreadStatus.js"; -import type { Turn } from "./Turn.js"; - -export type Thread = { - id: string; - /** - * Source thread id when this thread was created by forking another thread. - */ - forkedFromId: string | null; - /** - * Usually the first user message in the thread, if available. - */ - preview: string; - /** - * Whether the thread is ephemeral and should not be materialized on disk. - */ - ephemeral: boolean; - /** - * Model provider used for this thread (for example, 'openai'). - */ - modelProvider: string; - /** - * Unix timestamp (in seconds) when the thread was created. - */ - createdAt: number; - /** - * Unix timestamp (in seconds) when the thread was last updated. - */ - updatedAt: number; - /** - * Current runtime status for the thread. - */ - status: ThreadStatus; - /** - * [UNSTABLE] Path to the thread on disk. - */ - path: string | null; - /** - * Working directory captured for the thread. - */ - cwd: AbsolutePathBuf; - /** - * Version of the CLI that created the thread. - */ - cliVersion: string; - /** - * Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.). - */ - source: SessionSource; - /** - * Optional random unique nickname assigned to an AgentControl-spawned sub-agent. - */ - agentNickname: string | null; - /** - * Optional role (agent_role) assigned to an AgentControl-spawned sub-agent. - */ - agentRole: string | null; - /** - * Optional Git metadata captured when the thread was created. - */ - gitInfo: GitInfo | null; - /** - * Optional user-facing thread title. - */ - name: string | null; - /** - * Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` - * (when `includeTurns` is true) responses. - * For all other responses and notifications returning a Thread, - * the turns field will be an empty list. - */ - turns: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadActiveFlag.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadActiveFlag.ts deleted file mode 100644 index 73c875a00d8..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadActiveFlag.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadActiveFlag = "waitingOnApproval" | "waitingOnUserInput"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadApproveGuardianDeniedActionParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadApproveGuardianDeniedActionParams.ts deleted file mode 100644 index 336709464a0..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadApproveGuardianDeniedActionParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; - -export type ThreadApproveGuardianDeniedActionParams = { - threadId: string; - /** - * Serialized `codex_protocol::protocol::GuardianAssessmentEvent`. - */ - event: JsonValue; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadApproveGuardianDeniedActionResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadApproveGuardianDeniedActionResponse.ts deleted file mode 100644 index 856bb28cfb1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadApproveGuardianDeniedActionResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadApproveGuardianDeniedActionResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadArchiveParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadArchiveParams.ts deleted file mode 100644 index 81d1d47eaf8..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadArchiveParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadArchiveParams = { threadId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadArchiveResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadArchiveResponse.ts deleted file mode 100644 index b5954268e3e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadArchiveResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadArchiveResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadArchivedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadArchivedNotification.ts deleted file mode 100644 index a2e81a708bd..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadArchivedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadArchivedNotification = { threadId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadBackgroundTerminalsCleanParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadBackgroundTerminalsCleanParams.ts deleted file mode 100644 index 0b8f1811274..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadBackgroundTerminalsCleanParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadBackgroundTerminalsCleanParams = { threadId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadBackgroundTerminalsCleanResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadBackgroundTerminalsCleanResponse.ts deleted file mode 100644 index f531fe0e24d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadBackgroundTerminalsCleanResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadBackgroundTerminalsCleanResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadClosedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadClosedNotification.ts deleted file mode 100644 index 9efd8c21899..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadClosedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadClosedNotification = { threadId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadCompactStartParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadCompactStartParams.ts deleted file mode 100644 index bdb894f295a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadCompactStartParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadCompactStartParams = { threadId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadCompactStartResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadCompactStartResponse.ts deleted file mode 100644 index 3794feb270e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadCompactStartResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadCompactStartResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadDecrementElicitationParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadDecrementElicitationParams.ts deleted file mode 100644 index 3dcdceb26c3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadDecrementElicitationParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Parameters for `thread/decrement_elicitation`. - */ -export type ThreadDecrementElicitationParams = { - /** - * Thread whose out-of-band elicitation counter should be decremented. - */ - threadId: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadDecrementElicitationResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadDecrementElicitationResponse.ts deleted file mode 100644 index d0f20a25456..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadDecrementElicitationResponse.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Response for `thread/decrement_elicitation`. - */ -export type ThreadDecrementElicitationResponse = { - /** - * Current out-of-band elicitation count after the decrement. - */ - count: bigint; - /** - * Whether timeout accounting remains paused after applying the decrement. - */ - paused: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadForkParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadForkParams.ts deleted file mode 100644 index 928b244589c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadForkParams.ts +++ /dev/null @@ -1,63 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -import type { JsonValue } from "../serde_json/JsonValue.js"; -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ServiceTier } from "../ServiceTier.js"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer.js"; -import type { AskForApproval } from "./AskForApproval.js"; -import type { PermissionProfileSelectionParams } from "./PermissionProfileSelectionParams.js"; -import type { SandboxMode } from "./SandboxMode.js"; - -/** - * There are two ways to fork a thread: - * 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. - * 2. By path: load the thread from disk by path and fork it into a new thread. - * - * If using path, the thread_id param will be ignored. - * - * Prefer using thread_id whenever possible. - */ -export type ThreadForkParams = { - threadId: string; - /** - * [UNSTABLE] Specify the rollout path to fork from. - * If specified, the thread_id param will be ignored. - */ - path?: string | null; - /** - * Configuration overrides for the forked thread, if any. - */ - model?: string | null; - modelProvider?: string | null; - serviceTier?: ServiceTier | null; - cwd?: string | null; - approvalPolicy?: AskForApproval | null; - /** - * Override where approval requests are routed for review on this thread - * and subsequent turns. - */ - approvalsReviewer?: ApprovalsReviewer | null; - sandbox?: SandboxMode | null; - /** - * Named profile selection for the forked thread. Cannot be combined with - * `sandbox`. Use bounded `modifications` for supported thread - * adjustments instead of replacing the full permissions profile. - */ - permissions?: PermissionProfileSelectionParams | null; - config?: { [key in string]?: JsonValue } | null; - baseInstructions?: string | null; - developerInstructions?: string | null; - ephemeral?: boolean; - /** - * When true, return only thread metadata and live fork state without - * populating `thread.turns`. This is useful when the client plans to call - * `thread/turns/list` immediately after forking. - */ - excludeTurns?: boolean; - /** - * If true, persist additional EventMsg variants to the rollout file. - * However, `thread/read`, `thread/resume`, and `thread/fork` still only - * return the limited form of thread history for scalability reasons. - */ - persistExtendedHistory: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadForkResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadForkResponse.ts deleted file mode 100644 index 742aad989dd..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadForkResponse.ts +++ /dev/null @@ -1,46 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { ReasoningEffort } from "../ReasoningEffort.js"; -import type { ServiceTier } from "../ServiceTier.js"; -import type { ActivePermissionProfile } from "./ActivePermissionProfile.js"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer.js"; -import type { AskForApproval } from "./AskForApproval.js"; -import type { PermissionProfile } from "./PermissionProfile.js"; -import type { SandboxPolicy } from "./SandboxPolicy.js"; -import type { Thread } from "./Thread.js"; - -export type ThreadForkResponse = { - thread: Thread; - model: string; - modelProvider: string; - serviceTier: ServiceTier | null; - cwd: AbsolutePathBuf; - /** - * Instruction source files currently loaded for this thread. - */ - instructionSources: Array; - approvalPolicy: AskForApproval; - /** - * Reviewer currently used for approval requests on this thread. - */ - approvalsReviewer: ApprovalsReviewer; - /** - * Legacy sandbox policy retained for compatibility. Experimental clients - * should prefer `permissionProfile` when they need exact runtime - * permissions. - */ - sandbox: SandboxPolicy; - /** - * Full active permissions for this thread. `activePermissionProfile` - * carries display/provenance metadata for this runtime profile. - */ - permissionProfile: PermissionProfile | null; - /** - * Named or implicit built-in profile that produced the active - * permissions, when known. - */ - activePermissionProfile: ActivePermissionProfile | null; - reasoningEffort: ReasoningEffort | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoal.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoal.ts deleted file mode 100644 index 12605c385c4..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoal.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadGoalStatus } from "./ThreadGoalStatus.js"; - -export type ThreadGoal = { - threadId: string; - objective: string; - status: ThreadGoalStatus; - tokenBudget: number | null; - tokensUsed: number; - timeUsedSeconds: number; - createdAt: number; - updatedAt: number; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalClearParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalClearParams.ts deleted file mode 100644 index f12b4b8c126..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalClearParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadGoalClearParams = { threadId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalClearResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalClearResponse.ts deleted file mode 100644 index 3d17dd9925f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalClearResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadGoalClearResponse = { cleared: boolean }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalClearedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalClearedNotification.ts deleted file mode 100644 index ecb15e4e77d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalClearedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadGoalClearedNotification = { threadId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalGetParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalGetParams.ts deleted file mode 100644 index 334444e1a65..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalGetParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadGoalGetParams = { threadId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalGetResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalGetResponse.ts deleted file mode 100644 index 98820d18b52..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalGetResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadGoal } from "./ThreadGoal.js"; - -export type ThreadGoalGetResponse = { goal: ThreadGoal | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalSetParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalSetParams.ts deleted file mode 100644 index 834aded532b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalSetParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadGoalStatus } from "./ThreadGoalStatus.js"; - -export type ThreadGoalSetParams = { - threadId: string; - objective?: string | null; - status?: ThreadGoalStatus | null; - tokenBudget?: number | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalSetResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalSetResponse.ts deleted file mode 100644 index 9b369f4a88e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalSetResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadGoal } from "./ThreadGoal.js"; - -export type ThreadGoalSetResponse = { goal: ThreadGoal }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalStatus.ts deleted file mode 100644 index 7a4bf332fb0..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadGoalStatus = "active" | "paused" | "budgetLimited" | "complete"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalUpdatedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalUpdatedNotification.ts deleted file mode 100644 index 80276692ecb..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadGoalUpdatedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadGoal } from "./ThreadGoal.js"; - -export type ThreadGoalUpdatedNotification = { - threadId: string; - turnId: string | null; - goal: ThreadGoal; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadIncrementElicitationParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadIncrementElicitationParams.ts deleted file mode 100644 index 12208cc34ac..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadIncrementElicitationParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Parameters for `thread/increment_elicitation`. - */ -export type ThreadIncrementElicitationParams = { - /** - * Thread whose out-of-band elicitation counter should be incremented. - */ - threadId: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadIncrementElicitationResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadIncrementElicitationResponse.ts deleted file mode 100644 index eed5c35dcbb..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadIncrementElicitationResponse.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Response for `thread/increment_elicitation`. - */ -export type ThreadIncrementElicitationResponse = { - /** - * Current out-of-band elicitation count after the increment. - */ - count: bigint; - /** - * Whether timeout accounting is paused after applying the increment. - */ - paused: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadInjectItemsParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadInjectItemsParams.ts deleted file mode 100644 index 143c73e5ef5..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadInjectItemsParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; - -export type ThreadInjectItemsParams = { - threadId: string; - /** - * Raw Responses API items to append to the thread's model-visible history. - */ - items: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadInjectItemsResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadInjectItemsResponse.ts deleted file mode 100644 index 60dcf0d0b3d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadInjectItemsResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadInjectItemsResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadItem.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadItem.ts deleted file mode 100644 index 233162b0cee..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadItem.ts +++ /dev/null @@ -1,156 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { MessagePhase } from "../MessagePhase.js"; -import type { ReasoningEffort } from "../ReasoningEffort.js"; -import type { JsonValue } from "../serde_json/JsonValue.js"; -import type { CollabAgentState } from "./CollabAgentState.js"; -import type { CollabAgentTool } from "./CollabAgentTool.js"; -import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus.js"; -import type { CommandAction } from "./CommandAction.js"; -import type { CommandExecutionSource } from "./CommandExecutionSource.js"; -import type { CommandExecutionStatus } from "./CommandExecutionStatus.js"; -import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem.js"; -import type { DynamicToolCallStatus } from "./DynamicToolCallStatus.js"; -import type { FileUpdateChange } from "./FileUpdateChange.js"; -import type { HookPromptFragment } from "./HookPromptFragment.js"; -import type { McpToolCallError } from "./McpToolCallError.js"; -import type { McpToolCallResult } from "./McpToolCallResult.js"; -import type { McpToolCallStatus } from "./McpToolCallStatus.js"; -import type { MemoryCitation } from "./MemoryCitation.js"; -import type { PatchApplyStatus } from "./PatchApplyStatus.js"; -import type { UserInput } from "./UserInput.js"; -import type { WebSearchAction } from "./WebSearchAction.js"; - -export type ThreadItem = - | { type: "userMessage"; id: string; content: Array } - | { type: "hookPrompt"; id: string; fragments: Array } - | { - type: "agentMessage"; - id: string; - text: string; - phase: MessagePhase | null; - memoryCitation: MemoryCitation | null; - } - | { type: "plan"; id: string; text: string } - | { type: "reasoning"; id: string; summary: Array; content: Array } - | { - type: "commandExecution"; - id: string; - /** - * The command to be executed. - */ - command: string; - /** - * The command's working directory. - */ - cwd: AbsolutePathBuf; - /** - * Identifier for the underlying PTY process (when available). - */ - processId: string | null; - source: CommandExecutionSource; - status: CommandExecutionStatus; - /** - * A best-effort parsing of the command to understand the action(s) it will perform. - * This returns a list of CommandAction objects because a single shell command may - * be composed of many commands piped together. - */ - commandActions: Array; - /** - * The command's output, aggregated from stdout and stderr. - */ - aggregatedOutput: string | null; - /** - * The command's exit code. - */ - exitCode: number | null; - /** - * The duration of the command execution in milliseconds. - */ - durationMs: number | null; - } - | { type: "fileChange"; id: string; changes: Array; status: PatchApplyStatus } - | { - type: "mcpToolCall"; - id: string; - server: string; - tool: string; - status: McpToolCallStatus; - arguments: JsonValue; - mcpAppResourceUri?: string; - result: McpToolCallResult | null; - error: McpToolCallError | null; - /** - * The duration of the MCP tool call in milliseconds. - */ - durationMs: number | null; - } - | { - type: "dynamicToolCall"; - id: string; - namespace: string | null; - tool: string; - arguments: JsonValue; - status: DynamicToolCallStatus; - contentItems: Array | null; - success: boolean | null; - /** - * The duration of the dynamic tool call in milliseconds. - */ - durationMs: number | null; - } - | { - type: "collabAgentToolCall"; - /** - * Unique identifier for this collab tool call. - */ - id: string; - /** - * Name of the collab tool that was invoked. - */ - tool: CollabAgentTool; - /** - * Current status of the collab tool call. - */ - status: CollabAgentToolCallStatus; - /** - * Thread ID of the agent issuing the collab request. - */ - senderThreadId: string; - /** - * Thread ID of the receiving agent, when applicable. In case of spawn operation, - * this corresponds to the newly spawned agent. - */ - receiverThreadIds: Array; - /** - * Prompt text sent as part of the collab tool call, when available. - */ - prompt: string | null; - /** - * Model requested for the spawned agent, when applicable. - */ - model: string | null; - /** - * Reasoning effort requested for the spawned agent, when applicable. - */ - reasoningEffort: ReasoningEffort | null; - /** - * Last known status of the target agents, when available. - */ - agentsStates: { [key in string]?: CollabAgentState }; - } - | { type: "webSearch"; id: string; query: string; action: WebSearchAction | null } - | { type: "imageView"; id: string; path: AbsolutePathBuf } - | { - type: "imageGeneration"; - id: string; - status: string; - revisedPrompt: string | null; - result: string; - savedPath?: AbsolutePathBuf; - } - | { type: "enteredReviewMode"; id: string; review: string } - | { type: "exitedReviewMode"; id: string; review: string } - | { type: "contextCompaction"; id: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadListParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadListParams.ts deleted file mode 100644 index 589101d632b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadListParams.ts +++ /dev/null @@ -1,55 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SortDirection } from "./SortDirection.js"; -import type { ThreadSortKey } from "./ThreadSortKey.js"; -import type { ThreadSourceKind } from "./ThreadSourceKind.js"; - -export type ThreadListParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to a reasonable server-side value. - */ - limit?: number | null; - /** - * Optional sort key; defaults to created_at. - */ - sortKey?: ThreadSortKey | null; - /** - * Optional sort direction; defaults to descending (newest first). - */ - sortDirection?: SortDirection | null; - /** - * Optional provider filter; when set, only sessions recorded under these - * providers are returned. When present but empty, includes all providers. - */ - modelProviders?: Array | null; - /** - * Optional source filter; when set, only sessions from these source kinds - * are returned. When omitted or empty, defaults to interactive sources. - */ - sourceKinds?: Array | null; - /** - * Optional archived filter; when set to true, only archived threads are returned. - * If false or null, only non-archived threads are returned. - */ - archived?: boolean | null; - /** - * Optional cwd filter or filters; when set, only threads whose session cwd - * exactly matches one of these paths are returned. - */ - cwd?: string | Array | null; - /** - * If true, return from the state DB without scanning JSONL rollouts to - * repair thread metadata. Omitted or false preserves scan-and-repair - * behavior. - */ - useStateDbOnly?: boolean; - /** - * Optional substring filter for the extracted thread title. - */ - searchTerm?: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadListResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadListResponse.ts deleted file mode 100644 index 95815064359..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadListResponse.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread.js"; - -export type ThreadListResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * if None, there are no more items to return. - */ - nextCursor: string | null; - /** - * Opaque cursor to pass as `cursor` when reversing `sortDirection`. - * This is only populated when the page contains at least one thread. - * Use it with the opposite `sortDirection`; for timestamp sorts it anchors - * at the start of the page timestamp so same-second updates are not skipped. - */ - backwardsCursor: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadLoadedListParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadLoadedListParams.ts deleted file mode 100644 index 7269540838d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadLoadedListParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadLoadedListParams = { - /** - * Opaque pagination cursor returned by a previous call. - */ - cursor?: string | null; - /** - * Optional page size; defaults to no limit. - */ - limit?: number | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadLoadedListResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadLoadedListResponse.ts deleted file mode 100644 index 5555ac2d771..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadLoadedListResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadLoadedListResponse = { - /** - * Thread ids for sessions currently loaded in memory. - */ - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last item. - * if None, there are no more items to return. - */ - nextCursor: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMemoryModeSetParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMemoryModeSetParams.ts deleted file mode 100644 index 9eee31b8af7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMemoryModeSetParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadMemoryMode } from "../ThreadMemoryMode.js"; - -export type ThreadMemoryModeSetParams = { threadId: string; mode: ThreadMemoryMode }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMemoryModeSetResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMemoryModeSetResponse.ts deleted file mode 100644 index 49b42fd9add..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMemoryModeSetResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadMemoryModeSetResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMetadataGitInfoUpdateParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMetadataGitInfoUpdateParams.ts deleted file mode 100644 index 391427c4224..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMetadataGitInfoUpdateParams.ts +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadMetadataGitInfoUpdateParams = { - /** - * Omit to leave the stored commit unchanged, set to `null` to clear it, - * or provide a non-empty string to replace it. - */ - sha?: string | null; - /** - * Omit to leave the stored branch unchanged, set to `null` to clear it, - * or provide a non-empty string to replace it. - */ - branch?: string | null; - /** - * Omit to leave the stored origin URL unchanged, set to `null` to clear it, - * or provide a non-empty string to replace it. - */ - originUrl?: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMetadataUpdateParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMetadataUpdateParams.ts deleted file mode 100644 index d6a41f3bad7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMetadataUpdateParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadMetadataGitInfoUpdateParams } from "./ThreadMetadataGitInfoUpdateParams.js"; - -export type ThreadMetadataUpdateParams = { - threadId: string; - /** - * Patch the stored Git metadata for this thread. - * Omit a field to leave it unchanged, set it to `null` to clear it, or - * provide a string to replace the stored value. - */ - gitInfo?: ThreadMetadataGitInfoUpdateParams | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMetadataUpdateResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMetadataUpdateResponse.ts deleted file mode 100644 index 17574bd2df3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadMetadataUpdateResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread.js"; - -export type ThreadMetadataUpdateResponse = { thread: Thread }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadNameUpdatedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadNameUpdatedNotification.ts deleted file mode 100644 index 1bee8b50aa2..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadNameUpdatedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadNameUpdatedNotification = { threadId: string; threadName?: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadReadParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadReadParams.ts deleted file mode 100644 index 2159bd89d5e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadReadParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadReadParams = { - threadId: string; - /** - * When true, include turns and their items from rollout history. - */ - includeTurns: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadReadResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadReadResponse.ts deleted file mode 100644 index 2c6e207f682..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadReadResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread.js"; - -export type ThreadReadResponse = { thread: Thread }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendAudioParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendAudioParams.ts deleted file mode 100644 index 83f253bffd0..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendAudioParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk.js"; - -/** - * EXPERIMENTAL - append audio input to thread realtime. - */ -export type ThreadRealtimeAppendAudioParams = { threadId: string; audio: ThreadRealtimeAudioChunk }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendAudioResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendAudioResponse.ts deleted file mode 100644 index 063e8cba783..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendAudioResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - response for appending realtime audio input. - */ -export type ThreadRealtimeAppendAudioResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendTextParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendTextParams.ts deleted file mode 100644 index 7be6e7670d3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendTextParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - append text input to thread realtime. - */ -export type ThreadRealtimeAppendTextParams = { threadId: string; text: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendTextResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendTextResponse.ts deleted file mode 100644 index 1fb9f0738fd..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAppendTextResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - response for appending realtime text input. - */ -export type ThreadRealtimeAppendTextResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAudioChunk.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAudioChunk.ts deleted file mode 100644 index 61f4414f422..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeAudioChunk.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - thread realtime audio chunk. - */ -export type ThreadRealtimeAudioChunk = { - data: string; - sampleRate: number; - numChannels: number; - samplesPerChannel: number | null; - itemId: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeClosedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeClosedNotification.ts deleted file mode 100644 index 7f7e837e06f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeClosedNotification.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - emitted when thread realtime transport closes. - */ -export type ThreadRealtimeClosedNotification = { threadId: string; reason: string | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeErrorNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeErrorNotification.ts deleted file mode 100644 index 3c1e9d408a9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeErrorNotification.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - emitted when thread realtime encounters an error. - */ -export type ThreadRealtimeErrorNotification = { threadId: string; message: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeItemAddedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeItemAddedNotification.ts deleted file mode 100644 index 3195e4a02b8..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeItemAddedNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { JsonValue } from "../serde_json/JsonValue.js"; - -/** - * EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend. - */ -export type ThreadRealtimeItemAddedNotification = { threadId: string; item: JsonValue }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeListVoicesParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeListVoicesParams.ts deleted file mode 100644 index b456d89c26e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeListVoicesParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - list voices supported by thread realtime. - */ -export type ThreadRealtimeListVoicesParams = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeListVoicesResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeListVoicesResponse.ts deleted file mode 100644 index a83f67ed9c3..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeListVoicesResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RealtimeVoicesList } from "../RealtimeVoicesList.js"; - -/** - * EXPERIMENTAL - response for listing supported realtime voices. - */ -export type ThreadRealtimeListVoicesResponse = { voices: RealtimeVoicesList }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeOutputAudioDeltaNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeOutputAudioDeltaNotification.ts deleted file mode 100644 index 85946bd1b6f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeOutputAudioDeltaNotification.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk.js"; - -/** - * EXPERIMENTAL - streamed output audio emitted by thread realtime. - */ -export type ThreadRealtimeOutputAudioDeltaNotification = { - threadId: string; - audio: ThreadRealtimeAudioChunk; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeSdpNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeSdpNotification.ts deleted file mode 100644 index 38af1bc9d8c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeSdpNotification.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session. - */ -export type ThreadRealtimeSdpNotification = { threadId: string; sdp: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartParams.ts deleted file mode 100644 index fc1878ebabd..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartParams.ts +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RealtimeOutputModality } from "../RealtimeOutputModality.js"; -import type { RealtimeVoice } from "../RealtimeVoice.js"; -import type { ThreadRealtimeStartTransport } from "./ThreadRealtimeStartTransport.js"; - -/** - * EXPERIMENTAL - start a thread-scoped realtime session. - */ -export type ThreadRealtimeStartParams = { - threadId: string; - /** - * Selects text or audio output for the realtime session. Transport and voice stay - * independent so clients can choose how they connect separately from what the model emits. - */ - outputModality: RealtimeOutputModality; - prompt?: string | null; - realtimeSessionId?: string | null; - transport?: ThreadRealtimeStartTransport | null; - voice?: RealtimeVoice | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartResponse.ts deleted file mode 100644 index 56254564256..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - response for starting thread realtime. - */ -export type ThreadRealtimeStartResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartTransport.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartTransport.ts deleted file mode 100644 index 82802df1128..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartTransport.ts +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - transport used by thread realtime. - */ -export type ThreadRealtimeStartTransport = - | { type: "websocket" } - | { - type: "webrtc"; - /** - * SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the - * realtime events data channel. - */ - sdp: string; - }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartedNotification.ts deleted file mode 100644 index 730debc09ed..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStartedNotification.ts +++ /dev/null @@ -1,13 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RealtimeConversationVersion } from "../RealtimeConversationVersion.js"; - -/** - * EXPERIMENTAL - emitted when thread realtime startup is accepted. - */ -export type ThreadRealtimeStartedNotification = { - threadId: string; - realtimeSessionId: string | null; - version: RealtimeConversationVersion; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStopParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStopParams.ts deleted file mode 100644 index a74f6d8b204..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStopParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - stop thread realtime. - */ -export type ThreadRealtimeStopParams = { threadId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStopResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStopResponse.ts deleted file mode 100644 index c87f4402db5..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeStopResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - response for stopping thread realtime. - */ -export type ThreadRealtimeStopResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeTranscriptDeltaNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeTranscriptDeltaNotification.ts deleted file mode 100644 index 0e6e0f4344a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeTranscriptDeltaNotification.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - flat transcript delta emitted whenever realtime - * transcript text changes. - */ -export type ThreadRealtimeTranscriptDeltaNotification = { - threadId: string; - role: string; - /** - * Live transcript delta from the realtime event. - */ - delta: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeTranscriptDoneNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeTranscriptDoneNotification.ts deleted file mode 100644 index eafdbe76743..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRealtimeTranscriptDoneNotification.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL - final transcript text emitted when realtime completes - * a transcript part. - */ -export type ThreadRealtimeTranscriptDoneNotification = { - threadId: string; - role: string; - /** - * Final complete text for the transcript part. - */ - text: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadResumeParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadResumeParams.ts deleted file mode 100644 index c04230b6722..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadResumeParams.ts +++ /dev/null @@ -1,73 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Personality } from "../Personality.js"; -import type { ResponseItem } from "../ResponseItem.js"; -import type { JsonValue } from "../serde_json/JsonValue.js"; -import type { ServiceTier } from "../ServiceTier.js"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer.js"; -import type { AskForApproval } from "./AskForApproval.js"; -import type { PermissionProfileSelectionParams } from "./PermissionProfileSelectionParams.js"; -import type { SandboxMode } from "./SandboxMode.js"; - -/** - * There are three ways to resume a thread: - * 1. By thread_id: load the thread from disk by thread_id and resume it. - * 2. By history: instantiate the thread from memory and resume it. - * 3. By path: load the thread from disk by path and resume it. - * - * The precedence is: history > path > thread_id. - * If using history or path, the thread_id param will be ignored. - * - * Prefer using thread_id whenever possible. - */ -export type ThreadResumeParams = { - threadId: string; - /** - * [UNSTABLE] FOR CODEX CLOUD - DO NOT USE. - * If specified, the thread will be resumed with the provided history - * instead of loaded from disk. - */ - history?: Array | null; - /** - * [UNSTABLE] Specify the rollout path to resume from. - * If specified, the thread_id param will be ignored. - */ - path?: string | null; - /** - * Configuration overrides for the resumed thread, if any. - */ - model?: string | null; - modelProvider?: string | null; - serviceTier?: ServiceTier | null; - cwd?: string | null; - approvalPolicy?: AskForApproval | null; - /** - * Override where approval requests are routed for review on this thread - * and subsequent turns. - */ - approvalsReviewer?: ApprovalsReviewer | null; - sandbox?: SandboxMode | null; - /** - * Named profile selection for the resumed thread. Cannot be combined - * with `sandbox`. Use bounded `modifications` for supported thread - * adjustments instead of replacing the full permissions profile. - */ - permissions?: PermissionProfileSelectionParams | null; - config?: { [key in string]?: JsonValue } | null; - baseInstructions?: string | null; - developerInstructions?: string | null; - personality?: Personality | null; - /** - * When true, return only thread metadata and live-resume state without - * populating `thread.turns`. This is useful when the client plans to call - * `thread/turns/list` immediately after resuming. - */ - excludeTurns?: boolean; - /** - * If true, persist additional EventMsg variants to the rollout file. - * However, `thread/read`, `thread/resume`, and `thread/fork` still only - * return the limited form of thread history for scalability reasons. - */ - persistExtendedHistory: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadResumeResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadResumeResponse.ts deleted file mode 100644 index 9db70479b24..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadResumeResponse.ts +++ /dev/null @@ -1,46 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { ReasoningEffort } from "../ReasoningEffort.js"; -import type { ServiceTier } from "../ServiceTier.js"; -import type { ActivePermissionProfile } from "./ActivePermissionProfile.js"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer.js"; -import type { AskForApproval } from "./AskForApproval.js"; -import type { PermissionProfile } from "./PermissionProfile.js"; -import type { SandboxPolicy } from "./SandboxPolicy.js"; -import type { Thread } from "./Thread.js"; - -export type ThreadResumeResponse = { - thread: Thread; - model: string; - modelProvider: string; - serviceTier: ServiceTier | null; - cwd: AbsolutePathBuf; - /** - * Instruction source files currently loaded for this thread. - */ - instructionSources: Array; - approvalPolicy: AskForApproval; - /** - * Reviewer currently used for approval requests on this thread. - */ - approvalsReviewer: ApprovalsReviewer; - /** - * Legacy sandbox policy retained for compatibility. Experimental clients - * should prefer `permissionProfile` when they need exact runtime - * permissions. - */ - sandbox: SandboxPolicy; - /** - * Full active permissions for this thread. `activePermissionProfile` - * carries display/provenance metadata for this runtime profile. - */ - permissionProfile: PermissionProfile | null; - /** - * Named or implicit built-in profile that produced the active - * permissions, when known. - */ - activePermissionProfile: ActivePermissionProfile | null; - reasoningEffort: ReasoningEffort | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRollbackParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRollbackParams.ts deleted file mode 100644 index afe1bad9ffb..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRollbackParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadRollbackParams = { - threadId: string; - /** - * The number of turns to drop from the end of the thread. Must be >= 1. - * - * This only modifies the thread's history and does not revert local file changes - * that have been made by the agent. Clients are responsible for reverting these changes. - */ - numTurns: number; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRollbackResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRollbackResponse.ts deleted file mode 100644 index 5f2d0227997..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadRollbackResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread.js"; - -export type ThreadRollbackResponse = { - /** - * The updated thread after applying the rollback, with `turns` populated. - * - * The ThreadItems stored in each Turn are lossy since we explicitly do not - * persist all agent interactions, such as command executions. This is the same - * behavior as `thread/resume`. - */ - thread: Thread; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSetNameParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSetNameParams.ts deleted file mode 100644 index 17d12ee34ba..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSetNameParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadSetNameParams = { threadId: string; name: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSetNameResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSetNameResponse.ts deleted file mode 100644 index 09143d251cf..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSetNameResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadSetNameResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadShellCommandParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadShellCommandParams.ts deleted file mode 100644 index 7e7b11890e9..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadShellCommandParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadShellCommandParams = { - threadId: string; - /** - * Shell command string evaluated by the thread's configured shell. - * Unlike `command/exec`, this intentionally preserves shell syntax - * such as pipes, redirects, and quoting. This runs unsandboxed with full - * access rather than inheriting the thread sandbox policy. - */ - command: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadShellCommandResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadShellCommandResponse.ts deleted file mode 100644 index 9c54b45839d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadShellCommandResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadShellCommandResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSortKey.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSortKey.ts deleted file mode 100644 index dbf1b6c40fd..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSortKey.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadSortKey = "created_at" | "updated_at"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSourceKind.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSourceKind.ts deleted file mode 100644 index 39b188796d6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadSourceKind.ts +++ /dev/null @@ -1,15 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadSourceKind = - | "cli" - | "vscode" - | "exec" - | "appServer" - | "subAgent" - | "subAgentReview" - | "subAgentCompact" - | "subAgentThreadSpawn" - | "subAgentOther" - | "unknown"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartParams.ts deleted file mode 100644 index 5d4d1856257..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartParams.ts +++ /dev/null @@ -1,66 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Personality } from "../Personality.js"; -import type { JsonValue } from "../serde_json/JsonValue.js"; -import type { ServiceTier } from "../ServiceTier.js"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer.js"; -import type { AskForApproval } from "./AskForApproval.js"; -import type { DynamicToolSpec } from "./DynamicToolSpec.js"; -import type { PermissionProfileSelectionParams } from "./PermissionProfileSelectionParams.js"; -import type { SandboxMode } from "./SandboxMode.js"; -import type { ThreadStartSource } from "./ThreadStartSource.js"; -import type { TurnEnvironmentParams } from "./TurnEnvironmentParams.js"; - -export type ThreadStartParams = { - model?: string | null; - modelProvider?: string | null; - serviceTier?: ServiceTier | null; - cwd?: string | null; - approvalPolicy?: AskForApproval | null; - /** - * Override where approval requests are routed for review on this thread - * and subsequent turns. - */ - approvalsReviewer?: ApprovalsReviewer | null; - sandbox?: SandboxMode | null; - /** - * Named profile selection for this thread. Cannot be combined with - * `sandbox`. Use bounded `modifications` for supported turn/thread - * adjustments instead of replacing the full permissions profile. - */ - permissions?: PermissionProfileSelectionParams | null; - config?: { [key in string]?: JsonValue } | null; - serviceName?: string | null; - baseInstructions?: string | null; - developerInstructions?: string | null; - personality?: Personality | null; - ephemeral?: boolean | null; - sessionStartSource?: ThreadStartSource | null; - /** - * Optional sticky environments for this thread. - * - * Omitted selects the default environment when environment access is - * enabled. Empty disables environment access for turns that do not - * provide a turn override. Non-empty selects the first environment as the - * current turn environment. - */ - environments?: Array | null; - dynamicTools?: Array | null; - /** - * Test-only experimental field used to validate experimental gating and - * schema filtering behavior in a stable way. - */ - mockExperimentalField?: string | null; - /** - * If true, opt into emitting raw Responses API items on the event stream. - * This is for internal use only (e.g. Codex Cloud). - */ - experimentalRawEvents: boolean; - /** - * If true, persist additional EventMsg variants to the rollout file. - * However, `thread/read`, `thread/resume`, and `thread/fork` still only - * return the limited form of thread history for scalability reasons. - */ - persistExtendedHistory: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartResponse.ts deleted file mode 100644 index 019c189449b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartResponse.ts +++ /dev/null @@ -1,46 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { ReasoningEffort } from "../ReasoningEffort.js"; -import type { ServiceTier } from "../ServiceTier.js"; -import type { ActivePermissionProfile } from "./ActivePermissionProfile.js"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer.js"; -import type { AskForApproval } from "./AskForApproval.js"; -import type { PermissionProfile } from "./PermissionProfile.js"; -import type { SandboxPolicy } from "./SandboxPolicy.js"; -import type { Thread } from "./Thread.js"; - -export type ThreadStartResponse = { - thread: Thread; - model: string; - modelProvider: string; - serviceTier: ServiceTier | null; - cwd: AbsolutePathBuf; - /** - * Instruction source files currently loaded for this thread. - */ - instructionSources: Array; - approvalPolicy: AskForApproval; - /** - * Reviewer currently used for approval requests on this thread. - */ - approvalsReviewer: ApprovalsReviewer; - /** - * Legacy sandbox policy retained for compatibility. Experimental clients - * should prefer `permissionProfile` when they need exact runtime - * permissions. - */ - sandbox: SandboxPolicy; - /** - * Full active permissions for this thread. `activePermissionProfile` - * carries display/provenance metadata for this runtime profile. - */ - permissionProfile: PermissionProfile | null; - /** - * Named or implicit built-in profile that produced the active - * permissions, when known. - */ - activePermissionProfile: ActivePermissionProfile | null; - reasoningEffort: ReasoningEffort | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartSource.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartSource.ts deleted file mode 100644 index ea1b839c6ba..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartSource.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadStartSource = "startup" | "clear"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartedNotification.ts deleted file mode 100644 index 6e56f09d565..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStartedNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread.js"; - -export type ThreadStartedNotification = { thread: Thread }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStatus.ts deleted file mode 100644 index bcc3b0ae8cb..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStatus.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadActiveFlag } from "./ThreadActiveFlag.js"; - -export type ThreadStatus = - | { type: "notLoaded" } - | { type: "idle" } - | { type: "systemError" } - | { type: "active"; activeFlags: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStatusChangedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStatusChangedNotification.ts deleted file mode 100644 index 5ba79094297..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadStatusChangedNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadStatus } from "./ThreadStatus.js"; - -export type ThreadStatusChangedNotification = { threadId: string; status: ThreadStatus }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTokenUsage.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTokenUsage.ts deleted file mode 100644 index ca55ee9112f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTokenUsage.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TokenUsageBreakdown } from "./TokenUsageBreakdown.js"; - -export type ThreadTokenUsage = { - total: TokenUsageBreakdown; - last: TokenUsageBreakdown; - modelContextWindow: number | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTokenUsageUpdatedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTokenUsageUpdatedNotification.ts deleted file mode 100644 index 1881dcd5b97..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTokenUsageUpdatedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadTokenUsage } from "./ThreadTokenUsage.js"; - -export type ThreadTokenUsageUpdatedNotification = { - threadId: string; - turnId: string; - tokenUsage: ThreadTokenUsage; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTurnsListParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTurnsListParams.ts deleted file mode 100644 index 1f603cfb3b8..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTurnsListParams.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SortDirection } from "./SortDirection.js"; - -export type ThreadTurnsListParams = { - threadId: string; - /** - * Opaque cursor to pass to the next call to continue after the last turn. - */ - cursor?: string | null; - /** - * Optional turn page size. - */ - limit?: number | null; - /** - * Optional turn pagination direction; defaults to descending. - */ - sortDirection?: SortDirection | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTurnsListResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTurnsListResponse.ts deleted file mode 100644 index ad82d40fd01..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadTurnsListResponse.ts +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Turn } from "./Turn.js"; - -export type ThreadTurnsListResponse = { - data: Array; - /** - * Opaque cursor to pass to the next call to continue after the last turn. - * if None, there are no more turns to return. - */ - nextCursor: string | null; - /** - * Opaque cursor to pass as `cursor` when reversing `sortDirection`. - * This is only populated when the page contains at least one turn. - * Use it with the opposite `sortDirection` to include the anchor turn again - * and catch updates to that turn. - */ - backwardsCursor: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnarchiveParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnarchiveParams.ts deleted file mode 100644 index 9b01abc9f87..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnarchiveParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadUnarchiveParams = { threadId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnarchiveResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnarchiveResponse.ts deleted file mode 100644 index 4971de78e44..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnarchiveResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Thread } from "./Thread.js"; - -export type ThreadUnarchiveResponse = { thread: Thread }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnarchivedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnarchivedNotification.ts deleted file mode 100644 index 34a712ee066..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnarchivedNotification.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadUnarchivedNotification = { threadId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnsubscribeParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnsubscribeParams.ts deleted file mode 100644 index 0fd5aaef7ab..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnsubscribeParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadUnsubscribeParams = { threadId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnsubscribeResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnsubscribeResponse.ts deleted file mode 100644 index a5fba38a1d7..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnsubscribeResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadUnsubscribeStatus } from "./ThreadUnsubscribeStatus.js"; - -export type ThreadUnsubscribeResponse = { status: ThreadUnsubscribeStatus }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnsubscribeStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnsubscribeStatus.ts deleted file mode 100644 index 2970598dc1b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ThreadUnsubscribeStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ThreadUnsubscribeStatus = "notLoaded" | "notSubscribed" | "unsubscribed"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TokenUsageBreakdown.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TokenUsageBreakdown.ts deleted file mode 100644 index 19ed7b1968e..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TokenUsageBreakdown.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TokenUsageBreakdown = { - totalTokens: number; - inputTokens: number; - cachedInputTokens: number; - outputTokens: number; - reasoningOutputTokens: number; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputAnswer.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputAnswer.ts deleted file mode 100644 index 87e581b7cac..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputAnswer.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL. Captures a user's answer to a request_user_input question. - */ -export type ToolRequestUserInputAnswer = { answers: Array }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputOption.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputOption.ts deleted file mode 100644 index 6a92bc048da..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputOption.ts +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * EXPERIMENTAL. Defines a single selectable option for request_user_input. - */ -export type ToolRequestUserInputOption = { label: string; description: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputParams.ts deleted file mode 100644 index df67d270b6b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ToolRequestUserInputQuestion } from "./ToolRequestUserInputQuestion.js"; - -/** - * EXPERIMENTAL. Params sent with a request_user_input event. - */ -export type ToolRequestUserInputParams = { - threadId: string; - turnId: string; - itemId: string; - questions: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputQuestion.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputQuestion.ts deleted file mode 100644 index 5c9d690dd8a..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputQuestion.ts +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption.js"; - -/** - * EXPERIMENTAL. Represents one request_user_input question and its required options. - */ -export type ToolRequestUserInputQuestion = { - id: string; - header: string; - question: string; - isOther: boolean; - isSecret: boolean; - options: Array | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputResponse.ts deleted file mode 100644 index 75511716001..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolRequestUserInputResponse.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer.js"; - -/** - * EXPERIMENTAL. Response payload mapping question ids to answers. - */ -export type ToolRequestUserInputResponse = { - answers: { [key in string]?: ToolRequestUserInputAnswer }; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolsV2.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolsV2.ts deleted file mode 100644 index 4adfc8dfe9c..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/ToolsV2.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { WebSearchToolConfig } from "../WebSearchToolConfig.js"; - -export type ToolsV2 = { web_search: WebSearchToolConfig | null; view_image: boolean | null }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/Turn.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/Turn.ts deleted file mode 100644 index 9487af6c7c0..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/Turn.ts +++ /dev/null @@ -1,33 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ThreadItem } from "./ThreadItem.js"; -import type { TurnError } from "./TurnError.js"; -import type { TurnStatus } from "./TurnStatus.js"; - -export type Turn = { - id: string; - /** - * Only populated on a `thread/resume` or `thread/fork` response. - * For all other responses and notifications returning a Turn, - * the items field will be an empty list. - */ - items: Array; - status: TurnStatus; - /** - * Only populated when the Turn's status is failed. - */ - error: TurnError | null; - /** - * Unix timestamp (in seconds) when the turn started. - */ - startedAt: number | null; - /** - * Unix timestamp (in seconds) when the turn completed. - */ - completedAt: number | null; - /** - * Duration between turn start and completion in milliseconds, if known. - */ - durationMs: number | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnCompletedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnCompletedNotification.ts deleted file mode 100644 index da36e077684..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnCompletedNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Turn } from "./Turn.js"; - -export type TurnCompletedNotification = { threadId: string; turn: Turn }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnDiffUpdatedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnDiffUpdatedNotification.ts deleted file mode 100644 index 0f4bf14b15f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnDiffUpdatedNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Notification that the turn-level unified diff has changed. - * Contains the latest aggregated diff across all file changes in the turn. - */ -export type TurnDiffUpdatedNotification = { threadId: string; turnId: string; diff: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnEnvironmentParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnEnvironmentParams.ts deleted file mode 100644 index c814b1334f6..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnEnvironmentParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; - -export type TurnEnvironmentParams = { environmentId: string; cwd: AbsolutePathBuf }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnError.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnError.ts deleted file mode 100644 index c87517026c2..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnError.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CodexErrorInfo } from "./CodexErrorInfo.js"; - -export type TurnError = { - message: string; - codexErrorInfo: CodexErrorInfo | null; - additionalDetails: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnInterruptParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnInterruptParams.ts deleted file mode 100644 index 1373415ec08..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnInterruptParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TurnInterruptParams = { threadId: string; turnId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnInterruptResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnInterruptResponse.ts deleted file mode 100644 index 7ce6e35bd63..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnInterruptResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TurnInterruptResponse = Record; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnPlanStep.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnPlanStep.ts deleted file mode 100644 index 4064f46a67f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnPlanStep.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TurnPlanStepStatus } from "./TurnPlanStepStatus.js"; - -export type TurnPlanStep = { step: string; status: TurnPlanStepStatus }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnPlanStepStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnPlanStepStatus.ts deleted file mode 100644 index f6733a68853..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnPlanStepStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TurnPlanStepStatus = "pending" | "inProgress" | "completed"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnPlanUpdatedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnPlanUpdatedNotification.ts deleted file mode 100644 index fd3780bdf73..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnPlanUpdatedNotification.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TurnPlanStep } from "./TurnPlanStep.js"; - -export type TurnPlanUpdatedNotification = { - threadId: string; - turnId: string; - explanation: string | null; - plan: Array; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStartParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStartParams.ts deleted file mode 100644 index d395ba607c2..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStartParams.ts +++ /dev/null @@ -1,89 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CollaborationMode } from "../CollaborationMode.js"; -import type { Personality } from "../Personality.js"; -import type { ReasoningEffort } from "../ReasoningEffort.js"; -import type { ReasoningSummary } from "../ReasoningSummary.js"; -import type { JsonValue } from "../serde_json/JsonValue.js"; -import type { ServiceTier } from "../ServiceTier.js"; -import type { ApprovalsReviewer } from "./ApprovalsReviewer.js"; -import type { AskForApproval } from "./AskForApproval.js"; -import type { PermissionProfileSelectionParams } from "./PermissionProfileSelectionParams.js"; -import type { SandboxPolicy } from "./SandboxPolicy.js"; -import type { TurnEnvironmentParams } from "./TurnEnvironmentParams.js"; -import type { UserInput } from "./UserInput.js"; - -export type TurnStartParams = { - threadId: string; - input: Array; - /** - * Optional turn-scoped Responses API client metadata. - */ - responsesapiClientMetadata?: { [key in string]?: string } | null; - /** - * Optional turn-scoped environments. - * - * Omitted uses the thread sticky environments. Empty disables - * environment access for this turn. Non-empty selects the first - * environment as the current turn environment for this turn. - */ - environments?: Array | null; - /** - * Override the working directory for this turn and subsequent turns. - */ - cwd?: string | null; - /** - * Override the approval policy for this turn and subsequent turns. - */ - approvalPolicy?: AskForApproval | null; - /** - * Override where approval requests are routed for review on this turn and - * subsequent turns. - */ - approvalsReviewer?: ApprovalsReviewer | null; - /** - * Override the sandbox policy for this turn and subsequent turns. - */ - sandboxPolicy?: SandboxPolicy | null; - /** - * Select a named permissions profile for this turn and subsequent turns. - * Cannot be combined with `sandboxPolicy`. Use bounded `modifications` - * for supported turn adjustments instead of replacing the full - * permissions profile. - */ - permissions?: PermissionProfileSelectionParams | null; - /** - * Override the model for this turn and subsequent turns. - */ - model?: string | null; - /** - * Override the service tier for this turn and subsequent turns. - */ - serviceTier?: ServiceTier | null; - /** - * Override the reasoning effort for this turn and subsequent turns. - */ - effort?: ReasoningEffort | null; - /** - * Override the reasoning summary for this turn and subsequent turns. - */ - summary?: ReasoningSummary | null; - /** - * Override the personality for this turn and subsequent turns. - */ - personality?: Personality | null; - /** - * Optional JSON Schema used to constrain the final assistant message for - * this turn. - */ - outputSchema?: JsonValue | null; - /** - * EXPERIMENTAL - Set a pre-set collaboration mode. - * Takes precedence over model, reasoning_effort, and developer instructions if set. - * - * For `collaboration_mode.settings.developer_instructions`, `null` means - * "use the built-in instructions for the selected mode". - */ - collaborationMode?: CollaborationMode | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStartResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStartResponse.ts deleted file mode 100644 index 79e6fc98354..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStartResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Turn } from "./Turn.js"; - -export type TurnStartResponse = { turn: Turn }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStartedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStartedNotification.ts deleted file mode 100644 index cf4b5a94d5d..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStartedNotification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Turn } from "./Turn.js"; - -export type TurnStartedNotification = { threadId: string; turn: Turn }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStatus.ts deleted file mode 100644 index 476922edc20..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnSteerParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnSteerParams.ts deleted file mode 100644 index a2f7a2a0683..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnSteerParams.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { UserInput } from "./UserInput.js"; - -export type TurnSteerParams = { - threadId: string; - input: Array; - /** - * Optional turn-scoped Responses API client metadata. - */ - responsesapiClientMetadata?: { [key in string]?: string } | null; - /** - * Required active turn id precondition. The request fails when it does not - * match the currently active turn. - */ - expectedTurnId: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnSteerResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnSteerResponse.ts deleted file mode 100644 index 1ed155c1429..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/TurnSteerResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type TurnSteerResponse = { turnId: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/UserInput.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/UserInput.ts deleted file mode 100644 index d8531d3f27b..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/UserInput.ts +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TextElement } from "./TextElement.js"; - -export type UserInput = - | { - type: "text"; - text: string; - /** - * UI-defined spans within `text` used to render or persist special elements. - */ - text_elements: Array; - } - | { type: "image"; url: string } - | { type: "localImage"; path: string } - | { type: "skill"; name: string; path: string } - | { type: "mention"; name: string; path: string }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WarningNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/WarningNotification.ts deleted file mode 100644 index dc30ca05250..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WarningNotification.ts +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WarningNotification = { - /** - * Optional thread target when the warning applies to a specific thread. - */ - threadId: string | null; - /** - * Concise warning message for the user. - */ - message: string; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WebSearchAction.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/WebSearchAction.ts deleted file mode 100644 index 20801fb04c1..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WebSearchAction.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WebSearchAction = - | { type: "search"; query: string | null; queries: Array | null } - | { type: "openPage"; url: string | null } - | { type: "findInPage"; url: string | null; pattern: string | null } - | { type: "other" }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupCompletedNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupCompletedNotification.ts deleted file mode 100644 index 57deea42444..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupCompletedNotification.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode.js"; - -export type WindowsSandboxSetupCompletedNotification = { - mode: WindowsSandboxSetupMode; - success: boolean; - error: string | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupMode.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupMode.ts deleted file mode 100644 index a74bea42408..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupMode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WindowsSandboxSetupMode = "elevated" | "unelevated"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupStartParams.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupStartParams.ts deleted file mode 100644 index 9fd8992a3cf..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupStartParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf.js"; -import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode.js"; - -export type WindowsSandboxSetupStartParams = { - mode: WindowsSandboxSetupMode; - cwd?: AbsolutePathBuf | null; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupStartResponse.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupStartResponse.ts deleted file mode 100644 index 1aec14ca473..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsSandboxSetupStartResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WindowsSandboxSetupStartResponse = { started: boolean }; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsWorldWritableWarningNotification.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsWorldWritableWarningNotification.ts deleted file mode 100644 index b61824c8b3f..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WindowsWorldWritableWarningNotification.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WindowsWorldWritableWarningNotification = { - samplePaths: Array; - extraCount: number; - failedScan: boolean; -}; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WriteStatus.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/WriteStatus.ts deleted file mode 100644 index 068eb3bdb99..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/WriteStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type WriteStatus = "ok" | "okOverridden"; diff --git a/extensions/codex/src/app-server/protocol-generated/typescript/v2/index.ts b/extensions/codex/src/app-server/protocol-generated/typescript/v2/index.ts deleted file mode 100644 index 2db19d0fe30..00000000000 --- a/extensions/codex/src/app-server/protocol-generated/typescript/v2/index.ts +++ /dev/null @@ -1,470 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -export type { Account } from "./Account.js"; -export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification.js"; -export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification.js"; -export type { AccountUpdatedNotification } from "./AccountUpdatedNotification.js"; -export type { ActivePermissionProfile } from "./ActivePermissionProfile.js"; -export type { ActivePermissionProfileModification } from "./ActivePermissionProfileModification.js"; -export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType.js"; -export type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus.js"; -export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions.js"; -export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions.js"; -export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile.js"; -export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification.js"; -export type { AnalyticsConfig } from "./AnalyticsConfig.js"; -export type { AppBranding } from "./AppBranding.js"; -export type { AppInfo } from "./AppInfo.js"; -export type { AppListUpdatedNotification } from "./AppListUpdatedNotification.js"; -export type { AppMetadata } from "./AppMetadata.js"; -export type { AppReview } from "./AppReview.js"; -export type { AppScreenshot } from "./AppScreenshot.js"; -export type { AppSummary } from "./AppSummary.js"; -export type { AppToolApproval } from "./AppToolApproval.js"; -export type { AppToolsConfig } from "./AppToolsConfig.js"; -export type { ApprovalsReviewer } from "./ApprovalsReviewer.js"; -export type { AppsConfig } from "./AppsConfig.js"; -export type { AppsDefaultConfig } from "./AppsDefaultConfig.js"; -export type { AppsListParams } from "./AppsListParams.js"; -export type { AppsListResponse } from "./AppsListResponse.js"; -export type { AskForApproval } from "./AskForApproval.js"; -export type { AutoReviewDecisionSource } from "./AutoReviewDecisionSource.js"; -export type { ByteRange } from "./ByteRange.js"; -export type { CancelLoginAccountParams } from "./CancelLoginAccountParams.js"; -export type { CancelLoginAccountResponse } from "./CancelLoginAccountResponse.js"; -export type { CancelLoginAccountStatus } from "./CancelLoginAccountStatus.js"; -export type { ChatgptAuthTokensRefreshParams } from "./ChatgptAuthTokensRefreshParams.js"; -export type { ChatgptAuthTokensRefreshReason } from "./ChatgptAuthTokensRefreshReason.js"; -export type { ChatgptAuthTokensRefreshResponse } from "./ChatgptAuthTokensRefreshResponse.js"; -export type { CodexErrorInfo } from "./CodexErrorInfo.js"; -export type { CollabAgentState } from "./CollabAgentState.js"; -export type { CollabAgentStatus } from "./CollabAgentStatus.js"; -export type { CollabAgentTool } from "./CollabAgentTool.js"; -export type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus.js"; -export type { CollaborationModeListParams } from "./CollaborationModeListParams.js"; -export type { CollaborationModeListResponse } from "./CollaborationModeListResponse.js"; -export type { CollaborationModeMask } from "./CollaborationModeMask.js"; -export type { CommandAction } from "./CommandAction.js"; -export type { CommandExecOutputDeltaNotification } from "./CommandExecOutputDeltaNotification.js"; -export type { CommandExecOutputStream } from "./CommandExecOutputStream.js"; -export type { CommandExecParams } from "./CommandExecParams.js"; -export type { CommandExecResizeParams } from "./CommandExecResizeParams.js"; -export type { CommandExecResizeResponse } from "./CommandExecResizeResponse.js"; -export type { CommandExecResponse } from "./CommandExecResponse.js"; -export type { CommandExecTerminalSize } from "./CommandExecTerminalSize.js"; -export type { CommandExecTerminateParams } from "./CommandExecTerminateParams.js"; -export type { CommandExecTerminateResponse } from "./CommandExecTerminateResponse.js"; -export type { CommandExecWriteParams } from "./CommandExecWriteParams.js"; -export type { CommandExecWriteResponse } from "./CommandExecWriteResponse.js"; -export type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision.js"; -export type { CommandExecutionOutputDeltaNotification } from "./CommandExecutionOutputDeltaNotification.js"; -export type { CommandExecutionRequestApprovalParams } from "./CommandExecutionRequestApprovalParams.js"; -export type { CommandExecutionRequestApprovalResponse } from "./CommandExecutionRequestApprovalResponse.js"; -export type { CommandExecutionSource } from "./CommandExecutionSource.js"; -export type { CommandExecutionStatus } from "./CommandExecutionStatus.js"; -export type { CommandMigration } from "./CommandMigration.js"; -export type { Config } from "./Config.js"; -export type { ConfigBatchWriteParams } from "./ConfigBatchWriteParams.js"; -export type { ConfigEdit } from "./ConfigEdit.js"; -export type { ConfigLayer } from "./ConfigLayer.js"; -export type { ConfigLayerMetadata } from "./ConfigLayerMetadata.js"; -export type { ConfigLayerSource } from "./ConfigLayerSource.js"; -export type { ConfigReadParams } from "./ConfigReadParams.js"; -export type { ConfigReadResponse } from "./ConfigReadResponse.js"; -export type { ConfigRequirements } from "./ConfigRequirements.js"; -export type { ConfigRequirementsReadResponse } from "./ConfigRequirementsReadResponse.js"; -export type { ConfigValueWriteParams } from "./ConfigValueWriteParams.js"; -export type { ConfigWarningNotification } from "./ConfigWarningNotification.js"; -export type { ConfigWriteResponse } from "./ConfigWriteResponse.js"; -export type { ConfiguredHookHandler } from "./ConfiguredHookHandler.js"; -export type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup.js"; -export type { ContextCompactedNotification } from "./ContextCompactedNotification.js"; -export type { CreditsSnapshot } from "./CreditsSnapshot.js"; -export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotification.js"; -export type { DeviceKeyAlgorithm } from "./DeviceKeyAlgorithm.js"; -export type { DeviceKeyCreateParams } from "./DeviceKeyCreateParams.js"; -export type { DeviceKeyCreateResponse } from "./DeviceKeyCreateResponse.js"; -export type { DeviceKeyProtectionClass } from "./DeviceKeyProtectionClass.js"; -export type { DeviceKeyProtectionPolicy } from "./DeviceKeyProtectionPolicy.js"; -export type { DeviceKeyPublicParams } from "./DeviceKeyPublicParams.js"; -export type { DeviceKeyPublicResponse } from "./DeviceKeyPublicResponse.js"; -export type { DeviceKeySignParams } from "./DeviceKeySignParams.js"; -export type { DeviceKeySignPayload } from "./DeviceKeySignPayload.js"; -export type { DeviceKeySignResponse } from "./DeviceKeySignResponse.js"; -export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem.js"; -export type { DynamicToolCallParams } from "./DynamicToolCallParams.js"; -export type { DynamicToolCallResponse } from "./DynamicToolCallResponse.js"; -export type { DynamicToolCallStatus } from "./DynamicToolCallStatus.js"; -export type { DynamicToolSpec } from "./DynamicToolSpec.js"; -export type { ErrorNotification } from "./ErrorNotification.js"; -export type { ExecPolicyAmendment } from "./ExecPolicyAmendment.js"; -export type { ExperimentalFeature } from "./ExperimentalFeature.js"; -export type { ExperimentalFeatureEnablementSetParams } from "./ExperimentalFeatureEnablementSetParams.js"; -export type { ExperimentalFeatureEnablementSetResponse } from "./ExperimentalFeatureEnablementSetResponse.js"; -export type { ExperimentalFeatureListParams } from "./ExperimentalFeatureListParams.js"; -export type { ExperimentalFeatureListResponse } from "./ExperimentalFeatureListResponse.js"; -export type { ExperimentalFeatureStage } from "./ExperimentalFeatureStage.js"; -export type { ExternalAgentConfigDetectParams } from "./ExternalAgentConfigDetectParams.js"; -export type { ExternalAgentConfigDetectResponse } from "./ExternalAgentConfigDetectResponse.js"; -export type { ExternalAgentConfigImportCompletedNotification } from "./ExternalAgentConfigImportCompletedNotification.js"; -export type { ExternalAgentConfigImportParams } from "./ExternalAgentConfigImportParams.js"; -export type { ExternalAgentConfigImportResponse } from "./ExternalAgentConfigImportResponse.js"; -export type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem.js"; -export type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType.js"; -export type { FeedbackUploadParams } from "./FeedbackUploadParams.js"; -export type { FeedbackUploadResponse } from "./FeedbackUploadResponse.js"; -export type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision.js"; -export type { FileChangeOutputDeltaNotification } from "./FileChangeOutputDeltaNotification.js"; -export type { FileChangePatchUpdatedNotification } from "./FileChangePatchUpdatedNotification.js"; -export type { FileChangeRequestApprovalParams } from "./FileChangeRequestApprovalParams.js"; -export type { FileChangeRequestApprovalResponse } from "./FileChangeRequestApprovalResponse.js"; -export type { FileSystemAccessMode } from "./FileSystemAccessMode.js"; -export type { FileSystemPath } from "./FileSystemPath.js"; -export type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry.js"; -export type { FileSystemSpecialPath } from "./FileSystemSpecialPath.js"; -export type { FileUpdateChange } from "./FileUpdateChange.js"; -export type { FsChangedNotification } from "./FsChangedNotification.js"; -export type { FsCopyParams } from "./FsCopyParams.js"; -export type { FsCopyResponse } from "./FsCopyResponse.js"; -export type { FsCreateDirectoryParams } from "./FsCreateDirectoryParams.js"; -export type { FsCreateDirectoryResponse } from "./FsCreateDirectoryResponse.js"; -export type { FsGetMetadataParams } from "./FsGetMetadataParams.js"; -export type { FsGetMetadataResponse } from "./FsGetMetadataResponse.js"; -export type { FsReadDirectoryEntry } from "./FsReadDirectoryEntry.js"; -export type { FsReadDirectoryParams } from "./FsReadDirectoryParams.js"; -export type { FsReadDirectoryResponse } from "./FsReadDirectoryResponse.js"; -export type { FsReadFileParams } from "./FsReadFileParams.js"; -export type { FsReadFileResponse } from "./FsReadFileResponse.js"; -export type { FsRemoveParams } from "./FsRemoveParams.js"; -export type { FsRemoveResponse } from "./FsRemoveResponse.js"; -export type { FsUnwatchParams } from "./FsUnwatchParams.js"; -export type { FsUnwatchResponse } from "./FsUnwatchResponse.js"; -export type { FsWatchParams } from "./FsWatchParams.js"; -export type { FsWatchResponse } from "./FsWatchResponse.js"; -export type { FsWriteFileParams } from "./FsWriteFileParams.js"; -export type { FsWriteFileResponse } from "./FsWriteFileResponse.js"; -export type { GetAccountParams } from "./GetAccountParams.js"; -export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsResponse.js"; -export type { GetAccountResponse } from "./GetAccountResponse.js"; -export type { GitInfo } from "./GitInfo.js"; -export type { GrantedPermissionProfile } from "./GrantedPermissionProfile.js"; -export type { GuardianApprovalReview } from "./GuardianApprovalReview.js"; -export type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewAction.js"; -export type { GuardianApprovalReviewStatus } from "./GuardianApprovalReviewStatus.js"; -export type { GuardianCommandSource } from "./GuardianCommandSource.js"; -export type { GuardianRiskLevel } from "./GuardianRiskLevel.js"; -export type { GuardianUserAuthorization } from "./GuardianUserAuthorization.js"; -export type { GuardianWarningNotification } from "./GuardianWarningNotification.js"; -export type { HookCompletedNotification } from "./HookCompletedNotification.js"; -export type { HookErrorInfo } from "./HookErrorInfo.js"; -export type { HookEventName } from "./HookEventName.js"; -export type { HookExecutionMode } from "./HookExecutionMode.js"; -export type { HookHandlerType } from "./HookHandlerType.js"; -export type { HookMetadata } from "./HookMetadata.js"; -export type { HookMigration } from "./HookMigration.js"; -export type { HookOutputEntry } from "./HookOutputEntry.js"; -export type { HookOutputEntryKind } from "./HookOutputEntryKind.js"; -export type { HookPromptFragment } from "./HookPromptFragment.js"; -export type { HookRunStatus } from "./HookRunStatus.js"; -export type { HookRunSummary } from "./HookRunSummary.js"; -export type { HookScope } from "./HookScope.js"; -export type { HookSource } from "./HookSource.js"; -export type { HookStartedNotification } from "./HookStartedNotification.js"; -export type { HooksListEntry } from "./HooksListEntry.js"; -export type { HooksListParams } from "./HooksListParams.js"; -export type { HooksListResponse } from "./HooksListResponse.js"; -export type { ItemCompletedNotification } from "./ItemCompletedNotification.js"; -export type { ItemGuardianApprovalReviewCompletedNotification } from "./ItemGuardianApprovalReviewCompletedNotification.js"; -export type { ItemGuardianApprovalReviewStartedNotification } from "./ItemGuardianApprovalReviewStartedNotification.js"; -export type { ItemStartedNotification } from "./ItemStartedNotification.js"; -export type { ListMcpServerStatusParams } from "./ListMcpServerStatusParams.js"; -export type { ListMcpServerStatusResponse } from "./ListMcpServerStatusResponse.js"; -export type { LoginAccountParams } from "./LoginAccountParams.js"; -export type { LoginAccountResponse } from "./LoginAccountResponse.js"; -export type { LogoutAccountResponse } from "./LogoutAccountResponse.js"; -export type { ManagedHooksRequirements } from "./ManagedHooksRequirements.js"; -export type { MarketplaceAddParams } from "./MarketplaceAddParams.js"; -export type { MarketplaceAddResponse } from "./MarketplaceAddResponse.js"; -export type { MarketplaceInterface } from "./MarketplaceInterface.js"; -export type { MarketplaceLoadErrorInfo } from "./MarketplaceLoadErrorInfo.js"; -export type { MarketplaceRemoveParams } from "./MarketplaceRemoveParams.js"; -export type { MarketplaceRemoveResponse } from "./MarketplaceRemoveResponse.js"; -export type { MarketplaceUpgradeErrorInfo } from "./MarketplaceUpgradeErrorInfo.js"; -export type { MarketplaceUpgradeParams } from "./MarketplaceUpgradeParams.js"; -export type { MarketplaceUpgradeResponse } from "./MarketplaceUpgradeResponse.js"; -export type { McpAuthStatus } from "./McpAuthStatus.js"; -export type { McpElicitationArrayType } from "./McpElicitationArrayType.js"; -export type { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema.js"; -export type { McpElicitationBooleanType } from "./McpElicitationBooleanType.js"; -export type { McpElicitationConstOption } from "./McpElicitationConstOption.js"; -export type { McpElicitationEnumSchema } from "./McpElicitationEnumSchema.js"; -export type { McpElicitationLegacyTitledEnumSchema } from "./McpElicitationLegacyTitledEnumSchema.js"; -export type { McpElicitationMultiSelectEnumSchema } from "./McpElicitationMultiSelectEnumSchema.js"; -export type { McpElicitationNumberSchema } from "./McpElicitationNumberSchema.js"; -export type { McpElicitationNumberType } from "./McpElicitationNumberType.js"; -export type { McpElicitationObjectType } from "./McpElicitationObjectType.js"; -export type { McpElicitationPrimitiveSchema } from "./McpElicitationPrimitiveSchema.js"; -export type { McpElicitationSchema } from "./McpElicitationSchema.js"; -export type { McpElicitationSingleSelectEnumSchema } from "./McpElicitationSingleSelectEnumSchema.js"; -export type { McpElicitationStringFormat } from "./McpElicitationStringFormat.js"; -export type { McpElicitationStringSchema } from "./McpElicitationStringSchema.js"; -export type { McpElicitationStringType } from "./McpElicitationStringType.js"; -export type { McpElicitationTitledEnumItems } from "./McpElicitationTitledEnumItems.js"; -export type { McpElicitationTitledMultiSelectEnumSchema } from "./McpElicitationTitledMultiSelectEnumSchema.js"; -export type { McpElicitationTitledSingleSelectEnumSchema } from "./McpElicitationTitledSingleSelectEnumSchema.js"; -export type { McpElicitationUntitledEnumItems } from "./McpElicitationUntitledEnumItems.js"; -export type { McpElicitationUntitledMultiSelectEnumSchema } from "./McpElicitationUntitledMultiSelectEnumSchema.js"; -export type { McpElicitationUntitledSingleSelectEnumSchema } from "./McpElicitationUntitledSingleSelectEnumSchema.js"; -export type { McpResourceReadParams } from "./McpResourceReadParams.js"; -export type { McpResourceReadResponse } from "./McpResourceReadResponse.js"; -export type { McpServerElicitationAction } from "./McpServerElicitationAction.js"; -export type { McpServerElicitationRequestParams } from "./McpServerElicitationRequestParams.js"; -export type { McpServerElicitationRequestResponse } from "./McpServerElicitationRequestResponse.js"; -export type { McpServerMigration } from "./McpServerMigration.js"; -export type { McpServerOauthLoginCompletedNotification } from "./McpServerOauthLoginCompletedNotification.js"; -export type { McpServerOauthLoginParams } from "./McpServerOauthLoginParams.js"; -export type { McpServerOauthLoginResponse } from "./McpServerOauthLoginResponse.js"; -export type { McpServerRefreshResponse } from "./McpServerRefreshResponse.js"; -export type { McpServerStartupState } from "./McpServerStartupState.js"; -export type { McpServerStatus } from "./McpServerStatus.js"; -export type { McpServerStatusDetail } from "./McpServerStatusDetail.js"; -export type { McpServerStatusUpdatedNotification } from "./McpServerStatusUpdatedNotification.js"; -export type { McpServerToolCallParams } from "./McpServerToolCallParams.js"; -export type { McpServerToolCallResponse } from "./McpServerToolCallResponse.js"; -export type { McpToolCallError } from "./McpToolCallError.js"; -export type { McpToolCallProgressNotification } from "./McpToolCallProgressNotification.js"; -export type { McpToolCallResult } from "./McpToolCallResult.js"; -export type { McpToolCallStatus } from "./McpToolCallStatus.js"; -export type { MemoryCitation } from "./MemoryCitation.js"; -export type { MemoryCitationEntry } from "./MemoryCitationEntry.js"; -export type { MemoryResetResponse } from "./MemoryResetResponse.js"; -export type { MergeStrategy } from "./MergeStrategy.js"; -export type { MigrationDetails } from "./MigrationDetails.js"; -export type { MockExperimentalMethodParams } from "./MockExperimentalMethodParams.js"; -export type { MockExperimentalMethodResponse } from "./MockExperimentalMethodResponse.js"; -export type { Model } from "./Model.js"; -export type { ModelAvailabilityNux } from "./ModelAvailabilityNux.js"; -export type { ModelListParams } from "./ModelListParams.js"; -export type { ModelListResponse } from "./ModelListResponse.js"; -export type { ModelProviderCapabilitiesReadParams } from "./ModelProviderCapabilitiesReadParams.js"; -export type { ModelProviderCapabilitiesReadResponse } from "./ModelProviderCapabilitiesReadResponse.js"; -export type { ModelRerouteReason } from "./ModelRerouteReason.js"; -export type { ModelReroutedNotification } from "./ModelReroutedNotification.js"; -export type { ModelUpgradeInfo } from "./ModelUpgradeInfo.js"; -export type { ModelVerification } from "./ModelVerification.js"; -export type { ModelVerificationNotification } from "./ModelVerificationNotification.js"; -export type { NetworkAccess } from "./NetworkAccess.js"; -export type { NetworkApprovalContext } from "./NetworkApprovalContext.js"; -export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol.js"; -export type { NetworkDomainPermission } from "./NetworkDomainPermission.js"; -export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment.js"; -export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction.js"; -export type { NetworkRequirements } from "./NetworkRequirements.js"; -export type { NetworkUnixSocketPermission } from "./NetworkUnixSocketPermission.js"; -export type { NonSteerableTurnKind } from "./NonSteerableTurnKind.js"; -export type { OverriddenMetadata } from "./OverriddenMetadata.js"; -export type { PatchApplyStatus } from "./PatchApplyStatus.js"; -export type { PatchChangeKind } from "./PatchChangeKind.js"; -export type { PermissionGrantScope } from "./PermissionGrantScope.js"; -export type { PermissionProfile } from "./PermissionProfile.js"; -export type { PermissionProfileFileSystemPermissions } from "./PermissionProfileFileSystemPermissions.js"; -export type { PermissionProfileModificationParams } from "./PermissionProfileModificationParams.js"; -export type { PermissionProfileNetworkPermissions } from "./PermissionProfileNetworkPermissions.js"; -export type { PermissionProfileSelectionParams } from "./PermissionProfileSelectionParams.js"; -export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams.js"; -export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse.js"; -export type { PlanDeltaNotification } from "./PlanDeltaNotification.js"; -export type { PluginAuthPolicy } from "./PluginAuthPolicy.js"; -export type { PluginAvailability } from "./PluginAvailability.js"; -export type { PluginDetail } from "./PluginDetail.js"; -export type { PluginInstallParams } from "./PluginInstallParams.js"; -export type { PluginInstallPolicy } from "./PluginInstallPolicy.js"; -export type { PluginInstallResponse } from "./PluginInstallResponse.js"; -export type { PluginInterface } from "./PluginInterface.js"; -export type { PluginListParams } from "./PluginListParams.js"; -export type { PluginListResponse } from "./PluginListResponse.js"; -export type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry.js"; -export type { PluginReadParams } from "./PluginReadParams.js"; -export type { PluginReadResponse } from "./PluginReadResponse.js"; -export type { PluginShareDeleteParams } from "./PluginShareDeleteParams.js"; -export type { PluginShareDeleteResponse } from "./PluginShareDeleteResponse.js"; -export type { PluginShareListItem } from "./PluginShareListItem.js"; -export type { PluginShareListParams } from "./PluginShareListParams.js"; -export type { PluginShareListResponse } from "./PluginShareListResponse.js"; -export type { PluginShareSaveParams } from "./PluginShareSaveParams.js"; -export type { PluginShareSaveResponse } from "./PluginShareSaveResponse.js"; -export type { PluginSkillReadParams } from "./PluginSkillReadParams.js"; -export type { PluginSkillReadResponse } from "./PluginSkillReadResponse.js"; -export type { PluginSource } from "./PluginSource.js"; -export type { PluginSummary } from "./PluginSummary.js"; -export type { PluginUninstallParams } from "./PluginUninstallParams.js"; -export type { PluginUninstallResponse } from "./PluginUninstallResponse.js"; -export type { PluginsMigration } from "./PluginsMigration.js"; -export type { ProfileV2 } from "./ProfileV2.js"; -export type { RateLimitReachedType } from "./RateLimitReachedType.js"; -export type { RateLimitSnapshot } from "./RateLimitSnapshot.js"; -export type { RateLimitWindow } from "./RateLimitWindow.js"; -export type { RawResponseItemCompletedNotification } from "./RawResponseItemCompletedNotification.js"; -export type { ReasoningEffortOption } from "./ReasoningEffortOption.js"; -export type { ReasoningSummaryPartAddedNotification } from "./ReasoningSummaryPartAddedNotification.js"; -export type { ReasoningSummaryTextDeltaNotification } from "./ReasoningSummaryTextDeltaNotification.js"; -export type { ReasoningTextDeltaNotification } from "./ReasoningTextDeltaNotification.js"; -export type { RemoteControlClientConnectionAudience } from "./RemoteControlClientConnectionAudience.js"; -export type { RemoteControlClientEnrollmentAudience } from "./RemoteControlClientEnrollmentAudience.js"; -export type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus.js"; -export type { RemoteControlStatusChangedNotification } from "./RemoteControlStatusChangedNotification.js"; -export type { RequestPermissionProfile } from "./RequestPermissionProfile.js"; -export type { ResidencyRequirement } from "./ResidencyRequirement.js"; -export type { ReviewDelivery } from "./ReviewDelivery.js"; -export type { ReviewStartParams } from "./ReviewStartParams.js"; -export type { ReviewStartResponse } from "./ReviewStartResponse.js"; -export type { ReviewTarget } from "./ReviewTarget.js"; -export type { SandboxMode } from "./SandboxMode.js"; -export type { SandboxPolicy } from "./SandboxPolicy.js"; -export type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite.js"; -export type { SendAddCreditsNudgeEmailParams } from "./SendAddCreditsNudgeEmailParams.js"; -export type { SendAddCreditsNudgeEmailResponse } from "./SendAddCreditsNudgeEmailResponse.js"; -export type { ServerRequestResolvedNotification } from "./ServerRequestResolvedNotification.js"; -export type { SessionMigration } from "./SessionMigration.js"; -export type { SessionSource } from "./SessionSource.js"; -export type { SkillDependencies } from "./SkillDependencies.js"; -export type { SkillErrorInfo } from "./SkillErrorInfo.js"; -export type { SkillInterface } from "./SkillInterface.js"; -export type { SkillMetadata } from "./SkillMetadata.js"; -export type { SkillScope } from "./SkillScope.js"; -export type { SkillSummary } from "./SkillSummary.js"; -export type { SkillToolDependency } from "./SkillToolDependency.js"; -export type { SkillsChangedNotification } from "./SkillsChangedNotification.js"; -export type { SkillsConfigWriteParams } from "./SkillsConfigWriteParams.js"; -export type { SkillsConfigWriteResponse } from "./SkillsConfigWriteResponse.js"; -export type { SkillsListEntry } from "./SkillsListEntry.js"; -export type { SkillsListExtraRootsForCwd } from "./SkillsListExtraRootsForCwd.js"; -export type { SkillsListParams } from "./SkillsListParams.js"; -export type { SkillsListResponse } from "./SkillsListResponse.js"; -export type { SortDirection } from "./SortDirection.js"; -export type { SubagentMigration } from "./SubagentMigration.js"; -export type { TerminalInteractionNotification } from "./TerminalInteractionNotification.js"; -export type { TextElement } from "./TextElement.js"; -export type { TextPosition } from "./TextPosition.js"; -export type { TextRange } from "./TextRange.js"; -export type { Thread } from "./Thread.js"; -export type { ThreadActiveFlag } from "./ThreadActiveFlag.js"; -export type { ThreadApproveGuardianDeniedActionParams } from "./ThreadApproveGuardianDeniedActionParams.js"; -export type { ThreadApproveGuardianDeniedActionResponse } from "./ThreadApproveGuardianDeniedActionResponse.js"; -export type { ThreadArchiveParams } from "./ThreadArchiveParams.js"; -export type { ThreadArchiveResponse } from "./ThreadArchiveResponse.js"; -export type { ThreadArchivedNotification } from "./ThreadArchivedNotification.js"; -export type { ThreadBackgroundTerminalsCleanParams } from "./ThreadBackgroundTerminalsCleanParams.js"; -export type { ThreadBackgroundTerminalsCleanResponse } from "./ThreadBackgroundTerminalsCleanResponse.js"; -export type { ThreadClosedNotification } from "./ThreadClosedNotification.js"; -export type { ThreadCompactStartParams } from "./ThreadCompactStartParams.js"; -export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse.js"; -export type { ThreadDecrementElicitationParams } from "./ThreadDecrementElicitationParams.js"; -export type { ThreadDecrementElicitationResponse } from "./ThreadDecrementElicitationResponse.js"; -export type { ThreadForkParams } from "./ThreadForkParams.js"; -export type { ThreadForkResponse } from "./ThreadForkResponse.js"; -export type { ThreadGoal } from "./ThreadGoal.js"; -export type { ThreadGoalClearParams } from "./ThreadGoalClearParams.js"; -export type { ThreadGoalClearResponse } from "./ThreadGoalClearResponse.js"; -export type { ThreadGoalClearedNotification } from "./ThreadGoalClearedNotification.js"; -export type { ThreadGoalGetParams } from "./ThreadGoalGetParams.js"; -export type { ThreadGoalGetResponse } from "./ThreadGoalGetResponse.js"; -export type { ThreadGoalSetParams } from "./ThreadGoalSetParams.js"; -export type { ThreadGoalSetResponse } from "./ThreadGoalSetResponse.js"; -export type { ThreadGoalStatus } from "./ThreadGoalStatus.js"; -export type { ThreadGoalUpdatedNotification } from "./ThreadGoalUpdatedNotification.js"; -export type { ThreadIncrementElicitationParams } from "./ThreadIncrementElicitationParams.js"; -export type { ThreadIncrementElicitationResponse } from "./ThreadIncrementElicitationResponse.js"; -export type { ThreadInjectItemsParams } from "./ThreadInjectItemsParams.js"; -export type { ThreadInjectItemsResponse } from "./ThreadInjectItemsResponse.js"; -export type { ThreadItem } from "./ThreadItem.js"; -export type { ThreadListParams } from "./ThreadListParams.js"; -export type { ThreadListResponse } from "./ThreadListResponse.js"; -export type { ThreadLoadedListParams } from "./ThreadLoadedListParams.js"; -export type { ThreadLoadedListResponse } from "./ThreadLoadedListResponse.js"; -export type { ThreadMemoryModeSetParams } from "./ThreadMemoryModeSetParams.js"; -export type { ThreadMemoryModeSetResponse } from "./ThreadMemoryModeSetResponse.js"; -export type { ThreadMetadataGitInfoUpdateParams } from "./ThreadMetadataGitInfoUpdateParams.js"; -export type { ThreadMetadataUpdateParams } from "./ThreadMetadataUpdateParams.js"; -export type { ThreadMetadataUpdateResponse } from "./ThreadMetadataUpdateResponse.js"; -export type { ThreadNameUpdatedNotification } from "./ThreadNameUpdatedNotification.js"; -export type { ThreadReadParams } from "./ThreadReadParams.js"; -export type { ThreadReadResponse } from "./ThreadReadResponse.js"; -export type { ThreadRealtimeAppendAudioParams } from "./ThreadRealtimeAppendAudioParams.js"; -export type { ThreadRealtimeAppendAudioResponse } from "./ThreadRealtimeAppendAudioResponse.js"; -export type { ThreadRealtimeAppendTextParams } from "./ThreadRealtimeAppendTextParams.js"; -export type { ThreadRealtimeAppendTextResponse } from "./ThreadRealtimeAppendTextResponse.js"; -export type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk.js"; -export type { ThreadRealtimeClosedNotification } from "./ThreadRealtimeClosedNotification.js"; -export type { ThreadRealtimeErrorNotification } from "./ThreadRealtimeErrorNotification.js"; -export type { ThreadRealtimeItemAddedNotification } from "./ThreadRealtimeItemAddedNotification.js"; -export type { ThreadRealtimeListVoicesParams } from "./ThreadRealtimeListVoicesParams.js"; -export type { ThreadRealtimeListVoicesResponse } from "./ThreadRealtimeListVoicesResponse.js"; -export type { ThreadRealtimeOutputAudioDeltaNotification } from "./ThreadRealtimeOutputAudioDeltaNotification.js"; -export type { ThreadRealtimeSdpNotification } from "./ThreadRealtimeSdpNotification.js"; -export type { ThreadRealtimeStartParams } from "./ThreadRealtimeStartParams.js"; -export type { ThreadRealtimeStartResponse } from "./ThreadRealtimeStartResponse.js"; -export type { ThreadRealtimeStartTransport } from "./ThreadRealtimeStartTransport.js"; -export type { ThreadRealtimeStartedNotification } from "./ThreadRealtimeStartedNotification.js"; -export type { ThreadRealtimeStopParams } from "./ThreadRealtimeStopParams.js"; -export type { ThreadRealtimeStopResponse } from "./ThreadRealtimeStopResponse.js"; -export type { ThreadRealtimeTranscriptDeltaNotification } from "./ThreadRealtimeTranscriptDeltaNotification.js"; -export type { ThreadRealtimeTranscriptDoneNotification } from "./ThreadRealtimeTranscriptDoneNotification.js"; -export type { ThreadResumeParams } from "./ThreadResumeParams.js"; -export type { ThreadResumeResponse } from "./ThreadResumeResponse.js"; -export type { ThreadRollbackParams } from "./ThreadRollbackParams.js"; -export type { ThreadRollbackResponse } from "./ThreadRollbackResponse.js"; -export type { ThreadSetNameParams } from "./ThreadSetNameParams.js"; -export type { ThreadSetNameResponse } from "./ThreadSetNameResponse.js"; -export type { ThreadShellCommandParams } from "./ThreadShellCommandParams.js"; -export type { ThreadShellCommandResponse } from "./ThreadShellCommandResponse.js"; -export type { ThreadSortKey } from "./ThreadSortKey.js"; -export type { ThreadSourceKind } from "./ThreadSourceKind.js"; -export type { ThreadStartParams } from "./ThreadStartParams.js"; -export type { ThreadStartResponse } from "./ThreadStartResponse.js"; -export type { ThreadStartSource } from "./ThreadStartSource.js"; -export type { ThreadStartedNotification } from "./ThreadStartedNotification.js"; -export type { ThreadStatus } from "./ThreadStatus.js"; -export type { ThreadStatusChangedNotification } from "./ThreadStatusChangedNotification.js"; -export type { ThreadTokenUsage } from "./ThreadTokenUsage.js"; -export type { ThreadTokenUsageUpdatedNotification } from "./ThreadTokenUsageUpdatedNotification.js"; -export type { ThreadTurnsListParams } from "./ThreadTurnsListParams.js"; -export type { ThreadTurnsListResponse } from "./ThreadTurnsListResponse.js"; -export type { ThreadUnarchiveParams } from "./ThreadUnarchiveParams.js"; -export type { ThreadUnarchiveResponse } from "./ThreadUnarchiveResponse.js"; -export type { ThreadUnarchivedNotification } from "./ThreadUnarchivedNotification.js"; -export type { ThreadUnsubscribeParams } from "./ThreadUnsubscribeParams.js"; -export type { ThreadUnsubscribeResponse } from "./ThreadUnsubscribeResponse.js"; -export type { ThreadUnsubscribeStatus } from "./ThreadUnsubscribeStatus.js"; -export type { TokenUsageBreakdown } from "./TokenUsageBreakdown.js"; -export type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer.js"; -export type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption.js"; -export type { ToolRequestUserInputParams } from "./ToolRequestUserInputParams.js"; -export type { ToolRequestUserInputQuestion } from "./ToolRequestUserInputQuestion.js"; -export type { ToolRequestUserInputResponse } from "./ToolRequestUserInputResponse.js"; -export type { ToolsV2 } from "./ToolsV2.js"; -export type { Turn } from "./Turn.js"; -export type { TurnCompletedNotification } from "./TurnCompletedNotification.js"; -export type { TurnDiffUpdatedNotification } from "./TurnDiffUpdatedNotification.js"; -export type { TurnEnvironmentParams } from "./TurnEnvironmentParams.js"; -export type { TurnError } from "./TurnError.js"; -export type { TurnInterruptParams } from "./TurnInterruptParams.js"; -export type { TurnInterruptResponse } from "./TurnInterruptResponse.js"; -export type { TurnPlanStep } from "./TurnPlanStep.js"; -export type { TurnPlanStepStatus } from "./TurnPlanStepStatus.js"; -export type { TurnPlanUpdatedNotification } from "./TurnPlanUpdatedNotification.js"; -export type { TurnStartParams } from "./TurnStartParams.js"; -export type { TurnStartResponse } from "./TurnStartResponse.js"; -export type { TurnStartedNotification } from "./TurnStartedNotification.js"; -export type { TurnStatus } from "./TurnStatus.js"; -export type { TurnSteerParams } from "./TurnSteerParams.js"; -export type { TurnSteerResponse } from "./TurnSteerResponse.js"; -export type { UserInput } from "./UserInput.js"; -export type { WarningNotification } from "./WarningNotification.js"; -export type { WebSearchAction } from "./WebSearchAction.js"; -export type { WindowsSandboxSetupCompletedNotification } from "./WindowsSandboxSetupCompletedNotification.js"; -export type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode.js"; -export type { WindowsSandboxSetupStartParams } from "./WindowsSandboxSetupStartParams.js"; -export type { WindowsSandboxSetupStartResponse } from "./WindowsSandboxSetupStartResponse.js"; -export type { WindowsWorldWritableWarningNotification } from "./WindowsWorldWritableWarningNotification.js"; -export type { WriteStatus } from "./WriteStatus.js"; diff --git a/extensions/codex/src/app-server/protocol-validators.ts b/extensions/codex/src/app-server/protocol-validators.ts index 05ce0c11045..bbcb2c92d9b 100644 --- a/extensions/codex/src/app-server/protocol-validators.ts +++ b/extensions/codex/src/app-server/protocol-validators.ts @@ -6,12 +6,14 @@ import threadResumeResponseSchema from "./protocol-generated/json/v2/ThreadResum import threadStartResponseSchema from "./protocol-generated/json/v2/ThreadStartResponse.json" with { type: "json" }; import turnCompletedNotificationSchema from "./protocol-generated/json/v2/TurnCompletedNotification.json" with { type: "json" }; import turnStartResponseSchema from "./protocol-generated/json/v2/TurnStartResponse.json" with { type: "json" }; -import type { v2 } from "./protocol-generated/typescript/index.js"; import type { CodexDynamicToolCallParams, + CodexErrorNotification, + CodexModelListResponse, CodexThreadResumeResponse, CodexThreadStartResponse, CodexTurn, + CodexTurnCompletedNotification, CodexTurnStartResponse, } from "./protocol.js"; @@ -28,14 +30,14 @@ const ajv = new AjvCtor({ const validateDynamicToolCallParams = ajv.compile( dynamicToolCallParamsSchema, ); -const validateErrorNotification = ajv.compile(errorNotificationSchema); -const validateModelListResponse = ajv.compile(modelListResponseSchema); +const validateErrorNotification = ajv.compile(errorNotificationSchema); +const validateModelListResponse = ajv.compile(modelListResponseSchema); const validateThreadResumeResponse = ajv.compile( threadResumeResponseSchema, ); const validateThreadStartResponse = ajv.compile(threadStartResponseSchema); -const validateTurnCompletedNotification = ajv.compile( +const validateTurnCompletedNotification = ajv.compile( turnCompletedNotificationSchema, ); const validateTurnStartResponse = ajv.compile(turnStartResponseSchema); @@ -62,11 +64,11 @@ export function readCodexDynamicToolCallParams( return readCodexShape(validateDynamicToolCallParams, value); } -export function readCodexErrorNotification(value: unknown): v2.ErrorNotification | undefined { +export function readCodexErrorNotification(value: unknown): CodexErrorNotification | undefined { return readCodexShape(validateErrorNotification, value); } -export function readCodexModelListResponse(value: unknown): v2.ModelListResponse | undefined { +export function readCodexModelListResponse(value: unknown): CodexModelListResponse | undefined { return readCodexShape(validateModelListResponse, value); } @@ -77,7 +79,7 @@ export function readCodexTurn(value: unknown): CodexTurn | undefined { export function readCodexTurnCompletedNotification( value: unknown, -): v2.TurnCompletedNotification | undefined { +): CodexTurnCompletedNotification | undefined { return readCodexShape( validateTurnCompletedNotification, normalizeTurnCompletedNotification(value), diff --git a/extensions/codex/src/app-server/protocol.ts b/extensions/codex/src/app-server/protocol.ts index 14805ba3cbf..a0bb0b4ffb6 100644 --- a/extensions/codex/src/app-server/protocol.ts +++ b/extensions/codex/src/app-server/protocol.ts @@ -1,21 +1,12 @@ -import type { - ClientRequest as GeneratedClientRequest, - InitializeParams as GeneratedInitializeParams, - InitializeResponse as GeneratedInitializeResponse, - ServiceTier as GeneratedServiceTier, - v2, -} from "./protocol-generated/typescript/index.js"; -import type { JsonValue as GeneratedJsonValue } from "./protocol-generated/typescript/serde_json/JsonValue.js"; - -export type JsonValue = GeneratedJsonValue; +export type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject; export type JsonObject = { [key: string]: JsonValue }; -export type CodexServiceTier = GeneratedServiceTier; +export type CodexServiceTier = string; -export type CodexAppServerRequestMethod = GeneratedClientRequest["method"]; +export type CodexAppServerRequestMethod = keyof CodexAppServerRequestResultMap | (string & {}); export type CodexAppServerRequestParams = M extends keyof CodexAppServerRequestParamsOverride ? CodexAppServerRequestParamsOverride[M] - : Extract["params"]; + : unknown; export type CodexAppServerRequestResult = M extends keyof CodexAppServerRequestResultMap @@ -40,44 +31,270 @@ export type RpcResponse = { export type RpcMessage = RpcRequest | RpcResponse; -export type CodexInitializeParams = GeneratedInitializeParams; - -export type CodexInitializeResponse = GeneratedInitializeResponse; - -export type CodexUserInput = v2.UserInput; - -export type CodexDynamicToolSpec = v2.DynamicToolSpec; - -export type CodexThreadStartParams = v2.ThreadStartParams & { - dynamicTools?: CodexDynamicToolSpec[] | null; +export type CodexInitializeParams = { + clientInfo: { + name: string; + title?: string; + version?: string; + }; + capabilities?: JsonObject; }; -export type CodexThreadResumeParams = v2.ThreadResumeParams; +export type CodexInitializeResponse = { + serverInfo?: { + name?: string; + version?: string; + }; + protocolVersion?: string; + userAgent?: string; +}; -export type CodexThreadStartResponse = v2.ThreadStartResponse; +export type CodexUserInput = + | { + type: "text"; + text: string; + text_elements?: JsonValue[]; + } + | { + type: "image"; + url: string; + } + | { + type: "localImage"; + path: string; + }; -export type CodexThreadResumeResponse = v2.ThreadResumeResponse; +export type CodexDynamicToolSpec = JsonObject & { + name: string; + description: string; + inputSchema: JsonValue; +}; -export type CodexTurnStartParams = v2.TurnStartParams; +export type CodexThreadStartParams = JsonObject & { + input?: CodexUserInput[]; + cwd?: string; + model?: string; + modelProvider?: string | null; + approvalPolicy?: string; + approvalsReviewer?: string | null; + sandbox?: CodexSandboxPolicy; + serviceTier?: CodexServiceTier | null; + dynamicTools?: CodexDynamicToolSpec[] | null; + developerInstructions?: string; + experimentalRawEvents?: boolean; + persistExtendedHistory?: boolean; +}; -export type CodexSandboxPolicy = v2.SandboxPolicy; +export type CodexThreadResumeParams = JsonObject & { + threadId: string; + model?: string; + modelProvider?: string | null; +}; -export type CodexTurnStartResponse = v2.TurnStartResponse; +export type CodexThreadStartResponse = { + thread: CodexThread; + model: string; + modelProvider?: string | null; +}; -export type CodexTurn = v2.Turn; +export type CodexThreadResumeResponse = { + thread: CodexThread; + model: string; + modelProvider?: string | null; +}; -export type CodexThreadItem = v2.ThreadItem; +export type CodexTurnStartParams = JsonObject & { + threadId: string; + input?: CodexUserInput[]; + cwd?: string; + model?: string; + approvalPolicy?: string; + approvalsReviewer?: string | null; + sandboxPolicy?: CodexSandboxPolicy; + serviceTier?: CodexServiceTier | null; + effort?: string | null; + collaborationMode?: { + mode: string; + settings: JsonObject & { + developer_instructions: string | null; + }; + } | null; +}; + +export type CodexSandboxPolicy = string | JsonObject; + +export type CodexTurnStartResponse = { + turn: CodexTurn; +}; + +export type CodexTurn = { + id: string; + threadId: string; + status?: string; + error?: CodexErrorNotification["error"]; + startedAt?: string | null; + completedAt?: string | null; + durationMs?: number | null; + items: CodexThreadItem[]; +}; + +export type CodexThread = { + id: string; + name?: string | null; + cwd?: string | null; +}; + +export type CodexThreadItem = { + id: string; + type: string; + title: string | null; + status: string | null; + name: string | null; + tool: string | null; + server: string | null; + command: string | null; + cwd: string | null; + query: string | null; + arguments?: JsonValue; + result?: JsonValue; + error?: CodexErrorNotification["error"]; + exitCode?: number | null; + durationMs?: number | null; + aggregatedOutput: string | null; + text: string; + contentItems?: CodexDynamicToolCallOutputContentItem[] | null; + changes: Array<{ path: string; kind: string }>; + [key: string]: unknown; +}; export type CodexServerNotification = { method: string; params?: JsonValue; }; -export type CodexDynamicToolCallParams = v2.DynamicToolCallParams; +export type CodexDynamicToolCallParams = { + namespace?: string | null; + threadId: string; + turnId: string; + callId: string; + tool: string; + arguments?: JsonValue; +}; -export type CodexDynamicToolCallResponse = v2.DynamicToolCallResponse; +export type CodexDynamicToolCallResponse = { + contentItems: CodexDynamicToolCallOutputContentItem[]; + success: boolean; +}; -export type CodexDynamicToolCallOutputContentItem = v2.DynamicToolCallOutputContentItem; +export type CodexDynamicToolCallOutputContentItem = + | { + type: "inputText"; + text: string; + } + | { + type: "inputImage"; + imageUrl: string; + } + | JsonObject; + +export type CodexErrorNotification = { + error: { + message?: string; + codexErrorInfo?: { + message?: string; + [key: string]: unknown; + }; + [key: string]: unknown; + }; + message?: string; +}; + +export type CodexTurnCompletedNotification = { + turn: CodexTurn; +}; + +export type CodexModel = { + id?: string; + model?: string; + displayName?: string | null; + description?: string | null; + hidden: boolean; + isDefault: boolean; + inputModalities: string[]; + supportedReasoningEfforts: CodexReasoningEffortOption[]; + defaultReasoningEffort?: string | null; +}; + +export type CodexReasoningEffortOption = { + reasoningEffort?: string | null; +}; + +export type CodexModelListResponse = { + data: CodexModel[]; + nextCursor?: string | null; +}; + +export type CodexGetAccountResponse = { + account?: JsonValue; + requiresOpenaiAuth?: boolean; +}; + +export type CodexChatgptAuthTokensRefreshResponse = { + accessToken: string; + chatgptAccountId: string; + chatgptPlanType: string | null; +}; + +export type CodexLoginAccountParams = + | { + type: "apiKey"; + apiKey: string; + } + | { + type: "chatgptAuthTokens"; + accessToken: string; + chatgptAccountId: string; + chatgptPlanType: string | null; + }; + +export type CodexPluginSummary = { + id?: string; + name?: string; + installed: boolean; + enabled: boolean; +}; + +export type CodexPluginDetail = { + summary: CodexPluginSummary; + marketplaceName?: string; + marketplacePath?: string | null; +}; + +export type CodexPluginMarketplaceEntry = { + name: string; + path?: string | null; + plugins: CodexPluginSummary[]; +}; + +export type CodexPluginListResponse = { + marketplaces: CodexPluginMarketplaceEntry[]; +}; + +export type CodexPluginReadResponse = { + plugin: CodexPluginDetail; +}; + +export type CodexMcpServerStatus = { + name: string; + tools: JsonObject; +}; + +export type CodexListMcpServerStatusResponse = { + data: CodexMcpServerStatus[]; + nextCursor?: string | null; +}; + +export type CodexRequestObject = Record; type CodexAppServerRequestParamsOverride = { "thread/start": CodexThreadStartParams; @@ -85,20 +302,20 @@ type CodexAppServerRequestParamsOverride = { type CodexAppServerRequestResultMap = { initialize: CodexInitializeResponse; - "account/rateLimits/read": v2.GetAccountRateLimitsResponse; - "account/read": v2.GetAccountResponse; - "feedback/upload": v2.FeedbackUploadResponse; - "mcpServerStatus/list": v2.ListMcpServerStatusResponse; - "model/list": v2.ModelListResponse; - "review/start": v2.ReviewStartResponse; - "skills/list": v2.SkillsListResponse; - "thread/compact/start": v2.ThreadCompactStartResponse; - "thread/list": v2.ThreadListResponse; + "account/rateLimits/read": JsonValue; + "account/read": CodexGetAccountResponse; + "feedback/upload": JsonValue; + "mcpServerStatus/list": CodexListMcpServerStatusResponse; + "model/list": CodexModelListResponse; + "review/start": JsonValue; + "skills/list": JsonValue; + "thread/compact/start": JsonValue; + "thread/list": JsonValue; "thread/resume": CodexThreadResumeResponse; "thread/start": CodexThreadStartResponse; - "turn/interrupt": v2.TurnInterruptResponse; + "turn/interrupt": JsonValue; "turn/start": CodexTurnStartResponse; - "turn/steer": v2.TurnSteerResponse; + "turn/steer": JsonValue; }; export function isJsonObject(value: JsonValue | undefined): value is JsonObject { diff --git a/extensions/deepseek/index.test.ts b/extensions/deepseek/index.test.ts index 07f9c36f833..9cc0ed8ecb7 100644 --- a/extensions/deepseek/index.test.ts +++ b/extensions/deepseek/index.test.ts @@ -16,6 +16,30 @@ type PayloadCapture = { payload?: Record; }; +const emptyUsage = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, +}; + +const readToolCall = { type: "toolCall", id: "call_1", name: "read", arguments: {} }; +const readToolResult = { + role: "toolResult", + toolCallId: "call_1", + toolName: "read", + content: [{ type: "text", text: "ok" }], + isError: false, + timestamp: 3, +}; +const readTool = { + name: "read", + description: "Read data", + parameters: { type: "object", properties: {}, required: [], additionalProperties: false }, +}; + function deepSeekV4Model(id: "deepseek-v4-flash" | "deepseek-v4-pro"): OpenAICompletionsModel { return { provider: "deepseek", @@ -36,6 +60,49 @@ function deepSeekV4Model(id: "deepseek-v4-flash" | "deepseek-v4-pro"): OpenAICom } as OpenAICompletionsModel; } +function replayAssistantMessage(params: { + provider: string; + model: string; + content: Array>; + stopReason: "stop" | "toolUse"; +}) { + return { + role: "assistant", + api: "openai-completions", + provider: params.provider, + model: params.model, + content: params.content, + usage: emptyUsage, + stopReason: params.stopReason, + timestamp: 2, + }; +} + +function readToolReplayContext(assistantMessage: ReturnType) { + return { + messages: [{ role: "user", content: "hi", timestamp: 1 }, assistantMessage, readToolResult], + tools: [readTool], + } as Context; +} + +function deepSeekReasoningToolReplayContext() { + return readToolReplayContext( + replayAssistantMessage({ + provider: "deepseek", + model: "deepseek-v4-flash", + content: [ + { + type: "thinking", + thinking: "call reasoning", + thinkingSignature: "reasoning_content", + }, + readToolCall, + ], + stopReason: "toolUse", + }), + ); +} + function createPayloadCapturingStream(capture: PayloadCapture) { return ( streamModel: OpenAICompletionsModel, @@ -194,50 +261,7 @@ describe("deepseek provider plugin", () => { it("preserves replayed reasoning_content when DeepSeek V4 thinking is enabled", async () => { const capture: PayloadCapture = {}; const model = deepSeekV4Model("deepseek-v4-flash"); - const context = { - messages: [ - { role: "user", content: "hi", timestamp: 1 }, - { - role: "assistant", - api: "openai-completions", - provider: "deepseek", - model: "deepseek-v4-flash", - content: [ - { - type: "thinking", - thinking: "call reasoning", - thinkingSignature: "reasoning_content", - }, - { type: "toolCall", id: "call_1", name: "read", arguments: {} }, - ], - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "toolUse", - timestamp: 2, - }, - { - role: "toolResult", - toolCallId: "call_1", - toolName: "read", - content: [{ type: "text", text: "ok" }], - isError: false, - timestamp: 3, - }, - ], - tools: [ - { - name: "read", - description: "Read data", - parameters: { type: "object", properties: {}, required: [], additionalProperties: false }, - }, - ], - } as Context; + const context = deepSeekReasoningToolReplayContext(); const baseStreamFn = createPayloadCapturingStream(capture); const wrapThinkingHigh = createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high"); @@ -267,43 +291,14 @@ describe("deepseek provider plugin", () => { it("adds blank reasoning_content for replayed tool calls from non-DeepSeek turns", async () => { const capture: PayloadCapture = {}; const model = deepSeekV4Model("deepseek-v4-pro"); - const context = { - messages: [ - { role: "user", content: "hi", timestamp: 1 }, - { - role: "assistant", - api: "openai-completions", - provider: "openai", - model: "gpt-5.4", - content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }], - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "toolUse", - timestamp: 2, - }, - { - role: "toolResult", - toolCallId: "call_1", - toolName: "read", - content: [{ type: "text", text: "ok" }], - isError: false, - timestamp: 3, - }, - ], - tools: [ - { - name: "read", - description: "Read data", - parameters: { type: "object", properties: {}, required: [], additionalProperties: false }, - }, - ], - } as Context; + const context = readToolReplayContext( + replayAssistantMessage({ + provider: "openai", + model: "gpt-5.4", + content: [readToolCall], + stopReason: "toolUse", + }), + ); const baseStreamFn = createPayloadCapturingStream(capture); const wrapThinkingHigh = createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high"); @@ -332,23 +327,12 @@ describe("deepseek provider plugin", () => { const context = { messages: [ { role: "user", content: "hi", timestamp: 1 }, - { - role: "assistant", - api: "openai-completions", + replayAssistantMessage({ provider: "openai", model: "gpt-5.4", content: [{ type: "text", text: "Hello." }], - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, stopReason: "stop", - timestamp: 2, - }, + }), { role: "user", content: "next", timestamp: 3 }, ], } as Context; @@ -368,50 +352,7 @@ describe("deepseek provider plugin", () => { it("strips replayed reasoning_content when DeepSeek V4 thinking is disabled", async () => { const capture: PayloadCapture = {}; const model = deepSeekV4Model("deepseek-v4-flash"); - const context = { - messages: [ - { role: "user", content: "hi", timestamp: 1 }, - { - role: "assistant", - api: "openai-completions", - provider: "deepseek", - model: "deepseek-v4-flash", - content: [ - { - type: "thinking", - thinking: "call reasoning", - thinkingSignature: "reasoning_content", - }, - { type: "toolCall", id: "call_1", name: "read", arguments: {} }, - ], - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "toolUse", - timestamp: 2, - }, - { - role: "toolResult", - toolCallId: "call_1", - toolName: "read", - content: [{ type: "text", text: "ok" }], - isError: false, - timestamp: 3, - }, - ], - tools: [ - { - name: "read", - description: "Read data", - parameters: { type: "object", properties: {}, required: [], additionalProperties: false }, - }, - ], - } as Context; + const context = deepSeekReasoningToolReplayContext(); const baseStreamFn = createPayloadCapturingStream(capture); const wrapThinkingNone = createDeepSeekV4ThinkingWrapper( diff --git a/extensions/discord/src/channel.test.ts b/extensions/discord/src/channel.test.ts index b9f1ea85d94..4f06904e645 100644 --- a/extensions/discord/src/channel.test.ts +++ b/extensions/discord/src/channel.test.ts @@ -173,33 +173,6 @@ describe("discordPlugin outbound", () => { }); }); - it("resolves bare allowlisted Discord user IDs as message-tool DM targets", async () => { - const resolveTarget = discordPlugin.messaging?.targetResolver?.resolveTarget; - if (!resolveTarget) { - throw new Error( - "Expected discordPlugin.messaging.targetResolver.resolveTarget to be defined", - ); - } - - await expect( - resolveTarget({ - cfg: { - channels: { - discord: { - allowFrom: ["1439091261670948987"], - }, - }, - } as OpenClawConfig, - input: "1439091261670948987", - normalized: "channel:1439091261670948987", - preferredKind: "channel", - }), - ).resolves.toMatchObject({ - to: "user:1439091261670948987", - kind: "user", - }); - }); - it("honors per-account replyToMode overrides", () => { const resolveReplyToMode = discordPlugin.threading?.resolveReplyToMode; if (!resolveReplyToMode) { diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index ce393579e9d..a9941ea9f0f 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -80,7 +80,6 @@ import { discordSetupAdapter } from "./setup-adapter.js"; import { createDiscordPluginBase, discordConfigAdapter } from "./shared.js"; import { collectDiscordStatusIssues } from "./status-issues.js"; import { parseDiscordTarget } from "./target-parsing.js"; -import { resolveDiscordTarget } from "./target-resolver.js"; const DISCORD_ACCOUNT_STARTUP_STAGGER_MS = 10_000; const discordMessageAdapter = createChannelMessageAdapterFromOutbound({ @@ -327,21 +326,6 @@ export const discordPlugin: ChannelPlugin targetResolver: { looksLikeId: looksLikeDiscordTargetId, hint: "", - resolveTarget: async ({ cfg, accountId, input, preferredKind }) => { - const target = await resolveDiscordTarget( - input, - { cfg, accountId: accountId ?? undefined }, - { defaultKind: preferredKind === "user" ? "user" : "channel" }, - ); - return target - ? { - to: target.normalized, - kind: target.kind, - display: target.raw, - source: "normalized", - } - : null; - }, }, }, approvalCapability: getDiscordApprovalCapability(), diff --git a/extensions/discord/src/normalize.ts b/extensions/discord/src/normalize.ts index b755fa27650..2a5dddf4822 100644 --- a/extensions/discord/src/normalize.ts +++ b/extensions/discord/src/normalize.ts @@ -31,9 +31,6 @@ export function normalizeDiscordOutboundTarget( } return { ok: true, to: `channel:${trimmed}` }; } - if (/^discord:(?:channel|user):/i.test(trimmed)) { - return { ok: true, to: normalizeDiscordMessagingTarget(trimmed) ?? trimmed }; - } return { ok: true, to: trimmed }; } diff --git a/extensions/discord/src/outbound-adapter.test.ts b/extensions/discord/src/outbound-adapter.test.ts index fc552152e79..96719ca5502 100644 --- a/extensions/discord/src/outbound-adapter.test.ts +++ b/extensions/discord/src/outbound-adapter.test.ts @@ -30,13 +30,6 @@ describe("normalizeDiscordOutboundTarget", () => { expect(normalizeDiscordOutboundTarget("channel:123")).toEqual({ ok: true, to: "channel:123" }); }); - it("normalizes provider-prefixed channel targets", () => { - expect(normalizeDiscordOutboundTarget("discord:channel:123")).toEqual({ - ok: true, - to: "channel:123", - }); - }); - it("passes through user: prefixed targets", () => { expect(normalizeDiscordOutboundTarget("user:123")).toEqual({ ok: true, to: "user:123" }); }); diff --git a/extensions/discord/src/outbound-session-route.test.ts b/extensions/discord/src/outbound-session-route.test.ts index 66cf2925fcf..1d492163df6 100644 --- a/extensions/discord/src/outbound-session-route.test.ts +++ b/extensions/discord/src/outbound-session-route.test.ts @@ -31,36 +31,4 @@ describe("resolveDiscordOutboundSessionRoute", () => { }); expect(route?.threadId).toBeUndefined(); }); - - it("routes provider-prefixed channel targets as channels", () => { - const route = resolveDiscordOutboundSessionRoute({ - cfg: {}, - agentId: "main", - target: "discord:channel:123", - }); - - expect(route).toMatchObject({ - sessionKey: "agent:main:discord:channel:123", - baseSessionKey: "agent:main:discord:channel:123", - chatType: "channel", - from: "discord:channel:123", - to: "channel:123", - }); - }); - - it("keeps legacy provider-prefixed numeric targets as direct messages", () => { - const route = resolveDiscordOutboundSessionRoute({ - cfg: {}, - agentId: "main", - target: "discord:123", - }); - - expect(route).toMatchObject({ - sessionKey: "agent:main:main", - baseSessionKey: "agent:main:main", - chatType: "direct", - from: "discord:123", - to: "user:123", - }); - }); }); diff --git a/extensions/discord/src/target-parsing.ts b/extensions/discord/src/target-parsing.ts index 2f73b6d9b74..b76ec3dc496 100644 --- a/extensions/discord/src/target-parsing.ts +++ b/extensions/discord/src/target-parsing.ts @@ -21,10 +21,6 @@ export function parseDiscordTarget( if (!trimmed) { return undefined; } - const providerPrefixedTarget = parseDiscordProviderPrefixedTarget(trimmed); - if (providerPrefixedTarget) { - return providerPrefixedTarget; - } const userTarget = parseMentionPrefixOrAtUserTarget({ raw: trimmed, mentionPattern: /^<@!?(\d+)>$/, @@ -51,19 +47,6 @@ export function parseDiscordTarget( return buildMessagingTarget("channel", trimmed, trimmed); } -function parseDiscordProviderPrefixedTarget(raw: string): DiscordTarget | undefined { - const match = /^discord:(channel|user):(.+)$/i.exec(raw); - if (!match) { - return undefined; - } - const kind = match[1]?.toLowerCase() as "channel" | "user" | undefined; - const id = match[2]?.trim(); - if (!kind || !id) { - return undefined; - } - return buildMessagingTarget(kind, id, `${kind}:${id}`); -} - export function resolveDiscordChannelId(raw: string): string { const target = parseDiscordTarget(raw, { defaultKind: "channel" }); return requireTargetKind({ platform: "Discord", target, kind: "channel" }); diff --git a/extensions/discord/src/targets.test.ts b/extensions/discord/src/targets.test.ts index 2ab69a5f10f..fbac86c248d 100644 --- a/extensions/discord/src/targets.test.ts +++ b/extensions/discord/src/targets.test.ts @@ -18,7 +18,6 @@ describe("parseDiscordTarget", () => { { input: "<@123>", id: "123", normalized: "user:123" }, { input: "<@!456>", id: "456", normalized: "user:456" }, { input: "user:789", id: "789", normalized: "user:789" }, - { input: "discord:user:789", id: "789", normalized: "user:789" }, { input: "discord:987", id: "987", normalized: "user:987" }, ] as const; for (const testCase of cases) { @@ -33,7 +32,6 @@ describe("parseDiscordTarget", () => { it("parses channel targets", () => { const cases = [ { input: "channel:555", id: "555", normalized: "channel:555" }, - { input: "discord:channel:555", id: "555", normalized: "channel:555" }, { input: "general", id: "general", normalized: "channel:general" }, ] as const; for (const testCase of cases) { @@ -227,10 +225,6 @@ describe("normalizeDiscordMessagingTarget", () => { it("defaults raw numeric ids to channels", () => { expect(normalizeDiscordMessagingTarget("123")).toBe("channel:123"); }); - - it("normalizes provider-prefixed channel targets as channels", () => { - expect(normalizeDiscordMessagingTarget("discord:channel:123")).toBe("channel:123"); - }); }); describe("discord group policy", () => { diff --git a/extensions/memory-core/src/memory/search-manager.test.ts b/extensions/memory-core/src/memory/search-manager.test.ts index f54d23c1a77..bca6bced6f3 100644 --- a/extensions/memory-core/src/memory/search-manager.test.ts +++ b/extensions/memory-core/src/memory/search-manager.test.ts @@ -73,14 +73,18 @@ function createManagerMock(params: { }; } -const mockPrimary = vi.hoisted(() => ({ - ...createManagerMock({ +function createQmdManagerInstanceMock() { + return createManagerMock({ backend: "qmd", provider: "qmd", model: "qmd", requestedProvider: "qmd", withMemorySourceCounts: true, - }), + }); +} + +const mockPrimary = vi.hoisted(() => ({ + ...createQmdManagerInstanceMock(), })); const fallbackManager = vi.hoisted(() => ({ @@ -193,6 +197,58 @@ async function createFailedQmdSearchHarness(params: { agentId: string; errorMess return { cfg, manager: requireManager(first), firstResult: first }; } +async function expectPendingQmdReplacement(params: { + agentId: string; + firstCfg: OpenClawConfig; + secondCfg: OpenClawConfig; + firstAvailability: { command: string; cwd: string }; + secondAvailability: { command: string; cwd: string }; +}) { + const firstPrimary = createQmdManagerInstanceMock(); + const secondPrimary = createQmdManagerInstanceMock(); + const firstGate = createDeferred(); + const secondGate = createDeferred(); + createQmdManagerMock + .mockImplementationOnce(async () => await firstGate.promise) + .mockImplementationOnce(async () => await secondGate.promise); + + const firstPromise = getMemorySearchManager({ + cfg: params.firstCfg, + agentId: params.agentId, + }); + await Promise.resolve(); + const secondPromise = getMemorySearchManager({ + cfg: params.secondCfg, + agentId: params.agentId, + }); + await vi.waitFor(() => { + expect(createQmdManagerMock).toHaveBeenCalledTimes(1); + }); + + firstGate.resolve(firstPrimary as unknown as QmdManagerInstance); + await vi.waitFor(() => { + expect(createQmdManagerMock).toHaveBeenCalledTimes(2); + }); + + secondGate.resolve(secondPrimary as unknown as QmdManagerInstance); + const [first, second] = await Promise.all([firstPromise, secondPromise]); + + requireManager(first); + requireManager(second); + expect(first.manager).not.toBe(second.manager); + expect(firstPrimary.close).toHaveBeenCalledTimes(1); + expect(checkQmdBinaryAvailability).toHaveBeenNthCalledWith(1, { + command: params.firstAvailability.command, + env: process.env, + cwd: nativePath(params.firstAvailability.cwd), + }); + expect(checkQmdBinaryAvailability).toHaveBeenNthCalledWith(2, { + command: params.secondAvailability.command, + env: process.env, + cwd: nativePath(params.secondAvailability.cwd), + }); +} + beforeEach(async () => { await closeAllMemorySearchManagers(); mockPrimary.search.mockClear(); @@ -544,60 +600,12 @@ describe("getMemorySearchManager caching", () => { const agentId = "pending-qmd-workspace-reload"; const firstCfg = createQmdCfg(agentId, "/tmp/workspace-a"); const secondCfg = createQmdCfg(agentId, "/tmp/workspace-b"); - const firstPrimary = createManagerMock({ - backend: "qmd", - provider: "qmd", - model: "qmd", - requestedProvider: "qmd", - withMemorySourceCounts: true, - }); - const secondPrimary = createManagerMock({ - backend: "qmd", - provider: "qmd", - model: "qmd", - requestedProvider: "qmd", - withMemorySourceCounts: true, - }); - const firstGate = createDeferred(); - const secondGate = createDeferred(); - createQmdManagerMock - .mockImplementationOnce(async () => await firstGate.promise) - .mockImplementationOnce(async () => await secondGate.promise); - - const firstPromise = getMemorySearchManager({ cfg: firstCfg, agentId }); - await Promise.resolve(); - const secondPromise = getMemorySearchManager({ cfg: secondCfg, agentId }); - await vi.waitFor( - () => { - expect(createQmdManagerMock).toHaveBeenCalledTimes(1); - }, - { interval: 1 }, - ); - - firstGate.resolve(firstPrimary as unknown as QmdManagerInstance); - await vi.waitFor( - () => { - expect(createQmdManagerMock).toHaveBeenCalledTimes(2); - }, - { interval: 1 }, - ); - - secondGate.resolve(secondPrimary as unknown as QmdManagerInstance); - const [first, second] = await Promise.all([firstPromise, secondPromise]); - - requireManager(first); - requireManager(second); - expect(first.manager).not.toBe(second.manager); - expect(firstPrimary.close).toHaveBeenCalledTimes(1); - expect(checkQmdBinaryAvailability).toHaveBeenNthCalledWith(1, { - command: "qmd", - env: process.env, - cwd: nativePath("/tmp/workspace-a"), - }); - expect(checkQmdBinaryAvailability).toHaveBeenNthCalledWith(2, { - command: "qmd", - env: process.env, - cwd: nativePath("/tmp/workspace-b"), + await expectPendingQmdReplacement({ + agentId, + firstCfg, + secondCfg, + firstAvailability: { command: "qmd", cwd: "/tmp/workspace-a" }, + secondAvailability: { command: "qmd", cwd: "/tmp/workspace-b" }, }); }); @@ -605,60 +613,12 @@ describe("getMemorySearchManager caching", () => { const agentId = "pending-qmd-config-reload"; const firstCfg = createQmdCfg(agentId, "/tmp/workspace", { command: "qmd" }); const secondCfg = createQmdCfg(agentId, "/tmp/workspace", { command: "qmd-alt" }); - const firstPrimary = createManagerMock({ - backend: "qmd", - provider: "qmd", - model: "qmd", - requestedProvider: "qmd", - withMemorySourceCounts: true, - }); - const secondPrimary = createManagerMock({ - backend: "qmd", - provider: "qmd", - model: "qmd", - requestedProvider: "qmd", - withMemorySourceCounts: true, - }); - const firstGate = createDeferred(); - const secondGate = createDeferred(); - createQmdManagerMock - .mockImplementationOnce(async () => await firstGate.promise) - .mockImplementationOnce(async () => await secondGate.promise); - - const firstPromise = getMemorySearchManager({ cfg: firstCfg, agentId }); - await Promise.resolve(); - const secondPromise = getMemorySearchManager({ cfg: secondCfg, agentId }); - await vi.waitFor( - () => { - expect(createQmdManagerMock).toHaveBeenCalledTimes(1); - }, - { interval: 1 }, - ); - - firstGate.resolve(firstPrimary as unknown as QmdManagerInstance); - await vi.waitFor( - () => { - expect(createQmdManagerMock).toHaveBeenCalledTimes(2); - }, - { interval: 1 }, - ); - - secondGate.resolve(secondPrimary as unknown as QmdManagerInstance); - const [first, second] = await Promise.all([firstPromise, secondPromise]); - - requireManager(first); - requireManager(second); - expect(first.manager).not.toBe(second.manager); - expect(firstPrimary.close).toHaveBeenCalledTimes(1); - expect(checkQmdBinaryAvailability).toHaveBeenNthCalledWith(1, { - command: "qmd", - env: process.env, - cwd: nativePath("/tmp/workspace"), - }); - expect(checkQmdBinaryAvailability).toHaveBeenNthCalledWith(2, { - command: "qmd-alt", - env: process.env, - cwd: nativePath("/tmp/workspace"), + await expectPendingQmdReplacement({ + agentId, + firstCfg, + secondCfg, + firstAvailability: { command: "qmd", cwd: "/tmp/workspace" }, + secondAvailability: { command: "qmd-alt", cwd: "/tmp/workspace" }, }); }); diff --git a/extensions/qa-lab/src/mantis/crabbox-runtime.ts b/extensions/qa-lab/src/mantis/crabbox-runtime.ts new file mode 100644 index 00000000000..daeae7770bf --- /dev/null +++ b/extensions/qa-lab/src/mantis/crabbox-runtime.ts @@ -0,0 +1,208 @@ +import { spawn, type SpawnOptions } from "node:child_process"; +import path from "node:path"; +import { pathExists } from "openclaw/plugin-sdk/security-runtime"; + +export type CommandResult = { + stderr: string; + stdout: string; +}; + +export type CommandRunner = ( + command: string, + args: readonly string[], + options: SpawnOptions, +) => Promise; + +export type CrabboxInspect = { + host?: string; + id?: string; + provider?: string; + ready?: boolean; + slug?: string; + sshKey?: string; + sshPort?: string; + sshUser?: string; + state?: string; +}; + +function trimToValue(value: string | undefined) { + const trimmed = value?.trim(); + return trimmed && trimmed.length > 0 ? trimmed : undefined; +} + +export async function defaultCommandRunner( + command: string, + args: readonly string[], + options: SpawnOptions, +): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + ...options, + stdio: ["ignore", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + child.stdout?.on("data", (chunk: Buffer) => { + const text = chunk.toString(); + stdout += text; + if (options.stdio === "inherit") { + process.stdout.write(text); + } + }); + child.stderr?.on("data", (chunk: Buffer) => { + const text = chunk.toString(); + stderr += text; + if (options.stdio === "inherit") { + process.stderr.write(text); + } + }); + child.on("error", reject); + child.on("close", (code, signal) => { + if (code === 0) { + resolve({ stdout, stderr }); + return; + } + const detail = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`; + reject(new Error(`${command} ${args.join(" ")} failed with ${detail}`)); + }); + }); +} + +export async function resolveCrabboxBin(params: { + env: NodeJS.ProcessEnv; + envName: string; + explicit?: string; + repoRoot: string; +}) { + const configured = trimToValue(params.explicit) ?? trimToValue(params.env[params.envName]); + if (configured) { + return configured; + } + const sibling = path.resolve(params.repoRoot, "../crabbox/bin/crabbox"); + if (await pathExists(sibling)) { + return sibling; + } + return "crabbox"; +} + +export function extractLeaseId(output: string) { + return output.match(/\b(?:cbx_[a-f0-9]+|tbx_[A-Za-z0-9_-]+)\b/u)?.[0]; +} + +export function shellQuote(value: string) { + return `'${value.replaceAll("'", "'\\''")}'`; +} + +export async function runCommand(params: { + args: readonly string[]; + command: string; + cwd: string; + env: NodeJS.ProcessEnv; + runner: CommandRunner; + stdio?: "inherit" | "pipe"; +}) { + return params.runner(params.command, params.args, { + cwd: params.cwd, + env: params.env, + stdio: params.stdio ?? "pipe", + }); +} + +export async function warmupCrabbox(params: { + crabboxBin: string; + cwd: string; + env: NodeJS.ProcessEnv; + idleTimeout: string; + machineClass: string; + provider: string; + runner: CommandRunner; + ttl: string; +}) { + const result = await runCommand({ + command: params.crabboxBin, + args: [ + "warmup", + "--provider", + params.provider, + "--desktop", + "--browser", + "--class", + params.machineClass, + "--idle-timeout", + params.idleTimeout, + "--ttl", + params.ttl, + ], + cwd: params.cwd, + env: params.env, + runner: params.runner, + stdio: "inherit", + }); + const leaseId = extractLeaseId(`${result.stdout}\n${result.stderr}`); + if (!leaseId) { + throw new Error("Crabbox warmup did not print a lease id."); + } + return leaseId; +} + +export async function inspectCrabbox(params: { + crabboxBin: string; + cwd: string; + env: NodeJS.ProcessEnv; + leaseId: string; + provider: string; + runner: CommandRunner; +}) { + const result = await runCommand({ + command: params.crabboxBin, + args: ["inspect", "--provider", params.provider, "--id", params.leaseId, "--json"], + cwd: params.cwd, + env: params.env, + runner: params.runner, + }); + return JSON.parse(result.stdout) as CrabboxInspect; +} + +export async function stopCrabbox(params: { + crabboxBin: string; + cwd: string; + env: NodeJS.ProcessEnv; + leaseId: string; + provider: string; + runner: CommandRunner; +}) { + await runCommand({ + command: params.crabboxBin, + args: ["stop", "--provider", params.provider, params.leaseId], + cwd: params.cwd, + env: params.env, + runner: params.runner, + stdio: "inherit", + }); +} + +export function sshCommand(params: { inspect: CrabboxInspect }) { + const { host, sshKey, sshPort, sshUser } = params.inspect; + if (!host || !sshKey || !sshUser) { + throw new Error("Crabbox inspect output is missing SSH copy details."); + } + return { + host, + sshArgs: [ + "ssh", + "-i", + shellQuote(sshKey), + "-p", + sshPort ?? "22", + "-o", + "BatchMode=yes", + "-o", + "ConnectTimeout=15", + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + ].join(" "), + sshUser, + }; +} diff --git a/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.ts b/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.ts index 4a67982146d..74a710aa8eb 100644 --- a/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.ts +++ b/extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.ts @@ -1,10 +1,21 @@ -import { spawn, type SpawnOptions } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { pathExists } from "openclaw/plugin-sdk/security-runtime"; import { ensureRepoBoundDirectory, resolveRepoRelativeOutputDir } from "../cli-paths.js"; +import { + type CommandRunner, + type CrabboxInspect, + defaultCommandRunner, + inspectCrabbox, + resolveCrabboxBin, + runCommand, + shellQuote, + sshCommand, + stopCrabbox, + warmupCrabbox, +} from "./crabbox-runtime.js"; export type MantisDesktopBrowserSmokeOptions = { browserProfileArchiveEnv?: string; @@ -35,29 +46,6 @@ export type MantisDesktopBrowserSmokeResult = { videoPath?: string; }; -type CommandResult = { - stderr: string; - stdout: string; -}; - -type CommandRunner = ( - command: string, - args: readonly string[], - options: SpawnOptions, -) => Promise; - -type CrabboxInspect = { - host?: string; - id?: string; - provider?: string; - ready?: boolean; - slug?: string; - sshKey?: string; - sshPort?: string; - sshUser?: string; - state?: string; -}; - type MantisDesktopBrowserSmokeSummary = { artifacts: { reportPath: string; @@ -115,68 +103,6 @@ function defaultOutputDir(repoRoot: string, startedAt: Date) { return path.join(repoRoot, ".artifacts", "qa-e2e", "mantis", `desktop-browser-${stamp}`); } -async function defaultCommandRunner( - command: string, - args: readonly string[], - options: SpawnOptions, -): Promise { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - ...options, - stdio: ["ignore", "pipe", "pipe"], - }); - let stdout = ""; - let stderr = ""; - child.stdout?.on("data", (chunk: Buffer) => { - const text = chunk.toString(); - stdout += text; - if (options.stdio === "inherit") { - process.stdout.write(text); - } - }); - child.stderr?.on("data", (chunk: Buffer) => { - const text = chunk.toString(); - stderr += text; - if (options.stdio === "inherit") { - process.stderr.write(text); - } - }); - child.on("error", reject); - child.on("close", (code, signal) => { - if (code === 0) { - resolve({ stdout, stderr }); - return; - } - const detail = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`; - reject(new Error(`${command} ${args.join(" ")} failed with ${detail}`)); - }); - }); -} - -async function resolveCrabboxBin(params: { - env: NodeJS.ProcessEnv; - explicit?: string; - repoRoot: string; -}) { - const configured = trimToValue(params.explicit) ?? trimToValue(params.env[CRABBOX_BIN_ENV]); - if (configured) { - return configured; - } - const sibling = path.resolve(params.repoRoot, "../crabbox/bin/crabbox"); - if (await pathExists(sibling)) { - return sibling; - } - return "crabbox"; -} - -function extractLeaseId(output: string) { - return output.match(/\b(?:cbx_[a-f0-9]+|tbx_[A-Za-z0-9_-]+)\b/u)?.[0]; -} - -function shellQuote(value: string) { - return `'${value.replaceAll("'", "'\\''")}'`; -} - function assertSafeEnvName(value: string, label: string) { if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(value)) { throw new Error(`${label} must be an environment variable name.`); @@ -364,76 +290,6 @@ function renderReport(summary: MantisDesktopBrowserSmokeSummary) { return `${lines.join("\n")}\n`; } -async function runCommand(params: { - args: readonly string[]; - command: string; - cwd: string; - env: NodeJS.ProcessEnv; - runner: CommandRunner; - stdio?: "inherit" | "pipe"; -}) { - return params.runner(params.command, params.args, { - cwd: params.cwd, - env: params.env, - stdio: params.stdio ?? "pipe", - }); -} - -async function warmupCrabbox(params: { - crabboxBin: string; - cwd: string; - env: NodeJS.ProcessEnv; - idleTimeout: string; - machineClass: string; - provider: string; - runner: CommandRunner; - ttl: string; -}) { - const result = await runCommand({ - command: params.crabboxBin, - args: [ - "warmup", - "--provider", - params.provider, - "--desktop", - "--browser", - "--class", - params.machineClass, - "--idle-timeout", - params.idleTimeout, - "--ttl", - params.ttl, - ], - cwd: params.cwd, - env: params.env, - runner: params.runner, - stdio: "inherit", - }); - const leaseId = extractLeaseId(`${result.stdout}\n${result.stderr}`); - if (!leaseId) { - throw new Error("Crabbox warmup did not print a lease id."); - } - return leaseId; -} - -async function inspectCrabbox(params: { - crabboxBin: string; - cwd: string; - env: NodeJS.ProcessEnv; - leaseId: string; - provider: string; - runner: CommandRunner; -}) { - const result = await runCommand({ - command: params.crabboxBin, - args: ["inspect", "--provider", params.provider, "--id", params.leaseId, "--json"], - cwd: params.cwd, - env: params.env, - runner: params.runner, - }); - return JSON.parse(result.stdout) as CrabboxInspect; -} - async function copyRemoteArtifacts(params: { cwd: string; env: NodeJS.ProcessEnv; @@ -442,30 +298,13 @@ async function copyRemoteArtifacts(params: { remoteOutputDir: string; runner: CommandRunner; }) { - const { host, sshKey, sshPort, sshUser } = params.inspect; - if (!host || !sshKey || !sshUser) { - throw new Error("Crabbox inspect output is missing SSH copy details."); - } + const { host, sshArgs, sshUser } = sshCommand({ inspect: params.inspect }); await runCommand({ command: "rsync", args: [ "-az", "-e", - [ - "ssh", - "-i", - shellQuote(sshKey), - "-p", - sshPort ?? "22", - "-o", - "BatchMode=yes", - "-o", - "ConnectTimeout=15", - "-o", - "StrictHostKeyChecking=no", - "-o", - "UserKnownHostsFile=/dev/null", - ].join(" "), + sshArgs, "--exclude", "chrome-profile/**", `${sshUser}@${host}:${params.remoteOutputDir}/`, @@ -477,24 +316,6 @@ async function copyRemoteArtifacts(params: { }); } -async function stopCrabbox(params: { - crabboxBin: string; - cwd: string; - env: NodeJS.ProcessEnv; - leaseId: string; - provider: string; - runner: CommandRunner; -}) { - await runCommand({ - command: params.crabboxBin, - args: ["stop", "--provider", params.provider, params.leaseId], - cwd: params.cwd, - env: params.env, - runner: params.runner, - stdio: "inherit", - }); -} - export async function runMantisDesktopBrowserSmoke( opts: MantisDesktopBrowserSmokeOptions = {}, ): Promise { @@ -509,7 +330,12 @@ export async function runMantisDesktopBrowserSmoke( ); const summaryPath = path.join(outputDir, "mantis-desktop-browser-smoke-summary.json"); const reportPath = path.join(outputDir, "mantis-desktop-browser-smoke-report.md"); - const crabboxBin = await resolveCrabboxBin({ env, explicit: opts.crabboxBin, repoRoot }); + const crabboxBin = await resolveCrabboxBin({ + env, + envName: CRABBOX_BIN_ENV, + explicit: opts.crabboxBin, + repoRoot, + }); const provider = trimToValue(opts.provider) ?? trimToValue(env[CRABBOX_PROVIDER_ENV]) ?? DEFAULT_PROVIDER; const machineClass = diff --git a/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts b/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts index 5d523b9983f..83c268b3f73 100644 --- a/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts +++ b/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts @@ -1,4 +1,3 @@ -import { spawn, type SpawnOptions } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; @@ -8,6 +7,18 @@ import { acquireQaCredentialLease, startQaCredentialLeaseHeartbeat, } from "../live-transports/shared/credential-lease.runtime.js"; +import { + type CommandRunner, + type CrabboxInspect, + defaultCommandRunner, + inspectCrabbox, + resolveCrabboxBin, + runCommand, + shellQuote, + sshCommand, + stopCrabbox, + warmupCrabbox, +} from "./crabbox-runtime.js"; export type MantisSlackDesktopSmokeOptions = { alternateModel?: string; @@ -46,17 +57,6 @@ export type MantisSlackDesktopSmokeResult = { videoPath?: string; }; -type CommandResult = { - stderr: string; - stdout: string; -}; - -type CommandRunner = ( - command: string, - args: readonly string[], - options: SpawnOptions, -) => Promise; - type SlackGatewayCredentialPayload = { channelId: string; sutAppToken: string; @@ -68,18 +68,6 @@ type SlackGatewayCredentialLease = Awaited< >; type SlackGatewayCredentialHeartbeat = ReturnType; -type CrabboxInspect = { - host?: string; - id?: string; - provider?: string; - ready?: boolean; - slug?: string; - sshKey?: string; - sshPort?: string; - sshUser?: string; - state?: string; -}; - type MantisSlackDesktopSmokeSummary = { artifacts: { reportPath: string; @@ -218,44 +206,6 @@ function defaultOutputDir(repoRoot: string, startedAt: Date) { return path.join(repoRoot, ".artifacts", "qa-e2e", "mantis", `slack-desktop-${stamp}`); } -async function defaultCommandRunner( - command: string, - args: readonly string[], - options: SpawnOptions, -): Promise { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - ...options, - stdio: ["ignore", "pipe", "pipe"], - }); - let stdout = ""; - let stderr = ""; - child.stdout?.on("data", (chunk: Buffer) => { - const text = chunk.toString(); - stdout += text; - if (options.stdio === "inherit") { - process.stdout.write(text); - } - }); - child.stderr?.on("data", (chunk: Buffer) => { - const text = chunk.toString(); - stderr += text; - if (options.stdio === "inherit") { - process.stderr.write(text); - } - }); - child.on("error", reject); - child.on("close", (code, signal) => { - if (code === 0) { - resolve({ stdout, stderr }); - return; - } - const detail = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`; - reject(new Error(`${command} ${args.join(" ")} failed with ${detail}`)); - }); - }); -} - async function readRemoteMetadata( outputDir: string, ): Promise { @@ -281,22 +231,6 @@ async function readRemoteMetadata( return undefined; } } -async function resolveCrabboxBin(params: { - env: NodeJS.ProcessEnv; - explicit?: string; - repoRoot: string; -}) { - const configured = trimToValue(params.explicit) ?? trimToValue(params.env[CRABBOX_BIN_ENV]); - if (configured) { - return configured; - } - const sibling = path.resolve(params.repoRoot, "../crabbox/bin/crabbox"); - if (await pathExists(sibling)) { - return sibling; - } - return "crabbox"; -} - function buildCrabboxEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { const next = { ...env, @@ -411,14 +345,6 @@ async function prepareGatewayCredentialEnv(params: { }; } -function extractLeaseId(output: string) { - return output.match(/\b(?:cbx_[a-f0-9]+|tbx_[A-Za-z0-9_-]+)\b/u)?.[0]; -} - -function shellQuote(value: string) { - return `'${value.replaceAll("'", "'\\''")}'`; -} - function renderRemoteScript(params: { alternateModel: string; credentialRole: string; @@ -715,102 +641,6 @@ function renderReport(summary: MantisSlackDesktopSmokeSummary) { return `${lines.join("\n")}\n`; } -async function runCommand(params: { - args: readonly string[]; - command: string; - cwd: string; - env: NodeJS.ProcessEnv; - runner: CommandRunner; - stdio?: "inherit" | "pipe"; -}) { - return params.runner(params.command, params.args, { - cwd: params.cwd, - env: params.env, - stdio: params.stdio ?? "pipe", - }); -} - -async function warmupCrabbox(params: { - crabboxBin: string; - cwd: string; - env: NodeJS.ProcessEnv; - idleTimeout: string; - machineClass: string; - provider: string; - runner: CommandRunner; - ttl: string; -}) { - const result = await runCommand({ - command: params.crabboxBin, - args: [ - "warmup", - "--provider", - params.provider, - "--desktop", - "--browser", - "--class", - params.machineClass, - "--idle-timeout", - params.idleTimeout, - "--ttl", - params.ttl, - ], - cwd: params.cwd, - env: params.env, - runner: params.runner, - stdio: "inherit", - }); - const leaseId = extractLeaseId(`${result.stdout}\n${result.stderr}`); - if (!leaseId) { - throw new Error("Crabbox warmup did not print a lease id."); - } - return leaseId; -} - -async function inspectCrabbox(params: { - crabboxBin: string; - cwd: string; - env: NodeJS.ProcessEnv; - leaseId: string; - provider: string; - runner: CommandRunner; -}) { - const result = await runCommand({ - command: params.crabboxBin, - args: ["inspect", "--provider", params.provider, "--id", params.leaseId, "--json"], - cwd: params.cwd, - env: params.env, - runner: params.runner, - }); - return JSON.parse(result.stdout) as CrabboxInspect; -} - -function sshCommand(params: { inspect: CrabboxInspect }) { - const { host, sshKey, sshPort, sshUser } = params.inspect; - if (!host || !sshKey || !sshUser) { - throw new Error("Crabbox inspect output is missing SSH copy details."); - } - return { - host, - sshUser, - sshArgs: [ - "ssh", - "-i", - shellQuote(sshKey), - "-p", - sshPort ?? "22", - "-o", - "BatchMode=yes", - "-o", - "ConnectTimeout=15", - "-o", - "StrictHostKeyChecking=no", - "-o", - "UserKnownHostsFile=/dev/null", - ].join(" "), - }; -} - async function copyRemoteArtifacts(params: { cwd: string; env: NodeJS.ProcessEnv; @@ -849,24 +679,6 @@ async function copyRemoteArtifacts(params: { }).catch(() => ({ stdout: "", stderr: "" })); } -async function stopCrabbox(params: { - crabboxBin: string; - cwd: string; - env: NodeJS.ProcessEnv; - leaseId: string; - provider: string; - runner: CommandRunner; -}) { - await runCommand({ - command: params.crabboxBin, - args: ["stop", "--provider", params.provider, params.leaseId], - cwd: params.cwd, - env: params.env, - runner: params.runner, - stdio: "inherit", - }); -} - export async function runMantisSlackDesktopSmoke( opts: MantisSlackDesktopSmokeOptions = {}, ): Promise { @@ -882,7 +694,12 @@ export async function runMantisSlackDesktopSmoke( ); const summaryPath = path.join(outputDir, "mantis-slack-desktop-smoke-summary.json"); const reportPath = path.join(outputDir, "mantis-slack-desktop-smoke-report.md"); - const crabboxBin = await resolveCrabboxBin({ env, explicit: opts.crabboxBin, repoRoot }); + const crabboxBin = await resolveCrabboxBin({ + env, + envName: CRABBOX_BIN_ENV, + explicit: opts.crabboxBin, + repoRoot, + }); const provider = trimToValue(opts.provider) ?? trimToValue(env[CRABBOX_PROVIDER_ENV]) ?? DEFAULT_PROVIDER; const machineClass = diff --git a/extensions/qa-lab/src/mantis/visual-task.runtime.ts b/extensions/qa-lab/src/mantis/visual-task.runtime.ts index 64345ddb568..6183eed49a2 100644 --- a/extensions/qa-lab/src/mantis/visual-task.runtime.ts +++ b/extensions/qa-lab/src/mantis/visual-task.runtime.ts @@ -1,9 +1,18 @@ -import { spawn, type SpawnOptions } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { pathExists, writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime"; import { ensureRepoBoundDirectory, resolveRepoRelativeOutputDir } from "../cli-paths.js"; +import { + type CommandRunner, + type CrabboxInspect, + defaultCommandRunner, + inspectCrabbox, + resolveCrabboxBin, + runCommand, + stopCrabbox, + warmupCrabbox, +} from "./crabbox-runtime.js"; export type MantisVisualTaskVisionMode = "image-describe" | "metadata"; @@ -56,24 +65,6 @@ export type MantisVisualTaskResult = { videoPath?: string; }; -type CommandResult = { - stderr: string; - stdout: string; -}; - -type CommandRunner = ( - command: string, - args: readonly string[], - options: SpawnOptions, -) => Promise; - -type CrabboxInspect = { - id?: string; - provider?: string; - slug?: string; - state?: string; -}; - type MantisVisualDriverResult = { browserUrl: string; error?: string; @@ -174,44 +165,6 @@ function resolveMantisOutputDir(repoRoot: string, outputDir: string | undefined, : (resolveRepoRelativeOutputDir(repoRoot, configured) ?? defaultOutputDir(repoRoot, startedAt)); } -async function defaultCommandRunner( - command: string, - args: readonly string[], - options: SpawnOptions, -): Promise { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - ...options, - stdio: ["ignore", "pipe", "pipe"], - }); - let stdout = ""; - let stderr = ""; - child.stdout?.on("data", (chunk: Buffer) => { - const text = chunk.toString(); - stdout += text; - if (options.stdio === "inherit") { - process.stdout.write(text); - } - }); - child.stderr?.on("data", (chunk: Buffer) => { - const text = chunk.toString(); - stderr += text; - if (options.stdio === "inherit") { - process.stderr.write(text); - } - }); - child.on("error", reject); - child.on("close", (code, signal) => { - if (code === 0) { - resolve({ stdout, stderr }); - return; - } - const detail = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`; - reject(new Error(`${command} ${args.join(" ")} failed with ${detail}`)); - }); - }); -} - async function nonEmptyFileExists(filePath: string) { try { const stat = await fs.stat(filePath); @@ -221,26 +174,6 @@ async function nonEmptyFileExists(filePath: string) { } } -async function resolveCrabboxBin(params: { - env: NodeJS.ProcessEnv; - explicit?: string; - repoRoot: string; -}) { - const configured = trimToValue(params.explicit) ?? trimToValue(params.env[CRABBOX_BIN_ENV]); - if (configured) { - return configured; - } - const sibling = path.resolve(params.repoRoot, "../crabbox/bin/crabbox"); - if (await pathExists(sibling)) { - return sibling; - } - return "crabbox"; -} - -function extractLeaseId(output: string) { - return output.match(/\b(?:cbx_[a-f0-9]+|tbx_[A-Za-z0-9_-]+)\b/u)?.[0]; -} - function normalizeVisionMode(value: string | undefined): MantisVisualTaskVisionMode { const normalized = trimToValue(value); if (normalized === undefined || normalized === "image-describe") { @@ -270,21 +203,6 @@ function buildVisionPrompt(prompt: string | undefined, expectText: string | unde return `${base}\n\nVisual assertion contract: return only valid JSON: {"visible": boolean, "evidence": string, "reason": string}. Set visible=true only when the exact text "${expectText}" is actually visible in the screenshot; text quoted in the prompt or a negative statement is not evidence.`; } -async function runCommand(params: { - args: readonly string[]; - command: string; - cwd: string; - env: NodeJS.ProcessEnv; - runner: CommandRunner; - stdio?: "inherit" | "pipe"; -}) { - return params.runner(params.command, params.args, { - cwd: params.cwd, - env: params.env, - stdio: params.stdio ?? "pipe", - }); -} - async function runCommandWithExternalOutput(params: { outputPath: string; buildArgs: (tempPath: string) => readonly string[]; @@ -323,79 +241,6 @@ async function runCommandWithExternalOutput(params: { } } -async function warmupCrabbox(params: { - crabboxBin: string; - cwd: string; - env: NodeJS.ProcessEnv; - idleTimeout: string; - machineClass: string; - provider: string; - runner: CommandRunner; - ttl: string; -}) { - const result = await runCommand({ - command: params.crabboxBin, - args: [ - "warmup", - "--provider", - params.provider, - "--desktop", - "--browser", - "--class", - params.machineClass, - "--idle-timeout", - params.idleTimeout, - "--ttl", - params.ttl, - ], - cwd: params.cwd, - env: params.env, - runner: params.runner, - stdio: "inherit", - }); - const leaseId = extractLeaseId(`${result.stdout}\n${result.stderr}`); - if (!leaseId) { - throw new Error("Crabbox warmup did not print a lease id."); - } - return leaseId; -} - -async function inspectCrabbox(params: { - crabboxBin: string; - cwd: string; - env: NodeJS.ProcessEnv; - leaseId: string; - provider: string; - runner: CommandRunner; -}) { - const result = await runCommand({ - command: params.crabboxBin, - args: ["inspect", "--provider", params.provider, "--id", params.leaseId, "--json"], - cwd: params.cwd, - env: params.env, - runner: params.runner, - }); - return JSON.parse(result.stdout) as CrabboxInspect; -} - -async function stopCrabbox(params: { - crabboxBin: string; - cwd: string; - env: NodeJS.ProcessEnv; - leaseId: string; - provider: string; - runner: CommandRunner; -}) { - await runCommand({ - command: params.crabboxBin, - args: ["stop", "--provider", params.provider, params.leaseId], - cwd: params.cwd, - env: params.env, - runner: params.runner, - stdio: "inherit", - }); -} - function buildVisualDriverArgs(params: { browserUrl: string; crabboxBin: string; @@ -621,7 +466,12 @@ export async function runMantisVisualDriver( ); const resultPath = path.join(outputDir, "mantis-visual-task-driver-result.json"); const screenshotPath = path.join(outputDir, "visual-task.png"); - const crabboxBin = await resolveCrabboxBin({ env, explicit: opts.crabboxBin, repoRoot }); + const crabboxBin = await resolveCrabboxBin({ + env, + envName: CRABBOX_BIN_ENV, + explicit: opts.crabboxBin, + repoRoot, + }); const provider = trimToValue(opts.provider) ?? trimToValue(env.CRABBOX_RECORD_PROVIDER) ?? @@ -772,7 +622,12 @@ export async function runMantisVisualTask( const driverResultPath = path.join(outputDir, "mantis-visual-task-driver-result.json"); const screenshotPath = path.join(outputDir, "visual-task.png"); const videoPath = path.join(outputDir, "visual-task.mp4"); - const crabboxBin = await resolveCrabboxBin({ env, explicit: opts.crabboxBin, repoRoot }); + const crabboxBin = await resolveCrabboxBin({ + env, + envName: CRABBOX_BIN_ENV, + explicit: opts.crabboxBin, + repoRoot, + }); const provider = trimToValue(opts.provider) ?? trimToValue(env[CRABBOX_PROVIDER_ENV]) ?? DEFAULT_PROVIDER; const machineClass = diff --git a/extensions/telegram/src/bot-access.ts b/extensions/telegram/src/bot-access.ts index 724eac1a71f..c00236f054b 100644 --- a/extensions/telegram/src/bot-access.ts +++ b/extensions/telegram/src/bot-access.ts @@ -4,11 +4,6 @@ import { mergeDmAllowFromSources, type AllowlistMatch, } from "openclaw/plugin-sdk/allow-from"; -import { - parseAccessGroupAllowFromEntry, - resolveAccessGroupAllowFromMatches, -} from "openclaw/plugin-sdk/command-auth"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; @@ -81,39 +76,6 @@ export const isSenderAllowed = (params: { return isSenderIdAllowed(allow, senderId, true); }; -export async function expandTelegramAllowFromWithAccessGroups(params: { - cfg?: OpenClawConfig; - allowFrom?: Array; - accountId?: string; - senderId?: string; -}): Promise { - const allowFrom = (params.allowFrom ?? []).map(String); - if (!params.senderId) { - return allowFrom; - } - const matched = await resolveAccessGroupAllowFromMatches({ - cfg: params.cfg, - allowFrom, - channel: "telegram", - accountId: params.accountId ?? "default", - senderId: params.senderId, - isSenderAllowed: (senderId, entries) => - isSenderAllowed({ - allow: normalizeAllowFrom(entries), - senderId, - }), - }); - if (matched.length === 0) { - return allowFrom; - } - const matchedGroups = new Set(matched); - const expanded = allowFrom.filter((entry) => { - const groupName = parseAccessGroupAllowFromEntry(entry); - return groupName == null || !matchedGroups.has(`accessGroup:${groupName}`); - }); - return Array.from(new Set([...expanded, params.senderId])); -} - export { firstDefined }; export const resolveSenderAllowMatch = (params: { diff --git a/extensions/telegram/src/bot-handlers.runtime.ts b/extensions/telegram/src/bot-handlers.runtime.ts index 6f9df50d1fe..84d7a61c45d 100644 --- a/extensions/telegram/src/bot-handlers.runtime.ts +++ b/extensions/telegram/src/bot-handlers.runtime.ts @@ -37,7 +37,6 @@ import { import { resolveTelegramAccount, resolveTelegramMediaRuntimeOptions } from "./accounts.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; import { - expandTelegramAllowFromWithAccessGroups, isSenderAllowed, normalizeDmAllowFromWithStore, type NormalizedAllowFrom, @@ -710,17 +709,14 @@ export const registerTelegramHandlers = ({ chatId: number; isGroup: boolean; isForum: boolean; - senderId?: string; messageThreadId?: number; groupAllowContext?: TelegramGroupAllowContext; }): Promise => { const groupAllowContext = params.groupAllowContext ?? (await resolveTelegramGroupAllowFromContext({ - cfg, chatId: params.chatId, accountId, - senderId: params.senderId, isGroup: params.isGroup, isForum: params.isForum, messageThreadId: params.messageThreadId, @@ -921,7 +917,6 @@ export const registerTelegramHandlers = ({ chatId, isGroup, isForum, - senderId, }); const senderAuthorization = authorizeTelegramEventSender({ chatId, @@ -1351,13 +1346,10 @@ export const registerTelegramHandlers = ({ isForum: callbackMessage.chat.is_forum, getChat, }); - const senderId = callback.from?.id ? String(callback.from.id) : ""; - const senderUsername = callback.from?.username ?? ""; const eventAuthContext = await resolveTelegramEventAuthorizationContext({ chatId, isGroup, isForum, - senderId, messageThreadId, }); const { resolvedThreadId, dmThreadId, storeAllowFrom, groupConfig } = eventAuthContext; @@ -1368,6 +1360,8 @@ export const registerTelegramHandlers = ({ ); return; } + const senderId = callback.from?.id ? String(callback.from.id) : ""; + const senderUsername = callback.from?.username ?? ""; const authorizationMode: TelegramEventAuthorizationMode = !isGroup || (!execApprovalButtonsEnabled && inlineButtonsScope === "allowlist") ? "callback-allowlist" @@ -1894,7 +1888,6 @@ export const registerTelegramHandlers = ({ chatId: event.chatId, isGroup: event.isGroup, isForum: event.isForum, - senderId: event.senderId, messageThreadId: event.messageThreadId, }); const { @@ -1910,14 +1903,8 @@ export const registerTelegramHandlers = ({ } = eventAuthContext; // For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom const dmAllowFrom = groupAllowOverride ?? allowFrom; - const expandedDmAllowFrom = await expandTelegramAllowFromWithAccessGroups({ - cfg, - allowFrom: dmAllowFrom, - accountId, - senderId: event.senderId, - }); const effectiveDmAllow = normalizeDmAllowFromWithStore({ - allowFrom: expandedDmAllowFrom, + allowFrom: dmAllowFrom, storeAllowFrom, dmPolicy, }); diff --git a/extensions/telegram/src/bot-message-context.ts b/extensions/telegram/src/bot-message-context.ts index d79b132709b..f7bdd20edc3 100644 --- a/extensions/telegram/src/bot-message-context.ts +++ b/extensions/telegram/src/bot-message-context.ts @@ -10,12 +10,7 @@ import { normalizeAccountId, resolveThreadSessionKeys } from "openclaw/plugin-sd import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { mergeTelegramAccountConfig, resolveDefaultTelegramAccountId } from "./accounts.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; -import { - expandTelegramAllowFromWithAccessGroups, - firstDefined, - normalizeAllowFrom, - normalizeDmAllowFromWithStore, -} from "./bot-access.js"; +import { firstDefined, normalizeAllowFrom, normalizeDmAllowFromWithStore } from "./bot-access.js"; import { resolveTelegramInboundBody } from "./bot-message-context.body.js"; import { buildTelegramInboundContextPayload, @@ -263,25 +258,13 @@ export const buildTelegramMessageContext = async ({ const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); // For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom const dmAllowFrom = groupAllowOverride ?? allowFrom; - const expandedDmAllowFrom = await expandTelegramAllowFromWithAccessGroups({ - cfg: freshCfg, - allowFrom: dmAllowFrom, - accountId: account.accountId, - senderId, - }); const effectiveDmAllow = normalizeDmAllowFromWithStore({ - allowFrom: expandedDmAllowFrom, + allowFrom: dmAllowFrom, storeAllowFrom, dmPolicy: effectiveDmPolicy, }); // Group sender checks are explicit and must not inherit DM pairing-store entries. - const expandedGroupAllowFrom = await expandTelegramAllowFromWithAccessGroups({ - cfg: freshCfg, - allowFrom: groupAllowOverride ?? groupAllowFrom, - accountId: account.accountId, - senderId, - }); - const effectiveGroupAllow = normalizeAllowFrom(expandedGroupAllowFrom); + const effectiveGroupAllow = normalizeAllowFrom(groupAllowOverride ?? groupAllowFrom); const hasGroupAllowOverride = groupAllowOverride !== undefined; const senderUsername = msg.from?.username ?? ""; const baseAccess = evaluateTelegramGroupBaseAccess({ diff --git a/extensions/telegram/src/bot-message-dispatch.test.ts b/extensions/telegram/src/bot-message-dispatch.test.ts index 9b6c9d2b35a..b634ec9cb8f 100644 --- a/extensions/telegram/src/bot-message-dispatch.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.test.ts @@ -924,24 +924,6 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(deliverReplies).not.toHaveBeenCalled(); }); - it("waits for queued draft-lane partials before finalizing the Telegram reply", async () => { - const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 }); - dispatchReplyWithBufferedBlockDispatcher.mockImplementation( - async ({ dispatcherOptions, replyOptions }) => { - const pendingPartial = replyOptions?.onPartialReply?.({ text: "Working" }); - await dispatcherOptions.deliver({ text: "Done" }, { kind: "final" }); - await pendingPartial; - return { queuedFinal: true }; - }, - ); - - await dispatchWithContext({ context: createContext() }); - - expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, "Working"); - expect(answerDraftStream.update).toHaveBeenNthCalledWith(2, "Done"); - expect(deliverReplies).not.toHaveBeenCalled(); - }); - it("keeps progress updates in a draft and sends the final answer normally", async () => { const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 }); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( diff --git a/extensions/telegram/src/bot-native-commands.ts b/extensions/telegram/src/bot-native-commands.ts index e25c945e39d..00622c15603 100644 --- a/extensions/telegram/src/bot-native-commands.ts +++ b/extensions/telegram/src/bot-native-commands.ts @@ -53,11 +53,7 @@ import { } from "openclaw/plugin-sdk/text-runtime"; import { resolveTelegramAccount } from "./accounts.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; -import { - expandTelegramAllowFromWithAccessGroups, - isSenderAllowed, - normalizeDmAllowFromWithStore, -} from "./bot-access.js"; +import { isSenderAllowed, normalizeDmAllowFromWithStore } from "./bot-access.js"; import type { TelegramBotDeps } from "./bot-deps.js"; import type { TelegramMediaRef } from "./bot-message-context.js"; import type { TelegramMessageContextOptions } from "./bot-message-context.types.js"; @@ -493,13 +489,9 @@ async function resolveTelegramCommandAuth(params: { } = params; const { chatId, isGroup, isForum, messageThreadId, threadParams } = await resolveTelegramNativeCommandThreadContext({ msg, bot }); - const senderId = msg.from?.id ? String(msg.from.id) : ""; - const senderUsername = msg.from?.username ?? ""; const groupAllowContext = await resolveTelegramGroupAllowFromContext({ - cfg, chatId, accountId, - senderId, isGroup, isForum, messageThreadId, @@ -530,12 +522,8 @@ async function resolveTelegramCommandAuth(params: { } // For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom const dmAllowFrom = groupAllowOverride ?? allowFrom; - const expandedDmAllowFrom = await expandTelegramAllowFromWithAccessGroups({ - cfg, - allowFrom: dmAllowFrom, - accountId, - senderId, - }); + const senderId = msg.from?.id ? String(msg.from.id) : ""; + const senderUsername = msg.from?.username ?? ""; const commandsAllowFrom = cfg.commands?.allowFrom; const commandsAllowFromConfigured = commandsAllowFrom != null && @@ -639,7 +627,7 @@ async function resolveTelegramCommandAuth(params: { } const dmAllow = normalizeDmAllowFromWithStore({ - allowFrom: expandedDmAllowFrom, + allowFrom: dmAllowFrom, storeAllowFrom: isGroup ? [] : storeAllowFrom, dmPolicy: effectiveDmPolicy, }); diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index ae3a2ebc3ce..e1817bed49e 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -1584,60 +1584,6 @@ describe("createTelegramBot", () => { }, expectedReplyCount: 1, }, - { - name: "allows group messages from senders in accessGroup allowFrom when groupPolicy is 'allowlist'", - config: { - accessGroups: { - owners: { - type: "message.senders", - members: { - telegram: ["123456789"], - }, - }, - }, - channels: { - telegram: { - groupPolicy: "allowlist", - allowFrom: ["accessGroup:owners"], - groups: { "*": { requireMention: false } }, - }, - }, - }, - message: { - chat: { id: -100123456789, type: "group", title: "Test Group" }, - from: { id: 123456789, username: "testuser" }, - text: "hello", - date: 1736380800, - }, - expectedReplyCount: 1, - }, - { - name: "blocks group messages from senders outside accessGroup allowFrom when groupPolicy is 'allowlist'", - config: { - accessGroups: { - owners: { - type: "message.senders", - members: { - telegram: ["123456789"], - }, - }, - }, - channels: { - telegram: { - groupPolicy: "allowlist", - allowFrom: ["accessGroup:owners"], - groups: { "*": { requireMention: false } }, - }, - }, - }, - message: { - chat: { id: -100123456789, type: "group", title: "Test Group" }, - from: { id: 999999, username: "notallowed" }, - text: "hello", - date: 1736380800, - }, - expectedReplyCount: 0, - }, { name: "blocks group messages when allowFrom is configured with @username entries (numeric IDs required)", config: { @@ -2707,31 +2653,6 @@ describe("createTelegramBot", () => { }, expectedReplyCount: 1, }, - { - name: "allows direct messages with accessGroup allowFrom entries", - config: { - accessGroups: { - owners: { - type: "message.senders", - members: { - telegram: ["123456789"], - }, - }, - }, - channels: { - telegram: { - allowFrom: ["accessGroup:owners"], - }, - }, - }, - message: { - chat: { id: 777777777, type: "private" }, - from: { id: 123456789, username: "testuser" }, - text: "hello", - date: 1736380800, - }, - expectedReplyCount: 1, - }, { name: "falls back to direct message chat id when sender user id is missing", config: { diff --git a/extensions/telegram/src/bot/helpers.ts b/extensions/telegram/src/bot/helpers.ts index 31e62994786..f4440e4d0a3 100644 --- a/extensions/telegram/src/bot/helpers.ts +++ b/extensions/telegram/src/bot/helpers.ts @@ -1,7 +1,6 @@ import type { Chat, Message } from "@grammyjs/types"; import { formatLocationText } from "openclaw/plugin-sdk/channel-inbound"; import type { - OpenClawConfig, TelegramAccountConfig, TelegramDirectConfig, TelegramGroupConfig, @@ -11,12 +10,7 @@ import type { import { readChannelAllowFromStore } from "openclaw/plugin-sdk/conversation-runtime"; import { normalizeAccountId } from "openclaw/plugin-sdk/routing"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; -import { - expandTelegramAllowFromWithAccessGroups, - firstDefined, - normalizeAllowFrom, - type NormalizedAllowFrom, -} from "../bot-access.js"; +import { firstDefined, normalizeAllowFrom, type NormalizedAllowFrom } from "../bot-access.js"; import { normalizeTelegramReplyToMessageId } from "../outbound-params.js"; import { resolveTelegramPreviewStreamMode } from "../preview-streaming.js"; import { @@ -174,10 +168,8 @@ export function withResolvedTelegramForumFlag( } export async function resolveTelegramGroupAllowFromContext(params: { - cfg?: OpenClawConfig; chatId: string | number; accountId?: string; - senderId?: string; isGroup?: boolean; isForum?: boolean; messageThreadId?: number | null; @@ -222,13 +214,7 @@ export async function resolveTelegramGroupAllowFromContext(params: { const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); // Group sender access must remain explicit (groupAllowFrom/per-group allowFrom only). // DM pairing store entries are not a group authorization source. - const expandedGroupAllowFrom = await expandTelegramAllowFromWithAccessGroups({ - cfg: params.cfg, - allowFrom: groupAllowOverride ?? params.groupAllowFrom, - accountId, - senderId: params.senderId, - }); - const effectiveGroupAllow = normalizeAllowFrom(expandedGroupAllowFrom); + const effectiveGroupAllow = normalizeAllowFrom(groupAllowOverride ?? params.groupAllowFrom); const hasGroupAllowOverride = groupAllowOverride !== undefined; return { resolvedThreadId, diff --git a/extensions/telegram/src/polling-liveness.test.ts b/extensions/telegram/src/polling-liveness.test.ts index 27efc8b5ca9..b38b1bb886d 100644 --- a/extensions/telegram/src/polling-liveness.test.ts +++ b/extensions/telegram/src/polling-liveness.test.ts @@ -20,23 +20,19 @@ describe("TelegramPollingLivenessTracker", () => { ); }); - it("detects a polling stall while a recent non-polling API call is in flight", () => { + it("does not detect a polling stall while a recent non-polling API call is in flight", () => { let now = 0; const tracker = new TelegramPollingLivenessTracker({ now: () => now }); - tracker.noteGetUpdatesStarted({ offset: 9 }); - now = 60_000; const callId = tracker.noteApiCallStarted(); now = 120_001; - const stall = tracker.detectStall({ - thresholdMs: POLL_STALL_THRESHOLD_MS, - }); - expect(stall?.message).toContain("active getUpdates stuck"); - expect(stall?.message).toContain("inFlight=1 outcome=started startedAt=0"); - expect(stall?.message).toContain("offset=9"); - expect(stall?.message).toContain("apiElapsedMs=60001"); + expect( + tracker.detectStall({ + thresholdMs: POLL_STALL_THRESHOLD_MS, + }), + ).toBeNull(); tracker.noteApiCallFinished(callId); }); diff --git a/extensions/telegram/src/polling-liveness.ts b/extensions/telegram/src/polling-liveness.ts index 674a90f2185..b57237de020 100644 --- a/extensions/telegram/src/polling-liveness.ts +++ b/extensions/telegram/src/polling-liveness.ts @@ -10,12 +10,6 @@ type TelegramPollingStall = { message: string; }; -type TelegramPollingStallSnapshot = { - elapsedMs: number; - apiElapsedMs: number; - label: string; -}; - export class TelegramPollingLivenessTracker { #lastGetUpdatesAt: number; #lastApiActivityAt: number; @@ -97,9 +91,22 @@ export class TelegramPollingLivenessTracker { detectStall(params: { thresholdMs: number; now?: number }): TelegramPollingStall | null { const now = params.now ?? this.#now(); - const stall = this.#resolvePollingStallSnapshot(now); + const activeElapsed = + this.#inFlightGetUpdates > 0 && this.#lastGetUpdatesStartedAt != null + ? now - this.#lastGetUpdatesStartedAt + : 0; + const idleElapsed = + this.#inFlightGetUpdates > 0 + ? 0 + : now - (this.#lastGetUpdatesFinishedAt ?? this.#lastGetUpdatesAt); + const elapsed = this.#inFlightGetUpdates > 0 ? activeElapsed : idleElapsed; + const apiLivenessAt = + this.#latestInFlightApiStartedAt == null + ? this.#lastApiActivityAt + : Math.max(this.#lastApiActivityAt, this.#latestInFlightApiStartedAt); + const apiElapsed = now - apiLivenessAt; - if (stall.elapsedMs <= params.thresholdMs) { + if (elapsed <= params.thresholdMs || apiElapsed <= params.thresholdMs) { return null; } if (this.#stallDiagLoggedAt && now - this.#stallDiagLoggedAt < params.thresholdMs / 2) { @@ -107,8 +114,12 @@ export class TelegramPollingLivenessTracker { } this.#stallDiagLoggedAt = now; + const elapsedLabel = + this.#inFlightGetUpdates > 0 + ? `active getUpdates stuck for ${formatDurationPrecise(elapsed)}` + : `no completed getUpdates for ${formatDurationPrecise(elapsed)}`; return { - message: `Polling stall detected (${stall.label}); forcing restart. [diag ${this.formatDiagnosticFields("error")} apiElapsedMs=${stall.apiElapsedMs}]`, + message: `Polling stall detected (${elapsedLabel}); forcing restart. [diag ${this.formatDiagnosticFields("error")}]`, }; } @@ -127,28 +138,6 @@ export class TelegramPollingLivenessTracker { return newestStartedAt; } - #resolvePollingStallSnapshot(now: number): TelegramPollingStallSnapshot { - const activeElapsed = - this.#inFlightGetUpdates > 0 && this.#lastGetUpdatesStartedAt != null - ? now - this.#lastGetUpdatesStartedAt - : 0; - const idleElapsed = - this.#inFlightGetUpdates > 0 - ? 0 - : now - (this.#lastGetUpdatesFinishedAt ?? this.#lastGetUpdatesAt); - const elapsedMs = this.#inFlightGetUpdates > 0 ? activeElapsed : idleElapsed; - const apiLivenessAt = - this.#latestInFlightApiStartedAt == null - ? this.#lastApiActivityAt - : Math.max(this.#lastApiActivityAt, this.#latestInFlightApiStartedAt); - const apiElapsedMs = now - apiLivenessAt; - const label = - this.#inFlightGetUpdates > 0 - ? `active getUpdates stuck for ${formatDurationPrecise(elapsedMs)}` - : `no completed getUpdates for ${formatDurationPrecise(elapsedMs)}`; - return { elapsedMs, apiElapsedMs, label }; - } - #now(): number { return this.options.now?.() ?? Date.now(); } diff --git a/extensions/telegram/src/polling-session.test.ts b/extensions/telegram/src/polling-session.test.ts index 90de614f568..553fb750080 100644 --- a/extensions/telegram/src/polling-session.test.ts +++ b/extensions/telegram/src/polling-session.test.ts @@ -765,7 +765,7 @@ describe("TelegramPollingSession", () => { }); }); - it("triggers stall restart when getUpdates is stale despite recent non-getUpdates API success", async () => { + it("does not trigger stall restart when non-getUpdates API calls are active", async () => { const abort = new AbortController(); const botStop = vi.fn(async () => undefined); const runnerStop = vi.fn(async () => undefined); @@ -773,8 +773,9 @@ describe("TelegramPollingSession", () => { const resolveFirstTask = mockLongRunningPollingCycle(runnerStop); // t=0: lastGetUpdatesAt and lastApiActivityAt initialized - // t=150_001: watchdog fires (getUpdates stale for 150s). - // Right before watchdog, a sendMessage succeeds at t=150_001. + // t=150_001: watchdog fires (getUpdates stale for 150s) + // But right before watchdog, a sendMessage succeeds at t=150_001 + // All subsequent Date.now calls return the same value, giving apiIdle = 0. const watchdogHarness = installPollingStallWatchdogHarness(); const log = vi.fn(); @@ -788,19 +789,20 @@ describe("TelegramPollingSession", () => { const watchdog = await watchdogHarness.waitForWatchdog(); // Simulate a sendMessage call through the middleware before watchdog fires. - // This updates unrelated API liveness, but inbound getUpdates remains stale. + // This updates lastApiActivityAt, proving the network is alive. const apiMiddleware = getApiMiddleware(); if (apiMiddleware) { const fakePrev = vi.fn(async () => ({ ok: true })); await apiMiddleware(fakePrev, "sendMessage", { chat_id: 123, text: "hello" }); } - // Now fire the watchdog — getUpdates is stale (120s) even though API was just active. + // Now fire the watchdog — getUpdates is stale (120s) but API was just active watchdog?.(); - expect(runnerStop).toHaveBeenCalledTimes(1); - expect(botStop).toHaveBeenCalledTimes(1); - expect(log).toHaveBeenCalledWith(expect.stringContaining("Polling stall detected")); + // The watchdog should NOT have triggered a restart + expect(runnerStop).not.toHaveBeenCalled(); + expect(botStop).not.toHaveBeenCalled(); + expect(log).not.toHaveBeenCalledWith(expect.stringContaining("Polling stall detected")); // Clean up: abort to end the session abort.abort(); @@ -811,7 +813,7 @@ describe("TelegramPollingSession", () => { } }); - it("triggers stall restart while a recent non-getUpdates API call is in-flight", async () => { + it("does not trigger stall restart while a recent non-getUpdates API call is in-flight", async () => { const abort = new AbortController(); const botStop = vi.fn(async () => undefined); const runnerStop = vi.fn(async () => undefined); @@ -846,12 +848,13 @@ describe("TelegramPollingSession", () => { const sendPromise = apiMiddleware(slowPrev, "sendMessage", { chat_id: 123, text: "hello" }); // Fire the watchdog while sendMessage is still in-flight. - // API liveness is still recent, but inbound getUpdates is stale. + // The in-flight call started 60s ago, so API liveness is still recent. watchdog?.(); - expect(runnerStop).toHaveBeenCalledTimes(1); - expect(botStop).toHaveBeenCalledTimes(1); - expect(log).toHaveBeenCalledWith(expect.stringContaining("Polling stall detected")); + // The watchdog should NOT have triggered a restart + expect(runnerStop).not.toHaveBeenCalled(); + expect(botStop).not.toHaveBeenCalled(); + expect(log).not.toHaveBeenCalledWith(expect.stringContaining("Polling stall detected")); // Resolve the in-flight call to clean up resolveSendMessage?.({ ok: true }); @@ -917,7 +920,7 @@ describe("TelegramPollingSession", () => { } }); - it("triggers stall restart when getUpdates is stale despite newer in-flight non-getUpdates API activity", async () => { + it("does not trigger stall restart when a newer non-getUpdates API call starts while an older one is still in-flight", async () => { const abort = new AbortController(); const botStop = vi.fn(async () => undefined); const runnerStop = vi.fn(async () => undefined); @@ -962,13 +965,13 @@ describe("TelegramPollingSession", () => { { chat_id: 123, text: "newer" }, ); - // The older send is stale and the newer send started recently. - // Watchdog liveness must still follow getUpdates, not unrelated API calls. + // The older send is stale, but the newer send started just now. + // Watchdog liveness must follow the newest active non-getUpdates call. watchdog?.(); - expect(runnerStop).toHaveBeenCalledTimes(1); - expect(botStop).toHaveBeenCalledTimes(1); - expect(log).toHaveBeenCalledWith(expect.stringContaining("Polling stall detected")); + expect(runnerStop).not.toHaveBeenCalled(); + expect(botStop).not.toHaveBeenCalled(); + expect(log).not.toHaveBeenCalledWith(expect.stringContaining("Polling stall detected")); resolveFirstSend?.({ ok: true }); resolveSecondSend?.({ ok: true }); diff --git a/package.json b/package.json index 540f06a98fa..c8ec7aac25b 100644 --- a/package.json +++ b/package.json @@ -1302,10 +1302,10 @@ "audit:seams": "node scripts/audit-seams.mjs", "build": "node scripts/build-all.mjs", "build:ci-artifacts": "node scripts/build-all.mjs ciArtifacts", - "build:docker": "node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --experimental-strip-types scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts", + "build:docker": "node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && pnpm plugins:assets:copy && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --experimental-strip-types scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts", "build:plugin-sdk:dts": "node scripts/run-tsgo.mjs -p tsconfig.plugin-sdk.dts.json --declaration true", "build:plugin-sdk:strict-smoke": "pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts", - "build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node scripts/check-plugin-sdk-exports.mjs", + "build:strict-smoke": "pnpm plugins:assets:build && node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node scripts/check-plugin-sdk-exports.mjs", "canon:check": "node scripts/canon.mjs check", "canon:check:json": "node scripts/canon.mjs check --json", "canon:enforce": "node scripts/canon.mjs enforce --json", @@ -1458,6 +1458,8 @@ "plugins:boundary-report:ci": "node --import tsx scripts/plugin-boundary-report.ts --summary --fail-on-cross-owner --fail-on-unclassified-unused-reserved --fail-on-eligible-compat", "plugins:boundary-report:json": "node --import tsx scripts/plugin-boundary-report.ts --json", "plugins:boundary-report:summary": "node --import tsx scripts/plugin-boundary-report.ts --summary", + "plugins:assets:build": "node scripts/bundled-plugin-assets.mjs --phase build", + "plugins:assets:copy": "node scripts/bundled-plugin-assets.mjs --phase copy", "plugins:inventory:check": "node scripts/generate-plugin-inventory-doc.mjs --check", "plugins:inventory:gen": "node scripts/generate-plugin-inventory-doc.mjs --write", "plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts", @@ -1737,6 +1739,7 @@ "zod": "^4.4.3" }, "devDependencies": { + "@a2ui/lit": "0.9.3", "@copilotkit/aimock": "1.17.0", "@grammyjs/types": "^3.26.0", "@lit-labs/signals": "^0.2.0", @@ -1787,7 +1790,7 @@ "fast-xml-parser": "5.7.0", "request": "npm:@cypress/request@3.0.10", "request-promise": "npm:@cypress/request-promise@5.0.0", - "basic-ftp": "5.3.1", + "basic-ftp": "6.0.1", "file-type": "22.0.1", "form-data": "2.5.4", "ip-address": "10.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e4cd7d92ce..82be38d46ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,7 +15,7 @@ overrides: fast-xml-parser: 5.7.0 request: npm:@cypress/request@3.0.10 request-promise: npm:@cypress/request-promise@5.0.0 - basic-ftp: 5.3.1 + basic-ftp: 6.0.1 file-type: 22.0.1 form-data: 2.5.4 ip-address: 10.2.0 @@ -225,6 +225,9 @@ importers: specifier: ^4.4.3 version: 4.4.3 devDependencies: + '@a2ui/lit': + specifier: 0.9.3 + version: 0.9.3(signal-polyfill@0.2.2) '@copilotkit/aimock': specifier: 1.17.0 version: 1.17.0(vitest@4.1.5) @@ -450,6 +453,16 @@ importers: specifier: workspace:* version: link:../../packages/plugin-sdk + extensions/canvas: + dependencies: + typebox: + specifier: 1.1.37 + version: 1.1.37 + devDependencies: + '@openclaw/plugin-sdk': + specifier: workspace:* + version: link:../../packages/plugin-sdk + extensions/cerebras: devDependencies: '@openclaw/plugin-sdk': @@ -1684,6 +1697,17 @@ importers: packages: + '@a2ui/lit@0.9.3': + resolution: {integrity: sha512-9Z6LAPQ0w8Se+Atul1fKo/obJPLinSsP16h86xTjr8qRBQDmK6GFnT2fIkUJVALzVoADObidfIdmtbgosFDenA==} + peerDependencies: + '@a2ui/markdown-it': ^0.0.3 + peerDependenciesMeta: + '@a2ui/markdown-it': + optional: true + + '@a2ui/web_core@0.9.2': + resolution: {integrity: sha512-EOfhLOF7tnpPmNq4y116k3gxWdrXQW8h3dhKF0pC++21zLZnCSLSHl6zgQFG+kPeVAZb64t+sQiRXlnyS8+RBg==} + '@agentclientprotocol/claude-agent-acp@0.32.0': resolution: {integrity: sha512-3WIaD1bTmIciqHdeU97oeNajOG9H+ctloXnQ+R/T563C2CM8u1K7QsNqqgqR2F+Cn8NVBkXdHRvAMtUHglLzAw==} hasBin: true @@ -2823,6 +2847,9 @@ packages: resolution: {integrity: sha512-3NZJjeFm2BikwVRgA8osIVbgKhuL0CzphQOdrB8okXIC40qMRE4RRfHFN3G8/qTb/34RtB95mD4J/KW5MD+b8g==} engines: {node: '>=20'} + '@lit-labs/signals@0.1.3': + resolution: {integrity: sha512-P0yWgH5blwVyEwBg+WFspLzeu1i0ypJP1QB0l1Omr9qZLIPsUu0p4Fy2jshOg7oQyha5n163K3GJGeUhQQ682Q==} + '@lit-labs/signals@0.2.0': resolution: {integrity: sha512-68plyIbciumbwKaiilhLNyhz4Vg6/+nJwDufG2xxWA9r/fUw58jxLHCAlKs+q1CE5Lmh3cZ3ShyYKnOCebEpVA==} @@ -3707,6 +3734,9 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@preact/signals-core@1.14.1': + resolution: {integrity: sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng==} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -4809,8 +4839,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - basic-ftp@5.3.1: - resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==} + basic-ftp@6.0.1: + resolution: {integrity: sha512-3ilxa3n4276wGQp/ImRAuz4ALdsj/2Wd3FqoZBZlajDYnByCZ0JMb4+26Rde0wGXIbM0G2HWSfr/Fi8b21KX8g==} engines: {node: '>=10.0.0'} bidi-js@1.0.3: @@ -5085,6 +5115,9 @@ packages: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -7879,6 +7912,24 @@ packages: snapshots: + '@a2ui/lit@0.9.3(signal-polyfill@0.2.2)': + dependencies: + '@a2ui/web_core': 0.9.2 + '@lit-labs/signals': 0.1.3 + '@lit/context': 1.1.6 + lit: 3.3.2 + signal-utils: 0.21.1(signal-polyfill@0.2.2) + zod: 3.25.76 + transitivePeerDependencies: + - signal-polyfill + + '@a2ui/web_core@0.9.2': + dependencies: + '@preact/signals-core': 1.14.1 + date-fns: 4.1.0 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + '@agentclientprotocol/claude-agent-acp@0.32.0(patch_hash=1fe782f9679d7a725cbe59e51d61419fbb25d4c463d186c43c95644770cb2b98)': dependencies: '@agentclientprotocol/sdk': 0.21.0(zod@4.4.3) @@ -9570,6 +9621,11 @@ snapshots: dependencies: '@types/node': 24.12.2 + '@lit-labs/signals@0.1.3': + dependencies: + lit: 3.3.2 + signal-polyfill: 0.2.2 + '@lit-labs/signals@0.2.0': dependencies: lit: 3.3.2 @@ -10403,6 +10459,8 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@preact/signals-core@1.14.1': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -11649,7 +11707,7 @@ snapshots: base64-js@1.5.1: {} - basic-ftp@5.3.1: {} + basic-ftp@6.0.1: {} bidi-js@1.0.3: dependencies: @@ -11918,6 +11976,8 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + date-fns@4.1.0: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -12450,7 +12510,7 @@ snapshots: get-uri@6.0.5: dependencies: - basic-ftp: 5.3.1 + basic-ftp: 6.0.1 data-uri-to-buffer: 6.0.2 debug: 4.4.3 transitivePeerDependencies: @@ -12458,7 +12518,7 @@ snapshots: get-uri@8.0.0: dependencies: - basic-ftp: 5.3.1 + basic-ftp: 6.0.1 data-uri-to-buffer: 8.0.0 debug: 4.4.3 transitivePeerDependencies: @@ -15206,6 +15266,10 @@ snapshots: - bufferutil - utf-8-validate + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-to-json-schema@3.25.2(zod@4.4.3): dependencies: zod: 4.4.3 diff --git a/scripts/build-all.mjs b/scripts/build-all.mjs index 07e9dfc5105..377e3684853 100644 --- a/scripts/build-all.mjs +++ b/scripts/build-all.mjs @@ -11,7 +11,7 @@ const nodeBin = process.execPath; const WINDOWS_BUILD_MAX_OLD_SPACE_MB = 4096; const BUILD_CACHE_VERSION = 2; export const BUILD_ALL_STEPS = [ - { label: "canvas:a2ui:bundle", kind: "pnpm", pnpmArgs: ["canvas:a2ui:bundle"] }, + { label: "plugins:assets:build", kind: "pnpm", pnpmArgs: ["plugins:assets:build"] }, { label: "tsdown", kind: "node", args: ["scripts/tsdown-build.mjs"] }, { label: "check-cli-bootstrap-imports", @@ -53,13 +53,9 @@ export const BUILD_ALL_STEPS = [ args: ["scripts/check-plugin-sdk-exports.mjs"], }, { - label: "canvas-a2ui-copy", - kind: "node", - args: ["--import", "tsx", "scripts/canvas-a2ui-copy.ts"], - cache: { - inputs: ["scripts/canvas-a2ui-copy.ts", "src/canvas-host/a2ui"], - outputs: ["dist/canvas-host/a2ui/index.html", "dist/canvas-host/a2ui/a2ui.bundle.js"], - }, + label: "plugins:assets:copy", + kind: "pnpm", + pnpmArgs: ["plugins:assets:copy"], }, { label: "copy-hook-metadata", @@ -99,7 +95,7 @@ export const BUILD_ALL_STEPS = [ export const BUILD_ALL_PROFILES = { full: BUILD_ALL_STEPS.map((step) => step.label), ciArtifacts: [ - "canvas:a2ui:bundle", + "plugins:assets:build", "tsdown", "check-cli-bootstrap-imports", "runtime-postbuild", @@ -108,7 +104,7 @@ export const BUILD_ALL_PROFILES = { "build:plugin-sdk:dts", "write-plugin-sdk-entry-dts", "check-plugin-sdk-exports", - "canvas-a2ui-copy", + "plugins:assets:copy", "copy-hook-metadata", "copy-export-html-templates", "write-build-info", diff --git a/scripts/bundle-a2ui.mjs b/scripts/bundle-a2ui.mjs index 212e5a27aa7..ae2ea6439ed 100644 --- a/scripts/bundle-a2ui.mjs +++ b/scripts/bundle-a2ui.mjs @@ -1,223 +1,8 @@ #!/usr/bin/env node -import { spawnSync } from "node:child_process"; -import { createHash } from "node:crypto"; -import fs from "node:fs/promises"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { resolvePnpmRunner } from "./pnpm-runner.mjs"; - -const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const hashFile = path.join(rootDir, "src", "canvas-host", "a2ui", ".bundle.hash"); -const outputFile = path.join(rootDir, "src", "canvas-host", "a2ui", "a2ui.bundle.js"); -const a2uiRendererDir = path.join(rootDir, "vendor", "a2ui", "renderers", "lit"); -const a2uiAppDir = path.join(rootDir, "apps", "shared", "OpenClawKit", "Tools", "CanvasA2UI"); -const uiPackageFile = path.join(rootDir, "ui", "package.json"); -const repoInputPaths = [uiPackageFile, a2uiRendererDir, a2uiAppDir]; -const ignoredBundleHashInputPrefixes = ["vendor/a2ui/renderers/lit/dist"]; -const relativeRepoInputPaths = repoInputPaths.map((inputPath) => - normalizePath(path.relative(rootDir, inputPath)), -); - -function fail(message) { - console.error(message); - console.error("A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle"); - console.error("If this persists, verify pnpm deps and try again."); - process.exit(1); -} - -async function pathExists(targetPath) { - try { - await fs.stat(targetPath); - return true; - } catch { - return false; - } -} - -function normalizePath(filePath) { - return filePath.split(path.sep).join("/"); -} - -export function isBundleHashInputPath(filePath, repoRoot = rootDir) { - const relativePath = normalizePath(path.relative(repoRoot, filePath)); - return !ignoredBundleHashInputPrefixes.some( - (ignoredPath) => relativePath === ignoredPath || relativePath.startsWith(`${ignoredPath}/`), - ); -} - -export function getLocalRolldownCliCandidates(repoRoot = rootDir) { - return [ - path.join(repoRoot, "node_modules", "rolldown", "bin", "cli.mjs"), - path.join(repoRoot, "node_modules", ".pnpm", "node_modules", "rolldown", "bin", "cli.mjs"), - path.join( - repoRoot, - "node_modules", - ".pnpm", - "rolldown@1.0.0-rc.12", - "node_modules", - "rolldown", - "bin", - "cli.mjs", - ), - ]; -} - -export function getBundleHashRepoInputPaths(repoRoot = rootDir) { - return [ - path.join(repoRoot, "ui", "package.json"), - path.join(repoRoot, "vendor", "a2ui", "renderers", "lit"), - path.join(repoRoot, "apps", "shared", "OpenClawKit", "Tools", "CanvasA2UI"), - ]; -} - -export function getBundleHashInputPaths(repoRoot = rootDir) { - return getBundleHashRepoInputPaths(repoRoot); -} - -export function compareNormalizedPaths(left, right) { - const normalizedLeft = normalizePath(left); - const normalizedRight = normalizePath(right); - if (normalizedLeft < normalizedRight) { - return -1; - } - if (normalizedLeft > normalizedRight) { - return 1; - } - return 0; -} - -async function walkFiles(entryPath, files) { - if (!isBundleHashInputPath(entryPath)) { - return; - } - const stat = await fs.stat(entryPath); - if (!stat.isDirectory()) { - files.push(entryPath); - return; - } - const entries = await fs.readdir(entryPath); - for (const entry of entries) { - await walkFiles(path.join(entryPath, entry), files); - } -} - -function listTrackedInputFiles() { - const result = spawnSync("git", ["ls-files", "--", ...relativeRepoInputPaths], { - cwd: rootDir, - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - }); - if (result.status !== 0) { - return null; - } - const trackedFiles = result.stdout - .split("\n") - .filter(Boolean) - .map((filePath) => path.join(rootDir, filePath)) - .filter((filePath) => isBundleHashInputPath(filePath)); - return trackedFiles; -} - -async function computeHash() { - let files = listTrackedInputFiles(); - if (!files) { - files = []; - for (const inputPath of getBundleHashRepoInputPaths(rootDir)) { - await walkFiles(inputPath, files); - } - } - files = [...new Set(files)].toSorted(compareNormalizedPaths); - - const hash = createHash("sha256"); - for (const filePath of files) { - hash.update(normalizePath(path.relative(rootDir, filePath))); - hash.update("\0"); - hash.update(await fs.readFile(filePath)); - hash.update("\0"); - } - return hash.digest("hex"); -} - -function runStep(command, args, options = {}) { - const result = spawnSync(command, args, { - cwd: rootDir, - stdio: "inherit", - env: process.env, - ...options, - }); - if (result.status !== 0) { - process.exit(result.status ?? 1); - } -} - -function runPnpm(pnpmArgs) { - const runner = resolvePnpmRunner({ - pnpmArgs, - nodeExecPath: process.execPath, - npmExecPath: process.env.npm_execpath, - comSpec: process.env.ComSpec, - platform: process.platform, - }); - runStep(runner.command, runner.args, { - shell: runner.shell, - windowsVerbatimArguments: runner.windowsVerbatimArguments, - }); -} - -async function main() { - const hasRendererDir = await pathExists(a2uiRendererDir); - const hasAppDir = await pathExists(a2uiAppDir); - const hasOutputFile = await pathExists(outputFile); - if (!hasRendererDir || !hasAppDir) { - if (hasOutputFile) { - console.log("A2UI sources missing; keeping prebuilt bundle."); - return; - } - if (process.env.OPENCLAW_SPARSE_PROFILE || process.env.OPENCLAW_A2UI_SKIP_MISSING === "1") { - console.error( - "A2UI sources missing; skipping bundle because OPENCLAW_A2UI_SKIP_MISSING=1 or OPENCLAW_SPARSE_PROFILE is set.", - ); - return; - } - fail(`A2UI sources missing and no prebuilt bundle found at: ${outputFile}`); - } - - const currentHash = await computeHash(); - if (await pathExists(hashFile)) { - const previousHash = (await fs.readFile(hashFile, "utf8")).trim(); - if (previousHash === currentHash && hasOutputFile) { - console.log("A2UI bundle up to date; skipping."); - return; - } - } - - runPnpm(["-s", "exec", "tsgo", "-p", path.join(a2uiRendererDir, "tsconfig.json")]); - - const localRolldownCliCandidates = getLocalRolldownCliCandidates(rootDir); - const localRolldownCli = ( - await Promise.all( - localRolldownCliCandidates.map(async (candidate) => - (await pathExists(candidate)) ? candidate : null, - ), - ) - ).find(Boolean); - - if (localRolldownCli) { - runStep(process.execPath, [ - localRolldownCli, - "-c", - path.join(a2uiAppDir, "rolldown.config.mjs"), - ]); - } else { - runPnpm(["-s", "exec", "rolldown", "-c", path.join(a2uiAppDir, "rolldown.config.mjs")]); - } - - await fs.writeFile(hashFile, `${currentHash}\n`, "utf8"); -} +import { pathToFileURL } from "node:url"; +import { runBundledPluginAssetHooks } from "./bundled-plugin-assets.mjs"; if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - await main().catch((error) => { - fail(error instanceof Error ? error.message : String(error)); - }); + await runBundledPluginAssetHooks({ phase: "build", plugins: ["canvas"] }); } diff --git a/scripts/bundled-plugin-assets.mjs b/scripts/bundled-plugin-assets.mjs new file mode 100644 index 00000000000..4261a5eac94 --- /dev/null +++ b/scripts/bundled-plugin-assets.mjs @@ -0,0 +1,177 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const VALID_PHASES = new Set(["build", "copy"]); + +async function readJsonFile(filePath) { + return JSON.parse(await fs.readFile(filePath, "utf8")); +} + +async function pathExists(filePath) { + try { + await fs.stat(filePath); + return true; + } catch { + return false; + } +} + +function packagePluginAliases(packageName) { + if (typeof packageName !== "string") { + return []; + } + const aliases = [packageName]; + const unscopedName = packageName.split("/").at(-1); + if (unscopedName) { + aliases.push(unscopedName); + if (unscopedName.endsWith("-plugin")) { + aliases.push(unscopedName.slice(0, -"-plugin".length)); + } + } + return aliases; +} + +async function resolvePluginAliases(pluginDir, packageJson) { + const aliases = new Set([path.basename(pluginDir), ...packagePluginAliases(packageJson.name)]); + const manifestPath = path.join(pluginDir, "openclaw.plugin.json"); + if (await pathExists(manifestPath)) { + const manifest = await readJsonFile(manifestPath); + if (typeof manifest.id === "string" && manifest.id) { + aliases.add(manifest.id); + } + } + return aliases; +} + +function resolveAssetCommand(packageJson, phase) { + const assetScripts = packageJson.openclaw?.assetScripts; + if (!assetScripts || typeof assetScripts !== "object") { + return null; + } + const command = assetScripts[phase]; + return typeof command === "string" && command.trim() ? command.trim() : null; +} + +export async function readBundledPluginAssetHooks(options = {}) { + const repoRoot = options.rootDir ?? rootDir; + const phase = options.phase; + if (!VALID_PHASES.has(phase)) { + throw new Error(`Unsupported bundled plugin asset phase: ${String(phase)}`); + } + + const pluginFilters = new Set((options.plugins ?? []).filter(Boolean)); + const extensionsDir = path.join(repoRoot, "extensions"); + let entries; + try { + entries = await fs.readdir(extensionsDir, { withFileTypes: true }); + } catch { + return []; + } + + const hooks = []; + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + const pluginDir = path.join(extensionsDir, entry.name); + const packagePath = path.join(pluginDir, "package.json"); + if (!(await pathExists(packagePath))) { + continue; + } + + const packageJson = await readJsonFile(packagePath); + const aliases = await resolvePluginAliases(pluginDir, packageJson); + if (pluginFilters.size > 0 && ![...pluginFilters].some((plugin) => aliases.has(plugin))) { + continue; + } + + const command = resolveAssetCommand(packageJson, phase); + if (!command) { + continue; + } + + hooks.push({ + aliases: [...aliases].toSorted(), + command, + packageName: packageJson.name, + phase, + pluginDir, + pluginId: aliases.has(entry.name) ? entry.name : [...aliases][0], + }); + } + + return hooks.toSorted((left, right) => left.pluginDir.localeCompare(right.pluginDir)); +} + +export async function runBundledPluginAssetHooks(options = {}) { + const phase = options.phase; + const hooks = await readBundledPluginAssetHooks(options); + if (hooks.length === 0) { + const scope = options.plugins?.length ? ` for ${options.plugins.join(", ")}` : ""; + console.log(`No bundled plugin asset ${phase} hooks${scope}; skipping.`); + return; + } + + for (const hook of hooks) { + console.log(`[${hook.pluginId}] ${phase}: ${hook.command}`); + const result = spawnSync(hook.command, { + cwd: hook.pluginDir, + env: process.env, + shell: true, + stdio: "inherit", + }); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } + } +} + +export function parseBundledPluginAssetArgs(argv) { + const args = [...argv]; + const plugins = []; + let phase = null; + + while (args.length > 0) { + const arg = args.shift(); + if (arg === "--phase") { + phase = args.shift() ?? null; + continue; + } + if (arg?.startsWith("--phase=")) { + phase = arg.slice("--phase=".length); + continue; + } + if (arg === "--plugin") { + const plugin = args.shift(); + if (plugin) { + plugins.push(plugin); + } + continue; + } + if (arg?.startsWith("--plugin=")) { + plugins.push(arg.slice("--plugin=".length)); + continue; + } + throw new Error(`Unknown bundled plugin asset argument: ${String(arg)}`); + } + + if (!VALID_PHASES.has(phase)) { + throw new Error(`Expected --phase ${[...VALID_PHASES].join("|")}`); + } + + return { phase, plugins }; +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { + try { + await runBundledPluginAssetHooks(parseBundledPluginAssetArgs(process.argv.slice(2))); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} diff --git a/scripts/check-codex-app-server-protocol.ts b/scripts/check-codex-app-server-protocol.ts index 1fb6741b1f5..2f5e75e0e6b 100644 --- a/scripts/check-codex-app-server-protocol.ts +++ b/scripts/check-codex-app-server-protocol.ts @@ -2,7 +2,6 @@ import fs from "node:fs/promises"; import path from "node:path"; import { generateExperimentalCodexAppServerProtocolSource, - normalizeGeneratedTypeScript, selectedCodexAppServerJsonSchemas, } from "./lib/codex-app-server-protocol-source.js"; @@ -76,7 +75,7 @@ const failures: string[] = []; const source = await generateExperimentalCodexAppServerProtocolSource(); try { - await compareGeneratedProtocolMirror(source.typescriptRoot, source.jsonRoot); + await compareGeneratedProtocolMirror(source.jsonRoot); for (const check of checks) { const filePath = path.join(source.typescriptRoot, check.file); @@ -112,35 +111,7 @@ console.log( `Codex app-server generated protocol matches OpenClaw bridge assumptions: ${source.codexRepo}`, ); -async function compareGeneratedProtocolMirror( - sourceTsRoot: string, - sourceJsonRoot: string, -): Promise { - const targetTsRoot = path.join(generatedRoot, "typescript"); - const sourceFiles = await listFiles(sourceTsRoot, ".ts"); - const targetFiles = await listFiles(targetTsRoot, ".ts"); - const sourceSet = new Set(sourceFiles); - const targetSet = new Set(targetFiles); - - for (const file of sourceFiles) { - if (!targetSet.has(file)) { - failures.push(`protocol-generated/typescript/${file}: missing local mirror`); - continue; - } - const source = normalizeGeneratedTypeScript( - await fs.readFile(path.join(sourceTsRoot, file), "utf8"), - ); - const target = await fs.readFile(path.join(targetTsRoot, file), "utf8"); - if (source !== target) { - failures.push(`protocol-generated/typescript/${file}: differs from normalized source schema`); - } - } - for (const file of targetFiles) { - if (!sourceSet.has(file)) { - failures.push(`protocol-generated/typescript/${file}: no longer present in source schema`); - } - } - +async function compareGeneratedProtocolMirror(sourceJsonRoot: string): Promise { for (const schema of selectedCodexAppServerJsonSchemas) { const sourcePath = path.join(sourceJsonRoot, schema); const targetPath = path.join(generatedRoot, "json", schema); @@ -169,20 +140,3 @@ async function compareGeneratedProtocolMirror( function normalizeJsonSchema(source: string): string { return JSON.stringify(JSON.parse(source)); } - -async function listFiles(root: string, suffix: string): Promise { - const files: string[] = []; - async function visit(dir: string): Promise { - const entries = await fs.readdir(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - await visit(fullPath); - } else if (entry.isFile() && entry.name.endsWith(suffix)) { - files.push(path.relative(root, fullPath)); - } - } - } - await visit(root); - return files.toSorted(); -} diff --git a/scripts/ci-changed-scope.mjs b/scripts/ci-changed-scope.mjs index d2327cdcfa6..35bd17a9bcf 100644 --- a/scripts/ci-changed-scope.mjs +++ b/scripts/ci-changed-scope.mjs @@ -28,8 +28,7 @@ const EMPTY_SCOPE = { const DOCS_PATH_RE = /^(docs\/|.*\.mdx?$)/; const SKILLS_PYTHON_SCOPE_RE = /^(skills\/|skills\/pyproject\.toml$)/; const INSTALL_SMOKE_WORKFLOW_SCOPE_RE = /^\.github\/workflows\/install-smoke\.yml$/; -const MACOS_PROTOCOL_GEN_RE = - /^(apps\/macos\/Sources\/OpenClawProtocol\/|apps\/shared\/OpenClawKit\/Sources\/OpenClawProtocol\/)/; +const NATIVE_PROTOCOL_GEN_RE = /^apps\/shared\/OpenClawKit\/Sources\/OpenClawProtocol\//; const MACOS_NATIVE_RE = /^(apps\/macos\/|apps\/macos-mlx-tts\/|apps\/ios\/|apps\/shared\/|apps\/swabble\/|Swabble\/)/; const ANDROID_NATIVE_RE = /^(apps\/android\/|apps\/shared\/)/; @@ -57,8 +56,6 @@ const NODE_FAST_CI_ROUTING_SCOPE_RE = const NODE_FAST_SCOPE_RE = new RegExp( `${NODE_FAST_PLUGIN_CONTRACT_SCOPE_RE.source}|${NODE_FAST_CI_ROUTING_SCOPE_RE.source}`, ); -const PROMPT_SNAPSHOT_SCOPE_RE = - /^(test\/helpers\/agents\/happy-path-prompt-snapshots\.ts$|test\/fixtures\/agents\/prompt-snapshots\/|test\/scripts\/prompt-snapshots\.test\.ts$|scripts\/generate-prompt-snapshots\.ts$|scripts\/sync-codex-model-prompt-fixture\.ts$|extensions\/codex\/(?:test-api\.ts$|src\/app-server\/)|src\/auto-reply\/|src\/plugin-sdk\/agent-harness(?:-runtime)?\.ts$|src\/agents\/(?:apply-patch|bash-tools|channel-tools|codex-native-web-search|openclaw-plugin-tools|openclaw-tools|pi-tools|session-tools|subagent|tool-|workspace-dir)\b|src\/utils\/message-channel\.ts$|src\/config\/types\.(?:models|openclaw)\.ts$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$)/; /** * @param {string[]} changedPaths @@ -107,11 +104,11 @@ export function detectChangedScope(changedPaths) { runChangedSmoke = true; } - if (!MACOS_PROTOCOL_GEN_RE.test(path) && MACOS_NATIVE_RE.test(path)) { + if (!NATIVE_PROTOCOL_GEN_RE.test(path) && MACOS_NATIVE_RE.test(path)) { runMacos = true; } - if (ANDROID_NATIVE_RE.test(path)) { + if (!NATIVE_PROTOCOL_GEN_RE.test(path) && ANDROID_NATIVE_RE.test(path)) { runAndroid = true; } @@ -154,17 +151,6 @@ export function detectChangedScope(changedPaths) { }; } -/** - * @param {string[]} changedPaths - * @returns {boolean} - */ -export function detectPromptSnapshotScope(changedPaths) { - if (!Array.isArray(changedPaths) || changedPaths.length === 0) { - return true; - } - return changedPaths.some((rawPath) => PROMPT_SNAPSHOT_SCOPE_RE.test(rawPath.trim())); -} - /** * @param {string[]} changedPaths * @returns {NodeFastScope} @@ -269,7 +255,6 @@ export function writeGitHubOutput( runFullInstallSmoke: scope.runChangedSmoke, }, nodeFastScope = { runFastOnly: false, runPluginContracts: false, runCiRouting: false }, - runPromptSnapshots = scope.runNode, ) { if (!outputPath) { throw new Error("GITHUB_OUTPUT is required"); @@ -298,7 +283,6 @@ export function writeGitHubOutput( "utf8", ); appendFileSync(outputPath, `run_control_ui_i18n=${scope.runControlUiI18n}\n`, "utf8"); - appendFileSync(outputPath, `run_prompt_snapshots=${runPromptSnapshots}\n`, "utf8"); } function isDirectRun() { @@ -336,7 +320,6 @@ if (isDirectRun()) { process.env.GITHUB_OUTPUT, detectInstallSmokeScope(changedPaths), detectNodeFastScope(changedPaths), - detectPromptSnapshotScope(changedPaths), ); } catch { writeGitHubOutput(FULL_SCOPE); diff --git a/scripts/ci-run-timings.mjs b/scripts/ci-run-timings.mjs index 3388fe86b13..74e9adbc34b 100644 --- a/scripts/ci-run-timings.mjs +++ b/scripts/ci-run-timings.mjs @@ -2,10 +2,6 @@ import { execFileSync } from "node:child_process"; -const DEFAULT_REPOSITORY = "openclaw/openclaw"; -const CI_WORKFLOW_ID = "ci.yml"; -const GH_MAX_BUFFER = 32 * 1024 * 1024; - function parseTime(value) { if (!value || value === "0001-01-01T00:00:00Z") { return null; @@ -22,36 +18,15 @@ function formatSeconds(value) { return value === null ? "" : `${value}s`; } -function normalizeRun(run) { - return { - ...run, - createdAt: run.createdAt ?? run.created_at, - databaseId: run.databaseId ?? run.id, - displayTitle: run.displayTitle ?? run.display_title, - event: run.event, - headSha: run.headSha ?? run.head_sha, - runStartedAt: run.runStartedAt ?? run.run_started_at, - status: run.status, - conclusion: run.conclusion, - updatedAt: run.updatedAt ?? run.updated_at, - }; -} - -function normalizeJob(job) { - return { - ...job, - completedAt: job.completedAt ?? job.completed_at, - runnerName: job.runnerName ?? job.runner_name, - startedAt: job.startedAt ?? job.started_at, - }; +function parseRunList(raw) { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; } function collectRunTimingContext(run) { - const normalizedRun = normalizeRun(run); - const created = parseTime(normalizedRun.createdAt); - const runUpdated = parseTime(normalizedRun.updatedAt); - const jobs = (normalizedRun.jobs ?? []) - .map(normalizeJob) + const created = parseTime(run.createdAt); + const updated = parseTime(run.updatedAt); + const jobs = (run.jobs ?? []) .filter((job) => !job.name?.startsWith("matrix.")) .map((job) => { const started = parseTime(job.startedAt); @@ -67,18 +42,11 @@ function collectRunTimingContext(run) { }; }); - const completedTimes = jobs.map((job) => job.completed).filter((completed) => completed !== null); - const lastCompleted = completedTimes.length === 0 ? null : Math.max(...completedTimes); - const updated = - runUpdated !== null && lastCompleted !== null - ? Math.max(runUpdated, lastCompleted) - : (runUpdated ?? lastCompleted); - - return { created, jobs, run: normalizedRun, updated }; + return { created, jobs, updated }; } export function summarizeRunTimings(run, limit = 15) { - const { created, jobs, run: normalizedRun, updated } = collectRunTimingContext(run); + const { created, jobs, updated } = collectRunTimingContext(run); const byDuration = [...jobs] .filter((job) => job.durationSeconds !== null) .toSorted((left, right) => right.durationSeconds - left.durationSeconds) @@ -94,15 +62,15 @@ export function summarizeRunTimings(run, limit = 15) { return { byDuration, byQueue, - conclusion: normalizedRun.conclusion ?? "", - status: normalizedRun.status ?? "", + conclusion: run.conclusion ?? "", + status: run.status ?? "", wallSeconds: secondsBetween(created, updated), badJobs, }; } export function selectLatestMainPushCiRun(runs, headSha = null) { - const pushRuns = runs.map(normalizeRun).filter((run) => run.event === "push"); + const pushRuns = runs.filter((run) => run.event === "push"); if (headSha) { const matchingRun = pushRuns.find((run) => run.headSha === headSha); if (matchingRun) { @@ -112,37 +80,13 @@ export function selectLatestMainPushCiRun(runs, headSha = null) { return pushRuns[0] ?? null; } -function repositorySlug() { - return process.env.GITHUB_REPOSITORY || DEFAULT_REPOSITORY; -} - -function ghApiJson(path) { - return JSON.parse( - execFileSync("gh", ["api", path], { - encoding: "utf8", - maxBuffer: GH_MAX_BUFFER, - }), - ); -} - -function listMainCiRuns(limit) { - const runs = []; - const perPage = Math.max(1, Math.min(100, limit)); - for (let page = 1; runs.length < limit && page <= 10; page += 1) { - const data = ghApiJson( - `repos/${repositorySlug()}/actions/workflows/${CI_WORKFLOW_ID}/runs?branch=main&per_page=${perPage}&page=${page}&exclude_pull_requests=true`, - ); - const pageRuns = (data.workflow_runs ?? []).map(normalizeRun); - runs.push(...pageRuns); - if (pageRuns.length < perPage) { - break; - } - } - return runs.slice(0, limit); -} - function getLatestCiRunId() { - const runs = listMainCiRuns(1); + const raw = execFileSync( + "gh", + ["run", "list", "--branch", "main", "--workflow", "CI", "--limit", "1", "--json", "databaseId"], + { encoding: "utf8" }, + ); + const runs = JSON.parse(raw); const runId = runs[0]?.databaseId; if (!runId) { throw new Error("No CI runs found on main"); @@ -161,7 +105,23 @@ function getRemoteMainSha() { function getLatestMainPushCiRunId() { const headSha = getRemoteMainSha(); - const run = selectLatestMainPushCiRun(listMainCiRuns(40), headSha); + const raw = execFileSync( + "gh", + [ + "run", + "list", + "--branch", + "main", + "--workflow", + "CI", + "--limit", + "20", + "--json", + "databaseId,headSha,event,status,conclusion", + ], + { encoding: "utf8" }, + ); + const run = selectLatestMainPushCiRun(parseRunList(raw), headSha); if (!run?.databaseId) { throw new Error(`No push CI run found for origin/main ${headSha.slice(0, 10)}`); } @@ -169,30 +129,37 @@ function getLatestMainPushCiRunId() { } function listRecentSuccessfulCiRuns(limit) { - return listMainCiRuns(Math.max(limit * 12, 100)) + const raw = execFileSync( + "gh", + [ + "run", + "list", + "--branch", + "main", + "--workflow", + "CI", + "--limit", + String(Math.max(limit * 4, limit)), + "--json", + "databaseId,headSha,status,conclusion", + ], + { encoding: "utf8" }, + ); + return JSON.parse(raw) .filter((run) => run.status === "completed" && run.conclusion === "success") .slice(0, limit); } function loadRun(runId) { - const repository = repositorySlug(); - const run = normalizeRun(ghApiJson(`repos/${repository}/actions/runs/${runId}`)); - const jobs = []; - for (let page = 1; page <= 10; page += 1) { - const data = ghApiJson( - `repos/${repository}/actions/runs/${runId}/jobs?per_page=100&page=${page}`, - ); - const pageJobs = data.jobs ?? []; - jobs.push(...pageJobs.map(normalizeJob)); - if (pageJobs.length < 100) { - break; - } - } - return { - ...run, - createdAt: run.createdAt ?? run.runStartedAt, - jobs, - }; + return JSON.parse( + execFileSync( + "gh", + ["run", "view", runId, "--json", "status,conclusion,createdAt,updatedAt,jobs"], + { + encoding: "utf8", + }, + ), + ); } function summarizeJobs(run) { @@ -278,12 +245,7 @@ async function main() { process.argv.slice(2), ); if (recentLimit !== null) { - const runs = listRecentSuccessfulCiRuns(recentLimit); - if (runs.length === 0) { - console.log("No recent successful main CI runs found in the latest 100 runs."); - return; - } - for (const run of runs) { + for (const run of listRecentSuccessfulCiRuns(recentLimit)) { const summary = summarizeJobs(loadRun(run.databaseId)); console.log( [ diff --git a/scripts/ci-runner-labels.mjs b/scripts/ci-runner-labels.mjs deleted file mode 100644 index 1e242b68c2d..00000000000 --- a/scripts/ci-runner-labels.mjs +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env node - -import { appendFileSync } from "node:fs"; - -export const RUNNER_LABELS = { - runner_4vcpu_ubuntu: { - fallback: "ubuntu-24.04", - family: "ubuntu-2404", - primary: "blacksmith-4vcpu-ubuntu-2404", - }, - runner_8vcpu_ubuntu: { - fallback: "ubuntu-24.04", - family: "ubuntu-2404", - primary: "blacksmith-8vcpu-ubuntu-2404", - }, - runner_16vcpu_ubuntu: { - fallback: "ubuntu-24.04", - family: "ubuntu-2404", - primary: "blacksmith-16vcpu-ubuntu-2404", - }, - runner_16vcpu_windows: { - fallback: "windows-2025", - family: "windows-2025", - primary: "blacksmith-16vcpu-windows-2025", - }, - runner_6vcpu_macos: { - fallback: "macos-latest", - family: "macos-latest", - primary: "blacksmith-6vcpu-macos-latest", - }, - runner_12vcpu_macos: { - fallback: "macos-latest", - family: "macos-latest", - primary: "blacksmith-12vcpu-macos-latest", - }, -}; - -const DEFAULT_REPOSITORY = "openclaw/openclaw"; -const DEFAULT_QUEUE_THRESHOLD = 1; -const MAX_RUNS_TO_SCAN = 8; -const MAX_JOB_PAGES_PER_RUN = 2; - -function parseBoolean(value, fallback = false) { - if (value === undefined) { - return fallback; - } - const normalized = value.trim().toLowerCase(); - if (normalized === "1" || normalized === "true" || normalized === "yes") { - return true; - } - if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "") { - return false; - } - return fallback; -} - -function parsePositiveInteger(value, fallback) { - const parsed = Number.parseInt(value ?? "", 10); - return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; -} - -export function selectRunnerLabels({ - canonicalRepository = true, - fallbackEnabled = true, - queuedCountsByLabel = {}, - queueThreshold = DEFAULT_QUEUE_THRESHOLD, -} = {}) { - const selected = {}; - for (const [outputName, label] of Object.entries(RUNNER_LABELS)) { - const queuedCount = queuedCountsByLabel[label.primary] ?? 0; - selected[outputName] = - !canonicalRepository || (fallbackEnabled && queuedCount >= queueThreshold) - ? label.fallback - : label.primary; - } - return selected; -} - -async function githubApi(path, token) { - const response = await fetch(`https://api.github.com/${path}`, { - headers: { - accept: "application/vnd.github+json", - authorization: `Bearer ${token}`, - "x-github-api-version": "2022-11-28", - }, - }); - if (!response.ok) { - throw new Error(`GitHub API ${path} failed: ${response.status} ${response.statusText}`); - } - return response.json(); -} - -async function collectQueuedBlacksmithJobs({ repository, token }) { - const [queuedRuns, inProgressRuns] = await Promise.all([ - githubApi( - `repos/${repository}/actions/runs?status=queued&per_page=${MAX_RUNS_TO_SCAN}&exclude_pull_requests=true`, - token, - ), - githubApi( - `repos/${repository}/actions/runs?status=in_progress&per_page=${MAX_RUNS_TO_SCAN}&exclude_pull_requests=true`, - token, - ), - ]); - const runsById = new Map(); - for (const run of [ - ...(queuedRuns.workflow_runs ?? []), - ...(inProgressRuns.workflow_runs ?? []), - ]) { - runsById.set(run.id, run); - } - - const counts = {}; - await Promise.all( - [...runsById.values()].map(async (run) => { - const runCounts = {}; - for (let page = 1; page <= MAX_JOB_PAGES_PER_RUN; page += 1) { - const jobs = await githubApi( - `repos/${repository}/actions/runs/${run.id}/jobs?per_page=100&page=${page}`, - token, - ); - for (const job of jobs.jobs ?? []) { - if (job.status !== "queued") { - continue; - } - for (const label of job.labels ?? []) { - if (typeof label === "string" && label.startsWith("blacksmith-")) { - runCounts[label] = (runCounts[label] ?? 0) + 1; - } - } - } - if ((jobs.jobs ?? []).length < 100) { - break; - } - } - for (const [label, count] of Object.entries(runCounts)) { - counts[label] = (counts[label] ?? 0) + count; - } - }), - ); - return counts; -} - -function writeOutputs(outputs) { - const outputPath = process.env.GITHUB_OUTPUT; - if (!outputPath) { - console.log(JSON.stringify(outputs, null, 2)); - return; - } - for (const [key, value] of Object.entries(outputs)) { - appendFileSync(outputPath, `${key}=${String(value)}\n`, "utf8"); - } -} - -async function main() { - const repository = process.env.GITHUB_REPOSITORY || DEFAULT_REPOSITORY; - const canonicalRepository = repository === DEFAULT_REPOSITORY; - const fallbackEnabled = parseBoolean(process.env.OPENCLAW_CI_BLACKSMITH_FALLBACK, true); - const queueThreshold = parsePositiveInteger( - process.env.OPENCLAW_CI_BLACKSMITH_QUEUE_FALLBACK_THRESHOLD, - DEFAULT_QUEUE_THRESHOLD, - ); - let queuedCountsByLabel = {}; - - if (canonicalRepository && fallbackEnabled && process.env.GITHUB_TOKEN) { - try { - queuedCountsByLabel = await collectQueuedBlacksmithJobs({ - repository, - token: process.env.GITHUB_TOKEN, - }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.log(`::warning title=Blacksmith fallback probe failed::${message}`); - } - } - - const selected = selectRunnerLabels({ - canonicalRepository, - fallbackEnabled, - queuedCountsByLabel, - queueThreshold, - }); - - console.log( - JSON.stringify( - { - fallbackEnabled, - queueThreshold, - queuedCountsByLabel, - selected, - }, - null, - 2, - ), - ); - writeOutputs(selected); -} - -if (import.meta.url === `file://${process.argv[1]}`) { - await main(); -} diff --git a/scripts/e2e/lib/parallels-package-common.sh b/scripts/e2e/lib/parallels-package-common.sh index 6afdad8347f..cffd29f8c71 100644 --- a/scripts/e2e/lib/parallels-package-common.sh +++ b/scripts/e2e/lib/parallels-package-common.sh @@ -48,7 +48,7 @@ parallels_package_write_dist_inventory() { parallels_package_assert_no_generated_drift() { local drift - drift="$(git status --porcelain -- src/canvas-host/a2ui/.bundle.hash 2>/dev/null || true)" + drift="$(git status --porcelain -- ':(glob)extensions/*/src/host/**/.bundle.hash' 2>/dev/null || true)" if [[ -z "$drift" ]]; then return 0 fi diff --git a/scripts/e2e/parallels/package-artifact.ts b/scripts/e2e/parallels/package-artifact.ts index 787c331cdfe..eb5cae6dee2 100644 --- a/scripts/e2e/parallels/package-artifact.ts +++ b/scripts/e2e/parallels/package-artifact.ts @@ -105,9 +105,13 @@ async function ensureCurrentBuildUnlocked(input: { say("Build Control UI for current head"); run("pnpm", ["ui:build"]); } - const drift = run("git", ["status", "--porcelain", "--", "src/canvas-host/a2ui/.bundle.hash"], { - quiet: true, - }).stdout.trim(); + const drift = run( + "git", + ["status", "--porcelain", "--", ":(glob)extensions/*/src/host/**/.bundle.hash"], + { + quiet: true, + }, + ).stdout.trim(); if (drift) { die(`generated file drift after build; commit or revert before Parallels packaging:\n${drift}`); } diff --git a/scripts/lib/ci-node-test-plan.mjs b/scripts/lib/ci-node-test-plan.mjs index 8e040aa536c..37dfa50b9b0 100644 --- a/scripts/lib/ci-node-test-plan.mjs +++ b/scripts/lib/ci-node-test-plan.mjs @@ -304,22 +304,16 @@ const SPLIT_NODE_SHARDS = new Map([ ], requiresDist: false, }, - { - shardName: "core-runtime-cron", - configs: ["test/vitest/vitest.cron.config.ts"], - requiresDist: false, - runner: "blacksmith-4vcpu-ubuntu-2404", - }, { shardName: "core-runtime-shared", configs: [ "test/vitest/vitest.acp.config.ts", + "test/vitest/vitest.cron.config.ts", "test/vitest/vitest.shared-core.config.ts", "test/vitest/vitest.tasks.config.ts", "test/vitest/vitest.utils.config.ts", ], requiresDist: false, - runner: "blacksmith-4vcpu-ubuntu-2404", }, ], ], diff --git a/scripts/pre-commit/filter-staged-files.mjs b/scripts/pre-commit/filter-staged-files.mjs index 56681b80696..2206a0240ce 100644 --- a/scripts/pre-commit/filter-staged-files.mjs +++ b/scripts/pre-commit/filter-staged-files.mjs @@ -21,15 +21,15 @@ if (mode !== "lint" && mode !== "format") { } const lintExts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]); -const formatExts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".md", ".mdx"]); -const formatIgnoredPaths = new Set(["src/canvas-host/a2ui/a2ui.bundle.js"]); +const formatExts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".md", ".mdx"]); +const formatIgnoredPathPatterns = [/^extensions\/[^/]+\/src\/host\/.+\/[^/]+\.bundle\.js$/u]; const shouldSelect = (filePath) => { const ext = path.extname(filePath).toLowerCase(); if (mode === "lint") { return lintExts.has(ext); } - if (formatIgnoredPaths.has(filePath)) { + if (formatIgnoredPathPatterns.some((pattern) => pattern.test(filePath))) { return false; } return formatExts.has(ext); diff --git a/scripts/prepush-ci.sh b/scripts/prepush-ci.sh index 8111b3acfeb..cd0796f8bff 100644 --- a/scripts/prepush-ci.sh +++ b/scripts/prepush-ci.sh @@ -17,7 +17,6 @@ run_step() { run_protocol_ci_mirror() { local targets=( "dist/protocol.schema.json" - "apps/macos/Sources/OpenClawProtocol/GatewayModels.swift" "apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift" ) local before after @@ -55,7 +54,7 @@ run_linux_ci_mirror() { run_step pnpm build:strict-smoke run_step pnpm lint:ui:no-raw-window-open run_protocol_ci_mirror - run_step pnpm canvas:a2ui:bundle + run_step pnpm plugins:assets:build run_step node scripts/run-vitest.mjs run --config test/vitest/vitest.extensions.config.ts --maxWorkers=1 run_step env CI=true node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts --maxWorkers=1 diff --git a/scripts/protocol-gen-swift.ts b/scripts/protocol-gen-swift.ts index 5ae87a01210..12ae36baaa7 100644 --- a/scripts/protocol-gen-swift.ts +++ b/scripts/protocol-gen-swift.ts @@ -15,7 +15,6 @@ type JsonSchema = { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, ".."); const outPaths = [ - path.join(repoRoot, "apps", "macos", "Sources", "OpenClawProtocol", "GatewayModels.swift"), path.join( repoRoot, "apps", diff --git a/scripts/restart-mac.sh b/scripts/restart-mac.sh index ba1aab336b6..444c34c50ab 100755 --- a/scripts/restart-mac.sh +++ b/scripts/restart-mac.sh @@ -153,8 +153,8 @@ log "==> Killing existing OpenClaw instances" kill_all_openclaw stop_launch_agent -# Bundle Gateway-hosted Canvas A2UI assets. -run_step "bundle canvas a2ui" bash -lc "cd '${ROOT_DIR}' && pnpm canvas:a2ui:bundle" +# Bundle Gateway-hosted plugin assets. +run_step "bundle plugin assets" bash -lc "cd '${ROOT_DIR}' && pnpm plugins:assets:build" # 2) Rebuild into the same path the packager consumes (.build). run_step "clean build cache" bash -lc "cd '${ROOT_DIR}/apps/macos' && rm -rf .build .build-swift .swiftpm 2>/dev/null || true" diff --git a/scripts/run-additional-boundary-checks.mjs b/scripts/run-additional-boundary-checks.mjs index 468e7367589..b40d9a7fb99 100644 --- a/scripts/run-additional-boundary-checks.mjs +++ b/scripts/run-additional-boundary-checks.mjs @@ -3,6 +3,7 @@ import { spawn } from "node:child_process"; import { performance } from "node:perf_hooks"; export const BOUNDARY_CHECKS = [ + ["prompt:snapshots:check", "pnpm", ["prompt:snapshots:check"]], ["plugin-extension-boundary", "pnpm", ["run", "lint:plugins:no-extension-imports"]], ["lint:tmp:no-random-messaging", "pnpm", ["run", "lint:tmp:no-random-messaging"]], ["lint:tmp:channel-agnostic-boundaries", "pnpm", ["run", "lint:tmp:channel-agnostic-boundaries"]], @@ -56,13 +57,6 @@ export const BOUNDARY_CHECKS = [ ["lint:ui:no-raw-window-open", "pnpm", ["lint:ui:no-raw-window-open"]], ].map(([label, command, args]) => ({ label, command, args })); -export const PROMPT_SNAPSHOT_CHECK_LABEL = "prompt:snapshots:check"; -export const PROMPT_SNAPSHOT_CHECK = { - label: PROMPT_SNAPSHOT_CHECK_LABEL, - command: "pnpm", - args: ["prompt:snapshots:check"], -}; - export function resolveConcurrency(value, fallback = 4) { const parsed = Number.parseInt(String(value ?? ""), 10); if (!Number.isFinite(parsed) || parsed < 1) { @@ -101,21 +95,6 @@ export function selectChecksForShard(checks, shardSpec) { return checks.filter((_check, index) => index % shard.count === shard.index); } -export function shouldRunPromptSnapshots(value) { - return ( - String(value ?? "true") - .trim() - .toLowerCase() !== "false" - ); -} - -export function filterChecksForEnvironment(checks, env = process.env) { - if (shouldRunPromptSnapshots(env.OPENCLAW_RUN_PROMPT_SNAPSHOTS)) { - return checks; - } - return checks.filter((check) => check.label !== PROMPT_SNAPSHOT_CHECK_LABEL); -} - export function formatCommand({ command, args }) { return [command, ...args].join(" "); } @@ -256,7 +235,7 @@ if (import.meta.url === `file://${process.argv[1]}`) { process.env.OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY, ); const shard = parseShardSpec(resolveCliShardSpec(process.argv.slice(2), process.env)); - const checks = filterChecksForEnvironment(selectChecksForShard(BOUNDARY_CHECKS, shard)); + const checks = selectChecksForShard(BOUNDARY_CHECKS, shard); if (shard) { process.stdout.write( `Running ${checks.length}/${BOUNDARY_CHECKS.length} additional boundary checks (shard ${shard.label})\n`, diff --git a/scripts/run-node-watch-paths.mjs b/scripts/run-node-watch-paths.mjs index c04af8e25d9..c92fef8a3f3 100644 --- a/scripts/run-node-watch-paths.mjs +++ b/scripts/run-node-watch-paths.mjs @@ -9,10 +9,10 @@ export const runNodeConfigFiles = ["tsconfig.json", "package.json", "tsdown.conf export const runNodeWatchedPaths = [...runNodeSourceRoots, ...runNodeConfigFiles]; export const extensionRestartMetadataFiles = new Set(["openclaw.plugin.json", "package.json"]); -const ignoredRunNodeRepoPaths = new Set([ - "src/canvas-host/a2ui/.bundle.hash", - "src/canvas-host/a2ui/a2ui.bundle.js", -]); +const ignoredRunNodeRepoPathPatterns = [ + /^extensions\/[^/]+\/src\/host\/.+\/\.bundle\.hash$/u, + /^extensions\/[^/]+\/src\/host\/.+\/[^/]+\.bundle\.js$/u, +]; const extensionSourceFilePattern = /\.(?:[cm]?[jt]sx?)$/; export const normalizeRunNodePath = (filePath) => String(filePath ?? "").replaceAll("\\", "/"); @@ -41,7 +41,7 @@ const isRestartRelevantExtensionPath = (relativePath) => { const isRelevantRunNodePath = (repoPath, isRelevantBundledPluginPath) => { const normalizedPath = normalizeRunNodePath(repoPath).replace(/^\.\/+/, ""); - if (ignoredRunNodeRepoPaths.has(normalizedPath)) { + if (ignoredRunNodeRepoPathPatterns.some((pattern) => pattern.test(normalizedPath))) { return false; } if (runNodeConfigFiles.includes(normalizedPath)) { diff --git a/scripts/sync-codex-app-server-protocol.ts b/scripts/sync-codex-app-server-protocol.ts index 254b451b14d..cb82b068da3 100644 --- a/scripts/sync-codex-app-server-protocol.ts +++ b/scripts/sync-codex-app-server-protocol.ts @@ -14,16 +14,13 @@ const source = await generateExperimentalCodexAppServerProtocolSource(); try { await fs.rm(targetRoot, { recursive: true, force: true }); await fs.mkdir(targetRoot, { recursive: true }); - await fs.cp(source.typescriptRoot, path.join(targetRoot, "typescript"), { - recursive: true, - }); for (const schema of selectedCodexAppServerJsonSchemas) { await fs.mkdir(path.dirname(path.join(targetRoot, "json", schema)), { recursive: true }); const schemaSource = await fs.readFile(path.join(source.jsonRoot, schema), "utf8"); await fs.writeFile( path.join(targetRoot, "json", schema), - `${JSON.stringify(JSON.parse(schemaSource))}\n`, + `${JSON.stringify(JSON.parse(schemaSource), null, 2)}\n`, ); } } finally { diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 18158070d81..22589fe73d1 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -347,7 +347,6 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([ ["scripts/run-oxlint.mjs", ["test/scripts/run-oxlint.test.ts"]], ["scripts/run-node.mjs", ["src/infra/run-node.test.ts"]], ["scripts/ci-run-timings.mjs", ["test/scripts/ci-run-timings.test.ts"]], - ["scripts/ci-runner-labels.mjs", ["test/scripts/ci-runner-labels.test.ts"]], ["scripts/test-extension-batch.mjs", ["test/scripts/test-extension.test.ts"]], ["scripts/lib/extension-test-plan.mjs", ["test/scripts/test-extension.test.ts"]], ["scripts/lib/vitest-batch-runner.mjs", ["test/scripts/test-extension.test.ts"]], @@ -375,6 +374,10 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([ ["scripts/blacksmith-testbox-state.mjs", ["test/scripts/blacksmith-testbox-state.test.ts"]], ["scripts/blacksmith-testbox-runner.mjs", ["test/scripts/blacksmith-testbox-runner.test.ts"]], ["scripts/testbox-sync-sanity.mjs", ["test/scripts/testbox-sync-sanity.test.ts"]], + ["scripts/bundled-plugin-assets.mjs", ["test/scripts/bundled-plugin-assets.test.ts"]], + ["scripts/bundle-a2ui.mjs", ["test/scripts/bundled-plugin-assets.test.ts"]], + ["extensions/canvas/scripts/bundle-a2ui.mjs", ["test/scripts/bundle-a2ui.test.ts"]], + ["extensions/canvas/scripts/copy-a2ui.mjs", ["src/scripts/canvas-a2ui-copy.test.ts"]], ]); const TOOLING_TEST_TARGETS = new Map([ ["test/scripts/barnacle-auto-response.test.ts", ["test/scripts/barnacle-auto-response.test.ts"]], @@ -495,10 +498,10 @@ const SOURCE_TEST_TARGETS = new Map([ ["src/auto-reply/reply/dispatch-acp-command-bypass.test.ts"], ], ]); -const GENERATED_CHANGED_TEST_TARGETS = new Set([ - "src/canvas-host/a2ui/.bundle.hash", - "src/canvas-host/a2ui/a2ui.bundle.js", -]); +const GENERATED_CHANGED_TEST_TARGET_PATTERNS = [ + /^extensions\/[^/]+\/src\/host\/.+\/\.bundle\.hash$/u, + /^extensions\/[^/]+\/src\/host\/.+\/[^/]+\.bundle\.js$/u, +]; const SOURCE_ROOTS_FOR_IMPORT_GRAPH = ["src", "extensions", "packages", "ui/src", "test"]; const IMPORTABLE_FILE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"]; const IMPORT_SPECIFIER_PATTERN = @@ -940,7 +943,7 @@ function shouldUseBroadChangedTargets(env = process.env) { } function isRoutableChangedTarget(changedPath) { - if (GENERATED_CHANGED_TEST_TARGETS.has(changedPath)) { + if (GENERATED_CHANGED_TEST_TARGET_PATTERNS.some((pattern) => pattern.test(changedPath))) { return false; } if (changedPath.endsWith(".live.test.ts")) { diff --git a/src/agents/command/delivery.test.ts b/src/agents/command/delivery.test.ts index 8168d6722d9..2e82a135ead 100644 --- a/src/agents/command/delivery.test.ts +++ b/src/agents/command/delivery.test.ts @@ -216,22 +216,6 @@ describe("normalizeAgentCommandReplyPayloads", () => { }); it("reports successful requested delivery", async () => { - deliverOutboundPayloadsMock.mockResolvedValue([ - { - channel: "slack", - messageId: "m1", - }, - ]); - - const delivered = await deliverMediaReplyForTest({ - key: "agent:tester:slack:direct:alice", - agentId: "tester", - } as never); - - expect(delivered.deliverySucceeded).toBe(true); - }); - - it("does not report success when delivery claims no adapter result", async () => { deliverOutboundPayloadsMock.mockResolvedValue([]); const delivered = await deliverMediaReplyForTest({ @@ -239,7 +223,7 @@ describe("normalizeAgentCommandReplyPayloads", () => { agentId: "tester", } as never); - expect(delivered.deliverySucceeded).toBe(false); + expect(delivered.deliverySucceeded).toBe(true); }); it("does not report success when best-effort delivery records an error", async () => { diff --git a/src/agents/command/delivery.ts b/src/agents/command/delivery.ts index 5e334107a25..2cad2a18179 100644 --- a/src/agents/command/delivery.ts +++ b/src/agents/command/delivery.ts @@ -381,7 +381,7 @@ export async function deliverAgentCommandResult(params: { } if (deliver && deliveryChannel && !isInternalMessageChannel(deliveryChannel)) { if (deliveryTarget) { - const deliveryResults = await deliverOutboundPayloads({ + await deliverOutboundPayloads({ cfg, channel: deliveryChannel, to: deliveryTarget, @@ -395,7 +395,7 @@ export async function deliverAgentCommandResult(params: { onPayload: logPayload, deps: createOutboundSendDeps(deps), }); - deliverySucceeded = deliveryResults.length > 0 && !deliveryHadError; + deliverySucceeded = !deliveryHadError; } } diff --git a/src/agents/compaction.identifier-preservation.test.ts b/src/agents/compaction.identifier-preservation.test.ts index 92be73ef07f..957180c72a9 100644 --- a/src/agents/compaction.identifier-preservation.test.ts +++ b/src/agents/compaction.identifier-preservation.test.ts @@ -7,7 +7,6 @@ vi.mock("@mariozechner/pi-coding-agent", async () => { const actual = await vi.importActual("@mariozechner/pi-coding-agent"); return { ...actual, - estimateTokens: vi.fn((message: unknown) => Math.ceil(JSON.stringify(message).length / 4)), generateSummary: vi.fn(), }; }); diff --git a/src/agents/compaction.reserve-tokens-clamping.test.ts b/src/agents/compaction.reserve-tokens-clamping.test.ts deleted file mode 100644 index b23bbaf2aa9..00000000000 --- a/src/agents/compaction.reserve-tokens-clamping.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import type { AgentMessage } from "@mariozechner/pi-agent-core"; -import type { ExtensionContext } from "@mariozechner/pi-coding-agent"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const piCodingAgentMocks = vi.hoisted(() => ({ - estimateTokens: vi.fn((message: unknown) => Math.ceil(JSON.stringify(message).length / 4)), - generateSummary: vi.fn(), -})); - -vi.mock("@mariozechner/pi-coding-agent", async () => { - const actual = await vi.importActual( - "@mariozechner/pi-coding-agent", - ); - return { - ...actual, - estimateTokens: piCodingAgentMocks.estimateTokens, - generateSummary: piCodingAgentMocks.generateSummary, - }; -}); - -const mockGenerateSummary = piCodingAgentMocks.generateSummary; - -let summarizeInStages: typeof import("./compaction.js").summarizeInStages; - -async function loadFreshCompactionModuleForTest() { - vi.resetModules(); - ({ summarizeInStages } = await import("./compaction.js")); -} - -function makeMessage(index: number, size = 1200): AgentMessage { - return { - role: "user", - content: `m${index}-${"x".repeat(size)}`, - timestamp: index, - }; -} - -describe("compaction reserveTokens clamping", () => { - beforeEach(async () => { - await loadFreshCompactionModuleForTest(); - mockGenerateSummary.mockReset(); - mockGenerateSummary.mockResolvedValue("summary"); - piCodingAgentMocks.estimateTokens.mockReset(); - piCodingAgentMocks.estimateTokens.mockImplementation((message: unknown) => - Math.ceil(JSON.stringify(message).length / 4), - ); - }); - - it("clamps reserveTokens when model maxTokens is smaller than requested", async () => { - // Simulate the exact bug scenario: large context window (1M) with - // reserveTokensFloor of 300K, but model output limit is only 128K. - // Without clamping, generateSummary would receive 300K and compute - // max_tokens = floor(0.8 * 300K) = 240K, exceeding the 128K model limit. - const model = { - provider: "anthropic", - model: "claude-sonnet-4-6", - contextWindow: 1_000_000, - maxTokens: 128_000, - } as unknown as NonNullable; - - await summarizeInStages({ - model, - apiKey: "test-key", // pragma: allowlist secret - reserveTokens: 300_000, - maxChunkTokens: 8000, - contextWindow: 1_000_000, - signal: new AbortController().signal, - messages: [makeMessage(1), makeMessage(2)], - }); - - expect(mockGenerateSummary).toHaveBeenCalled(); - // Third argument to generateSummary is reserveTokens. - // With maxTokens 128K, the clamp should be floor(128_000 / 0.8) = 160_000. - const passedReserveTokens = mockGenerateSummary.mock.calls[0][2]; - expect(passedReserveTokens).toBeLessThanOrEqual(Math.floor(128_000 / 0.8)); - expect(passedReserveTokens).toBe(160_000); - }); - - it("does not clamp when model maxTokens is large enough", async () => { - const model = { - provider: "anthropic", - model: "claude-opus-4-6", - contextWindow: 200_000, - maxTokens: 32_000, - } as unknown as NonNullable; - - // reserveTokens 4000 is well under floor(32_000 / 0.8) = 40_000 - await summarizeInStages({ - model, - apiKey: "test-key", // pragma: allowlist secret - reserveTokens: 4000, - maxChunkTokens: 8000, - contextWindow: 200_000, - signal: new AbortController().signal, - messages: [makeMessage(1), makeMessage(2)], - }); - - expect(mockGenerateSummary).toHaveBeenCalled(); - const passedReserveTokens = mockGenerateSummary.mock.calls[0][2]; - expect(passedReserveTokens).toBe(4000); - }); - - it("falls back to 128K default when model has no maxTokens field", async () => { - // Model without maxTokens defined — should default to 128_000 as the cap. - const model = { - provider: "anthropic", - model: "claude-3-opus", - contextWindow: 1_000_000, - } as unknown as NonNullable; - - await summarizeInStages({ - model, - apiKey: "test-key", // pragma: allowlist secret - reserveTokens: 300_000, - maxChunkTokens: 8000, - contextWindow: 1_000_000, - signal: new AbortController().signal, - messages: [makeMessage(1), makeMessage(2)], - }); - - expect(mockGenerateSummary).toHaveBeenCalled(); - // Fallback maxTokens is 128_000, so clamp = floor(128_000 / 0.8) = 160_000 - const passedReserveTokens = mockGenerateSummary.mock.calls[0][2]; - expect(passedReserveTokens).toBe(160_000); - }); - - it("clamps consistently across all chunks in staged summarization", async () => { - const model = { - provider: "anthropic", - model: "claude-sonnet-4-6", - contextWindow: 1_000_000, - maxTokens: 128_000, - } as unknown as NonNullable; - - // Use enough messages and small chunk size to force multiple chunks - await summarizeInStages({ - model, - apiKey: "test-key", // pragma: allowlist secret - reserveTokens: 300_000, - maxChunkTokens: 1000, - contextWindow: 1_000_000, - signal: new AbortController().signal, - messages: Array.from({ length: 4 }, (_, i) => makeMessage(i + 1)), - parts: 2, - minMessagesForSplit: 4, - }); - - expect(mockGenerateSummary.mock.calls.length).toBeGreaterThan(1); - const expectedClamp = Math.floor(128_000 / 0.8); - for (const call of mockGenerateSummary.mock.calls) { - expect(call[2]).toBeLessThanOrEqual(expectedClamp); - } - }); -}); diff --git a/src/agents/compaction.ts b/src/agents/compaction.ts index fe64bfc8f9e..888daf7bfec 100644 --- a/src/agents/compaction.ts +++ b/src/agents/compaction.ts @@ -319,22 +319,13 @@ async function summarizeChunks(params: { params.customInstructions, params.summarizationInstructions, ); - - // Clamp reserveTokens to the model's maxTokens output cap. - // generateSummary() uses Math.floor(0.8 * reserveTokens) as max_tokens for the API call. - // With large context windows (1M tokens), reserveTokensFloor can be 300K+, producing - // max_tokens of 240K+ which exceeds model output limits (e.g. 128K for Anthropic). - // By clamping reserveTokens here, we ensure the downstream max_tokens stays within bounds. - const modelMaxTokens = params.model.maxTokens ?? 128_000; - const clampedReserveTokens = Math.min(params.reserveTokens, Math.floor(modelMaxTokens / 0.8)); - for (const chunk of chunks) { summary = await retryAsync( () => generateSummary( chunk, params.model, - clampedReserveTokens, + params.reserveTokens, params.apiKey, params.headers, params.signal, diff --git a/src/agents/failover-error.ts b/src/agents/failover-error.ts index 94093acb7ca..abe215d1f6c 100644 --- a/src/agents/failover-error.ts +++ b/src/agents/failover-error.ts @@ -210,7 +210,7 @@ function normalizeDirectErrorSignal(err: unknown): FailoverSignal { }; } -export function hasSessionWriteLockTimeout(err: unknown, seen: Set = new Set()): boolean { +function hasSessionWriteLockTimeout(err: unknown, seen: Set = new Set()): boolean { if (isSessionWriteLockTimeoutError(err)) { return true; } diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index a588a1e8c43..3fe1ba7e22a 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -21,7 +21,6 @@ import { } from "./model-fallback.js"; import { classifyEmbeddedPiRunResultForModelFallback } from "./pi-embedded-runner/result-fallback-classifier.js"; import type { EmbeddedPiRunResult } from "./pi-embedded-runner/types.js"; -import { SessionWriteLockTimeoutError } from "./session-write-lock-error.js"; import { makeModelFallbackCfg } from "./test-helpers/model-fallback-config-fixture.js"; vi.mock("../infra/file-lock.js", () => ({ @@ -571,35 +570,6 @@ describe("runWithModelFallback", () => { } }); - it("fails fast on session write-lock timeouts instead of trying model fallbacks", async () => { - const cfg = makeCfg({ - agents: { - defaults: { - model: { - primary: "openai/gpt-5.4", - fallbacks: ["anthropic/claude-opus-4-6"], - }, - }, - }, - }); - const lockError = new SessionWriteLockTimeoutError({ - timeoutMs: 10_000, - owner: "pid=37121", - lockPath: "/tmp/openclaw/session.jsonl.lock", - }); - const run = vi.fn().mockRejectedValueOnce(lockError); - - await expect( - runWithModelFallback({ - cfg, - provider: "openai", - model: "gpt-5.4", - run, - }), - ).rejects.toBe(lockError); - expect(run).toHaveBeenCalledTimes(1); - }); - it("uses optional result classification to continue to configured fallbacks", async () => { const cfg = makeCfg({ agents: { diff --git a/src/agents/model-fallback.ts b/src/agents/model-fallback.ts index 06af7d8a337..7271831dbff 100644 --- a/src/agents/model-fallback.ts +++ b/src/agents/model-fallback.ts @@ -16,7 +16,6 @@ import { FailoverError, coerceToFailoverError, describeFailoverError, - hasSessionWriteLockTimeout, isFailoverError, isTimeoutError, } from "./failover-error.js"; @@ -1049,9 +1048,6 @@ export async function runWithModelFallback(params: { sessionId: params.sessionId, lane: params.lane, }) ?? err; - if (hasSessionWriteLockTimeout(normalized)) { - throw err; - } // LiveSessionModelSwitchError during fallback may point at a later // candidate that is already the active live-session selection. Jump diff --git a/src/agents/openclaw-gateway-tool.test.ts b/src/agents/openclaw-gateway-tool.test.ts index ff808104b96..2aa8716b326 100644 --- a/src/agents/openclaw-gateway-tool.test.ts +++ b/src/agents/openclaw-gateway-tool.test.ts @@ -4,7 +4,6 @@ import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { __testing as restartTesting } from "../infra/restart.js"; import { withEnvAsync } from "../test-utils/env.js"; -import "./test-helpers/fast-core-tools.js"; import { createGatewayTool } from "./tools/gateway-tool.js"; import { callGatewayTool } from "./tools/gateway.js"; diff --git a/src/agents/openclaw-tools.subagents.scope.test.ts b/src/agents/openclaw-tools.subagents.scope.test.ts index fc233015064..23b3e4aa4a9 100644 --- a/src/agents/openclaw-tools.subagents.scope.test.ts +++ b/src/agents/openclaw-tools.subagents.scope.test.ts @@ -8,7 +8,6 @@ import { setSubagentsConfigOverride, } from "./openclaw-tools.subagents.test-harness.js"; import { addSubagentRunForTests, resetSubagentRegistryForTests } from "./subagent-registry.js"; -import "./test-helpers/fast-core-tools.js"; import { createPerSenderSessionConfig } from "./test-helpers/session-config.js"; import { createSubagentsTool } from "./tools/subagents-tool.js"; diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts index b9ecf8d0502..a4bf1a2b057 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts @@ -1,7 +1,6 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { AgentRouteBinding } from "../config/types.agents.js"; import { emitAgentEvent } from "../infra/agent-events.js"; -import "./test-helpers/fast-core-tools.js"; import { getCallGatewayMock, getSessionsSpawnTool, diff --git a/src/agents/openclaw-tools.subagents.steer-failure-clears-suppression.test.ts b/src/agents/openclaw-tools.subagents.steer-failure-clears-suppression.test.ts index 7c4ee1461cd..43e560b11e9 100644 --- a/src/agents/openclaw-tools.subagents.steer-failure-clears-suppression.test.ts +++ b/src/agents/openclaw-tools.subagents.steer-failure-clears-suppression.test.ts @@ -11,7 +11,6 @@ import { listSubagentRunsForRequester, resetSubagentRegistryForTests, } from "./subagent-registry.js"; -import "./test-helpers/fast-core-tools.js"; import { createSubagentsTool } from "./tools/subagents-tool.js"; describe("openclaw-tools: subagents steer failure", () => { diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index 911248cc8d4..7d133a2a643 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -26,7 +26,6 @@ import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; import type { SpawnedToolContext } from "./spawned-context.js"; import type { ToolFsPolicy } from "./tool-fs-policy.js"; import { createAgentsListTool } from "./tools/agents-list-tool.js"; -import { createCanvasTool } from "./tools/canvas-tool.js"; import type { AnyAgentTool } from "./tools/common.js"; import { createCronTool } from "./tools/cron-tool.js"; import { createEmbeddedCallGateway } from "./tools/embedded-gateway-stub.js"; @@ -324,7 +323,6 @@ export function createOpenClawTools( ...(embedded ? [] : [ - createCanvasTool({ config: options?.config }), nodesTool, createCronTool({ agentSessionKey: options?.agentSessionKey, diff --git a/src/agents/openclaw-tools.tts-config.test.ts b/src/agents/openclaw-tools.tts-config.test.ts index a592fbedf6a..0b22af8ae52 100644 --- a/src/agents/openclaw-tools.tts-config.test.ts +++ b/src/agents/openclaw-tools.tts-config.test.ts @@ -37,10 +37,6 @@ vi.mock("./tools/agents-list-tool.js", () => ({ createAgentsListTool: () => mocks.stubTool("agents_list"), })); -vi.mock("./tools/canvas-tool.js", () => ({ - createCanvasTool: () => mocks.stubTool("canvas"), -})); - vi.mock("./tools/cron-tool.js", () => ({ createCronTool: (options: unknown) => { mocks.createCronToolOptions(options); diff --git a/src/agents/pi-embedded-runner/system-prompt.ts b/src/agents/pi-embedded-runner/system-prompt.ts index 0cc2e993a5b..1b3b5982ca8 100644 --- a/src/agents/pi-embedded-runner/system-prompt.ts +++ b/src/agents/pi-embedded-runner/system-prompt.ts @@ -53,7 +53,6 @@ export function buildEmbeddedSystemPrompt(params: { channel?: string; /** Supported message actions for the current channel (e.g., react, edit, unsend) */ channelActions?: string[]; - canvasRootDir?: string; }; messageToolHints?: string[]; sandboxInfo?: EmbeddedSandboxInfo; diff --git a/src/agents/pi-embedded-runner/tool-result-context-guard.test.ts b/src/agents/pi-embedded-runner/tool-result-context-guard.test.ts index b54d38ed35a..4f1f641dcd5 100644 --- a/src/agents/pi-embedded-runner/tool-result-context-guard.test.ts +++ b/src/agents/pi-embedded-runner/tool-result-context-guard.test.ts @@ -760,49 +760,4 @@ describe("installContextEngineLoopHook", () => { expect(retryResult).toBe(compactedView); expect(engine.assemble).toHaveBeenCalledTimes(1); }); - - it("clears the cached assembled view when the source history shrinks", async () => { - const agent = makeGuardableAgent(); - const compactedView = [makeUser("compacted")]; - const engine = makeMockEngine({ - assemble: async () => ({ messages: compactedView, estimatedTokens: 0 }), - }); - installHook(agent, engine); - - const { transformed: firstResult } = await callAfterInitialToolResult(agent, { - includeSecondUser: false, - firstResultText: "r", - }); - expect(firstResult).toBe(compactedView); - - const postResetMessages = [makeUser("fresh post-reset turn")]; - const postResetResult = await callTransform(agent, postResetMessages); - expect(postResetResult).toBe(postResetMessages); - }); - - it("clears the cached assembled view when assemble fails", async () => { - const agent = makeGuardableAgent(); - const compactedView = [makeUser("compacted")]; - const engine = makeMockEngine({ - assemble: async () => ({ messages: compactedView, estimatedTokens: 0 }), - }); - installHook(agent, engine); - - const { withNew, transformed: firstResult } = await callAfterInitialToolResult(agent, { - includeSecondUser: false, - firstResultText: "r", - }); - expect(firstResult).toBe(compactedView); - - const afterFailureMessages = [...withNew, makeUser("after failure")]; - engine.assemble.mockImplementationOnce(async () => { - throw new Error("engine assemble boom"); - }); - - const failureResult = await callTransform(agent, afterFailureMessages); - expect(failureResult).toBe(afterFailureMessages); - - const retryResult = await callTransform(agent, afterFailureMessages); - expect(retryResult).toBe(afterFailureMessages); - }); }); diff --git a/src/agents/pi-embedded-runner/tool-result-context-guard.ts b/src/agents/pi-embedded-runner/tool-result-context-guard.ts index 3815e53fb58..8331589ef25 100644 --- a/src/agents/pi-embedded-runner/tool-result-context-guard.ts +++ b/src/agents/pi-embedded-runner/tool-result-context-guard.ts @@ -250,25 +250,12 @@ export function installContextEngineLoopHook(params: { const originalTransformContext = mutableAgent.transformContext; let lastSeenLength: number | null = null; let lastAssembledView: AgentMessage[] | null = null; - let lastAssembledFromSourceLength: number | null = null; - - const clearAssembledCache = () => { - lastAssembledView = null; - lastAssembledFromSourceLength = null; - }; mutableAgent.transformContext = (async (messages: AgentMessage[], signal: AbortSignal) => { const transformed = originalTransformContext ? await originalTransformContext.call(mutableAgent, messages, signal) : messages; const sourceMessages = Array.isArray(transformed) ? transformed : messages; - if ( - lastAssembledFromSourceLength !== null && - sourceMessages.length < lastAssembledFromSourceLength - ) { - clearAssembledCache(); - lastSeenLength = null; - } // Seed the loop fence from the attempt's pre-prompt message count when available. // This keeps the first real post-tool-call iteration eligible for compaction even @@ -330,14 +317,12 @@ export function installContextEngineLoopHook(params: { }); if (assembled && Array.isArray(assembled.messages) && assembled.messages !== sourceMessages) { lastAssembledView = assembled.messages; - lastAssembledFromSourceLength = sourceMessages.length; return assembled.messages; } - clearAssembledCache(); + lastAssembledView = null; } catch { // Best-effort: any engine failure falls through to the raw source // messages so the tool loop still makes forward progress. - clearAssembledCache(); } return sourceMessages; diff --git a/src/agents/pi-embedded-subscribe.handlers.tools.test.ts b/src/agents/pi-embedded-subscribe.handlers.tools.test.ts index a85c32f1fc4..0580533a1f4 100644 --- a/src/agents/pi-embedded-subscribe.handlers.tools.test.ts +++ b/src/agents/pi-embedded-subscribe.handlers.tools.test.ts @@ -755,315 +755,6 @@ describe("handleToolExecutionEnd derived tool events", () => { ); }); - it("throttles high-frequency exec output update events", async () => { - vi.useFakeTimers(); - vi.setSystemTime(1_000); - try { - const { ctx, onAgentEvent } = createTestContext(); - - await handleToolExecutionStart( - ctx as never, - { - type: "tool_execution_start", - toolName: "exec", - toolCallId: "tool-exec-throttled-output", - args: { command: "yes" }, - } as never, - ); - - const update = (aggregated: string) => - handleToolExecutionUpdate( - ctx as never, - { - type: "tool_execution_update", - toolName: "exec", - toolCallId: "tool-exec-throttled-output", - partialResult: { - details: { - aggregated, - }, - }, - } as never, - ); - - update("first"); - update("second"); - update("x".repeat(1024 * 1024)); - vi.setSystemTime(1_300); - update("third"); - - const commandOutputCalls = onAgentEvent.mock.calls - .map((call) => call[0] as { stream?: string; data?: { output?: string } }) - .filter((event) => event.stream === "command_output"); - - expect(commandOutputCalls.map((event) => event.data?.output)).toEqual(["first", "third"]); - } finally { - vi.useRealTimers(); - } - }); - - it("drops throttled exec output before emitting live events or callbacks", async () => { - vi.useFakeTimers(); - vi.setSystemTime(1_000); - resetAgentEventsForTest(); - const events: Array<{ stream?: string; data?: Record }> = []; - registerAgentEventListener((evt) => { - events.push(evt as never); - }); - try { - const { ctx, onAgentEvent } = createTestContext(); - - await handleToolExecutionStart( - ctx as never, - { - type: "tool_execution_start", - toolName: "exec", - toolCallId: "tool-exec-drop-suppressed-output", - args: { command: "yes" }, - } as never, - ); - - handleToolExecutionUpdate( - ctx as never, - { - type: "tool_execution_update", - toolName: "exec", - toolCallId: "tool-exec-drop-suppressed-output", - partialResult: { details: { aggregated: "first" } }, - } as never, - ); - const emittedEventCount = events.length; - const callbackCount = onAgentEvent.mock.calls.length; - const itemStartedCount = ctx.state.itemStartedCount; - - handleToolExecutionUpdate( - ctx as never, - { - type: "tool_execution_update", - toolName: "exec", - toolCallId: "tool-exec-drop-suppressed-output", - partialResult: { details: { aggregated: "x".repeat(1024 * 1024) } }, - } as never, - ); - - expect(events).toHaveLength(emittedEventCount); - expect(onAgentEvent).toHaveBeenCalledTimes(callbackCount); - expect(ctx.state.itemStartedCount).toBe(itemStartedCount); - } finally { - resetAgentEventsForTest(); - vi.useRealTimers(); - } - }); - - it("throttles exec output independently per tool call", async () => { - vi.useFakeTimers(); - vi.setSystemTime(1_000); - try { - const { ctx, onAgentEvent } = createTestContext(); - - for (const toolCallId of ["tool-exec-per-tool-a", "tool-exec-per-tool-b"]) { - await handleToolExecutionStart( - ctx as never, - { - type: "tool_execution_start", - toolName: "exec", - toolCallId, - args: { command: "yes" }, - } as never, - ); - } - - for (const toolCallId of ["tool-exec-per-tool-a", "tool-exec-per-tool-b"]) { - handleToolExecutionUpdate( - ctx as never, - { - type: "tool_execution_update", - toolName: "exec", - toolCallId, - partialResult: { details: { aggregated: `first-${toolCallId}` } }, - } as never, - ); - } - - const commandOutputCalls = onAgentEvent.mock.calls - .map((call) => call[0] as { stream?: string; data?: { output?: string } }) - .filter((event) => event.stream === "command_output"); - - expect(commandOutputCalls.map((event) => event.data?.output)).toEqual([ - "first-tool-exec-per-tool-a", - "first-tool-exec-per-tool-b", - ]); - } finally { - vi.useRealTimers(); - } - }); - - it("clears exec output throttle state when the tool ends", async () => { - vi.useFakeTimers(); - vi.setSystemTime(1_000); - try { - const { ctx, onAgentEvent } = createTestContext(); - const toolCallId = "tool-exec-throttle-cleared"; - - await handleToolExecutionStart( - ctx as never, - { - type: "tool_execution_start", - toolName: "exec", - toolCallId, - args: { command: "yes" }, - } as never, - ); - handleToolExecutionUpdate( - ctx as never, - { - type: "tool_execution_update", - toolName: "exec", - toolCallId, - partialResult: { details: { aggregated: "first run output" } }, - } as never, - ); - await handleToolExecutionEnd( - ctx as never, - { - type: "tool_execution_end", - toolName: "exec", - toolCallId, - isError: false, - result: { details: { status: "completed", aggregated: "done" } }, - } as never, - ); - await handleToolExecutionStart( - ctx as never, - { - type: "tool_execution_start", - toolName: "exec", - toolCallId, - args: { command: "yes" }, - } as never, - ); - handleToolExecutionUpdate( - ctx as never, - { - type: "tool_execution_update", - toolName: "exec", - toolCallId, - partialResult: { details: { aggregated: "second run output" } }, - } as never, - ); - - const commandOutputCalls = onAgentEvent.mock.calls - .map((call) => call[0] as { stream?: string; data?: { output?: string } }) - .filter((event) => event.stream === "command_output"); - - expect(commandOutputCalls.map((event) => event.data?.output)).toContain("second run output"); - } finally { - vi.useRealTimers(); - } - }); - - it("does not throttle exec update events that carry no output", async () => { - vi.useFakeTimers(); - vi.setSystemTime(1_000); - try { - const { ctx, onAgentEvent } = createTestContext(); - - await handleToolExecutionStart( - ctx as never, - { - type: "tool_execution_start", - toolName: "exec", - toolCallId: "tool-exec-no-output-updates", - args: { command: "sleep 1" }, - } as never, - ); - - for (let i = 0; i < 2; i += 1) { - handleToolExecutionUpdate( - ctx as never, - { - type: "tool_execution_update", - toolName: "exec", - toolCallId: "tool-exec-no-output-updates", - partialResult: { details: { status: "running", pid: 1234 + i } }, - } as never, - ); - } - - const updateCallbacks = onAgentEvent.mock.calls - .map((call) => call[0] as { stream?: string; data?: { phase?: string } }) - .filter((event) => event.stream === "tool" && event.data?.phase === "update"); - - expect(updateCallbacks).toHaveLength(2); - } finally { - vi.useRealTimers(); - } - }); - - it("caps oversized exec update payloads that pass the throttle window", async () => { - vi.useFakeTimers(); - vi.setSystemTime(1_000); - resetAgentEventsForTest(); - const events: Array<{ stream?: string; data?: Record }> = []; - registerAgentEventListener((evt) => { - events.push(evt as never); - }); - try { - const { ctx, onAgentEvent } = createTestContext(); - const aggregated = `head-${"x".repeat(90 * 1024)}-tail`; - - await handleToolExecutionStart( - ctx as never, - { - type: "tool_execution_start", - toolName: "exec", - toolCallId: "tool-exec-update-long-output", - args: { command: "yes" }, - } as never, - ); - - handleToolExecutionUpdate( - ctx as never, - { - type: "tool_execution_update", - toolName: "exec", - toolCallId: "tool-exec-update-long-output", - partialResult: { details: { aggregated: "first" } }, - } as never, - ); - vi.setSystemTime(1_300); - handleToolExecutionUpdate( - ctx as never, - { - type: "tool_execution_update", - toolName: "exec", - toolCallId: "tool-exec-update-long-output", - partialResult: { details: { aggregated } }, - } as never, - ); - - const lastCommandOutput = onAgentEvent.mock.calls - .map((call) => call[0] as { stream?: string; data?: { output?: string } }) - .findLast((event) => event.stream === "command_output"); - expect(lastCommandOutput?.data?.output).toContain("live command output truncated"); - expect(lastCommandOutput?.data?.output).toContain("-tail"); - expect(lastCommandOutput?.data?.output).not.toContain("head-"); - - const updateEvent = events.findLast( - (evt) => evt.stream === "tool" && (evt.data as { phase?: string })?.phase === "update", - ); - const partialResult = updateEvent?.data?.partialResult as - | { details?: { aggregated?: string } } - | undefined; - expect(partialResult?.details?.aggregated).toContain("live command output truncated"); - expect(partialResult?.details?.aggregated).toContain("-tail"); - expect(partialResult?.details?.aggregated).not.toContain("head-"); - } finally { - resetAgentEventsForTest(); - vi.useRealTimers(); - } - }); - it("emits command output events for exec results", async () => { const { ctx, onAgentEvent } = createTestContext(); @@ -1489,108 +1180,6 @@ describe("control UI credential redaction (issue #72283)", () => { expect(lastOutput?.data?.output).toContain("OPENROUTER_API_KEY="); }); - it("caps live exec command output events without changing the tool result shape", async () => { - const events: Array<{ stream?: string; data?: Record }> = []; - registerAgentEventListener((evt) => { - events.push(evt as never); - }); - const { ctx, onAgentEvent } = createTestContext(); - const aggregated = `head-${"x".repeat(90 * 1024)}-tail`; - - await handleToolExecutionStart( - ctx as never, - { - type: "tool_execution_start", - toolName: "exec", - toolCallId: "tool-exec-long-output", - args: { command: "yes" }, - } as never, - ); - - await handleToolExecutionEnd( - ctx as never, - { - type: "tool_execution_end", - toolName: "exec", - toolCallId: "tool-exec-long-output", - isError: false, - result: { - details: { - status: "completed", - aggregated, - exitCode: 0, - }, - }, - } as never, - ); - - const commandOutputCalls = onAgentEvent.mock.calls - .map((call) => call[0]) - .filter((arg: unknown) => (arg as { stream?: string })?.stream === "command_output"); - const lastOutput = commandOutputCalls.at(-1) as { data?: { output?: string } } | undefined; - expect(lastOutput?.data?.output).toContain("live command output truncated"); - expect(lastOutput?.data?.output).toContain("-tail"); - expect(lastOutput?.data?.output).not.toContain("head-"); - - const resultEvent = events.find( - (evt) => evt.stream === "tool" && (evt.data as { phase?: string })?.phase === "result", - ); - const result = resultEvent?.data?.result as { details?: { aggregated?: string } } | undefined; - expect(result?.details?.aggregated).toContain("live command output truncated"); - expect(result?.details?.aggregated).toContain("-tail"); - }); - - it("parses exec approval resolution from raw output even when live output is capped", async () => { - const { ctx, onAgentEvent } = createTestContext(); - const aggregated = `exec denied (user-denied): blocked by reviewer\n${"x".repeat( - 90 * 1024, - )}-tail`; - - await handleToolExecutionStart( - ctx as never, - { - type: "tool_execution_start", - toolName: "exec", - toolCallId: "tool-exec-denied-long-output", - args: { command: "rm -rf /tmp/example" }, - } as never, - ); - - await handleToolExecutionEnd( - ctx as never, - { - type: "tool_execution_end", - toolName: "exec", - toolCallId: "tool-exec-denied-long-output", - isError: true, - result: { - details: { - status: "failed", - aggregated, - exitCode: 1, - }, - }, - } as never, - ); - - const commandOutput = onAgentEvent.mock.calls - .map((call) => call[0] as { stream?: string; data?: { output?: string } }) - .findLast((event) => event.stream === "command_output"); - expect(commandOutput?.data?.output).toContain("live command output truncated"); - expect(commandOutput?.data?.output).not.toContain("exec denied"); - - expect(onAgentEvent).toHaveBeenCalledWith( - expect.objectContaining({ - stream: "approval", - data: expect.objectContaining({ - phase: "resolved", - status: "denied", - message: expect.stringContaining("blocked by reviewer"), - }), - }), - ); - }); - it("redacts details-only results before emitting the tool result event", async () => { const events: Array<{ stream?: string; data?: Record }> = []; registerAgentEventListener((evt) => { diff --git a/src/agents/pi-embedded-subscribe.handlers.tools.ts b/src/agents/pi-embedded-subscribe.handlers.tools.ts index 0a1135f74e6..3df28deeb87 100644 --- a/src/agents/pi-embedded-subscribe.handlers.tools.ts +++ b/src/agents/pi-embedded-subscribe.handlers.tools.ts @@ -20,7 +20,6 @@ import type { ExecApprovalDecision } from "../infra/exec-approvals.js"; import type { PluginHookAfterToolCallEvent } from "../plugins/types.js"; import { createLazyImportLoader } from "../shared/lazy-promise.js"; import { normalizeOptionalLowercaseString, readStringValue } from "../shared/string-coerce.js"; -import { truncateUtf16Safe } from "../utils.js"; import type { ApplyPatchSummary } from "./apply-patch.js"; import type { ExecToolDetails } from "./bash-tools.exec-types.js"; import { parseExecApprovalResultText } from "./exec-approval-result.js"; @@ -88,50 +87,11 @@ type ToolStartRecord = { /** Track tool execution start data for after_tool_call hook. */ const toolStartData = new Map(); -const EXEC_OUTPUT_DELTA_MIN_INTERVAL_MS = 250; -const LIVE_COMMAND_OUTPUT_MAX_CHARS = 64 * 1024; -type ExecOutputDeltaEmission = { - emittedAt: number; -}; -const execOutputDeltaEmissions = new Map(); function buildToolStartKey(runId: string, toolCallId: string): string { return `${runId}:${toolCallId}`; } -function buildExecOutputDeltaKey(runId: string, toolCallId: string): string { - return `${runId}:${toolCallId}`; -} - -function shouldEmitExecOutputDelta(params: { - runId: string; - toolCallId: string; - output: string; - now?: number; -}): boolean { - const key = buildExecOutputDeltaKey(params.runId, params.toolCallId); - const now = params.now ?? Date.now(); - const previous = execOutputDeltaEmissions.get(key); - if (!previous) { - execOutputDeltaEmissions.set(key, { - emittedAt: now, - }); - return true; - } - const elapsedMs = now - previous.emittedAt; - if (elapsedMs < EXEC_OUTPUT_DELTA_MIN_INTERVAL_MS) { - return false; - } - execOutputDeltaEmissions.set(key, { - emittedAt: now, - }); - return true; -} - -function clearExecOutputDeltaEmission(runId: string, toolCallId: string): void { - execOutputDeltaEmissions.delete(buildExecOutputDeltaKey(runId, toolCallId)); -} - export function countActiveToolExecutions(runId: string): number { const prefix = `${runId}:`; let count = 0; @@ -229,39 +189,6 @@ function readExecToolDetails(result: unknown): ExecToolDetails | null { return details as ExecToolDetails; } -function readExecOutputText(result: unknown): string | undefined { - const details = readToolResultDetailsRecord(result); - if (typeof details?.aggregated === "string") { - return details.aggregated; - } - return extractToolResultText(result); -} - -function limitLiveCommandOutput(output: string): string { - if (output.length <= LIVE_COMMAND_OUTPUT_MAX_CHARS) { - return output; - } - const tail = truncateUtf16Safe( - output.slice(-LIVE_COMMAND_OUTPUT_MAX_CHARS), - LIVE_COMMAND_OUTPUT_MAX_CHARS, - ); - return `[openclaw: live command output truncated to last ${tail.length} of ${output.length} chars]\n${tail}`; -} - -function limitExecToolResultForLiveEvent(result: unknown): unknown { - const details = readToolResultDetailsRecord(result); - if (!details || typeof details.aggregated !== "string") { - return result; - } - return { - ...(result as Record), - details: { - ...details, - aggregated: limitLiveCommandOutput(details.aggregated), - }, - }; -} - function readApplyPatchSummary(result: unknown): ApplyPatchSummary | null { const details = readToolResultDetailsRecord(result); const summary = @@ -814,23 +741,7 @@ export function handleToolExecutionUpdate( const toolName = normalizeToolName(evt.toolName); const toolCallId = evt.toolCallId; const partial = evt.partialResult; - if (isExecToolName(toolName)) { - const output = readExecOutputText(partial); - if ( - output && - !shouldEmitExecOutputDelta({ - runId: ctx.params.runId, - toolCallId, - output, - }) - ) { - return; - } - } const sanitized = sanitizeToolResult(partial); - const liveEventPartial = isExecToolName(toolName) - ? limitExecToolResultForLiveEvent(sanitized) - : sanitized; emitAgentEvent({ runId: ctx.params.runId, stream: "tool", @@ -838,7 +749,7 @@ export function handleToolExecutionUpdate( phase: "update", name: toolName, toolCallId, - partialResult: liveEventPartial, + partialResult: sanitized, }, }); const itemData: AgentItemEventData = { @@ -861,8 +772,11 @@ export function handleToolExecutionUpdate( }, }); if (isExecToolName(toolName)) { - const rawOutput = readExecOutputText(sanitized); - const output = rawOutput ? limitLiveCommandOutput(rawOutput) : undefined; + const execDetails = readExecToolDetails(sanitized); + const output = + execDetails && "aggregated" in execDetails + ? execDetails.aggregated + : extractToolResultText(sanitized); const commandData: AgentItemEventData = { itemId: buildCommandItemId(toolCallId), phase: "update", @@ -915,13 +829,9 @@ export async function handleToolExecutionEnd( const result = evt.result; const isToolError = isError || isToolResultError(result); const sanitizedResult = sanitizeToolResult(result); - const liveEventResult = isExecToolName(toolName) - ? limitExecToolResultForLiveEvent(sanitizedResult) - : sanitizedResult; const toolStartKey = buildToolStartKey(runId, toolCallId); const startData = toolStartData.get(toolStartKey); toolStartData.delete(toolStartKey); - clearExecOutputDeltaEmission(runId, toolCallId); const callSummary = ctx.state.toolMetaById.get(toolCallId); const completedMutatingAction = !isToolError && Boolean(callSummary?.mutatingAction); const meta = callSummary?.meta; @@ -1024,7 +934,7 @@ export async function handleToolExecutionEnd( toolCallId, meta, isError: isToolError, - result: liveEventResult, + result: sanitizedResult, }, }); const endedAt = Date.now(); @@ -1117,11 +1027,10 @@ export async function handleToolExecutionEnd( }), }); } else { - const rawOutput = + const output = execDetails && "aggregated" in execDetails ? execDetails.aggregated : extractToolResultText(sanitizedResult); - const output = rawOutput ? limitLiveCommandOutput(rawOutput) : undefined; const commandStatus = execDetails?.status === "failed" || isToolError ? "failed" : "completed"; emitTrackedItemEvent(ctx, { @@ -1166,8 +1075,8 @@ export async function handleToolExecutionEnd( data: outputData, }); - if (typeof rawOutput === "string") { - const parsedApprovalResult = parseExecApprovalResultText(rawOutput); + if (typeof output === "string") { + const parsedApprovalResult = parseExecApprovalResultText(output); if (parsedApprovalResult.kind === "denied") { const approvalData: AgentApprovalEventData = { phase: "resolved", diff --git a/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts b/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts index 1714aba80be..398e1fc9352 100644 --- a/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts +++ b/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts @@ -11,7 +11,7 @@ import { import { addTestHook, createMockPluginRegistry } from "../plugins/hooks.test-helpers.js"; import { patchPluginSessionExtension } from "../plugins/host-hook-state.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; -import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; import type { PluginHookRegistration } from "../plugins/types.js"; type ToolDefinitionAdapterModule = typeof import("./pi-tool-definition-adapter.js"); @@ -326,7 +326,6 @@ describe("before_tool_call hook deduplication (#15502)", () => { describe("before_tool_call hook integration for client tools", () => { beforeEach(() => { resetGlobalHookRunner(); - resetPluginRuntimeStateForTest(); resetDiagnosticSessionStateForTest(); installBeforeToolCallHook(); }); diff --git a/src/agents/subagent-registry.test.ts b/src/agents/subagent-registry.test.ts index 654f8511c4a..5a263580717 100644 --- a/src/agents/subagent-registry.test.ts +++ b/src/agents/subagent-registry.test.ts @@ -893,14 +893,6 @@ describe("subagent registry seam flow", () => { it("passes stored agentDir through swept context-engine cleanup paths", async () => { const now = Date.parse("2026-03-24T12:00:00Z"); - // Session-mode reaping now honors agents.defaults.subagents.archiveAfterMinutes - // (same knob run-mode uses for archiveAtMs). The default-config mock above sets - // archiveAfterMinutes: 0, which disables session-mode reaping; opt this test - // into a real retention window so the swept-cleanup path still fires. - mocks.getRuntimeConfig.mockReturnValueOnce({ - agents: { defaults: { subagents: { archiveAfterMinutes: 1 } } }, - session: { mainKey: "main", scope: "per-sender" as const }, - }); mod.addSubagentRunForTests({ runId: "run-session-swept-context-engine", childSessionKey: "agent:alt:session:child-session", diff --git a/src/agents/subagent-registry.ts b/src/agents/subagent-registry.ts index a9eb2da494f..e8dcec65420 100644 --- a/src/agents/subagent-registry.ts +++ b/src/agents/subagent-registry.ts @@ -36,7 +36,6 @@ import { reconcileOrphanedRestoredRuns, reconcileOrphanedRun, resolveAnnounceRetryDelayMs, - resolveArchiveAfterMs, resolveSubagentRunOrphanReason, resolveSubagentSessionStatus, safeRemoveAttachmentsDir, @@ -197,6 +196,8 @@ const LIFECYCLE_ERROR_RETRY_GRACE_MS = 15_000; * `timed out` completion right before the eventual success. */ const LIFECYCLE_TIMEOUT_RETRY_GRACE_MS = 15_000; +/** Absolute TTL for session-mode runs after cleanup completes (no archiveAtMs). */ +const SESSION_RUN_TTL_MS = 5 * 60_000; // 5 minutes /** Absolute TTL for orphaned pendingLifecycleError / pendingLifecycleTimeout entries. */ const PENDING_LIFECYCLE_TERMINAL_TTL_MS = 5 * 60_000; // 5 minutes /** Grace period before treating a "running" subagent without a live run context as stale. */ @@ -750,7 +751,6 @@ async function sweepSubagentRuns() { try { const now = Date.now(); const storeCache = new Map>(); - const sessionRetentionMs = resolveArchiveAfterMs(subagentRegistryDeps.getRuntimeConfig()); let mutated = false; for (const [runId, entry] of subagentRuns.entries()) { if (typeof entry.endedAt !== "number") { @@ -813,18 +813,12 @@ async function sweepSubagentRuns() { } } - // Session-mode runs have no archiveAtMs because the child session is retained - // independently — but the registry row itself still needs to be reaped after - // cleanup, otherwise `subagents list` and other registry-backed surfaces grow - // without bound. Honor the same `agents.defaults.subagents.archiveAfterMinutes` - // window run-mode uses for `archiveAtMs`, so operators get one consistent - // retention knob (default 60 minutes; 0 disables session-mode reaping). + // Session-mode runs have no archiveAtMs — apply absolute TTL after cleanup completes. // Use cleanupCompletedAt (not endedAt) to avoid interrupting deferred cleanup flows. if (!entry.archiveAtMs) { if ( - typeof sessionRetentionMs === "number" && typeof entry.cleanupCompletedAt === "number" && - now - entry.cleanupCompletedAt > sessionRetentionMs + now - entry.cleanupCompletedAt > SESSION_RUN_TTL_MS ) { clearPendingLifecycleError(runId); void notifyContextEngineSubagentEnded({ diff --git a/src/agents/system-prompt-params.test.ts b/src/agents/system-prompt-params.test.ts index f72f0e76047..a4215d3a869 100644 --- a/src/agents/system-prompt-params.test.ts +++ b/src/agents/system-prompt-params.test.ts @@ -3,7 +3,6 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { resolveStateDir } from "../config/paths.js"; import { buildSystemPromptParams } from "./system-prompt-params.js"; async function makeTempDir(label: string): Promise { @@ -102,12 +101,4 @@ describe("buildSystemPromptParams repo root", () => { expect(runtimeInfo.repoRoot).toBeUndefined(); }); - - it("includes the default profile canvas root in runtimeInfo", async () => { - const workspaceDir = await makeTempDir("canvas-root"); - - const { runtimeInfo } = buildParams({ workspaceDir }); - - expect(runtimeInfo.canvasRootDir).toBe(path.resolve(path.join(resolveStateDir(), "canvas"))); - }); }); diff --git a/src/agents/system-prompt-params.ts b/src/agents/system-prompt-params.ts index ec5c90b60f0..4e4138b3b17 100644 --- a/src/agents/system-prompt-params.ts +++ b/src/agents/system-prompt-params.ts @@ -1,9 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -import { resolveStateDir } from "../config/paths.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { findGitRoot } from "../infra/git-root.js"; -import { resolveHomeRelativePath } from "../infra/home-dir.js"; import { formatUserTime, resolveUserTimeFormat, @@ -25,7 +23,6 @@ type RuntimeInfoInput = { /** Supported message actions for the current channel (e.g., react, edit, unsend) */ channelActions?: string[]; repoRoot?: string; - canvasRootDir?: string; }; type SystemPromptRuntimeParams = { @@ -50,17 +47,11 @@ export function buildSystemPromptParams(params: { const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone); const userTimeFormat = resolveUserTimeFormat(params.config?.agents?.defaults?.timeFormat); const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat); - const stateDir = resolveStateDir(process.env); - const canvasRootDir = resolveCanvasRootDir({ - config: params.config, - stateDir, - }); return { runtimeInfo: { agentId: params.agentId, ...params.runtime, repoRoot, - canvasRootDir, }, userTimezone, userTime, @@ -68,18 +59,6 @@ export function buildSystemPromptParams(params: { }; } -function resolveCanvasRootDir(params: { config?: OpenClawConfig; stateDir: string }): string { - const configured = params.config?.canvasHost?.root?.trim(); - if (configured) { - return path.resolve( - resolveHomeRelativePath(configured, { - env: process.env, - }), - ); - } - return path.resolve(path.join(params.stateDir, "canvas")); -} - function resolveRepoRoot(params: { config?: OpenClawConfig; workspaceDir?: string; diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 73cef69d7f3..4092ae00130 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -286,7 +286,6 @@ describe("buildAgentSystemPrompt", () => { workspaceDir: "/tmp/openclaw", runtimeInfo: { channel: "webchat", - canvasRootDir: "/Users/example/.openclaw-dev/canvas", }, }); @@ -300,7 +299,7 @@ describe("buildAgentSystemPrompt", () => { "Never use local filesystem paths or `file://...` URLs in `[embed ...]`.", ); expect(prompt).toContain( - "The active hosted embed root for this session is: `/Users/example/.openclaw-dev/canvas`.", + "The active hosted embed root is profile-scoped, not workspace-scoped.", ); expect(prompt).not.toContain('[embed content_type="html" title="Status"]...[/embed]'); }); @@ -1056,7 +1055,6 @@ describe("buildAgentSystemPrompt", () => { runtimeInfo: { channel: "telegram", capabilities: ["inlineButtons"], - canvasRootDir: "/tmp/canvas", }, contextFiles: [ { diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 1078c7057ed..05e19e13f0e 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -378,11 +378,7 @@ function buildAssistantOutputDirectivesSection(isMinimal: boolean) { ]; } -function buildWebchatCanvasSection(params: { - isMinimal: boolean; - runtimeChannel?: string; - canvasRootDir?: string; -}) { +function buildWebchatCanvasSection(params: { isMinimal: boolean; runtimeChannel?: string }) { if (params.isMinimal || params.runtimeChannel !== "webchat") { return []; } @@ -394,9 +390,7 @@ function buildWebchatCanvasSection(params: { '- Use self-closing form for hosted embed documents: `[embed ref="cv_123" title="Status" height="320" /]`.', '- You may also use an explicit hosted URL: `[embed url="/__openclaw__/canvas/documents/cv_123/index.html" title="Status" height="320" /]`.', '- Never use local filesystem paths or `file://...` URLs in `[embed ...]`. Hosted embeds must point at `/__openclaw__/canvas/...` URLs or use `ref="..."`.', - params.canvasRootDir - ? `- The active hosted embed root for this session is: \`${sanitizeForPromptLiteral(params.canvasRootDir)}\`. If you manually stage a hosted embed file, write it there, not in the workspace.` - : "- The active hosted embed root is profile-scoped, not workspace-scoped. If you manually stage a hosted embed file, write it under the active profile embed root, not in the workspace.", + "- The active hosted embed root is profile-scoped, not workspace-scoped. If you manually stage a hosted embed file, write it under the active profile embed root, not in the workspace.", "- Quote all attribute values. Prefer `ref` for hosted documents unless you already have the full `/__openclaw__/canvas/documents//index.html` URL.", "", ]; @@ -603,7 +597,6 @@ export function buildAgentSystemPrompt(params: { channel?: string; capabilities?: string[]; repoRoot?: string; - canvasRootDir?: string; }; messageToolHints?: string[]; sandboxInfo?: EmbeddedSandboxInfo; @@ -1130,7 +1123,6 @@ export function buildAgentSystemPrompt(params: { ...buildWebchatCanvasSection({ isMinimal, runtimeChannel, - canvasRootDir: params.runtimeInfo?.canvasRootDir, }), ...buildMessagingSection({ isMinimal, diff --git a/src/agents/test-helpers/fast-core-tools.ts b/src/agents/test-helpers/fast-core-tools.ts deleted file mode 100644 index 13ccdee3d7d..00000000000 --- a/src/agents/test-helpers/fast-core-tools.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { vi } from "vitest"; -import { stubTool } from "./fast-tool-stubs.js"; - -vi.mock("../tools/canvas-tool.js", () => ({ - createCanvasTool: () => stubTool("canvas"), -})); diff --git a/src/agents/tool-catalog.ts b/src/agents/tool-catalog.ts index c47b0d491b2..dcb9950b3f2 100644 --- a/src/agents/tool-catalog.ts +++ b/src/agents/tool-catalog.ts @@ -208,10 +208,9 @@ const CORE_TOOL_DEFINITIONS: CoreToolDefinition[] = [ { id: "canvas", label: "canvas", - description: "Control canvases", + description: "Control node Canvas surfaces when the Canvas plugin is enabled", sectionId: "ui", profiles: [], - includeInOpenClawGroup: true, }, { id: "message", diff --git a/src/agents/tools/message-tool.test.ts b/src/agents/tools/message-tool.test.ts index 035728dd8c7..a29ead01f02 100644 --- a/src/agents/tools/message-tool.test.ts +++ b/src/agents/tools/message-tool.test.ts @@ -152,9 +152,6 @@ vi.mock("../../channels/plugins/message-tool-api.js", () => ({ vi.mock("./agents-list-tool.js", () => ({ createAgentsListTool: () => openClawToolsFactoryMocks.tool("agents"), })); -vi.mock("./canvas-tool.js", () => ({ - createCanvasTool: () => openClawToolsFactoryMocks.tool("canvas"), -})); vi.mock("./cron-tool.js", () => ({ createCronTool: () => openClawToolsFactoryMocks.tool("cron"), })); diff --git a/src/auto-reply/reply/session.test.ts b/src/auto-reply/reply/session.test.ts index 781a5373fe0..461ebef2f58 100644 --- a/src/auto-reply/reply/session.test.ts +++ b/src/auto-reply/reply/session.test.ts @@ -1407,36 +1407,6 @@ describe("initSessionState reset policy", () => { expect(result.sessionId).not.toBe(existingSessionId); }); - it("rotates sessionFile on daily reset when the stored path still points at the previous session id", async () => { - vi.setSystemTime(new Date(2026, 0, 18, 5, 0, 0)); - const root = await makeCaseDir("openclaw-reset-rotate-session-file-"); - const storePath = path.join(root, "sessions.json"); - const sessionKey = "agent:main:whatsapp:dm:s-rotate"; - const existingSessionId = "daily-rotate-old"; - const oldSessionFile = path.join(root, `${existingSessionId}.jsonl`); - - await writeSessionStoreFast(storePath, { - [sessionKey]: { - sessionId: existingSessionId, - updatedAt: new Date(2026, 0, 18, 3, 0, 0).getTime(), - sessionFile: oldSessionFile, - }, - }); - - const cfg = { session: { store: storePath } } as OpenClawConfig; - const result = await initSessionState({ - ctx: { Body: "hello", SessionKey: sessionKey }, - cfg, - commandAuthorized: true, - }); - - expect(result.isNewSession).toBe(true); - expect(result.sessionId).not.toBe(existingSessionId); - expect(result.sessionEntry.sessionFile).toBeTruthy(); - expect(path.basename(result.sessionEntry.sessionFile ?? "")).toBe(`${result.sessionId}.jsonl`); - expect(result.sessionEntry.sessionFile).not.toBe(oldSessionFile); - }); - it("drains stale system events when idle rollover creates a new session", async () => { vi.setSystemTime(new Date(2026, 0, 18, 5, 30, 0)); const root = await makeCaseDir("openclaw-reset-idle-system-events-"); diff --git a/src/canvas-host/a2ui/.bundle.hash b/src/canvas-host/a2ui/.bundle.hash deleted file mode 100644 index 1701217daf9..00000000000 --- a/src/canvas-host/a2ui/.bundle.hash +++ /dev/null @@ -1 +0,0 @@ -3fae45449ca71bedea4653a04ded2bdd63344f086fb8da0a6ff8800bc4c72bd4 diff --git a/src/canvas-host/a2ui/a2ui.bundle.js b/src/canvas-host/a2ui/a2ui.bundle.js deleted file mode 100644 index 6e5536560ee..00000000000 --- a/src/canvas-host/a2ui/a2ui.bundle.js +++ /dev/null @@ -1,14908 +0,0 @@ -var __defProp$1 = Object.defineProperty; -var __exportAll = (all, no_symbols) => { - let target = {}; - for (var name in all) __defProp$1(target, name, { - get: all[name], - enumerable: true - }); - if (!no_symbols) __defProp$1(target, Symbol.toStringTag, { value: "Module" }); - return target; -}; -/** -* @license -* Copyright 2019 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const t$6 = globalThis, e$13 = t$6.ShadowRoot && (void 0 === t$6.ShadyCSS || t$6.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, s$8 = Symbol(), o$14 = /* @__PURE__ */ new WeakMap(); -var n$12 = class { - constructor(t, e, o) { - if (this._$cssResult$ = !0, o !== s$8) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); - this.cssText = t, this.t = e; - } - get styleSheet() { - let t = this.o; - const s = this.t; - if (e$13 && void 0 === t) { - const e = void 0 !== s && 1 === s.length; - e && (t = o$14.get(s)), void 0 === t && ((this.o = t = new CSSStyleSheet()).replaceSync(this.cssText), e && o$14.set(s, t)); - } - return t; - } - toString() { - return this.cssText; - } -}; -const r$11 = (t) => new n$12("string" == typeof t ? t : t + "", void 0, s$8), i$9 = (t, ...e) => { - return new n$12(1 === t.length ? t[0] : e.reduce((e, s, o) => e + ((t) => { - if (!0 === t._$cssResult$) return t.cssText; - if ("number" == typeof t) return t; - throw Error("Value passed to 'css' function must be a 'css' function result: " + t + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security."); - })(s) + t[o + 1], t[0]), t, s$8); -}, S$1 = (s, o) => { - if (e$13) s.adoptedStyleSheets = o.map((t) => t instanceof CSSStyleSheet ? t : t.styleSheet); - else for (const e of o) { - const o = document.createElement("style"), n = t$6.litNonce; - void 0 !== n && o.setAttribute("nonce", n), o.textContent = e.cssText, s.appendChild(o); - } -}, c$6 = e$13 ? (t) => t : (t) => t instanceof CSSStyleSheet ? ((t) => { - let e = ""; - for (const s of t.cssRules) e += s.cssText; - return r$11(e); -})(t) : t; -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const { is: i$8, defineProperty: e$12, getOwnPropertyDescriptor: h$6, getOwnPropertyNames: r$10, getOwnPropertySymbols: o$13, getPrototypeOf: n$11 } = Object, a$1 = globalThis, c$5 = a$1.trustedTypes, l$4 = c$5 ? c$5.emptyScript : "", p$2 = a$1.reactiveElementPolyfillSupport, d$2 = (t, s) => t, u$3 = { - toAttribute(t, s) { - switch (s) { - case Boolean: - t = t ? l$4 : null; - break; - case Object: - case Array: t = null == t ? t : JSON.stringify(t); - } - return t; - }, - fromAttribute(t, s) { - let i = t; - switch (s) { - case Boolean: - i = null !== t; - break; - case Number: - i = null === t ? null : Number(t); - break; - case Object: - case Array: try { - i = JSON.parse(t); - } catch (t) { - i = null; - } - } - return i; - } -}, f$3 = (t, s) => !i$8(t, s), b$1 = { - attribute: !0, - type: String, - converter: u$3, - reflect: !1, - useDefault: !1, - hasChanged: f$3 -}; -Symbol.metadata ??= Symbol("metadata"), a$1.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap(); -var y$1 = class extends HTMLElement { - static addInitializer(t) { - this._$Ei(), (this.l ??= []).push(t); - } - static get observedAttributes() { - return this.finalize(), this._$Eh && [...this._$Eh.keys()]; - } - static createProperty(t, s = b$1) { - if (s.state && (s.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(t) && ((s = Object.create(s)).wrapped = !0), this.elementProperties.set(t, s), !s.noAccessor) { - const i = Symbol(), h = this.getPropertyDescriptor(t, i, s); - void 0 !== h && e$12(this.prototype, t, h); - } - } - static getPropertyDescriptor(t, s, i) { - const { get: e, set: r } = h$6(this.prototype, t) ?? { - get() { - return this[s]; - }, - set(t) { - this[s] = t; - } - }; - return { - get: e, - set(s) { - const h = e?.call(this); - r?.call(this, s), this.requestUpdate(t, h, i); - }, - configurable: !0, - enumerable: !0 - }; - } - static getPropertyOptions(t) { - return this.elementProperties.get(t) ?? b$1; - } - static _$Ei() { - if (this.hasOwnProperty(d$2("elementProperties"))) return; - const t = n$11(this); - t.finalize(), void 0 !== t.l && (this.l = [...t.l]), this.elementProperties = new Map(t.elementProperties); - } - static finalize() { - if (this.hasOwnProperty(d$2("finalized"))) return; - if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(d$2("properties"))) { - const t = this.properties, s = [...r$10(t), ...o$13(t)]; - for (const i of s) this.createProperty(i, t[i]); - } - const t = this[Symbol.metadata]; - if (null !== t) { - const s = litPropertyMetadata.get(t); - if (void 0 !== s) for (const [t, i] of s) this.elementProperties.set(t, i); - } - this._$Eh = /* @__PURE__ */ new Map(); - for (const [t, s] of this.elementProperties) { - const i = this._$Eu(t, s); - void 0 !== i && this._$Eh.set(i, t); - } - this.elementStyles = this.finalizeStyles(this.styles); - } - static finalizeStyles(s) { - const i = []; - if (Array.isArray(s)) { - const e = new Set(s.flat(Infinity).reverse()); - for (const s of e) i.unshift(c$6(s)); - } else void 0 !== s && i.push(c$6(s)); - return i; - } - static _$Eu(t, s) { - const i = s.attribute; - return !1 === i ? void 0 : "string" == typeof i ? i : "string" == typeof t ? t.toLowerCase() : void 0; - } - constructor() { - super(), this._$Ep = void 0, this.isUpdatePending = !1, this.hasUpdated = !1, this._$Em = null, this._$Ev(); - } - _$Ev() { - this._$ES = new Promise((t) => this.enableUpdating = t), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t) => t(this)); - } - addController(t) { - (this._$EO ??= /* @__PURE__ */ new Set()).add(t), void 0 !== this.renderRoot && this.isConnected && t.hostConnected?.(); - } - removeController(t) { - this._$EO?.delete(t); - } - _$E_() { - const t = /* @__PURE__ */ new Map(), s = this.constructor.elementProperties; - for (const i of s.keys()) this.hasOwnProperty(i) && (t.set(i, this[i]), delete this[i]); - t.size > 0 && (this._$Ep = t); - } - createRenderRoot() { - const t = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); - return S$1(t, this.constructor.elementStyles), t; - } - connectedCallback() { - this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(!0), this._$EO?.forEach((t) => t.hostConnected?.()); - } - enableUpdating(t) {} - disconnectedCallback() { - this._$EO?.forEach((t) => t.hostDisconnected?.()); - } - attributeChangedCallback(t, s, i) { - this._$AK(t, i); - } - _$ET(t, s) { - const i = this.constructor.elementProperties.get(t), e = this.constructor._$Eu(t, i); - if (void 0 !== e && !0 === i.reflect) { - const h = (void 0 !== i.converter?.toAttribute ? i.converter : u$3).toAttribute(s, i.type); - this._$Em = t, null == h ? this.removeAttribute(e) : this.setAttribute(e, h), this._$Em = null; - } - } - _$AK(t, s) { - const i = this.constructor, e = i._$Eh.get(t); - if (void 0 !== e && this._$Em !== e) { - const t = i.getPropertyOptions(e), h = "function" == typeof t.converter ? { fromAttribute: t.converter } : void 0 !== t.converter?.fromAttribute ? t.converter : u$3; - this._$Em = e; - const r = h.fromAttribute(s, t.type); - this[e] = r ?? this._$Ej?.get(e) ?? r, this._$Em = null; - } - } - requestUpdate(t, s, i, e = !1, h) { - if (void 0 !== t) { - const r = this.constructor; - if (!1 === e && (h = this[t]), i ??= r.getPropertyOptions(t), !((i.hasChanged ?? f$3)(h, s) || i.useDefault && i.reflect && h === this._$Ej?.get(t) && !this.hasAttribute(r._$Eu(t, i)))) return; - this.C(t, s, i); - } - !1 === this.isUpdatePending && (this._$ES = this._$EP()); - } - C(t, s, { useDefault: i, reflect: e, wrapped: h }, r) { - i && !(this._$Ej ??= /* @__PURE__ */ new Map()).has(t) && (this._$Ej.set(t, r ?? s ?? this[t]), !0 !== h || void 0 !== r) || (this._$AL.has(t) || (this.hasUpdated || i || (s = void 0), this._$AL.set(t, s)), !0 === e && this._$Em !== t && (this._$Eq ??= /* @__PURE__ */ new Set()).add(t)); - } - async _$EP() { - this.isUpdatePending = !0; - try { - await this._$ES; - } catch (t) { - Promise.reject(t); - } - const t = this.scheduleUpdate(); - return null != t && await t, !this.isUpdatePending; - } - scheduleUpdate() { - return this.performUpdate(); - } - performUpdate() { - if (!this.isUpdatePending) return; - if (!this.hasUpdated) { - if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) { - for (const [t, s] of this._$Ep) this[t] = s; - this._$Ep = void 0; - } - const t = this.constructor.elementProperties; - if (t.size > 0) for (const [s, i] of t) { - const { wrapped: t } = i, e = this[s]; - !0 !== t || this._$AL.has(s) || void 0 === e || this.C(s, void 0, i, e); - } - } - let t = !1; - const s = this._$AL; - try { - t = this.shouldUpdate(s), t ? (this.willUpdate(s), this._$EO?.forEach((t) => t.hostUpdate?.()), this.update(s)) : this._$EM(); - } catch (s) { - throw t = !1, this._$EM(), s; - } - t && this._$AE(s); - } - willUpdate(t) {} - _$AE(t) { - this._$EO?.forEach((t) => t.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = !0, this.firstUpdated(t)), this.updated(t); - } - _$EM() { - this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = !1; - } - get updateComplete() { - return this.getUpdateComplete(); - } - getUpdateComplete() { - return this._$ES; - } - shouldUpdate(t) { - return !0; - } - update(t) { - this._$Eq &&= this._$Eq.forEach((t) => this._$ET(t, this[t])), this._$EM(); - } - updated(t) {} - firstUpdated(t) {} -}; -y$1.elementStyles = [], y$1.shadowRootOptions = { mode: "open" }, y$1[d$2("elementProperties")] = /* @__PURE__ */ new Map(), y$1[d$2("finalized")] = /* @__PURE__ */ new Map(), p$2?.({ ReactiveElement: y$1 }), (a$1.reactiveElementVersions ??= []).push("2.1.2"); -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const t$5 = globalThis, i$7 = (t) => t, s$7 = t$5.trustedTypes, e$11 = s$7 ? s$7.createPolicy("lit-html", { createHTML: (t) => t }) : void 0, h$5 = "$lit$", o$12 = `lit$${Math.random().toFixed(9).slice(2)}$`, n$10 = "?" + o$12, r$9 = `<${n$10}>`, l$3 = document, c$4 = () => l$3.createComment(""), a = (t) => null === t || "object" != typeof t && "function" != typeof t, u$2 = Array.isArray, d$1 = (t) => u$2(t) || "function" == typeof t?.[Symbol.iterator], f$2 = "[ \n\f\r]", v$1 = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, _ = /-->/g, m$2 = />/g, p$1 = RegExp(`>|${f$2}(?:([^\\s"'>=/]+)(${f$2}*=${f$2}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`, "g"), g = /'/g, $ = /"/g, y = /^(?:script|style|textarea|title)$/i, x = (t) => (i, ...s) => ({ - _$litType$: t, - strings: i, - values: s -}), b = x(1), w = x(2); -x(3); -const E = Symbol.for("lit-noChange"), A = Symbol.for("lit-nothing"), C = /* @__PURE__ */ new WeakMap(), P = l$3.createTreeWalker(l$3, 129); -function V(t, i) { - if (!u$2(t) || !t.hasOwnProperty("raw")) throw Error("invalid template strings array"); - return void 0 !== e$11 ? e$11.createHTML(i) : i; -} -const N = (t, i) => { - const s = t.length - 1, e = []; - let n, l = 2 === i ? "" : 3 === i ? "" : "", c = v$1; - for (let i = 0; i < s; i++) { - const s = t[i]; - let a, u, d = -1, f = 0; - for (; f < s.length && (c.lastIndex = f, u = c.exec(s), null !== u);) f = c.lastIndex, c === v$1 ? "!--" === u[1] ? c = _ : void 0 !== u[1] ? c = m$2 : void 0 !== u[2] ? (y.test(u[2]) && (n = RegExp("" === u[0] ? (c = n ?? v$1, d = -1) : void 0 === u[1] ? d = -2 : (d = c.lastIndex - u[2].length, a = u[1], c = void 0 === u[3] ? p$1 : "\"" === u[3] ? $ : g) : c === $ || c === g ? c = p$1 : c === _ || c === m$2 ? c = v$1 : (c = p$1, n = void 0); - const x = c === p$1 && t[i + 1].startsWith("/>") ? " " : ""; - l += c === v$1 ? s + r$9 : d >= 0 ? (e.push(a), s.slice(0, d) + h$5 + s.slice(d) + o$12 + x) : s + o$12 + (-2 === d ? i : x); - } - return [V(t, l + (t[s] || "") + (2 === i ? "" : 3 === i ? "" : "")), e]; -}; -var S = class S { - constructor({ strings: t, _$litType$: i }, e) { - let r; - this.parts = []; - let l = 0, a = 0; - const u = t.length - 1, d = this.parts, [f, v] = N(t, i); - if (this.el = S.createElement(f, e), P.currentNode = this.el.content, 2 === i || 3 === i) { - const t = this.el.content.firstChild; - t.replaceWith(...t.childNodes); - } - for (; null !== (r = P.nextNode()) && d.length < u;) { - if (1 === r.nodeType) { - if (r.hasAttributes()) for (const t of r.getAttributeNames()) if (t.endsWith(h$5)) { - const i = v[a++], s = r.getAttribute(t).split(o$12), e = /([.?@])?(.*)/.exec(i); - d.push({ - type: 1, - index: l, - name: e[2], - strings: s, - ctor: "." === e[1] ? I : "?" === e[1] ? L : "@" === e[1] ? z : H - }), r.removeAttribute(t); - } else t.startsWith(o$12) && (d.push({ - type: 6, - index: l - }), r.removeAttribute(t)); - if (y.test(r.tagName)) { - const t = r.textContent.split(o$12), i = t.length - 1; - if (i > 0) { - r.textContent = s$7 ? s$7.emptyScript : ""; - for (let s = 0; s < i; s++) r.append(t[s], c$4()), P.nextNode(), d.push({ - type: 2, - index: ++l - }); - r.append(t[i], c$4()); - } - } - } else if (8 === r.nodeType) if (r.data === n$10) d.push({ - type: 2, - index: l - }); - else { - let t = -1; - for (; -1 !== (t = r.data.indexOf(o$12, t + 1));) d.push({ - type: 7, - index: l - }), t += o$12.length - 1; - } - l++; - } - } - static createElement(t, i) { - const s = l$3.createElement("template"); - return s.innerHTML = t, s; - } -}; -function M$1(t, i, s = t, e) { - if (i === E) return i; - let h = void 0 !== e ? s._$Co?.[e] : s._$Cl; - const o = a(i) ? void 0 : i._$litDirective$; - return h?.constructor !== o && (h?._$AO?.(!1), void 0 === o ? h = void 0 : (h = new o(t), h._$AT(t, s, e)), void 0 !== e ? (s._$Co ??= [])[e] = h : s._$Cl = h), void 0 !== h && (i = M$1(t, h._$AS(t, i.values), h, e)), i; -} -var R = class { - constructor(t, i) { - this._$AV = [], this._$AN = void 0, this._$AD = t, this._$AM = i; - } - get parentNode() { - return this._$AM.parentNode; - } - get _$AU() { - return this._$AM._$AU; - } - u(t) { - const { el: { content: i }, parts: s } = this._$AD, e = (t?.creationScope ?? l$3).importNode(i, !0); - P.currentNode = e; - let h = P.nextNode(), o = 0, n = 0, r = s[0]; - for (; void 0 !== r;) { - if (o === r.index) { - let i; - 2 === r.type ? i = new k(h, h.nextSibling, this, t) : 1 === r.type ? i = new r.ctor(h, r.name, r.strings, this, t) : 6 === r.type && (i = new Z(h, this, t)), this._$AV.push(i), r = s[++n]; - } - o !== r?.index && (h = P.nextNode(), o++); - } - return P.currentNode = l$3, e; - } - p(t) { - let i = 0; - for (const s of this._$AV) void 0 !== s && (void 0 !== s.strings ? (s._$AI(t, s, i), i += s.strings.length - 2) : s._$AI(t[i])), i++; - } -}; -var k = class k { - get _$AU() { - return this._$AM?._$AU ?? this._$Cv; - } - constructor(t, i, s, e) { - this.type = 2, this._$AH = A, this._$AN = void 0, this._$AA = t, this._$AB = i, this._$AM = s, this.options = e, this._$Cv = e?.isConnected ?? !0; - } - get parentNode() { - let t = this._$AA.parentNode; - const i = this._$AM; - return void 0 !== i && 11 === t?.nodeType && (t = i.parentNode), t; - } - get startNode() { - return this._$AA; - } - get endNode() { - return this._$AB; - } - _$AI(t, i = this) { - t = M$1(this, t, i), a(t) ? t === A || null == t || "" === t ? (this._$AH !== A && this._$AR(), this._$AH = A) : t !== this._$AH && t !== E && this._(t) : void 0 !== t._$litType$ ? this.$(t) : void 0 !== t.nodeType ? this.T(t) : d$1(t) ? this.k(t) : this._(t); - } - O(t) { - return this._$AA.parentNode.insertBefore(t, this._$AB); - } - T(t) { - this._$AH !== t && (this._$AR(), this._$AH = this.O(t)); - } - _(t) { - this._$AH !== A && a(this._$AH) ? this._$AA.nextSibling.data = t : this.T(l$3.createTextNode(t)), this._$AH = t; - } - $(t) { - const { values: i, _$litType$: s } = t, e = "number" == typeof s ? this._$AC(t) : (void 0 === s.el && (s.el = S.createElement(V(s.h, s.h[0]), this.options)), s); - if (this._$AH?._$AD === e) this._$AH.p(i); - else { - const t = new R(e, this), s = t.u(this.options); - t.p(i), this.T(s), this._$AH = t; - } - } - _$AC(t) { - let i = C.get(t.strings); - return void 0 === i && C.set(t.strings, i = new S(t)), i; - } - k(t) { - u$2(this._$AH) || (this._$AH = [], this._$AR()); - const i = this._$AH; - let s, e = 0; - for (const h of t) e === i.length ? i.push(s = new k(this.O(c$4()), this.O(c$4()), this, this.options)) : s = i[e], s._$AI(h), e++; - e < i.length && (this._$AR(s && s._$AB.nextSibling, e), i.length = e); - } - _$AR(t = this._$AA.nextSibling, s) { - for (this._$AP?.(!1, !0, s); t !== this._$AB;) { - const s = i$7(t).nextSibling; - i$7(t).remove(), t = s; - } - } - setConnected(t) { - void 0 === this._$AM && (this._$Cv = t, this._$AP?.(t)); - } -}; -var H = class { - get tagName() { - return this.element.tagName; - } - get _$AU() { - return this._$AM._$AU; - } - constructor(t, i, s, e, h) { - this.type = 1, this._$AH = A, this._$AN = void 0, this.element = t, this.name = i, this._$AM = e, this.options = h, s.length > 2 || "" !== s[0] || "" !== s[1] ? (this._$AH = Array(s.length - 1).fill(/* @__PURE__ */ new String()), this.strings = s) : this._$AH = A; - } - _$AI(t, i = this, s, e) { - const h = this.strings; - let o = !1; - if (void 0 === h) t = M$1(this, t, i, 0), o = !a(t) || t !== this._$AH && t !== E, o && (this._$AH = t); - else { - const e = t; - let n, r; - for (t = h[0], n = 0; n < h.length - 1; n++) r = M$1(this, e[s + n], i, n), r === E && (r = this._$AH[n]), o ||= !a(r) || r !== this._$AH[n], r === A ? t = A : t !== A && (t += (r ?? "") + h[n + 1]), this._$AH[n] = r; - } - o && !e && this.j(t); - } - j(t) { - t === A ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t ?? ""); - } -}; -var I = class extends H { - constructor() { - super(...arguments), this.type = 3; - } - j(t) { - this.element[this.name] = t === A ? void 0 : t; - } -}; -var L = class extends H { - constructor() { - super(...arguments), this.type = 4; - } - j(t) { - this.element.toggleAttribute(this.name, !!t && t !== A); - } -}; -var z = class extends H { - constructor(t, i, s, e, h) { - super(t, i, s, e, h), this.type = 5; - } - _$AI(t, i = this) { - if ((t = M$1(this, t, i, 0) ?? A) === E) return; - const s = this._$AH, e = t === A && s !== A || t.capture !== s.capture || t.once !== s.once || t.passive !== s.passive, h = t !== A && (s === A || e); - e && this.element.removeEventListener(this.name, this, s), h && this.element.addEventListener(this.name, this, t), this._$AH = t; - } - handleEvent(t) { - "function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t) : this._$AH.handleEvent(t); - } -}; -var Z = class { - constructor(t, i, s) { - this.element = t, this.type = 6, this._$AN = void 0, this._$AM = i, this.options = s; - } - get _$AU() { - return this._$AM._$AU; - } - _$AI(t) { - M$1(this, t); - } -}; -const j$1 = { - M: h$5, - P: o$12, - A: n$10, - C: 1, - L: N, - R, - D: d$1, - V: M$1, - I: k, - H, - N: L, - U: z, - B: I, - F: Z -}, B = t$5.litHtmlPolyfillSupport; -B?.(S, k), (t$5.litHtmlVersions ??= []).push("3.3.2"); -const D = (t, i, s) => { - const e = s?.renderBefore ?? i; - let h = e._$litPart$; - if (void 0 === h) { - const t = s?.renderBefore ?? null; - e._$litPart$ = h = new k(i.insertBefore(c$4(), t), t, void 0, s ?? {}); - } - return h._$AI(t), h; -}; -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const s$6 = globalThis; -var i$6 = class extends y$1 { - constructor() { - super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; - } - createRenderRoot() { - const t = super.createRenderRoot(); - return this.renderOptions.renderBefore ??= t.firstChild, t; - } - update(t) { - const r = this.render(); - this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t), this._$Do = D(r, this.renderRoot, this.renderOptions); - } - connectedCallback() { - super.connectedCallback(), this._$Do?.setConnected(!0); - } - disconnectedCallback() { - super.disconnectedCallback(), this._$Do?.setConnected(!1); - } - render() { - return E; - } -}; -i$6._$litElement$ = !0, i$6["finalized"] = !0, s$6.litElementHydrateSupport?.({ LitElement: i$6 }); -const o$11 = s$6.litElementPolyfillSupport; -o$11?.({ LitElement: i$6 }); -(s$6.litElementVersions ??= []).push("4.2.2"); -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const t$4 = { - ATTRIBUTE: 1, - CHILD: 2, - PROPERTY: 3, - BOOLEAN_ATTRIBUTE: 4, - EVENT: 5, - ELEMENT: 6 -}, e$10 = (t) => (...e) => ({ - _$litDirective$: t, - values: e -}); -var i$5 = class { - constructor(t) {} - get _$AU() { - return this._$AM._$AU; - } - _$AT(t, e, i) { - this._$Ct = t, this._$AM = e, this._$Ci = i; - } - _$AS(t, e) { - return this.update(t, e); - } - update(t, e) { - return this.render(...e); - } -}; -/** -* @license -* Copyright 2020 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const { I: t$3 } = j$1, i$4 = (o) => o, r$8 = (o) => void 0 === o.strings, s$5 = () => document.createComment(""), v = (o, n, e) => { - const l = o._$AA.parentNode, d = void 0 === n ? o._$AB : n._$AA; - if (void 0 === e) e = new t$3(l.insertBefore(s$5(), d), l.insertBefore(s$5(), d), o, o.options); - else { - const t = e._$AB.nextSibling, n = e._$AM, c = n !== o; - if (c) { - let t; - e._$AQ?.(o), e._$AM = o, void 0 !== e._$AP && (t = o._$AU) !== n._$AU && e._$AP(t); - } - if (t !== d || c) { - let o = e._$AA; - for (; o !== t;) { - const t = i$4(o).nextSibling; - i$4(l).insertBefore(o, d), o = t; - } - } - } - return e; -}, u$1 = (o, t, i = o) => (o._$AI(t, i), o), m$1 = {}, p = (o, t = m$1) => o._$AH = t, M = (o) => o._$AH, h$4 = (o) => { - o._$AR(), o._$AA.remove(); -}; -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const u = (e, s, t) => { - const r = /* @__PURE__ */ new Map(); - for (let l = s; l <= t; l++) r.set(e[l], l); - return r; -}, c$2 = e$10(class extends i$5 { - constructor(e) { - if (super(e), e.type !== t$4.CHILD) throw Error("repeat() can only be used in text expressions"); - } - dt(e, s, t) { - let r; - void 0 === t ? t = s : void 0 !== s && (r = s); - const l = [], o = []; - let i = 0; - for (const s of e) l[i] = r ? r(s, i) : i, o[i] = t(s, i), i++; - return { - values: o, - keys: l - }; - } - render(e, s, t) { - return this.dt(e, s, t).values; - } - update(s, [t, r, c]) { - const d = M(s), { values: p$3, keys: a } = this.dt(t, r, c); - if (!Array.isArray(d)) return this.ut = a, p$3; - const h = this.ut ??= [], v$2 = []; - let m, y, x = 0, j = d.length - 1, k = 0, w = p$3.length - 1; - for (; x <= j && k <= w;) if (null === d[x]) x++; - else if (null === d[j]) j--; - else if (h[x] === a[k]) v$2[k] = u$1(d[x], p$3[k]), x++, k++; - else if (h[j] === a[w]) v$2[w] = u$1(d[j], p$3[w]), j--, w--; - else if (h[x] === a[w]) v$2[w] = u$1(d[x], p$3[w]), v(s, v$2[w + 1], d[x]), x++, w--; - else if (h[j] === a[k]) v$2[k] = u$1(d[j], p$3[k]), v(s, d[x], d[j]), j--, k++; - else if (void 0 === m && (m = u(a, k, w), y = u(h, x, j)), m.has(h[x])) if (m.has(h[j])) { - const e = y.get(a[k]), t = void 0 !== e ? d[e] : null; - if (null === t) { - const e = v(s, d[x]); - u$1(e, p$3[k]), v$2[k] = e; - } else v$2[k] = u$1(t, p$3[k]), v(s, d[x], t), d[e] = null; - k++; - } else h$4(d[j]), j--; - else h$4(d[x]), x++; - for (; k <= w;) { - const e = v(s, v$2[w + 1]); - u$1(e, p$3[k]), v$2[k++] = e; - } - for (; x <= j;) { - const e = d[x++]; - null !== e && h$4(e); - } - return this.ut = a, p(s, v$2), E; - } -}); -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -var s$4 = class extends Event { - constructor(s, t, e, o) { - super("context-request", { - bubbles: !0, - composed: !0 - }), this.context = s, this.contextTarget = t, this.callback = e, this.subscribe = o ?? !1; - } -}; -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -function n$7(n) { - return n; -} -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ var s$3 = class { - constructor(t, s, i, h) { - if (this.subscribe = !1, this.provided = !1, this.value = void 0, this.t = (t, s) => { - this.unsubscribe && (this.unsubscribe !== s && (this.provided = !1, this.unsubscribe()), this.subscribe || this.unsubscribe()), this.value = t, this.host.requestUpdate(), this.provided && !this.subscribe || (this.provided = !0, this.callback && this.callback(t, s)), this.unsubscribe = s; - }, this.host = t, void 0 !== s.context) { - const t = s; - this.context = t.context, this.callback = t.callback, this.subscribe = t.subscribe ?? !1; - } else this.context = s, this.callback = i, this.subscribe = h ?? !1; - this.host.addController(this); - } - hostConnected() { - this.dispatchRequest(); - } - hostDisconnected() { - this.unsubscribe && (this.unsubscribe(), this.unsubscribe = void 0); - } - dispatchRequest() { - this.host.dispatchEvent(new s$4(this.context, this.host, this.t, this.subscribe)); - } -}; -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -var s$2 = class { - get value() { - return this.o; - } - set value(s) { - this.setValue(s); - } - setValue(s, t = !1) { - const i = t || !Object.is(s, this.o); - this.o = s, i && this.updateObservers(); - } - constructor(s) { - this.subscriptions = /* @__PURE__ */ new Map(), this.updateObservers = () => { - for (const [s, { disposer: t }] of this.subscriptions) s(this.o, t); - }, void 0 !== s && (this.value = s); - } - addCallback(s, t, i) { - if (!i) return void s(this.value); - this.subscriptions.has(s) || this.subscriptions.set(s, { - disposer: () => { - this.subscriptions.delete(s); - }, - consumerHost: t - }); - const { disposer: h } = this.subscriptions.get(s); - s(this.value, h); - } - clearCallbacks() { - this.subscriptions.clear(); - } -}; -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ var e$8 = class extends Event { - constructor(t, s) { - super("context-provider", { - bubbles: !0, - composed: !0 - }), this.context = t, this.contextTarget = s; - } -}; -var i$3 = class extends s$2 { - constructor(s, e, i) { - super(void 0 !== e.context ? e.initialValue : i), this.onContextRequest = (t) => { - if (t.context !== this.context) return; - const s = t.contextTarget ?? t.composedPath()[0]; - s !== this.host && (t.stopPropagation(), this.addCallback(t.callback, s, t.subscribe)); - }, this.onProviderRequest = (s) => { - if (s.context !== this.context) return; - if ((s.contextTarget ?? s.composedPath()[0]) === this.host) return; - const e = /* @__PURE__ */ new Set(); - for (const [s, { consumerHost: i }] of this.subscriptions) e.has(s) || (e.add(s), i.dispatchEvent(new s$4(this.context, i, s, !0))); - s.stopPropagation(); - }, this.host = s, void 0 !== e.context ? this.context = e.context : this.context = e, this.attachListeners(), this.host.addController?.(this); - } - attachListeners() { - this.host.addEventListener("context-request", this.onContextRequest), this.host.addEventListener("context-provider", this.onProviderRequest); - } - hostConnected() { - this.host.dispatchEvent(new e$8(this.context, this.host)); - } -}; -/** -* @license -* Copyright 2022 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function c$1({ context: c, subscribe: e }) { - return (o, n) => { - "object" == typeof n ? n.addInitializer((function() { - new s$3(this, { - context: c, - callback: (t) => { - o.set.call(this, t); - }, - subscribe: e - }); - })) : o.constructor.addInitializer(((o) => { - new s$3(o, { - context: c, - callback: (t) => { - o[n] = t; - }, - subscribe: e - }); - })); - }; -} -const eventInit = { - bubbles: true, - cancelable: true, - composed: true -}; -var StateEvent = class StateEvent extends CustomEvent { - static { - this.eventName = "a2uiaction"; - } - constructor(payload) { - super(StateEvent.eventName, { - detail: payload, - ...eventInit - }); - this.payload = payload; - } -}; -const opacityBehavior = ` - &:not([disabled]) { - cursor: pointer; - opacity: var(--opacity, 0); - transition: opacity var(--speed, 0.2s) cubic-bezier(0, 0, 0.3, 1); - - &:hover, - &:focus { - opacity: 1; - } - }`; -const behavior = ` - ${new Array(21).fill(0).map((_, idx) => { - return `.behavior-ho-${idx * 5} { - --opacity: ${idx / 20}; - ${opacityBehavior} - }`; -}).join("\n")} - - .behavior-o-s { - overflow: scroll; - } - - .behavior-o-a { - overflow: auto; - } - - .behavior-o-h { - overflow: hidden; - } - - .behavior-sw-n { - scrollbar-width: none; - } -`; -const border = ` - ${new Array(25).fill(0).map((_, idx) => { - return ` - .border-bw-${idx} { border-width: ${idx}px; } - .border-btw-${idx} { border-top-width: ${idx}px; } - .border-bbw-${idx} { border-bottom-width: ${idx}px; } - .border-blw-${idx} { border-left-width: ${idx}px; } - .border-brw-${idx} { border-right-width: ${idx}px; } - - .border-ow-${idx} { outline-width: ${idx}px; } - .border-br-${idx} { border-radius: ${idx * 4}px; overflow: hidden;}`; -}).join("\n")} - - .border-br-50pc { - border-radius: 50%; - } - - .border-bs-s { - border-style: solid; - } -`; -const shades = [ - 0, - 5, - 10, - 15, - 20, - 25, - 30, - 35, - 40, - 50, - 60, - 70, - 80, - 90, - 95, - 98, - 99, - 100 -]; -function merge(...classes) { - const styles = {}; - for (const clazz of classes) for (const [key, val] of Object.entries(clazz)) { - const prefix = key.split("-").with(-1, "").join("-"); - const existingKeys = Object.keys(styles).filter((key) => key.startsWith(prefix)); - for (const existingKey of existingKeys) delete styles[existingKey]; - styles[key] = val; - } - return styles; -} -function appendToAll(target, exclusions, ...classes) { - const updatedTarget = structuredClone(target); - for (const clazz of classes) for (const key of Object.keys(clazz)) { - const prefix = key.split("-").with(-1, "").join("-"); - for (const [tagName, classesToAdd] of Object.entries(updatedTarget)) { - if (exclusions.includes(tagName)) continue; - let found = false; - for (let t = 0; t < classesToAdd.length; t++) if (classesToAdd[t].startsWith(prefix)) { - found = true; - classesToAdd[t] = key; - } - if (!found) classesToAdd.push(key); - } - } - return updatedTarget; -} -function toProp(key) { - if (key.startsWith("nv")) return `--nv-${key.slice(2)}`; - return `--${key[0]}-${key.slice(1)}`; -} -const color = (src) => ` - ${src.map((key) => { - const inverseKey = getInverseKey(key); - return `.color-bc-${key} { border-color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`; -}).join("\n")} - - ${src.map((key) => { - const inverseKey = getInverseKey(key); - const vals = [`.color-bgc-${key} { background-color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`, `.color-bbgc-${key}::backdrop { background-color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`]; - for (let o = .1; o < 1; o += .1) vals.push(`.color-bbgc-${key}_${(o * 100).toFixed(0)}::backdrop { - background-color: light-dark(oklch(from var(${toProp(key)}) l c h / calc(alpha * ${o.toFixed(1)})), oklch(from var(${toProp(inverseKey)}) l c h / calc(alpha * ${o.toFixed(1)})) ); - } - `); - return vals.join("\n"); -}).join("\n")} - - ${src.map((key) => { - const inverseKey = getInverseKey(key); - return `.color-c-${key} { color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`; -}).join("\n")} - `; -const getInverseKey = (key) => { - const match = key.match(/^([a-z]+)(\d+)$/); - if (!match) return key; - const [, prefix, shadeStr] = match; - const target = 100 - parseInt(shadeStr, 10); - return `${prefix}${shades.reduce((prev, curr) => Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev)}`; -}; -const keyFactory = (prefix) => { - return shades.map((v) => `${prefix}${v}`); -}; -const structuralStyles$1 = [ - behavior, - border, - [ - color(keyFactory("p")), - color(keyFactory("s")), - color(keyFactory("t")), - color(keyFactory("n")), - color(keyFactory("nv")), - color(keyFactory("e")), - ` - .color-bgc-transparent { - background-color: transparent; - } - - :host { - color-scheme: var(--color-scheme); - } - ` - ], - ` - .g-icon { - font-family: "Material Symbols Outlined", "Google Symbols"; - font-weight: normal; - font-style: normal; - font-display: optional; - font-size: 20px; - width: 1em; - height: 1em; - user-select: none; - line-height: 1; - letter-spacing: normal; - text-transform: none; - display: inline-block; - white-space: nowrap; - word-wrap: normal; - direction: ltr; - -webkit-font-feature-settings: "liga"; - -webkit-font-smoothing: antialiased; - overflow: hidden; - - font-variation-settings: "FILL" 0, "wght" 300, "GRAD" 0, "opsz" 48, - "ROND" 100; - - &.filled { - font-variation-settings: "FILL" 1, "wght" 300, "GRAD" 0, "opsz" 48, - "ROND" 100; - } - - &.filled-heavy { - font-variation-settings: "FILL" 1, "wght" 700, "GRAD" 0, "opsz" 48, - "ROND" 100; - } - } -`, - ` - :host { - ${new Array(16).fill(0).map((_, idx) => { - return `--g-${idx + 1}: ${(idx + 1) * 4}px;`; - }).join("\n")} - } - - ${new Array(49).fill(0).map((_, index) => { - const idx = index - 24; - const lbl = idx < 0 ? `n${Math.abs(idx)}` : idx.toString(); - return ` - .layout-p-${lbl} { --padding: ${idx * 4}px; padding: var(--padding); } - .layout-pt-${lbl} { padding-top: ${idx * 4}px; } - .layout-pr-${lbl} { padding-right: ${idx * 4}px; } - .layout-pb-${lbl} { padding-bottom: ${idx * 4}px; } - .layout-pl-${lbl} { padding-left: ${idx * 4}px; } - - .layout-m-${lbl} { --margin: ${idx * 4}px; margin: var(--margin); } - .layout-mt-${lbl} { margin-top: ${idx * 4}px; } - .layout-mr-${lbl} { margin-right: ${idx * 4}px; } - .layout-mb-${lbl} { margin-bottom: ${idx * 4}px; } - .layout-ml-${lbl} { margin-left: ${idx * 4}px; } - - .layout-t-${lbl} { top: ${idx * 4}px; } - .layout-r-${lbl} { right: ${idx * 4}px; } - .layout-b-${lbl} { bottom: ${idx * 4}px; } - .layout-l-${lbl} { left: ${idx * 4}px; }`; - }).join("\n")} - - ${new Array(25).fill(0).map((_, idx) => { - return ` - .layout-g-${idx} { gap: ${idx * 4}px; }`; - }).join("\n")} - - ${new Array(8).fill(0).map((_, idx) => { - return ` - .layout-grd-col${idx + 1} { grid-template-columns: ${"1fr ".repeat(idx + 1).trim()}; }`; - }).join("\n")} - - .layout-pos-a { - position: absolute; - } - - .layout-pos-rel { - position: relative; - } - - .layout-dsp-none { - display: none; - } - - .layout-dsp-block { - display: block; - } - - .layout-dsp-grid { - display: grid; - } - - .layout-dsp-iflex { - display: inline-flex; - } - - .layout-dsp-flexvert { - display: flex; - flex-direction: column; - } - - .layout-dsp-flexhor { - display: flex; - flex-direction: row; - } - - .layout-fw-w { - flex-wrap: wrap; - } - - .layout-al-fs { - align-items: start; - } - - .layout-al-fe { - align-items: end; - } - - .layout-al-c { - align-items: center; - } - - .layout-as-n { - align-self: normal; - } - - .layout-js-c { - justify-self: center; - } - - .layout-sp-c { - justify-content: center; - } - - .layout-sp-ev { - justify-content: space-evenly; - } - - .layout-sp-bt { - justify-content: space-between; - } - - .layout-sp-s { - justify-content: start; - } - - .layout-sp-e { - justify-content: end; - } - - .layout-ji-e { - justify-items: end; - } - - .layout-r-none { - resize: none; - } - - .layout-fs-c { - field-sizing: content; - } - - .layout-fs-n { - field-sizing: none; - } - - .layout-flx-0 { - flex: 0 0 auto; - } - - .layout-flx-1 { - flex: 1 0 auto; - } - - .layout-c-s { - contain: strict; - } - - /** Widths **/ - - ${new Array(10).fill(0).map((_, idx) => { - const weight = (idx + 1) * 10; - return `.layout-w-${weight} { width: ${weight}%; max-width: ${weight}%; }`; - }).join("\n")} - - ${new Array(16).fill(0).map((_, idx) => { - return `.layout-wp-${idx} { width: ${idx * 4}px; }`; - }).join("\n")} - - /** Heights **/ - - ${new Array(10).fill(0).map((_, idx) => { - const height = (idx + 1) * 10; - return `.layout-h-${height} { height: ${height}%; }`; - }).join("\n")} - - ${new Array(16).fill(0).map((_, idx) => { - return `.layout-hp-${idx} { height: ${idx * 4}px; }`; - }).join("\n")} - - .layout-el-cv { - & img, - & video { - width: 100%; - height: 100%; - object-fit: cover; - margin: 0; - } - } - - .layout-ar-sq { - aspect-ratio: 1 / 1; - } - - .layout-ex-fb { - margin: calc(var(--padding) * -1) 0 0 calc(var(--padding) * -1); - width: calc(100% + var(--padding) * 2); - height: calc(100% + var(--padding) * 2); - } -`, - ` - ${new Array(21).fill(0).map((_, idx) => { - return `.opacity-el-${idx * 5} { opacity: ${idx / 20}; }`; - }).join("\n")} -`, - ` - :host { - --default-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - --default-font-family-mono: "Courier New", Courier, monospace; - } - - .typography-f-s { - font-family: var(--font-family, var(--default-font-family)); - font-optical-sizing: auto; - font-variation-settings: "slnt" 0, "wdth" 100, "GRAD" 0; - } - - .typography-f-sf { - font-family: var(--font-family-flex, var(--default-font-family)); - font-optical-sizing: auto; - } - - .typography-f-c { - font-family: var(--font-family-mono, var(--default-font-family)); - font-optical-sizing: auto; - font-variation-settings: "slnt" 0, "wdth" 100, "GRAD" 0; - } - - .typography-v-r { - font-variation-settings: "slnt" 0, "wdth" 100, "GRAD" 0, "ROND" 100; - } - - .typography-ta-s { - text-align: start; - } - - .typography-ta-c { - text-align: center; - } - - .typography-fs-n { - font-style: normal; - } - - .typography-fs-i { - font-style: italic; - } - - .typography-sz-ls { - font-size: 11px; - line-height: 16px; - } - - .typography-sz-lm { - font-size: 12px; - line-height: 16px; - } - - .typography-sz-ll { - font-size: 14px; - line-height: 20px; - } - - .typography-sz-bs { - font-size: 12px; - line-height: 16px; - } - - .typography-sz-bm { - font-size: 14px; - line-height: 20px; - } - - .typography-sz-bl { - font-size: 16px; - line-height: 24px; - } - - .typography-sz-ts { - font-size: 14px; - line-height: 20px; - } - - .typography-sz-tm { - font-size: 16px; - line-height: 24px; - } - - .typography-sz-tl { - font-size: 22px; - line-height: 28px; - } - - .typography-sz-hs { - font-size: 24px; - line-height: 32px; - } - - .typography-sz-hm { - font-size: 28px; - line-height: 36px; - } - - .typography-sz-hl { - font-size: 32px; - line-height: 40px; - } - - .typography-sz-ds { - font-size: 36px; - line-height: 44px; - } - - .typography-sz-dm { - font-size: 45px; - line-height: 52px; - } - - .typography-sz-dl { - font-size: 57px; - line-height: 64px; - } - - .typography-ws-p { - white-space: pre-line; - } - - .typography-ws-nw { - white-space: nowrap; - } - - .typography-td-none { - text-decoration: none; - } - - /** Weights **/ - - ${new Array(9).fill(0).map((_, idx) => { - const weight = (idx + 1) * 100; - return `.typography-w-${weight} { font-weight: ${weight}; }`; - }).join("\n")} -` -].flat(Infinity).join("\n"); -var guards_exports = /* @__PURE__ */ __exportAll({ - isComponentArrayReference: () => isComponentArrayReference, - isObject: () => isObject$1, - isPath: () => isPath, - isResolvedAudioPlayer: () => isResolvedAudioPlayer, - isResolvedButton: () => isResolvedButton, - isResolvedCard: () => isResolvedCard, - isResolvedCheckbox: () => isResolvedCheckbox, - isResolvedColumn: () => isResolvedColumn, - isResolvedDateTimeInput: () => isResolvedDateTimeInput, - isResolvedDivider: () => isResolvedDivider, - isResolvedIcon: () => isResolvedIcon, - isResolvedImage: () => isResolvedImage, - isResolvedList: () => isResolvedList, - isResolvedModal: () => isResolvedModal, - isResolvedMultipleChoice: () => isResolvedMultipleChoice, - isResolvedRow: () => isResolvedRow, - isResolvedSlider: () => isResolvedSlider, - isResolvedTabs: () => isResolvedTabs, - isResolvedText: () => isResolvedText, - isResolvedTextField: () => isResolvedTextField, - isResolvedVideo: () => isResolvedVideo, - isValueMap: () => isValueMap -}); -function isValueMap(value) { - return isObject$1(value) && "key" in value; -} -function isPath(key, value) { - return key === "path" && typeof value === "string"; -} -function isObject$1(value) { - return typeof value === "object" && value !== null && !Array.isArray(value); -} -function isComponentArrayReference(value) { - if (!isObject$1(value)) return false; - return "explicitList" in value || "template" in value; -} -function isStringValue(value) { - return isObject$1(value) && ("path" in value || "literal" in value && typeof value.literal === "string" || "literalString" in value); -} -function isNumberValue(value) { - return isObject$1(value) && ("path" in value || "literal" in value && typeof value.literal === "number" || "literalNumber" in value); -} -function isBooleanValue(value) { - return isObject$1(value) && ("path" in value || "literal" in value && typeof value.literal === "boolean" || "literalBoolean" in value); -} -function isAnyComponentNode(value) { - if (!isObject$1(value)) return false; - if (!("id" in value && "type" in value && "properties" in value)) return false; - return true; -} -function isResolvedAudioPlayer(props) { - return isObject$1(props) && "url" in props && isStringValue(props.url); -} -function isResolvedButton(props) { - return isObject$1(props) && "child" in props && isAnyComponentNode(props.child) && "action" in props; -} -function isResolvedCard(props) { - if (!isObject$1(props)) return false; - if (!("child" in props)) if (!("children" in props)) return false; - else return Array.isArray(props.children) && props.children.every(isAnyComponentNode); - return isAnyComponentNode(props.child); -} -function isResolvedCheckbox(props) { - return isObject$1(props) && "label" in props && isStringValue(props.label) && "value" in props && isBooleanValue(props.value); -} -function isResolvedColumn(props) { - return isObject$1(props) && "children" in props && Array.isArray(props.children) && props.children.every(isAnyComponentNode); -} -function isResolvedDateTimeInput(props) { - return isObject$1(props) && "value" in props && isStringValue(props.value); -} -function isResolvedDivider(props) { - return isObject$1(props); -} -function isResolvedImage(props) { - return isObject$1(props) && "url" in props && isStringValue(props.url); -} -function isResolvedIcon(props) { - return isObject$1(props) && "name" in props && isStringValue(props.name); -} -function isResolvedList(props) { - return isObject$1(props) && "children" in props && Array.isArray(props.children) && props.children.every(isAnyComponentNode); -} -function isResolvedModal(props) { - return isObject$1(props) && "entryPointChild" in props && isAnyComponentNode(props.entryPointChild) && "contentChild" in props && isAnyComponentNode(props.contentChild); -} -function isResolvedMultipleChoice(props) { - return isObject$1(props) && "selections" in props; -} -function isResolvedRow(props) { - return isObject$1(props) && "children" in props && Array.isArray(props.children) && props.children.every(isAnyComponentNode); -} -function isResolvedSlider(props) { - return isObject$1(props) && "value" in props && isNumberValue(props.value); -} -function isResolvedTabItem(item) { - return isObject$1(item) && "title" in item && isStringValue(item.title) && "child" in item && isAnyComponentNode(item.child); -} -function isResolvedTabs(props) { - return isObject$1(props) && "tabItems" in props && Array.isArray(props.tabItems) && props.tabItems.every(isResolvedTabItem); -} -function isResolvedText(props) { - return isObject$1(props) && "text" in props && isStringValue(props.text); -} -function isResolvedTextField(props) { - return isObject$1(props) && "label" in props && isStringValue(props.label); -} -function isResolvedVideo(props) { - return isObject$1(props) && "url" in props && isStringValue(props.url); -} -/** -* Processes and consolidates A2UIProtocolMessage objects into a structured, -* hierarchical model of UI surfaces. -*/ -var A2uiMessageProcessor = class A2uiMessageProcessor { - static { - this.DEFAULT_SURFACE_ID = "@default"; - } - #mapCtor = Map; - #arrayCtor = Array; - #setCtor = Set; - #objCtor = Object; - #surfaces; - constructor(opts = { - mapCtor: Map, - arrayCtor: Array, - setCtor: Set, - objCtor: Object - }) { - this.opts = opts; - this.#arrayCtor = opts.arrayCtor; - this.#mapCtor = opts.mapCtor; - this.#setCtor = opts.setCtor; - this.#objCtor = opts.objCtor; - this.#surfaces = new opts.mapCtor(); - } - getSurfaces() { - return this.#surfaces; - } - clearSurfaces() { - this.#surfaces.clear(); - } - processMessages(messages) { - for (const message of messages) { - if (message.beginRendering) this.#handleBeginRendering(message.beginRendering, message.beginRendering.surfaceId); - if (message.surfaceUpdate) this.#handleSurfaceUpdate(message.surfaceUpdate, message.surfaceUpdate.surfaceId); - if (message.dataModelUpdate) this.#handleDataModelUpdate(message.dataModelUpdate, message.dataModelUpdate.surfaceId); - if (message.deleteSurface) this.#handleDeleteSurface(message.deleteSurface); - } - } - /** - * Retrieves the data for a given component node and a relative path string. - * This correctly handles the special `.` path, which refers to the node's - * own data context. - */ - getData(node, relativePath, surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID) { - const surface = this.#getOrCreateSurface(surfaceId); - if (!surface) return null; - let finalPath; - if (relativePath === "." || relativePath === "") finalPath = node.dataContextPath ?? "/"; - else finalPath = this.resolvePath(relativePath, node.dataContextPath); - return this.#getDataByPath(surface.dataModel, finalPath); - } - setData(node, relativePath, value, surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID) { - if (!node) { - console.warn("No component node set"); - return; - } - const surface = this.#getOrCreateSurface(surfaceId); - if (!surface) return; - let finalPath; - if (relativePath === "." || relativePath === "") finalPath = node.dataContextPath ?? "/"; - else finalPath = this.resolvePath(relativePath, node.dataContextPath); - this.#setDataByPath(surface.dataModel, finalPath, value); - } - resolvePath(path, dataContextPath) { - if (path.startsWith("/")) return path; - if (dataContextPath && dataContextPath !== "/") return dataContextPath.endsWith("/") ? `${dataContextPath}${path}` : `${dataContextPath}/${path}`; - return `/${path}`; - } - #parseIfJsonString(value) { - if (typeof value !== "string") return value; - const trimmedValue = value.trim(); - if (trimmedValue.startsWith("{") && trimmedValue.endsWith("}") || trimmedValue.startsWith("[") && trimmedValue.endsWith("]")) try { - return JSON.parse(value); - } catch (e) { - console.warn(`Failed to parse potential JSON string: "${value.substring(0, 50)}..."`, e); - return value; - } - return value; - } - /** - * Converts a specific array format [{key: "...", value_string: "..."}, ...] - * into a standard Map. It also attempts to parse any string values that - * appear to be stringified JSON. - */ - #convertKeyValueArrayToMap(arr) { - const map = new this.#mapCtor(); - for (const item of arr) { - if (!isObject$1(item) || !("key" in item)) continue; - const key = item.key; - const valueKey = this.#findValueKey(item); - if (!valueKey) continue; - let value = item[valueKey]; - if (valueKey === "valueMap" && Array.isArray(value)) value = this.#convertKeyValueArrayToMap(value); - else if (typeof value === "string") value = this.#parseIfJsonString(value); - this.#setDataByPath(map, key, value); - } - return map; - } - #setDataByPath(root, path, value) { - if (Array.isArray(value) && (value.length === 0 || isObject$1(value[0]) && "key" in value[0])) if (value.length === 1 && isObject$1(value[0]) && value[0].key === ".") { - const item = value[0]; - const valueKey = this.#findValueKey(item); - if (valueKey) { - value = item[valueKey]; - if (valueKey === "valueMap" && Array.isArray(value)) value = this.#convertKeyValueArrayToMap(value); - else if (typeof value === "string") value = this.#parseIfJsonString(value); - } else value = this.#convertKeyValueArrayToMap(value); - } else value = this.#convertKeyValueArrayToMap(value); - const segments = this.#normalizePath(path).split("/").filter((s) => s); - if (segments.length === 0) { - if (value instanceof Map || isObject$1(value)) { - if (!(value instanceof Map) && isObject$1(value)) value = new this.#mapCtor(Object.entries(value)); - root.clear(); - for (const [key, v] of value.entries()) root.set(key, v); - } else console.error("Cannot set root of DataModel to a non-Map value."); - return; - } - let current = root; - for (let i = 0; i < segments.length - 1; i++) { - const segment = segments[i]; - let target; - if (current instanceof Map) target = current.get(segment); - else if (Array.isArray(current) && /^\d+$/.test(segment)) target = current[parseInt(segment, 10)]; - if (target === void 0 || typeof target !== "object" || target === null) { - target = new this.#mapCtor(); - if (current instanceof this.#mapCtor) current.set(segment, target); - else if (Array.isArray(current)) current[parseInt(segment, 10)] = target; - } - current = target; - } - const finalSegment = segments[segments.length - 1]; - const storedValue = value; - if (current instanceof this.#mapCtor) current.set(finalSegment, storedValue); - else if (Array.isArray(current) && /^\d+$/.test(finalSegment)) current[parseInt(finalSegment, 10)] = storedValue; - } - /** - * Normalizes a path string into a consistent, slash-delimited format. - * Converts bracket notation and dot notation in a two-pass. - * e.g., "bookRecommendations[0].title" -> "/bookRecommendations/0/title" - * e.g., "book.0.title" -> "/book/0/title" - */ - #normalizePath(path) { - return "/" + path.replace(/\[(\d+)\]/g, ".$1").split(".").filter((s) => s.length > 0).join("/"); - } - #getDataByPath(root, path) { - const segments = this.#normalizePath(path).split("/").filter((s) => s); - let current = root; - for (const segment of segments) { - if (current === void 0 || current === null) return null; - if (current instanceof Map) current = current.get(segment); - else if (Array.isArray(current) && /^\d+$/.test(segment)) current = current[parseInt(segment, 10)]; - else if (isObject$1(current)) current = current[segment]; - else return null; - } - return current; - } - #getOrCreateSurface(surfaceId) { - let surface = this.#surfaces.get(surfaceId); - if (!surface) { - surface = new this.#objCtor({ - rootComponentId: null, - componentTree: null, - dataModel: new this.#mapCtor(), - components: new this.#mapCtor(), - styles: new this.#objCtor() - }); - this.#surfaces.set(surfaceId, surface); - } - return surface; - } - #handleBeginRendering(message, surfaceId) { - const surface = this.#getOrCreateSurface(surfaceId); - surface.rootComponentId = message.root; - surface.styles = message.styles ?? {}; - this.#rebuildComponentTree(surface); - } - #handleSurfaceUpdate(message, surfaceId) { - const surface = this.#getOrCreateSurface(surfaceId); - for (const component of message.components) surface.components.set(component.id, component); - this.#rebuildComponentTree(surface); - } - #handleDataModelUpdate(message, surfaceId) { - const surface = this.#getOrCreateSurface(surfaceId); - const path = message.path ?? "/"; - this.#setDataByPath(surface.dataModel, path, message.contents); - this.#rebuildComponentTree(surface); - } - #handleDeleteSurface(message) { - this.#surfaces.delete(message.surfaceId); - } - /** - * Starts at the root component of the surface and builds out the tree - * recursively. This process involves resolving all properties of the child - * components, and expanding on any explicit children lists or templates - * found in the structure. - * - * @param surface The surface to be built. - */ - #rebuildComponentTree(surface) { - if (!surface.rootComponentId) { - surface.componentTree = null; - return; - } - const visited = new this.#setCtor(); - surface.componentTree = this.#buildNodeRecursive(surface.rootComponentId, surface, visited, "/", ""); - } - /** Finds a value key in a map. */ - #findValueKey(value) { - return Object.keys(value).find((k) => k.startsWith("value")); - } - /** - * Builds out the nodes recursively. - */ - #buildNodeRecursive(baseComponentId, surface, visited, dataContextPath, idSuffix = "") { - const fullId = `${baseComponentId}${idSuffix}`; - const { components } = surface; - if (!components.has(baseComponentId)) return null; - if (visited.has(fullId)) throw new Error(`Circular dependency for component "${fullId}".`); - visited.add(fullId); - const componentData = components.get(baseComponentId); - const componentProps = componentData.component ?? {}; - const componentType = Object.keys(componentProps)[0]; - const unresolvedProperties = componentProps[componentType]; - const resolvedProperties = new this.#objCtor(); - if (isObject$1(unresolvedProperties)) for (const [key, value] of Object.entries(unresolvedProperties)) resolvedProperties[key] = this.#resolvePropertyValue(value, surface, visited, dataContextPath, idSuffix, key); - visited.delete(fullId); - const baseNode = { - id: fullId, - dataContextPath, - weight: componentData.weight ?? "initial" - }; - switch (componentType) { - case "Text": - if (!isResolvedText(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "Text", - properties: resolvedProperties - }); - case "Image": - if (!isResolvedImage(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "Image", - properties: resolvedProperties - }); - case "Icon": - if (!isResolvedIcon(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "Icon", - properties: resolvedProperties - }); - case "Video": - if (!isResolvedVideo(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "Video", - properties: resolvedProperties - }); - case "AudioPlayer": - if (!isResolvedAudioPlayer(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "AudioPlayer", - properties: resolvedProperties - }); - case "Row": - if (!isResolvedRow(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "Row", - properties: resolvedProperties - }); - case "Column": - if (!isResolvedColumn(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "Column", - properties: resolvedProperties - }); - case "List": - if (!isResolvedList(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "List", - properties: resolvedProperties - }); - case "Card": - if (!isResolvedCard(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "Card", - properties: resolvedProperties - }); - case "Tabs": - if (!isResolvedTabs(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "Tabs", - properties: resolvedProperties - }); - case "Divider": - if (!isResolvedDivider(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "Divider", - properties: resolvedProperties - }); - case "Modal": - if (!isResolvedModal(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "Modal", - properties: resolvedProperties - }); - case "Button": - if (!isResolvedButton(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "Button", - properties: resolvedProperties - }); - case "CheckBox": - if (!isResolvedCheckbox(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "CheckBox", - properties: resolvedProperties - }); - case "TextField": - if (!isResolvedTextField(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "TextField", - properties: resolvedProperties - }); - case "DateTimeInput": - if (!isResolvedDateTimeInput(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "DateTimeInput", - properties: resolvedProperties - }); - case "MultipleChoice": - if (!isResolvedMultipleChoice(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "MultipleChoice", - properties: resolvedProperties - }); - case "Slider": - if (!isResolvedSlider(resolvedProperties)) throw new Error(`Invalid data; expected ${componentType}`); - return new this.#objCtor({ - ...baseNode, - type: "Slider", - properties: resolvedProperties - }); - default: return new this.#objCtor({ - ...baseNode, - type: componentType, - properties: resolvedProperties - }); - } - } - /** - * Recursively resolves an individual property value. If a property indicates - * a child node (a string that matches a component ID), an explicitList of - * children, or a template, these will be built out here. - */ - #resolvePropertyValue(value, surface, visited, dataContextPath, idSuffix = "", propertyKey = null) { - const isComponentIdReferenceKey = (key) => key === "child" || key.endsWith("Child"); - if (typeof value === "string" && propertyKey && isComponentIdReferenceKey(propertyKey) && surface.components.has(value)) return this.#buildNodeRecursive(value, surface, visited, dataContextPath, idSuffix); - if (isComponentArrayReference(value)) { - if (value.explicitList) return value.explicitList.map((id) => this.#buildNodeRecursive(id, surface, visited, dataContextPath, idSuffix)); - if (value.template) { - const fullDataPath = this.resolvePath(value.template.dataBinding, dataContextPath); - const data = this.#getDataByPath(surface.dataModel, fullDataPath); - const template = value.template; - if (Array.isArray(data)) return data.map((_, index) => { - const newSuffix = `:${[...dataContextPath.split("/").filter((segment) => /^\d+$/.test(segment)), index].join(":")}`; - const childDataContextPath = `${fullDataPath}/${index}`; - return this.#buildNodeRecursive(template.componentId, surface, visited, childDataContextPath, newSuffix); - }); - if (data instanceof this.#mapCtor) return Array.from(data.keys(), (key) => { - const newSuffix = `:${key}`; - const childDataContextPath = `${fullDataPath}/${key}`; - return this.#buildNodeRecursive(template.componentId, surface, visited, childDataContextPath, newSuffix); - }); - return new this.#arrayCtor(); - } - } - if (Array.isArray(value)) return value.map((item) => this.#resolvePropertyValue(item, surface, visited, dataContextPath, idSuffix, propertyKey)); - if (isObject$1(value)) { - const newObj = new this.#objCtor(); - for (const [key, propValue] of Object.entries(value)) { - let propertyValue = propValue; - if (isPath(key, propValue) && dataContextPath !== "/") { - propertyValue = propValue.replace(/^\.?\/item/, "").replace(/^\.?\/text/, "").replace(/^\.?\/label/, "").replace(/^\.?\//, ""); - newObj[key] = propertyValue; - continue; - } - newObj[key] = this.#resolvePropertyValue(propertyValue, surface, visited, dataContextPath, idSuffix, key); - } - return newObj; - } - return value; - } -}; -var __defProp = Object.defineProperty; -var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { - enumerable: true, - configurable: true, - writable: true, - value -}) : obj[key] = value; -var __publicField = (obj, key, value) => { - __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); - return value; -}; -var __accessCheck = (obj, member, msg) => { - if (!member.has(obj)) throw TypeError("Cannot " + msg); -}; -var __privateIn = (member, obj) => { - if (Object(obj) !== obj) throw TypeError("Cannot use the \"in\" operator on this value"); - return member.has(obj); -}; -var __privateAdd = (obj, member, value) => { - if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); - member instanceof WeakSet ? member.add(obj) : member.set(obj, value); -}; -var __privateMethod = (obj, member, method) => { - __accessCheck(obj, member, "access private method"); - return method; -}; -/** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ -function defaultEquals(a, b) { - return Object.is(a, b); -} -/** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ -let activeConsumer = null; -let inNotificationPhase = false; -let epoch = 1; -const SIGNAL = /* @__PURE__ */ Symbol("SIGNAL"); -function setActiveConsumer(consumer) { - const prev = activeConsumer; - activeConsumer = consumer; - return prev; -} -function getActiveConsumer() { - return activeConsumer; -} -function isInNotificationPhase() { - return inNotificationPhase; -} -const REACTIVE_NODE = { - version: 0, - lastCleanEpoch: 0, - dirty: false, - producerNode: void 0, - producerLastReadVersion: void 0, - producerIndexOfThis: void 0, - nextProducerIndex: 0, - liveConsumerNode: void 0, - liveConsumerIndexOfThis: void 0, - consumerAllowSignalWrites: false, - consumerIsAlwaysLive: false, - producerMustRecompute: () => false, - producerRecomputeValue: () => {}, - consumerMarkedDirty: () => {}, - consumerOnSignalRead: () => {} -}; -function producerAccessed(node) { - if (inNotificationPhase) throw new Error(typeof ngDevMode !== "undefined" && ngDevMode ? `Assertion error: signal read during notification phase` : ""); - if (activeConsumer === null) return; - activeConsumer.consumerOnSignalRead(node); - const idx = activeConsumer.nextProducerIndex++; - assertConsumerNode(activeConsumer); - if (idx < activeConsumer.producerNode.length && activeConsumer.producerNode[idx] !== node) { - if (consumerIsLive(activeConsumer)) { - const staleProducer = activeConsumer.producerNode[idx]; - producerRemoveLiveConsumerAtIndex(staleProducer, activeConsumer.producerIndexOfThis[idx]); - } - } - if (activeConsumer.producerNode[idx] !== node) { - activeConsumer.producerNode[idx] = node; - activeConsumer.producerIndexOfThis[idx] = consumerIsLive(activeConsumer) ? producerAddLiveConsumer(node, activeConsumer, idx) : 0; - } - activeConsumer.producerLastReadVersion[idx] = node.version; -} -function producerIncrementEpoch() { - epoch++; -} -function producerUpdateValueVersion(node) { - if (!node.dirty && node.lastCleanEpoch === epoch) return; - if (!node.producerMustRecompute(node) && !consumerPollProducersForChange(node)) { - node.dirty = false; - node.lastCleanEpoch = epoch; - return; - } - node.producerRecomputeValue(node); - node.dirty = false; - node.lastCleanEpoch = epoch; -} -function producerNotifyConsumers(node) { - if (node.liveConsumerNode === void 0) return; - const prev = inNotificationPhase; - inNotificationPhase = true; - try { - for (const consumer of node.liveConsumerNode) if (!consumer.dirty) consumerMarkDirty(consumer); - } finally { - inNotificationPhase = prev; - } -} -function producerUpdatesAllowed() { - return (activeConsumer == null ? void 0 : activeConsumer.consumerAllowSignalWrites) !== false; -} -function consumerMarkDirty(node) { - var _a; - node.dirty = true; - producerNotifyConsumers(node); - (_a = node.consumerMarkedDirty) == null || _a.call(node.wrapper ?? node); -} -function consumerBeforeComputation(node) { - node && (node.nextProducerIndex = 0); - return setActiveConsumer(node); -} -function consumerAfterComputation(node, prevConsumer) { - setActiveConsumer(prevConsumer); - if (!node || node.producerNode === void 0 || node.producerIndexOfThis === void 0 || node.producerLastReadVersion === void 0) return; - if (consumerIsLive(node)) for (let i = node.nextProducerIndex; i < node.producerNode.length; i++) producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]); - while (node.producerNode.length > node.nextProducerIndex) { - node.producerNode.pop(); - node.producerLastReadVersion.pop(); - node.producerIndexOfThis.pop(); - } -} -function consumerPollProducersForChange(node) { - assertConsumerNode(node); - for (let i = 0; i < node.producerNode.length; i++) { - const producer = node.producerNode[i]; - const seenVersion = node.producerLastReadVersion[i]; - if (seenVersion !== producer.version) return true; - producerUpdateValueVersion(producer); - if (seenVersion !== producer.version) return true; - } - return false; -} -function producerAddLiveConsumer(node, consumer, indexOfThis) { - var _a; - assertProducerNode(node); - assertConsumerNode(node); - if (node.liveConsumerNode.length === 0) { - (_a = node.watched) == null || _a.call(node.wrapper); - for (let i = 0; i < node.producerNode.length; i++) node.producerIndexOfThis[i] = producerAddLiveConsumer(node.producerNode[i], node, i); - } - node.liveConsumerIndexOfThis.push(indexOfThis); - return node.liveConsumerNode.push(consumer) - 1; -} -function producerRemoveLiveConsumerAtIndex(node, idx) { - var _a; - assertProducerNode(node); - assertConsumerNode(node); - if (typeof ngDevMode !== "undefined" && ngDevMode && idx >= node.liveConsumerNode.length) throw new Error(`Assertion error: active consumer index ${idx} is out of bounds of ${node.liveConsumerNode.length} consumers)`); - if (node.liveConsumerNode.length === 1) { - (_a = node.unwatched) == null || _a.call(node.wrapper); - for (let i = 0; i < node.producerNode.length; i++) producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]); - } - const lastIdx = node.liveConsumerNode.length - 1; - node.liveConsumerNode[idx] = node.liveConsumerNode[lastIdx]; - node.liveConsumerIndexOfThis[idx] = node.liveConsumerIndexOfThis[lastIdx]; - node.liveConsumerNode.length--; - node.liveConsumerIndexOfThis.length--; - if (idx < node.liveConsumerNode.length) { - const idxProducer = node.liveConsumerIndexOfThis[idx]; - const consumer = node.liveConsumerNode[idx]; - assertConsumerNode(consumer); - consumer.producerIndexOfThis[idxProducer] = idx; - } -} -function consumerIsLive(node) { - var _a; - return node.consumerIsAlwaysLive || (((_a = node == null ? void 0 : node.liveConsumerNode) == null ? void 0 : _a.length) ?? 0) > 0; -} -function assertConsumerNode(node) { - node.producerNode ?? (node.producerNode = []); - node.producerIndexOfThis ?? (node.producerIndexOfThis = []); - node.producerLastReadVersion ?? (node.producerLastReadVersion = []); -} -function assertProducerNode(node) { - node.liveConsumerNode ?? (node.liveConsumerNode = []); - node.liveConsumerIndexOfThis ?? (node.liveConsumerIndexOfThis = []); -} -/** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ -function computedGet(node) { - producerUpdateValueVersion(node); - producerAccessed(node); - if (node.value === ERRORED) throw node.error; - return node.value; -} -function createComputed(computation) { - const node = Object.create(COMPUTED_NODE); - node.computation = computation; - const computed = () => computedGet(node); - computed[SIGNAL] = node; - return computed; -} -const UNSET = /* @__PURE__ */ Symbol("UNSET"); -const COMPUTING = /* @__PURE__ */ Symbol("COMPUTING"); -const ERRORED = /* @__PURE__ */ Symbol("ERRORED"); -const COMPUTED_NODE = { - ...REACTIVE_NODE, - value: UNSET, - dirty: true, - error: null, - equal: defaultEquals, - producerMustRecompute(node) { - return node.value === UNSET || node.value === COMPUTING; - }, - producerRecomputeValue(node) { - if (node.value === COMPUTING) throw new Error("Detected cycle in computations."); - const oldValue = node.value; - node.value = COMPUTING; - const prevConsumer = consumerBeforeComputation(node); - let newValue; - let wasEqual = false; - try { - newValue = node.computation.call(node.wrapper); - wasEqual = oldValue !== UNSET && oldValue !== ERRORED && node.equal.call(node.wrapper, oldValue, newValue); - } catch (err) { - newValue = ERRORED; - node.error = err; - } finally { - consumerAfterComputation(node, prevConsumer); - } - if (wasEqual) { - node.value = oldValue; - return; - } - node.value = newValue; - node.version++; - } -}; -/** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ -function defaultThrowError() { - throw new Error(); -} -let throwInvalidWriteToSignalErrorFn = defaultThrowError; -function throwInvalidWriteToSignalError() { - throwInvalidWriteToSignalErrorFn(); -} -/** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ -function createSignal(initialValue) { - const node = Object.create(SIGNAL_NODE); - node.value = initialValue; - const getter = () => { - producerAccessed(node); - return node.value; - }; - getter[SIGNAL] = node; - return getter; -} -function signalGetFn() { - producerAccessed(this); - return this.value; -} -function signalSetFn(node, newValue) { - if (!producerUpdatesAllowed()) throwInvalidWriteToSignalError(); - if (!node.equal.call(node.wrapper, node.value, newValue)) { - node.value = newValue; - signalValueChanged(node); - } -} -const SIGNAL_NODE = { - ...REACTIVE_NODE, - equal: defaultEquals, - value: void 0 -}; -function signalValueChanged(node) { - node.version++; - producerIncrementEpoch(); - producerNotifyConsumers(node); -} -/** -* @license -* Copyright 2024 Bloomberg Finance L.P. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -const NODE = Symbol("node"); -var Signal; -((Signal2) => { - var _a, _brand, _b, _brand2; - class State { - constructor(initialValue, options = {}) { - __privateAdd(this, _brand); - __publicField(this, _a); - const node = createSignal(initialValue)[SIGNAL]; - this[NODE] = node; - node.wrapper = this; - if (options) { - const equals = options.equals; - if (equals) node.equal = equals; - node.watched = options[Signal2.subtle.watched]; - node.unwatched = options[Signal2.subtle.unwatched]; - } - } - get() { - if (!(0, Signal2.isState)(this)) throw new TypeError("Wrong receiver type for Signal.State.prototype.get"); - return signalGetFn.call(this[NODE]); - } - set(newValue) { - if (!(0, Signal2.isState)(this)) throw new TypeError("Wrong receiver type for Signal.State.prototype.set"); - if (isInNotificationPhase()) throw new Error("Writes to signals not permitted during Watcher callback"); - const ref = this[NODE]; - signalSetFn(ref, newValue); - } - } - _a = NODE; - _brand = /* @__PURE__ */ new WeakSet(); - Signal2.isState = (s) => typeof s === "object" && __privateIn(_brand, s); - Signal2.State = State; - class Computed { - constructor(computation, options) { - __privateAdd(this, _brand2); - __publicField(this, _b); - const node = createComputed(computation)[SIGNAL]; - node.consumerAllowSignalWrites = true; - this[NODE] = node; - node.wrapper = this; - if (options) { - const equals = options.equals; - if (equals) node.equal = equals; - node.watched = options[Signal2.subtle.watched]; - node.unwatched = options[Signal2.subtle.unwatched]; - } - } - get() { - if (!(0, Signal2.isComputed)(this)) throw new TypeError("Wrong receiver type for Signal.Computed.prototype.get"); - return computedGet(this[NODE]); - } - } - _b = NODE; - _brand2 = /* @__PURE__ */ new WeakSet(); - Signal2.isComputed = (c) => typeof c === "object" && __privateIn(_brand2, c); - Signal2.Computed = Computed; - ((subtle2) => { - var _a2, _brand3, _assertSignals, assertSignals_fn; - function untrack(cb) { - let output; - let prevActiveConsumer = null; - try { - prevActiveConsumer = setActiveConsumer(null); - output = cb(); - } finally { - setActiveConsumer(prevActiveConsumer); - } - return output; - } - subtle2.untrack = untrack; - function introspectSources(sink) { - var _a3; - if (!(0, Signal2.isComputed)(sink) && !(0, Signal2.isWatcher)(sink)) throw new TypeError("Called introspectSources without a Computed or Watcher argument"); - return ((_a3 = sink[NODE].producerNode) == null ? void 0 : _a3.map((n) => n.wrapper)) ?? []; - } - subtle2.introspectSources = introspectSources; - function introspectSinks(signal) { - var _a3; - if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) throw new TypeError("Called introspectSinks without a Signal argument"); - return ((_a3 = signal[NODE].liveConsumerNode) == null ? void 0 : _a3.map((n) => n.wrapper)) ?? []; - } - subtle2.introspectSinks = introspectSinks; - function hasSinks(signal) { - if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) throw new TypeError("Called hasSinks without a Signal argument"); - const liveConsumerNode = signal[NODE].liveConsumerNode; - if (!liveConsumerNode) return false; - return liveConsumerNode.length > 0; - } - subtle2.hasSinks = hasSinks; - function hasSources(signal) { - if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isWatcher)(signal)) throw new TypeError("Called hasSources without a Computed or Watcher argument"); - const producerNode = signal[NODE].producerNode; - if (!producerNode) return false; - return producerNode.length > 0; - } - subtle2.hasSources = hasSources; - class Watcher { - constructor(notify) { - __privateAdd(this, _brand3); - __privateAdd(this, _assertSignals); - __publicField(this, _a2); - let node = Object.create(REACTIVE_NODE); - node.wrapper = this; - node.consumerMarkedDirty = notify; - node.consumerIsAlwaysLive = true; - node.consumerAllowSignalWrites = false; - node.producerNode = []; - this[NODE] = node; - } - watch(...signals) { - if (!(0, Signal2.isWatcher)(this)) throw new TypeError("Called unwatch without Watcher receiver"); - __privateMethod(this, _assertSignals, assertSignals_fn).call(this, signals); - const node = this[NODE]; - node.dirty = false; - const prev = setActiveConsumer(node); - for (const signal of signals) producerAccessed(signal[NODE]); - setActiveConsumer(prev); - } - unwatch(...signals) { - if (!(0, Signal2.isWatcher)(this)) throw new TypeError("Called unwatch without Watcher receiver"); - __privateMethod(this, _assertSignals, assertSignals_fn).call(this, signals); - const node = this[NODE]; - assertConsumerNode(node); - for (let i = node.producerNode.length - 1; i >= 0; i--) if (signals.includes(node.producerNode[i].wrapper)) { - producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]); - const lastIdx = node.producerNode.length - 1; - node.producerNode[i] = node.producerNode[lastIdx]; - node.producerIndexOfThis[i] = node.producerIndexOfThis[lastIdx]; - node.producerNode.length--; - node.producerIndexOfThis.length--; - node.nextProducerIndex--; - if (i < node.producerNode.length) { - const idxConsumer = node.producerIndexOfThis[i]; - const producer = node.producerNode[i]; - assertProducerNode(producer); - producer.liveConsumerIndexOfThis[idxConsumer] = i; - } - } - } - getPending() { - if (!(0, Signal2.isWatcher)(this)) throw new TypeError("Called getPending without Watcher receiver"); - return this[NODE].producerNode.filter((n) => n.dirty).map((n) => n.wrapper); - } - } - _a2 = NODE; - _brand3 = /* @__PURE__ */ new WeakSet(); - _assertSignals = /* @__PURE__ */ new WeakSet(); - assertSignals_fn = function(signals) { - for (const signal of signals) if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) throw new TypeError("Called watch/unwatch without a Computed or State argument"); - }; - Signal2.isWatcher = (w) => __privateIn(_brand3, w); - subtle2.Watcher = Watcher; - function currentComputed() { - var _a3; - return (_a3 = getActiveConsumer()) == null ? void 0 : _a3.wrapper; - } - subtle2.currentComputed = currentComputed; - subtle2.watched = Symbol("watched"); - subtle2.unwatched = Symbol("unwatched"); - })(Signal2.subtle || (Signal2.subtle = {})); -})(Signal || (Signal = {})); -/** -* equality check here is always false so that we can dirty the storage -* via setting to _anything_ -* -* -* This is for a pattern where we don't *directly* use signals to back the values used in collections -* so that instanceof checks and getters and other native features "just work" without having -* to do nested proxying. -* -* (though, see deep.ts for nested / deep behavior) -*/ -const createStorage = (initial = null) => new Signal.State(initial, { equals: () => false }); -const ARRAY_GETTER_METHODS = new Set([ - Symbol.iterator, - "concat", - "entries", - "every", - "filter", - "find", - "findIndex", - "flat", - "flatMap", - "forEach", - "includes", - "indexOf", - "join", - "keys", - "lastIndexOf", - "map", - "reduce", - "reduceRight", - "slice", - "some", - "values" -]); -const ARRAY_WRITE_THEN_READ_METHODS = new Set([ - "fill", - "push", - "unshift" -]); -function convertToInt(prop) { - if (typeof prop === "symbol") return null; - const num = Number(prop); - if (isNaN(num)) return null; - return num % 1 === 0 ? num : null; -} -var SignalArray = class SignalArray { - /** - * Creates an array from an iterable object. - * @param iterable An iterable object to convert to an array. - */ - /** - * Creates an array from an iterable object. - * @param iterable An iterable object to convert to an array. - * @param mapfn A mapping function to call on every element of the array. - * @param thisArg Value of 'this' used to invoke the mapfn. - */ - static from(iterable, mapfn, thisArg) { - return mapfn ? new SignalArray(Array.from(iterable, mapfn, thisArg)) : new SignalArray(Array.from(iterable)); - } - static of(...arr) { - return new SignalArray(arr); - } - constructor(arr = []) { - let clone = arr.slice(); - let self = this; - let boundFns = /* @__PURE__ */ new Map(); - /** - Flag to track whether we have *just* intercepted a call to `.push()` or - `.unshift()`, since in those cases (and only those cases!) the `Array` - itself checks `.length` to return from the function call. - */ - let nativelyAccessingLengthFromPushOrUnshift = false; - return new Proxy(clone, { - get(target, prop) { - let index = convertToInt(prop); - if (index !== null) { - self.#readStorageFor(index); - self.#collection.get(); - return target[index]; - } - if (prop === "length") { - if (nativelyAccessingLengthFromPushOrUnshift) nativelyAccessingLengthFromPushOrUnshift = false; - else self.#collection.get(); - return target[prop]; - } - if (ARRAY_WRITE_THEN_READ_METHODS.has(prop)) nativelyAccessingLengthFromPushOrUnshift = true; - if (ARRAY_GETTER_METHODS.has(prop)) { - let fn = boundFns.get(prop); - if (fn === void 0) { - fn = (...args) => { - self.#collection.get(); - return target[prop](...args); - }; - boundFns.set(prop, fn); - } - return fn; - } - return target[prop]; - }, - set(target, prop, value) { - target[prop] = value; - let index = convertToInt(prop); - if (index !== null) { - self.#dirtyStorageFor(index); - self.#collection.set(null); - } else if (prop === "length") self.#collection.set(null); - return true; - }, - getPrototypeOf() { - return SignalArray.prototype; - } - }); - } - #collection = createStorage(); - #storages = /* @__PURE__ */ new Map(); - #readStorageFor(index) { - let storage = this.#storages.get(index); - if (storage === void 0) { - storage = createStorage(); - this.#storages.set(index, storage); - } - storage.get(); - } - #dirtyStorageFor(index) { - const storage = this.#storages.get(index); - if (storage) storage.set(null); - } -}; -Object.setPrototypeOf(SignalArray.prototype, Array.prototype); -var SignalMap = class { - collection = createStorage(); - storages = /* @__PURE__ */ new Map(); - vals; - readStorageFor(key) { - const { storages } = this; - let storage = storages.get(key); - if (storage === void 0) { - storage = createStorage(); - storages.set(key, storage); - } - storage.get(); - } - dirtyStorageFor(key) { - const storage = this.storages.get(key); - if (storage) storage.set(null); - } - constructor(existing) { - this.vals = existing ? new Map(existing) : /* @__PURE__ */ new Map(); - } - get(key) { - this.readStorageFor(key); - return this.vals.get(key); - } - has(key) { - this.readStorageFor(key); - return this.vals.has(key); - } - entries() { - this.collection.get(); - return this.vals.entries(); - } - keys() { - this.collection.get(); - return this.vals.keys(); - } - values() { - this.collection.get(); - return this.vals.values(); - } - forEach(fn) { - this.collection.get(); - this.vals.forEach(fn); - } - get size() { - this.collection.get(); - return this.vals.size; - } - [Symbol.iterator]() { - this.collection.get(); - return this.vals[Symbol.iterator](); - } - get [Symbol.toStringTag]() { - return this.vals[Symbol.toStringTag]; - } - set(key, value) { - this.dirtyStorageFor(key); - this.collection.set(null); - this.vals.set(key, value); - return this; - } - delete(key) { - this.dirtyStorageFor(key); - this.collection.set(null); - return this.vals.delete(key); - } - clear() { - this.storages.forEach((s) => s.set(null)); - this.collection.set(null); - this.vals.clear(); - } -}; -Object.setPrototypeOf(SignalMap.prototype, Map.prototype); -/** -* Create a reactive Object, backed by Signals, using a Proxy. -* This allows dynamic creation and deletion of signals using the object primitive -* APIs that most folks are familiar with -- the only difference is instantiation. -* ```js -* const obj = new SignalObject({ foo: 123 }); -* -* obj.foo // 123 -* obj.foo = 456 -* obj.foo // 456 -* obj.bar = 2 -* obj.bar // 2 -* ``` -*/ -const SignalObject = class SignalObjectImpl { - static fromEntries(entries) { - return new SignalObjectImpl(Object.fromEntries(entries)); - } - #storages = /* @__PURE__ */ new Map(); - #collection = createStorage(); - constructor(obj = {}) { - let proto = Object.getPrototypeOf(obj); - let descs = Object.getOwnPropertyDescriptors(obj); - let clone = Object.create(proto); - for (let prop in descs) Object.defineProperty(clone, prop, descs[prop]); - let self = this; - return new Proxy(clone, { - get(target, prop, receiver) { - self.#readStorageFor(prop); - return Reflect.get(target, prop, receiver); - }, - has(target, prop) { - self.#readStorageFor(prop); - return prop in target; - }, - ownKeys(target) { - self.#collection.get(); - return Reflect.ownKeys(target); - }, - set(target, prop, value, receiver) { - let result = Reflect.set(target, prop, value, receiver); - self.#dirtyStorageFor(prop); - self.#dirtyCollection(); - return result; - }, - deleteProperty(target, prop) { - if (prop in target) { - delete target[prop]; - self.#dirtyStorageFor(prop); - self.#dirtyCollection(); - } - return true; - }, - getPrototypeOf() { - return SignalObjectImpl.prototype; - } - }); - } - #readStorageFor(key) { - let storage = this.#storages.get(key); - if (storage === void 0) { - storage = createStorage(); - this.#storages.set(key, storage); - } - storage.get(); - } - #dirtyStorageFor(key) { - const storage = this.#storages.get(key); - if (storage) storage.set(null); - } - #dirtyCollection() { - this.#collection.set(null); - } -}; -var SignalSet = class { - collection = createStorage(); - storages = /* @__PURE__ */ new Map(); - vals; - storageFor(key) { - const storages = this.storages; - let storage = storages.get(key); - if (storage === void 0) { - storage = createStorage(); - storages.set(key, storage); - } - return storage; - } - dirtyStorageFor(key) { - const storage = this.storages.get(key); - if (storage) storage.set(null); - } - constructor(existing) { - this.vals = new Set(existing); - } - has(value) { - this.storageFor(value).get(); - return this.vals.has(value); - } - entries() { - this.collection.get(); - return this.vals.entries(); - } - keys() { - this.collection.get(); - return this.vals.keys(); - } - values() { - this.collection.get(); - return this.vals.values(); - } - forEach(fn) { - this.collection.get(); - this.vals.forEach(fn); - } - get size() { - this.collection.get(); - return this.vals.size; - } - [Symbol.iterator]() { - this.collection.get(); - return this.vals[Symbol.iterator](); - } - get [Symbol.toStringTag]() { - return this.vals[Symbol.toStringTag]; - } - add(value) { - this.dirtyStorageFor(value); - this.collection.set(null); - this.vals.add(value); - return this; - } - delete(value) { - this.dirtyStorageFor(value); - this.collection.set(null); - return this.vals.delete(value); - } - clear() { - this.storages.forEach((s) => s.set(null)); - this.collection.set(null); - this.vals.clear(); - } -}; -Object.setPrototypeOf(SignalSet.prototype, Set.prototype); -function create() { - return new A2uiMessageProcessor({ - arrayCtor: SignalArray, - mapCtor: SignalMap, - objCtor: SignalObject, - setCtor: SignalSet - }); -} -const Data = { - createSignalA2uiMessageProcessor: create, - A2uiMessageProcessor, - Guards: guards_exports -}; -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const t$1 = (t) => (e, o) => { - void 0 !== o ? o.addInitializer(() => { - customElements.define(t, e); - }) : customElements.define(t, e); -}; -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const o$9 = { - attribute: !0, - type: String, - converter: u$3, - reflect: !1, - hasChanged: f$3 -}, r$7 = (t = o$9, e, r) => { - const { kind: n, metadata: i } = r; - let s = globalThis.litPropertyMetadata.get(i); - if (void 0 === s && globalThis.litPropertyMetadata.set(i, s = /* @__PURE__ */ new Map()), "setter" === n && ((t = Object.create(t)).wrapped = !0), s.set(r.name, t), "accessor" === n) { - const { name: o } = r; - return { - set(r) { - const n = e.get.call(this); - e.set.call(this, r), this.requestUpdate(o, n, t, !0, r); - }, - init(e) { - return void 0 !== e && this.C(o, void 0, t, e), e; - } - }; - } - if ("setter" === n) { - const { name: o } = r; - return function(r) { - const n = this[o]; - e.call(this, r), this.requestUpdate(o, n, t, !0, r); - }; - } - throw Error("Unsupported decorator location: " + n); -}; -function n$6(t) { - return (e, o) => "object" == typeof o ? r$7(t, e, o) : ((t, e, o) => { - const r = e.hasOwnProperty(o); - return e.constructor.createProperty(o, t), r ? Object.getOwnPropertyDescriptor(e, o) : void 0; - })(t, e, o); -} -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function r$6(r) { - return n$6({ - ...r, - state: !0, - attribute: !1 - }); -} -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const e$6 = (e, t, c) => (c.configurable = !0, c.enumerable = !0, Reflect.decorate && "object" != typeof t && Object.defineProperty(e, t, c), c); -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function e$5(e, r) { - return (n, s, i) => { - const o = (t) => t.renderRoot?.querySelector(e) ?? null; - if (r) { - const { get: e, set: r } = "object" == typeof s ? n : i ?? (() => { - const t = Symbol(); - return { - get() { - return this[t]; - }, - set(e) { - this[t] = e; - } - }; - })(); - return e$6(n, s, { get() { - let t = e.call(this); - return void 0 === t && (t = o(this), (null !== t || this.hasUpdated) && r.call(this, t)), t; - } }); - } - return e$6(n, s, { get() { - return o(this); - } }); - }; -} -/** -* @license -* Copyright 2023 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ let i$2 = !1; -const s$1 = new Signal.subtle.Watcher(() => { - i$2 || (i$2 = !0, queueMicrotask(() => { - i$2 = !1; - for (const t of s$1.getPending()) t.get(); - s$1.watch(); - })); -}), h$3 = Symbol("SignalWatcherBrand"), e$3 = new FinalizationRegistry((i) => { - i.unwatch(...Signal.subtle.introspectSources(i)); -}), n$4 = /* @__PURE__ */ new WeakMap(); -function o$7(i) { - return !0 === i[h$3] ? (console.warn("SignalWatcher should not be applied to the same class more than once."), i) : class extends i { - constructor() { - super(...arguments), this._$St = /* @__PURE__ */ new Map(), this._$So = new Signal.State(0), this._$Si = !1; - } - _$Sl() { - var t, i; - const s = [], h = []; - this._$St.forEach((t, i) => { - ((null == t ? void 0 : t.beforeUpdate) ? s : h).push(i); - }); - const e = null === (t = this.h) || void 0 === t ? void 0 : t.getPending().filter((t) => t !== this._$Su && !this._$St.has(t)); - s.forEach((t) => t.get()), null === (i = this._$Su) || void 0 === i || i.get(), e.forEach((t) => t.get()), h.forEach((t) => t.get()); - } - _$Sv() { - this.isUpdatePending || queueMicrotask(() => { - this.isUpdatePending || this._$Sl(); - }); - } - _$S_() { - if (void 0 !== this.h) return; - this._$Su = new Signal.Computed(() => { - this._$So.get(), super.performUpdate(); - }); - const i = this.h = new Signal.subtle.Watcher(function() { - const t = n$4.get(this); - void 0 !== t && (!1 === t._$Si && (new Set(this.getPending()).has(t._$Su) ? t.requestUpdate() : t._$Sv()), this.watch()); - }); - n$4.set(i, this), e$3.register(this, i), i.watch(this._$Su), i.watch(...Array.from(this._$St).map(([t]) => t)); - } - _$Sp() { - if (void 0 === this.h) return; - let i = !1; - this.h.unwatch(...Signal.subtle.introspectSources(this.h).filter((t) => { - var s; - const h = !0 !== (null === (s = this._$St.get(t)) || void 0 === s ? void 0 : s.manualDispose); - return h && this._$St.delete(t), i || (i = !h), h; - })), i || (this._$Su = void 0, this.h = void 0, this._$St.clear()); - } - updateEffect(i, s) { - var h; - this._$S_(); - const e = new Signal.Computed(() => { - i(); - }); - return this.h.watch(e), this._$St.set(e, s), null !== (h = null == s ? void 0 : s.beforeUpdate) && void 0 !== h && h ? Signal.subtle.untrack(() => e.get()) : this.updateComplete.then(() => Signal.subtle.untrack(() => e.get())), () => { - this._$St.delete(e), this.h.unwatch(e), !1 === this.isConnected && this._$Sp(); - }; - } - performUpdate() { - this.isUpdatePending && (this._$S_(), this._$Si = !0, this._$So.set(this._$So.get() + 1), this._$Si = !1, this._$Sl()); - } - connectedCallback() { - super.connectedCallback(), this.requestUpdate(); - } - disconnectedCallback() { - super.disconnectedCallback(), queueMicrotask(() => { - !1 === this.isConnected && this._$Sp(); - }); - } - }; -} -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const s = (i, t) => { - const e = i._$AN; - if (void 0 === e) return !1; - for (const i of e) i._$AO?.(t, !1), s(i, t); - return !0; -}, o$6 = (i) => { - let t, e; - do { - if (void 0 === (t = i._$AM)) break; - e = t._$AN, e.delete(i), i = t; - } while (0 === e?.size); -}, r$3 = (i) => { - for (let t; t = i._$AM; i = t) { - let e = t._$AN; - if (void 0 === e) t._$AN = e = /* @__PURE__ */ new Set(); - else if (e.has(i)) break; - e.add(i), c(t); - } -}; -function h$2(i) { - void 0 !== this._$AN ? (o$6(this), this._$AM = i, r$3(this)) : this._$AM = i; -} -function n$3(i, t = !1, e = 0) { - const r = this._$AH, h = this._$AN; - if (void 0 !== h && 0 !== h.size) if (t) if (Array.isArray(r)) for (let i = e; i < r.length; i++) s(r[i], !1), o$6(r[i]); - else null != r && (s(r, !1), o$6(r)); - else s(this, i); -} -const c = (i) => { - i.type == t$4.CHILD && (i._$AP ??= n$3, i._$AQ ??= h$2); -}; -var f = class extends i$5 { - constructor() { - super(...arguments), this._$AN = void 0; - } - _$AT(i, t, e) { - super._$AT(i, t, e), r$3(this), this.isConnected = i._$AU; - } - _$AO(i, t = !0) { - i !== this.isConnected && (this.isConnected = i, i ? this.reconnected?.() : this.disconnected?.()), t && (s(this, i), o$6(this)); - } - setValue(t) { - if (r$8(this._$Ct)) this._$Ct._$AI(t, this); - else { - const i = [...this._$Ct._$AH]; - i[this._$Ci] = t, this._$Ct._$AI(i, this, 0); - } - } - disconnected() {} - reconnected() {} -}; -/** -* @license -* Copyright 2023 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -let o$5 = !1; -const n$2 = new Signal.subtle.Watcher(async () => { - o$5 || (o$5 = !0, queueMicrotask(() => { - o$5 = !1; - for (const i of n$2.getPending()) i.get(); - n$2.watch(); - })); -}); -var r$2 = class extends f { - _$S_() { - var i, t; - void 0 === this._$Sm && (this._$Sj = new Signal.Computed(() => { - var i; - const t = null === (i = this._$SW) || void 0 === i ? void 0 : i.get(); - return this.setValue(t), t; - }), this._$Sm = null !== (t = null === (i = this._$Sk) || void 0 === i ? void 0 : i.h) && void 0 !== t ? t : n$2, this._$Sm.watch(this._$Sj), Signal.subtle.untrack(() => { - var i; - return null === (i = this._$Sj) || void 0 === i ? void 0 : i.get(); - })); - } - _$Sp() { - void 0 !== this._$Sm && (this._$Sm.unwatch(this._$SW), this._$Sm = void 0); - } - render(i) { - return Signal.subtle.untrack(() => i.get()); - } - update(i, [t]) { - var o, n; - return null !== (o = this._$Sk) && void 0 !== o || (this._$Sk = null === (n = i.options) || void 0 === n ? void 0 : n.host), t !== this._$SW && void 0 !== this._$SW && this._$Sp(), this._$SW = t, this._$S_(), Signal.subtle.untrack(() => this._$SW.get()); - } - disconnected() { - this._$Sp(); - } - reconnected() { - this._$S_(); - } -}; -const h$1 = e$10(r$2), m = (o) => (t, ...m) => o(t, ...m.map((o) => o instanceof Signal.State || o instanceof Signal.Computed ? h$1(o) : o)); -m(b); -m(w); -Signal.State; -Signal.Computed; -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -function* o$3(o, f) { - if (void 0 !== o) { - let i = 0; - for (const t of o) yield f(t, i++); - } -} -let pending = false; -let watcher = new Signal.subtle.Watcher(() => { - if (!pending) { - pending = true; - queueMicrotask(() => { - pending = false; - flushPending(); - }); - } -}); -function flushPending() { - for (const signal of watcher.getPending()) signal.get(); - watcher.watch(); -} -/** -* ⚠️ WARNING: Nothing unwatches ⚠️ -* This will produce a memory leak. -*/ -function effect(cb) { - let c = new Signal.Computed(() => cb()); - watcher.watch(c); - c.get(); - return () => { - watcher.unwatch(c); - }; -} -const themeContext = n$7("A2UITheme"); -const structuralStyles = r$11(structuralStyles$1); -var ComponentRegistry = class { - constructor() { - this.registry = /* @__PURE__ */ new Map(); - } - register(typeName, constructor, tagName) { - if (!/^[a-zA-Z0-9]+$/.test(typeName)) throw new Error(`[Registry] Invalid typeName '${typeName}'. Must be alphanumeric.`); - this.registry.set(typeName, constructor); - const actualTagName = tagName || `a2ui-custom-${typeName.toLowerCase()}`; - const existingName = customElements.getName(constructor); - if (existingName) { - if (existingName !== actualTagName) throw new Error(`Component ${typeName} is already registered as ${existingName}, but requested as ${actualTagName}.`); - return; - } - if (!customElements.get(actualTagName)) customElements.define(actualTagName, constructor); - } - get(typeName) { - return this.registry.get(typeName); - } -}; -const componentRegistry = new ComponentRegistry(); -var __runInitializers$19 = function(thisArg, initializers, value) { - var useValue = arguments.length > 2; - for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); - return useValue ? value : void 0; -}; -var __esDecorate$19 = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { - function accept(f) { - if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); - return f; - } - var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; - var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; - var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); - var _, done = false; - for (var i = decorators.length - 1; i >= 0; i--) { - var context = {}; - for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; - for (var p in contextIn.access) context.access[p] = contextIn.access[p]; - context.addInitializer = function(f) { - if (done) throw new TypeError("Cannot add initializers after decoration has completed"); - extraInitializers.push(accept(f || null)); - }; - var result = (0, decorators[i])(kind === "accessor" ? { - get: descriptor.get, - set: descriptor.set - } : descriptor[key], context); - if (kind === "accessor") { - if (result === void 0) continue; - if (result === null || typeof result !== "object") throw new TypeError("Object expected"); - if (_ = accept(result.get)) descriptor.get = _; - if (_ = accept(result.set)) descriptor.set = _; - if (_ = accept(result.init)) initializers.unshift(_); - } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_); - else descriptor[key] = _; - } - if (target) Object.defineProperty(target, contextIn.name, descriptor); - done = true; -}; -let Root = (() => { - let _classDecorators = [t$1("a2ui-root")]; - let _classDescriptor; - let _classExtraInitializers = []; - let _classThis; - let _classSuper = o$7(i$6); - let _instanceExtraInitializers = []; - let _surfaceId_decorators; - let _surfaceId_initializers = []; - let _surfaceId_extraInitializers = []; - let _component_decorators; - let _component_initializers = []; - let _component_extraInitializers = []; - let _theme_decorators; - let _theme_initializers = []; - let _theme_extraInitializers = []; - let _childComponents_decorators; - let _childComponents_initializers = []; - let _childComponents_extraInitializers = []; - let _processor_decorators; - let _processor_initializers = []; - let _processor_extraInitializers = []; - let _dataContextPath_decorators; - let _dataContextPath_initializers = []; - let _dataContextPath_extraInitializers = []; - let _enableCustomElements_decorators; - let _enableCustomElements_initializers = []; - let _enableCustomElements_extraInitializers = []; - let _set_weight_decorators; - var Root = class extends _classSuper { - static { - _classThis = this; - } - static { - const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0; - _surfaceId_decorators = [n$6()]; - _component_decorators = [n$6()]; - _theme_decorators = [c$1({ context: themeContext })]; - _childComponents_decorators = [n$6({ attribute: false })]; - _processor_decorators = [n$6({ attribute: false })]; - _dataContextPath_decorators = [n$6()]; - _enableCustomElements_decorators = [n$6()]; - _set_weight_decorators = [n$6()]; - __esDecorate$19(this, null, _surfaceId_decorators, { - kind: "accessor", - name: "surfaceId", - static: false, - private: false, - access: { - has: (obj) => "surfaceId" in obj, - get: (obj) => obj.surfaceId, - set: (obj, value) => { - obj.surfaceId = value; - } - }, - metadata: _metadata - }, _surfaceId_initializers, _surfaceId_extraInitializers); - __esDecorate$19(this, null, _component_decorators, { - kind: "accessor", - name: "component", - static: false, - private: false, - access: { - has: (obj) => "component" in obj, - get: (obj) => obj.component, - set: (obj, value) => { - obj.component = value; - } - }, - metadata: _metadata - }, _component_initializers, _component_extraInitializers); - __esDecorate$19(this, null, _theme_decorators, { - kind: "accessor", - name: "theme", - static: false, - private: false, - access: { - has: (obj) => "theme" in obj, - get: (obj) => obj.theme, - set: (obj, value) => { - obj.theme = value; - } - }, - metadata: _metadata - }, _theme_initializers, _theme_extraInitializers); - __esDecorate$19(this, null, _childComponents_decorators, { - kind: "accessor", - name: "childComponents", - static: false, - private: false, - access: { - has: (obj) => "childComponents" in obj, - get: (obj) => obj.childComponents, - set: (obj, value) => { - obj.childComponents = value; - } - }, - metadata: _metadata - }, _childComponents_initializers, _childComponents_extraInitializers); - __esDecorate$19(this, null, _processor_decorators, { - kind: "accessor", - name: "processor", - static: false, - private: false, - access: { - has: (obj) => "processor" in obj, - get: (obj) => obj.processor, - set: (obj, value) => { - obj.processor = value; - } - }, - metadata: _metadata - }, _processor_initializers, _processor_extraInitializers); - __esDecorate$19(this, null, _dataContextPath_decorators, { - kind: "accessor", - name: "dataContextPath", - static: false, - private: false, - access: { - has: (obj) => "dataContextPath" in obj, - get: (obj) => obj.dataContextPath, - set: (obj, value) => { - obj.dataContextPath = value; - } - }, - metadata: _metadata - }, _dataContextPath_initializers, _dataContextPath_extraInitializers); - __esDecorate$19(this, null, _enableCustomElements_decorators, { - kind: "accessor", - name: "enableCustomElements", - static: false, - private: false, - access: { - has: (obj) => "enableCustomElements" in obj, - get: (obj) => obj.enableCustomElements, - set: (obj, value) => { - obj.enableCustomElements = value; - } - }, - metadata: _metadata - }, _enableCustomElements_initializers, _enableCustomElements_extraInitializers); - __esDecorate$19(this, null, _set_weight_decorators, { - kind: "setter", - name: "weight", - static: false, - private: false, - access: { - has: (obj) => "weight" in obj, - set: (obj, value) => { - obj.weight = value; - } - }, - metadata: _metadata - }, null, _instanceExtraInitializers); - __esDecorate$19(null, _classDescriptor = { value: _classThis }, _classDecorators, { - kind: "class", - name: _classThis.name, - metadata: _metadata - }, null, _classExtraInitializers); - Root = _classThis = _classDescriptor.value; - if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { - enumerable: true, - configurable: true, - writable: true, - value: _metadata - }); - } - #surfaceId_accessor_storage = (__runInitializers$19(this, _instanceExtraInitializers), __runInitializers$19(this, _surfaceId_initializers, null)); - get surfaceId() { - return this.#surfaceId_accessor_storage; - } - set surfaceId(value) { - this.#surfaceId_accessor_storage = value; - } - #component_accessor_storage = (__runInitializers$19(this, _surfaceId_extraInitializers), __runInitializers$19(this, _component_initializers, null)); - get component() { - return this.#component_accessor_storage; - } - set component(value) { - this.#component_accessor_storage = value; - } - #theme_accessor_storage = (__runInitializers$19(this, _component_extraInitializers), __runInitializers$19(this, _theme_initializers, void 0)); - get theme() { - return this.#theme_accessor_storage; - } - set theme(value) { - this.#theme_accessor_storage = value; - } - #childComponents_accessor_storage = (__runInitializers$19(this, _theme_extraInitializers), __runInitializers$19(this, _childComponents_initializers, null)); - get childComponents() { - return this.#childComponents_accessor_storage; - } - set childComponents(value) { - this.#childComponents_accessor_storage = value; - } - #processor_accessor_storage = (__runInitializers$19(this, _childComponents_extraInitializers), __runInitializers$19(this, _processor_initializers, null)); - get processor() { - return this.#processor_accessor_storage; - } - set processor(value) { - this.#processor_accessor_storage = value; - } - #dataContextPath_accessor_storage = (__runInitializers$19(this, _processor_extraInitializers), __runInitializers$19(this, _dataContextPath_initializers, "")); - get dataContextPath() { - return this.#dataContextPath_accessor_storage; - } - set dataContextPath(value) { - this.#dataContextPath_accessor_storage = value; - } - #enableCustomElements_accessor_storage = (__runInitializers$19(this, _dataContextPath_extraInitializers), __runInitializers$19(this, _enableCustomElements_initializers, false)); - get enableCustomElements() { - return this.#enableCustomElements_accessor_storage; - } - set enableCustomElements(value) { - this.#enableCustomElements_accessor_storage = value; - } - set weight(weight) { - this.#weight = weight; - this.style.setProperty("--weight", `${weight}`); - } - get weight() { - return this.#weight; - } - #weight = (__runInitializers$19(this, _enableCustomElements_extraInitializers), 1); - static { - this.styles = [structuralStyles, i$9` - :host { - display: flex; - flex-direction: column; - gap: 8px; - max-height: 80%; - } - `]; - } - /** - * Holds the cleanup function for our effect. - * We need this to stop the effect when the component is disconnected. - */ - #lightDomEffectDisposer = null; - willUpdate(changedProperties) { - if (changedProperties.has("childComponents")) { - if (this.#lightDomEffectDisposer) this.#lightDomEffectDisposer(); - this.#lightDomEffectDisposer = effect(() => { - const allChildren = this.childComponents ?? null; - D(this.renderComponentTree(allChildren), this, { host: this }); - }); - } - } - /** - * Clean up the effect when the component is removed from the DOM. - */ - disconnectedCallback() { - super.disconnectedCallback(); - if (this.#lightDomEffectDisposer) this.#lightDomEffectDisposer(); - } - /** - * Turns the SignalMap into a renderable TemplateResult for Lit. - */ - renderComponentTree(components) { - if (!components) return A; - if (!Array.isArray(components)) return A; - return b` ${o$3(components, (component) => { - if (this.enableCustomElements) { - const elCtor = componentRegistry.get(component.type) || customElements.get(component.type); - if (elCtor) { - const node = component; - const el = new elCtor(); - el.id = node.id; - if (node.slotName) el.slot = node.slotName; - el.component = node; - el.weight = node.weight ?? "initial"; - el.processor = this.processor; - el.surfaceId = this.surfaceId; - el.dataContextPath = node.dataContextPath ?? "/"; - for (const [prop, val] of Object.entries(component.properties)) el[prop] = val; - return b`${el}`; - } - } - switch (component.type) { - case "List": { - const node = component; - const childComponents = node.properties.children; - return b``; - } - case "Card": { - const node = component; - let childComponents = node.properties.children; - if (!childComponents && node.properties.child) childComponents = [node.properties.child]; - return b``; - } - case "Column": { - const node = component; - return b``; - } - case "Row": { - const node = component; - return b``; - } - case "Image": { - const node = component; - return b``; - } - case "Icon": { - const node = component; - return b``; - } - case "AudioPlayer": { - const node = component; - return b``; - } - case "Button": { - const node = component; - return b``; - } - case "Text": { - const node = component; - return b``; - } - case "CheckBox": { - const node = component; - return b``; - } - case "DateTimeInput": { - const node = component; - return b``; - } - case "Divider": { - const node = component; - return b``; - } - case "MultipleChoice": { - const node = component; - return b``; - } - case "Slider": { - const node = component; - return b``; - } - case "TextField": { - const node = component; - return b``; - } - case "Video": { - const node = component; - return b``; - } - case "Tabs": { - const node = component; - const titles = []; - const childComponents = []; - if (node.properties.tabItems) for (const item of node.properties.tabItems) { - titles.push(item.title); - childComponents.push(item.child); - } - return b``; - } - case "Modal": { - const node = component; - const childComponents = [node.properties.entryPointChild, node.properties.contentChild]; - node.properties.entryPointChild.slotName = "entry"; - return b``; - } - default: return this.renderCustomComponent(component); - } - })}`; - } - renderCustomComponent(component) { - if (!this.enableCustomElements) return; - const node = component; - const elCtor = componentRegistry.get(component.type) || customElements.get(component.type); - if (!elCtor) return b`Unknown element ${component.type}`; - const el = new elCtor(); - el.id = node.id; - if (node.slotName) el.slot = node.slotName; - el.component = node; - el.weight = node.weight ?? "initial"; - el.processor = this.processor; - el.surfaceId = this.surfaceId; - el.dataContextPath = node.dataContextPath ?? "/"; - for (const [prop, val] of Object.entries(component.properties)) el[prop] = val; - return b`${el}`; - } - render() { - return b``; - } - static { - __runInitializers$19(_classThis, _classExtraInitializers); - } - }; - return _classThis; -})(); -/** -* @license -* Copyright 2018 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const e$2 = e$10(class extends i$5 { - constructor(t) { - if (super(t), t.type !== t$4.ATTRIBUTE || "class" !== t.name || t.strings?.length > 2) throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute."); - } - render(t) { - return " " + Object.keys(t).filter((s) => t[s]).join(" ") + " "; - } - update(s, [i]) { - if (void 0 === this.st) { - this.st = /* @__PURE__ */ new Set(), void 0 !== s.strings && (this.nt = new Set(s.strings.join(" ").split(/\s/).filter((t) => "" !== t))); - for (const t in i) i[t] && !this.nt?.has(t) && this.st.add(t); - return this.render(i); - } - const r = s.element.classList; - for (const t of this.st) t in i || (r.remove(t), this.st.delete(t)); - for (const t in i) { - const s = !!i[t]; - s === this.st.has(t) || this.nt?.has(t) || (s ? (r.add(t), this.st.add(t)) : (r.remove(t), this.st.delete(t))); - } - return E; - } -}); -/** -* @license -* Copyright 2018 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const n$1 = "important", i = " !" + n$1, o$2 = e$10(class extends i$5 { - constructor(t) { - if (super(t), t.type !== t$4.ATTRIBUTE || "style" !== t.name || t.strings?.length > 2) throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute."); - } - render(t) { - return Object.keys(t).reduce((e, r) => { - const s = t[r]; - return null == s ? e : e + `${r = r.includes("-") ? r : r.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g, "-$&").toLowerCase()}:${s};`; - }, ""); - } - update(e, [r]) { - const { style: s } = e.element; - if (void 0 === this.ft) return this.ft = new Set(Object.keys(r)), this.render(r); - for (const t of this.ft) null == r[t] && (this.ft.delete(t), t.includes("-") ? s.removeProperty(t) : s[t] = null); - for (const t in r) { - const e = r[t]; - if (null != e) { - this.ft.add(t); - const r = "string" == typeof e && e.endsWith(i); - t.includes("-") || r ? s.setProperty(t, r ? e.slice(0, -11) : e, r ? n$1 : "") : s[t] = e; - } - } - return E; - } -}); -var __esDecorate$18 = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { - function accept(f) { - if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); - return f; - } - var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; - var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; - var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); - var _, done = false; - for (var i = decorators.length - 1; i >= 0; i--) { - var context = {}; - for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; - for (var p in contextIn.access) context.access[p] = contextIn.access[p]; - context.addInitializer = function(f) { - if (done) throw new TypeError("Cannot add initializers after decoration has completed"); - extraInitializers.push(accept(f || null)); - }; - var result = (0, decorators[i])(kind === "accessor" ? { - get: descriptor.get, - set: descriptor.set - } : descriptor[key], context); - if (kind === "accessor") { - if (result === void 0) continue; - if (result === null || typeof result !== "object") throw new TypeError("Object expected"); - if (_ = accept(result.get)) descriptor.get = _; - if (_ = accept(result.set)) descriptor.set = _; - if (_ = accept(result.init)) initializers.unshift(_); - } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_); - else descriptor[key] = _; - } - if (target) Object.defineProperty(target, contextIn.name, descriptor); - done = true; -}; -var __runInitializers$18 = function(thisArg, initializers, value) { - var useValue = arguments.length > 2; - for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); - return useValue ? value : void 0; -}; -(() => { - let _classDecorators = [t$1("a2ui-audioplayer")]; - let _classDescriptor; - let _classExtraInitializers = []; - let _classThis; - let _classSuper = Root; - let _url_decorators; - let _url_initializers = []; - let _url_extraInitializers = []; - var Audio = class extends _classSuper { - static { - _classThis = this; - } - static { - const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0; - _url_decorators = [n$6()]; - __esDecorate$18(this, null, _url_decorators, { - kind: "accessor", - name: "url", - static: false, - private: false, - access: { - has: (obj) => "url" in obj, - get: (obj) => obj.url, - set: (obj, value) => { - obj.url = value; - } - }, - metadata: _metadata - }, _url_initializers, _url_extraInitializers); - __esDecorate$18(null, _classDescriptor = { value: _classThis }, _classDecorators, { - kind: "class", - name: _classThis.name, - metadata: _metadata - }, null, _classExtraInitializers); - Audio = _classThis = _classDescriptor.value; - if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { - enumerable: true, - configurable: true, - writable: true, - value: _metadata - }); - } - #url_accessor_storage = __runInitializers$18(this, _url_initializers, null); - get url() { - return this.#url_accessor_storage; - } - set url(value) { - this.#url_accessor_storage = value; - } - static { - this.styles = [structuralStyles, i$9` - * { - box-sizing: border-box; - } - - :host { - display: block; - flex: var(--weight); - min-height: 0; - overflow: auto; - } - - audio { - display: block; - width: 100%; - } - `]; - } - #renderAudio() { - if (!this.url) return A; - if (this.url && typeof this.url === "object") { - if ("literalString" in this.url) return b`