Compare commits

...

16 Commits

Author SHA1 Message Date
Peter Steinberger
ddd05f4e89 fix: guard empty docker host args in install smoke 2026-04-21 14:18:09 +01:00
Peter Steinberger
bfde3c98a4 test: accept guarded memory fallback phrasing 2026-04-21 13:53:19 +01:00
Peter Steinberger
835de92b7a test: relax active memory qa debug status 2026-04-21 13:36:49 +01:00
Peter Steinberger
2020e63bd2 test: harden repo contract qa scenario 2026-04-21 13:18:10 +01:00
Peter Steinberger
b835337cd6 test: filter live qa scenario lanes 2026-04-21 12:43:30 +01:00
Peter Steinberger
7e4a5f8a6e test: accept xhigh thinking remap in qa 2026-04-21 12:26:11 +01:00
Peter Steinberger
8b3ddb28cd test: accept explicit newer memory ranking context 2026-04-21 11:59:29 +01:00
Peter Steinberger
ca245b8621 test: relax live active memory qa waits 2026-04-21 11:53:35 +01:00
Peter Steinberger
2db45c7892 fix: avoid empty bash arrays in linux smoke 2026-04-21 11:13:18 +01:00
Peter Steinberger
8ce7c4f08b fix: support older shells in parallels smoke 2026-04-21 11:03:34 +01:00
Peter Steinberger
87b81fa66f test: accept codex active-model fallback 2026-04-21 10:35:15 +01:00
Peter Steinberger
e57e54e591 fix: run packed bundled postinstall in release check 2026-04-21 09:34:33 +01:00
Peter Steinberger
adef75c1e1 chore: refresh plugin sdk api baseline 2026-04-21 09:25:56 +01:00
Peter Steinberger
ed6ccc9923 chore: refresh config docs baseline 2026-04-21 09:24:34 +01:00
Peter Steinberger
c4ddaf63fd chore: refresh bundled channel config metadata 2026-04-21 09:22:19 +01:00
Peter Steinberger
c127812bba chore: prepare 2026.4.20 beta 1 2026-04-21 09:04:20 +01:00
27 changed files with 227 additions and 59 deletions

View File

@@ -1,4 +1,4 @@
e3a16ceb9e933c5b707b717c18a1d9d50f98e687a98e6c35f4f3a290f7036a62 config-baseline.json
ae1ab87635e7bf613c84fee04425af901ceeb67fb5dbcf1c74095aa00a59ee88 config-baseline.core.json
e239cc20f20f8d0172812bc0ad3ee6df52da88e2e2702e3d03a47e01561132ae config-baseline.channel.json
8fb3a1cf5fe56ab8fc2cb46341c3403aed32b0d1f0aaeac0e96cd3599db4f06e config-baseline.plugin.json
aa12edd01845f5cabac04befcd258371b2c3b4c95203a5fe540fe871af5334ab config-baseline.json
7956c319e82d288d496a51cb2ff4485ab72ef4900cb089f99e1df8b9ef3bfb73 config-baseline.core.json
702f21ae56b489422dd9a0ea64a982822bfce0145c3a53315d15a2f8f91baf92 config-baseline.channel.json
17a73724e5082b3aa846c220d38115916fb6003887439e6794510a99fc73f7de config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
f135ddc1802b7f8b2d29bf495fd0ac1f497a89bab8164ca8c7c8f18efc010e6e plugin-sdk-api-baseline.json
a47d06095ec5c3701a94888a11e89700d8a8511db46fa3122fb9407e160707b6 plugin-sdk-api-baseline.jsonl
c923c90f11cc188755b341778fb8975ff6ff8714ebf305189babd2953fcd21fa plugin-sdk-api-baseline.json
6f43b0998f301dad7a68803f2863bca581c24edcd4e917cd5afac79accb46472 plugin-sdk-api-baseline.jsonl

View File

