惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
Malwarebytes
Malwarebytes
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cybersecurity and Infrastructure Security Agency CISA
F
Future of Privacy Forum
C
Cisco Blogs
T
The Exploit Database - CXSecurity.com
A
Arctic Wolf
S
Securelist
K
Kaspersky official blog
S
Schneier on Security
T
ThreatConnect
T
Tenable Blog
Spread Privacy
Spread Privacy
T
True Tiger Recordings
AWS News Blog
AWS News Blog
F
Fox-IT International blog
量子位
T
Threatpost
V
Vulnerabilities – Threatpost
C
CERT Recently Published Vulnerability Notes
Cisco Talos Blog
Cisco Talos Blog
GbyAI
GbyAI
宝玉的分享
宝玉的分享
腾讯CDC
G
Google Developers Blog
aimingoo的专栏
aimingoo的专栏
Cyberwarzone
Cyberwarzone
有赞技术团队
有赞技术团队
S
SegmentFault 最新的问题
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Visual Studio Blog
U
Unit 42
雷峰网
雷峰网
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Simon Willison's Weblog
Simon Willison's Weblog
O
OpenAI News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
The Register - Security
The Register - Security
MyScale Blog
MyScale Blog
小众软件
小众软件
A
About on SuperTechFans
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
博客园 - 三生石上(FineUI控件)
美团技术团队
Google Online Security Blog
Google Online Security Blog
P
Proofpoint News Feed
MongoDB | Blog
MongoDB | Blog

Recent Commits to openclaw:main

