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 CHANGED
@@ -15,17 +15,18 @@ pi install npm:pi-anti-doom-loop
15
15
 
16
16
  ## What it detects
17
17
 
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) |
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. A verbatim
27
- text loop (no tool calls involved) aborts the run immediately with a
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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-anti-doom-loop",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Detect and break agent doom loops in pi: blocks identical repeated tool calls and blind retries before they burn tokens.",
5
5
  "keywords": [
6
6
  "anti-doom-loop",