muonroi-cli 1.6.1 → 1.6.3

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.
@@ -1,2 +1,2 @@
1
- export declare const PACKAGE_VERSION = "1.6.1";
1
+ export declare const PACKAGE_VERSION = "1.6.3";
2
2
  export declare const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
@@ -1,5 +1,5 @@
1
1
  // AUTO-GENERATED by scripts/sync-version.cjs. DO NOT EDIT BY HAND.
2
2
  // Sourced from package.json at build time so it survives bun --compile bundling.
3
- export const PACKAGE_VERSION = "1.6.1";
3
+ export const PACKAGE_VERSION = "1.6.3";
4
4
  export const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
5
5
  //# sourceMappingURL=version.js.map
@@ -109,6 +109,27 @@ describe("acquireMcpTools — cross-turn client pool", () => {
109
109
  expect(results.every((r) => r === "pong")).toBe(true);
110
110
  expect(connectOneServer).toHaveBeenCalledTimes(2); // 14 failures → exactly ONE shared reconnect
111
111
  });
112
+ it("waits for a criticalServerId past the normal deadline so it lands THIS turn (session 584ba476c07a)", async () => {
113
+ // Normal deadline is 500ms (mock). docs connects at ~700ms — past the normal
114
+ // deadline but within the critical window → must be included when critical.
115
+ connectOneServer.mockImplementation((s) => new Promise((res) => {
116
+ if (s.id === "docs")
117
+ setTimeout(() => res(connected(s.id)), 700);
118
+ else
119
+ res(connected(s.id));
120
+ }));
121
+ const b = await acquireMcpTools([srv("docs")], { criticalServerIds: ["docs"], criticalDeadlineMs: 3000 });
122
+ expect(Object.keys(b.tools)).toContain("mcp_docs__ping");
123
+ expect(b.errors).toHaveLength(0);
124
+ });
125
+ it("without criticalServerIds, a slow server is reported still-connecting (available next turn)", async () => {
126
+ connectOneServer.mockImplementation((s) => new Promise((res) => {
127
+ setTimeout(() => res(connected(s.id)), 700);
128
+ }));
129
+ const b = await acquireMcpTools([srv("docs")]);
130
+ expect(Object.keys(b.tools)).not.toContain("mcp_docs__ping");
131
+ expect(b.errors.some((e) => /still connecting/.test(e))).toBe(true);
132
+ });
112
133
  it("keys by cwd/config — a different command reconnects rather than reusing", async () => {
113
134
  connectOneServer.mockImplementation(async (s) => connected(s.id));
114
135
  await acquireMcpTools([
@@ -179,6 +179,28 @@ export async function acquireMcpTools(servers, opts) {
179
179
  await Promise.race([Promise.allSettled(attempts), deadline]);
180
180
  if (deadlineTimer)
181
181
  clearTimeout(deadlineTimer);
182
+ // Critical-server extended wait: a turn that MUST have a specific server (e.g.
183
+ // muonroi-docs on an ecosystem question) waits for just that server's connect
184
+ // beyond the normal deadline, so a cold first-connect is included THIS turn
185
+ // rather than reported "still connecting → next turn". Only the named servers
186
+ // are awaited; everything already settled is untouched (no added latency for
187
+ // normal turns, which pass no criticalServerIds).
188
+ const critical = new Set((opts?.criticalServerIds ?? []).filter(Boolean));
189
+ if (critical.size > 0) {
190
+ const pendingIdx = enabled.map((s, i) => ({ s, i })).filter(({ s, i }) => critical.has(s.id) && !slots[i].done);
191
+ if (pendingIdx.length > 0) {
192
+ const criticalDeadlineMs = Math.max(deadlineMs, opts?.criticalDeadlineMs ?? 8000);
193
+ const extraMs = Math.max(0, criticalDeadlineMs - deadlineMs);
194
+ let extraTimer;
195
+ const extraDeadline = new Promise((resolve) => {
196
+ extraTimer = setTimeout(resolve, extraMs);
197
+ extraTimer.unref?.();
198
+ });
199
+ await Promise.race([Promise.allSettled(pendingIdx.map(({ i }) => attempts[i])), extraDeadline]);
200
+ if (extraTimer)
201
+ clearTimeout(extraTimer);
202
+ }
203
+ }
182
204
  for (let i = 0; i < slots.length; i++) {
183
205
  const slot = slots[i];
184
206
  if (slot.done) {
@@ -8,6 +8,17 @@ export interface McpToolBundle {
8
8
  }
9
9
  export interface McpBuildOptions {
10
10
  onOAuthRequired?: (serverId: string, url: URL) => void;
11
+ /**
12
+ * Server ids the CURRENT turn critically needs (e.g. muonroi-docs on an
13
+ * ecosystem question). acquireMcpTools waits for these specifically beyond the
14
+ * normal build deadline — up to `criticalDeadlineMs` — so a cold first-connect
15
+ * is included THIS turn instead of "ready next turn" (session 584ba476c07a:
16
+ * first ecosystem question missed muonroi-docs while it was still warming).
17
+ * Other servers are unaffected — only the named ones get the extended wait.
18
+ */
19
+ criticalServerIds?: string[];
20
+ /** Extended ceiling (ms) for criticalServerIds. Default 8000. */
21
+ criticalDeadlineMs?: number;
11
22
  }
12
23
  /**
13
24
  * Total wall-clock budget for building the MCP tool set. Servers connect in
@@ -60,6 +60,7 @@ import * as phaseTracker from "../ee/phase-tracker.js";
60
60
  import { buildScope as buildScopeForVeto } from "../ee/scope.js";
61
61
  import { fireTrajectoryEvent } from "../ee/session-trajectory.js";
62
62
  import { getTenantId as getTenantIdForVeto } from "../ee/tenant.js";
63
+ import { mentionsEcosystemScope } from "../gsd/directives.js";
63
64
  import { acquireMcpTools } from "../mcp/client-pool.js";
64
65
  import { dropRedundantFsMcpTools, filterMcpServersByMessage } from "../mcp/smart-filter.js";
65
66
  import { getModelInfo } from "../models/registry.js";
@@ -1022,6 +1023,16 @@ export class MessageProcessor {
1022
1023
  const filteredServers = filterMcpServersByMessage(loadMcpServers(), userMessage, {
1023
1024
  disabled: process.env.MUONROI_DISABLE_SMART_MCP === "1",
1024
1025
  });
1026
+ // Ecosystem question → muonroi-docs is the authoritative source the
1027
+ // agent is nudged to consult FIRST. Wait for it specifically beyond the
1028
+ // normal deadline so a cold first-connect lands THIS turn instead of
1029
+ // "ready next turn" (session 584ba476c07a: first ecosystem question
1030
+ // missed docs while warming → agent guessed from local files).
1031
+ const criticalServerIds = mentionsEcosystemScope(userMessage)
1032
+ ? filteredServers
1033
+ .filter((s) => /(^|[-_])docs([-_]|$)/.test(s.id) && /muonroi/i.test(s.id))
1034
+ .map((s) => s.id)
1035
+ : undefined;
1025
1036
  // MCP non-blocking: acquireMcpTools self-bounds — it connects servers
1026
1037
  // in parallel and returns PARTIAL results at its internal deadline
1027
1038
  // (fast/cached servers included; slow first-connects reported in
@@ -1038,6 +1049,7 @@ export class MessageProcessor {
1038
1049
  // command-injection vector the old exec() opener had.
1039
1050
  openUrl(url);
1040
1051
  },
1052
+ ...(criticalServerIds && criticalServerIds.length > 0 ? { criticalServerIds } : {}),
1041
1053
  });
1042
1054
  }
1043
1055
  catch (err) {
@@ -111,6 +111,16 @@ export declare const SUBAGENT_COMPACT_DEFAULT_KEEP_LAST = 3;
111
111
  * the native contract + native-capabilities tell the agent to rely on for "task finished?" and
112
112
  * rehydrate during long meta conversations about CLI/PIL/compaction/EE. */
113
113
  export declare const IMPORTANT_TOOL_NAMES: readonly ["read_file", "grep", "lsp", "bash", "ee_query", "usage_forensics", "selfverify_start", "selfverify_result", "selfverify_status"];
114
+ /**
115
+ * MCP tool prefixes whose results are an AUTHORITATIVE source the agent is
116
+ * explicitly steered to fetch FIRST and ground on (the ECOSYSTEM_DOCS_NUDGE in
117
+ * src/gsd/directives.ts). Eliding them defeats the nudge — the agent calls the
118
+ * ecosystem docs, then compaction discards them and it goes blind on the very
119
+ * source it was told to trust (session 584ba476c07a: mcp_muonroi-docs__setup_guide
120
+ * + bb_recipe_list elided, ee_unavailable, 0 rehydrated → "partially blind").
121
+ * Keep their results verbatim so the agent stays grounded across the session.
122
+ */
123
+ export declare const HIGH_VALUE_MCP_PREFIXES: readonly ["mcp_muonroi-docs__"];
114
124
  /**
115
125
  * Heuristic: keep full (no stub) for high-signal tool results.
116
126
  * Signals: allowlist tool + (error/todo/plan/keyfile/large output or explicit keep list).
@@ -73,6 +73,16 @@ export const IMPORTANT_TOOL_NAMES = [
73
73
  "selfverify_result",
74
74
  "selfverify_status",
75
75
  ];
76
+ /**
77
+ * MCP tool prefixes whose results are an AUTHORITATIVE source the agent is
78
+ * explicitly steered to fetch FIRST and ground on (the ECOSYSTEM_DOCS_NUDGE in
79
+ * src/gsd/directives.ts). Eliding them defeats the nudge — the agent calls the
80
+ * ecosystem docs, then compaction discards them and it goes blind on the very
81
+ * source it was told to trust (session 584ba476c07a: mcp_muonroi-docs__setup_guide
82
+ * + bb_recipe_list elided, ee_unavailable, 0 rehydrated → "partially blind").
83
+ * Keep their results verbatim so the agent stays grounded across the session.
84
+ */
85
+ export const HIGH_VALUE_MCP_PREFIXES = ["mcp_muonroi-docs__"];
76
86
  /**
77
87
  * Heuristic: keep full (no stub) for high-signal tool results.
78
88
  * Signals: allowlist tool + (error/todo/plan/keyfile/large output or explicit keep list).
@@ -86,6 +96,10 @@ export function isHighValueToolResult(toolName, preview, explicitKeepIds, toolCa
86
96
  // work/findings. Truncating it causes the agent to think it lost its answer.
87
97
  if (name.startsWith("respond_"))
88
98
  return true;
99
+ // Authoritative ecosystem-docs MCP results: the agent is nudged to fetch these
100
+ // FIRST, so eliding them strands it (session 584ba476c07a). Keep verbatim.
101
+ if (HIGH_VALUE_MCP_PREFIXES.some((p) => name.startsWith(p)))
102
+ return true;
89
103
  if (IMPORTANT_TOOL_NAMES.includes(name)) {
90
104
  const p = preview.toLowerCase();
91
105
  if (/error|fail|todo|plan|done|✔|blocked|critical/.test(p))
@@ -110,6 +110,60 @@ describe("subagent-compactor: compactSubAgentMessages", () => {
110
110
  expect(out[out.length - i]).toBe(msgs[msgs.length - i]);
111
111
  }
112
112
  });
113
+ it("keeps an OLDER authoritative muonroi-docs MCP result verbatim while eliding low-value peers (session 584ba476c07a)", () => {
114
+ // History: an early muonroi-docs setup_guide (older than keepLast=3) + many
115
+ // low-value tool turns. The ecosystem doc must survive compaction so the
116
+ // agent stays grounded on the source it was nudged to fetch first.
117
+ const docsValue = bigText("ECOSYSTEM_DOCS", 6); // ~6kb authoritative payload
118
+ const msgs = [
119
+ { role: "system", content: "You are the agent." },
120
+ { role: "user", content: "ecosystem question" },
121
+ {
122
+ role: "assistant",
123
+ content: [
124
+ {
125
+ type: "tool-call",
126
+ toolCallId: "call_docs",
127
+ toolName: "mcp_muonroi-docs__setup_guide",
128
+ input: JSON.stringify({ component: "ecosystem" }),
129
+ },
130
+ ],
131
+ },
132
+ {
133
+ role: "tool",
134
+ content: [
135
+ {
136
+ type: "tool-result",
137
+ toolCallId: "call_docs",
138
+ toolName: "mcp_muonroi-docs__setup_guide",
139
+ output: { type: "text", value: docsValue },
140
+ },
141
+ ],
142
+ },
143
+ ];
144
+ // Pile on low-value turns to push well past threshold and make the docs turn "old".
145
+ for (let i = 1; i <= 10; i++) {
146
+ const t = toolTurn(i, 10);
147
+ t[1].content[0].toolName = "mcp_filesystem__list_directory"; // low-value MCP
148
+ t[0].content[0].toolName = "mcp_filesystem__list_directory";
149
+ msgs.push(...t);
150
+ }
151
+ const out = compactSubAgentMessages(msgs);
152
+ expect(out).not.toBe(msgs); // compaction fired
153
+ // The muonroi-docs result is kept verbatim (full payload, no stub).
154
+ const docsMsg = out.find((m) => m.role === "tool" &&
155
+ Array.isArray(m.content) &&
156
+ m.content[0]?.toolName === "mcp_muonroi-docs__setup_guide");
157
+ const docsOut = (docsMsg?.content)[0].output.value;
158
+ expect(docsOut).toBe(docsValue);
159
+ expect(docsOut).not.toMatch(/elided by/);
160
+ // A low-value filesystem MCP peer from an OLD turn IS stubbed.
161
+ const stubbed = out.some((m) => m.role === "tool" &&
162
+ Array.isArray(m.content) &&
163
+ typeof m.content[0]?.output?.value === "string" &&
164
+ m.content[0].output.value.includes("elided by"));
165
+ expect(stubbed).toBe(true);
166
+ });
113
167
  it("rewrites older tool-result parts with elision stub", () => {
114
168
  const msgs = buildHistory(10, 10);
115
169
  // Neutralize tool so the basic elision test is not affected by high-value auto-keep (idea 1).
@@ -104,6 +104,16 @@ describe("layer4Gsd (gsd-native)", () => {
104
104
  const result = await layer4Gsd(makeCtx({ raw, enriched: raw, taskType: "analyze", intentKind: "task", deliverableKind: "answer" }));
105
105
  expect(result.enriched).toContain("QUESTION / explanatory");
106
106
  });
107
+ it("deliverableKind='report' is informational (no council/discuss scaffold) — session 666630479c1a", async () => {
108
+ // "Đọc và tóm tắt kiến trúc…" classifies as deliverableKind 'report'. A
109
+ // report is human-facing with NO code change, so it must route to the
110
+ // QUESTION directive, not the heavy implement/discuss/council scaffold that
111
+ // over-asked with askcards on a read/summarize task.
112
+ const raw = "đọc và tóm tắt kiến trúc src/orchestrator, src/pil, src/mcp kèm file:line";
113
+ const result = await layer4Gsd(makeCtx({ raw, enriched: raw, taskType: "analyze", intentKind: "task", deliverableKind: "report" }));
114
+ expect(result.enriched).toContain("QUESTION / explanatory");
115
+ expect(result.enriched).not.toContain("MANDATORY");
116
+ });
107
117
  it("Phase 2b: deliverableKind='code' is NOT informational even for a question-shaped prompt", async () => {
108
118
  // The raw text reads as a question — the legacy regex would mark it
109
119
  // informational. The model's deliverableKind='code' must override that so
@@ -84,15 +84,20 @@ export async function layer4Gsd(ctx) {
84
84
  // into the human-facing reply as a "2-3 line plan" + process narration
85
85
  // (session 829a83888dd2). Route them to the human-facing question directive.
86
86
  //
87
- // Phase 2b: when the model classified the deliverable, CONSUME it an
88
- // "answer" deliverable IS informational. Only when the model didn't emit one
89
- // (deliverableKind null legacy cascade, or the model omitted the word) do
90
- // we fall back to the legacy regex predicates:
87
+ // Phase 2b: when the model classified the deliverable, CONSUME it. Both an
88
+ // "answer" AND a "report" deliverable are HUMAN-FACING with no code change, so
89
+ // both are informational only "code" routes through the implement/verify (and
90
+ // heavy discuss/council) scaffold. Treating "report" as non-informational sent
91
+ // read/summarize/architecture tasks (deliverableKind "report") down the heavy
92
+ // council + AskUserQuestion path, over-asking on a task that just wanted a
93
+ // written summary (session 666630479c1a: "Đọc và tóm tắt kiến trúc…" raised 2
94
+ // askcards + a council loop). Only when the model emitted no deliverable
95
+ // (deliverableKind null → legacy cascade) do we fall back to regex predicates:
91
96
  // 1. isMetaAnalysisPrompt — self/CLI evaluation, prior-turn reflection.
92
97
  // 2. taskType "general" classified as a real task by L1.
93
98
  // 3. question-shaped prompt that is NOT an implementation request.
94
99
  const informational = ctx.deliverableKind
95
- ? ctx.deliverableKind === "answer"
100
+ ? ctx.deliverableKind !== "code"
96
101
  : isMetaAnalysisPrompt(ctx.raw) ||
97
102
  (ctx.taskType === "general" && ctx.intentKind === "task") ||
98
103
  (isQuestionLike(ctx.raw) && !isImplementationIntent(ctx.raw));
@@ -31,6 +31,29 @@ export function extractToolResultFromOutput(output) {
31
31
  output: String(output.value),
32
32
  };
33
33
  }
34
+ // MCP tool results: `{ type: "content", value: [{ type: "text", text }, ...] }`
35
+ // (see cap-tool-result.ts). Before this branch, extraction returned null, so
36
+ // persisted output_json was the raw envelope with NO `success` field — on
37
+ // reload the renderer read `toolResult.success` as undefined and displayed
38
+ // "Error" for a SUCCESSFUL call (session 63f2d542b772: 50 muonroi-docs calls,
39
+ // 0 DB failures, all shown as "Error"). Flatten the text parts so it round-
40
+ // trips as a real ToolResult. A genuinely failed MCP call throws → the SDK
41
+ // records an `error-text` part, handled above, so content == success here.
42
+ if ("type" in output && output.type === "content" && "value" in output && Array.isArray(output.value)) {
43
+ const parts = output.value;
44
+ const text = parts
45
+ .filter((p) => !!p &&
46
+ typeof p === "object" &&
47
+ p.type === "text" &&
48
+ typeof p.text === "string")
49
+ .map((p) => p.text)
50
+ .join("\n");
51
+ const nonText = parts.length - parts.filter((p) => p?.type === "text").length;
52
+ return {
53
+ success: true,
54
+ output: text || (nonText > 0 ? `[${nonText} non-text MCP part(s)]` : "(empty MCP result)"),
55
+ };
56
+ }
34
57
  return null;
35
58
  }
36
59
  export function getOutputKind(output) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { extractToolResultFromOutput, isOutputSuccess } from "./tool-results.js";
3
+ describe("extractToolResultFromOutput", () => {
4
+ it("passes through a native ToolResult shape", () => {
5
+ const r = extractToolResultFromOutput({ success: true, output: "hi" });
6
+ expect(r).toMatchObject({ success: true, output: "hi" });
7
+ });
8
+ it("treats error-text as a failure", () => {
9
+ const r = extractToolResultFromOutput({ type: "error-text", value: "boom" });
10
+ expect(r).toMatchObject({ success: false, error: "boom" });
11
+ });
12
+ it("flattens an MCP content envelope into a successful ToolResult (session 63f2d542b772)", () => {
13
+ // MCP tools return { type: "content", value: [{ type: "text", text }] }.
14
+ // Before the fix this returned null, so the persisted output_json had no
15
+ // `success` field and the renderer showed "Error" for a successful call.
16
+ const out = {
17
+ type: "content",
18
+ value: [
19
+ { type: "text", text: "package list" },
20
+ { type: "text", text: "more" },
21
+ ],
22
+ };
23
+ const r = extractToolResultFromOutput(out);
24
+ expect(r).toMatchObject({ success: true, output: "package list\nmore" });
25
+ });
26
+ it("round-trips through JSON so the renderer reads success=true (the actual bug)", () => {
27
+ // transcript.loadStoredToolResults does JSON.parse(output_json) and the
28
+ // renderer reads `.success`. Simulate persist→load and assert it is NOT
29
+ // misread as an error.
30
+ const mcpOutput = { type: "content", value: [{ type: "text", text: "## Muonroi.Core\nNuGet: Muonroi.Core" }] };
31
+ const persisted = JSON.stringify(extractToolResultFromOutput(mcpOutput));
32
+ const loaded = JSON.parse(persisted);
33
+ const rendered = loaded.success ? loaded.output || "Success" : loaded.error || "Error";
34
+ expect(rendered).toContain("Muonroi.Core");
35
+ expect(rendered).not.toBe("Error");
36
+ });
37
+ it("describes a non-text-only MCP content result instead of dropping to Error", () => {
38
+ const out = { type: "content", value: [{ type: "image", data: "..." }] };
39
+ const r = extractToolResultFromOutput(out);
40
+ expect(r?.success).toBe(true);
41
+ expect(r?.output).toMatch(/non-text MCP part/);
42
+ });
43
+ it("isOutputSuccess still treats content envelopes as success", () => {
44
+ expect(isOutputSuccess({ type: "content", value: [] })).toBe(true);
45
+ expect(isOutputSuccess({ type: "error-text", value: "x" })).toBe(false);
46
+ });
47
+ });
48
+ //# sourceMappingURL=tool-results.test.js.map
@@ -39,6 +39,23 @@ describe("parseInline — marker concealment", () => {
39
39
  expect(text(parseInline("a **partial answer", t))).toBe("a **partial answer");
40
40
  expect(text(parseInline("trailing `code", t))).toBe("trailing `code");
41
41
  });
42
+ it("does NOT treat intra-word underscores as emphasis (identifiers stay intact)", () => {
43
+ // Session 584ba476c07a rendered `mcp_filesystem__list_directory` as
44
+ // "mcpfilesystemlistdirectory" — underscores eaten as italic/bold.
45
+ expect(text(parseInline("mcp_filesystem__list_directory", t))).toBe("mcp_filesystem__list_directory");
46
+ expect(text(parseInline("a snake_case name", t))).toBe("a snake_case name");
47
+ expect(text(parseInline("call mcp_muonroi-docs__setup_guide first", t))).toBe("call mcp_muonroi-docs__setup_guide first");
48
+ // None of these should be emphasized.
49
+ expect(parseInline("mcp_filesystem__list_directory", t).some((s) => s.italic || s.bold)).toBe(false);
50
+ });
51
+ it("still emphasizes underscores at word boundaries", () => {
52
+ expect(text(parseInline("an _italic_ word", t))).toBe("an italic word");
53
+ expect(parseInline("an _italic_ word", t).find((s) => s.text === "italic")?.italic).toBe(true);
54
+ expect(text(parseInline("a __bold__ word", t))).toBe("a bold word");
55
+ expect(parseInline("a __bold__ word", t).find((s) => s.text === "bold")?.bold).toBe(true);
56
+ // Underscore emphasis adjacent to punctuation still works.
57
+ expect(parseInline("(_em_)", t).find((s) => s.text === "em")?.italic).toBe(true);
58
+ });
42
59
  it("never leaves ** ` ### markers in styled segments", () => {
43
60
  const sample = "**A** and `b` and ***c*** and [d](http://e) and ~~f~~";
44
61
  const out = text(parseInline(sample, t));
@@ -2525,7 +2525,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
2525
2525
  applyLocalAssistantDelta(`\n⚠ [Experience] ${eeChunk.experienceWarning?.message ?? eeChunk.content ?? ""}\nWhy: ${eeChunk.experienceWarning?.why ?? ""}\n`);
2526
2526
  }
2527
2527
  else if (eeChunk.type === "experience_injected") {
2528
- applyLocalAssistantDelta(`\n💡 [Experience Injected] ${eeChunk.experienceInjected?.pointCount ?? 0} point(s) loaded (score ≥ ${eeChunk.experienceInjected?.scoreFloor ?? 0})\n`);
2528
+ applyLocalAssistantDelta(formatExperienceInjectedBlock(eeChunk.experienceInjected ?? {}));
2529
2529
  }
2530
2530
  });
2531
2531
  for await (const chunk of agent.processMessage(text.trim(), undefined, images)) {
@@ -3472,10 +3472,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
3472
3472
  },
3473
3473
  ];
3474
3474
  }
3475
- return [
3476
- ...prev,
3477
- buildAssistantEntry(`💡 [Experience Injected] ${chunk.experienceInjected.pointCount} point(s)`),
3478
- ];
3475
+ return [...prev, buildAssistantEntry(formatExperienceInjectedBlock(chunk.experienceInjected))];
3479
3476
  });
3480
3477
  }
3481
3478
  if (chunk.type === "halt" && chunk.haltChunk) {
@@ -3694,10 +3691,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
3694
3691
  },
3695
3692
  ];
3696
3693
  }
3697
- return [
3698
- ...prev,
3699
- buildAssistantEntry(`💡 [Experience Injected] ${chunk.experienceInjected.pointCount} point(s)`),
3700
- ];
3694
+ return [...prev, buildAssistantEntry(formatExperienceInjectedBlock(chunk.experienceInjected))];
3701
3695
  });
3702
3696
  }
3703
3697
  if (chunk.type === "done")
@@ -77,6 +77,18 @@ export function parseInline(line, t, base = {}) {
77
77
  const inner = line.slice(i + m.open.length, close);
78
78
  if (inner.length === 0)
79
79
  continue;
80
+ // CommonMark: underscore does NOT open/close emphasis intra-word, so
81
+ // identifiers like `mcp_filesystem__list_directory` or `snake_case` keep
82
+ // their underscores instead of being eaten as italic/bold (session
83
+ // 584ba476c07a rendered "mcpfilesystemlistdirectory"). Asterisk markers
84
+ // keep intraword behaviour. Reject when a word char hugs the marker on the
85
+ // word-internal side.
86
+ if (m.open[0] === "_") {
87
+ const before = i > 0 ? line[i - 1] : "";
88
+ const after = line[close + m.close.length] ?? "";
89
+ if (/[A-Za-z0-9]/.test(before) || /[A-Za-z0-9]/.test(after))
90
+ continue;
91
+ }
80
92
  flushPlain(i);
81
93
  const isBold = "bold" in m && m.bold;
82
94
  const isItalic = "italic" in m && m.italic;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.6.1",
6
+ "version": "1.6.3",
7
7
  "description": "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.",
8
8
  "repository": {
9
9
  "type": "git",