mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-15 01:52:55 +01:00
Poll asynchronous workspace provider preparation (#20865)
## Thinking Path > - Paperclip starts agent runs through a workspace provider when a project uses remote storage. > - The provider must prepare the workspace before the agent starts. > - One long worker call ties the checkout to its transport. > - Some checkouts take longer than the transport allows. > - This change lets an opted-in provider accept work and report its deadline. > - Core waits for completion through short status calls and stops at the provider deadline. ## Linked Issues or Issue Description **What happened?** A workspace provider can lose its reply when preparation takes longer than a single transport request. This stops run bootstrap before agent execution. **Expected behavior** Core should wait for a provider-confirmed workspace through short requests. The provider's deadline must bound the wait. **Steps to reproduce** Use a workspace provider that takes longer to prepare a checkout than its transport can remain open. Start a run that requires a new checkout. ## What Changed - Add an optional workspace preparation method to the plugin SDK. - Validate pending receipts and keep the original binding checks on completed replies. - Poll only during active preparation. Retry a lost status reply within the advertised deadline. - Preserve the original operation for providers that do not offer asynchronous preparation. - Reject deadline offers that exceed the existing lifecycle lock budget. Honor accepted offers without shortening them. - Checkout verification uses the existing synchronous RPC with a 10-second deadline and a 12-second transport timeout. [Core verification PR #20876](https://github.com/paperclipai/paperclip/pull/20876) ## Verification - All 63 workspace SDK and Core integration tests passed with temporary local PostgreSQL. The tests cover lost status replies, deadline expiry, oversized offers, and synchronous verification with an async-capable worker. - All 58 runner SDK input, evidence, and public type tests passed with the regenerated source pins. The local server typecheck passed. ## Risks - This change activates only when the provider advertises the method. Existing providers keep their current behavior. - Polling uses one-second intervals during active preparation. It stops at completion, failure, or the provider deadline. - No database migration is required. ## Model Used - OpenAI GPT-6 Codex with local code execution and 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>
This commit is contained in:
1 parent
e601f9d2fc
commit
5dfbb04b85
11 files changed
+233
-27
No files matched your search
@@ -17,16 +17,17 @@ import {
|
||||
type WorkspaceProviderBinding,
|
||||
} from "@paperclipai/shared";
|
||||
import {
|
||||
parseWorkspaceProviderResult,
|
||||
workspaceProviderOperationSchema,
|
||||
type PluginWorkspaceOperation,
|
||||
type PluginWorkspaceRecoveryPullRequest,
|
||||
type PluginWorkspaceParams,
|
||||
type PluginWorkspaceResult,
|
||||
type PluginFreshWorkspace,
|
||||
} from "@paperclipai/plugin-sdk";
|
||||
import { invokeWorkspaceProvider } from "./plugin-workspace-preparation.js";
|
||||
import { notifyTerminalWorkspaceWork } from "./terminal-workspace-work.js";
|
||||
import { withExecutionWorkspaceLifecycleLock } from "./execution-workspace-lifecycle-lock.js";
|
||||
import { workspaceProviderCallFailure } from "./workspace-provider-failure.js";
|
||||
import { WorkspaceProviderFailureError, workspaceProviderCallFailure } from "./workspace-provider-failure.js";
|
||||
import {
|
||||
assertTaskWorkspaceExecutionAccess,
|
||||
assertTaskWorkspaceSourceProjectAccess,
|
||||
@@ -87,7 +88,7 @@ export async function callWorkspaceProvider(
|
||||
operation.fresh = await stageProviderFreshWorkspace({ db, companyId: workspace.companyId,
|
||||
issueId: workspace.sourceIssueId, ...operation.fresh, existing: operation.fresh });
|
||||
}
|
||||
const params: PluginWorkspaceParams = {
|
||||
const params: Omit<PluginWorkspaceParams, "deadline"> = {
|
||||
pluginId,
|
||||
companyId: workspace.companyId,
|
||||
projectId: workspace.projectId,
|
||||
@@ -102,22 +103,17 @@ export async function callWorkspaceProvider(
|
||||
)
|
||||
.digest("hex")),
|
||||
generation: operationGeneration,
|
||||
deadline: new Date(Date.now() + (verifyExisting ? 10_000 : operation.kind === "finalize" ? 30_000 : 300_000)).toISOString(),
|
||||
runId: input.runId ?? null,
|
||||
agentId: input.agentId ?? null,
|
||||
operation,
|
||||
};
|
||||
let value: unknown;
|
||||
let result: PluginWorkspaceResult;
|
||||
try {
|
||||
value = await workers!.call(
|
||||
pluginId,
|
||||
"workspaceOperation",
|
||||
params,
|
||||
verifyExisting ? 12_000 : operation.kind === "finalize" ? 40_000 : 310_000,
|
||||
);
|
||||
result = await invokeWorkspaceProvider(workers!, params);
|
||||
} catch (error) {
|
||||
// A worker that never received the call, a lost reply and a provider-reported
|
||||
// failure need different recovery; only a closed provider reason crosses over.
|
||||
if (error instanceof WorkspaceProviderFailureError) throw error;
|
||||
throw workspaceProviderCallFailure(error);
|
||||
}
|
||||
// Recheck enablement and ownership after an external effect, including an ambiguous retry.
|
||||
@@ -139,7 +135,6 @@ export async function callWorkspaceProvider(
|
||||
) {
|
||||
throw conflict("Workspace changed during storage preparation");
|
||||
}
|
||||
const result = parseWorkspaceProviderResult(params, value);
|
||||
if (result.kind === "prepared" && result.binding.pluginId !== pluginId)
|
||||
throw conflict("Workspace provider ownership changed");
|
||||
return result;
|
||||
|
||||
Reference in new issue
Block a user