@skydiveai/pi-extensions 0.1.255-beta.6 → 0.1.255-beta.8
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/dist/index.mjs +53 -10
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -4530,6 +4530,8 @@ const diskGuardExtension = (pi) => {
|
|
|
4530
4530
|
//#endregion
|
|
4531
4531
|
//#region src/extensions/noop-loop-guard.ts
|
|
4532
4532
|
const log$8 = logger.child({ module: "noop-loop-guard" });
|
|
4533
|
+
/** Length of the rolling window that counts silence commands across streak resets. */
|
|
4534
|
+
const SILENCE_WINDOW_MS = 300 * 1e3;
|
|
4533
4535
|
/** Bare shell no-op: `true` or `:` (case-insensitive). */
|
|
4534
4536
|
const BARE_NOOP_RE = /^(true|:)\s*$/i;
|
|
4535
4537
|
/**
|
|
@@ -4618,40 +4620,81 @@ function isSilenceCommand(command) {
|
|
|
4618
4620
|
if (trimmed.length === 0) return false;
|
|
4619
4621
|
return trimmed.split(/;|&&|\n/).every((s) => segmentIsNoop(s) || isSuppressReplyCall(s));
|
|
4620
4622
|
}
|
|
4621
|
-
function noopBlockReason(streak, aborted) {
|
|
4623
|
+
function noopBlockReason({ streak, windowCount, aborted }) {
|
|
4622
4624
|
const stop = "No-op loop: a bare `true`/`:` or a pure `echo` does nothing, and `platform channel suppress-reply` only works once per response scope; repeating either just costs a model round trip. If you are holding silence (e.g. right after `platform channel suppress-reply`) or waiting on something, the right move is to END THE TURN: emitting nothing IS the silence, and it costs zero commands. If a real process needs the wait, use `bg_run` or schedule a wake (`platform automation create --at <time>`).";
|
|
4623
|
-
const
|
|
4625
|
+
const trigger = streak >= 3 ? `${streak} consecutive no-ops` : `${windowCount} no-ops within the last ${Math.round(SILENCE_WINDOW_MS / 6e4)} minutes`;
|
|
4626
|
+
let escalate;
|
|
4627
|
+
if (aborted) escalate = ` This run has now fired ${trigger}; the harness is aborting it.`;
|
|
4628
|
+
else {
|
|
4629
|
+
const remaining = Math.max(Math.min(25 - streak, 40 - windowCount), 0);
|
|
4630
|
+
escalate = ` This run has fired ${trigger}; ${remaining} more no-op${remaining === 1 ? "" : "s"} before the harness aborts the run.`;
|
|
4631
|
+
}
|
|
4624
4632
|
const bypass = ` If this specific command genuinely needs to run, append \` # ${BYPASS_HINT$1}\`.`;
|
|
4625
4633
|
return stop + escalate + bypass;
|
|
4626
4634
|
}
|
|
4627
|
-
/**
|
|
4628
|
-
|
|
4629
|
-
|
|
4635
|
+
/**
|
|
4636
|
+
* Count one bash command against the streak AND the rolling window; returns
|
|
4637
|
+
* the new streak. A bypass-marked silence command resets the streak (the
|
|
4638
|
+
* marker means "this one is intentional") but its timestamp still lands in
|
|
4639
|
+
* the window, so the marker cannot double as an unbounded loop escape
|
|
4640
|
+
* hatch. `now` is required so callsites cannot silently disagree on what
|
|
4641
|
+
* "now" means; tests pass a fixed clock to exercise window expiry.
|
|
4642
|
+
*/
|
|
4643
|
+
function recordNoopCommand(state, command, now) {
|
|
4644
|
+
if (!isSilenceCommand(command)) {
|
|
4645
|
+
state.streak = 0;
|
|
4646
|
+
return 0;
|
|
4647
|
+
}
|
|
4648
|
+
if (hasBypassMarker$1(command)) {
|
|
4630
4649
|
state.streak = 0;
|
|
4650
|
+
state.silenceTimestamps.push(now);
|
|
4631
4651
|
return 0;
|
|
4632
4652
|
}
|
|
4633
4653
|
state.streak += 1;
|
|
4654
|
+
state.silenceTimestamps.push(now);
|
|
4634
4655
|
return state.streak;
|
|
4635
4656
|
}
|
|
4657
|
+
/**
|
|
4658
|
+
* Count silence commands recorded within the last SILENCE_WINDOW_MS, pruning
|
|
4659
|
+
* older entries. Unlike the streak, this survives resets from real work.
|
|
4660
|
+
*/
|
|
4661
|
+
function countSilenceInWindow(state, now) {
|
|
4662
|
+
const cutoff = now - SILENCE_WINDOW_MS;
|
|
4663
|
+
const oldest = state.silenceTimestamps[0];
|
|
4664
|
+
if (oldest !== void 0 && oldest < cutoff) state.silenceTimestamps = state.silenceTimestamps.filter((t) => t >= cutoff);
|
|
4665
|
+
return state.silenceTimestamps.length;
|
|
4666
|
+
}
|
|
4636
4667
|
const noopLoopGuardExtension = (pi) => {
|
|
4637
|
-
const state = {
|
|
4668
|
+
const state = {
|
|
4669
|
+
streak: 0,
|
|
4670
|
+
silenceTimestamps: []
|
|
4671
|
+
};
|
|
4638
4672
|
pi.on("tool_call", async (event, ctx) => {
|
|
4639
4673
|
if (event.toolName !== "bash") return;
|
|
4640
4674
|
if (guardDisabledByEnv$1()) return;
|
|
4641
4675
|
const command = event.input.command;
|
|
4642
4676
|
if (typeof command !== "string" || command.length === 0) return;
|
|
4643
|
-
const
|
|
4644
|
-
|
|
4645
|
-
const
|
|
4677
|
+
const now = Date.now();
|
|
4678
|
+
const bypassed = isSilenceCommand(command) && hasBypassMarker$1(command);
|
|
4679
|
+
const streak = recordNoopCommand(state, command, now);
|
|
4680
|
+
const windowCount = countSilenceInWindow(state, now);
|
|
4681
|
+
if (bypassed) return;
|
|
4682
|
+
if (streak < 3 && windowCount < 8) return;
|
|
4683
|
+
const aborted = streak >= 25 || windowCount >= 40;
|
|
4646
4684
|
log$8.warn({
|
|
4647
4685
|
streak,
|
|
4686
|
+
windowCount,
|
|
4648
4687
|
aborted,
|
|
4649
4688
|
toolCallId: event.toolCallId
|
|
4650
4689
|
}, "no-op loop guard fired");
|
|
4651
4690
|
if (aborted) ctx.abort();
|
|
4652
4691
|
return {
|
|
4653
4692
|
block: true,
|
|
4654
|
-
reason: noopBlockReason(
|
|
4693
|
+
reason: noopBlockReason({
|
|
4694
|
+
streak,
|
|
4695
|
+
windowCount,
|
|
4696
|
+
aborted
|
|
4697
|
+
})
|
|
4655
4698
|
};
|
|
4656
4699
|
});
|
|
4657
4700
|
};
|