mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 17:16:32 +00:00
fix(release): repair broad gate regressions
This commit is contained in:
@@ -55,6 +55,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram: log successful outbound text and media deliveries with account, chat, message, operation, thread, reply, silent, and chunk metadata while keeping message bodies out of logs. Fixes #83196. (#83247) Thanks @jrwrest.
|
||||
- Cron: link isolated scheduled task runs to their stable cron session so task status and cleanup can follow the backing agent run. (#83606) Thanks @jai.
|
||||
- CLI: enforce the documented Node.js 22.19 runtime floor in the source launcher.
|
||||
- Release stability: repair broad-gate regressions in requester-agent completion handoff, QA-Lab mock spawn attribution, Slack monitor test isolation, plugin uninstall peer fixtures, and Node-floor launcher contract coverage.
|
||||
- Agents/replies: persist queued follow-up user messages and assistant error stubs only once across model-fallback retries, preventing repeated provider rejections from corrupted same-role session transcripts. Fixes #83404. (#83417) Thanks @yetval.
|
||||
- Slack: persist delivered inbound message IDs and fail closed when same-channel thread replies lose their thread context, preventing delayed duplicate replies and accidental channel-root posts. Fixes #83521. Thanks @shannon0430.
|
||||
- Codex app-server: complete OpenClaw dynamic tool diagnostics at the request boundary so successful, failed, timed out, aborted, and blocked tool calls do not leave active tool state behind. Fixes #83474. Thanks @rozmiarD.
|
||||
|
||||
@@ -1257,11 +1257,17 @@ describe("processDiscordMessage session routing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("marks always-on guild replies as message-tool-only and disables source streaming", async () => {
|
||||
it("marks explicit message-tool guild replies as message-tool-only and disables source streaming", async () => {
|
||||
const ctx = await createBaseContext({
|
||||
shouldRequireMention: false,
|
||||
effectiveWasMentioned: false,
|
||||
discordConfig: { streaming: "partial", blockStreaming: true },
|
||||
cfg: {
|
||||
messages: {
|
||||
groupChat: { visibleReplies: "message_tool" },
|
||||
},
|
||||
session: { store: "/tmp/openclaw-discord-process-test-sessions.json" },
|
||||
},
|
||||
route: BASE_CHANNEL_ROUTE,
|
||||
});
|
||||
|
||||
@@ -1283,6 +1289,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
messages: {
|
||||
ackReaction: "👀",
|
||||
ackReactionScope: "all",
|
||||
groupChat: { visibleReplies: "message_tool" },
|
||||
statusReactions: {
|
||||
timing: { debounceMs: 0 },
|
||||
},
|
||||
@@ -1314,6 +1321,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
messages: {
|
||||
ackReaction: "👀",
|
||||
ackReactionScope: "all",
|
||||
groupChat: { visibleReplies: "message_tool" },
|
||||
statusReactions: {
|
||||
enabled: true,
|
||||
timing: { debounceMs: 0 },
|
||||
@@ -1500,7 +1508,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults guild replies to message-tool-only source delivery", async () => {
|
||||
it("resolves guild source delivery from default, explicit, and room-event modes", async () => {
|
||||
await runProcessDiscordMessage(
|
||||
await createBaseContext({
|
||||
shouldRequireMention: true,
|
||||
@@ -1508,7 +1516,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
route: BASE_CHANNEL_ROUTE,
|
||||
}),
|
||||
);
|
||||
expect(getLastDispatchReplyOptions()?.sourceReplyDeliveryMode).toBe("message_tool_only");
|
||||
expect(getLastDispatchReplyOptions()?.sourceReplyDeliveryMode).toBe("automatic");
|
||||
|
||||
dispatchInboundMessage.mockClear();
|
||||
await runProcessDiscordMessage(
|
||||
@@ -1518,7 +1526,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
cfg: {
|
||||
messages: {
|
||||
groupChat: {
|
||||
visibleReplies: "automatic",
|
||||
visibleReplies: "message_tool",
|
||||
},
|
||||
},
|
||||
session: { store: "/tmp/openclaw-discord-process-test-sessions.json" },
|
||||
@@ -1526,7 +1534,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
route: BASE_CHANNEL_ROUTE,
|
||||
}),
|
||||
);
|
||||
expect(getLastDispatchReplyOptions()?.sourceReplyDeliveryMode).toBe("automatic");
|
||||
expect(getLastDispatchReplyOptions()?.sourceReplyDeliveryMode).toBe("message_tool_only");
|
||||
|
||||
dispatchInboundMessage.mockClear();
|
||||
await runProcessDiscordMessage(
|
||||
@@ -1754,6 +1762,9 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
const ctx = await createBaseContext({
|
||||
cfg: {
|
||||
tools: { profile: "coding" },
|
||||
messages: {
|
||||
groupChat: { visibleReplies: "message_tool" },
|
||||
},
|
||||
session: { store: "/tmp/openclaw-discord-process-test-sessions.json" },
|
||||
},
|
||||
route: BASE_CHANNEL_ROUTE,
|
||||
|
||||
@@ -1604,7 +1604,8 @@ describe("qa mock openai server", () => {
|
||||
input: [
|
||||
{
|
||||
role: "system",
|
||||
content: "## /workspace/MEMORY.md\nThread-hidden codename: ORBIT-22.",
|
||||
content:
|
||||
"Available tools include sessions_spawn.\n## /workspace/MEMORY.md\nThread-hidden codename: ORBIT-22.",
|
||||
},
|
||||
makeUserInput(
|
||||
"@openclaw Thread memory check: what is the hidden thread codename stored only in memory? Use memory tools first and reply only in this thread.",
|
||||
|
||||
@@ -916,7 +916,6 @@ function buildExplicitSessionsSpawnArgs(text: string): Record<string, unknown> |
|
||||
}
|
||||
|
||||
function extractToolErrorForNamedCall(params: {
|
||||
allInputText: string;
|
||||
input: ResponsesInputItem[];
|
||||
name: string;
|
||||
toolJson: Record<string, unknown> | null;
|
||||
@@ -928,8 +927,7 @@ function extractToolErrorForNamedCall(params: {
|
||||
const namedFunctionCall = params.input.some(
|
||||
(item) => item.type === "function_call" && item.name === params.name,
|
||||
);
|
||||
const namedPromptReference = new RegExp(`\\b${params.name}\\b`, "i").test(params.allInputText);
|
||||
if (namedFunctionCall || namedPromptReference) {
|
||||
if (namedFunctionCall) {
|
||||
return error;
|
||||
}
|
||||
return undefined;
|
||||
@@ -1015,7 +1013,6 @@ function buildAssistantText(
|
||||
const activeMemorySummary = extractActiveMemorySummary(allInputText);
|
||||
const snackPreference = extractSnackPreference(activeMemorySummary ?? memorySnippet);
|
||||
const sessionsSpawnError = extractToolErrorForNamedCall({
|
||||
allInputText,
|
||||
input,
|
||||
name: "sessions_spawn",
|
||||
toolJson,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Mock, vi } from "vitest";
|
||||
import { clearSlackInboundDeliveryStateForTest } from "./monitor/inbound-delivery-state.js";
|
||||
|
||||
type SlackHandler = (args: unknown) => Promise<void>;
|
||||
type SlackMiddleware = (args: { next: () => Promise<void> } & Record<string, unknown>) => unknown;
|
||||
@@ -191,6 +192,7 @@ export const defaultSlackTestConfig = () => ({
|
||||
});
|
||||
|
||||
export function resetSlackTestState(config: Record<string, unknown> = defaultSlackTestConfig()) {
|
||||
clearSlackInboundDeliveryStateForTest();
|
||||
slackTestState.config = config;
|
||||
slackTestState.sendMock.mockReset().mockResolvedValue(undefined);
|
||||
slackTestState.replyMock.mockReset();
|
||||
@@ -208,6 +210,17 @@ export function resetSlackTestState(config: Record<string, unknown> = defaultSla
|
||||
.mockImplementation(async ({ entries }) =>
|
||||
entries.map((input) => ({ input, resolved: false })),
|
||||
);
|
||||
const client = getSlackClient();
|
||||
client.auth.test.mockReset().mockResolvedValue({ user_id: "bot-user" });
|
||||
client.conversations.info.mockReset().mockResolvedValue({
|
||||
channel: { name: "dm", is_im: true },
|
||||
});
|
||||
client.conversations.replies.mockReset().mockResolvedValue({ messages: [] });
|
||||
client.conversations.history.mockReset().mockResolvedValue({ messages: [] });
|
||||
client.users.info.mockReset().mockResolvedValue({
|
||||
user: { profile: { display_name: "Ada" } },
|
||||
});
|
||||
client.assistant.threads.setStatus.mockReset().mockResolvedValue({ ok: true });
|
||||
getSlackHandlers()?.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -518,11 +518,12 @@ describe("monitorSlackProvider tool results", () => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps always-on channel messages private by default", async () => {
|
||||
it("keeps always-on channel messages private when group visible replies use message_tool", async () => {
|
||||
slackTestState.config = {
|
||||
messages: {
|
||||
ackReaction: "👀",
|
||||
ackReactionScope: "all",
|
||||
groupChat: { visibleReplies: "message_tool" },
|
||||
statusReactions: {
|
||||
enabled: true,
|
||||
timing: { debounceMs: 0, doneHoldMs: 0, errorHoldMs: 0 },
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { completionRequiresMessageToolDelivery } from "../auto-reply/reply/completion-delivery-policy.js";
|
||||
import {
|
||||
completionRequiresMessageToolDelivery,
|
||||
resolveCompletionChatType,
|
||||
} from "../auto-reply/reply/completion-delivery-policy.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { ConversationRef } from "../infra/outbound/session-binding-service.js";
|
||||
import { stringifyRouteThreadId } from "../plugin-sdk/channel-route.js";
|
||||
@@ -351,10 +354,10 @@ export async function resolveSubagentCompletionOrigin(params: {
|
||||
const accountId = normalizeAccountId(requesterOrigin?.accountId);
|
||||
const threadId =
|
||||
requesterOrigin?.threadId != null && requesterOrigin.threadId !== ""
|
||||
? stringifyRouteThreadId(requesterOrigin.threadId)
|
||||
? requesterOrigin.threadId
|
||||
: undefined;
|
||||
const conversationId =
|
||||
threadId ||
|
||||
stringifyRouteThreadId(threadId) ||
|
||||
resolveConversationIdFromTargets({
|
||||
targets: [to],
|
||||
}) ||
|
||||
@@ -660,9 +663,18 @@ async function sendSubagentAnnounceDirectly(params: {
|
||||
sourceTool: params.sourceTool,
|
||||
});
|
||||
const expectedMediaUrls = collectExpectedMediaFromInternalEvents(params.internalEvents);
|
||||
const completionChatType = resolveCompletionChatType({
|
||||
requesterSessionKey: params.requesterSessionKey,
|
||||
targetRequesterSessionKey: canonicalRequesterSessionKey,
|
||||
requesterEntry,
|
||||
directOrigin: effectiveDirectOrigin,
|
||||
requesterSessionOrigin,
|
||||
});
|
||||
const requiresMessageToolDelivery =
|
||||
agentMediatedCompletion &&
|
||||
(expectedMediaUrls.length > 0 ||
|
||||
(completionChatType === "channel" ||
|
||||
completionChatType === "group" ||
|
||||
expectedMediaUrls.length > 0 ||
|
||||
completionRequiresMessageToolDelivery({
|
||||
cfg,
|
||||
requesterSessionKey: params.requesterSessionKey,
|
||||
|
||||
@@ -1209,7 +1209,7 @@ describe("uninstallPlugin", () => {
|
||||
const pluginDir = path.join(npmRoot, "node_modules", "missing-plugin");
|
||||
const peerPluginDir = path.join(npmRoot, "node_modules", "peer-plugin");
|
||||
const peerLink = path.join(peerPluginDir, "node_modules", "openclaw");
|
||||
await fs.mkdir(peerLink, { recursive: true });
|
||||
await fs.mkdir(path.dirname(peerLink), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(npmRoot, "package.json"),
|
||||
`${JSON.stringify(
|
||||
|
||||
@@ -120,13 +120,55 @@ describe("openclaw launcher", () => {
|
||||
);
|
||||
const engineMatch = packageJson.engines?.node?.match(/^>=(\d+)\.(\d+)\.(\d+)$/u);
|
||||
|
||||
expect(launcherMatch).not.toBeNull();
|
||||
expect(runtimeMatch).not.toBeNull();
|
||||
expect(engineMatch).not.toBeNull();
|
||||
expect(`${launcherMatch?.[1]}.${launcherMatch?.[2]}.0`).toBe(
|
||||
`${engineMatch?.[1]}.${engineMatch?.[2]}.${engineMatch?.[3]}`,
|
||||
if (!launcherMatch) {
|
||||
throw new Error("openclaw.mjs MIN_NODE_* constants were not found");
|
||||
}
|
||||
if (!runtimeMatch) {
|
||||
throw new Error("src/infra/runtime-guard.ts MIN_NODE constant was not found");
|
||||
}
|
||||
if (!engineMatch) {
|
||||
throw new Error("package.json engines.node must use >=<major>.<minor>.<patch>");
|
||||
}
|
||||
const [engineMajor, engineMinor, enginePatch] = engineMatch.slice(1, 4).map(Number);
|
||||
const launcherMinimumLabel = `${engineMajor}.${engineMinor}`;
|
||||
|
||||
expect(
|
||||
[Number(launcherMatch[1]), Number(launcherMatch[2]), 0],
|
||||
"openclaw.mjs MIN_NODE_* must match package.json engines.node",
|
||||
).toEqual([engineMajor, engineMinor, enginePatch]);
|
||||
expect(
|
||||
runtimeMatch.slice(1, 4).map(Number),
|
||||
"src/infra/runtime-guard.ts MIN_NODE must match package.json engines.node",
|
||||
).toEqual([engineMajor, engineMinor, enginePatch]);
|
||||
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
const mockedNodeVersion =
|
||||
engineMinor > 0 ? `${engineMajor}.${engineMinor - 1}.0` : `${engineMajor - 1}.999.0`;
|
||||
const mockNodeVersionPath = path.join(fixtureRoot, "mock-node-version.mjs");
|
||||
await fs.writeFile(
|
||||
mockNodeVersionPath,
|
||||
[
|
||||
"Object.defineProperty(process.versions, 'node', {",
|
||||
` value: ${JSON.stringify(mockedNodeVersion)},`,
|
||||
"});",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
["--import", mockNodeVersionPath, path.join(fixtureRoot, "openclaw.mjs"), "--help"],
|
||||
{
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain(
|
||||
`openclaw: Node.js v${launcherMinimumLabel}+ is required (current: v${mockedNodeVersion}).`,
|
||||
);
|
||||
expect(runtimeMatch?.slice(1, 4)).toEqual(engineMatch?.slice(1, 4));
|
||||
});
|
||||
|
||||
it("surfaces transitive entry import failures instead of masking them as missing dist", async () => {
|
||||
|
||||
Reference in New Issue
Block a user