Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bots/execTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
/* CODE HERE */
log(bot, 'Code finished.');

})
});
4 changes: 2 additions & 2 deletions bots/lintTemplate.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as skills from '../../../src/agent/library/skills.js';
import * as world from '../../../src/agent/library/world.js';
import Vec3 from 'vec3';
import * as _world from '../../../src/agent/library/world.js';
import _Vec3 from 'vec3';

const log = skills.log;

Expand Down
119 changes: 112 additions & 7 deletions src/agent/action_manager.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import assert from 'assert';

export class ActionManager {
constructor(agent) {
this.agent = agent;
Expand All @@ -9,6 +11,11 @@ export class ActionManager {
this.resume_name = '';
this.last_action_time = 0;
this.recent_action_counter = 0;
// Stuck detection: track repeated same-label calls within a time window
this._stuckTracker = {}; // { label: { count, firstSeen } }
// Cross-invocation zero-collect tracker for gathering actions
this._collectFailTracker = {}; // { blockType: { count, lastSeen } }
this._COLLECT_FAIL_THRESHOLD = 3; // after 3 zero-collect results, force intervention
}

async resumeAction(actionFn, timeout) {
Expand All @@ -26,15 +33,16 @@ export class ActionManager {
async stop() {
if (!this.executing) return;
const timeout = setTimeout(() => {
this.agent.cleanKill('Code execution refused stop after 10 seconds. Killing process.');
console.warn('Code execution refused stop after 10 seconds. Force-cancelling action.');
this.executing = false;
}, 10000);
while (this.executing) {
this.agent.requestInterrupt();
console.log('waiting for code to finish executing...');
await new Promise(resolve => setTimeout(resolve, 300));
}
clearTimeout(timeout);
}
}

cancelResume() {
this.resume_func = null;
Expand Down Expand Up @@ -80,6 +88,70 @@ export class ActionManager {
}
}
this.last_action_time = Date.now();

// Detect slow repeating action patterns
if (!this._actionHistory) this._actionHistory = [];
this._actionHistory.push(actionLabel);
if (this._actionHistory.length > 12) this._actionHistory.shift();
if (this._actionHistory.length >= 6) {
// Pattern repeat: exact 3-action sequence repeats
const last3 = this._actionHistory.slice(-3).join(',');
const prev3 = this._actionHistory.slice(-6, -3).join(',');
// Frequency: any single action appears N+ times in the window
const counts = {};
for (const a of this._actionHistory) counts[a] = (counts[a] || 0) + 1;
const maxCount = Math.max(...Object.values(counts));
const loopAction = Object.keys(counts).find(a => counts[a] === maxCount);
// RC27: Crafting chains legitimately need 5+ craftRecipe calls
// (logs→planks→sticks→tool = 5+ sequential crafts). Raise threshold
// for crafting actions and exempt non-identical craft sequences.
const isCraftAction = loopAction && loopAction.includes('craftRecipe');
const freqThreshold = isCraftAction ? 8 : 5;
const isPatternLoop = last3 === prev3;
const isFreqLoop = maxCount >= freqThreshold;
if (isPatternLoop || isFreqLoop) {
const reason = isPatternLoop ? `pattern "${last3}" repeated` : `"${loopAction}" called ${maxCount} times`;
console.warn(`[ActionManager] Slow loop detected: ${reason}. Cancelling resume.`);
this.cancelResume();
this._actionHistory = [];
return { success: false, message: `Action loop detected (${reason}). Stopping to avoid infinite loop.`, interrupted: true, timedout: false };
}
}
// ── Stuck detector: same action label ≥3 times within window ──────────
const STUCK_WINDOW_MS = 15000;
const STUCK_WINDOW_LONG_MS = 120000; // longer window for slow actions like collectBlocks
const STUCK_THRESHOLD = 3;
const stuckLabels = ['goToPlayer', 'collectBlocks', 'goToBlock', 'moveAway', 'goToBed', 'goToNearestBlock'];
const slowLabels = ['collectBlocks']; // actions that take a long time
const isStuckable = stuckLabels.some(l => actionLabel.includes(l));
const isSlow = slowLabels.some(l => actionLabel.includes(l));
const windowMs = isSlow ? STUCK_WINDOW_LONG_MS : STUCK_WINDOW_MS;
const now = Date.now();
if (isStuckable) {
const t = this._stuckTracker[actionLabel];
if (t && (now - t.firstSeen) < windowMs) {
t.count++;
if (t.count >= STUCK_THRESHOLD) {
console.warn(`[ActionManager] Stuck detected: "${actionLabel}" called ${t.count}x in ${Math.round((now - t.firstSeen)/1000)}s`);
this._stuckTracker = {};
this.cancelResume();
return {
success: false,
message: `Action output:\nStuck detected — "${actionLabel}" failed ${t.count} times in a row. Switch to a different approach immediately: try !searchForBlock with a range of 128+, !moveAway 50, or !newAction with an alternative strategy. Do NOT repeat the same command.`,
interrupted: true,
timedout: false
};
}
} else {
// New action type — reset all stuck tracking
this._stuckTracker = { [actionLabel]: { count: 1, firstSeen: now } };
}
} else {
// Non-stuckable action succeeded — reset tracker
this._stuckTracker = {};
}
// ────────────────────────────────────────────────────────────────────

console.log('executing code...\n');

// await current action to finish (executing=false), with 10 seconds timeout
Expand Down Expand Up @@ -110,12 +182,45 @@ export class ActionManager {
this.currentActionFn = null;
clearTimeout(TIMEOUT);

// get bot activity summary
// Capture raw output BEFORE truncation for reliable regex matching
const rawOutput = this.agent.bot.output || '';

// get bot activity summary (may truncate)
let output = this.getBotOutputSummary();
let interrupted = this.agent.bot.interrupt_code;
let timedout = this.timedout;
this.agent.clearBotLogs();

// ── Cross-invocation zero-collect detection ──────────────────────
// Use rawOutput for regex matching to avoid truncation issues
if (actionLabel.includes('collectBlocks') && rawOutput) {
const zeroMatch = rawOutput.match(/Collected 0 (\w+)/);
const successMatch = rawOutput.match(/Collected (\d+) (\w+)/);
if (zeroMatch) {
const blockType = zeroMatch[1];
const tracker = this._collectFailTracker;
if (!tracker[blockType]) tracker[blockType] = { count: 0, lastSeen: 0 };
tracker[blockType].count++;
tracker[blockType].lastSeen = Date.now();
if (tracker[blockType].count >= this._COLLECT_FAIL_THRESHOLD) {
const failCount = tracker[blockType].count;
tracker[blockType] = { count: 0, lastSeen: 0 }; // reset
console.warn(`[ActionManager] Gather loop: collected 0 ${blockType} ${failCount} times — forcing explore`);
this.cancelResume();
// Set flag so agent.js auto-executes !explore(200) bypassing the LLM
this.agent._forceExplore = { distance: 200, blockType };
output += `\n\nArea depleted: collected 0 ${blockType} ${failCount} times. Auto-exploring 200 blocks to find fresh resources.`;
}
} else if (successMatch && parseInt(successMatch[1]) > 0) {
// Success — reset tracker for this block type
const blockType = successMatch[2];
if (this._collectFailTracker[blockType]) {
this._collectFailTracker[blockType] = { count: 0, lastSeen: 0 };
}
}
}
// ────────────────────────────────────────────────────────────────

// if not interrupted and not generating, emit idle event
if (!interrupted) {
this.agent.bot.emit('idle');
Expand All @@ -131,14 +236,14 @@ export class ActionManager {
this.cancelResume();
console.error("Code execution triggered catch:", err);
// Log the full stack trace
console.error(err.stack);
const stackTrace = err.stack || '';
console.error(stackTrace);
await this.stop();
err = err.toString();

let message = this.getBotOutputSummary() +
'!!Code threw exception!!\n' +
'Error: ' + err + '\n' +
'Stack trace:\n' + err.stack+'\n';
'Error: ' + err.toString() + '\n' +
'Stack trace:\n' + stackTrace + '\n';

let interrupted = this.agent.bot.interrupt_code;
this.agent.clearBotLogs();
Expand Down
Loading
Loading