Commit Diff


commit - 056b9939639819c1a2e85f0e73fc02dda5b27b78
commit + 10b6dbfa3cb419e92d575e24b5f37c0748f97a97
blob - 93314c1cf5366d4e2cbfa834d399e80834883a5f
blob + 199147eb2fd7136670773126d6998110a272d666
--- regress/imsgev/Makefile
+++ regress/imsgev/Makefile
@@ -1,5 +1,5 @@
 PROG=	imsgev_test
-SRCS=	imsgev_test.c imsgev.c log.c
+SRCS=	imsgev_test.c imsgev_regress.c log.c
 
 LDADD=	-levent -lutil
 DPADD=	${LIBEVENT} ${LIBUTIL}
blob - f2fdf7f0d1295eab089010d5678c8daf04235d2d
blob + 6c3766888c8fd793a430fc901c944614bfa62815
--- regress/imsgev/imsgev_test.c
+++ regress/imsgev/imsgev_test.c
@@ -16,18 +16,28 @@
 
 #include <sys/types.h>
 #include <sys/socket.h>
+#include <sys/wait.h>
 
 #include <err.h>
 #include <errno.h>
+#include <event.h>
+#include <fcntl.h>
 #include <imsg.h>
 #include <stdint.h>
+#include <stdio.h>
 #include <string.h>
+#include <syslog.h>
 #include <unistd.h>
 
 #include "imsgev.h"
+#include "log.h"
 
+#include "imsgev_regress.h"
 #include "regress.h"
 
+#define TEST_DATA	41
+#define TEST_END	42
+
 static void
 make_imsg(const void *data, size_t len, struct imsgbuf *receiver,
     struct imsg *imsg)
@@ -54,15 +64,183 @@ make_imsg(const void *data, size_t len, struct imsgbuf
 	close(fds[1]);
 }
 
