Commit Diff


commit - 6700e6d8bc89354c9145ee84ad4d8a3a666cc8c2
commit + c07b6527d8b68986949d7889496b04f6756cbba1
blob - fe102d4dafbcf9bd82f474cd2053e352e8bcc847
blob + a8022ac95b25226875c4ad607b6180a345ec96b0
--- regress/README
+++ regress/README
@@ -43,8 +43,9 @@ I9  privileges only narrow		sandbox (each role reaches
 					steady pledge before untrusted
 					input; forbidden syscalls trip
 					PLDG violations)
-I10 mutual distrust across roles	imsgev (partially consumed payloads
-					and checked-send failures fail closed);
+I10 mutual distrust across roles	imsgev (partially consumed payloads,
+					checked sends, and bounded chunk
+					failures fail closed);
 					generation (canonical event ordering,
 					bounds, and exactly one terminal);
 					ui_config (typed ordering, bounded staging,
@@ -187,7 +188,8 @@ src/fugu/turn_txn.c			turn_txn (pure ordering plus rea
 					integration); turn (cache/SIGINT path)
 src/fugu-tty/ui_config_rx.c		ui_config; turn (full-palette PTY path)
 src/common/imsgev.c			imsgev (whole-payload views, checked
-					normal/teardown sends, event failures);
+					normal/teardown sends, bounded chunk
+					emission, event failures);
 					sandbox (event runtime)
 src/common/worker.c			sandbox (worker runtime)
 src/fugu-api (request/stream)		turn (correlated concurrent lead/agent
blob - 6c3766888c8fd793a430fc901c944614bfa62815
blob + 76378954702734712cbd8768607882c56c6553d8
--- regress/imsgev/imsgev_test.c
+++ regress/imsgev/imsgev_test.c
@@ -25,6 +25,7 @@
 #include <imsg.h>
 #include <stdint.h>
 #include <stdio.h>
+#include <stdlib.h>
 #include <string.h>
 #include <syslog.h>
 #include <unistd.h>
@@ -94,6 +95,57 @@ receive_one(struct imsgev *iev, struct imsgbuf *receiv
 	return (imsgbuf_get(receiver, imsg));
 }
 
+static void
+check_chunks(size_t len)
+{
+	struct imsgev_payload payload;
+	struct imsgbuf	 receiver;
+	struct imsgev	 iev;
+	struct imsg	 imsg;
+	u_char		*data;
+	size_t		 frames, i, n, off;
+	int		 fds[2], space = 128 * 1024;
+
+	data = malloc(len);
+	if (data == NULL)
+		err(1, "malloc chunk test");
+	for (i = 0; i < len; i++)
+		data[i] = (u_char)((i * 37 + 11) & 0xff);
+	frames = 1 + (len - 1) / IMSGEV_CHUNK_MAX;
+	channel_init(&iev, &receiver, fds);
+	if (setsockopt(fds[0], SOL_SOCKET, SO_SNDBUF, &space,
+	    sizeof(space)) == -1 ||
+	    setsockopt(fds[1], SOL_SOCKET, SO_RCVBUF, &space,
+	    sizeof(space)) == -1)
+		err(1, "setsockopt chunk test");
+	imsgev_regress_reset();
+	imsgev_send_chunks(&iev, TEST_DATA, 0x12345678, data, len);
+	CHECK(imsgbuf_queuelen(&iev.ibuf) == frames);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_COMPOSE) == frames);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_EVENT_DEL) == frames);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_EVENT_ADD) == frames);
+	if (imsgbuf_flush(&iev.ibuf) == -1)
+		err(1, "imsgbuf_flush chunks");
+	for (i = off = 0; i < frames; i++, off += n) {
+		while (imsgbuf_get(&receiver, &imsg) == 0)
+			if (imsgbuf_read(&receiver) != 1)
+				errx(1, "imsgbuf_read chunks");
+		n = len - off;
+		if (n > IMSGEV_CHUNK_MAX)
+			n = IMSGEV_CHUNK_MAX;
+		CHECK(imsg.hdr.type == TEST_DATA &&
+		    imsg.hdr.peerid == 0x12345678);
+		CHECK(imsgev_get_payload(&imsg, &payload) == 0);
+		CHECK(payload.len == n && payload.data != NULL &&
+		    memcmp(payload.data, data + off, n) == 0);
+		imsg_free(&imsg);
+	}
+	CHECK(off == len);
+	CHECK(imsgbuf_get(&receiver, &imsg) == 0);
+	channel_clear(&iev, &receiver, fds);
+	free(data);
+}
+
 static int
 dies(void (*fn)(void))
 {
@@ -182,8 +234,10 @@ die_clear_del(void)
 }
 
 static int
