Files
PaperClipAI/doc/idle-gateway-credential-expiry.md
T
DottaandPaperclip 5398275d2a fix(server): schedule gateway credential expiry from committed changes (#17450)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The tool gateway stores credentials for sessions and named gateways.
> - Those credentials already contain expiry dates.
> - The server repeatedly scans their tables even when no expiry work
exists.
> - Creation signals and saved deadlines can drive cleanup without idle
scans.
> - This PR registers that work with the existing coordinator.
> - Authentication continues to reject expired credentials independently
of physical cleanup.

## Linked Issues or Issue Description

Refs #17446, #17221, #17274. This is the third sleep-migration slice,
stacked on #17446.

**What problem does this solve?**
Gateway credential cleanup queries PostgreSQL on every maintenance tick,
including empty tables and retained non-expiring tokens.

**What is the proposed solution?**
Signal expiring token/session creation from its transaction. Restore the
earliest saved expiry at startup. Retry only while cleanup or
reconciliation remains outstanding.

**Why is this useful?**
Configured idle gateways no longer create recurring expiry traffic.
Existing credential data remains the durable source of truth.

Related work: #17221 and #17274 change run-token lifetime and cleanup
eligibility. They remain separate. This PR preserves current master
authorization and deletion rules. When those PRs are combined, update
the deadline query and signals to match their new operator-token expiry
predicate; run-token idle markers must not keep this worker scanning.
Retain the separate run-liveness reaper and run both regression suites.
Text mergeability is insufficient evidence for that integration.

## What Changed

- Make the credential test clock follow real scheduled work without
overshooting future-expiry assertions; production behavior is unchanged
by this fixture follow-up.

- Coalesce sequential credential notifications within the former cleanup
cadence, preserve loaded expiry deadlines and immediate authentication
rejection, and bound scheduled deletion to 500 rows per pass.

- Added transaction-aware intent before the two existing credential
inserts. Non-expiring named tokens do not signal expiry work.
- Added earliest-expiry selection in the gateway service and a narrow
registration wrapper for the shared deadline helper.
- Removed gateway credential cleanup from the aggregate maintenance
tick. Startup reconstructs saved deadlines. Heartbeat-disabled instances
retain their startup-only cleanup behavior.
- Preserved expiry/revocation checks, run authorization, audit behavior,
and the existing 500-token cleanup batch limit.
- Added real PostgreSQL tests and a lifecycle/producer document.
Tool-action review recovery remains a separate slice.

- Drain all expired session/token pages in explicit and
scheduler-disabled startup cleanup, while retaining one-page scheduled
passes and authentication enforcement.

## Verification

- At `907098cd3656bac59c60220c73b085d0349d4b5f`, all 290 Runner
transport tests pass locally (including all seven prepared-environment
variants). CI run `40570815360` attempt 1 had one provider-drain
settlement failure in the `close-attachment` case; the identical source
passed the preceding CI lane. A single failed-job rerun is in progress.
The failure remains recorded; no shutdown assertion or production
behavior was changed to suppress it.

- Master landing reconciliation at
`907098cd3656bac59c60220c73b085d0349d4b5f` after #17446 merged: all
production and test files are byte-identical to
`24318d9b99ee7f044a864ff3981fc1929bf3e509`, where the 150-case
acceptance and server types passed. Only commit ancestry and the plan's
merged status changed. Fresh CI/review is pending on this head.

- Parent reconciliation at `24318d9b99ee7f044a864ff3981fc1929bf3e509`:
150 affected tests pass across gateway credential expiry, the complete
tool gateway suite, custom-image worker and startup. The accepted
PostgreSQL runs skip no cases. Server types pass. Existing direct tests
cover sequential-write coalescing and saved-deadline precedence; the
older Ponytail finding needs a fresh review. Remote CI is pending.

Current head: `10c85a5eaa288867aa2997a27aee5f675c2a713d`. This remains a
single-duty PR stacked on #17446.

- 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-08-instance-sleep-and-scheduler-inventory.md](https://github.com/paperclipai/paperclip/blob/10c85a5eaa288867aa2997a27aee5f675c2a713d/doc/plans/2026-10-08-instance-sleep-and-scheduler-inventory.md),
[2026-10-24-recurring-server-sleep-stack.md](https://github.com/paperclipai/paperclip/blob/10c85a5eaa288867aa2997a27aee5f675c2a713d/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

- Shared-owner signals cover supported local producers. Independent SQL
writers or another replica need a separate ingress contract; startup
recovery is not live discovery.
- This is scheduling only. The open run-token lifetime PRs need semantic
integration as described above before combining their behavior.
- Expiry cleanup can wait during drain; authentication still enforces
expiry on use. Existing Cloud safety gates are preserved. Local
deadlines do not prove external wake coverage or deployed suspension.
- Revocation does not move expiry. Non-expiring tokens remain retained
by design; this PR does not invent a retention deadline.

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

Latest-head CI is now green after one evidence-backed retry. Greptile is
5/5 and all review threads are resolved.

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-27 14:58:11 -05:00

8.3 KiB

Gateway credential expiry and idle behavior

Gateway sessions and named gateway tokens already persist their expiry times. cleanupExpiredSessions remains the owner of physical deletion. Authentication continues to check expiry, revocation, company scope, run ownership, and active run state on use. No table, authorization rule, or external alarm is added.

Producer inventory

  • createSession registers intent before inserting a run-bound session in a transaction. Its existing audit and returned credential remain unchanged.
  • createNamedGatewayToken registers intent before inserting an expiring token in a transaction. A token without an expiry owes no expiry cleanup and does not notify this worker.
  • Last-use updates do not change expiry. Token/session revocation, including heartbeat completion and connection removal, also leaves the saved expiry intact. Revoked rows retain the same physical deletion deadline.
  • Gateway/company deletion removes dependent rows and can leave one harmless stale deadline. It cannot create overdue work. No supported service changes a saved credential expiry after creation.

The database signal layer owns outer commit, rollback, savepoints, and uncertain outcomes. The worker supports producers sharing that signal owner. Independent SQL writers and another process need a separate live ingress contract. Startup reconstructs existing rows but does not provide continuous replica discovery.

Worker contract

nextCredentialExpiryAt reads one earliest non-null expiry from each table using expiry-first indexes, then compares those at most two values. The migration adds the session expiry index; the token expiry index already exists. An empty credential store or retained non-expiring tokens returns no deadline. registerGatewayCredentialExpiryWork supplies cleanup and deadline selection to the shared registerDeadlineWork helper. Provider and authentication behavior stay in the gateway service; startup only registers the worker.

The worker starts where the old startup cleanup ran. With heartbeat scheduling enabled, committed changes and actual expiry times replace the recurring cleanup call. With heartbeat scheduling disabled, the existing startup-only cleanup is preserved. Tool action-review recovery is a separate obligation and is unchanged.

Token and session deletion are each bounded to 500 rows per pass. Session selection orders by expiry and rechecks expiry when deleting the selected IDs. A remaining overdue expiry schedules another pass after 30 seconds. Failed reconciliation also retries at that bound. No unfinished reconciliation or future expiry means no recurring SQL. A generation retains writes arriving during reconciliation. Startup restores deadlines; drain retains first writes without passive SQL; resume completes overdue cleanup. The coordinator owns shutdown and waits for in-flight work.

Sequential committed writes are coalesced for up to 30 seconds after a successful cleanup pass, matching the former heartbeat cleanup cadence. A previously loaded expiry can preempt that window. A newly reported credential may physically remain until that bounded follow-up; authentication still rejects it immediately at expiry. A successful pass with no future expiry and no new notifications creates no timer. This uses the shared optional notification coalescing capability, promoted from the later plugin/decision recovery slices; workers that omit it retain their existing behavior.

Evidence and limits

The concurrent session index runs through the migration executor's existing per-file top-level path. Initial application and repeat application were tested. A PostgreSQL plan with 20,000 sessions and 20,000 tokens, including 19,000 non-expiring tokens, used both expiry indexes and read one row from each. This establishes bounded lookup on that fixture, not production latency.

The new PostgreSQL suite exercises real session/token creation, configured idle state, non-expiring history, expiry ordering, revoked-token cleanup, outer commit and rollback, startup reconstruction, a backlog larger than one cleanup batch, and drain/resume. It tests expired authentication before deferred physical cleanup, through both session and named-gateway entry points. Existing gateway acceptance and server-startup tests cover the changed integration boundaries.

Use only disposable migrated databases for the test URL overrides. Their names must end in _test; the suites delete their fixtures. Ordinary CI uses embedded PostgreSQL. Test credentials are synthetic and no external provider is contacted by the expiry suite.

The migration proves worker behavior, not host suspension. Existing Cloud idle proof remains conservative. An in-process deadline cannot wake stopped compute; external wake coverage must be verified before changing sleep eligibility.

At implementation time, PRs #17221 and #17274 change run-scoped token lifetime and cleanup eligibility. This slice preserves the current master policy and changes scheduling only. When combining those changes, the deadline query and producer signals must use the same token eligibility as cleanup: run-token idle markers must not schedule the operator-token expiry worker. Keep the run-liveness reaper with its own owner and retain both regression suites. A conflict-free text merge alone does not prove this integration.

Notification amplification review repair (2026-10-27)

Ten sequential real session/token creation transactions reproduced ten extra instance-wide cleanup passes. With coalescing they cause one follow-up pass. A separate 501-session fixture reproduced the unbounded delete, then passed with 500 removed initially and the remaining overdue row removed at the saved 30-second retry. A still-valid session survives both passes. The tests also preserve an already loaded expiry only 500 milliseconds away and verify that a new one-second session is rejected while physical cleanup waits for coalescing.

The initial full run exposed four outer-transaction tests whose fake-clock wait expected the intent hold to release before the new coalescing follow-up. Their settlement helper now advances the clock through that window while preserving the original commit/rollback assertions. The complete gateway expiry, gateway acceptance, custom-image deadline, and startup suites pass: 147 tests in four files. Server typechecking also passes on the owning branch. This is local behavioral evidence; combined-stack and fresh remote checks remain required. The full local test:run remains unverified because of embedded PostgreSQL startup/capacity limits; the two real gateway suites used separate migrated, task-owned disposable PostgreSQL databases.

A later combined run exposed a test-clock overshoot: repeated blind 30-second advances could expire the future session before its intermediate assertion. The settlement helper now lets real SQL settle and advances only to an actually armed follow-up within the coalescing window. All 12 credential expiry tests pass on the owning gateway branch with the existing exact-deadline and transaction assertions intact. This follow-up changes the test fixture only.

One-shot cleanup preserves complete backlog removal

Review found that bounding worker passes also truncated startup cleanup when the heartbeat scheduler was disabled. That path calls cleanup only once, so it could leave every expired token or session after the first 500 rows indefinitely.

cleanupExpiredSessions now drains the existing backlog in 500-row SQL batches by default, using one fixed expiry cutoff for the invocation. The deadline worker opts into singleBatch: true and retains its existing coalescing and next-page deadline. Future and nonexpiring credentials survive both paths. The return value still counts deleted sessions, as before. No periodic fallback was added.

Three real-PostgreSQL regressions reproduced the truncation with 1,001 expired sessions, tokens, or both, without registering a worker. They pass after the fix. All 55 expiry/startup cases pass, including the existing worker's 501-row bounded pass and commit/rollback tests. The original explicit-cleanup compatibility case and server typecheck also pass. Evidence is in /tmp/sleep-master-gateway-manual- logs: red, green, compatibility, and types. This fixes one-shot behavior; it does not establish provider suspension or complete the stack's CI acceptance.