@botcord/daemon 0.2.53 → 0.2.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -276,7 +276,7 @@ export class OpenclawAcpAdapter {
276
276
  }
277
277
  const pickedText = normalizeAssistantText(pickFinalText(promptResult));
278
278
  const streamedText = normalizeAssistantText(assistantText);
279
- finalText = pickedText && !looksLikeReasoningLeak(pickedText) ? pickedText : streamedText;
279
+ finalText = pickSafeAssistantText(pickedText) || pickSafeAssistantText(streamedText);
280
280
  if (capped) {
281
281
  log.warn("openclaw-acp.assistant-text-capped", { sessionId: acpSessionId });
282
282
  }
@@ -741,8 +741,7 @@ function createAssistantTextFilter() {
741
741
  if (flush && !seenFinal && fallback) {
742
742
  const text = normalizeAssistantText(fallback);
743
743
  fallback = "";
744
- if (!looksLikeReasoningLeak(text))
745
- return text;
744
+ return pickSafeAssistantText(text);
746
745
  }
747
746
  return out;
748
747
  };
@@ -800,6 +799,37 @@ function looksLikeReasoningLeak(text) {
800
799
  /\bi('|’)ll respond\b/i.test(t) ||
801
800
  /\bi need to\b/i.test(t));
802
801
  }
