## Thinking Path > - Paperclip manages agent work and the decisions that authorize it. > - A committed decision can leave effects unfinished after an interrupted execution. > - Recovery must preserve that debt without scanning settled history forever. > - Claims already store the state and time needed to schedule recovery. > - A failed claim must get another attempt even while newer claims arrive. > - Malformed claims need a durable failure outcome instead of an endless retry. > - This PR uses committed signals, saved deadlines, and bounded recovery rotations. ## Linked Issues or Issue Description **What happened?** Periodic decision maintenance queries for unfinished effects even when none remain. A cursor that only wraps after a short page can postpone an old failure indefinitely under continuous new work. Missing saved deciders or chosen options can also leave non-executable claims scanning forever. **Expected behavior** Open decisions and completed history produce no recurring effect-recovery SQL. Committed claims wake recovery, and restart reconstructs their deadlines. Genuine failures retain retries. Known malformed outcomes settle with an audit and preserve completed effects. Recovery must not invent authority, repeat committed effects, or lose continuation delivery. **Steps to reproduce** Create an old failing claim with a one-row recovery page, then keep adding newer due claims. Before the rotation fix, the old failure is never revisited. Separately, recover a claim with a missing decider or chosen option: the old implementation skips or throws on every attempt without settling it. Creating an ordinary open decision also caused an unnecessary effect scan. Stacked on #17479. Other decision expiry, continuation, target and retention duties have separate PRs in the stack. ## What Changed - Coalesce sequential committed notifications for up to one second without delaying already saved deadlines. This bounds instance-wide recovery reads and adds no idle polling. - Register effect recovery with the shared scheduler and transaction-aware notifications. Derive deadlines from the existing claim timestamp and recovery grace. - Remove effect recovery from recurring aggregate maintenance when the worker owns it. Preserve the explicit combined recovery entry point. - Page within a frozen final timestamp/ID and retain PostgreSQL precision at both boundaries. Failed claims and healthy later claims both make progress. - Load the current claim and derive recovery authority inside the shared execution flight. Preserve foreground joining, current authorization and per-effect locks. - Recheck known malformed claims under a row lock. Save a failed outcome, or partial when an executed effect receipt exists, with `effectRecoveryError` and an atomic `decision.recovery_failed` audit. Publish activity after commit and use normal continuation delivery. - Avoid effect-recovery notifications for open-decision creation. Claim and settlement writes retain their existing notification boundary. - Add generated migration `0390_salty_killraven.sql`, the concurrent, idempotent partial index for running-claim lookup. No queue table is added. - Cover quietness, transaction and lifecycle boundaries, retry fairness, malformed outcomes, and concurrent repair. Document the contract in `doc/idle-decision-effect-recovery.md`. ## Verification Current candidate normally integrates landed expiry `3421366903`, including expiry-only creation signals and all 21 independent master changes. The test revision is `dba8a79d1e1a5b97b8b5f6dac0f7c7837536531a`; the publication adds only this evidence documentation. - **143 current cases pass** across all 26 recovery, 54 decision, 20 expiry, 42 startup and one scoped-deadline cases. Server types/build pass with the rebuilt Runner vendor (1,524 inputs and 1,563 outputs). These results follow the landed dependency reconciliation; prior 69- and 141-case results remain historical evidence for their stated revisions. - Generated migration `0390_salty_killraven.sql` adds the existing running-claim partial index through the concurrent executor. All 388 parent journal entries are unchanged; five generated snapshots remain. Schema drift, numbering/safety and actual migration apply/replay pass. - Existing regressions exercise failed-claim rotation under sustained arrivals, malformed decider/option outcomes, atomic audit failure, row-lock repair races, current authority, foreground effect joining, coalesced tenant notifications, transaction commit/rollback, drain/resume, paging with PostgreSQL timestamp precision, and shutdown/restart. Completed effects are not repeated. - The first integrated run passed 140 cases and failed an overlap test that assumed the effect deadline was the scheduler's earliest deadline. Continuation coalescing now creates an earlier registered wake. The test services that earlier wake before asserting the original effect grace deadline; it still requires one shared effect flight, two lock attempts including the test gate, one committed comment and one continuation. No production timing was relaxed. - Earlier red/green fairness, malformed-claim, notification amplification and fixture failures remain recorded in [the owning audit](doc/idle-decision-effect-recovery.md). Extra upstream embedded-only tests attempted during parent integration could not initialize locally; CI must exercise that environment. - Previous candidate `f54a707d74` received both 5/5 reviews but its CI server shard 6 was interrupted (run 40597921712). The step was cancelled, with no failing assertion or suite summary, and the verify gate also failed. No retry or passing claim was made for that run. The necessary parent repair is included here; fresh CI and both reviews are required. Commands: server typecheck/build and Vitest for `decision-effect-recovery-work`, `decisions-service`, `decision-expiry-work`, `server-startup-feedback-export`, and `scoped-deadline-work`; DB migration checks, schema snapshot drift and real migration apply/replay. No full local repository-suite or deployed sleep/cost claim is made. Fresh current-head CI and Greptile/Ponytail are required before merge. ## Risks Missing deciders and missing chosen options now produce durable failed or partial outcomes instead of remaining running forever. Their audit and receipts remain available; no effect executes under invented authority. Only these known malformed states are settled this way. Database, audit, and other execution failures retain retries. Continuations retain their existing at-least-once delivery contract. The supported writer model remains process-local notifications plus restart reconstruction. Independent SQL writers and multiple replicas gain no new wake protocol. Cloud admission and durable external alarms are unchanged. These tests establish worker behavior, not deployed host suspension or fleet savings. ## 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>
15 KiB
Idle decision-effect recovery
Interrupted decision effects now have a dedicated deadline worker. The main maintenance tick no longer scans running decisions. Target invalidation remains in that tick; TTL expiry and continuation delivery have their own workers.
Obligation and activation
The existing decision row is durable truth. A row with status = decided and
executionStatus = running owes effect recovery. Its next deadline is
updatedAt + PAPERCLIP_DECISIONS_RECOVERY_GRACE_MS, default 60 seconds. Zero is
supported; invalid or negative values retain the existing default. Reads do not
move the timestamp. Open decisions and completed history create no recovery debt.
The existing writeDecisionChange transaction boundary signals effect recovery
for running claims, effect/archive settlement, cancellation, dismissal and expiry.
The generic notifyDecisionChange helper signals continuation and expiry only;
open-decision creation does not owe effect recovery. Notifications are awaited
before writes. Outer commit activates work; rollback does not publish a claim.
The additional topic does not wrap provider work or agent wakes in SQL transactions
and does not introduce a new queue table.
Committed notification bursts coalesce for up to one second after a successful pass. This bounds sequential tenant writes to at most one notification-driven global pass per window, rather than only coalescing overlapping scans. A saved recovery deadline can preempt that window. New obligations discovered by a signal can wait up to one second; foreground effects still execute immediately. The window alone never schedules work when there is no notification or saved debt.
The supported writer model is the application DB signal owner and its child pools. Independent SQL clients and multiple active writers need a separate notification contract. Startup reconstructs committed claims; it does not observe future writes from unsupported writers.
Ownership and recovery
decision-effect-recovery.ts owns selection, grace, bounded paging, and earliest
deadline lookup. decisions.ts owns execution, current membership, effect claims,
audit, archive policy, and continuation delivery. The worker adapter only binds
that domain contract to the shared coordinator. Startup registers it only when
heartbeat scheduling is enabled. The public combined sweep retains its default
recovery behavior for existing callers.
Each pass honors the existing configured batch size (default 100, finite values clamped to at least one integer). Its timestamp/ID cursor advances past failed candidates within a frozen final timestamp/ID. Newer due claims wait for the next rotation, so a continuous stream cannot postpone an earlier failure forever. Both cursor positions retain PostgreSQL precision. Genuine execution or settlement failures retain their retry obligation.
A missing saved decider or chosen option is a deterministic malformed outcome. Recovery rechecks it under a row lock, records an audited failed outcome (partial if a completed effect receipt exists), and preserves all effect receipts. It creates no authority and executes no effect for the malformed claim. The failure reason and continuation obligation are saved in the same transaction as the audit. An audit or settlement failure rolls back and leaves the claim pending. A claim repaired before the lock is acquired remains eligible for normal recovery.
The decision service re-reads current status before recovery. Foreground and recovery execution share the existing per-decision flight scoped to the DB owner. Individual effects keep their durable records and transaction advisory locks. Completed effects are not repeated after a failed final decision status write. Recovery derives authority from the saved decider's current active membership. It preserves the current authorization intersection and current target checks.
The scheduler has a 30-second minimum retry spacing for pending or failed work. A first claim or failed retry during drain retains its wake without passive SQL. Shutdown stops subscriptions and checks cancellation between candidates. An already admitted effect/continuation is awaited; later candidates remain durable for restart. No periodic pass remains after all claims settle.
Existing guarantees and limits
Regular effects preserve their existing per-effect transaction boundaries. Continuation delivery retains its separate at-least-once wake/acknowledgement contract. This migration does not establish exactly-once external delivery.
Archive proposals have a separate versioned manifest contract. Recovery still verifies the saved signature/manifest and current authority. A crash after an archive commits but before the decision settles can encounter the changed manifest version on restart. The existing result is a failed decision with an archive error, while the archive remains committed once. The new worker preserves and tests that outcome; it does not claim to repair this preexisting atomicity gap.
Cloud sleep guards remain conservative. The internal next deadline is not a persisted external alarm and does not authorize host sleep with running decisions. This change establishes worker quiet, not actual instance/database suspension.
Verification
The new real-PostgreSQL suite covers 17 cases: empty/open/completed-history quiet, real foreground claim and failed settlement, exact grace and zero override, restart from a claimed effect, active foreground/recovery overlap, outer commit and rollback, first claim and failed retry through drain, revoked authority, continuation failure, aggregate exclusion, fair paging past missing-decider and malformed claims, archive recovery before and after an archive commit, a claim created during delivery, and shutdown between candidates. Quiet assertions observe SQL methods through one hour of fake time after settling.
The combined selection passes 212 tests: the 17 new cases, 50 existing decision service cases, 14 TTL cases, 39 startup cases, and 92 agent-history route cases. The first 15-case recovery run also passed. Workspace typecheck/build and the final server typecheck pass. No failure was suppressed or timeout increased.
The current regenerated migration 0390_salty_killraven.sql adds one partial index concurrently, with IF NOT EXISTS, through
the migration owner's top-level statement path. Generation retains the newest
five schema snapshots. Fresh application, replay, safety, and numbering checks
pass. With 40,000 completed decisions and one running claim, both due selection
and earliest-deadline lookup use the partial index and read two buffers (0.012 ms
and 0.003 ms in the local probe). The probe transaction is rolled back.
Full PR CI and review must pass separately before this slice is ready.
A follow-up regression seeds a legacy invalid claim with a PostgreSQL-generated microsecond timestamp ahead of a valid claim. With a one-row page, the original Date cursor selected the invalid claim again and starved the valid effect. That regression failed before repair. Recovery now retains the timestamp as database text for cursor comparisons, leaving business grace deadlines unchanged. The unresolved claim remains durable debt and is revisited after paging wraps; the valid claim progresses without repeating its completed effect. The regression now holds a candidate unresolved through the dispatch callback, because known malformed claims receive a durable failed outcome as described above. All 18 effect-recovery lifecycle cases and server typechecking pass after repair.
Foreground join test timing
CI exposed a timing assumption in the foreground/recovery overlap test: it expected exactly two lifetime recovery sweeps. A controlled PostgreSQL replay waited for the committed claim's deadline discovery before advancing time. It recorded two completed, zero-resumption scans before the deadline (startup and claim notification), followed by the legitimate deadline scan. The old assertion then failed deterministically with three calls. No recurring idle poll or duplicate effect was found in that sequence.
The test now waits for discovery, measures the deadline recovery relative to that point, and proves that its promise stays pending while foreground execution holds the effect lock. Advancing through several retry periods cannot start another sweep or effect. After release, recovery completes, one comment and one continuation wake exist, and the worker performs no SQL through the existing hour-long quiet check. Counting actual lock attempts also distinguishes joining from a second execution blocked on the same PostgreSQL advisory lock. A temporary mutation that removed flight joining made this stronger assertion fail; production code was restored before final verification. No runtime code, retry policy, timeout, or business deadline changed.
All 123 tests across the four complete effect-recovery, decision-service, expiry,
and startup suites passed, followed by server typechecking. Evidence:
/tmp/sleep-effect-join-red.log, /tmp/sleep-effect-join-green.log,
/tmp/sleep-effect-join-mutation.log, /tmp/sleep-effect-join-owning-tests.log,
and /tmp/sleep-effect-join-root-types.log with their status files. The initial
failure and mutation failure are expected evidence, not passing runs. Remaining
review findings and fresh stack CI remain separate acceptance requirements.
Recovery review repairs (October 26)
Two regressions failed before repair: an old failure was not revisited while newer due claims kept each page full, and creating an open decision caused an unnecessary effect-recovery scan. Frozen rotation boundaries now preserve both retry fairness and forward progress. Creation still signals its continuation/expiry consumers; the effect queue is signaled by the existing claim/settlement write boundary.
Four malformed-outcome cases also failed before repair. Recovery now records
effectRecoveryError and a decision.recovery_failed activity atomically. Live
activity publication follows commit. Existing completed effect receipts remain
unchanged, and the normal continuation owner delivers the result. A real audit
insert failure leaves both status and audit uncommitted, then later recovery
settles successfully. A real PostgreSQL row-lock test repairs a claim while
settlement waits; the locked recheck leaves that claim running, and its valid
effect executes once on the next pass.
The shared execution flight now loads the current claim and derives recovery authority together. Recovery no longer performs a redundant full-row lookup before entering the flight. Foreground overlap, current membership checks, per-effect locks, continuation behavior, and the resumption count are preserved. Only the two known malformed states are terminalized; other failures retain their existing retry policy. No schema, queue, or external-wake contract is added.
All 24 recovery lifecycle cases and server typechecking pass. Controlled failures
and final evidence are retained in /tmp/sleep-effect-fairness-red.log,
/tmp/sleep-effect-invalid-red.log, /tmp/sleep-effect-owner-tests.log, and
/tmp/sleep-effect-owner-types.log. Broader stack and remote acceptance remain
separate requirements; these results do not prove host suspension.
The owning branch also passes all 129 cases across the four complete recovery,
decision-service, expiry, and startup suites. Database-backed suites use separate
migrated disposable databases. See /tmp/sleep-effect-review-owner-acceptance.log.
Bounded notification review repair
Ten sequential real cancellation transactions reproduced ten extra recovery scans. After enabling the existing shared notification-coalescing capability, the same transactions cause one follow-up scan and then return to SQL quiet. A separate real-database case preserves an already loaded deadline 500 milliseconds away. The initial deadline fixture observed the scheduler while its SQL pass was still active; waiting for that read removed the fixture race before the baseline was recorded. The cancellation amplification still failed before the production fix.
All 131 tests in the four complete recovery, decision-service, expiry and startup suites pass at the owning branch, as does server typechecking. The helper's optional coalescing capability is promoted from the later plugin scheduling slice into this PR; consumers that omit it keep their previous behavior. The integrated stack is verified separately before publication. The original full-suite and external-wake limitations remain.
Current parent integration
The expiry and continuation review repairs are integrated normally. The running claim index is regenerated as 0390 after parent 0388/0389, with concurrent creation registered in the existing executor. All 388 parent journal entries are unchanged; the five newest generated snapshots remain. Schema drift, numbering/safety and fresh migration apply/replay pass. No generated snapshot was hand-edited.
All 141 cases in the five complete effect-recovery, decision/continuation, expiry, startup and scoped-deadline suites pass. This includes all 26 recovery cases and the parent's deterministic cross-pool archive contention test. Server types and build pass. The first integrated run passed 140 cases and failed an old scheduler assertion: the coordinator now also holds an earlier continuation-coalescing deadline. The overlap test services that earlier registered wake, then still requires the original effect grace deadline, one active effect flight, two total advisory-lock attempts (test gate plus foreground), one committed effect and one continuation. No production timing or retry was changed to pass the test.
Existing one-second effect-notification coalescing and thirty-second failure retry remain. Continuation notification coalescing is owned separately by the parent. Extra upstream embedded-only tests attempted during parent integration could not initialize locally; fresh current-head CI must cover that environment. Full CI and both automated reviews remain merge gates.
After normal integration of master-target expiry parent f7c61e9912, the
effect-recovery owner, worker, decision execution and notification implementations
are unchanged. All 69 current recovery/startup/deadline cases and server types/
build pass with the updated Runner vendor. The earlier full 141-case selection
remains evidence at its stated revision. Parent migration history is an exact
prefix and five generated snapshots remain. Fresh head CI and both automated
reviews are required after this parent integration.
Landed expiry integration and CI interruption
Expiry-only creation notification from aa03d2c4c2 is integrated. All 143 cases
in the complete recovery, decision, expiry, startup and scoped-deadline suites
pass, including single/bundle creation without continuation signals. The normal
merge of landed expiry 3421366903 also preserves all 21 independently merged
master files. The same 143 cases pass after that reconciliation.
The previous candidate f54a707d74 had both reviews at 5/5, but CI run
40597921712 failed because server shard 6 was interrupted. Its test step was
cancelled and orphan cleanup ran; no failing test assertion or suite summary was
reported. The workflow eventually completed with that shard and its dependent
verify gate failed. It was not retried or presented as passing. The necessary
expiry parent repair creates a new candidate with fresh CI and reviews required.