pi-anti-doom-loop 0.0.2 → 0.0.3
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 +9 -8
- package/extensions/detector.ts +44 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,17 +15,18 @@ pi install npm:pi-anti-doom-loop
|
|
|
15
15
|
|
|
16
16
|
## What it detects
|
|
17
17
|
|
|
18
|
-
| Signal
|
|
19
|
-
|
|
|
20
|
-
| Same `(tool, args)` repeated
|
|
21
|
-
| Same tool failing consecutively
|
|
22
|
-
| Same assistant text verbatim
|
|
18
|
+
| Signal | Default | Blocked when |
|
|
19
|
+
| -------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------- |
|
|
20
|
+
| Same `(tool, args)` repeated | 3× in the last 10 calls | The pattern has repeated `3` times with no change |
|
|
21
|
+
| Same tool failing consecutively | 3× | A tool errored `3` times in a row — stop retrying it blindly |
|
|
22
|
+
| Same assistant text verbatim | 3× in a row | The model re-emitted identical text `3` times (text-only loops) |
|
|
23
|
+
| Same sentence inside ONE message | 3× | A sentence repeats `3`+ times within a single message (growing self-concatenation loops) |
|
|
23
24
|
|
|
24
25
|
Blocks hand the model an instructive reason ("change your approach, use a
|
|
25
26
|
different tool, or ask the user"). If the model ignores the block and re-issues
|
|
26
|
-
the exact same call, the turn is **aborted** and you are notified.
|
|
27
|
-
text
|
|
28
|
-
notification.
|
|
27
|
+
the exact same call, the turn is **aborted** and you are notified. Verbatim
|
|
28
|
+
text loops and within-message self-repetition (no tool calls involved) abort
|
|
29
|
+
the run immediately with a notification.
|
|
29
30
|
|
|
30
31
|
Counters reset on every user prompt, so a task legitimately repeated later in
|
|
31
32
|
the same session is never a false positive.
|
package/extensions/detector.ts
CHANGED
|
@@ -142,6 +142,23 @@ export class LoopDetector {
|
|
|
142
142
|
checkText(text: string): Result<{ reason: string }, undefined> {
|
|
143
143
|
const norm = normalizeText(text);
|
|
144
144
|
if (!norm) return Result.err(undefined); // blank text is not a loop signal
|
|
145
|
+
|
|
146
|
+
// Within-message self-repetition: the model pastes the same sentence
|
|
147
|
+
// `textRepeatThreshold`+ times inside ONE message (growing loops like
|
|
148
|
+
// "…X:…X:…X"). Liquid.ai's loop definition — a section repeats at least N
|
|
149
|
+
// times. No streak needed: the message itself is the loop.
|
|
150
|
+
if (!this.textFired) {
|
|
151
|
+
const chunk = repeatedSegment(norm, this.opts.textRepeatThreshold);
|
|
152
|
+
if (chunk !== null) {
|
|
153
|
+
this.textFired = true;
|
|
154
|
+
return Result.ok({
|
|
155
|
+
reason:
|
|
156
|
+
`Assistant message repeats "${truncate(chunk, 60)}" ${this.opts.textRepeatThreshold}+ times ` +
|
|
157
|
+
`within a single message. You appear to be in a loop — this run is aborted.`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
145
162
|
this.textStreak = norm === this.lastText ? this.textStreak + 1 : 1;
|
|
146
163
|
this.lastText = norm;
|
|
147
164
|
if (this.textStreak >= this.opts.textRepeatThreshold && !this.textFired) {
|
|
@@ -192,6 +209,33 @@ export function truncate(text: string, max: number): string {
|
|
|
192
209
|
return text.length <= max ? text : `${text.slice(0, max)}…`;
|
|
193
210
|
}
|
|
194
211
|
|
|
212
|
+
/** Minimum length of a segment worth treating as a repeated loop chunk. */
|
|
213
|
+
export const MIN_REPEAT_CHUNK = 16;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Returns the first sentence-ish segment that repeats `threshold` times
|
|
217
|
+
* within a single normalized message, or null.
|
|
218
|
+
*
|
|
219
|
+
* Catches growing doom loops where the model self-concatenates the same
|
|
220
|
+
* sentence ("…X:…X:…X") — the pattern that evaded cross-message verbatim
|
|
221
|
+
* detection in production (each message differs, so no streak forms).
|
|
222
|
+
* Short segments (< MIN_REPEAT_CHUNK) are ignored so pasted logs with
|
|
223
|
+
* repeated one-word lines never false-positive.
|
|
224
|
+
*/
|
|
225
|
+
export function repeatedSegment(normalized: string, threshold: number): string | null {
|
|
226
|
+
const segments = normalized
|
|
227
|
+
.split(/(?<=[.:!?])\s*/)
|
|
228
|
+
.map((s) => s.trim().replace(/[.:!?]+$/, ""))
|
|
229
|
+
.filter((s) => s.length >= MIN_REPEAT_CHUNK);
|
|
230
|
+
const counts = new Map<string, number>();
|
|
231
|
+
for (const seg of segments) {
|
|
232
|
+
const n = (counts.get(seg) ?? 0) + 1;
|
|
233
|
+
if (n >= threshold) return seg;
|
|
234
|
+
counts.set(seg, n);
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
195
239
|
// --- self-check (runs under `node extensions/detector.ts`, skipped when loaded by pi) ---
|
|
196
240
|
if (import.meta.main) {
|
|
197
241
|
const opts: LoopOptions = {
|
package/package.json
CHANGED