Skip to content

Commit 2d08921

Browse files
authored
fix: remove empty tagged messages after tool drop (#98, closes #97)
Persistence-boundary strip now removes whole §N§ pairs (not bare §), eliminating the digit-residue that stacked across turns into runaway 'multiplying numbers' overflow (#97). hasMeaningfulPart strips the tag prefix before the emptiness check so tag-only ghost messages are pruned. Shared tag-strip helpers in tag-content-primitives.ts are reused by both OpenCode and Pi. Thanks to @Zireael for the fix.
1 parent a068489 commit 2d08921

10 files changed

Lines changed: 393 additions & 246 deletions

packages/pi-plugin/src/strip-tag-prefix.test.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe("stripTagPrefixFromAssistantMessage", () => {
5252
});
5353

5454
describe("cargo-culted § mid-text (models mimicking MC notation)", () => {
55-
it("strips § from mid-text §N§ pair (cargo-cult defense)", () => {
55+
it("removes mid-text §N§ pair entirely (cargo-cult defense)", () => {
5656
const msg = {
5757
role: "assistant",
5858
content: [
@@ -62,21 +62,20 @@ describe("stripTagPrefixFromAssistantMessage", () => {
6262
},
6363
],
6464
};
65-
// V7: mid-text § is stripped as cargo-cult defense; digits stay as plain text.
6665
expect(stripTagPrefixFromAssistantMessage(msg)).toBe(true);
6766
expect((msg.content[0] as { type: string; text: string }).text).toBe(
68-
"Looking at 5 which references the earlier discussion",
67+
"Looking at which references the earlier discussion",
6968
);
7069
});
7170

72-
it('strips § from malformed §N"> hybrid mid-text', () => {
71+
it('removes malformed §N"> hybrid mid-text', () => {
7372
const msg = {
7473
role: "assistant",
7574
content: [{ type: "text", text: `Hello §40827">Oracle confirmed` }],
7675
};
7776
expect(stripTagPrefixFromAssistantMessage(msg)).toBe(true);
7877
expect((msg.content[0] as { type: string; text: string }).text).toBe(
79-
`Hello 40827">Oracle confirmed`,
78+
"Hello Oracle confirmed",
8079
);
8180
});
8281

@@ -103,7 +102,7 @@ describe("stripTagPrefixFromAssistantMessage", () => {
103102
};
104103
expect(stripTagPrefixFromAssistantMessage(msg)).toBe(true);
105104
expect((msg.content[0] as { type: string; text: string }).text).toBe(
106-
"The pattern 40827 appeared.",
105+
"The pattern appeared.",
107106
);
108107
});
109108
});
Lines changed: 22 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,36 @@
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+
6923
export 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
}

packages/pi-plugin/src/temporal-awareness-pi.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ import {
2727
TEMPORAL_MARKER_PATTERN,
2828
temporalMarkerPrefix,
2929
} from "@magic-context/core/hooks/magic-context/temporal-awareness";
30+
import {
31+
peelLeadingMcTagNotation,
32+
stripTagPrefix,
33+
} from "../../plugin/src/hooks/magic-context/tag-content-primitives";
3034

3135
type PiTextContent = { type: "text"; text: string; textSignature?: string };
3236
type PiImageContent = { type: "image"; data: string; mimeType: string };
@@ -74,9 +78,9 @@ export function injectPiTemporalMarkers(messages: unknown[]): number {
7478
const userMsg = msg as PiUserMessage;
7579
if (typeof userMsg.content === "string") {
7680
if (!TEMPORAL_MARKER_PATTERN.test(stripTagPrefix(userMsg.content))) {
77-
const tagMatch = userMsg.content.match(/^(?:§\d+§\s*)+/);
78-
const tagPrefix = tagMatch ? tagMatch[0] : "";
79-
const body = userMsg.content.slice(tagPrefix.length);
81+
const { tagPrefix, body } = peelLeadingMcTagNotation(
82+
userMsg.content,
83+
);
8084
(messages as PiAgentMessage[])[i] = {
8185
...userMsg,
8286
content: tagPrefix + prefix + body,
@@ -92,9 +96,7 @@ export function injectPiTemporalMarkers(messages: unknown[]): number {
9296
);
9397
if (firstTextIndex >= 0) {
9498
const existing = userMsg.content[firstTextIndex] as PiTextContent;
95-
const tagMatch = existing.text.match(/^(?:§\d+§\s*)+/);
96-
const tagPrefix = tagMatch ? tagMatch[0] : "";
97-
const body = existing.text.slice(tagPrefix.length);
99+
const { tagPrefix, body } = peelLeadingMcTagNotation(existing.text);
98100
if (!TEMPORAL_MARKER_PATTERN.test(body)) {
99101
const newContent = userMsg.content.slice();
100102
newContent[firstTextIndex] = {
@@ -122,14 +124,3 @@ export function injectPiTemporalMarkers(messages: unknown[]): number {
122124

123125
return injected;
124126
}
125-
126-
/**
127-
* Strip the leading `§N§` tag prefix(es) so we test the marker pattern
128-
* against the body, not against the tagged form. Matches OpenCode's
129-
* approach in `injectTemporalMarkers`.
130-
*/
131-
function stripTagPrefix(text: string): string {
132-
const match = text.match(/^(?:§\d+§\s*)+/);
133-
if (!match) return text;
134-
return text.slice(match[0].length);
135-
}

0 commit comments

Comments
 (0)