2026-09-21 23:46:56 +02:00
---
id: STATE-WP-0093
type: workplan
title: "Per-recipient broadcast receipts and standing notices"
domain: infotech
repo: state-hub
STATE-WP-0093: per-recipient broadcast receipts and standing notices (T01-T06).
Founder approved T01 on 2026-09-22 (D2, D3, D6 as recommended).
- T02: message_receipts table; kind/expires_at/supersedes_id on
agent_messages; migration d7e8f9a0b1c2 archives existing broadcasts,
leaves direct messages untouched, reversible.
- T03: reader-aware mark-read (unattributed broadcast mark-read is a
metered, deprecated no-op), delivery receipts on the scoped unread inbox,
POST /messages/{id}/ack, news/standing kinds, expiry, supersede, broadcast
archive no longer stamps read_at, reply writes the replier's receipt.
- T04 (state-hub part): Codex MCP reader param and acknowledge_notice;
hub-core part handed off (message 69fc387c).
- T05: GET /messages/notices, standing_notices in /state/summary,
dashboard standing-notices panel.
- T06: 17 new tests; full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-22 00:26:33 +02:00
status: active
2026-09-21 23:46:56 +02:00
owner: claude
topic_slug: infotech
flavor: implementation
created: "2026-09-21"
STATE-WP-0093: per-recipient broadcast receipts and standing notices (T01-T06).
Founder approved T01 on 2026-09-22 (D2, D3, D6 as recommended).
- T02: message_receipts table; kind/expires_at/supersedes_id on
agent_messages; migration d7e8f9a0b1c2 archives existing broadcasts,
leaves direct messages untouched, reversible.
- T03: reader-aware mark-read (unattributed broadcast mark-read is a
metered, deprecated no-op), delivery receipts on the scoped unread inbox,
POST /messages/{id}/ack, news/standing kinds, expiry, supersede, broadcast
archive no longer stamps read_at, reply writes the replier's receipt.
- T04 (state-hub part): Codex MCP reader param and acknowledge_notice;
hub-core part handed off (message 69fc387c).
- T05: GET /messages/notices, standing_notices in /state/summary,
dashboard standing-notices panel.
- T06: 17 new tests; full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-22 00:26:33 +02:00
updated: "2026-09-22"
2026-09-21 23:46:56 +02:00
related:
- STATE-WP-0091
origin: founder-direction
origin_ref: the-custodian/docs/agent-environment-orientation.md
2026-09-21 23:49:07 +02:00
state_hub_workstream_id: "f885387e-5335-5aea-ae83-9c72c177d4c3"
2026-09-21 23:46:56 +02:00
---
# Per-recipient broadcast receipts and standing notices
Founder direction 2026-09-21: fleet-wide guidance should be **pulled** by
agents through the inbox they already check at session start, not pushed
as edits into ~120 repositories. `to_agent: "broadcast"` is that channel,
but it is broken by global read state. This plan fixes it. Planning was
approved; **implementation waits on the founder's review of this plan** .
## Problem (verified in code 2026-09-21)
- `api/routers/messages.py` `list_messages` (l.62-67) already ORs
`to_agent == "broadcast"` into every agent's inbox query.
- Read state is one column, `AgentMessage.read_at`
(`api/models/agent_message.py` ). `unread_only=true` filters on it
(l.72-73). `PATCH /messages/{id}/read` (l.100-110) sets it globally,
`PATCH /{id}/archive` sets it too, and `POST /{id}/reply` (l.137-138)
marks the original read. So the **first** agent that reads a broadcast
hides it from all others.
- Live evidence: all 5 broadcasts ever sent are read, none archived —
including gate-house 2026-08-29 "ACCEPTED: security layer model v0.7 —
and start here", invisible to every agent that oriented since.
Findings that refine the custodian's reading:
1. **The MCP message tools are not in this repo.** `get_messages` ,
`mark_message_read` and `reply_to_message` for the main MCP server are
registered in `hub-core` (`/home/worsch/hub-core/hub_core/mcp/server.py`
l.129-160). Only the Codex MCP server (`mcp_server/codex_server.py`
l.55-64) lives here. MCP parity is therefore a cross-repo change.
2. **A mark-read request carries no context.** It is a separate request
from the inbox listing; the reader cannot be inferred from a
`to_agent` query. An unattributed mark-read must therefore not touch
broadcast visibility at all (see D2), and "seen" is best recorded at
**delivery** (D3), which needs no protocol change.
3. **The system itself broadcasts.** `api/routers/capability_requests.py`
(l.100-102) routes unmatched capability requests to `broadcast` . Those
become `news` broadcasts under this design (D4); first-reader-hides
was never their intended semantics either.
4. `reply_to_message` already carries `from_agent` , so the reply path
has a reader identity for free.
## Design
**D1 — Receipts table.** New `message_receipts`
(`message_id` FK → `agent_messages.id` ON DELETE CASCADE,
`agent` String(100) canonical slug, `delivered_at` , `read_at` ,
`acknowledged_at` , all timestamptz nullable; PK `(message_id, agent)` ).
Receipts are written **only for broadcasts** . The reader slug goes
through `canonicalize_repository_slug` , so renamed repos keep their
receipts. Direct messages keep today's semantics exactly: global
`read_at` , no receipt rows.
**D2 — Mark-read with and without a reader.** `PATCH /messages/{id}/read`
gains an optional `reader` (query param; also accepted in the JSON body).
- Direct message: unchanged, with or without `reader` .
- Broadcast + `reader` : upsert receipt `read_at` for that reader;
global `read_at` untouched.
- Broadcast without `reader` (every existing curl, both MCP servers):
**no-op on visibility** , response `200` with the message body plus
`Deprecation: true` and `Warning: 299 - "broadcast mark-read needs
?reader=< agent > "` headers, and a legacy-meter hit so the callers can
be counted and retired. It never hides the broadcast from anyone.
- `PATCH /{id}/archive` on a broadcast stays global (sender or founder
withdrawal) but no longer stamps `read_at` .
- `POST /{id}/reply` on a broadcast writes a receipt (`read_at` ) for
`body.from_agent` instead of the global `read_at` ; direct replies
unchanged.
**D3 — Delivery receipts (zero protocol change).** When
`GET /messages/?to_agent=X&unread_only=true` returns a broadcast, the
API upserts a receipt with `delivered_at` for X in the same request.
That exact call shape is the orientation step in every repo's protocol,
so "who has seen it" becomes visible without any instruction change.
Listing without `to_agent` (dashboard) or without `unread_only` writes
nothing. A write inside a GET is a deliberate trade-off; it is
idempotent (upsert, first timestamp wins) and scoped to broadcasts.
**D4 — Kinds, expiry, supersede.** New columns on `agent_messages` :
`kind` String(20) NOT NULL default `'message'`
(`message` | `news` | `standing` ), `expires_at` timestamptz null,
`supersedes_id` UUID FK self null. `kind` is only meaningful for
broadcasts; sending a broadcast without `kind` defaults to `news` .
- `news` : in X's unread inbox until X has **any** receipt (delivered,
read, or acknowledged). Agents see it once at orientation — which is
what a broadcast should do — and it drops off without a mark-read.
Default `expires_at` = created + 30 days if the sender gives none.
- `standing` : in X's unread inbox until X has `acknowledged_at` , or the
notice expires, or it is superseded. Delivery and plain read do not
clear it. Acknowledgement: `POST /messages/{id}/ack` with
`{"agent": "<slug>"}` (also accepted as `PATCH /{id}/read?reader=X&ack=true` ).
- `supersedes_id` : creating a notice that supersedes an older one sets
the older one's `archived_at` ; archived and expired notices never
appear in inboxes. Chains are allowed; only the newest stays live.
- Expired broadcasts are filtered at query time (`expires_at > now()` );
no sweeper job.
**D5 — Visibility.** `GET /messages/notices` returns live standing
notices with, per notice, counts and lists of agents that have
acknowledged / only been delivered / never been reached, measured
against the set of active registered repositories (the same set
fix-consistency uses). `/state/summary` gains `standing_notices:
[{id, subject, expires_at, acked, pending}]` (counts only). The
dashboard inbox page (`dashboard/src/inbox.md` ,
`dashboard/src/data/messages.json.py` ) gets a "Standing notices" panel
with the not-yet-acknowledged repo list.
**D6 — Existing 5 broadcasts.** All 5 have global `read_at` set and
would re-surface to every agent once visibility ignores `read_at` for
broadcasts. **Default: the migration archives them** (`archived_at =
now()`, ` kind = 'news'`), nothing re-surfaces, archive is reversible.
The gate-house v0.7 "start here" note is gate-house's content, so the
hub does not re-author it; instead T08 asks gate-house (or the founder)
whether to re-publish it as a `standing` notice. Alternatives the
founder may pick at T01: re-surface the three gate-house ones as `news`
(one-time appearance per agent), or leave all five read with no archive.
**D7 — Protocol change.** Receipts and `news` : **none** . `standing`
acknowledgement needs one instruction line, changed once:
- `scripts/project_rules/session-protocol.template` l.34 and
`scripts/project_rules/agents-codex.template` l.62: the mark-read curl
becomes `.../messages/<id>/read?reader={REPO_SLUG}` with a one-line
note "standing notices: `POST /messages/<id>/ack` after acting".
- Carried once by `scripts/update_agent_instruction_files.py` at the
next routine propagation (it renders these templates); no recurring
push. Until an agent's copy is updated, standing notices still reach
it at every orientation (delivery is logged) — it just cannot clear
them, which surfaces it on the notices view. That is the intended
"who is out of date" signal, not a failure.
## Confirm design with the founder
```task
id: STATE-WP-0093-T01
STATE-WP-0093: per-recipient broadcast receipts and standing notices (T01-T06).
Founder approved T01 on 2026-09-22 (D2, D3, D6 as recommended).
- T02: message_receipts table; kind/expires_at/supersedes_id on
agent_messages; migration d7e8f9a0b1c2 archives existing broadcasts,
leaves direct messages untouched, reversible.
- T03: reader-aware mark-read (unattributed broadcast mark-read is a
metered, deprecated no-op), delivery receipts on the scoped unread inbox,
POST /messages/{id}/ack, news/standing kinds, expiry, supersede, broadcast
archive no longer stamps read_at, reply writes the replier's receipt.
- T04 (state-hub part): Codex MCP reader param and acknowledge_notice;
hub-core part handed off (message 69fc387c).
- T05: GET /messages/notices, standing_notices in /state/summary,
dashboard standing-notices panel.
- T06: 17 new tests; full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-22 00:26:33 +02:00
status: done
2026-09-21 23:46:56 +02:00
priority: high
2026-09-21 23:49:07 +02:00
state_hub_task_id: "94eec945-b728-50cb-aefb-60eca5aeccd9"
2026-09-21 23:46:56 +02:00
```
Founder reviews D1-D7, in particular the unattributed mark-read no-op
(D2), write-on-delivery GET (D3), and the default for the 5 existing
broadcasts (D6). Record the choices as decisions in this file.
Done when D2, D3 and D6 each have a recorded founder choice and this
workplan moves to `ready` .
STATE-WP-0093: per-recipient broadcast receipts and standing notices (T01-T06).
Founder approved T01 on 2026-09-22 (D2, D3, D6 as recommended).
- T02: message_receipts table; kind/expires_at/supersedes_id on
agent_messages; migration d7e8f9a0b1c2 archives existing broadcasts,
leaves direct messages untouched, reversible.
- T03: reader-aware mark-read (unattributed broadcast mark-read is a
metered, deprecated no-op), delivery receipts on the scoped unread inbox,
POST /messages/{id}/ack, news/standing kinds, expiry, supersede, broadcast
archive no longer stamps read_at, reply writes the replier's receipt.
- T04 (state-hub part): Codex MCP reader param and acknowledge_notice;
hub-core part handed off (message 69fc387c).
- T05: GET /messages/notices, standing_notices in /state/summary,
dashboard standing-notices panel.
- T06: 17 new tests; full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-22 00:26:33 +02:00
**Founder decisions (Bernd Worsch, 2026-09-22) — all accepted as recommended:**
1. **D3 ACCEPTED:** `GET /messages/?to_agent=X&unread_only=true` records an
idempotent `delivered_at` receipt for X on each broadcast it returns (a
deliberate write inside the GET; no protocol change for delivery tracking).
2. **D6 ACCEPTED:** the migration ARCHIVES the 5 existing broadcasts
(reversible; nothing re-surfaces). T08 asks gate-house whether to
republish its v0.7 "start here" note as a standing notice.
3. **D2/D7 ACCEPTED:** unattributed mark-read of a broadcast is a visibility
no-op with deprecation headers; one line in
`scripts/project_rules/session-protocol.template` and
`agents-codex.template` (inbox curl gains `?reader={REPO_SLUG}` plus an
ack note), carried ONCE by `update_agent_instruction_files.py` . The fleet
propagation run itself is T08, not part of the implementation session.
T07 (release) still waits on its own founder go-ahead.
2026-09-21 23:46:56 +02:00
## Schema and migration
```task
id: STATE-WP-0093-T02
2026-09-22 00:38:08 +02:00
status: done
2026-09-21 23:46:56 +02:00
priority: high
2026-09-21 23:49:07 +02:00
state_hub_task_id: "6680d408-ebce-5b2c-8a97-84f0bf011754"
2026-09-21 23:46:56 +02:00
```
Model `MessageReceipt` in `api/models/` ; add `kind` , `expires_at` ,
`supersedes_id` to `AgentMessage` . One Alembic revision on head
`c6f7a8b9d0e1` : create `message_receipts` , add the three columns
(`kind` server default `'message'` , broadcasts backfilled to `'news'` ),
index `(agent)` on receipts and `(to_agent, kind, archived_at)` on
messages, and apply the D6 choice to the 5 existing broadcasts.
Downgrade drops the table and columns (archive stamps stay).
Done when `alembic upgrade head` and `downgrade -1` both run clean on a
copy of a production dump, and the 5 broadcasts end in the D6 state.
STATE-WP-0093: per-recipient broadcast receipts and standing notices (T01-T06).
Founder approved T01 on 2026-09-22 (D2, D3, D6 as recommended).
- T02: message_receipts table; kind/expires_at/supersedes_id on
agent_messages; migration d7e8f9a0b1c2 archives existing broadcasts,
leaves direct messages untouched, reversible.
- T03: reader-aware mark-read (unattributed broadcast mark-read is a
metered, deprecated no-op), delivery receipts on the scoped unread inbox,
POST /messages/{id}/ack, news/standing kinds, expiry, supersede, broadcast
archive no longer stamps read_at, reply writes the replier's receipt.
- T04 (state-hub part): Codex MCP reader param and acknowledge_notice;
hub-core part handed off (message 69fc387c).
- T05: GET /messages/notices, standing_notices in /state/summary,
dashboard standing-notices panel.
- T06: 17 new tests; full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-22 00:26:33 +02:00
**2026-09-22 (progress):** `MessageReceipt` model and Alembic revision
`d7e8f9a0b1c2` (on `c6f7a8b9d0e1` ) implemented: `message_receipts` ,
`kind` /`expires_at` /`supersedes_id` , both indexes, broadcasts backfilled to
`news` and archived (D6); direct messages untouched. Verified on a scratch
local database built from the full migration chain (upgrade head, downgrade
-1, upgrade head all clean; a read broadcast ended `news` + archived, a direct
message unchanged) and by an isolated-schema up/down test. **Remaining:** the
rehearsal on a copy of a production dump — not run from the implementation
session (no production access); do it in the T07 preflight.
2026-09-21 23:46:56 +02:00
## API: receipts, notices, ack
```task
id: STATE-WP-0093-T03
STATE-WP-0093: per-recipient broadcast receipts and standing notices (T01-T06).
Founder approved T01 on 2026-09-22 (D2, D3, D6 as recommended).
- T02: message_receipts table; kind/expires_at/supersedes_id on
agent_messages; migration d7e8f9a0b1c2 archives existing broadcasts,
leaves direct messages untouched, reversible.
- T03: reader-aware mark-read (unattributed broadcast mark-read is a
metered, deprecated no-op), delivery receipts on the scoped unread inbox,
POST /messages/{id}/ack, news/standing kinds, expiry, supersede, broadcast
archive no longer stamps read_at, reply writes the replier's receipt.
- T04 (state-hub part): Codex MCP reader param and acknowledge_notice;
hub-core part handed off (message 69fc387c).
- T05: GET /messages/notices, standing_notices in /state/summary,
dashboard standing-notices panel.
- T06: 17 new tests; full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-22 00:26:33 +02:00
status: done
2026-09-21 23:46:56 +02:00
priority: high
2026-09-21 23:49:07 +02:00
state_hub_task_id: "33010aa6-326d-5f52-9ff3-32b09c9e88c4"
2026-09-21 23:46:56 +02:00
```
Implement D2-D4 in `api/routers/messages.py` and
`api/schemas/agent_message.py` (`MessageCreate` gains `kind` ,
`expires_at` , `supersedes_id` ; `MessageRead` exposes them; per-reader
`read_at` /`acknowledged_at` are returned when the list is scoped by
`to_agent` ). Add `POST /messages/{id}/ack` . Default `news` expiry.
Capability-request broadcasts send `kind='news'` .
Done when direct-message behavior is byte-for-byte unchanged in the
existing test suite and T06's new tests pass.
STATE-WP-0093: per-recipient broadcast receipts and standing notices (T01-T06).
Founder approved T01 on 2026-09-22 (D2, D3, D6 as recommended).
- T02: message_receipts table; kind/expires_at/supersedes_id on
agent_messages; migration d7e8f9a0b1c2 archives existing broadcasts,
leaves direct messages untouched, reversible.
- T03: reader-aware mark-read (unattributed broadcast mark-read is a
metered, deprecated no-op), delivery receipts on the scoped unread inbox,
POST /messages/{id}/ack, news/standing kinds, expiry, supersede, broadcast
archive no longer stamps read_at, reply writes the replier's receipt.
- T04 (state-hub part): Codex MCP reader param and acknowledge_notice;
hub-core part handed off (message 69fc387c).
- T05: GET /messages/notices, standing_notices in /state/summary,
dashboard standing-notices panel.
- T06: 17 new tests; full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-22 00:26:33 +02:00
**2026-09-22 (done):** D2-D4 in `api/routers/messages.py` ,
`api/services/message_receipts.py` , `api/schemas/agent_message.py` (local
subclasses of the hub-core schemas). `PATCH /read?reader=&ack=` (also JSON
body), unattributed broadcast mark-read = no-op + `Deprecation` /`Warning`
headers + legacy-meter key `rest_api:PATCH /messages/{id}/read
broadcast-without-reader`, broadcast reply writes a receipt for the replier,
`POST /messages/{id}/ack` , news default expiry 30d, supersede archives the
predecessor, expiry filtered at query time, capability-request broadcasts are
`news` . Direct-message paths unchanged; existing suite green.
2026-09-21 23:46:56 +02:00
## MCP parity (state-hub and hub-core)
```task
id: STATE-WP-0093-T04
STATE-WP-0093: per-recipient broadcast receipts and standing notices (T01-T06).
Founder approved T01 on 2026-09-22 (D2, D3, D6 as recommended).
- T02: message_receipts table; kind/expires_at/supersedes_id on
agent_messages; migration d7e8f9a0b1c2 archives existing broadcasts,
leaves direct messages untouched, reversible.
- T03: reader-aware mark-read (unattributed broadcast mark-read is a
metered, deprecated no-op), delivery receipts on the scoped unread inbox,
POST /messages/{id}/ack, news/standing kinds, expiry, supersede, broadcast
archive no longer stamps read_at, reply writes the replier's receipt.
- T04 (state-hub part): Codex MCP reader param and acknowledge_notice;
hub-core part handed off (message 69fc387c).
- T05: GET /messages/notices, standing_notices in /state/summary,
dashboard standing-notices panel.
- T06: 17 new tests; full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-22 00:26:33 +02:00
status: progress
2026-09-21 23:46:56 +02:00
priority: medium
2026-09-21 23:49:07 +02:00
state_hub_task_id: "43529a93-6e49-51c8-a358-726f7e737fab"
2026-09-21 23:46:56 +02:00
```
`mcp_server/codex_server.py` : `mark_message_read(message_id, reader=None)` ,
new `acknowledge_notice(message_id, agent)` , `send_message` accepts
`kind` /`expires_at` /`supersedes_id` if exposed. The main MCP tools live
in `hub-core` (`hub_core/mcp/server.py` ): same signature changes there
via a hub-core workplan/handoff message (cross-repo; do not edit
hub-core from this repo), then bump the pinned hub-core. Update
`mcp_server/TOOLS.md` .
Done when both MCP servers can mark a broadcast read per reader and
acknowledge a standing notice, with tests in
`tests/test_codex_mcp_server.py` and hub-core's suite.
STATE-WP-0093: per-recipient broadcast receipts and standing notices (T01-T06).
Founder approved T01 on 2026-09-22 (D2, D3, D6 as recommended).
- T02: message_receipts table; kind/expires_at/supersedes_id on
agent_messages; migration d7e8f9a0b1c2 archives existing broadcasts,
leaves direct messages untouched, reversible.
- T03: reader-aware mark-read (unattributed broadcast mark-read is a
metered, deprecated no-op), delivery receipts on the scoped unread inbox,
POST /messages/{id}/ack, news/standing kinds, expiry, supersede, broadcast
archive no longer stamps read_at, reply writes the replier's receipt.
- T04 (state-hub part): Codex MCP reader param and acknowledge_notice;
hub-core part handed off (message 69fc387c).
- T05: GET /messages/notices, standing_notices in /state/summary,
dashboard standing-notices panel.
- T06: 17 new tests; full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-22 00:26:33 +02:00
**2026-09-22 (progress — split):** state-hub part done:
`mcp_server/codex_server.py` `mark_message_read(message_id, reader=None)` and
new `acknowledge_notice(message_id, agent)` , tests in
`tests/test_codex_mcp_server.py` , `mcp_server/TOOLS.md` updated (the Codex
server has no `send_message` ). hub-core part handed off via State Hub message
`69fc387c-5bd3-47bc-9a3f-d3d83b6dc213` (addendum
`f8eecebf-cf76-44f9-832e-20401b1b37ec` : absorb the schema fields so state-hub
can return to a pure re-export). **Remaining:** hub-core change + release,
then bump the pinned hub-core here.
2026-09-21 23:46:56 +02:00
## Visibility: notices view, summary, dashboard
```task
id: STATE-WP-0093-T05
2026-09-22 00:46:00 +02:00
status: done
2026-09-21 23:46:56 +02:00
priority: medium
2026-09-21 23:49:07 +02:00
state_hub_task_id: "fdb44fd9-ab44-5e26-86b3-a4f0f8a4e701"
2026-09-21 23:46:56 +02:00
```
Implement D5: `GET /messages/notices` , `standing_notices` in
`/state/summary` , dashboard panel.
Done when a live standing notice shows acked / delivered-only /
unreached repo lists on the endpoint and the dashboard.
STATE-WP-0093: per-recipient broadcast receipts and standing notices (T01-T06).
Founder approved T01 on 2026-09-22 (D2, D3, D6 as recommended).
- T02: message_receipts table; kind/expires_at/supersedes_id on
agent_messages; migration d7e8f9a0b1c2 archives existing broadcasts,
leaves direct messages untouched, reversible.
- T03: reader-aware mark-read (unattributed broadcast mark-read is a
metered, deprecated no-op), delivery receipts on the scoped unread inbox,
POST /messages/{id}/ack, news/standing kinds, expiry, supersede, broadcast
archive no longer stamps read_at, reply writes the replier's receipt.
- T04 (state-hub part): Codex MCP reader param and acknowledge_notice;
hub-core part handed off (message 69fc387c).
- T05: GET /messages/notices, standing_notices in /state/summary,
dashboard standing-notices panel.
- T06: 17 new tests; full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-22 00:26:33 +02:00
**2026-09-22 (progress):** `GET /messages/notices` (acknowledged /
delivered_only / unreached against active `managed_repos` ),
`standing_notices` in `/state/summary` (computed per request, outside the
revision cache), dashboard inbox "Standing notices" panel + separate
"Broadcasts" section (no global mark-read for broadcasts), loader
`dashboard/src/data/notices.json.py` . Endpoint and summary covered by tests;
dashboard build passes. **Remaining:** check the panel against a live notice
after T07.
2026-09-21 23:46:56 +02:00
## Tests
```task
id: STATE-WP-0093-T06
STATE-WP-0093: per-recipient broadcast receipts and standing notices (T01-T06).
Founder approved T01 on 2026-09-22 (D2, D3, D6 as recommended).
- T02: message_receipts table; kind/expires_at/supersedes_id on
agent_messages; migration d7e8f9a0b1c2 archives existing broadcasts,
leaves direct messages untouched, reversible.
- T03: reader-aware mark-read (unattributed broadcast mark-read is a
metered, deprecated no-op), delivery receipts on the scoped unread inbox,
POST /messages/{id}/ack, news/standing kinds, expiry, supersede, broadcast
archive no longer stamps read_at, reply writes the replier's receipt.
- T04 (state-hub part): Codex MCP reader param and acknowledge_notice;
hub-core part handed off (message 69fc387c).
- T05: GET /messages/notices, standing_notices in /state/summary,
dashboard standing-notices panel.
- T06: 17 new tests; full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-22 00:26:33 +02:00
status: done
2026-09-21 23:46:56 +02:00
priority: high
2026-09-21 23:49:07 +02:00
state_hub_task_id: "d68aa1fa-0f09-5d64-a4fc-cbc24180f2c3"
2026-09-21 23:46:56 +02:00
```
API tests: broadcast read by A stays unread for B; unattributed
mark-read of a broadcast is a no-op with deprecation headers; direct
message mark-read, archive and reply unchanged; reply to a broadcast
writes a receipt for the replier only; delivery receipt written only for
`to_agent` +`unread_only` listing; `news` disappears after delivery,
`standing` persists until ack; expiry hides; supersede archives the
predecessor; renamed-repo reader resolves to the canonical receipt;
migration state of pre-existing broadcasts.
Done when the new tests pass and the full suite is green.
STATE-WP-0093: per-recipient broadcast receipts and standing notices (T01-T06).
Founder approved T01 on 2026-09-22 (D2, D3, D6 as recommended).
- T02: message_receipts table; kind/expires_at/supersedes_id on
agent_messages; migration d7e8f9a0b1c2 archives existing broadcasts,
leaves direct messages untouched, reversible.
- T03: reader-aware mark-read (unattributed broadcast mark-read is a
metered, deprecated no-op), delivery receipts on the scoped unread inbox,
POST /messages/{id}/ack, news/standing kinds, expiry, supersede, broadcast
archive no longer stamps read_at, reply writes the replier's receipt.
- T04 (state-hub part): Codex MCP reader param and acknowledge_notice;
hub-core part handed off (message 69fc387c).
- T05: GET /messages/notices, standing_notices in /state/summary,
dashboard standing-notices panel.
- T06: 17 new tests; full suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-22 00:26:33 +02:00
**2026-09-22 (done):** `tests/test_message_receipts.py` (17 tests: all
listed cases, incl. reader in JSON body, ack via mark-read flag, capability
broadcast as news, isolated-schema migration up/down). Full suite: 894 passed
(`tests/test_hub_core_imports.py` message-schema check relaxed from identity
to subclass, see T04).
2026-09-21 23:46:56 +02:00
## Release through the normal promotion path
```task
id: STATE-WP-0093-T07
2026-09-22 00:38:08 +02:00
status: done
2026-09-21 23:46:56 +02:00
priority: high
2026-09-21 23:49:07 +02:00
state_hub_task_id: "e9978994-8b6a-5f98-8056-6ee3970342d1"
2026-09-21 23:46:56 +02:00
```
**Waits on the founder's go-ahead.** Promote per
`deploy/railiance/apps/charts/state-hub/PROMOTE.md` : headroom preflight
(`make railiance-state-hub-headroom` ), image build, `helm upgrade
--atomic`; migration runs as part of the release. Restart MCP servers
after the hub-core bump.
Done when the release is live, `alembic current` shows the new head in
the cluster, and a smoke test sends a test broadcast, reads it as two
agents, and archives it.
## Publish the first standing notice and propagate the one-line change
```task
id: STATE-WP-0093-T08
2026-09-22 00:46:00 +02:00
status: done
2026-09-21 23:46:56 +02:00
priority: medium
2026-09-21 23:49:07 +02:00
state_hub_task_id: "7866d802-eaed-5a7d-be74-eda3185f55d0"
2026-09-21 23:46:56 +02:00
```
After T07: publish `kind: standing` from `the-custodian` , subject "Read
the-custodian/docs/agent-environment-orientation.md (revision
2026-09-21)", `expires_at` ~60 days. Apply the D7 template line change
and let the next routine `update_agent_instruction_files.py` run carry
it once. Ask gate-house whether its v0.7 "start here" note should be
re-published as a standing notice (D6).
Done when the notice is live, visible on `/messages/notices` , the
templates carry the reader/ack line, and gate-house has answered.
2026-09-22 00:38:08 +02:00
## Release record, 2026-09-22 (founder go-ahead; custodian session)
- **Migration trial on a production copy (closes T02).** A read-only `pg_dump` taken inside `databases/state-hub-db-1` (the pod's local postgres user; no credential handled) supplied the schema, `alembic_version` and `agent_messages` (998 rows). They were restored into a scratch database. `alembic upgrade head` applied `b5e6f7a8c9d0 → c6f7a8b9d0e1 → d7e8f9a0b1c2` : 5 of 5 broadcasts archived, direct messages untouched (2 archived before and after), 998 total, `message_receipts` created, kinds `message` and `news` . `downgrade -1` then `upgrade head` were clean. The dumps and the scratch database were deleted.
- The release carried exactly two app commits since `main-0747b4d` : `ddc3338` (STATE-WP-0092 flavor, migration `c6f7a8b9d0e1` , unreleased since 2026-09-14) and `ef541f5` (this workplan).
- **Gates:** the full suite passed, 894 of 894 (`uv run pytest` , local `custodian_test` ). CI `build-and-push` for `ef541f5` succeeded. Headroom preflight `ok` , 585m CPU free. The render diff showed only the image changing on the API and MCP Deployments.
- **Release (T07):** `helm upgrade --atomic --set image.tag=main-ef541f5` gave **revision 65** , deployed. API and MCP both run `main-ef541f5` , 1/1. The migrate hook completed.
- **Verified live:** `/state/health` ok with schema `d7e8f9a0b1c2` ; `GET /messages/notices` 200 (empty); `standing_notices` in `/state/summary` ; the 5 old broadcasts archived and out of inboxes; direct-message mark-read unchanged.
- **Still open:** T04 hub-core (handoff `69fc387c` plus addendum). T05 check against a live notice, which comes with T08. T08: the first standing notice, the template line, and the gate-house question.
2026-09-22 00:46:00 +02:00
## T08 record, 2026-09-22 (founder go-ahead; custodian session)
- **First standing notice published:** `4ff51ffa-2b95-49da-a5a9-2c1965d5fcbe` , from the-custodian to broadcast, kind `standing` , expires 2027-03-20. It points every coding agent to `the-custodian/docs/agent-environment-orientation.md` (revision 2026-09-21), and its body carries the ack command.
- **Live end-to-end check (closes T05):**
- the notice appears in the-custodian's and gate-house's unread inbox;
- `POST /ack` as the-custodian clears it for the-custodian only, and gate-house still sees it;
- `GET /messages/notices` reported 1 acknowledged, 1 delivered-only and 135 unreached. The delivered-only entry was gate-house, recorded by the custodian's own test query.
- **Template line:** `session-protocol.template` and `agents-codex.template` now mark read with `?reader={REPO_SLUG}` and document `POST /messages/<id>/ack` .
- **Fleet propagation NOT run, deliberately.** `update_agent_instruction_files.py` regenerates each repository's whole CLAUDE.md, rules and AGENTS.md without committing. Across the fleet it would overwrite local customisations and leave ~120 dirty working trees. It is also unnecessary: agents on the old instructions already *see* standing notices at every inbox check, and the notice body tells them how to acknowledge. The template change reaches each repository whenever it is next regenerated for its own reasons.
- The gate-house question (republish the archived v0.7 "start here" note as a standing notice?) was sent to gate-house.