@ouro.bot/cli 0.1.0-alpha.497 → 0.1.0-alpha.498
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/changelog.json +8 -0
- package/dist/senses/inner-dialog-worker.js +66 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.498",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Heartbeat / habit recursion detection in the inner-dialog worker. The existing instinct cap (`MAX_CONSECUTIVE_INSTINCT_TURNS=3`) protects against the *internal* pending-dir self-loop (a turn writes back to its own pending dir, drains it, repeats). It does not protect against the *external* IPC self-loop where heartbeat-shaped messages get re-issued faster than their cadence — e.g. a hook misconfigured to repost on every heartbeat, a daemon retry storm, or two timers drifting into the same window.",
|
|
8
|
+
"Two new warn-level nerve events: `senses.habit_recursion_suspected` fires when two of the same habit (e.g. `heartbeat`) arrive within `HABIT_RECURSION_MIN_INTERVAL_MS` (5s) — no realistic cadence runs that fast. `senses.habit_recursion_burst` fires when `HABIT_RECURSION_BURST_THRESHOLD` (5) or more habit messages of any kind land within `HABIT_RECURSION_BURST_WINDOW_MS` (60s) — catches slower runaways that stay just under the min-interval threshold.",
|
|
9
|
+
"Detection is observation-only by design: it emits the warn signal so an operator (or a follow-up auto-recovery layer) can act on it. The message is not dropped — the signal is the value. Per-habit-name tracking, so two distinct habits firing close together don't trip the min-interval warning. `nowSource` is injectable via the `createInnerDialogWorker` factory for deterministic tests. 5 new tests cover both detectors plus the trim-window and per-habit isolation cases."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
4
12
|
{
|
|
5
13
|
"version": "0.1.0-alpha.497",
|
|
6
14
|
"changes": [
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.MAX_CONSECUTIVE_INSTINCT_TURNS = void 0;
|
|
36
|
+
exports.HABIT_RECURSION_BURST_THRESHOLD = exports.HABIT_RECURSION_BURST_WINDOW_MS = exports.HABIT_RECURSION_MIN_INTERVAL_MS = exports.MAX_CONSECUTIVE_INSTINCT_TURNS = void 0;
|
|
37
37
|
exports.createInnerDialogWorker = createInnerDialogWorker;
|
|
38
38
|
exports.startInnerDialogWorker = startInnerDialogWorker;
|
|
39
39
|
const path = __importStar(require("path"));
|
|
@@ -56,9 +56,69 @@ const habit_runtime_state_1 = require("../heart/habits/habit-runtime-state");
|
|
|
56
56
|
* batch of delegated returns) get through; a true self-loop caps fast.
|
|
57
57
|
*/
|
|
58
58
|
exports.MAX_CONSECUTIVE_INSTINCT_TURNS = 3;
|
|
59
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Habit recursion detector thresholds. The instinct cap above protects
|
|
61
|
+
* against pending-dir self-loops; this protects against the *external*
|
|
62
|
+
* IPC self-loop where heartbeat-shaped messages get re-issued faster
|
|
63
|
+
* than their cadence — e.g. a hook misconfigured to repost on every
|
|
64
|
+
* heartbeat, a daemon retry storm, or a stuck timer firing back-to-back.
|
|
65
|
+
*
|
|
66
|
+
* MIN_INTERVAL_MS — two of the same habit within this window is suspect
|
|
67
|
+
* regardless of cadence (no realistic habit fires every few seconds).
|
|
68
|
+
* BURST_THRESHOLD over BURST_WINDOW_MS catches slower runaways that stay
|
|
69
|
+
* just under MIN_INTERVAL_MS.
|
|
70
|
+
*
|
|
71
|
+
* Detection is observation-only: it emits warn-level nerves events, it
|
|
72
|
+
* does not drop the message. An operator (or follow-up auto-recovery)
|
|
73
|
+
* decides what to do with the signal.
|
|
74
|
+
*/
|
|
75
|
+
exports.HABIT_RECURSION_MIN_INTERVAL_MS = 5_000;
|
|
76
|
+
exports.HABIT_RECURSION_BURST_WINDOW_MS = 60_000;
|
|
77
|
+
exports.HABIT_RECURSION_BURST_THRESHOLD = 5;
|
|
78
|
+
function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runInnerDialogTurn)(options), hasPendingWork = () => (0, pending_1.hasPendingMessages)((0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)())), nowSource = () => Date.now()) {
|
|
60
79
|
let running = false;
|
|
61
80
|
const queue = [];
|
|
81
|
+
const lastFireByHabit = new Map();
|
|
82
|
+
const recentHabitFires = [];
|
|
83
|
+
function recordHabitFireForRecursion(habitName) {
|
|
84
|
+
const now = nowSource();
|
|
85
|
+
const previous = lastFireByHabit.get(habitName);
|
|
86
|
+
if (previous !== undefined) {
|
|
87
|
+
const intervalMs = now - previous;
|
|
88
|
+
if (intervalMs < exports.HABIT_RECURSION_MIN_INTERVAL_MS) {
|
|
89
|
+
(0, runtime_1.emitNervesEvent)({
|
|
90
|
+
level: "warn",
|
|
91
|
+
component: "senses",
|
|
92
|
+
event: "senses.habit_recursion_suspected",
|
|
93
|
+
message: "habit fired suspiciously fast after the previous fire — possible self-recursion or duplicate dispatch",
|
|
94
|
+
meta: {
|
|
95
|
+
habitName,
|
|
96
|
+
intervalMs,
|
|
97
|
+
thresholdMs: exports.HABIT_RECURSION_MIN_INTERVAL_MS,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
lastFireByHabit.set(habitName, now);
|
|
103
|
+
recentHabitFires.push(now);
|
|
104
|
+
while (recentHabitFires.length > 0 && now - recentHabitFires[0] > exports.HABIT_RECURSION_BURST_WINDOW_MS) {
|
|
105
|
+
recentHabitFires.shift();
|
|
106
|
+
}
|
|
107
|
+
if (recentHabitFires.length >= exports.HABIT_RECURSION_BURST_THRESHOLD) {
|
|
108
|
+
(0, runtime_1.emitNervesEvent)({
|
|
109
|
+
level: "warn",
|
|
110
|
+
component: "senses",
|
|
111
|
+
event: "senses.habit_recursion_burst",
|
|
112
|
+
message: "habit messages arriving in a burst — possible runaway loop",
|
|
113
|
+
meta: {
|
|
114
|
+
count: recentHabitFires.length,
|
|
115
|
+
windowMs: exports.HABIT_RECURSION_BURST_WINDOW_MS,
|
|
116
|
+
thresholdCount: exports.HABIT_RECURSION_BURST_THRESHOLD,
|
|
117
|
+
lastHabitName: habitName,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
62
122
|
async function run(reason, taskId, habitName) {
|
|
63
123
|
if (running) {
|
|
64
124
|
queue.push({ reason, taskId, habitName });
|
|
@@ -145,11 +205,15 @@ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runIn
|
|
|
145
205
|
return;
|
|
146
206
|
const maybeMessage = message;
|
|
147
207
|
if (maybeMessage.type === "habit") {
|
|
208
|
+
/* v8 ignore next -- defensive fallback: live habit dispatch always sets habitName @preserve */
|
|
209
|
+
const habitName = maybeMessage.habitName ?? "(unnamed)";
|
|
210
|
+
recordHabitFireForRecursion(habitName);
|
|
148
211
|
await run("habit", undefined, maybeMessage.habitName);
|
|
149
212
|
return;
|
|
150
213
|
}
|
|
151
214
|
if (maybeMessage.type === "heartbeat") {
|
|
152
215
|
// Backward compatibility: heartbeat -> habit/heartbeat
|
|
216
|
+
recordHabitFireForRecursion("heartbeat");
|
|
153
217
|
await run("habit", undefined, "heartbeat");
|
|
154
218
|
return;
|
|
155
219
|
}
|