fix(telegram): preserve inbound text entities (#83873) · openclaw/openclaw@b552919 chore: ignore Python bytecode caches · openclaw/openclaw@b6b2755 fix: make autoreview progress visible · openclaw/openclaw@236edb2 ci(release): fix plugin prerelease extension batch invocation test(telegram): provide topic cache store in message context harness · openclaw/openclaw@ff1fde1 test(agents): complete provider runtime test mocks · openclaw/openclaw@be8cd12 test(telegram): type topic cache harness store · openclaw/openclaw@84ab206 test(agents): sync provider runtime mocks · openclaw/openclaw@a289dd9 fix(discord): suppress self-reply prompt echoes (#86238) docs: clarify config migration policy · openclaw/openclaw@c44367f fix(perf): fail startup bench on early gateway exit · openclaw/openclaw@a8fc28c fix: prevent plain text tool call leaks (#86222) · openclaw/openclaw@cd62780 fix: handle npm min-release-age in installers · openclaw/openclaw@316d97c fix(scripts): include ui:build in build-all full and ciArtifacts prof… · openclaw/openclaw@6704d0a fix(e2e): sample Windows kitchen sink gateway RSS · openclaw/openclaw@73189e3 fix(cron): respect isolated target and error on missing remove id (#8… · openclaw/openclaw@6709f4e fix(pi-embedded-runner): propagate trigger-derived priority to the gl… · openclaw/openclaw@0580f57 fix(cli): suppress self-update version warnings · openclaw/openclaw@e2bd20f fix: preserve webchat source reply details · openclaw/openclaw@aa50c51 docs: replace OpenClaw docs skill and add plugin permissions guide · openclaw/openclaw@0dabb70 fix(codex): preserve source reply mode for active runs (#86325) · openclaw/openclaw@b962110 fix: make compaction reinjection opt-in · openclaw/openclaw@ab910f8 fix codex usage-limit recovery copy (#86305) · openclaw/openclaw@c3c8a65 feat(ui): add ephemeral Activity tab · openclaw/openclaw@3dd0e8e fix(tests): harden native macos plugin proof · openclaw/openclaw@a5d5604 fix(commitments): serialize load-modify-save with in-process queue + … · openclaw/openclaw@d3c293d Fail Codex compaction at the Codex boundary (#85958) · openclaw/openclaw@dd47e47 fix(docker): restore config parent ownership · openclaw/openclaw@908b894 docs: clarify config default review policy (#86329) · openclaw/openclaw@3a03dd5 docs: clean changelog script entries · openclaw/openclaw@0eead19 fix(scripts): budget restart benchmark timeouts · openclaw/openclaw@5bd5509 fix: align ui vitest config assertion · openclaw/openclaw@730fd19 fix: route unit ui vitest targets narrowly · openclaw/openclaw@777402e fix: route explicit ui vitest targets narrowly · openclaw/openclaw@56a383c fix(android): harden play media permission removal fix(webchat): stabilize live transcript run state · openclaw/openclaw@119a01c fix(scripts): fail restart benchmark regressions · openclaw/openclaw@95d1b39 fix(openai): scope external codex auth to realtime fix(openai): prefer codex auth for GPT realtime · openclaw/openclaw@48c4f57 fix(openai): discover codex cli auth for provider checks · openclaw/openclaw@4656275 fix(android): keep talk mode on realtime relay · openclaw/openclaw@70614f8 test(android): add gateway connect adb probe · openclaw/openclaw@d7aa1f3 fix(android): stabilize realtime talk connection state · openclaw/openclaw@ffb02a5 test(android): add voice mode adb e2e harness · openclaw/openclaw@e52a3b3 fix(ci): stabilize deadcode and catalog checks · openclaw/openclaw@3db1508 fix(scripts): prebuild gateway cpu bench · openclaw/openclaw@ca70015 fix(e2e): harden bundled lifecycle probe on Windows · openclaw/openclaw@4798264 test(e2e): sample kitchen sink rpc peak rss · openclaw/openclaw@60c0f24 fix(scripts): remove stale deadcode allowlist entries · openclaw/openclaw@ea3bb92 fix(telegram): route polling diagnostics away from errors · openclaw/openclaw@b5c1199 fix(plugins): support linked source checkouts on Windows · openclaw/openclaw@793e300 fix(gateway): back off session tool mirrors under pressure (#84846) · openclaw/openclaw@42bdc94 fix(config): skip shell env fallback on Windows (#85739) · openclaw/openclaw@06bf302 fix(gateway): avoid duplicate session message broadcasts · openclaw/openclaw@1459044 fix: repair anchorless iMessage watch payloads · openclaw/openclaw@f37fbc9 fix(cli): route node status hints to stdout (#85780) · openclaw/openclaw@749692e fix(oc-path): support deep config edits (#86060) · openclaw/openclaw@3a72a30 fix(config): quiet benign metadata anomaly output · openclaw/openclaw@f3f4f29 fix(test): fail multi-node update regressions · openclaw/openclaw@732cf54 fix(google-vertex): support production ADC modes (#83971) · openclaw/openclaw@f09b4eb test(e2e): expose corrupt plugin deps smoke · openclaw/openclaw@fa3ff4d fix(codex): log app-server approval promotion trigger · openclaw/openclaw@d9af23f test(e2e): harden multi-node update smoke Clean up browser MCP subprocess tree (#85832) · openclaw/openclaw@8dc6b4d fix(agents): log warnings instead of swallowing subagent errors (#82943) · openclaw/openclaw@907bc03 fix(compaction): preserve partial summary on mid-chain chunk failure … · openclaw/openclaw@f0061dd fix(config): do not suppress recovery retry after failed backup resto… · openclaw/openclaw@5d174a5 chore: release 2026.5.25 fix(installer): support alpine cli installs · openclaw/openclaw@f68ed72 test(agents): keep runtime-plan provider mock current fix(scripts): launch env package scripts on Windows · openclaw/openclaw@4d4ce9e fix(agents): cache fallback provider resolution · openclaw/openclaw@3c8d101 fix(test): make import timing scripts Windows-safe · openclaw/openclaw@8ae9977 fix(telegram): transient Telegram pairing prompts (#85555) · openclaw/openclaw@8209426 fix(test): make max Vitest scripts Windows-safe · openclaw/openclaw@b681d5d fix(doctor): migrate Feishu account bot names (#86081) · openclaw/openclaw@9e8cc7e fix(scripts): prefilter conflict marker scans docs: add ClawSweeper review policy to AGENTS (#86197) · openclaw/openclaw@242e876 fix(installer): avoid before with npm release-age configs (#85491) · openclaw/openclaw@4742db6 fix(e2e): retry Windows kitchen sink probes · openclaw/openclaw@3e275a5 fix(installer): install node with apk on alpine fix(installer): detect musl linux shells · openclaw/openclaw@acfed37 perf(plugins,gateway): thread metadata snapshot + discovery through h… · openclaw/openclaw@8ccb11c fix(ui): split control ui runtime chunks · openclaw/openclaw@8bf4f7d refactor(config): extract GoogleChat schema into zod-schema.providers… · openclaw/openclaw@fe34141 fix(update): suppress internal handoff version warnings · openclaw/openclaw@6cc8244 test(e2e): select installable bundled plugins · openclaw/openclaw@0acc3e3 fix(scripts): harden Windows native opus install · openclaw/openclaw@43252c8 fix(agents): match runtime policy entries when session provider is em… fix(scripts): harden Windows generated formatting · openclaw/openclaw@0a98559 fix(mcp): bound tools/list during catalog discovery (#85063) · openclaw/openclaw@07f500a fix(test): focus plugin binding Docker smoke · openclaw/openclaw@dfa1a51 test(e2e): fail release memory indexing errors test(daemon): fail launchd integration bootstrap errors · openclaw/openclaw@af07769 feat(imessage): support thumb approval reactions (#85952) · openclaw/openclaw@5c7980f fix(crabbox): default macos aws runs on demand fix(scripts): preserve test passthrough args · openclaw/openclaw@e4332f7 fix(e2e): harden Windows plugin assertions fix(test): mount upgrade survivor helper · openclaw/openclaw@5f03154 fix(android): keep permission setup action visible · openclaw/openclaw@94bc18a
refactor: keep plain text tool-call promotion private (#86374) · openclaw/openclaw@c3ab2de
steipete · 2026-05-25 · via Recent Commits to openclaw:main

@@ -0,0 +1,247 @@

1+

import { randomUUID } from "node:crypto";

2+

import type { StreamFn } from "@earendil-works/pi-agent-core";

3+

import { createAssistantMessageEventStream, streamSimple } from "@earendil-works/pi-ai";

4+

import { parseStandalonePlainTextToolCallBlocks } from "./tool-payload.js";

5+6+

function toRecord(value: unknown): Record<string, unknown> | undefined {

7+

return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;

8+

}

9+10+

function resolveContextToolNames(context: Parameters<StreamFn>[1]): Set<string> {

11+

const tools = (context as { tools?: unknown }).tools;

12+

if (!Array.isArray(tools)) {

13+

return new Set();

14+

}

15+

const names = tools

16+

.map((tool) => {

17+

const record = toRecord(tool);

18+

return typeof record?.name === "string" && record.name.trim() ? record.name : undefined;

19+

})

20+

.filter((name): name is string => Boolean(name));

21+

return new Set(names);

22+

}

23+24+

function couldStillBePlainTextToolCall(text: string): boolean {

25+

if (text.length > 256_000) {

26+

return false;

27+

}

28+

const trimmed = text.trimStart();

29+

return (

30+

trimmed.length === 0 ||

31+

trimmed.startsWith("[") ||

32+

trimmed.startsWith("<|channel|>") ||

33+

trimmed.startsWith("commentary") ||

34+

trimmed.startsWith("analysis") ||

35+

trimmed.startsWith("final")

36+

);

37+

}

38+39+

function createSyntheticToolCallId(): string {

40+

return `call_${randomUUID().replace(/-/g, "").slice(0, 24)}`;

41+

}

42+43+

function createPlainTextToolCallBlock(parsed: {

44+

arguments: Record<string, unknown>;

45+

name: string;

46+

}): Record<string, unknown> {

47+

return {

48+

type: "toolCall",

49+

id: createSyntheticToolCallId(),

50+

name: parsed.name,

51+

arguments: parsed.arguments,

52+

partialArgs: JSON.stringify(parsed.arguments),

53+

};

54+

}

55+56+

function promotePlainTextToolCalls(

57+

message: unknown,

58+

toolNames: Set<string>,

59+

): Record<string, unknown> | undefined {

60+

const messageRecord = toRecord(message);

61+

if (!messageRecord) {

62+

return undefined;

63+

}

64+

if (!Array.isArray(messageRecord.content)) {

65+

if (typeof messageRecord.content !== "string" || !messageRecord.content.trim()) {

66+

return undefined;

67+

}

68+

const parsed = parseStandalonePlainTextToolCallBlocks(messageRecord.content, {

69+

allowedToolNames: toolNames,

70+

});

71+

if (!parsed) {

72+

return undefined;

73+

}

74+

return {

75+

...messageRecord,

76+

content: parsed.map(createPlainTextToolCallBlock),

77+

stopReason: "toolUse",

78+

};

79+

}

80+

if (

81+

messageRecord.content.some((block) => toRecord(block)?.type === "toolCall") ||

82+

messageRecord.content.length === 0

83+

) {

84+

return undefined;

85+

}

86+87+

let promoted = false;

88+

const nextContent: Array<Record<string, unknown>> = [];

89+

for (const block of messageRecord.content) {

90+

const blockRecord = toRecord(block);

91+

if (!blockRecord) {

92+

return undefined;

93+

}

94+

if (blockRecord.type !== "text") {

95+

nextContent.push(blockRecord);

96+

continue;

97+

}

98+

const text = typeof blockRecord.text === "string" ? blockRecord.text : "";

99+

if (!text.trim()) {

100+

continue;

101+

}

102+

const parsed = parseStandalonePlainTextToolCallBlocks(text, {

103+

allowedToolNames: toolNames,

104+

});

105+

if (!parsed) {

106+

return undefined;

107+

}

108+

nextContent.push(...parsed.map(createPlainTextToolCallBlock));

109+

promoted = true;

110+

}

111+112+

if (!promoted) {

113+

return undefined;

114+

}

115+

return {

116+

...messageRecord,

117+

content: nextContent,

118+

stopReason: "toolUse",

119+

};

120+

}

121+122+

function emitPromotedToolCallEvents(

123+

stream: { push(event: unknown): void },

124+

message: Record<string, unknown>,

125+

): void {

126+

const content = Array.isArray(message.content) ? message.content : [];

127+

content.forEach((block, contentIndex) => {

128+

const record = toRecord(block);

129+

if (record?.type !== "toolCall") {

130+

return;

131+

}

132+

stream.push({ type: "toolcall_start", contentIndex, partial: message });

133+

stream.push({

134+

type: "toolcall_delta",

135+

contentIndex,

136+

delta: typeof record.partialArgs === "string" ? record.partialArgs : "{}",

137+

partial: message,

138+

});

139+

});

140+

}

141+142+

function wrapPlainTextToolCallStream(

143+

source: ReturnType<StreamFn>,

144+

context: Parameters<StreamFn>[1],

145+

): ReturnType<StreamFn> {

146+

const toolNames = resolveContextToolNames(context);

147+

if (toolNames.size === 0) {

148+

return source;

149+

}

150+

const output = createAssistantMessageEventStream();

151+

const stream = output as unknown as { push(event: unknown): void; end(): void };

152+153+

void (async () => {

154+

const bufferedTextEvents: unknown[] = [];

155+

let bufferedText = "";

156+

let ended = false;

157+

const endStream = () => {

158+

if (!ended) {

159+

ended = true;

160+

stream.end();

161+

}

162+

};

163+

const flushBufferedTextEvents = () => {

164+

for (const event of bufferedTextEvents.splice(0)) {

165+

stream.push(event);

166+

}

167+

bufferedText = "";

168+

};

169+170+

try {

171+

for await (const event of source as AsyncIterable<unknown>) {

172+

const record = toRecord(event);

173+

const type = typeof record?.type === "string" ? record.type : "";

174+175+

if (type === "text_start" || type === "text_delta" || type === "text_end") {

176+

bufferedTextEvents.push(event);

177+

if (typeof record?.delta === "string") {

178+

bufferedText += record.delta;

179+

} else if (typeof record?.content === "string" && !bufferedText) {

180+

bufferedText = record.content;

181+

}

182+

if (!couldStillBePlainTextToolCall(bufferedText)) {

183+

flushBufferedTextEvents();

184+

}

185+

continue;

186+

}

187+188+

if (type === "done") {

189+

const promotedMessage = promotePlainTextToolCalls(record?.message, toolNames);

190+

if (promotedMessage) {

191+

bufferedTextEvents.splice(0);

192+

bufferedText = "";

193+

emitPromotedToolCallEvents(stream, promotedMessage);

194+

stream.push({ ...record, reason: "toolUse", message: promotedMessage });

195+

} else {

196+

flushBufferedTextEvents();

197+

stream.push(event);

198+

}

199+

endStream();

200+

return;

201+

}

202+203+

flushBufferedTextEvents();

204+

stream.push(event);

205+

if (type === "error") {

206+

endStream();

207+

return;

208+

}

209+

}

210+

flushBufferedTextEvents();

211+

} catch (error) {

212+

stream.push({

213+

type: "error",

214+

reason: "error",

215+

error: {

216+

role: "assistant",

217+

content: [],

218+

stopReason: "error",

219+

errorMessage: error instanceof Error ? error.message : String(error),

220+

},

221+

});

222+

} finally {

223+

endStream();

224+

}

225+

})();

226+227+

return output as ReturnType<StreamFn>;

228+

}

229+230+

/**

231+

* Bundled-provider runtime hygiene for providers that can leak tool-use syntax

232+

* as assistant text even when native tool calling is enabled.

233+

*/

234+

export function createPlainTextToolCallPromotionWrapper(

235+

baseStreamFn: StreamFn | undefined,

236+

): StreamFn {

237+

const underlying = baseStreamFn ?? streamSimple;

238+

return (model, context, options) => {

239+

const maybeStream = underlying(model, context, options);

240+

if (maybeStream && typeof maybeStream === "object" && "then" in maybeStream) {

241+

return Promise.resolve(maybeStream).then((stream) =>

242+

wrapPlainTextToolCallStream(stream, context),

243+

) as ReturnType<StreamFn>;

244+

}

245+

return wrapPlainTextToolCallStream(maybeStream, context);

246+

};

247+

}