// Stuck-incident detector. Today's failure-tracker in bot.js fires // proposals on repeated *exception-class* failures (TypeError, timeout). // This module fires on "no-progress" stagnation — the bot is healthy and // the reflex loop hasn't crashed, but a single reason code (e.g. // no_food_source, planner_empty) keeps coming back tick after tick. // // When the same reason persists past STUCK_THRESHOLD_MS we build a // proposal body summarising the situation, including: // - the no-progress reason // - the active milestone + suggested skill // - a slim snapshot // - the last skill result // - per-skill success/failure metrics (if available) // - the allowed edit scope ("runtime/skills/.js" plus tests) // // The caller (bot.js) is responsible for actually writing the proposal — // this module just produces the body string when the threshold trips. const STUCK_THRESHOLD_MS = 5 * 60 * 1000; const COOLDOWN_MS = 30 * 60 * 1000; export function createStuckIncidentDetector({ thresholdMs = STUCK_THRESHOLD_MS, cooldownMs = COOLDOWN_MS } = {}) { let currentReason = null; let firstSeenAt = 0; let lastFiredAt = 0; function reset() { currentReason = null; firstSeenAt = 0; } // Returns { fire: true, body, kind, editScope } when an incident should // be filed this tick; null otherwise. function check({ snapshot, lastResult, metrics, journalSummary, scenarioTail, now = Date.now() }) { const reason = snapshot?.noProgressReason ?? null; if (!reason) { reset(); return null; } if (reason !== currentReason) { currentReason = reason; firstSeenAt = now; return null; } if (now - firstSeenAt < thresholdMs) return null; if (lastFiredAt > 0 && now - lastFiredAt < cooldownMs) return null; lastFiredAt = now; const milestone = snapshot?.curriculum?.milestone; const suggested = snapshot?.curriculum?.plan?.skillId; const slim = { runtimeState: snapshot.runtimeState, noProgressReason: reason, milestone: milestone?.title ?? null, suggestedSkill: suggested ?? null, position: snapshot.position, health: snapshot.health, food: snapshot.food, isDay: snapshot.isDay, inventoryCounts: Object.keys(snapshot.inventory ?? {}).length, closestHostile: snapshot.closestHostile, }; const editScope = suggested ? [`runtime/skills/${suggested.replace(/\./g, "-")}.js`, `runtime/skills/`] : ["runtime/skills/", "runtime/"]; const metricsLine = metrics && Object.keys(metrics).length ? Object.entries(metrics) .map(([id, m]) => `${id}: ok=${m.ok} fail=${m.fail}`) .join(", ") : "(none)"; const journalLine = journalSummary ? `byKind=${JSON.stringify(journalSummary.byKind ?? {})} buckets=${journalSummary.totalBuckets ?? 0}` : "(no journal)"; const scenarioLines = Array.isArray(scenarioTail) && scenarioTail.length ? scenarioTail.map((e) => `- \`${e.skillId}\` ${e.ok ? "OK" : "FAIL"} code=${e.code ?? "?"} ${e.detail ? `(${e.detail.slice?.(0, 120) ?? ""})` : ""} situ=${e.situationHashShort ?? "?"}`, ).join("\n") : "_(no scenario memory recorded yet)_"; const body = [ `# Stuck on \`${reason}\``, "", `The runtime has reported the same no-progress reason for >${Math.round(thresholdMs / 60000)} min without a productive action.`, "", "## Current state", "", "```json", JSON.stringify(slim, null, 2), "```", "", "## Last action result", "", lastResult ? `\`${lastResult.label}\` → ${lastResult.code ?? (lastResult.ok ? "ok" : "fail")}${lastResult.detail ? ` (${JSON.stringify(lastResult.detail).slice(0, 200)})` : ""}` : "_(none recorded)_", "", "## Skill metrics so far (this process lifetime)", "", metricsLine, "", "## World journal (what we have discovered so far)", "", journalLine, "", "## Recent scenario memory (last attempts, what worked / failed in similar situations)", "", scenarioLines, "", "## Suggested fix", "", suggested ? `Improve \`${suggested}\` so the bot can clear the \`${reason}\` blocker, OR teach a NEW skill that handles this kind of situation if no single edit fixes it. Touch only the listed files (the test files under runtime/**/*.test.js are auto-allowed). Use the scenario-memory entries above to avoid re-introducing patterns that already failed.` : `The curriculum has no suggested skill for this state. Either teach the curriculum a new milestone OR add a recovery skill that turns this reason code into a productive action. The scenario memory above shows what's been tried.`, "", "## Edit scope (auto-patch must obey this)", "", editScope.map((p) => `- ${p}`).join("\n"), "", "## Forbidden", "", "- Don't touch `.env`, `state/`, `extensions/`, `tui/` unless the scope above includes them.", "- Don't add new npm dependencies.", "- Don't change git history (no `--amend`, no `git reset --hard`).", "", ].join("\n"); return { fire: true, kind: `stuck-${reason}`, summary: `stuck on ${reason}${milestone ? ` (milestone: ${milestone.title})` : ""}`, body, editScope, }; } return { check, reset }; }