diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 892885ef..2ed11245 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -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, diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 215bb873..e0735936 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -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; diff --git a/src/browser/tools.zig b/src/browser/tools.zig index 04ce5bc5..767a6568 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -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(); }