+static void
+channel_init(struct imsgev *iev, struct imsgbuf *receiver, int fds[2])
+{
+	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, fds) == -1)
+		err(1, "socketpair");
+	imsgev_regress_reset();
+	imsgev_init(iev, fds[0], NULL, NULL);
+	if (imsgbuf_init(receiver, fds[1]) == -1)
+		err(1, "imsgbuf_init receiver");
+}
+
+static void
+channel_clear(struct imsgev *iev, struct imsgbuf *receiver, int fds[2])
+{
+	imsgev_clear(iev);
+	imsgbuf_clear(receiver);
+	close(fds[0]);
+	close(fds[1]);
+}
+
+static int
+receive_one(struct imsgev *iev, struct imsgbuf *receiver, struct imsg *imsg)
+{
+	if (imsgbuf_flush(&iev->ibuf) == -1)
+		err(1, "imsgbuf_flush");
+	if (imsgbuf_read(receiver) != 1)
+		errx(1, "imsgbuf_read");
+	return (imsgbuf_get(receiver, imsg));
+}
+
+static int
+dies(void (*fn)(void))
+{
+	pid_t	 pid;
+	int	 devnull, status;
+
+	fflush(stdout);
+	switch (pid = fork()) {
+	case -1:
+		err(1, "fork");
+	case 0:
+		if ((devnull = open("/dev/null", O_WRONLY)) != -1)
+			(void)dup2(devnull, STDERR_FILENO);
+		fn();
+		_exit(0);
+	default:
+		if (waitpid(pid, &status, 0) == -1)
+			err(1, "waitpid");
+		return (WIFEXITED(status) && WEXITSTATUS(status) == 1);
+	}
+}
+
+static void
+die_init_add(void)
+{
+	struct imsgev	 iev;
+	int		 fds[2];
+
+	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, fds) == -1)
+		err(1, "socketpair");
+	imsgev_regress_reset();
+	imsgev_regress_fail(IMSGEV_REGRESS_EVENT_ADD, 1);
+	imsgev_init(&iev, fds[0], NULL, NULL);
+}
+
+static void
+die_compose(void)
+{
+	struct imsgbuf	 receiver;
+	struct imsgev	 iev;
+	int		 fds[2];
+
+	channel_init(&iev, &receiver, fds);
+	imsgev_regress_reset();
+	imsgev_regress_fail(IMSGEV_REGRESS_COMPOSE, 1);
+	imsgev_send(&iev, TEST_DATA, 0, NULL, 0);
+}
+
+static void
+die_rearm_del(void)
+{
+	struct imsgbuf	 receiver;
+	struct imsgev	 iev;
+	int		 fds[2];
+
+	channel_init(&iev, &receiver, fds);
+	imsgev_regress_reset();
+	imsgev_regress_fail(IMSGEV_REGRESS_EVENT_DEL, 1);
+	imsgev_send(&iev, TEST_DATA, 0, NULL, 0);
+}
+
+static void
+die_rearm_add(void)
+{
+	struct imsgbuf	 receiver;
+	struct imsgev	 iev;
+	int		 fds[2];
+
+	channel_init(&iev, &receiver, fds);
+	imsgev_regress_reset();
+	imsgev_regress_fail(IMSGEV_REGRESS_EVENT_ADD, 1);
+	imsgev_send(&iev, TEST_DATA, 0, NULL, 0);
+}
+
+static void
+die_clear_del(void)
+{
+	struct imsgbuf	 receiver;
+	struct imsgev	 iev;
+	int		 fds[2];
+
+	channel_init(&iev, &receiver, fds);
+	imsgev_regress_reset();
+	imsgev_regress_fail(IMSGEV_REGRESS_EVENT_DEL, 1);
+	imsgev_clear(&iev);
+}
+
+static int
+failed_sequence(unsigned int fail_at, uint32_t *trace, size_t *trace_len)
+{
+	struct imsgbuf	 receiver;
+	struct imsgev	 iev;
+	ssize_t		 n;
+	size_t		 off;
+	pid_t		 pid;
+	int		 devnull, fds[2], pfd[2], status;
+
+	if (pipe(pfd) == -1)
+		err(1, "pipe");
+	fflush(stdout);
+	switch (pid = fork()) {
+	case -1:
+		err(1, "fork");
+	case 0:
+		close(pfd[0]);
+		if ((devnull = open("/dev/null", O_WRONLY)) != -1)
+			(void)dup2(devnull, STDERR_FILENO);
+		channel_init(&iev, &receiver, fds);
+		imsgev_regress_reset();
+		imsgev_regress_trace(pfd[1]);
+		imsgev_regress_fail(IMSGEV_REGRESS_COMPOSE, fail_at);
+		imsgev_send(&iev, TEST_DATA, 0, "a", 1);
+		imsgev_send(&iev, TEST_DATA, 0, "b", 1);
+		imsgev_send(&iev, TEST_END, 0, NULL, 0);
+		_exit(0);
+	default:
+		close(pfd[1]);
+		off = 0;
+		while (off < 4 * sizeof(*trace) &&
+		    (n = read(pfd[0], (u_char *)trace + off,
+		    4 * sizeof(*trace) - off)) > 0)
+			off += (size_t)n;
+		close(pfd[0]);
+		if (waitpid(pid, &status, 0) == -1)
+			err(1, "waitpid");
+		*trace_len = off / sizeof(*trace);
+		return (WIFEXITED(status) && WEXITSTATUS(status) == 1);
+	}
+}
+
 int
 main(void)
 {
 	static const u_char	 binary[] = { 'a', '\0', 'b' };
 	struct imsgev_payload	 payload;
 	struct imsgbuf		 receiver;
+	struct imsgev		 iev;
 	struct imsg		 imsg;
+	uint32_t		 trace[4];
+	size_t			 trace_len;
 	u_char			 byte;
+	int			 fds[2];
 
+	log_init(LOG_TO_STDERR, 0, LOG_USER);
+	if (event_init() == NULL)
+		errx(1, "event_init");
+
 	make_imsg(NULL, 0, &receiver, &imsg);
 	payload.data = (const u_char *)1;
 	payload.len = 1;
@@ -93,5 +271,80 @@ main(void)
 	imsg_free(&imsg);
 	imsgbuf_clear(&receiver);
 
+	/* A normal send queues one exact empty frame and arms EV_WRITE. */
+	channel_init(&iev, &receiver, fds);
+	imsgev_send(&iev, TEST_DATA, 7, NULL, 0);
+	CHECK(imsgbuf_queuelen(&iev.ibuf) == 1);
+	CHECK(iev.events == (EV_READ | EV_WRITE));
+	CHECK(receive_one(&iev, &receiver, &imsg) == 1);
+	CHECK(imsg.hdr.type == TEST_DATA && imsg.hdr.peerid == 7);
+	CHECK(imsg_get_len(&imsg) == 0);
+	imsg_free(&imsg);
+	channel_clear(&iev, &receiver, fds);
+
+	/* Embedded NULs remain bytes across the checked-send Interface. */
+	channel_init(&iev, &receiver, fds);
+	imsgev_send(&iev, TEST_DATA, 9, binary, sizeof(binary));
+	CHECK(imsgbuf_queuelen(&iev.ibuf) == 1);
+	CHECK(iev.events == (EV_READ | EV_WRITE));
+	CHECK(receive_one(&iev, &receiver, &imsg) == 1);
+	CHECK(imsg.hdr.type == TEST_DATA && imsg.hdr.peerid == 9);
+	CHECK(imsgev_get_payload(&imsg, &payload) == 0);
+	CHECK(payload.len == sizeof(binary) &&
+	    memcmp(payload.data, binary, sizeof(binary)) == 0);
+	imsg_free(&imsg);
+	channel_clear(&iev, &receiver, fds);
+
+	/* Every checked event operation and composition failure is fatal. */
+	CHECK(dies(die_init_add));
+	CHECK(dies(die_compose));
+	CHECK(dies(die_rearm_del));
+	CHECK(dies(die_rearm_add));
+	CHECK(dies(die_clear_del));
+
+	/* A failed chunk stops the exchange before any terminal send. */
+	CHECK(failed_sequence(1, trace, &trace_len));
+	CHECK(trace_len == 1 && trace[0] == TEST_DATA);
+	CHECK(failed_sequence(2, trace, &trace_len));
+	CHECK(trace_len == 2 && trace[0] == TEST_DATA &&
+	    trace[1] == TEST_DATA);
+	CHECK(failed_sequence(3, trace, &trace_len));
+	CHECK(trace_len == 3 && trace[0] == TEST_DATA &&
+	    trace[1] == TEST_DATA && trace[2] == TEST_END);
+
+	/* Teardown is synchronous, best effort, and never re-arms events. */
+	channel_init(&iev, &receiver, fds);
+	imsgev_regress_reset();
+	imsgev_send_teardown(&iev, TEST_END);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_COMPOSE) == 1);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_FLUSH) == 1);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_EVENT_ADD) == 0);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_EVENT_DEL) == 0);
+	CHECK(imsgbuf_read(&receiver) == 1);
+	CHECK(imsgbuf_get(&receiver, &imsg) == 1 &&
+	    imsg.hdr.type == TEST_END && imsg_get_len(&imsg) == 0);
+	imsg_free(&imsg);
+	channel_clear(&iev, &receiver, fds);
+
+	channel_init(&iev, &receiver, fds);
+	imsgev_regress_reset();
+	imsgev_regress_fail(IMSGEV_REGRESS_COMPOSE, 1);
+	imsgev_send_teardown(&iev, TEST_END);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_COMPOSE) == 1);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_FLUSH) == 0);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_EVENT_ADD) == 0);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_EVENT_DEL) == 0);
+	channel_clear(&iev, &receiver, fds);
+
+	channel_init(&iev, &receiver, fds);
+	imsgev_regress_reset();
+	imsgev_regress_fail(IMSGEV_REGRESS_FLUSH, 1);
+	imsgev_send_teardown(&iev, TEST_END);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_COMPOSE) == 1);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_FLUSH) == 1);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_EVENT_ADD) == 0);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_EVENT_DEL) == 0);
+	channel_clear(&iev, &receiver, fds);
+
 	REGRESS_END();
 }
