Files
PaperClipAI/docs/api/attention.md
T
8818402e7a feat(inbox): route requests to people and record personal outcomes (#20959)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - The inbox collects requests that need a human decision.
> - Teams need to know who owns each request and who may act on it.
> - A shared close operation must differ from hiding a card for one
person.
> - This pull request adds personal routing and durable outcomes to the
existing attention service.
> - People can claim or transfer work and inspect what another person
did for them.
> - Domain permissions still decide who may resolve each request.

## Linked Issues or Issue Description

**Subsystem affected**

Server, shared contracts, and database. This is the routing foundation
for a stacked inbox UI change.

**Problem or motivation**

A company-wide attention feed does not give teams a clear personal
queue. Archive, withdrawal, transfer, and decisions by another person
need distinct scopes and records.

**Proposed solution**

Resolve a request audience from its addressee, task participants, and
configured owners. Provide personal, unassigned, company, all-assignee,
and archived views. Add claim, transfer, personal archive and snooze,
shared withdrawal, receipts, and personal history.

**Alternatives considered**

A single shared inbox leaves ownership and personal hiding ambiguous.
The service keeps one canonical request and projects its audience
instead of copying requests for each person.

**Roadmap alignment**

This extends the existing Multiple Human Users and Agent Reviews and
Approvals capabilities in ROADMAP.md.

**Additional context**

Refs #19363 and #19520 for the current inbox navigation and confirmation
replacement rules. Refs #20268 for related actionable-request work. That
PR filters actionability; this stack adds audience routing and personal
outcomes.

This is part 1 of a two-PR stack. The inbox UI PR #20961 targets this
branch.

## What Changed

- Add nullable company and project decision owners and a company budget
owner.
- Add audience views and personal badge counts to the canonical
attention projections.
- Add claim, transfer, withdrawal, personal archive, snooze, and restore
operations with permission checks and activity records. Lock addressed
transfers to the current addressee or routing administrators, and
validate the recipient against canonical resolution policies.
- Add receipts and scoped history for work handled by another person.
- Keep closed-task requests and replaced confirmations out of current
cards.
- Add an idempotent migration and a concurrent receipt-history index.
- Update typed fixtures so the existing UI remains compatible with the
new contracts.

## Verification

- Server layered typechecking passes, including all three test-tree
partitions.
- Seven routing and interaction-resolution suites pass all 249 tests
against a dedicated external PostgreSQL test database on `4caecc21d8`.
Six new regressions failed before the repair; they cover takeover
followed by approval, legitimate delegation, excluded creators/review
requesters, and unauthorized claims.
- Prior focused verification covers private approval history and
receipts, transfer visibility, audience escalation, withdrawal
governance, hidden-card counts, per-recipient confirmation replacement,
and SDK source contracts.
- A synthetic three-person company passed 37 routing and lifecycle
acceptance checks. The UI stack adds browser acceptance for the shared
controls.
- The complete [GitHub Actions CI
matrix](https://github.com/paperclipai/paperclip/actions/runs/43134997331)
passes on `4caecc21d891677323a7335f23b1a015cce24c13` (Linux/Node 24:
typecheck, build, general and serialized tests, Runner support, browser
shards, and aggregate gates). Greptile is 5/5 on that exact head with no
open findings. The fresh security scan passes after explicitly
rechecking the addressee-transfer repair.
- `pnpm verify` reports exit 2 because a CI-global script changed; full
CI supplies that broader coverage. Embedded PostgreSQL could not start
on this Mac because shared-memory slots were exhausted, so local
database checks use a separate external test database. Linux CI covers
the embedded setup.

## Risks

- Personal badge counts change to the viewer's assigned requests.
Accessible feed totals and task membership keep their existing scope.
- The migration adds nullable owner fields and a dismissal fingerprint.
Its index runs outside a transaction. The migration is classified and
rerunnable.
- Routing changes the addressee used by resolution authorization. The
current addressee or a routing administrator must authorize an addressed
transfer; the recipient must satisfy writer access, canonical resolver
exclusions, and task review policy under row locks.
- The shared validator export barrel crosses 1,000 lines because it
exports the new attention schemas. Schema implementation stays in the
dedicated attention validator module.

## Model Used

OpenAI GPT-6 through Codex. Used repository tools, code execution, and
browser checks. This session does not expose a more specific runtime
model ID or its context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
Paperclip ticket id
- [x] I have run focused tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
(OpenAPI contracts and docs/api/attention.md)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Scott Tong <scott@scottsmbpm5max.lan>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-11 17:49:39 -08:00

129 lines
5.9 KiB
Markdown

---
title: Attention inbox
summary: Personal request routing, close actions, and decision history
---
The attention API projects pending requests and operational alerts into an inbox.
All routes require a board user with access to the company. Task and approval
privacy checks apply to cards, counts, history, and receipts. Agents cannot use
these board routes.
## Read the inbox
```text
GET /api/companies/{companyId}/attention
GET /api/companies/{companyId}/attention/summary
GET /api/companies/{companyId}/attention/tasks
```
The feed accepts these query parameters:
| Parameter | Meaning |
| --- | --- |
| `view` | `mine`, `unassigned`, `company`, `archived`, or `all`. The API default is `all`. |
| `includeDismissed` | Include personally hidden cards. Default: `false`. |
| `archived` | Read retained source archives instead of current sources. This differs from personal `view=archived`. |
| `all` | Return a complete snapshot instead of a page. |
| `limit`, `cursor` | Page size and opaque continuation cursor. Default size: 50; maximum: 100. |
| `sort` | `activity` or `decide`. |
| `activitySince`, `activityUntil` | Inclusive activity bounds in ISO date-time format. |
| `queue` | Filter by a decision queue key. |
The response includes `items`, `totalCount`, `countsBySourceKind`, `viewCounts`,
`deskBadgeCount`, and `nextCursor`. Related evidence shares one obligation lead
and counts once. `deskBadgeCount` counts requests assigned to the viewer.
Summary returns the same current counts without card bodies. Tasks also returns
unique `taskIds` for visible requests. Neither endpoint returns card bodies.
Routing uses explicit recipients, chat ownership, review participants, task
assignees, human instigators, and configured decision owners. A card with no
owner enters Unassigned. After seven days it moves to Company. A required
request that waits 30 days escalates to the configured decision owner or Company.
Task privacy still applies. Escalation keeps the request pending and its gate closed.
`view=mine` also includes receipts when someone else handles the viewer's request.
Receipts expire after seven days. They do not count as decisions or in the badge.
Every approval-linked task must remain readable to return an approval receipt.
## Claim or transfer
```text
POST /api/companies/{companyId}/attention/{sourceKind}/{sourceId}/claim
POST /api/companies/{companyId}/attention/{sourceKind}/{sourceId}/transfer
```
Claim has no body. Transfer takes `{ "userId": "<user ID>", "reason": "optional" }`.
These operations support `issue_thread_interaction` sources. Claim assigns an
ownerless request to the caller. Changing an addressed request requires its current
addressee or an active company owner/administrator (or an instance administrator).
Transfer assigns it to a writer who can read the task and satisfy the interaction
creator exclusions and the task review policy. Viewers cannot claim or transfer
requests. A chat question stays with its chat owner. A request to an agent that
can still answer stays with that agent.
The addressee is part of resolution authorization, so routing validates authority
and the recipient under task and interaction row locks before updating it. Inbox
view placement alone grants no authority. Concurrent claims and transfers retain
their compare-and-set checks. An identical repeated assignment returns
`alreadyClaimed` or `alreadyTransferred`. Changes record `attention.claimed` or
`attention.transferred` activity.
## Close a card
```text
POST /api/companies/{companyId}/attention/{sourceKind}/{sourceId}/close
```
Use one of these bodies:
```json
{ "mode": "archive", "fingerprint": "<changeFingerprint from the card>" }
```
```json
{ "mode": "snooze", "until": "2099-01-01T12:00:00.000Z", "fingerprint": "<changeFingerprint>" }
```
```json
{ "mode": "withdraw", "reason": "The proposal is no longer needed" }
```
Archive and snooze affect only the caller. The source stays pending. Hiding
obligation evidence hides its lead and all related evidence. The returned
`itemKey` identifies the personal dismissal. Restore it through the existing
inbox-dismissal restore endpoint. A content change or a new assignment to the
viewer can make a hidden card return. A snoozed card also returns when time expires.
Withdrawal affects everyone. It cancels an interaction, approval, or decision
through that source's existing lifecycle. Review policy and execution checks
still apply. A hire request must be rejected through approvals instead.
The card's `closeModes` lists its supported actions. A successful close records
`attention.closed` activity and returns the source, mode, dismissal key, and time.
## Decision history
```text
GET /api/companies/{companyId}/attention/history
```
History includes resolved interactions, decisions, and approvals. `scope=mine` shows work handled by or for the caller. `scope=all`
shows accessible company history. `handledForMe=true` narrows it to work handled
for the caller. Filters include `search`, `outcome`, `agentId`, `projectId`,
`since`, `sort=newest|oldest`, `limit` (maximum 100), and `cursor`.
Withdrawn requests, expired requests, and requests closed with their task have
separate outcomes. Personal archive and snooze do not record consent.
## Errors
| Status | Meaning |
| --- | --- |
| `400` | Invalid source kind, UUID, filters, cursor, or request body; a snooze needs a future time. |
| `403` | No company access, no board user context, read-only membership, or a denied review policy. |
| `404` | Missing source or a source the caller cannot read. |
| `409` | Another person claimed, routed, or answered first; the request is already closed; or execution prevents withdrawal. |
| `422` | Unsupported operation, unavailable recipient, recipient without task access, or a chat ownership constraint. |
A closed-source conflict includes `code: attention_already_closed` and the
recorded resolver when known. Clients should refresh the feed and show that
outcome instead of retrying a decision automatically.