mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-02 16:58:52 +01:00
## Thinking Path > - Paperclip manages AI agent work and its execution environments. > - Interactive image setup sessions must expire even when users leave. > - The server currently scans these sessions on every maintenance tick. > - Empty scans keep the instance and database active. > - Existing session rows already record status and expiry. > - This pull request uses transaction signals and the shared scheduler for those rows. > - Empty and completed setup history no longer create recurring cleanup queries. ## Linked Issues or Issue Description **What happened?** The server checks custom-image setup expiry every heartbeat maintenance tick, even when no active session exists. **What did you expect?** Session writes should update the next cleanup deadline. Existing provider cancellation and cleanup ownership must remain intact. Empty state should stay quiet. **Steps to reproduce** Run an instance with heartbeat scheduling enabled and no custom-image setup sessions. Observe repeated setup-expiry reads. Refs #17145. This is the first PR in the recurring server sleep stack. Its base is master. The full requested scope and verification contract are recorded in `doc/plans/2026-10-24-recurring-server-sleep-stack.md`. Follow-up PRs target their predecessor and each own a separate reason for recurring work. ## What Changed - Carry the historical cleanup retry-observation fixture repair and its existing external test-database hook at the stack foundation. Runtime scheduling is unchanged. - Carry the previously verified login-cleanup fixture settlement correction at the stack foundation so every intermediate CI run receives it. This is test-only maintenance; custom-image expiry behavior is unchanged. - Add a custom-image expiry worker to the existing delivery coordinator. Remove its maintenance-tick scan. - Signal initial creation, provider refresh, cancellation/failure, and promotion inside their owning transaction. - Keep the active-status predicate, earliest saved expiry, and provider behavior in the custom-image service. Shared code gains only a queue topic. - Preserve startup recovery, bounded retries, concurrent write generations, and drain deferral. No tables or external alarm changes. - Add PostgreSQL lifecycle tests and repair this suite's database cleanup callback. A dedicated external test URL permits disposable PostgreSQL when the host cannot start another embedded cluster. ## Verification Current head: `3507c6be1e054fdd93353d5dcf77d4899b44518f`. This remains a single-duty PR stacked on master. - 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/3507c6be1e054fdd93353d5dcf77d4899b44518f/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/3507c6be1e054fdd93353d5dcf77d4899b44518f/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 supported service producers sharing the DB owner. Independent SQL and another process require the existing restart/reconciliation contract. - Failed provider cancellation retains the existing failed-session and historical-cleanup behavior. This PR changes scheduling, not teardown authority. - Active rows without an expiry preserve the existing behavior; no deadline is invented. Cloud sleep eligibility remains conservative. - The worker remains in the existing heartbeat-enabled startup path. It does not change disabled-scheduler behavior or prove deployed 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 Review follow-up: Added a real producer commit during a paused deadline read. Updated startup assertions for coordinator ownership and removed added code comments. The three affected suites pass (102 tests). The separate login-cleanup suite also passes locally (16 tests); its prior CI consume test timed out, and the fresh complete CI run passed. External PostgreSQL tests now use the repository guard for disposable databases ending in `_test`. --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
149 lines
9.4 KiB
Markdown
149 lines
9.4 KiB
Markdown
# Idle environment cleanup
|
|
|
|
Pending lease cleanup and historical resource verification use the existing
|
|
transaction signals, delivery coordinator, and in-process scheduler. They no
|
|
longer run unconditionally from the server maintenance tick. The run reaper
|
|
performs its inline pending-cleanup sweep only after actually recovering a run
|
|
or an orphaned active lease. Explicit user retry keeps its existing direct path.
|
|
|
|
## Work and ownership
|
|
|
|
| Duty | Truth | Trigger and next deadline | Quiet condition |
|
|
| --- | --- | --- | --- |
|
|
| Failed teardown | Existing `environment_leases` rows in `pending_cleanup` | Commit signal; later of row backoff and saved retry/ownership deadline | No pending lease or deferred orphan |
|
|
| Failed acquisition before a DB row exists | Existing local orphan spool and the producing runtime's buffer | Local buffer registration; five-minute retry while buffered; startup spool load | Durable row owns recovery and spool copy is removed |
|
|
| Historical provider verification | Existing failed ephemeral leases and finished custom-image setups without matching cleanup proof | Commit signal; five-minute retry while an unverified candidate remains | No unverified candidates, including retained verified history |
|
|
|
|
`environment-cleanup-state.ts` owns the deadline query and the shared safe reader
|
|
for retry/ownership metadata. Cleanup claims and the scheduler use that same
|
|
reader, including malformed-number handling and the existing maximum horizon.
|
|
Startup retains the immediate scan, but never bypasses a saved in-flight claim or
|
|
retry cooldown. Subsequent scans respect the five-minute row backoff. Claims,
|
|
provider readiness, Stop authority, workspace retention, copyback, receipt
|
|
validation, and escalation rules stay with their current owners.
|
|
|
|
`environment-cleanup-work.ts` registers two independent workers. Their generation
|
|
counters preserve a wake until successful reconciliation, including a first write
|
|
during drain, changes during a sweep, and failure followed by drain. The existing
|
|
heartbeat interval bounds pending-cleanup starts after bursts of writes. Historical
|
|
verification has a separate minimum five-minute spacing, including commit-triggered
|
|
passes, and does not delay server readiness. No timer remains once
|
|
there is no obligation. A self-generated write can require a final reconciliation
|
|
before quiet. A failed sweep retains a retry. Shutdown unsubscribes local buffer
|
|
wakes; the delivery coordinator stops subscriptions/timers and joins active work.
|
|
|
|
## Producer coverage
|
|
|
|
The shared `writeEnvironmentCleanup` helper registers and awaits intent before a
|
|
write in a transaction. Existing transaction owners call
|
|
`notifyEnvironmentCleanup(tx)` directly. Nested calls retain outer transaction
|
|
settlement and uncertainty handling through the shared DB implementation.
|
|
|
|
- `environments.ts`: orphan insertion, release, metadata replacement, and run-wide
|
|
release. These stores cover normal provider teardown and failed acquisition.
|
|
- `environment-runtime.ts`: reusable sandbox cleanup claims and their settlement,
|
|
and Stop/export cleanup claims. Renewal of an already active claim retains its
|
|
bounded timer; it cannot create new pending work.
|
|
- `heartbeat/recovery.ts` and `heartbeat/run-control.ts`: orphan-active recovery,
|
|
retry settlement, and explicit recovery of historical leases.
|
|
- `environment-task-runtime.ts`: unconfirmed plugin task termination and plugin
|
|
replacement orphans, including a registered deferred flusher when the row
|
|
cannot be written. The flusher retains an idle hold until the row is durable.
|
|
- `sandbox-stop-and-retain.ts`: persisted Stop intent and its settlement.
|
|
- `native-workspace-export-resume.ts` and `native-workspace-export-retry.ts`:
|
|
terminal-export cleanup, resumed-export cleanup claims, and settlement.
|
|
- `workspace-recovery.ts`: verified copyback schedules destruction.
|
|
- `native-workspace-sync.ts` and the native session executor's backup stamp:
|
|
writes that can invalidate an existing historical row-version proof.
|
|
- `environment-custom-images.ts`: provider state, terminal state, and promotion.
|
|
Initial active setup insertion is not a historical verification candidate.
|
|
|
|
Other lease metadata writes neither create pending cleanup nor change the
|
|
row-version/provider identity of a matching historical proof. Changes to an
|
|
already pending claim remain covered by its existing deadline. Deletion removes
|
|
work; an already scheduled reconciliation observes that removal. Independent SQL
|
|
writers and another server process are not covered by these process-local signals.
|
|
|
|
## Deferred orphan buffers
|
|
|
|
Runtimes that hold an unwritten orphan register their flusher in a registry scoped
|
|
to the shared database handle. Registration wakes the worker without contacting
|
|
the database. A sweep flushes registered runtimes as well as its own startup spool;
|
|
this reaches a producer runtime's buffer even when it differs from the heartbeat's
|
|
runtime. The registry retains only runtimes with unresolved buffer ownership.
|
|
|
|
Unreadable directories, records, and malformed records retain a retry; a missing
|
|
spool directory is an empty spool. A spool read failure still allows other runtimes to flush their buffers and
|
|
persisted leases to clean up, then fails the sweep so recovery stays scheduled.
|
|
Each concrete cleanup pass reloads the spool, including entries appended by
|
|
plugin task replacement after startup and entries beyond buffer capacity.
|
|
No empty-spool filesystem or database polling is introduced. Existing limits on a simultaneous database,
|
|
spool, and buffer-capacity failure remain; this is not a new durable queue.
|
|
|
|
## Verification and boundaries
|
|
|
|
The PostgreSQL worker tests exercise quiet configured state and verified history,
|
|
real store activation, persisted backoff, failed teardown and restart, nested
|
|
commit/rollback, drain/resume, cross-runtime deferred work, failed spool reads,
|
|
shutdown joining, provider uncertainty, and historical pagination. Existing
|
|
runtime, reaper, Stop, custom-image, and workspace tests cover the underlying
|
|
cleanup authority and provider contracts.
|
|
|
|
The runtime fixture now gives each simulated instance its own DB handle and spool.
|
|
Its journal-write failure injection targets the transaction boundary rather than
|
|
the direct update that moved inside that transaction. Fake-clock worker tests wait
|
|
for PostgreSQL I/O and distinguish an executing sweep from a retained next wake.
|
|
|
|
This does not migrate discovery of orphaned active leases or legacy shutdown
|
|
workspace settlement from the broader run-recovery loop. Custom-image *active
|
|
session expiry* also remains on its existing tick; only historical verification
|
|
moves here. Other maintenance duties, external probes, and connected transports
|
|
still affect idle traffic. No Cloud alarm coverage or host-sleep exemption changes.
|
|
These tests prove the changed workers become quiet and recover; they do not prove
|
|
deployed suspension or fleet savings.
|
|
|
|
## Custom-image setup expiry
|
|
|
|
Setup expiry uses a separate `custom-image-expiry` worker. The custom-image
|
|
service owns the active-status predicate, the earliest saved `expiresAt`, and
|
|
provider cancellation. Startup registers the worker in the existing enabled
|
|
heartbeat housekeeping path; the maintenance tick no longer calls its sweep.
|
|
|
|
All supported setup producers live in `environment-custom-images.ts`: initial
|
|
creation, provider refresh, cancellation/failure status, and promotion. They
|
|
register intent in their owning transaction. A provider refresh can move the
|
|
deadline earlier; a terminal transition removes it. Historical verification
|
|
metadata does not change active expiry eligibility and does not need this signal.
|
|
Environment deletion can only remove obligations; an already scheduled pass
|
|
observes the removal. Independent SQL and other DB roots are outside the existing
|
|
process-local notification contract.
|
|
|
|
No active dated session means no recurring reads or provider calls. Overdue
|
|
batches and failures retain a bounded retry, while first writes and failed
|
|
reconciliation survive drain. A missing expiry retains the existing behavior
|
|
(no inferred expiration); sleep safety still treats unsupported live resources
|
|
conservatively. Provider cancellation, failure state, and historical resource
|
|
verification keep their existing semantics. No Cloud alarm coverage changes.
|
|
|
|
## Historical retry test observation
|
|
|
|
The historical verification worker intentionally does not delay server readiness.
|
|
Its provider sweep finishes before the following database pending-state read
|
|
registers a retry deadline. The regression test now waits for that future
|
|
deadline before inspecting it; sweep completion alone is not scheduler settlement.
|
|
The runtime worker, retry interval, and readiness contract are unchanged.
|
|
|
|
A controlled 150ms delay before the real pending-state query reproduced the
|
|
current CI failure: the immediate assertion observed a null deadline. The
|
|
corrected assertion passed with the same delay. The temporary delay was removed,
|
|
and all 14 cleanup tests passed against an isolated PostgreSQL database. The
|
|
existing external test-database hook is carried at the stack foundation so
|
|
intermediate branches can use this same verification path.
|
|
|
|
An initial attempt at the foundation could not start embedded PostgreSQL because
|
|
that earlier branch lacked the external fixture hook. A broader fixture wait for
|
|
all idle holds to clear then failed three cases that legitimately retain pending
|
|
cleanup obligations. That wait was removed. Only the historical deadline assertion
|
|
changed; its requirement is now a deadline strictly in the future. The complete
|
|
suite passes with the original readiness and idle-hold behavior preserved.
|