From b65897f9bf05121290895b48e4794c301790ef67 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Sun, 25 Oct 2026 09:53:27 -0700 Subject: [PATCH] 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 --- doc/DATABASE.md | 9 +++-- .../services/execution-blocker-read.test.ts | 37 ++++++++++++++++++- server/src/services/execution-blocker.ts | 5 +++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/doc/DATABASE.md b/doc/DATABASE.md index 4d6622bc784..043493acd8f 100644 --- a/doc/DATABASE.md +++ b/doc/DATABASE.md @@ -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 diff --git a/server/src/services/execution-blocker-read.test.ts b/server/src/services/execution-blocker-read.test.ts index 309385a25cb..5b16c521af1 100644 --- a/server/src/services/execution-blocker-read.test.ts +++ b/server/src/services/execution-blocker-read.test.ts @@ -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 = { + 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; + 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 | null }); expect(await compare(f)).toMatchObject({ canRetry: true }); diff --git a/server/src/services/execution-blocker.ts b/server/src/services/execution-blocker.ts index 4777d5947b3..3ee94aedb72 100644 --- a/server/src/services/execution-blocker.ts +++ b/server/src/services/execution-blocker.ts @@ -28,6 +28,11 @@ const blockerProfileColumn = sql | 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)