Files
PaperClipAI/server/src/modules
DottaandClaude Fable 5.2 39442400b7 fix(native): let a saved governed wait settle on its own same-run resume (#20838)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs can save a governed wait (a question or confirmation for
a person) and park the provider turn
> - If shutdown of the retained execution cannot be confirmed in time,
the run keeps its checkpoint, records
`native_governed_wait_settlement_pending`, and schedules a same-run
resume that settles the saved wait (#20336, #20494, #20519)
> - That failure writes an active recovery action for the run. Its cause
is in `EXECUTION_RECONCILIATION_CAUSES`, and its wake policy resumes
that same run
> - When the run is a resolved-interaction continuation, its dispatch
gate checks execution blockers. The gate read the run's own resume
action as a hold and cancelled the resume with
`execution_reconciliation_required`
> - The task then stayed blocked with no Retry and no continue control,
and the saved question could not wake the agent
> - This pull request makes the gate skip only the run's own
scheduled-resume action
> - The benefit is that a saved governed wait settles by itself, with no
person involved

## Linked Issues or Issue Description

Refs #20494, #20519, #20809

**What happened?**
A staging Codex run on a remote sandbox was a continuation after an
answered question. The agent saved a second governed wait (a
confirmation). Shutdown confirmation of the retained execution was still
pending, so the run recorded `native_governed_wait_settlement_pending`
and scheduled its exact-checkpoint resume. The resume reattached to the
remote runner and started. 32 s later it ended `cancelled` with
`execution_reconciliation_required`. No person cancelled it. The
lifecycle event of the cancel carried the run's own recovery action id.
The task showed `blocked` with `canRetry: false` and `canContinue:
false`, and no later run started.

**Expected behavior**
The same-run resume settles the saved governed wait. The task waits for
the person's answer. No run is cancelled and no recovery action needs a
person.

**Steps to reproduce**
1. A native run starts as a resolved-interaction continuation (context
`mutation: interaction`, `interactionStatus: answered`).
2. The run saves a governed wait, and shutdown confirmation does not
arrive in time (`native_governed_wait_settlement_pending`).
3. The coordinator schedules the same-run resume. At dispatch,
`decideCurrentRunStaleness` calls `getExecutionBlocker`, which returns
the run's own active action, and `cancelStaleRunInTx` cancels the run.

## What Changed

- `server/src/services/execution-blocker.ts`: `getExecutionBlocker`
takes `resumingNativeRunId`. For that run it skips only the active
action whose wake policy is `resume_native_run` for the same run and
whose evidence names that run. Workspace-restore holds and all other
actions still block.
- `server/src/modules/run-dispatch/adapters/postgres.ts`: the dispatch
staleness decision passes the native run id.
- `server/src/modules/run-dispatch/adapters/postgres.test.ts`: the
failing case and two controls (an action for another run, an action
without a resume policy still cancel).
- `tests/restart-chaos`: new opt-in workload Q4 (a second saved question
asked by the first answer's continuation run). Provider-fault rules take
`occurrence`; Q4 rules use 2, so PV1/PV2 hit the second governed wait.
Codex shim and ACPX tap both honor it. README documents Q4.
- `doc/execution-semantics.md`: states that the run's own scheduled
resume is not a hold against it.

## Verification

- Red on master: `postgres.test.ts -t "saved governed wait"` fails with
`{ dispatched: false, status: "cancelled", errorCode:
"execution_reconciliation_required" }` (the staging error code).
- Green: the same test passes; the two controls still cancel.
- Focused: `postgres.test.ts`, `explicit-native-continuation.test.ts`,
`native-preclaim-continuation.test.ts`,
`workspace-recovery-preparation-gates.test.ts`; server typecheck.
- Harness: `question-handoff.test.ts`, `provider-faults.test.ts`,
`acpx-provider-faults.test.ts`, `journeys.test.ts`; harness typecheck.
- Chaos: `--adapters codex --workloads Q4 --faults PV2 --tasks 1 --seed
111` on master and on this branch (results in a comment below).

## Risks

- Low. The exemption is exact: same run id in the wake policy and the
evidence, status `active`, only for a native run being dispatched. A
recovery action that names another run, has no resume policy, or is
resolved keeps its current effect.
- Saved state from older versions: actions already written have the same
shape, so a resume that is still scheduled settles after deploy.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude (Anthropic), model ID `claude-opus-5-5`, used through Claude
Code with tool use (shell, file edits, test runs).

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 09:59:22 -06:00
..

Feature modules

A feature module under server/src/modules/<name>/ uses three layers. Each layer has one rule: which layer below it, it may import from.

adapters  →  application  →  domain
  • domain/ holds pure business rules. A domain file takes plain data in and returns plain data out. A domain file must not import drizzle-orm, @paperclipai/db, a service under server/src/services/, a route under server/src/routes/, or a Node.js I/O module (node:child_process, node:fs, node:net). A domain function must not read the system clock; the caller passes now as an explicit Date value.
  • application/ holds use cases and the ports they need. A use case takes its ports as constructor arguments and calls domain functions for policy decisions. An application file must not import drizzle-orm, a SQL client, a concrete adapter, or the server's HTTP error helpers. The outer service or route translates application errors into transport responses.
  • adapters/ holds the concrete implementations of the ports: Postgres queries, transactions, and process control. An adapter file may import drizzle-orm, @paperclipai/db, and Node.js I/O modules.

A module exposes its commands through index.ts, which composes the adapters and the use cases behind a factory function. Code outside the module imports that entry point, never a file inside domain/, application/, or adapters/ directly.

pnpm check:module-boundaries enforces these rules for production source files. It also checks access to the company deletion entry point below.

Company deletion

The existing company deletion service owns the database transaction and deletion order. A module can expose a separate company-deletion.ts entry point. Only services/company-deletion.ts can import this entry point. The entry point exports an implementation of CompanyDeletionParticipant from lib/company-deletion.ts. Do not export it through the normal module index.

The service must complete external cleanup before it starts the transaction. It must lock the company before it deletes dependent records or calls a module. Creation commands must use the same company lock. The service removes dependent records before it calls the module. The module checks its deletion conditions and deletes the records it owns. Use the supplied transaction. Do not start another transaction or call external systems. Throw an error if deletion cannot proceed. An error must roll back all database changes in the deletion transaction.

Call each module explicitly in dependency order. No module registry is required.

The agent lifecycle module also has a restricted configuration.ts entry point. It saves configuration and invalidates verification in the caller's transaction. The lifecycle guide lists its permitted callers. Ordinary configuration updates use the root lifecycle command.

The agent lifecycle module owns agent creation, pause, resume, and termination. Use its commands for these changes.

The execution admission module owns project task-execution policy and the durable admission records that charge project capacity. Today it exposes only internal policy commands and the deletion guards that project, agent, and company deletion call. Settlement releases active charges but keeps each admission's complete project set for later retained-session resolution. Execution claims still belong to services/heartbeat/queue.ts and native review dispatch. See execution admission accounting.