11/**
2- * Strip injected `§N§` tag prefixes AND defensively strip any cargo-cult
3- * `§` characters from assistant text before Pi persists the message.
2+ * Strip injected `§N§` tag prefixes AND defensively strip cargo-cult MC tag
3+ * notation from assistant text before Pi persists the message.
44 *
5- * # Why this exists
5+ * Mirrors OpenCode's `text-complete.ts` via {@link stripPersistedAssistantText}:
6+ * whole `§N§` pairs globally, malformed hybrids, then stray `§`. Does not strip
7+ * bare leading digits on the transform path.
68 *
7- * Magic Context tags every visible message part with a `§N§` prefix in
8- * the transform pipeline so the agent can reference parts by tag id
9- * (`ctx_reduce(§3§)`). LLMs frequently mimic that prefix in their own
10- * generated text — emitting `§4§ Yes...` at the start of an assistant
11- * response. This is harmless for cache (the agent emitting the prefix
12- * doesn't bust prefix cache; it's the same content shape we already
13- * inject) and the next transform pass strips/re-injects with the
14- * correct tag id.
9+ * Pi persists raw assistant text from `message_end`; this hook mutates text parts
10+ * before `agent-session.ts:appendMessage()` writes jsonl.
1511 *
16- * BUT: Pi persists the raw assistant text from `message_end` events
17- * directly into the session jsonl, and the Pi UI renders from that
18- * stored text. So while OpenCode hides the prefix from its own UI via
19- * `experimental.text.complete` (which mutates `output.text` before
20- * persistence), Pi's UI shows the raw mimicked prefix to the user
21- * because nothing scrubs it on the way to disk.
22- *
23- * This module mirrors OpenCode's `text-complete.ts` for Pi by hooking
24- * `pi.on("message_end", ...)`. The event fires synchronously before
25- * `agent-session.ts:appendMessage()` persists the message — the event
26- * runner emits to extensions FIRST, then persists by reference, so
27- * mutating `event.message.content[i].text` is visible to the
28- * persistence call.
29- *
30- * # Two-step strip (matches OpenCode `text-complete.ts`)
31- *
32- * 1. `^(§N§\s*)+` removes well-formed leading prefix runs (the canonical
33- * case where the model correctly mimics MC's tag prefix at the start
34- * of a response). Digit-aware: removes the whole `§N§ ` pair cleanly,
35- * leaving no digit residue.
36- *
37- * 2. Global `§` strip removes ANY remaining `§` character anywhere in
38- * the text. Defends against cargo-cult patterns observed when execute
39- * passes drop most tool structure:
40- * - `§40827§` mid-text (well-formed cargo-cult pair)
41- * - `§40827">` (malformed partial — hybrid of MC tag and XML)
42- * - stray `§` anywhere
43- *
44- * Only the MC transform layer is authorized to write `§N§` prefixes.
45- * Any `§` reaching this hook from the model is by definition wrong.
46- * Cost: legitimate `§5.1` section refs become `5.1`; models adapt
47- * naturally to alternative notation.
48- *
49- * # Scope
50- *
51- * Only `assistant` messages need stripping. User messages are
52- * user-typed text (no LLM mimicking). Tool result messages keep their
53- * tagger-injected prefix because that prefix is intentional context.
12+ * Only `assistant` messages are stripped. User/tool messages keep intentional tags.
5413 */
5514
56- const LEADING_TAG_PREFIX_REGEX = / ^ ( \u00a7 \d + \u00a7 \s * ) + / ;
57- const SECTION_CHAR_REGEX = / \u00a7 / g;
15+ import { stripPersistedAssistantText } from "../../plugin/src/hooks/magic-context/tag-content-primitives" ;
5816
5917/**
60- * Mutate the given assistant message's text parts in place to strip
61- * any leading `§N§` tag prefixes.
18+ * Mutate the given assistant message's text parts in place to strip MC tag notation.
6219 *
63- * Returns true if any text was modified, false otherwise. The return
64- * value is informational; the actual mutation happens on the passed
65- * message reference.
66- *
67- * Exported for testing. Production callers should use `registerStripTagPrefix`.
20+ * Returns true if any text was modified. Production callers use `registerStripTagPrefix`.
6821 */
22+
6923export function stripTagPrefixFromAssistantMessage ( message : {
7024 role : string ;
25+
7126 content : unknown ;
7227} ) : boolean {
7328 if ( message . role !== "assistant" ) return false ;
29+
7430 if ( ! Array . isArray ( message . content ) ) return false ;
7531
7632 let mutated = false ;
33+
7734 for ( const part of message . content ) {
7835 if (
7936 part === null ||
@@ -82,15 +39,19 @@ export function stripTagPrefixFromAssistantMessage(message: {
8239 ) {
8340 continue ;
8441 }
42+
8543 const textPart = part as { type : "text" ; text : unknown } ;
44+
8645 if ( typeof textPart . text !== "string" ) continue ;
87- const stripped = textPart . text
88- . replace ( LEADING_TAG_PREFIX_REGEX , "" )
89- . replace ( SECTION_CHAR_REGEX , "" ) ;
46+
47+ const stripped = stripPersistedAssistantText ( textPart . text ) ;
48+
9049 if ( stripped !== textPart . text ) {
9150 textPart . text = stripped ;
51+
9252 mutated = true ;
9353 }
9454 }
55+
9556 return mutated ;
9657}
0 commit comments