-failed_sequence(unsigned int fail_at, uint32_t *trace, size_t *trace_len)
+failed_exchange(enum imsgev_regress_op op, unsigned int fail_at,
+    int null_payload, uint32_t *trace, size_t *trace_len)
 {
+	u_char		 data[2 * IMSGEV_CHUNK_MAX + 1];
 	struct imsgbuf	 receiver;
 	struct imsgev	 iev;
 	ssize_t		 n;
@@ -204,9 +258,11 @@ failed_sequence(unsigned int fail_at, uint32_t *trace,
 		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);
+		if (null_payload)
+			imsgev_send_chunks(&iev, TEST_DATA, 0, NULL, 1);
+		memset(data, 0xa5, sizeof(data));
+		imsgev_regress_fail(op, fail_at);
+		imsgev_send_chunks(&iev, TEST_DATA, 0, data, sizeof(data));
 		imsgev_send(&iev, TEST_END, 0, NULL, 0);
 		_exit(0);
 	default:
@@ -241,6 +297,28 @@ main(void)
 	if (event_init() == NULL)
 		errx(1, "event_init");
 
+	/* Empty payloads emit no data frames; the exchange owns its terminal. */
+	channel_init(&iev, &receiver, fds);
+	imsgev_regress_reset();
+	imsgev_send_chunks(&iev, TEST_DATA, 7, NULL, 0);
+	imsgev_send_chunks(&iev, TEST_DATA, 7, binary, 0);
+	imsgev_send(&iev, TEST_END, 7, NULL, 0);
+	CHECK(imsgbuf_queuelen(&iev.ibuf) == 1);
+	CHECK(imsgev_regress_calls(IMSGEV_REGRESS_COMPOSE) == 1);
+	CHECK(receive_one(&iev, &receiver, &imsg) == 1);
+	CHECK(imsg.hdr.type == TEST_END && imsg.hdr.peerid == 7 &&
+	    imsg_get_len(&imsg) == 0);
+	imsg_free(&imsg);
+	channel_clear(&iev, &receiver, fds);
+
+	/* Exact boundaries preserve bytes, frame size, type, and correlation. */
+	check_chunks(1);
+	check_chunks(IMSGEV_CHUNK_MAX - 1);
+	check_chunks(IMSGEV_CHUNK_MAX);
+	check_chunks(IMSGEV_CHUNK_MAX + 1);
+	check_chunks(2 * IMSGEV_CHUNK_MAX);
+	check_chunks(2 * IMSGEV_CHUNK_MAX + 1);
+
 	make_imsg(NULL, 0, &receiver, &imsg);
 	payload.data = (const u_char *)1;
 	payload.len = 1;
@@ -302,16 +380,38 @@ main(void)
 	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));
+	/* NULL/nonzero is rejected before even the first composition. */
+	CHECK(failed_exchange(IMSGEV_REGRESS_OPS, 0, 1, trace, &trace_len));
+	CHECK(trace_len == 0);
+
+	/* A failed data chunk stops the exchange before any later terminal. */
+	CHECK(failed_exchange(IMSGEV_REGRESS_COMPOSE, 1, 0,
+	    trace, &trace_len));
 	CHECK(trace_len == 1 && trace[0] == TEST_DATA);
-	CHECK(failed_sequence(2, trace, &trace_len));
+	CHECK(failed_exchange(IMSGEV_REGRESS_COMPOSE, 2, 0,
+	    trace, &trace_len));
 	CHECK(trace_len == 2 && trace[0] == TEST_DATA &&
 	    trace[1] == TEST_DATA);
