Files
PaperClipAI/doc/idle-join-request-retention.md
T

6.2 KiB

Join request retention while idle

Pending join requests already have durable activity timestamps. Retention now uses their next 90-day archive deadline instead of including them in the main maintenance tick's attention-feed scan. The regular attention UI still includes join requests. This changes automatic retention discovery, not access approval.

Owner and eligibility

join-request-retention.ts owns the candidate query, bounded scan, and deadline. A candidate is a pending_approval request belonging to an active company, with an invite whose company matches the request. Kept and already archived items do not schedule work. Invite expiration or revocation does not remove the existing attention item and therefore does not disable its retention. No provider policy is interpreted by the worker or shared scheduler.

The next deadline is the earliest eligible updatedAt plus 90 days. The partial pending index orders updated_at and id. Resolved request history is excluded by the index predicate. Each pass attempts at most 100 due rows. The private cursor preserves database timestamp precision so a failed page cannot starve later work. Errors keep the shared 30-second retry; successful full pages continue until work is exhausted. No eligible rows means no scheduled SQL or network scan.

Each candidate is re-read under its request row lock before retention changes. This preserves a concurrent request update or status transition. The shared retention owner still controls keep, archive/revive, version checks, activity logging, and notification creation. Archiving does not approve or reject a join. Join requests have no origin agent, so their archival creates no agent delivery. Company and invite joins preserve existing visibility policy; this does not add cross-table serializability beyond the existing retention contract.

Producer audit

Producer Notification boundary
Invite acceptance creates a pending request Existing acceptance transaction, before the request insert
Supported gateway invite replay changes activity Small writeJoinRequestChange transaction around the existing SQL update
Automatic human approval after acceptance Same helper around the status update
Explicit approval and rejection Same helper around the status update
Slack self-service access request Existing platform transaction, before the request insert
Company pause, resume, archive, or deletion Company-level retention notification inside the existing transaction
Budget-driven company pause or resume Same company notification in the accounting transaction
Keep/unkeep, manual archive/revive, accepted archive proposal Existing typed retention notification now routes join_request to its worker

Provider calls, membership grants, and unrelated route behavior stay outside the small SQL mutation helper. Nested callers retain the existing outer transaction settlement and uncertain-outcome protocol. The access router itself requires a root database connection; it is not wrapped in a caller transaction.

API-key consumption only permits an already-approved request. It cannot create pending retention work and does not acquire a new wrapper or signal. Invite acceptance that reuses a human request does not update that request. Revocation, expiry, and acceptedAt changes do not change the feed predicate. Invite/company identity is immutable in supported routes; company deletion is the invite delete owner. A future producer that changes these facts must join the same contract. Process-local signals do not cover independent SQL writers or another instance.

Lifecycle and limits

Startup reconstructs pending deadlines. The worker is registered only when the existing heartbeat maintenance switch is enabled. Writes during drain retain a wake without passive SQL. Failed work retains its retry across drain. Shutdown joins an admitted archive and stops before subsequent candidates; restart picks up the durable remainder. There is no new queue table or permanent DB listener. The in-process deadline is not a Cloud alarm. Existing unsupported-future-work sleep blockers remain in place.

Verification

Seventeen new real-PostgreSQL cases pass. They cover actual invite acceptance, rejection, human approval, gateway replay, and Slack access requests; empty and resolved-history quiet; saved deadlines; startup reconstruction; keep and manual archival controls; company pause/resume; unchanged expired/revoked invite policy; normal versus maintenance feeds; nested commit/rollback; first writes and failed work during drain; concurrent source updates; 101-row microsecond paging with a full failed page; and shutdown/restart.

Another 139 consumer cases pass: 71 invitation/startup cases, 21 approval worker cases, and 47 shared retention/attention/budget cases. This is 156 selected tests, not a full local suite claim. The first consumer command used a nonexistent retention test filename and ran only the approval suite; the corrected selection ran the actual retention suite against its dedicated external database.

Initial test failures exposed fixture assumptions: the access router cannot run on a transaction handle; unresolved recovery cannot be expected to become idle; Slack links require actual endpoint/principal ownership rows; and shutdown tests must establish admission and deterministic candidate order. Those fixtures were corrected without weakening assertions or production checks. The first replay fixture lacked required gateway credentials. The final fixture supplies test-only credentials. A reused transaction spy retained admission calls during the quiet assertion; restoring it before the quiet measurement fixed the measurement.

Migration 0387 applies, the full migration set replays, and the concurrent index statement replays idempotently. Migration safety and numbering checks pass; the five newest schema snapshots are retained. The actual next-deadline query with 40,000 resolved requests and one pending request uses the pending index and executes in 0.017 ms locally. The probe rolls back its data. This is query-plan evidence, not production latency or measured savings. Workspace typechecking and the production build pass. Fresh PR CI/review remain required before readiness.