fix(native-runner): prove live Phase 6 effects

Route corpus operations through cancellation, reconciliation, attention, and rollout entrypoints, materialize effects through owning services, and document the live-versus-policy proof boundary.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-09 06:46:53 +00:00
1 parent 3bed55f3f7
commit d53b7902ab
13 files changed
+1272 -168

No files matched your search

@@ -27,6 +27,7 @@ import {
applyNativeAttentionStatusDecision,
materializeNativeInteractionResponses,
rejectUnsupportedNativeRuntimeRequest,
routeNativeAttention,
resolveNativeAttentionStatus,
} from "../services/native-runtime/native-interaction-bridge.js";
import {
@@ -109,6 +110,8 @@ type FixtureObservation = PolicyObservation & {
consumerEvidenceByRow: Map<string, string>;
};
type DisabledLiveEntrypoint = "attention" | "cancellation" | "reconciliation" | "rollout";
const corpusPath = fileURLToPath(new URL(
"../../../packages/paperclip-runner/spec/fixtures/status-authority-phase5.json",
import.meta.url,
@@ -239,6 +242,10 @@ const supersedingDecisionStates = new Set([
"board_cancelled_before_cas", "new_policy_requires_review", "authorized_writer_incremented_version",
]);
const liveReconciliationStates = new Set([
"board_cancelled_before_cas", "new_evidence_satisfies_contract", "new_policy_requires_review",
]);
function initialRunStatus(fixture: Fixture) {
const terminalState = fixture.given.runTerminalState;
if (["succeeded", "failed", "cancelled", "active"].includes(String(terminalState))) {
@@ -362,18 +369,39 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
let db: ReturnType<typeof createDb>;
const companyId = randomUUID();
const agentId = randomUUID();
const delegateAgentId = randomUUID();
const outsideCompanyId = randomUUID();
const outsideAgentId = randomUUID();
beforeAll(async () => {
temporary = await startEmbeddedPostgresTestDatabase("paperclip-status-corpus-");
db = createDb(temporary.connectionString);
await db.insert(companies).values({ id: companyId, name: "Status corpus", issuePrefix: "PSC" });
await db.insert(agents).values({
id: agentId,
companyId,
name: "Status corpus agent",
adapterType: "codex_local",
status: "running",
});
await db.insert(companies).values({ id: outsideCompanyId, name: "Outside corpus", issuePrefix: "OUT" });
await db.insert(agents).values([
{
id: agentId,
companyId,
name: "Status corpus agent",
adapterType: "codex_local",
status: "running",
},
{
id: delegateAgentId,
companyId,
name: "Eligible status corpus delegate",
adapterType: "codex_local",
status: "idle",
capabilities: "Provide production status-authority expertise",
},
{
id: outsideAgentId,
companyId: outsideCompanyId,
name: "Outside-company agent",
adapterType: "codex_local",
status: "idle",
},
]);
}, 30_000);
afterAll(async () => temporary?.cleanup());
@@ -385,6 +413,7 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
const resultId = randomUUID();
const assessmentId = randomUUID();
const workProductId = randomUUID();
const assessmentCreatedAt = new Date();
const priorStatus = String(fixture.given.priorIssueStatus ?? "in_progress");
const completionState = String(fixture.given.completionState ?? "");
const nativeRecords = fixture.mode === "native" && !noNativeRecordStates.has(completionState);
@@ -415,6 +444,8 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
runtimeMode: "native",
runtimeModeResolvedAt: new Date(),
contextSnapshot: { issueId, fixtureId: fixture.id },
completionContractId: contractId,
completionContractSha256: `contract:${fixture.id}`,
});
await db.insert(completionContracts).values({
id: contractId,
@@ -441,6 +472,30 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
schemaStatus: "accepted",
resultJson: {
fixtureId: fixture.id,
result: {
reportedWorkDisposition: fixtureDisposition(fixture),
summary: fixture.id,
completionClaim: {
contractRevision: "corpus-v1",
objectiveSatisfied: true,
criteria: [{
criterionId: "objective",
status: "satisfied",
evidenceRefs: [`work_product:${workProductId}`],
}],
remainingWork: [],
},
verification: [{
commandOrCheck: "fixture",
status: "passed",
artifactRef: `work_product:${workProductId}`,
}],
},
terminal: {
runTerminalState: fixture.given.runTerminalState === "failed"
? "failed"
: fixture.given.runTerminalState === "cancelled" ? "cancelled" : "succeeded",
},
...(fixture.given.reportedWorkDisposition === null && fixture.given.nativeFinalization !== "invalid"
? {}
: { completionClaim: { fixtureId: fixture.id, preserved: true } }),
@@ -455,7 +510,9 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
provider: "paperclip",
title: `${fixture.id} evidence`,
status: "ready_for_review",
reviewState: "approved",
reviewState: completionState === "new_evidence_satisfies_contract" ? "none" : "approved",
createdAt: new Date(assessmentCreatedAt.getTime() - 1_000),
updatedAt: new Date(assessmentCreatedAt.getTime() - 1_000),
});
await db.insert(workAssessments).values({
id: assessmentId,
@@ -466,11 +523,15 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
resultId,
triggerKind: "native_result",
triggerActorCompanyId: companyId,
priorIssueStatus: priorStatus,
priorIssueStatus: completionState === "board_cancelled_before_cas" ? "in_progress" : priorStatus,
priorStatusVersion: 0,
policyVersion: "phase6-v2",
assessmentJson: { fixtureId: fixture.id },
policyVersion: completionState === "new_policy_requires_review" ? "phase6-v1" : "phase6-v2",
assessmentJson: {
fixtureId: fixture.id,
...(completionState === "new_evidence_satisfies_contract" ? { allCriteriaSatisfied: false } : {}),
},
inputDigest: `assessment:${fixture.id}`,
createdAt: assessmentCreatedAt,
});
await db.insert(nativeRunFinalizations).values({
runId,
@@ -483,7 +544,10 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
return { issueId, runId, workProductId, nativeRecords, assessmentId, contractId, resultId };
}
async function executeFixture(fixture: Fixture): Promise<FixtureObservation> {
async function executeFixture(
fixture: Fixture,
options: { disableLiveEntrypoint?: DisabledLiveEntrypoint } = {},
): Promise<FixtureObservation> {
const completionState = String(fixture.given.completionState ?? "");
const seeded = await seedFixture(fixture);
const consumerExecutions: ConsumerExecution[] = [];
@@ -536,22 +600,134 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
let semanticConsumer: string | null = null;
let consumerDecision: NativeStatusDecision | null = null;
let liveEntrypointCommitted = false;
let liveAttentionInteractionId: string | null = null;
const attentionFacts = attentionFactsFor(completionState, fixture.id, governanceGate);
if (
seeded.nativeRecords
&& options.disableLiveEntrypoint !== "attention"
&& attentionFacts
&& ["attention_response", "attention_candidate", "interaction", "monitor"].includes(String(fixture.given.trigger))
) {
let canonicalRequestId: string | undefined;
if (attentionFacts.route === "duplicate") {
canonicalRequestId = randomUUID();
liveAttentionInteractionId = randomUUID();
await db.insert(issueThreadInteractions).values([
{
id: canonicalRequestId,
companyId,
issueId: seeded.issueId,
kind: "request_confirmation",
status: "pending",
idempotencyKey: `canonical:${fixture.id}`,
payload: { version: 1, prompt: `Canonical ${fixture.id}` },
},
{
id: liveAttentionInteractionId,
companyId,
issueId: seeded.issueId,
kind: "request_confirmation",
status: "expired",
resolvedAt: new Date(),
idempotencyKey: `duplicate:${fixture.id}`,
payload: { version: 1, prompt: `Duplicate ${fixture.id}` },
result: { version: 1, outcome: "superseded_by_newer_request", supersededByInteractionId: canonicalRequestId },
},
]);
} else if (attentionFacts.responseState === "stale") {
liveAttentionInteractionId = randomUUID();
await db.insert(issueThreadInteractions).values({
id: liveAttentionInteractionId,
companyId,
issueId: seeded.issueId,
kind: "request_confirmation",
status: "expired",
resolvedAt: new Date(),
payload: { version: 1, prompt: `Stale ${fixture.id}` },
result: { version: 1, outcome: "superseded_by_comment", commentId: randomUUID() },
});
} else if (attentionFacts.responseState === "expired") {
liveAttentionInteractionId = randomUUID();
await db.insert(issueThreadInteractions).values({
id: liveAttentionInteractionId,
companyId,
issueId: seeded.issueId,
kind: "ask_user_questions",
status: "pending",
payload: { version: 1, questions: [] },
});
}
const routed = await routeNativeAttention({
db,
runId: seeded.runId,
candidate: {
route: attentionFacts.route,
summary: attentionFacts.summary,
responseState: attentionFacts.responseState,
budgetExhausted: attentionFacts.budgetExhausted,
governanceGate: attentionFacts.governanceGate,
targetAgentId: completionState === "cross_company_target" ? outsideAgentId : null,
targetInteractionId: liveAttentionInteractionId ?? undefined,
canonicalRequestId,
},
});
semanticConsumer = "native-attention-resolver";
consumerDecision = pushDecisionConsumer(semanticConsumer, routed.decision);
liveEntrypointCommitted = true;
for (const target of routed.targets) materializedEffects.add(target.effectKind);
consumerExecutions.push({
consumer: "native-attention-entrypoint",
observed: {
decisionId: routed.decisionId,
resolvedTargetAgentId: routed.resolvedTargetAgentId,
targetCount: routed.targets.length,
},
});
if (attentionFacts.route === "agent" && completionState !== "cross_company_target") {
expect(routed.resolvedTargetAgentId, `${fixture.id}:eligible delegate`).toBe(delegateAgentId);
}
}
if (["turn_scope", "run_scope", "issue_scope_authorized", "replacement_turn_accepted"].includes(completionState)) {
semanticConsumer = "native-cancellation-authority";
const scope = completionState === "run_scope" ? "run" : completionState === "issue_scope_authorized" ? "issue" : "turn";
consumerDecision = pushDecisionConsumer(semanticConsumer, resolveNativeCancellationStatus({
scope,
priorIssueStatus,
agentId,
replacementAccepted: completionState === "replacement_turn_accepted",
}));
if (options.disableLiveEntrypoint === "cancellation") {
consumerDecision = pushDecisionConsumer(semanticConsumer, resolveNativeCancellationStatus({
scope,
priorIssueStatus,
agentId,
replacementAccepted: completionState === "replacement_turn_accepted",
}));
} else {
const cancellation = await cancelNativeSession(seeded.runId, `fixture:${fixture.id}`, {
db,
scope,
replacementAccepted: completionState === "replacement_turn_accepted",
});
if (typeof cancellation === "boolean" || !cancellation.decision || !cancellation.auditId) {
throw new Error(`${fixture.id}: live cancellation did not persist an authoritative outcome`);
}
consumerDecision = pushDecisionConsumer(semanticConsumer, cancellation.decision);
liveEntrypointCommitted = true;
for (const effect of cancellation.decision.effects) materializedEffects.add(effect.kind);
consumerExecutions.push({
consumer: "native-session-cancellation",
observed: {
dispatched: cancellation.dispatched,
scope,
reasonCode: cancellation.decision.reasonCode,
decisionId: cancellation.decisionId,
auditId: cancellation.auditId,
},
});
}
} else {
const reconciliationFacts = reconciliationFactsFor(completionState);
const attentionFacts = attentionFactsFor(completionState, fixture.id, governanceGate);
const compatibilityFacts = compatibilityFactsFor(completionState);
const migrationFacts = migrationFactsFor(completionState);
if (
reconciliationFacts
&& !liveReconciliationStates.has(completionState)
&& (
supersedingDecisionStates.has(completionState)
|| (fixture.covers.reconciliationRows ?? []).length > 0
@@ -564,10 +740,12 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
priorIssueStatus,
agentId,
}));
} else if (attentionFacts && ["attention_response", "attention_candidate", "interaction", "monitor"].includes(String(fixture.given.trigger))) {
} else if (!consumerDecision && attentionFacts && ["attention_response", "attention_candidate", "interaction", "monitor"].includes(String(fixture.given.trigger))) {
semanticConsumer = "native-attention-resolver";
consumerDecision = pushDecisionConsumer(semanticConsumer, resolveNativeAttentionStatus({
facts: attentionFacts,
facts: options.disableLiveEntrypoint === "attention" && attentionFacts.route === "agent"
? { ...attentionFacts, resolvedTargetAgentId: delegateAgentId }
: attentionFacts,
priorIssueStatus,
agentId,
}));
@@ -598,7 +776,12 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
if (attentionFacts && (fixture.covers.attentionRows ?? []).length > 0 && semanticConsumer !== "native-attention-resolver") {
pushDecisionConsumer("native-attention-resolver", resolveNativeAttentionStatus({ facts: attentionFacts, priorIssueStatus, agentId }));
}
if (reconciliationFacts && (fixture.covers.reconciliationRows ?? []).length > 0 && semanticConsumer !== "native-reconciliation-consumer") {
if (
reconciliationFacts
&& !liveReconciliationStates.has(completionState)
&& (fixture.covers.reconciliationRows ?? []).length > 0
&& semanticConsumer !== "native-reconciliation-consumer"
) {
pushDecisionConsumer("native-reconciliation-consumer", resolveNativeReconciliationStatus({ facts: reconciliationFacts, priorIssueStatus, agentId }));
}
if (compatibilityFacts && (fixture.covers.compatibilityRows ?? []).some((row) => ["COMP-02", "COMP-05", "COMP-06", "COMP-07", "COMP-08"].includes(row)) && semanticConsumer !== "native-compatibility-status") {
@@ -609,6 +792,69 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
}
}
if (
seeded.nativeRecords
&& options.disableLiveEntrypoint !== "reconciliation"
&& liveReconciliationStates.has(completionState)
) {
const [priorDecision] = await db.insert(statusDecisions).values({
companyId,
issueId: seeded.issueId,
assessmentId: seeded.assessmentId,
decisionVersion: 1,
policyVersion: completionState === "new_policy_requires_review" ? "phase6-v1" : "phase6-v2",
fromStatus: completionState === "board_cancelled_before_cas" ? "in_progress" : priorIssueStatus,
toStatus: completionState === "board_cancelled_before_cas" ? "in_progress" : priorIssueStatus,
reasonCode: "prior_fixture_decision",
decisionJson: { superseded: true },
decisionDigest: `prior-decision:${fixture.id}`,
applicationState: "applied",
appliedAt: new Date(),
}).returning({ id: statusDecisions.id });
await db.update(issues).set({
statusVersion: 1,
lastStatusDecisionId: priorDecision!.id,
}).where(eq(issues.id, seeded.issueId));
await db.update(nativeRunFinalizations).set({
phase: "committed",
decisionId: priorDecision!.id,
}).where(eq(nativeRunFinalizations.runId, seeded.runId));
if (completionState === "new_evidence_satisfies_contract") {
await db.update(issueWorkProducts).set({
reviewState: "approved",
updatedAt: new Date(Date.now() + 1_000),
}).where(eq(issueWorkProducts.id, seeded.workProductId));
}
const [reconciled] = await reconcileNativeFinalizations(db, [seeded.runId]);
if (!reconciled?.reconciliationDecision || !reconciled.decisionId) {
throw new Error(`${fixture.id}: live reconciliation did not commit an authoritative decision`);
}
semanticConsumer = "native-reconciliation-consumer";
consumerDecision = pushDecisionConsumer(semanticConsumer, reconciled.reconciliationDecision);
liveEntrypointCommitted = true;
consumerExecutions.push({
consumer: "native-reconciliation-entrypoint",
observed: {
action: reconciled.reconciliationAction,
decisionId: reconciled.decisionId,
},
});
}
if (
seeded.nativeRecords
&& options.disableLiveEntrypoint === "reconciliation"
&& liveReconciliationStates.has(completionState)
) {
const facts = reconciliationFactsFor(completionState);
if (!facts) throw new Error(`${fixture.id}: missing reconciliation facts`);
semanticConsumer = "native-reconciliation-consumer";
consumerDecision = pushDecisionConsumer(semanticConsumer, resolveNativeReconciliationStatus({
facts,
priorIssueStatus,
agentId,
}));
}
let assessment: NativeEvidenceAssessment | null = null;
if (seeded.nativeRecords && ["runner_finalizer", "dependency", "shadow_comparator", "read_model", "authorized_agent"].includes(String(fixture.given.trigger))) {
const accepted = completeEvidenceStates.has(completionState);
@@ -679,7 +925,11 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
}
}
if (seeded.nativeRecords && consumerDecision?.effects.some((effect) => effect.kind === "resume_workspace_operation")) {
if (
seeded.nativeRecords
&& options.disableLiveEntrypoint !== "reconciliation"
&& consumerDecision?.effects.some((effect) => effect.kind === "resume_workspace_operation")
) {
await db.insert(workspaceOperations).values({
companyId,
heartbeatRunId: seeded.runId,
@@ -687,11 +937,25 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
phase: "workspace_finalize",
status: "failed",
exitCode: 1,
cwd: process.cwd(),
finishedAt: new Date(),
});
const [reconciled] = await reconcileNativeFinalizations(db, [seeded.runId]);
if (!reconciled || reconciled.reconciliationAction !== "resume_workspace_operation") {
throw new Error(`${fixture.id}: live workspace reconciliation did not execute (${JSON.stringify(reconciled)})`);
}
liveEntrypointCommitted = true;
materializedEffects.add("resume_workspace_operation");
consumerExecutions.push({
consumer: "native-reconciliation-entrypoint",
observed: {
action: reconciled.reconciliationAction,
workspaceOperationId: reconciled.workspaceOperationId,
workspaceFinalizeStatus: reconciled.workspaceFinalizeStatus,
},
});
}
let liveAttentionInteractionId: string | null = null;
if (seeded.nativeRecords && consumerDecision?.effects.some((effect) => effect.kind === "link_canonical_request")) {
if (!liveEntrypointCommitted && seeded.nativeRecords && consumerDecision?.effects.some((effect) => effect.kind === "link_canonical_request")) {
const canonicalInteractionId = randomUUID();
liveAttentionInteractionId = randomUUID();
await db.insert(issueThreadInteractions).values([
@@ -721,7 +985,7 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
},
]);
}
if (seeded.nativeRecords && consumerDecision?.effects.some((effect) => effect.kind === "record_stale_response")) {
if (!liveEntrypointCommitted && seeded.nativeRecords && consumerDecision?.effects.some((effect) => effect.kind === "record_stale_response")) {
liveAttentionInteractionId = randomUUID();
await db.insert(issueThreadInteractions).values({
id: liveAttentionInteractionId,
@@ -734,7 +998,7 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
result: { version: 1, outcome: "superseded_by_comment", commentId: randomUUID() },
});
}
if (seeded.nativeRecords && consumerDecision?.effects.some((effect) => effect.kind === "record_expiry")) {
if (!liveEntrypointCommitted && seeded.nativeRecords && consumerDecision?.effects.some((effect) => effect.kind === "record_expiry")) {
await db.insert(issueThreadInteractions).values({
companyId,
issueId: seeded.issueId,
@@ -797,6 +1061,7 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
if (
seeded.nativeRecords
&& zeroDecisionStates.has(completionState)
&& !liveEntrypointCommitted
&& consumerDecision
&& consumerDecision.effects.some((effect) => [
"link_canonical_request",
@@ -842,6 +1107,7 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
&& consumerDecision.reasonCode !== null
&& completionState !== "replacement_turn_accepted"
&& !zeroDecisionStates.has(completionState)
&& !liveEntrypointCommitted
) {
let priorStatusVersion = 0;
let priorDecisionId: string | null = null;
@@ -925,6 +1191,12 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
}
}
if (options.disableLiveEntrypoint === "rollout" && completionState === "kill_switch_during_active_native_run") {
await db.update(agents).set({
runtimeConfig: { nativeRunner: { mode: "native", backend: "codex_app_server", protocolVersion: 1 } },
}).where(eq(agents.id, agentId));
}
if (seeded.nativeRecords && (rolledBack || completionState === "safe_partial_parse")) {
const failure = await recordNativeFinalizationFailure({
db,
@@ -978,28 +1250,7 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
});
}
if (["turn_scope", "run_scope", "issue_scope_authorized", "replacement_turn_accepted"].includes(completionState)) {
const cancellation = await cancelNativeSession(seeded.runId, `fixture:${fixture.id}`, {
db,
scope: completionState === "run_scope" ? "run" : completionState === "issue_scope_authorized" ? "issue" : "turn",
replacementAccepted: completionState === "replacement_turn_accepted",
});
if (completionState === "replacement_turn_accepted" && cancellation.decision?.effects.some((effect) => effect.kind === "accept_replacement_turn")) {
materializedEffects.add("accept_replacement_turn");
}
consumerExecutions.push({
consumer: "native-session-cancellation",
observed: {
dispatched: cancellation.dispatched,
scope: completionState,
reasonCode: cancellation.decision?.reasonCode ?? null,
},
});
}
const boardTarget = completionState === "issue_scope_authorized"
? "cancelled"
: completionState === "explicit_resume_capability"
const boardTarget = completionState === "explicit_resume_capability"
? "in_progress"
: completionState === "authorized_status_write"
? "in_review"
@@ -1127,6 +1378,10 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
const target = await db.select({ id: agentWakeupRequests.id }).from(agentWakeupRequests)
.where(eq(agentWakeupRequests.id, effectRow.targetId!)).then((rows) => rows[0] ?? null);
expect(target, `${fixture.id}:${effectRow.effectKind}:wake-target`).not.toBeNull();
} else if (effectRow.targetType === "agent") {
const target = await db.select({ id: agents.id }).from(agents)
.where(eq(agents.id, effectRow.targetId!)).then((rows) => rows[0] ?? null);
expect(target, `${fixture.id}:${effectRow.effectKind}:agent-target`).not.toBeNull();
} else if (effectRow.targetType === "delegated_issue" || effectRow.targetType === "issue" || effectRow.targetType === "issue_checkout" || effectRow.targetType === "issue_unblock_descriptor") {
const target = await db.select({ id: issues.id }).from(issues)
.where(eq(issues.id, effectRow.targetId!)).then((rows) => rows[0] ?? null);
@@ -1182,7 +1437,19 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
if (persistedEffects.includes("record_mode_native")) expect(persistedRun?.runtimeMode, `${fixture.id}:mode target`).toBe("native");
if (persistedEffects.includes("record_policy_version")) expect(persistedRunnerProfile.nativeStatusPolicyVersion, `${fixture.id}:policy target`).toBe("phase6-v2");
if (persistedEffects.includes("finish_as_native")) expect(persistedRunResult.nativeKillSwitchDisposition, `${fixture.id}:finish-native target`).toBe("finish_as_native");
if (persistedEffects.includes("stop_new_native_dispatch")) expect(persistedRunnerProfile.nativeDispatchAfterRun, `${fixture.id}:kill-switch target`).toBe("legacy");
if (persistedEffects.includes("stop_new_native_dispatch")) {
const runtimeConfig = await db.select({ runtimeConfig: agents.runtimeConfig }).from(agents)
.where(eq(agents.id, agentId)).then((rows) => rows[0]?.runtimeConfig ?? {});
expect((runtimeConfig as { nativeRunner?: { mode?: string } }).nativeRunner?.mode, `${fixture.id}:kill-switch target`).toBe("legacy");
expect(resolveNativeRuntimeMode({
enabled: true,
runtimeConfig,
agent: { id: agentId, status: "running", adapterType: "codex_local" },
issue: { id: seeded.issueId, workMode: "standard" },
target: { kind: "local" },
workspaceId: "fixture-workspace",
})).toEqual(expect.objectContaining({ kind: "legacy", reason: "native_kill_switch_persisted" }));
}
if (materializedEffects.has("link_canonical_request")) {
expect(interactionRows.some((row) => row.summary?.startsWith("Canonical native attention request:")), `${fixture.id}:canonical link target`).toBe(true);
}
@@ -1319,6 +1586,27 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
},
});
if (consumerExecutions.length < 2) throw new Error(`${fixture.id} did not execute a concern consumer`);
if (
["turn_scope", "run_scope", "issue_scope_authorized", "replacement_turn_accepted"].includes(completionState)
&& !consumerExecutions.some((execution) =>
execution.consumer === "native-session-cancellation" && typeof execution.observed.auditId === "string"
)
) {
throw new Error(`${fixture.id}: live cancellation proof missing`);
}
if (
attentionFacts
&& ["attention_response", "attention_candidate", "interaction", "monitor"].includes(String(fixture.given.trigger))
&& !consumerExecutions.some((execution) => execution.consumer === "native-attention-entrypoint")
) {
throw new Error(`${fixture.id}: live attention proof missing`);
}
if (
["REC-04", "REC-06", "REC-07", "REC-08"].some((row) => (fixture.covers.reconciliationRows ?? []).includes(row))
&& !consumerExecutions.some((execution) => execution.consumer === "native-reconciliation-entrypoint")
) {
throw new Error(`${fixture.id}: live reconciliation proof missing`);
}
return {
fixtureId: fixture.id,
@@ -1418,6 +1706,109 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
}
}, 60_000);
it("fails mapped fixtures when live entrypoints or owning actions are removed despite matching policy labels", async () => {
const byId = (id: string) => {
const fixture = corpus.fixtures.find((candidate) => candidate.id === id);
if (!fixture) throw new Error(`fixture missing: ${id}`);
return fixture;
};
expect(resolveNativeCancellationStatus({
scope: "turn",
priorIssueStatus: "in_progress",
agentId,
}).reasonCode).toBe("cancellation_turn_only");
await expect(executeFixture(byId("turn-only-cancellation"), {
disableLiveEntrypoint: "cancellation",
})).rejects.toThrow("live cancellation proof missing");
expect(resolveNativeAttentionStatus({
facts: {
companyScopeValid: true,
responseState: "none",
route: "agent",
summary: "delegate",
resolvedTargetAgentId: delegateAgentId,
},
priorIssueStatus: "in_progress",
agentId,
}).reasonCode).toBe("attention_routed_to_agent");
await expect(executeFixture(byId("human-request-routed-to-agent"), {
disableLiveEntrypoint: "attention",
})).rejects.toThrow("live attention proof missing");
expect(resolveNativeReconciliationStatus({
facts: { workspaceOperationPending: true },
priorIssueStatus: "in_progress",
agentId,
}).effects.map((effect) => effect.kind)).toContain("resume_workspace_operation");
await expect(executeFixture(byId("crash-before-workspace-finalization"), {
disableLiveEntrypoint: "reconciliation",
})).rejects.toThrow("native_workspace_operation_not_executed");
expect(resolveNativeMigrationStatus({
facts: { killSwitchActiveForNewRuns: true },
priorIssueStatus: "in_progress",
agentId,
}).effects.map((effect) => effect.kind)).toContain("stop_new_native_dispatch");
await expect(executeFixture(byId("migration-kill-switch-rollback"), {
disableLiveEntrypoint: "rollout",
})).rejects.toThrow("kill-switch target");
const pendingFixture = byId("crash-after-decision-commit");
const seeded = await seedFixture(pendingFixture);
const [decision] = await db.insert(statusDecisions).values({
companyId,
issueId: seeded.issueId,
assessmentId: seeded.assessmentId,
decisionVersion: 1,
policyVersion: "phase6-v2",
fromStatus: "in_progress",
toStatus: "in_progress",
reasonCode: "live_continuation_registered",
decisionJson: { fixtureId: pendingFixture.id },
decisionDigest: `pending-negative:${seeded.issueId}`,
applicationState: "applied",
appliedAt: new Date(),
}).returning({ id: statusDecisions.id });
const [wake] = await db.insert(agentWakeupRequests).values({
companyId,
agentId,
source: "automation",
triggerDetail: "system",
reason: "issue_status_changed",
payload: { issueId: seeded.issueId, nativeDecisionId: decision!.id },
requestedByActorType: "system",
requestedByActorId: "native-status-committer",
idempotencyKey: `pending-negative:${seeded.issueId}`,
}).returning({ id: agentWakeupRequests.id });
await db.insert(statusDecisionEffects).values({
companyId,
issueId: seeded.issueId,
decisionId: decision!.id,
ordinal: 1,
effectKind: "enqueue_continuation",
targetType: "agent_wakeup_request",
targetId: wake!.id,
idempotencyKey: `pending-negative:${seeded.issueId}`,
payload: { fixtureId: pendingFixture.id },
deliveryState: "pending",
attemptCount: 0,
});
await db.update(nativeRunFinalizations).set({
phase: "committed",
decisionId: decision!.id,
}).where(eq(nativeRunFinalizations.runId, seeded.runId));
await db.delete(agentWakeupRequests).where(eq(agentWakeupRequests.id, wake!.id));
expect(resolveNativeReconciliationStatus({
facts: { undeliveredEffectCount: 1 },
priorIssueStatus: "in_progress",
agentId,
}).effects.map((effect) => effect.kind)).toContain("dispatch_pending_effect");
await expect(reconcileNativeFinalizations(db, [seeded.runId]))
.rejects.toThrow("native_pending_effect_target_missing:enqueue_continuation");
}, 30_000);
it("fails the transaction closed for an unknown status effect", async () => {
const fixture = corpus.fixtures.find((candidate) => candidate.mode === "native");
if (!fixture) throw new Error("native corpus fixture missing");
+10 -2
View File
@@ -18737,7 +18737,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
const running = runningProcesses.get(run.id);
try {
await cancelNativeSession(run.id, reason, { db, scope: "run" });
const nativeCancellation = await cancelNativeSession(run.id, reason, { db, scope: "run" });
if (run.runtimeMode === "native" && (!nativeCancellation.decision || !nativeCancellation.auditId)) {
throw new Error("native_cancellation_outcome_not_audited");
}
if (running) {
await terminateHeartbeatRunProcess({
pid: running.child.pid ?? run.processPid,
@@ -18755,11 +18758,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
}
const finishedAt = new Date();
const persistedCancellationResult = run.runtimeMode === "native"
? await getRun(run.id).then((current) => parseObject(current?.resultJson))
: {};
const cancelled = await setRunStatus(run.id, "cancelled", {
finishedAt,
error: reason,
errorCode,
...(resultJson ? { resultJson } : {}),
...(resultJson || Object.keys(persistedCancellationResult).length > 0
? { resultJson: { ...persistedCancellationResult, ...(resultJson ?? {}) } }
: {}),
});
await setWakeupStatus(run.wakeupRequestId, "cancelled", {
@@ -2,15 +2,26 @@ import { createHash, randomUUID } from "node:crypto";
import { and, desc, eq, gt, inArray, isNotNull, isNull, lte, notInArray, or } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
completionContracts,
heartbeatRuns,
issueWorkProducts,
issues,
nativeRunFinalizations,
nativeRunResults,
statusDecisionEffects,
statusDecisions,
workAssessments,
workspaceOperations,
} from "@paperclipai/db";
import { finalizeNativeRun, recordNativeFinalizationFailure } from "./native-run-finalizer.js";
import { dispatchPendingNativeStatusEffects } from "./status-decision-committer.js";
import {
commitNativeStatusDecision,
dispatchPendingNativeStatusEffects,
} from "./status-decision-committer.js";
import { issueRecoveryActionService } from "../issue-recovery-actions.js";
import { resumeNativeWorkspaceFinalization } from "./native-workspace-finalizer.js";
import { classifyNativeEvidence } from "./evidence-classifier.js";
import { recordNativeWorkAssessment } from "./work-assessments.js";
import {
NATIVE_STATUS_ARBITER_POLICY_VERSION,
type NativeAuthoritativeIssueStatus,
@@ -31,6 +42,12 @@ export type NativeReconciliationFacts = {
statusVersionAdvanced?: boolean;
};
function record(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
/** Append-only recovery/reconciliation re-arbitration from durable facts. */
export function resolveNativeReconciliationStatus(input: {
facts: NativeReconciliationFacts;
@@ -288,6 +305,9 @@ export async function reconcileNativeFinalizations(db: Db, runIds?: string[]) {
agentId: heartbeatRuns.agentId,
issueId: nativeRunFinalizations.issueId,
issueStatus: issues.status,
issueStatusVersion: issues.statusVersion,
issueDecisionId: issues.lastStatusDecisionId,
assessmentId: nativeRunFinalizations.assessmentId,
decisionId: nativeRunFinalizations.decisionId,
})
.from(heartbeatRuns)
@@ -340,6 +360,145 @@ export async function reconcileNativeFinalizations(db: Db, runIds?: string[]) {
results.push({ runId: row.runId, phase: "committed", deliveredEffectIds });
continue;
}
if (row.assessmentId) {
const assessment = await db.select({
id: workAssessments.id,
contractId: workAssessments.contractId,
resultId: workAssessments.resultId,
policyVersion: workAssessments.policyVersion,
priorIssueStatus: workAssessments.priorIssueStatus,
priorStatusVersion: workAssessments.priorStatusVersion,
assessmentJson: workAssessments.assessmentJson,
createdAt: workAssessments.createdAt,
}).from(workAssessments).where(and(
eq(workAssessments.id, row.assessmentId),
eq(workAssessments.companyId, row.companyId),
eq(workAssessments.issueId, row.issueId),
)).limit(1).then((entries) => entries[0] ?? null);
const changedEvidence = assessment
? await db.select({ id: issueWorkProducts.id }).from(issueWorkProducts).where(and(
eq(issueWorkProducts.companyId, row.companyId),
eq(issueWorkProducts.issueId, row.issueId),
or(
gt(issueWorkProducts.createdAt, assessment.createdAt),
gt(issueWorkProducts.updatedAt, assessment.createdAt),
),
)).limit(1).then((entries) => entries[0] ?? null)
: null;
const policyVersionChanged = assessment?.policyVersion !== NATIVE_STATUS_ARBITER_POLICY_VERSION;
const currentDecision = row.decisionId
? await db.select({
assessmentId: statusDecisions.assessmentId,
decisionVersion: statusDecisions.decisionVersion,
toStatus: statusDecisions.toStatus,
decisionJson: statusDecisions.decisionJson,
}).from(statusDecisions).where(and(
eq(statusDecisions.id, row.decisionId),
eq(statusDecisions.companyId, row.companyId),
eq(statusDecisions.issueId, row.issueId),
)).limit(1).then((entries) => entries[0] ?? null)
: null;
const currentDecisionJson = record(currentDecision?.decisionJson);
const expectedProjectedVersion = currentDecision
? Number(currentDecision.decisionVersion) - (currentDecisionJson.statusAction === "preserve" ? 1 : 0)
: null;
const issueMatchesCurrentDecision = !!assessment
&& !!currentDecision
&& row.issueDecisionId === row.decisionId
&& currentDecision.assessmentId === assessment.id
&& currentDecision.toStatus === row.issueStatus
&& expectedProjectedVersion === Number(row.issueStatusVersion);
const authoritativeStatusChanged = !!assessment && !issueMatchesCurrentDecision && (
assessment.priorIssueStatus !== row.issueStatus
|| Number(assessment.priorStatusVersion) !== Number(row.issueStatusVersion)
);
let reassessment = null;
let resultRow = null;
let contractRow = null;
if (assessment && (policyVersionChanged || authoritativeStatusChanged || changedEvidence)) {
[resultRow, contractRow] = await Promise.all([
db.select().from(nativeRunResults).where(and(
eq(nativeRunResults.id, assessment.resultId),
eq(nativeRunResults.companyId, row.companyId),
eq(nativeRunResults.issueId, row.issueId),
eq(nativeRunResults.runId, row.runId),
)).limit(1).then((entries) => entries[0] ?? null),
db.select().from(completionContracts).where(and(
eq(completionContracts.id, assessment.contractId),
eq(completionContracts.companyId, row.companyId),
eq(completionContracts.issueId, row.issueId),
)).limit(1).then((entries) => entries[0] ?? null),
]);
if (!resultRow || !contractRow) throw new Error("native_reconciliation_evidence_binding_missing");
reassessment = await classifyNativeEvidence({
db,
companyId: row.companyId,
issueId: row.issueId,
runId: row.runId,
contract: record(contractRow.contractJson),
result: record(record(resultRow.resultJson).result),
});
}
const previousAssessment = record(assessment?.assessmentJson);
const newEvidenceSatisfiesContract = !!changedEvidence
&& previousAssessment.allCriteriaSatisfied !== true
&& reassessment?.allCriteriaSatisfied === true
&& reassessment.verificationPassed === true;
const facts: NativeReconciliationFacts = policyVersionChanged
? { policyVersionChanged: true }
: newEvidenceSatisfiesContract
? { newEvidenceSatisfiesContract: true }
: authoritativeStatusChanged
? { authoritativeStatusChanged: true }
: {};
if (Object.keys(facts).length > 0) {
if (!assessment || !reassessment || !resultRow || !contractRow) {
throw new Error("native_reconciliation_reassessment_missing");
}
const reassessmentRow = await recordNativeWorkAssessment({
db,
companyId: row.companyId,
issueId: row.issueId,
runId: row.runId,
turnId: resultRow.turnId,
contractId: contractRow.id,
contractCanonicalSha256: contractRow.canonicalSha256,
resultId: resultRow.id,
resultCanonicalSha256: resultRow.canonicalSha256,
priorIssueStatus: row.issueStatus,
priorStatusVersion: Number(row.issueStatusVersion),
priorDecisionId: row.issueDecisionId,
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
assessment: reassessment,
supersedesAssessmentId: assessment.id,
});
const decision = resolveNativeReconciliationStatus({
facts,
priorIssueStatus: row.issueStatus as NativeAuthoritativeIssueStatus,
agentId: row.agentId,
});
const committed = await commitNativeStatusDecision({
db,
companyId: row.companyId,
issueId: row.issueId,
runId: row.runId,
assessmentId: reassessmentRow.id,
priorStatus: row.issueStatus,
priorStatusVersion: Number(row.issueStatusVersion),
priorDecisionId: row.issueDecisionId,
decision,
allowSupersedingCommittedDecision: true,
});
results.push({
runId: row.runId,
phase: "committed",
reconciliationAction: decision.reasonCode,
decisionId: committed.decision.id,
reconciliationDecision: decision,
});
continue;
}
}
const barrier = await db.select({ status: workspaceOperations.status })
.from(workspaceOperations)
.where(and(
@@ -349,7 +508,48 @@ export async function reconcileNativeFinalizations(db: Db, runIds?: string[]) {
.orderBy(desc(workspaceOperations.createdAt))
.limit(1)
.then((entries) => entries[0] ?? null);
if (!barrier || !["succeeded", "failed"].includes(barrier.status)) continue;
if (!barrier || barrier.status !== "succeeded") {
const recoveryDecision = resolveNativeReconciliationStatus({
facts: { workspaceOperationPending: true },
priorIssueStatus: row.issueStatus as NativeAuthoritativeIssueStatus,
agentId: row.agentId,
});
if (!recoveryDecision.effects.some((effect) => effect.kind === "resume_workspace_operation")) {
throw new Error("native_reconciliation_workspace_resume_policy_missing");
}
const operation = await resumeNativeWorkspaceFinalization({ db, runId: row.runId });
const workspaceFinalizeStatus = operation.status === "succeeded" ? "succeeded" : "failed";
try {
const finalized = await finalizeNativeRun({
db,
runId: row.runId,
workspaceFinalizeStatus,
projectRunStatus: true,
});
results.push({
...finalized,
reconciliationAction: "resume_workspace_operation" as const,
workspaceOperationId: operation.id,
workspaceFinalizeStatus,
reconciliationDecision: recoveryDecision,
});
} catch (error) {
const failure = await recordNativeFinalizationFailure({
db,
runId: row.runId,
error,
projectRunStatus: true,
});
results.push({
...failure,
reconciliationAction: "resume_workspace_operation" as const,
workspaceOperationId: operation.id,
workspaceFinalizeStatus,
reconciliationDecision: recoveryDecision,
});
}
continue;
}
const replayDecision = resolveNativeReconciliationStatus({
facts: { canonicalReplay: true },
priorIssueStatus: row.issueStatus as NativeAuthoritativeIssueStatus,
@@ -1,8 +1,10 @@
import type { Db } from "@paperclipai/db";
import type { NativeInteractionResponseEnvelope } from "@paperclipai/paperclip-runner";
import { and, eq, inArray } from "drizzle-orm";
import { heartbeatRuns, issues, issueThreadInteractions, nativeRunFinalizations } from "@paperclipai/db";
import { and, asc, eq, inArray } from "drizzle-orm";
import { agents, heartbeatRuns, issues, issueThreadInteractions, nativeRunFinalizations } from "@paperclipai/db";
import { getAgentWorkEligibility } from "@paperclipai/shared";
import { issueThreadInteractionService } from "../issue-thread-interactions.js";
import { commitNativeStatusDecision } from "./status-decision-committer.js";
import {
NATIVE_STATUS_ARBITER_POLICY_VERSION,
type NativeAuthoritativeIssueStatus,
@@ -319,8 +321,132 @@ export type NativeAttentionFacts = {
summary: string;
budgetExhausted?: boolean;
governanceGate?: NativeGovernanceGate | null;
resolvedTargetAgentId?: string | null;
};
export type NativeAttentionCandidate = {
route: NativeAttentionFacts["route"];
summary: string;
responseState?: NativeAttentionFacts["responseState"];
targetAgentId?: string | null;
budgetExhausted?: boolean;
governanceGate?: NativeGovernanceGate | null;
targetInteractionId?: string;
canonicalRequestId?: string;
};
/**
* Canonical production ingress for native attention. It resolves the run and
* issue binding, derives company scope, chooses an eligible in-company
* delegate, and commits (or explicitly audit-materializes) the resulting
* authority decision.
*/
export async function routeNativeAttention(input: {
db: Db;
runId: string;
candidate: NativeAttentionCandidate;
}) {
const binding = await input.db.select({
companyId: heartbeatRuns.companyId,
agentId: heartbeatRuns.agentId,
runtimeMode: heartbeatRuns.runtimeMode,
issueId: nativeRunFinalizations.issueId,
assessmentId: nativeRunFinalizations.assessmentId,
issueStatus: issues.status,
issueStatusVersion: issues.statusVersion,
issueDecisionId: issues.lastStatusDecisionId,
}).from(heartbeatRuns)
.innerJoin(nativeRunFinalizations, eq(nativeRunFinalizations.runId, heartbeatRuns.id))
.innerJoin(issues, and(
eq(issues.id, nativeRunFinalizations.issueId),
eq(issues.companyId, heartbeatRuns.companyId),
))
.where(eq(heartbeatRuns.id, input.runId))
.limit(1)
.then((rows) => rows[0] ?? null);
if (!binding || binding.runtimeMode !== "native") {
throw new NativeInteractionBridgeError("native_attention_binding_mismatch", "Native attention run binding not found");
}
const companyAgents = await input.db.select({
id: agents.id,
companyId: agents.companyId,
name: agents.name,
status: agents.status,
reportsTo: agents.reportsTo,
}).from(agents).where(eq(agents.companyId, binding.companyId)).orderBy(asc(agents.createdAt), asc(agents.id));
const targetSuggestion = input.candidate.targetAgentId
? await input.db.select({ id: agents.id, companyId: agents.companyId })
.from(agents).where(eq(agents.id, input.candidate.targetAgentId)).limit(1)
.then((rows) => rows[0] ?? null)
: null;
const companyScopeValid = !input.candidate.targetAgentId
|| targetSuggestion?.companyId === binding.companyId;
let resolvedTargetAgentId: string | null = null;
if (companyScopeValid && input.candidate.route === "agent") {
const eligible = companyAgents.filter((candidate) =>
candidate.id !== binding.agentId
&& getAgentWorkEligibility({ agent: candidate, agents: companyAgents }).invokable
);
const requested = eligible.find((candidate) => candidate.id === input.candidate.targetAgentId);
resolvedTargetAgentId = requested?.id ?? eligible[0]?.id ?? null;
}
const facts: NativeAttentionFacts = {
companyScopeValid,
responseState: input.candidate.responseState ?? "none",
route: input.candidate.route === "agent" && !resolvedTargetAgentId
? "recovery"
: input.candidate.route,
summary: input.candidate.summary,
budgetExhausted: input.candidate.budgetExhausted,
governanceGate: input.candidate.governanceGate,
resolvedTargetAgentId,
};
const decision = resolveNativeAttentionStatus({
facts,
priorIssueStatus: binding.issueStatus as NativeAuthoritativeIssueStatus,
agentId: binding.agentId,
});
const auditOnly = !companyScopeValid
|| decision.reasonCode === "attention_duplicate_suppressed";
if (auditOnly) {
const targets = await applyNativeAttentionStatusDecision({
db: input.db,
companyId: binding.companyId,
issueId: binding.issueId,
runId: input.runId,
decision,
targetInteractionId: input.candidate.targetInteractionId,
canonicalRequestId: input.candidate.canonicalRequestId,
});
return { decision, decisionId: null, targets, resolvedTargetAgentId };
}
if (!binding.assessmentId) {
throw new NativeInteractionBridgeError(
"native_attention_assessment_missing",
"Native attention requires the server-owned work assessment for this result",
);
}
const committed = await commitNativeStatusDecision({
db: input.db,
companyId: binding.companyId,
issueId: binding.issueId,
runId: input.runId,
assessmentId: binding.assessmentId,
priorStatus: binding.issueStatus,
priorStatusVersion: Number(binding.issueStatusVersion),
priorDecisionId: binding.issueDecisionId,
decision,
});
return {
decision,
decisionId: committed.decision.id,
targets: [],
resolvedTargetAgentId,
};
}
/** Server-owned attention resolution from canonical routing facts. */
export function resolveNativeAttentionStatus(input: {
facts: NativeAttentionFacts;
@@ -395,11 +521,19 @@ export function resolveNativeAttentionStatus(input: {
return continuation("attention_resolved_from_context");
}
if (input.facts.route === "agent") {
if (!input.facts.resolvedTargetAgentId) {
return preserve("attention_budget_exhausted", [{
kind: "record_finalization_error",
cause: "attention_no_valid_route",
nextAction: "Assign a same-company eligible delegate.",
agentId: input.agentId,
}]);
}
const decision = continuation("attention_routed_to_agent");
return {
...decision,
effects: [
{ kind: "create_delegated_issue", agentId: input.agentId, summary: input.facts.summary },
{ kind: "create_delegated_issue", agentId: input.facts.resolvedTargetAgentId, summary: input.facts.summary },
...decision.effects,
],
};
@@ -15,6 +15,8 @@ import { and, eq, sql } from "drizzle-orm";
import { heartbeatRuns, issues, nativeRunFinalizations } from "@paperclipai/db";
import { PaperclipControlPlanePort } from "./paperclip-control-plane-port.js";
import { issueRecoveryActionService } from "../issue-recovery-actions.js";
import { persistActivity, publishActivity } from "../activity-log.js";
import { commitNativeStatusDecision } from "./status-decision-committer.js";
import {
NATIVE_STATUS_ARBITER_POLICY_VERSION,
type NativeAuthoritativeIssueStatus,
@@ -48,58 +50,146 @@ export function cancelNativeSession(
runId: string,
reason: string,
options: { db: Db; scope?: "turn" | "run" | "issue"; replacementAccepted?: boolean },
): Promise<{ dispatched: boolean; decision: NativeStatusDecision | null }>;
): Promise<{ dispatched: boolean; decision: NativeStatusDecision | null; decisionId: string | null; auditId: string | null }>;
export async function cancelNativeSession(
runId: string,
reason: string,
options?: { db: Db; scope?: "turn" | "run" | "issue"; replacementAccepted?: boolean },
): Promise<boolean | { dispatched: boolean; decision: NativeStatusDecision | null }> {
): Promise<boolean | { dispatched: boolean; decision: NativeStatusDecision | null; decisionId: string | null; auditId: string | null }> {
let decision: NativeStatusDecision | null = null;
let decisionContext: {
companyId: string;
issueId: string;
assessmentId: string | null;
priorStatus: string;
priorStatusVersion: number;
priorDecisionId: string | null;
agentId: string;
} | null = null;
if (options) {
const run = await options.db.select({
agentId: heartbeatRuns.agentId,
companyId: heartbeatRuns.companyId,
contextSnapshot: heartbeatRuns.contextSnapshot,
runtimeMode: heartbeatRuns.runtimeMode,
}).from(heartbeatRuns).where(eq(heartbeatRuns.id, runId)).limit(1).then((rows) => rows[0] ?? null);
const context = record(run?.contextSnapshot);
const issueId = typeof context.issueId === "string"
? context.issueId
: typeof context.taskId === "string" ? context.taskId : null;
const issue = run && issueId
? await options.db.select({ status: issues.status }).from(issues).where(and(
? await options.db.select({
status: issues.status,
statusVersion: issues.statusVersion,
lastStatusDecisionId: issues.lastStatusDecisionId,
}).from(issues).where(and(
eq(issues.id, issueId),
eq(issues.companyId, run.companyId),
)).limit(1).then((rows) => rows[0] ?? null)
: null;
if (run && issue) {
if (run && issue && issueId && run.runtimeMode === "native") {
decision = resolveNativeCancellationStatus({
scope: options.scope ?? "run",
priorIssueStatus: issue.status as NativeAuthoritativeIssueStatus,
agentId: run.agentId,
replacementAccepted: options.replacementAccepted,
});
if (options.replacementAccepted && decision.effects.some((effect) => effect.kind === "accept_replacement_turn")) {
await options.db.update(heartbeatRuns).set({
status: "running",
continuationAttempt: sql`${heartbeatRuns.continuationAttempt} + 1`,
nextAction: "Accept a replacement native turn on the existing run.",
updatedAt: new Date(),
}).where(eq(heartbeatRuns.id, runId));
}
const coordinator = await options.db.select({
assessmentId: nativeRunFinalizations.assessmentId,
}).from(nativeRunFinalizations).where(and(
eq(nativeRunFinalizations.runId, runId),
eq(nativeRunFinalizations.companyId, run.companyId),
eq(nativeRunFinalizations.issueId, issueId),
)).limit(1).then((rows) => rows[0] ?? null);
decisionContext = {
companyId: run.companyId,
issueId,
assessmentId: coordinator?.assessmentId ?? null,
priorStatus: issue.status,
priorStatusVersion: Number(issue.statusVersion),
priorDecisionId: issue.lastStatusDecisionId,
agentId: run.agentId,
};
}
}
const active = activeNativeSessions.get(runId);
if (!active) return options ? { dispatched: false, decision } : false;
if (active.cancelRequested) return options ? { dispatched: true, decision } : true;
active.cancelRequested = true;
try {
if (active.session.cancel) await active.session.cancel({ reason });
else if (active.session.interrupt) await active.session.interrupt({ reason });
} catch (error) {
active.cancelRequested = false;
throw error;
let dispatched = false;
if (active) {
dispatched = true;
if (!active.cancelRequested) {
active.cancelRequested = true;
try {
if (active.session.cancel) await active.session.cancel({ reason });
else if (active.session.interrupt) await active.session.interrupt({ reason });
} catch (error) {
active.cancelRequested = false;
throw error;
}
}
}
return options ? { dispatched: true, decision } : true;
if (!options) return dispatched;
let decisionId: string | null = null;
let auditId: string | null = null;
if (decision && decisionContext) {
if (decisionContext.assessmentId && decision.reasonCode !== null) {
const committed = await commitNativeStatusDecision({
db: options.db,
companyId: decisionContext.companyId,
issueId: decisionContext.issueId,
runId,
assessmentId: decisionContext.assessmentId,
priorStatus: decisionContext.priorStatus,
priorStatusVersion: decisionContext.priorStatusVersion,
priorDecisionId: decisionContext.priorDecisionId,
decision,
});
decisionId = committed.decision.id;
} else if (options.replacementAccepted && decision.effects.some((effect) => effect.kind === "accept_replacement_turn")) {
await options.db.update(heartbeatRuns).set({
status: "running",
continuationAttempt: sql`${heartbeatRuns.continuationAttempt} + 1`,
nextAction: "Accept a replacement native turn on the existing run.",
updatedAt: new Date(),
}).where(eq(heartbeatRuns.id, runId));
}
const currentRun = await options.db.select({ resultJson: heartbeatRuns.resultJson })
.from(heartbeatRuns).where(eq(heartbeatRuns.id, runId)).limit(1).then((rows) => rows[0] ?? null);
await options.db.update(heartbeatRuns).set({
resultJson: {
...record(currentRun?.resultJson),
nativeCancellation: {
scope: options.scope ?? "run",
reasonCode: decision.reasonCode,
effects: decision.effects.map((effect) => effect.kind),
dispatched,
decisionId,
},
},
updatedAt: new Date(),
}).where(eq(heartbeatRuns.id, runId));
const activity = await persistActivity(options.db, {
companyId: decisionContext.companyId,
actorType: "system",
actorId: "native-session-cancellation",
action: decisionId ? "issue.status_decision_recorded" : "native.cancellation_audited",
entityType: "heartbeat_run",
entityId: runId,
agentId: decisionContext.agentId,
runId,
issueId: decisionContext.issueId,
details: {
scope: options.scope ?? "run",
reasonCode: decision.reasonCode,
effects: decision.effects.map((effect) => effect.kind),
dispatched,
decisionId,
},
});
auditId = activity.activity?.id ?? null;
publishActivity(activity.publication);
}
return { dispatched, decision, decisionId, auditId };
}
/** Authenticated cancellation scope projected through the shared arbiter. */
@@ -0,0 +1,133 @@
import fs from "node:fs/promises";
import { and, desc, eq } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
executionWorkspaces,
heartbeatRuns,
issues,
nativeRunFinalizations,
workspaceOperations,
} from "@paperclipai/db";
import { workspaceOperationService } from "../workspace-operations.js";
import { inspectManagedGitWorktreeBranch } from "../workspace-runtime.js";
function record(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
function readString(value: unknown) {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
/**
* Resume the real workspace-finalization action for a result-bearing native
* run. This service observes the workspace; it never fabricates a successful
* marker. The caller decides whether a failed observation is retryable.
*/
export async function resumeNativeWorkspaceFinalization(input: {
db: Db;
runId: string;
}) {
const bound = await input.db.select({
companyId: heartbeatRuns.companyId,
runtimeMode: heartbeatRuns.runtimeMode,
runnerProfileJson: heartbeatRuns.runnerProfileJson,
issueId: nativeRunFinalizations.issueId,
resultId: nativeRunFinalizations.resultId,
issueWorkspaceId: issues.executionWorkspaceId,
})
.from(heartbeatRuns)
.innerJoin(nativeRunFinalizations, eq(nativeRunFinalizations.runId, heartbeatRuns.id))
.innerJoin(issues, and(
eq(issues.id, nativeRunFinalizations.issueId),
eq(issues.companyId, heartbeatRuns.companyId),
))
.where(eq(heartbeatRuns.id, input.runId))
.limit(1)
.then((rows) => rows[0] ?? null);
if (!bound || bound.runtimeMode !== "native" || !bound.resultId) {
throw new Error("native_workspace_finalization_binding_missing");
}
const previous = await input.db.select().from(workspaceOperations).where(and(
eq(workspaceOperations.companyId, bound.companyId),
eq(workspaceOperations.heartbeatRunId, input.runId),
eq(workspaceOperations.phase, "workspace_finalize"),
)).orderBy(desc(workspaceOperations.createdAt)).limit(1).then((rows) => rows[0] ?? null);
const persistedInput = record(record(bound.runnerProfileJson).nativeExecutionInput);
const binding = record(persistedInput.binding);
const workspaceId = previous?.executionWorkspaceId
?? bound.issueWorkspaceId
?? readString(binding.executionWorkspaceId);
const workspace = workspaceId
? await input.db.select().from(executionWorkspaces).where(and(
eq(executionWorkspaces.id, workspaceId),
eq(executionWorkspaces.companyId, bound.companyId),
)).limit(1).then((rows) => rows[0] ?? null)
: null;
const cwd = workspace?.providerRef ?? workspace?.cwd ?? previous?.cwd ?? null;
const recorder = workspaceOperationService(input.db).createRecorder({
companyId: bound.companyId,
heartbeatRunId: input.runId,
executionWorkspaceId: workspace?.id ?? workspaceId,
issueId: bound.issueId,
});
return recorder.recordOperation({
phase: "workspace_finalize",
cwd,
metadata: {
resumedFromOperationId: previous?.id ?? null,
owningService: "native_workspace_finalizer",
observation: workspace?.strategyType === "git_worktree"
? "managed_git_worktree_branch"
: "workspace_directory",
},
run: async () => {
if (!cwd) {
return {
status: "failed",
exitCode: 1,
stderr: "Native workspace finalization cannot resolve the persisted workspace path.\n",
};
}
const isDirectory = await fs.stat(cwd).then((stat) => stat.isDirectory()).catch(() => false);
if (!isDirectory) {
return {
status: "failed",
exitCode: 1,
stderr: `Native workspace finalization could not access workspace directory ${cwd}.\n`,
};
}
if (workspace?.strategyType === "git_worktree") {
const inspection = await inspectManagedGitWorktreeBranch({
worktreePath: cwd,
expectedBranchName: workspace.branchName,
});
if (!inspection.valid) {
return {
status: "failed",
exitCode: 1,
stderr: `Managed git worktree validation failed: ${inspection.reason ?? "unknown failure"}.\n`,
metadata: { managedGitWorktreeBranch: inspection },
};
}
return {
status: "succeeded",
exitCode: 0,
system: "Native workspace finalization validated the managed git worktree.\n",
metadata: { managedGitWorktreeBranch: inspection },
};
}
return {
status: "succeeded",
exitCode: 0,
system: "Native workspace finalization validated the persisted workspace directory.\n",
metadata: { observedWorkspacePath: cwd },
};
},
});
}
@@ -45,6 +45,13 @@ export function resolveNativeRuntimeMode(input: {
}): NativeRuntimeResolution {
const nativeRunner = record(record(input.runtimeConfig).nativeRunner);
const mode = nativeRunner.mode;
if (nativeRunner.disabledByNativeKillSwitch === true) {
return {
kind: "legacy",
resolverVersion: NATIVE_RUNTIME_RESOLVER_VERSION,
reason: "native_kill_switch_persisted",
};
}
if (!input.enabled) {
const compatibility = resolveNativeCompatibilityStatus({
facts: { shadowApplicationDisabled: true },
@@ -3,6 +3,7 @@ import type { Db } from "@paperclipai/db";
import type { CreateIssueThreadInteraction } from "@paperclipai/shared";
import {
agentWakeupRequests,
agents,
approvals,
completionContracts,
heartbeatRuns,
@@ -13,6 +14,7 @@ import {
nativeRunFinalizations,
statusDecisionEffects,
statusDecisions,
workAssessments,
workspaceOperations,
} from "@paperclipai/db";
import {
@@ -24,6 +26,7 @@ import { nativeSha256 } from "./canonical.js";
import { issueService } from "../issues.js";
import { issueThreadInteractionService } from "../issue-thread-interactions.js";
import { issueRecoveryActionService } from "../issue-recovery-actions.js";
import { agentService } from "../agents.js";
import { buildIssueBlockersResolvedWakeIdempotencyKey } from "../issue-dependency-wakeups.js";
import { persistActivity, publishActivity, type ActivityPublication } from "../activity-log.js";
@@ -44,6 +47,16 @@ export async function dispatchPendingNativeStatusEffects(input: {
runId: string;
}) {
return input.db.transaction(async (tx) => {
const coordinator = await tx.select({
decisionId: nativeRunFinalizations.decisionId,
}).from(nativeRunFinalizations).where(and(
eq(nativeRunFinalizations.runId, input.runId),
eq(nativeRunFinalizations.companyId, input.companyId),
eq(nativeRunFinalizations.issueId, input.issueId),
)).for("update").limit(1).then((rows) => rows[0] ?? null);
if (!coordinator || coordinator.decisionId !== input.decisionId) {
throw new Error("native_pending_effect_decision_binding_mismatch");
}
const pending = await tx.select({
id: statusDecisionEffects.id,
effectKind: statusDecisionEffects.effectKind,
@@ -93,7 +106,7 @@ export type NativeStatusCommitFailpoint =
| "recovery_materialization"
| "status_projection";
type MaterializedEffect = {
export type NativeMaterializedStatusEffect = {
effectKind: string;
targetType: string;
targetId: string | null;
@@ -125,6 +138,11 @@ async function assertPendingEffectTarget(tx: Db, input: {
eq(agentWakeupRequests.id, input.targetId),
eq(agentWakeupRequests.companyId, input.companyId),
)).limit(1).then((rows) => rows.length === 1);
} else if (input.targetType === "agent") {
found = await tx.select({ id: agents.id }).from(agents).where(and(
eq(agents.id, input.targetId),
eq(agents.companyId, input.companyId),
)).limit(1).then((rows) => rows.length === 1);
} else if (input.targetType === "issue_thread_interaction" || input.targetType === "interaction") {
found = await tx.select({ id: issueThreadInteractions.id }).from(issueThreadInteractions).where(and(
eq(issueThreadInteractions.id, input.targetId),
@@ -306,7 +324,8 @@ async function materializeDecisionEffect(input: {
decisionId: string;
effect: NativeStatusEffect;
failpoint?: NativeStatusCommitFailpoint;
}): Promise<MaterializedEffect> {
preMaterializedEffects?: ReadonlyMap<string, NativeMaterializedStatusEffect>;
}): Promise<NativeMaterializedStatusEffect> {
const { effect } = input;
if (effect.kind === "create_interaction") {
failAt("governance_materialization", input.failpoint);
@@ -396,8 +415,7 @@ async function materializeDecisionEffect(input: {
};
}
if (effect.kind === "create_delegated_issue") {
const delegated = await input.tx.insert(issues).values({
companyId: input.companyId,
const delegated = await issueService(input.tx).create(input.companyId, {
projectId: input.issue.projectId,
projectWorkspaceId: input.issue.projectWorkspaceId,
goalId: input.issue.goalId,
@@ -408,7 +426,9 @@ async function materializeDecisionEffect(input: {
assigneeAgentId: effect.agentId,
createdByAgentId: input.issue.assigneeAgentId,
workMode: "standard",
}).returning({ id: issues.id }).then((rows) => rows[0] ?? null);
idempotencyKey: `native-delegation:${input.decisionId}`,
inheritExecutionWorkspaceFromIssueId: input.issue.id,
});
if (!delegated) throw new Error("native_delegated_issue_not_persisted");
return {
effectKind: effect.kind,
@@ -666,6 +686,27 @@ async function materializeDecisionEffect(input: {
}
if (effect.kind === "append_superseding_assessment") {
if (!input.issue.lastStatusDecisionId) throw new Error("native_superseded_decision_missing");
const lineage = await input.tx.select({
currentAssessmentId: statusDecisions.assessmentId,
}).from(statusDecisions).where(and(
eq(statusDecisions.id, input.decisionId),
eq(statusDecisions.companyId, input.companyId),
)).limit(1).then((rows) => rows[0] ?? null);
const prior = await input.tx.select({
assessmentId: statusDecisions.assessmentId,
}).from(statusDecisions).where(and(
eq(statusDecisions.id, input.issue.lastStatusDecisionId),
eq(statusDecisions.companyId, input.companyId),
)).limit(1).then((rows) => rows[0] ?? null);
if (!lineage || !prior) throw new Error("native_superseded_assessment_missing");
const [assessment] = await input.tx.update(workAssessments).set({
supersedesAssessmentId: prior.assessmentId,
}).where(and(
eq(workAssessments.id, lineage.currentAssessmentId),
eq(workAssessments.companyId, input.companyId),
eq(workAssessments.issueId, input.issue.id),
)).returning({ id: workAssessments.id });
if (!assessment) throw new Error("native_superseding_assessment_not_linked");
const [decision] = await input.tx.update(statusDecisions).set({
supersedesDecisionId: input.issue.lastStatusDecisionId,
}).where(and(
@@ -677,7 +718,11 @@ async function materializeDecisionEffect(input: {
effectKind: effect.kind,
targetType: "status_decision",
targetId: decision.id,
payload: { supersedesDecisionId: input.issue.lastStatusDecisionId },
payload: {
supersedesDecisionId: input.issue.lastStatusDecisionId,
assessmentId: assessment.id,
supersedesAssessmentId: prior.assessmentId,
},
};
}
if (effect.kind === "dispatch_pending_effect") {
@@ -711,13 +756,11 @@ async function materializeDecisionEffect(input: {
};
}
if (effect.kind === "increment_status_version") {
const [updated] = await input.tx.update(issues).set({
statusVersion: sql`${issues.statusVersion} + 1`,
updatedAt: new Date(),
}).where(and(
eq(issues.id, input.issue.id),
eq(issues.companyId, input.companyId),
)).returning({ id: issues.id, statusVersion: issues.statusVersion });
const updated = await issueService(input.tx).update(input.issue.id, {
statusVersion: Number(input.issue.statusVersion) + 1,
actorAgentId: null,
actorUserId: null,
}, input.tx);
if (!updated) throw new Error("native_status_version_not_incremented");
return {
effectKind: effect.kind,
@@ -839,42 +882,54 @@ async function materializeDecisionEffect(input: {
return { effectKind: effect.kind, targetType: "heartbeat_run", targetId, payload: { runtimeMode: "native" } };
}
if (effect.kind === "stop_new_native_dispatch") {
const targetId = await updateRunEffectState({
tx: input.tx,
companyId: input.companyId,
runId: input.runId,
key: "nativeDispatchAfterRun",
value: "legacy",
runnerProfile: true,
const run = await input.tx.select({ agentId: heartbeatRuns.agentId }).from(heartbeatRuns).where(and(
eq(heartbeatRuns.id, input.runId),
eq(heartbeatRuns.companyId, input.companyId),
)).limit(1).then((rows) => rows[0] ?? null);
if (!run) throw new Error("native_kill_switch_run_missing");
const agent = await agentService(input.tx).getById(run.agentId);
if (!agent || agent.companyId !== input.companyId) throw new Error("native_kill_switch_agent_missing");
const runtimeConfig = record(agent.runtimeConfig);
const nativeRunner = record(runtimeConfig.nativeRunner);
const updated = await agentService(input.tx).update(run.agentId, {
runtimeConfig: {
...runtimeConfig,
nativeRunner: {
...nativeRunner,
mode: "legacy",
disabledByNativeKillSwitch: true,
},
},
}, {
recordRevision: {
source: "native_kill_switch",
createdByAgentId: null,
createdByUserId: null,
},
});
return { effectKind: effect.kind, targetType: "heartbeat_run", targetId, payload: { nextDispatchMode: "legacy" } };
}
if (effect.kind === "resume_workspace_operation") {
const previous = await input.tx.select().from(workspaceOperations).where(and(
eq(workspaceOperations.companyId, input.companyId),
eq(workspaceOperations.heartbeatRunId, input.runId),
eq(workspaceOperations.phase, "workspace_finalize"),
)).orderBy(desc(workspaceOperations.createdAt)).limit(1).then((rows) => rows[0] ?? null);
if (!previous) throw new Error("native_workspace_operation_missing");
const [resumed] = await input.tx.insert(workspaceOperations).values({
companyId: input.companyId,
executionWorkspaceId: previous.executionWorkspaceId,
heartbeatRunId: input.runId,
issueId: input.issue.id,
phase: "workspace_finalize",
status: "succeeded",
exitCode: 0,
metadata: { resumedFromOperationId: previous.id, nativeDecisionId: input.decisionId },
finishedAt: new Date(),
}).returning({ id: workspaceOperations.id });
if (!resumed) throw new Error("native_workspace_operation_not_resumed");
if (!updated) throw new Error("native_kill_switch_not_persisted");
return {
effectKind: effect.kind,
targetType: "workspace_operation",
targetId: resumed.id,
payload: { resumedFromOperationId: previous.id },
targetType: "agent",
targetId: updated.id,
payload: { nextDispatchMode: "legacy", runtimeConfigOwner: updated.id },
};
}
if (effect.kind === "resume_workspace_operation") {
const resumed = input.preMaterializedEffects?.get(effect.kind);
if (!resumed || resumed.targetType !== "workspace_operation" || !resumed.targetId) {
throw new Error("native_workspace_operation_not_executed");
}
const operation = await input.tx.select({ status: workspaceOperations.status }).from(workspaceOperations).where(and(
eq(workspaceOperations.id, resumed.targetId),
eq(workspaceOperations.companyId, input.companyId),
eq(workspaceOperations.heartbeatRunId, input.runId),
eq(workspaceOperations.issueId, input.issue.id),
eq(workspaceOperations.phase, "workspace_finalize"),
)).limit(1).then((rows) => rows[0] ?? null);
if (operation?.status !== "succeeded") throw new Error("native_workspace_operation_not_succeeded");
return resumed;
}
if (effect.kind === "record_stale_response") {
const interaction = await input.tx.select({ id: issueThreadInteractions.id })
.from(issueThreadInteractions).where(and(
@@ -964,6 +1019,8 @@ export async function commitNativeStatusDecision(input: {
priorDecisionId: string | null;
decision: NativeStatusDecision;
failpoint?: NativeStatusCommitFailpoint;
preMaterializedEffects?: NativeMaterializedStatusEffect[];
allowSupersedingCommittedDecision?: boolean;
}) {
if (input.decision.reasonCode === null) {
throw new Error("native_status_reason_code_required");
@@ -983,13 +1040,22 @@ export async function commitNativeStatusDecision(input: {
)).for("update").limit(1).then((rows) => rows[0] ?? null);
if (!issue) throw new NativeStatusRaceError();
if (coordinator.phase === "committed" && coordinator.decisionId) {
const existingDecision = await tx.select().from(statusDecisions).where(and(
eq(statusDecisions.id, coordinator.decisionId),
eq(statusDecisions.companyId, input.companyId),
eq(statusDecisions.issueId, input.issueId),
)).limit(1).then((rows) => rows[0] ?? null);
if (!existingDecision) throw new Error("native_committed_decision_missing");
return { decision: existingDecision, issue, replayed: true };
if (input.allowSupersedingCommittedDecision) {
if (
coordinator.decisionId !== input.priorDecisionId
|| coordinator.assessmentId === input.assessmentId
) {
throw new NativeStatusRaceError();
}
} else {
const existingDecision = await tx.select().from(statusDecisions).where(and(
eq(statusDecisions.id, coordinator.decisionId),
eq(statusDecisions.companyId, input.companyId),
eq(statusDecisions.issueId, input.issueId),
)).limit(1).then((rows) => rows[0] ?? null);
if (!existingDecision) throw new Error("native_committed_decision_missing");
return { decision: existingDecision, issue, replayed: true };
}
}
if (
issue.status !== input.priorStatus
@@ -1038,7 +1104,10 @@ export async function commitNativeStatusDecision(input: {
}
if (!decisionRow) throw new Error("native_status_decision_not_persisted");
const materialized: MaterializedEffect[] = [];
const materialized: NativeMaterializedStatusEffect[] = [];
const preMaterializedEffects = new Map(
(input.preMaterializedEffects ?? []).map((effect) => [effect.effectKind, effect]),
);
for (const effect of input.decision.effects) {
materialized.push(await materializeDecisionEffect({
tx: tx as unknown as Db,
@@ -1048,6 +1117,7 @@ export async function commitNativeStatusDecision(input: {
decisionId: decisionRow.id,
effect,
failpoint: input.failpoint,
preMaterializedEffects,
}));
}