Commit 9d1e6dd

Eric Bower  ·  2026-05-16 11:02:50 -0400 EDT
parent 936c1fa
feat(wait): print last 20 lines of session history on failure
1 files changed,  +92, -2
M src/main.zig
+92, -2
  1@@ -1585,12 +1585,38 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
  2         }
  3         if (session.task_exit_code.? > 0) {
  4             try stdout.print("---\n", .{});
  5-            try stdout.print("[{d}] failed task={s} exit_status={d}\n\n", .{
  6+            try stdout.print("[{d}] failed task={s} exit_status={d}\n", .{
  7                 session.task_ended_at.?,
  8                 session.name,
  9                 session.task_exit_code.?,
 10             });
 11-            try stdout.print("See the logs:\nzmx history {s}\nzmx attach {s}\n", .{ session.name, session.name });
 12+
 13+            // Fetch and print the last 20 lines of history for debugging
 14+            const history_lines: usize = 20;
 15+            const history_text = fetchHistory(alloc, cfg, session.name) catch null;
 16+            if (history_text) |text| {
 17+                defer alloc.free(text);
 18+                try stdout.print("\nLast {d} lines of {s} history:\n", .{ history_lines, session.name });
 19+
 20+                // Count lines and find the start of the last N lines
 21+                var total_lines: usize = 0;
 22+                var it = std.mem.splitScalar(u8, text, '\n');
 23+                while (it.next()) |_| {
 24+                    total_lines += 1;
 25+                }
 26+
 27+                const skip = if (total_lines > history_lines) total_lines - history_lines else 0;
 28+                var current: usize = 0;
 29+                it = std.mem.splitScalar(u8, text, '\n');
 30+                while (it.next()) |line| {
 31+                    if (current >= skip) {
 32+                        try stdout.print("{s}\n", .{line});
 33+                    }
 34+                    current += 1;
 35+                }
 36+            }
 37+
 38+            try stdout.print("\nSee the logs:\nzmx history {s}\nzmx attach {s}\n", .{ session.name, session.name });
 39             try stdout.flush();
 40         }
 41     }
 42@@ -1713,6 +1739,70 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void {
 43     try w.interface.flush();
 44 }
 45 
 46+/// Fetch terminal history from a session socket, returning it as an allocated
 47+/// string. Caller owns the returned memory and must free it.
 48+fn fetchHistory(
 49+    alloc: std.mem.Allocator,
 50+    cfg: *Cfg,
 51+    session_name: []const u8,
 52+) ![]const u8 {
 53+    const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
 54+        error.NameTooLong => {
 55+            socket.printSessionNameTooLong(session_name, cfg.socket_dir);
 56+            return error.NameTooLong;
 57+        },
 58+        error.OutOfMemory => return err,
 59+    };
 60+    defer alloc.free(socket_path);
 61+
 62+    var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
 63+    defer dir.close();
 64+
 65+    const exists = try socket.sessionExists(dir, session_name);
 66+    if (!exists) {
 67+        return error.SessionNotFound;
 68+    }
 69+
 70+    const fd = ipc.connectSession(socket_path) catch |err| {
 71+        if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, session_name);
 72+        return err;
 73+    };
 74+    defer posix.close(fd);
 75+
 76+    const format_byte: u8 = @intFromEnum(util.HistoryFormat.plain);
 77+    const payload = [_]u8{format_byte};
 78+    ipc.send(fd, .History, &payload) catch |err| switch (err) {
 79+        error.BrokenPipe, error.ConnectionResetByPeer => return error.SessionUnresponsive,
 80+        else => return err,
 81+    };
 82+
 83+    var sb = try ipc.SocketBuffer.init(alloc);
 84+    defer sb.deinit();
 85+
 86+    var result = std.ArrayList(u8).initCapacity(alloc, 4096) catch return error.OutOfMemory;
 87+    errdefer result.deinit(alloc);
 88+
 89+    while (true) {
 90+        var poll_fds = [_]posix.pollfd{.{ .fd = fd, .events = posix.POLL.IN, .revents = 0 }};
 91+        const poll_result = posix.poll(&poll_fds, 5000) catch return error.Timeout;
 92+        if (poll_result == 0) {
 93+            return error.Timeout;
 94+        }
 95+
 96+        const n = sb.read(fd) catch return error.ReadFailed;
 97+        if (n == 0) break;
 98+
 99+        while (sb.next()) |msg| {
100+            if (msg.header.tag == .History) {
101+                try result.appendSlice(alloc, msg.payload);
102+                return result.toOwnedSlice(alloc);
103+            }
104+        }
105+    }
106+
107+    return error.NoHistoryResponse;
108+}
109+
110 fn history(cfg: *Cfg, session_name: []const u8, format: util.HistoryFormat) !void {
111     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
112     defer _ = gpa.deinit();