Files
PaperClipAI/server
DottaandClaude Opus 5.5 fca1f1be4f fix(attention): keep replaced confirmations out of Needs you after the newest is settled (#19520)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Needs you is the board inbox. It lists the decisions that wait for a
person.
> - Agents often ask for the same confirmation again on one task. Needs
you shows only the newest pending confirmation for each task.
> - When a person approved that newest card (for example in chat), or
the card expired, the older card that it replaced came back into Needs
you.
> - Manual onboarding tests reported this: decision cards that were
already approved in chat still showed in Needs you.
> - This pull request makes the newest answered or expired confirmation
also hide the older pending confirmations on the same task.
> - The benefit is that Needs you does not show stale cards after a
person answers the current one.

## Linked Issues or Issue Description

**What happened?**
A person approved a decision card in chat. Needs you still showed a
decision card for the same task. Expired cards also caused an older card
to show again.

**Steps to reproduce**
1. An agent creates a confirmation on a task. Then it creates a second
confirmation on the same task.
2. Needs you shows only the second card.
3. Approve the second card in chat (or let it expire).
4. The first card now shows in Needs you.

**Expected behavior**
Needs you shows no card for the task, because the newest ask is settled.

**Root cause**
`collapsePendingConfirmationsToNewest` compared only pending rows. Chat
cards use the same `issue_thread_interactions` row and the same accept
service, so there is no duplicate record. The server already drops
answered rows. The problem was the older pending row that the collapse
hid before.

## What Changed

- `server/src/services/attention-source-eligibility.ts`: the collapse
takes the newest settled (accepted, rejected, answered, expired)
ordinary confirmation for each issue. Older pending ordinary
confirmations stay hidden. A new `loadNewestSettledConfirmationAt`
helper reads that value with one grouped query.
- Cancelled and failed replacements do not settle the ask, so the older
card returns (the existing retention test keeps this behavior).
- Tool-action confirmations and quality-review confirmations are
excluded from the settled check. A settled tool action never hides a
different pending tool action.
- `server/src/services/attention.ts` and
`server/src/services/attention-retention.ts` use the new input, so the
feed and the retention worker agree.
- Review follow-up: the summary and task-index payload keeps
`toolAction` and `secretProposal`. A settled card only hides older asks
addressed to the same recipient. Tool actions, secret proposals and app
connections (`connectionAuthorization`) form one governed set. They
match the set the other confirmation readers use. A settled governed
card never settles an ordinary ask, and a pending governed card is never
hidden.
- `server/src/__tests__/attention-service.test.ts`: new tests for
"approved from chat", "rejected from chat", "expired", a re-ask after a
settled card, another recipient, a settled secret proposal, tool actions
in `list()`, and tool actions in `summary()`/`taskIndex()`. The tests
use two agents, because the service already expires a same-agent re-ask
at creation.

## Verification

Red on `master` (819420a60a), before the code change:

```
 RUN  v5.0.3 /Users/dotta/paperclipai/paperclip/.claude/worktrees/fix-n10-stale-needs-you-cards-47c895/server
 ❯ src/__tests__/attention-service.test.ts (36 tests | 2 failed | 34 skipped) 14165ms
   ❯ attention service (36)
     × does not resurface an older confirmation once the newest one is approved from chat  3502ms
     × does not resurface an older confirmation once the newest one is expired  2097ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
 FAIL  src/__tests__/attention-service.test.ts > attention service > does not resurface an older confirmation once the newest one is approved from chat 
AssertionError: expected [ Array(1) ] to deeply equal []
- Expected
+ Received
- []
+ [
+   "a1f2f085-e4b2-42f1-ac3c-71d85f6e164c",
+ ]
 ❯ src/__tests__/attention-service.test.ts:1609:32
    1607|     }
    1608|
    1609|     expect(await pendingIds()).toEqual([]);
       |                                ^
    1610|   });
    1611|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
 FAIL  src/__tests__/attention-service.test.ts > attention service > does not resurface an older confirmation once the newest one is expired 
AssertionError: expected [ Array(1) ] to deeply equal []
- Expected
+ Received
- []
+ [
+   "8c8d01ad-1084-47b2-927a-24d18529a9a6",
+ ]
```

Green after the change (same tests):

```
$ npx vitest run src/__tests__/attention-service.test.ts -t "resurface an older confirmation|tool-action approval"
 ✓ does not resurface an older confirmation once the newest one is approved from chat
 ✓ does not resurface an older confirmation once the newest one is expired
 ✓ keeps a pending tool-action approval visible after a newer tool action is settled
```

Review follow-up red (on ebe8e0c0ae, before the follow-up code change):

```
 × keeps a pending tool-action approval in the summary and task index after a newer ordinary confirmation is settled
 × does not let another recipient's settled confirmation hide this recipient's pending one
 × does not let a settled secret proposal hide an older ordinary confirmation
AssertionError: expected [] to include '4c636383-…'
AssertionError: expected [] to deeply equal [ Array(1) ]
```

All three pass after 9245bd7b17.

Second review round red (before a36923c7fc):

```
 × does not let a settled app connection hide an older ordinary confirmation
AssertionError: expected [] to deeply equal [ Array(1) ]
```

It passes after a36923c7fc.

Neighbouring suites (attention-service, attention-retention-work,
issue-review-attention, issue-blocker-attention):

```
      Tests  125 passed (125)
 Test Files  4 passed (4)
```

Staging check (read-only): the ONB staging company had one pending Needs
you item, on a task with no newer card. Staging has no Codex company, so
I could not reproduce the Codex path there.

## Risks

- Low risk. The change is read-only and adds one grouped query when
pending confirmations exist.
- Older ordinary confirmations from the same agent were already expired
at creation. This change affects asks from different agents or users on
the same task.
- The service already expires an older ordinary confirmation from the
same agent at creation. This change affects asks from different agents
or users on the same task.
- Behavior change: an older ordinary confirmation that is still pending
stays out of Needs you after a newer one is answered or expired. It
stays visible in the task thread.
- Not changed: the live-update listener does not match
`issue.interaction.withdrawn` or `issue.interaction_cancelled`. Those
cards can stay in the client cache until the next refetch.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with
tool use and code execution.

## 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 either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:10:26 -06:00
..