Files
PaperClipAI/doc/idle-secret-proposal-expiry.md
T
DottaandPaperclip 623f1dc289 fix(server): schedule secret-proposal expiry from committed changes (#17446)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents can propose secrets and bindings for approval.
> - Pending proposals already contain their expiry and encrypted value.
> - The server scans for expired proposals on every maintenance tick,
even when none exist.
> - Committed proposal changes and saved expiry times provide the
required triggers.
> - This PR uses those triggers and preserves scrubbing, approval
checks, and recovery.
> - Empty and completed proposal history no longer causes recurring
expiry queries.

## Linked Issues or Issue Description

Refs #17443. This is the second PR in the sleep migration stack and
targets its branch.

**What problem does this solve?**
The periodic proposal scan contacts PostgreSQL when no proposal needs
maintenance. That obstructs idle database behavior across tenant
instances.

**What is the proposed solution?**
Use transaction-aware notifications for creation and resolution.
Reconstruct the earliest pending expiry at startup. Run cleanup at that
deadline, or retry while reconciliation remains unfinished.

**Why is this useful?**
It removes an idle scan while retaining security checks and physical
ciphertext cleanup. No queue table or migration is required.

## What Changed

- Added intent notifications before secret/binding creation, approval,
terminal transitions, and linked interaction resolution. The existing
outer transaction owns settlement.
- Kept expiry selection, ciphertext scrubbing, dependent rejection, and
authorization in the secret-proposal service.
- Registered a startup and deadline worker in place of the aggregate
maintenance sweep. Empty/terminal states disarm; overdue batches and
failures retain retries.
- Reused shared scoped deadline dispatch for secret expiry. The service
owns company-filtered SQL and expiry policy. Custom-image expiry
continues to use `registerDeadlineWork`.
- Added real PostgreSQL producer, outer-transaction, concurrent-write,
expiry, restart, interaction resolution, and drain/resume tests. Updated
startup wiring fixtures and documented the supported writer/lifecycle
contract.

## Verification

- Landing reconciliation at `c10eb1bcf752b058713e05d3345e5cf38dd3fa72`:
160 tests pass across the complete secret-proposal, scoped deadline,
company batch, coordinator, startup, custom-image service and
custom-image worker suites; no cases skipped in the accepted runs.
Server typecheck passes. This integrates master after #17443 landed;
fresh remote CI and Ponytail review are required before merge.

Current head: `5bbe17ad9ebc4573b08ed1a9323644ef9608ca09`. This remains a
single-duty PR stacked on #17443.

- 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/5bbe17ad9ebc4573b08ed1a9323644ef9608ca09/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/5bbe17ad9ebc4573b08ed1a9323644ef9608ca09/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

- Notifications cover supported writers sharing the database signal
owner. Independent SQL writers and other process roots require a
separate ingress contract; startup recovery alone is not live replica
support.
- The shared coordinator and scoped worker also serve later stack
duties. Their defaults remain intact; existing voice, chat, native, and
startup consumers are included in integrated verification.
- Startup now checks overdue proposals immediately. During drain,
physical cleanup waits; approval still rejects expired proposals
synchronously.
- Each due company pass remains bounded to 100 rows. Failed scopes back
off from 30 seconds to five minutes; healthy overdue work keeps
draining. Self-generated write hints coalesce and do not erase a failed
sweep's backoff.
- Heartbeat-disabled behavior and Cloud sleep-safety gates remain
unchanged. This does not establish deployed suspension or external wake
coverage. Tool review and gateway token cleanup remain separate slices.

## 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 14:36:28 -05:00

5.3 KiB

Secret-proposal expiry and idle behavior

Secret proposals already store status, expiresAt, and encrypted pending values. This worker uses those records; it adds no queue table or migration.

Ownership and activation

secret-proposals.ts still owns quotas, authority checks, approval, expiry, ciphertext scrubbing, dependent binding rejection, audit records, and interaction reflection. expiryDeadlines(companyId?) returns one saved deadline per pending company. Normal write notifications reload only the affected company. Startup reconstruction loads all companies once. The manual nextExpiryAt() query remains available to existing callers. Terminal history does not create a deadline. Approval still rejects an expired proposal synchronously, even when maintenance is paused.

The supported producers register secretProposalExpiry intent before their writes in the owning transaction:

  • createWithinQuota: both secret and binding creation, after quota admission.
  • markApproved: ordinary approval and prerequisite approval during a cascade.
  • transition: rejection, withdrawal, expiry, and dependent binding rejection.
  • Interaction resolution: rejection, withdrawal/cancellation, expiry, and failed acceptance execution that resolves a linked pending binding.
  • Company deletion: a company hint removes its cached deadline after deletion commits.

Each producer includes its company ID. The remaining proposal write only attaches an interaction ID and does not change expiry eligibility. Independent SQL writers or another DB root are not supported live producers of these process-local signals; startup reconstructs their persisted rows, but it is not a live replica contract.

The shared database signal boundary defers notification until the outer transaction settles. Nested savepoints, rollback, and uncertain commit outcomes retain the existing protocol. Callers do not pass recovery counters or reconnect the database.

Scheduling and lifecycle

registerSecretProposalExpiryWork runs once at startup while heartbeat scheduling is enabled. It replaces the secret-proposal sweep in the aggregate maintenance tick. Disabled heartbeat scheduling keeps its existing behavior.

The worker reloads the affected company's saved minimum expiry after a committed change. It does not sweep future work. At a due deadline it sweeps at most 100 expired proposals for that company, then reads that company's next deadline. Companies rotate fairly between bounded passes. Retained history creates no work. No pending proposal, unresolved write, or failed reconciliation means no recurring query. Unscoped legacy hints still reconstruct all companies.

scoped-deadline-work.ts owns scope invalidation, cached deadlines, fair dispatch, and failure backoff. It owns no SQL or proposal policy. Failed loads and failed company sweeps back off independently from 30 seconds to five minutes. A new write reloads that company without erasing its failed sweep's backoff. Other companies keep their own deadlines, including already-due work. The coordinator's optional nextRunAtOnError contract permits immediate eligible work only after the worker has recorded per-item backoffs. Failed transaction reconciliation and failed pending-state inspection retain the coordinator's normal retry.

Secret expiry opts out of the shared helper's default one-second pass spacing so newly learned short deadlines are not clamped. Other scoped consumers retain that default. Custom-image expiry continues to use deadline-work.ts unchanged. No shared scheduler or startup wiring interprets secret-proposal policy.

A request stays pending until reconciliation succeeds. Changes arriving during a pass, first writes during drain, and failures followed by drain retain a request. Passive drain performs no proposal SQL. Resume reconciles retained requests; restart reconstructs deadlines. Coordinator shutdown unsubscribes and waits for active work. Deferred physical cleanup does not extend approval authorization.

Verification and limits

The PostgreSQL route suite exercises actual service producers, nested outer commit and rollback, quiet empty and terminal states, quiet future deadlines, automatic ciphertext scrubbing and dependent-card resolution, startup reconstruction, withdrawal, interaction rejection/failed execution, failure retry, and drain/resume. It also verifies that expired approval fails while cleanup is paused. The existing coordinator suite verifies fallback retry and uncertainty boundaries. New cases record actual SQL to prove normal writes and due expiry stay company-scoped; they also cover five-second deadlines, earlier replacement deadlines, company deletion, and another company making progress while one sweep fails. A deterministic shared-worker test covers a 250ms replacement deadline.

The worker tests use a controlled clock and a real PostgreSQL database. Existing route tests cover quotas, isolation, authorization, concurrency, and lifecycle semantics. The test-only database URL must refer to a disposable migrated database: the suite deletes its fixture tables. Ordinary CI uses its isolated embedded DB.

This is worker quiet, not proof of host or database suspension. No Cloud alarm or sleep-eligibility exemption is added. Existing sleep-safety checks remain in force; a local expiry timer cannot wake a stopped host. Tool review expiry and gateway session cleanup are separate migrations.