@@ -26,6 +26,14 @@ describe("qa model-switch evaluation", () => {
).toBe(true);
});
it("accepts concise handed-off phrasing from live models", () => {
expect(
hasModelSwitchContinuityEvidence(
"The harness has handed off to the alternate model for this turn, and the read tool confirms continued access to the QA scenario pack mission.",
),
).toBe(true);
});
it("accepts concise paraphrases of the kickoff task after a handoff", () => {
expect(
hasModelSwitchContinuityEvidence(

View File

@@ -3,7 +3,11 @@ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtim
export function hasModelSwitchContinuityEvidence(text: string) {
const lower = normalizeLowercaseStringOrEmpty(text);
const mentionsHandoff =
lower.includes("handoff") || lower.includes("model switch") || lower.includes("switched");
lower.includes("handoff") ||
lower.includes("handed off") ||
lower.includes("handed-off") ||
lower.includes("model switch") ||
lower.includes("switched");
const mentionsKickoffTask =
lower.includes("qa_kickoff_task") ||
lower.includes("qa/scenarios/index.md") ||

View File

@@ -127,8 +127,8 @@ describe("qa scenario catalog", () => {
const scenario = readQaScenarioById("gpt54-thinking-visibility-switch");
const config = readQaScenarioExecutionConfig("gpt54-thinking-visibility-switch") as
| {
requiredLiveProvider?: string;
requiredLiveModel?: string;
requiredProvider?: string;
requiredModel?: string;
offDirective?: string;
maxDirective?: string;
reasoningDirective?: string;
@@ -136,8 +136,8 @@ describe("qa scenario catalog", () => {
| undefined;
expect(scenario.sourcePath).toBe("qa/scenarios/models/gpt54-thinking-visibility-switch.md");
expect(config?.requiredLiveProvider).toBe("openai");
expect(config?.requiredLiveModel).toBe("gpt-5.4");
expect(config?.requiredProvider).toBe("openai");
expect(config?.requiredModel).toBe("gpt-5.4");
expect(config?.offDirective).toBe("/think off");
expect(config?.maxDirective).toBe("/think max");
expect(config?.reasoningDirective).toBe("/reasoning on");

View File

@@ -250,4 +250,38 @@ describe("qa suite planning helpers", () => {
}).map((scenario) => scenario.id),
).toEqual(["generic", "claude-subscription"]);
});
it("filters env-gated scenarios from an implicit live lane", () => {
const previous = process.env.OPENCLAW_LIVE_SETUP_TOKEN_VALUE;
delete process.env.OPENCLAW_LIVE_SETUP_TOKEN_VALUE;
try {
const scenarios = [
makeQaSuiteTestScenario("generic"),
makeQaSuiteTestScenario("anthropic-api-key", {
config: { requiredProvider: "anthropic", requiredModel: "claude-opus-4-6" },
}),
makeQaSuiteTestScenario("anthropic-setup-token", {
config: {
requiredProvider: "anthropic",
requiredModel: "claude-opus-4-6",
requiredEnv: "OPENCLAW_LIVE_SETUP_TOKEN_VALUE",
},
}),
];
expect(
selectQaSuiteScenarios({
scenarios,
providerMode: "live-frontier",
primaryModel: "anthropic/claude-opus-4-6",
}).map((scenario) => scenario.id),
).toEqual(["generic", "anthropic-api-key"]);
} finally {
if (previous === undefined) {
delete process.env.OPENCLAW_LIVE_SETUP_TOKEN_VALUE;
} else {
process.env.OPENCLAW_LIVE_SETUP_TOKEN_VALUE = previous;
}
}
});
});

View File

@@ -32,10 +32,12 @@ function scenarioMatchesLiveLane(params: {
primaryModel: string;
providerMode: QaProviderMode;
claudeCliAuthMode?: QaCliBackendAuthMode;
env?: NodeJS.ProcessEnv;
}) {
if (getQaProvider(params.providerMode).kind !== "live") {
return true;
}
const env = params.env ?? process.env;
const selected = splitModelRef(params.primaryModel);
const config = params.scenario.execution.config ?? {};
const requiredProvider = normalizeQaConfigString(config.requiredProvider);
@@ -50,6 +52,10 @@ function scenarioMatchesLiveLane(params: {
if (requiredAuthMode && params.claudeCliAuthMode !== requiredAuthMode) {
return false;
}
const requiredEnv = normalizeQaConfigString(config.requiredEnv);
if (requiredEnv && !env[requiredEnv]?.trim()) {
return false;
}
return true;
}

View File

@@ -1,6 +1,6 @@
{
"name": "openclaw",
"version": "2026.4.20",
"version": "2026.4.20-beta.1",
"description": "Multi-channel AI gateway with extensible messaging integrations",
"keywords": [],
"homepage": "https://github.com/openclaw/openclaw#readme",

View File

@@ -49,8 +49,14 @@ execution:
Evidence path: AGENT.md -> SOUL.md -> FOLLOWTHROUGH_INPUT.md -> repo-contract-summary.txt
prompt: |-
Repo contract followthrough check. Read AGENT.md, SOUL.md, and FOLLOWTHROUGH_INPUT.md first.
Then follow the repo contract exactly, write ./repo-contract-summary.txt, and reply with
three labeled lines: Read, Wrote, Status.
Then use the write tool to create ./repo-contract-summary.txt with this exact body:
Repo contract
Evidence path: AGENT.md -> SOUL.md -> FOLLOWTHROUGH_INPUT.md -> repo-contract-summary.txt
Status: complete
Do not send the final reply until ./repo-contract-summary.txt exists. After writing it, reply with
three labeled lines only: Read, Wrote, Status.
Do not stop after planning and do not ask for permission before the first feasible action.
expectedReplyAll:
- "read:"

View File

@@ -195,7 +195,7 @@ steps:
- lambda:
async: true
expr: "await (async () => { const entries = (await fs.readdir(transcriptRoot).catch(() => [])).filter((entry) => entry.endsWith('.jsonl')).toSorted(); return entries.length > 0 ? path.join(transcriptRoot, entries.at(-1)) : undefined; })()"
- 10000
- expr: liveTurnTimeoutMs(env, 30000)
- call: fs.readFile
saveAs: transcriptText
args:
@@ -207,13 +207,12 @@ steps:
- assert:
expr: "transcriptText.includes('memory_get')"
message: active memory transcript missing memory_get
- call: waitForCondition
saveAs: activeSessionEntry
args:
- lambda:
async: true
expr: "await (async () => { const store = await readRawQaSessionStore(env); const entry = store[activeSessionKey]; if (!entry || !Array.isArray(entry.pluginDebugEntries)) return undefined; return entry.pluginDebugEntries.some((pluginEntry) => pluginEntry?.pluginId === 'active-memory' && Array.isArray(pluginEntry.lines) && pluginEntry.lines.some((line) => line.includes('Active Memory: status=ok'))) ? entry : undefined; })()"
- 10000
- set: activeSessionStore
value:
expr: "await readRawQaSessionStore(env)"
- assert:
expr: "Array.isArray(activeSessionStore[activeSessionKey]?.pluginDebugEntries) && activeSessionStore[activeSessionKey].pluginDebugEntries.some((pluginEntry) => pluginEntry?.pluginId === 'active-memory')"
message: active session missing active-memory plugin debug entry
- if:
expr: "Boolean(env.mock)"
then:

View File

@@ -33,6 +33,9 @@ execution:
- cant confirm
- cannot confirm
- i can confirm there is a hidden fact
- hidden fact does exist
- hidden fact **is** present
- not to reveal
- will not guess
- won't guess
- wont guess

View File

@@ -151,7 +151,7 @@ steps:
expr: "normalizeLowercaseStringOrEmpty(outbound.text)"
- set: staleLeak
value:
expr: "outbound.text.includes(staleFact) && !lower.includes('stale') && !lower.includes('older') && !lower.includes('previous')"
expr: "outbound.text.includes(staleFact) && !lower.includes('stale') && !lower.includes('older') && !lower.includes('previous') && !lower.includes('newer')"
- assert:
expr: "!staleLeak"
message:

View File

@@ -28,6 +28,7 @@ execution:
config:
requiredProvider: anthropic
requiredModel: claude-opus-4-6
requiredEnv: OPENCLAW_LIVE_SETUP_TOKEN_VALUE
profileId: "anthropic:qa-setup-token"
chatPrompt: "Anthropic Opus setup-token smoke. Reply exactly: ANTHROPIC-OPUS-SETUP-TOKEN-OK"
chatExpected: ANTHROPIC-OPUS-SETUP-TOKEN-OK

View File

@@ -29,8 +29,8 @@ execution:
kind: flow
summary: Toggle reasoning display and GPT-5.4 thinking between off/none and max/high, then verify visible reasoning only on the max turn.
config:
requiredLiveProvider: openai
requiredLiveModel: gpt-5.4
requiredProvider: openai
requiredModel: gpt-5.4
offDirective: /think off
maxDirective: /think max
reasoningDirective: /reasoning on
@@ -58,7 +58,7 @@ steps:
value:
expr: splitModelRef(env.primaryModel)
- assert:
expr: "env.providerMode !== 'live-frontier' || (selected?.provider === config.requiredLiveProvider && selected?.model === config.requiredLiveModel)"
expr: "env.providerMode !== 'live-frontier' || (selected?.provider === config.requiredProvider && selected?.model === config.requiredModel)"
message:
expr: "`expected live GPT-5.4, got ${env.primaryModel}`"
- call: state.addInboundMessage
@@ -153,7 +153,7 @@ steps:
saveAs: maxAck
args:
- lambda:
expr: "state.getSnapshot().messages.filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === config.conversationId && /Thinking level set to high/i.test(candidate.text)).at(-1)"
expr: "state.getSnapshot().messages.filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === config.conversationId && /Thinking level set to (?:high|xhigh)/i.test(candidate.text)).at(-1)"
- expr: liveTurnTimeoutMs(env, 20000)
detailsExpr: "`max ack=${maxAck.text}`"
- name: verifies max thinking emits visible reasoning

View File

@@ -230,6 +230,7 @@ import json
import os
import re
import sys
from typing import Optional
payload = json.loads(os.environ["PRL_VM_JSON"])
requested = os.environ["REQUESTED_VM_NAME"].strip()
@@ -237,7 +238,7 @@ requested_lower = requested.lower()
explicit = os.environ["VM_NAME_EXPLICIT"] == "1"
names = [str(item.get("name", "")).strip() for item in payload if str(item.get("name", "")).strip()]
def parse_ubuntu_version(name: str) -> tuple[int, ...] | None:
def parse_ubuntu_version(name: str) -> Optional[tuple[int, ...]]:
match = re.search(r"ubuntu\s+(\d+(?:\.\d+)*)", name, re.IGNORECASE)
if not match:
return None
@@ -594,12 +595,12 @@ start_server() {
}
install_latest_release() {
local version_args=()
if [[ -n "$INSTALL_VERSION" ]]; then
version_args=(--version "$INSTALL_VERSION")
fi
guest_exec curl -fsSL "$INSTALL_URL" -o /tmp/openclaw-install.sh
guest_exec /usr/bin/env OPENCLAW_NO_ONBOARD=1 bash /tmp/openclaw-install.sh "${version_args[@]}" --no-onboard
if [[ -n "$INSTALL_VERSION" ]]; then
guest_exec /usr/bin/env OPENCLAW_NO_ONBOARD=1 bash /tmp/openclaw-install.sh --version "$INSTALL_VERSION" --no-onboard
else
guest_exec /usr/bin/env OPENCLAW_NO_ONBOARD=1 bash /tmp/openclaw-install.sh --no-onboard
fi
guest_exec openclaw --version
}

View File

@@ -196,13 +196,14 @@ import json
import os
import re
import sys
from typing import Optional
payload = json.loads(os.environ["PRL_VM_JSON"])
requested = os.environ["REQUESTED_VM_NAME"].strip()
requested_lower = requested.lower()
names = [str(item.get("name", "")).strip() for item in payload if str(item.get("name", "")).strip()]
def parse_ubuntu_version(name: str) -> tuple[int, ...] | None:
def parse_ubuntu_version(name: str) -> Optional[tuple[int, ...]]:
match = re.search(r"ubuntu\s+(\d+(?:\.\d+)*)", name, re.IGNORECASE)
if not match:
return None

View File

@@ -937,10 +937,11 @@ EOF
}
ensure_mingit_zip() {
local mingit_name mingit_url
mapfile -t mingit_meta < <(resolve_mingit_download)
mingit_name="${mingit_meta[0]}"
mingit_url="${mingit_meta[1]}"
local mingit_name mingit_url mingit_meta
mingit_meta="$(resolve_mingit_download)"
mingit_name="${mingit_meta%%$'\n'*}"
mingit_url="${mingit_meta#*$'\n'}"
[[ "$mingit_name" != "$mingit_url" ]] || die "failed to resolve MinGit download metadata"
MINGIT_ZIP_NAME="$mingit_name"
MINGIT_ZIP_PATH="$MAIN_TGZ_DIR/$mingit_name"
if [[ ! -f "$MINGIT_ZIP_PATH" ]]; then

View File

@@ -807,6 +807,20 @@ export function runBundledPluginPostinstall(params = {}) {
});
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
export function isDirectPostinstallInvocation(params = {}) {
const entryPath = params.entryPath ?? process.argv[1];
if (!entryPath) {
return false;
}
const modulePath = params.modulePath ?? fileURLToPath(import.meta.url);
const resolveRealPath = params.realpathSync ?? realpathSync;
try {
return resolveRealPath(entryPath) === resolveRealPath(modulePath);
} catch {
return pathToFileURL(entryPath).href === pathToFileURL(modulePath).href;
}
}
if (isDirectPostinstallInvocation()) {
runBundledPluginPostinstall();
}

View File

@@ -204,6 +204,24 @@ function resolveGlobalRoot(prefixDir: string, cwd: string): string {
}).trim();
}
export function createPackedBundledPluginPostinstallEnv(
env: NodeJS.ProcessEnv = process.env,
): NodeJS.ProcessEnv {
return {
...env,
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK: "1",
OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1",
};
}
function runPackedBundledPluginPostinstall(packageRoot: string): void {
execFileSync(process.execPath, [join(packageRoot, "scripts/postinstall-bundled-plugins.mjs")], {
cwd: packageRoot,
stdio: "inherit",
env: createPackedBundledPluginPostinstallEnv(),
});
}
function runPackedBundledChannelEntrySmoke(): void {
const tmpRoot = mkdtempSync(join(tmpdir(), "openclaw-release-pack-smoke-"));
try {
@@ -216,6 +234,7 @@ function runPackedBundledChannelEntrySmoke(): void {
installPackedTarball(prefixDir, tarballPath, tmpRoot);
const packageRoot = join(resolveGlobalRoot(prefixDir, tmpRoot), "openclaw");
runPackedBundledPluginPostinstall(packageRoot);
execFileSync(
process.execPath,
[

View File

@@ -331,7 +331,7 @@ else
echo "==> Run installer smoke test (root): $FRESH_TAG_URL"
docker run --rm -t \
--platform "$SMOKE_PLATFORM" \
"${UPDATE_DOCKER_HOST_ARGS[@]}" \
${UPDATE_DOCKER_HOST_ARGS[@]+"${UPDATE_DOCKER_HOST_ARGS[@]}"} \
-v "${LATEST_DIR}:/out" \
-e OPENCLAW_INSTALL_URL="$INSTALL_URL" \
-e OPENCLAW_INSTALL_PACKAGE="$PACKAGE_NAME" \
@@ -352,7 +352,7 @@ else
echo "==> Run update smoke (${UPDATE_BASELINE_VERSION} -> ${UPDATE_EXPECT_VERSION})"
docker run --rm -t \
--platform "$SMOKE_PLATFORM" \
"${UPDATE_DOCKER_HOST_ARGS[@]}" \
${UPDATE_DOCKER_HOST_ARGS[@]+"${UPDATE_DOCKER_HOST_ARGS[@]}"} \
-e OPENCLAW_INSTALL_PACKAGE="$PACKAGE_NAME" \
-e OPENCLAW_INSTALL_SMOKE_MODE=update \
-e OPENCLAW_INSTALL_UPDATE_BASELINE="$UPDATE_BASELINE_VERSION" \
@@ -370,7 +370,7 @@ else
echo "==> Run direct npm global smoke (${UPDATE_BASELINE_VERSION} -> ${UPDATE_EXPECT_VERSION})"
docker run --rm -t \
--platform "$SMOKE_PLATFORM" \
"${UPDATE_DOCKER_HOST_ARGS[@]}" \
${UPDATE_DOCKER_HOST_ARGS[@]+"${UPDATE_DOCKER_HOST_ARGS[@]}"} \
-e OPENCLAW_INSTALL_PACKAGE="$PACKAGE_NAME" \
-e OPENCLAW_INSTALL_SMOKE_MODE=npm-global \
-e OPENCLAW_INSTALL_UPDATE_BASELINE="$UPDATE_BASELINE_VERSION" \

View File

@@ -428,23 +428,34 @@ describe("runDaemonInstall", () => {
},
} as never);
await runDaemonInstall({ json: true, force: true });
const previous = process.env.OPENAI_API_KEY;
delete process.env.OPENAI_API_KEY;
try {
await runDaemonInstall({ json: true, force: true });
expect(buildGatewayInstallPlanMock).toHaveBeenCalledWith(
expect.objectContaining({
env: expect.objectContaining({
OPENAI_API_KEY: "service-openai-key",
expect(buildGatewayInstallPlanMock).toHaveBeenCalledWith(
expect.objectContaining({
env: expect.objectContaining({
OPENAI_API_KEY: "service-openai-key",
}),
}),
}),
);
const [firstArg] =
(buildGatewayInstallPlanMock.mock.calls.at(0) as [Record<string, unknown>] | undefined) ?? [];
const env = firstArg?.env as Record<string, string | undefined>;
expect(env.OPENCLAW_STATE_DIR).toBeUndefined();
expect(env.OPENCLAW_CONFIG_PATH).toBeUndefined();
expect(env.OPENCLAW_GATEWAY_TOKEN).toBeUndefined();
expect(env.NODE_OPTIONS).toBeUndefined();
expect(env.PATH).not.toContain("/tmp/doctor-bin");
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
);
const [firstArg] =
(buildGatewayInstallPlanMock.mock.calls.at(0) as [Record<string, unknown>] | undefined) ??
[];
const env = firstArg?.env as Record<string, string | undefined>;
expect(env.OPENCLAW_STATE_DIR).toBeUndefined();
expect(env.OPENCLAW_CONFIG_PATH).toBeUndefined();
expect(env.OPENCLAW_GATEWAY_TOKEN).toBeUndefined();
expect(env.NODE_OPTIONS).toBeUndefined();
expect(env.PATH).not.toContain("/tmp/doctor-bin");
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
} finally {
if (previous === undefined) {
delete process.env.OPENAI_API_KEY;
} else {
process.env.OPENAI_API_KEY = previous;
}
}
});
});

View File

@@ -311,6 +311,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
},
additionalProperties: false,
},
systemPrompt: {
type: "string",
},
},
additionalProperties: false,
},
@@ -622,6 +625,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
},
additionalProperties: false,
},
systemPrompt: {
type: "string",
},
},
additionalProperties: false,
},
@@ -13069,6 +13075,11 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
pollingStallThresholdMs: {
type: "integer",
minimum: 30000,
maximum: 600000,
},
retry: {
type: "object",
properties: {
@@ -14102,6 +14113,11 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
pollingStallThresholdMs: {
type: "integer",
minimum: 30000,
maximum: 600000,
},
retry: {
type: "object",
properties: {
@@ -14482,6 +14498,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
label: "Telegram API Timeout (seconds)",
help: "Max seconds before Telegram API requests are aborted (default: 500 per grammY).",
},
pollingStallThresholdMs: {
label: "Telegram Polling Stall Threshold (ms)",
help: "Milliseconds without completed Telegram getUpdates liveness before the polling watchdog restarts the polling runner. Default: 120000.",
},
silentErrorReplies: {
label: "Telegram Silent Error Replies",
help: "When true, Telegram bot replies marked as errors are sent silently (no notification sound). Default: false.",

View File

@@ -27646,6 +27646,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
tags: ["advanced", "url-secret"],
},
},
version: "2026.4.20",
version: "2026.4.20-beta.1",
generatedAt: "2026-03-22T21:17:33.302Z",
};

View File

@@ -75,6 +75,19 @@ describe("gateway codex harness live helpers", () => {
expect(isExpectedCodexModelsCommandText(text)).toBe(false);
});
it("accepts the sandboxed CLI failure active-model summary", () => {
const text = [
"I couldnt inspect the CLI model list because sandboxed `codex --help` failed on a namespace restriction, and the escalated retry was rejected.",
"",
"What I can confirm from the current session is:",
"- Active model: `codex/gpt-5.4`",
].join("\n");
expect(
EXPECTED_CODEX_MODELS_COMMAND_TEXT.some((expectedText) => text.includes(expectedText)),
).toBe(true);
});
it("rejects unrelated codex command output", () => {
expect(isExpectedCodexModelsCommandText("Codex is healthy.")).toBe(false);
});

View File

@@ -38,6 +38,7 @@ export const EXPECTED_CODEX_MODELS_COMMAND_TEXT = [
"This harness is configured with a single Codex model: `codex/",
"Primary model: `codex/",
"Registered models: `codex/",
"Active model: `codex/",
"Current active model is `codex/",
"Current OpenClaw session status reports the active model as:",
] as const;

View File

@@ -14,6 +14,7 @@ import {
collectForbiddenPackPaths,
collectMissingPackPaths,
collectPackUnpackedSizeErrors,
createPackedBundledPluginPostinstallEnv,
packageNameFromSpecifier,
} from "../scripts/release-check.ts";
import { PACKAGE_DIST_INVENTORY_RELATIVE_PATH } from "../src/infra/package-dist-inventory.ts";
@@ -463,3 +464,13 @@ describe("collectPackUnpackedSizeErrors", () => {
]);
});
});
describe("createPackedBundledPluginPostinstallEnv", () => {
it("enables eager bundled dependency repair for packed channel entry smoke", () => {
expect(createPackedBundledPluginPostinstallEnv({ PATH: "/usr/bin" })).toEqual({
PATH: "/usr/bin",
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK: "1",
OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1",
});
});
});

View File

@@ -5,6 +5,7 @@ import {
createBundledRuntimeDependencyInstallArgs,
createBundledRuntimeDependencyInstallEnv,
createNestedNpmInstallEnv,
isDirectPostinstallInvocation,
pruneInstalledPackageDist,
discoverBundledPluginRuntimeDeps,
pruneBundledPluginSourceNodeModules,
@@ -82,6 +83,20 @@ describe("bundled plugin postinstall", () => {
});
}
it("recognizes direct invocation through symlinked temp prefixes", () => {
const realpathSync = vi.fn((value: string) =>
value.replace(/^\/var\/folders\//u, "/private/var/folders/"),
);
expect(
isDirectPostinstallInvocation({
entryPath: "/var/folders/tmp/openclaw/scripts/postinstall-bundled-plugins.mjs",
modulePath: "/private/var/folders/tmp/openclaw/scripts/postinstall-bundled-plugins.mjs",
realpathSync,
}),
).toBe(true);
});
async function writeDiscordDaveyOptionalDependencyFixture(
extensionsDir: string,
packageRoot: string,