From addb874a6410bf764b2253a00fac278a424e5a8f Mon Sep 17 00:00:00 2001 From: xaeon2026 Date: Mon, 9 Mar 2026 17:27:01 -0400 Subject: [PATCH 01/10] fix(codex): normalize openai-codex transport in all remaining runtime paths After #38736, openai-codex/gpt-5.4 still timed out in some paths because model discovery, media tools, and image understanding used the stale openai-responses transport instead of openai-codex-responses. Hoist normalizeResolvedProviderModel to a shared module and apply it at model discovery (Proxy wrapper on ModelRegistry), media tool resolution, and image understanding model lookup. Fixes #41282 --- src/agents/model.provider-normalization.ts | 62 +++++++++ .../model.provider-normalization.ts | 63 +-------- src/agents/pi-model-discovery.models.test.ts | 120 ++++++++++++++++++ src/agents/pi-model-discovery.ts | 41 +++++- src/agents/tools/media-tool-shared.test.ts | 52 ++++++++ src/agents/tools/media-tool-shared.ts | 6 +- .../providers/image.test.ts | 52 ++++++++ src/media-understanding/providers/image.ts | 12 +- 8 files changed, 342 insertions(+), 66 deletions(-) create mode 100644 src/agents/model.provider-normalization.ts create mode 100644 src/agents/pi-model-discovery.models.test.ts create mode 100644 src/agents/tools/media-tool-shared.test.ts diff --git a/src/agents/model.provider-normalization.ts b/src/agents/model.provider-normalization.ts new file mode 100644 index 00000000000..9050963a6dc --- /dev/null +++ b/src/agents/model.provider-normalization.ts @@ -0,0 +1,62 @@ +import type { Api, Model } from "@mariozechner/pi-ai"; +import { normalizeModelCompat } from "./model-compat.js"; +import { normalizeProviderId } from "./model-selection.js"; + +export const OPENAI_CODEX_BASE_URL = "https://chatgpt.com/backend-api"; + +function isOpenAIApiBaseUrl(baseUrl?: string): boolean { + const trimmed = baseUrl?.trim(); + if (!trimmed) { + return false; + } + return /^https?:\/\/api\.openai\.com(?:\/v1)?\/?$/i.test(trimmed); +} + +function isOpenAICodexBaseUrl(baseUrl?: string): boolean { + const trimmed = baseUrl?.trim(); + if (!trimmed) { + return false; + } + return /^https?:\/\/chatgpt\.com\/backend-api\/?$/i.test(trimmed); +} + +function normalizeOpenAICodexTransport(params: { + provider: string; + model: Model; +}): Model { + if (normalizeProviderId(params.provider) !== "openai-codex") { + return params.model; + } + + const useCodexTransport = + !params.model.baseUrl || + isOpenAIApiBaseUrl(params.model.baseUrl) || + isOpenAICodexBaseUrl(params.model.baseUrl); + + const nextApi = + useCodexTransport && params.model.api === "openai-responses" + ? ("openai-codex-responses" as const) + : params.model.api; + const nextBaseUrl = + nextApi === "openai-codex-responses" && + (!params.model.baseUrl || isOpenAIApiBaseUrl(params.model.baseUrl)) + ? OPENAI_CODEX_BASE_URL + : params.model.baseUrl; + + if (nextApi === params.model.api && nextBaseUrl === params.model.baseUrl) { + return params.model; + } + + return { + ...params.model, + api: nextApi, + baseUrl: nextBaseUrl, + } as Model; +} + +export function normalizeResolvedProviderModel(params: { + provider: string; + model: Model; +}): Model { + return normalizeModelCompat(normalizeOpenAICodexTransport(params)); +} diff --git a/src/agents/pi-embedded-runner/model.provider-normalization.ts b/src/agents/pi-embedded-runner/model.provider-normalization.ts index ecf1a25e7d3..b586519f012 100644 --- a/src/agents/pi-embedded-runner/model.provider-normalization.ts +++ b/src/agents/pi-embedded-runner/model.provider-normalization.ts @@ -1,62 +1 @@ -import type { Api, Model } from "@mariozechner/pi-ai"; -import { normalizeModelCompat } from "../model-compat.js"; -import { normalizeProviderId } from "../model-selection.js"; - -const OPENAI_CODEX_BASE_URL = "https://chatgpt.com/backend-api"; - -function isOpenAIApiBaseUrl(baseUrl?: string): boolean { - const trimmed = baseUrl?.trim(); - if (!trimmed) { - return false; - } - return /^https?:\/\/api\.openai\.com(?:\/v1)?\/?$/i.test(trimmed); -} - -function isOpenAICodexBaseUrl(baseUrl?: string): boolean { - const trimmed = baseUrl?.trim(); - if (!trimmed) { - return false; - } - return /^https?:\/\/chatgpt\.com\/backend-api\/?$/i.test(trimmed); -} - -function normalizeOpenAICodexTransport(params: { - provider: string; - model: Model; -}): Model { - if (normalizeProviderId(params.provider) !== "openai-codex") { - return params.model; - } - - const useCodexTransport = - !params.model.baseUrl || - isOpenAIApiBaseUrl(params.model.baseUrl) || - isOpenAICodexBaseUrl(params.model.baseUrl); - - const nextApi = - useCodexTransport && params.model.api === "openai-responses" - ? ("openai-codex-responses" as const) - : params.model.api; - const nextBaseUrl = - nextApi === "openai-codex-responses" && - (!params.model.baseUrl || isOpenAIApiBaseUrl(params.model.baseUrl)) - ? OPENAI_CODEX_BASE_URL - : params.model.baseUrl; - - if (nextApi === params.model.api && nextBaseUrl === params.model.baseUrl) { - return params.model; - } - - return { - ...params.model, - api: nextApi, - baseUrl: nextBaseUrl, - } as Model; -} - -export function normalizeResolvedProviderModel(params: { - provider: string; - model: Model; -}): Model { - return normalizeModelCompat(normalizeOpenAICodexTransport(params)); -} +export { normalizeResolvedProviderModel } from "../model.provider-normalization.js"; diff --git a/src/agents/pi-model-discovery.models.test.ts b/src/agents/pi-model-discovery.models.test.ts new file mode 100644 index 00000000000..28d9b17b94f --- /dev/null +++ b/src/agents/pi-model-discovery.models.test.ts @@ -0,0 +1,120 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const STALE_CODEX_MODEL = { + id: "gpt-5.4", + name: "GPT-5.4", + provider: "openai-codex", + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", + input: ["text", "image"], + reasoning: true, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_050_000, + maxTokens: 128_000, +}; + +const OPENAI_MODEL = { + id: "gpt-5.4", + name: "GPT-5.4", + provider: "openai", + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", + input: ["text", "image"], + reasoning: true, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_050_000, + maxTokens: 128_000, +}; + +afterEach(() => { + vi.resetModules(); + vi.doUnmock("@mariozechner/pi-coding-agent"); +}); + +describe("discoverModels", () => { + it("normalizes openai-codex models returned by registry discovery", async () => { + vi.doMock("@mariozechner/pi-coding-agent", () => { + class MockAuthStorage {} + class MockModelRegistry { + find(provider: string, modelId: string) { + if (provider === "openai-codex" && modelId === "gpt-5.4") { + return { ...STALE_CODEX_MODEL }; + } + if (provider === "openai" && modelId === "gpt-5.4") { + return { ...OPENAI_MODEL }; + } + return null; + } + + getAll() { + return [{ ...STALE_CODEX_MODEL }, { ...OPENAI_MODEL }]; + } + + getAvailable() { + return [{ ...STALE_CODEX_MODEL }]; + } + } + + return { + AuthStorage: MockAuthStorage, + ModelRegistry: MockModelRegistry, + }; + }); + + const { discoverModels } = await import("./pi-model-discovery.js"); + const registry = discoverModels({} as never, "/tmp/openclaw-agent"); + + expect(registry.find("openai-codex", "gpt-5.4")).toMatchObject({ + provider: "openai-codex", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api", + }); + expect(registry.find("openai", "gpt-5.4")).toMatchObject({ + provider: "openai", + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", + }); + expect(registry.getAll()).toContainEqual( + expect.objectContaining({ + provider: "openai-codex", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api", + }), + ); + expect(registry.getAvailable()).toContainEqual( + expect.objectContaining({ + provider: "openai-codex", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api", + }), + ); + }); + + it("does not rewrite custom openai-codex proxy endpoints", async () => { + vi.doMock("@mariozechner/pi-coding-agent", () => { + class MockAuthStorage {} + class MockModelRegistry { + find() { + return { + ...STALE_CODEX_MODEL, + baseUrl: "https://proxy.example.com/v1", + }; + } + } + + return { + AuthStorage: MockAuthStorage, + ModelRegistry: MockModelRegistry, + }; + }); + + const { discoverModels } = await import("./pi-model-discovery.js"); + const registry = discoverModels({} as never, "/tmp/openclaw-agent"); + + expect(registry.find("openai-codex", "gpt-5.4")).toMatchObject({ + provider: "openai-codex", + api: "openai-responses", + baseUrl: "https://proxy.example.com/v1", + }); + }); +}); diff --git a/src/agents/pi-model-discovery.ts b/src/agents/pi-model-discovery.ts index 6ed1fc0b338..b9ebca278e8 100644 --- a/src/agents/pi-model-discovery.ts +++ b/src/agents/pi-model-discovery.ts @@ -5,7 +5,9 @@ import type { AuthStorage as PiAuthStorage, ModelRegistry as PiModelRegistry, } from "@mariozechner/pi-coding-agent"; +import type { Api, Model } from "@mariozechner/pi-ai"; import { ensureAuthProfileStore } from "./auth-profiles.js"; +import { normalizeResolvedProviderModel } from "./model.provider-normalization.js"; import { resolvePiCredentialMapFromStore, type PiCredentialMap } from "./pi-auth-credentials.js"; const PiAuthStorageClass = PiCodingAgent.AuthStorage; @@ -148,5 +150,42 @@ export function discoverAuthStorage(agentDir: string): PiAuthStorage { } export function discoverModels(authStorage: PiAuthStorage, agentDir: string): PiModelRegistry { - return new PiModelRegistryClass(authStorage, path.join(agentDir, "models.json")); + const registry = new PiModelRegistryClass(authStorage, path.join(agentDir, "models.json")); + return wrapModelRegistryWithProviderNormalization(registry); +} + +function normalizeRegistryModel(model: unknown): unknown { + if (!model || typeof model !== "object") { + return model; + } + const provider = (model as { provider?: unknown }).provider; + if (typeof provider !== "string" || !provider.trim()) { + return model; + } + return normalizeResolvedProviderModel({ + provider, + model: model as Model, + }); +} + +function wrapModelRegistryWithProviderNormalization(registry: PiModelRegistry): PiModelRegistry { + return new Proxy(registry, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (typeof value !== "function" || prop === "constructor") { + return value; + } + if (prop === "find") { + return (provider: string, modelId: string) => + normalizeRegistryModel(Reflect.apply(value, target, [provider, modelId])); + } + if (prop === "getAll" || prop === "getAvailable") { + return () => { + const result = Reflect.apply(value, target, []); + return Array.isArray(result) ? result.map((model) => normalizeRegistryModel(model)) : result; + }; + } + return value.bind(target); + }, + }) as PiModelRegistry; } diff --git a/src/agents/tools/media-tool-shared.test.ts b/src/agents/tools/media-tool-shared.test.ts new file mode 100644 index 00000000000..23f9ff52ec5 --- /dev/null +++ b/src/agents/tools/media-tool-shared.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { resolveModelFromRegistry } from "./media-tool-shared.js"; + +const STALE_CODEX_MODEL = { + id: "gpt-5.4", + name: "GPT-5.4", + provider: "openai-codex", + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", + input: ["text", "image"], + reasoning: true, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_050_000, + maxTokens: 128_000, +} as const; + +describe("resolveModelFromRegistry", () => { + it("normalizes stale openai-codex transport/baseUrl pairs", () => { + const model = resolveModelFromRegistry({ + modelRegistry: { + find: () => ({ ...STALE_CODEX_MODEL }), + }, + provider: "openai-codex", + modelId: "gpt-5.4", + }); + + expect(model).toMatchObject({ + provider: "openai-codex", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api", + }); + }); + + it("keeps custom openai-codex proxy endpoints unchanged", () => { + const model = resolveModelFromRegistry({ + modelRegistry: { + find: () => ({ + ...STALE_CODEX_MODEL, + baseUrl: "https://proxy.example.com/v1", + }), + }, + provider: "openai-codex", + modelId: "gpt-5.4", + }); + + expect(model).toMatchObject({ + provider: "openai-codex", + api: "openai-responses", + baseUrl: "https://proxy.example.com/v1", + }); + }); +}); diff --git a/src/agents/tools/media-tool-shared.ts b/src/agents/tools/media-tool-shared.ts index 177bf296275..8809d08dafa 100644 --- a/src/agents/tools/media-tool-shared.ts +++ b/src/agents/tools/media-tool-shared.ts @@ -1,6 +1,7 @@ import { type Api, type Model } from "@mariozechner/pi-ai"; import type { OpenClawConfig } from "../../config/config.js"; import { getDefaultLocalRoots } from "../../web/media.js"; +import { normalizeResolvedProviderModel } from "../model.provider-normalization.js"; import type { ImageModelConfig } from "./image-tool.helpers.js"; import { getApiKeyForModel, normalizeWorkspaceDir, requireApiKey } from "./tool-runtime.helpers.js"; @@ -91,7 +92,10 @@ export function resolveModelFromRegistry(params: { if (!model) { throw new Error(`Unknown model: ${params.provider}/${params.modelId}`); } - return model; + return normalizeResolvedProviderModel({ + provider: params.provider, + model, + }); } export async function resolveModelRuntimeApiKey(params: { diff --git a/src/media-understanding/providers/image.test.ts b/src/media-understanding/providers/image.test.ts index 51c8739f43a..5adddbe4b48 100644 --- a/src/media-understanding/providers/image.test.ts +++ b/src/media-understanding/providers/image.test.ts @@ -131,6 +131,58 @@ describe("describeImageWithModel", () => { expect(minimaxUnderstandImageMock).not.toHaveBeenCalled(); }); + it("normalizes openai-codex models before generic completion", async () => { + discoverModelsMock.mockReturnValue({ + find: vi.fn(() => ({ + provider: "openai-codex", + id: "gpt-5.4", + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", + input: ["text", "image"], + })), + }); + completeMock.mockResolvedValue({ + role: "assistant", + api: "openai-codex-responses", + provider: "openai-codex", + model: "gpt-5.4", + stopReason: "stop", + timestamp: Date.now(), + content: [{ type: "text", text: "codex ok" }], + }); + + const { describeImageWithModel } = await import("./image.js"); + + const result = await describeImageWithModel({ + cfg: {}, + agentDir: "/tmp/openclaw-agent", + provider: "openai-codex", + model: "gpt-5.4", + buffer: Buffer.from("png-bytes"), + fileName: "image.png", + mime: "image/png", + prompt: "Describe the image.", + timeoutMs: 1000, + }); + + expect(result).toEqual({ + text: "codex ok", + model: "gpt-5.4", + }); + expect(setRuntimeApiKeyMock).toHaveBeenCalledWith("openai-codex", "oauth-test"); + expect(completeMock).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "openai-codex", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api", + }), + expect.any(Object), + expect.objectContaining({ + apiKey: "oauth-test", + }), + ); + }); + it("normalizes deprecated google flash ids before lookup and keeps profile auth selection", async () => { const findMock = vi.fn((provider: string, modelId: string) => { expect(provider).toBe("google"); diff --git a/src/media-understanding/providers/image.ts b/src/media-understanding/providers/image.ts index 1511a7c9bb9..c8415da00ab 100644 --- a/src/media-understanding/providers/image.ts +++ b/src/media-understanding/providers/image.ts @@ -2,6 +2,7 @@ import type { Api, Context, Model } from "@mariozechner/pi-ai"; import { complete } from "@mariozechner/pi-ai"; import { isMinimaxVlmModel, minimaxUnderstandImage } from "../../agents/minimax-vlm.js"; import { getApiKeyForModel, requireApiKey } from "../../agents/model-auth.js"; +import { normalizeResolvedProviderModel } from "../../agents/model.provider-normalization.js"; import { normalizeModelRef } from "../../agents/model-selection.js"; import { ensureOpenClawModelsJson } from "../../agents/models-config.js"; import { coerceImageAssistantText } from "../../agents/tools/image-tool.helpers.js"; @@ -25,10 +26,17 @@ export async function describeImageWithModel( const modelRegistry = discoverModels(authStorage, params.agentDir); // Keep direct media config entries compatible with deprecated provider model aliases. const resolvedRef = normalizeModelRef(params.provider, params.model); - const model = modelRegistry.find(resolvedRef.provider, resolvedRef.model) as Model | null; - if (!model) { + const discoveredModel = modelRegistry.find( + resolvedRef.provider, + resolvedRef.model, + ) as Model | null; + if (!discoveredModel) { throw new Error(`Unknown model: ${resolvedRef.provider}/${resolvedRef.model}`); } + const model = normalizeResolvedProviderModel({ + provider: resolvedRef.provider, + model: discoveredModel, + }); if (!model.input?.includes("image")) { throw new Error(`Model does not support images: ${params.provider}/${params.model}`); } From 1eff16adec9fe56afd155f5e3155bc35f5a6c9d2 Mon Sep 17 00:00:00 2001 From: Jackal Xin Date: Mon, 9 Mar 2026 22:23:45 -0400 Subject: [PATCH 02/10] fix: oxfmt formatting, secrets pragma, regenerate protocol Swift after rebase --- .../OpenClawProtocol/GatewayModels.swift | 96 +++++++++++++++++++ .../OpenClawProtocol/GatewayModels.swift | 96 +++++++++++++++++++ src/agents/pi-model-discovery.ts | 6 +- .../providers/image.test.ts | 2 +- src/media-understanding/providers/image.ts | 2 +- 5 files changed, 198 insertions(+), 4 deletions(-) diff --git a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift index a6223d95bee..cf69609e673 100644 --- a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift @@ -950,6 +950,102 @@ public struct NodeEventParams: Codable, Sendable { } } +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 diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index a6223d95bee..cf69609e673 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -950,6 +950,102 @@ public struct NodeEventParams: Codable, Sendable { } } +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 diff --git a/src/agents/pi-model-discovery.ts b/src/agents/pi-model-discovery.ts index b9ebca278e8..36004873de9 100644 --- a/src/agents/pi-model-discovery.ts +++ b/src/agents/pi-model-discovery.ts @@ -1,11 +1,11 @@ import fs from "node:fs"; import path from "node:path"; +import type { Api, Model } from "@mariozechner/pi-ai"; import * as PiCodingAgent from "@mariozechner/pi-coding-agent"; import type { AuthStorage as PiAuthStorage, ModelRegistry as PiModelRegistry, } from "@mariozechner/pi-coding-agent"; -import type { Api, Model } from "@mariozechner/pi-ai"; import { ensureAuthProfileStore } from "./auth-profiles.js"; import { normalizeResolvedProviderModel } from "./model.provider-normalization.js"; import { resolvePiCredentialMapFromStore, type PiCredentialMap } from "./pi-auth-credentials.js"; @@ -182,7 +182,9 @@ function wrapModelRegistryWithProviderNormalization(registry: PiModelRegistry): if (prop === "getAll" || prop === "getAvailable") { return () => { const result = Reflect.apply(value, target, []); - return Array.isArray(result) ? result.map((model) => normalizeRegistryModel(model)) : result; + return Array.isArray(result) + ? result.map((model) => normalizeRegistryModel(model)) + : result; }; } return value.bind(target); diff --git a/src/media-understanding/providers/image.test.ts b/src/media-understanding/providers/image.test.ts index 5adddbe4b48..2b8b49978de 100644 --- a/src/media-understanding/providers/image.test.ts +++ b/src/media-understanding/providers/image.test.ts @@ -178,7 +178,7 @@ describe("describeImageWithModel", () => { }), expect.any(Object), expect.objectContaining({ - apiKey: "oauth-test", + apiKey: "oauth-test", // pragma: allowlist secret }), ); }); diff --git a/src/media-understanding/providers/image.ts b/src/media-understanding/providers/image.ts index c8415da00ab..6d99caa2e7d 100644 --- a/src/media-understanding/providers/image.ts +++ b/src/media-understanding/providers/image.ts @@ -2,8 +2,8 @@ import type { Api, Context, Model } from "@mariozechner/pi-ai"; import { complete } from "@mariozechner/pi-ai"; import { isMinimaxVlmModel, minimaxUnderstandImage } from "../../agents/minimax-vlm.js"; import { getApiKeyForModel, requireApiKey } from "../../agents/model-auth.js"; -import { normalizeResolvedProviderModel } from "../../agents/model.provider-normalization.js"; import { normalizeModelRef } from "../../agents/model-selection.js"; +import { normalizeResolvedProviderModel } from "../../agents/model.provider-normalization.js"; import { ensureOpenClawModelsJson } from "../../agents/models-config.js"; import { coerceImageAssistantText } from "../../agents/tools/image-tool.helpers.js"; import type { ImageDescriptionRequest, ImageDescriptionResult } from "../types.js"; From 9e9c421af70e3f37bc30a73dcab3cf8ac6a36d87 Mon Sep 17 00:00:00 2001 From: Jackal Xin Date: Mon, 9 Mar 2026 22:32:00 -0400 Subject: [PATCH 03/10] fix: remove unnecessary type cast flagged by oxlint --- src/agents/pi-model-discovery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/pi-model-discovery.ts b/src/agents/pi-model-discovery.ts index 36004873de9..1dd36a2c805 100644 --- a/src/agents/pi-model-discovery.ts +++ b/src/agents/pi-model-discovery.ts @@ -189,5 +189,5 @@ function wrapModelRegistryWithProviderNormalization(registry: PiModelRegistry): } return value.bind(target); }, - }) as PiModelRegistry; + }); } From 9839dc9a950f6f33dfb5208a727d88630cfc59f0 Mon Sep 17 00:00:00 2001 From: xaeon2026 Date: Thu, 12 Mar 2026 05:02:47 -0400 Subject: [PATCH 04/10] fix(protocol): regenerate swift models after rebase --- .../Sources/OpenClawProtocol/GatewayModels.swift | 14 +++++--------- .../Sources/OpenClawProtocol/GatewayModels.swift | 14 +++++--------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift index ea85e6c1511..b743060f6c0 100644 --- a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift @@ -538,8 +538,6 @@ public struct AgentParams: Codable, Sendable { public let inputprovenance: [String: AnyCodable]? public let idempotencykey: String public let label: String? - public let spawnedby: String? - public let workspacedir: String? public init( message: String, @@ -566,9 +564,7 @@ public struct AgentParams: Codable, Sendable { internalevents: [[String: AnyCodable]]?, inputprovenance: [String: AnyCodable]?, idempotencykey: String, - label: String?, - spawnedby: String?, - workspacedir: String?) + label: String?) { self.message = message self.agentid = agentid @@ -595,8 +591,6 @@ public struct AgentParams: Codable, Sendable { self.inputprovenance = inputprovenance self.idempotencykey = idempotencykey self.label = label - self.spawnedby = spawnedby - self.workspacedir = workspacedir } private enum CodingKeys: String, CodingKey { @@ -625,8 +619,6 @@ public struct AgentParams: Codable, Sendable { case inputprovenance = "inputProvenance" case idempotencykey = "idempotencyKey" case label - case spawnedby = "spawnedBy" - case workspacedir = "workspaceDir" } } @@ -1336,6 +1328,7 @@ public struct SessionsPatchParams: Codable, Sendable { 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? @@ -1356,6 +1349,7 @@ public struct SessionsPatchParams: Codable, Sendable { execnode: AnyCodable?, model: AnyCodable?, spawnedby: AnyCodable?, + spawnedworkspacedir: AnyCodable?, spawndepth: AnyCodable?, subagentrole: AnyCodable?, subagentcontrolscope: AnyCodable?, @@ -1375,6 +1369,7 @@ public struct SessionsPatchParams: Codable, Sendable { self.execnode = execnode self.model = model self.spawnedby = spawnedby + self.spawnedworkspacedir = spawnedworkspacedir self.spawndepth = spawndepth self.subagentrole = subagentrole self.subagentcontrolscope = subagentcontrolscope @@ -1396,6 +1391,7 @@ public struct SessionsPatchParams: Codable, Sendable { case execnode = "execNode" case model case spawnedby = "spawnedBy" + case spawnedworkspacedir = "spawnedWorkspaceDir" case spawndepth = "spawnDepth" case subagentrole = "subagentRole" case subagentcontrolscope = "subagentControlScope" diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index ea85e6c1511..b743060f6c0 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -538,8 +538,6 @@ public struct AgentParams: Codable, Sendable { public let inputprovenance: [String: AnyCodable]? public let idempotencykey: String public let label: String? - public let spawnedby: String? - public let workspacedir: String? public init( message: String, @@ -566,9 +564,7 @@ public struct AgentParams: Codable, Sendable { internalevents: [[String: AnyCodable]]?, inputprovenance: [String: AnyCodable]?, idempotencykey: String, - label: String?, - spawnedby: String?, - workspacedir: String?) + label: String?) { self.message = message self.agentid = agentid @@ -595,8 +591,6 @@ public struct AgentParams: Codable, Sendable { self.inputprovenance = inputprovenance self.idempotencykey = idempotencykey self.label = label - self.spawnedby = spawnedby - self.workspacedir = workspacedir } private enum CodingKeys: String, CodingKey { @@ -625,8 +619,6 @@ public struct AgentParams: Codable, Sendable { case inputprovenance = "inputProvenance" case idempotencykey = "idempotencyKey" case label - case spawnedby = "spawnedBy" - case workspacedir = "workspaceDir" } } @@ -1336,6 +1328,7 @@ public struct SessionsPatchParams: Codable, Sendable { 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? @@ -1356,6 +1349,7 @@ public struct SessionsPatchParams: Codable, Sendable { execnode: AnyCodable?, model: AnyCodable?, spawnedby: AnyCodable?, + spawnedworkspacedir: AnyCodable?, spawndepth: AnyCodable?, subagentrole: AnyCodable?, subagentcontrolscope: AnyCodable?, @@ -1375,6 +1369,7 @@ public struct SessionsPatchParams: Codable, Sendable { self.execnode = execnode self.model = model self.spawnedby = spawnedby + self.spawnedworkspacedir = spawnedworkspacedir self.spawndepth = spawndepth self.subagentrole = subagentrole self.subagentcontrolscope = subagentcontrolscope @@ -1396,6 +1391,7 @@ public struct SessionsPatchParams: Codable, Sendable { case execnode = "execNode" case model case spawnedby = "spawnedBy" + case spawnedworkspacedir = "spawnedWorkspaceDir" case spawndepth = "spawnDepth" case subagentrole = "subagentRole" case subagentcontrolscope = "subagentControlScope" From 21900aeb25ff37180afcb464d4b30be8b02ed587 Mon Sep 17 00:00:00 2001 From: xaeon2026 Date: Thu, 12 Mar 2026 09:52:35 -0400 Subject: [PATCH 05/10] chore(ci): drop unrelated workflow drift from codex transport fix --- .github/workflows/auto-response.yml | 34 ++++++++++ .github/workflows/ci.yml | 28 -------- .github/workflows/codeql.yml | 6 +- .github/workflows/docker-release.yml | 8 --- .github/workflows/install-smoke.yml | 10 +-- .github/workflows/openclaw-npm-release.yml | 79 ++++++++++++++++++++++ 6 files changed, 120 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/openclaw-npm-release.yml diff --git a/.github/workflows/auto-response.yml b/.github/workflows/auto-response.yml index a40149b7ccb..d9d810bffa7 100644 --- a/.github/workflows/auto-response.yml +++ b/.github/workflows/auto-response.yml @@ -51,6 +51,7 @@ jobs: }, { label: "r: no-ci-pr", + close: true, message: "Please don't make PRs for test failures on main.\n\n" + "The team is aware of those and will handle them directly on the codebase, not only fixing the tests but also investigating what the root cause is. Having to sift through test-fix-PRs (including some that have been out of date for weeks...) on top of that doesn't help. There are already way too many PRs for humans to manage; please don't make the flood worse.\n\n" + @@ -392,6 +393,7 @@ jobs: } const invalidLabel = "invalid"; + const spamLabel = "r: spam"; const dirtyLabel = "dirty"; const noisyPrMessage = "Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch."; @@ -428,6 +430,21 @@ jobs: }); return; } + if (labelSet.has(spamLabel)) { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pullRequest.number, + state: "closed", + }); + await github.rest.issues.lock({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pullRequest.number, + lock_reason: "spam", + }); + return; + } if (labelSet.has(invalidLabel)) { await github.rest.issues.update({ owner: context.repo.owner, @@ -439,6 +456,23 @@ jobs: } } + if (issue && labelSet.has(spamLabel)) { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: "closed", + state_reason: "not_planned", + }); + await github.rest.issues.lock({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + lock_reason: "spam", + }); + return; + } + if (issue && labelSet.has(invalidLabel)) { await github.rest.issues.update({ owner: context.repo.owner, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d248d5c804..2562d84d223 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -302,34 +302,6 @@ jobs: python -m pip install --upgrade pip python -m pip install pre-commit - - name: Detect secrets - run: | - set -euo pipefail - - if [ "${{ github.event_name }}" = "push" ]; then - echo "Running full detect-secrets scan on push." - pre-commit run --all-files detect-secrets - exit 0 - fi - - BASE="${{ github.event.pull_request.base.sha }}" - changed_files=() - if git rev-parse --verify "$BASE^{commit}" >/dev/null 2>&1; then - while IFS= read -r path; do - [ -n "$path" ] || continue - [ -f "$path" ] || continue - changed_files+=("$path") - done < <(git diff --name-only --diff-filter=ACMR "$BASE" HEAD) - fi - - if [ "${#changed_files[@]}" -gt 0 ]; then - echo "Running detect-secrets on ${#changed_files[@]} changed file(s)." - pre-commit run detect-secrets --files "${changed_files[@]}" - else - echo "Falling back to full detect-secrets scan." - pre-commit run --all-files detect-secrets - fi - - name: Detect committed private keys run: pre-commit run --all-files detect-private-key diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9b78a3c6172..1d8e473af4f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -93,7 +93,11 @@ jobs: - name: Setup Swift build tools if: matrix.needs_swift_tools - run: brew install xcodegen swiftlint swiftformat + run: | + sudo xcode-select -s /Applications/Xcode_26.1.app + xcodebuild -version + brew install xcodegen swiftlint swiftformat + swift --version - name: Initialize CodeQL uses: github/codeql-action/init@v4 diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index f991b7f8653..2cc29748c91 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -109,8 +109,6 @@ jobs: labels: ${{ steps.labels.outputs.value }} provenance: false push: true - cache-from: type=gha,scope=docker-release-amd64 - cache-to: type=gha,mode=max,scope=docker-release-amd64 - name: Build and push amd64 slim image id: build-slim @@ -124,8 +122,6 @@ jobs: labels: ${{ steps.labels.outputs.value }} provenance: false push: true - cache-from: type=gha,scope=docker-release-amd64 - cache-to: type=gha,mode=max,scope=docker-release-amd64 # Build arm64 images (default + slim share the build stage cache) build-arm64: @@ -214,8 +210,6 @@ jobs: labels: ${{ steps.labels.outputs.value }} provenance: false push: true - cache-from: type=gha,scope=docker-release-arm64 - cache-to: type=gha,mode=max,scope=docker-release-arm64 - name: Build and push arm64 slim image id: build-slim @@ -229,8 +223,6 @@ jobs: labels: ${{ steps.labels.outputs.value }} provenance: false push: true - cache-from: type=gha,scope=docker-release-arm64 - cache-to: type=gha,mode=max,scope=docker-release-arm64 # Create multi-platform manifests create-manifest: diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index 36f64d2d6ad..f18ba38a091 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -43,6 +43,8 @@ jobs: - name: Set up Docker Builder uses: useblacksmith/setup-docker-builder@v1 + # Blacksmith can fall back to the local docker driver, which rejects gha + # cache export/import. Keep smoke builds driver-agnostic. - name: Build root Dockerfile smoke image uses: useblacksmith/build-push-action@v2 with: @@ -52,8 +54,6 @@ jobs: load: true push: false provenance: false - cache-from: type=gha,scope=install-smoke-root-dockerfile - cache-to: type=gha,mode=max,scope=install-smoke-root-dockerfile - name: Run root Dockerfile CLI smoke run: | @@ -73,8 +73,6 @@ jobs: load: true push: false provenance: false - cache-from: type=gha,scope=install-smoke-root-dockerfile-ext - cache-to: type=gha,mode=max,scope=install-smoke-root-dockerfile-ext - name: Smoke test Dockerfile with extension build arg run: | @@ -89,8 +87,6 @@ jobs: load: true push: false provenance: false - cache-from: type=gha,scope=install-smoke-installer-root - cache-to: type=gha,mode=max,scope=install-smoke-installer-root - name: Build installer non-root image if: github.event_name != 'pull_request' @@ -102,8 +98,6 @@ jobs: load: true push: false provenance: false - cache-from: type=gha,scope=install-smoke-installer-nonroot - cache-to: type=gha,mode=max,scope=install-smoke-installer-nonroot - name: Run installer docker tests env: diff --git a/.github/workflows/openclaw-npm-release.yml b/.github/workflows/openclaw-npm-release.yml new file mode 100644 index 00000000000..09126ed6ad2 --- /dev/null +++ b/.github/workflows/openclaw-npm-release.yml @@ -0,0 +1,79 @@ +name: OpenClaw NPM Release + +on: + push: + tags: + - "v*" + +concurrency: + group: openclaw-npm-release-${{ github.ref }} + cancel-in-progress: false + +env: + NODE_VERSION: "22.x" + PNPM_VERSION: "10.23.0" + +jobs: + publish_openclaw_npm: + # npm trusted publishing + provenance requires a GitHub-hosted runner. + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + with: + node-version: ${{ env.NODE_VERSION }} + pnpm-version: ${{ env.PNPM_VERSION }} + install-bun: "false" + use-sticky-disk: "false" + + - name: Validate release tag and package metadata + env: + RELEASE_SHA: ${{ github.sha }} + RELEASE_TAG: ${{ github.ref_name }} + RELEASE_MAIN_REF: origin/main + run: | + set -euo pipefail + # Fetch the full main ref so merge-base ancestry checks keep working + # for older tagged commits that are still contained in main. + git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main + pnpm release:openclaw:npm:check + + - name: Ensure version is not already published + run: | + set -euo pipefail + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + if npm view "openclaw@${PACKAGE_VERSION}" version >/dev/null 2>&1; then + echo "openclaw@${PACKAGE_VERSION} is already published on npm." + exit 1 + fi + + echo "Publishing openclaw@${PACKAGE_VERSION}" + + - name: Check + run: pnpm check + + - name: Build + run: pnpm build + + - name: Verify release contents + run: pnpm release:check + + - name: Publish + run: | + set -euo pipefail + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + if [[ "$PACKAGE_VERSION" == *-beta.* ]]; then + npm publish --access public --tag beta --provenance + else + npm publish --access public --provenance + fi From caf6d02056110cc45fe52844d9535137f857e1b9 Mon Sep 17 00:00:00 2001 From: xaeon2026 Date: Fri, 20 Mar 2026 11:45:19 -0400 Subject: [PATCH 06/10] style: fix oxfmt in docs/automation/standing-orders for PR #41450 --- docs/automation/standing-orders.md | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/automation/standing-orders.md b/docs/automation/standing-orders.md index 495d6adee05..b0d52494fdb 100644 --- a/docs/automation/standing-orders.md +++ b/docs/automation/standing-orders.md @@ -16,12 +16,14 @@ This is the difference between telling your assistant "send the weekly report" e ## Why Standing Orders? **Without standing orders:** + - You must prompt the agent for every task - The agent sits idle between requests - Routine work gets forgotten or delayed - You become the bottleneck **With standing orders:** + - The agent executes autonomously within defined boundaries - Routine work happens on schedule without prompting - You only get involved for exceptions and approvals @@ -55,6 +57,7 @@ Put standing orders in `AGENTS.md` to guarantee they're loaded every session. Th **Escalation:** If data source is unavailable or metrics look unusual (>2σ from norm) ### Execution Steps + 1. Pull metrics from configured sources 2. Compare to prior week and targets 3. Generate report in Reports/weekly/YYYY-MM-DD.md @@ -62,6 +65,7 @@ Put standing orders in `AGENTS.md` to guarantee they're loaded every session. Th 5. Log completion to Agent/Logs/ ### What NOT to Do + - Do not send reports to external parties - Do not modify source data - Do not skip delivery if metrics look bad — report accurately @@ -105,11 +109,13 @@ openclaw cron create \ **Trigger:** Weekly cycle (Monday review → mid-week drafts → Friday brief) ### Weekly Cycle + - **Monday:** Review platform metrics and audience engagement - **Tuesday–Thursday:** Draft social posts, create blog content - **Friday:** Compile weekly marketing brief → deliver to owner ### Content Rules + - Voice must match the brand (see SOUL.md or brand voice guide) - Never identify as AI in public-facing content - Include metrics when available @@ -126,6 +132,7 @@ openclaw cron create \ **Trigger:** New data file detected OR scheduled monthly cycle ### When New Data Arrives + 1. Detect new file in designated input directory 2. Parse and categorize all transactions 3. Compare against budget targets @@ -134,6 +141,7 @@ openclaw cron create \ 6. Deliver summary to owner via configured channel ### Escalation Rules + - Single item > $500: immediate alert - Category > budget by 20%: flag in report - Unrecognizable transaction: ask owner for categorization @@ -150,18 +158,20 @@ openclaw cron create \ **Trigger:** Every heartbeat cycle ### Checks + - Service health endpoints responding - Disk space above threshold - Pending tasks not stale (>24 hours) - Delivery channels operational ### Response Matrix -| Condition | Action | Escalate? | -|-----------|--------|-----------| -| Service down | Restart automatically | Only if restart fails 2x | -| Disk space < 10% | Alert owner | Yes | -| Stale task > 24h | Remind owner | No | -| Channel offline | Log and retry next cycle | If offline > 2 hours | + +| Condition | Action | Escalate? | +| ---------------- | ------------------------ | ------------------------ | +| Service down | Restart automatically | Only if restart fails 2x | +| Disk space < 10% | Alert owner | Yes | +| Stale task > 24h | Remind owner | No | +| Channel offline | Log and retry next cycle | If offline > 2 hours | ``` ## The Execute-Verify-Report Pattern @@ -174,6 +184,7 @@ Standing orders work best when combined with strict execution discipline. Every ```markdown ### Execution Rules + - Every task follows Execute-Verify-Report. No exceptions. - "I'll do that" is not execution. Do it, then report. - "Done" without verification is not acceptable. Prove it. @@ -192,20 +203,25 @@ For agents managing multiple concerns, organize standing orders as separate prog # Standing Orders ## Program 1: [Domain A] (Weekly) + ... ## Program 2: [Domain B] (Monthly + On-Demand) + ... ## Program 3: [Domain C] (As-Needed) + ... ## Escalation Rules (All Programs) + - [Common escalation criteria] - [Approval gates that apply across programs] ``` Each program should have: + - Its own **trigger cadence** (weekly, monthly, event-driven, continuous) - Its own **approval gates** (some programs need more oversight than others) - Clear **boundaries** (the agent should know where one program ends and another begins) @@ -213,6 +229,7 @@ Each program should have: ## Best Practices ### Do + - Start with narrow authority and expand as trust builds - Define explicit approval gates for high-risk actions - Include "What NOT to do" sections — boundaries matter as much as permissions @@ -221,6 +238,7 @@ Each program should have: - Update standing orders as your needs evolve — they're living documents ### Don't + - Grant broad authority on day one ("do whatever you think is best") - Skip escalation rules — every program needs a "when to stop and ask" clause - Assume the agent will remember verbal instructions — put everything in the file From 370b24e8795fbb9f56cb73a46c72fb45b44e586c Mon Sep 17 00:00:00 2001 From: xaeon2026 Date: Fri, 20 Mar 2026 12:45:12 -0400 Subject: [PATCH 07/10] docs(azure): satisfy markdown fence spacing lint --- docs/install/azure.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/install/azure.md b/docs/install/azure.md index 7c6abae64fe..012434bc43f 100644 --- a/docs/install/azure.md +++ b/docs/install/azure.md @@ -284,10 +284,12 @@ Azure Bastion Standard SKU runs approximately **\$140/month** and the VM (Standa To reduce costs: - **Deallocate the VM** when not in use (stops compute billing; disk charges remain). The OpenClaw Gateway will not be reachable while the VM is deallocated — restart it when you need it live again: + ```bash az vm deallocate -g "${RG}" -n "${VM_NAME}" az vm start -g "${RG}" -n "${VM_NAME}" # restart later ``` + - **Delete Bastion when not needed** and recreate it when you need SSH access. Bastion is the largest cost component and takes only a few minutes to provision. - **Use the Basic Bastion SKU** (~\$38/month) if you only need Portal-based SSH and don't require CLI tunneling (`az network bastion ssh`). From 83559227599547f52c43c73f38d31e5f78ebc488 Mon Sep 17 00:00:00 2001 From: xaeon2026 Date: Fri, 20 Mar 2026 13:46:11 -0400 Subject: [PATCH 08/10] fix(line): restore runtime-api exports for setup helpers --- extensions/line/runtime-api.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/extensions/line/runtime-api.ts b/extensions/line/runtime-api.ts index af6082ba155..e439c4020b0 100644 --- a/extensions/line/runtime-api.ts +++ b/extensions/line/runtime-api.ts @@ -1 +1,19 @@ -export * from "openclaw/plugin-sdk/line-core"; +// Private runtime barrel for the bundled LINE extension. +// Keep this barrel thin and aligned with the local extension surface. + +export type { OpenClawConfig } from "openclaw/plugin-sdk/line-core"; +export type { ChannelSetupDmPolicy, ChannelSetupWizard } from "openclaw/plugin-sdk/channel-setup"; +export type { LineConfig, ResolvedLineAccount } from "openclaw/plugin-sdk/line-core"; +export { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema"; +export { + DEFAULT_ACCOUNT_ID, + formatDocsLink, + LineConfigSchema, + listLineAccountIds, + normalizeAccountId, + resolveDefaultLineAccountId, + resolveExactLineGroupConfigKey, + resolveLineAccount, + setSetupChannelEnabled, + splitSetupEntries, +} from "openclaw/plugin-sdk/line-core"; From a642bfa7b3f1d84d1bfde53072e4996c37287eba Mon Sep 17 00:00:00 2001 From: xaeon2026 Date: Fri, 20 Mar 2026 17:45:08 -0400 Subject: [PATCH 09/10] fix(line): restore runtime-api imports to local plugin-sdk paths --- extensions/line/runtime-api.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/extensions/line/runtime-api.ts b/extensions/line/runtime-api.ts index e439c4020b0..675c11a7467 100644 --- a/extensions/line/runtime-api.ts +++ b/extensions/line/runtime-api.ts @@ -1,19 +1,13 @@ // Private runtime barrel for the bundled LINE extension. // Keep this barrel thin and aligned with the local extension surface. -export type { OpenClawConfig } from "openclaw/plugin-sdk/line-core"; -export type { ChannelSetupDmPolicy, ChannelSetupWizard } from "openclaw/plugin-sdk/channel-setup"; -export type { LineConfig, ResolvedLineAccount } from "openclaw/plugin-sdk/line-core"; -export { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema"; +export * from "../../src/plugin-sdk/line.js"; export { DEFAULT_ACCOUNT_ID, formatDocsLink, - LineConfigSchema, - listLineAccountIds, - normalizeAccountId, - resolveDefaultLineAccountId, resolveExactLineGroupConfigKey, - resolveLineAccount, setSetupChannelEnabled, splitSetupEntries, -} from "openclaw/plugin-sdk/line-core"; + type ChannelSetupDmPolicy, + type ChannelSetupWizard, +} from "../../src/plugin-sdk/line-core.js"; From 579cf19054a7589eaae80f944b737d12f32b0a13 Mon Sep 17 00:00:00 2001 From: xaeon2026 Date: Sat, 21 Mar 2026 01:45:33 -0400 Subject: [PATCH 10/10] fix(ci): sync plugin extension import boundary inventory --- .../plugin-extension-import-boundary-inventory.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/fixtures/plugin-extension-import-boundary-inventory.json b/test/fixtures/plugin-extension-import-boundary-inventory.json index 0894fe0d5b5..ead171321f9 100644 --- a/test/fixtures/plugin-extension-import-boundary-inventory.json +++ b/test/fixtures/plugin-extension-import-boundary-inventory.json @@ -31,14 +31,6 @@ "resolvedPath": "extensions/imessage/runtime-api.js", "reason": "imports extension-owned file from src/plugins" }, - { - "file": "src/plugins/runtime/runtime-matrix.ts", - "line": 4, - "kind": "import", - "specifier": "../../../extensions/matrix/runtime-api.js", - "resolvedPath": "extensions/matrix/runtime-api.js", - "reason": "imports extension-owned file from src/plugins" - }, { "file": "src/plugins/runtime/runtime-slack-ops.runtime.ts", "line": 10,