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

5.9 KiB

title, summary
title summary
Attention inbox 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

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

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

POST /api/companies/{companyId}/attention/{sourceKind}/{sourceId}/close

Use one of these bodies:

{ "mode": "archive", "fingerprint": "<changeFingerprint from the card>" }
{ "mode": "snooze", "until": "2099-01-01T12:00:00.000Z", "fingerprint": "<changeFingerprint>" }
{ "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

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.