802
+ function pickSafeAssistantText(text) {
803
+ if (!text)
804
+ return "";
805
+ const trimmed = text.trim();
806
+ if (!looksLikeReasoningLeak(trimmed))
807
+ return trimmed;
808
+ return stripLeadingReasoningLeak(trimmed);
809
+ }
810
+ function stripLeadingReasoningLeak(text) {
811
+ const paragraphs = text
812
+ .replace(/\r\n/g, "\n")
813
+ .split(/\n{2,}/)
814
+ .map((p) => p.trim())
815
+ .filter(Boolean);
816
+ while (paragraphs.length > 0 && isReasoningNarration(paragraphs[0])) {
817
+ paragraphs.shift();
818
+ }
819
+ const candidate = paragraphs.join("\n\n").trim();
820
+ return candidate && !looksLikeReasoningLeak(candidate) ? candidate : "";
821
+ }
822
+ function isReasoningNarration(text) {
823
+ const t = text.trim();
824
+ if (!t)
825
+ return false;
826
+ return (looksLikeReasoningLeak(t) ||
827
+ /^let me\b/i.test(t) ||
828
+ /^i('|’)ll\b/i.test(t) ||
829
+ /^i will\b/i.test(t) ||
830
+ /^i should\b/i.test(t) ||
831
+ /^i need to\b/i.test(t));
832
+ }
803
833
  function stringField(bag, key) {
804
834
  if (!bag)
805
835
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.53",
3
+ "version": "0.2.54",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -353,6 +353,62 @@ describe("OpenclawAcpAdapter.run", () => {
353
353
  expect(assistantChunks).toEqual(["好!终于可以正常交流了。"]);
354
354
  });
355
355
 
356
+ it("keeps final streamed text after an untagged reasoning preamble", async () => {
357
+ const child = new FakeChild();
358
+ const adapter = new OpenclawAcpAdapter({ spawnFn: makeSpawn(child) });
359
+ const gateway: ResolvedOpenclawGateway = {
360
+ name: "local",
361
+ url: "ws://127.0.0.1:1",
362
+ openclawAgent: "main",
363
+ };
364
+
365
+ child.stdin.on("data", (chunk: Buffer) => {
366
+ for (const line of chunk.toString("utf8").split("\n").filter(Boolean)) {
367
+ const frame = JSON.parse(line);
368
+ if (frame.method === "initialize") {
369
+ child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { protocolVersion: 1 } }) + "\n");
370
+ } else if (frame.method === "session/new") {
371
+ child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { sessionId: "sid-reasoning-preamble" } }) + "\n");
372
+ } else if (frame.method === "session/prompt") {
373
+ child.stdout.write(
374
+ JSON.stringify({
375
+ jsonrpc: "2.0",
376
+ method: "session/update",
377
+ params: {
378
+ sessionId: "sid-reasoning-preamble",
379
+ update: {
380
+ sessionUpdate: "agent_message_chunk",
381
+ content: {
382
+ type: "text",
383
+ text: "The user is asking about today's weather in Chinese. Let me check the weather skill to see how to get this info.\n\n今天上海天气不错!\n\n- 气温:29°C\n- 湿度:52%",
384
+ },
385
+ },
386
+ },
387
+ }) + "\n",
388
+ );
389
+ child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { stopReason: "end_turn" } }) + "\n");
390
+ }
391
+ }
392
+ });
393
+
394
+ const blocks: any[] = [];
395
+ const res = await adapter.run({
396
+ text: "今天天气咋样",
397
+ sessionId: null,
398
+ cwd: "/tmp",
399
+ accountId: "ag_alice",
400
+ signal: new AbortController().signal,
401
+ trustLevel: "owner",
402
+ gateway,
403
+ onBlock: (b) => blocks.push(b),
404
+ });
405
+
406
+ expect(res.error).toBeUndefined();
407
+ expect(res.text).toBe("今天上海天气不错!\n\n- 气温:29°C\n- 湿度:52%");
408
+ expect(blocks.filter((b) => b.kind === "assistant_text")).toHaveLength(1);
409
+ expect(JSON.stringify(blocks)).not.toContain("The user is asking");
410
+ });
411
+
356
412
  it("preserves real leading angle syntax in streamed fallback text", async () => {
357
413
  const child = new FakeChild();
358
414
  const adapter = new OpenclawAcpAdapter({ spawnFn: makeSpawn(child) });
@@ -350,7 +350,7 @@ export class OpenclawAcpAdapter implements RuntimeAdapter {
350
350
  }
351
351
  const pickedText = normalizeAssistantText(pickFinalText(promptResult));
352
352
  const streamedText = normalizeAssistantText(assistantText);
353
- finalText = pickedText && !looksLikeReasoningLeak(pickedText) ? pickedText : streamedText;
353
+ finalText = pickSafeAssistantText(pickedText) || pickSafeAssistantText(streamedText);
354
354
 
355
355
  if (capped) {
356
356
  log.warn("openclaw-acp.assistant-text-capped", { sessionId: acpSessionId });
@@ -853,7 +853,7 @@ function createAssistantTextFilter(): {
853
853
  if (flush && !seenFinal && fallback) {
854
854
  const text = normalizeAssistantText(fallback);
855
855
  fallback = "";
856
- if (!looksLikeReasoningLeak(text)) return text;
856
+ return pickSafeAssistantText(text);
857
857
  }
858
858
  return out;
859
859
  };
@@ -909,6 +909,41 @@ function looksLikeReasoningLeak(text: string): boolean {
909
909
  );
910
910
  }
911
911
 
912
+ function pickSafeAssistantText(text: string | undefined): string {
913
+ if (!text) return "";
914
+ const trimmed = text.trim();
915
+ if (!looksLikeReasoningLeak(trimmed)) return trimmed;
916
+ return stripLeadingReasoningLeak(trimmed);
917
+ }
918
+
919
+ function stripLeadingReasoningLeak(text: string): string {
920
+ const paragraphs = text
921
+ .replace(/\r\n/g, "\n")
922
+ .split(/\n{2,}/)
923
+ .map((p) => p.trim())
924
+ .filter(Boolean);
925
+
926
+ while (paragraphs.length > 0 && isReasoningNarration(paragraphs[0])) {
927
+ paragraphs.shift();
928
+ }
929
+
930
+ const candidate = paragraphs.join("\n\n").trim();
931
+ return candidate && !looksLikeReasoningLeak(candidate) ? candidate : "";
932
+ }
933
+
934
+ function isReasoningNarration(text: string): boolean {
935
+ const t = text.trim();
936
+ if (!t) return false;
937
+ return (
938
+ looksLikeReasoningLeak(t) ||
939
+ /^let me\b/i.test(t) ||
940
+ /^i('|’)ll\b/i.test(t) ||
941
+ /^i will\b/i.test(t) ||
942
+ /^i should\b/i.test(t) ||
943
+ /^i need to\b/i.test(t)
944
+ );
945
+ }
946
+
912
947
  function stringField(bag: Record<string, unknown> | undefined, key: string): string | undefined {
913
948
  if (!bag) return undefined;
914
949
  const v = bag[key];