@wrongstack/plugins 1.0.13 → 1.0.14
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 +2 -1
- package/dist/index.js +163 -150
- package/dist/loop-breaker.js +163 -150
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -699,7 +699,7 @@ once you're confident in the rewrite.
|
|
|
699
699
|
### 22 — `loop-breaker`
|
|
700
700
|
|
|
701
701
|
**Tools**: `loop_breaker_status`
|
|
702
|
-
**Hooks**: `PreToolUse`
|
|
702
|
+
**Hooks**: `PreToolUse`, `PostToolUse`, `UserPromptSubmit`
|
|
703
703
|
|
|
704
704
|
Detects repeated tool calls before they execute. It tracks exact-repeat
|
|
705
705
|
streaks and A-B-A-B oscillations, then either blocks the call or injects a
|
|
@@ -711,6 +711,7 @@ warning depending on `mode`.
|
|
|
711
711
|
"loop-breaker": {
|
|
712
712
|
"enabled": true,
|
|
713
713
|
"mode": "block", // "block" | "warn"
|
|
714
|
+
"maxSteps": 0, // unlimited; set a positive value to opt into a cap
|
|
714
715
|
"repeatThreshold": 3,
|
|
715
716
|
"oscillationThreshold": 2
|
|
716
717
|
}
|
package/dist/index.js
CHANGED
|
@@ -12465,29 +12465,70 @@ var llm_cache_default = plugin34;
|
|
|
12465
12465
|
// src/loop-breaker/index.ts
|
|
12466
12466
|
import { execFile as execFile8 } from "node:child_process";
|
|
12467
12467
|
import { isAbsolute as isAbsolute15, relative as relative15 } from "node:path";
|
|
12468
|
-
var
|
|
12469
|
-
|
|
12470
|
-
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
|
|
12477
|
-
|
|
12478
|
-
|
|
12479
|
-
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
|
|
12468
|
+
var FALLBACK_SESSION = "__default__";
|
|
12469
|
+
var MAX_TRACKED_RUNS = 32;
|
|
12470
|
+
function createRunState() {
|
|
12471
|
+
return {
|
|
12472
|
+
lastFingerprint: null,
|
|
12473
|
+
streak: 0,
|
|
12474
|
+
recent: [],
|
|
12475
|
+
pendingBlockReason: null,
|
|
12476
|
+
lastDiffFingerprint: null,
|
|
12477
|
+
noDiffStreak: 0,
|
|
12478
|
+
lastErrorFingerprint: null,
|
|
12479
|
+
repeatedErrorStreak: 0,
|
|
12480
|
+
invocations: 0,
|
|
12481
|
+
postInvocations: 0,
|
|
12482
|
+
warnings: 0,
|
|
12483
|
+
blocks: 0,
|
|
12484
|
+
oscillationsDetected: 0,
|
|
12485
|
+
stepBudgetBlocks: 0,
|
|
12486
|
+
noDiffWarnings: 0,
|
|
12487
|
+
noDiffBlocks: 0,
|
|
12488
|
+
repeatedErrorWarnings: 0,
|
|
12489
|
+
repeatedErrorBlocks: 0
|
|
12490
|
+
};
|
|
12491
|
+
}
|
|
12492
|
+
var state32 = { runs: /* @__PURE__ */ new Map(), hookUnregister: null };
|
|
12493
|
+
function sessionKey(sessionId) {
|
|
12494
|
+
return sessionId || FALLBACK_SESSION;
|
|
12495
|
+
}
|
|
12496
|
+
function storeRun(key, current) {
|
|
12497
|
+
state32.runs.set(key, current);
|
|
12498
|
+
if (state32.runs.size <= MAX_TRACKED_RUNS) return;
|
|
12499
|
+
const oldest = state32.runs.keys().next().value;
|
|
12500
|
+
if (oldest !== void 0 && oldest !== key) state32.runs.delete(oldest);
|
|
12501
|
+
}
|
|
12502
|
+
function runState(sessionId) {
|
|
12503
|
+
const key = sessionKey(sessionId);
|
|
12504
|
+
let current = state32.runs.get(key);
|
|
12505
|
+
if (!current) {
|
|
12506
|
+
current = createRunState();
|
|
12507
|
+
storeRun(key, current);
|
|
12508
|
+
}
|
|
12509
|
+
return current;
|
|
12510
|
+
}
|
|
12511
|
+
function resetRun(sessionId) {
|
|
12512
|
+
storeRun(sessionKey(sessionId), createRunState());
|
|
12513
|
+
}
|
|
12514
|
+
function aggregateRuns() {
|
|
12515
|
+
const total = createRunState();
|
|
12516
|
+
for (const current of state32.runs.values()) {
|
|
12517
|
+
total.invocations += current.invocations;
|
|
12518
|
+
total.postInvocations += current.postInvocations;
|
|
12519
|
+
total.warnings += current.warnings;
|
|
12520
|
+
total.blocks += current.blocks;
|
|
12521
|
+
total.oscillationsDetected += current.oscillationsDetected;
|
|
12522
|
+
total.stepBudgetBlocks += current.stepBudgetBlocks;
|
|
12523
|
+
total.noDiffWarnings += current.noDiffWarnings;
|
|
12524
|
+
total.noDiffBlocks += current.noDiffBlocks;
|
|
12525
|
+
total.repeatedErrorWarnings += current.repeatedErrorWarnings;
|
|
12526
|
+
total.repeatedErrorBlocks += current.repeatedErrorBlocks;
|
|
12527
|
+
}
|
|
12528
|
+
return total;
|
|
12529
|
+
}
|
|
12489
12530
|
var DEFAULTS30 = {
|
|
12490
|
-
enabled:
|
|
12531
|
+
enabled: false,
|
|
12491
12532
|
// Documented contract (feature matrix, plugin description): warn, then
|
|
12492
12533
|
// block. A warn-only default meant an agent stuck re-issuing the same call
|
|
12493
12534
|
// was never actually stopped unless the user happened to set any option.
|
|
@@ -12495,7 +12536,7 @@ var DEFAULTS30 = {
|
|
|
12495
12536
|
warnAfter: 3,
|
|
12496
12537
|
blockAfter: 5,
|
|
12497
12538
|
oscillationWindow: 8,
|
|
12498
|
-
maxSteps:
|
|
12539
|
+
maxSteps: 0,
|
|
12499
12540
|
noDiffWarnAfter: 6,
|
|
12500
12541
|
noDiffBlockAfter: 10,
|
|
12501
12542
|
repeatedErrorWarnAfter: 2,
|
|
@@ -12521,7 +12562,7 @@ function readConfig30(raw) {
|
|
|
12521
12562
|
const rawOsc = r["oscillationWindow"] ?? r["oscillation_window"] ?? r["window"];
|
|
12522
12563
|
const rawMaxSteps = r["maxSteps"] ?? r["max_steps"] ?? r["stepLimit"] ?? r["step_limit"];
|
|
12523
12564
|
return {
|
|
12524
|
-
enabled: r["enabled"]
|
|
12565
|
+
enabled: r["enabled"] === true,
|
|
12525
12566
|
mode,
|
|
12526
12567
|
warnAfter,
|
|
12527
12568
|
blockAfter,
|
|
@@ -12634,7 +12675,11 @@ var plugin35 = {
|
|
|
12634
12675
|
configSchema: {
|
|
12635
12676
|
type: "object",
|
|
12636
12677
|
properties: {
|
|
12637
|
-
enabled: {
|
|
12678
|
+
enabled: {
|
|
12679
|
+
type: "boolean",
|
|
12680
|
+
default: false,
|
|
12681
|
+
description: "Opt-in master switch; disabled unless explicitly enabled."
|
|
12682
|
+
},
|
|
12638
12683
|
mode: {
|
|
12639
12684
|
type: "string",
|
|
12640
12685
|
enum: ["warn", "block"],
|
|
@@ -12662,8 +12707,8 @@ var plugin35 = {
|
|
|
12662
12707
|
maxSteps: {
|
|
12663
12708
|
type: "number",
|
|
12664
12709
|
minimum: 0,
|
|
12665
|
-
default:
|
|
12666
|
-
description: "
|
|
12710
|
+
default: 0,
|
|
12711
|
+
description: "Optional maximum tool steps before blocking; 0 keeps runs unlimited."
|
|
12667
12712
|
},
|
|
12668
12713
|
noDiffWarnAfter: {
|
|
12669
12714
|
type: "number",
|
|
@@ -12698,24 +12743,7 @@ var plugin35 = {
|
|
|
12698
12743
|
}
|
|
12699
12744
|
},
|
|
12700
12745
|
setup(api) {
|
|
12701
|
-
state32.
|
|
12702
|
-
state32.streak = 0;
|
|
12703
|
-
state32.recent = [];
|
|
12704
|
-
state32.pendingBlockReason = null;
|
|
12705
|
-
state32.lastDiffFingerprint = null;
|
|
12706
|
-
state32.noDiffStreak = 0;
|
|
12707
|
-
state32.lastErrorFingerprint = null;
|
|
12708
|
-
state32.repeatedErrorStreak = 0;
|
|
12709
|
-
state32.invocations = 0;
|
|
12710
|
-
state32.postInvocations = 0;
|
|
12711
|
-
state32.warnings = 0;
|
|
12712
|
-
state32.blocks = 0;
|
|
12713
|
-
state32.oscillationsDetected = 0;
|
|
12714
|
-
state32.stepBudgetBlocks = 0;
|
|
12715
|
-
state32.noDiffWarnings = 0;
|
|
12716
|
-
state32.noDiffBlocks = 0;
|
|
12717
|
-
state32.repeatedErrorWarnings = 0;
|
|
12718
|
-
state32.repeatedErrorBlocks = 0;
|
|
12746
|
+
state32.runs.clear();
|
|
12719
12747
|
if (state32.hookUnregister) {
|
|
12720
12748
|
try {
|
|
12721
12749
|
state32.hookUnregister();
|
|
@@ -12728,56 +12756,57 @@ var plugin35 = {
|
|
|
12728
12756
|
if (!cfg.enabled) return;
|
|
12729
12757
|
const toolName = input.toolName ?? "unknown";
|
|
12730
12758
|
if (ignoreToolsSet.has(toolName)) return;
|
|
12731
|
-
|
|
12732
|
-
if (
|
|
12733
|
-
const reason =
|
|
12734
|
-
|
|
12735
|
-
|
|
12759
|
+
const current = runState(input.sessionId);
|
|
12760
|
+
if (current.pendingBlockReason && cfg.mode === "block") {
|
|
12761
|
+
const reason = current.pendingBlockReason;
|
|
12762
|
+
current.pendingBlockReason = null;
|
|
12763
|
+
current.blocks += 1;
|
|
12736
12764
|
api.metrics.counter("blocks");
|
|
12737
12765
|
return { decision: "block", reason };
|
|
12738
12766
|
}
|
|
12739
|
-
if (cfg.maxSteps > 0 &&
|
|
12740
|
-
|
|
12741
|
-
|
|
12767
|
+
if (cfg.maxSteps > 0 && current.invocations >= cfg.maxSteps && cfg.mode === "block") {
|
|
12768
|
+
current.blocks += 1;
|
|
12769
|
+
current.stepBudgetBlocks += 1;
|
|
12742
12770
|
api.metrics.counter("blocks");
|
|
12743
12771
|
api.metrics.counter("step_budget_blocks");
|
|
12744
12772
|
return {
|
|
12745
12773
|
decision: "block",
|
|
12746
|
-
reason: `loop-breaker: step budget exceeded (${
|
|
12774
|
+
reason: `loop-breaker: step budget exceeded (${current.invocations}/${cfg.maxSteps} tool calls). Stop and report what has been tried instead of continuing an unbounded loop.`
|
|
12747
12775
|
};
|
|
12748
12776
|
}
|
|
12777
|
+
current.invocations += 1;
|
|
12749
12778
|
const fp = fingerprint(toolName, input.toolInput);
|
|
12750
|
-
if (fp ===
|
|
12751
|
-
|
|
12779
|
+
if (fp === current.lastFingerprint) {
|
|
12780
|
+
current.streak += 1;
|
|
12752
12781
|
} else {
|
|
12753
|
-
|
|
12754
|
-
|
|
12782
|
+
current.lastFingerprint = fp;
|
|
12783
|
+
current.streak = 1;
|
|
12755
12784
|
}
|
|
12756
|
-
|
|
12757
|
-
if (
|
|
12758
|
-
|
|
12785
|
+
current.recent.push(fp);
|
|
12786
|
+
if (current.recent.length > Math.max(cfg.oscillationWindow, 16)) {
|
|
12787
|
+
current.recent.splice(0, current.recent.length - Math.max(cfg.oscillationWindow, 16));
|
|
12759
12788
|
}
|
|
12760
|
-
if (
|
|
12761
|
-
|
|
12789
|
+
if (current.streak >= cfg.blockAfter && cfg.mode === "block") {
|
|
12790
|
+
current.blocks += 1;
|
|
12762
12791
|
api.metrics.counter("blocks");
|
|
12763
12792
|
return {
|
|
12764
12793
|
decision: "block",
|
|
12765
|
-
reason: `loop-breaker: "${toolName}" has been called ${
|
|
12794
|
+
reason: `loop-breaker: "${toolName}" has been called ${current.streak} times in a row with identical input. This looks like a runaway loop. Change the input, try a different tool, or explain to the user why repetition is needed. (Disable this guard via config.extensions["loop-breaker"].enabled = false.)`
|
|
12766
12795
|
};
|
|
12767
12796
|
}
|
|
12768
|
-
if (
|
|
12769
|
-
|
|
12797
|
+
if (current.streak >= cfg.warnAfter) {
|
|
12798
|
+
current.warnings += 1;
|
|
12770
12799
|
api.metrics.counter("warnings");
|
|
12771
|
-
const remaining = cfg.mode === "block" ? cfg.blockAfter -
|
|
12800
|
+
const remaining = cfg.mode === "block" ? cfg.blockAfter - current.streak : null;
|
|
12772
12801
|
return {
|
|
12773
12802
|
decision: "allow",
|
|
12774
|
-
additionalContext: `loop-breaker: "${toolName}" repeated ${
|
|
12803
|
+
additionalContext: `loop-breaker: "${toolName}" repeated ${current.streak}x with identical input.` + (remaining !== null && remaining > 0 ? ` It will be BLOCKED after ${remaining} more identical call(s).` : "") + " If the previous result was not what you needed, change the approach instead of retrying."
|
|
12775
12804
|
};
|
|
12776
12805
|
}
|
|
12777
|
-
if (isOscillating(
|
|
12778
|
-
|
|
12806
|
+
if (isOscillating(current.recent, cfg.oscillationWindow)) {
|
|
12807
|
+
current.oscillationsDetected += 1;
|
|
12779
12808
|
api.metrics.counter("oscillations");
|
|
12780
|
-
|
|
12809
|
+
current.recent = [];
|
|
12781
12810
|
return {
|
|
12782
12811
|
decision: "allow",
|
|
12783
12812
|
additionalContext: `loop-breaker: the last ${cfg.oscillationWindow} tool calls alternate between two identical calls (A-B-A-B pattern). You appear to be undoing and redoing the same work. Step back and pick a single approach.`
|
|
@@ -12789,32 +12818,33 @@ var plugin35 = {
|
|
|
12789
12818
|
if (!cfg.enabled) return;
|
|
12790
12819
|
const toolName = input.toolName ?? "unknown";
|
|
12791
12820
|
if (ignoreToolsSet.has(toolName)) return;
|
|
12792
|
-
|
|
12821
|
+
const current = runState(input.sessionId);
|
|
12822
|
+
current.postInvocations += 1;
|
|
12793
12823
|
if (input.toolResult?.isError) {
|
|
12794
12824
|
const errorFingerprint = normalizeError(input.toolResult.content);
|
|
12795
|
-
if (errorFingerprint && errorFingerprint ===
|
|
12796
|
-
|
|
12825
|
+
if (errorFingerprint && errorFingerprint === current.lastErrorFingerprint) {
|
|
12826
|
+
current.repeatedErrorStreak += 1;
|
|
12797
12827
|
} else {
|
|
12798
|
-
|
|
12799
|
-
|
|
12828
|
+
current.lastErrorFingerprint = errorFingerprint;
|
|
12829
|
+
current.repeatedErrorStreak = errorFingerprint ? 1 : 0;
|
|
12800
12830
|
}
|
|
12801
|
-
|
|
12802
|
-
if (cfg.repeatedErrorBlockAfter > 0 &&
|
|
12803
|
-
|
|
12804
|
-
|
|
12831
|
+
current.noDiffStreak = 0;
|
|
12832
|
+
if (cfg.repeatedErrorBlockAfter > 0 && current.repeatedErrorStreak >= cfg.repeatedErrorBlockAfter && cfg.mode === "block") {
|
|
12833
|
+
current.repeatedErrorBlocks += 1;
|
|
12834
|
+
current.pendingBlockReason = `loop-breaker: the same tool error repeated ${current.repeatedErrorStreak} times. The next tool call is blocked so you can stop, summarize the repeated failure, and change approach.`;
|
|
12805
12835
|
}
|
|
12806
|
-
if (
|
|
12807
|
-
|
|
12836
|
+
if (current.repeatedErrorStreak >= cfg.repeatedErrorWarnAfter) {
|
|
12837
|
+
current.repeatedErrorWarnings += 1;
|
|
12808
12838
|
api.metrics.counter("repeated_error_warnings");
|
|
12809
12839
|
return {
|
|
12810
12840
|
decision: "allow",
|
|
12811
|
-
additionalContext: `loop-breaker: same error repeated ${
|
|
12841
|
+
additionalContext: `loop-breaker: same error repeated ${current.repeatedErrorStreak}x. Do not retry the same command/tool unchanged; inspect the root cause or ask for help.`
|
|
12812
12842
|
};
|
|
12813
12843
|
}
|
|
12814
12844
|
return;
|
|
12815
12845
|
}
|
|
12816
|
-
|
|
12817
|
-
|
|
12846
|
+
current.lastErrorFingerprint = null;
|
|
12847
|
+
current.repeatedErrorStreak = 0;
|
|
12818
12848
|
if (!MUTATING_TOOLS.has(toolName)) return;
|
|
12819
12849
|
const toolInput = input.toolInput ?? {};
|
|
12820
12850
|
const rawTarget = toolInput["path"] ?? toolInput["TargetFile"] ?? toolInput["filePath"] ?? toolInput["targetFile"] ?? toolInput["file_path"] ?? toolInput["destination"] ?? toolInput["file"];
|
|
@@ -12826,22 +12856,22 @@ var plugin35 = {
|
|
|
12826
12856
|
runtime.signal
|
|
12827
12857
|
);
|
|
12828
12858
|
if (diffFingerprint === null) return;
|
|
12829
|
-
if (diffFingerprint ===
|
|
12830
|
-
|
|
12859
|
+
if (diffFingerprint === current.lastDiffFingerprint) {
|
|
12860
|
+
current.noDiffStreak += 1;
|
|
12831
12861
|
} else {
|
|
12832
|
-
|
|
12833
|
-
|
|
12862
|
+
current.lastDiffFingerprint = diffFingerprint;
|
|
12863
|
+
current.noDiffStreak = 0;
|
|
12834
12864
|
}
|
|
12835
|
-
if (cfg.noDiffBlockAfter > 0 &&
|
|
12836
|
-
|
|
12837
|
-
|
|
12865
|
+
if (cfg.noDiffBlockAfter > 0 && current.noDiffStreak >= cfg.noDiffBlockAfter && cfg.mode === "block") {
|
|
12866
|
+
current.noDiffBlocks += 1;
|
|
12867
|
+
current.pendingBlockReason = `loop-breaker: no diff was produced in the last ${current.noDiffStreak} mutating step(s). The next tool call is blocked because continued edits are not changing the working tree.`;
|
|
12838
12868
|
}
|
|
12839
|
-
if (
|
|
12840
|
-
|
|
12869
|
+
if (current.noDiffStreak >= cfg.noDiffWarnAfter) {
|
|
12870
|
+
current.noDiffWarnings += 1;
|
|
12841
12871
|
api.metrics.counter("no_diff_warnings");
|
|
12842
12872
|
return {
|
|
12843
12873
|
decision: "allow",
|
|
12844
|
-
additionalContext: `loop-breaker: no diff has changed for ${
|
|
12874
|
+
additionalContext: `loop-breaker: no diff has changed for ${current.noDiffStreak} mutating step(s). Stop repeating edits; read the file/status, explain why no change is happening, or choose a different approach.`
|
|
12845
12875
|
};
|
|
12846
12876
|
}
|
|
12847
12877
|
return;
|
|
@@ -12856,9 +12886,18 @@ var plugin35 = {
|
|
|
12856
12886
|
timeoutMs: 2e3,
|
|
12857
12887
|
failurePolicy: "open"
|
|
12858
12888
|
});
|
|
12889
|
+
const unregisterPrompt = api.registerHook(
|
|
12890
|
+
"UserPromptSubmit",
|
|
12891
|
+
void 0,
|
|
12892
|
+
((input) => {
|
|
12893
|
+
resetRun(input.sessionId);
|
|
12894
|
+
}),
|
|
12895
|
+
{ name: "loop-breaker-turn-reset", failurePolicy: "open" }
|
|
12896
|
+
);
|
|
12859
12897
|
state32.hookUnregister = () => {
|
|
12860
12898
|
unregisterPre();
|
|
12861
12899
|
unregisterPost();
|
|
12900
|
+
unregisterPrompt();
|
|
12862
12901
|
};
|
|
12863
12902
|
api.tools.register({
|
|
12864
12903
|
name: "loop_breaker_status",
|
|
@@ -12867,7 +12906,8 @@ var plugin35 = {
|
|
|
12867
12906
|
permission: "auto",
|
|
12868
12907
|
category: "Diagnostics",
|
|
12869
12908
|
mutating: false,
|
|
12870
|
-
async execute() {
|
|
12909
|
+
async execute(_input, ctx) {
|
|
12910
|
+
const current = runState(ctx?.session?.id);
|
|
12871
12911
|
return {
|
|
12872
12912
|
ok: true,
|
|
12873
12913
|
enabled: cfg.enabled,
|
|
@@ -12881,20 +12921,20 @@ var plugin35 = {
|
|
|
12881
12921
|
repeatedErrorWarnAfter: cfg.repeatedErrorWarnAfter,
|
|
12882
12922
|
repeatedErrorBlockAfter: cfg.repeatedErrorBlockAfter,
|
|
12883
12923
|
ignoreTools: cfg.ignoreTools,
|
|
12884
|
-
currentStreak:
|
|
12885
|
-
noDiffStreak:
|
|
12886
|
-
repeatedErrorStreak:
|
|
12924
|
+
currentStreak: current.streak,
|
|
12925
|
+
noDiffStreak: current.noDiffStreak,
|
|
12926
|
+
repeatedErrorStreak: current.repeatedErrorStreak,
|
|
12887
12927
|
counters: {
|
|
12888
|
-
invocations:
|
|
12889
|
-
postInvocations:
|
|
12890
|
-
warnings:
|
|
12891
|
-
blocks:
|
|
12892
|
-
oscillationsDetected:
|
|
12893
|
-
stepBudgetBlocks:
|
|
12894
|
-
noDiffWarnings:
|
|
12895
|
-
noDiffBlocks:
|
|
12896
|
-
repeatedErrorWarnings:
|
|
12897
|
-
repeatedErrorBlocks:
|
|
12928
|
+
invocations: current.invocations,
|
|
12929
|
+
postInvocations: current.postInvocations,
|
|
12930
|
+
warnings: current.warnings,
|
|
12931
|
+
blocks: current.blocks,
|
|
12932
|
+
oscillationsDetected: current.oscillationsDetected,
|
|
12933
|
+
stepBudgetBlocks: current.stepBudgetBlocks,
|
|
12934
|
+
noDiffWarnings: current.noDiffWarnings,
|
|
12935
|
+
noDiffBlocks: current.noDiffBlocks,
|
|
12936
|
+
repeatedErrorWarnings: current.repeatedErrorWarnings,
|
|
12937
|
+
repeatedErrorBlocks: current.repeatedErrorBlocks
|
|
12898
12938
|
}
|
|
12899
12939
|
};
|
|
12900
12940
|
}
|
|
@@ -12915,53 +12955,26 @@ var plugin35 = {
|
|
|
12915
12955
|
}
|
|
12916
12956
|
state32.hookUnregister = null;
|
|
12917
12957
|
}
|
|
12918
|
-
const final =
|
|
12919
|
-
|
|
12920
|
-
postInvocations: state32.postInvocations,
|
|
12921
|
-
warnings: state32.warnings,
|
|
12922
|
-
blocks: state32.blocks,
|
|
12923
|
-
oscillationsDetected: state32.oscillationsDetected,
|
|
12924
|
-
stepBudgetBlocks: state32.stepBudgetBlocks,
|
|
12925
|
-
noDiffWarnings: state32.noDiffWarnings,
|
|
12926
|
-
noDiffBlocks: state32.noDiffBlocks,
|
|
12927
|
-
repeatedErrorWarnings: state32.repeatedErrorWarnings,
|
|
12928
|
-
repeatedErrorBlocks: state32.repeatedErrorBlocks
|
|
12929
|
-
};
|
|
12930
|
-
state32.lastFingerprint = null;
|
|
12931
|
-
state32.streak = 0;
|
|
12932
|
-
state32.recent = [];
|
|
12933
|
-
state32.pendingBlockReason = null;
|
|
12934
|
-
state32.lastDiffFingerprint = null;
|
|
12935
|
-
state32.noDiffStreak = 0;
|
|
12936
|
-
state32.lastErrorFingerprint = null;
|
|
12937
|
-
state32.repeatedErrorStreak = 0;
|
|
12938
|
-
state32.invocations = 0;
|
|
12939
|
-
state32.postInvocations = 0;
|
|
12940
|
-
state32.warnings = 0;
|
|
12941
|
-
state32.blocks = 0;
|
|
12942
|
-
state32.oscillationsDetected = 0;
|
|
12943
|
-
state32.stepBudgetBlocks = 0;
|
|
12944
|
-
state32.noDiffWarnings = 0;
|
|
12945
|
-
state32.noDiffBlocks = 0;
|
|
12946
|
-
state32.repeatedErrorWarnings = 0;
|
|
12947
|
-
state32.repeatedErrorBlocks = 0;
|
|
12958
|
+
const final = aggregateRuns();
|
|
12959
|
+
state32.runs.clear();
|
|
12948
12960
|
api.log.info("loop-breaker: teardown complete", { final });
|
|
12949
12961
|
},
|
|
12950
12962
|
async health() {
|
|
12963
|
+
const totals = aggregateRuns();
|
|
12951
12964
|
return {
|
|
12952
12965
|
ok: true,
|
|
12953
|
-
message: `loop-breaker: ${
|
|
12966
|
+
message: `loop-breaker: ${totals.invocations} call(s) observed, ${totals.warnings + totals.noDiffWarnings + totals.repeatedErrorWarnings} warning(s), ${totals.blocks} block(s), ${totals.oscillationsDetected} oscillation(s)`,
|
|
12954
12967
|
counters: {
|
|
12955
|
-
invocations:
|
|
12956
|
-
postInvocations:
|
|
12957
|
-
warnings:
|
|
12958
|
-
blocks:
|
|
12959
|
-
oscillationsDetected:
|
|
12960
|
-
stepBudgetBlocks:
|
|
12961
|
-
noDiffWarnings:
|
|
12962
|
-
noDiffBlocks:
|
|
12963
|
-
repeatedErrorWarnings:
|
|
12964
|
-
repeatedErrorBlocks:
|
|
12968
|
+
invocations: totals.invocations,
|
|
12969
|
+
postInvocations: totals.postInvocations,
|
|
12970
|
+
warnings: totals.warnings,
|
|
12971
|
+
blocks: totals.blocks,
|
|
12972
|
+
oscillationsDetected: totals.oscillationsDetected,
|
|
12973
|
+
stepBudgetBlocks: totals.stepBudgetBlocks,
|
|
12974
|
+
noDiffWarnings: totals.noDiffWarnings,
|
|
12975
|
+
noDiffBlocks: totals.noDiffBlocks,
|
|
12976
|
+
repeatedErrorWarnings: totals.repeatedErrorWarnings,
|
|
12977
|
+
repeatedErrorBlocks: totals.repeatedErrorBlocks
|
|
12965
12978
|
}
|
|
12966
12979
|
};
|
|
12967
12980
|
}
|
package/dist/loop-breaker.js
CHANGED
|
@@ -1,29 +1,70 @@
|
|
|
1
1
|
// src/loop-breaker/index.ts
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { isAbsolute, relative } from "node:path";
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
4
|
+
var FALLBACK_SESSION = "__default__";
|
|
5
|
+
var MAX_TRACKED_RUNS = 32;
|
|
6
|
+
function createRunState() {
|
|
7
|
+
return {
|
|
8
|
+
lastFingerprint: null,
|
|
9
|
+
streak: 0,
|
|
10
|
+
recent: [],
|
|
11
|
+
pendingBlockReason: null,
|
|
12
|
+
lastDiffFingerprint: null,
|
|
13
|
+
noDiffStreak: 0,
|
|
14
|
+
lastErrorFingerprint: null,
|
|
15
|
+
repeatedErrorStreak: 0,
|
|
16
|
+
invocations: 0,
|
|
17
|
+
postInvocations: 0,
|
|
18
|
+
warnings: 0,
|
|
19
|
+
blocks: 0,
|
|
20
|
+
oscillationsDetected: 0,
|
|
21
|
+
stepBudgetBlocks: 0,
|
|
22
|
+
noDiffWarnings: 0,
|
|
23
|
+
noDiffBlocks: 0,
|
|
24
|
+
repeatedErrorWarnings: 0,
|
|
25
|
+
repeatedErrorBlocks: 0
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
var state = { runs: /* @__PURE__ */ new Map(), hookUnregister: null };
|
|
29
|
+
function sessionKey(sessionId) {
|
|
30
|
+
return sessionId || FALLBACK_SESSION;
|
|
31
|
+
}
|
|
32
|
+
function storeRun(key, current) {
|
|
33
|
+
state.runs.set(key, current);
|
|
34
|
+
if (state.runs.size <= MAX_TRACKED_RUNS) return;
|
|
35
|
+
const oldest = state.runs.keys().next().value;
|
|
36
|
+
if (oldest !== void 0 && oldest !== key) state.runs.delete(oldest);
|
|
37
|
+
}
|
|
38
|
+
function runState(sessionId) {
|
|
39
|
+
const key = sessionKey(sessionId);
|
|
40
|
+
let current = state.runs.get(key);
|
|
41
|
+
if (!current) {
|
|
42
|
+
current = createRunState();
|
|
43
|
+
storeRun(key, current);
|
|
44
|
+
}
|
|
45
|
+
return current;
|
|
46
|
+
}
|
|
47
|
+
function resetRun(sessionId) {
|
|
48
|
+
storeRun(sessionKey(sessionId), createRunState());
|
|
49
|
+
}
|
|
50
|
+
function aggregateRuns() {
|
|
51
|
+
const total = createRunState();
|
|
52
|
+
for (const current of state.runs.values()) {
|
|
53
|
+
total.invocations += current.invocations;
|
|
54
|
+
total.postInvocations += current.postInvocations;
|
|
55
|
+
total.warnings += current.warnings;
|
|
56
|
+
total.blocks += current.blocks;
|
|
57
|
+
total.oscillationsDetected += current.oscillationsDetected;
|
|
58
|
+
total.stepBudgetBlocks += current.stepBudgetBlocks;
|
|
59
|
+
total.noDiffWarnings += current.noDiffWarnings;
|
|
60
|
+
total.noDiffBlocks += current.noDiffBlocks;
|
|
61
|
+
total.repeatedErrorWarnings += current.repeatedErrorWarnings;
|
|
62
|
+
total.repeatedErrorBlocks += current.repeatedErrorBlocks;
|
|
63
|
+
}
|
|
64
|
+
return total;
|
|
65
|
+
}
|
|
25
66
|
var DEFAULTS = {
|
|
26
|
-
enabled:
|
|
67
|
+
enabled: false,
|
|
27
68
|
// Documented contract (feature matrix, plugin description): warn, then
|
|
28
69
|
// block. A warn-only default meant an agent stuck re-issuing the same call
|
|
29
70
|
// was never actually stopped unless the user happened to set any option.
|
|
@@ -31,7 +72,7 @@ var DEFAULTS = {
|
|
|
31
72
|
warnAfter: 3,
|
|
32
73
|
blockAfter: 5,
|
|
33
74
|
oscillationWindow: 8,
|
|
34
|
-
maxSteps:
|
|
75
|
+
maxSteps: 0,
|
|
35
76
|
noDiffWarnAfter: 6,
|
|
36
77
|
noDiffBlockAfter: 10,
|
|
37
78
|
repeatedErrorWarnAfter: 2,
|
|
@@ -57,7 +98,7 @@ function readConfig(raw) {
|
|
|
57
98
|
const rawOsc = r["oscillationWindow"] ?? r["oscillation_window"] ?? r["window"];
|
|
58
99
|
const rawMaxSteps = r["maxSteps"] ?? r["max_steps"] ?? r["stepLimit"] ?? r["step_limit"];
|
|
59
100
|
return {
|
|
60
|
-
enabled: r["enabled"]
|
|
101
|
+
enabled: r["enabled"] === true,
|
|
61
102
|
mode,
|
|
62
103
|
warnAfter,
|
|
63
104
|
blockAfter,
|
|
@@ -170,7 +211,11 @@ var plugin = {
|
|
|
170
211
|
configSchema: {
|
|
171
212
|
type: "object",
|
|
172
213
|
properties: {
|
|
173
|
-
enabled: {
|
|
214
|
+
enabled: {
|
|
215
|
+
type: "boolean",
|
|
216
|
+
default: false,
|
|
217
|
+
description: "Opt-in master switch; disabled unless explicitly enabled."
|
|
218
|
+
},
|
|
174
219
|
mode: {
|
|
175
220
|
type: "string",
|
|
176
221
|
enum: ["warn", "block"],
|
|
@@ -198,8 +243,8 @@ var plugin = {
|
|
|
198
243
|
maxSteps: {
|
|
199
244
|
type: "number",
|
|
200
245
|
minimum: 0,
|
|
201
|
-
default:
|
|
202
|
-
description: "
|
|
246
|
+
default: 0,
|
|
247
|
+
description: "Optional maximum tool steps before blocking; 0 keeps runs unlimited."
|
|
203
248
|
},
|
|
204
249
|
noDiffWarnAfter: {
|
|
205
250
|
type: "number",
|
|
@@ -234,24 +279,7 @@ var plugin = {
|
|
|
234
279
|
}
|
|
235
280
|
},
|
|
236
281
|
setup(api) {
|
|
237
|
-
state.
|
|
238
|
-
state.streak = 0;
|
|
239
|
-
state.recent = [];
|
|
240
|
-
state.pendingBlockReason = null;
|
|
241
|
-
state.lastDiffFingerprint = null;
|
|
242
|
-
state.noDiffStreak = 0;
|
|
243
|
-
state.lastErrorFingerprint = null;
|
|
244
|
-
state.repeatedErrorStreak = 0;
|
|
245
|
-
state.invocations = 0;
|
|
246
|
-
state.postInvocations = 0;
|
|
247
|
-
state.warnings = 0;
|
|
248
|
-
state.blocks = 0;
|
|
249
|
-
state.oscillationsDetected = 0;
|
|
250
|
-
state.stepBudgetBlocks = 0;
|
|
251
|
-
state.noDiffWarnings = 0;
|
|
252
|
-
state.noDiffBlocks = 0;
|
|
253
|
-
state.repeatedErrorWarnings = 0;
|
|
254
|
-
state.repeatedErrorBlocks = 0;
|
|
282
|
+
state.runs.clear();
|
|
255
283
|
if (state.hookUnregister) {
|
|
256
284
|
try {
|
|
257
285
|
state.hookUnregister();
|
|
@@ -264,56 +292,57 @@ var plugin = {
|
|
|
264
292
|
if (!cfg.enabled) return;
|
|
265
293
|
const toolName = input.toolName ?? "unknown";
|
|
266
294
|
if (ignoreToolsSet.has(toolName)) return;
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
const reason =
|
|
270
|
-
|
|
271
|
-
|
|
295
|
+
const current = runState(input.sessionId);
|
|
296
|
+
if (current.pendingBlockReason && cfg.mode === "block") {
|
|
297
|
+
const reason = current.pendingBlockReason;
|
|
298
|
+
current.pendingBlockReason = null;
|
|
299
|
+
current.blocks += 1;
|
|
272
300
|
api.metrics.counter("blocks");
|
|
273
301
|
return { decision: "block", reason };
|
|
274
302
|
}
|
|
275
|
-
if (cfg.maxSteps > 0 &&
|
|
276
|
-
|
|
277
|
-
|
|
303
|
+
if (cfg.maxSteps > 0 && current.invocations >= cfg.maxSteps && cfg.mode === "block") {
|
|
304
|
+
current.blocks += 1;
|
|
305
|
+
current.stepBudgetBlocks += 1;
|
|
278
306
|
api.metrics.counter("blocks");
|
|
279
307
|
api.metrics.counter("step_budget_blocks");
|
|
280
308
|
return {
|
|
281
309
|
decision: "block",
|
|
282
|
-
reason: `loop-breaker: step budget exceeded (${
|
|
310
|
+
reason: `loop-breaker: step budget exceeded (${current.invocations}/${cfg.maxSteps} tool calls). Stop and report what has been tried instead of continuing an unbounded loop.`
|
|
283
311
|
};
|
|
284
312
|
}
|
|
313
|
+
current.invocations += 1;
|
|
285
314
|
const fp = fingerprint(toolName, input.toolInput);
|
|
286
|
-
if (fp ===
|
|
287
|
-
|
|
315
|
+
if (fp === current.lastFingerprint) {
|
|
316
|
+
current.streak += 1;
|
|
288
317
|
} else {
|
|
289
|
-
|
|
290
|
-
|
|
318
|
+
current.lastFingerprint = fp;
|
|
319
|
+
current.streak = 1;
|
|
291
320
|
}
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
|
|
321
|
+
current.recent.push(fp);
|
|
322
|
+
if (current.recent.length > Math.max(cfg.oscillationWindow, 16)) {
|
|
323
|
+
current.recent.splice(0, current.recent.length - Math.max(cfg.oscillationWindow, 16));
|
|
295
324
|
}
|
|
296
|
-
if (
|
|
297
|
-
|
|
325
|
+
if (current.streak >= cfg.blockAfter && cfg.mode === "block") {
|
|
326
|
+
current.blocks += 1;
|
|
298
327
|
api.metrics.counter("blocks");
|
|
299
328
|
return {
|
|
300
329
|
decision: "block",
|
|
301
|
-
reason: `loop-breaker: "${toolName}" has been called ${
|
|
330
|
+
reason: `loop-breaker: "${toolName}" has been called ${current.streak} times in a row with identical input. This looks like a runaway loop. Change the input, try a different tool, or explain to the user why repetition is needed. (Disable this guard via config.extensions["loop-breaker"].enabled = false.)`
|
|
302
331
|
};
|
|
303
332
|
}
|
|
304
|
-
if (
|
|
305
|
-
|
|
333
|
+
if (current.streak >= cfg.warnAfter) {
|
|
334
|
+
current.warnings += 1;
|
|
306
335
|
api.metrics.counter("warnings");
|
|
307
|
-
const remaining = cfg.mode === "block" ? cfg.blockAfter -
|
|
336
|
+
const remaining = cfg.mode === "block" ? cfg.blockAfter - current.streak : null;
|
|
308
337
|
return {
|
|
309
338
|
decision: "allow",
|
|
310
|
-
additionalContext: `loop-breaker: "${toolName}" repeated ${
|
|
339
|
+
additionalContext: `loop-breaker: "${toolName}" repeated ${current.streak}x with identical input.` + (remaining !== null && remaining > 0 ? ` It will be BLOCKED after ${remaining} more identical call(s).` : "") + " If the previous result was not what you needed, change the approach instead of retrying."
|
|
311
340
|
};
|
|
312
341
|
}
|
|
313
|
-
if (isOscillating(
|
|
314
|
-
|
|
342
|
+
if (isOscillating(current.recent, cfg.oscillationWindow)) {
|
|
343
|
+
current.oscillationsDetected += 1;
|
|
315
344
|
api.metrics.counter("oscillations");
|
|
316
|
-
|
|
345
|
+
current.recent = [];
|
|
317
346
|
return {
|
|
318
347
|
decision: "allow",
|
|
319
348
|
additionalContext: `loop-breaker: the last ${cfg.oscillationWindow} tool calls alternate between two identical calls (A-B-A-B pattern). You appear to be undoing and redoing the same work. Step back and pick a single approach.`
|
|
@@ -325,32 +354,33 @@ var plugin = {
|
|
|
325
354
|
if (!cfg.enabled) return;
|
|
326
355
|
const toolName = input.toolName ?? "unknown";
|
|
327
356
|
if (ignoreToolsSet.has(toolName)) return;
|
|
328
|
-
|
|
357
|
+
const current = runState(input.sessionId);
|
|
358
|
+
current.postInvocations += 1;
|
|
329
359
|
if (input.toolResult?.isError) {
|
|
330
360
|
const errorFingerprint = normalizeError(input.toolResult.content);
|
|
331
|
-
if (errorFingerprint && errorFingerprint ===
|
|
332
|
-
|
|
361
|
+
if (errorFingerprint && errorFingerprint === current.lastErrorFingerprint) {
|
|
362
|
+
current.repeatedErrorStreak += 1;
|
|
333
363
|
} else {
|
|
334
|
-
|
|
335
|
-
|
|
364
|
+
current.lastErrorFingerprint = errorFingerprint;
|
|
365
|
+
current.repeatedErrorStreak = errorFingerprint ? 1 : 0;
|
|
336
366
|
}
|
|
337
|
-
|
|
338
|
-
if (cfg.repeatedErrorBlockAfter > 0 &&
|
|
339
|
-
|
|
340
|
-
|
|
367
|
+
current.noDiffStreak = 0;
|
|
368
|
+
if (cfg.repeatedErrorBlockAfter > 0 && current.repeatedErrorStreak >= cfg.repeatedErrorBlockAfter && cfg.mode === "block") {
|
|
369
|
+
current.repeatedErrorBlocks += 1;
|
|
370
|
+
current.pendingBlockReason = `loop-breaker: the same tool error repeated ${current.repeatedErrorStreak} times. The next tool call is blocked so you can stop, summarize the repeated failure, and change approach.`;
|
|
341
371
|
}
|
|
342
|
-
if (
|
|
343
|
-
|
|
372
|
+
if (current.repeatedErrorStreak >= cfg.repeatedErrorWarnAfter) {
|
|
373
|
+
current.repeatedErrorWarnings += 1;
|
|
344
374
|
api.metrics.counter("repeated_error_warnings");
|
|
345
375
|
return {
|
|
346
376
|
decision: "allow",
|
|
347
|
-
additionalContext: `loop-breaker: same error repeated ${
|
|
377
|
+
additionalContext: `loop-breaker: same error repeated ${current.repeatedErrorStreak}x. Do not retry the same command/tool unchanged; inspect the root cause or ask for help.`
|
|
348
378
|
};
|
|
349
379
|
}
|
|
350
380
|
return;
|
|
351
381
|
}
|
|
352
|
-
|
|
353
|
-
|
|
382
|
+
current.lastErrorFingerprint = null;
|
|
383
|
+
current.repeatedErrorStreak = 0;
|
|
354
384
|
if (!MUTATING_TOOLS.has(toolName)) return;
|
|
355
385
|
const toolInput = input.toolInput ?? {};
|
|
356
386
|
const rawTarget = toolInput["path"] ?? toolInput["TargetFile"] ?? toolInput["filePath"] ?? toolInput["targetFile"] ?? toolInput["file_path"] ?? toolInput["destination"] ?? toolInput["file"];
|
|
@@ -362,22 +392,22 @@ var plugin = {
|
|
|
362
392
|
runtime.signal
|
|
363
393
|
);
|
|
364
394
|
if (diffFingerprint === null) return;
|
|
365
|
-
if (diffFingerprint ===
|
|
366
|
-
|
|
395
|
+
if (diffFingerprint === current.lastDiffFingerprint) {
|
|
396
|
+
current.noDiffStreak += 1;
|
|
367
397
|
} else {
|
|
368
|
-
|
|
369
|
-
|
|
398
|
+
current.lastDiffFingerprint = diffFingerprint;
|
|
399
|
+
current.noDiffStreak = 0;
|
|
370
400
|
}
|
|
371
|
-
if (cfg.noDiffBlockAfter > 0 &&
|
|
372
|
-
|
|
373
|
-
|
|
401
|
+
if (cfg.noDiffBlockAfter > 0 && current.noDiffStreak >= cfg.noDiffBlockAfter && cfg.mode === "block") {
|
|
402
|
+
current.noDiffBlocks += 1;
|
|
403
|
+
current.pendingBlockReason = `loop-breaker: no diff was produced in the last ${current.noDiffStreak} mutating step(s). The next tool call is blocked because continued edits are not changing the working tree.`;
|
|
374
404
|
}
|
|
375
|
-
if (
|
|
376
|
-
|
|
405
|
+
if (current.noDiffStreak >= cfg.noDiffWarnAfter) {
|
|
406
|
+
current.noDiffWarnings += 1;
|
|
377
407
|
api.metrics.counter("no_diff_warnings");
|
|
378
408
|
return {
|
|
379
409
|
decision: "allow",
|
|
380
|
-
additionalContext: `loop-breaker: no diff has changed for ${
|
|
410
|
+
additionalContext: `loop-breaker: no diff has changed for ${current.noDiffStreak} mutating step(s). Stop repeating edits; read the file/status, explain why no change is happening, or choose a different approach.`
|
|
381
411
|
};
|
|
382
412
|
}
|
|
383
413
|
return;
|
|
@@ -392,9 +422,18 @@ var plugin = {
|
|
|
392
422
|
timeoutMs: 2e3,
|
|
393
423
|
failurePolicy: "open"
|
|
394
424
|
});
|
|
425
|
+
const unregisterPrompt = api.registerHook(
|
|
426
|
+
"UserPromptSubmit",
|
|
427
|
+
void 0,
|
|
428
|
+
((input) => {
|
|
429
|
+
resetRun(input.sessionId);
|
|
430
|
+
}),
|
|
431
|
+
{ name: "loop-breaker-turn-reset", failurePolicy: "open" }
|
|
432
|
+
);
|
|
395
433
|
state.hookUnregister = () => {
|
|
396
434
|
unregisterPre();
|
|
397
435
|
unregisterPost();
|
|
436
|
+
unregisterPrompt();
|
|
398
437
|
};
|
|
399
438
|
api.tools.register({
|
|
400
439
|
name: "loop_breaker_status",
|
|
@@ -403,7 +442,8 @@ var plugin = {
|
|
|
403
442
|
permission: "auto",
|
|
404
443
|
category: "Diagnostics",
|
|
405
444
|
mutating: false,
|
|
406
|
-
async execute() {
|
|
445
|
+
async execute(_input, ctx) {
|
|
446
|
+
const current = runState(ctx?.session?.id);
|
|
407
447
|
return {
|
|
408
448
|
ok: true,
|
|
409
449
|
enabled: cfg.enabled,
|
|
@@ -417,20 +457,20 @@ var plugin = {
|
|
|
417
457
|
repeatedErrorWarnAfter: cfg.repeatedErrorWarnAfter,
|
|
418
458
|
repeatedErrorBlockAfter: cfg.repeatedErrorBlockAfter,
|
|
419
459
|
ignoreTools: cfg.ignoreTools,
|
|
420
|
-
currentStreak:
|
|
421
|
-
noDiffStreak:
|
|
422
|
-
repeatedErrorStreak:
|
|
460
|
+
currentStreak: current.streak,
|
|
461
|
+
noDiffStreak: current.noDiffStreak,
|
|
462
|
+
repeatedErrorStreak: current.repeatedErrorStreak,
|
|
423
463
|
counters: {
|
|
424
|
-
invocations:
|
|
425
|
-
postInvocations:
|
|
426
|
-
warnings:
|
|
427
|
-
blocks:
|
|
428
|
-
oscillationsDetected:
|
|
429
|
-
stepBudgetBlocks:
|
|
430
|
-
noDiffWarnings:
|
|
431
|
-
noDiffBlocks:
|
|
432
|
-
repeatedErrorWarnings:
|
|
433
|
-
repeatedErrorBlocks:
|
|
464
|
+
invocations: current.invocations,
|
|
465
|
+
postInvocations: current.postInvocations,
|
|
466
|
+
warnings: current.warnings,
|
|
467
|
+
blocks: current.blocks,
|
|
468
|
+
oscillationsDetected: current.oscillationsDetected,
|
|
469
|
+
stepBudgetBlocks: current.stepBudgetBlocks,
|
|
470
|
+
noDiffWarnings: current.noDiffWarnings,
|
|
471
|
+
noDiffBlocks: current.noDiffBlocks,
|
|
472
|
+
repeatedErrorWarnings: current.repeatedErrorWarnings,
|
|
473
|
+
repeatedErrorBlocks: current.repeatedErrorBlocks
|
|
434
474
|
}
|
|
435
475
|
};
|
|
436
476
|
}
|
|
@@ -451,53 +491,26 @@ var plugin = {
|
|
|
451
491
|
}
|
|
452
492
|
state.hookUnregister = null;
|
|
453
493
|
}
|
|
454
|
-
const final =
|
|
455
|
-
|
|
456
|
-
postInvocations: state.postInvocations,
|
|
457
|
-
warnings: state.warnings,
|
|
458
|
-
blocks: state.blocks,
|
|
459
|
-
oscillationsDetected: state.oscillationsDetected,
|
|
460
|
-
stepBudgetBlocks: state.stepBudgetBlocks,
|
|
461
|
-
noDiffWarnings: state.noDiffWarnings,
|
|
462
|
-
noDiffBlocks: state.noDiffBlocks,
|
|
463
|
-
repeatedErrorWarnings: state.repeatedErrorWarnings,
|
|
464
|
-
repeatedErrorBlocks: state.repeatedErrorBlocks
|
|
465
|
-
};
|
|
466
|
-
state.lastFingerprint = null;
|
|
467
|
-
state.streak = 0;
|
|
468
|
-
state.recent = [];
|
|
469
|
-
state.pendingBlockReason = null;
|
|
470
|
-
state.lastDiffFingerprint = null;
|
|
471
|
-
state.noDiffStreak = 0;
|
|
472
|
-
state.lastErrorFingerprint = null;
|
|
473
|
-
state.repeatedErrorStreak = 0;
|
|
474
|
-
state.invocations = 0;
|
|
475
|
-
state.postInvocations = 0;
|
|
476
|
-
state.warnings = 0;
|
|
477
|
-
state.blocks = 0;
|
|
478
|
-
state.oscillationsDetected = 0;
|
|
479
|
-
state.stepBudgetBlocks = 0;
|
|
480
|
-
state.noDiffWarnings = 0;
|
|
481
|
-
state.noDiffBlocks = 0;
|
|
482
|
-
state.repeatedErrorWarnings = 0;
|
|
483
|
-
state.repeatedErrorBlocks = 0;
|
|
494
|
+
const final = aggregateRuns();
|
|
495
|
+
state.runs.clear();
|
|
484
496
|
api.log.info("loop-breaker: teardown complete", { final });
|
|
485
497
|
},
|
|
486
498
|
async health() {
|
|
499
|
+
const totals = aggregateRuns();
|
|
487
500
|
return {
|
|
488
501
|
ok: true,
|
|
489
|
-
message: `loop-breaker: ${
|
|
502
|
+
message: `loop-breaker: ${totals.invocations} call(s) observed, ${totals.warnings + totals.noDiffWarnings + totals.repeatedErrorWarnings} warning(s), ${totals.blocks} block(s), ${totals.oscillationsDetected} oscillation(s)`,
|
|
490
503
|
counters: {
|
|
491
|
-
invocations:
|
|
492
|
-
postInvocations:
|
|
493
|
-
warnings:
|
|
494
|
-
blocks:
|
|
495
|
-
oscillationsDetected:
|
|
496
|
-
stepBudgetBlocks:
|
|
497
|
-
noDiffWarnings:
|
|
498
|
-
noDiffBlocks:
|
|
499
|
-
repeatedErrorWarnings:
|
|
500
|
-
repeatedErrorBlocks:
|
|
504
|
+
invocations: totals.invocations,
|
|
505
|
+
postInvocations: totals.postInvocations,
|
|
506
|
+
warnings: totals.warnings,
|
|
507
|
+
blocks: totals.blocks,
|
|
508
|
+
oscillationsDetected: totals.oscillationsDetected,
|
|
509
|
+
stepBudgetBlocks: totals.stepBudgetBlocks,
|
|
510
|
+
noDiffWarnings: totals.noDiffWarnings,
|
|
511
|
+
noDiffBlocks: totals.noDiffBlocks,
|
|
512
|
+
repeatedErrorWarnings: totals.repeatedErrorWarnings,
|
|
513
|
+
repeatedErrorBlocks: totals.repeatedErrorBlocks
|
|
501
514
|
}
|
|
502
515
|
};
|
|
503
516
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/plugins",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "Official WrongStack collection of focused plugins for code quality, security, observability, planning, and agent coordination",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ECOSTACK TECHNOLOGY OÜ",
|
|
@@ -303,10 +303,10 @@
|
|
|
303
303
|
"vitest": "^5.0.0"
|
|
304
304
|
},
|
|
305
305
|
"dependencies": {
|
|
306
|
-
"@wrongstack/core": "1.0.
|
|
307
|
-
"@wrongstack/plugin-sdk": "1.0.
|
|
308
|
-
"@wrongstack/primitives": "1.0.
|
|
309
|
-
"@wrongstack/tools": "1.0.
|
|
306
|
+
"@wrongstack/core": "1.0.14",
|
|
307
|
+
"@wrongstack/plugin-sdk": "1.0.14",
|
|
308
|
+
"@wrongstack/primitives": "1.0.14",
|
|
309
|
+
"@wrongstack/tools": "1.0.14"
|
|
310
310
|
},
|
|
311
311
|
"scripts": {
|
|
312
312
|
"build": "node ../../scripts/build-package.mjs",
|