fix(runner): retain provider sessions and send only new continuation events

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-18 22:05:48 -05:00
1 parent 4197c9f9c0
commit 3c66c65f9c
14 files changed
+251 -15

No files matched your search

@@ -96,6 +96,18 @@ describe("NativeExecutionInputV1", () => {
schema: "paperclip.native-execution-input.v4",
provider: { kind: "codex", model: null, approvalPolicy: "on-request" },
});
const withDelta = parseNativeExecutionInput({ ...current, continuationPrompt: '{"messages":[{"authorType":"user","body":"Just this new comment"}]}' });
// No checkpoint / failed provider recovery must retain full bootstrap input.
expect(buildNativeModelEnvelope(withDelta)).toEqual(buildNativeModelEnvelope(current));
const delta = buildNativeModelEnvelope(withDelta, { resumedSession: true });
expect(delta).toEqual({
schema: "paperclip.native-continuation.v1",
events: '{"messages":[{"authorType":"user","body":"Just this new comment"}]}',
completion: { revision: "1", criterionIds: ["objective"] },
});
expect(JSON.stringify(delta)).not.toContain(input.task.title);
expect(JSON.stringify(delta)).not.toContain(input.completionContract.contract.objective);
expect(JSON.stringify(delta)).not.toContain("opaque-binding");
expect(current).toMatchObject({
schema: "paperclip.native-execution-input.v4",
provider: { kind: "codex", approvalPolicy: "on-request" },
@@ -194,6 +194,8 @@ export interface NativeExecutionInputV3 extends Omit<NativeExecutionInputV2, "sc
export interface NativeExecutionInputV4 extends Omit<NativeExecutionInputV3, "schema" | "provider"> {
schema: typeof NATIVE_EXECUTION_INPUT_SCHEMA;
provider: NativeProviderConfigV4;
/** Used only after the runtime proves provider-session recovery succeeded. */
continuationPrompt?: string | null;
}
export type NativeExecutionInput = NativeExecutionInputV1 | NativeExecutionInputV2 | NativeExecutionInputV3 | NativeExecutionInputV4;
@@ -290,6 +292,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
"credentialBindings",
...(isV2 ? ["executionMode", "planningContext"] : []),
...(isV3 ? ["runtimeContext"] : []),
...(isV4 ? ["continuationPrompt"] : []),
], "input");
if (!isV2 && input.schema !== NATIVE_EXECUTION_INPUT_SCHEMA_V1) {
throw new NativeExecutionInputError(
@@ -713,11 +716,30 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
return {
...withRuntimeContext,
schema: NATIVE_EXECUTION_INPUT_SCHEMA,
...(input.continuationPrompt !== undefined ? { continuationPrompt: nullableText(input.continuationPrompt, "input.continuationPrompt") } : {}),
provider: parsedProvider as NativeProviderConfigV4,
};
}
export function buildNativeModelEnvelope(input: NativeExecutionInput): NativeModelEnvelopeV1 | NativeModelEnvelopeV2 {
export interface NativeContinuationEnvelope {
schema: "paperclip.native-continuation.v1";
events: string;
completion: { revision: string; criterionIds: string[] };
}
export function buildNativeModelEnvelope(input: NativeExecutionInput, options: { resumedSession: true }): NativeModelEnvelopeV1 | NativeModelEnvelopeV2 | NativeContinuationEnvelope;
export function buildNativeModelEnvelope(input: NativeExecutionInput): NativeModelEnvelopeV1 | NativeModelEnvelopeV2;
export function buildNativeModelEnvelope(input: NativeExecutionInput, options?: { resumedSession: boolean }): NativeModelEnvelopeV1 | NativeModelEnvelopeV2 | NativeContinuationEnvelope {
if (options?.resumedSession && "continuationPrompt" in input && input.continuationPrompt) {
return {
schema: "paperclip.native-continuation.v1",
events: input.continuationPrompt,
completion: {
revision: input.completionContract.contract.revision,
criterionIds: input.completionContract.contract.criteria.map((criterion) => criterion.id),
},
};
}
if (input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V1) {
return {
schema: NATIVE_MODEL_ENVELOPE_SCHEMA_V1,
@@ -1353,6 +1353,20 @@ describe("Codex app-server Codex driver", () => {
},
);
it("passes native continuation events without repeating task wrappers or skill invocation", async () => {
const transport = new FakeCodexTransport();
const session = await makeDriver([transport], {
skillInputs: [{ type: "skill", name: "first-task", path: "/skills/first-task/SKILL.md" }],
}).openSession({ runId: "run-delta", normalizedSessionId: "session-delta", workingDirectory: TEST_WORKING_DIRECTORY });
const text = JSON.stringify({ schema: "paperclip.native-continuation.v1", events: '{"messages":[{"body":"Go ahead"}]}', completion: { revision: "2", criterionIds: ["comment"] } });
await session.startTurn({ message: { role: "user", text } });
const params = transport.calls.find((call) => call.method === "turn/start")!.params;
expect(params.input).toEqual([{ type: "text", text, text_elements: [] }]);
expect(JSON.stringify(params.input)).not.toContain("constraints");
expect(JSON.stringify(params.input)).not.toContain("first-task");
await session.close({ reason: "test complete" });
});
it("allows eval fixtures to opt out of Codex collaboration instructions", async () => {
const transport = new FakeCodexTransport();
const session = await makeDriver([transport], {
@@ -160,10 +160,16 @@ export class CodexHarnessSession
);
}
const dispositionOnlyRecovery = this.dispositionOnlyRecoveryAvailable;
// A native continuation already carries just new events and the current
// completion IDs. Do not wrap it in the prior task objective/constraints
// or re-invoke a skill whose instructions are already in this session.
let continuationTurn = false;
try { continuationTurn = record(JSON.parse(input.message.text)).schema === "paperclip.native-continuation.v1"; } catch { /* Ordinary text. */ }
const turnSkills = continuationTurn ? [] : this.skillInputs;
const taskText =
this.conversationMode === "direct"
? input.message.text
: dispositionOnlyRecovery
: dispositionOnlyRecovery || continuationTurn
? input.message.text
: JSON.stringify({
task: this.taskEnvelope,
@@ -192,7 +198,7 @@ export class CodexHarnessSession
this.emit("turn.submitted", {
envelopeSchema: this.taskEnvelope.schema,
text: input.message.text,
...(this.skillInputs.length ? { skillInputs: this.skillInputs } : {}),
...(turnSkills.length ? { skillInputs: turnSkills } : {}),
requestedCollaborationMode:
input.requestedCollaborationMode ?? effectiveCollaborationMode,
effectiveCollaborationMode,
@@ -219,11 +225,11 @@ export class CodexHarnessSession
input: [
userInput({
role: "user",
text: this.skillInputs.length
? `${this.skillInputs.map((skill) => `$${skill.name}`).join(" ")}\n\n${taskText}`
text: turnSkills.length
? `${turnSkills.map((skill) => `$${skill.name}`).join(" ")}\n\n${taskText}`
: taskText,
}),
...this.skillInputs,
...turnSkills,
],
...(this.conversationMode === "direct"
? {}
@@ -6143,6 +6143,13 @@ describe("executeNativeSession recovery", () => {
});
it("replaces a provider session that already ended with a failed terminal", async () => {
const digest = "0".repeat(64);
const context = {
prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } },
skills: [],
mcp: { assignmentSetId: "none", digest, bindingId: null },
} as const;
const checkpoint: PersistedNativeSession = {
backendKind: "mock",
sessionId: "driver-failed",
@@ -6205,6 +6212,7 @@ describe("executeNativeSession recovery", () => {
kind: "mock",
name: "replacement-backend",
version: "1",
runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" },
capabilities: {
resume: true,
typedEvents: true,
@@ -6243,7 +6251,10 @@ describe("executeNativeSession recovery", () => {
await expect(
executeNativeSession({
input,
input: { ...input, schema: "paperclip.native-execution-input.v4", executionMode: "default", planningContext: null,
provider: { kind: "codex", model: null, approvalPolicy: "never" },
runtimeContext: { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) },
continuationPrompt: "ONLY_NEW_COMMENT" },
backend,
controlPlane: port,
runnerInstanceId: "runner-replacement",
@@ -6258,6 +6269,7 @@ describe("executeNativeSession recovery", () => {
startTurn.mock.calls[0]![0].message.text,
) as { task: { prompt: string } };
expect(replacementEnvelope.task.prompt).toBe(input.task.prompt);
expect(JSON.stringify(replacementEnvelope)).not.toContain("ONLY_NEW_COMMENT");
expect(onContinuityBreak).toHaveBeenCalledWith({
reason: "provider session ended with a failed terminal",
previousDriverSessionId: "driver-failed",
@@ -2329,7 +2329,9 @@ export async function executeNativeSession(
}
await checkpoint();
} else if (shouldStartFreshTurn) {
const modelEnvelope = buildNativeModelEnvelope(input);
let modelEnvelope = recovered
? buildNativeModelEnvelope(input, { resumedSession: true })
: buildNativeModelEnvelope(input);
const dispositionOnlyRecovery = Boolean(
recovered &&
!recoveredSnapshot.semanticResult &&
@@ -2346,6 +2348,7 @@ export async function executeNativeSession(
})
: false;
if (dispositionOnlyRecovery && !effectFreeInitialAcpxTurn) {
modelEnvelope = buildNativeModelEnvelope(input);
modelEnvelope.task.prompt = [
"Paperclip semantic-result recovery for a prior completed provider turn.",
"The prior turn already performed the work and its user-facing final answer is recorded.",
+20 -3
View File
@@ -21642,6 +21642,11 @@ export function heartbeatService(
selectedEnvironmentForConfig?.driver === "sandbox" &&
selectedEnvironmentConfigForFingerprint.reuseLease === true &&
selectedEnvironmentConfigForFingerprint.runnerLifecycleMode === "warm";
// Native provider checkpoints bind to the workspace row, including ordinary
// local shared workspaces. Persist that binding independently of the opt-in
// isolated-workspace UI, just as warm sandbox continuity already does.
const nativeSharedWorkspace = agent.adapterType === "paperclip_runner" &&
requestedExecutionWorkspaceMode === "shared_workspace";
const bindIssueToPersistedExecutionWorkspace = async (
workspace: ExecutionWorkspace | null,
) => {
@@ -21655,7 +21660,7 @@ export function heartbeatService(
issueRef?.executionWorkspacePreference === "reuse_existing" ||
requestedExecutionWorkspaceMode === "isolated_workspace" ||
requestedExecutionWorkspaceMode === "operator_branch" ||
warmReusableExecutionWorkspace;
warmReusableExecutionWorkspace || nativeSharedWorkspace;
const nextIssuePatch: Record<string, unknown> = {};
if (issueExecutionWorkspaceIdForRun !== workspace.id) {
nextIssuePatch.executionWorkspaceId = workspace.id;
@@ -21684,7 +21689,7 @@ export function heartbeatService(
db,
undefined,
undefined,
{ bindRuntimeSharedWorkspace: warmReusableExecutionWorkspace && workspace.mode === "shared_workspace" },
{ bindRuntimeSharedWorkspace: (warmReusableExecutionWorkspace || nativeSharedWorkspace) && workspace.mode === "shared_workspace" },
);
issueExecutionWorkspaceIdForRun = workspace.id;
issueProjectWorkspaceIdForRun =
@@ -23294,7 +23299,7 @@ export function heartbeatService(
taskPrompt: [
nativeReviewRequest ?? readNonEmptyString(
selectPaperclipTaskMarkdown(context, {
resumedSession,
resumedSession: false,
}),
) ??
`# ${issueRef.identifier ?? issueRef.id}: ${issueRef.title}`,
@@ -23304,6 +23309,18 @@ export function heartbeatService(
].filter(Boolean).join("\n\n"),
wakePayload: context.paperclipWake,
resumedSession,
previousTurn: (() => {
if (!previousNativeRun || nativeReviewRequest) return null;
try {
return {
runId: previousNativeRun.id,
task: parseNativeExecutionInput(parseObject(previousNativeRun.runnerProfileJson).nativeExecutionInput).task,
};
} catch {
// An invalid prior snapshot must use the fresh bootstrap.
return null;
}
})(),
conversationMode: context.conversationMode === true,
agentId: agent.id,
workspace: {
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { buildNativeContinuationPrompt } from "./native-continuation.js";
const issue = { title: "Original task", description: "Retain my long task brief." };
const message = { id: "new", body: "Yes, please proceed.", authorType: "user", authorId: "board", sourceTrust: "user", createdByRunId: null };
const wake = {
reason: "issue_commented",
executionContinuation: {
version: 1,
objective: message.body,
messages: [{ ...message, id: "old", body: "OLD_HISTORY" }, message],
resumeDelta: { baseRunId: "prior", messages: [message] },
completedWork: { summary: "OLD_SUMMARY" },
humanResponses: [{ id: "old-question", result: { answer: "OLD_ANSWER" } }],
},
comments: [message], continuationSummary: { markdown: "OLD_SUMMARY" },
};
const build = (value: unknown = wake, previousIssue = issue) => buildNativeContinuationPrompt({ wakePayload: value, previousRunId: "prior", issue, previousIssue });
describe("native continuation event projection", () => {
it("sends a new comment once without the unchanged brief, objective, history or summary", () => {
const text = build()!;
expect(text.split(message.body)).toHaveLength(2);
for (const old of [issue.description, issue.title, "OLD_HISTORY", "OLD_SUMMARY", "OLD_ANSWER"]) expect(text).not.toContain(old);
expect(JSON.parse(text).messages[0]).toMatchObject({ authorType: "user", body: message.body });
expect(text.length).toBeLessThan(600);
});
it("only uses a delta whose base matches the actual previous run", () => {
expect(build({ ...wake, executionContinuation: { ...wake.executionContinuation, resumeDelta: { baseRunId: "different", messages: [message] } } })).toBeNull();
expect(build({ ...wake, executionContinuation: null })).toBeNull();
});
it("delivers the current authenticated answer once, not all prior answers", () => {
const text = build({ ...wake, interactionId: "new-question", executionContinuation: { ...wake.executionContinuation, resumeDelta: { baseRunId: "prior", messages: [] }, humanResponses: [...wake.executionContinuation.humanResponses, { id: "new-question", kind: "ask_user_questions", status: "answered", resolvedByUserId: "board", result: { answer: "NEW_ANSWER" } }] } })!;
expect(text.split("NEW_ANSWER")).toHaveLength(2);
expect(text).not.toContain("OLD_ANSWER");
expect(JSON.parse(text).humanResponses[0].resolvedByUserId).toBe("board");
});
it("includes external child completion and actual brief edits", () => {
const text = build({ ...wake, reason: "issue_children_completed", childIssueSummaries: [{ id: "child", status: "done", summary: "CHILD_RESULT" }] }, { ...issue, description: "old brief" })!;
expect(JSON.parse(text).taskChanges).toEqual({ description: issue.description });
expect(text).toContain("CHILD_RESULT");
});
it.each([{ fallbackFetchNeeded: true }, { recovery: { cause: "interrupted" } }, { externalChatExecutionBound: true }, { reason: "issue_assigned" }])("retains bootstrap framing for special wakes: %j", (extra) => {
expect(build({ ...wake, ...extra })).toBeNull();
});
});
@@ -0,0 +1,43 @@
import { normalizePaperclipWakePayload } from "@paperclipai/adapter-utils/server-utils";
/** Only new, authorized events belong in an already retained provider conversation.
* Full bootstrap input is kept separately for an actual provider resume failure.
*/
export function buildNativeContinuationPrompt(input: {
wakePayload: unknown;
previousRunId: string;
issue: { title: string; description: string | null };
previousIssue: { title: string; description: string | null };
}): string | null {
const wake = normalizePaperclipWakePayload(input.wakePayload);
const continuation = wake?.executionContinuation;
const delta = continuation?.resumeDelta;
if (!wake || !delta || delta.baseRunId !== input.previousRunId) return null;
// These paths have specialized delivery/recovery contracts. Preserve their
// existing framing until they have an event-specific continuation projection.
if (!['issue_commented', 'issue_children_completed'].includes(wake.reason ?? '') ||
wake.fallbackFetchNeeded || wake.truncated || wake.recovery || continuation?.interruptedRunId ||
wake.externalChatExecutionBound || wake.externalChatQuestionResponse ||
wake.taskWatchdog || wake.livenessContinuation || wake.activeTreeHold ||
wake.skillTest || wake.executionStage || wake.agentMessage ||
wake.documentReviewContext || wake.planReviewContext || wake.annotationDeltas.length > 0
) return null;
const taskChanges = Object.fromEntries(
(["title", "description"] as const)
.filter((key) => input.issue[key] !== input.previousIssue[key])
.map((key) => [key, input.issue[key]]),
);
const humanResponses = (continuation?.humanResponses ?? []).filter((answer) => answer.id === wake.interactionId);
// Do not substitute an unverified interaction outcome for an authorized answer.
if (wake.interactionId && humanResponses.length === 0) return null;
const events = {
messages: delta.messages,
...(humanResponses.length ? { humanResponses } : {}),
...(Object.keys(taskChanges).length ? { taskChanges } : {}),
...(wake.childIssueSummaries.length ? { childResults: wake.childIssueSummaries } : {}),
...(wake.childIssueSummaryTruncated ? { childResultsTruncated: true } : {}),
...(wake.unresolvedBlockerIssueIds.length ? { unresolvedBlockerIssueIds: wake.unresolvedBlockerIssueIds } : {}),
};
if (!events.messages.length && !humanResponses.length && !wake.childIssueSummaries.length && !Object.keys(taskChanges).length) return null;
return JSON.stringify(events);
}
@@ -1,3 +1,4 @@
import { buildNativeContinuationPrompt } from "./native-continuation.js";
import type {
NativeAcpxAgent,
NativeAcpxPermissionMode,
@@ -45,6 +46,7 @@ export function buildNativeExecutionInput(input: {
*/
wakePayload?: unknown;
resumedSession?: boolean;
previousTurn?: { runId: string; task: { title: string; description: string | null } } | null;
conversationMode?: boolean;
agentId: string;
workspace: {
@@ -146,7 +148,7 @@ export function buildNativeExecutionInput(input: {
}
: input.wakePayload;
const wakePrompt = renderPaperclipWakePrompt(wakePayload, {
resumedSession: input.resumedSession === true,
resumedSession: false,
conversationMode: input.conversationMode === true,
suppressIssueDescription: input.taskPrompt.trim().length > 0,
nativeWakeReaderAvailable: true,
@@ -168,6 +170,14 @@ export function buildNativeExecutionInput(input: {
.join("\n\n");
return parseNativeExecutionInput({
schema: "paperclip.native-execution-input.v4",
...(input.resumedSession && input.previousTurn && !input.conversationMode ? {
continuationPrompt: buildNativeContinuationPrompt({
wakePayload: input.wakePayload,
previousRunId: input.previousTurn.runId,
previousIssue: input.previousTurn.task,
issue: input.issue,
}),
} : {}),
executionMode,
planningContext: input.planningContext ?? null,
binding: {
@@ -2598,9 +2598,10 @@ describe("buildNativeExecutionInput wake projection", () => {
runtimeContext: nativeRuntimeContextFixture(),
});
expect(input.task.prompt.includes("Execution contract:")).toBe(!conversationMode);
expect(input.task.prompt.includes("Use child issues")).toBe(!conversationMode);
expect(input.task.prompt).toContain("## Paperclip Resume Delta");
expect(input.task.prompt).not.toContain("Execution contract:");
expect(input.task.prompt).not.toContain("Use child issues");
// Full bootstrap stays available if provider recovery fails after admission.
expect(input.task.prompt).toContain("## Paperclip Wake Payload");
expect(input.task.prompt).toContain("reason: issue_children_completed");
expect(input.task.prompt).toContain("DOT-147 Build utility (done)");
expect(input.task.prompt).toContain(
+15
View File
@@ -742,3 +742,18 @@ pnpm test:e2e:runner:browser-support
# To use an installed Chrome instead of Playwright's Chromium:
PAPERCLIP_PLAYWRIGHT_CHANNEL=chrome pnpm test:e2e:runner:browser-support
```
### Native provider continuity
The first-task `task-reply-accept` and `task-card-accept` journeys also verify that
ordinary native follow-ups retain the parent task's workspace, native session,
and provider session identities. A generic `sessionReused` flag is insufficient.
The check excludes child runs and applies only to native profiles.
For ordinary native comment and child-completion wakes, a verified provider resume
receives only new attributed messages, the current authenticated interaction result,
actual task edits, child results, and completion-report identifiers. The provider
retains conversation history. Paperclip retains task state and authorization. A new
or replacement session still receives the full bootstrap; specialized recovery,
review, external-chat and planning paths retain their existing context. Legacy
adapter prompts are unchanged.
+21
View File
@@ -179,6 +179,23 @@ function isPlanningAttachment(a: Row): boolean {
function verifiedFirstTaskOutputs(checkpoint: FirstTaskCheckpoint): Row[] {
return [...checkpoint.documents, ...(checkpoint.attachments ?? []).filter(isVerifiedAttachment).map(attachmentDocument)];
}
/** Provider identities, not generic sessionReused flags, prove continuity. */
export function gradeNativeSessionContinuity(runs: Row[], issueId: string): FirstTaskCheck {
const parent = [...new Map(runs.filter((run) => run.nativeIssueId === issueId).map((run) => [run.id, run])).values()];
const identities = parent.map((run) => ({
run: run.id,
session: run.nativeSessionId,
provider: run.runnerProfileJson?.sessionCheckpoint?.providerSessionId,
workspace: run.runnerProfileJson?.nativeExecutionInput?.binding?.executionWorkspaceId,
}));
const passed = parent.length >= 2 && ["session", "provider", "workspace"].every((key) =>
identities.every((identity) => typeof identity[key as "session"] === "string" && identity[key as "session"].length > 0) &&
new Set(identities.map((identity) => identity[key as "session"])).size === 1,
);
return { id: "native-session-continuity", passed, evidence: ["finished"],
detail: `Same-task follow-ups must retain native/provider/workspace identities: ${JSON.stringify(identities)}` };
}
export function gradeFirstTask(e: FirstTaskEvidence): FirstTaskCheck[] {
const scenario = firstTaskScenario(e.caseId, e.nonce);
const checks: FirstTaskCheck[] = [];
@@ -440,5 +457,9 @@ export function gradeFirstTask(e: FirstTaskEvidence): FirstTaskCheck[] {
"All observed provider runs settled successfully",
[last.id],
);
if (e.runtimeSettings?.adapterType === "paperclip_runner" &&
["task-reply-accept", "task-card-accept"].includes(e.caseId) && last.phase === "finished") {
checks.push({ ...gradeNativeSessionContinuity(e.checkpoints.flatMap((checkpoint) => checkpoint.runs), e.onboardingIssueId), evidence: [last.id] });
}
return checks;
}
+14
View File
@@ -20,6 +20,7 @@ import {
digestText,
snapshotInstruction,
gradeFirstTask,
gradeNativeSessionContinuity,
firstTaskCompletionSettled,
type FirstTaskEvidence,
type FirstTaskCheckpoint,
@@ -1216,3 +1217,16 @@ describe("accept-while-running overlap evidence", () => {
expect(Boolean(check.notReached)).toBe(!overlap);
});
});
describe("native provider session continuity", () => {
const row = (id: string) => ({ id, nativeIssueId: "parent", nativeSessionId: "native", usageJson: { sessionReused: true }, runnerProfileJson: { sessionCheckpoint: { providerSessionId: "provider" }, nativeExecutionInput: { binding: { executionWorkspaceId: "workspace" } } } });
it("accepts stable parent identity, deduplicates checkpoints, and excludes children", () => {
expect(gradeNativeSessionContinuity([row("one"), row("one"), row("two"), { ...row("child"), nativeIssueId: "child", nativeSessionId: "different" }], "parent").passed).toBe(true);
});
it("rejects a fresh provider despite generic sessionReused metadata", () => {
const next = row("two"); next.runnerProfileJson.sessionCheckpoint.providerSessionId = "fresh";
expect(gradeNativeSessionContinuity([row("one"), next], "parent").passed).toBe(false);
expect(gradeNativeSessionContinuity([row("one")], "parent").passed).toBe(false);
});
});