feat(runner): add feature-flagged Paperclip adapter tracer

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-09 03:08:55 +00:00
1 parent 3a38c8f931
commit 37c219b297
72 files changed
+44123 -240

No files matched your search

+1
View File
@@ -43,6 +43,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@paperclipai/shared": "workspace:*",
"acpx": "0.12.0",
"picocolors": "^1.1.1"
},
+3
View File
@@ -5,6 +5,7 @@
import type { SshRemoteExecutionSpec } from "./ssh.js";
import type { AdapterExecutionTarget } from "./execution-target.js";
import type { RuntimeStatusSink } from "./runtime-progress.js";
import type { NativeFinalizationResult } from "@paperclipai/shared";
export interface AdapterAgent {
id: string;
@@ -127,6 +128,8 @@ export interface AdapterExecutionResult {
description?: string;
}>;
} | null;
/** Present only for a persisted native-mode run; legacy adapters omit it. */
nativeFinalization?: NativeFinalizationResult;
}
export interface AdapterSessionCodec {
@@ -0,0 +1,201 @@
CREATE TABLE "completion_contracts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"revision" integer NOT NULL,
"schema_version" text NOT NULL,
"policy_version" text NOT NULL,
"risk" text NOT NULL,
"completion_authority" text NOT NULL,
"incomplete_criteria_policy" text NOT NULL,
"contract_json" jsonb NOT NULL,
"canonical_sha256" text NOT NULL,
"created_by_actor_type" text NOT NULL,
"created_by_actor_id" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"supersedes_contract_id" uuid
);
--> statement-breakpoint
CREATE TABLE "native_run_finalizations" (
"run_id" uuid PRIMARY KEY NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"phase" text NOT NULL,
"attempt" integer DEFAULT 0 NOT NULL,
"lease_owner" text,
"lease_expires_at" timestamp with time zone,
"result_id" uuid,
"assessment_id" uuid,
"decision_id" uuid,
"failure_code" text,
"failure_detail" text,
"next_attempt_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "native_run_results" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"run_id" uuid NOT NULL,
"turn_id" text,
"completion_contract_id" uuid NOT NULL,
"caller_result_id" text,
"caller_dedupe_key" text,
"server_fingerprint" text NOT NULL,
"schema_status" text NOT NULL,
"rejection_code" text,
"result_json" jsonb NOT NULL,
"canonical_sha256" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "status_decision_effects" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"decision_id" uuid NOT NULL,
"ordinal" integer NOT NULL,
"effect_kind" text NOT NULL,
"target_type" text NOT NULL,
"target_id" text,
"idempotency_key" text NOT NULL,
"payload" jsonb NOT NULL,
"delivery_state" text DEFAULT 'pending' NOT NULL,
"attempt_count" integer DEFAULT 0 NOT NULL,
"next_attempt_at" timestamp with time zone,
"last_error" text,
"delivered_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "status_decisions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"assessment_id" uuid NOT NULL,
"decision_version" integer NOT NULL,
"policy_version" text NOT NULL,
"from_status" text NOT NULL,
"to_status" text NOT NULL,
"reason_code" text NOT NULL,
"decision_json" jsonb NOT NULL,
"decision_digest" text NOT NULL,
"application_state" text DEFAULT 'pending' NOT NULL,
"supersedes_decision_id" uuid,
"applied_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "work_assessments" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"run_id" uuid NOT NULL,
"turn_id" text,
"contract_id" uuid NOT NULL,
"result_id" uuid NOT NULL,
"trigger_kind" text NOT NULL,
"trigger_ref" text,
"trigger_capability" text,
"trigger_actor_company_id" uuid NOT NULL,
"prior_issue_status" text NOT NULL,
"prior_status_version" bigint NOT NULL,
"prior_decision_id" uuid,
"policy_version" text NOT NULL,
"assessment_json" jsonb NOT NULL,
"input_digest" text NOT NULL,
"supersedes_assessment_id" uuid,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "heartbeat_run_events" ALTER COLUMN "seq" SET DATA TYPE bigint;--> statement-breakpoint
ALTER TABLE "heartbeat_run_events" ADD COLUMN "source_instance_id" text;--> statement-breakpoint
ALTER TABLE "heartbeat_run_events" ADD COLUMN "source_event_id" text;--> statement-breakpoint
ALTER TABLE "heartbeat_run_events" ADD COLUMN "source_seq" bigint;--> statement-breakpoint
ALTER TABLE "heartbeat_run_events" ADD COLUMN "source_payload_sha256" text;--> statement-breakpoint
ALTER TABLE "heartbeat_run_events" ADD COLUMN "protocol_schema_version" integer;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "runtime_mode" text DEFAULT 'legacy' NOT NULL;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "runtime_mode_resolver_version" text;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "runtime_mode_reason" text;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "runtime_mode_resolved_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "runner_profile_json" jsonb;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "runner_instance_id" uuid;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "native_session_id" uuid;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "driver_kind" text;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "driver_version" text;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "completion_contract_id" uuid;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "completion_contract_sha256" text;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "next_event_seq" bigint DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "native_phase" text;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN "native_phase_updated_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "issues" ADD COLUMN "status_version" bigint DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE "issues" ADD COLUMN "last_status_decision_id" uuid;--> statement-breakpoint
-- Historical event writers computed max(seq) outside their insert transaction. Repair any
-- collisions deterministically before enforcing the native runner's single-writer invariant.
WITH resequenced AS (
SELECT "id", row_number() OVER (PARTITION BY "run_id" ORDER BY "created_at", "id") AS "new_seq"
FROM "heartbeat_run_events"
)
UPDATE "heartbeat_run_events" AS event
SET "seq" = resequenced."new_seq"
FROM resequenced
WHERE event."id" = resequenced."id";--> statement-breakpoint
UPDATE "heartbeat_runs" AS run
SET "next_event_seq" = COALESCE((
SELECT max(event."seq") + 1
FROM "heartbeat_run_events" AS event
WHERE event."run_id" = run."id"
), 1);--> statement-breakpoint
CREATE OR REPLACE FUNCTION paperclip_bump_issue_status_version()
RETURNS trigger AS $$
BEGIN
IF NEW."status" IS DISTINCT FROM OLD."status" THEN
NEW."status_version" := OLD."status_version" + 1;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;--> statement-breakpoint
CREATE TRIGGER paperclip_issue_status_version_trigger
BEFORE UPDATE OF "status" ON "issues"
FOR EACH ROW EXECUTE FUNCTION paperclip_bump_issue_status_version();--> statement-breakpoint
ALTER TABLE "completion_contracts" ADD CONSTRAINT "completion_contracts_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "completion_contracts" ADD CONSTRAINT "completion_contracts_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_result_id_native_run_results_id_fk" FOREIGN KEY ("result_id") REFERENCES "public"."native_run_results"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "native_run_results" ADD CONSTRAINT "native_run_results_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "native_run_results" ADD CONSTRAINT "native_run_results_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "native_run_results" ADD CONSTRAINT "native_run_results_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "native_run_results" ADD CONSTRAINT "native_run_results_completion_contract_id_completion_contracts_id_fk" FOREIGN KEY ("completion_contract_id") REFERENCES "public"."completion_contracts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "status_decision_effects" ADD CONSTRAINT "status_decision_effects_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "status_decision_effects" ADD CONSTRAINT "status_decision_effects_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "status_decision_effects" ADD CONSTRAINT "status_decision_effects_decision_id_status_decisions_id_fk" FOREIGN KEY ("decision_id") REFERENCES "public"."status_decisions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "status_decisions" ADD CONSTRAINT "status_decisions_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "status_decisions" ADD CONSTRAINT "status_decisions_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "status_decisions" ADD CONSTRAINT "status_decisions_assessment_id_work_assessments_id_fk" FOREIGN KEY ("assessment_id") REFERENCES "public"."work_assessments"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_contract_id_completion_contracts_id_fk" FOREIGN KEY ("contract_id") REFERENCES "public"."completion_contracts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_result_id_native_run_results_id_fk" FOREIGN KEY ("result_id") REFERENCES "public"."native_run_results"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_trigger_actor_company_id_companies_id_fk" FOREIGN KEY ("trigger_actor_company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "completion_contracts_issue_revision_uq" ON "completion_contracts" USING btree ("issue_id","revision");--> statement-breakpoint
CREATE UNIQUE INDEX "completion_contracts_issue_hash_uq" ON "completion_contracts" USING btree ("issue_id","canonical_sha256");--> statement-breakpoint
CREATE UNIQUE INDEX "native_run_results_run_fingerprint_uq" ON "native_run_results" USING btree ("run_id","server_fingerprint");--> statement-breakpoint
CREATE UNIQUE INDEX "native_run_results_run_caller_result_uq" ON "native_run_results" USING btree ("run_id","caller_result_id");--> statement-breakpoint
CREATE UNIQUE INDEX "native_run_results_run_caller_dedupe_uq" ON "native_run_results" USING btree ("run_id","caller_dedupe_key");--> statement-breakpoint
CREATE UNIQUE INDEX "status_decision_effects_decision_ordinal_uq" ON "status_decision_effects" USING btree ("decision_id","ordinal");--> statement-breakpoint
CREATE UNIQUE INDEX "status_decision_effects_idempotency_uq" ON "status_decision_effects" USING btree ("idempotency_key");--> statement-breakpoint
CREATE UNIQUE INDEX "status_decisions_assessment_version_uq" ON "status_decisions" USING btree ("assessment_id","decision_version");--> statement-breakpoint
CREATE UNIQUE INDEX "status_decisions_issue_digest_uq" ON "status_decisions" USING btree ("issue_id","decision_digest");--> statement-breakpoint
CREATE UNIQUE INDEX "work_assessments_result_input_uq" ON "work_assessments" USING btree ("result_id","input_digest");--> statement-breakpoint
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: duplicate rows are deterministically resequenced above before this invariant is enabled in the same transaction.
CREATE UNIQUE INDEX "heartbeat_run_events_run_seq_uq" ON "heartbeat_run_events" USING btree ("run_id","seq");--> statement-breakpoint
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: nullable native source IDs mean historical rows need no backfill and the invariant must commit atomically with the new columns.
CREATE UNIQUE INDEX "heartbeat_run_events_run_source_event_uq" ON "heartbeat_run_events" USING btree ("run_id","source_event_id") WHERE "heartbeat_run_events"."source_event_id" is not null;--> statement-breakpoint
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: nullable native source sequence fields mean historical rows need no backfill and the invariant must commit atomically with the new columns.
CREATE UNIQUE INDEX "heartbeat_run_events_run_source_seq_uq" ON "heartbeat_run_events" USING btree ("run_id","source_instance_id","source_seq") WHERE "heartbeat_run_events"."source_instance_id" is not null and "heartbeat_run_events"."source_seq" is not null;--> statement-breakpoint
File diff suppressed because it is too large. Load diff
@@ -1464,6 +1464,13 @@
"when": 1786032087897,
"tag": "0210_heartbeat_context_taskkey_index",
"breakpoints": true
},
{
"idx": 211,
"version": "7",
"when": 1786243779777,
"tag": "0211_famous_guardsmen",
"breakpoints": true
}
]
}
}
@@ -0,0 +1,24 @@
import { pgTable, uuid, text, timestamp, integer, jsonb, uniqueIndex } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
export const completionContracts = pgTable("completion_contracts", {
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
issueId: uuid("issue_id").notNull().references(() => issues.id),
revision: integer("revision").notNull(),
schemaVersion: text("schema_version").notNull(),
policyVersion: text("policy_version").notNull(),
risk: text("risk").notNull(),
completionAuthority: text("completion_authority").notNull(),
incompleteCriteriaPolicy: text("incomplete_criteria_policy").notNull(),
contractJson: jsonb("contract_json").$type<Record<string, unknown>>().notNull(),
canonicalSha256: text("canonical_sha256").notNull(),
createdByActorType: text("created_by_actor_type").notNull(),
createdByActorId: text("created_by_actor_id").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
supersedesContractId: uuid("supersedes_contract_id"),
}, (table) => ({
issueRevisionUq: uniqueIndex("completion_contracts_issue_revision_uq").on(table.issueId, table.revision),
issueHashUq: uniqueIndex("completion_contracts_issue_hash_uq").on(table.issueId, table.canonicalSha256),
}));
+15 -3
View File
@@ -1,4 +1,5 @@
import { pgTable, uuid, text, timestamp, integer, jsonb, index, bigserial } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
import { pgTable, uuid, text, timestamp, integer, jsonb, index, bigserial, bigint, uniqueIndex } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { agents } from "./agents.js";
import { heartbeatRuns } from "./heartbeat_runs.js";
@@ -10,19 +11,30 @@ export const heartbeatRunEvents = pgTable(
companyId: uuid("company_id").notNull().references(() => companies.id),
runId: uuid("run_id").notNull().references(() => heartbeatRuns.id),
agentId: uuid("agent_id").notNull().references(() => agents.id),
seq: integer("seq").notNull(),
seq: bigint("seq", { mode: "number" }).notNull(),
eventType: text("event_type").notNull(),
stream: text("stream"),
level: text("level"),
color: text("color"),
message: text("message"),
payload: jsonb("payload").$type<Record<string, unknown>>(),
sourceInstanceId: text("source_instance_id"),
sourceEventId: text("source_event_id"),
sourceSeq: bigint("source_seq", { mode: "number" }),
sourcePayloadSha256: text("source_payload_sha256"),
protocolSchemaVersion: integer("protocol_schema_version"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
runSeqIdx: index("heartbeat_run_events_run_seq_idx").on(table.runId, table.seq),
runSeqUq: uniqueIndex("heartbeat_run_events_run_seq_uq").on(table.runId, table.seq),
runSourceEventUq: uniqueIndex("heartbeat_run_events_run_source_event_uq")
.on(table.runId, table.sourceEventId)
.where(sql`${table.sourceEventId} is not null`),
runSourceSeqUq: uniqueIndex("heartbeat_run_events_run_source_seq_uq")
.on(table.runId, table.sourceInstanceId, table.sourceSeq)
.where(sql`${table.sourceInstanceId} is not null and ${table.sourceSeq} is not null`),
companyRunIdx: index("heartbeat_run_events_company_run_idx").on(table.companyId, table.runId),
companyCreatedIdx: index("heartbeat_run_events_company_created_idx").on(table.companyId, table.createdAt),
}),
);
+14
View File
@@ -22,6 +22,20 @@ export const heartbeatRuns = pgTable(
signal: text("signal"),
usageJson: jsonb("usage_json").$type<Record<string, unknown>>(),
resultJson: jsonb("result_json").$type<Record<string, unknown>>(),
runtimeMode: text("runtime_mode").notNull().default("legacy"),
runtimeModeResolverVersion: text("runtime_mode_resolver_version"),
runtimeModeReason: text("runtime_mode_reason"),
runtimeModeResolvedAt: timestamp("runtime_mode_resolved_at", { withTimezone: true }),
runnerProfileJson: jsonb("runner_profile_json").$type<Record<string, unknown>>(),
runnerInstanceId: uuid("runner_instance_id"),
nativeSessionId: uuid("native_session_id"),
driverKind: text("driver_kind"),
driverVersion: text("driver_version"),
completionContractId: uuid("completion_contract_id"),
completionContractSha256: text("completion_contract_sha256"),
nextEventSeq: bigint("next_event_seq", { mode: "number" }).notNull().default(1),
nativePhase: text("native_phase"),
nativePhaseUpdatedAt: timestamp("native_phase_updated_at", { withTimezone: true }),
sessionIdBefore: text("session_id_before"),
sessionIdAfter: text("session_id_after"),
logStore: text("log_store"),
+6
View File
@@ -107,6 +107,12 @@ export { documentAnnotationComments } from "./document_annotation_comments.js";
export { documentAnnotationAnchorSnapshots } from "./document_annotation_anchor_snapshots.js";
export { heartbeatRuns } from "./heartbeat_runs.js";
export { heartbeatRunEvents } from "./heartbeat_run_events.js";
export { completionContracts } from "./completion_contracts.js";
export { nativeRunResults } from "./native_run_results.js";
export { nativeRunFinalizations } from "./native_run_finalizations.js";
export { workAssessments } from "./work_assessments.js";
export { statusDecisions } from "./status_decisions.js";
export { statusDecisionEffects } from "./status_decision_effects.js";
export { heartbeatRunWatchdogDecisions } from "./heartbeat_run_watchdog_decisions.js";
export { smokeRuns, smokeRunSteps } from "./smoke_lab.js";
export { costEvents } from "./cost_events.js";
+3
View File
@@ -9,6 +9,7 @@ import {
jsonb,
index,
uniqueIndex,
bigint,
} from "drizzle-orm/pg-core";
import { agents } from "./agents.js";
import { projects } from "./projects.js";
@@ -31,6 +32,8 @@ export const issues = pgTable(
title: text("title").notNull(),
description: text("description"),
status: text("status").notNull().default("backlog"),
statusVersion: bigint("status_version", { mode: "number" }).notNull().default(0),
lastStatusDecisionId: uuid("last_status_decision_id"),
workMode: text("work_mode").notNull().default("standard"),
harnessKind: text("harness_kind"),
priority: text("priority").notNull().default("medium"),
@@ -0,0 +1,23 @@
import { pgTable, uuid, text, timestamp, integer } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
import { heartbeatRuns } from "./heartbeat_runs.js";
import { nativeRunResults } from "./native_run_results.js";
export const nativeRunFinalizations = pgTable("native_run_finalizations", {
runId: uuid("run_id").primaryKey().references(() => heartbeatRuns.id),
companyId: uuid("company_id").notNull().references(() => companies.id),
issueId: uuid("issue_id").notNull().references(() => issues.id),
phase: text("phase").notNull(),
attempt: integer("attempt").notNull().default(0),
leaseOwner: text("lease_owner"),
leaseExpiresAt: timestamp("lease_expires_at", { withTimezone: true }),
resultId: uuid("result_id").references(() => nativeRunResults.id),
assessmentId: uuid("assessment_id"),
decisionId: uuid("decision_id"),
failureCode: text("failure_code"),
failureDetail: text("failure_detail"),
nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
@@ -0,0 +1,26 @@
import { pgTable, uuid, text, timestamp, jsonb, uniqueIndex } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
import { heartbeatRuns } from "./heartbeat_runs.js";
import { completionContracts } from "./completion_contracts.js";
export const nativeRunResults = pgTable("native_run_results", {
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
issueId: uuid("issue_id").notNull().references(() => issues.id),
runId: uuid("run_id").notNull().references(() => heartbeatRuns.id),
turnId: text("turn_id"),
completionContractId: uuid("completion_contract_id").notNull().references(() => completionContracts.id),
callerResultId: text("caller_result_id"),
callerDedupeKey: text("caller_dedupe_key"),
serverFingerprint: text("server_fingerprint").notNull(),
schemaStatus: text("schema_status").notNull(),
rejectionCode: text("rejection_code"),
resultJson: jsonb("result_json").$type<Record<string, unknown>>().notNull(),
canonicalSha256: text("canonical_sha256").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
runFingerprintUq: uniqueIndex("native_run_results_run_fingerprint_uq").on(table.runId, table.serverFingerprint),
runCallerResultUq: uniqueIndex("native_run_results_run_caller_result_uq").on(table.runId, table.callerResultId),
runCallerDedupeUq: uniqueIndex("native_run_results_run_caller_dedupe_uq").on(table.runId, table.callerDedupeKey),
}));
@@ -0,0 +1,27 @@
import { pgTable, uuid, text, timestamp, integer, jsonb, uniqueIndex } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
import { statusDecisions } from "./status_decisions.js";
export const statusDecisionEffects = pgTable("status_decision_effects", {
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
issueId: uuid("issue_id").notNull().references(() => issues.id),
decisionId: uuid("decision_id").notNull().references(() => statusDecisions.id),
ordinal: integer("ordinal").notNull(),
effectKind: text("effect_kind").notNull(),
targetType: text("target_type").notNull(),
targetId: text("target_id"),
idempotencyKey: text("idempotency_key").notNull(),
payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
deliveryState: text("delivery_state").notNull().default("pending"),
attemptCount: integer("attempt_count").notNull().default(0),
nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }),
lastError: text("last_error"),
deliveredAt: timestamp("delivered_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
decisionOrdinalUq: uniqueIndex("status_decision_effects_decision_ordinal_uq").on(table.decisionId, table.ordinal),
idempotencyUq: uniqueIndex("status_decision_effects_idempotency_uq").on(table.idempotencyKey),
}));
@@ -0,0 +1,25 @@
import { pgTable, uuid, text, timestamp, integer, jsonb, uniqueIndex } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
import { workAssessments } from "./work_assessments.js";
export const statusDecisions = pgTable("status_decisions", {
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
issueId: uuid("issue_id").notNull().references(() => issues.id),
assessmentId: uuid("assessment_id").notNull().references(() => workAssessments.id),
decisionVersion: integer("decision_version").notNull(),
policyVersion: text("policy_version").notNull(),
fromStatus: text("from_status").notNull(),
toStatus: text("to_status").notNull(),
reasonCode: text("reason_code").notNull(),
decisionJson: jsonb("decision_json").$type<Record<string, unknown>>().notNull(),
decisionDigest: text("decision_digest").notNull(),
applicationState: text("application_state").notNull().default("pending"),
supersedesDecisionId: uuid("supersedes_decision_id"),
appliedAt: timestamp("applied_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
assessmentVersionUq: uniqueIndex("status_decisions_assessment_version_uq").on(table.assessmentId, table.decisionVersion),
issueDigestUq: uniqueIndex("status_decisions_issue_digest_uq").on(table.issueId, table.decisionDigest),
}));
@@ -0,0 +1,30 @@
import { pgTable, uuid, text, timestamp, bigint, jsonb, uniqueIndex } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
import { heartbeatRuns } from "./heartbeat_runs.js";
import { completionContracts } from "./completion_contracts.js";
import { nativeRunResults } from "./native_run_results.js";
export const workAssessments = pgTable("work_assessments", {
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
issueId: uuid("issue_id").notNull().references(() => issues.id),
runId: uuid("run_id").notNull().references(() => heartbeatRuns.id),
turnId: text("turn_id"),
contractId: uuid("contract_id").notNull().references(() => completionContracts.id),
resultId: uuid("result_id").notNull().references(() => nativeRunResults.id),
triggerKind: text("trigger_kind").notNull(),
triggerRef: text("trigger_ref"),
triggerCapability: text("trigger_capability"),
triggerActorCompanyId: uuid("trigger_actor_company_id").notNull().references(() => companies.id),
priorIssueStatus: text("prior_issue_status").notNull(),
priorStatusVersion: bigint("prior_status_version", { mode: "number" }).notNull(),
priorDecisionId: uuid("prior_decision_id"),
policyVersion: text("policy_version").notNull(),
assessmentJson: jsonb("assessment_json").$type<Record<string, unknown>>().notNull(),
inputDigest: text("input_digest").notNull(),
supersedesAssessmentId: uuid("supersedes_assessment_id"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
resultInputUq: uniqueIndex("work_assessments_result_input_uq").on(table.resultId, table.inputDigest),
}));
+6
View File
@@ -24,6 +24,9 @@ replay, and a redacted protocol inspector. Phase 5 extracts that accepted
transport, reducer projection, hook, components, styles, and reference console
into versioned public subpaths, then proves the surface with a second minimal
consumer.
Phase 6 adds a default-off Paperclip adapter at the public control-plane/session
boundary, durable PRP replay and result finalization, and a kill switch that
keeps the legacy adapter path as the default.
## Phase 0–5 quick start
@@ -73,6 +76,9 @@ pnpm --filter @paperclipai/paperclip-runner browser:dev --host 127.0.0.1 --port
# Phase 5: open the public-SDK reference console and mini consumer.
pnpm --filter @paperclipai/paperclip-runner console:phase5
# Phase 6: prove the mock-side integration contract.
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- --target mock --scenario happy-path
```
Phase 4b provider-backed routes are loopback-only and reject wildcard/LAN
+4 -4
View File
@@ -13,7 +13,7 @@ Each tutorial starts with an explanation of what the phase is and what the phase
- [Phase 4b: run the protocol demo server](tutorials/phase-04b-protocol-server.md) — exercise the server-only Codex boundary, canonical replay, typed controls, and reconnect with `curl`.
- [Phase 4b: run the live Codex protocol console](tutorials/phase-04b-live-console.md) — chat with a live session in the browser, steer it, stop it three ways, answer its requests, change its goal, break its connection, and replay the record.
- [Phase 5: run the SDK console and mini consumer](tutorials/phase-05-sdk-console.md) — exercise the versioned public browser/React surface in two independent consumers against fake and real drivers.
- [Phase 6: thin Paperclip adapter tutorial outline](tutorials/phase-06-thin-paperclip-adapter.md) — planned mock/real conformance, canonical sequencing, durable mode/result recovery, Section 18 arbitration, credential isolation, kill switch, and legacy fallback proof. Design only until CTO approval.
- [Phase 6: run the thin Paperclip adapter](tutorials/phase-06-thin-paperclip-adapter.md) — prove mock/real conformance, run one feature-flagged local task, inspect replay/finalization, disable the kill switch, and verify legacy fallback.
- [Cumulative end-to-end tutorial](tutorials/end-to-end.md) — the shortest complete workflow available at the current phase.
## Reference
@@ -26,6 +26,7 @@ Each tutorial starts with an explanation of what the phase is and what the phase
- [Phase 4b protocol and demo server](phase-04b-protocol-server.md)
- [Phase 4b live console](phase-04b-live-console.md)
- [Phase 5 browser SDK and reference console](phase-05-sdk.md)
- [Phase 6 thin Paperclip adapter](phase-06-thin-paperclip-adapter.md)
- [Engineering journal guide](journal.md)
- [Dated shadcn/ui and AI Elements compatibility note](research/2026-08-07-ui-library-compatibility.md)
- [Phase 4b live-console interaction map](design/phase-4b-interaction-map.md)
@@ -35,6 +36,5 @@ Each tutorial starts with an explanation of what the phase is and what the phase
- [Phase 6 thin Paperclip adapter boundary](design/phase-6-thin-paperclip-adapter.md)
- [Package README](../README.md)
Phase 5 is implemented under this standalone package boundary. Phase 6 has a
design-only, default-off integration proposal awaiting CTO approval. Production
Paperclip UI integration remains deferred.
Phase 6 is implemented as a default-off server integration at the public
package boundary. Production Paperclip UI integration remains deferred.
@@ -0,0 +1,72 @@
# Phase 6 Thin Paperclip Adapter
Phase 6 adds one production integration seam without moving runner behavior
into Paperclip core. Paperclip owns workspace lifecycle, company/auth scope,
budgets, approvals, audit, cancellation, durable persistence, and issue status.
The package owns normalized sessions, provider-driver construction, PRP event
production, and semantic result production.
## Selection and rollback
Native mode requires both `experimental.enableNativeRunner=true` and an
eligible agent profile:
```json
{
"nativeRunner": {
"mode": "native",
"backend": "codex_app_server",
"protocolVersion": 1
}
}
```
Only active local `codex_local` agents running standard, workspace-backed
issues are eligible. The default and the kill switch are legacy. An explicit
but ineligible native profile fails closed; there is no same-run fallback.
Selection, resolver version/reason, profile, runner instance, and completion
contract are persisted before provider execution.
## Public boundary
`PaperclipControlPlanePort` implements the package's `ControlPlanePort`:
- `openRun` verifies company/run/issue/agent/contract binding;
- `appendEvent` validates PRP, commits through the shared per-run allocator,
deduplicates source identity by canonical digest, and acknowledges afterward;
- `replayEvents` uses an exclusive source cursor;
- `completeRun` validates and idempotently persists the immutable structured
result plus terminal fact.
The package-owned `HarnessDriverBackend` is the approved
`NativeSessionBackend`. Core passes only the closed `NativeExecutionInputV1`.
The model receives the smaller `NativeModelEnvelopeV1`; bindings and credential
references do not cross that boundary.
## Persistence and authority
Migration 0211 adds immutable completion contracts/results/assessments,
restart-safe finalization coordinators, status decisions/effects, issue status
versions, native run metadata, and native source identity on heartbeat events.
All event writers allocate from the row-locked `heartbeat_runs.next_event_seq`.
Finalization runs only after result persistence and workspace finalization. The
pure arbiter marks `done` only when the terminal state succeeded, the objective
and every criterion are satisfied, verification passed, and no blocking work
remains. It marks `blocked` only for a first-class reported blocker. A failed
workspace barrier preserves the immutable native result, fails the run, and
leaves issue status unchanged. Review/yield/incomplete/failure/cancellation
remain non-terminal.
Status projection is a versioned CAS and emits an audit record.
## Recovery and safety
The finalization reconciler selects persisted `runtime_mode=native` rows; it
does not consult the current flag. Cancellation uses a run-scoped normalized
session handle before the existing process cleanup. Native execution does not
construct a Paperclip JWT, managed MCP access, legacy context, or raw adapter
environment.
See the [runnable tutorial](tutorials/phase-06-thin-paperclip-adapter.md), the
[verification record](../knowledge/evidence/2026-08-09-phase-06-verification.md),
and the [approved design](design/phase-6-thin-paperclip-adapter.md).
@@ -1,178 +1,196 @@
# Phase 6: Thin Paperclip Adapter Tutorial Outline
# Phase 6: Run the Thin Paperclip Adapter
Status: design only; commands marked as Phase 6 commands are not implemented yet.
Phase 6 connects one local, issue-bound `codex_local` task to the runner's
public `ControlPlanePort`/`NativeSessionBackend` contract. Selection is
default-off, explicit per agent, and persisted before provider execution. The
runner never receives a Paperclip API key, local JWT, managed MCP token, raw
environment, wake payload, or skill instructions.
## What this phase is
## Prerequisites
Phase 6 connects one Paperclip task to the native runner package. The connection
is behind a flag. Paperclip still prepares and finalizes the workspace. The
runner package still owns session and protocol behavior.
- a local Paperclip instance built from this branch;
- a board/operator API key in `PAPERCLIP_API_KEY` (never print it);
- `PAPERCLIP_API_URL`, `PAPERCLIP_AGENT_ID`, and `PAPERCLIP_TASK_ID`;
- an active `codex_local` agent, a standard issue assigned to it, and a
realized local execution workspace;
- Codex already authenticated on the host.
## What the runnable proof will establish
Normalize the API base once:
The proof will run the same contract against the mock core and a real local
Paperclip service. It will store and replay native events through one canonical
sequence allocator, preserve the typed terminal result, and apply the complete
Section 18 server-owned finalization/arbitration flow. It will then turn the
flag off, prove persisted native recovery still completes as native, and prove
that a new task uses the old adapter path.
```sh
PAPERCLIP_API_BASE="${PAPERCLIP_API_URL%/}"
PAPERCLIP_API_BASE="${PAPERCLIP_API_BASE%/api}"
PHASE6_SCRATCH="${PAPERCLIP_RUN_SCRATCH_DIR:-$(mktemp -d)}"
```
## Planned prerequisites
- an isolated local Paperclip development instance;
- Node.js 20+, pnpm 9+, Rust, and Codex already authenticated;
- a test company, `codex_local` test agent, project workspace, and test issue;
- board/operator authorization for the flag and agent-profile changes;
- `PAPERCLIP_API_URL`, `PAPERCLIP_API_KEY`, `PAPERCLIP_COMPANY_ID`,
`PAPERCLIP_AGENT_ID`, `PAPERCLIP_TASK_ID`, and `PAPERCLIP_RUN_ID` set without
printing their values.
## Planned procedure
### 1. Prove the package contract with the mock core
## 1. Prove the mock package contract
```sh
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/conformance/control-plane-port.test.ts \
src/backends/harness-driver-backend.test.ts
pnpm check:runner-phase5-spec
src/backends/harness-driver-backend.test.ts \
src/contracts/native-execution.test.ts
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- \
--target mock --scenario happy-path
```
Expected: stable identities, one semantic terminal result, contiguous source
sequence, replay parity, and no Paperclip server import.
Expected: five targeted tests pass and the trace reports `resolvedMode` as
`native`, three replay-stable events, a successful workspace barrier, and one
server-owned `done` decision.
### 2. Run the focused Paperclip integration tests
## 2. Prove the database-backed Paperclip port
```sh
pnpm --filter @paperclipai/paperclip-runner build:typescript
pnpm --filter @paperclipai/server exec vitest run \
src/__tests__/native-runner-phase6.integration.test.ts \
src/__tests__/heartbeat-native-runner-selection.test.ts \
src/__tests__/heartbeat-native-runner-cancellation.test.ts \
src/__tests__/heartbeat-run-event-sequencing.test.ts \
src/__tests__/native-runner-input-boundary.test.ts \
src/__tests__/native-run-finalizer.test.ts \
src/__tests__/native-status-arbiter-corpus.test.ts \
src/__tests__/native-finalization-recovery.test.ts \
src/__tests__/native-finalization-migration.test.ts \
src/__tests__/legacy-finalization-regression.test.ts
src/services/native-runtime/paperclip-control-plane-port.test.ts \
src/services/native-runtime/runtime-mode.test.ts \
src/services/native-runtime/status-arbiter.test.ts
```
Expected: company/auth denials, workspace/finalization ordering, cancellation,
budget, audit, canonical event sequencing, terminal replay, typed input
isolation, Section 18 arbitration, and legacy regression pass.
This runs the unchanged package conformance suite against the real Paperclip
port. It also checks company binding, duplicate/gap replay, immutable result
ingestion, workspace-gated finalization, status-version CAS, and audit output.
### 3. Enable one isolated agent
## 3. Enable one isolated agent
Use the existing board-authorized instance-settings and agent-update APIs to
set `experimental.enableNativeRunner=true` and the test agent's
`runtimeConfig.nativeRunner.mode="native"`. The implemented tutorial will give
copyable API requests for the exact local auth mode. Do not give the runner or
harness a Paperclip API key.
### 4. Run one local native task
Save the agent's current runtime profile, merge the native profile, then enable
the instance flag. These endpoints require board/operator authority.
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- \
--target paperclip --scenario happy-path
```
Expected stable summary fields:
```text
resolvedMode=native
runStatus=succeeded
nativeEventCount>0
highestContiguousSourceSeq=nativeEventCount
workspaceFinalizeStatus=succeeded
legacyAdapterInvocationCount=0
reportedWorkDisposition=done
authoritativeDecision=done
issueStatusBefore=in_progress
issueStatusAfter=done
statusVersionAfter=statusVersionBefore+1
```
The separate claim and decision fields prove that the runner report did not
seize issue-status authority; the server-owned arbiter applied the transition
only after the completion contract, evidence, workspace barrier, and legal
liveness effects passed.
### 5. Inspect persistence, replay, and finalization
```sh
PAPERCLIP_API_BASE="${PAPERCLIP_API_URL%/}"
PAPERCLIP_API_BASE="${PAPERCLIP_API_BASE%/api}"
curl -fsS \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_BASE/api/heartbeat-runs/$PAPERCLIP_RUN_ID/events?after=0&limit=200" \
| jq '[.[] | select(.sourceEventId != null)] | {count: length, events: map({sourceSeq, sourceEventId, eventType})}'
"$PAPERCLIP_API_BASE/api/agents/$PAPERCLIP_AGENT_ID" \
| jq '.runtimeConfig' > "$PHASE6_SCRATCH/phase6-runtime-before.json"
jq -n \
--slurpfile current "$PHASE6_SCRATCH/phase6-runtime-before.json" \
'{runtimeConfig: ($current[0] * {nativeRunner: {mode: "native", backend: "codex_app_server", protocolVersion: 1}})}' \
| curl -fsS -X PATCH \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- \
"$PAPERCLIP_API_BASE/api/agents/$PAPERCLIP_AGENT_ID" >/dev/null
curl -fsS -X PATCH \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enableNativeRunner":true}' \
"$PAPERCLIP_API_BASE/api/instance/settings/experimental" >/dev/null
```
Repeat the read with an `after` cursor and inspect the run finalization and
issue status-decision read models. Expected: no duplicate semantic event,
stable IDs, unique canonical `seq`, increasing source sequence, a persisted
`native_run_results` digest, a committed coordinator/decision, and a workspace-
finalize barrier before assessment or clean run success.
### 6. Prove cancellation
Run the deterministic cancellation scenario through the focused server test.
Expected: one native cancel effect, one terminal run outcome, only the
server-authorized cancellation/resume scope from Section 18, and the normal
environment/runtime/scratch cleanup path. The focused concurrency case also
appends lifecycle, cancellation, native, and log events at once and proves one
unique replay-stable `(run_id, seq)` order.
### 7. Prove credential and recovery boundaries
Run the typed-input boundary test with unique canaries in the local agent JWT,
Paperclip API key, managed MCP gateway credentials, wake payload, skill
instructions, raw env, and legacy context. Expected: no canary key/value in the
package launch input, model request, event, terminal result, log, or digest.
Then persist a native terminal result, simulate process/server disconnect, and
disable the flag before reconciliation. Expected: recovery reads the persisted
`heartbeat_runs.runtime_mode`, `native_run_finalizations.result_id`, and
`native_run_results` row and completes without re-running selection or invoking
the legacy adapter.
### 8. Disable the kill switch and prove fallback
Set `experimental.enableNativeRunner=false` through the existing board-
authorized settings API, then run a fresh task:
## 4. Run one local native Paperclip task
```sh
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- \
--target paperclip --scenario legacy-fallback
PAPERCLIP_NATIVE_RUN_ID="$({
curl -fsS -X POST \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"source\":\"on_demand\",\"triggerDetail\":\"manual\",\"payload\":{\"issueId\":\"$PAPERCLIP_TASK_ID\"},\"forceFreshSession\":true}" \
"$PAPERCLIP_API_BASE/api/agents/$PAPERCLIP_AGENT_ID/wakeup"
} | jq -r '.id')"
pnpm --filter @paperclipai/server exec vitest run \
src/__tests__/native-runner-phase6.integration.test.ts \
-t "uses the unchanged legacy path when the kill switch is off"
while :; do
PAPERCLIP_NATIVE_STATUS="$({
curl -fsS \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_BASE/api/heartbeat-runs/$PAPERCLIP_NATIVE_RUN_ID"
} | jq -r '.status')"
case "$PAPERCLIP_NATIVE_STATUS" in
succeeded|failed|cancelled|timed_out) break ;;
esac
sleep 2
done
test "$PAPERCLIP_NATIVE_STATUS" = succeeded
```
Expected:
The expected run has `runtimeMode=native`, a persisted completion contract and
runner instance, no legacy adapter invocation, and a typed terminal fact.
```text
resolvedMode=legacy
nativeEventCount=0
legacyAdapterInvocationCount=1
## 5. Inspect replay and finalization
```sh
curl -fsS \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_BASE/api/heartbeat-runs/$PAPERCLIP_NATIVE_RUN_ID/events?afterSeq=0&limit=200" \
| jq '[.[] | select(.payload.prpEvent != null)] | {count: length, events: map({seq, sourceSeq, sourceEventId, eventType})}'
curl -fsS \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_BASE/api/heartbeat-runs/$PAPERCLIP_NATIVE_RUN_ID" \
| jq '{runtimeMode, nativePhase, runnerInstanceId, completionContractId, resultJson}'
curl -fsS \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_BASE/api/issues/$PAPERCLIP_TASK_ID" \
| jq '{status, statusVersion, lastStatusDecisionId, unblockDescriptor}'
```
### 9. Record evidence and clean up
Repeat the events request with `afterSeq=<seq>`. Replay is exclusive and stable;
native result/finalization rows remain authoritative even if the flag changes.
The implementation record will save only redacted summaries and test results
under `knowledge/evidence/`. It will restore the test agent profile and leave
the global flag off. It will not delete a shared database or workspace.
## 6. Exercise cancellation and budget hard-stop
Start a deliberately long native task, then use the existing cancellation API:
```sh
curl -fsS -X POST \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{}' \
"$PAPERCLIP_API_BASE/api/heartbeat-runs/$PAPERCLIP_NATIVE_RUN_ID/cancel" >/dev/null
```
The same run-scoped native cancel handle is invoked by explicit cancellation,
agent pause, and budget-pause enforcement. Resource, environment, scratch, and
workspace cleanup remain owned by the existing heartbeat path.
## 7. Disable the flag and prove legacy fallback
Set `PAPERCLIP_LEGACY_TASK_ID` to a fresh eligible issue assigned to the same
agent; the native proof issue may already be terminal.
```sh
curl -fsS -X PATCH \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enableNativeRunner":false}' \
"$PAPERCLIP_API_BASE/api/instance/settings/experimental" >/dev/null
PAPERCLIP_LEGACY_RUN_ID="$({
curl -fsS -X POST \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"source\":\"on_demand\",\"triggerDetail\":\"manual\",\"payload\":{\"issueId\":\"$PAPERCLIP_LEGACY_TASK_ID\"},\"forceFreshSession\":true}" \
"$PAPERCLIP_API_BASE/api/agents/$PAPERCLIP_AGENT_ID/wakeup"
} | jq -r '.id')"
curl -fsS \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_BASE/api/heartbeat-runs/$PAPERCLIP_LEGACY_RUN_ID" \
| jq '{runtimeMode, runtimeModeReason}'
```
Expected: the new run reports `legacy` and `instance_flag_disabled`; an already
selected native run still reconciles from its persisted native coordinator and
never falls through to the legacy adapter.
Restore the saved profile after the proof:
```sh
jq -n \
--slurpfile saved "$PHASE6_SCRATCH/phase6-runtime-before.json" \
'{runtimeConfig: $saved[0]}' \
| curl -fsS -X PATCH \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- \
"$PAPERCLIP_API_BASE/api/agents/$PAPERCLIP_AGENT_ID" >/dev/null
```
## Stop conditions
Stop and fail the proof if mode changes during a run, a native failure invokes
the legacy adapter, a forbidden legacy-context field or credential reaches the
package/model, a workspace-finalization failure reports success, a PRP claim
bypasses the arbiter, a non-terminal status lacks its atomic liveness path, a
concurrent writer duplicates canonical `seq`, or replay changes canonical
event/result/effect bytes.
Stop if a native failure invokes legacy, credentials appear in model input or
stored PRP bytes, a workspace-finalization failure reports success, replay
changes bytes, or a model-reported disposition bypasses the server arbiter.
@@ -0,0 +1,126 @@
---
type: Verification Evidence
title: Phase 6 thin Paperclip adapter verification
description: Targeted package, database-port, finalization, security-boundary, feature-selection, and legacy fallback evidence.
tags: [native-runner, phase-6, verification, paperclip, replay, finalization]
status: stable
generated: { by: openai/gpt-5.6, at: 2026-08-09T03:01:00Z }
---
# Scope
This record verifies the default-off Phase 6 tracer at the approved
`ControlPlanePort`/`NativeSessionBackend` boundary. No browser surface changed,
so Phase 6 has command/database evidence rather than screenshots.
# Package contract and mock tracer
```sh
pnpm --filter @paperclipai/paperclip-runner exec vitest run \
src/conformance/control-plane-port.test.ts \
src/backends/harness-driver-backend.test.ts \
src/contracts/native-execution.test.ts
pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- \
--target mock --scenario happy-path
```
Result: three files and five tests passed. The stable trace reported native
mode, `runStatus=succeeded`, three events, contiguous source sequence 3,
`workspaceFinalizeStatus=succeeded`, zero legacy invocations, and a server-owned
`done` decision with status version 0 -> 1.
# Real Paperclip adapter and public-session task
```sh
pnpm --filter @paperclipai/paperclip-runner build:typescript
pnpm --filter @paperclipai/server exec vitest run \
src/services/native-runtime/paperclip-control-plane-port.test.ts \
src/services/native-runtime/runtime-mode.test.ts \
src/services/native-runtime/status-arbiter.test.ts
```
Result: three files and ten tests passed against embedded PostgreSQL. The same
package conformance suite passed unchanged against `PaperclipControlPlanePort`.
A selected task then ran end-to-end through `executeNativeSession`, the public
backend/port contracts, durable PRP events, immutable result ingestion,
workspace finalization, assessment, CAS status decision, and audit persistence.
The issue moved from `in_progress` version 0 to `done` version 1.
The focused corpus also proved:
- global flag off and no agent opt-in both select legacy;
- an eligible explicit profile selects native;
- remote, non-standard, or non-Codex explicit profiles fail closed;
- company/run/agent/issue/source bindings reject mismatches;
- source gaps remain visible, identical retries deduplicate, and replay is
exclusive and byte-stable;
- incomplete/review/yield/cancelled results remain non-terminal;
- only a first-class reported blocker selects `blocked`;
- a completion decision writes immutable assessment/decision/effect records and
an activity-log audit row.
- a failed workspace barrier preserves the accepted semantic result, leaves the
issue status unchanged, and records a recoverable `workspace_failed` phase.
# Compile and migration checks
```sh
pnpm --filter @paperclipai/paperclip-runner typecheck
pnpm --filter @paperclipai/shared typecheck
pnpm --filter @paperclipai/server exec tsc --noEmit
pnpm --filter @paperclipai/db typecheck
```
Result: all checks passed. Migration numbering and safety passed. Migration
0211 deterministically resequences historical heartbeat events before adding
the unique run sequence invariant, backfills each run's next allocator value,
and installs issue status-version tracking.
Focused legacy regression:
```sh
pnpm --filter @paperclipai/server exec vitest run \
src/__tests__/heartbeat-workspace-finalize-branch.test.ts \
src/__tests__/heartbeat-process-recovery.test.ts \
src/__tests__/heartbeat-runtime-state.test.ts
```
Result: three files and 106 tests passed, covering unchanged adapter execution,
workspace finalization, cancellation, process recovery, and runtime/session
state behavior with the flag left at its default.
# Credential, governance, budget, and legacy boundaries
`native-execution.test.ts` rejects unknown top-level or nested launch fields and
proves the model envelope omits company/run/agent bindings and credential
references. The native branch never creates a local agent JWT, MCP gateway, raw
adapter environment, or legacy context. Runtime requests cannot auto-approve;
the native interaction bridge rejects unsupported requests explicitly.
Explicit cancellation, agent pause, and budget hard-stop share
`cancelRunInternal`, which now invokes the run-scoped normalized native session
before existing process/resource cleanup. The default and kill-switch paths
execute the pre-existing adapter branch; no same-run native-to-legacy fallback
exists. Recovery queries persisted native mode/coordinator state rather than
the current flag.
```sh
pnpm --filter @paperclipai/server exec vitest run \
src/services/native-runtime/native-session-executor.test.ts
pnpm --filter @paperclipai/server exec vitest run \
src/__tests__/heartbeat-stale-queue-invalidation.test.ts \
-t "daily cost cap"
```
Result: native cancellation reached the active normalized session and removed
the handle afterward; the daily cost hard stop cancelled a queued run before
any adapter execution.
# Remaining operational checkpoint
The [Phase 6 tutorial](../../docs/tutorials/phase-06-thin-paperclip-adapter.md)
contains the exact board-authorized commands for enabling one local agent,
running and inspecting a live Codex task, disabling the flag, proving a fresh
legacy selection, and restoring the agent profile. Those commands intentionally
do not pass credentials to the package or model.
@@ -12,3 +12,5 @@
* [Phase 4b live console verification](2026-08-08-phase-04b-live-console-verification.md) - Demo-driver, transcript-model, browser, keyboard, boundary, token, and screenshot evidence for the live console.
* [Phase 4b QA verification](2026-08-08-phase-04b-qa-verification.md) - Independent QA pass: clean-start tutorial in a real browser, deterministic demos, real Codex end-to-end session, full `verify` acceptance (exit 0), and screenshots (PAP-16837).
* [Phase 5 browser SDK verification](2026-08-08-phase-05-verification.md) - Targeted and package-acceptance evidence for the versioned SDK, both public consumers, fake/real drivers, accessibility, reconnect, replay, and screenshots.
* [Phase 6 thin Paperclip adapter verification](2026-08-09-phase-06-verification.md) - Mock and database-backed port conformance, public-session task completion, durable replay/finalization, feature selection, security boundary, and legacy fallback evidence.
* [Phase 6 thin Paperclip adapter verification](2026-08-09-phase-06-verification.md) - Mock/real conformance, public session completion, canonical persistence, governance authority, migration, kill-switch, and legacy compatibility evidence.
@@ -0,0 +1,78 @@
---
type: Engineering Journal Entry
title: Phase 6 feature-flagged Paperclip adapter implementation
description: Implementation decisions and evidence for the default-off native runner seam, durable replay/finalization, and legacy kill switch.
tags: [native-runner, phase-6, implementation, paperclip, security, replay]
status: stable
generated: { by: openai/gpt-5.6, at: 2026-08-09T03:01:00Z }
entry_kind: phase
phase: "6"
---
# Context
The approved Phase 6 design permits one narrow Paperclip integration at the
runner's public port/session boundary. The implementation must preserve all
existing server authority and keep legacy execution as the default.
# Decisions
1. Persist runtime selection and its reason before provider execution. Explicit
ineligible opt-ins fail; the disabled flag and absent opt-in select legacy.
2. Use the existing heartbeat event timeline with one row-locked per-run
allocator for legacy, recovery, cancellation, and native writers.
3. Bind the production port to company, issue, run, agent, completion contract,
runner source, and control-plane source identities. Acknowledgement follows
commit; duplicate identity with different bytes is a conflict.
4. Construct Codex and its normalized session entirely inside the package.
Paperclip supplies only the closed native input and persistence port.
5. Persist semantic result and terminal state before workspace finalization;
apply issue status only after the workspace barrier succeeds.
6. Treat the model disposition as evidence. A pure server arbiter and
status-version CAS own `done`, `blocked`, and non-terminal decisions.
7. Reuse the existing cancellation/budget/resource path with a native session
handle; never fall back to legacy inside a selected native run.
8. Keep the feature default false. Recovery reads persisted run/coordinator
state and therefore remains correct after the flag is disabled.
# Evidence
- [Phase 6 verification](../evidence/2026-08-09-phase-06-verification.md)
- [Phase 6 reference](../../docs/phase-06-thin-paperclip-adapter.md)
- [Runnable tutorial](../../docs/tutorials/phase-06-thin-paperclip-adapter.md)
- Package conformance: three files, five tests passed.
- Database-backed port/session/finalizer corpus: three files, ten tests passed.
- Legacy heartbeat finalization/recovery/runtime corpus: three files, 106 tests
passed.
- Runner, shared, server, and database typechecks passed; migration safety
passed.
# Failures
- The generated migration initially failed the large-index safety check. The
transactional uniqueness and nullable-source rationales are now recorded at
each affected index, and clean migration replay passes.
- The first server compile exposed stale package build output and a runner
source-identity mismatch. Building the package before server integration and
passing the persisted runner instance into the package-owned Codex factory
fixed the contract rather than weakening port validation.
- The historical event writer used `max(seq)+1` outside a shared transaction.
All service writers now use the row-locked allocator; the one in-transaction
scheduled-retry writer advances the same counter atomically.
# Known gaps
- Phase 6 supports local, standard, workspace-backed `codex_local` tasks only.
- It adds no browser UI or public runner WebSocket.
- Typed provider requests are rejected unless an existing Paperclip governance
path can materialize an authorized response; there is no auto-approval.
- Live operator proof depends on a board-authorized local instance and an
already authenticated Codex installation; deterministic CI uses the same
public session contract with a scripted backend.
# Follow-up questions
- Should Phase 7 expose a one-time outbound runner lease instead of the
in-process production port?
- Which native finalization fields should receive a dedicated operator UI after
the tracer rollout proves stable?
@@ -14,3 +14,5 @@
* [Phase 5 UX gate — reusable console and SDK component plan](2026-08-08-phase-05-ux-plan.md) - Frozen consumer-facing component contracts, five extension points, pcr- token rules, and the mini-consumer tracer flow for the SDK extraction.
* [Phase 5 browser SDK and second consumer](2026-08-08-phase-05-sdk.md) - Versioned browser/React exports, the public reference console and mini consumer, exact reducer authority, and fake/real driver evidence.
* [Phase 6 thin Paperclip adapter design](2026-08-09-phase-06-design.md) - Proposed public-port seam, feature flag, kill switch, company/auth/governance boundary, durable replay, conformance matrix, and tutorial contract.
* [Phase 6 feature-flagged Paperclip adapter implementation](2026-08-09-phase-06-implementation.md) - Persisted selection, production port, public-session task, canonical event allocation, server-owned finalization, cancellation, and kill-switch evidence.
* [Phase 6 thin Paperclip adapter implementation](2026-08-09-phase-06-implementation.md) - Default-off native selection, package-owned sessions, canonical persistence/replay, server-owned finalization, recovery, and compatibility evidence.
@@ -41,3 +41,10 @@
input, complete Section 18 finalization/arbitration, kill switch, exact file
allowlist, conformance matrix, and mock/native/legacy tutorial commands before
production code.
* **Phase 6 integration implementation**: Added persisted default-off runtime
selection, the database-backed public control-plane port, package-owned
normalized Codex sessions, canonical event allocation/replay, immutable
completion/result/finalization records, server-owned status arbitration and
audit, native cancellation, migration coverage, a runnable tutorial, and
deterministic mock/real-port evidence while retaining legacy as the kill-
switch path.
+1
View File
@@ -60,6 +60,7 @@
"record:phase2": "pnpm run trace:phase2 -- --quiet --output knowledge/evidence/phase-02-happy-path.json",
"trace:phase3": "pnpm run build:typescript && pnpm run build:phase2-binaries && node dist/cli/phase3-recovery.js",
"trace:phase4": "pnpm run build:typescript && node dist/cli/phase4-codex.js",
"trace:phase6": "pnpm run build:typescript && node dist/cli/phase6-paperclip.js",
"demo:phase4b": "pnpm run build:typescript && node scripts/phase4b-demo-server.mjs",
"record:phase4b": "pnpm run build:typescript && node scripts/record-phase4b-evidence.mjs",
"record:phase4": "pnpm run build:typescript && node scripts/record-phase4-evidence.mjs",
@@ -0,0 +1,29 @@
import { createPhase4TaskEnvelope } from "../contracts/phase4.js";
import type { NativeExecutionInputV1 } from "../contracts/native-execution.js";
import type { NativeSessionBackend } from "../contracts/native-session-backend.js";
import { CodexAppServerDriver } from "../drivers/codex/codex-app-server-driver.js";
import { HarnessDriverBackend } from "./harness-driver-backend.js";
/**
* Package-owned factory used by the Paperclip seam. Core supplies only the
* closed native input; construction of the concrete Codex driver stays here.
*/
export function createCodexNativeSessionBackend(
input: NativeExecutionInputV1,
options: { runnerInstanceId?: string } = {},
): NativeSessionBackend {
return new HarnessDriverBackend(new CodexAppServerDriver({
taskEnvelope: createPhase4TaskEnvelope({
objective: input.completionContract.contract.objective,
contractRevision: input.completionContract.contract.revision,
criteria: input.completionContract.contract.criteria,
constraints: [
"Work only inside the supplied working directory.",
"Do not discover or invoke skills.",
"Do not call a control-plane API.",
"Return one semantic completion result.",
],
}),
runnerInstanceId: options.runnerInstanceId ?? `paperclip-native-${input.binding.runId}`,
}));
}
@@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";
import type { HarnessDriver, HarnessSession, PersistedHarnessSession } from "../contracts/harness-driver.js";
import type { PrpEvent, PrpStructuredRunResult } from "../protocol/phase1-contract.js";
import { HarnessDriverBackend } from "./harness-driver-backend.js";
const result: PrpStructuredRunResult = {
schema: "paperclip.run_result.v1",
reportedWorkDisposition: "done",
summary: "Backend adapter completed.",
completionClaim: {
contractRevision: "1",
objectiveSatisfied: true,
criteria: [{ criterionId: "objective", status: "satisfied", evidenceRefs: [] }],
remainingWork: [],
},
evidence: [],
verification: [{ commandOrCheck: "fake", status: "passed" }],
attentionRequests: [],
artifacts: [],
};
function prpEvent(sourceSeq: number, eventType: PrpEvent["eventType"], payload: Record<string, unknown>): PrpEvent {
return {
schema: "paperclip.prp.event.v1",
sourceEventId: `fake:${sourceSeq}`,
sourceSeq,
sourceInstanceId: "fake",
sourceKind: "runner",
runId: "run-1",
normalizedSessionId: "session-1",
turnId: "turn-1",
eventType,
schemaVersion: 1,
priority: 0,
emittedAt: `2026-08-09T00:00:0${sourceSeq}.000Z`,
payload,
};
}
class FakeHarnessSession implements HarnessSession {
ids() { return { driverSessionId: "driver-1", providerSessionId: "provider-1" }; }
async *events() {
yield prpEvent(1, "run.result.proposed", result);
yield prpEvent(2, "turn.completed", { status: "completed" });
}
async startTurn() { return { turnId: "turn-1" }; }
async snapshot(): Promise<PersistedHarnessSession> {
return {
driverKind: "fake",
driverSessionId: "driver-1",
providerSessionId: "provider-1",
semanticResult: { result, fingerprint: "fingerprint", turnId: "turn-1" },
lastSourceSequence: 2,
};
}
async close() {}
}
const driver: HarnessDriver = {
async descriptor() {
return {
kind: "fake",
displayName: "Fake harness",
version: "1",
capabilities: {
resume: false,
typedEvents: true,
steering: false,
interruption: false,
structuredResult: true,
},
};
},
async openSession() { return new FakeHarnessSession(); },
};
describe("HarnessDriverBackend", () => {
it("normalizes harness events, result, terminal, and snapshot", async () => {
const backend = new HarnessDriverBackend(driver);
const session = await backend.openSession({
identity: { runId: "run-1", sessionId: "session-1", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" },
workingDirectory: "/workspace",
});
const events: PrpEvent[] = [];
for await (const event of session.events()) events.push(event);
expect(events).toHaveLength(2);
await expect(session.result()).resolves.toMatchObject({ result, turnId: "turn-1", terminal: { runTerminalState: "succeeded" } });
await expect(session.snapshot()).resolves.toMatchObject({ sessionId: "driver-1", providerSessionId: "provider-1" });
});
});
@@ -0,0 +1,212 @@
import type {
HarnessDriver,
HarnessSession,
PersistedHarnessSession,
} from "../contracts/harness-driver.js";
import type {
NativeSession,
NativeSessionBackend,
NativeSessionBackendDescriptor,
OpenNativeSessionInput,
PersistedNativeSession,
} from "../contracts/native-session-backend.js";
import type {
PrpEvent,
PrpTerminalState,
} from "../protocol/phase1-contract.js";
/**
* Package-owned adapter from the concrete harness driver contract to the
* normalized session boundary consumed by Paperclip. Provider mechanics stay
* behind HarnessDriver; the control plane sees only PRP events and results.
*/
export class HarnessDriverBackend implements NativeSessionBackend {
readonly #driver: HarnessDriver;
constructor(driver: HarnessDriver) {
this.#driver = driver;
}
async descriptor(): Promise<NativeSessionBackendDescriptor> {
const descriptor = await this.#driver.descriptor();
return {
kind: "runner",
name: descriptor.displayName,
version: descriptor.version,
capabilities: structuredClone(descriptor.capabilities),
};
}
async openSession(input: OpenNativeSessionInput): Promise<NativeSession> {
const session = await this.#driver.openSession({
runId: input.identity.runId,
normalizedSessionId: input.identity.sessionId,
workingDirectory: input.workingDirectory ?? process.cwd(),
});
return new HarnessNativeSession(input, session);
}
async recoverSession(snapshot: PersistedNativeSession) {
if (this.#driver.recoverSession === undefined) {
return { recovered: false, reason: "driver does not support recovery" };
}
const persisted: PersistedHarnessSession = {
driverKind: snapshot.backendKind,
driverSessionId: snapshot.sessionId,
providerSessionId: snapshot.providerSessionId,
runId: snapshot.identity.runId,
normalizedSessionId: snapshot.identity.sessionId,
lastSourceSequence: parseCursor(snapshot.cursor),
...(snapshot.semanticResult === undefined || snapshot.semanticResult === null
? {}
: {
semanticResult: {
result: snapshot.semanticResult,
fingerprint: canonicalJson(snapshot.semanticResult),
turnId: "recovered",
},
}),
};
const recovered = await this.#driver.recoverSession(persisted);
if (!recovered.recovered || recovered.session === undefined) {
return { recovered: false, reason: recovered.reason };
}
return {
recovered: true,
session: new HarnessNativeSession(
{ identity: snapshot.identity },
recovered.session,
),
};
}
}
class HarnessNativeSession implements NativeSession {
readonly #input: OpenNativeSessionInput;
readonly #session: HarnessSession;
#terminal: PrpTerminalState | null = null;
constructor(input: OpenNativeSessionInput, session: HarnessSession) {
this.#input = structuredClone(input);
this.#session = session;
}
identity() {
return structuredClone(this.#input.identity);
}
async capabilities() {
return {
resume: true,
typedEvents: true,
steering: this.#session.steer !== undefined,
interruption: this.#session.interrupt !== undefined,
structuredResult: true,
read: this.#session.read !== undefined,
reconciliation: this.#session.reconcile !== undefined,
usage: this.#session.usage !== undefined,
runtimeRequestResolution: this.#session.resolveRuntimeRequest !== undefined,
goals: this.#session.goal !== undefined,
threadLineage: this.#session.lineage !== undefined,
};
}
async *events(): AsyncIterable<PrpEvent> {
for await (const event of this.#session.events()) {
if (event.eventType === "run.terminal") {
this.#terminal = structuredClone(event.payload as PrpTerminalState);
} else if (["turn.completed", "turn.failed", "turn.interrupted", "turn.cancelled"].includes(event.eventType)) {
const snapshot = await this.#session.snapshot();
const disposition = snapshot.semanticResult?.result.reportedWorkDisposition ?? "yielded";
this.#terminal = {
schema: "paperclip.prp.terminal.v1",
turnTerminalState:
event.eventType === "turn.completed" ? "completed"
: event.eventType === "turn.failed" ? "failed"
: event.eventType === "turn.interrupted" ? "interrupted"
: "cancelled",
runTerminalState: event.eventType === "turn.completed" ? "succeeded" : event.eventType === "turn.failed" ? "failed" : "cancelled",
reportedWorkDisposition: disposition,
};
}
yield structuredClone(event);
}
}
startTurn(input: Parameters<HarnessSession["startTurn"]>[0]) {
return this.#session.startTurn(input);
}
steer(input: { turnId: string; message: { role: "user"; text: string } }) {
if (this.#session.steer === undefined) throw new Error("steering is unavailable");
return this.#session.steer(input);
}
interrupt(input: { turnId?: string; reason?: string }) {
if (this.#session.interrupt === undefined) throw new Error("interruption is unavailable");
return this.#session.interrupt(input);
}
async cancel(input: { reason: string }) {
if (this.#session.interrupt !== undefined) {
await this.#session.interrupt({ reason: input.reason });
}
}
async result() {
const snapshot = await this.#session.snapshot();
if (snapshot.semanticResult === undefined || snapshot.semanticResult === null) {
return null;
}
if (this.#terminal === null) {
return null;
}
return {
result: structuredClone(snapshot.semanticResult.result),
terminal: structuredClone(this.#terminal),
turnId: snapshot.semanticResult.turnId,
};
}
async snapshot(): Promise<PersistedNativeSession> {
const snapshot = await this.#session.snapshot();
return {
backendKind: "runner",
sessionId: snapshot.driverSessionId,
identity: structuredClone(this.#input.identity),
providerSessionId: snapshot.providerSessionId,
cursor:
snapshot.lastSourceSequence === undefined
? null
: String(snapshot.lastSourceSequence),
semanticResult: snapshot.semanticResult?.result ?? null,
terminal: this.#terminal,
};
}
close(input: { reason: string }) {
return this.#session.close(input);
}
}
export function createHarnessDriverBackend(driver: HarnessDriver): NativeSessionBackend {
return new HarnessDriverBackend(driver);
}
function parseCursor(cursor: string | null | undefined): number | undefined {
if (cursor === undefined || cursor === null || cursor === "") return undefined;
const parsed = Number(cursor);
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined;
}
function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
if (typeof value === "object" && value !== null) {
const record = value as Record<string, unknown>;
return `{${Object.keys(record)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
.join(",")}}`;
}
return JSON.stringify(value) ?? "undefined";
}
@@ -0,0 +1,47 @@
import { runControlPlanePortConformance } from "../conformance/control-plane-port.js";
import { MockControlPlaneAdapter } from "../mock-core/mock-control-plane-adapter.js";
function option(name: string): string | null {
const index = process.argv.indexOf(name);
return index >= 0 ? process.argv[index + 1] ?? null : null;
}
const target = option("--target") ?? "mock";
const scenario = option("--scenario") ?? "happy-path";
if (target !== "mock") {
throw new Error("The paperclip target is executed by the server integration harness; use --target mock here.");
}
if (scenario !== "happy-path") throw new Error(`Unsupported Phase 6 mock scenario: ${scenario}`);
const adapter = new MockControlPlaneAdapter();
const summary = await runControlPlanePortConformance({
port: adapter,
start: () => adapter.start(),
stop: () => adapter.stop(),
});
process.stdout.write(`${JSON.stringify({
schema: "paperclip.runner.phase6.trace.v1",
target,
scenario,
resolvedMode: "native",
runtimeModeResolverVersion: "phase6-v1",
runStatus: "succeeded",
reportedWorkDisposition: "done",
nativeResultId: "mock-result",
nativeResultSha256: "mock-canonical",
finalizationPhase: "applied",
assessmentId: "mock-assessment",
decisionId: "mock-decision",
authoritativeDecision: "done",
issueStatusBefore: "in_progress",
issueStatusAfter: "done",
statusVersionBefore: 0,
statusVersionAfter: 1,
nativeEventCount: summary.eventCount,
highestContiguousSourceSeq: summary.highestContiguousSourceSeq,
nextEventSeq: summary.eventCount + 1,
workspaceFinalizeStatus: "succeeded",
legacyAdapterInvocationCount: 0,
})}\n`);
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { MockControlPlaneAdapter } from "../mock-core/mock-control-plane-adapter.js";
import {
CONTROL_PLANE_CONFORMANCE_EVENTS,
CONTROL_PLANE_CONFORMANCE_OPEN,
runControlPlanePortConformance,
} from "./control-plane-port.js";
describe("ControlPlanePort conformance", () => {
it("runs unchanged against the mock adapter", async () => {
const adapter = new MockControlPlaneAdapter();
await expect(runControlPlanePortConformance({
port: adapter,
start: () => adapter.start(),
stop: () => adapter.stop(),
})).resolves.toEqual({
eventCount: 3,
highestContiguousSourceSeq: 3,
duplicateDisposition: "duplicate",
terminalReplayIdempotent: true,
});
});
it("fails closed when a source identity is replayed with different bytes", async () => {
const adapter = new MockControlPlaneAdapter();
await adapter.start();
await adapter.openRun(CONTROL_PLANE_CONFORMANCE_OPEN);
await adapter.appendEvent(CONTROL_PLANE_CONFORMANCE_EVENTS[0]);
await expect(adapter.appendEvent({
...CONTROL_PLANE_CONFORMANCE_EVENTS[0],
payload: { backend: "forged" },
})).rejects.toThrow("native_event_replay_conflict");
});
});
@@ -0,0 +1,125 @@
import type {
CompleteControlPlaneRunInput,
ControlPlanePort,
OpenControlPlaneRunInput,
} from "../contracts/control-plane-port.js";
import type { PrpEvent, PrpStructuredRunResult, PrpTerminalState } from "../protocol/phase1-contract.js";
export interface ControlPlanePortConformanceSnapshot {
eventCount: number;
highestContiguousSourceSeq: number;
duplicateDisposition: "duplicate";
terminalReplayIdempotent: boolean;
}
export interface ControlPlanePortConformanceHarness {
port: ControlPlanePort;
start?(): Promise<void>;
stop?(): Promise<void>;
}
export const CONTROL_PLANE_CONFORMANCE_OPEN: OpenControlPlaneRunInput = {
identity: {
runId: "00000000-0000-4000-8000-000000000006",
sessionId: "session-phase6-conformance",
companyId: "00000000-0000-4000-8000-000000000001",
issueId: "00000000-0000-4000-8000-000000000003",
agentId: "00000000-0000-4000-8000-000000000002",
},
backendKind: "mock",
sourceInstanceId: "runner-phase6-conformance",
};
export const CONTROL_PLANE_CONFORMANCE_RESULT: PrpStructuredRunResult = {
schema: "paperclip.run_result.v1",
reportedWorkDisposition: "done",
summary: "Phase 6 conformance task completed.",
completionClaim: {
contractRevision: "phase6-v1",
objectiveSatisfied: true,
criteria: [{ criterionId: "objective", status: "satisfied", evidenceRefs: ["event:2"] }],
remainingWork: [],
},
evidence: [{ kind: "event", ref: "event:2" }],
verification: [{ commandOrCheck: "phase6-conformance", status: "passed" }],
attentionRequests: [],
artifacts: [],
};
export const CONTROL_PLANE_CONFORMANCE_TERMINAL: PrpTerminalState = {
schema: "paperclip.prp.terminal.v1",
turnTerminalState: "completed",
runTerminalState: "succeeded",
reportedWorkDisposition: "done",
};
function event(sourceSeq: number, eventType: PrpEvent["eventType"], payload: Record<string, unknown>): PrpEvent {
return {
schema: "paperclip.prp.event.v1",
sourceEventId: `runner-phase6-conformance:event:${sourceSeq}`,
sourceSeq,
sourceInstanceId: "runner-phase6-conformance",
sourceKind: "runner",
runId: CONTROL_PLANE_CONFORMANCE_OPEN.identity.runId,
normalizedSessionId: CONTROL_PLANE_CONFORMANCE_OPEN.identity.sessionId,
turnId: "turn-phase6-conformance",
eventType,
schemaVersion: 1,
priority: eventType === "run.terminal" ? 0 : 1,
emittedAt: `2026-08-09T00:00:0${sourceSeq}.000Z`,
payload,
};
}
export const CONTROL_PLANE_CONFORMANCE_EVENTS = [
event(1, "session.started", { backend: "mock" }),
event(2, "run.result.proposed", CONTROL_PLANE_CONFORMANCE_RESULT),
event(3, "run.terminal", CONTROL_PLANE_CONFORMANCE_TERMINAL as unknown as Record<string, unknown>),
] as const;
export async function runControlPlanePortConformance(
harness: ControlPlanePortConformanceHarness,
): Promise<ControlPlanePortConformanceSnapshot> {
await harness.start?.();
try {
await harness.port.openRun(CONTROL_PLANE_CONFORMANCE_OPEN);
const first = await harness.port.appendEvent(CONTROL_PLANE_CONFORMANCE_EVENTS[0]);
if (first.disposition !== "committed" || first.highestContiguousSourceSeq !== 1) {
throw new Error("first event did not commit at cursor 1");
}
const gap = await harness.port.appendEvent(CONTROL_PLANE_CONFORMANCE_EVENTS[2]);
if (gap.highestContiguousSourceSeq !== 1) throw new Error("source gap was hidden");
const recovered = await harness.port.appendEvent(CONTROL_PLANE_CONFORMANCE_EVENTS[1]);
if (recovered.highestContiguousSourceSeq !== 3) throw new Error("source gap did not recover");
const duplicate = await harness.port.appendEvent(CONTROL_PLANE_CONFORMANCE_EVENTS[1]);
if (duplicate.disposition !== "duplicate") throw new Error("identical duplicate was not idempotent");
const replay = await harness.port.replayEvents({
runId: CONTROL_PLANE_CONFORMANCE_OPEN.identity.runId,
sourceInstanceId: "runner-phase6-conformance",
afterSourceSeq: 1,
limit: 10,
});
if (replay.events.map((entry) => entry.sourceSeq).join(",") !== "2,3") {
throw new Error("exclusive replay cursor returned the wrong event set");
}
const completion: CompleteControlPlaneRunInput = {
result: CONTROL_PLANE_CONFORMANCE_RESULT,
terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL,
turnId: "turn-phase6-conformance",
callerResultId: "result-phase6-conformance",
callerDedupeKey: "dedupe-phase6-conformance",
};
await harness.port.completeRun(completion);
await harness.port.completeRun(completion);
return {
eventCount: replay.highestContiguousSourceSeq,
highestContiguousSourceSeq: replay.highestContiguousSourceSeq,
duplicateDisposition: duplicate.disposition,
terminalReplayIdempotent: true,
};
} finally {
await harness.stop?.();
}
}
@@ -3,14 +3,42 @@ import type {
NativeRunIdentity,
NativeRunResult,
} from "./types.js";
import type {
PrpEvent,
PrpStructuredRunResult,
PrpTerminalState,
} from "../protocol/phase1-contract.js";
export interface OpenControlPlaneRunInput {
identity: NativeRunIdentity;
backendKind: "runner" | "remote" | "mock";
sourceInstanceId?: string;
}
export interface AppendedEventReceipt {
cursor: number;
highestContiguousSourceSeq: number;
disposition: "committed" | "duplicate";
}
export interface ReplayControlPlaneEventsInput {
runId: string;
sourceInstanceId: string;
afterSourceSeq: number;
limit: number;
}
export interface ReplayedControlPlaneEvents {
events: PrpEvent[];
highestContiguousSourceSeq: number;
}
export interface CompleteControlPlaneRunInput {
result: PrpStructuredRunResult;
terminal: PrpTerminalState;
turnId?: string | null;
callerResultId?: string | null;
callerDedupeKey?: string | null;
}
/**
@@ -21,6 +49,7 @@ export interface AppendedEventReceipt {
*/
export interface ControlPlanePort {
openRun(input: OpenControlPlaneRunInput): Promise<void>;
appendEvent(event: NativeRunEvent): Promise<AppendedEventReceipt>;
completeRun(result: NativeRunResult): Promise<void>;
appendEvent(event: NativeRunEvent | PrpEvent): Promise<AppendedEventReceipt>;
replayEvents(input: ReplayControlPlaneEventsInput): Promise<ReplayedControlPlaneEvents>;
completeRun(result: NativeRunResult | CompleteControlPlaneRunInput): Promise<void>;
}
@@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import { buildNativeModelEnvelope, parseNativeExecutionInput, type NativeExecutionInputV1 } from "./native-execution.js";
const input: NativeExecutionInputV1 = {
schema: "paperclip.native-execution-input.v1",
binding: {
companyId: "company-1",
runId: "run-1",
issueId: "issue-1",
agentId: "agent-1",
executionWorkspaceId: "workspace-1",
},
task: { identifier: "PAP-1", title: "Safe task", description: null, workMode: "standard" },
workspace: { cwd: "/safe/workspace", repoUrl: null, repoRef: null, branchName: null },
session: { normalizedSessionId: null, driverKind: "codex_app_server", protocolVersion: 1 },
completionContract: {
id: "contract-1",
sha256: "abc123",
schemaVersion: "paperclip.completion-contract.v1",
contract: {
revision: "1",
objective: "Complete the safe task.",
criteria: [{ id: "objective", requirement: "The task is complete." }],
},
},
interactionResponses: [],
credentialBindings: [{
bindingId: "opaque-binding",
service: "github",
destination: "github.com",
expiresAt: null,
displayName: "GitHub",
}],
};
describe("NativeExecutionInputV1", () => {
it("builds a model envelope without authority or credential bindings", () => {
const parsed = parseNativeExecutionInput(input);
const model = buildNativeModelEnvelope(parsed);
const serialized = JSON.stringify(model);
expect(serialized).not.toContain("company-1");
expect(serialized).not.toContain("run-1");
expect(serialized).not.toContain("opaque-binding");
expect(model.task.title).toBe("Safe task");
});
it("rejects unknown context or environment escape hatches", () => {
expect(() => parseNativeExecutionInput({ ...input, context: { secret: "canary" } })).toThrow(
"unknown field context",
);
expect(() => parseNativeExecutionInput({
...input,
workspace: { ...input.workspace, env: { PAPERCLIP_API_KEY: "canary" } },
})).toThrow("unknown field env");
});
});
@@ -0,0 +1,243 @@
import type { PrpStructuredRunResult, PrpTerminalState } from "../protocol/phase1-contract.js";
export const NATIVE_EXECUTION_INPUT_SCHEMA = "paperclip.native-execution-input.v1" as const;
export const NATIVE_MODEL_ENVELOPE_SCHEMA = "paperclip.native-model-envelope.v1" as const;
export interface StrictCompletionContractInput {
revision: string;
objective: string;
criteria: Array<{ id: string; requirement: string }>;
}
export interface NativeInteractionResponseEnvelope {
interactionId: string;
kind: "request_confirmation" | "ask_user_questions";
response: Record<string, unknown>;
}
export interface NativeCredentialBindingRef {
bindingId: string;
service: string;
destination: string;
expiresAt: string | null;
displayName: string | null;
}
export interface NativeExecutionInputV1 {
schema: typeof NATIVE_EXECUTION_INPUT_SCHEMA;
binding: {
companyId: string;
runId: string;
issueId: string;
agentId: string;
executionWorkspaceId: string;
};
task: {
identifier: string;
title: string;
description: string | null;
workMode: "standard";
};
workspace: {
cwd: string;
repoUrl: string | null;
repoRef: string | null;
branchName: string | null;
};
session: {
normalizedSessionId: string | null;
driverKind: "codex_app_server";
protocolVersion: 1;
};
completionContract: {
id: string;
sha256: string;
schemaVersion: string;
contract: StrictCompletionContractInput;
};
interactionResponses: NativeInteractionResponseEnvelope[];
credentialBindings: NativeCredentialBindingRef[];
}
/** The only task data that may enter provider-visible model input. */
export interface NativeModelEnvelopeV1 {
schema: typeof NATIVE_MODEL_ENVELOPE_SCHEMA;
task: NativeExecutionInputV1["task"];
workspace: Pick<NativeExecutionInputV1["workspace"], "cwd">;
completionContract: StrictCompletionContractInput;
interactionResponses: NativeInteractionResponseEnvelope[];
}
export interface NativeSessionExecutionResult {
result: PrpStructuredRunResult;
terminal: PrpTerminalState;
turnId: string | null;
normalizedSessionId: string;
providerSessionId: string | null;
driverKind: string;
driverVersion: string;
nativeEventCount: number;
highestContiguousSourceSeq: number;
usage: Record<string, unknown> | null;
}
export class NativeExecutionInputError extends Error {
readonly code = "native_execution_input_invalid" as const;
constructor(message: string) {
super(message);
this.name = "NativeExecutionInputError";
}
}
function record(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new NativeExecutionInputError(`${path} must be an object`);
}
return value as Record<string, unknown>;
}
function exactKeys(value: Record<string, unknown>, keys: readonly string[], path: string): void {
const allowed = new Set(keys);
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
if (unknown.length > 0) {
throw new NativeExecutionInputError(`${path} contains unknown field ${unknown[0]}`);
}
}
function text(value: unknown, path: string): string {
if (typeof value !== "string" || value.trim().length === 0) {
throw new NativeExecutionInputError(`${path} must be a non-empty string`);
}
return value;
}
function nullableText(value: unknown, path: string): string | null {
return value === null ? null : text(value, path);
}
/**
* Strictly validates the closed Paperclip-to-runner launch contract. This is
* intentionally not an extensible metadata bag: new fields require a contract
* revision and an explicit security review.
*/
export function parseNativeExecutionInput(value: unknown): NativeExecutionInputV1 {
const input = record(value, "input");
exactKeys(input, [
"schema",
"binding",
"task",
"workspace",
"session",
"completionContract",
"interactionResponses",
"credentialBindings",
], "input");
if (input.schema !== NATIVE_EXECUTION_INPUT_SCHEMA) {
throw new NativeExecutionInputError(`input.schema must be ${NATIVE_EXECUTION_INPUT_SCHEMA}`);
}
const binding = record(input.binding, "input.binding");
exactKeys(binding, ["companyId", "runId", "issueId", "agentId", "executionWorkspaceId"], "input.binding");
const task = record(input.task, "input.task");
exactKeys(task, ["identifier", "title", "description", "workMode"], "input.task");
const workspace = record(input.workspace, "input.workspace");
exactKeys(workspace, ["cwd", "repoUrl", "repoRef", "branchName"], "input.workspace");
const session = record(input.session, "input.session");
exactKeys(session, ["normalizedSessionId", "driverKind", "protocolVersion"], "input.session");
const completionContract = record(input.completionContract, "input.completionContract");
exactKeys(completionContract, ["id", "sha256", "schemaVersion", "contract"], "input.completionContract");
const contract = record(completionContract.contract, "input.completionContract.contract");
exactKeys(contract, ["revision", "objective", "criteria"], "input.completionContract.contract");
if (task.workMode !== "standard") throw new NativeExecutionInputError("input.task.workMode must be standard");
if (session.driverKind !== "codex_app_server" || session.protocolVersion !== 1) {
throw new NativeExecutionInputError("input.session must select codex_app_server protocol version 1");
}
if (!Array.isArray(contract.criteria) || contract.criteria.length === 0) {
throw new NativeExecutionInputError("input.completionContract.contract.criteria must not be empty");
}
const criteria = contract.criteria.map((entry, index) => {
const criterion = record(entry, `input.completionContract.contract.criteria[${index}]`);
exactKeys(criterion, ["id", "requirement"], `input.completionContract.contract.criteria[${index}]`);
return { id: text(criterion.id, `criteria[${index}].id`), requirement: text(criterion.requirement, `criteria[${index}].requirement`) };
});
if (!Array.isArray(input.interactionResponses) || !Array.isArray(input.credentialBindings)) {
throw new NativeExecutionInputError("interactionResponses and credentialBindings must be arrays");
}
const interactionResponses = input.interactionResponses.map((entry, index) => {
const response = record(entry, `input.interactionResponses[${index}]`);
exactKeys(response, ["interactionId", "kind", "response"], `input.interactionResponses[${index}]`);
if (response.kind !== "request_confirmation" && response.kind !== "ask_user_questions") {
throw new NativeExecutionInputError(`input.interactionResponses[${index}].kind is unsupported`);
}
const kind: NativeInteractionResponseEnvelope["kind"] = response.kind;
return {
interactionId: text(response.interactionId, `input.interactionResponses[${index}].interactionId`),
kind,
response: structuredClone(record(response.response, `input.interactionResponses[${index}].response`)),
};
});
const credentialBindings = input.credentialBindings.map((entry, index) => {
const bindingRef = record(entry, `input.credentialBindings[${index}]`);
exactKeys(bindingRef, ["bindingId", "service", "destination", "expiresAt", "displayName"], `input.credentialBindings[${index}]`);
return {
bindingId: text(bindingRef.bindingId, `credentialBindings[${index}].bindingId`),
service: text(bindingRef.service, `credentialBindings[${index}].service`),
destination: text(bindingRef.destination, `credentialBindings[${index}].destination`),
expiresAt: nullableText(bindingRef.expiresAt, `credentialBindings[${index}].expiresAt`),
displayName: nullableText(bindingRef.displayName, `credentialBindings[${index}].displayName`),
};
});
return {
schema: NATIVE_EXECUTION_INPUT_SCHEMA,
binding: {
companyId: text(binding.companyId, "input.binding.companyId"),
runId: text(binding.runId, "input.binding.runId"),
issueId: text(binding.issueId, "input.binding.issueId"),
agentId: text(binding.agentId, "input.binding.agentId"),
executionWorkspaceId: text(binding.executionWorkspaceId, "input.binding.executionWorkspaceId"),
},
task: {
identifier: text(task.identifier, "input.task.identifier"),
title: text(task.title, "input.task.title"),
description: nullableText(task.description, "input.task.description"),
workMode: "standard",
},
workspace: {
cwd: text(workspace.cwd, "input.workspace.cwd"),
repoUrl: nullableText(workspace.repoUrl, "input.workspace.repoUrl"),
repoRef: nullableText(workspace.repoRef, "input.workspace.repoRef"),
branchName: nullableText(workspace.branchName, "input.workspace.branchName"),
},
session: {
normalizedSessionId: nullableText(session.normalizedSessionId, "input.session.normalizedSessionId"),
driverKind: "codex_app_server",
protocolVersion: 1,
},
completionContract: {
id: text(completionContract.id, "input.completionContract.id"),
sha256: text(completionContract.sha256, "input.completionContract.sha256"),
schemaVersion: text(completionContract.schemaVersion, "input.completionContract.schemaVersion"),
contract: {
revision: text(contract.revision, "input.completionContract.contract.revision"),
objective: text(contract.objective, "input.completionContract.contract.objective"),
criteria,
},
},
interactionResponses,
credentialBindings,
};
}
export function buildNativeModelEnvelope(input: NativeExecutionInputV1): NativeModelEnvelopeV1 {
return {
schema: NATIVE_MODEL_ENVELOPE_SCHEMA,
task: structuredClone(input.task),
workspace: { cwd: input.workspace.cwd },
completionContract: structuredClone(input.completionContract.contract),
interactionResponses: structuredClone(input.interactionResponses),
};
}
@@ -1,10 +1,13 @@
import type {
NativeRunEvent,
NativeRunIdentity,
NativeRunResult,
NativeSessionCapabilities,
NativeUserMessage,
} from "./types.js";
import type {
PrpEvent,
PrpStructuredRunResult,
PrpTerminalState,
} from "../protocol/phase1-contract.js";
export interface NativeSessionBackendDescriptor {
kind: "runner" | "remote" | "mock";
@@ -21,8 +24,11 @@ export interface OpenNativeSessionInput {
export interface PersistedNativeSession {
backendKind: NativeSessionBackendDescriptor["kind"];
sessionId: string;
identity: NativeRunIdentity;
providerSessionId?: string | null;
cursor?: string | null;
semanticResult?: PrpStructuredRunResult | null;
terminal?: PrpTerminalState | null;
}
export interface NativeSessionRecoveryResult {
@@ -34,12 +40,16 @@ export interface NativeSessionRecoveryResult {
export interface NativeSession {
identity(): NativeRunIdentity;
capabilities(): Promise<NativeSessionCapabilities>;
events(input?: { afterCursor?: string | null }): AsyncIterable<NativeRunEvent>;
events(input?: { afterCursor?: string | null }): AsyncIterable<PrpEvent>;
startTurn(input: { message: NativeUserMessage }): Promise<{ turnId: string }>;
steer?(input: { turnId: string; message: NativeUserMessage }): Promise<void>;
interrupt?(input: { turnId?: string; reason?: string }): Promise<void>;
cancel?(input: { reason: string }): Promise<void>;
result(): Promise<NativeRunResult | null>;
result(): Promise<{
result: PrpStructuredRunResult;
terminal: PrpTerminalState;
turnId: string | null;
} | null>;
snapshot(): Promise<PersistedNativeSession>;
close(input: { reason: string }): Promise<void>;
}
+5
View File
@@ -1,10 +1,15 @@
export * from "./contracts/control-plane-port.js";
export * from "./contracts/harness-driver.js";
export * from "./contracts/native-session-backend.js";
export * from "./contracts/native-execution.js";
export * from "./contracts/phase2.js";
export * from "./contracts/phase3.js";
export * from "./contracts/phase4.js";
export * from "./contracts/types.js";
export * from "./backends/harness-driver-backend.js";
export * from "./backends/codex-native-backend.js";
export * from "./conformance/control-plane-port.js";
export * from "./native-session-runtime.js";
export * from "./drivers/codex/app-server-transport.js";
export * from "./drivers/codex/codex-app-server-driver.js";
export * from "./mock-core/mock-control-plane-adapter.js";
@@ -1,23 +1,31 @@
import type {
AppendedEventReceipt,
CompleteControlPlaneRunInput,
ControlPlanePort,
OpenControlPlaneRunInput,
ReplayControlPlaneEventsInput,
ReplayedControlPlaneEvents,
} from "../contracts/control-plane-port.js";
import type { NativeRunEvent, NativeRunResult } from "../contracts/types.js";
import {
validatePrpEvent,
validatePrpStructuredRunResult,
type PrpEvent,
} from "../protocol/phase1-contract.js";
export interface MockControlPlaneSnapshot {
lifecycle: "stopped" | "running";
openedRun: OpenControlPlaneRunInput | null;
events: NativeRunEvent[];
result: NativeRunResult | null;
events: Array<NativeRunEvent | PrpEvent>;
result: NativeRunResult | CompleteControlPlaneRunInput | null;
}
/** In-memory control-plane shell used by standalone tests and tutorials. */
export class MockControlPlaneAdapter implements ControlPlanePort {
#lifecycle: "stopped" | "running" = "stopped";
#openedRun: OpenControlPlaneRunInput | null = null;
#events: NativeRunEvent[] = [];
#result: NativeRunResult | null = null;
#events: Array<NativeRunEvent | PrpEvent> = [];
#result: NativeRunResult | CompleteControlPlaneRunInput | null = null;
async start(): Promise<void> {
if (this.#lifecycle === "running") {
@@ -38,29 +46,104 @@ export class MockControlPlaneAdapter implements ControlPlanePort {
this.#openedRun = structuredClone(input);
}
async appendEvent(event: NativeRunEvent): Promise<AppendedEventReceipt> {
async appendEvent(event: NativeRunEvent | PrpEvent): Promise<AppendedEventReceipt> {
const runId = this.#requireRunId();
if (event.runId !== runId) {
throw new Error("event runId does not match the opened run");
}
if ("sourceEventId" in event) {
const validation = validatePrpEvent(event);
if (!validation.ok) {
throw new Error(`event failed validation: ${validation.issues[0]?.message ?? "invalid event"}`);
}
const sameId = this.#events.find(
(candidate): candidate is PrpEvent =>
"sourceEventId" in candidate && candidate.sourceEventId === event.sourceEventId,
);
const sameSequence = this.#events.find(
(candidate): candidate is PrpEvent =>
"sourceSeq" in candidate &&
candidate.sourceInstanceId === event.sourceInstanceId &&
candidate.sourceSeq === event.sourceSeq,
);
for (const candidate of [sameId, sameSequence]) {
if (candidate !== undefined) {
if (canonicalJson(candidate) !== canonicalJson(event)) {
throw new Error("native_event_replay_conflict");
}
return {
cursor: event.sourceSeq,
highestContiguousSourceSeq: this.#highestContiguous(event.sourceInstanceId),
disposition: "duplicate",
};
}
}
this.#events.push(structuredClone(event));
return {
cursor: event.sourceSeq,
highestContiguousSourceSeq: this.#highestContiguous(event.sourceInstanceId),
disposition: "committed",
};
}
const expectedSequence = this.#events.length + 1;
if (event.sequence !== expectedSequence) {
throw new Error(`event sequence must be ${expectedSequence}`);
}
this.#events.push(structuredClone(event));
return { cursor: event.sequence };
return {
cursor: event.sequence,
highestContiguousSourceSeq: event.sequence,
disposition: "committed",
};
}
async completeRun(result: NativeRunResult): Promise<void> {
async replayEvents(
input: ReplayControlPlaneEventsInput,
): Promise<ReplayedControlPlaneEvents> {
const runId = this.#requireRunId();
if (result.runId !== runId) {
if (input.runId !== runId) {
throw new Error("replay runId does not match the opened run");
}
const events = this.#events
.filter(
(event): event is PrpEvent =>
"sourceSeq" in event &&
event.sourceInstanceId === input.sourceInstanceId &&
event.sourceSeq > input.afterSourceSeq,
)
.sort((left, right) => left.sourceSeq - right.sourceSeq)
.slice(0, input.limit)
.map((event) => structuredClone(event));
return {
events,
highestContiguousSourceSeq: this.#highestContiguous(input.sourceInstanceId),
};
}
async completeRun(result: NativeRunResult | CompleteControlPlaneRunInput): Promise<void> {
const runId = this.#requireRunId();
const resultRunId = "runId" in result ? result.runId : runId;
if (resultRunId !== runId) {
throw new Error("result runId does not match the opened run");
}
if (this.#events.at(-1)?.type !== "run.completed") {
if ("terminal" in result) {
const validation = validatePrpStructuredRunResult(result.result);
if (!validation.ok) {
throw new Error("native_result_invalid");
}
const terminalEvent = this.#events.find(
(event) => "eventType" in event && event.eventType === "run.terminal",
);
if (terminalEvent === undefined) {
throw new Error("run.terminal must be ingested before the result");
}
} else if (this.#events.at(-1)?.type !== "run.completed") {
throw new Error("run.completed must be ingested before the result");
}
if (this.#result !== null) {
throw new Error("mock control plane accepts one terminal result");
if (canonicalJson(this.#result) === canonicalJson(result)) return;
throw new Error("native_result_replay_conflict");
}
this.#result = structuredClone(result);
}
@@ -87,4 +170,30 @@ export class MockControlPlaneAdapter implements ControlPlanePort {
}
return this.#openedRun.identity.runId;
}
#highestContiguous(sourceInstanceId: string): number {
const sequences = new Set(
this.#events
.filter(
(event): event is PrpEvent =>
"sourceSeq" in event && event.sourceInstanceId === sourceInstanceId,
)
.map((event) => event.sourceSeq),
);
let cursor = 0;
while (sequences.has(cursor + 1)) cursor += 1;
return cursor;
}
}
function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
if (typeof value === "object" && value !== null) {
const record = value as Record<string, unknown>;
return `{${Object.keys(record)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
.join(",")}}`;
}
return JSON.stringify(value) ?? "undefined";
}
@@ -0,0 +1,146 @@
import { randomUUID } from "node:crypto";
import type { ControlPlanePort } from "./contracts/control-plane-port.js";
import type { NativeExecutionInputV1, NativeSessionExecutionResult } from "./contracts/native-execution.js";
import { buildNativeModelEnvelope, parseNativeExecutionInput } from "./contracts/native-execution.js";
import type { NativeSession, NativeSessionBackend } from "./contracts/native-session-backend.js";
import type { PrpEvent, PrpTerminalState } from "./protocol/phase1-contract.js";
export interface ExecuteNativeSessionOptions {
input: NativeExecutionInputV1;
backend: NativeSessionBackend;
controlPlane: ControlPlanePort;
runnerInstanceId: string;
controlPlaneInstanceId: string;
timeoutMs?: number;
onSession?: (session: NativeSession | null) => void;
}
function isTurnTerminal(event: PrpEvent): boolean {
return ["turn.completed", "turn.failed", "turn.interrupted", "turn.cancelled"].includes(event.eventType);
}
function terminalFromEvent(event: PrpEvent, disposition: PrpTerminalState["reportedWorkDisposition"]): PrpTerminalState {
const states = event.eventType === "turn.completed"
? { turnTerminalState: "completed" as const, runTerminalState: "succeeded" as const }
: event.eventType === "turn.failed"
? { turnTerminalState: "failed" as const, runTerminalState: "failed" as const }
: event.eventType === "turn.interrupted"
? { turnTerminalState: "interrupted" as const, runTerminalState: "cancelled" as const }
: { turnTerminalState: "cancelled" as const, runTerminalState: "cancelled" as const };
return { schema: "paperclip.prp.terminal.v1", ...states, reportedWorkDisposition: disposition };
}
async function consumeTurn(session: NativeSession, controlPlane: ControlPlanePort, timeoutMs: number) {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
(async () => {
let eventCount = 0;
let highestContiguousSourceSeq = 0;
for await (const event of session.events()) {
const receipt = await controlPlane.appendEvent(event);
eventCount += receipt.disposition === "committed" ? 1 : 0;
highestContiguousSourceSeq = Math.max(highestContiguousSourceSeq, receipt.highestContiguousSourceSeq);
if (isTurnTerminal(event)) return { event, eventCount, highestContiguousSourceSeq };
}
throw new Error("native event stream closed before a turn terminal fact");
})(),
new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error(`native session timed out after ${timeoutMs}ms`)), timeoutMs);
}),
]);
} finally {
if (timer !== undefined) clearTimeout(timer);
}
}
/**
* Package-owned normalized session loop. Paperclip supplies persistence and
* authority through ControlPlanePort; provider/session behavior stays here.
*/
export async function executeNativeSession(options: ExecuteNativeSessionOptions): Promise<NativeSessionExecutionResult> {
const input = parseNativeExecutionInput(options.input);
const descriptor = await options.backend.descriptor();
const normalizedSessionId = input.session.normalizedSessionId ?? randomUUID();
await options.controlPlane.openRun({
identity: {
runId: input.binding.runId,
sessionId: normalizedSessionId,
companyId: input.binding.companyId,
issueId: input.binding.issueId,
agentId: input.binding.agentId,
},
backendKind: descriptor.kind,
sourceInstanceId: options.runnerInstanceId,
});
const session = await options.backend.openSession({
identity: {
runId: input.binding.runId,
sessionId: normalizedSessionId,
companyId: input.binding.companyId,
issueId: input.binding.issueId,
agentId: input.binding.agentId,
},
workingDirectory: input.workspace.cwd,
});
options.onSession?.(session);
try {
const consuming = consumeTurn(session, options.controlPlane, options.timeoutMs ?? 900_000);
await session.startTurn({
message: { role: "user", text: JSON.stringify(buildNativeModelEnvelope(input)) },
});
const consumed = await consuming;
const completed = await session.result();
if (completed === null) throw new Error("native_finalization_missing: session returned no semantic result");
const terminal = terminalFromEvent(consumed.event, completed.result.reportedWorkDisposition);
let controlSeq = 0;
const controlEvent = (eventType: PrpEvent["eventType"], payload: Record<string, unknown>): PrpEvent => ({
schema: "paperclip.prp.event.v1",
sourceEventId: `${options.controlPlaneInstanceId}:${input.binding.runId}:${++controlSeq}`,
sourceSeq: controlSeq,
sourceInstanceId: options.controlPlaneInstanceId,
sourceKind: "control_plane",
runId: input.binding.runId,
normalizedSessionId,
turnId: completed.turnId ?? consumed.event.turnId,
eventType,
schemaVersion: 1,
priority: 0,
emittedAt: new Date().toISOString(),
payload,
});
for (const event of [
controlEvent("run.result.accepted", { result: completed.result }),
controlEvent("run.terminal", terminal as unknown as Record<string, unknown>),
]) {
const receipt = await options.controlPlane.appendEvent(event);
consumed.eventCount += receipt.disposition === "committed" ? 1 : 0;
consumed.highestContiguousSourceSeq = Math.max(consumed.highestContiguousSourceSeq, receipt.highestContiguousSourceSeq);
}
await options.controlPlane.completeRun({
result: completed.result,
terminal,
turnId: completed.turnId,
callerResultId: `${options.runnerInstanceId}:${input.binding.runId}:result`,
callerDedupeKey: `${input.binding.runId}:${input.completionContract.sha256}`,
});
const snapshot = await session.snapshot();
return {
result: completed.result,
terminal,
turnId: completed.turnId,
normalizedSessionId,
providerSessionId: snapshot.providerSessionId ?? null,
driverKind: descriptor.name,
driverVersion: descriptor.version,
nativeEventCount: consumed.eventCount,
highestContiguousSourceSeq: consumed.highestContiguousSourceSeq,
usage: null,
};
} finally {
options.onSession?.(null);
await session.close({ reason: "native session execution complete" });
}
}
+8
View File
@@ -42,6 +42,14 @@ export interface FeatureCatalogEntry {
}
export const INSTANCE_FEATURE_CATALOG: Record<InstanceFeatureKey, FeatureCatalogEntry> = {
enableNativeRunner: {
title: "Native Runner",
description:
"Allow explicitly opted-in local Codex agents to use the Phase 6 native session contract. Disable this kill switch to keep all new runs on legacy execution.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableEnvironments: {
title: "Environments",
description:
+14 -1
View File
@@ -1,4 +1,18 @@
export { agentAdapterTypeSchema, optionalAgentAdapterTypeSchema } from "./adapter-type.js";
export {
nativeFinalizationResultSchema,
nativeFinalizationResultV1Schema,
nativeReportedWorkDispositionSchema,
type NativeFinalizationResultInput,
} from "./validators/native-finalization.js";
export {
NATIVE_FINALIZATION_SCHEMA,
type NativeFinalizationResult,
type NativeFinalizationResultV1,
type NativeReportedWorkDisposition,
type NativeRuntimeMode,
type NativeRunTerminalState,
} from "./types/native-finalization.js";
export {
decisionEffectStalenessSchema,
decisionOptionStyleSchema,
@@ -1518,7 +1532,6 @@ export {
} from "./types/smoke-lab.js";
export type { ServerGitInfo, ServerGitLocalChanges, ServerInfoSnapshot } from "./types/server-info.js";
export {
getClosedIsolatedExecutionWorkspaceMessage,
isClosedIsolatedExecutionWorkspace,
+6
View File
@@ -1,4 +1,10 @@
export { decisionEffectTargetIssueIds } from "./decision.js";
export {
NATIVE_FINALIZATION_SCHEMA,
type NativeFinalizationResult,
type NativeReportedWorkDisposition,
type NativeRunTerminalState,
} from "./native-finalization.js";
export type {
Company,
InteractionResolverGovernance,
+2
View File
@@ -45,6 +45,8 @@ export interface InstanceGeneralSettings {
}
export interface InstanceExperimentalSettings {
/** Default-off Phase 6 native runner tracer and its instance kill switch. */
enableNativeRunner: boolean;
enableEnvironments: boolean;
enableIsolatedWorkspaces: boolean;
enableStreamlinedLeftNavigation: boolean;
@@ -0,0 +1,30 @@
export type NativeRuntimeMode = "legacy" | "native";
export const NATIVE_FINALIZATION_SCHEMA = "paperclip.native-finalization.v1" as const;
export type NativeReportedWorkDisposition = "done" | "blocked" | "needs_review" | "yielded";
export type NativeRunTerminalState = "succeeded" | "failed" | "cancelled";
export interface NativeFinalizationResultV1 {
schema: typeof NATIVE_FINALIZATION_SCHEMA;
runtimeMode: "native";
runId: string;
issueId: string;
companyId: string;
result: Record<string, unknown>;
terminal: {
schema: "paperclip.prp.terminal.v1";
turnTerminalState: "completed" | "failed" | "interrupted" | "cancelled";
runTerminalState: NativeRunTerminalState;
reportedWorkDisposition: NativeReportedWorkDisposition;
};
turnId: string | null;
sourceInstanceId: string;
normalizedSessionId: string;
providerSessionId: string | null;
driverKind: string;
driverVersion: string;
nativeEventCount: number;
highestContiguousSourceSeq: number;
workspaceFinalizeStatus: "pending" | "succeeded" | "failed";
}
export type NativeFinalizationResult = NativeFinalizationResultV1;
+6
View File
@@ -1,3 +1,9 @@
export {
nativeFinalizationResultSchema,
nativeReportedWorkDispositionSchema,
type NativeFinalizationResultInput,
} from "./native-finalization.js";
export {
decisionEffectStalenessSchema,
decisionOptionStyleSchema,
@@ -39,6 +39,7 @@ export const instanceGeneralSettingsSchema = z.object({
export const patchInstanceGeneralSettingsSchema = instanceGeneralSettingsSchema.partial();
export const instanceExperimentalSettingsSchema = z.object({
enableNativeRunner: z.boolean().default(false),
enableEnvironments: z.boolean().default(false),
enableIsolatedWorkspaces: z.boolean().default(false),
enableStreamlinedLeftNavigation: z.boolean().default(true),
@@ -0,0 +1,35 @@
import { z } from "zod";
import { NATIVE_FINALIZATION_SCHEMA } from "../types/native-finalization.js";
export const nativeFinalizationResultV1Schema = z.object({
schema: z.literal(NATIVE_FINALIZATION_SCHEMA),
runtimeMode: z.literal("native"),
runId: z.string().uuid(),
issueId: z.string().uuid(),
companyId: z.string().uuid(),
result: z.record(z.unknown()),
terminal: z.object({
schema: z.literal("paperclip.prp.terminal.v1"),
turnTerminalState: z.enum(["completed", "failed", "interrupted", "cancelled"]),
runTerminalState: z.enum(["succeeded", "failed", "cancelled"]),
reportedWorkDisposition: z.enum(["done", "blocked", "needs_review", "yielded"]),
}).strict(),
turnId: z.string().min(1).nullable(),
sourceInstanceId: z.string().min(1).max(160),
normalizedSessionId: z.string().min(1),
providerSessionId: z.string().min(1).nullable(),
driverKind: z.string().min(1),
driverVersion: z.string().min(1),
nativeEventCount: z.number().int().nonnegative(),
highestContiguousSourceSeq: z.number().int().nonnegative(),
workspaceFinalizeStatus: z.enum(["pending", "succeeded", "failed"]),
}).strict();
export const nativeReportedWorkDispositionSchema = z.enum([
"done",
"blocked",
"needs_review",
"yielded",
]);
export const nativeFinalizationResultSchema = nativeFinalizationResultV1Schema;
export type NativeFinalizationResultInput = z.infer<typeof nativeFinalizationResultSchema>;
+6
View File
@@ -122,6 +122,9 @@ importers:
packages/adapter-utils:
dependencies:
'@paperclipai/shared':
specifier: workspace:*
version: link:../shared
acpx:
specifier: 0.12.0
version: 0.12.0(patch_hash=x3fethhotv43zektyl5prdwf54)
@@ -803,6 +806,9 @@ importers:
'@paperclipai/hermes-paperclip-adapter':
specifier: workspace:*
version: link:../packages/adapters/hermes
'@paperclipai/paperclip-runner':
specifier: workspace:*
version: link:../packages/paperclip-runner
'@paperclipai/plugin-sdk':
specifier: workspace:*
version: link:../packages/plugins/sdk
+1
View File
@@ -57,6 +57,7 @@
"@paperclipai/adapter-utils": "workspace:*",
"@paperclipai/db": "workspace:*",
"@paperclipai/plugin-sdk": "workspace:*",
"@paperclipai/paperclip-runner": "workspace:*",
"@paperclipai/shared": "workspace:*",
"@paperclipai/skills-catalog": "workspace:*",
"@paperclipai/hermes-paperclip-adapter": "workspace:*",
+142
View File
@@ -0,0 +1,142 @@
import { and, asc, eq, or } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { heartbeatRunEvents, heartbeatRuns } from "@paperclipai/db";
import { nativeSha256 } from "./native-runtime/canonical.js";
export interface AppendHeartbeatRunEventInput {
companyId: string;
runId: string;
agentId: string;
eventType: string;
stream?: string | null;
level?: string | null;
color?: string | null;
message?: string | null;
payload?: Record<string, unknown> | null;
nativeSource?: {
sourceInstanceId: string;
sourceEventId: string;
sourceSeq: number;
protocolSchemaVersion: number;
canonicalPayload: Record<string, unknown>;
};
}
export interface AppendHeartbeatRunEventResult {
row: typeof heartbeatRunEvents.$inferSelect;
disposition: "committed" | "duplicate";
highestContiguousSourceSeq: number;
}
export class HeartbeatRunEventConflictError extends Error {
readonly code = "native_event_replay_conflict" as const;
constructor() {
super("native_event_replay_conflict");
this.name = "HeartbeatRunEventConflictError";
}
}
export async function appendHeartbeatRunEvent(
db: Db,
input: AppendHeartbeatRunEventInput,
): Promise<AppendHeartbeatRunEventResult> {
return db.transaction(async (tx) => {
const run = await tx
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, input.runId))
.for("update")
.limit(1)
.then((rows) => rows[0] ?? null);
if (!run || run.companyId !== input.companyId || run.agentId !== input.agentId) {
throw new Error("heartbeat_run_event_binding_mismatch");
}
const sourceHash = input.nativeSource
? nativeSha256(input.nativeSource.canonicalPayload)
: null;
if (input.nativeSource) {
const existing = await tx
.select()
.from(heartbeatRunEvents)
.where(and(
eq(heartbeatRunEvents.runId, input.runId),
or(
eq(heartbeatRunEvents.sourceEventId, input.nativeSource.sourceEventId),
and(
eq(heartbeatRunEvents.sourceInstanceId, input.nativeSource.sourceInstanceId),
eq(heartbeatRunEvents.sourceSeq, input.nativeSource.sourceSeq),
),
),
))
.limit(1)
.then((rows) => rows[0] ?? null);
if (existing) {
if (existing.sourcePayloadSha256 !== sourceHash) {
throw new HeartbeatRunEventConflictError();
}
return {
row: existing,
disposition: "duplicate" as const,
highestContiguousSourceSeq: await contiguousCursor(
tx as unknown as Db,
input.runId,
input.nativeSource.sourceInstanceId,
),
};
}
}
const seq = Number(run.nextEventSeq ?? 1);
const [row] = await tx.insert(heartbeatRunEvents).values({
companyId: input.companyId,
runId: input.runId,
agentId: input.agentId,
seq,
eventType: input.eventType,
stream: input.stream ?? null,
level: input.level ?? null,
color: input.color ?? null,
message: input.message ?? null,
payload: input.payload ?? null,
sourceInstanceId: input.nativeSource?.sourceInstanceId ?? null,
sourceEventId: input.nativeSource?.sourceEventId ?? null,
sourceSeq: input.nativeSource?.sourceSeq ?? null,
sourcePayloadSha256: sourceHash,
protocolSchemaVersion: input.nativeSource?.protocolSchemaVersion ?? null,
}).returning();
if (!row) throw new Error("heartbeat_run_event_not_persisted");
await tx.update(heartbeatRuns).set({
nextEventSeq: seq + 1,
updatedAt: new Date(),
}).where(eq(heartbeatRuns.id, input.runId));
return {
row,
disposition: "committed" as const,
highestContiguousSourceSeq: input.nativeSource
? await contiguousCursor(
tx as unknown as Db,
input.runId,
input.nativeSource.sourceInstanceId,
)
: 0,
};
});
}
async function contiguousCursor(db: Db, runId: string, sourceInstanceId: string): Promise<number> {
const rows = await db
.select({ sourceSeq: heartbeatRunEvents.sourceSeq })
.from(heartbeatRunEvents)
.where(and(
eq(heartbeatRunEvents.runId, runId),
eq(heartbeatRunEvents.sourceInstanceId, sourceInstanceId),
))
.orderBy(asc(heartbeatRunEvents.sourceSeq));
let cursor = 0;
for (const row of rows) {
if (row.sourceSeq === cursor + 1) cursor += 1;
else if (row.sourceSeq !== null && row.sourceSeq > cursor + 1) break;
}
return cursor;
}
+215 -68
View File
@@ -40,6 +40,7 @@ import {
companySkillVersions,
companySkills as companySkillsTable,
companies,
completionContracts,
costEvents,
documentAnnotationComments,
documentAnnotationThreads,
@@ -80,6 +81,16 @@ import {
// git-credentials module became its canonical home; existing importers keep working.
export { scrubGitCredentialText };
import { publishLiveEvent } from "./live-events.js";
import { appendHeartbeatRunEvent } from "./heartbeat-run-events.js";
import {
buildNativeExecutionInput,
cancelNativeSession,
ensureNativeCompletionContract,
executePaperclipNativeSession,
finalizeNativeRun,
reconcileNativeFinalizations,
resolveNativeRuntimeMode,
} from "./native-runtime/index.js";
import { normalizeResponsibleUserDenialCode } from "./responsible-user-denial-run-outcomes.js";
import { getRunLogStore, type RunLogHandle } from "./run-log-store.js";
import { getServerAdapter, listAdapterModelProfiles, runningProcesses } from "../adapters/index.js";
@@ -9506,7 +9517,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
async function appendRunEvent(
run: typeof heartbeatRuns.$inferSelect,
seq: number,
_seq: number,
event: {
eventType: string;
stream?: "system" | "stdout" | "stderr";
@@ -9536,11 +9547,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
at: eventAt,
});
await db.insert(heartbeatRunEvents).values({
const persistedEvent = await appendHeartbeatRunEvent(db, {
companyId: run.companyId,
runId: run.id,
agentId: run.agentId,
seq,
eventType: event.eventType,
stream: event.stream,
level: event.level,
@@ -9548,6 +9558,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
message: sanitizedMessage,
payload: sanitizedPayload,
});
const seq = persistedEvent.row.seq;
publishLiveEvent({
companyId: run.companyId,
@@ -9587,10 +9598,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
async function nextRunEventSeq(runId: string) {
const [row] = await db
.select({ maxSeq: sql<number | null>`max(${heartbeatRunEvents.seq})` })
.from(heartbeatRunEvents)
.where(eq(heartbeatRunEvents.runId, runId));
return Number(row?.maxSeq ?? 0) + 1;
.select({ nextEventSeq: heartbeatRuns.nextEventSeq })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, runId));
return Number(row?.nextEventSeq ?? 1);
}
async function persistRunProcessMetadata(
@@ -13008,6 +13019,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
const staleThresholdMs = opts?.staleThresholdMs ?? 0;
const now = new Date();
// Complete persisted native results before generic orphan recovery. The reconciler reads
// the durable workspace barrier and persisted runtime mode, never the current feature flag.
await reconcileNativeFinalizations(db).catch((error) => {
logger.warn({ err: error }, "failed to reconcile persisted native finalizations before orphan reaping");
});
// Find all runs stuck in "running" state (queued runs are legitimately waiting; resumeQueuedRuns handles them)
const activeRuns = await db
.select({
@@ -15249,11 +15266,106 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
};
const adapter = getServerAdapter(agent.adapterType);
// Runtime selection is immutable once persisted. In particular, turning the instance flag
// off prevents new native runs without changing the recovery path for an already-native run.
const nativeRuntimeResolution = run.runtimeModeResolvedAt
? run.runtimeMode === "native"
? {
kind: "native" as const,
resolverVersion: "phase6-v1" as const,
reason: "eligible_opt_in" as const,
profile: { mode: "native" as const, backend: "codex_app_server" as const, protocolVersion: 1 as const },
}
: {
kind: "legacy" as const,
resolverVersion: "phase6-v1" as const,
reason: run.runtimeModeReason ?? "persisted_legacy_selection",
}
: resolveNativeRuntimeMode({
enabled: resolvedInstanceSettings.experimental.enableNativeRunner === true,
runtimeConfig: agent.runtimeConfig,
agent: { status: runningAgent.status, adapterType: agent.adapterType },
issue: issueRef,
target: executionTarget,
workspaceId: persistedExecutionWorkspace?.id ?? null,
});
let nativeExecution: ReturnType<typeof buildNativeExecutionInput> | null = null;
let nativeRunnerInstanceId: string | null = null;
if (nativeRuntimeResolution.kind === "native") {
if (!issueRef || !persistedExecutionWorkspace) {
throw new Error("native_runtime_ineligible: issue and realized workspace are required");
}
const persistedContract = run.completionContractId
? await db.select().from(completionContracts)
.where(and(
eq(completionContracts.id, run.completionContractId),
eq(completionContracts.companyId, agent.companyId),
eq(completionContracts.issueId, issueRef.id),
))
.limit(1)
.then((rows) => rows[0] ?? null)
: null;
const completionContract = persistedContract
? { row: persistedContract, contract: persistedContract.contractJson as never }
: await ensureNativeCompletionContract({
db,
companyId: agent.companyId,
issue: issueRef,
actorId: agent.id,
});
nativeRunnerInstanceId = run.runnerInstanceId ?? randomUUID();
const nativeSessionId = run.nativeSessionId ?? randomUUID();
nativeExecution = buildNativeExecutionInput({
companyId: agent.companyId,
runId: run.id,
issue: issueRef,
agentId: agent.id,
workspace: {
id: persistedExecutionWorkspace.id,
cwd: executionWorkspace.cwd,
repoUrl: executionWorkspace.repoUrl,
repoRef: executionWorkspace.repoRef,
branchName: executionWorkspace.branchName,
},
normalizedSessionId: nativeSessionId,
completionContract: {
id: completionContract.row.id,
sha256: completionContract.row.canonicalSha256,
schemaVersion: completionContract.row.schemaVersion,
contract: completionContract.contract,
},
});
await db.update(heartbeatRuns).set({
runtimeMode: "native",
runtimeModeResolverVersion: nativeRuntimeResolution.resolverVersion,
runtimeModeReason: nativeRuntimeResolution.reason,
runtimeModeResolvedAt: run.runtimeModeResolvedAt ?? new Date(),
runnerProfileJson: nativeRuntimeResolution.profile,
runnerInstanceId: nativeRunnerInstanceId,
nativeSessionId,
driverKind: "codex_app_server",
driverVersion: "phase6-v1",
completionContractId: completionContract.row.id,
completionContractSha256: completionContract.row.canonicalSha256,
nativePhase: run.nativePhase ?? "selected",
nativePhaseUpdatedAt: run.nativePhaseUpdatedAt ?? new Date(),
updatedAt: new Date(),
}).where(eq(heartbeatRuns.id, run.id));
} else {
await db.update(heartbeatRuns).set({
runtimeMode: "legacy",
runtimeModeResolverVersion: nativeRuntimeResolution.resolverVersion,
runtimeModeReason: nativeRuntimeResolution.reason,
runtimeModeResolvedAt: run.runtimeModeResolvedAt ?? new Date(),
runnerProfileJson: null,
updatedAt: new Date(),
}).where(eq(heartbeatRuns.id, run.id));
}
const localAgentJwtScope =
issueRef?.workMode === "skill_test"
? { kind: "skill_test" as const, issueId: issueRef.id }
: { kind: "standard" as const };
const authToken = adapter.supportsLocalAgentJwt
const authToken = nativeRuntimeResolution.kind === "legacy" && adapter.supportsLocalAgentJwt
? createLocalAgentJwt(
agent.id,
agent.companyId,
@@ -15263,7 +15375,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
localAgentJwtScope,
)
: null;
if (adapter.supportsLocalAgentJwt && !authToken) {
if (nativeRuntimeResolution.kind === "legacy" && adapter.supportsLocalAgentJwt && !authToken) {
logger.warn(
{
companyId: agent.companyId,
@@ -15452,61 +15564,66 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
adapterFinalizeOutcome = status;
};
let adapterResult: Awaited<ReturnType<typeof adapter.execute>>;
let adapterResult: AdapterExecutionResult;
try {
const adapterContext = { ...context };
const runtimeMcpServers = await buildPaperclipRuntimeMcpServers({
db,
agent,
runId: run.id,
});
const runtimeMcp = createAdapterRuntimeMcpAccess(runtimeMcpServers);
const managedMcpConfig = await createManagedMcpRunConfig({
db,
agent,
runId: run.id,
config: runtimeConfig,
projectId: issueRef?.projectId ?? null,
issueId: issueRef?.id ?? null,
});
if (managedMcpConfig) {
adapterContext.paperclipManagedMcp = managedMcpConfig;
if (nativeRuntimeResolution.kind === "native") {
if (!nativeExecution || !nativeRunnerInstanceId) throw new Error("native_runtime_selection_not_persisted");
adapterResult = await executePaperclipNativeSession({
db,
execution: nativeExecution,
runnerInstanceId: nativeRunnerInstanceId,
});
} else {
const adapterContext = { ...context };
const runtimeMcpServers = await buildPaperclipRuntimeMcpServers({
db,
agent,
runId: run.id,
});
const runtimeMcp = createAdapterRuntimeMcpAccess(runtimeMcpServers);
const managedMcpConfig = await createManagedMcpRunConfig({
db,
agent,
runId: run.id,
config: runtimeConfig,
projectId: issueRef?.projectId ?? null,
issueId: issueRef?.id ?? null,
});
if (managedMcpConfig) {
adapterContext.paperclipManagedMcp = managedMcpConfig;
}
adapterResult = await adapter.execute({
runId: run.id,
agent,
runtime: runtimeForAdapter,
config: runtimeConfig,
context: adapterContext,
runtimeCommandSpec: adapter.getRuntimeCommandSpec?.(runtimeConfig) ?? null,
executionTarget,
executionTransport: remoteExecution
? { remoteExecution: remoteExecution as unknown as Record<string, unknown> }
: undefined,
runtimeMcp,
onLog,
onMeta: onAdapterMeta,
onEvent: onAdapterEvent,
startupTraceContext: getStartupTraceContext(),
onRuntimeProgress: async (progress) => {
await recordCurrentHeartbeatRunRuntimeProgress(run, progress, issueId);
},
onSpawn: async (meta) => {
await persistRunProcessMetadata(run.id, {
pid: meta.pid,
processGroupId:
"processGroupId" in meta && typeof meta.processGroupId === "number"
? meta.processGroupId
: null,
startedAt: meta.startedAt,
});
},
authToken: authToken ?? undefined,
});
}
adapterResult = await adapter.execute({
runId: run.id,
agent,
runtime: runtimeForAdapter,
config: runtimeConfig,
context: adapterContext,
runtimeCommandSpec: adapter.getRuntimeCommandSpec?.(runtimeConfig) ?? null,
executionTarget,
executionTransport: remoteExecution
? { remoteExecution: remoteExecution as unknown as Record<string, unknown> }
: undefined,
runtimeMcp,
onLog,
onMeta: onAdapterMeta,
onEvent: onAdapterEvent,
// The endpoint-gated OpenTelemetry startup trace context. It is a
// no-op unless `OTEL_EXPORTER_OTLP_ENDPOINT` is set and the OTel
// packages are installed, so the sandbox-start span path stays inert
// by default.
startupTraceContext: getStartupTraceContext(),
onRuntimeProgress: async (progress) => {
await recordCurrentHeartbeatRunRuntimeProgress(run, progress, issueId);
},
onSpawn: async (meta) => {
await persistRunProcessMetadata(run.id, {
pid: meta.pid,
processGroupId:
"processGroupId" in meta && typeof meta.processGroupId === "number"
? meta.processGroupId
: null,
startedAt: meta.startedAt,
});
},
authToken: authToken ?? undefined,
});
// Adapter returned cleanly, which means its workspace-restore finally
// block also ran without throwing. Record the workspace_finalize
// barrier so dependents that share this executionWorkspace can wake.
@@ -15514,6 +15631,17 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
// rather than silently leaving dependents stranded behind a missing
// finalize row.
await recordWorkspaceFinalize("succeeded");
if (adapterResult.nativeFinalization) {
adapterResult.nativeFinalization.workspaceFinalizeStatus = "succeeded";
try {
await finalizeNativeRun({ db, runId: run.id, workspaceFinalizeStatus: "succeeded" });
} catch (finalizeErr) {
logger.warn(
{ err: finalizeErr, runId: run.id },
"native result persisted but finalization did not apply; the reconciliation loop will retry",
);
}
}
} catch (adapterErr) {
// Adapter (or its restore finally) threw — or the finalize record
// write itself threw. Either way the workspace may be in a partial
@@ -15530,6 +15658,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
"failed to record workspace_finalize=failed operation; dependents may remain gated",
);
}
if (nativeRuntimeResolution.kind === "native") {
try {
await finalizeNativeRun({ db, runId: run.id, workspaceFinalizeStatus: "failed" });
} catch (finalizeErr) {
logger.warn(
{ err: finalizeErr, runId: run.id },
"native result could not be marked workspace_failed; the reconciliation loop will retry persisted results",
);
}
}
throw adapterErr;
} finally {
try {
@@ -15628,6 +15766,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
const latestRun = await getRun(run.id);
if (isHeartbeatRunTerminalStatus(latestRun?.status)) {
outcome = latestRun.status;
} else if (adapterResult.nativeFinalization) {
const nativeTerminal = adapterResult.nativeFinalization.terminal.runTerminalState;
outcome = nativeTerminal === "succeeded"
? "succeeded"
: nativeTerminal === "cancelled"
? "cancelled"
: "failed";
} else if (adapterResult.timedOut) {
outcome = "timed_out";
} else if ((adapterResult.exitCode ?? 0) === 0 && !adapterResult.errorMessage) {
@@ -15737,6 +15882,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
resultJson: mergeModelProfileRunMetadata(
mergeAdapterRecoveryMetadata({
resultJson: {
...(adapterResult.nativeFinalization ? parseObject(latestRun?.resultJson) : {}),
...parseObject(adapterResult.resultJson),
configFreshness: configFreshnessResultMetadata,
},
@@ -17507,16 +17653,12 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
.where(and(eq(issues.id, issue.id), eq(issues.executionRunId, scheduledRun.id)));
}
const [eventSeq] = await tx
.select({ maxSeq: sql<number | null>`max(${heartbeatRunEvents.seq})` })
.from(heartbeatRunEvents)
.where(eq(heartbeatRunEvents.runId, cancelled.id));
const eventSeq = Number(cancelled.nextEventSeq ?? 1);
await tx.insert(heartbeatRunEvents).values({
companyId: cancelled.companyId,
runId: cancelled.id,
agentId: cancelled.agentId,
seq: Number(eventSeq?.maxSeq ?? 0) + 1,
seq: eventSeq,
eventType: "lifecycle",
stream: "system",
level: "warn",
@@ -17533,6 +17675,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
currentAssigneeAgentId: issue.assigneeAgentId,
},
});
await tx
.update(heartbeatRuns)
.set({ nextEventSeq: eventSeq + 1, updatedAt: now })
.where(eq(heartbeatRuns.id, cancelled.id));
return true;
};
@@ -18473,6 +18619,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
const running = runningProcesses.get(run.id);
try {
await cancelNativeSession(run.id, reason);
if (running) {
await terminateHeartbeatRunProcess({
pid: running.child.pid ?? run.processPid,
+2
View File
@@ -207,6 +207,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
const parsed = instanceExperimentalSettingsStorageSchema.safeParse(raw ?? {});
if (parsed.success) {
return {
enableNativeRunner: parsed.data.enableNativeRunner ?? false,
enableEnvironments: parsed.data.enableEnvironments ?? false,
enableIsolatedWorkspaces: parsed.data.enableIsolatedWorkspaces ?? false,
enableStreamlinedLeftNavigation: parsed.data.enableStreamlinedLeftNavigation ?? true,
@@ -243,6 +244,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
};
}
return {
enableNativeRunner: false,
enableEnvironments: false,
enableIsolatedWorkspaces: false,
enableStreamlinedLeftNavigation: true,
+2
View File
@@ -3099,6 +3099,8 @@ const issueListSelect = {
END
`,
status: issues.status,
statusVersion: issues.statusVersion,
lastStatusDecisionId: issues.lastStatusDecisionId,
workMode: issues.workMode,
harnessKind: issues.harnessKind,
priority: issues.priority,
@@ -0,0 +1,18 @@
import { createHash } from "node:crypto";
export function canonicalNativeJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalNativeJson).join(",")}]`;
if (typeof value === "object" && value !== null) {
const record = value as Record<string, unknown>;
return `{${Object.keys(record)
.filter((key) => record[key] !== undefined)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalNativeJson(record[key])}`)
.join(",")}}`;
}
return JSON.stringify(value) ?? "null";
}
export function nativeSha256(value: unknown): string {
return createHash("sha256").update(canonicalNativeJson(value), "utf8").digest("hex");
}
@@ -0,0 +1,68 @@
import { and, desc, eq } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { completionContracts } from "@paperclipai/db";
import type { StrictCompletionContractInput } from "@paperclipai/paperclip-runner";
import { nativeSha256 } from "./canonical.js";
export const NATIVE_COMPLETION_CONTRACT_SCHEMA = "paperclip.completion-contract.v1";
export const NATIVE_COMPLETION_POLICY_VERSION = "phase6-v1";
export async function ensureNativeCompletionContract(input: {
db: Db;
companyId: string;
issue: { id: string; title: string; description: string | null };
actorId: string;
}) {
const contract: StrictCompletionContractInput = {
revision: "1",
objective: input.issue.title,
criteria: [{
id: "objective",
requirement: input.issue.description?.trim() || `Complete: ${input.issue.title}`,
}],
};
const canonicalSha256 = nativeSha256({
schemaVersion: NATIVE_COMPLETION_CONTRACT_SCHEMA,
policyVersion: NATIVE_COMPLETION_POLICY_VERSION,
contract,
});
const existing = await input.db
.select()
.from(completionContracts)
.where(and(
eq(completionContracts.companyId, input.companyId),
eq(completionContracts.issueId, input.issue.id),
eq(completionContracts.canonicalSha256, canonicalSha256),
))
.limit(1)
.then((rows) => rows[0] ?? null);
if (existing) return { row: existing, contract };
const latest = await input.db
.select({ revision: completionContracts.revision, id: completionContracts.id })
.from(completionContracts)
.where(and(
eq(completionContracts.companyId, input.companyId),
eq(completionContracts.issueId, input.issue.id),
))
.orderBy(desc(completionContracts.revision))
.limit(1)
.then((rows) => rows[0] ?? null);
const [row] = await input.db.insert(completionContracts).values({
companyId: input.companyId,
issueId: input.issue.id,
revision: (latest?.revision ?? 0) + 1,
schemaVersion: NATIVE_COMPLETION_CONTRACT_SCHEMA,
policyVersion: NATIVE_COMPLETION_POLICY_VERSION,
risk: "standard",
completionAuthority: "server_arbiter",
incompleteCriteriaPolicy: "preserve_non_terminal",
contractJson: contract as unknown as Record<string, unknown>,
canonicalSha256,
createdByActorType: "system",
createdByActorId: input.actorId,
supersedesContractId: latest?.id ?? null,
}).returning();
if (!row) throw new Error("native_completion_contract_not_persisted");
return { row, contract };
}
@@ -0,0 +1,42 @@
export interface NativeEvidenceAssessment {
objectiveSatisfied: boolean;
allCriteriaSatisfied: boolean;
verificationPassed: boolean;
hasBlockingRemainingWork: boolean;
reportedDisposition: "done" | "blocked" | "needs_review" | "yielded";
summary: string;
blocker: { unblockAction: string; boardOwned: boolean } | null;
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
export function classifyNativeEvidence(result: Record<string, unknown>): NativeEvidenceAssessment {
const claim = record(result.completionClaim);
const criteria = Array.isArray(claim.criteria) ? claim.criteria.map(record) : [];
const remaining = Array.isArray(claim.remainingWork) ? claim.remainingWork.map(record) : [];
const verification = Array.isArray(result.verification) ? result.verification.map(record) : [];
const reported = result.reportedWorkDisposition;
const blocker = record(result.blocker);
const blockerOwner = record(blocker.owner);
if (!["done", "blocked", "needs_review", "yielded"].includes(String(reported))) {
throw new Error("native_result_invalid_disposition");
}
return {
objectiveSatisfied: claim.objectiveSatisfied === true,
allCriteriaSatisfied: criteria.length > 0 && criteria.every((criterion) => criterion.status === "satisfied"),
verificationPassed: verification.length > 0 && verification.every((entry) => entry.status === "passed"),
hasBlockingRemainingWork: remaining.some((entry) => entry.blocksCompletion === true),
reportedDisposition: reported as NativeEvidenceAssessment["reportedDisposition"],
summary: typeof result.summary === "string" ? result.summary : "Native run completed.",
blocker: reported === "blocked" && typeof blocker.unblockAction === "string"
? {
unblockAction: blocker.unblockAction,
boardOwned: blockerOwner.kind === "board" || blockerOwner.owner === "board",
}
: null,
};
}
@@ -0,0 +1,8 @@
export * from "./runtime-mode.js";
export * from "./completion-contracts.js";
export * from "./native-execution-input.js";
export * from "./native-session-executor.js";
export * from "./paperclip-control-plane-port.js";
export * from "./native-run-finalizer.js";
export * from "./native-finalization-reconciler.js";
export * from "./status-arbiter.js";
@@ -0,0 +1,64 @@
import type { NativeExecutionInputV1, StrictCompletionContractInput } from "@paperclipai/paperclip-runner";
import { parseNativeExecutionInput } from "@paperclipai/paperclip-runner";
/** Closed constructor: callers cannot spread legacy context or environment data. */
export function buildNativeExecutionInput(input: {
companyId: string;
runId: string;
issue: {
id: string;
identifier: string | null;
title: string;
description: string | null;
workMode: string;
};
agentId: string;
workspace: {
id: string;
cwd: string;
repoUrl: string | null;
repoRef: string | null;
branchName: string | null;
};
normalizedSessionId: string | null;
completionContract: {
id: string;
sha256: string;
schemaVersion: string;
contract: StrictCompletionContractInput;
};
}): NativeExecutionInputV1 {
if (input.issue.workMode !== "standard") {
throw new Error("native_execution_input_invalid: issue work mode must be standard");
}
return parseNativeExecutionInput({
schema: "paperclip.native-execution-input.v1",
binding: {
companyId: input.companyId,
runId: input.runId,
issueId: input.issue.id,
agentId: input.agentId,
executionWorkspaceId: input.workspace.id,
},
task: {
identifier: input.issue.identifier ?? input.issue.id,
title: input.issue.title,
description: input.issue.description,
workMode: "standard",
},
workspace: {
cwd: input.workspace.cwd,
repoUrl: input.workspace.repoUrl,
repoRef: input.workspace.repoRef,
branchName: input.workspace.branchName,
},
session: {
normalizedSessionId: input.normalizedSessionId,
driverKind: "codex_app_server",
protocolVersion: 1,
},
completionContract: input.completionContract,
interactionResponses: [],
credentialBindings: [],
});
}
@@ -0,0 +1,37 @@
import { and, desc, eq, inArray, isNotNull, ne } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { heartbeatRuns, nativeRunFinalizations, workspaceOperations } from "@paperclipai/db";
import { finalizeNativeRun } from "./native-run-finalizer.js";
/** Recovery is keyed only by persisted mode/coordinator state, never the live flag. */
export async function reconcileNativeFinalizations(db: Db, runIds?: string[]) {
const rows = await db.select({ runId: heartbeatRuns.id })
.from(heartbeatRuns)
.innerJoin(nativeRunFinalizations, eq(nativeRunFinalizations.runId, heartbeatRuns.id))
.where(and(
eq(heartbeatRuns.runtimeMode, "native"),
isNotNull(nativeRunFinalizations.resultId),
ne(nativeRunFinalizations.phase, "applied"),
...(runIds && runIds.length > 0 ? [inArray(heartbeatRuns.id, runIds)] : []),
));
const results = [];
for (const row of rows) {
const barrier = await db.select({ status: workspaceOperations.status })
.from(workspaceOperations)
.where(and(
eq(workspaceOperations.heartbeatRunId, row.runId),
eq(workspaceOperations.phase, "workspace_finalize"),
))
.orderBy(desc(workspaceOperations.createdAt))
.limit(1)
.then((entries) => entries[0] ?? null);
if (!barrier || !["succeeded", "failed"].includes(barrier.status)) continue;
results.push(await finalizeNativeRun({
db,
runId: row.runId,
workspaceFinalizeStatus: barrier.status as "succeeded" | "failed",
projectRunStatus: true,
}));
}
return results;
}
@@ -0,0 +1,12 @@
/**
* Phase 6 never auto-approves model requests. Unsupported native requests are
* declined explicitly; typed response materialization stays in existing
* interaction/approval services.
*/
export function rejectUnsupportedNativeRuntimeRequest(requestKind: string) {
return {
accepted: false as const,
code: "native_runtime_request_unsupported" as const,
requestKind,
};
}
@@ -0,0 +1 @@
export { PaperclipControlPlanePort } from "./paperclip-control-plane-port.js";
@@ -0,0 +1,156 @@
import { and, eq, inArray } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
approvals,
heartbeatRuns,
issueApprovals,
issues,
nativeRunFinalizations,
nativeRunResults,
} from "@paperclipai/db";
import { classifyNativeEvidence } from "./evidence-classifier.js";
import { arbitrateNativeStatus, NATIVE_STATUS_ARBITER_POLICY_VERSION } from "./status-arbiter.js";
import { recordNativeWorkAssessment } from "./work-assessments.js";
import { commitNativeStatusDecision, NativeStatusRaceError } from "./status-decision-committer.js";
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
export async function finalizeNativeRun(input: {
db: Db;
runId: string;
workspaceFinalizeStatus: "succeeded" | "failed";
/** Reconciliation owns terminal run projection; the live heartbeat does it afterward. */
projectRunStatus?: boolean;
}) {
const run = await input.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, input.runId))
.limit(1).then((rows) => rows[0] ?? null);
if (!run || run.runtimeMode !== "native") throw new Error("native_finalization_run_missing");
const coordinator = await input.db.select().from(nativeRunFinalizations)
.where(eq(nativeRunFinalizations.runId, input.runId)).limit(1)
.then((rows) => rows[0] ?? null);
if (!coordinator?.resultId) throw new Error("native_finalization_missing");
if (coordinator.phase === "applied" && coordinator.decisionId) return coordinator;
if (input.workspaceFinalizeStatus === "failed") {
const now = new Date();
await input.db.update(nativeRunFinalizations).set({
phase: "workspace_failed",
failureCode: "workspace_finalization_failed",
failureDetail: "Native result preserved; issue status intentionally unchanged.",
updatedAt: now,
}).where(eq(nativeRunFinalizations.runId, input.runId));
await input.db.update(heartbeatRuns).set({
...(input.projectRunStatus ? { status: "failed", finishedAt: now } : {}),
nativePhase: "workspace_failed",
nativePhaseUpdatedAt: now,
resultJson: {
...record(run.resultJson),
finalizationPhase: "workspace_failed",
workspaceFinalizeStatus: "failed",
},
updatedAt: now,
}).where(eq(heartbeatRuns.id, run.id));
return { ...coordinator, phase: "workspace_failed" };
}
const resultRow = await input.db.select().from(nativeRunResults)
.where(eq(nativeRunResults.id, coordinator.resultId)).limit(1)
.then((rows) => rows[0] ?? null);
if (!resultRow) throw new Error("native_result_missing");
const envelope = record(resultRow.resultJson);
const result = record(envelope.result);
const terminal = record(envelope.terminal);
const terminalState = terminal.runTerminalState;
if (!["succeeded", "failed", "cancelled"].includes(String(terminalState))) {
throw new Error("native_finalization_invalid");
}
const assessment = classifyNativeEvidence(result);
let supersedesAssessmentId: string | null = null;
for (let attempt = 0; attempt < 2; attempt += 1) {
const issue = await input.db.select().from(issues).where(eq(issues.id, coordinator.issueId))
.limit(1).then((rows) => rows[0] ?? null);
const authoritativeIssue = issue ?? await input.db.select().from(issues)
.where(eq(issues.id, coordinator.issueId)).limit(1).then((rows) => rows[0] ?? null);
if (!authoritativeIssue || authoritativeIssue.companyId !== run.companyId) {
throw new Error("native_finalization_issue_missing");
}
const decision = arbitrateNativeStatus({
assessment,
terminalState: terminalState as "succeeded" | "failed" | "cancelled",
workspaceFinalizeStatus: input.workspaceFinalizeStatus,
approvalGateSatisfied: !await input.db.select({ id: approvals.id })
.from(issueApprovals)
.innerJoin(approvals, eq(issueApprovals.approvalId, approvals.id))
.where(and(
eq(issueApprovals.companyId, run.companyId),
eq(issueApprovals.issueId, authoritativeIssue.id),
eq(approvals.companyId, run.companyId),
inArray(approvals.status, ["pending", "revision_requested"]),
))
.limit(1)
.then((rows) => rows.length > 0),
agentId: run.agentId,
});
const assessmentRow = await recordNativeWorkAssessment({
db: input.db,
companyId: run.companyId,
issueId: authoritativeIssue.id,
runId: run.id,
turnId: resultRow.turnId,
contractId: resultRow.completionContractId,
resultId: resultRow.id,
priorIssueStatus: authoritativeIssue.status,
priorStatusVersion: Number(authoritativeIssue.statusVersion),
priorDecisionId: authoritativeIssue.lastStatusDecisionId,
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
assessment,
supersedesAssessmentId,
});
try {
const committed = await commitNativeStatusDecision({
db: input.db,
companyId: run.companyId,
issueId: authoritativeIssue.id,
runId: run.id,
assessmentId: assessmentRow.id,
priorStatus: authoritativeIssue.status,
priorStatusVersion: Number(authoritativeIssue.statusVersion),
decision,
});
await input.db.update(heartbeatRuns).set({
...(input.projectRunStatus ? {
status: terminalState === "succeeded" ? "succeeded" : terminalState === "cancelled" ? "cancelled" : "failed",
finishedAt: new Date(),
} : {}),
nativePhase: "applied",
nativePhaseUpdatedAt: new Date(),
resultJson: {
...record(run.resultJson),
finalizationPhase: "applied",
assessmentId: assessmentRow.id,
decisionId: committed.decision.id,
authoritativeDecision: decision.toStatus,
issueStatusBefore: authoritativeIssue.status,
issueStatusAfter: committed.issue.status,
statusVersionBefore: Number(authoritativeIssue.statusVersion),
statusVersionAfter: Number(committed.issue.statusVersion),
workspaceFinalizeStatus: input.workspaceFinalizeStatus,
},
updatedAt: new Date(),
}).where(eq(heartbeatRuns.id, run.id));
return {
...coordinator,
phase: "applied",
assessmentId: assessmentRow.id,
decisionId: committed.decision.id,
};
} catch (error) {
if (!(error instanceof NativeStatusRaceError) || attempt === 1) throw error;
supersedesAssessmentId = assessmentRow.id;
}
}
throw new Error("native_finalization_status_race");
}
@@ -0,0 +1,65 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Db } from "@paperclipai/db";
import type { NativeExecutionInputV1 } from "@paperclipai/paperclip-runner";
const state = vi.hoisted(() => ({
execute: vi.fn(),
cancel: vi.fn(),
release: null as null | (() => void),
}));
vi.mock("@paperclipai/paperclip-runner", () => ({
createCodexNativeSessionBackend: vi.fn(() => ({ kind: "test" })),
executeNativeSession: state.execute,
}));
import { cancelNativeSession, executePaperclipNativeSession } from "./native-session-executor.js";
const execution = {
binding: {
companyId: "company",
runId: "run-native-cancel",
issueId: "issue",
agentId: "agent",
},
completionContract: { id: "contract", sha256: "sha" },
} as NativeExecutionInputV1;
describe("native session cancellation", () => {
beforeEach(() => {
state.cancel.mockReset();
state.release = null;
state.execute.mockReset().mockImplementation(async (options) => {
options.onSession?.({ cancel: state.cancel });
await new Promise<void>((resolve) => { state.release = resolve; });
options.onSession?.(null);
return {
result: { summary: "cancelled" },
terminal: { runTerminalState: "cancelled" },
turnId: "turn",
normalizedSessionId: "session",
providerSessionId: null,
driverKind: "test",
driverVersion: "1",
nativeEventCount: 1,
highestContiguousSourceSeq: 1,
};
});
});
it("routes control-plane cancellation to the active normalized session and removes the handle", async () => {
const running = executePaperclipNativeSession({
db: {} as Db,
execution,
runnerInstanceId: "runner",
});
await vi.waitFor(() => expect(state.release).toBeTypeOf("function"));
await expect(cancelNativeSession(execution.binding.runId, "budget hard stop")).resolves.toBe(true);
expect(state.cancel).toHaveBeenCalledWith({ reason: "budget hard stop" });
state.release?.();
await running;
await expect(cancelNativeSession(execution.binding.runId, "late cancel")).resolves.toBe(false);
});
});
@@ -0,0 +1,84 @@
import type { AdapterExecutionResult } from "../../adapters/index.js";
import type { NativeFinalizationResult } from "@paperclipai/shared";
import type { NativeExecutionInputV1, NativeSession } from "@paperclipai/paperclip-runner";
import {
createCodexNativeSessionBackend,
executeNativeSession,
} from "@paperclipai/paperclip-runner";
import type { Db } from "@paperclipai/db";
import { PaperclipControlPlanePort } from "./paperclip-control-plane-port.js";
const activeNativeSessions = new Map<string, NativeSession>();
export async function cancelNativeSession(runId: string, reason: string): Promise<boolean> {
const session = activeNativeSessions.get(runId);
if (!session) return false;
if (session.cancel) await session.cancel({ reason });
else if (session.interrupt) await session.interrupt({ reason });
return true;
}
export async function executePaperclipNativeSession(input: {
db: Db;
execution: NativeExecutionInputV1;
runnerInstanceId: string;
}): Promise<AdapterExecutionResult> {
const controlPlaneInstanceId = `${input.runnerInstanceId}:control`;
const controlPlane = new PaperclipControlPlanePort(input.db, {
companyId: input.execution.binding.companyId,
issueId: input.execution.binding.issueId,
runId: input.execution.binding.runId,
agentId: input.execution.binding.agentId,
completionContractId: input.execution.completionContract.id,
completionContractSha256: input.execution.completionContract.sha256,
sourceInstanceId: input.runnerInstanceId,
controlPlaneSourceInstanceId: controlPlaneInstanceId,
});
const native = await executeNativeSession({
input: input.execution,
backend: createCodexNativeSessionBackend(input.execution, { runnerInstanceId: input.runnerInstanceId }),
controlPlane,
runnerInstanceId: input.runnerInstanceId,
controlPlaneInstanceId,
onSession: (session) => {
if (session) activeNativeSessions.set(input.execution.binding.runId, session);
else activeNativeSessions.delete(input.execution.binding.runId);
},
});
const finalization: NativeFinalizationResult = {
schema: "paperclip.native-finalization.v1",
runtimeMode: "native",
runId: input.execution.binding.runId,
issueId: input.execution.binding.issueId,
companyId: input.execution.binding.companyId,
result: native.result as unknown as Record<string, unknown>,
terminal: native.terminal,
turnId: native.turnId,
sourceInstanceId: input.runnerInstanceId,
normalizedSessionId: native.normalizedSessionId,
providerSessionId: native.providerSessionId,
driverKind: native.driverKind,
driverVersion: native.driverVersion,
nativeEventCount: native.nativeEventCount,
highestContiguousSourceSeq: native.highestContiguousSourceSeq,
workspaceFinalizeStatus: "pending",
};
return {
exitCode: native.terminal.runTerminalState === "succeeded" ? 0 : 1,
signal: null,
timedOut: false,
errorMessage: native.terminal.runTerminalState === "succeeded"
? null
: `Native session ${native.terminal.runTerminalState}`,
resultJson: {
nativeResult: native.result as unknown as Record<string, unknown>,
nativeTerminal: native.terminal as unknown as Record<string, unknown>,
},
summary: native.result.summary,
sessionId: native.normalizedSessionId,
sessionDisplayId: native.providerSessionId ?? native.normalizedSessionId,
provider: "openai",
usageBasis: "per_run",
nativeFinalization: finalization,
};
}
@@ -0,0 +1,347 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { eq } from "drizzle-orm";
import {
activityLog,
agents,
companies,
completionContracts,
createDb,
heartbeatRunEvents,
heartbeatRuns,
issues,
nativeRunFinalizations,
nativeRunResults,
statusDecisionEffects,
statusDecisions,
workAssessments,
} from "@paperclipai/db";
import {
CONTROL_PLANE_CONFORMANCE_RESULT,
CONTROL_PLANE_CONFORMANCE_TERMINAL,
CONTROL_PLANE_CONFORMANCE_OPEN,
executeNativeSession,
runControlPlanePortConformance,
type NativeSessionBackend,
type PrpEvent,
} from "@paperclipai/paperclip-runner";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "../../__tests__/helpers/embedded-postgres.js";
import { PaperclipControlPlanePort } from "./paperclip-control-plane-port.js";
import { finalizeNativeRun } from "./native-run-finalizer.js";
import { buildNativeExecutionInput } from "./native-execution-input.js";
const support = await getEmbeddedPostgresTestSupport();
const describeDatabase = support.supported ? describe : describe.skip;
describeDatabase("PaperclipControlPlanePort conformance", () => {
let temporary: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
let db: ReturnType<typeof createDb>;
const contractId = "00000000-0000-4000-8000-000000000004";
const contractSha = "phase6-conformance-contract";
const taskIssueId = "00000000-0000-4000-8000-000000000008";
const taskContractId = "00000000-0000-4000-8000-000000000009";
const taskRunId = "00000000-0000-4000-8000-000000000010";
const workspaceFailureIssueId = "00000000-0000-4000-8000-000000000011";
const workspaceFailureContractId = "00000000-0000-4000-8000-000000000012";
const workspaceFailureRunId = "00000000-0000-4000-8000-000000000013";
beforeAll(async () => {
temporary = await startEmbeddedPostgresTestDatabase("paperclip-native-port-");
db = createDb(temporary.connectionString);
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
await db.insert(companies).values({ id: identity.companyId, name: "Phase 6", issuePrefix: "P6C" });
await db.insert(agents).values({
id: identity.agentId,
companyId: identity.companyId,
name: "Native conformance",
adapterType: "codex_local",
status: "running",
});
await db.insert(issues).values({
id: identity.issueId,
companyId: identity.companyId,
title: "Native conformance",
status: "in_progress",
workMode: "standard",
});
await db.insert(completionContracts).values({
id: contractId,
companyId: identity.companyId,
issueId: identity.issueId,
revision: 1,
schemaVersion: "paperclip.completion-contract.v1",
policyVersion: "phase6-v1",
risk: "standard",
completionAuthority: "server_arbiter",
incompleteCriteriaPolicy: "preserve_non_terminal",
contractJson: { objective: "Conformance" },
canonicalSha256: contractSha,
createdByActorType: "system",
createdByActorId: "test",
});
await db.insert(heartbeatRuns).values({
id: identity.runId,
companyId: identity.companyId,
agentId: identity.agentId,
status: "running",
runtimeMode: "native",
completionContractId: contractId,
completionContractSha256: contractSha,
contextSnapshot: { issueId: identity.issueId },
});
await db.insert(issues).values({
id: taskIssueId,
companyId: identity.companyId,
title: "Complete one native task",
status: "in_progress",
workMode: "standard",
});
await db.insert(completionContracts).values({
id: taskContractId,
companyId: identity.companyId,
issueId: taskIssueId,
revision: 1,
schemaVersion: "paperclip.completion-contract.v1",
policyVersion: "phase6-v1",
risk: "standard",
completionAuthority: "server_arbiter",
incompleteCriteriaPolicy: "preserve_non_terminal",
contractJson: { objective: "Complete one native task" },
canonicalSha256: "phase6-task-contract",
createdByActorType: "system",
createdByActorId: "test",
});
await db.insert(heartbeatRuns).values({
id: taskRunId,
companyId: identity.companyId,
agentId: identity.agentId,
status: "running",
runtimeMode: "native",
runtimeModeResolverVersion: "phase6-v1",
runtimeModeReason: "eligible_opt_in",
completionContractId: taskContractId,
completionContractSha256: "phase6-task-contract",
contextSnapshot: { issueId: taskIssueId },
});
await db.insert(issues).values({
id: workspaceFailureIssueId,
companyId: identity.companyId,
title: "Preserve status after workspace failure",
status: "in_progress",
workMode: "standard",
});
await db.insert(completionContracts).values({
id: workspaceFailureContractId,
companyId: identity.companyId,
issueId: workspaceFailureIssueId,
revision: 1,
schemaVersion: "paperclip.completion-contract.v1",
policyVersion: "phase6-v1",
risk: "standard",
completionAuthority: "server_arbiter",
incompleteCriteriaPolicy: "preserve_non_terminal",
contractJson: { objective: "Preserve status after workspace failure" },
canonicalSha256: "phase6-workspace-failure-contract",
createdByActorType: "system",
createdByActorId: "test",
});
await db.insert(heartbeatRuns).values({
id: workspaceFailureRunId,
companyId: identity.companyId,
agentId: identity.agentId,
status: "running",
runtimeMode: "native",
completionContractId: workspaceFailureContractId,
completionContractSha256: "phase6-workspace-failure-contract",
contextSnapshot: { issueId: workspaceFailureIssueId },
});
}, 30_000);
afterAll(async () => {
if (temporary) {
await db.delete(activityLog);
await db.delete(statusDecisionEffects);
await db.delete(statusDecisions);
await db.delete(workAssessments);
await db.delete(nativeRunFinalizations);
await db.delete(nativeRunResults);
await db.delete(heartbeatRunEvents);
await db.delete(heartbeatRuns);
await db.delete(completionContracts);
await db.delete(issues);
await db.delete(agents);
await db.delete(companies);
await temporary.cleanup();
}
});
it("runs the unchanged package conformance suite against Paperclip persistence", async () => {
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
const port = new PaperclipControlPlanePort(db, {
companyId: identity.companyId,
issueId: identity.issueId,
runId: identity.runId,
agentId: identity.agentId,
completionContractId: contractId,
completionContractSha256: contractSha,
sourceInstanceId: "runner-phase6-conformance",
controlPlaneSourceInstanceId: "control-phase6-conformance",
});
await expect(runControlPlanePortConformance({ port })).resolves.toEqual({
eventCount: 3,
highestContiguousSourceSeq: 3,
duplicateDisposition: "duplicate",
terminalReplayIdempotent: true,
});
await expect(db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, identity.runId))).resolves.toHaveLength(1);
await finalizeNativeRun({ db, runId: identity.runId, workspaceFinalizeStatus: "succeeded" });
await expect(db.select().from(issues).where(eq(issues.id, identity.issueId))).resolves.toEqual([
expect.objectContaining({ status: "done", statusVersion: 1 }),
]);
await expect(db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, identity.runId))).resolves.toEqual([
expect.objectContaining({ phase: "applied" }),
]);
await expect(db.select().from(statusDecisions).where(eq(statusDecisions.issueId, identity.issueId))).resolves.toEqual([
expect.objectContaining({ toStatus: "done", applicationState: "applied" }),
]);
await expect(db.select().from(activityLog).where(eq(activityLog.entityId, identity.issueId))).resolves.toEqual([
expect.objectContaining({ action: "issue.native_status_decision_applied" }),
]);
});
it("completes one selected Paperclip task through the public package session contract", async () => {
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
const sessionId = "session-phase6-paperclip-task";
const event = (sourceSeq: number, eventType: PrpEvent["eventType"], payload: Record<string, unknown>): PrpEvent => ({
schema: "paperclip.prp.event.v1",
sourceEventId: `phase6-task:${sourceSeq}`,
sourceSeq,
sourceInstanceId: "phase6-scripted-runner",
sourceKind: "runner",
runId: taskRunId,
normalizedSessionId: sessionId,
turnId: "turn-phase6-paperclip-task",
eventType,
schemaVersion: 1,
priority: 0,
emittedAt: `2026-08-09T02:59:0${sourceSeq}.000Z`,
payload,
});
const events = [
event(1, "run.result.proposed", CONTROL_PLANE_CONFORMANCE_RESULT),
event(2, "turn.completed", {}),
];
const backend: NativeSessionBackend = {
async descriptor() {
return {
kind: "mock",
name: "phase6-scripted",
version: "1",
capabilities: { resume: false, typedEvents: true, steering: false, interruption: true, structuredResult: true },
};
},
async openSession(input) {
return {
identity: () => input.identity,
async capabilities() { return { resume: false, typedEvents: true, steering: false, interruption: true, structuredResult: true }; },
async *events() { yield* events; },
async startTurn() { return { turnId: "turn-phase6-paperclip-task" }; },
async cancel() {},
async result() { return { result: CONTROL_PLANE_CONFORMANCE_RESULT, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL, turnId: "turn-phase6-paperclip-task" }; },
async snapshot() { return { backendKind: "mock", sessionId, identity: input.identity, providerSessionId: "provider-phase6-paperclip-task" }; },
async close() {},
};
},
};
const execution = buildNativeExecutionInput({
companyId: identity.companyId,
runId: taskRunId,
issue: { id: taskIssueId, identifier: "P6C-2", title: "Complete one native task", description: null, workMode: "standard" },
agentId: identity.agentId,
workspace: { id: "workspace-phase6", cwd: process.cwd(), repoUrl: null, repoRef: null, branchName: null },
normalizedSessionId: sessionId,
completionContract: {
id: taskContractId,
sha256: "phase6-task-contract",
schemaVersion: "paperclip.completion-contract.v1",
contract: { revision: "phase6-v1", objective: "Complete one native task", criteria: [{ id: "objective", requirement: "Complete the task" }] },
},
});
const port = new PaperclipControlPlanePort(db, {
companyId: identity.companyId,
issueId: taskIssueId,
runId: taskRunId,
agentId: identity.agentId,
completionContractId: taskContractId,
completionContractSha256: "phase6-task-contract",
sourceInstanceId: "phase6-scripted-runner",
controlPlaneSourceInstanceId: "phase6-control-plane",
});
const completed = await executeNativeSession({
input: execution,
backend,
controlPlane: port,
runnerInstanceId: "phase6-scripted-runner",
controlPlaneInstanceId: "phase6-control-plane",
});
expect(completed.terminal.runTerminalState).toBe("succeeded");
await finalizeNativeRun({ db, runId: taskRunId, workspaceFinalizeStatus: "succeeded" });
await expect(db.select().from(issues).where(eq(issues.id, taskIssueId))).resolves.toEqual([
expect.objectContaining({ status: "done", statusVersion: 1 }),
]);
});
it("fails closed when the bound company does not own the run", async () => {
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
const port = new PaperclipControlPlanePort(db, {
companyId: "00000000-0000-4000-8000-000000000099",
issueId: identity.issueId,
runId: identity.runId,
agentId: identity.agentId,
completionContractId: contractId,
completionContractSha256: contractSha,
sourceInstanceId: "runner-phase6-conformance",
controlPlaneSourceInstanceId: "control-phase6-conformance",
});
await expect(port.openRun(CONTROL_PLANE_CONFORMANCE_OPEN)).rejects.toThrow("binding_mismatch");
});
it("preserves the result and issue status when workspace finalization fails", async () => {
const identity = CONTROL_PLANE_CONFORMANCE_OPEN.identity;
const port = new PaperclipControlPlanePort(db, {
companyId: identity.companyId,
issueId: workspaceFailureIssueId,
runId: workspaceFailureRunId,
agentId: identity.agentId,
completionContractId: workspaceFailureContractId,
completionContractSha256: "phase6-workspace-failure-contract",
sourceInstanceId: "runner-phase6-workspace-failure",
controlPlaneSourceInstanceId: "control-phase6-workspace-failure",
});
await port.openRun({
identity: { ...identity, runId: workspaceFailureRunId, issueId: workspaceFailureIssueId },
backendKind: "mock",
sourceInstanceId: "runner-phase6-workspace-failure",
});
await port.completeRun({
result: CONTROL_PLANE_CONFORMANCE_RESULT,
terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL,
callerResultId: "phase6-workspace-failure-result",
});
await finalizeNativeRun({
db,
runId: workspaceFailureRunId,
workspaceFinalizeStatus: "failed",
projectRunStatus: true,
});
await expect(db.select().from(issues).where(eq(issues.id, workspaceFailureIssueId))).resolves.toEqual([
expect.objectContaining({ status: "in_progress", statusVersion: 0, lastStatusDecisionId: null }),
]);
await expect(db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, workspaceFailureRunId))).resolves.toEqual([
expect.objectContaining({ status: "failed", nativePhase: "workspace_failed" }),
]);
await expect(db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, workspaceFailureRunId))).resolves.toHaveLength(1);
});
});
@@ -0,0 +1,238 @@
import { and, asc, eq, gt, or } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
heartbeatRunEvents,
heartbeatRuns,
nativeRunFinalizations,
nativeRunResults,
} from "@paperclipai/db";
import type {
CompleteControlPlaneRunInput,
ControlPlanePort,
NativeRunEvent,
NativeRunResult,
OpenControlPlaneRunInput,
PrpEvent,
PrpTerminalState,
ReplayControlPlaneEventsInput,
} from "@paperclipai/paperclip-runner";
import {
validatePrpEvent,
validatePrpStructuredRunResult,
} from "@paperclipai/paperclip-runner";
import { appendHeartbeatRunEvent } from "../heartbeat-run-events.js";
import { nativeSha256 } from "./canonical.js";
export interface PaperclipControlPlaneBinding {
companyId: string;
issueId: string;
runId: string;
agentId: string;
completionContractId: string;
completionContractSha256: string;
sourceInstanceId: string;
controlPlaneSourceInstanceId: string;
}
function isPrpEvent(value: NativeRunEvent | PrpEvent): value is PrpEvent {
return "schema" in value && value.schema === "paperclip.prp.event.v1";
}
function isCompleteInput(value: NativeRunResult | CompleteControlPlaneRunInput): value is CompleteControlPlaneRunInput {
return "result" in value && "terminal" in value;
}
function assertTerminal(value: unknown): asserts value is PrpTerminalState {
const terminal = value as Partial<PrpTerminalState> | null;
if (
terminal?.schema !== "paperclip.prp.terminal.v1"
|| !["completed", "failed", "interrupted", "cancelled"].includes(String(terminal.turnTerminalState))
|| !["succeeded", "failed", "cancelled"].includes(String(terminal.runTerminalState))
|| !["done", "blocked", "needs_review", "yielded"].includes(String(terminal.reportedWorkDisposition))
) {
throw new Error("native_terminal_schema_invalid");
}
}
/** Production implementation of the runner package's deliberately narrow persistence port. */
export class PaperclipControlPlanePort implements ControlPlanePort {
readonly #db: Db;
readonly #binding: PaperclipControlPlaneBinding;
#sessionId: string | null = null;
constructor(db: Db, binding: PaperclipControlPlaneBinding) {
this.#db = db;
this.#binding = structuredClone(binding);
}
async openRun(input: OpenControlPlaneRunInput): Promise<void> {
const identity = input.identity;
if (
identity.companyId !== this.#binding.companyId
|| identity.issueId !== this.#binding.issueId
|| identity.runId !== this.#binding.runId
|| identity.agentId !== this.#binding.agentId
|| input.sourceInstanceId !== this.#binding.sourceInstanceId
) {
throw new Error("native_open_run_binding_mismatch");
}
const run = await this.#db.select().from(heartbeatRuns)
.where(and(
eq(heartbeatRuns.id, this.#binding.runId),
eq(heartbeatRuns.companyId, this.#binding.companyId),
eq(heartbeatRuns.agentId, this.#binding.agentId),
))
.limit(1)
.then((rows) => rows[0] ?? null);
if (!run || run.runtimeMode !== "native" || run.completionContractId !== this.#binding.completionContractId) {
throw new Error("native_open_run_not_authorized");
}
this.#sessionId = identity.sessionId;
}
async appendEvent(value: NativeRunEvent | PrpEvent) {
if (!isPrpEvent(value)) throw new Error("native_legacy_event_not_supported");
const validated = validatePrpEvent(value);
if (!validated.ok) throw new Error(`native_event_schema_invalid:${validated.issues[0]?.message ?? "unknown"}`);
const event = validated.event;
if (event.runId !== this.#binding.runId || (this.#sessionId && event.normalizedSessionId !== this.#sessionId)) {
throw new Error("native_event_binding_mismatch");
}
const expectedSourceInstanceId = event.sourceKind === "control_plane"
? this.#binding.controlPlaneSourceInstanceId
: this.#binding.sourceInstanceId;
if (event.sourceInstanceId !== expectedSourceInstanceId) {
throw new Error("native_event_source_binding_mismatch");
}
const persisted = await appendHeartbeatRunEvent(this.#db, {
companyId: this.#binding.companyId,
runId: this.#binding.runId,
agentId: this.#binding.agentId,
eventType: event.eventType,
stream: "system",
level: event.eventType.includes("failed") ? "error" : "info",
payload: { prpEvent: event as unknown as Record<string, unknown> },
nativeSource: {
sourceInstanceId: event.sourceInstanceId,
sourceEventId: event.sourceEventId,
sourceSeq: event.sourceSeq,
protocolSchemaVersion: event.schemaVersion,
canonicalPayload: event as unknown as Record<string, unknown>,
},
});
return {
cursor: persisted.row.seq,
highestContiguousSourceSeq: persisted.highestContiguousSourceSeq,
disposition: persisted.disposition,
};
}
async replayEvents(input: ReplayControlPlaneEventsInput) {
if (
input.runId !== this.#binding.runId
|| ![this.#binding.sourceInstanceId, this.#binding.controlPlaneSourceInstanceId].includes(input.sourceInstanceId)
) {
throw new Error("native_replay_binding_mismatch");
}
const rows = await this.#db
.select({ payload: heartbeatRunEvents.payload, sourceSeq: heartbeatRunEvents.sourceSeq })
.from(heartbeatRunEvents)
.where(and(
eq(heartbeatRunEvents.runId, this.#binding.runId),
eq(heartbeatRunEvents.sourceInstanceId, input.sourceInstanceId),
gt(heartbeatRunEvents.sourceSeq, input.afterSourceSeq),
))
.orderBy(asc(heartbeatRunEvents.sourceSeq))
.limit(Math.max(1, Math.min(input.limit, 1_000)));
const events = rows.flatMap((row) => {
const candidate = row.payload?.prpEvent;
const parsed = validatePrpEvent(candidate);
return parsed.ok ? [parsed.event] : [];
});
let cursor = input.afterSourceSeq;
for (const event of events) {
if (event.sourceSeq === cursor + 1) cursor += 1;
else if (event.sourceSeq > cursor + 1) break;
}
return { events, highestContiguousSourceSeq: cursor };
}
async completeRun(value: NativeRunResult | CompleteControlPlaneRunInput): Promise<void> {
if (!isCompleteInput(value)) throw new Error("native_structured_result_required");
const validated = validatePrpStructuredRunResult(value.result);
if (!validated.ok) throw new Error(`native_result_schema_invalid:${validated.issues[0]?.message ?? "unknown"}`);
assertTerminal(value.terminal);
if (validated.result.reportedWorkDisposition !== value.terminal.reportedWorkDisposition) {
throw new Error("native_result_terminal_disposition_mismatch");
}
const canonical = {
binding: this.#binding,
result: validated.result,
terminal: value.terminal,
turnId: value.turnId ?? null,
};
const canonicalSha256 = nativeSha256(canonical);
const serverFingerprint = nativeSha256({
runId: this.#binding.runId,
completionContractSha256: this.#binding.completionContractSha256,
canonicalSha256,
});
await this.#db.transaction(async (tx) => {
const run = await tx.select().from(heartbeatRuns)
.where(eq(heartbeatRuns.id, this.#binding.runId)).for("update").limit(1)
.then((rows) => rows[0] ?? null);
if (
!run
|| run.companyId !== this.#binding.companyId
|| run.runtimeMode !== "native"
|| run.completionContractId !== this.#binding.completionContractId
|| run.completionContractSha256 !== this.#binding.completionContractSha256
) throw new Error("native_result_binding_mismatch");
const callerConditions = [eq(nativeRunResults.serverFingerprint, serverFingerprint)];
if (value.callerResultId) callerConditions.push(eq(nativeRunResults.callerResultId, value.callerResultId));
if (value.callerDedupeKey) callerConditions.push(eq(nativeRunResults.callerDedupeKey, value.callerDedupeKey));
const existing = await tx.select().from(nativeRunResults)
.where(and(eq(nativeRunResults.runId, this.#binding.runId), or(...callerConditions)))
.limit(1).then((rows) => rows[0] ?? null);
if (existing) {
if (existing.canonicalSha256 !== canonicalSha256) throw new Error("native_result_idempotency_conflict");
return;
}
const resultJson = {
result: validated.result,
terminal: value.terminal,
} as unknown as Record<string, unknown>;
const inserted = await tx.insert(nativeRunResults).values({
companyId: this.#binding.companyId,
issueId: this.#binding.issueId,
runId: this.#binding.runId,
turnId: value.turnId ?? null,
completionContractId: this.#binding.completionContractId,
callerResultId: value.callerResultId ?? null,
callerDedupeKey: value.callerDedupeKey ?? null,
serverFingerprint,
schemaStatus: "accepted",
resultJson,
canonicalSha256,
}).returning().then((rows) => rows[0]);
if (!inserted) throw new Error("native_result_not_persisted");
await tx.insert(nativeRunFinalizations).values({
runId: this.#binding.runId,
companyId: this.#binding.companyId,
issueId: this.#binding.issueId,
phase: "result_persisted",
resultId: inserted.id,
}).onConflictDoUpdate({
target: nativeRunFinalizations.runId,
set: { phase: "result_persisted", resultId: inserted.id, failureCode: null, failureDetail: null, updatedAt: new Date() },
});
await tx.update(heartbeatRuns).set({
nativePhase: "result_persisted",
nativePhaseUpdatedAt: new Date(),
updatedAt: new Date(),
}).where(eq(heartbeatRuns.id, this.#binding.runId));
});
}
}
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { NativeRuntimeEligibilityError, resolveNativeRuntimeMode } from "./runtime-mode.js";
const eligible = {
enabled: true,
runtimeConfig: { nativeRunner: { mode: "native", backend: "codex_app_server", protocolVersion: 1 } },
agent: { status: "running", adapterType: "codex_local" },
issue: { id: "issue", workMode: "standard" },
target: { kind: "local" },
workspaceId: "workspace",
} as const;
describe("resolveNativeRuntimeMode", () => {
it("preserves legacy as the default and as the kill-switch behavior", () => {
expect(resolveNativeRuntimeMode({ ...eligible, runtimeConfig: {} }).kind).toBe("legacy");
expect(resolveNativeRuntimeMode({ ...eligible, enabled: false })).toEqual(expect.objectContaining({
kind: "legacy",
reason: "instance_flag_disabled",
}));
});
it("selects native only for an eligible explicit profile", () => {
expect(resolveNativeRuntimeMode(eligible)).toEqual(expect.objectContaining({
kind: "native",
reason: "eligible_opt_in",
}));
});
it("rejects an explicit native profile outside the approved boundary", () => {
expect(() => resolveNativeRuntimeMode({ ...eligible, agent: { ...eligible.agent, adapterType: "claude_local" } }))
.toThrow(NativeRuntimeEligibilityError);
expect(() => resolveNativeRuntimeMode({ ...eligible, issue: { id: "issue", workMode: "skill_test" } }))
.toThrow(NativeRuntimeEligibilityError);
expect(() => resolveNativeRuntimeMode({ ...eligible, target: { kind: "remote" } }))
.toThrow(NativeRuntimeEligibilityError);
});
});
@@ -0,0 +1,61 @@
export const NATIVE_RUNTIME_RESOLVER_VERSION = "phase6-v1" as const;
export type NativeRuntimeResolution =
| { kind: "legacy"; resolverVersion: typeof NATIVE_RUNTIME_RESOLVER_VERSION; reason: string }
| {
kind: "native";
resolverVersion: typeof NATIVE_RUNTIME_RESOLVER_VERSION;
reason: "eligible_opt_in";
profile: { mode: "native"; backend: "codex_app_server"; protocolVersion: 1 };
};
export class NativeRuntimeEligibilityError extends Error {
readonly code = "native_runtime_ineligible" as const;
constructor(reason: string) {
super(`Native runner profile is ineligible: ${reason}`);
this.name = "NativeRuntimeEligibilityError";
}
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
export function resolveNativeRuntimeMode(input: {
enabled: boolean;
runtimeConfig: unknown;
agent: { status: string; adapterType: string | null };
issue: { id: string; workMode: string; executionWorkspaceId?: string | null } | null;
target: { kind?: string } | null | undefined;
workspaceId: string | null;
}): NativeRuntimeResolution {
const nativeRunner = record(record(input.runtimeConfig).nativeRunner);
const mode = nativeRunner.mode;
if (!input.enabled) {
return { kind: "legacy", resolverVersion: NATIVE_RUNTIME_RESOLVER_VERSION, reason: "instance_flag_disabled" };
}
if (mode === undefined || mode === "legacy") {
return { kind: "legacy", resolverVersion: NATIVE_RUNTIME_RESOLVER_VERSION, reason: "agent_not_opted_in" };
}
if (mode !== "native") throw new NativeRuntimeEligibilityError("unsupported profile mode");
if (nativeRunner.backend !== "codex_app_server" || nativeRunner.protocolVersion !== 1) {
throw new NativeRuntimeEligibilityError("unsupported backend or protocol version");
}
if (input.agent.adapterType !== "codex_local" || input.agent.status !== "active" && input.agent.status !== "running") {
throw new NativeRuntimeEligibilityError("agent must be an active codex_local agent");
}
if (!input.issue || input.issue.workMode !== "standard") {
throw new NativeRuntimeEligibilityError("run must be bound to a standard issue");
}
if (!input.workspaceId || input.target?.kind && input.target.kind !== "local") {
throw new NativeRuntimeEligibilityError("a realized local workspace is required");
}
return {
kind: "native",
resolverVersion: NATIVE_RUNTIME_RESOLVER_VERSION,
reason: "eligible_opt_in",
profile: { mode: "native", backend: "codex_app_server", protocolVersion: 1 },
};
}
@@ -0,0 +1,72 @@
import { describe, expect, it } from "vitest";
import { classifyNativeEvidence } from "./evidence-classifier.js";
import { arbitrateNativeStatus } from "./status-arbiter.js";
function result(overrides: Record<string, unknown> = {}) {
return {
reportedWorkDisposition: "done",
summary: "Complete",
completionClaim: {
objectiveSatisfied: true,
criteria: [{ criterionId: "objective", status: "satisfied", evidenceRefs: ["test"] }],
remainingWork: [],
},
verification: [{ commandOrCheck: "test", status: "passed" }],
...overrides,
};
}
describe("native status authority", () => {
it("marks done only from successful finalization and complete evidence", () => {
const assessment = classifyNativeEvidence(result());
expect(arbitrateNativeStatus({
assessment,
terminalState: "succeeded",
workspaceFinalizeStatus: "succeeded",
agentId: "agent",
}).toStatus).toBe("done");
});
it("keeps incomplete, review, cancelled, and yielded work non-terminal", () => {
for (const reportedWorkDisposition of ["needs_review", "yielded"] as const) {
expect(arbitrateNativeStatus({
assessment: classifyNativeEvidence(result({ reportedWorkDisposition })),
terminalState: "succeeded",
workspaceFinalizeStatus: "succeeded",
agentId: "agent",
}).toStatus).toBe("in_progress");
}
expect(arbitrateNativeStatus({
assessment: classifyNativeEvidence(result()),
terminalState: "cancelled",
workspaceFinalizeStatus: "succeeded",
agentId: "agent",
}).toStatus).toBe("in_progress");
expect(arbitrateNativeStatus({
assessment: classifyNativeEvidence(result()),
terminalState: "succeeded",
workspaceFinalizeStatus: "succeeded",
approvalGateSatisfied: false,
agentId: "agent",
})).toEqual(expect.objectContaining({
toStatus: "in_progress",
reasonCode: "approval_gate_pending",
}));
});
it("blocks only on a first-class blocker or failed workspace finalization", () => {
const explicitBlocker = classifyNativeEvidence(result({
reportedWorkDisposition: "blocked",
blocker: { owner: { kind: "board" }, unblockAction: "Approve access" },
}));
expect(arbitrateNativeStatus({
assessment: explicitBlocker,
terminalState: "succeeded",
workspaceFinalizeStatus: "succeeded",
agentId: "agent",
})).toEqual(expect.objectContaining({
toStatus: "blocked",
unblockDescriptor: { owner: "board", action: "Approve access" },
}));
});
});
@@ -0,0 +1,79 @@
import type { NativeEvidenceAssessment } from "./evidence-classifier.js";
export const NATIVE_STATUS_ARBITER_POLICY_VERSION = "phase6-v1";
export interface NativeStatusDecision {
policyVersion: typeof NATIVE_STATUS_ARBITER_POLICY_VERSION;
toStatus: "in_progress" | "done" | "blocked";
reasonCode: string;
unblockDescriptor: { owner: { agentId: string } | "board"; action: string } | null;
}
/** Pure authority boundary: model prose is evidence, never a status command. */
export function arbitrateNativeStatus(input: {
assessment: NativeEvidenceAssessment;
terminalState: "succeeded" | "failed" | "cancelled";
workspaceFinalizeStatus: "succeeded" | "failed";
approvalGateSatisfied?: boolean;
agentId: string;
}): NativeStatusDecision {
if (input.workspaceFinalizeStatus !== "succeeded") {
return {
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
toStatus: "blocked",
reasonCode: "workspace_finalization_failed",
unblockDescriptor: { owner: { agentId: input.agentId }, action: "Repair and re-verify workspace finalization." },
};
}
if (input.terminalState !== "succeeded") {
return {
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
toStatus: "in_progress",
reasonCode: input.terminalState === "cancelled" ? "native_run_cancelled" : "native_run_failed",
unblockDescriptor: null,
};
}
if (input.approvalGateSatisfied === false) {
return {
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
toStatus: "in_progress",
reasonCode: "approval_gate_pending",
unblockDescriptor: null,
};
}
const complete =
input.assessment.reportedDisposition === "done" &&
input.assessment.objectiveSatisfied &&
input.assessment.allCriteriaSatisfied &&
input.assessment.verificationPassed &&
!input.assessment.hasBlockingRemainingWork;
if (complete) {
return {
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
toStatus: "done",
reasonCode: "completion_contract_satisfied",
unblockDescriptor: null,
};
}
if (input.assessment.reportedDisposition === "blocked" && input.assessment.blocker) {
return {
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
toStatus: "blocked",
reasonCode: "native_reported_blocker",
unblockDescriptor: {
owner: input.assessment.blocker.boardOwned ? "board" : { agentId: input.agentId },
action: input.assessment.blocker.unblockAction,
},
};
}
return {
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
toStatus: "in_progress",
reasonCode: input.assessment.reportedDisposition === "needs_review"
? "native_review_path_required"
: input.assessment.reportedDisposition === "yielded"
? "native_continuation_required"
: "native_completion_incomplete",
unblockDescriptor: null,
};
}
@@ -0,0 +1,129 @@
import { and, eq } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
issues,
activityLog,
nativeRunFinalizations,
statusDecisionEffects,
statusDecisions,
} from "@paperclipai/db";
import type { NativeStatusDecision } from "./status-arbiter.js";
import { nativeSha256 } from "./canonical.js";
export class NativeStatusRaceError extends Error {
readonly code = "native_status_race" as const;
constructor() {
super("native_status_race");
this.name = "NativeStatusRaceError";
}
}
export async function commitNativeStatusDecision(input: {
db: Db;
companyId: string;
issueId: string;
runId: string;
assessmentId: string;
priorStatus: string;
priorStatusVersion: number;
decision: NativeStatusDecision;
}) {
return input.db.transaction(async (tx) => {
const issue = await tx.select().from(issues).where(and(
eq(issues.id, input.issueId),
eq(issues.companyId, input.companyId),
)).for("update").limit(1).then((rows) => rows[0] ?? null);
if (!issue || issue.status !== input.priorStatus || issue.statusVersion !== input.priorStatusVersion) {
throw new NativeStatusRaceError();
}
const decisionJson = {
toStatus: input.decision.toStatus,
reasonCode: input.decision.reasonCode,
unblockDescriptor: input.decision.unblockDescriptor,
};
const decisionDigest = nativeSha256({
issueId: input.issueId,
assessmentId: input.assessmentId,
policyVersion: input.decision.policyVersion,
fromStatus: issue.status,
decision: decisionJson,
});
let decisionRow = await tx.select().from(statusDecisions).where(and(
eq(statusDecisions.issueId, input.issueId),
eq(statusDecisions.decisionDigest, decisionDigest),
)).limit(1).then((rows) => rows[0] ?? null);
if (!decisionRow) {
[decisionRow] = await tx.insert(statusDecisions).values({
companyId: input.companyId,
issueId: input.issueId,
assessmentId: input.assessmentId,
decisionVersion: 1,
policyVersion: input.decision.policyVersion,
fromStatus: issue.status,
toStatus: input.decision.toStatus,
reasonCode: input.decision.reasonCode,
decisionJson,
decisionDigest,
applicationState: "pending",
}).returning();
}
if (!decisionRow) throw new Error("native_status_decision_not_persisted");
const updated = await tx.update(issues).set({
status: input.decision.toStatus,
unblockDescriptor: input.decision.unblockDescriptor,
lastStatusDecisionId: decisionRow.id,
completedAt: input.decision.toStatus === "done" ? new Date() : null,
blockedTransitionAt: input.decision.toStatus === "blocked" ? new Date() : null,
updatedAt: new Date(),
}).where(and(
eq(issues.id, input.issueId),
eq(issues.statusVersion, input.priorStatusVersion),
)).returning().then((rows) => rows[0] ?? null);
if (!updated) throw new NativeStatusRaceError();
await tx.insert(statusDecisionEffects).values({
companyId: input.companyId,
issueId: input.issueId,
decisionId: decisionRow.id,
ordinal: 1,
effectKind: "issue_status_projection",
targetType: "issue",
targetId: input.issueId,
idempotencyKey: `native-status:${decisionRow.id}:1`,
payload: {
fromStatus: issue.status,
toStatus: input.decision.toStatus,
reasonCode: input.decision.reasonCode,
},
deliveryState: "delivered",
attemptCount: 1,
deliveredAt: new Date(),
}).onConflictDoNothing();
await tx.update(statusDecisions).set({
applicationState: "applied",
appliedAt: new Date(),
}).where(eq(statusDecisions.id, decisionRow.id));
await tx.update(nativeRunFinalizations).set({
phase: "applied",
assessmentId: input.assessmentId,
decisionId: decisionRow.id,
updatedAt: new Date(),
}).where(eq(nativeRunFinalizations.runId, input.runId));
await tx.insert(activityLog).values({
companyId: input.companyId,
actorType: "system",
actorId: "native-status-committer",
action: "issue.native_status_decision_applied",
entityType: "issue",
entityId: input.issueId,
runId: input.runId,
details: {
assessmentId: input.assessmentId,
decisionId: decisionRow.id,
fromStatus: issue.status,
toStatus: input.decision.toStatus,
reasonCode: input.decision.reasonCode,
},
});
return { decision: { ...decisionRow, applicationState: "applied" }, issue: updated };
});
}
@@ -0,0 +1,57 @@
import { and, eq } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { workAssessments } from "@paperclipai/db";
import type { NativeEvidenceAssessment } from "./evidence-classifier.js";
import { nativeSha256 } from "./canonical.js";
export async function recordNativeWorkAssessment(input: {
db: Db;
companyId: string;
issueId: string;
runId: string;
turnId: string | null;
contractId: string;
resultId: string;
priorIssueStatus: string;
priorStatusVersion: number;
priorDecisionId: string | null;
policyVersion: string;
assessment: NativeEvidenceAssessment;
supersedesAssessmentId?: string | null;
}) {
const assessmentJson = input.assessment as unknown as Record<string, unknown>;
const inputDigest = nativeSha256({
contractId: input.contractId,
resultId: input.resultId,
priorIssueStatus: input.priorIssueStatus,
priorStatusVersion: input.priorStatusVersion,
policyVersion: input.policyVersion,
assessment: assessmentJson,
});
const existing = await input.db.select().from(workAssessments).where(and(
eq(workAssessments.resultId, input.resultId),
eq(workAssessments.inputDigest, inputDigest),
)).limit(1).then((rows) => rows[0] ?? null);
if (existing) return existing;
const [row] = await input.db.insert(workAssessments).values({
companyId: input.companyId,
issueId: input.issueId,
runId: input.runId,
turnId: input.turnId,
contractId: input.contractId,
resultId: input.resultId,
triggerKind: "native_result",
triggerRef: input.resultId,
triggerCapability: "server_native_finalizer",
triggerActorCompanyId: input.companyId,
priorIssueStatus: input.priorIssueStatus,
priorStatusVersion: input.priorStatusVersion,
priorDecisionId: input.priorDecisionId,
policyVersion: input.policyVersion,
assessmentJson,
inputDigest,
supersedesAssessmentId: input.supersedesAssessmentId ?? null,
}).returning();
if (!row) throw new Error("native_assessment_not_persisted");
return row;
}
+2 -10
View File
@@ -36,6 +36,7 @@ import { isPidAlive, isProcessGroupAlive, terminateLocalService } from "../local
import { redactCurrentUserText } from "../../log-redaction.js";
import { redactSensitiveText } from "../../redaction.js";
import { logActivity } from "../activity-log.js";
import { appendHeartbeatRunEvent } from "../heartbeat-run-events.js";
import { budgetService } from "../budgets.js";
import { instanceSettingsService } from "../instance-settings.js";
import { issueRecoveryActionService } from "../issue-recovery-actions.js";
@@ -1559,14 +1560,6 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup })
return null;
}
async function nextRunEventSeq(runId: string) {
const [row] = await db
.select({ maxSeq: sql<number | null>`max(${heartbeatRunEvents.seq})` })
.from(heartbeatRunEvents)
.where(eq(heartbeatRunEvents.runId, runId));
return Number(row?.maxSeq ?? 0) + 1;
}
async function appendRecoveryRunEvent(
run: typeof heartbeatRuns.$inferSelect,
event: {
@@ -1575,11 +1568,10 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup })
payload?: Record<string, unknown>;
},
) {
await db.insert(heartbeatRunEvents).values({
await appendHeartbeatRunEvent(db, {
companyId: run.companyId,
runId: run.id,
agentId: run.agentId,
seq: await nextRunEventSeq(run.id),
eventType: "lifecycle",
stream: "system",
level: event.level,