Skip to content

Commit d9f9157

Browse files
Add accepted-plan decomposition exact-once guards and UI state (paperclipai#6831)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies, so planning approvals and child-issue fan-out are part of the core control-plane loop. > - Accepted plans are supposed to be a safe bridge from planning into execution, especially when agents wake from review decisions and reuse isolated workspaces. > - The duplicate-subtask incident showed that an accepted plan revision could be interpreted more than once across overlapping runs, which broke the single-source-of-truth model for issue decomposition. > - Fixing that required tightening the backend contract first: accepted-plan decomposition needs an exact-once fingerprint, durable claim state, and retry-safe child creation. > - Once that backend behavior existed, the board still needed visibility into what happened, so the issue detail view needed a dedicated decomposition section instead of forcing operators to reconstruct child creation from raw activity. > - This pull request adds the exact-once decomposition primitive, hardens wake routing and regressions around the incident, and surfaces decomposition state in the UI so future incidents are both prevented and easier to inspect. ## What Changed - Added accepted-plan decomposition semantics to `doc/execution-semantics.md`, including the exact-once fingerprint, durable claim/result expectations, and retry/resume behavior. - Added persistent accepted-plan decomposition claims in the backend, including schema, shared types/validators, service logic, and issue routes for creating and listing decomposition state. - Hardened heartbeat routing so an accepted-plan continuation stays scoped to the relevant planning issue instead of opportunistically re-decomposing another accepted issue on the same assignee. - Added regression coverage for the original failure modes: concurrent same-parent retries, cross-issue accepted-plan isolation, and partial child recreation under the same fingerprint. - Added the `Plan decomposition` issue-detail section plus supporting API/query-key/activity formatting updates so operators can see revision status, owner, child counts, and the linked child issues directly in the UI. - Included the small follow-up UI fix so the decomposition section still renders when the issue work mode is no longer `planning`. ## Verification - `pnpm --filter @paperclipai/server typecheck` - `pnpm --filter @paperclipai/ui typecheck` - `pnpm --filter @paperclipai/db typecheck` - `pnpm exec vitest run server/src/__tests__/issues-service.test.ts` - `pnpm exec vitest run server/src/__tests__/issues-service.test.ts -t "lists persisted decompositions with child issue summaries"` - `pnpm exec vitest run server/src/__tests__/issues-service.test.ts -t "accepted plan decomposition" server/src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts server/src/__tests__/heartbeat-context-summary.test.ts` - Manual UI path: create a planning issue without an isolated execution workspace, add a `plan` document, accept the `request_confirmation`, let Paperclip create child issues, then reopen the parent issue detail page and confirm the `Plan decomposition` section shows the accepted revision, status, idempotent-claim badge, and child links. - Separate follow-up bug noted during manual UI validation: accepting a plan on an issue whose run never records `workspace_finalize` is tracked in `PAPA-445` and is not part of this PR’s fix scope. ## Risks - This adds a new migration and a large Drizzle snapshot update; reviewers should confirm the schema shape and generated metadata match the intended decomposition table. - The exact-once claim changes sit on the accepted-plan fan-out path, so regressions there could block legitimate child creation or mis-handle retries if the claim state machine is wrong. - The new UI only appears when decomposition records exist; reviewers should use the manual verification path above rather than expecting existing issues on a stale local instance to show the section automatically. - `PAPA-445` remains an open follow-up for the `workspace_finalize` accept gate when a planning handoff never records finalize; that bug can interfere with reproducing the UI flow on isolated workspaces but does not change the correctness of the exact-once decomposition feature itself. > Checked `ROADMAP.md`: this PR is a bug fix / control-plane hardening change for accepted-plan decomposition, not a new uncoordinated roadmap feature. ## Model Used - OpenAI Codex via Paperclip `codex_local` (GPT-5-based coding agent; exact backend model ID/context window not exposed in the run context), with repository tool use, shell execution, and code-editing capabilities. <img width="806" height="1069" alt="Screenshot 2026-05-27 at 11 05 48 PM" src="https://github.com/user-attachments/assets/5b00b670-96cd-4470-b0a3-581743bcae28" /> ## 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
1 parent 9eac727 commit d9f9157

32 files changed

Lines changed: 22308 additions & 16 deletions

doc/execution-semantics.md

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Execution Semantics
22

33
Status: Current implementation guide
4-
Date: 2026-04-26
4+
Date: 2026-05-23
55
Audience: Product and engineering
66

77
This document explains how Paperclip interprets issue assignment, issue status, execution runs, wakeups, parent/sub-issue structure, and blocker relationships.
@@ -152,7 +152,73 @@ Blocked issues should stay idle while blockers remain unresolved. Paperclip shou
152152

153153
If a parent is truly waiting on a child, model that with blockers. Do not rely on the parent/child relationship alone.
154154

155-
## 7. Non-Terminal Issue Liveness Contract
155+
## 7. Accepted-Plan Decomposition
156+
157+
An accepted plan confirmation is permission to decompose one specific accepted plan revision into child issues.
158+
159+
This complements the existing accepted-plan continuation rule: once a plan is accepted, the source issue may create child implementation issues, but it must not start implementation work on the source issue itself during that continuation.
160+
161+
Paperclip must treat accepted-plan decomposition as an exact-once control-plane primitive, not as a free-floating wake that any later run may interpret again.
162+
163+
### Exact-once fingerprint
164+
165+
The canonical decomposition fingerprint is:
166+
167+
- `(sourceIssueId, acceptedPlanRevisionId)`
168+
169+
Where:
170+
171+
- `sourceIssueId` is the issue whose `plan` document revision was accepted
172+
- `acceptedPlanRevisionId` is the accepted `plan` document revision
173+
174+
This is the product contract because the accepted revision is the thing being authorized for decomposition. Re-accepting, re-waking, or re-reading the same accepted revision must not authorize a second child tree. A later accepted revision on the same source issue is a new fingerprint and may produce a different decomposition result.
175+
176+
An implementation may also store the accepted interaction id, acceptance run id, or other evidence, but those values must collapse onto the same uniqueness guarantee. They must not allow a second decomposition claim for the same `(sourceIssueId, acceptedPlanRevisionId)` pair.
177+
178+
### Durable claim and durable result
179+
180+
Before creating child issues, the first decomposition attempt must create or reuse a durable record for the fingerprint.
181+
182+
That durable record must be able to answer, without reconstructing the thread from comments or transcripts:
183+
184+
- whether decomposition for the fingerprint is `in_flight` or `completed`
185+
- which run or owner currently holds the in-flight claim
186+
- which child issues, if any, have already been created under that fingerprint
187+
- which final child issue ids belong to the completed result
188+
189+
Paperclip does not need to mandate a specific storage shape in this document. The record may live in a dedicated table, source-issue execution state, interaction metadata, or another durable product surface. What matters is the contract:
190+
191+
- the claim is durable before fan-out starts
192+
- partial progress is durable while fan-out is underway
193+
- the completed child result set is durable after fan-out finishes
194+
195+
If a run creates some children and then dies, retries must continue from the same fingerprint and reuse the already-recorded partial result. They must not restart decomposition as if nothing happened.
196+
197+
### Parent live path while decomposition is in flight
198+
199+
While decomposition for an accepted fingerprint is incomplete, the source issue must expose an explicit live path for that same fingerprint.
200+
201+
The accepted interaction by itself is only evidence that the plan was approved. It is not a sufficient live path once decomposition begins. The source issue must make it clear what moves the fingerprint forward next, such as:
202+
203+
- the active decomposition run
204+
- a queued continuation wake for the same assignee
205+
- a monitor or explicit recovery action tied to the same decomposition claim
206+
- a blocked state that names the real blocker for finishing that claimed decomposition
207+
208+
If the live run disappears, Paperclip must repair, resume, or visibly block the existing claim. It must not leave the source issue in a state where a second run can interpret the same acceptance as fresh permission to create sibling issues again.
209+
210+
### Concurrent and repeat attempts
211+
212+
Every later run that encounters the same accepted-plan fingerprint must consult the durable claim/result before creating children.
213+
214+
- If no claim exists, the run may atomically create the claim and become the decomposition owner.
215+
- If a claim exists and is `in_flight`, the later run must reuse that claim. It may resume the same decomposition if it is the valid continuation owner, or it may exit after observing that another run already owns the work.
216+
- If a claim exists and is `completed`, the later run must reuse the recorded child result and must not create new sibling issues.
217+
- If the prior attempt ended after partial child creation, the retry must continue under the same fingerprint and preserve the already-created child ids.
218+
219+
Concurrent accepted-plan runs are therefore idempotent relative to the fingerprint. Creating multiple child trees for the same `(sourceIssueId, acceptedPlanRevisionId)` pair is a product bug.
220+
221+
## 8. Non-Terminal Issue Liveness Contract
156222

157223
For agent-owned, non-terminal issues, Paperclip should never leave work in a state where nobody is responsible for the next move and nothing will wake or surface it.
158224

@@ -292,13 +358,13 @@ A blocker chain is covered only when its unresolved leaf is live or explicitly w
292358

293359
A `blocked` issue is stalled when the unresolved blocker leaf has no active run, queued wake, typed participant, pending interaction or approval, user owner, external owner/action, or recovery action. In that case the parent should show the first stalled leaf instead of presenting the dependency as calmly covered.
294360

295-
## 8. Crash and Restart Recovery
361+
## 9. Crash and Restart Recovery
296362

297363
Paperclip now treats crash/restart recovery as a stranded-assigned-work problem, not just a stranded-run problem.
298364

299365
There are two distinct failure modes.
300366

301-
### 8.1 Stranded assigned `todo`
367+
### 9.1 Stranded assigned `todo`
302368

303369
Example:
304370

@@ -314,7 +380,7 @@ Recovery rule:
314380

315381
This is a dispatch recovery, not a continuation recovery.
316382

317-
### 8.2 Stranded assigned `in_progress`
383+
### 9.2 Stranded assigned `in_progress`
318384

319385
Example:
320386

@@ -330,13 +396,13 @@ Recovery rule:
330396

331397
This is an active-work continuity recovery.
332398

333-
### 8.3 Recovery model-profile lane
399+
### 9.3 Recovery model-profile lane
334400

335401
Cheap model profiles are only for status-only operational recovery overhead. Paperclip may request `modelProfile: "cheap"` for bounded recovery-owner work that updates task liveness, clears bad status, records a disposition, or asks for human/manager intervention. Those wakes must carry guard context such as `allowDeliverableWork: false`, `allowDocumentUpdates: false`, and `resumeRequiresNormalModel: true`.
336402

337403
Automatic retries that can continue source work must use the original/normal model lane. This includes failed source-work retries, process-loss retries, transient/scheduled retries, max-turn continuations, source-assignee continuations, assigned-todo dispatch recovery, and any run that can update repo files, issue documents, plans, work products, or attachments. When a cheap status-only recovery determines that actual work remains, it must hand back to a normal-model worker run before source work or persistent deliverable updates resume. Cheap recovery hints must be scrubbed from copied retry, resume, child, and downstream source-work contexts.
338404

339-
## 9. Startup and Periodic Reconciliation
405+
## 10. Startup and Periodic Reconciliation
340406

341407
Startup recovery and periodic recovery are different from normal wakeup delivery.
342408

@@ -350,7 +416,7 @@ On startup and on the periodic recovery loop, Paperclip now does five things in
350416

351417
The stranded-work pass closes the gap where issue state survives a crash but the wake/run path does not. The silent-run scan covers the separate case where a live process exists but has stopped producing observable output. The productivity-review pass is later and separate; it reviews unusual progression patterns on assigned source issues, not stale run handles after a source issue already has a valid disposition.
352418

353-
## 10. Silent Active-Run Watchdog
419+
## 11. Silent Active-Run Watchdog
354420

355421
An active run can still be unhealthy even when its process is `running`. Paperclip treats prolonged output silence as a watchdog signal, not as proof that the run is failed.
356422

@@ -402,7 +468,7 @@ This is distinct from productivity review. Productivity review asks whether an a
402468

403469
Detached process cleanup is operational hygiene, not source issue liveness. Cleanup should be best-effort and auditable. If cleanup fails but the source issue is already terminal with same-run durable evidence, Paperclip should preserve the cleanup failure on the run/watchdog audit trail and route only the cleanup concern to bounded recovery when a real owner/action remains.
404470

405-
## 11. Auto-Recover vs Explicit Recovery vs Human Escalation
471+
## 12. Auto-Recover vs Explicit Recovery vs Human Escalation
406472

407473
Paperclip uses three different recovery outcomes, depending on how much it can safely infer.
408474

@@ -446,7 +512,7 @@ Examples:
446512

447513
In these cases Paperclip should leave a visible issue/comment trail instead of silently retrying.
448514

449-
## 12. What This Does Not Mean
515+
## 13. What This Does Not Mean
450516

451517
These semantics do not change V1 into an auto-reassignment system.
452518

@@ -463,7 +529,7 @@ The recovery model is intentionally conservative:
463529
- open an explicit recovery action when the system can identify a bounded recovery owner/action
464530
- escalate visibly when the system cannot safely keep going
465531

466-
## 13. Practical Interpretation
532+
## 14. Practical Interpretation
467533

468534
For a board operator, the intended meaning is:
469535

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
CREATE TABLE "issue_plan_decompositions" (
2+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3+
"company_id" uuid NOT NULL,
4+
"source_issue_id" uuid NOT NULL,
5+
"accepted_plan_revision_id" uuid NOT NULL,
6+
"accepted_interaction_id" uuid,
7+
"status" text DEFAULT 'in_flight' NOT NULL,
8+
"request_fingerprint" text NOT NULL,
9+
"requested_child_count" integer DEFAULT 0 NOT NULL,
10+
"requested_children" jsonb DEFAULT '[]'::jsonb NOT NULL,
11+
"child_issue_ids" jsonb DEFAULT '[]'::jsonb NOT NULL,
12+
"owner_agent_id" uuid,
13+
"owner_user_id" text,
14+
"owner_run_id" uuid,
15+
"completed_at" timestamp with time zone,
16+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
17+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
18+
);
19+
--> statement-breakpoint
20+
ALTER TABLE "issue_plan_decompositions" ADD CONSTRAINT "issue_plan_decompositions_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
21+
ALTER TABLE "issue_plan_decompositions" ADD CONSTRAINT "issue_plan_decompositions_source_issue_id_issues_id_fk" FOREIGN KEY ("source_issue_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
22+
ALTER TABLE "issue_plan_decompositions" ADD CONSTRAINT "issue_plan_decompositions_accepted_plan_revision_id_document_revisions_id_fk" FOREIGN KEY ("accepted_plan_revision_id") REFERENCES "public"."document_revisions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
23+
ALTER TABLE "issue_plan_decompositions" ADD CONSTRAINT "issue_plan_decompositions_accepted_interaction_id_issue_thread_interactions_id_fk" FOREIGN KEY ("accepted_interaction_id") REFERENCES "public"."issue_thread_interactions"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
24+
ALTER TABLE "issue_plan_decompositions" ADD CONSTRAINT "issue_plan_decompositions_owner_agent_id_agents_id_fk" FOREIGN KEY ("owner_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
25+
ALTER TABLE "issue_plan_decompositions" ADD CONSTRAINT "issue_plan_decompositions_owner_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("owner_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
26+
CREATE INDEX "issue_plan_decompositions_company_source_status_idx" ON "issue_plan_decompositions" USING btree ("company_id","source_issue_id","status");--> statement-breakpoint
27+
CREATE INDEX "issue_plan_decompositions_active_owner_idx" ON "issue_plan_decompositions" USING btree ("company_id","owner_agent_id") WHERE "issue_plan_decompositions"."status" = 'in_flight';--> statement-breakpoint
28+
CREATE UNIQUE INDEX "issue_plan_decompositions_source_revision_uq" ON "issue_plan_decompositions" USING btree ("company_id","source_issue_id","accepted_plan_revision_id");

0 commit comments

Comments
 (0)