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;