mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 13:20:54 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Native agents keep remote processes and conversation history across warm turns. > - A completed turn can leave handoff authority only in controller memory. > - Losing that controller can block a follow-up even while its sandbox survives. > - This pull request verifies durable ownership and reconnects the exact prior process before handing it to the next run. > - Healthy follow-ups stay warm. Recovery preserves saved work without replaying a completed turn. ## Linked Issues or Issue Description **What happened?** A remote warm turn can finish without suspended backup authority. A replacement controller rejects that prior owner even when its exact process and completed checkpoint remain. A single unavailable identity probe can also stop an otherwise valid reattachment. **Expected behavior** Keep healthy follow-ups warm. Recover a completed session after proving that its controller stopped and that the successor holds current authority. Reuse compatible processes; otherwise save and resume the conversation. Retry unavailable identity reads within one shared deadline. **Steps to reproduce** Use `doc/testing-native-remote-continuity.md`. Its Linux cases execute real turns through PostgreSQL, TLS ingress, and the Rust runner. They kill a separate controller process, recover old checkpoint metadata, and change process configuration. The provider is deterministic. Related: #16432 and #16441 fix live warm-owner handoffs. #16341 adds recovery diagnostics. #14695 covers provider process retention. ## What Changed - Preserve warm process reuse between healthy remote turns. - Verify a committed prior result, stopped controller, matching checkpoint and sandbox, and current task/coordinator ownership before recovering an orphaned warm session. - Reconnect through the prior run route, then use the existing authenticated run attachment to change authority. - Persist a process-configuration digest. Close and save recovered sessions when old or changed configuration cannot support reuse. - Retry unavailable state, marker, and process reads within one 30-second deadline. Keep signals and definitive identity failures single-attempt. - Record closed recovery diagnostics, including unreadable or malformed saved state. - Retire the recovery-only ownership check after verified attachment, so later warm turns use their current authority. - Add real-process Linux regressions and refusal controls. - Expose the existing source-only file-recovery form for retained native holds, with the same board authority, evidence, and exact stop-receipt requirements as legacy holds. Recording recovered files does not change task status or replay a run. ## Verification - `pnpm -r typecheck` passed; the server check was repeated for the third-turn fix. - `pnpm build` passed after the final UI change. The UI typecheck, token gates, and map checks also pass. - Four Linux continuity cases passed: warm reuse, controller loss, legacy checkpoint metadata, and changed configuration. Each creates one conversation and starts exactly three turns, including a healthy follow-up after recovery. The fixture uses expired prior leases; the third turn keeps the runner and provider processes. - Greptile caught the retained recovery gate; the third-turn regression now covers it. An initial test omitted the heartbeat’s saved input and left a test process after failure; the fixture was corrected and its disposable container reset before the passing run. - The current focused executor/restart group passes all 749 tests, including the original failure classifications. - Native file-recovery and UI tests pass (52 tests), including blocked tasks and both native and legacy holds. - After merging current master and retaining its equivalent CI expression check, the CI policy tests pass (15 tests) and all four three-turn Linux continuity cases pass again on `dec418ae55ff21b4fb067132613658cce445f181`. - `pnpm test:run` completed its general server group with 20,572 passes, 103 skips, and three failures: the skill-audit child process and two HTTP test timeouts. The same three suites then passed in isolation (89 tests) without code changes. That local command stopped before later groups; the complete final-head CI, including general, serialized, Runner, build, and browser gates, is green. - Greptile reviewed the combined head `dec418ae55ff21b4fb067132613658cce445f181` at 5/5 with no actionable findings; both earlier findings are resolved. All final-head CI gates and the security scan passed. Staging deployment and retained-incident disposition remain deployment acceptance work. ## Risks - Recovery requires exact durable evidence. Live conflicting controllers, replacement sandboxes, Stop/pause, and uncertain effects must remain blocked. - After controller loss, tool-contract or process-configuration changes can restart the provider. Normal healthy follow-ups retain their processes. - The new idle-recovery path has Linux fake-provider evidence. Live Codex/OpenCode and retained quarantined-state recovery still need staging qualification. - No database schema, telemetry, or runner protocol changes. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository inspection, code execution, and browser tools. The session does not expose a more specific model version or context-window size. ## 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 (targeted checks and isolated reruns; full-run limitation recorded above) - [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 on `dec418ae55ff21b4fb067132613658cce445f181` - [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>
57 lines
3.2 KiB
TypeScript
57 lines
3.2 KiB
TypeScript
import { useId, useState } from "react";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import type { ExecutionBlocker, Issue } from "@paperclipai/shared";
|
|
import { issuesApi } from "../api/issues";
|
|
import { Button } from "./ui/button";
|
|
import { Checkbox } from "./ui/checkbox";
|
|
import { Label } from "./ui/label";
|
|
import { Textarea } from "./ui/textarea";
|
|
|
|
export function WorkspaceFileRecovery({ issueId, issueStatus, blocker, canManage, onRecorded }: {
|
|
issueId: string;
|
|
issueStatus: Issue["status"];
|
|
blocker: ExecutionBlocker;
|
|
canManage: boolean;
|
|
onRecorded: () => void;
|
|
}) {
|
|
const noteId = useId();
|
|
const confirmationId = useId();
|
|
const [evidence, setEvidence] = useState("");
|
|
const [confirmed, setConfirmed] = useState(false);
|
|
const record = useMutation({
|
|
mutationFn: () => issuesApi.resolveRecoveryAction(issueId, {
|
|
actionId: blocker.recoveryActionId!, outcome: "restored", sourceIssueStatus: issueStatus,
|
|
resolutionNote: evidence.trim(),
|
|
executionReconciliation: {
|
|
runId: blocker.runId!, providerStopped: true, actionOutcome: "mixed",
|
|
outcomeEvidence: "This request records recovered workspace files only. It does not replay the source run or certify its external action outcomes.",
|
|
workspaceRepairEvidence: evidence.trim(),
|
|
},
|
|
}),
|
|
onSuccess: onRecorded,
|
|
});
|
|
if (!blocker.workspaceRepairRequired || !blocker.canRecordWorkspaceRepair ||
|
|
!["legacy_execution_requires_reconciliation", "native_continuation_requires_reconciliation"].includes(blocker.cause)
|
|
|| !blocker.recoveryActionId || !blocker.runId) return null;
|
|
return <section aria-label="Record workspace file recovery" className="flex w-full flex-col gap-2">
|
|
{record.isSuccess ? <p role="status">File recovery recorded. The task status is unchanged. Continue the task when it is ready.</p>
|
|
: canManage ? <>
|
|
<Label htmlFor={noteId}>Recovered files and verification</Label>
|
|
<Textarea id={noteId} value={evidence} onChange={event => setEvidence(event.target.value)} maxLength={12_000}
|
|
placeholder="Describe how you recovered and verified the files from the original workspace. Include the backup or artifact link."
|
|
disabled={record.isPending} />
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox id={confirmationId} checked={confirmed} onCheckedChange={value => setConfirmed(value === true)} disabled={record.isPending} />
|
|
<Label htmlFor={confirmationId}>I recovered and verified the original workspace files.</Label>
|
|
</div>
|
|
<p className="text-muted-foreground">This records file recovery. It keeps the current task status and does not start a run. Paperclip verifies that the original execution stopped.</p>
|
|
<div className="flex justify-end">
|
|
<Button disabled={record.isPending || !confirmed || evidence.trim().length < 20} onClick={() => record.mutate()}>
|
|
{record.isPending ? "Recording…" : "Record file recovery"}
|
|
</Button>
|
|
</div>
|
|
{record.isError && <p role="alert" className="text-destructive">{record.error.message}</p>}
|
|
</> : <p className="text-muted-foreground">A board member with runtime access can record verified file recovery here.</p>}
|
|
</section>;
|
|
}
|