SmolPaws · message-work · outbox

Two outboxes, compared

Your read weeks ago: in the automation repo the message queue's inbox was carefully designed, but the outbox felt thin. This is the KISS comparison — the automation repo's result-return vs our coordinator's delivery queue — and where Rohit's TaskOutcome (PR #4465) lands.

sources: ~/repos/automation @ main · smolpaws/src/coordinator/ @ Aug 5 · message-work ADR · #4465 show-me
🐾 smolpaws · for Engel · from a code read, not a live run

1The one idea

They solve different halves of "outbox," and that's the whole comparison. The automation repo's outbox is one terminal result callback per run — "the run finished, here's the status." Our coordinator's outbox is a durable per-message delivery queue — "these N messages must reach this channel, exactly-ish once, in order, and I must know if a send's fate is unknown." PR #4465 makes the automation repo's one callback richer (a structured TaskOutcome); it does not give it delivery semantics.

automation repo Result-return, once

The SDK inside the sandbox POSTs /v1/runs/{id}/complete on exit with {status, conversation_id, error}. Coarse, terminal, fire-and-forget. A watchdog covers a missed callback. No per-message delivery, no ret/ack of user-facing messages.

coordinator Delivery queue, per message

Each outbound (a send_message the agent emits) becomes a durable delivery work row: claim → send → ack, with head-of-line order, fenced claims, backoff, and a first-class delivery_unknown for "the send's effect is ambiguous — never blind-retry."

2The two outboxes, side by side

AUTOMATION REPO · one terminal callback agent runin sandbox SDK __exit__POST /runs/:id/complete automation_runsstatus COMPLETED|FAILED one shot · {status, conversation_id, error} · watchdog if missed COORDINATOR · durable per-message delivery queue agent emitssend_message ×N projectorevents → delivery rows work (delivery)claim→send→ackorder · backoff · unknown channelWA/Slack/… per message · exactly-ish once · head-of-line order · delivery_unknown never blind-retries
Same word, different scope. The automation outbox returns one run result; the coordinator outbox delivers each message with durability guarantees.

3Automation repo — one completion callback

The automation service's result-return is a single endpoint. The SDK inside the execution sandbox calls it on context-manager exit:

# automation/schemas.py:614 — the entire outbound payload
class RunCompleteRequest(BaseModel):
    status: Literal["COMPLETED", "FAILED"]
    run_id: str | None = None
    conversation_id: str | None = None
    error: str | None = None      # free-text, only when FAILED

# automation/router.py:325 — RUNNING → COMPLETED|FAILED, optimistic lock
@router.post("/runs/{run_id}/complete")
  • Binary status — COMPLETED or FAILED, plus a free-text error. No "blocked", no "needs user action", no confidence, no structured blockers.
  • Terminal, once per run — it reports the run ending, not individual user-facing messages. Fire-and-forget; a watchdog marks FAILED if the callback is missed (watchdog.py:198).
  • No message delivery at all — there is no notion of "deliver these replies to a channel, retry, ack." That was never the automation outbox's job. OpenHands/automation

This is the "unfinished outbox" feel: the inbox side (dispatcher: SELECT … FOR UPDATE SKIP LOCKED atomic claim, PENDING queue, per-run dedup) is a real queue; the outbox side is a single status ping. Fine for "did the background run pass or fail," thin for "reliably return work products to a human."

4Coordinator — a real delivery queue

Our coordinator treats delivery as first-class durable work. Each outbound message is a delivery row in the unified work table with its own lifecycle:

# coordinator/types.ts — the delivery vocabulary
WorkState = 'ready' | 'claimed' | 'done' | 'retry_wait' | 'failed' | 'delivery_unknown'
SettleOutcome = {done, externalMessageId} | {retry} | {delivery_unknown} | {fail}

markSending(claim)   # durably set send_attempted=1 BEFORE the network send
settle(claim, outcome)
  • Per-message — the projector turns durable send_message action events into delivery rows (destination = the conversation's bound lane). DESIGN.md §8 D1
  • Head-of-line order — a later message never overtakes an unresolved earlier one in the same lane.
  • Fenced claims + backoff — single-winner claim under concurrent workers; expiry → exponential backoff → failed.
  • delivery_unknown is the headline — send_attempted=1 is set durably before the send; if the claim then expires with the response lost, the row goes to delivery_unknown and never auto-retries (an ambiguous send is never blind-replayed). This is the exact gap the old runner's outbox had ("no delivery_unknown: an ambiguous send is either dropped or blindly re-sent").

So the coordinator's outbox answers the questions the automation callback can't: which message, in what order, delivered or not, and what to do when we can't tell.

5Feature-by-feature

Outbox concernAutomation repoCoordinator
Unitwhole runeach message / outbound
Result vocabularyCOMPLETED / FAILED + free-text errorready/claimed/done/retry_wait/failed/delivery_unknown
User-facing message delivery— (not its job)projected from send_message, delivered to a lane
Orderingn/aper-lane head-of-line, monotonic sequence
Retry / claimwatchdog re-check on missed callbackfenced compare-and-set claim, expiry, backoff
Ambiguous send— (can double-send or drop)delivery_unknown, never blind-retried
Idempotencyoptimistic lock on run rowunique (kind, source_key); deterministic event id on intake
Semantic outcome (success/blocked/…)binary — #4465 enriches thisnot modeled here (delivery ≠ semantics)

6Where PR #4465 fits

#4465 (TaskOutcome) is an enrichment of the automation repo's completion callback, not a delivery-queue feature. It upgrades the coarse {status: COMPLETED|FAILED, error} into a structured {status: success|partial_success|blocked|failed|unknown, blockers[], needs_user_action, confidence, source} and forwards it as agent_outcome on that same completion callback (opt-in via AUTOMATION_CALLBACK_SUPPORTS_AGENT_OUTCOME). details

The clean way to see all three: the automation callback is a run-level result channel; #4465 makes that result semantically rich; the coordinator is a message-level delivery channel. #4465 and the coordinator are complementary, not overlapping — one answers "how did the task go," the other "did each reply actually get out."

The one place they touch: a "blocked, needs user action" TaskOutcome is exactly the kind of thing that should then be delivered to the user — i.e. #4465 produces content that a coordinator-style outbox would deliver. Task outcome = what to say; delivery queue = getting it there reliably.

7So what

  • Your instinct holds: the automation repo's outbox is genuinely the thin side — a single terminal status callback, no per-message delivery, no ambiguous-send handling. The inbox (dispatcher/queue) is the finished part.
  • #4465 improves the thin side without changing its shape — richer result, still one callback per run. It does not add delivery semantics, and it doesn't need to.
  • The coordinator is the missing delivery half — and it already models the hard case (delivery_unknown) that neither the automation callback nor the old SmolPaws runner handled.
  • Open question for us: do we want the coordinator to also carry a semantic outcome per lane (mirroring #4465's TaskOutcome) so a "blocked" result can be delivered as a message, or keep semantics strictly upstream and let the coordinator stay a dumb, reliable pipe? KISS says: keep it a pipe; let TaskOutcome be payload.
automation @ ~/repos/automation main · coordinator @ smolpaws/src/coordinator (Aug 5) · ADR smolpaws-message-work-adr
🐾 smolpaws · for Engel · code read, not a live run · KISS by request