-	CHECK(failed_sequence(3, trace, &trace_len));
+	CHECK(failed_exchange(IMSGEV_REGRESS_COMPOSE, 3, 0,
+	    trace, &trace_len));
 	CHECK(trace_len == 3 && trace[0] == TEST_DATA &&
-	    trace[1] == TEST_DATA && trace[2] == TEST_END);
+	    trace[1] == TEST_DATA && trace[2] == TEST_DATA);
+	CHECK(failed_exchange(IMSGEV_REGRESS_COMPOSE, 4, 0,
+	    trace, &trace_len));
+	CHECK(trace_len == 4 && trace[0] == TEST_DATA &&
+	    trace[1] == TEST_DATA && trace[2] == TEST_DATA &&
+	    trace[3] == TEST_END);
 
+	/* Every chunk inherits the checked event re-arm policy. */
+	CHECK(failed_exchange(IMSGEV_REGRESS_EVENT_DEL, 2, 0,
+	    trace, &trace_len));
+	CHECK(trace_len == 2 && trace[0] == TEST_DATA &&
+	    trace[1] == TEST_DATA);
+	CHECK(failed_exchange(IMSGEV_REGRESS_EVENT_ADD, 2, 0,
+	    trace, &trace_len));
+	CHECK(trace_len == 2 && trace[0] == TEST_DATA &&
+	    trace[1] == TEST_DATA);
+
 	/* Teardown is synchronous, best effort, and never re-arms events. */
 	channel_init(&iev, &receiver, fds);
 	imsgev_regress_reset();
blob - a871534c0365bae20aa5ea5ee4e649d5bc73f8dd
blob + f08725e30d20ea6b7831cbda22421c098dea831d
--- src/common/imsgev.c
+++ src/common/imsgev.c
@@ -68,6 +68,23 @@ imsgev_send(struct imsgev *iev, uint32_t type, uint32_
 	imsgev_add(iev);
 }
 
+void
+imsgev_send_chunks(struct imsgev *iev, uint32_t type, uint32_t peerid,
+    const void *data, size_t datalen)
+{
+	const u_char	*p = data;
+	size_t		 off, n;
+
+	if (data == NULL && datalen != 0)
+		fatalx("NULL chunk payload");
+	for (off = 0; off < datalen; off += n) {
+		n = datalen - off;
+		if (n > IMSGEV_CHUNK_MAX)
+			n = IMSGEV_CHUNK_MAX;
+		imsgev_send(iev, type, peerid, p + off, n);
+	}
+}
+
 /*
  * Teardown has no event-loop future in which queued output can drain.
  * Attempt one synchronous empty frame and flush, but never turn cleanup
blob - 17920eec05b8c5a24a21b4d3d47c72941dd0298f
blob + 1ceb3f1eda3dd126f60dcacb3cb3e8137bf036e9
--- src/common/imsgev.h
+++ src/common/imsgev.h
@@ -20,6 +20,9 @@
 #include <event.h>
 #include <imsg.h>
 
+/* A conservative payload slice below imsg's 16 KiB frame ceiling. */
+#define IMSGEV_CHUNK_MAX	(8 * 1024)
+
 /*
  * The imsg-over-libevent wrapper, in the vmd proc.c lineage: one
  * struct per channel, write interest re-armed only when output is
@@ -49,6 +52,12 @@ void	imsgev_add(struct imsgev *);
 /* 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);
+/*
+ * Emit a contiguous payload as checked bounded frames.  A zero length emits
+ * none; a nonzero length requires data.  Typed callers own terminal frames.
+ */
+void	imsgev_send_chunks(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 *);
blob - b995cdd31bd24787f89b407a6beeb22d9b533c9f
blob + 942a23cf5d400d1e79a25735bb7088635fd8235f
--- src/fugu/coord.c
+++ src/fugu/coord.c
@@ -57,6 +57,7 @@
 #define MAX_TOOL_CALLS	64		/* tool_use blocks per assistant msg */
 #define MODELS_RELAY_MAX 4096		/* picker entries per listing (I10) */
 #define AGENT_WEB_MEMORY_MAX (64 * 1024 * 1024)
+#define CAPTURE_TEXT_MAX (8 * 1024)	/* one bounded CTX_APPEND frame */
 
 /* The /compact summarization instruction (behavior.md section 3). */
 #define COMPACT_INSTR \
