agent: improve self-healing and console message handling

Roll back message history on failed self-heal attempts to prevent
context pollution. Cap console messages at 1000 to prevent memory
exhaustion and add a helper to drain the message buffer. Refactor
system prompt initialization into a shared helper function.
This commit is contained in:
Adrià Arrufat
2026-04-13 18:22:12 +02:00
parent 570c901239
commit 0c56f085dc
3 changed files with 26 additions and 11 deletions

View File

@@ -421,6 +421,7 @@ fn flushReplacements(self: *Self, path: []const u8, content: []const u8, replace
// relies on this to compute byte offsets.
const content_base = @intFromPtr(content.ptr);
var new_content: std.ArrayList(u8) = .empty;
new_content.ensureTotalCapacity(self.allocator, content.len) catch {};
var pos: usize = 0;
for (replacements) |r| {
const r_start = @intFromPtr(r.original_span.ptr) - content_base;
@@ -463,15 +464,19 @@ const self_heal_max_attempts = 3;
/// Runs a single LLM turn and returns the commands it executed, without
/// recording them to the Recorder. Used by attemptSelfHeal so that the
/// caller can capture healed commands for script rewriting.
fn runHealTurn(self: *Self, prompt: []const u8, arena: std.mem.Allocator) ![]Command.Command {
const ma = self.message_arena.allocator();
fn ensureSystemPrompt(self: *Self) !void {
if (self.messages.items.len == 0) {
try self.messages.append(self.allocator, .{
.role = .system,
.content = self.system_prompt,
});
}
}
fn runHealTurn(self: *Self, prompt: []const u8, arena: std.mem.Allocator) ![]Command.Command {
const ma = self.message_arena.allocator();
try self.ensureSystemPrompt();
try self.messages.append(self.allocator, .{
.role = .user,
@@ -535,6 +540,10 @@ fn attemptSelfHeal(self: *Self, intent: ?[]const u8, failed_command: []const u8,
self_heal_prompt_instructions,
}) catch return null;
// Save message count so we can roll back between attempts — each failed
// heal turn would otherwise accumulate in context, confusing the next try.
const msg_baseline = self.messages.items.len;
var attempt: u8 = 0;
while (attempt < self_heal_max_attempts) : (attempt += 1) {
const cmds = self.runHealTurn(prompt, arena) catch |err| {
@@ -543,9 +552,11 @@ fn attemptSelfHeal(self: *Self, intent: ?[]const u8, failed_command: []const u8,
self_heal_max_attempts,
@errorName(err),
});
self.messages.shrinkRetainingCapacity(msg_baseline);
continue;
};
if (cmds.len > 0) return cmds;
self.messages.shrinkRetainingCapacity(msg_baseline);
}
return null;
}
@@ -553,12 +564,7 @@ fn attemptSelfHeal(self: *Self, intent: ?[]const u8, failed_command: []const u8,
fn processUserMessage(self: *Self, user_input: []const u8, record_comment: []const u8) !void {
const ma = self.message_arena.allocator();
if (self.messages.items.len == 0) {
try self.messages.append(self.allocator, .{
.role = .system,
.content = self.system_prompt,
});
}
try self.ensureSystemPrompt();
try self.messages.append(self.allocator, .{
.role = .user,

View File

@@ -2559,7 +2559,10 @@ fn isXmlNameChar(c: u21) bool {
(c >= 0x203F and c <= 0x2040);
}
const max_console_messages = 1000;
pub fn appendConsoleMessage(self: *Page, level: ConsoleMessage.Level, values: []JS.Value) void {
if (self._console_messages.items.len >= max_console_messages) return;
var aw: std.Io.Writer.Allocating = .init(self.arena);
for (values, 0..) |value, i| {
if (i > 0) aw.writer.writeAll(" ") catch return;
@@ -2569,6 +2572,13 @@ pub fn appendConsoleMessage(self: *Page, level: ConsoleMessage.Level, values: []
self._console_messages.append(self.arena, .{ .level = level, .text = text }) catch return;
}
/// Returns buffered console messages and clears the buffer.
pub fn drainConsoleMessages(self: *Page) []const ConsoleMessage {
const items = self._console_messages.items;
self._console_messages.clearRetainingCapacity();
return items;
}
pub fn dupeString(self: *Page, value: []const u8) ![]const u8 {
if (String.intern(value)) |v| {
return v;

View File

@@ -842,7 +842,7 @@ fn execConsoleLogs(
arena: std.mem.Allocator,
) ToolError![]const u8 {
const page = session.currentPage() orelse return ToolError.PageNotLoaded;
const messages = page._console_messages.items;
const messages = page.drainConsoleMessages();
if (messages.len == 0) return "No console messages.";
var aw: std.Io.Writer.Allocating = .init(arena);
@@ -850,7 +850,6 @@ fn execConsoleLogs(
for (messages) |msg| {
writer.print("[{s}] {s}\n", .{ @tagName(msg.level), msg.text }) catch return ToolError.InternalError;
}
page._console_messages.clearRetainingCapacity();
return aw.written();
}