From 519a9e3ead79048bcb3687bc5644e1be91a1841d Mon Sep 17 00:00:00 2001 From: "(CJ) Chukwudi Nwobodo" <142016413+chuks-qua@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:49:20 +0100 Subject: [PATCH 1/4] test(web): cover literal characters in sticky message previews --- .../__tests__/user-message-preview.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/web/src/components/chat/__tests__/user-message-preview.test.ts b/apps/web/src/components/chat/__tests__/user-message-preview.test.ts index 73805db06..d8ccebc76 100644 --- a/apps/web/src/components/chat/__tests__/user-message-preview.test.ts +++ b/apps/web/src/components/chat/__tests__/user-message-preview.test.ts @@ -31,6 +31,24 @@ describe("resolveUserMessagePreview", () => { ); }); + it.each([ + ["call thread_target_list then delegate_task", "call thread_target_list then delegate_task"], + ["see `src/__tests__/a_b.ts`", "see src/__tests__/a_b.ts"], + ["open https://example.com/wiki/Foo_(bar)", "open https://example.com/wiki/Foo_(bar)"], + ["fix issue #2269 when a > b and [x] is set", "fix issue #2269 when a > b and [x] is set"], + ["2 * 3 * 4 = 24", "2 * 3 * 4 = 24"], + ])("keeps literal characters in %j", (content, expected) => { + expect(resolveUserMessagePreview(userMessage({ content }))).toBe(expected); + }); + + it.each([ + ["## Plan\n> quoted _note_", "Plan quoted note"], + ["read [the docs](https://example.com/a_(b)) first", "read the docs first"], + ["__bold__ and ~~gone~~", "bold and gone"], + ])("drops markdown syntax in %j", (content, expected) => { + expect(resolveUserMessagePreview(userMessage({ content }))).toBe(expected); + }); + it("returns goal conditions for /goal set commands", () => { expect(resolveUserMessagePreview(userMessage({ content: "/goal ship sticky messages" }))).toBe( "ship sticky messages", From 754415895aee863d919339b70b1731e9f400cad8 Mon Sep 17 00:00:00 2001 From: "(CJ) Chukwudi Nwobodo" <142016413+chuks-qua@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:51:56 +0100 Subject: [PATCH 2/4] fix(web): keep literal characters in sticky message previews The sticky header stripped every _, *, [, ], (, ) and # from the prompt, so identifiers like delegate_task showed as delegatetask. Move the release notes Markdown stripper into a shared helper that removes only real delimiters and keeps code span contents verbatim, and use it for both. --- .../components/chat/user-message-preview.ts | 9 +++-- .../web/src/features/updates/release-notes.ts | 16 ++------ apps/web/src/lib/inline-markdown.ts | 37 +++++++++++++++++++ 3 files changed, 45 insertions(+), 17 deletions(-) create mode 100644 apps/web/src/lib/inline-markdown.ts diff --git a/apps/web/src/components/chat/user-message-preview.ts b/apps/web/src/components/chat/user-message-preview.ts index 18d37363d..452cc5a4a 100644 --- a/apps/web/src/components/chat/user-message-preview.ts +++ b/apps/web/src/components/chat/user-message-preview.ts @@ -1,5 +1,6 @@ import { PLAN_ANSWER_MESSAGE_PREFIX } from "@mcode/contracts"; import { stripInjectedFiles } from "@/lib/file-tags"; +import { stripInlineMarkdown } from "@/lib/inline-markdown"; import type { Message } from "@/transport"; /** @@ -16,16 +17,16 @@ function parseUserGoalCommand(content: string): { condition: string } | null { return { condition: arg }; } +const BLOCK_MARKER = /^[ \t]{0,3}(?:#{1,6}[ \t]+|(?:>[ \t]?)+)/gm; + /** Collapses markdown noise so sticky previews stay readable in one or two lines. */ function toPlainPreview(text: string): string { const codeBlocks = [...text.matchAll(/```[\w-]*\n?([\s\S]*?)```/g)] .map((match) => match[1]?.trim() ?? "") .filter(Boolean); - const plain = text - .replace(/```[\s\S]*?```/g, " ") - .replace(/`([^`]+)`/g, "$1") - .replace(/[#*_>~[\]()]/g, "") + const plain = stripInlineMarkdown(text.replace(/```[\s\S]*?```/g, " ")) + .replace(BLOCK_MARKER, "") .replace(/\s+/g, " ") .trim(); diff --git a/apps/web/src/features/updates/release-notes.ts b/apps/web/src/features/updates/release-notes.ts index 6fe0857e7..1ca68bff5 100644 --- a/apps/web/src/features/updates/release-notes.ts +++ b/apps/web/src/features/updates/release-notes.ts @@ -1,3 +1,5 @@ +import { stripInlineMarkdown } from "@/lib/inline-markdown"; + /** Release notes reduced to plain text the update card can render safely. */ export interface ParsedReleaseNotes { /** Up to {@link MAX_RELEASE_NOTE_ITEMS} list items, as plain text. */ @@ -16,8 +18,6 @@ const MARKDOWN_HEADING = /^\s{0,3}#{1,6}\s/; const EXECUTABLE_BLOCK = /<(script|style)\b[^>]*>[\s\S]*?(?:<\/\1\s*>|$)/gi; const HTML_HEADING_BLOCK = /]*>[\s\S]*?<\/h[1-6]\s*>/gi; const BLOCK_BOUNDARY = /<\/?(?:p|div|ul|ol|li|br|hr|h[1-6]|blockquote|pre|table|tr)\b[^>]*>/gi; -// Link targets may hold one level of parentheses, as in javascript:alert(1). -const MARKDOWN_LINK = /!?\[([^\]]*)\]\((?:[^()]|\([^()]*\))*\)/g; const ANY_TAG = /<\/?[a-z!][^>]*>/gi; const NAMED_ENTITIES: Readonly> = { @@ -39,20 +39,10 @@ function decodeEntities(text: string): string { }); } -function stripMarkdown(text: string): string { - return text - .replace(MARKDOWN_LINK, "$1") - .replace(/\[([^\]]*)\]\[[^\]]*\]/g, "$1") - .replace(/`+([^`]*)`+/g, "$1") - .replace(/(\*\*|__)(.+?)\1/g, "$2") - .replace(/(^|[^\w*])[*_]([^*_]+)[*_](?=[^\w*]|$)/g, "$1$2") - .replace(/~~(.+?)~~/g, "$1"); -} - /** Reduces one fragment of HTML or markdown to a single line of plain text. */ function toPlainText(fragment: string): string { const withoutTags = fragment.replace(EXECUTABLE_BLOCK, " ").replace(BLOCK_BOUNDARY, " ").replace(ANY_TAG, ""); - return decodeEntities(stripMarkdown(withoutTags)).replace(/\s+/g, " ").trim(); + return decodeEntities(stripInlineMarkdown(withoutTags)).replace(/\s+/g, " ").trim(); } function htmlListItems(notes: string): string[] { diff --git a/apps/web/src/lib/inline-markdown.ts b/apps/web/src/lib/inline-markdown.ts new file mode 100644 index 000000000..e67b359c4 --- /dev/null +++ b/apps/web/src/lib/inline-markdown.ts @@ -0,0 +1,37 @@ +const CODE_SPAN = /(`+)([\s\S]*?)\1/g; +// Link targets may hold one level of parentheses, as in javascript:alert(1). +const INLINE_LINK = /!?\[([^\]]*)\]\((?:[^()]|\([^()]*\))*\)/g; +const REFERENCE_LINK = /\[([^\]]*)\]\[[^\]]*\]/g; +// Delimiters only count when they hug non-space text, so `2 * 3 * 4` keeps its stars. +const STRONG_STARS = /\*\*(?=\S)([\s\S]*?\S)\*\*/g; +const EMPHASIS_STAR = /\*(?=[^\s*])([^*]*?[^\s*])\*/g; +// Underscores never open or close inside a word, so `snake_case` stays intact. +const STRONG_UNDERSCORES = /(? Date: Wed, 21 Oct 2026 14:58:15 +0100 Subject: [PATCH 3/4] refactor(web): name the link pattern's one-level paren limit --- apps/web/src/lib/inline-markdown.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/web/src/lib/inline-markdown.ts b/apps/web/src/lib/inline-markdown.ts index e67b359c4..a714c7009 100644 --- a/apps/web/src/lib/inline-markdown.ts +++ b/apps/web/src/lib/inline-markdown.ts @@ -1,6 +1,5 @@ const CODE_SPAN = /(`+)([\s\S]*?)\1/g; -// Link targets may hold one level of parentheses, as in javascript:alert(1). -const INLINE_LINK = /!?\[([^\]]*)\]\((?:[^()]|\([^()]*\))*\)/g; +const INLINE_LINK_ONE_PAREN_LEVEL = /!?\[([^\]]*)\]\((?:[^()]|\([^()]*\))*\)/g; const REFERENCE_LINK = /\[([^\]]*)\]\[[^\]]*\]/g; // Delimiters only count when they hug non-space text, so `2 * 3 * 4` keeps its stars. const STRONG_STARS = /\*\*(?=\S)([\s\S]*?\S)\*\*/g; @@ -12,7 +11,7 @@ const STRIKETHROUGH = /~~(?=\S)([\s\S]*?\S)~~/g; function stripDelimiters(prose: string): string { return prose - .replace(INLINE_LINK, "$1") + .replace(INLINE_LINK_ONE_PAREN_LEVEL, "$1") .replace(REFERENCE_LINK, "$1") .replace(STRONG_STARS, "$1") .replace(STRONG_UNDERSCORES, "$1") From 3179f1b33e11b8c93941df17e97b85e94039ca61 Mon Sep 17 00:00:00 2001 From: "(CJ) Chukwudi Nwobodo" <142016413+chuks-qua@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:10:38 +0100 Subject: [PATCH 4/4] fix(server): never expose a half-written handoff audit writeHandoffAudit created the audit directory before writing handoff.md, so a reader between the two steps hit ENOENT. The provider-switch test read the audit in that window and failed in CI. The file is now written under a temporary name and renamed into place, readers skip a directory whose handoff.md is not there yet, and the tests wait for the switch audit, which trails turn settlement. --- .../__tests__/agent-service-provider-switch.test.ts | 11 +++++++++-- .../persistence/__tests__/handoff-storage.test.ts | 8 ++++++++ .../features/handoff/persistence/handoff-storage.ts | 7 ++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/apps/server/src/features/agents/composition/__tests__/agent-service-provider-switch.test.ts b/apps/server/src/features/agents/composition/__tests__/agent-service-provider-switch.test.ts index 6dbce21c3..28ca86518 100644 --- a/apps/server/src/features/agents/composition/__tests__/agent-service-provider-switch.test.ts +++ b/apps/server/src/features/agents/composition/__tests__/agent-service-provider-switch.test.ts @@ -377,7 +377,11 @@ describe("provider switch on send", () => { const request = lastRequest(claude); expect(request.codexFastMode).toBeUndefined(); expect(request.reasoningLevel).toBeUndefined(); - const audit = await container.resolve(HandoffStorage).readLatestHandoffAudit(threadId); + const audit = await vi.waitFor(async () => { + const written = await container.resolve(HandoffStorage).readLatestHandoffAudit(threadId); + if (written === null) throw new Error("The switch audit is not written yet"); + return written; + }); expect(audit).toContain("schemaVersion: 2"); expect(audit).toContain("kind: switch"); expect(audit).toContain('fromProvider: "codex"'); @@ -446,7 +450,10 @@ describe("provider switch on send", () => { expect(retry.message.match(/\[mcode-handoff:begin /g)).toHaveLength(1); expect(retry.message.match(/ancient question/g)).toHaveLength(1); expect(retry.message.endsWith("back on claude")).toBe(true); - expect(await container.resolve(HandoffStorage).readLatestHandoffAudit(threadId)).toContain("kind: switch"); + // The switch writes its audit after it saves the provider, so the audit can trail the turn's settlement. + await vi.waitFor(async () => { + expect(await container.resolve(HandoffStorage).readLatestHandoffAudit(threadId)).toContain("kind: switch"); + }); }); function notices(): string[] { diff --git a/apps/server/src/features/handoff/persistence/__tests__/handoff-storage.test.ts b/apps/server/src/features/handoff/persistence/__tests__/handoff-storage.test.ts index 2dfd36fb5..3863eef6d 100644 --- a/apps/server/src/features/handoff/persistence/__tests__/handoff-storage.test.ts +++ b/apps/server/src/features/handoff/persistence/__tests__/handoff-storage.test.ts @@ -69,6 +69,14 @@ describe("HandoffStorage", () => { expect((await storage.readLatest("child"))?.meta).toMatchObject({ fromProvider: null, toProvider: "custom: provider\nname" }); }); + it("skips an audit whose write is still in progress", async () => { + await storage.writeHandoffAudit("child", audit); + const inProgress = NodePath.join(dir, "threads", "child", "handoffs", "ZZZ-in-progress"); + await NodeFSPromises.mkdir(inProgress, { recursive: true }); + await NodeFSPromises.writeFile(NodePath.join(inProgress, "handoff.md.partial"), "---\nschemaVersion: 2\n"); + expect((await storage.readLatest("child"))?.meta.kind).toBe("switch"); + }); + it("returns null for an absent thread and removes audits on thread deletion", async () => { expect(await storage.readLatest("absent")).toBeNull(); await storage.writeHandoffAudit("child", audit); diff --git a/apps/server/src/features/handoff/persistence/handoff-storage.ts b/apps/server/src/features/handoff/persistence/handoff-storage.ts index b9b77f80e..30a1672d8 100644 --- a/apps/server/src/features/handoff/persistence/handoff-storage.ts +++ b/apps/server/src/features/handoff/persistence/handoff-storage.ts @@ -42,6 +42,8 @@ export class HandoffStorage { const dir = NodePath.join(handoffsRoot, entry); // Pre-v2 artifacts have separate JSON metadata and must not enter the v2 wire contract. if (NodeFS.existsSync(NodePath.join(dir, "handoff.json"))) continue; + // An audit still being written has its directory but not yet its renamed handoff.md. + if (!NodeFS.existsSync(NodePath.join(dir, "handoff.md"))) continue; const markdown = await NodeFSPromises.readFile(NodePath.join(dir, "handoff.md"), "utf8"); const meta = readAuditMetadata(markdown); if (meta) return { markdown, meta }; @@ -64,7 +66,10 @@ export class HandoffStorage { `budgetBytes: ${audit.budgetBytes}`, `droppedOptions: ${JSON.stringify(audit.droppedOptions)}`, ].join("\n"); - await NodeFSPromises.writeFile(NodePath.join(handoffDir, "handoff.md"), `---\n${frontmatter}\n---\n\n${audit.history}\n`, "utf8"); + // Readers must never see a half-written audit, so handoff.md appears only once it is complete. + const partialPath = NodePath.join(handoffDir, "handoff.md.partial"); + await NodeFSPromises.writeFile(partialPath, `---\n${frontmatter}\n---\n\n${audit.history}\n`, "utf8"); + await NodeFSPromises.rename(partialPath, NodePath.join(handoffDir, "handoff.md")); return ulid; }