Files
PaperClipAI/doc/idle-decision-continuations.md
T
DottaandPaperclip a63d19816f fix(server): wake decision continuations from committed changes (#17478)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A completed decision can owe a wake to the agent that proposed it.
> - The decision row already stores the pending continuation.
> - The main maintenance tick scans this debt even when no continuation
is ready.
> - Committed changes can activate recovery and leave settled history
quiet.
> - This pull request gives continuation delivery its own worker and
shares foreground delivery ownership.
> - Pending work stays recoverable without an unconditional continuation
scan.

## Linked Issues or Issue Description

Targets master after #17475 merged. This is slice 09 of the recurring
server work migration. Each PR owns one reason for background work. The
remaining inventory is in
`doc/plans/2026-10-24-recurring-server-sleep-stack.md`.

**What existing behavior does this improve?**

Decision continuation delivery and recovery in the server.

**Current behavior**

Foreground paths send a wake and clear a pending flag. The combined
decision sweep scans pending continuations on every maintenance tick. It
can overlap foreground delivery.

**Proposed behavior**

Terminal decision writes notify a dedicated worker through the existing
transaction boundary. Foreground and recovery calls share one in-flight
delivery per decision and DB owner. Recovery re-reads readiness, pages
past failures, and retries only unresolved debt or errors.

**Reason and benefit**

Open decisions, running effects, and delivered history no longer cause
an independent continuation scan. No queue table is added. Decision
expiry, target invalidation, interrupted effects, and retention remain
separate duties.

## What Changed

- Extract continuation readiness, delivery, and paged recovery into the
decision owner. Preserve the wake payload and at-least-once crash
behavior.
- Signal the ten decision update sites before SQL in their existing
transaction scope. Keep effects and wake calls outside those
transactions.
- Explicitly create the continuation obligation on all three
archive-proposal failure settlements, including recovered running rows
without inherited pending metadata.
- Serialize concurrent decision effect execution through final
settlement. Coalesce foreground and worker wakes. Re-read the durable
flag before sending. Fence effect completion to running state so
competing completion cannot restore acknowledged debt.
- Expose an opaque DB work-scope key. Child pools and nested
transactions share that identity; independent roots remain separate.
Existing signal consumers keep their behavior.
- Bound each recovery pass to 100 candidates, retain a fair cursor
across failures, and stop between deliveries on shutdown. Forward the
existing AbortSignal through the shared deadline adapter.
- Register enabled-heartbeat continuation recovery before decision
housekeeping. Remove only its independent scan from the main tick.
Preserve the public combined sweep and disabled-heartbeat behavior.
- Add generated migration `0388_ambitious_micromax.sql`, an idempotent
concurrent partial index for pending continuations, and keep five
generated snapshots.
- Add real producer, transaction, failure, concurrency, paging, quiet,
and shutdown tests. Document ownership and remaining limits in
`doc/idle-decision-continuations.md`.

## Verification

Current candidate: `18f682863cadb91281a86b4da2553a4579ee054a`, normally
integrated with landed archive squash `5785090ff1`. All 31 independent
incoming master files are preserved exactly, including
controller-authority recovery. All 97 decision/startup/deadline cases
plus server types/build pass after the required Runner vendor rebuild.
The preceding full five-suite selection passed 293 cases; the
continuation owner behavior is unchanged by master reconciliation.

- **97 cases pass at this master integration** across decision, startup
and scoped-deadline suites, plus server types/build. The preceding
parent candidate passed the full 293-case
decision/gateway/archive/startup/deadline selection; those results
remain evidence for that revision. All selected cases executed with
separate PostgreSQL fixtures where needed.
- Tenant-driven global scan amplification reproduced six sweeps for six
sequential real cancellations. Existing thirty-second notification
coalescing now permits one recovery sweep per window while retaining
pending work. The first green fixture wrongly expected the intent hold
to clear before coalesced reconciliation; the hold remains and quiet is
checked after settlement. Foreground delivery stays immediate.
- The archive contention test uses a signaled dedicated
single-connection read and two FIFO SQL round trips instead of a fixed
delay. Removing effect coalescing makes it fail with two contender
reads; restoring production code passes. No production test hook was
added.
- **Three new real PostgreSQL regressions failed before repair**:
recovered archive proposals with invalid signatures, mismatched
manifests or archive execution failures and no inherited pending flag.
Each now persists failure, wakes once, acknowledges continuation
delivery and remains quiet on replay. Running-state fencing is
preserved.
- The unmerged continuation index is regenerated as
`0388_ambitious_micromax.sql` after parent migrations 386/387. All
parent journal entries are unchanged, five generated snapshots remain,
numbering/safety and snapshot/schema-drift checks pass. Fresh database
migration apply and replay pass; the concurrent index is valid.
- Existing lifecycle coverage includes foreground/worker coalescing,
child pools, transaction commit/rollback, failed acknowledgement,
restart, drain/resume, shutdown and fair 100-row recovery. Earlier
bounded-pass and concurrent archive-result regressions and their repairs
remain recorded in [the owning behavior
audit](doc/idle-decision-continuations.md).
- Failure accounting: the first combined command used an incorrect
retention fixture variable and skipped 15 cases. The corrected separate
external-PostgreSQL retention run passed all 15. The historical nine
embedded-only work-signal cases were not executed by this selection.
Extra upstream embedded-only suites attempted on the parent could not
initialize locally; they remain CI verification, as recorded in the
parent PR. Earlier reused-fixture, assertion and compile failures remain
in the behavior audit and stack ledger; they are not erased by later
passing checks.

Commands: `pnpm --filter @paperclipai/server typecheck`, `pnpm --filter
@paperclipai/server build`, server Vitest for `decisions-service`,
`decision-retention-service`, `server-startup-feedback-export` and
`scoped-deadline-work`; `pnpm --filter @paperclipai/db
check:migrations`, migration apply/replay, and the generated
snapshot-drift test. Local targeted evidence does not establish a full
local repository run, deployed sleep or fleet savings. Fresh exact-head
CI and both automated reviews remain merge gates.

## Risks

- Notification-driven recovery remains global but is coalesced to thirty
seconds. This bounds request-driven scans; it does not introduce
company-scoped selection. Foreground delivery remains immediate.
- Delivery remains at least once. A crash or acknowledgement failure
after accepting a wake can send it again. Local coalescing is not a
cross-process claim.
- Supported writers share the application DB signal owner. Independent
SQL writers or multiple active server processes need a separate wake
contract. Wake-producing service methods do not gain a new guarantee
when called inside an uncommitted caller transaction.
- The concurrent index needs top-level migration execution; it is
registered with the existing migration owner and can be replayed safely.
- No Cloud sleep blocker or external alarm coverage changes. Worker
quiet does not prove deployed app or database 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
- [x] All Paperclip CI gates are green
- [x] 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 17:44:51 -05:00

12 KiB

Decision continuation delivery while idle

A terminal decision can owe an agent wake. Its existing row stores that debt: continuationPolicy = wake_origin_agent, metadata.continuationPending = true, and either expired/cancelled status or a decided status with succeeded/partial/ failed execution. Open decisions and running effects are not ready to deliver. This migration adds no queue table and changes no wake payload.

Ownership and producers

decisions.ts owns decision creation, authorization, signatures, effects, and state changes. Its ten update sites now use a narrow transaction-aware write helper before the SQL statement. These cover the running claim; ordinary effect completion; archive-proposal signature, manifest, and execution failures; archive success; TTL rejection on use; cancel; direct dismissal; and background expiry. The helper acquires intent in the existing outer transaction or its savepoint. It does not include effect execution, provider calls, activity publication, or agent wake callbacks in a new database transaction.

Creation and bundle creation insert open decisions, so they do not create ready continuation debt. Acknowledgement only clears debt and belongs to the delivery owner; it does not signal itself. The production decision services are created by server startup and routes with the application's DB handle. Independent SQL writers and additional active processes require a separate notification and ownership contract. This change does not make wake-producing service methods safe to call inside an uncommitted caller transaction; only their SQL mutation boundary has the existing outer-commit guarantee.

decision-continuations.ts owns the readiness predicate, delivery, and recovery. Each worker pass attempts at most 100 candidates. A saved in-memory cursor advances past failed candidates between passes and wraps after the last page. Remaining durable work retains another pass. This avoids starving later rows behind a failing first page. A partial ID index contains ready pending continuations only. The predicate uses fixed SQL literals so a prepared generic plan can still use that index.

The shared DB package exposes an opaque work-scope identity. Raw DB object identity is insufficient because dedicated and liveness child pools already share their owner's signals. The key is shared by those children and nested transactions; independent roots have different keys. No connection credentials, private transaction state, or decision policy are exposed. Existing signal consumers and transactions that do not register work keep their behavior.

Foreground, recovery, and failure

Foreground delivery remains immediate. All service instances sharing the DB owner coalesce a simultaneous wake for the same decision into one promise. Delivery re-reads durable readiness after claiming that local flight, so a late foreground call cannot resend a continuation already acknowledged by recovery. The promise is removed on success or failure. Effect execution and its final state write also share one local flight per decision. This prevents concurrent archive callers from turning a successful archive into a stale-manifest failure. This is process-local coalescing, not a new durable claim or an exactly-once guarantee.

Effect completion requires the row to remain running. A competing completion cannot restore pending metadata after another caller has completed and delivered it. Effects retain their own existing durable claims and authorization checks. The delivery worker never executes effects.

A wake failure leaves pending metadata intact. A failed acknowledgement also leaves debt, so a later attempt can send the wake again. A crash after accepting the wake but before acknowledgement retains the existing at-least-once contract. An uncertain producer transaction remains held until the shared work-signal protocol establishes settlement; one empty scan cannot clear that uncertainty.

Lifecycle and scope

Startup reconstructs pending delivery. Committed changes wake the worker through the existing coordinator. Failures or remaining ready debt retain a 30-second retry. Open/running decisions, no decisions, and delivered history have no recurring continuation scan or deadline. Changes during a pass retain another request. Drain/standby defer dispatch without consuming that request; shutdown waits for the current admitted delivery and stops before starting another. The shared deadline adapter forwards its existing AbortSignal to task execution; existing callbacks that do not consume the signal keep their behavior. A future process-local retry is not a Cloud alarm.

Enabled-heartbeat startup registers this worker before the combined decision sweep. The main tick skips that sweep's independent continuation scan. Its public API retains combined behavior by default. Expiry or effect recovery can still deliver their newly produced continuations immediately. Disabled-heartbeat startup keeps its existing behavior and does not dispatch this new worker.

Decision expiry, invalidated-target scanning, interrupted effect recovery, and retention remain separate migrations. Existing Cloud safety blockers are not relaxed. This establishes worker quiet under the named conditions, not deployed host/database suspension or measured fleet savings.

Verification

The selected run passes 93 tests: 50 real-PostgreSQL decision cases, 39 startup cases, and four database work-signal unit cases. Fifteen new continuation cases cover real decide/cancel/dismiss/expiry producers, running-to-terminal completion, foreground contention, dedicated child pools, stale caller re-read, restart, failed acknowledgement, outer rollback/commit, first write during drain, failed retry through drain, changes during delivery, shutdown, and paging past a failed candidate. Quiet checks observe no SQL calls across an hour of controlled time. Nine pre-existing embedded-PostgreSQL signal tests are skipped locally because the embedded runtime is unavailable; CI must run that supported environment. Workspace typecheck and build pass. The final literal-predicate adjustment also passes all 49 decision tests before the additional archive-race regression.

The original continuation index applied and replayed without error. After parent integration it is generated migration 0388_ambitious_micromax.sql; the fresh apply and replay also pass. A rolled-back synthetic 40,000-row history with one pending continuation uses the partial index under force_generic_plan, reads two shared buffers, and executes in 0.015 ms on the local PostgreSQL server. This is query evidence, not a production latency claim.

The first test selection had 90 passes and one failure: the injected PostgreSQL acknowledgement error is wrapped by Drizzle, but the assertion expected its message at the top level. The assertion now checks the original cause. No production retry, hold, or timeout was weakened. A recursive unit-fixture type initially prevented migration generation; an explicit fixture type fixed it. Full CI and fresh review remain required before PR readiness.

Review found two real regressions. Before repair, a controlled concurrent archive reported failed after archiving successfully, and shutdown sent a second wake after stop was requested. Both regressions failed before the fix. Shared local effect ownership fixes archive completion; bounded passes and abort propagation fix backlog shutdown. The 93-case selection passes after repair. The paging test also proves a failed first candidate cannot hide the 101st candidate across bounded passes. A result-type alias initially made the known decision ID optional; the alias now matches the existing outcome contract.

The existing tool-review and archive-delivery consumers also pass 124 real-DB tests after AbortSignal propagation. Together with the decision/startup/signal selection, 217 selected tests pass; the same nine older embedded-only cases remain locally skipped. Workspace typecheck passes after the outcome type fix.

Explicit archive-failure delivery

The three archive-proposal failure paths now explicitly set continuationPending: true for wake_origin_agent, rather than assuming the pre-read running row already carries that metadata. The existing running-state fence remains, so stale settlement cannot re-arm an acknowledged result. Invalid signatures, manifest mismatches and archive execution failures each failed a real-PostgreSQL recovery regression before repair. Each now saves the failed result, wakes once, acknowledges delivery and remains quiet on replay.

Normal parent integration preserves the projection interaction-link index and archive pending-order index. The unmerged continuation index was regenerated as 0388 from the parent's generated metadata; every parent journal entry is unchanged, five snapshots remain, the SQL uses the existing top-level concurrent executor, and snapshot/schema drift passes. No generated snapshot was hand-edited.

The final affected selection passes 111 cases: 53 decisions, 15 retention/archive, 42 startup and one scoped-deadline case, with all four suites exercised. The first combined command used the wrong retention DB variable and skipped those 15 cases; the corrected external-PostgreSQL retention run passes all 15. Server typecheck and build pass. The historical nine embedded-only work-signal skips above are not reclassified as executed by this selection. Fresh CI and reviews remain required.

Bounded notification-driven recovery

Security review identified that sequential decision writes could each start an instance-wide continuation scan. A real-producer regression reproduced six sweeps for six sequential cancellations. The worker now uses the existing notificationCoalesceMs: 30_000 option. The first wake after a quiet window still reconciles promptly, and subsequent notifications in that window share its end deadline. Foreground continuation delivery is unchanged and immediate. Recovery can wait up to thirty seconds, matching the prior maintenance cadence. This bounds global scan frequency; it does not make queries company-scoped or add a cross-process notification contract.

The regression verifies one scan during repeated real create/cancel writes, retention and eventual delivery of a later pending row, and no recurring SQL after settlement. Its first green attempt wrongly expected zero active work while the coalesced request was pending. The existing intent hold correctly remains until reconciliation; the test now checks that hold's actual terminal boundary through the existing quiet assertion. No production hold was removed. All 293 integrated cases across decisions, archive, gateway/projection, startup and scoped-deadline suites pass without skips; server types/build pass. Fresh security review, Greptile, Ponytail and exact-head CI remain merge gates.

The archive contention regression no longer waits a fixed 100 ms. A dedicated single-connection contender signals its decision read; two FIFO SQL round trips then establish that it reached shared execution while the first archive remains held. The test requires one contender decision read and one archive execution. Temporarily removing the effect-flight guard made this test fail (two reads); restoring the production guard passes. The mutation was restored before commit. This verifies actual cross-pool contention without a timing delay or a new production-only test hook.

Archive #17475 is merged. Normal reconciliation with its exact squash 5785090ff1 preserves all 31 independent incoming master files, including Runner controller-authority recovery. All 97 decision/startup/deadline cases and server types/build pass after that integration. The Runner vendor rebuilt from its changed inputs. The preceding full five-suite selection passed 293 cases; the continuation owner's production behavior is unchanged by reconciliation. Fresh exact-head reviews and CI remain required on the master-target candidate.