Files
PaperClipAI/doc/idle-tool-review-projection.md
T
DottaandPaperclip 8161f2a813 fix(server): wake tool-review card repair from committed changes (#17468)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Tool requests and invocations store the execution outcome. Review
cards show that outcome.
> - The maintenance tick repeatedly joins these tables even when every
card matches.
> - Committed changes can wake repair. Existing rows can restore work
after restart.
> - A large company backlog also needs a bounded pass so other companies
and shutdown can proceed.
> - This PR uses the shared coordinator for scoped repair, continuation
and failure deadlines. Settled history becomes quiet.

## Linked Issues or Issue Description

**What happened?**

The aggregate maintenance tick scans tool-review projections on every
tick. Matching terminal history still causes database work. A company
with many stale cards can also hold one worker in an unlimited drain.

**What did you expect?**

Relevant writes and startup should wake repair. Failed or unresolved
cards should retain recovery. Matching history should cause no recurring
SQL. Each normal worker pass should honor drain and shutdown.

**Steps to reproduce**

Start an instance with completed tool requests and matching cards. Leave
it idle. The old maintenance tick keeps querying for mismatches. A large
mismatch backlog also drains without a row limit or cancellation check.

This is slice 07 of the sleep migration. It now targets master after
#17458 landed. Related owners are pending expiry #17453 and approved
recovery #17454. Their execution duties remain separate from card
repair. The prior open-PR search found no duplicate projection
migration.

## What Changed

- Register company-scoped projection repair with the existing delivery
coordinator. Remove its recurring aggregate-tick scan.
- Signal through the existing review-write transaction owner and linked
interaction mutations. Ordinary unlinked confirmations do not wake
repair.
- Keep interpretation, locks, current-state checks, activity and
continuation callbacks in the gateway. A repair never repeats a provider
tool call.
- Limit each normal worker pass to 500 rows. Check abort and admission
between rows. Save company progress through truncated and failed passes
so blocked early pages cannot hide later cards.
- Schedule truncated work after one second. Back off unresolved cards
from thirty seconds to five minutes; fresh committed hints reset the
delay. Clear obsolete cursors after empty reconstruction.
- Skip repeated company discovery for scoped hints and the extra
candidate query on an empty sweep with no saved cursor. Attempted or
resumed passes still verify remaining mismatches because reflection
returns no settlement result.
- Add generated migration `0386_steady_epoch.sql` for the existing
`(company_id, interaction_id)` lookup. Use the existing concurrent-index
executor. Add no table.

## Verification

Current candidate: `d4706fbf2a04a00ade30181eb1bd9322e8455818`. All 222
cases in the full gateway/projection/interruption, startup and
scoped-deadline suites pass without skips after the query/retry
efficiency repair; server types and build pass. A single committed
company hint with matching retained history and a separate empty direct
sweep each perform one candidate query. The unresolved-card regression
failed before repair and now verifies backoff through the five-minute
cap, prompt producer activation and quiet after settlement. An initial
assertion incorrectly assumed an entire approval emitted one wake; its
separate real writes require isolating the single-hint measurement.

The earlier master integration `fe6e10629b` additionally established the
following evidence before this efficiency follow-up:

- **320 cases pass** across full gateway/projection/interruption,
startup, scoped-deadline, narrowed identity-read and authenticated
gateway HTTP suites. The HTTP selection includes all 81 cases.
- **Six real PostgreSQL regressions failed before their fixes:** pass
bounds, abort, drain, progress past unprojectable pages, progress past
failed pages, and first repair after empty reconstruction. The complete
owning selection passes all 220 cases.
- Server production typechecking and build pass. Generated index
migration, snapshot drift and numbering/safety checks pass. The five
newest snapshots remain; master journal entries are unchanged.
- A real missing-link query on 30,000 history rows changed from a
sequential scan that filtered all 30,000 rows and touched 429 buffers to
the new index with two buffers. Both treatments returned no rows.
- Normal master integration preserves 35 independent performance files
exactly. The gateway also retains master's narrowed identity read.

Commands include `pnpm --filter @paperclipai/server typecheck`, `pnpm
--filter @paperclipai/server build`, and server Vitest selections for
`tool-gateway-service`, `server-startup-feedback-export`,
`scoped-deadline-work`, `run-identity-projections`, and
`connection-management.integration`, with separate PostgreSQL test
fixtures.

[The behavior and failure audit](doc/idle-tool-review-projection.md)
records the producer contract, earlier fixture failures, index
measurement, red/green regressions and integration evidence. Fresh
current-head CI and Greptile/Ponytail remain required before merge.

## Risks

- Signals use the existing single-active-writer database owner. Another
replica or independent SQL writer needs a wake contract.
- An already issued database query or reflection finishes before
cancellation takes effect. Checks between rows bound further dispatch.
- Legacy manual/global and disabled-scheduler one-shot repairs keep
their complete-sweep behavior. The normal coordinator worker always
supplies its row budget and admission controls.
- Unprojectable cards and thrown failures retain recovery with a
five-minute backoff cap. The company mismatch query still grows with
terminal history. Empty history does not retain a recurring scan.
- The index migration uses concurrent creation and the existing
invalid-index recovery path. Prior SQL and journal entries remain
unchanged.
- Cloud sleep eligibility and external-wake coverage do not change.
Local worker quiet is not proof of host or database suspension.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning and code execution. The exact
serving model identifier and context window were not exposed.

## 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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-27 17:09:31 -05:00

230 lines
14 KiB
Markdown

# Tool-review interaction repair
The tool-action request and invocation are authoritative. The interaction card
is a projection of their outcome. Previously the aggregate maintenance tick
joined these tables repeatedly, even when every card already matched.
The projection worker reconstructs affected companies at startup and reconciles
after committed changes. Producer hints carry the company ID. Subsequent
candidate discovery and paginated repair queries filter by that company; review
writes cannot drive a scan of another company's terminal history. The shared
scoped worker dispatches one company per pass with one-second minimum spacing.
It retains a 30-second retry only for a mismatch or failed reconciliation.
Configured idle tools, pending reviews without terminal outcomes, and matching
terminal history have no recurring projection SQL or provider calls. This is
worker quiet, not whole-instance suspension.
## Ownership and trigger contract
`tool-review-projection.ts` owns candidate discovery and pagination. The gateway
retains card interpretation, current-state checks, locks, activity publication,
and the existing continuation callback. The scheduler consumes company discovery,
`sweep(companyId)`, and the returned retry obligation. No provider payload enters shared scheduling.
No table is added. A concurrent company/interaction lookup index supports the
existing link checks and linked-request updates.
The existing review notification helper now also signals projection repair.
Notifications are awaited before writes and inherit the outer transaction's
commit, rollback, and uncertainty tracking. Unrelated review changes can cause
one empty company-scoped reconciliation; they do not create a recurring deadline.
Interaction-only writes first check for an actual linked tool request using the
company and affected card IDs. Generic confirmation kinds alone do not signal
any tool-review queue. This also preserves recovery for legacy linked cards
without current tool payload flags. These reads happen in the mutation owner,
not a background poll. Sibling selection shares the update predicate and runs
under the existing issue lock before publishing the notification.
| Producer | Notification ownership |
| --- | --- |
| Gateway signing, approval inference, execution claims, provider outcomes, and invalidation | Existing `writeToolReviewChange`; the closed-issue expiry path now uses it too |
| Human approval/rejection | `commitToolActionReview` signals in its existing transaction |
| Pending expiry, approved recovery, interrupted claims, Test-tab settlement | Existing review-write boundary |
| Review-queue invalidation | Existing review-write boundary |
| Interaction withdrawal, skip, terminal-issue expiry | Linked-request resolution signals before the mutation |
| Linked tool-card accept/reject, deleted-addressee cancellation | Projection notification before replacing card state in the owning transaction; unlinked confirmations do not notify |
| Sibling supersession, startup duplicate cleanup, new/historical comment supersession | Projection notification before card replacement, including cases where the request is already terminal |
| Comment-based confirmation resolution | Calls the interaction owner inside the same outer transaction; its later comment-ID update shares that signal |
The audited other interaction writers do not create this mismatch: native bridge
and status-committer summary/status-only updates preserve the projection; native
review retirement owns separate completion-review cards; connection and secret
flows own their separate card types. Document-staleness and plan-resume paths
use document targets, while tool cards use `tool-action:` custom targets.
Reflection consent uses its instruction/profile/skill target keys, not tool-action
keys. A future writer that replaces tool-card result state must use the same
transaction notification boundary. A future writer that changes authoritative
tool outcomes must signal the review boundary.
## Repair, concurrency, and lifecycle
The mismatch predicate preserves the previous policy: terminal executed, failed,
expired, or cancelled requests with a linked interaction whose displayed tool
status differs. Cancelled requests display `expired`. Invocation error and result
summaries come from saved state. Repair never repeats a provider action.
Candidate queries page in request-ID order, 100 rows at a time. A card that cannot
yet be projected does not starve later pages. Per-row failures do not prevent
attempting the remaining candidates; failures keep the retry. A final mismatch
check retains retries for no-op repairs rather than silently declaring them done.
Failed companies retain their own backoff across notifications while other
companies progress. An unresolved no-op projection retains a 30-second deadline;
a new relevant company change can make it actionable sooner. A company pass
still drains all its pages, preserving progress past an unprojectable card.
The joins inspect that company's history on activation. This change removes repeated
idle scans; it does not introduce an indexed projection-debt table or claim that
an activated reconciliation has constant cost independent of history size.
The gateway locks the interaction and request, then checks the current request
status. If another repair already wrote the same status and outcome fields,
it avoids a second write and duplicate activity. Different saved details still
use the existing update path. Missing projection state remains repairable.
The shared deadline worker retains first writes during drain, failed retries,
and changes arriving during reconciliation. Shutdown joins coordinator work.
Startup reconstructs debt from existing rows. With heartbeat scheduling disabled,
startup still performs one pass, preserving the existing disabled-mode contract.
The public/manual combined review sweep remains available; server maintenance
no longer calls it periodically.
## Continuation and sleep limits
Card state is not proof that continuation delivery finished. Existing
`tool_action_deliveries` receipts and their coordinator remain responsible for
that work. Activity publication still occurs after the card transaction; this
slice does not add exactly-once activity delivery or a new outbox.
Signals remain process-local to the supported database owner. Independent SQL
writers and another active server require a separate notification/reconciliation
contract. Cloud eligibility still conservatively blocks retained tool-action
requests. No external wake or sleep-safety guard is relaxed here.
## Verification
Twelve new real-PostgreSQL cases exercise normal approval and single execution,
quiet configured and terminal-history states, four persisted outcome states,
outer commit/rollback, first write during drain, failure retry through drain,
foreground/worker repair contention, multi-page progress past an unprojectable
card, and an administrative card overwrite that reactivates repair. Quiet checks
observe absence of database reads across an hour of controlled time.
The first focused run caught a missing-projection null access in the new
idempotency check; optional access fixes it. The pagination fixture also attempted
to change a referenced request ID; it now orders the additional candidates without
mutating that key. The focused rerun passed all eleven then-existing tests.
After adding the concurrent repair case, all 148 service/startup tests and 92
gateway acceptance tests passed. Workspace typechecking and build passed. All 469 tool-access and 106 interaction
service tests also pass (815 tests across the final selections). The first
interaction run passed 105 cases but timed out in one cleanup hook during a
53-minute local run; the isolated full rerun passed in nine seconds. No production
change or timeout increase was made to force that result. Full repository tests
will run in CI; the local selections do not claim a complete repository test run.
Fresh PR CI/review remains required.
Company-scoping follow-up reproduced two regressions: captured recovery SQL lacked
a company predicate, and ordinary confirmation creation/replacement/cancellation
published three unnecessary tool-projection wakes. Both regressions now pass
through real producers. Additional coverage keeps failed-company retry timing
independent, proves ordinary mutations still complete without recovery scans,
and exercises administrative repair of legacy cards identified by their saved
link rather than payload flags.
Company-scoping follow-up verification: all 180 gateway and startup cases and all 152 interaction and secret-proposal route cases passed against dedicated PostgreSQL test databases. Server typechecking passed. These are local checks; stack CI and deployed sleep qualification remain separate.
## Indexed interaction links
Review found that the new link check had no supporting interaction index. An
ordinary unlinked confirmation could scan all tool-action history for its company.
The generated `0386_steady_epoch.sql` migration adds the non-unique
`(company_id, interaction_id)` index concurrently, and the existing top-level
migration executor handles its creation and interrupted-build recovery. The
schema and generated snapshot describe the same index; older SQL and journal
entries are preserved and snapshot pruning retains the five newest snapshots.
A real PostgreSQL check applied the migration to a fresh dedicated test database
and seeded 30,000 tool-action rows. The ordinary missing-link query returned no
rows in both treatments: without the index it performed a sequential scan,
filtered 30,000 rows and touched 429 shared buffers; with the index it used the
new index and touched two buffers. These are local planner measurements, not a
fleet latency or cost claim. The baseline index removal was rolled back in that
dedicated database, and the index remained valid afterwards. An initial probe
failed while binding a JSON fixture parameter before any comparison; correcting
the parameter encoding made the complete probe pass. That failed fixture is not
counted as a product regression.
Migration numbering/safety checks, generated snapshot drift, server types and
server build pass. All 214 cases in the full gateway/projection/startup/scoped-
deadline selection pass against a separate freshly migrated database, preserving
notification quiet, company isolation, transaction settlement, and interruption
behavior. Latest-head CI and automated reviews remain required merge gates.
## Bounded projection and lifecycle interruption
A review found that one company could drain an unlimited backlog in a single
worker pass. Five real PostgreSQL regressions reproduced 501 rows being consumed
instead of the 500-row budget, missing abort/drain handling, and the absence of
continuation past a full page of unprojectable or failed cards.
The worker now gives its existing service a 500-row budget, the coordinator's
abort signal, and its admission predicate. The service checks admission between
rows and reads at most 100 candidates per page. It retains the last attempted
request ID for each unfinished company, including after a failed pass, so a
poisoned early page cannot hide later healthy cards. A truncated pass schedules
a one-second continuation; the existing shared per-company fairness and failure
backoff remain in force. A completed pass that still has an unprojectable card
backs off from thirty seconds to five minutes. Fresh committed company changes
reset that delay and wake the affected scope promptly. These are real unresolved projections,
not an unconditional empty-history scan.
Reconstruction clears continuation state for companies that no longer have
mismatches. A sixth real regression reproduced an abandoned cursor skipping the
first new earlier card after an empty reconstruction; clearing that cursor makes
the first new pass repair it immediately. The maps contain no durable truth:
restart reconstructs from the existing rows, and completed rows stop matching.
There is no cursor table, independent timer or per-producer recovery wrapper.
Legacy manual/global and scheduler-disabled one-shot repair calls retain their
complete-sweep behavior unless they request a budget. The normal coordinator
worker always supplies the bounded options. Cancellation is observed between
rows and pages; an already issued database query or reflection finishes before
the service stops. A sweep never replays a provider tool action.
All 220 cases across the full gateway/projection/interruption, startup and scoped
deadline suites pass, with no skipped cases. The five initial regressions and
sixth cursor regression failed before their fixes; server types and build pass.
The earlier 219-case acceptance remains evidence for the preceding revision.
Fresh integrated-head CI and both automated reviews remain merge gates.
After normal reconciliation with master `f6bc653f7f`, all 320 integrated cases
pass, including the 81 authenticated gateway HTTP cases and the upstream narrowed
identity-read tests. Server types and build pass. Thirty-five independent master
performance files are preserved exactly; the gateway's narrowed identity read
is preserved alongside the projection extraction. The pre-integration 220-case
selection above remains evidence for its stated revision.
## Review efficiency follow-up
A company-scoped hint now enters the bounded owner directly rather than first
repeating company discovery. Startup/global reconstruction still discovers
companies from existing rows. An empty sweep with no saved cursor returns after
one candidate query; a resumed cursor or attempted reflection still needs the
final query to detect unresolved earlier cards. The reflection callback returns
no settlement result, so an attempted reflection cannot establish that a card
was repaired.
Returned unresolved work now backs off through 30, 60, 120, 240 and 300 seconds,
matching the shared error-backoff cap. Truncated backlog continuation remains
one second. Settlement removes the retry; a new committed hint resets backoff.
No pending row is discarded, and no independent timer or table is added.
The fixed-cadence regression failed before repair and now verifies the full
backoff cap, producer reactivation and quiet after settlement. An initial scan
assertion incorrectly assumed that a complete approval emitted only one wake;
its separate legitimate writes caused two scans even after optimization. The
corrected case isolates one committed hint and verifies one candidate scan with
matching history, followed by an empty direct sweep with one query. All 222
cases in the full gateway, startup and scoped-deadline suites pass without skips;
server typecheck and build pass. Fresh-head CI and both reviews are still required.