mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
## Thinking Path > - Paperclip manages agent work in isolated execution workspaces. > - A workspace depends on a valid database seed before it can run. > - Deferred seed failures were hidden behind a successful provision status. > - The seed restore also had two possible owners for the embedded PostgreSQL process. > - That allowed the target database to stop while the restore was still running. > - This pull request makes seed failures visible and gives the seed process sole lifecycle ownership. > - The benefit is that workspace provisioning reports the real result and does not stop its own target database. ## Linked Issues or Issue Description Related: #11684 **What happened?** Initial worktree provisioning could report success before its deferred database seed completed. The seed restore could also reuse a target embedded PostgreSQL process with another shutdown owner. This could stop the target database during the restore. **Expected behavior** Workspace status must show a failed deferred seed as a failure. The seed restore must own the target embedded PostgreSQL process until restore, migration, and validation finish. **Steps to reproduce** 1. Provision a worktree with deferred database seeding. 2. Make the seed manifest end in a failed state while the command exits with code 0. 3. Observe that the provision status remains successful on `master`. 4. Start a seed restore against an already-running target embedded PostgreSQL process. 5. Observe that another lifecycle owner can stop the target during restore. **Paperclip version or commit** `51a843e135` **Deployment mode** Local dev with execution workspaces and embedded PostgreSQL. ## What Changed - Add a first-class `workspace_seed` operation for deferred database seeds. - Require terminal, verified seed evidence before the seed operation succeeds. - Surface the seed phase and failure metadata in workspace status and UI state. - Give the seed process exclusive lifecycle ownership of the target embedded PostgreSQL process. - Suppress imported embedded-Postgres exit hooks without removing existing host listeners. - Record a credential-safe shutdown diagnostic in failed seed manifests. ## Verification - The original deferred-seed commit passed 4 server tests, 24 workspace-status UI tests, shared/server/UI typechecks, and the UI token gate. - The original PostgreSQL-lifecycle commit passed 3 lifecycle tests, 3 ownership/diagnostic tests, 1 real embedded-Postgres seed integration, and the affected package typechecks. - No local tests were rerun after the clean cherry-pick because the operator requested the shortest landing path. - Review the automatic PR checks for the clean `origin/master` replay. ## Risks - A live target database now causes an early error instead of being reused. The error includes recovery guidance. - Workspace consumers must handle the new `workspace_seed` operation type. Shared types and UI state handling are updated in this pull request. > 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 - OpenAI Codex, GPT-5, high-reasoning mode, with repository, shell, and GitHub tool use. The runtime does not expose a more specific deployment suffix or context-window value. ## 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 --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
102 lines
3.7 KiB
TypeScript
102 lines
3.7 KiB
TypeScript
import type { WorkspaceOperation } from "@paperclipai/shared";
|
|
import { describe, expect, it } from "vitest";
|
|
import { resolveRuntimeProvisionStatus } from "./ExecutionWorkspaceDetail";
|
|
|
|
function operation(overrides: Partial<WorkspaceOperation> = {}): WorkspaceOperation {
|
|
return {
|
|
id: overrides.id ?? "op-1",
|
|
companyId: "company-1",
|
|
executionWorkspaceId: "ews-1",
|
|
heartbeatRunId: null,
|
|
issueId: null,
|
|
phase: overrides.phase ?? "workspace_seed",
|
|
command: overrides.command ?? "bash ./scripts/provision-worktree-runtime.sh",
|
|
cwd: null,
|
|
status: overrides.status ?? "succeeded",
|
|
exitCode: overrides.exitCode ?? 0,
|
|
logStore: null,
|
|
logRef: null,
|
|
logBytes: null,
|
|
logSha256: null,
|
|
logCompressed: false,
|
|
stdoutExcerpt: null,
|
|
stderrExcerpt: null,
|
|
metadata: null,
|
|
startedAt: overrides.startedAt ?? new Date("2026-08-01T00:00:00.000Z"),
|
|
finishedAt: "finishedAt" in overrides ? overrides.finishedAt! : new Date("2026-08-01T00:01:00.000Z"),
|
|
createdAt: new Date("2026-08-01T00:00:00.000Z"),
|
|
updatedAt: new Date("2026-08-01T00:01:00.000Z"),
|
|
};
|
|
}
|
|
|
|
describe("resolveRuntimeProvisionStatus", () => {
|
|
it("reports eager when no runtime provision command is configured", () => {
|
|
expect(resolveRuntimeProvisionStatus({ runtimeProvisionCommand: null, operations: [] })).toEqual({
|
|
kind: "eager",
|
|
});
|
|
expect(resolveRuntimeProvisionStatus({ runtimeProvisionCommand: " ", operations: undefined })).toEqual({
|
|
kind: "eager",
|
|
});
|
|
});
|
|
|
|
it("reports deferred when configured but no provision operation has run", () => {
|
|
expect(
|
|
resolveRuntimeProvisionStatus({
|
|
runtimeProvisionCommand: "bash ./seed.sh",
|
|
operations: [operation({ phase: "workspace_provision" })],
|
|
}),
|
|
).toEqual({ kind: "deferred" });
|
|
});
|
|
|
|
it("reports provisioned with the finished time when the latest op succeeded", () => {
|
|
const finishedAt = new Date("2026-08-01T12:00:00.000Z");
|
|
expect(
|
|
resolveRuntimeProvisionStatus({
|
|
runtimeProvisionCommand: "bash ./seed.sh",
|
|
operations: [operation({ status: "succeeded", finishedAt })],
|
|
}),
|
|
).toEqual({ kind: "provisioned", at: finishedAt });
|
|
});
|
|
|
|
it("continues to recognize custom runtime dependency provisioning", () => {
|
|
expect(
|
|
resolveRuntimeProvisionStatus({
|
|
runtimeProvisionCommand: "pnpm install",
|
|
operations: [operation({ phase: "workspace_runtime_provision" })],
|
|
}),
|
|
).toMatchObject({ kind: "provisioned" });
|
|
});
|
|
|
|
it("reports provisioning while the op is running", () => {
|
|
const startedAt = new Date("2026-08-01T12:00:00.000Z");
|
|
expect(
|
|
resolveRuntimeProvisionStatus({
|
|
runtimeProvisionCommand: "bash ./seed.sh",
|
|
operations: [operation({ status: "running", startedAt, finishedAt: null })],
|
|
}),
|
|
).toEqual({ kind: "provisioning", at: startedAt });
|
|
});
|
|
|
|
it("reports failed with the finished time when the latest op failed", () => {
|
|
const finishedAt = new Date("2026-08-01T12:05:00.000Z");
|
|
expect(
|
|
resolveRuntimeProvisionStatus({
|
|
runtimeProvisionCommand: "bash ./seed.sh",
|
|
operations: [operation({ status: "failed", finishedAt, exitCode: 1 })],
|
|
}),
|
|
).toEqual({ kind: "failed", at: finishedAt });
|
|
});
|
|
|
|
it("uses only the latest runtime provision op (operations are most-recent first)", () => {
|
|
const latest = new Date("2026-08-02T00:00:00.000Z");
|
|
const status = resolveRuntimeProvisionStatus({
|
|
runtimeProvisionCommand: "bash ./seed.sh",
|
|
operations: [
|
|
operation({ id: "op-new", status: "failed", finishedAt: latest, exitCode: 1 }),
|
|
operation({ id: "op-old", status: "succeeded" }),
|
|
],
|
|
});
|
|
expect(status).toEqual({ kind: "failed", at: latest });
|
|
});
|
|
});
|