blob - /dev/null
blob + 12829ba9ecd954ba6f7469ad64678f1a2ff6585e (mode 644)
--- /dev/null
+++ regress/imsgev/imsgev_regress.c
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * Compile the production implementation against regression-private
+ * transport adapters.  The public imsgev Interface stays free of failure
+ * injection while this internal seam can force every checked operation.
+ */
+
+#include <sys/types.h>
+
+#include <event.h>
+#include <errno.h>
+#include <imsg.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "imsgev.h"
+#include "imsgev_regress.h"
+
+static struct {
+	enum imsgev_regress_op fail_op;
+	unsigned int	 fail_at;
+	unsigned int	 calls[IMSGEV_REGRESS_OPS];
+	int		 trace_fd;
+} regress_state = {
+	.fail_op = IMSGEV_REGRESS_OPS,
+	.trace_fd = -1
+};
+
+void
+imsgev_regress_reset(void)
+{
+	memset(regress_state.calls, 0, sizeof(regress_state.calls));
+	regress_state.fail_op = IMSGEV_REGRESS_OPS;
+	regress_state.fail_at = 0;
+	regress_state.trace_fd = -1;
+}
+
+void
+imsgev_regress_fail(enum imsgev_regress_op op, unsigned int call)
+{
+	regress_state.fail_op = op;
+	regress_state.fail_at = call;
+}
+
+void
+imsgev_regress_trace(int fd)
+{
+	regress_state.trace_fd = fd;
+}
+
+unsigned int
+imsgev_regress_calls(enum imsgev_regress_op op)
+{
+	return (regress_state.calls[op]);
+}
+
+static int
+regress_fails(enum imsgev_regress_op op)
+{
+	regress_state.calls[op]++;
+	if (regress_state.fail_op == op &&
+	    regress_state.calls[op] == regress_state.fail_at) {
+		errno = EIO;
+		return (1);
+	}
+	return (0);
+}
+
+static int
+regress_imsg_compose(struct imsgbuf *ibuf, uint32_t type, uint32_t id,
+    pid_t pid, int fd, const void *data, size_t len)
+{
+	ssize_t	 n;
+
+	if (regress_state.trace_fd != -1) {
+		n = write(regress_state.trace_fd, &type, sizeof(type));
+		if (n != (ssize_t)sizeof(type)) {
+			errno = EIO;
+			return (-1);
+		}
+	}
+	if (regress_fails(IMSGEV_REGRESS_COMPOSE))
+		return (-1);
+	return (imsg_compose(ibuf, type, id, pid, fd, data, len));
+}
+
+static int
+regress_imsgbuf_flush(struct imsgbuf *ibuf)
+{
+	if (regress_fails(IMSGEV_REGRESS_FLUSH))
+		return (-1);
+	return (imsgbuf_flush(ibuf));
+}
+
+static int
+regress_event_add(struct event *ev, const struct timeval *tv)
+{
+	if (regress_fails(IMSGEV_REGRESS_EVENT_ADD))
+		return (-1);
+	return (event_add(ev, tv));
+}
+
+static int
+regress_event_del(struct event *ev)
+{
+	if (regress_fails(IMSGEV_REGRESS_EVENT_DEL))
+		return (-1);
+	return (event_del(ev));
+}
+
+#define imsg_compose	regress_imsg_compose
+#define imsgbuf_flush	regress_imsgbuf_flush
+#define event_add	regress_event_add
+#define event_del	regress_event_del
+
+#include "../../src/common/imsgev.c"
+
+#undef imsg_compose
+#undef imsgbuf_flush
+#undef event_add
+#undef event_del
blob - /dev/null
blob + 5feb4bd80968781b2a2b5843315aae47adfcd4fd (mode 644)
--- /dev/null
+++ regress/imsgev/imsgev_regress.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IMSGEV_REGRESS_H
+#define IMSGEV_REGRESS_H
+
+enum imsgev_regress_op {
+	IMSGEV_REGRESS_COMPOSE,
+	IMSGEV_REGRESS_FLUSH,
+	IMSGEV_REGRESS_EVENT_ADD,
+	IMSGEV_REGRESS_EVENT_DEL,
+	IMSGEV_REGRESS_OPS
+};
+
+void	 imsgev_regress_reset(void);
+void	 imsgev_regress_fail(enum imsgev_regress_op, unsigned int);
+void	 imsgev_regress_trace(int);
+unsigned int imsgev_regress_calls(enum imsgev_regress_op);
+
+#endif /* IMSGEV_REGRESS_H */
blob - eae76c222a9645b9bd3757daa9fcf0204a9b45c7
blob + a871534c0365bae20aa5ea5ee4e649d5bc73f8dd
--- src/common/imsgev.c
+++ src/common/imsgev.c
@@ -35,7 +35,8 @@ imsgev_init(struct imsgev *iev, int fd,
 	iev->data = data;
 	iev->events = EV_READ;
 	event_set(&iev->ev, fd, iev->events, handler, data);
-	event_add(&iev->ev, NULL);
+	if (event_add(&iev->ev, NULL) == -1)
+		fatal("event_add");
 }
 
 /*
@@ -49,23 +50,35 @@ imsgev_add(struct imsgev *iev)
 	if (imsgbuf_queuelen(&iev->ibuf) > 0)
 		iev->events |= EV_WRITE;
 
-	event_del(&iev->ev);
+	if (event_del(&iev->ev) == -1)
+		fatal("event_del");
 	event_set(&iev->ev, iev->ibuf.fd, iev->events, iev->handler,
 	    iev->data);
-	event_add(&iev->ev, NULL);
+	if (event_add(&iev->ev, NULL) == -1)
+		fatal("event_add");
 }
 
-int
-imsgev_compose(struct imsgev *iev, uint32_t type, uint32_t peerid,
-    pid_t pid, int fd, const void *data, size_t datalen)
+void
+imsgev_send(struct imsgev *iev, uint32_t type, uint32_t peerid,
+    const void *data, size_t datalen)
 {
-	int	 ret;
+	if (imsg_compose(&iev->ibuf, type, peerid, -1, -1, data,
+	    datalen) == -1)
+		fatal("imsg_compose");
+	imsgev_add(iev);
+}
 
-	ret = imsg_compose(&iev->ibuf, type, peerid, pid, fd,
-	    data, datalen);
-	if (ret != -1)
-		imsgev_add(iev);
-	return (ret);
+/*
+ * Teardown has no event-loop future in which queued output can drain.
+ * Attempt one synchronous empty frame and flush, but never turn cleanup
+ * failure into a second fatal path while the Role is already exiting.
+ */
+void
+imsgev_send_teardown(struct imsgev *iev, uint32_t type)
+{
+	if (imsg_compose(&iev->ibuf, type, 0, -1, -1, NULL, 0) == -1)
+		return;
+	(void)imsgbuf_flush(&iev->ibuf);
 }
 
 int
