Commit 00897d8
Eric Bower
·
2026-08-11 10:16:10 -0400 EDT
parent b7033a8
refactor: ZMX_TASK_COMPLETE now uses a 4ch hex id for task detection This ensures we are correctly scanning for the task that was actually triggered from the current daemon. A zmx session is allowed to `zmx run` to another session and then it immediately tails that command. That command will also output the ZMX_TASK_COMPLETE marker which could mark the parent session as complete erroneously. We now look for `ZMX_TASK_COMPLETE:wxyz:0`
3 files changed,
+32,
-14
+11,
-5
1@@ -362,7 +362,7 @@ fn daemonLoop(daemon: *Daemon, gpa: std.mem.Allocator, io: std.Io, server_sock_f
2 @memcpy(scan_buf[marker_carry_len..][0..n], buf[0..n]);
3 const scan_len = marker_carry_len + n;
4
5- if (util.findTaskExitMarker(scan_buf[0..scan_len])) |exit_code| {
6+ if (try util.findTaskExitMarker(scan_buf[0..scan_len], daemon.task_id)) |exit_code| {
7 daemon.task_exit_code = exit_code;
8 daemon.task_ended_at = @intCast(std.Io.Timestamp.now(io, .real).toSeconds());
9
10@@ -474,7 +474,7 @@ fn daemonLoop(daemon: *Daemon, gpa: std.mem.Allocator, io: std.Io, server_sock_f
11 .LabelSet => try daemon.handleLabelSet(gpa, client, msg.payload),
12 .LabelClear => try daemon.handleLabelClear(gpa, client),
13 .History => try daemon.handleHistory(gpa, client, &term, msg.payload),
14- .Run => try daemon.handleRun(gpa, client, msg.payload),
15+ .Run => try daemon.handleRun(gpa, io, client, msg.payload),
16 .Ack, .TaskComplete, .LabelData => {},
17 .Write => try daemon.handleWrite(gpa, client, msg.payload),
18 _ => std.log.warn(
19@@ -566,6 +566,7 @@ pub const Daemon = struct {
20 has_terminal_client: bool = false, // true only after a real attach (.Init received)
21 created_at: u64, // unix timestamp (ns)
22 is_task_mode: bool = false, // flag for when session is run as a task
23+ task_id: [4]u8 = undefined,
24 task_exit_code: ?u8 = null, // null = running or n/a, set when task completes
25 task_ended_at: ?u64 = null, // timestamp when task exited
26 pty_fd: i32 = -1, // set by daemonLoop so handleRun can probe the foreground process
27@@ -1102,13 +1103,14 @@ pub const Daemon = struct {
28 }
29 }
30
31- pub fn handleRun(self: *Daemon, gpa: std.mem.Allocator, client: *Client, payload: []const u8) !void {
32+ pub fn handleRun(self: *Daemon, gpa: std.mem.Allocator, io: std.Io, client: *Client, payload: []const u8) !void {
33 // Reset task tracking so the new command's exit marker is detected.
34 // Without this, a second `zmx run` on the same session is ignored
35 // because task_exit_code is still set from the first run.
36 self.task_exit_code = null;
37 self.task_ended_at = null;
38 self.is_task_mode = true;
39+ self.task_id = util.generateTaskId(io);
40
41 if (payload.len == 0) return;
42
43@@ -1118,8 +1120,12 @@ pub const Daemon = struct {
44 // exit code of the command (not the `;`). The sole exception is when
45 // the command contains a heredoc (`<<`), the delimiter must be alone
46 // on its line, so the marker goes on the next line instead.
47- const single_line_marker = "; echo ZMX_TASK_COMPLETED:$?\r";
48- const heredoc_marker = "\r\necho ZMX_TASK_COMPLETED:$?\r";
49+ var buf: [1024]u8 = undefined;
50+ const marker = try util.getTaskExitMarker(&buf, self.task_id);
51+ var single_buf: [1024]u8 = undefined;
52+ const single_line_marker = try std.fmt.bufPrint(&single_buf, "; echo {s}$?\r", .{marker});
53+ var here_buf: [1024]u8 = undefined;
54+ const heredoc_marker = try std.fmt.bufPrint(&here_buf, "\r\necho {s}$?\r", .{marker});
55 const uses_heredoc = std.mem.indexOf(u8, cmd, "<<") != null;
56
57 if (cmd.len > 0 and cmd[cmd.len - 1] == '\r') {
+6,
-5
1@@ -642,15 +642,16 @@ fn tail(alloc: std.mem.Allocator, client_socket_fds: std.ArrayList(i32), detache
2 },
3 .Output => {
4 if (msg.payload.len > 0) {
5+ // TODO: figure out how to bring this back
6 // Fallback: scan output for task exit marker in case
7 // .TaskComplete was lost (e.g. daemon exited before
8 // flushing). This ensures we detect completion even
9 // when the IPC message doesn't arrive.
10- if (task_complete_code == null and is_run_cmd) {
11- if (util.findTaskExitMarker(msg.payload)) |ec| {
12- task_complete_code = ec;
13- }
14- }
15+ // if (task_complete_code == null and is_run_cmd) {
16+ // if (util.findTaskExitMarker(msg.payload)) |ec| {
17+ // task_complete_code = ec;
18+ // }
19+ // }
20
21 // Strip the first line (command echo) for run mode.
22 var payload = msg.payload;
+15,
-4
1@@ -350,13 +350,24 @@ test "rewritePromptRedraw: embedded in larger output" {
2 try std.testing.expectEqualStrings("some output\r\n\x1b]133;A;redraw=0\x07prompt$ \x1b]133;B\x07", result);
3 }
4
5-pub fn findTaskExitMarker(output: []const u8) ?u8 {
6- const marker = "ZMX_TASK_COMPLETED:";
7+pub fn generateTaskId(io: std.Io) [4]u8 {
8+ var bytes: [2]u8 = undefined;
9+ io.random(&bytes);
10+ return std.fmt.bytesToHex(bytes, .lower);
11+}
12+
13+pub fn getTaskExitMarker(buf: []u8, id_marker: [4]u8) ![]u8 {
14+ return std.fmt.bufPrint(buf, "ZMX_TASK_COMPLETED:{s}:", .{id_marker});
15+}
16+
17+pub fn findTaskExitMarker(output: []const u8, id_marker: [4]u8) !?u8 {
18+ var buf: [1024]u8 = undefined;
19+ const marker = try getTaskExitMarker(&buf, id_marker);
20
21 // The command line is echoed back by the PTY (canonical mode) before the
22 // shell evaluates it, so the *first* occurrence of the marker in the
23- // output is often the literal, unexpanded "ZMX_TASK_COMPLETED:$?" from
24- // the echo, not the real "ZMX_TASK_COMPLETED:<code>" written once the
25+ // output is often the literal, unexpanded "ZMX_TASK_COMPLETED:{id}:$?" from
26+ // the echo, not the real "ZMX_TASK_COMPLETED:{id}:<code>" written once the
27 // shell actually runs it. Keep scanning past unparseable occurrences
28 // instead of giving up on the first one.
29 var search_start: usize = 0;