Handoff: email-proposal Matrix review live (v0.1.0:91); bot role + whole-thread redaction
Durable updates after the email-proposal review session: - AGENTS.md: roles admin/member -> admin/member/bot; add a Conventions entry on the bot role and the reach(role)-vs-autonomy(approval gate) principle. - matrix-intake guide: rewrite the bridge section to final behavior (redact_thread whole-thread redaction, the Element 'show deleted' client-setting dependency for full clearing, the redact_resolved.py backfill tool, deploy gotchas). - Current state rewritten lean (14->8 bullets); test count 27->30.
This commit is contained in:
@@ -108,17 +108,36 @@ the web panel. The CRM (box, stdlib, no matrix-nio) can't post to Matrix, so the
|
||||
- **open** — pending, posted, not closed → the bot rebuilds its `event_id → proposal` routing map
|
||||
from these on **every poll**, so replies still route **after a bot restart** (unlike intake's
|
||||
in-memory-only store — the state lives CRM-side in `email_proposal_matrix`).
|
||||
- **to_close** — decided on the **web** while a thread was open → the bot posts a "decided on the
|
||||
web — thread closed" line and `POST .../{id}/matrix {closed:true}`.
|
||||
- **to_close** — decided on the **web** while a thread was open → the bot clears it (see redaction
|
||||
below) and `POST .../{id}/matrix {closed:true}`.
|
||||
- **In-thread replies** (`bot.handle_email_reply`, `email_proposals.interpret`): `yes` →
|
||||
`POST .../{id}/decide {decision:"approve", note}` (appends the note to the grid, source='matrix',
|
||||
closes the thread atomically); `no` → dismiss; **anything else → NL revision of the note** via
|
||||
local Qwen (`email_proposals.revise_note`, no Claude/scrub) — re-rendered for re-approval, so the
|
||||
draft→approve gate holds. A no-op/empty revision re-prompts instead of saying "Updated".
|
||||
- **Two surfaces, one source of truth.** Decide on the web → the bot announces + closes the thread;
|
||||
- **Card formatting:** `email_proposals.render_card` frames every card/reply with a `RULE` dash line
|
||||
top and bottom (`frame()`) so threads don't bleed together on mobile, and the note **names who
|
||||
emailed whom** ("{teammate} emailed {investor}" / "{sender} emailed the team") rather than a bare
|
||||
Sent/Received — the wording is built server-side in `propose_email_activity_notes`.
|
||||
- **Decided threads are redacted, not just closed.** On any conclusive decision (Matrix or web) the
|
||||
bot calls `redact_thread(root)`: redact the card, then scan recent history (`room_messages`,
|
||||
`MessageDirection.back`) for that root's `m.thread` replies and redact those too — so a resolved
|
||||
thread clears from the **threads view**, not only the timeline. **No confirmation is posted on
|
||||
success** (the thread vanishing is the ack; a confirmation reply would keep the thread alive).
|
||||
- **Needs the bot to hold a `redact`/moderator power level** in the review room — required to
|
||||
redact the *human's* yes/no reply (its own card needs no power). Without it, the reply lingers.
|
||||
- **Full clearing depends on a client setting:** redaction removes the events, but Element shows a
|
||||
"Message deleted" placeholder by default — turn OFF "show removed/deleted messages" in Element and
|
||||
both the main chat and the threads view clear completely. (Verified the intended UX 2026-06-18.)
|
||||
- **One-time backfill:** `backend/matrix_intake/redact_resolved.py` (dry-run default; `--apply`)
|
||||
clears threads decided *before* this shipped (already `closed`, so the poll's to_close never
|
||||
touches them). Run on the Spark: `docker compose run --rm intake python -u
|
||||
backend/matrix_intake/redact_resolved.py [--apply]`. It keeps cards still pending (CRM `open`)
|
||||
and redacts every other card + its replies.
|
||||
- **Two surfaces, one source of truth.** Decide on the web → the bot redacts + closes the thread;
|
||||
decide on Matrix → the web panel polls `/api/activity/proposals` (~25s) and the card clears.
|
||||
`email_proposal_matrix` (1:1 side row, migration `0003`) carries `event_id`/`posted_at`/`closed_at`;
|
||||
a matrix decision sets `closed_at` in the same txn so it's never re-announced via `to_close`.
|
||||
a matrix decision sets `closed_at` in the same txn so it's never re-processed via `to_close`.
|
||||
- **Pure logic is `email_proposals.py`** (card render, reply grammar, note revision) — unit-tested
|
||||
offline in `test_email_proposals.py`; the async poll/post wiring is in `bot.py` (live-smoke only).
|
||||
- **Known minors (low-likelihood, ~5-person team):** if the CRM is unreachable *between* posting a
|
||||
|
||||
Reference in New Issue
Block a user