Files
PaperClipAI/doc/architecture/native-status-arbitration.md
DottaandPaperclip a12bbd1824 fix: stop completion reviews caused by policy upgrades (#13266)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The native runtime records completion assessments and task
decisions.
> - An application update can change the assessment policy version.
> - The previous code treated that version change as a reason for human
review.
> - An upgrade alone does not give the user a new decision to make.
> - This change keeps existing decisions and withdraws obsolete upgrade
review cards.

## Linked Issues or Issue Description

**What happened?**

A rules version change moved unfinished tasks into review and created a
card that said, "Review the superseding native policy assessment." The
task did not need new work or a human decision.

**Expected behavior**

New runs use the current rules. An upgrade leaves existing task
decisions alone. The saved policy version remains available in the audit
history.

**Steps to reproduce**

1. Save a native run assessment and a task decision.
2. Change the native status policy version.
3. Run finalization reconciliation without new task evidence.
4. The previous code created a review card. The corrected code keeps the
saved assessment and decision.

Related context: #13038 changed the native policy version. Searches
found no duplicate fix for upgrade-only completion reviews.

## What Changed

- Remove policy-version mismatch as a reconciliation trigger.
- Withdraw pending cards only when their source decision, effect ledger,
creator, key, and prompt match the old upgrade-only review.
- Restore the previous status only while that decision and status
version remain current and no other review gate is pending.
- Preserve answered cards, real review requests, later task changes, and
historical assessments and decisions.
- Record cleanup activity and retire any corresponding chat review
actions.
- Isolate cleanup failures so one old card cannot block other cleanup or
normal finalization.
- Update the status conformance fixture, regression tests, and
architecture documentation.

## Verification

- Passed: `pnpm exec vitest run
server/src/__tests__/native-status-arbiter-corpus.test.ts` (23 tests,
including the 53-fixture status corpus).
- Passed: `pnpm -r typecheck`.
- Passed: `git diff --check`.
- Passed: `pnpm build`.
- Passed: fresh Greptile review at 5/5 on `d24e5ecaf`, with no open
review threads.
- Passed: the 23 focused tests in the isolated full-suite environment.
- Local `pnpm test:run`: the server group finished with 10,638 passed
and one missing-fixture failure. Built the required
`fake-codex-app-server` fixture and reran the entire affected native
session-resume suite: 37/37 passed. The initial full command exited on
that server-group failure, so remaining groups are covered by CI.
- Passed: the workspace-runtime-exposure suite (25 tests, 3 platform
skips).
- Passed: all CI gates, including every test shard, browser tests,
typecheck, and build. The server shard passed on one retry after an
unrelated host-port conflict in the unchanged workspace-exposure tests.
- Cleanup tests cover repeat runs, real reviews, answered cards, later
statuses, newer decision identity, status changes back to review, other
pending requests, run scope, cleanup failures, retry, and live
publication failures.

## Risks

- Cleanup changes stored task state. It checks the exact obsolete
decision and status version under database locks before restoring
status.
- Pending approvals, interactions, and execution stages prevent
restoration out of review.
- The cleanup handles at most 100 matching cards per reconciliation
pass. It does not rewrite old decisions or accept an agent's completion
claim.
- New evidence and explicit task changes still use the existing
reconciliation paths. This change does not reevaluate old work merely
because Paperclip was updated.

## Model Used

OpenAI Codex, GPT-6. The exact deployment identifier and context window
are not exposed in this session. Used reasoning, repository inspection,
code editing, shell execution, and automated tests.

## 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-09-11 16:55:46 -05:00

283 lines
11 KiB
Markdown

# Native status arbitration
Native runner results do not directly mutate an issue's status. The model may
report that work is done, blocked, ready for review, or yielded, but Paperclip's
server remains the authority that decides and commits the resulting workflow
state.
The native status pipeline is:
```text
structured runner result
|
v
schema and terminal validation
|
v
evidence classification
|
v
pure status arbitration
|
v
transactional decision commit
|
+--> issue status/version
+--> durable side effects
+--> audit and recovery records
```
This separation prevents model prose from acting as a privileged status
command, protects newer issue state from stale runs, and makes every decision
replayable and auditable.
## Inputs to finalization
The runner returns a `paperclip.run_result.v1` result and a matching terminal
record. Important result fields include:
- `reportedWorkDisposition`: `done`, `blocked`, `needs_review`, or `yielded`;
- `completionClaim`, including the contract revision, criterion claims, and
remaining work;
- `verification` claims;
- evidence references;
- an optional blocker; and
- an optional continuation.
The server also owns facts the runner cannot choose:
- the persisted completion contract;
- the run's actual terminal state;
- whether workspace finalization succeeded;
- the issue's current status and status version;
- pending approvals, interactions, and execution-policy stages; and
- the completion-authority policy recorded on the contract.
## Evidence classification
`classifyNativeEvidence()` compares model claims with durable Paperclip
records. It recognizes these evidence families:
- run events with an authoritative control-plane evidence verdict;
- issue work products;
- approvals;
- issue-thread interactions; and
- attachments.
Each evidence reference becomes one of:
| Outcome | Meaning |
| --- | --- |
| `accepted` | A matching durable record exists and authoritatively supports the claim. |
| `missing` | The required claim or referenced record does not exist or is still pending. |
| `rejected` | The record or claim explicitly contradicts completion. |
| `unverifiable` | A record exists, or a string was supplied, but it is not authoritative evidence. |
For example, a model-authored reference such as `task-response` is not trusted
merely because it looks descriptive. Likewise, the model's own
`run.result.proposed` event is a claim, not independent proof.
The classifier produces a `NativeEvidenceAssessment` containing:
- contract-revision validity;
- criterion claim and evidence outcomes;
- verification claim and evidence outcomes;
- accepted, missing, rejected, and unverifiable references;
- blocking remaining work;
- normalized blocker or continuation data; and
- pending attention requests.
The classifier does not update the issue.
## Completion authority
The persisted completion contract controls how a `done` claim may be accepted.
### Durable-evidence completion
The strongest completion path requires all of the following:
- the runner reports `done`;
- the objective is satisfied;
- every contract criterion has accepted durable evidence;
- every verification has accepted durable evidence; and
- no remaining work blocks completion.
This produces `completion_contract_satisfied`.
### Low-risk claim-policy completion
Ordinary issue completion changes Paperclip workflow state, but it does not by
itself authorize deployments, spending, secret access, approvals, or arbitrary
API calls. Default native completion contracts therefore use low-risk
`agent_claim_policy` authority.
Under that policy, a result can complete the issue when:
- it reports `done`;
- its contract revision matches;
- every contract criterion is claimed `satisfied`;
- every verification is claimed `passed`; and
- no remaining work blocks completion.
This produces `completion_claim_policy_accepted`. Independently governed tools
and effects still enforce their own authorization and approval rules.
## Decision order
`arbitrateNativeStatus()` is a pure function. It evaluates higher-authority
conditions before model disposition:
| Condition | Status decision | Important effects/reason |
| --- | --- | --- |
| Issue is already `done` or `cancelled` | Preserve | `terminal_status_preserved` |
| Workspace finalization failed | Preserve | Record a retryable finalization error |
| Run was cancelled | Preserve | Release run resources |
| Run failed | Preserve | Schedule recovery |
| Approval, interaction, or execution stage is pending | `in_review` | Materialize/bind the governance gate and notify its owner |
| Completion satisfies its authority policy | `done` | Release checkout |
| Runner reports `needs_review` | `in_review` | Bind a reviewer and notify the owner |
| Runner reports a task-wide blocker | `blocked` | Persist blocker owner and unblock action |
| Runner reports a current-track blocker | `in_progress` | Enqueue another productive track |
| Runner reports `yielded` with a valid continuation | `in_progress` | Enqueue the declared continuation |
| Completion evidence is incomplete and continuation is forbidden | Preserve | Record a finalization error and named next action |
| Completion evidence is otherwise incomplete | `in_progress` | Enqueue a bounded, idempotent continuation |
The output is a `NativeStatusDecision` containing:
- the arbiter policy version;
- `statusAction` and `toStatus`;
- a stable reason code;
- an optional unblock descriptor; and
- declarative side effects.
The pure arbiter performs no database or network writes.
## Transactional decision commit
`commitNativeStatusDecision()` applies the decision against authoritative issue
state. It uses the issue's prior status, status version, and prior decision ID
as compare-and-swap inputs. If another actor changed the issue first, the commit
raises `NativeStatusRaceError`; finalization reloads current state, reassesses,
and retries a bounded number of times.
Within the transaction, the committer:
1. validates that the assessment and issue bindings still match;
2. records the status decision and reason;
3. updates the issue status and increments its version when required;
4. materializes declared effects;
5. writes an effect ledger with deterministic idempotency keys;
6. updates the native-finalization coordinator; and
7. persists audit activity.
After commit, activity publications are emitted. Reconciliation may redeliver a
pending effect, but it resumes the recorded ledger decision; it does not ask the
model or arbiter to invent a new decision.
## Durable side effects
Depending on the decision, materialized effects may include:
- an idempotent agent wake request;
- an issue-thread interaction;
- a reviewer binding or owner notification;
- a persisted blocker and unblock action;
- a scheduled retry or recovery action;
- a delegated child issue;
- checkout/resource release; or
- finalization/reconciliation records.
Effect rows bind company, issue, decision, target, ordinal, delivery state, and
idempotency key. This is what makes a status transition with follow-up work
recoverable after a process crash.
## Governance precedence
A model cannot bypass a pending governance gate by reporting `done`. Before
completion is considered, finalization checks:
- an active execution-policy stage;
- a pending issue-thread interaction; and
- a pending or revision-requested approval linked to the issue.
If one exists, the issue goes to `in_review` and the durable gate remains the
path forward.
## Failure and recovery behavior
Status finalization is coordinated by `native_run_finalizations`. Important
phases include observation, workspace finalization, assessment, arbitration,
commit, and retryable or terminal failure.
Examples:
- a workspace-finalization failure preserves the claim and records a retryable
error rather than falsely completing the issue;
- a failed provider run preserves partial evidence and schedules recovery;
- a status-version race causes bounded reassessment against current issue
state; and
- a materialization failure records the failed phase and next retry time rather
than silently dropping the side effect.
## Policy upgrades
The policy version on an assessment is audit metadata. New runs use the current
rules. A version change alone does not reassess an old run, change task status,
or ask a person to review completion. New evidence and explicit status changes
still use the existing reconciliation paths.
Reconciliation also withdraws pending review cards created solely by the old
policy-version check. It restores the previous status only if that exact decision
and status version are still current and no other review gate is pending. A later
user or agent decision takes precedence. The old assessments and decisions remain
in the audit history; cleanup does not accept or reject the agent's work.
## Diagnosing an unexpected status
Start with the terminal heartbeat run and inspect:
1. `resultJson.nativeResult.reportedWorkDisposition`;
2. completion-claim contract revision and criterion statuses;
3. verification statuses and evidence references;
4. `resultJson.assessmentId` and `decisionId`;
5. `resultJson.authoritativeDecision`;
6. `resultJson.issueStatusBefore` and `issueStatusAfter`;
7. `finalizationPhase` and `workspaceFinalizeStatus`; and
8. pending approvals, interactions, execution stages, blockers, or
continuations on the issue.
Common patterns:
- `done` claim + `in_progress` decision: evidence/claim policy did not accept
completion, or blocking work remained;
- `done` claim + `in_review`: a governance gate took precedence;
- successful run + unchanged status + retryable finalization: workspace or
status-effect commit failed;
- repeated `issue_continuation_needed` runs: the issue remains open without a
converging completion or durable wait path; and
- terminal issue unchanged by a stale run: terminal-state preservation or
status-version race protection worked as designed.
## Primary implementation locations
- `server/src/services/native-runtime/evidence-classifier.ts` — validates
claims against durable records.
- `server/src/services/native-runtime/status-arbiter.ts` — pure policy and
decision table.
- `server/src/services/native-runtime/status-decision-committer.ts` — CAS
commit, effect materialization, ledger, and audit.
- `server/src/services/native-runtime/native-run-finalizer.ts` — orchestrates
assessment, governance checks, arbitration, bounded race retry, and result
projection.
- `server/src/services/native-runtime/completion-contracts.ts` — creates and
versions default native completion contracts.
- `server/src/services/native-runtime/native-finalization-reconciler.ts` —
resumes interrupted finalization without re-inventing committed decisions.
- `server/src/services/recovery/service.ts` — repairs open issues that finish
without a live or durable wait path.
See also
[`durable-continuation-scheduler.md`](./durable-continuation-scheduler.md) for
the scheduler and recovery behavior that follows an `in_progress` decision.