@capdiem/pi-repetition-guard 0.2.0 → 0.2.1
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/README.md +71 -26
- package/index.min.js +4 -4
- package/index.min.js.map +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,26 +22,56 @@ the final `text` stream (whichever hits the threshold first).
|
|
|
22
22
|
|
|
23
23
|
## Features
|
|
24
24
|
|
|
25
|
-
- **
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- **
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
25
|
+
- **Two runaway detectors**, both model-agnostic and strictly within-message:
|
|
26
|
+
- **Text repetition loop** (thinking-runaway / 万字复读) — the model re-emits
|
|
27
|
+
the same text contiguously during long thinking or output. Detected via
|
|
28
|
+
**tail periodicity** on `message_update`: the trailing `2×P` characters
|
|
29
|
+
being an exact repetition of a fixed unit `P` (a "tape loop").
|
|
30
|
+
- **Variation loop** (v5) — the model rewording the same intent over and over
|
|
31
|
+
("Let me read L1000-1035" / "Let me read the signature end" / ...) without
|
|
32
|
+
advancing. Detected via **short-segment near-repetition dominance**: if
|
|
33
|
+
≥90% of a message's short segments (≤100 chars) are near-duplicates of each
|
|
34
|
+
other (5-gram shingle overlap ≥0.7), the message is a loop. Restricting to
|
|
35
|
+
short segments is what keeps it from false-positiving on legitimately long,
|
|
36
|
+
templated thinking (which is built of long paragraphs).
|
|
37
|
+
- **Tool-call loop** — the model repeatedly invokes the **same tool with the
|
|
38
|
+
same input** and never settles (e.g. checking `git status` / reading a file
|
|
39
|
+
over and over). Detected on `tool_call` by counting identical
|
|
40
|
+
(tool, normalized-input) calls in a sliding window.
|
|
41
|
+
- The text signals are OR'd: a message triggers if either exact tail
|
|
42
|
+
periodicity OR short-segment near-repetition dominance fires.
|
|
43
|
+
- **Zero false positives on legitimate content** — long self-verifying thinking,
|
|
44
|
+
recaps, code, and templated lists interleave new material, so their tails are
|
|
45
|
+
never periodic; varied tool usage (distinct calls) never trips the tool-loop
|
|
46
|
+
counter. (v1 used a trailing-window shingle-novelty signal that
|
|
47
|
+
false-triggered on legitimate long thinking — the bug this v2/v3 line fixes.
|
|
48
|
+
See [ADR 0002](#design-notes-adr-0002) and [CONTEXT.md](../../CONTEXT.md)
|
|
49
|
+
触发策略.)
|
|
50
|
+
- **Intervention**, per failure mode:
|
|
51
|
+
- *Text loop*: `ctx.abort()` stops the generation, then a corrective `steer`
|
|
52
|
+
is sent from `message_end` with a **truncated sample of the repeated unit**.
|
|
53
|
+
- *Tool loop*: the repetitive tool call is **blocked** (`tool_call` →
|
|
54
|
+
`{ block, terminate }`), then a steer naming the tool + repeated input is
|
|
55
|
+
sent after the run settles.
|
|
56
|
+
- **Action-oriented steering (v4)**: steer messages redirect the model to
|
|
57
|
+
*execute*, not to "give an answer" — a stuck loop is usually "rehearsing the
|
|
58
|
+
next action without doing it" (e.g. repeating "let me check git status"
|
|
59
|
+
without ever committing), so the steer tells it to stop describing and do the
|
|
60
|
+
operation.
|
|
61
|
+
- **Retry budget**: max **3** steer retries per logical user turn, with
|
|
62
|
+
escalating wording (1 → 2 → 3) and a clear "give up or state the blocker"
|
|
63
|
+
fallback on the final retry. Still hard-capped — no infinite loop.
|
|
42
64
|
- **User-only control**: `/runaway on|off` slash command, default **on**. The
|
|
43
65
|
guard cannot be disabled by the LLM itself (there is no LLM-callable toggle).
|
|
44
66
|
|
|
67
|
+
> **Why is there no "stage-1 / two-stage" trigger anymore?** The original design
|
|
68
|
+
> had a two-stage trigger (record a "suspect" early, abort only on a harder
|
|
69
|
+
> threshold). Measured against realistic content, *every* early-warning signal
|
|
70
|
+
> (shingle novelty, block-repeat, fractional-period) false-triggers on
|
|
71
|
+
> legitimate long structured thinking, while exact ≥2-copy contiguity never
|
|
72
|
+
> does. So the two-stage was collapsed into the single clean signal. See
|
|
73
|
+
> CONTEXT.md 触发策略 for the record.
|
|
74
|
+
|
|
45
75
|
## Install
|
|
46
76
|
|
|
47
77
|
```bash
|
|
@@ -58,12 +88,16 @@ pi -e ./extensions/pi-repetition-guard/index.ts
|
|
|
58
88
|
|
|
59
89
|
Nothing to configure — it is on by default and runs silently in the background.
|
|
60
90
|
|
|
61
|
-
- **Normal operation:** you see nothing.
|
|
62
|
-
|
|
63
|
-
- **
|
|
91
|
+
- **Normal operation:** you see nothing. The guard only acts on an unambiguous
|
|
92
|
+
contiguous tape-loop or a repeated identical tool call.
|
|
93
|
+
- **Text runaway detected:** the generation is aborted and a `steer` retry runs
|
|
64
94
|
automatically. In the TUI you'll see the aborted message, then the corrective
|
|
65
95
|
message, then a fresh answer. (The aborted junk stays in history — no cleanup,
|
|
66
96
|
per the settled design; see ADR 0002.)
|
|
97
|
+
- **Tool-call loop detected:** the repetitive tool call is blocked (a
|
|
98
|
+
"Repetition guard: tool-call loop" reason), the run terminates, and a steer
|
|
99
|
+
naming the tool + repeated input is sent. If it loops again, later steers
|
|
100
|
+
escalate (2nd, 3rd); after the final retry it stops.
|
|
67
101
|
- **Disable / re-enable:**
|
|
68
102
|
```
|
|
69
103
|
/runaway off
|
|
@@ -79,18 +113,29 @@ Nothing to configure — it is on by default and runs silently in the background
|
|
|
79
113
|
- **Scope:** only the current assistant message's own text is compared — the
|
|
80
114
|
detector does not compare against the user's message or prior assistant
|
|
81
115
|
messages, so restating the user's words never false-triggers.
|
|
82
|
-
- **
|
|
83
|
-
|
|
116
|
+
- **Trigger signal (text):** either exact tail periodicity (≥2 exact copies of a
|
|
117
|
+
≥30-char unit at the tail) OR short-segment near-repetition dominance (≥90% of
|
|
118
|
+
the message's short ≤100-char segments are near-duplicates). Both strictly
|
|
119
|
+
within one message. Wholesale contiguous repetition is, by definition, the
|
|
120
|
+
runaway signature; content that merely *echoes* earlier material while still
|
|
121
|
+
advancing is not.
|
|
122
|
+
- **Trigger signal (tool):** the same tool called with the same normalized input
|
|
123
|
+
≥4 times within the last 8 calls. Varied legitimate tool usage never trips it.
|
|
124
|
+
- **Known limitation:** the text signals are deliberately conservative — a
|
|
125
|
+
message is only flagged when either exactly periodic OR dominated by short
|
|
126
|
+
near-duplicate restatements. A loop that is neither (long-form repetition with
|
|
127
|
+
meaningful variation between copies) may slip through. This is the accepted
|
|
128
|
+
trade-off to keep false positives at zero. See ADR 0002.
|
|
84
129
|
- **Sampling parameters are untouched:** Pi exposes no built-in
|
|
85
130
|
`repetition_penalty` / `no_repeat_ngram_size`; this guard detects and
|
|
86
131
|
intervenes client-side instead of relying on provider-side sampling knobs.
|
|
87
132
|
|
|
88
133
|
## Diagnostics
|
|
89
134
|
|
|
90
|
-
Detection events (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
135
|
+
Detection events (hard trigger, retry count, budget exhaustion) are logged with
|
|
136
|
+
a `[pi-repetition-guard]` prefix. These records support tuning the fixed
|
|
137
|
+
detection thresholds over time — consistent with the repo's diagnostics
|
|
138
|
+
discipline (ADR 0001).
|
|
94
139
|
|
|
95
140
|
## License
|
|
96
141
|
|
package/index.min.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)}
|
|
1
|
+
var B=3;function A(j,Q=2,J=30,q=3000){let W=Math.min(q,Math.floor(j.length/Q));for(let Z=J;Z<=W;Z++){let $=j.length-Math.floor(Q*Z),H=!0;for(let F=$+Z;F<j.length;F++)if(j[F]!==j[F-Z]){H=!1;break}if(H)return Z}return null}function f(j){if(j.length<200)return{fraction:0,sample:""};let Q=j.split(/\n+/).map((F)=>F.trim()).filter((F)=>F.length>15&&F.length<=100),J=Q.length;if(J<3)return{fraction:0,sample:""};let q=[],W=[];for(let F of Q){let C=!1;for(let V=0;V<q.length;V++)if(Math.abs(q[V].length-F.length)<=12&&K(q[V],F)>=0.7){W[V]+=1,C=!0;break}if(!C)q.push(F),W.push(1)}let Z=1-W.filter((F)=>F===1).length/J,$=-1,H=1;for(let F=0;F<W.length;F++)if(W[F]>H)H=W[F],$=F;return{fraction:Z,sample:$>=0?q[$]:""}}function K(j,Q){let q=new Set,W=j.toLowerCase();for(let F=0;F+5<=W.length;F++)q.add(W.slice(F,F+5));let Z=Q.toLowerCase(),$=0,H=0;for(let F=0;F+5<=Z.length;F++){if(q.has(Z.slice(F,F+5)))$++;H++}return H>0?$/H:0}class L{text="";lastChecked=0;lastResult={suspect:!1,hard:!1,sample:""};reset(){this.text="",this.lastChecked=0,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=A(this.text);if(j!==null){let J=this.text.slice(this.text.length-j);return{suspect:!0,hard:!0,sample:J.length>200?`${J.slice(0,200)}…`:J}}let Q=f(this.text);if(Q.fraction>=0.9)return{suspect:!0,hard:!0,sample:Q.sample.length>200?`${Q.sample.slice(0,200)}…`:Q.sample};return{suspect:!1,hard:!1,sample:""}}}function D(j){if(!Array.isArray(j))return"";let Q=[];for(let J of j){if(!J||typeof J!=="object")continue;let q=J;if(q.type==="text"&&typeof q.text==="string")Q.push(q.text);else if(q.type==="thinking"){if(typeof q.thinking==="string")Q.push(q.thinking);else if(typeof q.text==="string")Q.push(q.text)}}return Q.join(`
|
|
2
|
+
`)}class M{calls=[];counts=new Map;reset(){this.calls=[],this.counts.clear()}record(j,Q){let J=`${j}:${Q}`;if(this.calls.push(J),this.counts.set(J,(this.counts.get(J)??0)+1),this.calls.length>8){let q=this.calls.shift(),W=this.counts.get(q);if(W<=1)this.counts.delete(q);else this.counts.set(q,W-1)}return(this.counts.get(J)??0)>=4}}function I(j,Q,J){if(j>=3)return"[自动护栏] 这是第 3 次工具循环。请立即停止调用工具,以最短篇幅完成任务目标,或明确声明"+"无法完成并说明原因。";if(j>=2)return"[自动护栏] 这是第 2 次工具循环。请停止调用 `"+Q+"`,直接基于已有信息完成目标并给出简短结果;如果无法完成,明确说明卡在哪里。";let q=J.length>80?`${J.slice(0,80)}…`:J;return"[自动护栏] 检测到你陷入了工具调用循环:连续多次调用 `"+Q+"`(参数 "+(q||"无参数")+")却迟迟不完成任务。请停止重复调用同一工具,直接基于已有信息完成目标操作并给出结果,不要重复已执行过的操作。"}class U{max;remaining;sentSteers=new Set;constructor(j){this.max=j;this.remaining=j}consume(){return this.remaining-=1,{retryNum:this.max-this.remaining,allowSteer:this.remaining>=0}}recordSentSteer(j){this.sentSteers.add(j)}onUserMessage(j){if(j&&this.sentSteers.has(j))return this.sentSteers.delete(j),!1;return this.reset(),!0}reset(){this.remaining=this.max}get remainingCount(){return this.remaining}}function w(j,Q){if(j>=3)return"[自动护栏] 这是第 3 次。你一直循环未完成目标。请立即以最短篇幅完成任务目标,或明确声明"+"无法完成并说明原因。不要再重复任何已说过的内容。";if(j>=2)return"[自动护栏] 这是第 2 次检测到循环。你仍在重复却没有执行。请立即停止任何描述性文本,直接完成"+"当前任务的目标操作并给出简短结果;如果无法完成,明确说明卡在哪里。";return"[自动护栏] 检测到你陷入了循环:反复复述同一个动作/内容却始终没有真正执行下一步。请立即停止"+`复述,直接执行你要做的操作并给出结果,不要描述你将要做什么。
|
|
3
3
|
|
|
4
4
|
`+`刚才重复的片段(节选):
|
|
5
|
-
`+(
|
|
5
|
+
`+(Q||"(无样例)")}function P(j){let Q=new L,J=new M,q=new U(B),W=!0,Z,$,H=!1,F=(V)=>{console.log(`[pi-repetition-guard] ${V}`)},C=(V,Y)=>{let{retryNum:O,allowSteer:G}=q.consume();if(G)Z=w(O,Y),F(`text runaway — aborting, will steer (retry ${O}/${B})`);else F("text runaway — aborting, retry budget exhausted (giving up)");V.abort()};j.on("message_start",(V)=>{let Y=V.message?.role;if(Y==="assistant")Q.reset(),H=!1;else if(Y==="user"){let O=h(V.message.content);if(q.onUserMessage(O))F(`new user turn — budget reset to ${B}`)}}),j.on("agent_start",()=>{J.reset()}),j.on("message_update",(V,Y)=>{if(!W||H)return;if(V.message.role!=="assistant")return;let O=Q.ingest(D(V.message.content));if(O.hard)H=!0,C(Y,O.sample)}),j.on("tool_call",(V)=>{if(!W)return;let Y=_(V.input);if(!J.record(V.toolName,Y))return;let{retryNum:G,allowSteer:z}=q.consume();if(z)$=I(G,V.toolName,Y),F(`tool-call loop — blocking ${V.toolName} (${Y.slice(0,60)}), will steer (retry ${G}/${B})`);else F("tool-call loop — budget exhausted, blocking without steer");return{block:!0,reason:"Repetition guard: tool-call loop (same tool+args repeated)",terminate:!0}}),j.on("message_end",(V)=>{if(!Z)return;if(V.message.role!=="assistant")return;let Y=Z;Z=void 0,q.recordSentSteer(Y),F("sending text steer retry"),j.sendUserMessage(Y,{deliverAs:"steer"})}),j.on("agent_end",()=>{if(!$)return;let V=$;$=void 0,q.recordSentSteer(V),F("sending tool-loop steer retry"),j.sendUserMessage(V,{deliverAs:"steer"})}),j.registerCommand("runaway",{description:"Toggle the repetition-loop guard (on | off). Default: on.",handler:async(V,Y)=>{let O=V.trim().toLowerCase();if(O==="on")W=!0;else if(O==="off")W=!1;else W=!W;if(Y.hasUI)Y.ui.notify(`Repetition guard ${W?"ON":"OFF"}`,W?"info":"warning");if(Y.mode==="tui")Y.ui.setStatus("pi-repetition-guard",W?"guard:on":"guard:off")}})}function _(j){if(j===void 0||j===null)return"";if(typeof j==="string")return j;try{return X(j)}catch{return String(j)}}function h(j){if(!Array.isArray(j))return"";let Q="";for(let J of j)if(J&&typeof J==="object"){let q=J.text;if(typeof q==="string")Q+=q}return Q}function X(j){if(Array.isArray(j))return`[${j.map(X).join(",")}]`;if(j&&typeof j==="object")return`{${Object.entries(j).sort(([J],[q])=>J<q?-1:J>q?1:0).map(([J,q])=>`${JSON.stringify(J)}:${X(q)}`).join(",")}}`;return JSON.stringify(j)}export{P as default};
|
|
6
6
|
|
|
7
|
-
//# debugId=
|
|
7
|
+
//# debugId=D5C9893C3F98A8AF64756E2164756E21
|
|
8
8
|
//# sourceMappingURL=index.min.js.map
|
package/index.min.js.map
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../extensions/pi-repetition-guard/detector.ts", "../../extensions/pi-repetition-guard/index.ts"],
|
|
4
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
|
|
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 (fixed, not user-configurable) ─────────────────────────\nexport const GUARD_STEER_MAX_RETRIES = 3;\n/** Rescan cadence — we re-scan only after this many new chars accumulate. */\nconst CHECKPOINT_CHARS = 200;\n/** Smallest loop unit (chars) we treat as a runaway tape-loop. */\nconst MIN_PERIOD = 30;\n/** Largest loop unit to scan for; capped by message length at runtime. */\nconst MAX_PERIOD = 3000;\n/** A loop is \"proven\" when the last `copies × P` chars are P-periodic. */\nconst HARD_PERIOD_COPIES = 2;\n/** Repeated-block sample length for the steer message. */\nexport const SAMPLE_MAX_CHARS = 200;\n/** Tool-loop trigger: same tool + same normalized input this many times in a\n * sliding window of recent calls → blocked as a tool-call loop. */\nexport const TOOL_LOOP_MAX_REPEATS = 4;\n/** Sliding window of recent tool calls used for tool-loop counting. */\nconst TOOL_LOOP_WINDOW = 8;\n/** Two segments are \"near-duplicates\" when this fraction of their 5-gram\n * shingles overlap (tolerates reworded copies). */\nexport const NEAR_DUP_OVERLAP_THRESH = 0.7;\n/** A message is a variation-loop when this fraction of its SHORT segments are\n * near-duplicates of another segment (within-message dominance). Calibrated on\n * real data: clear degenerate loops 0.90-1.0, productive messages with a\n * repetitive tail ~0.88 (kept quiet), normal messages median ~0.00. 0.9 (not\n * 0.85) cleanly separates clear loops from the productive+tail gray zone. */\nexport const NEAR_DUP_HARD_FRACTION = 0.9;\n/** Only segments up to this length count toward variation-loop detection. This\n * is what separates degenerate loops (dominated by short \"Let me read X.\"\n * restatements) from legitimate long thinking (built of long paragraphs that\n * advance). Legit templated thinking has ~0 short segments → never fires. */\nconst NEAR_DUP_MAX_SEG_LEN = 100;\n/** Need at least this many short segments before the signal is meaningful. */\nconst NEAR_DUP_MIN_SHORT_SEGS = 3;\n/** Minimum message length to bother computing near-repetition dominance. */\nconst NEAR_DUP_MIN_CHARS = 200;\n\nexport interface DetectionResult {\n suspect: boolean;\n hard: boolean;\n /** Repeated unit, truncated to SAMPLE_MAX_CHARS, for the steer message. */\n sample: string;\n}\n\n/**\n * Find the smallest period P in [minP, cap] such that the trailing\n * `copies × P` characters of `text` are exactly P-periodic\n * (text[i] === text[i - P] for every i in the tail window).\n *\n * This is the tape-loop signature: a runaway re-emits the same block\n * contiguously, so the tail becomes a clean repetition of a fixed unit.\n *\n * Deliberately EXACT (no fuzzy tolerance): every measured \"similarity\" signal\n * (shingle novelty, block-repeat, fractional-period) was shown to false-trigger\n * on legitimate long structured thinking, while exact ≥2-copy contiguity never\n * does. A loop with minor noise may be missed — acceptable, since false\n * positives are the far worse failure here.\n *\n * Returns the period, or null when no period exists.\n */\nexport function findPeriod(\n text: string,\n copies = HARD_PERIOD_COPIES,\n minP = MIN_PERIOD,\n maxP = MAX_PERIOD,\n): number | null {\n const cap = Math.min(maxP, Math.floor(text.length / copies));\n for (let P = minP; P <= cap; P++) {\n const start = text.length - Math.floor(copies * P);\n let ok = true;\n for (let i = start + P; i < text.length; i++) {\n if (text[i] !== text[i - P]) {\n ok = false;\n break;\n }\n }\n if (ok) return P;\n }\n return null;\n}\n\n/**\n * Within-message SHORT-segment near-repetition dominance: the fraction of a\n * message's SHORT segments (≤ NEAR_DUP_MAX_SEG_LEN chars) that are\n * near-duplicates (5-gram shingle overlap ≥ NEAR_DUP_OVERLAP_THRESH) of another\n * SHORT segment in the SAME message. Requires ≥ NEAR_DUP_MIN_SHORT_SEGS short\n * segments to be meaningful.\n *\n * Catches the \"variation loop\" class that exact tail periodicity misses: the\n * model rewording the same intent over and over (\"Let me read L1000-1035\" /\n * \"Let me read the signature end\" / ...) without advancing. Such loops are\n * DOMINATED by SHORT near-identical restatements (real data: 0.90-1.0), while\n * genuine messages — including legitimately long, templated thinking — are\n * built of long paragraphs and have ~0 short near-duplicates (median ~0.00).\n * Restricting to SHORT segments is what keeps this from false-positiving on\n * legit structured thinking. Strictly within-message; never cross-message.\n */\nexport function nearDupDominance(text: string): { fraction: number; sample: string } {\n if (text.length < NEAR_DUP_MIN_CHARS) return { fraction: 0, sample: \"\" };\n const segs = text.split(/\\n+/)\n .map((s) => s.trim())\n .filter((s) => s.length > 15 && s.length <= NEAR_DUP_MAX_SEG_LEN);\n const n = segs.length;\n if (n < NEAR_DUP_MIN_SHORT_SEGS) return { fraction: 0, sample: \"\" };\n // Greedy near-duplicate clustering by 5-gram shingle overlap.\n const reps: string[] = [];\n const sizes: number[] = [];\n for (const seg of segs) {\n let placed = false;\n for (let k = 0; k < reps.length; k++) {\n if (Math.abs(reps[k].length - seg.length) <= 12 && shingleOverlap(reps[k], seg) >= NEAR_DUP_OVERLAP_THRESH) {\n sizes[k] += 1;\n placed = true;\n break;\n }\n }\n if (!placed) {\n reps.push(seg);\n sizes.push(1);\n }\n }\n const fraction = 1 - sizes.filter((v) => v === 1).length / n;\n let bestIdx = -1;\n let bestSize = 1;\n for (let k = 0; k < sizes.length; k++) {\n if (sizes[k] > bestSize) {\n bestSize = sizes[k];\n bestIdx = k;\n }\n }\n return { fraction, sample: bestIdx >= 0 ? reps[bestIdx] : \"\" };\n}\n\n/** Fraction of b's 5-char shingles that also appear in a (lowercased). */\nfunction shingleOverlap(a: string, b: string): number {\n const n = 5;\n const set = new Set<string>();\n const ta = a.toLowerCase();\n for (let i = 0; i + n <= ta.length; i++) set.add(ta.slice(i, i + n));\n const tb = b.toLowerCase();\n let hit = 0;\n let total = 0;\n for (let i = 0; i + n <= tb.length; i++) {\n if (set.has(tb.slice(i, i + n))) hit++;\n total++;\n }\n return total > 0 ? hit / total : 0;\n}\n\n/**\n * Repetition-loop detector over the accumulated text of one assistant message\n * (thinking + final blocks concatenated).\n *\n * v5: TWO within-message signals, OR'd:\n * 1. Exact tail periodicity (≥2 exact contiguous copies) — catches tape-loops.\n * 2. Short-segment near-repetition dominance (≥90% of the message's short\n * segments are near-duplicates) — catches variation-loops (reworded copies).\n * Both are strictly within one message; no cross-message comparison.\n * `suspect` mirrors `hard` for API compatibility.\n */\nexport class RepetitionDetector {\n private text = \"\";\n private lastChecked = 0;\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.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 // 1) Exact tail periodicity (tape-loop).\n const period = findPeriod(this.text);\n if (period !== null) {\n const sample = this.text.slice(this.text.length - period);\n return {\n suspect: true,\n hard: true,\n sample: sample.length > SAMPLE_MAX_CHARS\n ? `${sample.slice(0, SAMPLE_MAX_CHARS)}…`\n : sample,\n };\n }\n // 2) Within-message short-segment near-repetition dominance (variation-loop).\n const nd = nearDupDominance(this.text);\n if (nd.fraction >= NEAR_DUP_HARD_FRACTION) {\n return {\n suspect: true,\n hard: true,\n sample: nd.sample.length > SAMPLE_MAX_CHARS\n ? `${nd.sample.slice(0, SAMPLE_MAX_CHARS)}…`\n : nd.sample,\n };\n }\n return { suspect: false, hard: false, sample: \"\" };\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/**\n * Tool-call-loop detector over one agent run (reset on agent_start).\n *\n * The real-world failure the user hit (and that pure text signals miss): the\n * model repeatedly invokes the SAME tool with the SAME input — e.g. checking\n * `git status` / reading CHANGES.md over and over — and never settles. The text\n * it emits is near-identical *variations*, not an exact periodic loop, so text\n * periodicity does not fire. Counting identical (toolName, inputKey) calls in a\n * sliding window catches it directly.\n */\nexport class ToolLoopTracker {\n private calls: string[] = [];\n private counts = new Map<string, number>();\n\n /** Start a fresh agent run: call on agent_start. */\n reset(): void {\n this.calls = [];\n this.counts.clear();\n }\n\n /**\n * Record a tool call; returns true when the same (toolName, inputKey) has\n * been seen `TOOL_LOOP_MAX_REPEATS` times within the recent window.\n */\n record(toolName: string, inputKey: string): boolean {\n const key = `${toolName}:${inputKey}`;\n this.calls.push(key);\n this.counts.set(key, (this.counts.get(key) ?? 0) + 1);\n if (this.calls.length > TOOL_LOOP_WINDOW) {\n const dropped = this.calls.shift()!;\n const c = this.counts.get(dropped)!;\n if (c <= 1) this.counts.delete(dropped);\n else this.counts.set(dropped, c - 1);\n }\n return (this.counts.get(key) ?? 0) >= TOOL_LOOP_MAX_REPEATS;\n }\n}\n\n/** Build the tool-loop steer message; retryNum is 1-based (1 = first retry).\n * Action-oriented: stop re-invoking, finish the goal from existing info.\n * Escalates over 3 retries. */\nexport function buildToolLoopSteer(retryNum: number, toolName: string, inputKey: string): string {\n if (retryNum >= 3) {\n return (\n \"[自动护栏] 这是第 3 次工具循环。请立即停止调用工具,以最短篇幅完成任务目标,或明确声明\" +\n \"无法完成并说明原因。\"\n );\n }\n if (retryNum >= 2) {\n return (\n \"[自动护栏] 这是第 2 次工具循环。请停止调用 `\" + toolName +\n \"`,直接基于已有信息完成目标并给出简短结果;如果无法完成,明确说明卡在哪里。\"\n );\n }\n const sample = inputKey.length > 80 ? `${inputKey.slice(0, 80)}…` : inputKey;\n return (\n \"[自动护栏] 检测到你陷入了工具调用循环:连续多次调用 `\" + toolName + \"`(参数 \" +\n (sample || \"无参数\") +\n \")却迟迟不完成任务。请停止重复调用同一工具,直接基于已有信息完成目标操作并给出结果,不要重复已执行过的操作。\"\n );\n}\n\n/**\n * Retry budget shared by the text-loop and tool-loop paths, with a\n * deterministic \"real user turn\" reset.\n *\n * The budget must span one logical user turn (the original request + any number\n * of steer retries) and reset on the NEXT real user message. We identify our\n * own steer messages by their exact content (Pi delivers a queued steer as a\n * user message whose text matches what we sent — this is how Pi itself clears\n * its steering queue). Using a boolean \"steer in flight\" flag is fragile: if a\n * steer's message_start is missed in an abort/auto-continue race, the flag\n * sticks and the budget never resets — the bug reported as \"after 3 retries the\n * count never resets\".\n */\nexport class RetryBudget {\n private remaining: number;\n private sentSteers = new Set<string>();\n\n constructor(private readonly max: number) {\n this.remaining = max;\n }\n\n /** Consume one retry slot; 1-based retry number and whether a steer is still\n * allowed. Shared by the text and tool-loop paths. */\n consume(): { retryNum: number; allowSteer: boolean } {\n this.remaining -= 1;\n return { retryNum: this.max - this.remaining, allowSteer: this.remaining >= 0 };\n }\n\n /** Record the exact text of a steer we sent, so its user-message delivery can\n * be recognized and NOT treated as a new user turn. */\n recordSentSteer(steerText: string): void {\n this.sentSteers.add(steerText);\n }\n\n /**\n * Handle a user message_start. Returns true when it was a REAL user turn\n * (budget reset to max); false when it was our own steer (budget kept).\n */\n onUserMessage(text: string): boolean {\n if (text && this.sentSteers.has(text)) {\n this.sentSteers.delete(text);\n return false;\n }\n this.reset();\n return true;\n }\n\n reset(): void {\n this.remaining = this.max;\n }\n\n get remainingCount(): number {\n return this.remaining;\n }\n}\n\n/** Build the corrective steer message; retryNum is 1-based (1 = first retry).\n *\n * Action-oriented wording (v4): the real-world loop this guard catches is often\n * \"rehearsing the next action without doing it\" (e.g. repeating \"let me check\n * git status\" without ever committing). Telling a stuck model to \"give a final\n * answer\" does not break that — it must be told to EXECUTE. Escalates over 3\n * retries. */\nexport function buildSteer(retryNum: number, sample: string): string {\n if (retryNum >= 3) {\n return (\n \"[自动护栏] 这是第 3 次。你一直循环未完成目标。请立即以最短篇幅完成任务目标,或明确声明\" +\n \"无法完成并说明原因。不要再重复任何已说过的内容。\"\n );\n }\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 RetryBudget,\n ToolLoopTracker,\n buildSteer,\n buildToolLoopSteer,\n extractText,\n} from \"./detector.ts\";\n\n/**\n * pi-repetition-guard — a repetition-loop guard for the Pi coding agent.\n *\n * Detects two distinct runaway failure modes and intervenes with\n * abort/re-steer (ADR 0002: active intervention, beyond ADR 0001's ask_user\n * scope):\n *\n * 1. Text repetition loop (thinking-runaway / 万字复读): the model repeats the\n * same text contiguously during long thinking or output. Detected via tail\n * periodicity on `message_update`; intervened with ctx.abort() + steer.\n * 2. Tool-call loop: the model repeatedly invokes the SAME tool with the SAME\n * input and never settles (e.g. checking git status over and over). Detected\n * on `tool_call`; intervened by blocking the repetitive call (block +\n * terminate) and steering.\n *\n * Shared control: `/runaway on|off` (user-only), retry budget of 3 steers per\n * logical user turn (escalated wording 1→2→3, then give up).\n */\nexport default function repetitionGuardExtension(pi: ExtensionAPI): void {\n const detector = new RepetitionDetector();\n const toolLoopTracker = new ToolLoopTracker();\n const budget = new RetryBudget(GUARD_STEER_MAX_RETRIES);\n let enabled = true;\n let pendingSteer: string | undefined;\n let pendingToolSteer: string | undefined;\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 const { retryNum, allowSteer } = budget.consume();\n if (allowSteer) {\n pendingSteer = buildSteer(retryNum, sample);\n diag(`text runaway — aborting, will steer (retry ${retryNum}/${GUARD_STEER_MAX_RETRIES})`);\n } else {\n diag(\"text runaway — aborting, retry budget exhausted (giving up)\");\n }\n ctx.abort();\n };\n\n /** Track per-message state and the retry budget across logical turns. */\n pi.on(\"message_start\", (event) => {\n const role = event.message?.role;\n if (role === \"assistant\") {\n detector.reset();\n hardTriggered = false;\n } else if (role === \"user\") {\n const text = contentText(event.message.content);\n if (budget.onUserMessage(text)) {\n diag(`new user turn — budget reset to ${GUARD_STEER_MAX_RETRIES}`);\n }\n }\n });\n\n pi.on(\"agent_start\", () => {\n toolLoopTracker.reset();\n });\n\n // Text runaway detection: the one real-time observation point, stream\n // updates with the accumulated 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 }\n });\n\n // Tool-call-loop detection: same tool + same input repeated in a window.\n // Block the repetitive call and queue a tool-loop steer.\n pi.on(\"tool_call\", (event) => {\n if (!enabled) return;\n const inputKey = normalizeInput(event.input);\n const isLoop = toolLoopTracker.record(event.toolName, inputKey);\n if (!isLoop) return;\n const { retryNum, allowSteer } = budget.consume();\n if (allowSteer) {\n pendingToolSteer = buildToolLoopSteer(retryNum, event.toolName, inputKey);\n diag(\n `tool-call loop — blocking ${event.toolName} (${inputKey.slice(0, 60)}), will steer ` +\n `(retry ${retryNum}/${GUARD_STEER_MAX_RETRIES})`,\n );\n } else {\n diag(\"tool-call loop — budget exhausted, blocking without steer\");\n }\n return {\n block: true,\n reason: \"Repetition guard: tool-call loop (same tool+args repeated)\",\n terminate: true,\n };\n });\n\n // Send the text steer once the aborted message is finalized (race-free).\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 budget.recordSentSteer(steer);\n diag(\"sending text steer retry\");\n pi.sendUserMessage(steer, { deliverAs: \"steer\" });\n });\n\n // Send the tool-loop steer after the (terminated) run settles.\n pi.on(\"agent_end\", () => {\n if (!pendingToolSteer) return;\n const steer = pendingToolSteer;\n pendingToolSteer = undefined;\n budget.recordSentSteer(steer);\n diag(\"sending tool-loop 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\n/** Stable, order-insensitive string key for a tool input. */\nfunction normalizeInput(input: unknown): string {\n if (input === undefined || input === null) return \"\";\n if (typeof input === \"string\") return input;\n try {\n return stableStringify(input);\n } catch {\n return String(input);\n }\n}\n\n/** Concatenate the text of a user message, for matching against our steers. */\nfunction contentText(content: unknown): string {\n if (!Array.isArray(content)) return \"\";\n let text = \"\";\n for (const item of content) {\n if (item && typeof item === \"object\") {\n const t = (item as { text?: unknown }).text;\n if (typeof t === \"string\") text += t;\n }\n }\n return text;\n}\n\nfunction stableStringify(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(\",\")}]`;\n }\n if (value && typeof value === \"object\") {\n const entries = Object.entries(value as Record<string, unknown>)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`);\n return `{${entries.join(\",\")}}`;\n }\n return JSON.stringify(value);\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "AAIO,IAAM,EAA0B,
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "AAIO,IAAM,EAA0B,EA0DhC,SAAS,CAAU,CACxB,EACA,EApDyB,EAqDzB,EAzDiB,GA0DjB,EAxDiB,KAyDF,CACf,IAAM,EAAM,KAAK,IAAI,EAAM,KAAK,MAAM,EAAK,OAAS,CAAM,CAAC,EAC3D,QAAS,EAAI,EAAM,GAAK,EAAK,IAAK,CAChC,IAAM,EAAQ,EAAK,OAAS,KAAK,MAAM,EAAS,CAAC,EAC7C,EAAK,GACT,QAAS,EAAI,EAAQ,EAAG,EAAI,EAAK,OAAQ,IACvC,GAAI,EAAK,KAAO,EAAK,EAAI,GAAI,CAC3B,EAAK,GACL,MAGJ,GAAI,EAAI,OAAO,EAEjB,OAAO,KAmBF,SAAS,CAAgB,CAAC,EAAoD,CACnF,GAAI,EAAK,OA/DgB,IA+Da,MAAO,CAAE,SAAU,EAAG,OAAQ,EAAG,EACvE,IAAM,EAAO,EAAK,MAAM,KAAK,EAC1B,IAAI,CAAC,IAAM,EAAE,KAAK,CAAC,EACnB,OAAO,CAAC,IAAM,EAAE,OAAS,IAAM,EAAE,QAtET,GAsEuC,EAC5D,EAAI,EAAK,OACf,GAAI,EAtE0B,EAsEG,MAAO,CAAE,SAAU,EAAG,OAAQ,EAAG,EAElE,IAAM,EAAiB,CAAC,EAClB,EAAkB,CAAC,EACzB,QAAW,KAAO,EAAM,CACtB,IAAI,EAAS,GACb,QAAS,EAAI,EAAG,EAAI,EAAK,OAAQ,IAC/B,GAAI,KAAK,IAAI,EAAK,GAAG,OAAS,EAAI,MAAM,GAAK,IAAM,EAAe,EAAK,GAAI,CAAG,GA1F7C,IA0F2E,CAC1G,EAAM,IAAM,EACZ,EAAS,GACT,MAGJ,GAAI,CAAC,EACH,EAAK,KAAK,CAAG,EACb,EAAM,KAAK,CAAC,EAGhB,IAAM,EAAW,EAAI,EAAM,OAAO,CAAC,IAAM,IAAM,CAAC,EAAE,OAAS,EACvD,EAAU,GACV,EAAW,EACf,QAAS,EAAI,EAAG,EAAI,EAAM,OAAQ,IAChC,GAAI,EAAM,GAAK,EACb,EAAW,EAAM,GACjB,EAAU,EAGd,MAAO,CAAE,WAAU,OAAQ,GAAW,EAAI,EAAK,GAAW,EAAG,EAI/D,SAAS,CAAc,CAAC,EAAW,EAAmB,CAEpD,IAAM,EAAM,IAAI,IACV,EAAK,EAAE,YAAY,EACzB,QAAS,EAAI,EAAG,EAHN,GAGe,EAAG,OAAQ,IAAK,EAAI,IAAI,EAAG,MAAM,EAAG,EAHnD,CAGwD,CAAC,EACnE,IAAM,EAAK,EAAE,YAAY,EACrB,EAAM,EACN,EAAQ,EACZ,QAAS,EAAI,EAAG,EAPN,GAOe,EAAG,OAAQ,IAAK,CACvC,GAAI,EAAI,IAAI,EAAG,MAAM,EAAG,EARhB,CAQqB,CAAC,EAAG,IACjC,IAEF,OAAO,EAAQ,EAAI,EAAM,EAAQ,EAc5B,MAAM,CAAmB,CACtB,KAAO,GACP,YAAc,EACd,WAA8B,CAAE,QAAS,GAAO,KAAM,GAAO,OAAQ,EAAG,EAGhF,KAAK,EAAS,CACZ,KAAK,KAAO,GACZ,KAAK,YAAc,EACnB,KAAK,WAAa,CAAE,QAAS,GAAO,KAAM,GAAO,OAAQ,EAAG,EAI9D,MAAM,CAAC,EAAkC,CAEvC,GADA,KAAK,KAAO,EACR,KAAK,KAAK,OAAS,KAAK,aA3KP,IA4KnB,KAAK,YAAc,KAAK,KAAK,OAC7B,KAAK,WAAa,KAAK,KAAK,EAE9B,OAAO,KAAK,WAGN,IAAI,EAAoB,CAE9B,IAAM,EAAS,EAAW,KAAK,IAAI,EACnC,GAAI,IAAW,KAAM,CACnB,IAAM,EAAS,KAAK,KAAK,MAAM,KAAK,KAAK,OAAS,CAAM,EACxD,MAAO,CACL,QAAS,GACT,KAAM,GACN,OAAQ,EAAO,OAlLS,IAmLpB,GAAG,EAAO,MAAM,EAnLI,GAmLe,KACnC,CACN,EAGF,IAAM,EAAK,EAAiB,KAAK,IAAI,EACrC,GAAI,EAAG,UA3K2B,IA4KhC,MAAO,CACL,QAAS,GACT,KAAM,GACN,OAAQ,EAAG,OAAO,OA7LM,IA8LpB,GAAG,EAAG,OAAO,MAAM,EA9LC,GA8LkB,KACtC,EAAG,MACT,EAEF,MAAO,CAAE,QAAS,GAAO,KAAM,GAAO,OAAQ,EAAG,EAErD,CAGO,SAAS,CAAW,CAAC,EAA0B,CACpD,GAAI,CAAC,MAAM,QAAQ,CAAO,EAAG,MAAO,GACpC,IAAM,EAAkB,CAAC,EACzB,QAAW,KAAQ,EAAS,CAC1B,GAAI,CAAC,GAAQ,OAAO,IAAS,SAAU,SACvC,IAAM,EAAQ,EACd,GAAI,EAAM,OAAS,QAAU,OAAO,EAAM,OAAS,SACjD,EAAM,KAAK,EAAM,IAAI,EAChB,QAAI,EAAM,OAAS,YACxB,GAAI,OAAO,EAAM,WAAa,SAAU,EAAM,KAAK,EAAM,QAAQ,EAC5D,QAAI,OAAO,EAAM,OAAS,SAAU,EAAM,KAAK,EAAM,IAAI,GAGlE,OAAO,EAAM,KAAK;AAAA,CAAI,EAajB,MAAM,CAAgB,CACnB,MAAkB,CAAC,EACnB,OAAS,IAAI,IAGrB,KAAK,EAAS,CACZ,KAAK,MAAQ,CAAC,EACd,KAAK,OAAO,MAAM,EAOpB,MAAM,CAAC,EAAkB,EAA2B,CAClD,IAAM,EAAM,GAAG,KAAY,IAG3B,GAFA,KAAK,MAAM,KAAK,CAAG,EACnB,KAAK,OAAO,IAAI,GAAM,KAAK,OAAO,IAAI,CAAG,GAAK,GAAK,CAAC,EAChD,KAAK,MAAM,OA9OM,EA8OqB,CACxC,IAAM,EAAU,KAAK,MAAM,MAAM,EAC3B,EAAI,KAAK,OAAO,IAAI,CAAO,EACjC,GAAI,GAAK,EAAG,KAAK,OAAO,OAAO,CAAO,EACjC,UAAK,OAAO,IAAI,EAAS,EAAI,CAAC,EAErC,OAAQ,KAAK,OAAO,IAAI,CAAG,GAAK,IAtPC,EAwPrC,CAKO,SAAS,CAAkB,CAAC,EAAkB,EAAkB,EAA0B,CAC/F,GAAI,GAAY,EACd,MACE,iDACA,aAGJ,GAAI,GAAY,EACd,MACE,6BAA8B,EAC9B,yCAGJ,IAAM,EAAS,EAAS,OAAS,GAAK,GAAG,EAAS,MAAM,EAAG,EAAE,KAAM,EACnE,MACE,gCAAiC,EAAW,SAC3C,GAAU,OACX,yDAiBG,MAAM,CAAY,CAIM,IAHrB,UACA,WAAa,IAAI,IAEzB,WAAW,CAAkB,EAAa,CAAb,WAC3B,KAAK,UAAY,EAKnB,OAAO,EAA8C,CAEnD,OADA,KAAK,WAAa,EACX,CAAE,SAAU,KAAK,IAAM,KAAK,UAAW,WAAY,KAAK,WAAa,CAAE,EAKhF,eAAe,CAAC,EAAyB,CACvC,KAAK,WAAW,IAAI,CAAS,EAO/B,aAAa,CAAC,EAAuB,CACnC,GAAI,GAAQ,KAAK,WAAW,IAAI,CAAI,EAElC,OADA,KAAK,WAAW,OAAO,CAAI,EACpB,GAGT,OADA,KAAK,MAAM,EACJ,GAGT,KAAK,EAAS,CACZ,KAAK,UAAY,KAAK,OAGpB,eAAc,EAAW,CAC3B,OAAO,KAAK,UAEhB,CASO,SAAS,CAAU,CAAC,EAAkB,EAAwB,CACnE,GAAI,GAAY,EACd,MACE,iDACA,2BAGJ,GAAI,GAAY,EACd,MACE,mDACA,oCAGJ,MACE,kDACA;AAAA;AAAA,EACA;AAAA,GACC,GAAU,SCtVf,SAAwB,CAAwB,CAAC,EAAwB,CACvE,IAAM,EAAW,IAAI,EACf,EAAkB,IAAI,EACtB,EAAS,IAAI,EAAY,CAAuB,EAClD,EAAU,GACV,EACA,EACA,EAAgB,GAEd,EAAO,CAAC,IAA0B,CACtC,QAAQ,IAAI,yBAAyB,GAAS,GAG1C,EAAgB,CAAC,EAAuB,IAAyB,CACrE,IAAQ,WAAU,cAAe,EAAO,QAAQ,EAChD,GAAI,EACF,EAAe,EAAW,EAAU,CAAM,EAC1C,EAAK,8CAA6C,KAAY,IAA0B,EAExF,OAAK,6DAA4D,EAEnE,EAAI,MAAM,GAIZ,EAAG,GAAG,gBAAiB,CAAC,IAAU,CAChC,IAAM,EAAO,EAAM,SAAS,KAC5B,GAAI,IAAS,YACX,EAAS,MAAM,EACf,EAAgB,GACX,QAAI,IAAS,OAAQ,CAC1B,IAAM,EAAO,EAAY,EAAM,QAAQ,OAAO,EAC9C,GAAI,EAAO,cAAc,CAAI,EAC3B,EAAK,mCAAkC,GAAyB,GAGrE,EAED,EAAG,GAAG,cAAe,IAAM,CACzB,EAAgB,MAAM,EACvB,EAID,EAAG,GAAG,iBAAkB,CAAC,EAAO,IAAQ,CACtC,GAAI,CAAC,GAAW,EAAe,OAC/B,GAAI,EAAM,QAAQ,OAAS,YAAa,OACxC,IAAM,EAAS,EAAS,OAAO,EAAY,EAAM,QAAQ,OAAO,CAAC,EACjE,GAAI,EAAO,KACT,EAAgB,GAChB,EAAc,EAAK,EAAO,MAAM,EAEnC,EAID,EAAG,GAAG,YAAa,CAAC,IAAU,CAC5B,GAAI,CAAC,EAAS,OACd,IAAM,EAAW,EAAe,EAAM,KAAK,EAE3C,GAAI,CADW,EAAgB,OAAO,EAAM,SAAU,CAAQ,EACjD,OACb,IAAQ,WAAU,cAAe,EAAO,QAAQ,EAChD,GAAI,EACF,EAAmB,EAAmB,EAAU,EAAM,SAAU,CAAQ,EACxE,EACE,6BAA4B,EAAM,aAAa,EAAS,MAAM,EAAG,EAAE,yBACvD,KAAY,IAC1B,EAEA,OAAK,2DAA0D,EAEjE,MAAO,CACL,MAAO,GACP,OAAQ,6DACR,UAAW,EACb,EACD,EAGD,EAAG,GAAG,cAAe,CAAC,IAAU,CAC9B,GAAI,CAAC,EAAc,OACnB,GAAI,EAAM,QAAQ,OAAS,YAAa,OACxC,IAAM,EAAQ,EACd,EAAe,OACf,EAAO,gBAAgB,CAAK,EAC5B,EAAK,0BAA0B,EAC/B,EAAG,gBAAgB,EAAO,CAAE,UAAW,OAAQ,CAAC,EACjD,EAGD,EAAG,GAAG,YAAa,IAAM,CACvB,GAAI,CAAC,EAAkB,OACvB,IAAM,EAAQ,EACd,EAAmB,OACnB,EAAO,gBAAgB,CAAK,EAC5B,EAAK,+BAA+B,EACpC,EAAG,gBAAgB,EAAO,CAAE,UAAW,OAAQ,CAAC,EACjD,EAGD,EAAG,gBAAgB,UAAW,CAC5B,YAAa,4DACb,QAAS,MAAO,EAAM,IAAQ,CAC5B,IAAM,EAAM,EAAK,KAAK,EAAE,YAAY,EACpC,GAAI,IAAQ,KAAM,EAAU,GACvB,QAAI,IAAQ,MAAO,EAAU,GAC7B,OAAU,CAAC,EAChB,GAAI,EAAI,MACN,EAAI,GAAG,OAAO,oBAAoB,EAAU,KAAO,QAAS,EAAU,OAAS,SAAS,EAE1F,GAAI,EAAI,OAAS,MACf,EAAI,GAAG,UAAU,sBAAuB,EAAU,WAAa,WAAW,EAGhF,CAAC,EAIH,SAAS,CAAc,CAAC,EAAwB,CAC9C,GAAI,IAAU,QAAa,IAAU,KAAM,MAAO,GAClD,GAAI,OAAO,IAAU,SAAU,OAAO,EACtC,GAAI,CACF,OAAO,EAAgB,CAAK,EAC5B,KAAM,CACN,OAAO,OAAO,CAAK,GAKvB,SAAS,CAAW,CAAC,EAA0B,CAC7C,GAAI,CAAC,MAAM,QAAQ,CAAO,EAAG,MAAO,GACpC,IAAI,EAAO,GACX,QAAW,KAAQ,EACjB,GAAI,GAAQ,OAAO,IAAS,SAAU,CACpC,IAAM,EAAK,EAA4B,KACvC,GAAI,OAAO,IAAM,SAAU,GAAQ,EAGvC,OAAO,EAGT,SAAS,CAAe,CAAC,EAAwB,CAC/C,GAAI,MAAM,QAAQ,CAAK,EACrB,MAAO,IAAI,EAAM,IAAI,CAAe,EAAE,KAAK,GAAG,KAEhD,GAAI,GAAS,OAAO,IAAU,SAI5B,MAAO,IAHS,OAAO,QAAQ,CAAgC,EAC5D,KAAK,EAAE,IAAK,KAAQ,EAAI,EAAI,GAAK,EAAI,EAAI,EAAI,CAAE,EAC/C,IAAI,EAAE,EAAG,KAAO,GAAG,KAAK,UAAU,CAAC,KAAK,EAAgB,CAAC,GAAG,EAC5C,KAAK,GAAG,KAE7B,OAAO,KAAK,UAAU,CAAK",
|
|
9
|
+
"debugId": "D5C9893C3F98A8AF64756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capdiem/pi-repetition-guard",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"pi": {
|