fix(native-runner): wire live attention and rollback proofs

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-09 07:22:30 +00:00
1 parent d53b7902ab
commit c82a2142c9
18 files changed
+1038 -166

No files matched your search

@@ -843,14 +843,16 @@ Policy resolvers remain pure and may be tested directly, but their return value
is not operational proof. Cancellation fixtures enter through
`cancelNativeSession`, which commits the decision or an explicit audit-only
replacement-turn outcome. Attention fixtures enter through
`routeNativeAttention`, which resolves a same-company eligible delegate and
uses the company-scoped issue service. REC-04/06/07/08 fixtures enter through
the persisted accepted-result ingress, which calls `routeNativeAttention`
internally, resolves a same-company eligible delegate, and uses the
company-scoped issue service. REC-04/06/07/08 fixtures enter through
`reconcileNativeFinalizations`; REC-04 invokes the workspace operation recorder
and observes the actual finalizer result. REC-06/07/08 reclassify the persisted
result and contract into a new append-only assessment before an explicitly
authorized superseding commit; REC-07 cannot be selected by an unrelated work
product. MIG-08 persists the kill switch in the agent-owned runtime profile,
and `resolveNativeRuntimeMode` enforces that state for subsequent dispatches.
product. MIG-08 uses the instance-global flag through the production heartbeat selector:
persisted native mode wins for an active run and a fresh unresolved run selects
legacy while the agent profile remains unchanged.
`resolveNativeFinalizerStatus`, `resolveNativeAttentionStatus`,
`resolveNativeCancellationStatus`, `resolveNativeReconciliationStatus`, the
@@ -863,6 +865,58 @@ tests deliberately remove each live entrypoint/action and the replay target;
the mapped fixture fails even though the corresponding pure resolver still
returns its expected label.
### Remediation 6 persisted-attention and global-kill-switch amendment (2026-08-09)
Accepted native attention now enters through the runtime finalization call
graph, not through a corpus-owned call to `routeNativeAttention`:
```text
PaperclipControlPlanePort.completeRun
-> native_run_results (accepted immutable package result)
-> finalizeNativeRun
-> routePersistedNativeResultAttention
-> recordNativeAttentionAssessment
-> routeNativeAttention
-> StatusDecisionCommitter
-> issueService / issueThreadInteractionService / recovery + activity owners
```
The persisted-result ingress derives company, issue, run, agent, result, and
contract identity from database bindings. A same-company eligible delegate is
materialized by `issueService`; a human-authority request is materialized as a
typed issue-thread interaction; an explicit cross-company target is rejected
into an immutable decision, recovery action, failed native finalization, and
activity receipt. The corpus mutates the accepted result row and calls the
persisted-result ingress. The database integration test starts one layer
earlier at `PaperclipControlPlanePort.completeRun` and then calls the same
production finalizer. `routeNativeAttention` remains an internal policy-to-
owner helper and is no longer itself accepted as the operational proof.
MIG-08 now represents the actual instance-global transition. The production
heartbeat selector reads the persisted mode first: an already-selected native
run remains native and can finish or reconcile from its coordinator after
`experimental.enableNativeRunner` turns off. An unresolved fresh run for the
same still-opted-in agent resolves to legacy with
`instance_flag_disabled`. The agent's `runtimeConfig.nativeRunner` profile is
not rewritten, and no `disabledByNativeKillSwitch` or other second rollback
control is persisted. Re-enabling the instance flag therefore restores normal
fresh-run selection without an operator profile repair.
Sabotage changes the production inputs rather than the policy labels: removing
the persisted-result ingress breaks the attention fixtures, and changing the
global flag input from off to on breaks MIG-08 while
`resolveNativeAttentionStatus` and `resolveNativeMigrationStatus` continue to
return their pure labels. The full recovery test observes both durable
outcomes: the original native rows reach committed terminal state and the
fresh run persists legacy mode with zero native result/finalization rows.
This amendment does not add an attention UI, a public native-attention route,
external-system execution, credential delegation, or auto-approval. The Codex
v1 structured-output surface still emits the compact `kind`/`summary` form;
the canonical PRP result can carry richer target metadata, which the server
validates as untrusted routing hints. Richer provider authoring UX and
additional resolver kinds remain deferred.
## Commands the implementation must make runnable
These commands are the acceptance contract for the implementation issue. They
@@ -114,21 +114,29 @@ decision, ledger, issue-version, or coordinator mutation.
Operational rows use the production call graph, not resolver calls as a proxy.
Cancellation is committed and audited by the normalized session cancellation
entrypoint. Attention resolves an eligible same-company target and delegates
through the issue service. Reconciliation selects the persisted
entrypoint. Accepted attention starts at the immutable package-result row:
`finalizeNativeRun` calls `routePersistedNativeResultAttention`, records a
request-specific assessment, and then delegates an eligible same-company
target through `issueService`, creates a human issue-thread interaction, or
persists a cross-company rejection decision/recovery/audit trail.
`routeNativeAttention` is an internal helper, not the operational test ingress.
Reconciliation selects the persisted
status/evidence/policy branches and executes workspace recovery through the
workspace operation recorder. The rollout kill switch is stored in the owning
agent profile and consumed by subsequent runtime selection. Pending replay
workspace operation recorder. The rollout kill switch is only the global
`experimental.enableNativeRunner` flag. Persisted native runs keep their mode
and finish/reconcile natively after flag-off; fresh unresolved runs for the
same agent persist legacy mode with `instance_flag_disabled`. The opted-in
agent profile remains unchanged, so re-enabling the flag does not require a
profile reset. Pending replay
only acknowledges its original decision-scoped target after verifying that the
target still exists.
Direct calls to the finalizer, attention, cancellation, reconciliation,
compatibility, and migration resolvers are explicitly pure policy/read-model
tests. They do not satisfy an operational fixture without a live-entrypoint
receipt and concrete target state. Sabotage cases remove cancellation,
attention, reconciliation, rollout enforcement, and a pending replay target;
each mapped fixture fails while its pure resolver continues to return the
expected label.
Direct resolver calls are explicitly pure policy/read-model tests. They do not
satisfy an operational fixture without a runtime-reachable entrypoint receipt
and concrete durable target. Sabotage removes the persisted accepted-attention
input, changes the real global-flag input, removes cancellation/reconciliation
actions, and deletes a pending replay target; mapped fixtures fail while their
pure resolver labels remain unchanged.
Result-less same-run recovery is also proven through the production heartbeat
seam: a persisted envelope/checkpoint is leased by `reapOrphanedRuns`, enters
@@ -146,6 +154,13 @@ the instance flag and one agent profile. The package `trace:phase6` command is a
mock contract tracer; real Paperclip proof is the database-backed server matrix
and the explicitly gated procedure in the tutorial.
Deferred boundaries are unchanged: there is no attention UI or public
attention API, no external-system action execution, no credential delegation,
and no auto-approval. The Codex v1 structured-output schema authors compact
`kind`/`summary` attention requests; richer canonical PRP target metadata is
accepted and company-validated at the server boundary but does not yet have
provider authoring UX.
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).
@@ -60,7 +60,7 @@ pnpm --filter @paperclipai/server exec vitest run \
```
This runs the unchanged package conformance suite against the real Paperclip
port. Forty-three targeted tests check company binding, duplicate/gap replay,
port. Forty-five targeted tests check company binding, duplicate/gap replay,
checkpoint recovery, immutable result ingestion, durable-evidence authority,
workspace-gated finalization, atomic liveness rollback, status-version CAS,
migration repair, legacy byte equivalence, typed interaction materialization,
@@ -69,11 +69,16 @@ start embedded PostgreSQL and do not skip when a developer database URL is
absent.
The corpus cases dispatch fixture-specific database shapes through live
cancellation, attention, reconciliation, and rollout entrypoints whenever an
operational effect is claimed. Workspace recovery records the actual operation
result, delegation uses an eligible same-company agent and the issue service,
and a persisted kill switch changes the next runtime selection. Pure resolver
and read-model calls remain useful policy tests but cannot satisfy these rows.
cancellation, persisted-result attention, reconciliation, and heartbeat
selection entrypoints whenever an operational effect is claimed. Workspace
recovery records the actual operation result. Accepted attention is read back
from `native_run_results`, records a request-specific assessment, delegates an
eligible same-company agent through the issue service, routes human authority
through an issue-thread interaction, and rejects a cross-company target with a
durable decision/recovery/audit trail. The kill-switch case uses the global
instance flag: an active persisted native run remains native while a fresh run
for the same unchanged agent profile selects legacy. Pure resolver and
read-model calls remain useful policy tests but cannot satisfy these rows.
Each native effect must have a delivered, company-bound target or concrete
target-state mutation, and pending replay may only acknowledge its original
decision-scoped target. Sabotage tests remove each live action and the replay
@@ -212,7 +217,9 @@ curl -fsS \
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.
never falls through to the legacy adapter. The agent's `nativeRunner` profile
must still say `mode=native`; flag-off must not persist a second per-agent
disable bit.
Restore the saved profile after the proof:
@@ -231,4 +238,12 @@ jq -n \
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.
changes bytes, a model-reported disposition bypasses the server arbiter, or
flag-off rewrites the agent profile.
This procedure does not exercise an attention UI or public attention endpoint;
neither exists in Phase 6. The Codex v1 result tool authors compact
`kind`/`summary` attention requests. Rich target metadata is a canonical PRP
input accepted and company-validated by the server, but provider authoring UX,
external-system action execution, credential delegation, and auto-approval are
deferred.
@@ -13,7 +13,7 @@ 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.
The counts below are the PAP-16879 remediation rerun. “Internal canary” means
The counts below are the PAP-16880 remediation rerun. “Internal canary” means
the selected-task database test through the public package session contract;
it does not mean that a new live Codex provider task was dispatched.
@@ -39,7 +39,7 @@ existing accepted result event, and appended only the missing terminal fact.
# Real Paperclip adapter and public-session task
The complete acceptance gate below passed 43 tests in thirteen files against
The complete acceptance gate below passed 45 tests in thirteen files against
embedded PostgreSQL with zero skipped tests. Database-backed tests start their
own database instead of skipping when a developer `DATABASE_URL` is absent.
The same package conformance suite passed unchanged against
@@ -70,6 +70,11 @@ The focused corpus also proved:
- resolved confirmations and question answers enter the persisted native input
only through the authorized issue-interaction service; governed, unresolved,
unsupported, or self-approved paths fail closed without credentials;
- accepted package-result attention enters through `finalizeNativeRun` and the
persisted-result router: same-company delegation uses `issueService`, human
authority creates an issue-thread interaction, and cross-company targeting
persists a rejection decision, recovery action, failed finalization, and
audit receipt;
- incomplete/review/yield/cancelled results receive a durable review,
continuation, or recovery path;
- only a task-wide blocker with a named owner and action selects `blocked`;
@@ -82,7 +87,7 @@ The focused corpus also proved:
bindings, recovery, status,
decisions, and effects roll back together before retry ownership is recorded.
The acceptance-matrix entry points passed as one 13-file, 43-test gate:
The acceptance-matrix entry points passed as one 13-file, 45-test gate:
```sh
pnpm --filter @paperclipai/server exec vitest run \
@@ -106,18 +111,21 @@ bounded retry exhaustion, actual flag-off legacy execution, and executable
Section 18.13 fixture-to-consumer traceability. All 52 fixtures and 70 rows are
classified as either live operational proofs or explicit pure policy/read-model
checks. Cancellation persists its decision or audit-only receipt through
`cancelNativeSession`; attention resolves a same-company eligible target and
uses the issue service; REC-04/06/07/08 enter the reconciler, with REC-04
`cancelNativeSession`; attention enters from the accepted result through the
persisted-result router, then resolves a same-company eligible target through
the issue service; REC-04/06/07/08 enter the reconciler, with REC-04
recording an observed workspace operation and REC-06/07/08 writing a newly
classified append-only assessment before superseding the prior decision; and
MIG-08 is enforced by the next runtime selection from the agent-owned
kill-switch profile.
MIG-08 is enforced by the production heartbeat selector using the global flag:
the persisted active run remains native, a fresh unresolved run selects legacy,
and the agent profile remains unchanged.
All eleven expected fields remain derived from entrypoint returns and durable
rows. Every required native effect is joined to its owning target state, each
decision replay retains one identity and one delivery attempt, and pending
replay verifies the original company/issue/decision target before acknowledging
it. The negative suite removes each live action and deletes a replay target;
it. The negative suite removes each live action, changes the real global-flag
input, and deletes a replay target;
the mapped fixtures fail while the direct policy resolver still returns the
expected label. An unknown effect rolls the entire transaction back. No
test-owned scenario policy supplies an observation. The selected-task
@@ -176,7 +184,14 @@ Explicit cancellation, agent pause, and budget hard-stop share
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.
the current flag. The kill switch does not mutate `agents.runtime_config` or
persist a per-agent disable bit.
No attention UI or public attention endpoint is claimed. The Codex v1 result
tool authors compact `kind`/`summary` requests; richer canonical PRP target
metadata is server-validated but does not yet have provider authoring UX.
External-system execution, credential delegation, and auto-approval remain
deferred.
```sh
pnpm --filter @paperclipai/server exec vitest run \
@@ -62,11 +62,17 @@ existing server authority and keep legacy execution as the default.
reconciliation, or runtime selection and retain that entrypoint receipt.
17. Resume workspace finalization through the workspace operation recorder and
observe its actual result; never insert a pre-succeeded recovery marker.
18. Resolve attention delegates from eligible same-company agents and create
delegated work through the issue service. Persist authorized status and
rollout changes through their owning issue and agent services.
18. Begin accepted attention routing at the immutable package-result boundary.
The finalizer records a request-specific assessment, resolves delegates
from eligible same-company agents, creates delegated work through the issue
service, routes human authority through the interaction service, and
records cross-company rejection through decision/recovery/audit owners.
19. Keep pending replay acknowledgement-only, scoped to its original decision,
and fail when the owning transaction's target no longer exists.
20. Treat `experimental.enableNativeRunner` as the only kill switch. Persisted
native runs retain native recovery after flag-off; fresh unresolved runs
select legacy. Never rewrite the agent profile or persist a second rollback
control.
# Evidence
@@ -74,7 +80,7 @@ existing server authority and keep legacy execution as the default.
- [Phase 6 reference](../../docs/phase-06-thin-paperclip-adapter.md)
- [Runnable tutorial](../../docs/tutorials/phase-06-thin-paperclip-adapter.md)
- Package conformance and recovery: four files, seven tests passed.
- Phase 6 acceptance-matrix entry points: thirteen files, forty-three tests passed
- Phase 6 acceptance-matrix entry points: thirteen files, forty-five tests passed
with zero skips, including the database-backed selected-task canary, six
atomic-liveness failpoints, migration, sequencing, bounded recovery, and
legacy compatibility.
@@ -93,10 +99,11 @@ existing server authority and keep legacy execution as the default.
recovery, run, workspace-operation, contract, decision, or governance state.
Unknown effects fail before any decision, ledger, status-version, or
coordinator mutation.
- Negative entrypoint proofs remove cancellation auditing, attention routing,
reconciliation/workspace execution, rollout selector enforcement, and the
original pending-effect target. Every mapped fixture fails even though its
pure resolver still returns the expected policy label.
- Negative entrypoint proofs remove cancellation auditing and the persisted
attention ingress, change the global flag input, remove reconciliation/
workspace execution, and delete the original pending-effect target. Every
mapped fixture fails even though its pure resolver still returns the expected
policy label.
- The production heartbeat reaper resumed one persisted result-less run through
the original execution/finalization path with the flag disabled. It recovered
an already-active scripted provider turn and produced exactly one result,
@@ -125,6 +132,10 @@ existing server authority and keep legacy execution as the default.
- Resolved confirmations and question answers are materialized only through the
existing authorized Paperclip interaction service; governed and unsupported
requests cannot auto-approve.
- Phase 6 adds no attention UI or public attention endpoint. The Codex v1 tool
authors compact `kind`/`summary` requests; richer canonical PRP target hints
are server-validated but do not yet have provider authoring UX. External
execution, credential delegation, and auto-approval remain deferred.
- 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.
@@ -261,8 +261,8 @@
"covers": { "decisionRows": [], "terminalRows": [], "attentionRows": ["ATT-09"], "livenessRows": [], "reconciliationRows": [], "compatibilityRows": [], "migrationRows": [] },
"tags": ["excessive_human_request"],
"given": { "priorIssueStatus": "in_progress", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "yielded", "nativeFinalization": "present", "completionState": "cross_company_target", "trigger": "attention_candidate" },
"expected": { "runStatus": "failed", "statusAction": "preserve", "reasonCode": "result_schema_rejected", "requiredEffects": ["record_finalization_error"], "forbiddenEffects": ["create_interaction", "notify_owner", "enqueue_continuation"], "livePathKind": null, "preserveClaim": true, "nativeRecords": true, "decisionCount": 0, "maxWakeCount": 0, "maxNotificationCount": 0 },
"replay": { "attempts": 2, "sameDecisionDigest": true, "maxSemanticDecisions": 0, "maxDomainEffectsPerKey": 1 }
"expected": { "runStatus": "failed", "statusAction": "preserve", "reasonCode": "result_schema_rejected", "requiredEffects": ["record_finalization_error"], "forbiddenEffects": ["create_interaction", "notify_owner", "enqueue_continuation"], "livePathKind": "recovery", "preserveClaim": true, "nativeRecords": true, "decisionCount": 1, "maxWakeCount": 0, "maxNotificationCount": 0 },
"replay": { "attempts": 2, "sameDecisionDigest": true, "maxSemanticDecisions": 1, "maxDomainEffectsPerKey": 1 }
},
{
"id": "stale-attention-response",
@@ -459,7 +459,7 @@
"covers": { "decisionRows": [], "terminalRows": [], "attentionRows": [], "livenessRows": [], "reconciliationRows": [], "compatibilityRows": [], "migrationRows": ["MIG-08"] },
"tags": ["rollback", "native_legacy_distinction", "reconciliation"],
"given": { "priorIssueStatus": "in_progress", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "yielded", "nativeFinalization": "present", "completionState": "kill_switch_during_active_native_run", "trigger": "runner_finalizer" },
"expected": { "runStatus": "succeeded", "statusAction": "in_progress", "reasonCode": "live_continuation_registered", "requiredEffects": ["enqueue_continuation", "finish_as_native", "stop_new_native_dispatch"], "forbiddenEffects": ["convert_active_run_to_legacy", "drop_coordinator"], "livePathKind": "continuation", "preserveClaim": true, "nativeRecords": true, "decisionCount": 1, "maxWakeCount": 1, "maxNotificationCount": 0 },
"expected": { "runStatus": "succeeded", "statusAction": "in_progress", "reasonCode": "live_continuation_registered", "requiredEffects": ["enqueue_continuation", "finish_as_native", "fresh_flag_off_run_selects_legacy"], "forbiddenEffects": ["convert_active_run_to_legacy", "persist_agent_kill_switch", "drop_coordinator"], "livePathKind": "continuation", "preserveClaim": true, "nativeRecords": true, "decisionCount": 1, "maxWakeCount": 1, "maxNotificationCount": 0 },
"replay": { "attempts": 3, "sameDecisionDigest": true, "maxSemanticDecisions": 1, "maxDomainEffectsPerKey": 1 }
},
{
+5 -3
View File
@@ -262,9 +262,11 @@ check("native and legacy expectations remain intentionally distinct", () => {
check("rollback fixture forbids active-run mode conversion", () => {
const rollback = corpus.fixtures.find((fixture) => fixture.covers.migrationRows.includes("MIG-08"));
if (!rollback) throw new Error("missing MIG-08 fixture");
if (!rollback.expected.requiredEffects.includes("stop_new_native_dispatch") ||
!rollback.expected.forbiddenEffects.includes("convert_active_run_to_legacy")) {
throw new Error("MIG-08 does not prove dispatch stop plus immutable active-run mode");
if (!rollback.expected.requiredEffects.includes("fresh_flag_off_run_selects_legacy") ||
!rollback.expected.requiredEffects.includes("finish_as_native") ||
!rollback.expected.forbiddenEffects.includes("convert_active_run_to_legacy") ||
!rollback.expected.forbiddenEffects.includes("persist_agent_kill_switch")) {
throw new Error("MIG-08 does not prove global flag-off selection plus immutable active-run mode");
}
return rollback.id;
});
@@ -1,17 +1,27 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
activityLog,
agents,
companies,
completionContracts,
createDb,
heartbeatRuns,
issueRecoveryActions,
issueThreadInteractions,
issues,
nativeRunFinalizations,
statusDecisionEffects,
statusDecisions,
workAssessments,
} from "@paperclipai/db";
import { and, eq } from "drizzle-orm";
import { startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
import {
materializeNativeInteractionResponses,
NativeInteractionBridgeError,
} from "../services/native-runtime/native-interaction-bridge.js";
import { PaperclipControlPlanePort } from "../services/native-runtime/paperclip-control-plane-port.js";
import { finalizeNativeRun } from "../services/native-runtime/native-run-finalizer.js";
describe("P6-19 native interaction bridge", () => {
let temporary: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
@@ -169,4 +179,203 @@ describe("P6-19 native interaction bridge", () => {
expect(error).toBeInstanceOf(NativeInteractionBridgeError);
expect(error).toMatchObject({ code });
});
it("routes accepted package-result attention through live issue and interaction owners", async () => {
const delegateId = "78000000-0000-4000-8000-000000000020";
const outsideCompanyId = "78000000-0000-4000-8000-000000000021";
const outsideAgentId = "78000000-0000-4000-8000-000000000022";
await db.insert(companies).values({ id: outsideCompanyId, name: "Outside attention", issuePrefix: "NIO" });
await db.insert(agents).values([
{
id: delegateId,
companyId,
name: "Native attention delegate",
adapterType: "codex_local",
status: "idle",
},
{
id: outsideAgentId,
companyId: outsideCompanyId,
name: "Outside attention delegate",
adapterType: "codex_local",
status: "idle",
},
]);
const scenarios = [
{
key: "agent",
issueId: "78000000-0000-4000-8000-000000000023",
runId: "78000000-0000-4000-8000-000000000024",
contractId: "78000000-0000-4000-8000-000000000025",
disposition: "yielded" as const,
request: {
id: "attention-agent",
requestedCapability: "domain_expertise",
summary: "Delegate a same-company native investigation",
target: { ownerClass: "agent", agentId: delegateId, companyId },
},
},
{
key: "human",
issueId: "78000000-0000-4000-8000-000000000026",
runId: "78000000-0000-4000-8000-000000000027",
contractId: "78000000-0000-4000-8000-000000000028",
disposition: "needs_review" as const,
request: {
id: "attention-human",
requestedCapability: "subjective_decision",
requiredAuthority: "board",
summary: "Choose the acceptable native result",
target: { ownerClass: "board_user", companyId },
},
},
{
key: "cross-company",
issueId: "78000000-0000-4000-8000-000000000029",
runId: "78000000-0000-4000-8000-000000000030",
contractId: "78000000-0000-4000-8000-000000000031",
disposition: "yielded" as const,
request: {
id: "attention-cross-company",
requestedCapability: "domain_expertise",
summary: "Reject an outside-company native delegate",
target: { ownerClass: "agent", agentId: outsideAgentId, companyId: outsideCompanyId },
},
},
];
for (const scenario of scenarios) {
const contractSha = `attention-contract:${scenario.key}`;
await db.insert(issues).values({
id: scenario.issueId,
companyId,
title: `Native attention ${scenario.key}`,
status: "in_progress",
assigneeAgentId: agentId,
workMode: "standard",
});
await db.insert(completionContracts).values({
id: scenario.contractId,
companyId,
issueId: scenario.issueId,
revision: 1,
schemaVersion: "paperclip.completion-contract.v1",
policyVersion: "phase6-v1",
risk: "standard",
completionAuthority: "server_arbiter",
incompleteCriteriaPolicy: "preserve_non_terminal",
contractJson: { revision: "attention-v1", objective: scenario.key, criteria: [{ id: "objective" }] },
canonicalSha256: contractSha,
createdByActorType: "system",
createdByActorId: "test",
});
await db.insert(heartbeatRuns).values({
id: scenario.runId,
companyId,
agentId,
status: "running",
runtimeMode: "native",
runtimeModeResolverVersion: "phase6-v1",
runtimeModeReason: "eligible_opt_in",
runtimeModeResolvedAt: new Date(),
completionContractId: scenario.contractId,
completionContractSha256: contractSha,
contextSnapshot: { issueId: scenario.issueId },
});
const port = new PaperclipControlPlanePort(db, {
companyId,
issueId: scenario.issueId,
runId: scenario.runId,
agentId,
completionContractId: scenario.contractId,
completionContractSha256: contractSha,
sourceInstanceId: `runner:${scenario.key}`,
controlPlaneSourceInstanceId: `control:${scenario.key}`,
});
await port.completeRun({
result: {
schema: "paperclip.run_result.v1",
reportedWorkDisposition: scenario.disposition,
summary: `Native attention ${scenario.key}`,
completionClaim: {
contractRevision: "attention-v1",
objectiveSatisfied: false,
criteria: [{ criterionId: "objective", status: "unknown", evidenceRefs: [] }],
remainingWork: [{ description: "Resolve attention", blocksCompletion: true }],
},
evidence: [],
verification: [{ commandOrCheck: "attention", status: "not_run" }],
attentionRequests: [scenario.request],
artifacts: [],
...(scenario.disposition === "yielded" ? {
continuation: {
kind: "response_wake" as const,
summary: "Resume after attention",
idempotencyKey: `attention:${scenario.key}`,
},
} : {}),
},
terminal: {
schema: "paperclip.prp.terminal.v1",
turnTerminalState: "completed",
runTerminalState: "succeeded",
reportedWorkDisposition: scenario.disposition,
},
turnId: `turn:${scenario.key}`,
});
await finalizeNativeRun({
db,
runId: scenario.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
}
const delegated = await db.select().from(issues).where(and(
eq(issues.parentId, scenarios[0]!.issueId),
eq(issues.companyId, companyId),
));
expect(delegated).toEqual([
expect.objectContaining({ assigneeAgentId: delegateId, status: "todo" }),
]);
const humanInteractions = await db.select().from(issueThreadInteractions)
.where(eq(issueThreadInteractions.issueId, scenarios[1]!.issueId));
expect(humanInteractions).toEqual([
expect.objectContaining({ kind: "ask_user_questions", status: "pending", sourceRunId: scenarios[1]!.runId }),
]);
await expect(db.select().from(issues).where(eq(issues.id, scenarios[1]!.issueId))).resolves.toEqual([
expect.objectContaining({ status: "in_review" }),
]);
await expect(db.select().from(issues).where(eq(issues.parentId, scenarios[2]!.issueId))).resolves.toHaveLength(0);
await expect(db.select().from(issueRecoveryActions)
.where(eq(issueRecoveryActions.sourceIssueId, scenarios[2]!.issueId))).resolves.toEqual([
expect.objectContaining({ cause: "result_schema_rejected", ownerAgentId: agentId }),
]);
await expect(db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, scenarios[2]!.runId))).resolves.toEqual([
expect.objectContaining({ status: "failed", nativePhase: "retryable_failure" }),
]);
for (const scenario of scenarios) {
const assessments = await db.select().from(workAssessments)
.where(eq(workAssessments.issueId, scenario.issueId));
expect(assessments.map((row) => row.triggerKind)).toEqual(expect.arrayContaining(["native_result", "native_attention"]));
const decisions = await db.select().from(statusDecisions)
.where(eq(statusDecisions.issueId, scenario.issueId));
expect(decisions).toHaveLength(1);
const effects = await db.select().from(statusDecisionEffects)
.where(eq(statusDecisionEffects.decisionId, decisions[0]!.id));
expect(effects.length).toBeGreaterThan(0);
await expect(db.select().from(activityLog).where(and(
eq(activityLog.entityId, scenario.issueId),
eq(activityLog.runId, scenario.runId),
))).resolves.toEqual(expect.arrayContaining([
expect.objectContaining({ actorId: "native-status-committer" }),
]));
await expect(db.select().from(nativeRunFinalizations)
.where(eq(nativeRunFinalizations.runId, scenario.runId))).resolves.toEqual([
expect.objectContaining({ assessmentId: decisions[0]!.assessmentId, decisionId: decisions[0]!.id }),
]);
}
}, 30_000);
});
@@ -38,9 +38,15 @@ import {
dispatchNativeSessionResumptions,
} from "../services/native-runtime/native-finalization-reconciler.js";
const legacyAdapterExecute = vi.hoisted(() => vi.fn(async () => {
throw new Error("legacy adapter must not execute while resuming a persisted native run");
}));
const legacyAdapterExecute = vi.hoisted(() => vi.fn(async () => ({
exitCode: 0,
signal: null,
timedOut: false,
summary: "Fresh flag-off run completed through legacy.",
resultJson: { summary: "fresh legacy after persisted native recovery" },
provider: "test",
model: "legacy-test",
})));
vi.mock("../adapters/index.js", async () => {
const actual = await vi.importActual<typeof import("../adapters/index.js")>("../adapters/index.js");
@@ -234,6 +240,7 @@ describe("P6-25 persisted reaper-to-finalization recovery", () => {
const projectWorkspaceId = randomUUID();
const executionWorkspaceId = randomUUID();
const issueId = randomUUID();
const freshIssueId = randomUUID();
const runId = randomUUID();
const contractId = randomUUID();
const workProductId = randomUUID();
@@ -471,7 +478,7 @@ describe("P6-25 persisted reaper-to-finalization recovery", () => {
}
});
it("runs the production reaper, original heartbeat, recovered active turn, and finalizer exactly once", async () => {
it("finishes the persisted native run after flag-off and selects legacy only for a fresh run", async () => {
legacyAdapterExecute.mockClear();
const backendFactory = vi.fn(() => backend);
const heartbeat = heartbeatService(db, {
@@ -537,5 +544,45 @@ describe("P6-25 persisted reaper-to-finalization recovery", () => {
await expect(db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, runId))).resolves.toHaveLength(1);
await expect(db.select().from(workAssessments).where(eq(workAssessments.runId, runId))).resolves.toHaveLength(1);
await expect(db.select().from(statusDecisions).where(eq(statusDecisions.issueId, issueId))).resolves.toHaveLength(1);
await db.insert(issues).values({
id: freshIssueId,
companyId,
projectId,
projectWorkspaceId,
issueNumber: 2,
identifier: "NRR-2",
title: "Start only after the native kill switch is off",
status: "in_progress",
assigneeAgentId: agentId,
workMode: "standard",
});
const fresh = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId: freshIssueId },
contextSnapshot: { issueId: freshIssueId, taskId: freshIssueId, skipIssueComment: true },
});
expect(fresh).not.toBeNull();
await drainHeartbeatRunsToQuiescence(db, heartbeat);
expect(legacyAdapterExecute).toHaveBeenCalledOnce();
await expect(db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fresh!.id))).resolves.toEqual([
expect.objectContaining({
agentId,
runtimeMode: "legacy",
runtimeModeReason: "instance_flag_disabled",
status: "succeeded",
}),
]);
await expect(db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, fresh!.id))).resolves.toHaveLength(0);
await expect(db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, fresh!.id))).resolves.toHaveLength(0);
await expect(db.select({ runtimeConfig: agents.runtimeConfig }).from(agents).where(eq(agents.id, agentId))).resolves.toEqual([
expect.objectContaining({
runtimeConfig: expect.objectContaining({
nativeRunner: { mode: "native", backend: "codex_app_server", protocolVersion: 1 },
}),
}),
]);
}, 30_000);
});
@@ -27,7 +27,7 @@ import {
applyNativeAttentionStatusDecision,
materializeNativeInteractionResponses,
rejectUnsupportedNativeRuntimeRequest,
routeNativeAttention,
routePersistedNativeResultAttention,
resolveNativeAttentionStatus,
} from "../services/native-runtime/native-interaction-bridge.js";
import {
@@ -38,6 +38,7 @@ import {
import {
inspectNativeCompatibilityState,
inspectNativeMigrationState,
resolveHeartbeatNativeRuntimeMode,
resolveNativeCompatibilityStatus,
resolveNativeMigrationStatus,
resolveNativeRuntimeMode,
@@ -188,7 +189,7 @@ const matrixRowConsumers: Record<string, string> = {
"MIG-05": "native-migration-status",
"MIG-06": "native-migration-status",
"MIG-07": "native-migration-status",
"MIG-08": "native-migration-status",
"MIG-08": "heartbeat-runtime-selection",
"MIG-09": "native-migration-read-model",
};
@@ -221,7 +222,7 @@ const completeEvidenceStates = new Set([
]);
const zeroDecisionStates = new Set([
"safe_partial_parse", "equivalent_attention_family", "cross_company_target",
"safe_partial_parse", "equivalent_attention_family",
"response_after_supersession", "reused_id_changed_material", "decision_committed_delivery_pending",
]);
@@ -232,7 +233,7 @@ const nativeStatusEffectKinds = new Set<NativeStatusEffect["kind"]>([
"append_superseding_assessment", "dispatch_pending_effect", "increment_status_version",
"schedule_reconciliation", "record_shadow_decision", "render_four_layers",
"materialize_contract", "record_mode_labeled_divergence", "record_mode_native",
"record_policy_version", "finish_as_native", "stop_new_native_dispatch",
"record_policy_version", "finish_as_native",
"resume_workspace_operation", "record_expiry", "record_stale_response",
"link_canonical_request", "record_recovery", "release_checkout",
]);
@@ -552,6 +553,7 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
const seeded = await seedFixture(fixture);
const consumerExecutions: ConsumerExecution[] = [];
const materializedEffects = new Set<string>();
const operationalEffects = new Set<string>();
const priorIssueStatus = String(fixture.given.priorIssueStatus ?? "in_progress") as Parameters<typeof arbitrateNativeStatus>[0]["priorIssueStatus"];
const mode = resolveNativeRuntimeMode({
enabled: fixture.mode === "native",
@@ -658,34 +660,84 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
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,
const target = attentionFacts.route === "agent"
? {
ownerClass: "agent",
agentId: completionState === "cross_company_target" ? outsideAgentId : delegateAgentId,
companyId: completionState === "cross_company_target" ? outsideCompanyId : companyId,
}
: attentionFacts.route === "human"
? { ownerClass: "board_user", companyId }
: { ownerClass: "current_agent", companyId };
const requestedCapability = attentionFacts.route === "context"
? "context_lookup"
: attentionFacts.route === "retry"
? "retry"
: attentionFacts.route === "duplicate"
? "duplicate"
: attentionFacts.route === "alternate_track"
? "alternate_track"
: attentionFacts.route === "human"
? "subjective_decision"
: "domain_expertise";
const resultRow = await db.select({ resultJson: nativeRunResults.resultJson })
.from(nativeRunResults).where(eq(nativeRunResults.id, seeded.resultId!))
.then((rows) => rows[0] ?? null);
if (!resultRow) throw new Error(`${fixture.id}: persisted result missing`);
const resultEnvelope = resultRow.resultJson as Record<string, unknown>;
const persistedResult = resultEnvelope.result as Record<string, unknown>;
await db.update(nativeRunResults).set({
resultJson: {
...resultEnvelope,
result: {
...persistedResult,
attentionRequests: [{
id: `attention:${fixture.id}`,
requestedCapability,
requiredAuthority: attentionFacts.route === "human" ? "board" : "agent",
target,
summary: attentionFacts.summary,
responseState: attentionFacts.responseState,
budgetExhausted: attentionFacts.budgetExhausted === true,
governanceGate: attentionFacts.governanceGate,
targetInteractionId: liveAttentionInteractionId,
canonicalRequestId,
}],
},
},
});
}).where(eq(nativeRunResults.id, seeded.resultId!));
const [receipt] = await routePersistedNativeResultAttention({ db, runId: seeded.runId });
if (!receipt) throw new Error(`${fixture.id}: persisted attention route missing`);
const persistedDecision = receipt.decisionId
? await db.select().from(statusDecisions).where(eq(statusDecisions.id, receipt.decisionId))
.then((rows) => rows[0] ?? null)
: null;
const persistedDecisionJson = persistedDecision?.decisionJson as Record<string, unknown> | undefined;
const routedDecision = persistedDecision
? {
policyVersion: persistedDecision.policyVersion,
statusAction: persistedDecisionJson?.statusAction,
toStatus: persistedDecision.toStatus,
reasonCode: persistedDecision.reasonCode,
unblockDescriptor: persistedDecisionJson?.unblockDescriptor ?? null,
effects: persistedDecisionJson?.effects,
} as NativeStatusDecision
: resolveNativeAttentionStatus({ facts: attentionFacts, priorIssueStatus, agentId });
semanticConsumer = "native-attention-resolver";
consumerDecision = pushDecisionConsumer(semanticConsumer, routed.decision);
consumerDecision = pushDecisionConsumer(semanticConsumer, routedDecision);
liveEntrypointCommitted = true;
for (const target of routed.targets) materializedEffects.add(target.effectKind);
for (const effect of routedDecision.effects) materializedEffects.add(effect.kind);
consumerExecutions.push({
consumer: "native-attention-entrypoint",
observed: {
decisionId: routed.decisionId,
resolvedTargetAgentId: routed.resolvedTargetAgentId,
targetCount: routed.targets.length,
decisionId: receipt.decisionId,
resolvedTargetAgentId: receipt.resolvedTargetAgentId,
reasonCode: receipt.reasonCode,
persistedResultId: seeded.resultId,
},
});
if (attentionFacts.route === "agent" && completionState !== "cross_company_target") {
expect(routed.resolvedTargetAgentId, `${fixture.id}:eligible delegate`).toBe(delegateAgentId);
expect(receipt.resolvedTargetAgentId, `${fixture.id}:eligible delegate`).toBe(delegateAgentId);
}
}
if (["turn_scope", "run_scope", "issue_scope_authorized", "replacement_turn_accepted"].includes(completionState)) {
@@ -749,6 +801,56 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
priorIssueStatus,
agentId,
}));
} else if (
completionState === "kill_switch_during_active_native_run"
&& (fixture.covers.migrationRows ?? []).includes("MIG-08")
) {
const runtimeConfig = {
nativeRunner: { mode: "native", backend: "codex_app_server", protocolVersion: 1 },
};
const enabled = options.disableLiveEntrypoint === "rollout";
const activeResolution = resolveHeartbeatNativeRuntimeMode({
persisted: {
runtimeMode: "native",
runtimeModeReason: "eligible_opt_in",
runtimeModeResolvedAt: new Date(),
},
enabled,
runtimeConfig,
agent: { id: agentId, status: "running", adapterType: "codex_local" },
issue: { id: seeded.issueId, workMode: "standard" },
target: { kind: "local" },
workspaceId: "fixture-workspace",
});
const freshResolution = resolveHeartbeatNativeRuntimeMode({
persisted: { runtimeMode: null, runtimeModeReason: null, runtimeModeResolvedAt: null },
enabled,
runtimeConfig,
agent: { id: agentId, status: "running", adapterType: "codex_local" },
issue: { id: seeded.issueId, workMode: "standard" },
target: { kind: "local" },
workspaceId: "fixture-workspace",
});
if (
activeResolution.kind !== "native"
|| freshResolution.kind !== "legacy"
|| freshResolution.reason !== "instance_flag_disabled"
|| runtimeConfig.nativeRunner.mode !== "native"
) {
throw new Error(`${fixture.id}: global kill-switch transition missing`);
}
semanticConsumer = "native-migration-status";
consumerDecision = pushDecisionConsumer(semanticConsumer, activeResolution.authorityDecision);
operationalEffects.add("fresh_flag_off_run_selects_legacy");
consumerExecutions.push({
consumer: "heartbeat-runtime-selection",
observed: {
activeMode: activeResolution.kind,
freshMode: freshResolution.kind,
freshReason: freshResolution.reason,
profileMode: runtimeConfig.nativeRunner.mode,
},
});
} else if (migrationFacts && (fixture.covers.migrationRows ?? []).length > 0 && (fixture.covers.decisionRows ?? []).length === 0) {
semanticConsumer = "native-migration-status";
consumerDecision = pushDecisionConsumer(
@@ -1437,19 +1539,6 @@ 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")) {
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);
}
@@ -1482,6 +1571,7 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
let effects = [...new Set([
...persistedEffects,
...(consumerDecision?.effects.map((effect) => effect.kind) ?? []),
...operationalEffects,
])];
if (rolledBack) effects = ["record_finalization_error"];
if (!consumerDecision) {
@@ -1572,6 +1662,13 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
} else if (consumer === "status-decision-committer") {
const expectedApplicationState = rolledBack ? "rolled_back" : "applied";
if (execution.observed.applicationState !== expectedApplicationState) continue;
} else if (consumer === "heartbeat-runtime-selection") {
if (
execution.observed.activeMode !== "native"
|| execution.observed.freshMode !== "legacy"
|| execution.observed.freshReason !== "instance_flag_disabled"
|| execution.observed.profileMode !== "native"
) continue;
}
consumerEvidenceByRow.set(matrixRow, consumer);
}
@@ -1750,10 +1847,10 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
facts: { killSwitchActiveForNewRuns: true },
priorIssueStatus: "in_progress",
agentId,
}).effects.map((effect) => effect.kind)).toContain("stop_new_native_dispatch");
}).effects.map((effect) => effect.kind)).toContain("finish_as_native");
await expect(executeFixture(byId("migration-kill-switch-rollback"), {
disableLiveEntrypoint: "rollout",
})).rejects.toThrow("kill-switch target");
})).rejects.toThrow("global kill-switch transition missing");
const pendingFixture = byId("crash-after-decision-commit");
const seeded = await seedFixture(pendingFixture);
+10 -22
View File
@@ -92,7 +92,7 @@ import {
finalizeNativeRun,
materializeNativeInteractionResponses,
reconcileNativeFinalizations,
resolveNativeRuntimeMode,
resolveHeartbeatNativeRuntimeMode,
} from "./native-runtime/index.js";
import {
parseNativeExecutionInput,
@@ -15310,27 +15310,15 @@ 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: { id: agent.id, status: runningAgent.status, adapterType: agent.adapterType },
issue: issueRef,
target: executionTarget,
workspaceId: persistedExecutionWorkspace?.id ?? null,
});
const nativeRuntimeResolution = resolveHeartbeatNativeRuntimeMode({
persisted: run,
enabled: resolvedInstanceSettings.experimental.enableNativeRunner === true,
runtimeConfig: agent.runtimeConfig,
agent: { id: agent.id, 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") {
@@ -1,10 +1,21 @@
import type { Db } from "@paperclipai/db";
import type { NativeInteractionResponseEnvelope } from "@paperclipai/paperclip-runner";
import { and, asc, eq, inArray } from "drizzle-orm";
import { agents, heartbeatRuns, issues, issueThreadInteractions, nativeRunFinalizations } from "@paperclipai/db";
import {
agents,
heartbeatRuns,
issues,
issueThreadInteractions,
nativeRunFinalizations,
nativeRunResults,
statusDecisions,
workAssessments,
} from "@paperclipai/db";
import { getAgentWorkEligibility } from "@paperclipai/shared";
import { issueThreadInteractionService } from "../issue-thread-interactions.js";
import { commitNativeStatusDecision } from "./status-decision-committer.js";
import { nativeSha256 } from "./canonical.js";
import { recordNativeAttentionAssessment } from "./work-assessments.js";
import {
NATIVE_STATUS_ARBITER_POLICY_VERSION,
type NativeAuthoritativeIssueStatus,
@@ -28,7 +39,7 @@ function record(value: unknown): Record<string, unknown> {
export type NativeAttentionMaterializedTarget = {
effectKind: string;
targetType: "issue_thread_interaction" | "native_run_finalization";
targetType: string;
targetId: string;
};
@@ -329,21 +340,255 @@ export type NativeAttentionCandidate = {
summary: string;
responseState?: NativeAttentionFacts["responseState"];
targetAgentId?: string | null;
targetCompanyId?: string | null;
budgetExhausted?: boolean;
governanceGate?: NativeGovernanceGate | null;
targetInteractionId?: string;
canonicalRequestId?: string;
};
function nonEmptyText(value: unknown) {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function candidateFromPersistedRequest(request: Record<string, unknown>): NativeAttentionCandidate {
const summary = nonEmptyText(request.summary);
if (!summary) {
throw new NativeInteractionBridgeError(
"native_attention_request_invalid",
"Persisted native attention requires a summary",
);
}
const target = record(request.target);
const capability = nonEmptyText(request.requestedCapability) ?? nonEmptyText(request.kind);
const ownerClass = nonEmptyText(target.ownerClass);
const requiredAuthority = nonEmptyText(request.requiredAuthority);
const responseState = nonEmptyText(request.responseState);
const governanceGate = record(request.governanceGate);
let route: NativeAttentionCandidate["route"];
if (capability === "context_lookup") route = "context";
else if (capability === "retry") route = "retry";
else if (capability === "duplicate") route = "duplicate";
else if (capability === "alternate_track" || ownerClass === "current_agent") route = "alternate_track";
else if (capability === "domain_expertise" || ownerClass === "agent") route = "agent";
else if (
["review", "credential_grant", "governed_approval", "subjective_decision", "external_action"].includes(capability ?? "")
|| ["board_user", "external_system"].includes(ownerClass ?? "")
|| ["board", "external"].includes(requiredAuthority ?? "")
) route = "human";
else {
throw new NativeInteractionBridgeError(
"native_attention_request_invalid",
`Persisted native attention capability ${capability ?? "unknown"} has no authorized route`,
);
}
return {
route,
summary,
responseState: ["none", "resolved", "stale", "expired"].includes(responseState ?? "")
? responseState as NativeAttentionFacts["responseState"]
: undefined,
targetAgentId: nonEmptyText(target.agentId),
targetCompanyId: nonEmptyText(target.companyId),
budgetExhausted: request.budgetExhausted === true,
governanceGate: governanceGate.kind === "interaction" && nonEmptyText(governanceGate.id)
? { kind: "interaction", id: nonEmptyText(governanceGate.id)! }
: null,
targetInteractionId: nonEmptyText(request.targetInteractionId) ?? undefined,
canonicalRequestId: nonEmptyText(request.canonicalRequestId) ?? undefined,
};
}
export type PersistedNativeAttentionReceipt = {
requestId: string;
route: NativeAttentionCandidate["route"];
decisionId: string | null;
reasonCode: string | null;
resolvedTargetAgentId: string | null;
replayed: boolean;
};
/**
* 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
* Runtime-reachable ingress for accepted attention facts. It starts from the
* immutable package result row, derives an authorized route, appends one
* assessment/decision lineage per request, and lets the existing issue and
* interaction owners materialize the effect.
*/
export async function routePersistedNativeResultAttention(input: {
db: Db;
runId: string;
}): Promise<PersistedNativeAttentionReceipt[]> {
const binding = await input.db.select({
companyId: heartbeatRuns.companyId,
runtimeMode: heartbeatRuns.runtimeMode,
resultId: nativeRunFinalizations.resultId,
issueId: nativeRunFinalizations.issueId,
}).from(heartbeatRuns)
.innerJoin(nativeRunFinalizations, eq(nativeRunFinalizations.runId, heartbeatRuns.id))
.where(eq(heartbeatRuns.id, input.runId))
.limit(1)
.then((rows) => rows[0] ?? null);
if (!binding || binding.runtimeMode !== "native" || !binding.resultId) {
throw new NativeInteractionBridgeError(
"native_attention_binding_mismatch",
"Accepted native result binding not found",
);
}
const resultRow = await input.db.select().from(nativeRunResults).where(and(
eq(nativeRunResults.id, binding.resultId),
eq(nativeRunResults.companyId, binding.companyId),
eq(nativeRunResults.issueId, binding.issueId),
eq(nativeRunResults.runId, input.runId),
eq(nativeRunResults.schemaStatus, "accepted"),
)).limit(1).then((rows) => rows[0] ?? null);
if (!resultRow) {
throw new NativeInteractionBridgeError(
"native_attention_result_missing",
"Accepted native result row not found",
);
}
const result = record(record(resultRow.resultJson).result);
const requests = Array.isArray(result.attentionRequests)
? result.attentionRequests.map(record)
: [];
const receipts: PersistedNativeAttentionReceipt[] = [];
for (const [index, request] of requests.entries()) {
const candidate = candidateFromPersistedRequest(request);
const requestId = nonEmptyText(request.id) ?? nativeSha256({
runId: input.runId,
resultId: resultRow.id,
index,
request,
});
const existingAssessment = await input.db.select({ id: workAssessments.id })
.from(workAssessments).where(and(
eq(workAssessments.companyId, binding.companyId),
eq(workAssessments.issueId, binding.issueId),
eq(workAssessments.runId, input.runId),
eq(workAssessments.triggerKind, "native_attention"),
eq(workAssessments.triggerRef, requestId),
)).limit(1).then((rows) => rows[0] ?? null);
if (existingAssessment) {
const existingDecision = await input.db.select({
id: statusDecisions.id,
reasonCode: statusDecisions.reasonCode,
decisionJson: statusDecisions.decisionJson,
}).from(statusDecisions).where(and(
eq(statusDecisions.companyId, binding.companyId),
eq(statusDecisions.assessmentId, existingAssessment.id),
)).limit(1).then((rows) => rows[0] ?? null);
if (existingDecision) {
const decisionJson = record(existingDecision.decisionJson);
const effects = Array.isArray(decisionJson.effects) ? decisionJson.effects.map(record) : [];
const delegation = effects.find((effect) => effect.kind === "create_delegated_issue");
const finalizationError = effects.find((effect) => effect.kind === "record_finalization_error");
await input.db.update(nativeRunFinalizations).set({
phase: finalizationError ? "retryable_failure" : "committed",
assessmentId: existingAssessment.id,
decisionId: existingDecision.id,
leaseOwner: null,
leaseExpiresAt: null,
failureCode: nonEmptyText(finalizationError?.cause),
failureDetail: finalizationError ? {
originalFailureCode: finalizationError.cause,
nextAction: finalizationError.nextAction,
} : null,
nextAttemptAt: finalizationError ? new Date(Date.now() + 30_000) : null,
updatedAt: new Date(),
}).where(and(
eq(nativeRunFinalizations.runId, input.runId),
eq(nativeRunFinalizations.companyId, binding.companyId),
eq(nativeRunFinalizations.issueId, binding.issueId),
));
receipts.push({
requestId,
route: candidate.route,
decisionId: existingDecision.id,
reasonCode: existingDecision.reasonCode,
resolvedTargetAgentId: nonEmptyText(delegation?.agentId),
replayed: true,
});
continue;
}
}
const issue = await input.db.select({
status: issues.status,
statusVersion: issues.statusVersion,
lastStatusDecisionId: issues.lastStatusDecisionId,
}).from(issues).where(and(
eq(issues.id, binding.issueId),
eq(issues.companyId, binding.companyId),
)).limit(1).then((rows) => rows[0] ?? null);
if (!issue) throw new NativeInteractionBridgeError("native_attention_binding_mismatch", "Issue binding not found");
const coordinator = await input.db.select({
phase: nativeRunFinalizations.phase,
assessmentId: nativeRunFinalizations.assessmentId,
}).from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, input.runId))
.limit(1).then((rows) => rows[0] ?? null);
if (!coordinator) throw new NativeInteractionBridgeError("native_attention_binding_mismatch", "Coordinator binding not found");
const assessment = await recordNativeAttentionAssessment({
db: input.db,
companyId: binding.companyId,
issueId: binding.issueId,
runId: input.runId,
turnId: resultRow.turnId,
contractId: resultRow.completionContractId,
resultId: resultRow.id,
requestId,
request,
routingFacts: candidate as unknown as Record<string, unknown>,
priorIssueStatus: issue.status,
priorStatusVersion: Number(issue.statusVersion),
priorDecisionId: issue.lastStatusDecisionId,
supersedesAssessmentId: coordinator.assessmentId,
});
const routed = await routeNativeAttention({
db: input.db,
runId: input.runId,
assessmentId: assessment.id,
allowSupersedingCommittedDecision: coordinator.phase === "committed",
candidate,
});
receipts.push({
requestId,
route: candidate.route,
decisionId: routed.decisionId,
reasonCode: routed.decision.reasonCode,
resolvedTargetAgentId: routed.resolvedTargetAgentId,
replayed: false,
});
}
if (receipts.length > 0) {
await input.db.transaction(async (tx) => {
const run = await tx.select({ resultJson: heartbeatRuns.resultJson })
.from(heartbeatRuns).where(and(
eq(heartbeatRuns.id, input.runId),
eq(heartbeatRuns.companyId, binding.companyId),
)).for("update").limit(1).then((rows) => rows[0] ?? null);
if (!run) throw new NativeInteractionBridgeError("native_attention_binding_mismatch", "Heartbeat run missing");
await tx.update(heartbeatRuns).set({
resultJson: { ...record(run.resultJson), nativeAttentionRouting: receipts },
updatedAt: new Date(),
}).where(eq(heartbeatRuns.id, input.runId));
});
}
return receipts;
}
/**
* Internal owner-routing step for native attention. The persisted-result
* ingress above supplies the server-owned assessment and untrusted candidate;
* this step resolves company scope and commits or audit-materializes the
* authority decision.
*/
export async function routeNativeAttention(input: {
db: Db;
runId: string;
assessmentId?: string;
allowSupersedingCommittedDecision?: boolean;
candidate: NativeAttentionCandidate;
}) {
const binding = await input.db.select({
@@ -380,8 +625,13 @@ export async function routeNativeAttention(input: {
.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;
const companyScopeValid = (
!input.candidate.targetCompanyId
|| input.candidate.targetCompanyId === binding.companyId
) && (
!input.candidate.targetAgentId
|| targetSuggestion?.companyId === binding.companyId
);
let resolvedTargetAgentId: string | null = null;
if (companyScopeValid && input.candidate.route === "agent") {
const eligible = companyAgents.filter((candidate) =>
@@ -408,8 +658,7 @@ export async function routeNativeAttention(input: {
priorIssueStatus: binding.issueStatus as NativeAuthoritativeIssueStatus,
agentId: binding.agentId,
});
const auditOnly = !companyScopeValid
|| decision.reasonCode === "attention_duplicate_suppressed";
const auditOnly = decision.reasonCode === "attention_duplicate_suppressed";
if (auditOnly) {
const targets = await applyNativeAttentionStatusDecision({
db: input.db,
@@ -422,7 +671,8 @@ export async function routeNativeAttention(input: {
});
return { decision, decisionId: null, targets, resolvedTargetAgentId };
}
if (!binding.assessmentId) {
const assessmentId = input.assessmentId ?? binding.assessmentId;
if (!assessmentId) {
throw new NativeInteractionBridgeError(
"native_attention_assessment_missing",
"Native attention requires the server-owned work assessment for this result",
@@ -433,11 +683,12 @@ export async function routeNativeAttention(input: {
companyId: binding.companyId,
issueId: binding.issueId,
runId: input.runId,
assessmentId: binding.assessmentId,
assessmentId,
priorStatus: binding.issueStatus,
priorStatusVersion: Number(binding.issueStatusVersion),
priorDecisionId: binding.issueDecisionId,
decision,
allowSupersedingCommittedDecision: input.allowSupersedingCommittedDecision,
});
return {
decision,
@@ -10,6 +10,7 @@ import {
issues,
nativeRunFinalizations,
nativeRunResults,
statusDecisions,
} from "@paperclipai/db";
import { classifyNativeEvidence } from "./evidence-classifier.js";
import {
@@ -26,6 +27,7 @@ import {
} from "./status-decision-committer.js";
import { issueRecoveryActionService } from "../issue-recovery-actions.js";
import { nativeSha256 } from "./canonical.js";
import { routePersistedNativeResultAttention } from "./native-interaction-bridge.js";
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
@@ -359,6 +361,82 @@ export async function finalizeNativeRun(input: {
eq(nativeRunFinalizations.runId, input.runId),
eq(nativeRunFinalizations.leaseOwner, claim.leaseOwner!),
));
if (assessment.attentionRequests.length > 0) {
try {
const attentionReceipts = await routePersistedNativeResultAttention({
db: input.db,
runId: input.runId,
});
const [latestCoordinator, latestIssue] = await Promise.all([
input.db.select().from(nativeRunFinalizations)
.where(eq(nativeRunFinalizations.runId, input.runId))
.limit(1).then((rows) => rows[0] ?? null),
input.db.select({ status: issues.status, statusVersion: issues.statusVersion })
.from(issues).where(and(
eq(issues.id, authoritativeIssue.id),
eq(issues.companyId, run.companyId),
)).limit(1).then((rows) => rows[0] ?? null),
]);
const latestDecision = latestCoordinator?.decisionId
? await input.db.select({ toStatus: statusDecisions.toStatus })
.from(statusDecisions).where(and(
eq(statusDecisions.id, latestCoordinator.decisionId),
eq(statusDecisions.companyId, run.companyId),
eq(statusDecisions.issueId, authoritativeIssue.id),
)).limit(1).then((rows) => rows[0] ?? null)
: null;
if (!latestCoordinator || !latestIssue || !latestDecision) {
throw new Error("native_attention_routing_incomplete");
}
const now = new Date();
const attentionFailed = latestCoordinator.phase === "retryable_failure"
|| latestCoordinator.phase === "terminal_failure";
const currentRun = await input.db.select({ resultJson: heartbeatRuns.resultJson })
.from(heartbeatRuns).where(eq(heartbeatRuns.id, run.id))
.limit(1).then((rows) => rows[0] ?? null);
await input.db.update(heartbeatRuns).set({
...(input.projectRunStatus || attentionFailed ? {
status: attentionFailed
? "failed"
: terminalState === "succeeded" ? "succeeded" : terminalState === "cancelled" ? "cancelled" : "failed",
finishedAt: now,
} : {}),
nativePhase: latestCoordinator.phase,
nativePhaseUpdatedAt: now,
resultJson: {
...record(currentRun?.resultJson),
finalizationPhase: latestCoordinator.phase,
assessmentId: latestCoordinator.assessmentId,
decisionId: latestCoordinator.decisionId,
authoritativeDecision: latestDecision.toStatus,
issueStatusBefore: authoritativeIssue.status,
issueStatusAfter: latestIssue.status,
statusVersionBefore: Number(authoritativeIssue.statusVersion),
statusVersionAfter: Number(latestIssue.statusVersion),
workspaceFinalizeStatus: input.workspaceFinalizeStatus,
nativeAttentionRouting: attentionReceipts,
},
updatedAt: now,
}).where(eq(heartbeatRuns.id, run.id));
return { ...latestCoordinator, attentionReceipts };
} catch (error) {
if (error instanceof NativeStatusRaceError && attempt < 2) {
supersedesAssessmentId = assessmentRow.id;
continue;
}
return recordRetryableFailure({
db: input.db,
run,
coordinator,
failureCode: error instanceof NativeStatusRaceError ? "status_cas_exhausted" : "side_effect_planning_failed",
message: error instanceof Error ? error.message : String(error),
nextAction: error instanceof NativeStatusRaceError
? "Reassess native attention against the latest authoritative issue status version."
: "Retry native attention routing from the persisted accepted package result.",
projectRunStatus: input.projectRunStatus,
});
}
}
try {
const committed = await commitNativeStatusDecision({
db: input.db,
@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
import { NativeRuntimeEligibilityError, resolveNativeRuntimeMode } from "./runtime-mode.js";
import {
NativeRuntimeEligibilityError,
resolveHeartbeatNativeRuntimeMode,
resolveNativeRuntimeMode,
} from "./runtime-mode.js";
const eligible = {
enabled: true,
@@ -26,6 +30,27 @@ describe("resolveNativeRuntimeMode", () => {
}));
});
it("keeps a persisted active run native while the global flag makes a fresh run legacy", () => {
const disabled = { ...eligible, enabled: false };
expect(resolveHeartbeatNativeRuntimeMode({
...disabled,
persisted: {
runtimeMode: "native",
runtimeModeReason: "eligible_opt_in",
runtimeModeResolvedAt: new Date(),
},
})).toEqual(expect.objectContaining({
kind: "native",
reason: "eligible_opt_in",
authorityDecision: expect.objectContaining({ reasonCode: "live_continuation_registered" }),
}));
expect(resolveHeartbeatNativeRuntimeMode({
...disabled,
persisted: { runtimeMode: null, runtimeModeReason: null, runtimeModeResolvedAt: null },
})).toEqual(expect.objectContaining({ kind: "legacy", reason: "instance_flag_disabled" }));
expect(disabled.runtimeConfig.nativeRunner.mode).toBe("native");
});
it("rejects an explicit native profile outside the approved boundary", () => {
expect(() => resolveNativeRuntimeMode({ ...eligible, agent: { ...eligible.agent, adapterType: "claude_local" } }))
.toThrow(NativeRuntimeEligibilityError);
@@ -45,13 +45,6 @@ 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 },
@@ -101,6 +94,49 @@ export function resolveNativeRuntimeMode(input: {
};
}
/**
* Production heartbeat selection seam. A resolved run keeps its persisted
* mode across configuration changes; only a fresh unresolved run consults the
* current global flag and agent profile.
*/
export function resolveHeartbeatNativeRuntimeMode(input: {
persisted: {
runtimeMode: string | null;
runtimeModeReason: string | null;
runtimeModeResolvedAt: Date | null;
};
enabled: boolean;
runtimeConfig: unknown;
agent: { id?: string; status: string; adapterType: string | null };
issue: { id: string; workMode: string; executionWorkspaceId?: string | null } | null;
target: { kind?: string } | null | undefined;
workspaceId: string | null;
}): NativeRuntimeResolution {
if (input.persisted.runtimeModeResolvedAt) {
if (input.persisted.runtimeMode === "native") {
return {
kind: "native",
resolverVersion: NATIVE_RUNTIME_RESOLVER_VERSION,
reason: "eligible_opt_in",
profile: { mode: "native", backend: "codex_app_server", protocolVersion: 1 },
authorityDecision: resolveNativeMigrationStatus({
facts: input.enabled
? { applicationEnabled: true }
: { killSwitchActiveForNewRuns: true },
priorIssueStatus: "in_progress",
agentId: input.agent.id ?? "00000000-0000-4000-8000-000000000000",
}),
};
}
return {
kind: "legacy",
resolverVersion: NATIVE_RUNTIME_RESOLVER_VERSION,
reason: input.persisted.runtimeModeReason ?? "persisted_legacy_selection",
};
}
return resolveNativeRuntimeMode(input);
}
/** Production read-model facts used by compatibility and mixed-ledger views. */
export function inspectNativeCompatibilityState(input: {
resolution: NativeRuntimeResolution;
@@ -267,7 +303,6 @@ export function resolveNativeMigrationStatus(input: {
agentId: input.agentId,
},
{ kind: "finish_as_native" },
{ kind: "stop_new_native_dispatch" },
],
};
}
@@ -45,7 +45,6 @@ export type NativeStatusEffect =
| { kind: "record_mode_native" }
| { kind: "record_policy_version" }
| { kind: "finish_as_native" }
| { kind: "stop_new_native_dispatch" }
| { kind: "resume_workspace_operation" }
| { kind: "record_expiry" }
| { kind: "record_stale_response" }
@@ -26,7 +26,6 @@ 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";
@@ -881,40 +880,6 @@ async function materializeDecisionEffect(input: {
.where(eq(heartbeatRuns.id, input.runId));
return { effectKind: effect.kind, targetType: "heartbeat_run", targetId, payload: { runtimeMode: "native" } };
}
if (effect.kind === "stop_new_native_dispatch") {
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,
},
});
if (!updated) throw new Error("native_kill_switch_not_persisted");
return {
effectKind: effect.kind,
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) {
@@ -64,3 +64,69 @@ export async function recordNativeWorkAssessment(input: {
if (!row) throw new Error("native_assessment_not_persisted");
return row;
}
export async function recordNativeAttentionAssessment(input: {
db: Db;
companyId: string;
issueId: string;
runId: string;
turnId: string | null;
contractId: string;
resultId: string;
requestId: string;
request: Record<string, unknown>;
routingFacts: Record<string, unknown>;
priorIssueStatus: string;
priorStatusVersion: number;
priorDecisionId: string | null;
supersedesAssessmentId: string | null;
}) {
const assessmentJson = {
kind: "native_attention",
requestId: input.requestId,
request: input.request,
routingFacts: input.routingFacts,
};
const inputDigest = nativeSha256({
companyId: input.companyId,
issueId: input.issueId,
runId: input.runId,
resultId: input.resultId,
requestId: input.requestId,
request: input.request,
routingFacts: input.routingFacts,
priorIssueStatus: input.priorIssueStatus,
priorStatusVersion: input.priorStatusVersion,
priorDecisionId: input.priorDecisionId,
policyVersion: "phase6-v2",
});
const existing = await input.db.select().from(workAssessments).where(and(
eq(workAssessments.companyId, input.companyId),
eq(workAssessments.issueId, input.issueId),
eq(workAssessments.runId, input.runId),
eq(workAssessments.triggerKind, "native_attention"),
eq(workAssessments.triggerRef, input.requestId),
)).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_attention",
triggerRef: input.requestId,
triggerCapability: "server_native_attention_router",
triggerActorCompanyId: input.companyId,
priorIssueStatus: input.priorIssueStatus,
priorStatusVersion: input.priorStatusVersion,
priorDecisionId: input.priorDecisionId,
policyVersion: "phase6-v2",
assessmentJson,
inputDigest,
supersedesAssessmentId: input.supersedesAssessmentId,
}).returning();
if (!row) throw new Error("native_attention_assessment_not_persisted");
return row;
}