matrix bridge: proper JSON sync parsing, backlog forwarding, message provenance

- sync(): std.json walk of rooms.join.*.timeline (old string-window scraper
  misattributed senders across events); Event gains ts
- startup backlog: peer messages from last 30min forwarded to agent as
  context (reply suppressed); failures logged, never fatal (systemd
  Restart=on-failure would otherwise re-forward every 3s)
- messages to agent prefixed [matrix HH:MMZ] with UTC event timestamp
- sendText: jsonEscape covers control chars (\u00XX), markdown -> HTML
  formatted_body (bold/italic/code/fences) for Element rendering
- no startup banner in room; harness now runs as systemd user service
  matrix-harness (unit + .matrix-env, untracked)
This commit is contained in:
pierre committed 2026-09-02 08:57:43 +03:00
1 parent 77f3150af4
commit 9f1a839a22
2 files changed
+269 -40

No files matched your search

+48 -6
View File
@@ -55,13 +55,42 @@ pub fn main(init: std.process.Init) !void {
defer if (owned_room) |r| alloc.free(r);
std.debug.print("matrix_harness: room {s}\n", .{room_id});
_ = try matrix.sendText(alloc, io, session.token, room_id, "matrix_harness online in persistent room. Messages reach the agent, replies come back here.");
// No room banner on startup — restarts (deploys, watchdog) would spam
// the DM. Startup is logged to journald instead.
// Establish a sync point (drop history before now).
// Establish a sync point. RECENT peer messages (last 30 min only, so
// harness restarts don't re-forward ancient history every boot) are
// forwarded to the agent as one [matrix backlog] context message
// (reply suppressed).
const first = try matrix.sync(alloc, io, session.token, "", 0);
for (first.events) |ev| {
alloc.free(ev.sender);
alloc.free(ev.body);
{
const now_ms: i64 = @intCast(@divTrunc(std.Io.Clock.now(.real, io).nanoseconds, std.time.ns_per_ms));
var backlog: std.ArrayListUnmanaged(u8) = .empty;
defer backlog.deinit(alloc);
for (first.events) |ev| {
defer alloc.free(ev.sender);
defer alloc.free(ev.body);
if (!std.mem.eql(u8, ev.sender, peer)) continue;
if (ev.body.len == 0) continue;
if (ev.ts > 0 and now_ms - ev.ts > 30 * 60 * 1000) continue; // stale
try backlog.appendSlice(alloc, ev.sender);
try backlog.appendSlice(alloc, ": ");
try backlog.appendSlice(alloc, ev.body);
try backlog.append(alloc, '\n');
}
if (backlog.items.len > 0) {
std.debug.print("matrix_harness: forwarding backlog ({d} bytes) to agent\n", .{backlog.items.len});
const hist_msg = try std.fmt.allocPrint(alloc, "[matrix backlog — messages received while harness was offline]\n{s}", .{backlog.items});
defer alloc.free(hist_msg);
// Do NOT exit on backlog failure: with systemd Restart=on-failure
// this would re-forward the same backlog every 3s forever.
// Log, skip, keep polling.
if (letta.infer(io, alloc, agent, hist_msg)) |reply| {
alloc.free(reply); // backlog replies are not sent back
} else |e| {
std.debug.print("matrix_harness: backlog letta failed: {s} (skipping)\n", .{@errorName(e)});
}
}
}
alloc.free(first.events);
var since = first.next;
@@ -86,7 +115,20 @@ pub fn main(init: std.process.Init) !void {
std.debug.print("matrix_harness: [{s}] {s}\n", .{ ev.sender, ev.body });
const reply = letta.infer(io, alloc, agent, ev.body) catch |e| {
// Prefix with origin + event UTC timestamp so the agent can tell
// Matrix-origin messages from CLI ones (and when they were sent).
var ts_buf: [16]u8 = undefined;
const ts_str: []const u8 = blk: {
if (ev.ts <= 0) break :blk "";
const secs: u64 = @intCast(@divTrunc(ev.ts, 1000));
const epoch = std.time.epoch.EpochSeconds{ .secs = secs };
const sod = epoch.getDaySeconds();
break :blk std.fmt.bufPrint(&ts_buf, " {d:0>2}:{d:0>2}Z", .{ sod.getHoursIntoDay(), sod.getMinutesIntoHour() }) catch "";
};
const prefixed = try std.fmt.allocPrint(alloc, "[matrix{s}] {s}", .{ ts_str, ev.body });
defer alloc.free(prefixed);
const reply = letta.infer(io, alloc, agent, prefixed) catch |e| {
std.debug.print("matrix_harness: letta failed: {s}\n", .{@errorName(e)});
const err_txt = "agent failed — see harness logs";
if (matrix.sendText(alloc, io, session.token, room_id, err_txt)) |ev_id| alloc.free(ev_id) else |_| {}