@virtengine/openfleet 0.25.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/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* error-detector.mjs — Detects common agent failure patterns and provides recovery actions.
|
|
3
|
+
*
|
|
4
|
+
* Classifies errors into: plan_stuck, rate_limit, token_overflow, api_error,
|
|
5
|
+
* session_expired, build_failure, git_conflict, unknown.
|
|
6
|
+
* Returns recommended recovery actions so the orchestrator can respond automatically.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
const TAG = "[error-detector]";
|
|
12
|
+
|
|
13
|
+
// ── Detection patterns ──────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export const PLAN_STUCK_PATTERNS = [
|
|
16
|
+
/ready to (start|begin|implement)/i,
|
|
17
|
+
/would you like me to (proceed|start|implement|continue)/i,
|
|
18
|
+
/shall i (start|begin|implement|proceed)/i,
|
|
19
|
+
/here'?s the plan/i,
|
|
20
|
+
/created plan at/i,
|
|
21
|
+
/plan\.md$/m,
|
|
22
|
+
/I'?ve (?:created|outlined|prepared) a plan/i,
|
|
23
|
+
/Let me know (?:if|when|how) you'?d like/i,
|
|
24
|
+
/awaiting (?:your|further) (?:input|instructions|confirmation)/i,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const RATE_LIMIT_PATTERNS = [
|
|
28
|
+
/429|rate.?limit|too many requests/i,
|
|
29
|
+
/quota exceeded|billing.*limit/i,
|
|
30
|
+
/tokens per minute|TPM.*limit/i,
|
|
31
|
+
/resource exhausted|capacity/i,
|
|
32
|
+
/please try again later/i,
|
|
33
|
+
/Request too large|max.*tokens/i,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const TOKEN_OVERFLOW_PATTERNS = [
|
|
37
|
+
/context.*(too long|exceeded|overflow|maximum)/i,
|
|
38
|
+
/max.*(context|token|length).*exceeded/i,
|
|
39
|
+
/conversation.*too.*long/i,
|
|
40
|
+
/input.*too.*large/i,
|
|
41
|
+
/reduce.*(context|input|message)/i,
|
|
42
|
+
/maximum.*context.*length/i,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const API_ERROR_PATTERNS = [
|
|
46
|
+
/ECONNREFUSED|ETIMEDOUT|ENOTFOUND/i,
|
|
47
|
+
/500 Internal Server Error/i,
|
|
48
|
+
/502 Bad Gateway|503 Service Unavailable/i,
|
|
49
|
+
/network.*(error|failure|unreachable)/i,
|
|
50
|
+
/fetch failed|request failed/i,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const SESSION_EXPIRED_PATTERNS = [
|
|
54
|
+
/session.*expired|invalid.*session/i,
|
|
55
|
+
/thread.*not.*found|conversation.*not.*found/i,
|
|
56
|
+
/authentication.*failed|unauthorized/i,
|
|
57
|
+
/token.*expired|invalid.*token/i,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const BUILD_FAILURE_PATTERNS = [
|
|
61
|
+
/go build.*failed|compilation error/i,
|
|
62
|
+
/FAIL\s+\S+/m,
|
|
63
|
+
/golangci-lint.*error/i,
|
|
64
|
+
/pre-push hook.*failed/i,
|
|
65
|
+
/npm ERR|pnpm.*error/i,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const GIT_CONFLICT_PATTERNS = [
|
|
69
|
+
/merge conflict|CONFLICT.*Merge/i,
|
|
70
|
+
/rebase.*conflict/i,
|
|
71
|
+
/cannot.*merge|unable to merge/i,
|
|
72
|
+
/both modified/i,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Ordered list of pattern groups to check. Earlier entries win on ties.
|
|
77
|
+
* Each entry: [patternName, regexArray, baseConfidence]
|
|
78
|
+
*/
|
|
79
|
+
const PATTERN_GROUPS = [
|
|
80
|
+
["plan_stuck", PLAN_STUCK_PATTERNS, 0.85],
|
|
81
|
+
["rate_limit", RATE_LIMIT_PATTERNS, 0.95],
|
|
82
|
+
["token_overflow", TOKEN_OVERFLOW_PATTERNS, 0.9],
|
|
83
|
+
["api_error", API_ERROR_PATTERNS, 0.9],
|
|
84
|
+
["session_expired", SESSION_EXPIRED_PATTERNS, 0.9],
|
|
85
|
+
["build_failure", BUILD_FAILURE_PATTERNS, 0.8],
|
|
86
|
+
["git_conflict", GIT_CONFLICT_PATTERNS, 0.85],
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/** Safely truncate a string for logging / details. */
|
|
92
|
+
function truncate(str, max = 200) {
|
|
93
|
+
if (!str) return "";
|
|
94
|
+
return str.length > max ? str.slice(0, max) + "…" : str;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Return the first regex match from `patterns` against `text`, or null. */
|
|
98
|
+
function firstMatch(text, patterns) {
|
|
99
|
+
if (!text) return null;
|
|
100
|
+
for (const rx of patterns) {
|
|
101
|
+
const m = rx.exec(text);
|
|
102
|
+
if (m) return m;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Description strings for each pattern type. */
|
|
108
|
+
const PATTERN_DESCRIPTIONS = {
|
|
109
|
+
plan_stuck: "Agent created a plan but did not implement it",
|
|
110
|
+
rate_limit: "API rate limit or quota exceeded",
|
|
111
|
+
token_overflow: "Context or token limit exceeded",
|
|
112
|
+
api_error: "API connection or server error",
|
|
113
|
+
session_expired: "Agent session or thread expired",
|
|
114
|
+
build_failure: "Build, test, or lint failure",
|
|
115
|
+
git_conflict: "Git merge conflict detected",
|
|
116
|
+
unknown: "Unclassified error",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// ── ErrorDetector ───────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
export class ErrorDetector {
|
|
122
|
+
/**
|
|
123
|
+
* @param {object} [options]
|
|
124
|
+
* @param {number} [options.maxConsecutiveErrors=5]
|
|
125
|
+
* @param {number} [options.cooldownMs=300000] 5 min default
|
|
126
|
+
* @param {number} [options.rateLimitCooldownMs=60000] 1 min default
|
|
127
|
+
* @param {Function} [options.onErrorDetected]
|
|
128
|
+
* @param {Function} [options.sendTelegram]
|
|
129
|
+
*/
|
|
130
|
+
constructor(options = {}) {
|
|
131
|
+
this.maxConsecutiveErrors = options.maxConsecutiveErrors ?? 5;
|
|
132
|
+
this.cooldownMs = options.cooldownMs ?? 5 * 60 * 1000;
|
|
133
|
+
this.rateLimitCooldownMs = options.rateLimitCooldownMs ?? 60 * 1000;
|
|
134
|
+
this.onErrorDetected = options.onErrorDetected ?? null;
|
|
135
|
+
this.sendTelegram = options.sendTelegram ?? null;
|
|
136
|
+
|
|
137
|
+
/** @type {Map<string, { errors: Array<{pattern:string, timestamp:number, details:string}>, consecutiveErrors: number, lastErrorAt: number }>} */
|
|
138
|
+
this._tasks = new Map();
|
|
139
|
+
|
|
140
|
+
/** Global stats */
|
|
141
|
+
this._global = {
|
|
142
|
+
rateLimitHits: [], // timestamps
|
|
143
|
+
totalErrors: 0,
|
|
144
|
+
totalRecoveries: 0,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── classify ────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Analyse agent output (and optional stderr) to classify the failure.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} output Agent stdout / response text
|
|
154
|
+
* @param {string} [error] Agent stderr or error message
|
|
155
|
+
* @returns {{ pattern: string, confidence: number, details: string, rawMatch: string|null }}
|
|
156
|
+
*/
|
|
157
|
+
classify(output, error) {
|
|
158
|
+
const combined = [output, error].filter(Boolean).join("\n");
|
|
159
|
+
if (!combined) {
|
|
160
|
+
return {
|
|
161
|
+
pattern: "unknown",
|
|
162
|
+
confidence: 0,
|
|
163
|
+
details: "No output to analyse",
|
|
164
|
+
rawMatch: null,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let best = null;
|
|
169
|
+
|
|
170
|
+
for (const [pattern, regexes, baseConfidence] of PATTERN_GROUPS) {
|
|
171
|
+
const m = firstMatch(combined, regexes);
|
|
172
|
+
if (m) {
|
|
173
|
+
// Boost confidence when multiple patterns in the same group match.
|
|
174
|
+
let hits = 0;
|
|
175
|
+
for (const rx of regexes) {
|
|
176
|
+
if (rx.test(combined)) hits++;
|
|
177
|
+
}
|
|
178
|
+
const confidence = Math.min(1, baseConfidence + (hits - 1) * 0.05);
|
|
179
|
+
|
|
180
|
+
if (!best || confidence > best.confidence) {
|
|
181
|
+
best = {
|
|
182
|
+
pattern,
|
|
183
|
+
confidence,
|
|
184
|
+
details: PATTERN_DESCRIPTIONS[pattern],
|
|
185
|
+
rawMatch: truncate(m[0]),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
best || {
|
|
193
|
+
pattern: "unknown",
|
|
194
|
+
confidence: 0.3,
|
|
195
|
+
details: PATTERN_DESCRIPTIONS.unknown,
|
|
196
|
+
rawMatch: null,
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── recordError ─────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Record an error for a task and return the recommended recovery action.
|
|
205
|
+
*
|
|
206
|
+
* @param {string} taskId
|
|
207
|
+
* @param {{ pattern: string, confidence: number, details: string, rawMatch: string|null }} classification
|
|
208
|
+
* @returns {{ action: string, prompt?: string, cooldownMs?: number, reason: string, errorCount: number }}
|
|
209
|
+
*/
|
|
210
|
+
recordError(taskId, classification) {
|
|
211
|
+
if (!taskId || !classification) {
|
|
212
|
+
return {
|
|
213
|
+
action: "manual",
|
|
214
|
+
reason: "Missing taskId or classification",
|
|
215
|
+
errorCount: 0,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Ensure task record exists.
|
|
220
|
+
if (!this._tasks.has(taskId)) {
|
|
221
|
+
this._tasks.set(taskId, {
|
|
222
|
+
errors: [],
|
|
223
|
+
consecutiveErrors: 0,
|
|
224
|
+
lastErrorAt: 0,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const rec = this._tasks.get(taskId);
|
|
228
|
+
const now = Date.now();
|
|
229
|
+
|
|
230
|
+
rec.errors.push({
|
|
231
|
+
pattern: classification.pattern,
|
|
232
|
+
timestamp: now,
|
|
233
|
+
details: classification.details,
|
|
234
|
+
});
|
|
235
|
+
rec.consecutiveErrors += 1;
|
|
236
|
+
rec.lastErrorAt = now;
|
|
237
|
+
this._global.totalErrors += 1;
|
|
238
|
+
|
|
239
|
+
// Track global rate-limit hits.
|
|
240
|
+
if (classification.pattern === "rate_limit") {
|
|
241
|
+
this._global.rateLimitHits.push(now);
|
|
242
|
+
// Prune old entries (> 5 minutes).
|
|
243
|
+
const cutoff = now - this.cooldownMs;
|
|
244
|
+
this._global.rateLimitHits = this._global.rateLimitHits.filter(
|
|
245
|
+
(t) => t > cutoff,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Fire callback.
|
|
250
|
+
if (typeof this.onErrorDetected === "function") {
|
|
251
|
+
try {
|
|
252
|
+
this.onErrorDetected({
|
|
253
|
+
taskId,
|
|
254
|
+
classification,
|
|
255
|
+
errorCount: rec.consecutiveErrors,
|
|
256
|
+
});
|
|
257
|
+
} catch {
|
|
258
|
+
/* swallow */
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Determine recovery action.
|
|
263
|
+
const errorCount = rec.consecutiveErrors;
|
|
264
|
+
|
|
265
|
+
// Block after too many consecutive errors.
|
|
266
|
+
if (errorCount >= this.maxConsecutiveErrors) {
|
|
267
|
+
const reason = `Task has ${errorCount} consecutive errors (max ${this.maxConsecutiveErrors}) — blocking`;
|
|
268
|
+
this._notifyTelegram(`🛑 Task ${taskId} blocked: ${reason}`);
|
|
269
|
+
return { action: "block", reason, errorCount };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
switch (classification.pattern) {
|
|
273
|
+
case "plan_stuck":
|
|
274
|
+
return {
|
|
275
|
+
action: "retry_with_prompt",
|
|
276
|
+
prompt: this.getPlanStuckRecoveryPrompt("(unknown)", ""),
|
|
277
|
+
reason:
|
|
278
|
+
"Agent stuck in planning mode — sending implementation prompt",
|
|
279
|
+
errorCount,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
case "rate_limit":
|
|
283
|
+
if (this.shouldPauseExecutor()) {
|
|
284
|
+
return {
|
|
285
|
+
action: "pause_executor",
|
|
286
|
+
cooldownMs: this.cooldownMs,
|
|
287
|
+
reason: `>${this._rateLimitThreshold()} rate limits in 5 min window — pausing executor`,
|
|
288
|
+
errorCount,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
action: "cooldown",
|
|
293
|
+
cooldownMs: this.rateLimitCooldownMs,
|
|
294
|
+
reason: "Rate limited — cooling down before retry",
|
|
295
|
+
errorCount,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
case "token_overflow":
|
|
299
|
+
return {
|
|
300
|
+
action: "new_session",
|
|
301
|
+
prompt: this.getTokenOverflowRecoveryPrompt("(unknown)"),
|
|
302
|
+
reason:
|
|
303
|
+
"Token/context overflow — starting fresh session on same worktree",
|
|
304
|
+
errorCount,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
case "api_error":
|
|
308
|
+
if (errorCount >= 3) {
|
|
309
|
+
return {
|
|
310
|
+
action: "block",
|
|
311
|
+
reason: "API errors persist after 3 retries — blocking",
|
|
312
|
+
errorCount,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
action: "cooldown",
|
|
317
|
+
cooldownMs: this.rateLimitCooldownMs,
|
|
318
|
+
reason: `API error (attempt ${errorCount}/3) — retry after cooldown`,
|
|
319
|
+
errorCount,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
case "session_expired":
|
|
323
|
+
return {
|
|
324
|
+
action: "new_session",
|
|
325
|
+
reason: "Session/thread expired — creating new session",
|
|
326
|
+
errorCount,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
case "build_failure":
|
|
330
|
+
if (errorCount >= 3) {
|
|
331
|
+
return {
|
|
332
|
+
action: "manual",
|
|
333
|
+
reason:
|
|
334
|
+
"Build failures persist after 3 retries — needs manual review",
|
|
335
|
+
errorCount,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
action: "retry_with_prompt",
|
|
340
|
+
prompt:
|
|
341
|
+
"The previous build/test/lint step failed. Carefully read the error output, fix the root cause, and try again. Do NOT skip tests.",
|
|
342
|
+
reason: `Build failure (attempt ${errorCount}/3) — retry with fix prompt`,
|
|
343
|
+
errorCount,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
case "git_conflict":
|
|
347
|
+
if (errorCount >= 2) {
|
|
348
|
+
return {
|
|
349
|
+
action: "manual",
|
|
350
|
+
reason:
|
|
351
|
+
"Git conflicts persist after 2 retries — needs manual resolution",
|
|
352
|
+
errorCount,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
action: "retry_with_prompt",
|
|
357
|
+
prompt:
|
|
358
|
+
"There are git merge conflicts. Run `git status` to find conflicting files, resolve each conflict by choosing the correct code, then `git add` and `git commit`. Do NOT leave conflict markers in the code.",
|
|
359
|
+
reason: "Git conflict detected — retry with resolution prompt",
|
|
360
|
+
errorCount,
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
default:
|
|
364
|
+
if (errorCount >= 3) {
|
|
365
|
+
return {
|
|
366
|
+
action: "manual",
|
|
367
|
+
reason: `Unknown error repeated ${errorCount} times — needs manual review`,
|
|
368
|
+
errorCount,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
action: "cooldown",
|
|
373
|
+
cooldownMs: this.rateLimitCooldownMs,
|
|
374
|
+
reason: "Unknown error — retry after cooldown",
|
|
375
|
+
errorCount,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Recovery prompts ────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get recovery prompt for plan-stuck errors.
|
|
384
|
+
*
|
|
385
|
+
* @param {string} taskTitle
|
|
386
|
+
* @param {string} lastOutput
|
|
387
|
+
* @returns {string}
|
|
388
|
+
*/
|
|
389
|
+
getPlanStuckRecoveryPrompt(taskTitle, lastOutput) {
|
|
390
|
+
const outputSummary = (lastOutput || "").slice(-1500);
|
|
391
|
+
return `You previously created a plan for task "${taskTitle}" but did not implement it.
|
|
392
|
+
|
|
393
|
+
CRITICAL: Do NOT create another plan. Do NOT ask for permission. Implement the changes NOW.
|
|
394
|
+
|
|
395
|
+
Your previous output ended with planning but no code changes were made. This is a Vibe-Kanban autonomous task — you must implement, test, commit, and push without any human interaction.
|
|
396
|
+
|
|
397
|
+
Previous output summary:
|
|
398
|
+
${outputSummary}
|
|
399
|
+
|
|
400
|
+
IMPLEMENT NOW. Start by making the actual code changes, then test, commit, and push.`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get recovery prompt for token overflow.
|
|
405
|
+
*
|
|
406
|
+
* @param {string} taskTitle
|
|
407
|
+
* @returns {string}
|
|
408
|
+
*/
|
|
409
|
+
getTokenOverflowRecoveryPrompt(taskTitle) {
|
|
410
|
+
return `Continue working on task "${taskTitle}". Your previous session exceeded context limits.
|
|
411
|
+
|
|
412
|
+
This is a fresh session on the same worktree. Check what was already done:
|
|
413
|
+
1. Run \`git log --oneline -10\` to see recent commits
|
|
414
|
+
2. Run \`git diff --stat\` to see uncommitted changes
|
|
415
|
+
3. Review the task requirements and continue from where the previous session left off
|
|
416
|
+
|
|
417
|
+
Do NOT restart from scratch — build on existing progress.`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Executor kill-switch ────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Returns true if the executor should pause due to excessive rate limiting.
|
|
424
|
+
* Triggers when >3 rate-limit errors hit within the cooldown window (5 min).
|
|
425
|
+
*
|
|
426
|
+
* @returns {boolean}
|
|
427
|
+
*/
|
|
428
|
+
shouldPauseExecutor() {
|
|
429
|
+
const now = Date.now();
|
|
430
|
+
const cutoff = now - this.cooldownMs;
|
|
431
|
+
this._global.rateLimitHits = this._global.rateLimitHits.filter(
|
|
432
|
+
(t) => t > cutoff,
|
|
433
|
+
);
|
|
434
|
+
return this._global.rateLimitHits.length > this._rateLimitThreshold();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** @private */
|
|
438
|
+
_rateLimitThreshold() {
|
|
439
|
+
return 3;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── Task lifecycle ──────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Reset error tracking for a task (call on success).
|
|
446
|
+
*
|
|
447
|
+
* @param {string} taskId
|
|
448
|
+
*/
|
|
449
|
+
resetTask(taskId) {
|
|
450
|
+
if (this._tasks.has(taskId)) {
|
|
451
|
+
this._global.totalRecoveries += this._tasks.get(taskId).consecutiveErrors;
|
|
452
|
+
this._tasks.delete(taskId);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ── Stats ───────────────────────────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Get error statistics.
|
|
460
|
+
*
|
|
461
|
+
* @returns {{ totalErrors: number, totalRecoveries: number, activeTaskErrors: number, rateLimitHitsLast5m: number, taskBreakdown: Record<string, number> }}
|
|
462
|
+
*/
|
|
463
|
+
getStats() {
|
|
464
|
+
const now = Date.now();
|
|
465
|
+
const cutoff = now - this.cooldownMs;
|
|
466
|
+
const rateLimitHitsLast5m = this._global.rateLimitHits.filter(
|
|
467
|
+
(t) => t > cutoff,
|
|
468
|
+
).length;
|
|
469
|
+
|
|
470
|
+
const taskBreakdown = {};
|
|
471
|
+
for (const [taskId, rec] of this._tasks) {
|
|
472
|
+
taskBreakdown[taskId] = rec.consecutiveErrors;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
totalErrors: this._global.totalErrors,
|
|
477
|
+
totalRecoveries: this._global.totalRecoveries,
|
|
478
|
+
activeTaskErrors: this._tasks.size,
|
|
479
|
+
rateLimitHitsLast5m,
|
|
480
|
+
taskBreakdown,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ── Session-Aware Analysis ────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Analyze a sequence of session messages (from SessionTracker) to detect
|
|
488
|
+
* behavioral patterns that single-event classification would miss.
|
|
489
|
+
*
|
|
490
|
+
* Detects:
|
|
491
|
+
* - tool_loop: Same tools repeated 5+ times without progress
|
|
492
|
+
* - analysis_paralysis: Only reading files, never editing (after 10+ tool calls)
|
|
493
|
+
* - plan_stuck: Agent wrote a plan but stopped (plan keywords + no edits)
|
|
494
|
+
* - needs_clarification: Agent explicitly says it needs input/clarification
|
|
495
|
+
* - false_completion: Agent claims done but there are no commits
|
|
496
|
+
* - rate_limited: Multiple rate limit errors in sequence
|
|
497
|
+
*
|
|
498
|
+
* @param {Array<{type: string, content: string, meta?: {toolName?: string}}>} messages
|
|
499
|
+
* @returns {{ patterns: string[], primary: string|null, details: Record<string, string> }}
|
|
500
|
+
*/
|
|
501
|
+
analyzeMessageSequence(messages) {
|
|
502
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
503
|
+
return { patterns: [], primary: null, details: {} };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const patterns = [];
|
|
507
|
+
const details = {};
|
|
508
|
+
|
|
509
|
+
// ── Tool loop detection ──
|
|
510
|
+
const toolCalls = messages.filter((m) => m.type === "tool_call");
|
|
511
|
+
if (toolCalls.length >= 5) {
|
|
512
|
+
const toolNames = toolCalls.map((m) => m.meta?.toolName || "unknown");
|
|
513
|
+
const lastFive = toolNames.slice(-5);
|
|
514
|
+
const uniqueInLastFive = new Set(lastFive).size;
|
|
515
|
+
if (uniqueInLastFive <= 2) {
|
|
516
|
+
patterns.push("tool_loop");
|
|
517
|
+
details.tool_loop = `Repeated tools: ${[...new Set(lastFive)].join(", ")} (${lastFive.length}x in last 5)`;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ── Analysis paralysis ──
|
|
522
|
+
if (toolCalls.length >= 10) {
|
|
523
|
+
const readTools = toolCalls.filter((m) => {
|
|
524
|
+
const name = (m.meta?.toolName || m.content || "").toLowerCase();
|
|
525
|
+
return (
|
|
526
|
+
name.includes("read") ||
|
|
527
|
+
name.includes("search") ||
|
|
528
|
+
name.includes("grep") ||
|
|
529
|
+
name.includes("list") ||
|
|
530
|
+
name.includes("find") ||
|
|
531
|
+
name.includes("cat")
|
|
532
|
+
);
|
|
533
|
+
});
|
|
534
|
+
const editTools = toolCalls.filter((m) => {
|
|
535
|
+
const name = (m.meta?.toolName || m.content || "").toLowerCase();
|
|
536
|
+
return (
|
|
537
|
+
name.includes("write") ||
|
|
538
|
+
name.includes("edit") ||
|
|
539
|
+
name.includes("create") ||
|
|
540
|
+
name.includes("replace") ||
|
|
541
|
+
name.includes("patch") ||
|
|
542
|
+
name.includes("append")
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
if (readTools.length >= 8 && editTools.length === 0) {
|
|
547
|
+
patterns.push("analysis_paralysis");
|
|
548
|
+
details.analysis_paralysis = `${readTools.length} read ops, 0 write ops in ${toolCalls.length} tool calls`;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ── Plan stuck ──
|
|
553
|
+
const agentMessages = messages.filter((m) => m.type === "agent_message");
|
|
554
|
+
const allAgentText = agentMessages
|
|
555
|
+
.map((m) => m.content)
|
|
556
|
+
.join(" ")
|
|
557
|
+
.toLowerCase();
|
|
558
|
+
const planPhrases = [
|
|
559
|
+
"here's the plan",
|
|
560
|
+
"here is my plan",
|
|
561
|
+
"i'll create a plan",
|
|
562
|
+
"plan.md",
|
|
563
|
+
"ready to start implementing",
|
|
564
|
+
"ready to begin",
|
|
565
|
+
"would you like me to proceed",
|
|
566
|
+
"shall i start",
|
|
567
|
+
"would you like me to implement",
|
|
568
|
+
];
|
|
569
|
+
const hasPlanPhrase = planPhrases.some((p) => allAgentText.includes(p));
|
|
570
|
+
const editToolCalls = toolCalls.filter((m) => {
|
|
571
|
+
const name = (m.meta?.toolName || m.content || "").toLowerCase();
|
|
572
|
+
return (
|
|
573
|
+
name.includes("write") ||
|
|
574
|
+
name.includes("edit") ||
|
|
575
|
+
name.includes("create") ||
|
|
576
|
+
name.includes("replace")
|
|
577
|
+
);
|
|
578
|
+
});
|
|
579
|
+
if (hasPlanPhrase && editToolCalls.length <= 1) {
|
|
580
|
+
patterns.push("plan_stuck");
|
|
581
|
+
details.plan_stuck = "Agent created a plan but did not implement it";
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ── Needs clarification ──
|
|
585
|
+
const clarificationPhrases = [
|
|
586
|
+
"need clarification",
|
|
587
|
+
"need more information",
|
|
588
|
+
"could you clarify",
|
|
589
|
+
"unclear",
|
|
590
|
+
"ambiguous",
|
|
591
|
+
"which approach",
|
|
592
|
+
"please specify",
|
|
593
|
+
"i need to know",
|
|
594
|
+
"can you provide",
|
|
595
|
+
"what should i",
|
|
596
|
+
];
|
|
597
|
+
if (clarificationPhrases.some((p) => allAgentText.includes(p))) {
|
|
598
|
+
patterns.push("needs_clarification");
|
|
599
|
+
details.needs_clarification =
|
|
600
|
+
"Agent expressed uncertainty or asked for input";
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ── False completion ──
|
|
604
|
+
const completionPhrases = [
|
|
605
|
+
"task complete",
|
|
606
|
+
"task is complete",
|
|
607
|
+
"i've completed",
|
|
608
|
+
"all done",
|
|
609
|
+
"successfully completed",
|
|
610
|
+
"changes have been committed",
|
|
611
|
+
"pushed to",
|
|
612
|
+
"pr created",
|
|
613
|
+
"pull request created",
|
|
614
|
+
];
|
|
615
|
+
const claimsDone = completionPhrases.some((p) => allAgentText.includes(p));
|
|
616
|
+
const hasGitCommit = toolCalls.some((m) => {
|
|
617
|
+
const content = (m.content || "").toLowerCase();
|
|
618
|
+
return content.includes("git commit") || content.includes("git push");
|
|
619
|
+
});
|
|
620
|
+
if (claimsDone && !hasGitCommit) {
|
|
621
|
+
patterns.push("false_completion");
|
|
622
|
+
details.false_completion =
|
|
623
|
+
"Agent claims completion but no git commit/push detected in tool calls";
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ── Rate limited ──
|
|
627
|
+
const errors = messages.filter((m) => m.type === "error");
|
|
628
|
+
const rateLimitErrors = errors.filter((m) =>
|
|
629
|
+
/rate.?limit|429|too many requests|quota/i.test(m.content || ""),
|
|
630
|
+
);
|
|
631
|
+
if (rateLimitErrors.length >= 2) {
|
|
632
|
+
patterns.push("rate_limited");
|
|
633
|
+
details.rate_limited = `${rateLimitErrors.length} rate limit errors detected`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Determine primary pattern (most actionable)
|
|
637
|
+
const priority = [
|
|
638
|
+
"rate_limited",
|
|
639
|
+
"plan_stuck",
|
|
640
|
+
"false_completion",
|
|
641
|
+
"needs_clarification",
|
|
642
|
+
"tool_loop",
|
|
643
|
+
"analysis_paralysis",
|
|
644
|
+
];
|
|
645
|
+
const primary = priority.find((p) => patterns.includes(p)) || null;
|
|
646
|
+
|
|
647
|
+
return { patterns, primary, details };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Analyze agent log files for historical error patterns.
|
|
652
|
+
* Reads log files from the agent logs directory and returns frequency data.
|
|
653
|
+
*
|
|
654
|
+
* @param {string} logsDir - Path to the agent logs directory
|
|
655
|
+
* @returns {{ patterns: Record<string, number>, recommendations: string[] }}
|
|
656
|
+
*/
|
|
657
|
+
analyzeHistoricalErrors(logsDir) {
|
|
658
|
+
const patterns = {};
|
|
659
|
+
const recommendations = [];
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
const files = readdirSync(logsDir).filter((f) => f.endsWith(".log"));
|
|
663
|
+
|
|
664
|
+
for (const file of files.slice(-20)) {
|
|
665
|
+
// Only last 20 logs
|
|
666
|
+
try {
|
|
667
|
+
const content = readFileSync(`${logsDir}/${file}`, "utf8");
|
|
668
|
+
const classification = this.classify(content);
|
|
669
|
+
const pattern = classification.pattern;
|
|
670
|
+
patterns[pattern] = (patterns[pattern] || 0) + 1;
|
|
671
|
+
} catch {
|
|
672
|
+
/* skip unreadable files */
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Generate recommendations
|
|
677
|
+
if ((patterns.rate_limit || 0) > 3) {
|
|
678
|
+
recommendations.push(
|
|
679
|
+
"Frequent rate limiting — consider reducing parallelism or adding delays",
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
if ((patterns.plan_stuck || 0) > 3) {
|
|
683
|
+
recommendations.push(
|
|
684
|
+
"Agents frequently get stuck in planning mode — ensure instructions explicitly say 'implement immediately'",
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
if ((patterns.token_overflow || 0) > 2) {
|
|
688
|
+
recommendations.push(
|
|
689
|
+
"Token overflow occurring — consider splitting large tasks or using summarization",
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
} catch {
|
|
693
|
+
/* logsDir might not exist */
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return { patterns, recommendations };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Generate a recovery prompt based on session analysis results.
|
|
701
|
+
* Used by task-executor when a behavioral pattern is detected mid-session.
|
|
702
|
+
*
|
|
703
|
+
* @param {string} taskTitle
|
|
704
|
+
* @param {{ primary: string|null, details: Record<string, string> }} analysis
|
|
705
|
+
* @param {string} [lastOutput] - Last agent output for additional context
|
|
706
|
+
* @returns {string}
|
|
707
|
+
*/
|
|
708
|
+
getRecoveryPromptForAnalysis(taskTitle, analysis, lastOutput = "") {
|
|
709
|
+
if (!analysis?.primary) {
|
|
710
|
+
return `Continue working on task "${taskTitle}". Focus on implementation.`;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
switch (analysis.primary) {
|
|
714
|
+
case "plan_stuck":
|
|
715
|
+
return [
|
|
716
|
+
`# CONTINUE IMPLEMENTATION — Do Not Plan`,
|
|
717
|
+
``,
|
|
718
|
+
`You wrote a plan for "${taskTitle}" but stopped before implementing it.`,
|
|
719
|
+
``,
|
|
720
|
+
`DO NOT create another plan. DO NOT ask for permission.`,
|
|
721
|
+
`Implement the changes NOW:`,
|
|
722
|
+
`1. Edit the necessary files`,
|
|
723
|
+
`2. Run tests to verify`,
|
|
724
|
+
`3. Commit with conventional commit message`,
|
|
725
|
+
`4. Push to the branch`,
|
|
726
|
+
``,
|
|
727
|
+
`This is autonomous execution — implement immediately.`,
|
|
728
|
+
].join("\n");
|
|
729
|
+
|
|
730
|
+
case "tool_loop":
|
|
731
|
+
return [
|
|
732
|
+
`# BREAK THE LOOP — Change Approach`,
|
|
733
|
+
``,
|
|
734
|
+
`You've been repeating the same tools without making progress on "${taskTitle}".`,
|
|
735
|
+
analysis.details?.tool_loop
|
|
736
|
+
? `Detail: ${analysis.details.tool_loop}`
|
|
737
|
+
: "",
|
|
738
|
+
``,
|
|
739
|
+
`STOP and take a different approach:`,
|
|
740
|
+
`1. Summarize what you've learned so far`,
|
|
741
|
+
`2. Identify what's blocking you`,
|
|
742
|
+
`3. Try a completely different strategy`,
|
|
743
|
+
`4. Make incremental progress — edit files, commit, push`,
|
|
744
|
+
]
|
|
745
|
+
.filter(Boolean)
|
|
746
|
+
.join("\n");
|
|
747
|
+
|
|
748
|
+
case "analysis_paralysis":
|
|
749
|
+
return [
|
|
750
|
+
`# START EDITING — Stop Just Reading`,
|
|
751
|
+
``,
|
|
752
|
+
`You've been reading files but not making any changes for "${taskTitle}".`,
|
|
753
|
+
analysis.details?.analysis_paralysis
|
|
754
|
+
? `Detail: ${analysis.details.analysis_paralysis}`
|
|
755
|
+
: "",
|
|
756
|
+
``,
|
|
757
|
+
`You have enough context. Start implementing:`,
|
|
758
|
+
`1. Create or edit the files needed`,
|
|
759
|
+
`2. Don't try to understand everything first — work incrementally`,
|
|
760
|
+
`3. Commit and push after each meaningful change`,
|
|
761
|
+
]
|
|
762
|
+
.filter(Boolean)
|
|
763
|
+
.join("\n");
|
|
764
|
+
|
|
765
|
+
case "needs_clarification":
|
|
766
|
+
return [
|
|
767
|
+
`# MAKE A DECISION — Do Not Wait for Input`,
|
|
768
|
+
``,
|
|
769
|
+
`You expressed uncertainty about "${taskTitle}" but this is autonomous execution.`,
|
|
770
|
+
`No one will respond to your questions.`,
|
|
771
|
+
``,
|
|
772
|
+
`Choose the most reasonable approach and proceed:`,
|
|
773
|
+
`1. Pick the simplest correct implementation`,
|
|
774
|
+
`2. Document any assumptions in code comments`,
|
|
775
|
+
`3. Implement, test, commit, and push`,
|
|
776
|
+
].join("\n");
|
|
777
|
+
|
|
778
|
+
case "false_completion":
|
|
779
|
+
return [
|
|
780
|
+
`# ACTUALLY COMPLETE THE TASK`,
|
|
781
|
+
``,
|
|
782
|
+
`You claimed "${taskTitle}" was complete, but no git commit or push was detected.`,
|
|
783
|
+
``,
|
|
784
|
+
`The task is NOT complete until changes are committed and pushed:`,
|
|
785
|
+
`1. Stage your changes: git add -A`,
|
|
786
|
+
`2. Commit: git commit -m "feat(scope): description"`,
|
|
787
|
+
`3. Push: git push origin <branch>`,
|
|
788
|
+
`4. Verify the push succeeded`,
|
|
789
|
+
].join("\n");
|
|
790
|
+
|
|
791
|
+
case "rate_limited":
|
|
792
|
+
return [
|
|
793
|
+
`# RATE LIMITED — Wait and Retry`,
|
|
794
|
+
``,
|
|
795
|
+
`You hit rate limits while working on "${taskTitle}".`,
|
|
796
|
+
`Wait 30 seconds, then continue with smaller, focused operations.`,
|
|
797
|
+
`Avoid large file reads or many parallel tool calls.`,
|
|
798
|
+
].join("\n");
|
|
799
|
+
|
|
800
|
+
default:
|
|
801
|
+
return `Continue working on task "${taskTitle}". Focus on making concrete progress.`;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// ── Private helpers ─────────────────────────────────────────────────────
|
|
806
|
+
|
|
807
|
+
/** @private */
|
|
808
|
+
_notifyTelegram(message) {
|
|
809
|
+
if (typeof this.sendTelegram === "function") {
|
|
810
|
+
try {
|
|
811
|
+
this.sendTelegram(message);
|
|
812
|
+
} catch {
|
|
813
|
+
/* swallow */
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ── Factory ─────────────────────────────────────────────────────────────────
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Convenience factory for creating an ErrorDetector.
|
|
823
|
+
*
|
|
824
|
+
* @param {object} [options] Same options as ErrorDetector constructor.
|
|
825
|
+
* @returns {ErrorDetector}
|
|
826
|
+
*/
|
|
827
|
+
export function createErrorDetector(options) {
|
|
828
|
+
return new ErrorDetector(options);
|
|
829
|
+
}
|