@capdiem/pi-repetition-guard 0.2.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 capdiem
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # pi-repetition-guard
2
+
3
+ A **repetition-loop guard** for the [Pi coding agent](https://pi.dev/). It
4
+ detects **thinking-runaway / 万字复读** — the failure mode where a model in a
5
+ long-thinking (or plain output) stage starts repeating the same text in a loop,
6
+ producing tens of thousands of redundant characters instead of converging to an
7
+ answer — and **aborts + re-steers** the model so you get a real answer instead
8
+ of a wall of repeated prose.
9
+
10
+ It is model-agnostic: it wraps whatever model Pi is calling, observes the
11
+ streamed output, and never rewrites mid-stream (Pi does not allow that — see
12
+ [ADR 0001](../../docs/adr/0001-ask-user-triggering-metadata-first.md)). It works
13
+ purely on the assistant message text, covering both the `thinking` stream and
14
+ the final `text` stream (whichever hits the threshold first).
15
+
16
+ > **Design note (ADR 0002):** this extension deliberately performs *active
17
+ > intervention* (`ctx.abort()` + a corrective `steer` message), which goes
18
+ > beyond the "no active injection" decision in ADR 0001. That ADR was scoped to
19
+ > `ask_user` triggering; the repetition guard is a safety mechanism where
20
+ > active abort is the whole point. See
21
+ > [docs/adr/0002-repetition-guard-active-abort.md](../../docs/adr/0002-repetition-guard-active-abort.md).
22
+
23
+ ## Features
24
+
25
+ - **Hybrid detection** over the accumulated streamed text (`message_update`):
26
+ - *Block-repeat* (primary, low false-positive): a long block (line) repeated
27
+ 2× → suspect, 3× → hard.
28
+ - *Shingle novelty* (confirmatory, earlier): the trailing ~40×40-char window
29
+ mostly re-emitting already-seen content → suspect at ≥80%, hard at ≥95%.
30
+ - **Two-stage trigger**: stage-1 "suspect" is **record-only** (a log line); only
31
+ stage-2 "hard" aborts. This gives normal long-form content (code blocks,
32
+ lists, prose) room before anything is interrupted.
33
+ - **Abort + re-steer** on hard trigger:
34
+ - `ctx.abort()` stops the runaway generation in `message_update`.
35
+ - A corrective `steer` message is sent from `message_end` (race-free — the
36
+ aborted message is finalized by then): *"you are in a repetition loop, stop
37
+ repeating, give a concise final answer"* plus a **truncated sample of the
38
+ repeated block** so the model can see exactly what it was repeating.
39
+ - **Retry budget**: max **2** steer retries per logical user turn, with
40
+ escalated wording on the 2nd; if it still runaways, it aborts without another
41
+ steer (no infinite abort↔steer loop).
42
+ - **User-only control**: `/runaway on|off` slash command, default **on**. The
43
+ guard cannot be disabled by the LLM itself (there is no LLM-callable toggle).
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pi install npm:@capdiem/pi-repetition-guard
49
+ ```
50
+
51
+ To try it without installing:
52
+
53
+ ```bash
54
+ pi -e ./extensions/pi-repetition-guard/index.ts
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ Nothing to configure — it is on by default and runs silently in the background.
60
+
61
+ - **Normal operation:** you see nothing. Stage-1 "suspect" writes a
62
+ `[pi-repetition-guard]` log line only.
63
+ - **Runaway detected:** the generation is aborted and a `steer` retry runs
64
+ automatically. In the TUI you'll see the aborted message, then the corrective
65
+ message, then a fresh answer. (The aborted junk stays in history — no cleanup,
66
+ per the settled design; see ADR 0002.)
67
+ - **Disable / re-enable:**
68
+ ```
69
+ /runaway off
70
+ /runaway on
71
+ ```
72
+ A bare `/runaway` toggles. In the TUI the footer status shows `guard:on` /
73
+ `guard:off`.
74
+
75
+ ## Behavior details
76
+
77
+ - **Budget reset:** a new logical user turn (a user message that is *not* our
78
+ own steer) resets the retry budget to 2.
79
+ - **Scope:** only the current assistant message's own text is compared — the
80
+ detector does not compare against the user's message or prior assistant
81
+ messages, so restating the user's words never false-triggers.
82
+ - **Wholesale repetition is always flagged:** re-emitting any content verbatim
83
+ (even a "checklist") is, by definition, the runaway signature.
84
+ - **Sampling parameters are untouched:** Pi exposes no built-in
85
+ `repetition_penalty` / `no_repeat_ngram_size`; this guard detects and
86
+ intervenes client-side instead of relying on provider-side sampling knobs.
87
+
88
+ ## Diagnostics
89
+
90
+ Detection events (stage-1 suspect, stage-2 hard trigger, retry count, budget
91
+ exhaustion) are logged with a `[pi-repetition-guard]` prefix. These records
92
+ support tuning the fixed two-stage thresholds over time — consistent with the
93
+ repo's diagnostics discipline (ADR 0001).
94
+
95
+ ## License
96
+
97
+ MIT
package/index.min.js ADDED
@@ -0,0 +1,8 @@
1
+ var $=2;class f{text="";lastChecked=0;seenShingles=new Set;lastResult={suspect:!1,hard:!1,sample:""};reset(){this.text="",this.lastChecked=0,this.seenShingles.clear(),this.lastResult={suspect:!1,hard:!1,sample:""}}ingest(J){if(this.text=J,this.text.length-this.lastChecked>=200)this.lastChecked=this.text.length,this.lastResult=this.scan();return this.lastResult}scan(){let J=!1,O=!1,z="",K=new Map,Q=0,Z="";for(let Y of this.text.split(/\n+/)){let j=Y.trim();if(j.length<60)continue;let q=(K.get(j)??0)+1;if(K.set(j,q),q>Q)Q=q,Z=j}if(Q>=3)O=!0,z=Z;else if(Q>=2)J=!0,z=Z;let P=[];for(let Y=0;Y+40<=this.text.length;Y++){let j=this.text.slice(Y,Y+40),q=this.seenShingles.has(j);if(P.length<40)P.push(q);else P.shift(),P.push(q);this.seenShingles.add(j)}let H=P.filter(Boolean).length,W=P.length>0?H/P.length:0;if(W>=0.95)O=!0;else if(W>=0.8)J=!0;return{suspect:J,hard:O,sample:z.length>200?`${z.slice(0,200)}…`:z}}}function w(J){if(!Array.isArray(J))return"";let O=[];for(let z of J){if(!z||typeof z!=="object")continue;let K=z;if(K.type==="text"&&typeof K.text==="string")O.push(K.text);else if(K.type==="thinking"){if(typeof K.thinking==="string")O.push(K.thinking);else if(typeof K.text==="string")O.push(K.text)}}return O.join(`
2
+ `)}function F(J,O){if(J>=2)return"[自动护栏] 你刚才再次陷入了复读循环,这是第 2 次。请立即停止重复,直接给出简洁、收敛的"+"最终回答。不要输出思考过程,不要复述任何已说过的内容,直接回答。";return"[自动护栏] 检测到你陷入了复读循环(重复输出相同内容)。请立即停止重复,直接给出简洁、收敛的"+`最终回答,不要复述或重复任何已说过的内容。
3
+
4
+ `+`刚才重复的片段(节选):
5
+ `+(O||"(无样例)")}function G(J){let O=new f,z=!0,K=$,Q,Z=!1,P=!1,H=!1,W=(j)=>{console.log(`[pi-repetition-guard] ${j}`)},Y=(j,q)=>{K-=1;let V=$-K;if(K>=0)Q=F(V,q),W(`hard trigger — aborting, will steer (retry ${V}/${$})`);else W("hard trigger — aborting, retry budget exhausted (giving up)");j.abort()};J.on("message_start",(j)=>{let q=j.message?.role;if(q==="assistant")O.reset(),P=!1,H=!1;else if(q==="user")if(Z)Z=!1;else K=$,W(`new user turn — budget reset to ${$}`)}),J.on("message_update",(j,q)=>{if(!z||H)return;if(j.message.role!=="assistant")return;let V=O.ingest(w(j.message.content));if(V.hard)H=!0,Y(q,V.sample);else if(V.suspect&&!P)P=!0,W("suspect (stage-1): possible repetition loop, recording only")}),J.on("message_end",(j)=>{if(!Q)return;if(j.message.role!=="assistant")return;let q=Q;Q=void 0,Z=!0,W("sending steer retry"),J.sendUserMessage(q,{deliverAs:"steer"})}),J.registerCommand("runaway",{description:"Toggle the repetition-loop guard (on | off). Default: on.",handler:async(j,q)=>{let V=j.trim().toLowerCase();if(V==="on")z=!0;else if(V==="off")z=!1;else z=!z;if(q.hasUI)q.ui.notify(`Repetition guard ${z?"ON":"OFF"}`,z?"info":"warning");if(q.mode==="tui")q.ui.setStatus("pi-repetition-guard",z?"guard:on":"guard:off")}})}export{G as default};
6
+
7
+ //# debugId=E00E93383848526F64756E2164756E21
8
+ //# sourceMappingURL=index.min.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["..\\..\\extensions\\pi-repetition-guard\\detector.ts", "..\\..\\extensions\\pi-repetition-guard\\index.ts"],
4
+ "sourcesContent": [
5
+ "// Pure detection logic for pi-repetition-guard — no pi extension APIs here,\n// so it is unit-testable in isolation (mirrors pi-todo's state.ts split).\n\n// ── Tunable defaults (two-stage trigger: fixed, not user-configurable) ──────\nexport const GUARD_STEER_MAX_RETRIES = 2;\n/** Rescan cadence — we re-scan only after this many new chars accumulate. */\nconst CHECKPOINT_CHARS = 200;\n/** A block must be at least this long (chars) to count as a repeatable block. */\nconst MIN_BLOCK_CHARS = 60;\n/** Block seen this many times → stage-1 suspect. */\nconst SUSPECT_BLOCK_REPEATS = 2;\n/** Block seen this many times → stage-2 hard trigger. */\nconst HARD_BLOCK_REPEATS = 3;\n/** Shingle length (chars) for the n-gram novelty signal. */\nconst SHINGLE_SIZE = 40;\n/** \"Recent\" window = this many trailing shingles (~RECENT_SHINGLES × 40 chars). */\nconst RECENT_SHINGLES = 40;\n/** Recent-window overlap ratio at or above this → stage-1 suspect. */\nconst SUSPECT_RECENT_RATIO = 0.8;\n/** Recent-window overlap ratio at or above this → stage-2 hard trigger. */\nconst HARD_RECENT_RATIO = 0.95;\n/** Repeated-block sample length for the steer message. */\nexport const SAMPLE_MAX_CHARS = 200;\n\nexport interface DetectionResult {\n suspect: boolean;\n hard: boolean;\n /** Most-repeated block, truncated to SAMPLE_MAX_CHARS, for the steer message. */\n sample: string;\n}\n\n/**\n * Hybrid repetition-loop detector over the accumulated text of one assistant\n * message (thinking + final blocks concatenated).\n *\n * Two signals:\n * 1. Block-repeat (primary, low false-positive): a long block (line) repeated\n * 2× → suspect, 3× → hard.\n * 2. Shingle novelty (confirmatory, earlier): over a trailing window of 40-char\n * shingles, the fraction that already appeared earlier in the message. ≥80%\n * → suspect, ≥95% → hard.\n */\nexport class RepetitionDetector {\n private text = \"\";\n private lastChecked = 0;\n private seenShingles = new Set<string>();\n private lastResult: DetectionResult = { suspect: false, hard: false, sample: \"\" };\n\n /** Start a fresh message: call on assistant message_start. */\n reset(): void {\n this.text = \"\";\n this.lastChecked = 0;\n this.seenShingles.clear();\n this.lastResult = { suspect: false, hard: false, sample: \"\" };\n }\n\n /** Feed the latest full accumulated text; returns the current verdict. */\n ingest(newText: string): DetectionResult {\n this.text = newText;\n if (this.text.length - this.lastChecked >= CHECKPOINT_CHARS) {\n this.lastChecked = this.text.length;\n this.lastResult = this.scan();\n }\n return this.lastResult;\n }\n\n private scan(): DetectionResult {\n let suspect = false;\n let hard = false;\n let sample = \"\";\n\n // 1) Block-repeat (primary signal).\n const blockCounts = new Map<string, number>();\n let maxCount = 0;\n let mostRepeated = \"\";\n for (const rawBlock of this.text.split(/\\n+/)) {\n const block = rawBlock.trim();\n if (block.length < MIN_BLOCK_CHARS) continue;\n const count = (blockCounts.get(block) ?? 0) + 1;\n blockCounts.set(block, count);\n if (count > maxCount) {\n maxCount = count;\n mostRepeated = block;\n }\n }\n if (maxCount >= HARD_BLOCK_REPEATS) {\n hard = true;\n sample = mostRepeated;\n } else if (maxCount >= SUSPECT_BLOCK_REPEATS) {\n suspect = true;\n sample = mostRepeated;\n }\n\n // 2) Shingle novelty (confirmatory signal). The seen-set persists across\n // scans, so trailing shingles that repeat earlier content get flagged.\n const recent: boolean[] = [];\n for (let i = 0; i + SHINGLE_SIZE <= this.text.length; i++) {\n const shingle = this.text.slice(i, i + SHINGLE_SIZE);\n const isRepeat = this.seenShingles.has(shingle);\n if (recent.length < RECENT_SHINGLES) {\n recent.push(isRepeat);\n } else {\n recent.shift();\n recent.push(isRepeat);\n }\n this.seenShingles.add(shingle);\n }\n const recentRepeats = recent.filter(Boolean).length;\n const recentRatio = recent.length > 0 ? recentRepeats / recent.length : 0;\n if (recentRatio >= HARD_RECENT_RATIO) hard = true;\n else if (recentRatio >= SUSPECT_RECENT_RATIO) suspect = true;\n\n return {\n suspect,\n hard,\n sample: sample.length > SAMPLE_MAX_CHARS ? `${sample.slice(0, SAMPLE_MAX_CHARS)}…` : sample,\n };\n }\n}\n\n/** Concatenate text + thinking blocks of an assistant message for detection. */\nexport function extractText(content: unknown): string {\n if (!Array.isArray(content)) return \"\";\n const parts: string[] = [];\n for (const item of content) {\n if (!item || typeof item !== \"object\") continue;\n const block = item as { type?: unknown; text?: unknown; thinking?: unknown };\n if (block.type === \"text\" && typeof block.text === \"string\") {\n parts.push(block.text);\n } else if (block.type === \"thinking\") {\n if (typeof block.thinking === \"string\") parts.push(block.thinking);\n else if (typeof block.text === \"string\") parts.push(block.text);\n }\n }\n return parts.join(\"\\n\");\n}\n\n/** Build the corrective steer message; retryNum is 1-based (1 = first retry). */\nexport function buildSteer(retryNum: number, sample: string): string {\n if (retryNum >= 2) {\n return (\n \"[自动护栏] 你刚才再次陷入了复读循环,这是第 2 次。请立即停止重复,直接给出简洁、收敛的\" +\n \"最终回答。不要输出思考过程,不要复述任何已说过的内容,直接回答。\"\n );\n }\n return (\n \"[自动护栏] 检测到你陷入了复读循环(重复输出相同内容)。请立即停止重复,直接给出简洁、收敛的\" +\n \"最终回答,不要复述或重复任何已说过的内容。\\n\\n\" +\n \"刚才重复的片段(节选):\\n\" +\n (sample || \"(无样例)\")\n );\n}\n",
6
+ "import type { ExtensionAPI, ExtensionContext } from \"@earendil-works/pi-coding-agent\";\nimport {\n GUARD_STEER_MAX_RETRIES,\n RepetitionDetector,\n buildSteer,\n extractText,\n} from \"./detector.ts\";\n\n/**\n * pi-repetition-guard — a repetition-loop guard for the Pi coding agent.\n *\n * Detects thinking-runaway / 万字复读 (the model repeating the same text in a\n * loop during long thinking or output) by observing `message_update` stream\n * events, then aborts the generation and re-steers the model with a corrective\n * steer message (ADR 0002: active abort, beyond ADR 0001's ask_user scope).\n *\n * Design (settled in grilling, see docs/adr/0002):\n * - Hybrid detection: block-repeat primary + n-gram shingle novelty confirmatory.\n * - Two-stage trigger: stage-1 \"suspect\" is record-only; stage-2 \"hard\" aborts.\n * - Intervention pipeline: ctx.abort() in message_update, then send the steer\n * from message_end (race-free: the aborted message is finalized by then).\n * - Retry budget: max 2 steer retries per logical user turn, escalated wording\n * on the 2nd, then give up (abort without steer).\n * - Control: `/runaway on|off` slash command, default on.\n */\nexport default function repetitionGuardExtension(pi: ExtensionAPI): void {\n const detector = new RepetitionDetector();\n let enabled = true;\n let steerBudget = GUARD_STEER_MAX_RETRIES;\n let pendingSteer: string | undefined;\n let steerInFlight = false;\n let suspectLogged = false;\n let hardTriggered = false;\n\n const diag = (message: string): void => {\n console.log(`[pi-repetition-guard] ${message}`);\n };\n\n const onHardTrigger = (ctx: ExtensionContext, sample: string): void => {\n steerBudget -= 1;\n const retryNum = GUARD_STEER_MAX_RETRIES - steerBudget; // 1-based: 1 or 2\n if (steerBudget >= 0) {\n pendingSteer = buildSteer(retryNum, sample);\n diag(`hard trigger — aborting, will steer (retry ${retryNum}/${GUARD_STEER_MAX_RETRIES})`);\n } else {\n diag(\"hard trigger — aborting, retry budget exhausted (giving up)\");\n }\n ctx.abort();\n };\n\n // Track per-message state and the retry budget across logical user turns.\n pi.on(\"message_start\", (event) => {\n const role = event.message?.role;\n if (role === \"assistant\") {\n detector.reset();\n suspectLogged = false;\n hardTriggered = false;\n } else if (role === \"user\") {\n if (steerInFlight) {\n steerInFlight = false; // our own steer — keep the budget\n } else {\n steerBudget = GUARD_STEER_MAX_RETRIES; // real user message — new turn\n diag(`new user turn — budget reset to ${GUARD_STEER_MAX_RETRIES}`);\n }\n }\n });\n\n // The one real-time observation point: stream updates with the accumulated\n // full message snapshot (thinking + final text).\n pi.on(\"message_update\", (event, ctx) => {\n if (!enabled || hardTriggered) return;\n if (event.message.role !== \"assistant\") return;\n const result = detector.ingest(extractText(event.message.content));\n if (result.hard) {\n hardTriggered = true;\n onHardTrigger(ctx, result.sample);\n } else if (result.suspect && !suspectLogged) {\n suspectLogged = true;\n diag(\"suspect (stage-1): possible repetition loop, recording only\");\n }\n });\n\n // Send the steer once the aborted message is finalized (avoids racing the\n // abort; message_end replacement is not used — ADR 0002, no cleanup).\n pi.on(\"message_end\", (event) => {\n if (!pendingSteer) return;\n if (event.message.role !== \"assistant\") return;\n const steer = pendingSteer;\n pendingSteer = undefined;\n steerInFlight = true;\n diag(\"sending steer retry\");\n pi.sendUserMessage(steer, { deliverAs: \"steer\" });\n });\n\n // User-only control surface: the guard cannot be disabled by the LLM.\n pi.registerCommand(\"runaway\", {\n description: \"Toggle the repetition-loop guard (on | off). Default: on.\",\n handler: async (args, ctx) => {\n const arg = args.trim().toLowerCase();\n if (arg === \"on\") enabled = true;\n else if (arg === \"off\") enabled = false;\n else enabled = !enabled;\n if (ctx.hasUI) {\n ctx.ui.notify(`Repetition guard ${enabled ? \"ON\" : \"OFF\"}`, enabled ? \"info\" : \"warning\");\n }\n if (ctx.mode === \"tui\") {\n ctx.ui.setStatus(\"pi-repetition-guard\", enabled ? \"guard:on\" : \"guard:off\");\n }\n },\n });\n}\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "E00E93383848526F64756E2164756E21",
10
+ "names": []
11
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@capdiem/pi-repetition-guard",
3
+ "version": "0.2.0",
4
+ "description": "A repetition-loop guard for the Pi coding agent: detects thinking-runaway / 万字复读 (the model repeating the same text in a loop) in streamed output via message_update, then aborts and re-steers the model with a corrective steer message",
5
+ "type": "module",
6
+ "pi": {
7
+ "extensions": [
8
+ "./index.min.js"
9
+ ]
10
+ },
11
+ "keywords": [
12
+ "pi-package",
13
+ "pi",
14
+ "coding-agent",
15
+ "repetition",
16
+ "repetition-loop",
17
+ "thinking-runaway",
18
+ "runaway",
19
+ "guard",
20
+ "abort",
21
+ "steer"
22
+ ],
23
+ "author": "capdiem <capdiem@live.com>",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/capdiem/pi-extensions.git",
28
+ "directory": "extensions/pi-repetition-guard"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "registry": "https://registry.npmjs.org/"
33
+ },
34
+ "peerDependencies": {
35
+ "@earendil-works/pi-ai": "*",
36
+ "@earendil-works/pi-coding-agent": "*",
37
+ "@earendil-works/pi-tui": "*",
38
+ "typebox": "*"
39
+ },
40
+ "files": [
41
+ "LICENSE",
42
+ "README.md",
43
+ "index.min.js",
44
+ "index.min.js.map"
45
+ ]
46
+ }