mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 20:21:50 +01:00
perf(recovery): read workspace contract without native launch prompts (#17621)
Project native input to the complete schema, binding, provider, and workspace evidence used by execution-blocker checks. Preserve absent and malformed fields and retain all saved inputs and recovery holds. 149 focused tests and 123 workspace-recovery tests pass. Server typecheck, build, all 55 CI checks, and exact-head Apex 5/5 pass. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
e302f2098f
commit
b65897f9bf
3 files changed
+47
-4
No files matched your search
+6
-3
@@ -68,9 +68,12 @@ update, including an edit outside the projection, defers repair to a fresh sweep
|
||||
|
||||
Execution-blocker reads select process, lifecycle, and task-binding fields rather
|
||||
than full execution rows. Context retains typed task and pre-dispatch evidence;
|
||||
Runner profiles retain the adapter type and complete native execution input and
|
||||
workspace-sync receipts. Full result evidence stays available to the existing
|
||||
retry and workspace checks. Stored context, profiles, and output are unchanged.
|
||||
Runner profiles retain the adapter type, native input schema, binding, provider,
|
||||
workspace-access evidence, and complete workspace-sync receipts. Native task and
|
||||
runtime prompt payloads are not needed for these checks. Missing fields and
|
||||
malformed input values retain their original shape. Full result evidence stays
|
||||
available to the existing retry and workspace checks. Stored context, profiles,
|
||||
and output are unchanged.
|
||||
|
||||
Committed native finalization reads run identity and ownership state before it
|
||||
claims the coordinator. It loads complete saved input only when finalization
|
||||
|
||||
@@ -100,7 +100,9 @@ describe.skipIf(!support.supported)("execution blocker PostgreSQL read projectio
|
||||
});
|
||||
const input = { schema: "paperclip.native-execution-input.v6",
|
||||
binding: { runId: f.run.id, companyId: f.companyId, agentId: f.agentId, issueId: f.issueId },
|
||||
provider: { kind: "grok_bot" }, workspace: { access: "none", ...(hasCwd ? { cwd: null } : {}) } };
|
||||
provider: { kind: "grok_bot" }, workspace: { access: "none", ...(hasCwd ? { cwd: null } : {}) },
|
||||
task: { description: randomBytes(128 * 1024).toString("base64") },
|
||||
runtimeContext: { instructions: randomBytes(128 * 1024).toString("base64") } };
|
||||
await db.update(heartbeatRuns).set({ nativeIssueId: f.issueId,
|
||||
runnerProfileJson: { ...f.run.runnerProfileJson, nativeExecutionInput: input } }).where(eq(heartbeatRuns.id, f.run.id));
|
||||
expect(await compare(f)).toMatchObject({ workspaceRepairRequired: !hasCwd, canRetry: false });
|
||||
@@ -108,6 +110,39 @@ describe.skipIf(!support.supported)("execution blocker PostgreSQL read projectio
|
||||
expect(stored.runnerProfileJson?.nativeExecutionInput).toEqual(input);
|
||||
});
|
||||
|
||||
it.each(["run", "company", "agent", "issue", "provider", "schema", "access", "workspace", "binding", "missing_binding",
|
||||
"null_input", "array_input", "scalar_input", "number_input"])(
|
||||
"does not relax native workspace recovery for invalid input (%s)", async kind => {
|
||||
const f = await fixture({ runtimeMode: "native", driverKind: "grok_bot_mcp" }, {
|
||||
workspaceRestoreFailure: "restore_unsafe_archive",
|
||||
workspaceRestoreRecovery: { schema: "paperclip.workspace-restore-recovery.v1", leaseIds: [] },
|
||||
});
|
||||
const input: Record<string, unknown> = {
|
||||
schema: "paperclip.native-execution-input.v6",
|
||||
binding: { runId: f.run.id, companyId: f.companyId, agentId: f.agentId, issueId: f.issueId },
|
||||
provider: { kind: "grok_bot" }, workspace: { access: "none", cwd: null },
|
||||
task: { description: randomBytes(128 * 1024).toString("base64") },
|
||||
};
|
||||
const binding = input.binding as Record<string, unknown>;
|
||||
if (kind === "run") binding.runId = randomUUID();
|
||||
if (kind === "company") binding.companyId = randomUUID();
|
||||
if (kind === "agent") binding.agentId = randomUUID();
|
||||
if (kind === "issue") binding.issueId = randomUUID();
|
||||
if (kind === "provider") input.provider = { kind: "codex" };
|
||||
if (kind === "schema") input.schema = "unsupported";
|
||||
if (kind === "access") input.workspace = { cwd: null };
|
||||
if (kind === "workspace") input.workspace = "none";
|
||||
if (kind === "binding") input.binding = [];
|
||||
if (kind === "missing_binding") delete input.binding;
|
||||
const selectedInput = kind === "null_input" ? null : kind === "array_input" ? []
|
||||
: kind === "scalar_input" ? "input" : kind === "number_input" ? 42 : input;
|
||||
await db.update(heartbeatRuns).set({ nativeIssueId: f.issueId,
|
||||
runnerProfileJson: { ...f.run.runnerProfileJson, nativeExecutionInput: selectedInput } }).where(eq(heartbeatRuns.id, f.run.id));
|
||||
expect(await compare(f)).toMatchObject({ workspaceRepairRequired: true, canRetry: false });
|
||||
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.run.id)))[0].runnerProfileJson?.nativeExecutionInput).toEqual(selectedInput);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([null, [], "scalar", 42, { issueId: 42, taskId: false }].map(context => ({ context })))("preserves decisions for malformed context $context", async ({ context }) => {
|
||||
const f = await fixture({ contextSnapshot: context as Record<string, unknown> | null });
|
||||
expect(await compare(f)).toMatchObject({ canRetry: true });
|
||||
|
||||
@@ -28,6 +28,11 @@ const blockerProfileColumn = sql<Record<string, unknown> | null>`case
|
||||
when ${heartbeatRuns.runnerProfileJson} is null then null else (
|
||||
select coalesce(jsonb_object_agg(field.key, case field.key
|
||||
when 'adapterDispatch' then jsonb_build_object('adapterType', field.value->'adapterType')
|
||||
when 'nativeExecutionInput' then case when jsonb_typeof(field.value) = 'object' then (
|
||||
select coalesce(jsonb_object_agg(input_field.key, input_field.value), '{}'::jsonb)
|
||||
from jsonb_each(field.value) as input_field(key, value)
|
||||
where input_field.key in ('schema', 'binding', 'provider', 'workspace')
|
||||
) else field.value end
|
||||
else field.value end), '{}'::jsonb)
|
||||
from jsonb_each(case when jsonb_typeof(${heartbeatRuns.runnerProfileJson}) = 'object'
|
||||
then ${heartbeatRuns.runnerProfileJson} else '{}'::jsonb end) as field(key, value)
|
||||
|
||||
Reference in new issue
Block a user