Files
PaperClipAI/doc/idle-approval-retention.md
T
DottaandPaperclip 553a004926 fix(server): schedule approval retention from durable deadlines (#17497)
## Thinking Path

> - Paperclip manages agent work and should become idle when it owes no
background work.
> - The attention feed archives old pending approvals under the existing
90-day policy.
> - The server rebuilds the feed on each maintenance tick, even when
approval state has not changed.
> - Some approvals have never been read, so retention rows alone cannot
define the work.
> - Approval records and committed eligibility changes define the real
deadline and wake triggers.
> - This PR gives approval retention its own deadline worker and reuses
the existing retention and archive-delivery contracts.
> - The approval lane stops idle scans while other attention sources
remain separate migration work.

## Linked Issues or Issue Description

Stacked on #17490. This PR owns approval retention only. Archive
notification delivery is already separated in #17475. Other
attention-source retention and remaining recurring server duties remain
in the stack inventory. The related-PR search found no overlapping
approval-retention worker migration.

**What existing behavior does this improve?**
Automatic archival of old pending approvals from attention.

**Current behavior**
The shared maintenance tick builds the attention feed for each active
company. That query discovers pending approvals and materializes
retention records before checking their age. An unchanged or empty
approval source is read repeatedly.

**Proposed behavior**
Reconstruct eligible approvals at startup. Schedule the earliest
approval activity timestamp plus 90 days. Wake after committed changes
to approvals, links, issue visibility, company activity, or retention
controls. Stop scheduling when no eligible approval remains. Archive
records through the existing version checks, audit writes, and
notification outbox.

**Reason and benefit**
Idle approval retention should generate no recurring SQL. Unseen
approvals still need eventual archival. A stale candidate must not
archive an approval that was resubmitted while the worker waited for its
source lock.

**Breaking changes**
None intended. The archive age, eligibility, keep controls, audit, and
notification policy remain. Normal attention reads still include
approvals. No Cloud sleep blocker is relaxed.

## What Changed

- Restrict tool-review signals to their existing terminal transition
owners (approve/reject/cancel). Creation/link/revision/resubmit signal
retention only.
- Coalesce retention notifications under one thirty-second aggregate
read budget. Known deadlines still preempt it; no notification or
durable deadline means no scan.
- Add a bounded approval-retention worker. It uses the shared deadline
coordinator and reads pending approvals directly.
- Share first-linked-issue ordering and execution visibility through
`approval-visibility.ts`, used by both the feed and worker. Preserve
actor-specific read filtering and locked archival rechecks.
- Materialize unseen approvals on demand inside manual
keep/archive/revive transactions. Use the saved activity timestamp and
existing versioned state; retain company scoping and rollback on
failure.
- Add awaited pre-write signals to approval, budget, tool, link,
visibility, company, and retention mutation owners. Preserve outer
transaction ownership; keep provider calls outside SQL wrappers.
- Narrow retention inputs to the source fields the existing service
uses. Signal materialization only for inserted or changed state so
unchanged reads converge.
- Exclude approval collection from the maintenance feed before its
query. Other source kinds remain in that sweep.
- Add the pending-only approval deadline index with concurrent,
idempotent creation. Keep the five newest migration snapshots.
- Add real-Postgres lifecycle tests and startup wiring tests. Enable
validated external test databases for existing attention and budget
suites.
- Document producer coverage, quiet conditions, recovery and remaining
scope in `doc/idle-approval-retention.md` and the stack ledger.

## Verification

Current test revision: `dd844f4e8cbbf1db681a774dc31cd552301bef48`,
followed by docs-only `3efda33812`, which received both 5/5 reviews and
passing CI. This candidate normally integrates landed Grok `2adcde162e`.
All runtime and acceptance-test sources are byte-identical to that
tested/reviewed tree; only four independent Runner E2E accounting files
and the ledger differ. Migration 0392 and the parent journal prefix are
unchanged. Local checks are reused for their unchanged source; fresh
final-head CI and reviews are required against master. This remains the
approval-retention duty.

- **315 cases pass** across eight complete suites: approval
retention/service, attention, decision retention, budgets, tool gateway,
server startup and scoped deadlines. All 30 real-PostgreSQL lifecycle
cases pass. Server production typecheck and build pass.
- **Four regressions failed before repair**: unseen young
keep/archive/revive returned 404; the manual keep rollback case could
not reach its update. They now pass. A failed keep update leaves no
retention row or audit; retry succeeds with one audit.
- Both linked-issue orders are checked against the feed: hidden
first/visible later is excluded; visible first/hidden later remains
eligible. The feed and worker use the same owner query, with actor read
filtering preserved.
- Generated concurrent/idempotent migration
`0392_small_frightful_four.sql` adds only the pending approval index.
Every historical parent SQL migration and all 390 parent journal entries
are unchanged. Five generated snapshots remain (0388–0392). Schema
drift, migration safety/numbering and actual fresh apply/replay on five
isolated PostgreSQL databases pass.
- Existing precision, transaction, drain/resume, failure fairness and
shutdown tests remain. Earlier transaction-stub, budget-import,
diagnostic-client and CI failures are recorded in the owning audit and
stack ledger; later passes do not erase them.

- Three additional real regressions failed before the wake repair:
ordinary mutations emitted tool-review signals in both resolution cases,
and sequential tenant writes caused repeated global reads. Tests now
prove terminal approve/reject/cancel signals and one aggregate
reconciliation per thirty-second window with a different company's
pending approval retained. The aggregate budget is stricter than a
separate per-company budget and matches the old default cadence; known
deadlines preempt it.
- The initial coalescing fixture expected the concrete pending-work hold
to clear before reconciliation; it now preserves that hold and measures
SQL. The first broad repair passed 313 cases but failed the existing
parked-review cancel case. The original cancel producer is restored; all
315 now pass without weakening assertions. At old head `5991f257bb`,
security was action-required and Greptile returned service error
`8fb1835c-e564-476b-b802-d5be164ea915`. Fresh repaired-head results are
required, not assumed.

Commands: server Vitest for `approval-retention-work`,
`approvals-service`, `attention-service`, `decision-retention-service`,
`budgets-service`, `tool-gateway-service`,
`server-startup-feedback-export`, `scoped-deadline-work`; server
typecheck/build; DB snapshot drift, migration checks and fresh
apply/replay. Fresh exact-head Greptile/Ponytail and CI remain merge
gates. No full local repository suite or deployed host suspension/cost
claim is made.

Earlier stack verification below is historical evidence at the
explicitly named revisions, not this head's acceptance:

Approved delegated Test-tab calls now evaluate caller and target
policies in the existing claim transaction. Real HTTP regressions first
showed a missing caller charge and shared-limit self-rejection. All 78
connection-management cases and 92 gateway cases now pass on the owning
branch, along with server production typechecking. All 78
connection-management cases also pass at combined tip
`d25f830db89c1623bff3547bfc68015c1c66e0cf`. The three new cases cover
shared charges, separate charges, rollback on denial, duplicate
approval, and rejection of a later authorized call. Intermediate
counter/identity fixture failures and a reused-database Slack collision
are recorded in [the owning
audit](https://github.com/paperclipai/paperclip/blob/20557d2c3dd0949c734eaa8095fd78b9b07d7fd5/doc/idle-approved-tool-recovery.md#delegated-test-tab-admission-after-policy-changes).
The earlier combined checks below remain evidence for their stated
revisions. Current-head CI and review remain required.

Earlier published head: `20557d2c3dd0949c734eaa8095fd78b9b07d7fd5`. This
remains a single-duty PR stacked on #17490.

- Integrated pinned master `8548173fc9efdc0fa07644fb92dcc0b6851a95e5`
with normal merges. Every branch passed generated-schema checks and its
target-branch migration-order gate. Existing SQL is unchanged; the 25
stack migrations follow master’s new migration.
- Duty-specific producer coverage, lifecycle tests, and earlier failure
records:
[2026-10-24-recurring-server-sleep-stack.md](https://github.com/paperclipai/paperclip/blob/76a49810c7ff403ae7720bef0d453747bd844731/doc/plans/2026-10-24-recurring-server-sleep-stack.md).
- Combined tip `51e6cbe67979b55b340ea9f0f434c5b5c682ccd6`: **355
startup/cancellation/recovery/dispatch/preservation tests**, **75
connection-management tests**, and production server typechecking pass.
These combined checks do not mean every intermediate PR independently
ran the full repository suite.
- Real master-to-stack database upgrade passes: **379 unchanged prior
migration receipts plus 25 new migrations**, with the gateway
responsibility fixture preserved.
- Owning-branch checks: 84 runner installation/settlement cases plus
runner typechecking and generated TypeScript/Rust profile agreement; 253
GitHub review workflow cases. Stop-attribution and held-worker readiness
regressions failed before their fixes.
- Failure accounting: the initial combined run passed 349 cases and
failed six idle assertions in a reused preservation database containing
old cancelled runs. Repeating the combined command with a fresh
preservation database passed. Earlier invalid project selection, merge
import/type errors, and the interrupted full local suite are not counted
as passing checks.
- **Latest-head CI and review remain required.** Prior CI findings
include GitHub idle observation, route teardown, attention cleanup,
touch-picker E2E, workspace health, and voice-cleanup timing; local
passes do not erase those failures. No full local repository-suite,
deployed sleep, fleet cost, or live Copilot qualification claim is made.

## Risks

- Signals cover the application database owner and its child pools.
Independent SQL writers and other active processes need an explicit wake
integration. Startup reconstructs committed durable state.
- The source row lock prevents stale approval-activity archival. It does
not add cross-table serializability for concurrent link, visibility, or
company-policy changes.
- Newly changed eligibility can wait up to the thirty-second
notification window, matching the former default maintenance cadence.
Saved deadlines still preempt; no new idle polling is added.
- Failed records retain recovery debt. Bounded cursor progress lets
later rows proceed without dropping failures.
- The shared retention transaction now groups materialization and
activity-version updates. Existing attention and retention consumers are
tested against PostgreSQL.
- Kept, archived, resolved or inactive-company approvals can become
quiet, but their supported mutations must retain their wake
notifications.
- The local scheduler cannot wake stopped compute. This PR neither adds
approval deadlines to Cloud's external alarm contract nor claims
measured 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 19:23:24 -05:00

9.8 KiB
Raw Blame History

Approval retention without idle polling

This slice removes approval retention from the recurring attention-feed sweep. It does not migrate the other attention source kinds or establish host sleep.

Obligation and owner

A pending approval in an active company is eligible for automatic archiving 90 days after its last approval activity, unless kept or already archived. This is a presentation/attention archive: it does not approve, reject, cancel, or delete the approval. The deadline and policy are unchanged.

Approval rows are the source of truth, including those never viewed in the attention UI. Scheduling only existing decision_retention rows would miss unseen approvals because the old feed reader materializes those rows lazily. approval-retention.ts discovers eligible approvals directly and registers the earliest actual deadline with the existing delivery coordinator.

The query retains the attention feed's first-linked-issue rule and consumes executionIssueCondition from the issue visibility owner. Hidden, harness and agent-conversation tasks remain excluded. A waiting external conversation with a linked pending approval is active under that same shared visibility policy; its chat state alone cannot hide this approval. Tests exercise the Slack case.

Each pass visits at most 100 candidates in activity/id order. The cursor keeps the database timestamp as text so sub-millisecond precision cannot repeat a failed page. A failed row does not prevent later pages from progressing. The failed durable record remains eligible for recovery. Each candidate is re-read and locked before archiving, so a resubmission committed while the worker waited cannot archive the new activity using an old selection. Existing retention version, keep and archive compare-and-set checks remain authoritative. This does not add cross-table serializability for concurrent link/visibility/company policy changes.

Producer coverage

Supported producer Notification boundary
Approval create, cancel, revision, resubmit SQL write transaction in approval records
Approval resolution, including hire lifecycle SQL write, retaining the caller's outer transaction
Hard budget approval and budget resolution Existing accounting transaction
Formal tool approval and issue link SQL write transaction; provider work is outside it
Link, unlink and bulk link Issue approval write transaction
Issue visibility changes and deletion Existing issue mutation transaction
Routine creation rollback cleanup Existing transaction before cascading issue deletion
Company activation, pause, archive and deletion Existing company/accounting transaction
Keep, manual archive, revive and accepted archive manifest Existing retention mutation transaction
Retention materialization or source activity change Signal only when inserting or changing actual state

Approval comments do not change approval activity under the existing policy. Company portability imports do not write approvals; restored durable records are reconstructed at startup. Signals belong to the application DB owner and its dedicated child pools. Independent SQL writers or another active process need explicit integration; this change does not claim to observe them.

Shared contracts and lifecycle

The main maintenance feed excludes the approval source before collecting it. Public attention reads keep their existing default behavior. Other attention sources remain in the aggregate sweep until separately migrated.

The retention service accepts the source fields it actually uses rather than requiring fabricated full UI attention items. Its existing versioned state, audit writes, authorization and archive-notification outbox are reused. No new queue table is introduced. Transaction-aware notifications retain intent until the outer transaction settles, including nested writes and rollback.

The deadline worker uses the shared scheduler's generation and drain behavior. Startup reconstructs pending work; idle drain retains requests without passive SQL; resume reconciles them. Failed work retains a 30-second recovery spacing. No eligible approval means no recurring deadline or query. Completed history, kept/archived approvals, and inactive companies are quiet until a supported mutation changes eligibility. Shutdown joins admitted work and stops between candidates. Notification delivery remains owned by the separate archive worker.

A pending-only (updated_at, id) index bounds deadline lookup independently of resolved approval history. The migration uses concurrent, idempotent creation and the existing migration runner's nontransactional execution list.

Verification and limits

Real-Postgres lifecycle tests cover unseen sources, future deadlines, retained history, keep/unkeep, archive/revive, revision/resubmission, company activity, linked issue visibility/deletion/order, normal versus maintenance attention reads, nested commits/rollback, startup and first-write drain, failed database writes through drain, bounded progress past failure, shutdown, and a concurrent resubmission. The waiting-Slack case also checks durable notification creation.

Existing approval, attention, retention, budget, tool-gateway and startup suites are relevant consumers. Track final results and any failures in the PR and the stack ledger.

The Cloud sleep guards are unchanged. An in-process deadline cannot wake stopped compute, and this approval deadline has not been added to Cloud's durable wake contract. This proves only the named worker's quiet/reactivation behavior.

Initial review repair verification

The feed and retention worker now share approval-visibility.ts for the first linked issue and its execution visibility. The feed still applies actor-specific read access before including a linked approval. Both link orders are covered: hidden first/visible later stays excluded; visible first/hidden later stays eligible. The worker's current-state recheck remains under the approval row lock.

Manual keep, archive and revive materialize an unseen approval on demand inside the existing retention write transaction. They lock the company-scoped approval row in the same order as automatic archival, read its real activity timestamp, and use the existing versioned syncItems operation. Missing or unauthorized sources retain their existing rejection; other source kinds are unchanged. This adds no eager startup materialization or recurring scan.

Four real regressions failed before repair: unseen young keep/archive/revive returned 404, and the atomic materialization test could not reach the keep write. All pass after repair. A failing keep update rolls back the inserted retention row and emits no audit; the next successful keep persists exactly one audit. All 26 lifecycle cases pass, including the existing timestamp-precision failure rotation. The complete eight-suite consumer selection passes 311 cases at 063b61584d: approval worker/service, attention, retention, budget, tool gateway, startup and scoped deadlines. Server typecheck and production build pass.

Normal parent reconciliation preserves every historical parent SQL file and all 390 parent journal entries. The approval index is regenerated through the normal workflow as concurrent/idempotent 0392_small_frightful_four.sql; five generated snapshots remain (0388–0392). Schema drift, safety/numbering and actual fresh migration apply/replay on five isolated PostgreSQL databases pass. Earlier transaction-stub, budget-import, query-probe and CI failures remain recorded in the stack ledger. Fresh current-head CI and both reviews are required.

Wake amplification repair

Ordinary creation, links, revision requests and resubmission signal retention only. Tool-review queues are notified by the actual terminal transition owners: approve/reject in approval resolution, and cancel in the approval service. These are the same terminal transitions that notified tool reviews before this PR. Cancellation must remain a producer because it settles parked tool actions as unavailable. No extra wrapper or scope argument is threaded through those calls.

The aggregate retention worker now coalesces sequential notifications for thirty seconds after a successful reconciliation. This is a single instance-wide read budget, stricter than granting each company an independent thirty-second budget, and matches the former default maintenance cadence. Known business deadlines still preempt the coalescing window. The window schedules no work on its own; without a notification or saved deadline the worker remains quiet. Newly changed eligibility can wait up to that window, as under the old default maintenance.

Three new real regressions failed before repair: approve and reject cases observed unwanted tool-review signals from ordinary mutations, and four sequential tenant writes caused eight global reads rather than one two-read reconciliation. Terminal approve/reject/cancel signals are now tested explicitly. With another company's pending approval present, sequential writes cause no extra SQL within the window and one reconciliation when the window ends.

The first coalescing fixture incorrectly expected the pending-work hold to clear before the requested reconciliation; it now lets the deferred wake remain owed and measures SQL directly. The first broad repair passed 313 cases and failed the existing parked-review cancellation case. Restoring the original cancel producer fixed that regression without weakening its outcome assertion. All 315 cases in eight full suites now pass at dd844f4e8c, including 30 approval worker cases; server types/build pass. The earlier security check at 5991f257bb was action-required and Greptile reported service error reference 8fb1835c-e564-476b-b802-d5be164ea915. Neither is claimed as passing; the repaired head requires fresh security, CI and both bot reviews.