Skip to content

Commit cb408bb

Browse files
committed
fix(release): repair broad gate regressions
1 parent fa814eb commit cb408bb

9 files changed

Lines changed: 100 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Docs: https://docs.openclaw.ai
5555
- 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.
5656
- 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.
5757
- CLI: enforce the documented Node.js 22.19 runtime floor in the source launcher.
58+
- 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.
5859
- 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.
5960
- 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.
6061
- 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.

extensions/discord/src/monitor/message-handler.process.test.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,11 +1257,17 @@ describe("processDiscordMessage session routing", () => {
12571257
});
12581258
});
12591259

1260-
it("marks always-on guild replies as message-tool-only and disables source streaming", async () => {
1260+
it("marks explicit message-tool guild replies as message-tool-only and disables source streaming", async () => {
12611261
const ctx = await createBaseContext({
12621262
shouldRequireMention: false,
12631263
effectiveWasMentioned: false,
12641264
discordConfig: { streaming: "partial", blockStreaming: true },
1265+
cfg: {
1266+
messages: {
1267+
groupChat: { visibleReplies: "message_tool" },
1268+
},
1269+
session: { store: "/tmp/openclaw-discord-process-test-sessions.json" },
1270+
},
12651271
route: BASE_CHANNEL_ROUTE,
12661272
});
12671273

@@ -1283,6 +1289,7 @@ describe("processDiscordMessage session routing", () => {
12831289
messages: {
12841290
ackReaction: "👀",
12851291
ackReactionScope: "all",
1292+
groupChat: { visibleReplies: "message_tool" },
12861293
statusReactions: {
12871294
timing: { debounceMs: 0 },
12881295
},
@@ -1314,6 +1321,7 @@ describe("processDiscordMessage session routing", () => {
13141321
messages: {
13151322
ackReaction: "👀",
13161323
ackReactionScope: "all",
1324+
groupChat: { visibleReplies: "message_tool" },
13171325
statusReactions: {
13181326
enabled: true,
13191327
timing: { debounceMs: 0 },
@@ -1500,15 +1508,15 @@ describe("processDiscordMessage session routing", () => {
15001508
});
15011509
});
15021510

1503-
it("defaults guild replies to message-tool-only source delivery", async () => {
1511+
it("resolves guild source delivery from default, explicit, and room-event modes", async () => {
15041512
await runProcessDiscordMessage(
15051513
await createBaseContext({
15061514
shouldRequireMention: true,
15071515
effectiveWasMentioned: true,
15081516
route: BASE_CHANNEL_ROUTE,
15091517
}),
15101518
);
1511-
expect(getLastDispatchReplyOptions()?.sourceReplyDeliveryMode).toBe("message_tool_only");
1519+
expect(getLastDispatchReplyOptions()?.sourceReplyDeliveryMode).toBe("automatic");
15121520

15131521
dispatchInboundMessage.mockClear();
15141522
await runProcessDiscordMessage(
@@ -1518,15 +1526,15 @@ describe("processDiscordMessage session routing", () => {
15181526
cfg: {
15191527
messages: {
15201528
groupChat: {
1521-
visibleReplies: "automatic",
1529+
visibleReplies: "message_tool",
15221530
},
15231531
},
15241532
session: { store: "/tmp/openclaw-discord-process-test-sessions.json" },
15251533
},
15261534
route: BASE_CHANNEL_ROUTE,
15271535
}),
15281536
);
1529-
expect(getLastDispatchReplyOptions()?.sourceReplyDeliveryMode).toBe("automatic");
1537+
expect(getLastDispatchReplyOptions()?.sourceReplyDeliveryMode).toBe("message_tool_only");
15301538

15311539
dispatchInboundMessage.mockClear();
15321540
await runProcessDiscordMessage(
@@ -1754,6 +1762,9 @@ describe("processDiscordMessage draft streaming", () => {
17541762
const ctx = await createBaseContext({
17551763
cfg: {
17561764
tools: { profile: "coding" },
1765+
messages: {
1766+
groupChat: { visibleReplies: "message_tool" },
1767+
},
17571768
session: { store: "/tmp/openclaw-discord-process-test-sessions.json" },
17581769
},
17591770
route: BASE_CHANNEL_ROUTE,

extensions/qa-lab/src/providers/mock-openai/server.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1604,7 +1604,8 @@ describe("qa mock openai server", () => {
16041604
input: [
16051605
{
16061606
role: "system",
1607-
content: "## /workspace/MEMORY.md\nThread-hidden codename: ORBIT-22.",
1607+
content:
1608+
"Available tools include sessions_spawn.\n## /workspace/MEMORY.md\nThread-hidden codename: ORBIT-22.",
16081609
},
16091610
makeUserInput(
16101611
"@openclaw Thread memory check: what is the hidden thread codename stored only in memory? Use memory tools first and reply only in this thread.",

extensions/qa-lab/src/providers/mock-openai/server.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -916,7 +916,6 @@ function buildExplicitSessionsSpawnArgs(text: string): Record<string, unknown> |
916916
}
917917

918918
function extractToolErrorForNamedCall(params: {
919-
allInputText: string;
920919
input: ResponsesInputItem[];
921920
name: string;
922921
toolJson: Record<string, unknown> | null;
@@ -928,8 +927,7 @@ function extractToolErrorForNamedCall(params: {
928927
const namedFunctionCall = params.input.some(
929928
(item) => item.type === "function_call" && item.name === params.name,
930929
);
931-
const namedPromptReference = new RegExp(`\\b${params.name}\\b`, "i").test(params.allInputText);
932-
if (namedFunctionCall || namedPromptReference) {
930+
if (namedFunctionCall) {
933931
return error;
934932
}
935933
return undefined;
@@ -1015,7 +1013,6 @@ function buildAssistantText(
10151013
const activeMemorySummary = extractActiveMemorySummary(allInputText);
10161014
const snackPreference = extractSnackPreference(activeMemorySummary ?? memorySnippet);
10171015
const sessionsSpawnError = extractToolErrorForNamedCall({
1018-
allInputText,
10191016
input,
10201017
name: "sessions_spawn",
10211018
toolJson,

extensions/slack/src/monitor.test-helpers.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Mock, vi } from "vitest";
2+
import { clearSlackInboundDeliveryStateForTest } from "./monitor/inbound-delivery-state.js";
23

34
type SlackHandler = (args: unknown) => Promise<void>;
45
type SlackMiddleware = (args: { next: () => Promise<void> } & Record<string, unknown>) => unknown;
@@ -191,6 +192,7 @@ export const defaultSlackTestConfig = () => ({
191192
});
192193

193194
export function resetSlackTestState(config: Record<string, unknown> = defaultSlackTestConfig()) {
195+
clearSlackInboundDeliveryStateForTest();
194196
slackTestState.config = config;
195197
slackTestState.sendMock.mockReset().mockResolvedValue(undefined);
196198
slackTestState.replyMock.mockReset();
@@ -208,6 +210,17 @@ export function resetSlackTestState(config: Record<string, unknown> = defaultSla
208210
.mockImplementation(async ({ entries }) =>
209211
entries.map((input) => ({ input, resolved: false })),
210212
);
213+
const client = getSlackClient();
214+
client.auth.test.mockReset().mockResolvedValue({ user_id: "bot-user" });
215+
client.conversations.info.mockReset().mockResolvedValue({
216+
channel: { name: "dm", is_im: true },
217+
});
218+
client.conversations.replies.mockReset().mockResolvedValue({ messages: [] });
219+
client.conversations.history.mockReset().mockResolvedValue({ messages: [] });
220+
client.users.info.mockReset().mockResolvedValue({
221+
user: { profile: { display_name: "Ada" } },
222+
});
223+
client.assistant.threads.setStatus.mockReset().mockResolvedValue({ ok: true });
211224
getSlackHandlers()?.clear();
212225
}
213226

extensions/slack/src/monitor.tool-result.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,11 +518,12 @@ describe("monitorSlackProvider tool results", () => {
518518
expect(sendMock).toHaveBeenCalledTimes(1);
519519
});
520520

521-
it("keeps always-on channel messages private by default", async () => {
521+
it("keeps always-on channel messages private when group visible replies use message_tool", async () => {
522522
slackTestState.config = {
523523
messages: {
524524
ackReaction: "👀",
525525
ackReactionScope: "all",
526+
groupChat: { visibleReplies: "message_tool" },
526527
statusReactions: {
527528
enabled: true,
528529
timing: { debounceMs: 0, doneHoldMs: 0, errorHoldMs: 0 },

src/agents/subagent-announce-delivery.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { completionRequiresMessageToolDelivery } from "../auto-reply/reply/completion-delivery-policy.js";
1+
import {
2+
completionRequiresMessageToolDelivery,
3+
resolveCompletionChatType,
4+
} from "../auto-reply/reply/completion-delivery-policy.js";
25
import type { OpenClawConfig } from "../config/types.openclaw.js";
36
import type { ConversationRef } from "../infra/outbound/session-binding-service.js";
47
import { stringifyRouteThreadId } from "../plugin-sdk/channel-route.js";
@@ -351,10 +354,10 @@ export async function resolveSubagentCompletionOrigin(params: {
351354
const accountId = normalizeAccountId(requesterOrigin?.accountId);
352355
const threadId =
353356
requesterOrigin?.threadId != null && requesterOrigin.threadId !== ""
354-
? stringifyRouteThreadId(requesterOrigin.threadId)
357+
? requesterOrigin.threadId
355358
: undefined;
356359
const conversationId =
357-
threadId ||
360+
stringifyRouteThreadId(threadId) ||
358361
resolveConversationIdFromTargets({
359362
targets: [to],
360363
}) ||
@@ -660,9 +663,18 @@ async function sendSubagentAnnounceDirectly(params: {
660663
sourceTool: params.sourceTool,
661664
});
662665
const expectedMediaUrls = collectExpectedMediaFromInternalEvents(params.internalEvents);
666+
const completionChatType = resolveCompletionChatType({
667+
requesterSessionKey: params.requesterSessionKey,
668+
targetRequesterSessionKey: canonicalRequesterSessionKey,
669+
requesterEntry,
670+
directOrigin: effectiveDirectOrigin,
671+
requesterSessionOrigin,
672+
});
663673
const requiresMessageToolDelivery =
664674
agentMediatedCompletion &&
665-
(expectedMediaUrls.length > 0 ||
675+
(completionChatType === "channel" ||
676+
completionChatType === "group" ||
677+
expectedMediaUrls.length > 0 ||
666678
completionRequiresMessageToolDelivery({
667679
cfg,
668680
requesterSessionKey: params.requesterSessionKey,

src/plugins/uninstall.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1209,7 +1209,7 @@ describe("uninstallPlugin", () => {
12091209
const pluginDir = path.join(npmRoot, "node_modules", "missing-plugin");
12101210
const peerPluginDir = path.join(npmRoot, "node_modules", "peer-plugin");
12111211
const peerLink = path.join(peerPluginDir, "node_modules", "openclaw");
1212-
await fs.mkdir(peerLink, { recursive: true });
1212+
await fs.mkdir(path.dirname(peerLink), { recursive: true });
12131213
await fs.writeFile(
12141214
path.join(npmRoot, "package.json"),
12151215
`${JSON.stringify(

test/openclaw-launcher.e2e.test.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,55 @@ describe("openclaw launcher", () => {
120120
);
121121
const engineMatch = packageJson.engines?.node?.match(/^>=(\d+)\.(\d+)\.(\d+)$/u);
122122

123-
expect(launcherMatch).not.toBeNull();
124-
expect(runtimeMatch).not.toBeNull();
125-
expect(engineMatch).not.toBeNull();
126-
expect(`${launcherMatch?.[1]}.${launcherMatch?.[2]}.0`).toBe(
127-
`${engineMatch?.[1]}.${engineMatch?.[2]}.${engineMatch?.[3]}`,
123+
if (!launcherMatch) {
124+
throw new Error("openclaw.mjs MIN_NODE_* constants were not found");
125+
}
126+
if (!runtimeMatch) {
127+
throw new Error("src/infra/runtime-guard.ts MIN_NODE constant was not found");
128+
}
129+
if (!engineMatch) {
130+
throw new Error("package.json engines.node must use >=<major>.<minor>.<patch>");
131+
}
132+
const [engineMajor, engineMinor, enginePatch] = engineMatch.slice(1, 4).map(Number);
133+
const launcherMinimumLabel = `${engineMajor}.${engineMinor}`;
134+
135+
expect(
136+
[Number(launcherMatch[1]), Number(launcherMatch[2]), 0],
137+
"openclaw.mjs MIN_NODE_* must match package.json engines.node",
138+
).toEqual([engineMajor, engineMinor, enginePatch]);
139+
expect(
140+
runtimeMatch.slice(1, 4).map(Number),
141+
"src/infra/runtime-guard.ts MIN_NODE must match package.json engines.node",
142+
).toEqual([engineMajor, engineMinor, enginePatch]);
143+
144+
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
145+
const mockedNodeVersion =
146+
engineMinor > 0 ? `${engineMajor}.${engineMinor - 1}.0` : `${engineMajor - 1}.999.0`;
147+
const mockNodeVersionPath = path.join(fixtureRoot, "mock-node-version.mjs");
148+
await fs.writeFile(
149+
mockNodeVersionPath,
150+
[
151+
"Object.defineProperty(process.versions, 'node', {",
152+
` value: ${JSON.stringify(mockedNodeVersion)},`,
153+
"});",
154+
].join("\n"),
155+
"utf8",
156+
);
157+
158+
const result = spawnSync(
159+
process.execPath,
160+
["--import", mockNodeVersionPath, path.join(fixtureRoot, "openclaw.mjs"), "--help"],
161+
{
162+
cwd: fixtureRoot,
163+
env: launcherEnv(),
164+
encoding: "utf8",
165+
},
166+
);
167+
168+
expect(result.status).toBe(1);
169+
expect(result.stderr).toContain(
170+
`openclaw: Node.js v${launcherMinimumLabel}+ is required (current: v${mockedNodeVersion}).`,
128171
);
129-
expect(runtimeMatch?.slice(1, 4)).toEqual(engineMatch?.slice(1, 4));
130172
});
131173

132174
it("surfaces transitive entry import failures instead of masking them as missing dist", async () => {

0 commit comments

Comments
 (0)