harness: throttle bubble edits — cap 12/bubble, min 8s interval

Element X chokes loading rooms with long m.replace edit chains (the tool
bubble got an edit per tool call — dozens per turn). Throttle pushes; the
final state still flushes via the reply/closing path.
This commit is contained in:
pierre committed 2026-09-24 18:09:49 +03:00
1 parent 890970de81
commit df4d00ee29
1 file changed
+12
+12
View File
@@ -201,6 +201,8 @@ const PROGRESS_ARG_CHARS = 120;
const PROGRESS_RET_CHARS = 400; const PROGRESS_RET_CHARS = 400;
/// Char cap for the bubble body (edits re-send the full body every time). /// Char cap for the bubble body (edits re-send the full body every time).
const PROGRESS_MAX_CHARS = 3500; const PROGRESS_MAX_CHARS = 3500;
/// Edit throttling: Element X chokes on long m.replace edit chains.
const PROGRESS_MAX_EDITS = 12;
const Progress = struct { const Progress = struct {
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
@@ -212,6 +214,8 @@ const Progress = struct {
reply_event_id: ?[]u8 = null, // the streamed-reply message (edited live) reply_event_id: ?[]u8 = null, // the streamed-reply message (edited live)
reply: std.ArrayListUnmanaged(u8) = .empty, // accumulated reply text reply: std.ArrayListUnmanaged(u8) = .empty, // accumulated reply text
replied: bool = false, // true once the full reply was edited in replied: bool = false, // true once the full reply was edited in
last_edit_ms: i64 = 0,
edits_done: usize = 0,
fn deinit(self: *Progress) void { fn deinit(self: *Progress) void {
if (self.event_id) |e| self.alloc.free(e); if (self.event_id) |e| self.alloc.free(e);
@@ -309,6 +313,14 @@ const Progress = struct {
} }
fn push(self: *Progress) void { fn push(self: *Progress) void {
// Throttle: Element X chokes on long m.replace edit chains, so cap
// total edits per bubble and rate-limit to ~1 per 8 seconds. The
// final state is always flushed by the caller (flush()).
const now_ms: i64 = @intCast(@divTrunc(std.Io.Clock.now(.real, self.io).nanoseconds, std.time.ns_per_ms));
if (self.edits_done >= PROGRESS_MAX_EDITS) return;
if (now_ms - self.last_edit_ms < 8000 and self.edits_done > 0) return;
self.last_edit_ms = now_ms;
self.edits_done += 1;
const id = self.event_id orelse { const id = self.event_id orelse {
std.debug.print("DBG push: no event_id, blocks={d}\n", .{self.blocks.items.len}); std.debug.print("DBG push: no event_id, blocks={d}\n", .{self.blocks.items.len});
return; return;