a73x

c10c84b6

refactor: muxa asks Opts for the session name, not a second field

a73x   2026-08-27 07:17

Commit message
refactor: muxa asks Opts for the session name, not a second field

`session` (as typed) and `_session` (as sent) held the same answer, kept
in step by one assignment in parseArgs. An accessor cannot fall out of
step.

src/cli/muxa.zig
Old New
@@ -59,20 +59,21 @@ const Opts = struct {
59 vt: bool = false, 59 vt: bool = false,
60 /// Optional so that only a name that was TYPED is validated: `""` is 60 /// Optional so that only a name that was TYPED is validated: `""` is
61 /// the wire's own default spelling and would fail a check written for 61 /// the wire's own default spelling and would fail a check written for
62 /// a name. `_session` below is what the frames actually carry. 62 /// a name. `sessionName` is what the frames actually carry.
63 session: ?[]const u8 = null, 63 session: ?[]const u8 = null,
64 /// Which session every verb this invocation makes asks about — the
65 /// attach it opens with AND every ask that follows carry the same
66 /// name, which is what keeps the daemon's attached-tail equality rule
67 /// (server.zig) from ever seeing a mismatch out of this binary. Empty
68 /// is the wire's own default spelling, so a bare `muxa status` builds
69 /// byte-identical frames to before this flag existed.
70 _session: []const u8 = "",
71 /// Null until `parseArgs` reads argv[1]; a parse that returned an Opts 64 /// Null until `parseArgs` reads argv[1]; a parse that returned an Opts
72 /// has one. 65 /// has one.
73 _verb: ?Verb = null, 66 _verb: ?Verb = null,
74 _arg: ?[]const u8 = null, 67 _arg: ?[]const u8 = null,
75 68
69 pub fn sessionName(o: Opts) []const u8 {
70 // ONE name for the attach AND every ask after it, so the daemon's
71 // attached-tail equality rule (server.zig) never sees a mismatch
72 // out of this binary. Empty is the wire's own default spelling, so
73 // a bare `muxa status` builds the frames it always did.
74 return o.session orelse "";
75 }
76
76 /// The verb's argument. A second is a mistake: no verb here takes two. 77 /// The verb's argument. A second is a mistake: no verb here takes two.
77 pub fn positional(self: *Opts, word: []const u8) bool { 78 pub fn positional(self: *Opts, word: []const u8) bool {
78 if (self._arg != null) return false; 79 if (self._arg != null) return false;
@@ -113,8 +114,6 @@ fn parseArgs(args: []const [:0]const u8) ParseError!Opts {
113 if (o.session) |name| { 114 if (o.session) |name| {
114 if (!proto.validSessionName(name)) return error.Usage; 115 if (!proto.validSessionName(name)) return error.Usage;
115 } 116 }
116 o._session = o.session orelse "";
117
118 // Name ONE transport. A `--sock` silently ignored beside a `--quic` 117 // Name ONE transport. A `--sock` silently ignored beside a `--quic`
119 // would send an agent's frames somewhere other than the socket it 118 // would send an agent's frames somewhere other than the socket it
120 // named, and the two answers differ — this is the mistake `mux` 119 // named, and the two answers differ — this is the mistake `mux`
@@ -248,11 +247,11 @@ test "muxa: --help and --version are answered wherever they can be typed" {
248 test "muxa: --session rides every verb; a bad name is usage, not wire bytes" { 247 test "muxa: --session rides every verb; a bad name is usage, not wire bytes" {
249 const a = [_][:0]const u8{ "muxa", "status", "--session", "b" }; 248 const a = [_][:0]const u8{ "muxa", "status", "--session", "b" };
250 const o = try parseArgs(&a); 249 const o = try parseArgs(&a);
251 try std.testing.expectEqualStrings("b", o._session); 250 try std.testing.expectEqualStrings("b", o.sessionName());
252 251
253 // No --session named: the wire's own default spelling, empty. 252 // No --session named: the wire's own default spelling, empty.
254 const bare = [_][:0]const u8{ "muxa", "status" }; 253 const bare = [_][:0]const u8{ "muxa", "status" };
255 try std.testing.expectEqualStrings("", (try parseArgs(&bare))._session); 254 try std.testing.expectEqualStrings("", (try parseArgs(&bare)).sessionName());
256 255
257 // A name no tool could ever address is refused at parse (the usage 256 // A name no tool could ever address is refused at parse (the usage
258 // exit, 2) rather than reaching a daemon as a payload nothing can 257 // exit, 2) rather than reaching a daemon as a payload nothing can
@@ -1266,9 +1265,9 @@ pub fn main() !u8 {
1266 /// else that distinguishes them, which is the property `--quic` is selling. 1265 /// else that distinguishes them, which is the property `--quic` is selling.
1267 fn dispatch(alloc: std.mem.Allocator, conn: *Conn, o: Opts, deadline: i64) !u8 { 1266 fn dispatch(alloc: std.mem.Allocator, conn: *Conn, o: Opts, deadline: i64) !u8 {
1268 return switch (o._verb.?) { 1267 return switch (o._verb.?) {
1269 .status => verbStatus(alloc, conn, o._session, deadline), 1268 .status => verbStatus(alloc, conn, o.sessionName(), deadline),
1270 .capture => verbCapture(alloc, conn, o.vt, o._session, deadline), 1269 .capture => verbCapture(alloc, conn, o.vt, o.sessionName(), deadline),
1271 .send => verbSend(alloc, conn, o._arg, o._session, deadline), 1270 .send => verbSend(alloc, conn, o._arg, o.sessionName(), deadline),
1272 // The one thing `run` needs that `await` does not, checked here so 1271 // The one thing `run` needs that `await` does not, checked here so
1273 // the shared pipeline below can read `cmdline == null` as "this is 1272 // the shared pipeline below can read `cmdline == null` as "this is
1274 // an await" rather than as "a run that was spelled wrong". 1273 // an await" rather than as "a run that was spelled wrong".
@@ -1562,7 +1561,7 @@ fn doAwait(
1562 .since_seq = since_seq, 1561 .since_seq = since_seq,
1563 .settle_ms = o.settle, 1562 .settle_ms = o.settle,
1564 .timeout_ms = o.timeout, 1563 .timeout_ms = o.timeout,
1565 }, o._session); 1564 }, o.sessionName());
1566 try conn.sendFrame(.await_req, payload, deadline); 1565 try conn.sendFrame(.await_req, payload, deadline);
1567 const frame = try conn.awaitFrame(.await_reply, deadline); 1566 const frame = try conn.awaitFrame(.await_reply, deadline);
1568 defer frame.deinit(alloc); 1567 defer frame.deinit(alloc);
@@ -1597,7 +1596,7 @@ fn awaitReissuing(
1597 // daemon that lost our connection lost the client slot with 1596 // daemon that lost our connection lost the client slot with
1598 // it, so an await_req arriving unattached asks about nothing. 1597 // it, so an await_req arriving unattached asks about nothing.
1599 // A failure here is still the reconnect failing. 1598 // A failure here is still the reconnect failing.
1600 attachZero(conn, o._session, deadline) catch |reattach| { 1599 attachZero(conn, o.sessionName(), deadline) catch |reattach| {
1601 // A refused re-attach is the daemon's answer, not the tear 1600 // A refused re-attach is the daemon's answer, not the tear
1602 // that got us here: `ConnectionLost` would send an agent 1601 // that got us here: `ConnectionLost` would send an agent
1603 // to check the network for a session that is gone. 1602 // to check the network for a session that is gone.
@@ -1816,16 +1815,16 @@ fn awaitVerb(
1816 const who = if (cmdline == null) "await" else "run"; 1815 const who = if (cmdline == null) "await" else "run";
1817 const started = std.time.milliTimestamp(); 1816 const started = std.time.milliTimestamp();
1818 1817
1819 attachZero(conn, o._session, deadline) catch |e| 1818 attachZero(conn, o.sessionName(), deadline) catch |e|
1820 return failSend(e, o._session, .attach, who, "attach failed"); 1819 return failSend(e, o.sessionName(), .attach, who, "attach failed");
1821 1820
1822 // BEFORE the input, not after: the watermark has to be the one this 1821 // BEFORE the input, not after: the watermark has to be the one this
1823 // command must beat. Read afterwards, a command fast enough to return 1822 // command must beat. Read afterwards, a command fast enough to return
1824 // between the two would have already moved the seq past a value we 1823 // between the two would have already moved the seq past a value we
1825 // never recorded, and the await would sit waiting for a return that 1824 // never recorded, and the await would sit waiting for a return that
1826 // had happened. 1825 // had happened.
1827 const since = currentSeq(alloc, conn, o._session, deadline) catch |e| switch (e) { 1826 const since = currentSeq(alloc, conn, o.sessionName(), deadline) catch |e| switch (e) {
1828 error.AttachRefused => return failAttachRefused(o._session, .attach), 1827 error.AttachRefused => return failAttachRefused(o.sessionName(), .attach),
1829 error.SessionExited => return reportSessionEnded(alloc, conn.session_exit, elapsed(started)), 1828 error.SessionExited => return reportSessionEnded(alloc, conn.session_exit, elapsed(started)),
1830 else => return failAs(who, "status failed", @errorName(e)), 1829 else => return failAs(who, "status failed", @errorName(e)),
1831 }; 1830 };
@@ -1840,11 +1839,11 @@ fn awaitVerb(
1840 return failAs(who, "cannot build the command line", @errorName(e)); 1839 return failAs(who, "cannot build the command line", @errorName(e));
1841 defer alloc.free(line); 1840 defer alloc.free(line);
1842 conn.sendFrame(.input, line, deadline) catch |e| 1841 conn.sendFrame(.input, line, deadline) catch |e|
1843 return failSend(e, o._session, .attach, who, "input failed"); 1842 return failSend(e, o.sessionName(), .attach, who, "input failed");
1844 } 1843 }
1845 1844
1846 const r = awaitReissuing(alloc, conn, o, since, awaitDeadline(o, conn)) catch |e| switch (e) { 1845 const r = awaitReissuing(alloc, conn, o, since, awaitDeadline(o, conn)) catch |e| switch (e) {
1847 error.AttachRefused => return failAttachRefused(o._session, .attach), 1846 error.AttachRefused => return failAttachRefused(o.sessionName(), .attach),
1848 error.SessionExited => return reportSessionEnded(alloc, conn.session_exit, elapsed(started)), 1847 error.SessionExited => return reportSessionEnded(alloc, conn.session_exit, elapsed(started)),
1849 else => { 1848 else => {
1850 var detail: [128]u8 = undefined; 1849 var detail: [128]u8 = undefined;