@@ -1833,7 +1834,6 @@ agent_web_start_next(struct coord *c)
 {
 	struct subagent *a;
 	struct tool_req req;
-	size_t off;
 	int i;
 
 	if (c->agent_web_active != NULL || c->web_busy_id != 0)
@@ -1855,15 +1855,8 @@ agent_web_start_next(struct coord *c)
 		c->web_busy_id = a->web_id;
 		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;
-			imsgev_send(&c->iev[ROLE_WEB], FUGU_IMSG_WEB_ARG,
-			    a->web_id, a->web_arg.data + off, k);
-			off += k;
-		}
+		imsgev_send_chunks(&c->iev[ROLE_WEB], FUGU_IMSG_WEB_ARG,
+		    a->web_id, a->web_arg.data, a->web_arg.len);
 		imsgev_send(&c->iev[ROLE_WEB], FUGU_IMSG_WEB_RUN,
 		    a->web_id, NULL, 0);
 		agent_web_arg_free(c, a);
@@ -2093,17 +2086,9 @@ send_request(struct coord *c, struct buf *body, uint32
 {
 	struct imsgev	*iev = &c->iev[ROLE_API];
 	struct api_request_end end;
-	size_t		 off;
 
-	for (off = 0; off < body->len; ) {
-		size_t	 n = body->len - off;
-
-		if (n > FUGU_CHUNK)
-			n = FUGU_CHUNK;
-		imsgev_send(iev, FUGU_IMSG_REQUEST, id,
-		    body->data + off, n);
-		off += n;
-	}
+	imsgev_send_chunks(iev, FUGU_IMSG_REQUEST, id,
+	    body->data, body->len);
 	memset(&end, 0, sizeof(end));
 	end.provider_slot = provider_slot;
 	imsgev_send(iev, FUGU_IMSG_REQUEST_END, id, &end,
@@ -2116,20 +2101,12 @@ run_tool(struct coord *c, struct tool_call *tc)
 {
 	struct imsgev	*iev = &c->iev[ROLE_TOOL];
 	struct tool_req	 req;
-	size_t		 off;
 
 	memset(&req, 0, sizeof(req));
 	strlcpy(req.name, tc->name, sizeof(req.name));
 	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_send(iev, FUGU_IMSG_TOOL_ARG, 0,
-		    tc->input.data + off, n);
-		off += n;
-	}
+	imsgev_send_chunks(iev, FUGU_IMSG_TOOL_ARG, 0,
+	    tc->input.data, tc->input.len);
 	imsgev_send(iev, FUGU_IMSG_TOOL_RUN, 0, NULL, 0);
 
 	buf_reset(&c->tool_out);
@@ -2148,7 +2125,6 @@ run_web(struct coord *c, struct tool_call *tc)
 {
 	struct imsgev	*iev = &c->iev[ROLE_WEB];
 	struct tool_req	 req;
-	size_t		 off;
 	int		 rc;
 
 	buf_reset(&c->tool_out);
@@ -2175,15 +2151,8 @@ run_web(struct coord *c, struct tool_call *tc)
 	c->web_id = agent_unique_id(c);
 	c->web_busy_id = c->web_id;
 	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;
-		imsgev_send(iev, FUGU_IMSG_WEB_ARG, c->web_id,
-		    tc->input.data + off, n);
-		off += n;
-	}
+	imsgev_send_chunks(iev, FUGU_IMSG_WEB_ARG, c->web_id,
+	    tc->input.data, tc->input.len);
 	imsgev_send(iev, FUGU_IMSG_WEB_RUN, c->web_id, NULL, 0);
 
 	c->wait = WAIT_WEB;
@@ -2918,21 +2887,6 @@ agent_system(struct coord *c, struct buf *out)
 }
 
 static void
-agent_compose_bytes(struct imsgev *iev, uint32_t type, const void *data,
-    size_t len)
-{
-	const u_char *p = data;
-	size_t off, n;
-
-	for (off = 0; off < len; off += n) {
-		n = len - off;
-		if (n > FUGU_CHUNK)
-			n = FUGU_CHUNK;
-		imsgev_send(iev, type, 0, p + off, n);
-	}
-}
-
-static void
 agent_start(struct coord *c, struct subagent *a, const struct buf *system)
 {
 	struct agent_start start;
@@ -2952,9 +2906,9 @@ agent_start(struct coord *c, struct subagent *a, const
 	buf_addc(&b, '\0');
 	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,
+	imsgev_send_chunks(&a->iev, FUGU_IMSG_AGENT_SYSTEM, 0, system->data,
 	    system->len);
-	agent_compose_bytes(&a->iev, FUGU_IMSG_AGENT_PROMPT, a->prompt.data,
+	imsgev_send_chunks(&a->iev, FUGU_IMSG_AGENT_PROMPT, 0, a->prompt.data,
 	    a->prompt.len);
 	imsgev_send(&a->iev, FUGU_IMSG_AGENT_RUN, 0, NULL, 0);
 }
@@ -3577,7 +3531,7 @@ cmd_capture(struct coord *c, const char *text)
 		return (1);
 	}
 	/* the whole append must fit one imsg frame; keep well under it */
-	if (strlen(text) > FUGU_CHUNK) {
+	if (strlen(text) > CAPTURE_TEXT_MAX) {
 		emit_error(c, "fugu: capture line too long");
 		return (1);
 	}
blob - 34516048ceae9b0faa99bce0196bb2d6681151d6
blob + 37523910880611bd05f9b3059637d3487170f0a5
--- src/fugu/proto.h
+++ src/fugu/proto.h
@@ -60,9 +60,6 @@
 #define PROVIDER_ANTHROPIC	0
 #define PROVIDER_OPENAI		1
 
-/* Body chunk size for the REQUEST stream (below the 16 KiB imsg cap). */
-#define FUGU_CHUNK	(8 * 1024)
-
 /* Credentials cross one HELLO frame and later become HTTP headers. */
 #define FUGU_SECRET_MAX	(4 * 1024)
 
@@ -93,7 +90,7 @@
 #define FUGU_TOOL_NAME_MAX	32
 
 /* The network worker uses its own authority-separated exchange. */
-#define FUGU_WEB_HELLO_MAX	FUGU_CHUNK
+#define FUGU_WEB_HELLO_MAX	(8 * 1024)
 #define FUGU_WEB_ARG_MAX	FUGU_TOOL_ARG_MAX
 #define FUGU_WEB_OUT_MAX	(256 * 1024)
 
blob - c6f79ced52336f1652d68757b1b3409f1ade861f
blob + 3b8b8723d9c7524969a9ff6212020e3950c0a91f
--- src/fugu-api/main.c
+++ src/fugu-api/main.c
@@ -603,15 +603,7 @@ relay(struct api_request *r, uint32_t type, const void
 static void
 relay_bytes(struct api_request *r, uint32_t type, const void *data, size_t len)
 {
-	const u_char	*p = data;
-	size_t		 off, n;
-
-	for (off = 0; off < len; off += n) {
-		n = len - off;
-		if (n > FUGU_CHUNK)
-			n = FUGU_CHUNK;
-		relay(r, type, p + off, n);
-	}
+	imsgev_send_chunks(&r->worker->iev, type, r->id, data, len);
 }
 
 static void
@@ -624,8 +616,8 @@ relay_tool_bytes(struct api_request *r, int index, con
 		struct buf	 b;
 
 		n = len - off;
-		if (n > FUGU_CHUNK)
-			n = FUGU_CHUNK;
+		if (n > IMSGEV_CHUNK_MAX)
+			n = IMSGEV_CHUNK_MAX;
 		buf_init(&b);
 		buf_add(&b, &index, sizeof(index));
 		buf_add(&b, p + off, n);
@@ -727,8 +719,8 @@ on_event(const struct astream_event *ev, void *arg)
 		for (off = 0; off < ev->textlen; ) {
 			size_t	 n = ev->textlen - off;
 
-			if (n > FUGU_CHUNK)
-				n = FUGU_CHUNK;
+			if (n > IMSGEV_CHUNK_MAX)
+				n = IMSGEV_CHUNK_MAX;
 			if (filter_text(r, ev->text + off, n, 0) == -1) {
 				relay_error(r,
 				    "provider response contained an API credential");
@@ -787,8 +779,8 @@ on_event(const struct astream_event *ev, void *arg)
 		for (off = 0; off < ev->partiallen; ) {
 			size_t		 n = ev->partiallen - off;
 
-			if (n > FUGU_CHUNK)
-				n = FUGU_CHUNK;
+			if (n > IMSGEV_CHUNK_MAX)
+				n = IMSGEV_CHUNK_MAX;
 			if (filter_tool(r, ts, ev->partial + off, n, 0) == -1) {
 				relay_error(r,
 				    "provider response contained an API credential");
blob - a8a09dd372615a9daec10e4c7d8ee5ac4e2ecfdf
blob + 069e63f2ee77a067e43d2b3a5a003715b89581dd
--- src/fugu-editor/main.c
+++ src/fugu-editor/main.c
@@ -227,7 +227,7 @@ compose(struct worker *w)
 {
 	struct stat	 st;
 	char		 path[] = "/tmp/fugu-edit.XXXXXXXXXX";
-	char		 tmp[FUGU_CHUNK];
+	char		 tmp[IMSGEV_CHUNK_MAX];
 	ssize_t		 n;
 	size_t		 off, sent;
 	sigset_t	 oldmask;
blob - 83c5b7c960279f612572e5e849cb4f2e3152ad0b
blob + 1a4fe85c811b6807de7951646229f2ad3e684147
--- src/fugu-tool/agent.c
+++ src/fugu-tool/agent.c
@@ -258,8 +258,8 @@ agent_send_bytes(struct agent_state *a, uint32_t type,
 
 	for (off = 0; off < len; off += n) {
 		n = len - off;
-		if (n > FUGU_CHUNK)
-			n = FUGU_CHUNK;
+		if (n > IMSGEV_CHUNK_MAX)
+			n = IMSGEV_CHUNK_MAX;
 		agent_send(a, type, p + off, n);
 	}
 }
blob - 07b7f6d8e57a4e1802416d7fe25b7a3b420d0546
blob + 7302d1885910203803ab6bdd772ce678a258f08b
--- src/fugu-tool/main.c
+++ src/fugu-tool/main.c
@@ -358,27 +358,13 @@ send_result(struct worker *w, const struct buf *out, c
     int is_error)
 {
 	struct tool_result	 r;
-	size_t			 off, dlen;
+	size_t			 dlen;
 
-	for (off = 0; off < out->len; ) {
-		size_t	 n = out->len - off;
-
-		if (n > FUGU_CHUNK)
-			n = FUGU_CHUNK;
-		imsgev_send(&w->iev, FUGU_IMSG_TOOL_OUT, 0,
-		    out->data + off, n);
-		off += n;
-	}
+	imsgev_send_chunks(&w->iev, FUGU_IMSG_TOOL_OUT, 0,
+	    out->data, out->len);
 	dlen = diff->len > FUGU_TOOL_DIFF_MAX ? FUGU_TOOL_DIFF_MAX : diff->len;
-	for (off = 0; off < dlen; ) {
-		size_t	 n = dlen - off;
-
-		if (n > FUGU_CHUNK)
-			n = FUGU_CHUNK;
-		imsgev_send(&w->iev, FUGU_IMSG_TOOL_DIFF, 0,
-		    diff->data + off, n);
-		off += n;
-	}
+	imsgev_send_chunks(&w->iev, FUGU_IMSG_TOOL_DIFF, 0,
+	    diff->data, dlen);
 	memset(&r, 0, sizeof(r));
 	r.is_error = is_error;
 	imsgev_send(&w->iev, FUGU_IMSG_TOOL_RESULT, 0, &r,
@@ -476,7 +462,6 @@ tool_dispatch(struct worker *w, struct imsg *imsg)
 	case FUGU_IMSG_CTX_READ: {
 		struct buf	 ctx;
 		char		 path[1024];
-		size_t		 off;
 
 		/* the payload is a NUL-terminated path (coordinator-built) */
 		if (imsgev_get_payload(imsg, &payload) == -1)
@@ -490,15 +475,8 @@ tool_dispatch(struct worker *w, struct imsg *imsg)
 		path[len] = '\0';
 		buf_init(&ctx);
 		ctx_file_read(path, &ctx);
-		for (off = 0; off < ctx.len; ) {	/* chunk under the cap */
-			size_t	 k = ctx.len - off;
-
-			if (k > FUGU_CHUNK)
-				k = FUGU_CHUNK;
-			imsgev_send(&w->iev, FUGU_IMSG_CTX_DATA, 0,
-			    ctx.data + off, k);
-			off += k;
-		}
+		imsgev_send_chunks(&w->iev, FUGU_IMSG_CTX_DATA, 0,
+		    ctx.data, ctx.len);
 		imsgev_send(&w->iev, FUGU_IMSG_CTX_END, 0, NULL, 0);
 		buf_free(&ctx);
 		return (0);
blob - eece9db7071abfafc31067ad2192b24c8a84511a
blob + 05f771be2db08fe8748ce411937e60884c6fc586
--- src/fugu-tty/ui.c
+++ src/fugu-tty/ui.c
@@ -886,7 +886,7 @@ scroll_by(struct ui *u, int delta)
 static void	arm_esc(struct ui *);
 
 /*
- * Send a message to the coordinator in FUGU_CHUNK slices terminated by
+ * Send a message to the coordinator in bounded slices terminated by
  * an end marker (behavior.md 2.1): a submitted line or a steered entry
  * can be a whole paste (up to 64 KiB), which does not fit one imsg
  * frame (16 KiB).  The editor caps a message at ED_PASTE_MAX, so the
@@ -896,16 +896,7 @@ static void
 send_chunked(struct ui *u, int chunk_type, int end_type, const char *p,
     size_t len)
 {
-	size_t	off;
-
-	for (off = 0; off < len; ) {
-		size_t	n = len - off;
-
-		if (n > FUGU_CHUNK)
-			n = FUGU_CHUNK;
-		imsgev_send(&u->w->iev, chunk_type, 0, p + off, n);
-		off += n;
-	}
+	imsgev_send_chunks(&u->w->iev, chunk_type, 0, p, len);
 	imsgev_send(&u->w->iev, end_type, 0, NULL, 0);
 }
 
@@ -987,7 +978,7 @@ dequeue(struct ui *u)
 static void
 compose_begin(struct ui *u)
 {
-	size_t		 len, off;
+	size_t		 len;
 	const char	*text = ed_text(&u->ed, &len);
 
 	if (u->composing)
@@ -1004,15 +995,7 @@ compose_begin(struct ui *u)
 	event_del(&u->ev_in);
 	(void)write(STDOUT_FILENO, "\033[?2004l", 8);	/* paste off */
 	endwin();
-	for (off = 0; off < len; ) {
-		size_t	n = len - off;
-
-		if (n > FUGU_CHUNK)
-			n = FUGU_CHUNK;
-		imsgev_send(&u->w->iev, FUGU_IMSG_UI_COMPOSE, 0,
-		    text + off, n);
-		off += n;
-	}
+	imsgev_send_chunks(&u->w->iev, FUGU_IMSG_UI_COMPOSE, 0, text, len);
 	imsgev_send(&u->w->iev, FUGU_IMSG_UI_COMPOSE_GO, 0,
 	    NULL, 0);
 	u->composing = 1;
blob - c2665884939d8193e3321c1b2f77f94048dae9bd
blob + 5de80b1424edcb87c8f49ed39b04f63ac360650d
--- src/fugu-web/main.c
+++ src/fugu-web/main.c
@@ -195,17 +195,9 @@ send_result(struct worker *w, uint32_t reqid, const st
     int is_error)
 {
 	struct tool_result r;
-	size_t off;
 
-	for (off = 0; off < out->len; ) {
-		size_t n = out->len - off;
-
-		if (n > FUGU_CHUNK)
-			n = FUGU_CHUNK;
-		imsgev_send(&w->iev, FUGU_IMSG_WEB_OUT, reqid,
-		    out->data + off, n);
-		off += n;
-	}
+	imsgev_send_chunks(&w->iev, FUGU_IMSG_WEB_OUT, reqid,
+	    out->data, out->len);
 	memset(&r, 0, sizeof(r));
 	r.is_error = is_error;
 	imsgev_send(&w->iev, FUGU_IMSG_WEB_RESULT, reqid, &r, sizeof(r));