Files
stack/agents/dewey/work/wui/SLICE1-VIEWS.md
T
jason.woltjeandClaude Opus 5.5 08b428ecf1 feat(webui,conversation): S5 WebUI views and CHAT-03 follow-ups (row 40, #1522)
Dewey's round 3 candidate, manifest
agents/dewey/work/queue-40/candidate-manifest-r3.sha256 (d0aa0ded,
27 files, checked OK in the canonical tree).

- WebUI inbox, tasks, agents and trail views, read-only over /api/bus.
  The README says the bus proof ends at the Console process.
- CHAT-03 seal: the engine command is fixed, the engine environment is
  explicit, SEAL_FLAGS has --no-approve, escalating is cleared on throw.
- Terminal input typed after Ctrl-T or Ctrl-O is held. Only the run whose
  own parse set held drains it (T1), and #run catches errors per action.
- DEFERRED keeps N2 and moves F2 to done, citing T1.

Reviews: Filbert approve (comment 27011, rev 260), Darkwing approve
(27013, rev 264). Landing gate on 8cad7722 plus the candidate: webui 22,
conversation 161, control-board 124, every scripts/test-*.sh green,
test-task 98/0. Mutant Mr survives; its flows test is the first
follow-up row.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 22:05:43 -05:00

526 lines
27 KiB
Markdown

# Slice 1 S5: WebUI views for inbox, tasks, agents and trails (design note)
Dewey, 2026-10-04. Row 40, #1522. Design only: no code, no commit. S5
follows S4, so nothing here starts the build.
Revision 2, same day: lead decision 56 (c9c1699a) answered Q1 to Q5 and
accepted all five recommendations. Sections 1, 3.2, 3.5, 5 and 7 now say
what was decided. Section 8 is a new finding for the Q1 module. The
mockup is in `mockups/slice1/`, checked by `checks/slice1-verify.mjs`.
Its fixtures follow schema v3a, and the verify script loads every row
into that schema.
Revision 3, same day. Three things changed:
- Schema v3b (179ffe35, lead decision 60) landed. It adds the
`task_current` view and builds `tasks_open` on it (lead decision 59).
The fixture check now loads v3b and reads current state from those two
views. It agrees with the page on every task, #42 included. Section 8
records the ruling.
- The mockup is reshaped to the Q1 module (Sage's Q1 shapes). `data.js`
holds raw v3b rows written the way the candidate broker writes them.
`mockups/slice1/q1.js` ports the four reader verbs, and `reads(S)` in
`app.js` builds the views only from what those verbs return. A new
check runs Rocko's own candidate reader over the same rows and
compares. Section 1 and section 7 describe this.
- Inbox rows now carry `authorization {action, target, approvalChoice}`
from the `decision.raise` event, and the views show it (section 3.1).
What the page needs and no Q1 verb returns is labelled on the page and
listed for Rocko in section 9.
Sources, pinned by sha256 prefix:
- brief `docs/plans/2026-10-04_slice-1.md` (655e1290, commit 43c48d7a), S5
section and the shared rules;
- data model note (948b94ce), addendum A (0b36acb0), addendum B (14e747f5).
B wins over A, and A wins over the note;
- schema v3 `agents/darkwing/work/slice1-proto/schema-v3.sql` (084d0a07,
commit 7ed83178, lead decision 55), replaced by schema v3a
`schema-v3a.sql` (d55e41fd), which S2 builds on, then by schema v3b
`schema-v3b.sql` (179ffe35, lead decision 60). The mockup follows v3b;
- `packages/bus` as committed in d27042fa (S2c, row 44; S2 was
38828a2c, S2b 4afab552): broker.mjs 51e03b33, store.mjs e69cb395,
views.mjs 446b6da4;
- PRD draft 0.4 `docs/prd/mosaic-stack.md` (d907705a): REQ-WEB-1,
REQ-DEC-1 to 4, REQ-CLI-1, REQ-EVT-1; round 3, 7B;
- DEFERRED.md, "CHAT-03 I1 round 2 follow-ups, for the I3 brief".
Not read on purpose: today's queue. The views follow the data model.
## 1. What the WebUI reads, and through what
REQ-WEB-1 says the WebUI shows the same data as the CLI. The S4 brief says
every CLI command reads through the broker and never opens `bus.sqlite`.
The WebUI follows the same rule:
- The browser never talks to the broker. The WebUI server (`packages/webui`)
calls the broker's read verbs over the local socket and serves JSON to
the page, the way it proxies the board today.
- The WebUI never opens `bus.sqlite`, the business file or a token file.
Bucket titles, bot names and launch limits come from the broker, which
already loaded them.
- The WebUI adds no write route for slice 1 data (section 6).
One set of read functions serves both renderers, so the two can't drift.
Lead decision 56, Q1: the module lives in `packages/bus`, Rocko owns it,
and both the CLI and the WebUI import it. The CLI formats text and the
WebUI server serves JSON. The module, `packages/bus/src/views.mjs`, is a
thin client: `inbox`, `tasks`, `agents` and `trail(subject)`, each
one broker call. The mockup's `mockups/slice1/q1.js` stands in for those
four calls over fixture rows, and the verify script checks it returns
what the broker at commit d27042fa returns. `reads(S)` in `mockups/slice1/app.js`
builds the view objects from the stand-in alone. A second object,
`missing(S)`, holds what no verb returns. Each entry names its source,
and every renderer that shows one labels it "not in the Q1 module".
When Rocko pins an example fixture, it replaces `data.js`.
## 2. Routes
Console keeps its shell, its palettes and its keyboard rules. The left
column gains a section list above the project tree. Hash routes work with
the back button, as the mockups did.
| Route | View | Main source |
|---|---|---|
| `#/` | Control board (unchanged) | the board scan |
| `#/inbox` | Decisions routed to the human, open first | `decisions`, `decision_events` |
| `#/inbox/<id>` | One decision, its options and its history | same, plus `supersedes` chain |
| `#/tasks` | Tasks by state | latest `task_snapshots` per task, `task.*` events |
| `#/tasks/<ref>` | One task and its trail | same, plus the trail (section 3.4) |
| `#/agents` | Role instances, holders and live sessions | business file via broker, `role_claims`, `session.*` events |
| `#/trail/task/<ref>`, `#/trail/decision/<id>` | The ordered trail | events, decisions, messages, deliveries |
| `#/conversation/...` | CHAT-02 history and CHAT-03 drive (existing) | the board and `packages/conversation` |
A task ref in a URL is `vikunja:<project>/<task>`, encoded. The `#12` a
person sees in Vikunja is display only (addendum A section 1).
## 3. The views
Every view keeps the Console states: loading, empty, error with the last
good data kept and a warning, and refusal with the reason. All
agent-authored text (questions, option text, recommendations, message
bodies, task titles and descriptions) is shown as text under the CHAT-02
rules: Markdown stays as source, and controls and bidi marks show as
visible symbols.
### 3.1 Inbox
- Rows: decisions with `route_to = 'human'` and no closing row (`resolved`,
`withdrawn`, `expired`). Blocking first (the `urgent_inbox` view), then
oldest first. The note's rule is "gated first, then oldest", and every
human-routed decision is gated, so `blocking` is the useful split.
- Each row: question, class and action, raised by role and run, age,
`task_ref` and `requirement_ref` as links, a "blocking" badge, and
whether a DM went out (a `deliveries` row with transport `discord-dm`).
- Detail: the options as a numbered list of key and text, the
recommendation marked, the `seen` history, and earlier versions through
`supersedes`, each shown `withdrawn`.
- Approval context, from the row's `authorization`: the card and the
detail say "Choosing “<key>” approves <action> on <target>.", and the
approving option carries an "approves <action>" badge. The approving
choice need not be the recommendation: in the fixture, the conflict on
#48 recommends `queue`, and `vikunja` approves `task.close`. A decision
without a recorded raise says no option is marked as approving.
- Resolution stays in the CLI (REQ-DEC-3). The detail shows the exact
command, `mosaic decide <id> <key>`, one per option, with a Copy button.
Copy puts text on the clipboard and does nothing else. There is no
Resolve button, disabled or otherwise.
- A second tab, "Routed to roles", lists open cross-role decisions by
arbiter (`pm`, `cto`). It's read-only too. Closed decisions get a
"Recent" list: choice, by, via and time open.
### 3.2 Tasks
- Rows: the latest snapshot of each task in the business, grouped by the
five bucket titles (`todo`, `in-progress`, `in-review`, `blocked`,
`done`). `done` is collapsed and shows the last 20.
- Columns: `#index` and title, assignee (bot name), requirement id (from
`task.created`, not the description line), due date, priority, percent
done, queue row if the task has one, and the time of the last change.
- Markers, each linked to its event:
- "changed in Vikunja" for a task in `task_external_changes`. A task the
broker never wrote is always in that view (addendum B section 5), so
the marker reads "not created by the stack" when no `self` snapshot
exists;
- "conflict" for an unresolved `task.conflict`, linked to the decision
raised to the PM;
- "missing" with the reason (`moved`, `not-found`, `no-access`) for a
tombstone, and the new project id for `moved`.
- Freshness: a line under the heading, "Vikunja last read <time>", from
the broker status read S3 adds (Q2).
- Which snapshot to show: not always the latest one. Section 8.
- "Open in Vikunja" links to the task on `tracker.baseUrl`. It leaves
Console and changes nothing.
### 3.3 Agents
- One row per role instance in the business file (`pm`, `cto`, `coder`,
`reviewer` in slice 1): definition, harness, model.
- Holder: the latest `role_claims` row for the instance. A `claim` shows
the holder run, harness, address and time held. `release` and `revoke`
show "not held" and who released or revoked it; a revoke links its
decision.
- A claim from a T3 seat carries "advisory" (decision 46, note 6.3).
- Live sessions: `session.launched` rows with no matching `session.ended`,
with model family and `launched_by`. Above the table, live counts against
the business file's limits, for example "Opus 2 of 4, Sonnet 1 of 4", and
the launch state from the `launch_state` view ("launching allowed" or
"launching revoked at <time>").
- Credentials: per instance and service, the latest of
`credential.expiring`, `credential.expired` and `credential.changed`.
Service names and dates only. There is never a value or a path.
- Today's board seats stay on `#/`. The Agents view is about roles. A seat
that holds a role links to its board row.
### 3.4 Trail
The S4 brief lists the order: request, decisions, launches, task changes,
review, close. The trail view is a single time-ordered list with one row
per event and a filter by kind. Each row says what happened, which role
and run did it, and links to the object.
For a task, the rows I can join from schema v3:
- events with `subject` = the task ref (`task.created`, `.assigned`,
`.state`, `.closed`, `.changed.external`, `.conflict`, `.missing`);
- decisions with `task_ref` = the task ref, and their `decision_events`;
- messages that cite one of those decisions, with their deliveries;
- `review.requested` and `review.verdict`, if their subject is the task ref
or its queue row.
What I couldn't join from the schema alone: which launch worked on the
task, and which `human.input` was the original request. Q3 was decided:
`task.created` names the request in `body.request`, and the broker's
trail follows it to the `human.input` and its message. Launches come in
through the runs seen on the trail's rows. For a decision, the trail is the decision, its events, the
messages that cite it and the events whose body cites it.
### 3.5 Conversations and CHAT-03
The brief puts two I3 follow-ups into S5, because the live PM session
needs them:
- the seal covers the engine command: the I3 entry point takes no engine
command or `preArgs` from config, so only the pinned Pi starts;
- an explicit engine environment: the engine starts with an allow-listed
environment, not `process.env`. That matches addendum A section 4, which
starts every agent with an allow-listed environment.
Lead decision 56, Q5, adds two more of the DEFERRED items to S5: clear
`escalating` after a throw, and handle takeover plus Enter arriving in one
chunk. Both sit in files S5 already changes. The test that members are
listed only under freeze stays deferred.
Which session Console drives depends on S6. The PM moves off T3 there, and
the stack owns its session. Console shows the PM's conversation from
whatever S6 records, through `packages/conversation`.
## 4. Gate E
The script for the demonstration is now `S5-GATE-E.md` (2026-10-09).
This section stays as the design draft it replaced.
Round 3, 7B: Gate E is shown during this step, not separately. The brief:
an interactive demonstration with all seats, then Jason's workday ruling.
Live cutover still needs its own approval.
A draft script, to be fixed once S4 and S6 exist:
1. Jason opens Console. The board shows the seats, and Agents shows the
four role instances with their holders.
2. He sends the PM a one-sentence request from the CLI, then watches the
task appear in Tasks with its requirement id.
3. A gated decision reaches the inbox and his DM. He reads it in Console,
copies the `mosaic decide` command and runs it in a terminal. The
inbox row closes on the next refresh.
4. He follows the task's trail to review and close.
5. He drives a conversation through CHAT-03 and takes control, then hands
it back.
6. He uses Console for the rest of his workday and rules on it.
Evidence: browser runs against a fixture broker with recorded data, and
the live run's screen captures. Model calls during the demonstration are
Jason's to start. I don't run them for discovery.
## 5. Questions, now decided
Lead decision 56 (c9c1699a) accepted all five recommendations below. Each
answer is quoted after the question; the original text stays for the
record.
Q1. **One read layer for CLI and WebUI.** Recommendation: S4 exposes the
inbox, tasks, agents and trail reads as functions over the broker client,
in a module `packages/webui` can import. The CLI formats text and the
WebUI serves JSON. For Rocko, through Sage.
*Decided:* one read module in `packages/bus`, owned by Rocko, imported by
the CLI and the WebUI.
Q2. **Freshness of task data.** A quiet poll writes no snapshot (addendum
B section 5), so the database can't say when Vikunja was last read, or
whether the poller is failing. Recommendation: the broker answers a status
read with the last successful board read and cursor read per project, and
the last poll error. S3 owns the poller and S2 the read verb. For Darkwing
and Rocko.
*Decided:* the broker status read goes in S3, Darkwing's.
Q3. **Trail join keys.** `session.launched` and `human.input` carry no
task ref in the schema. Recommendation: put the task ref in `subject` for
any event about one task, including `session.launched` when the launch is
for a task, and `human.input` when it creates one. That is a body and
subject rule, not a new kind, so the closed kind list holds. For Rocko and
Darkwing.
*Decided:* the task ref goes in `subject` for any event about one task.
`task.create` names the `human.input` that asked for it. Darkwing picks
whether a trigger or the broker enforces it. Schema v3a does it with
triggers: `events_task_subject` for every `task.*` event, and
`events_task_created_body`, which requires `request` (the `human.input`
id) and `requirement` in the `task.created` body. For `session.launched`
and the other kinds, the broker sets `subject`.
My original Q3 text was wrong on one point. A `human.input` can't carry
the task ref, because the task doesn't exist when the request is
recorded. The join runs the other way: the task trail takes the
`human.input` named in `task.created.request`. The mockup does that, and
a check confirms that #41's trail shows its own request and not another
task's.
Q4. **`seen` from the browser.** `decision_events` allows `op = 'seen'`,
and the note lists `webui` under `via`. Recommendation: the WebUI writes
nothing in slice 1, not even `seen`. Every broker write is stamped from a
launch record or comes from the CLI outside a run, and an unauthenticated
loopback page is neither. `seen` from the WebUI comes with Pocket ID. For
Sage.
*Decided:* the WebUI writes nothing in slice 1. It shows the `mosaic
decide` command to copy.
Q5. **I3 follow-ups beyond the brief's two.** Recommendation: fix
`escalating` and the takeover-plus-Enter case in S5, since both sit in
files S5 already owns. Keep the freeze test as a follow-up. For Sage.
*Decided:* as recommended (section 3.5).
## 6. Boundaries
- No write route for decisions, tasks, claims, messages or events. Copy is
clipboard only.
- The page and server stay loopback-only with the existing checks (Host,
Origin, body limits, no CORS). The new routes are GET only.
- No token value, token path or business-file path reaches the page.
Credential rows show the service and dates.
- Agent-authored text is inert.
- No model calls. Tests use a fixture broker.
## 7. Before S4 lands
- Done: a Console mockup of the four views, `mockups/slice1/`.
`checks/slice1-verify.mjs` checks it at eight widths, ten palettes in
three modes, and covers inert text, read-only behaviour, the inbox,
tasks, agents and trail rules, the loading/empty/failed/refused states,
the keyboard and long text.
- Done: fixtures as raw schema v3b rows (`data.js`), written in the
broker's order and with its millisecond timestamps. The Store
refuses any `at` or `read_at` that isn't the 24-character
`toISOString()` form; the schema doesn't check that, so the fixture had
to change.
- Check 15 inserts every fixture row (104) into schema v3b (179ffe35) in
time order, so its CHECKs and triggers judge the fixture. It compares
`urgent_inbox`, `task_external_changes`, `task_current` and
`tasks_open` with the page, and requires the "stale read" marks to be
exactly the snapshot rows `task_current` skips (#42 only).
- Check 16 extracts `packages/bus` from commit d27042fa (S2c, row 44;
S2 was 38828a2c, S2b 4afab552) with `git archive`, so working-tree
edits don't count. It opens that Store and Broker on a temporary data
root, inserts the same rows, binds a reader and requests `inbox`,
`tasks`, `agents` and 21 trails (every task, decision and message). The
results must equal `q1.js` on the same rows: 203 rows, equal, with
broker.mjs 51e03b33, store.mjs e69cb395 and schema.sql 179ffe35. It
checks the reader only: rows go in by SQL, not through the broker's
writers, so it can't tell whether the broker would write them
(section 10). It
writes nothing outside its temporary directory, which it removes. Run
from a copy outside the repository, it needs `DEWEY_S1_REPO`, or it
records "not verified".
- A mutation run in a scratch copy breaks one rule at a time, and each
check has to fail on the mutant it guards against. Round 3, 21
mutants, 20 killed on the first pass. The survivor reversed the
decision trail's order; check 8 only checked the task trail's order,
and now checks both. M5 (stale mark on the current row) failed check
15, not check 6 as I had expected; check 15 now owns that rule. After
the fix, 21 of 21 (`~/dewey-scratch/s1mut2/mutants.txt`, scratch only).
Report: `evidence/slice1/report.json`.
- Next: switch `data.js` to Rocko's pinned example fixture when it
arrives. Section 9's gaps decide what S5-B1 needs beyond the four verbs.
## 8. Finding: a stale poll read becomes the latest snapshot
*Ruled:* lead decision 59 (e153c3a3) takes the read-side fix and puts it
in the schema. The poller keeps writing the stale row as evidence. A v3b
amendment (Darkwing) adds a view of each task's current snapshot that
skips the stale `poll` row, and rebuilds `tasks_open` on that view. The
Q1 module, the CLI and the WebUI read that view and don't reimplement the
rule. When v3b lands, the fixture check moves to it and expects
`tasks_open` to match the page on every row, #42 included. The Tasks view
keeps the "stale read, not shown" marker in the snapshot list.
*Landed:* v3b (179ffe35, lead decision 60). Check 15 loads it and agrees
with the page on every task, #42 included. The page reads the current row
from the stand-in `tasks` verb, which reads `task_current`, and marks as
stale the snapshot rows after it. The original finding follows.
For Rocko (the Q1 module) and Darkwing (S3 poller), through Sage.
Addendum B section 5 writes a poll snapshot whenever its digest differs
from the task's latest snapshot, and ignores a stale read when it detects
external changes: `task_external_changes` requires
`p.read_at > ls.at`, so a board read that started before the broker's
own write doesn't count as a change in Vikunja. The next tick settles it.
The stale read is still stored, though, and it is now the latest
snapshot. Anything that shows "the latest snapshot per task", including
`tasks_open` and the Tasks view, shows the old bucket until the next tick.
Example in the fixtures, task #42:
- 14:59:40, `self` snapshot: the broker moved #42 to `in-progress`.
`updated` stays 09:50, because a move between open columns doesn't change
it (probe P3).
- 14:59:41, `poll` snapshot, `read_at` 14:59:39: the board read started
before the move and still sees `todo`. Its digest differs, so it is
written.
- Latest snapshot: `todo`. Not external, correctly. Shown as `todo`,
wrongly.
The verify script confirms this against the real schema. After the
fixture is loaded into schema v3a, `tasks_open` gives `todo` for #42,
while the page shows `in-progress`. That is the only row where they
differ.
What the mockup does: when choosing the row to display, skip a poll
snapshot whose `read_at` is at or before the latest `self` snapshot's
`at`. The task's snapshot list still shows that row, marked "stale read,
not shown". The verify script checks #42 sits under `in-progress` and the
marker.
Recommendation: the Q1 module applies the same rule, and `tasks_open` gets
it too, so the CLI and the WebUI agree. The other fix is in the poller:
don't write a poll snapshot whose `read_at` is at or before the latest
`self` snapshot. That loses no information the external-change rule uses,
but it drops a row from the evidence, so I'd leave the choice to Darkwing.
## 9. What the views need that no Q1 verb returns
*Ruled:* lead decision 63 (Sage, 2026-10-05) sends this list to Rocko
for S4. Session events go to S6 and credential events to S3. Whatever S4
doesn't add keeps its "not in the Q1 module" label in S5.
For Rocko, through Sage. The reader (d27042fa) has four verbs: `inbox`,
`tasks`, `agents` and `trail(subject)`. The mockup builds everything it
can from those. The rest comes from `missing(S)` in `q1.js`, and the page
labels each one "not in the Q1 module". Suggestions are the smallest
change I can see; the choice is Rocko's.
Inbox and decisions:
- Decisions routed to roles, and closed decisions (the "Routed to roles"
tab and "Recent"). `inbox` returns only open, human-routed decisions.
Suggestion: an argument to `inbox`, or a `decisions` verb.
- Seen and DM state per inbox row. The page gets them with one trail read
per decision. Suggestion: `seen_at` and `dm_at` on the inbox row.
- The forward link from a withdrawn decision to the one that replaced it.
The trail's decision query matches `id` and `task_ref` only.
Suggestion: add `OR supersedes = ?`.
- Approval context for a decision that isn't in the inbox. The page reads
it from the `decision.raise` event in the trail, which works, but it is
the same derivation `#decisionView` already does.
Tasks:
- Requirement, conflict, external-change fields, review and the missing
reason all need one trail read per task. Suggestion: an `external` flag
from `task_external_changes` and the requirement on each `tasks` row.
- Bucket titles, bot names and the tracker `baseUrl`. They are in the
business file and the kanban view, which the broker has loaded.
- Freshness ("Vikunja last read"): the S3 status read, not built yet.
Agents:
- Role instances, limits and model families, from the business file.
- Launch state: the `launch_state` view exists in the schema; no verb
returns it.
- Live and ended sessions. Nothing writes `session.ended` yet, and
`session.launched` carries no model and no `launched_by`.
- Credential notices. Nothing writes credential events yet.
- `agents` returns only roles whose latest row is a claim, so the view
can't say who released or revoked a role. Suggestion: return the latest
row per role and let the reader filter.
## 10. Finding: S2b stopped an agent message citing its decision; S2c restores it
*Ruled:* lead decision 65 (dd35a0ab) took none of the options below as
written. For a within-role `message.send`, `decision` is a citation: the
broker checks the decision exists and stores it in `messages.decision`,
but doesn't class-match it, doesn't consume it, and leaves it out of the
`action.allowed` event. For a send that isn't within-role, it stays the
authority. `messages.decision` keeps its "about this decision" meaning,
and messages 101 and 102 stay valid. S4 still sends and records the
Discord DM. The fix is row 44, S2c (#1526, Rocko, Darkwing reviewing).
*Landed:* S2c is commit d27042fa (row 44 done at queue rev 157), and it
restores messages 101 and 102: the broker at d27042fa writes both. The
probe below, rerun on d27042fa, shows a within-role citing send is
written, the decision is stored and not consumed (a second citing
message is written too), an unknown decision refuses with
`decision-not-found`, and the send's `action.allowed` event is {action
`message.send`, class `within-role`, target `human`} with no decision.
No `action.allowed` row outside `decision.raise` names the decision. The
fixture now carries that event 1 ms before each of 101 and 102, and
check 16 is pinned to d27042fa. Check 16 still inserts by SQL, so a
broker-written fixture with a citing `DECISION` message remains the
regression test it can't give. The original finding follows.
For Rocko and Sage. Found 2026-10-05 while repinning check 16 to S2b.
`message.send` takes `decision?`, and the broker stores it in
`messages.decision`. Up to S2 (38828a2c), a role with `message.send` in
its within-role authority returned before the decision was looked at, so
`decision` worked as a citation: the PM could send the human a `DECISION`
message about an open gated decision. S2b (4afab552) passes every agent
send through `#consumeAuthority`, which now checks a supplied decision
even for a within-role action. The decision's class and action must
match `message.send`, so a citation of a `role.launch` decision refuses
with `decision-mismatch`.
Probe (`~/dewey-scratch/s2b-probe.*/probe.mjs`, both commits extracted
with `git archive`, PM with `message.send` within role, an open blocking
`role.launch` decision routed to the human):
| | 38828a2c | 4afab552 | d27042fa (S2c) |
| --- | --- | --- | --- |
| PM `DECISION` message with `decision: <id>` | written | refused, `decision-mismatch` | written; decision stored, not consumed |
| the same message without `decision` | written | written, plus `action.allowed` {action `message.send`, class `within-role`, target `human`} with subject `human` | written, plus the same `action.allowed` |
| citing an unknown decision | refused, `decision-not-found` | refused, `decision-not-found` | refused, `decision-not-found` |
What depends on the citation:
- the trail's decision query collects `messages WHERE decision = ?`, and
deliveries through those messages;
- the inbox's "DM sent" state, which comes from a `discord-dm` delivery
of a message citing the decision (section 3.1). The fixture's messages
101 and 102 are those DMs, and the broker would now refuse both.
After S2b, a non-null `messages.decision` can only name a decision that
approved `message.send` itself, and that decision is used up by the
send. The column now means "the decision that authorized this message".
The data model and the views read it as "the decision this message is
about".
Options, for Rocko and Sage:
1. A separate citation argument (for example `about`), stored apart from
the authorizing decision. That needs a schema column or an event.
2. The Discord connector sends the DM from the decision itself, not from
an agent message, and records its own delivery. The views then read
"DM sent" from that record.
3. Keep S2b's rule, and drop "DM sent" and decision messages from the
views.
My recommendation is 2. REQ-DEC-4 and the slice 1 brief's S4 section
already give the outbound DM for a blocking gated decision to S4,
"through the existing connector (#1509)", so the PM doesn't need to send
one. It also leaves `message.send`'s `decision` meaning one thing. S4
then has to record the DM somewhere the views can read it; Darkwing's
prototype has a `digest.sent` event with a `transport` for the digest
side. Until a ruling, the fixture keeps messages
101 and 102 as they are, and this section marks them as rows the broker
at 4afab552 would refuse. (Ruled above: they stay, and S2c restores
them.)