@a5c-ai/babysitter-pi 0.1.0
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 +79 -0
- package/bin/cli.cjs +78 -0
- package/bin/install.cjs +144 -0
- package/bin/uninstall.cjs +40 -0
- package/commands/babysitter-call.md +12 -0
- package/commands/babysitter-doctor.md +10 -0
- package/commands/babysitter-resume.md +16 -0
- package/commands/babysitter-status.md +15 -0
- package/extensions/babysitter/cli-wrapper.ts +95 -0
- package/extensions/babysitter/constants.ts +77 -0
- package/extensions/babysitter/custom-tools.ts +208 -0
- package/extensions/babysitter/effect-executor.ts +362 -0
- package/extensions/babysitter/guards.ts +257 -0
- package/extensions/babysitter/index.ts +554 -0
- package/extensions/babysitter/loop-driver.ts +256 -0
- package/extensions/babysitter/result-poster.ts +115 -0
- package/extensions/babysitter/sdk-bridge.ts +243 -0
- package/extensions/babysitter/session-binder.ts +284 -0
- package/extensions/babysitter/status-line.ts +54 -0
- package/extensions/babysitter/task-interceptor.ts +82 -0
- package/extensions/babysitter/todo-replacement.ts +125 -0
- package/extensions/babysitter/tool-renderer.ts +263 -0
- package/extensions/babysitter/tui-widgets.ts +164 -0
- package/extensions/babysitter/types.ts +222 -0
- package/package.json +56 -0
- package/scripts/setup.sh +74 -0
- package/scripts/sync-command-docs.cjs +115 -0
- package/skills/babysitter/SKILL.md +45 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Iteration guards and runaway-loop detection.
|
|
3
|
+
*
|
|
4
|
+
* Before every orchestration iteration the guard module is consulted.
|
|
5
|
+
* If any limit is breached the run is halted gracefully rather than
|
|
6
|
+
* allowed to spiral into the void -- which, admittedly, is where
|
|
7
|
+
* everything ends up eventually.
|
|
8
|
+
*
|
|
9
|
+
* @module guards
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { RunState } from './session-binder.js';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Guard configuration constants
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** Default maximum orchestration iterations per run. */
|
|
19
|
+
export const MAX_ITERATIONS_DEFAULT = 256;
|
|
20
|
+
|
|
21
|
+
/** Maximum wall-clock time (ms) for a single run -- 2 hours. */
|
|
22
|
+
export const MAX_RUN_DURATION_MS = 7_200_000;
|
|
23
|
+
|
|
24
|
+
/** Consecutive errors before the guard trips. */
|
|
25
|
+
export const MAX_CONSECUTIVE_ERRORS = 3;
|
|
26
|
+
|
|
27
|
+
/** Number of suspiciously fast iterations that signal a doom loop. */
|
|
28
|
+
export const DOOM_LOOP_THRESHOLD = 3;
|
|
29
|
+
|
|
30
|
+
/** Minimum duration (ms) for an iteration to be considered "real work". */
|
|
31
|
+
export const DOOM_LOOP_MIN_DURATION_MS = 2_000;
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Guard result type
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/** Result returned by {@link checkGuards}. */
|
|
38
|
+
export interface GuardResult {
|
|
39
|
+
/** Whether all guards passed. */
|
|
40
|
+
passed: boolean;
|
|
41
|
+
/** Human-readable reason when `passed` is false. */
|
|
42
|
+
reason?: string;
|
|
43
|
+
/** Suggested action when `passed` is false. */
|
|
44
|
+
action?: 'stop' | 'warn';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Internal mutable state
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/** Rolling count of consecutive iteration errors. */
|
|
52
|
+
let consecutiveErrorCount = 0;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* History of recent pending-effect counts, used for doom-loop detection.
|
|
56
|
+
* Each entry records the number of pending effects at the end of an iteration.
|
|
57
|
+
*/
|
|
58
|
+
const pendingCountHistory: number[] = [];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Recent iteration digests used for doom-loop detection (legacy compat).
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
const iterationDigests: string[] = [];
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Public API
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check all guards against the current run state.
|
|
72
|
+
*
|
|
73
|
+
* Evaluates, in order:
|
|
74
|
+
* 1. Maximum iteration count
|
|
75
|
+
* 2. Wall-clock time budget
|
|
76
|
+
* 3. Consecutive error threshold
|
|
77
|
+
* 4. Doom-loop detection
|
|
78
|
+
*
|
|
79
|
+
* Returns `{ passed: true }` when the next iteration may proceed, or
|
|
80
|
+
* `{ passed: false, reason, action }` when a limit has been breached.
|
|
81
|
+
*
|
|
82
|
+
* @param runState - The current {@link RunState} snapshot.
|
|
83
|
+
* @returns Whether the next iteration is allowed.
|
|
84
|
+
*/
|
|
85
|
+
export function checkGuards(runState: RunState): GuardResult {
|
|
86
|
+
// 1. Max iterations
|
|
87
|
+
const maxIter = runState.maxIterations ?? MAX_ITERATIONS_DEFAULT;
|
|
88
|
+
if (runState.iteration >= maxIter) {
|
|
89
|
+
return {
|
|
90
|
+
passed: false,
|
|
91
|
+
reason: `Maximum iterations reached (${runState.iteration} >= ${maxIter}). The run must stop.`,
|
|
92
|
+
action: 'stop',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 2. Wall-clock time limit
|
|
97
|
+
const elapsedMs = Date.now() - new Date(runState.startedAt).getTime();
|
|
98
|
+
if (elapsedMs >= MAX_RUN_DURATION_MS) {
|
|
99
|
+
return {
|
|
100
|
+
passed: false,
|
|
101
|
+
reason: `Maximum duration exceeded (${Math.round(elapsedMs / 1000)}s >= ${Math.round(MAX_RUN_DURATION_MS / 1000)}s).`,
|
|
102
|
+
action: 'stop',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Consecutive error threshold
|
|
107
|
+
if (consecutiveErrorCount > MAX_CONSECUTIVE_ERRORS) {
|
|
108
|
+
return {
|
|
109
|
+
passed: false,
|
|
110
|
+
reason: `Too many consecutive errors (${consecutiveErrorCount} > ${MAX_CONSECUTIVE_ERRORS}).`,
|
|
111
|
+
action: 'stop',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 4. Doom-loop detection
|
|
116
|
+
if (isDoomLoop(runState)) {
|
|
117
|
+
return {
|
|
118
|
+
passed: false,
|
|
119
|
+
reason: `Doom loop detected: the last ${DOOM_LOOP_THRESHOLD} iterations were suspiciously fast with no progress.`,
|
|
120
|
+
action: 'stop',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 4b. Legacy digest-based doom-loop detection (for callers using recordIterationDigest)
|
|
125
|
+
if (isDigestDoomLoop(DOOM_LOOP_THRESHOLD)) {
|
|
126
|
+
return {
|
|
127
|
+
passed: false,
|
|
128
|
+
reason: `Doom loop detected: the last ${DOOM_LOOP_THRESHOLD} iterations produced identical output.`,
|
|
129
|
+
action: 'warn',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { passed: true };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reset all internal guard state (counters, histories).
|
|
138
|
+
*
|
|
139
|
+
* Called on session cleanup or when starting a fresh run.
|
|
140
|
+
*/
|
|
141
|
+
export function resetGuardState(): void {
|
|
142
|
+
consecutiveErrorCount = 0;
|
|
143
|
+
pendingCountHistory.length = 0;
|
|
144
|
+
iterationDigests.length = 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Record the outcome of an iteration for consecutive-error tracking.
|
|
149
|
+
*
|
|
150
|
+
* @param success - `true` if the iteration succeeded, `false` if it errored.
|
|
151
|
+
*/
|
|
152
|
+
export function recordIterationOutcome(success: boolean): void {
|
|
153
|
+
if (success) {
|
|
154
|
+
consecutiveErrorCount = 0;
|
|
155
|
+
} else {
|
|
156
|
+
consecutiveErrorCount += 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Record the number of pending effects after an iteration, for doom-loop
|
|
162
|
+
* detection.
|
|
163
|
+
*
|
|
164
|
+
* @param pendingCount - The number of effects still pending.
|
|
165
|
+
*/
|
|
166
|
+
export function recordPendingCount(pendingCount: number): void {
|
|
167
|
+
pendingCountHistory.push(pendingCount);
|
|
168
|
+
// Keep a bounded history
|
|
169
|
+
if (pendingCountHistory.length > 32) {
|
|
170
|
+
pendingCountHistory.shift();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check whether the run is stuck in a doom loop.
|
|
176
|
+
*
|
|
177
|
+
* A doom loop is detected when the last {@link DOOM_LOOP_THRESHOLD}
|
|
178
|
+
* iterations all completed in under {@link DOOM_LOOP_MIN_DURATION_MS}
|
|
179
|
+
* each AND the pending effect count has not changed across those
|
|
180
|
+
* iterations.
|
|
181
|
+
*
|
|
182
|
+
* @param runState - The current {@link RunState} snapshot.
|
|
183
|
+
* @returns `true` if the run appears to be looping without making progress.
|
|
184
|
+
*/
|
|
185
|
+
export function isDoomLoop(runState: RunState): boolean {
|
|
186
|
+
const times = runState.iterationTimes;
|
|
187
|
+
|
|
188
|
+
// Need at least DOOM_LOOP_THRESHOLD iterations to check
|
|
189
|
+
if (!times || times.length < DOOM_LOOP_THRESHOLD) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check if the last N iteration times are all suspiciously fast
|
|
194
|
+
const recentTimes = times.slice(-DOOM_LOOP_THRESHOLD);
|
|
195
|
+
const allFast = recentTimes.every((t) => t < DOOM_LOOP_MIN_DURATION_MS);
|
|
196
|
+
if (!allFast) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check if pending effect count hasn't changed over the same window
|
|
201
|
+
if (pendingCountHistory.length < DOOM_LOOP_THRESHOLD) {
|
|
202
|
+
// Not enough pending-count data -- fall through to time-only check
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const recentPending = pendingCountHistory.slice(-DOOM_LOOP_THRESHOLD);
|
|
207
|
+
const firstPending = recentPending[0];
|
|
208
|
+
const pendingUnchanged = recentPending.every((c) => c === firstPending);
|
|
209
|
+
|
|
210
|
+
return pendingUnchanged;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Legacy compatibility exports
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Record the digest of an iteration's output for doom-loop tracking.
|
|
219
|
+
*
|
|
220
|
+
* @deprecated Prefer {@link recordPendingCount} for doom-loop detection.
|
|
221
|
+
* @param digest - A string digest (e.g. JSON.stringify of pending effects).
|
|
222
|
+
*/
|
|
223
|
+
export function recordIterationDigest(digest: string): void {
|
|
224
|
+
iterationDigests.push(digest);
|
|
225
|
+
if (iterationDigests.length > 32) {
|
|
226
|
+
iterationDigests.shift();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Reset digest history.
|
|
232
|
+
*
|
|
233
|
+
* @deprecated Use {@link resetGuardState} instead.
|
|
234
|
+
*/
|
|
235
|
+
export function resetDigests(): void {
|
|
236
|
+
resetGuardState();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Internal helpers
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Legacy digest-based doom-loop check.
|
|
245
|
+
*
|
|
246
|
+
* @param windowSize - Number of consecutive identical digests to trigger.
|
|
247
|
+
* @returns `true` if the last `windowSize` digests are all equal.
|
|
248
|
+
*/
|
|
249
|
+
function isDigestDoomLoop(windowSize: number): boolean {
|
|
250
|
+
if (iterationDigests.length < windowSize) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const tail = iterationDigests.slice(-windowSize);
|
|
255
|
+
const first = tail[0];
|
|
256
|
+
return tail.every((d) => d === first);
|
|
257
|
+
}
|