@@ -99,6 +112,7 @@ imsgev_get_payload(struct imsg *imsg, struct imsgev_pa
 void
 imsgev_clear(struct imsgev *iev)
 {
-	event_del(&iev->ev);
+	if (event_del(&iev->ev) == -1)
+		fatal("event_del");
 	imsgbuf_clear(&iev->ibuf);
 }
blob - c19e3d504553e550f77a3268b8d2a63617840730
blob + 17920eec05b8c5a24a21b4d3d47c72941dd0298f
--- src/common/imsgev.h
+++ src/common/imsgev.h
@@ -46,8 +46,11 @@ struct imsgev_payload {
 void	imsgev_init(struct imsgev *, int, void (*)(int, short, void *),
 	    void *);
 void	imsgev_add(struct imsgev *);
-int	imsgev_compose(struct imsgev *, uint32_t, uint32_t, pid_t, int,
-	    const void *, size_t);
+/* Normal sends are checked and own the no-fd/no-pid transport policy. */
+void	imsgev_send(struct imsgev *, uint32_t, uint32_t, const void *,
+	    size_t);
+/* Teardown attempts one synchronous empty frame without re-arming events. */
+void	imsgev_send_teardown(struct imsgev *, uint32_t);
 int	imsgev_get_payload(struct imsg *, struct imsgev_payload *);
 void	imsgev_clear(struct imsgev *);
 
blob - fb18c42b8dae64d769db946ee08450cff6456cce
blob + 3bec9f667fc5ff3e063fd8573fc16846cb499c22
--- src/common/worker.c
+++ src/common/worker.c
@@ -114,7 +114,7 @@ selftest(int action)
 
 	/* If we are still alive, the action was not pledge-killed;
 	 * report the syscall outcome for the test to judge. */
-	imsgev_compose(&worker->iev, FUGU_IMSG_SELFTEST_RESULT, 0, -1, -1,
+	imsgev_send(&worker->iev, FUGU_IMSG_SELFTEST_RESULT, 0,
 	    &r, sizeof(r));
 }
 
@@ -177,14 +177,10 @@ worker_dispatch(int fd, short event, void *arg)
 			if (w->lockdown(w, &imsg) == -1)
 				fatalx("%s: lockdown failed", w->title);
 			w->ready = 1;
-			if (imsgev_compose(&w->iev, FUGU_IMSG_READY, 0, -1,
-			    -1, NULL, 0) == -1)
-				fatal("compose READY");
+			imsgev_send(&w->iev, FUGU_IMSG_READY, 0, NULL, 0);
 			break;
 		case FUGU_IMSG_PING:
-			if (imsgev_compose(&w->iev, FUGU_IMSG_PONG, 0, -1,
-			    -1, NULL, 0) == -1)
-				fatal("compose PONG");
+			imsgev_send(&w->iev, FUGU_IMSG_PONG, 0, NULL, 0);
 			break;
 		case FUGU_IMSG_LOG_SYSLOG:
 			if (imsg_get_len(&imsg) != 0)
blob - 6a041592b8100a7964a10178d9a337589172090f
blob + 23e224028b672b78a69570779b65f39049b5ca2e
--- src/fugu/coord.c
+++ src/fugu/coord.c
@@ -357,9 +357,7 @@ tool_turn_ctl(struct coord *c, uint32_t type)
 		fatalx("nested tool Turn control");
 	c->tool_ctl_expected = type;
 	c->tool_ctl_done = 0;
-	if (imsgev_compose(&c->iev[ROLE_TOOL], type, 0, -1, -1,
-	    NULL, 0) == -1)
-		fatal("compose tool Turn control");
+	imsgev_send(&c->iev[ROLE_TOOL], type, 0, NULL, 0);
 	while (!c->tool_ctl_done) {
 		c->wait = WAIT_TOOL_CTL;
 		rc = event_dispatch();
@@ -668,9 +666,7 @@ send_tool_hello(struct coord *c, struct imsgev *iev, i
 	h.agent = agent;
 	h.protect_snapshot = 1;
 	h.protect_len = c->protect_len;
-	if (imsgev_compose(iev, FUGU_IMSG_HELLO, 0, -1, -1, &h,
-	    sizeof(h)) == -1)
-		fatal("compose tool HELLO");
+	imsgev_send(iev, FUGU_IMSG_HELLO, 0, &h, sizeof(h));
 }
 
 static void
@@ -686,9 +682,7 @@ send_hello(struct coord *c, enum fugu_role role)
 		h.cols = 80;
 		h.rows = 24;
 		h.curses = c->ui;	/* only start curses in terminal mode */
-		if (imsgev_compose(iev, FUGU_IMSG_HELLO, 0, -1, -1, &h,
-		    sizeof(h)) == -1)
-			fatal("compose tty HELLO");
+		imsgev_send(iev, FUGU_IMSG_HELLO, 0, &h, sizeof(h));
 		break;
 	}
 	case ROLE_API: {
@@ -714,11 +708,7 @@ send_hello(struct coord *c, enum fugu_role role)
 		buf_add(&b, &h, sizeof(h));
 		if (key != NULL)
 			buf_add(&b, key, strlen(key));
-		if (imsgev_compose(iev, FUGU_IMSG_HELLO, 0, -1, -1, b.data,
-		    b.len) == -1) {
-			buf_freezero(&b);
-			fatal("compose api HELLO");
-		}
+		imsgev_send(iev, FUGU_IMSG_HELLO, 0, b.data, b.len);
 		buf_freezero(&b);	/* the key traversed this buffer */
 
 		/* every named provider block follows: the custodian holds
@@ -745,11 +735,7 @@ send_hello(struct coord *c, enum fugu_role role)
 			buf_add(&b, &hp, sizeof(hp));
 			if (fp->api_key != NULL)
 				buf_add(&b, fp->api_key, strlen(fp->api_key));
-			if (imsgev_compose(iev, FUGU_IMSG_PROV, 0, -1, -1,
-			    b.data, b.len) == -1) {
-				buf_freezero(&b);
-				fatal("compose provider");
-			}
+			imsgev_send(iev, FUGU_IMSG_PROV, 0, b.data, b.len);
 			buf_freezero(&b);
 		}
 		if (c->conf->initial_provider_slot > 0) {
@@ -757,9 +743,8 @@ send_hello(struct coord *c, enum fugu_role role)
 
 			memset(&sel, 0, sizeof(sel));
 			sel.slot = c->conf->initial_provider_slot;
-			if (imsgev_compose(iev, FUGU_IMSG_SET_PROVIDER, 0, -1,
-			    -1, &sel, sizeof(sel)) == -1)
-				fatal("compose initial provider");
+			imsgev_send(iev, FUGU_IMSG_SET_PROVIDER, 0, &sel,
+			    sizeof(sel));
 		}
 		break;
 	}
@@ -794,16 +779,12 @@ send_hello(struct coord *c, enum fugu_role role)
 		buf_add(&b, token, h.tokenlen);
 		buf_add(&b, allow, h.allowlen);
 		buf_add(&b, block, h.blocklen);
-		if (imsgev_compose(iev, FUGU_IMSG_HELLO, 0, -1, -1,
-		    b.data, b.len) == -1)
-			fatal("compose web HELLO");
+		imsgev_send(iev, FUGU_IMSG_HELLO, 0, b.data, b.len);
 		buf_freezero(&b);
 		break;
 	}
 	case ROLE_EDIT:
-		if (imsgev_compose(iev, FUGU_IMSG_HELLO, 0, -1, -1,
-		    NULL, 0) == -1)
-			fatal("compose editor HELLO");
+		imsgev_send(iev, FUGU_IMSG_HELLO, 0, NULL, 0);
 		break;
 	case ROLE_TOOL: {
 		send_tool_hello(c, iev, 0);
@@ -835,7 +816,7 @@ tcall_at(struct coord *c, int index)
 static void
 ui_send(struct coord *c, int type, const void *p, size_t len)
 {
-	imsgev_compose(&c->iev[ROLE_TTY], type, 0, -1, -1, p, len);
+	imsgev_send(&c->iev[ROLE_TTY], type, 0, p, len);
 }
 
 static void
@@ -1622,8 +1603,8 @@ ui_event(struct coord *c, struct imsg *imsg)
 			c->conf->provider_type = fp->type;
 		} else
 			fatalx("bad provider slot");
-		imsgev_compose(&c->iev[ROLE_API], FUGU_IMSG_SET_PROVIDER, 0,
-		    -1, -1, &sel, sizeof(sel));
+		imsgev_send(&c->iev[ROLE_API], FUGU_IMSG_SET_PROVIDER, 0,
+		    &sel, sizeof(sel));
 		c->active_provider_slot = sel.slot;
 		c->provider_window = sel.context_window;
 		free(c->conf->model);
@@ -1740,13 +1721,11 @@ compose_event(struct coord *c, struct imsg *imsg)
 		if (c->compose_in + len > FUGU_EDIT_MAX)
 			fatalx("compose seed overrun");
 		c->compose_in += len;
-		imsgev_compose(&c->iev[ROLE_EDIT], FUGU_IMSG_EDIT_SEED, 0,
-		    -1, -1, p, len);
+		imsgev_send(&c->iev[ROLE_EDIT], FUGU_IMSG_EDIT_SEED, 0, p, len);
 		break;
 	case FUGU_IMSG_UI_COMPOSE_GO:
 		c->compose_in = 0;
-		imsgev_compose(&c->iev[ROLE_EDIT], FUGU_IMSG_EDIT_RUN, 0,
-		    -1, -1, NULL, 0);
+		imsgev_send(&c->iev[ROLE_EDIT], FUGU_IMSG_EDIT_RUN, 0, NULL, 0);
 		break;
 	case FUGU_IMSG_EDIT_DATA:
 		if (len == 0)
@@ -1832,14 +1811,10 @@ agent_web_local_error(struct subagent *a, const char *
 
 	if (a->done)
 		return;
-	if (imsgev_compose(&a->iev, FUGU_IMSG_AGENT_WEB_OUT, 0, -1, -1,
-	    s, strlen(s)) == -1)
-		fatal("compose subagent web local error");
+	imsgev_send(&a->iev, FUGU_IMSG_AGENT_WEB_OUT, 0, s, strlen(s));
 	memset(&r, 0, sizeof(r));
 	r.is_error = 1;
-	if (imsgev_compose(&a->iev, FUGU_IMSG_AGENT_WEB_RESULT, 0, -1, -1,
-	    &r, sizeof(r)) == -1)
-		fatal("compose subagent web local result");
+	imsgev_send(&a->iev, FUGU_IMSG_AGENT_WEB_RESULT, 0, &r, sizeof(r));
 }
 
 static void
@@ -1867,23 +1842,19 @@ agent_web_start_next(struct coord *c)
 		a->web_inflight = 1;
 		c->agent_web_active = a;
 		c->web_busy_id = a->web_id;
-		if (imsgev_compose(&c->iev[ROLE_WEB], FUGU_IMSG_WEB_REQ,
-		    a->web_id, -1, -1, &req, sizeof(req)) == -1)
-			fatal("relay subagent WEB_REQ");
+		imsgev_send(&c->iev[ROLE_WEB], FUGU_IMSG_WEB_REQ,
+		    a->web_id, &req, sizeof(req));
 		for (off = 0; off < a->web_arg.len; ) {
 			size_t k = a->web_arg.len - off;
 
 			if (k > FUGU_CHUNK)
 				k = FUGU_CHUNK;
-			if (imsgev_compose(&c->iev[ROLE_WEB],
-			    FUGU_IMSG_WEB_ARG, a->web_id, -1, -1,
-			    a->web_arg.data + off, k) == -1)
-				fatal("relay subagent WEB_ARG");
+			imsgev_send(&c->iev[ROLE_WEB], FUGU_IMSG_WEB_ARG,
+			    a->web_id, a->web_arg.data + off, k);
 			off += k;
 		}
-		if (imsgev_compose(&c->iev[ROLE_WEB], FUGU_IMSG_WEB_RUN,
-		    a->web_id, -1, -1, NULL, 0) == -1)
-			fatal("relay subagent WEB_RUN");
+		imsgev_send(&c->iev[ROLE_WEB], FUGU_IMSG_WEB_RUN,
+		    a->web_id, NULL, 0);
 		agent_web_arg_free(c, a);
 		return;
 	}
@@ -1903,8 +1874,8 @@ agent_provider_event(struct subagent *a, struct imsg *
 		fatal("get relayed provider payload");
 	p = payload.data;
 	len = payload.len;
-	if (!a->done && imsgev_compose(&a->iev, type, 0, -1, -1, p, len) == -1)
-		fatal("relay provider event to subagent");
+	if (!a->done)
+		imsgev_send(&a->iev, type, 0, p, len);
 	if (type == FUGU_IMSG_A_DONE || type == FUGU_IMSG_A_ERROR)
 		a->api_inflight = 0;
 	if (a->coord->wait == WAIT_AGENTS && agents_settled(a->coord))
@@ -1933,9 +1904,8 @@ agent_web_event(struct subagent *a, struct imsg *imsg)
 		outtype = FUGU_IMSG_AGENT_WEB_RESULT;
 	else
 		fatalx("bad subagent web event");
-	if (!a->done && imsgev_compose(&a->iev, outtype, 0, -1, -1, p,
-	    len) == -1)
-		fatal("relay web event to subagent");
+	if (!a->done)
+		imsgev_send(&a->iev, outtype, 0, p, len);
 	if (outtype == FUGU_IMSG_AGENT_WEB_RESULT) {
 		a->web_inflight = 0;
 		a->web_id = 0;
@@ -2010,10 +1980,8 @@ coord_dispatch(int fd, short event, void *arg)
 				log_init(LOG_TO_SYSLOG, coord->verbose,
 				    LOG_DAEMON);
 				for (r = ROLE_TTY; r < ROLE_MAX; r++)
-					if (imsgev_compose(&coord->iev[r],
-					    FUGU_IMSG_LOG_SYSLOG, 0, -1, -1,
-					    NULL, 0) == -1)
-						fatal("compose LOG_SYSLOG");
+					imsgev_send(&coord->iev[r],
+					    FUGU_IMSG_LOG_SYSLOG, 0, NULL, 0);
 			}
 			if (++coord->nready == coord->nkids)
 				event_loopexit(NULL);
@@ -2121,13 +2089,13 @@ send_request(struct coord *c, struct buf *body, uint32
 
 		if (n > FUGU_CHUNK)
 			n = FUGU_CHUNK;
-		imsgev_compose(iev, FUGU_IMSG_REQUEST, id, -1, -1,
+		imsgev_send(iev, FUGU_IMSG_REQUEST, id,
 		    body->data + off, n);
 		off += n;
 	}
 	memset(&end, 0, sizeof(end));
 	end.provider_slot = provider_slot;
-	imsgev_compose(iev, FUGU_IMSG_REQUEST_END, id, -1, -1, &end,
+	imsgev_send(iev, FUGU_IMSG_REQUEST_END, id, &end,
 	    sizeof(end));
 }
 
@@ -2141,17 +2109,17 @@ run_tool(struct coord *c, struct tool_call *tc)
 
 	memset(&req, 0, sizeof(req));
 	strlcpy(req.name, tc->name, sizeof(req.name));
-	imsgev_compose(iev, FUGU_IMSG_TOOL_REQ, 0, -1, -1, &req, sizeof(req));
+	imsgev_send(iev, FUGU_IMSG_TOOL_REQ, 0, &req, sizeof(req));
 	for (off = 0; off < tc->input.len; ) {
 		size_t	 n = tc->input.len - off;
 
 		if (n > FUGU_CHUNK)
 			n = FUGU_CHUNK;
-		imsgev_compose(iev, FUGU_IMSG_TOOL_ARG, 0, -1, -1,
+		imsgev_send(iev, FUGU_IMSG_TOOL_ARG, 0,
 		    tc->input.data + off, n);
 		off += n;
 	}
-	imsgev_compose(iev, FUGU_IMSG_TOOL_RUN, 0, -1, -1, NULL, 0);
+	imsgev_send(iev, FUGU_IMSG_TOOL_RUN, 0, NULL, 0);
 
 	buf_reset(&c->tool_out);
 	buf_reset(&c->diff);
@@ -2195,22 +2163,17 @@ run_web(struct coord *c, struct tool_call *tc)
 	strlcpy(req.name, tc->name, sizeof(req.name));
 	c->web_id = agent_unique_id(c);
 	c->web_busy_id = c->web_id;
-	if (imsgev_compose(iev, FUGU_IMSG_WEB_REQ, c->web_id, -1, -1, &req,
-	    sizeof(req)) == -1)
-		fatal("compose WEB_REQ");
+	imsgev_send(iev, FUGU_IMSG_WEB_REQ, c->web_id, &req, sizeof(req));
 	for (off = 0; off < tc->input.len; ) {
 		size_t	 n = tc->input.len - off;
 
 		if (n > FUGU_CHUNK)
 			n = FUGU_CHUNK;
-		if (imsgev_compose(iev, FUGU_IMSG_WEB_ARG, c->web_id, -1, -1,
-		    tc->input.data + off, n) == -1)
-			fatal("compose WEB_ARG");
+		imsgev_send(iev, FUGU_IMSG_WEB_ARG, c->web_id,
+		    tc->input.data + off, n);
 		off += n;
 	}
-	if (imsgev_compose(iev, FUGU_IMSG_WEB_RUN, c->web_id, -1, -1,
-	    NULL, 0) == -1)
-		fatal("compose WEB_RUN");
+	imsgev_send(iev, FUGU_IMSG_WEB_RUN, c->web_id, NULL, 0);
 
 	c->wait = WAIT_WEB;
 	rc = event_dispatch();
@@ -2241,10 +2204,8 @@ agent_failed(struct subagent *a, const char *reason)
 		agent_web_arg_free(c, a);
 	}
 	if (a->api_assembling || a->api_inflight) {
-		if (imsgev_compose(&c->iev[ROLE_API],
-		    FUGU_IMSG_REQUEST_CANCEL, a->api_id, -1, -1,
-		    NULL, 0) == -1)
-			fatal("compose failed-agent REQUEST_CANCEL");
+		imsgev_send(&c->iev[ROLE_API], FUGU_IMSG_REQUEST_CANCEL,
+		    a->api_id, NULL, 0);
 		a->api_assembling = 0;
 		a->api_inflight = 1;	/* cleared by the cancellation terminal */
 	}
@@ -2340,9 +2301,8 @@ agent_dispatch(int fd, short event, void *arg)
 				fatalx("bad subagent provider request chunk");
 			a->api_assembling = 1;
 			a->request_bytes += len;
-			if (imsgev_compose(&c->iev[ROLE_API], FUGU_IMSG_REQUEST,
-			    a->api_id, -1, -1, p, len) == -1)
-				fatal("relay subagent provider request");
+			imsgev_send(&c->iev[ROLE_API], FUGU_IMSG_REQUEST,
+			    a->api_id, p, len);
 			break;
 		case FUGU_IMSG_AGENT_REQUEST_END: {
 			struct api_request_end end;
@@ -2351,10 +2311,8 @@ agent_dispatch(int fd, short event, void *arg)
 				fatalx("bad subagent provider request end");
 			memset(&end, 0, sizeof(end));
 			end.provider_slot = a->provider_slot;
-			if (imsgev_compose(&c->iev[ROLE_API],
-			    FUGU_IMSG_REQUEST_END, a->api_id, -1, -1, &end,
-			    sizeof(end)) == -1)
-				fatal("relay subagent request end");
+			imsgev_send(&c->iev[ROLE_API], FUGU_IMSG_REQUEST_END,
+			    a->api_id, &end, sizeof(end));
 			a->api_assembling = 0;
 			a->api_inflight = 1;
 			a->request_bytes = 0;
@@ -2524,7 +2482,7 @@ read_personal(struct coord *c)
 static void
 ctx_read_project(struct coord *c, const char *path)
 {
-	imsgev_compose(&c->iev[ROLE_TOOL], FUGU_IMSG_CTX_READ, 0, -1, -1,
+	imsgev_send(&c->iev[ROLE_TOOL], FUGU_IMSG_CTX_READ, 0,
 	    path, strlen(path));
 	buf_reset(&c->ctx_data);
 	c->ctx_done = 0;
@@ -2959,8 +2917,7 @@ agent_compose_bytes(struct imsgev *iev, uint32_t type,
 		n = len - off;
 		if (n > FUGU_CHUNK)
 			n = FUGU_CHUNK;
-		if (imsgev_compose(iev, type, 0, -1, -1, p + off, n) == -1)
-			fatal("compose subagent launch bytes");
+		imsgev_send(iev, type, 0, p + off, n);
 	}
 }
 
@@ -2982,17 +2939,13 @@ agent_start(struct coord *c, struct subagent *a, const
 	buf_addc(&b, '\0');
 	buf_addstr(&b, a->label);
 	buf_addc(&b, '\0');
-	if (imsgev_compose(&a->iev, FUGU_IMSG_AGENT_START, 0, -1, -1,
-	    b.data, b.len) == -1)
-		fatal("compose AGENT_START");
+	imsgev_send(&a->iev, FUGU_IMSG_AGENT_START, 0, b.data, b.len);
 	buf_free(&b);
 	agent_compose_bytes(&a->iev, FUGU_IMSG_AGENT_SYSTEM, system->data,
 	    system->len);
 	agent_compose_bytes(&a->iev, FUGU_IMSG_AGENT_PROMPT, a->prompt.data,
 	    a->prompt.len);
-	if (imsgev_compose(&a->iev, FUGU_IMSG_AGENT_RUN, 0, -1, -1,
-	    NULL, 0) == -1)
-		fatal("compose AGENT_RUN");
+	imsgev_send(&a->iev, FUGU_IMSG_AGENT_RUN, 0, NULL, 0);
 }
 
 static void
@@ -3012,10 +2965,8 @@ agent_wave_cleanup(struct coord *c, int cancel)
 		if (cancel && (a->api_assembling || a->api_inflight)) {
 			cancelled_id_add(c->cancelled_api, &c->ncancelled_api,
 			    a->api_id);
-			if (imsgev_compose(&c->iev[ROLE_API],
-			    FUGU_IMSG_REQUEST_CANCEL, a->api_id, -1, -1,
-			    NULL, 0) == -1)
-				fatal("compose subagent REQUEST_CANCEL");
+			imsgev_send(&c->iev[ROLE_API], FUGU_IMSG_REQUEST_CANCEL,
+			    a->api_id, NULL, 0);
 			a->api_assembling = 0;
 			a->api_inflight = 0;
 		}
@@ -3038,9 +2989,8 @@ agent_wave_cleanup(struct coord *c, int cancel)
 		}
 		if (a->kid.fd >= 0) {
 			if (cancel) {
-				(void)imsgev_compose(&a->iev, FUGU_IMSG_SHUTDOWN, 0,
-				    -1, -1, NULL, 0);
-				(void)imsgbuf_flush(&a->iev.ibuf);
+				imsgev_send_teardown(&a->iev,
+				    FUGU_IMSG_SHUTDOWN);
 				(void)kill(a->kid.pid, SIGTERM);
 			}
 			imsgev_clear(&a->iev);
@@ -3628,7 +3578,7 @@ cmd_capture(struct coord *c, const char *text)
 	buf_addstr(&m, "\n- ");
 	buf_addstr(&m, text);
 	buf_addc(&m, '\n');
-	imsgev_compose(&c->iev[ROLE_TOOL], FUGU_IMSG_CTX_APPEND, 0, -1, -1,
+	imsgev_send(&c->iev[ROLE_TOOL], FUGU_IMSG_CTX_APPEND, 0,
 	    m.data, m.len);
 	buf_free(&m);
 
@@ -3784,8 +3734,8 @@ line_command(struct coord *c, const char *line, int *t
 			c->listed_models = NULL;
 			c->nlisted_models = 0;
 			c->models_collecting = 1;
-			imsgev_compose(&c->iev[ROLE_API],
-			    FUGU_IMSG_LIST_MODELS, 0, -1, -1, NULL, 0);
+			imsgev_send(&c->iev[ROLE_API],
+			    FUGU_IMSG_LIST_MODELS, 0, NULL, 0);
 			emit_note(c, "fetching the model list...");
 		} else
 			fprintf(stderr, "fugu: /model is interactive-only\n");
@@ -4036,9 +3986,7 @@ coord_teardown(struct coord *c)
 	for (role = ROLE_TTY; role < ROLE_MAX; role++) {
 		if (c->kids[role].pid <= 0)
 			continue;
-		(void)imsgev_compose(&c->iev[role], FUGU_IMSG_SHUTDOWN, 0,
-		    -1, -1, NULL, 0);
-		(void)imsgbuf_flush(&c->iev[role].ibuf);
+		imsgev_send_teardown(&c->iev[role], FUGU_IMSG_SHUTDOWN);
 	}
 	/* Give cooperative workers time to dispatch SHUTDOWN and run atexit.
 	 * In particular fugu-tty must restore termios/bracketed paste before a
blob - 2935e1ff82c36ffc61949854b34bf9e6fe29eda1
blob + c6f79ced52336f1652d68757b1b3409f1ade861f
--- src/fugu-api/main.c
+++ src/fugu-api/main.c
@@ -591,8 +591,7 @@ static void
 relay_id(struct worker *w, uint32_t id, uint32_t type, const void *data,
     size_t len)
 {
-	if (imsgev_compose(&w->iev, type, id, -1, -1, data, len) == -1)
-		fatal("compose provider event");
+	imsgev_send(&w->iev, type, id, data, len);
 }
 
 static void
blob - 7840fd7766fb54edcb5b70b8462764f98d184973
blob + a8a09dd372615a9daec10e4c7d8ee5ac4e2ecfdf
--- src/fugu-editor/main.c
+++ src/fugu-editor/main.c
@@ -111,9 +111,7 @@ send_result(struct worker *w, int ok, int reason)
 	memset(&r, 0, sizeof(r));
 	r.ok = ok;
 	r.reason = reason;
-	if (imsgev_compose(&w->iev, FUGU_IMSG_EDIT_RESULT, 0, -1, -1, &r,
-	    sizeof(r)) == -1)
-		fatal("compose EDIT_RESULT");
+	imsgev_send(&w->iev, FUGU_IMSG_EDIT_RESULT, 0, &r, sizeof(r));
 }
 
 /*
@@ -285,9 +283,8 @@ compose(struct worker *w)
 			send_result(w, 0, EDIT_R_OVERSIZE);
 			return;
 		}
-		if (imsgev_compose(&w->iev, FUGU_IMSG_EDIT_DATA, 0, -1, -1,
-		    tmp, (size_t)n) == -1)
-			fatal("compose EDIT_DATA");
+		imsgev_send(&w->iev, FUGU_IMSG_EDIT_DATA, 0, tmp,
+		    (size_t)n);
 		sent += (size_t)n;
 	}
 	close(fd);
blob - 2f07ba49a039d9992f3210584cd7ced3f6ef9042
blob + 07b7f6d8e57a4e1802416d7fe25b7a3b420d0546
--- src/fugu-tool/main.c
+++ src/fugu-tool/main.c
@@ -365,7 +365,7 @@ send_result(struct worker *w, const struct buf *out, c
 
 		if (n > FUGU_CHUNK)
 			n = FUGU_CHUNK;
-		imsgev_compose(&w->iev, FUGU_IMSG_TOOL_OUT, 0, -1, -1,
+		imsgev_send(&w->iev, FUGU_IMSG_TOOL_OUT, 0,
 		    out->data + off, n);
 		off += n;
 	}
@@ -375,13 +375,13 @@ send_result(struct worker *w, const struct buf *out, c
 
 		if (n > FUGU_CHUNK)
 			n = FUGU_CHUNK;
-		imsgev_compose(&w->iev, FUGU_IMSG_TOOL_DIFF, 0, -1, -1,
+		imsgev_send(&w->iev, FUGU_IMSG_TOOL_DIFF, 0,
 		    diff->data + off, n);
 		off += n;
 	}
 	memset(&r, 0, sizeof(r));
 	r.is_error = is_error;
-	imsgev_compose(&w->iev, FUGU_IMSG_TOOL_RESULT, 0, -1, -1, &r,
+	imsgev_send(&w->iev, FUGU_IMSG_TOOL_RESULT, 0, &r,
 	    sizeof(r));
 }
 
@@ -443,9 +443,8 @@ tool_dispatch(struct worker *w, struct imsg *imsg)
 		if (type == FUGU_IMSG_TOOL_TURN_ABORT)
 			cancel_tool = 0;
 		ack.control = type;
-		if (imsgev_compose(&w->iev, FUGU_IMSG_TOOL_TURN_ACK, 0,
-		    -1, -1, &ack, sizeof(ack)) == -1)
-			fatal("compose TURN_ACK");
+		imsgev_send(&w->iev, FUGU_IMSG_TOOL_TURN_ACK, 0, &ack,
+		    sizeof(ack));
 		return (0);
 	}
 	case FUGU_IMSG_TOOL_REQ: {
@@ -496,11 +495,11 @@ tool_dispatch(struct worker *w, struct imsg *imsg)
 
 			if (k > FUGU_CHUNK)
 				k = FUGU_CHUNK;
-			imsgev_compose(&w->iev, FUGU_IMSG_CTX_DATA, 0, -1, -1,
+			imsgev_send(&w->iev, FUGU_IMSG_CTX_DATA, 0,
 			    ctx.data + off, k);
 			off += k;
 		}
-		imsgev_compose(&w->iev, FUGU_IMSG_CTX_END, 0, -1, -1, NULL, 0);
+		imsgev_send(&w->iev, FUGU_IMSG_CTX_END, 0, NULL, 0);
 		buf_free(&ctx);
 		return (0);
 	}
@@ -522,7 +521,7 @@ tool_dispatch(struct worker *w, struct imsg *imsg)
 		r.reason = ctx_file_append(ts->ctx, path, bytes,
 		    len - plen - 1);
 		r.ok = (r.reason == 0);
-		imsgev_compose(&w->iev, FUGU_IMSG_CTX_RESULT, 0, -1, -1, &r,
+		imsgev_send(&w->iev, FUGU_IMSG_CTX_RESULT, 0, &r,
 		    sizeof(r));
 		return (0);
 	}
blob - a3bfff69f738ee02517ed6fd50a3bd136efdecc8
blob + 5606c58654257e575fb61aaa0b53c773f7495e87
--- src/fugu-tty/ui.c
+++ src/fugu-tty/ui.c
@@ -901,10 +901,10 @@ send_chunked(struct ui *u, int chunk_type, int end_typ
 
 		if (n > FUGU_CHUNK)
 			n = FUGU_CHUNK;
-		imsgev_compose(&u->w->iev, chunk_type, 0, -1, -1, p + off, n);
+		imsgev_send(&u->w->iev, chunk_type, 0, p + off, n);
 		off += n;
 	}
-	imsgev_compose(&u->w->iev, end_type, 0, -1, -1, NULL, 0);
+	imsgev_send(&u->w->iev, end_type, 0, NULL, 0);
 }
 
 static void
@@ -1007,11 +1007,11 @@ compose_begin(struct ui *u)
 
 		if (n > FUGU_CHUNK)
 			n = FUGU_CHUNK;
-		imsgev_compose(&u->w->iev, FUGU_IMSG_UI_COMPOSE, 0, -1, -1,
+		imsgev_send(&u->w->iev, FUGU_IMSG_UI_COMPOSE, 0,
 		    text + off, n);
 		off += n;
 	}
-	imsgev_compose(&u->w->iev, FUGU_IMSG_UI_COMPOSE_GO, 0, -1, -1,
+	imsgev_send(&u->w->iev, FUGU_IMSG_UI_COMPOSE_GO, 0,
 	    NULL, 0);
 	u->composing = 1;
 }
@@ -1066,8 +1066,8 @@ drawer_key(struct ui *u, enum ed_key k, uint32_t cp)
 		break;
 	case K_ENTER:
 		if (u->drawer_sel == 0) {	/* a fresh session */
-			imsgev_compose(&u->w->iev, FUGU_IMSG_UI_SWITCH, 0,
-			    -1, -1, NULL, 0);
+			imsgev_send(&u->w->iev, FUGU_IMSG_UI_SWITCH, 0,
+			    NULL, 0);
 			strlcpy(u->notice, "switching...", sizeof(u->notice));
 		} else {
 			struct sess	*s = &u->sess[u->drawer_sel - 1];
@@ -1076,8 +1076,8 @@ drawer_key(struct ui *u, enum ed_key k, uint32_t cp)
 				drawer_close(u);
 				break;
 			}
-			imsgev_compose(&u->w->iev, FUGU_IMSG_UI_SWITCH, 0,
-			    -1, -1, s->id, strlen(s->id));
+			imsgev_send(&u->w->iev, FUGU_IMSG_UI_SWITCH, 0,
+			    s->id, strlen(s->id));
 			strlcpy(u->notice, "switching...", sizeof(u->notice));
 		}
 		break;
@@ -1149,8 +1149,8 @@ pick_key(struct ui *u, enum ed_key k, uint32_t cp)
 		buf_init(&b);
 		buf_add(&b, &sel, sizeof(sel));
 		buf_addstr(&b, m->id);
-		imsgev_compose(&u->w->iev, FUGU_IMSG_UI_SELECT_MODEL, 0,
-		    -1, -1, b.data, b.len);
+		imsgev_send(&u->w->iev, FUGU_IMSG_UI_SELECT_MODEL, 0,
+		    b.data, b.len);
 		buf_free(&b);
 		pick_close(u);
 		break;
@@ -1246,7 +1246,7 @@ handle_result(struct ui *u, enum ed_result r)
 	case R_QUIT:
 		/* ask the coordinator to quit; it will send SHUTDOWN back,
 		 * which restores the terminal and exits cleanly */
-		imsgev_compose(&u->w->iev, FUGU_IMSG_UI_QUIT, 0, -1, -1,
+		imsgev_send(&u->w->iev, FUGU_IMSG_UI_QUIT, 0,
 		    NULL, 0);
 		break;
 	case R_SCROLL_LINE_UP:	scroll_by(u, -1); break;
blob - efcc30a384f2600ea0ffd58567d501f97d1025dc
blob + c2665884939d8193e3321c1b2f77f94048dae9bd
--- src/fugu-web/main.c
+++ src/fugu-web/main.c
@@ -202,16 +202,13 @@ send_result(struct worker *w, uint32_t reqid, const st
 
 		if (n > FUGU_CHUNK)
 			n = FUGU_CHUNK;
-		if (imsgev_compose(&w->iev, FUGU_IMSG_WEB_OUT, reqid, -1, -1,
-		    out->data + off, n) == -1)
-			fatal("compose WEB_OUT");
+		imsgev_send(&w->iev, FUGU_IMSG_WEB_OUT, reqid,
+		    out->data + off, n);
 		off += n;
 	}
 	memset(&r, 0, sizeof(r));
 	r.is_error = is_error;
-	if (imsgev_compose(&w->iev, FUGU_IMSG_WEB_RESULT, reqid, -1, -1, &r,
-	    sizeof(r)) == -1)
-		fatal("compose WEB_RESULT");
+	imsgev_send(&w->iev, FUGU_IMSG_WEB_RESULT, reqid, &r, sizeof(r));
 }
 
 static int