Problem
`scripts/inbox.sh` reads unread rows and marks them read as two separate SQL statements (a SELECT and an UPDATE, not a single atomic operation). This is fine for a single interactive reader, but it opens two gaps for any downstream that consumes messages non-interactively:
- Message loss on consumer failure. Between the SELECT and the receiver actually delivering the message (paste into a TUI, HTTP POST, whatever), any crash or inject failure loses the row silently. `inbox.sh` has already run the UPDATE.
- Duplicate delivery under concurrent readers. Two processes tailing the same inbox both see the same unread row; there is no primitive to atomically reserve one to a single owner.
This bites anyone building a receiver on top of agmsg: external delivery daemons, the grok-build monitor watcher (#245), the Codex Desktop wake-on-unread work (#263), the doctor/dashboard (#267), and — I hit it while wiring an external delivery daemon that pipes messages into a Grok TUI, which is what motivated the fix.
Proposal
Add a small claim / lease / ack primitive next to `inbox.sh`, so any receiver can atomically reserve a message before consuming it:
- `agmsg_claim_next(team, agent, owner, ttl)` — inside a single `BEGIN IMMEDIATE` transaction, garbage-collect expired claims, `INSERT OR IGNORE` a claim for the next unread row belonging to (team, agent), return the claimed row. Concurrent callers with different owners can safely race here.
- `agmsg_ack_claim(message_id, owner)` — verify the owner still holds the claim, mark the message read, delete the claim.
- `agmsg_release_claim(message_id, owner)` — drop your own claim so another receiver can retry (used on inject failure).
Storage: a small `claims` table (`message_id` PK, `owner`, `expires_at`) added via `CREATE TABLE IF NOT EXISTS` so existing DBs upgrade in place. TTL means a crashed owner's claims become reclaimable on the next `agmsg_claim_next` call without any external janitor.
Semantics:
- inject failure → `release`, next poll retries. No loss.
- concurrent receivers → exactly one wins the claim. No duplicate.
- process crash → lease expires, next receiver reclaims. Self-healing.
Scope
The primitive is orthogonal to the existing monitor watcher: nothing in-tree needs to change to consume it. Upstream's monitor watcher could adopt it later to atomicize its own delivery loop; external daemons and #263's wake-on-unread work can start using it immediately.
PR
Implemented in #372 — adds `scripts/lib/claims.sh` (52 lines), `claims` table in `scripts/internal/init-db.sh`, and 5 bats cases covering exclusion, release/reclaim, wrong-owner ack rejection, TTL expiry, newline/tab body round-trip. No existing behavior is changed.
(日本語要約)現状の `inbox.sh` は SELECT と read_at UPDATE が別 SQL のため、消費者側の inject 失敗で無音の取りこぼしが起きます。同時に複数レシーバが読むと同じ行を両方に配ります。これを防ぐ atomic な claim/lease/ack API を `scripts/lib/claims.sh` として追加する提案です。`claims` テーブルは `CREATE TABLE IF NOT EXISTS` で既存 DB を破壊せず追加、TTL で crash 耐性を持たせています。#372 で実装、既存挙動は変更しません。
Problem
`scripts/inbox.sh` reads unread rows and marks them read as two separate SQL statements (a SELECT and an UPDATE, not a single atomic operation). This is fine for a single interactive reader, but it opens two gaps for any downstream that consumes messages non-interactively:
This bites anyone building a receiver on top of agmsg: external delivery daemons, the grok-build monitor watcher (#245), the Codex Desktop wake-on-unread work (#263), the doctor/dashboard (#267), and — I hit it while wiring an external delivery daemon that pipes messages into a Grok TUI, which is what motivated the fix.
Proposal
Add a small claim / lease / ack primitive next to `inbox.sh`, so any receiver can atomically reserve a message before consuming it:
Storage: a small `claims` table (`message_id` PK, `owner`, `expires_at`) added via `CREATE TABLE IF NOT EXISTS` so existing DBs upgrade in place. TTL means a crashed owner's claims become reclaimable on the next `agmsg_claim_next` call without any external janitor.
Semantics:
Scope
The primitive is orthogonal to the existing monitor watcher: nothing in-tree needs to change to consume it. Upstream's monitor watcher could adopt it later to atomicize its own delivery loop; external daemons and #263's wake-on-unread work can start using it immediately.
PR
Implemented in #372 — adds `scripts/lib/claims.sh` (52 lines), `claims` table in `scripts/internal/init-db.sh`, and 5 bats cases covering exclusion, release/reclaim, wrong-owner ack rejection, TTL expiry, newline/tab body round-trip. No existing behavior is changed.
(日本語要約)現状の `inbox.sh` は SELECT と read_at UPDATE が別 SQL のため、消費者側の inject 失敗で無音の取りこぼしが起きます。同時に複数レシーバが読むと同じ行を両方に配ります。これを防ぐ atomic な claim/lease/ack API を `scripts/lib/claims.sh` として追加する提案です。`claims` テーブルは `CREATE TABLE IF NOT EXISTS` で既存 DB を破壊せず追加、TTL で crash 耐性を持たせています。#372 で実装、既存挙動は変更しません。