fix(native-runner): commit audit-only attention

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-09 07:43:28 +00:00
1 parent c82a2142c9
commit bacf670497
11 files changed
+688 -52

No files matched your search

+4
View File
@@ -27,6 +27,10 @@ consumer.
Phase 6 adds a default-off Paperclip adapter at the public control-plane/session
boundary, durable PRP replay and result finalization, and a kill switch that
keeps the legacy adapter path as the default.
Its audit-only attention path also terminates through the public port/finalizer
call graph: duplicate and stale requests commit exact interaction receipts with
no status decision, wake, notification, or issue status/version change, and a
committed coordinator makes replay a no-op.
## Phase 0–5 quick start
@@ -886,11 +886,22 @@ 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.
activity receipt. The corpus mutates the accepted result row and calls
`finalizeNativeRun`, which owns the persisted-result ingress. The database
integration test starts one layer earlier at
`PaperclipControlPlanePort.completeRun` and then calls the same production
finalizer. `routePersistedNativeResultAttention` and `routeNativeAttention`
remain internal helpers and are no longer accepted as the operational proof.
Audit-only duplicate and stale requests are a terminal success even though
they intentionally create no status decision. The finalizer accepts that shape
only when every request has an `attention_duplicate_suppressed` receipt bound
to the exact durable interaction target. It commits the coordinator with a null
decision, projects the successful heartbeat, and preserves issue status and
version. A committed zero-decision coordinator is replayable only while those
durable receipts remain present; replay does not update the named interaction a
second time. Missing or cross-company interaction bindings fail closed into
named finalization recovery.
MIG-08 now represents the actual instance-global transition. The production
heartbeat selector reads the persisted mode first: an already-selected native
@@ -917,6 +928,111 @@ 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.
### Remediation 7 authoritative exact-file reconciliation (2026-08-09)
The provisional list and seam amendments above remain the decision history.
For final diff auditing, their authoritative union is the following exact
93-file allowlist. It matches the Phase 6 branch diff from the parent of the
authorization commit through this remediation, including the package README,
the Section 18.13 checker, package/runtime tests, `canonical.ts`, and
`runtime-mode.test.ts`. No file outside this list is authorized as Phase 6 work.
```text
packages/adapter-utils/package.json
packages/adapter-utils/src/types.ts
packages/db/src/migrations/0211_famous_guardsmen.sql
packages/db/src/migrations/meta/0211_snapshot.json
packages/db/src/migrations/meta/_journal.json
packages/db/src/schema/completion_contracts.ts
packages/db/src/schema/heartbeat_run_events.ts
packages/db/src/schema/heartbeat_runs.ts
packages/db/src/schema/index.ts
packages/db/src/schema/issues.ts
packages/db/src/schema/native_run_finalizations.ts
packages/db/src/schema/native_run_results.ts
packages/db/src/schema/status_decision_effects.ts
packages/db/src/schema/status_decisions.ts
packages/db/src/schema/work_assessments.ts
packages/paperclip-runner/README.md
packages/paperclip-runner/docs/architecture.md
packages/paperclip-runner/docs/design/phase-6-thin-paperclip-adapter.md
packages/paperclip-runner/docs/index.md
packages/paperclip-runner/docs/phase-06-thin-paperclip-adapter.md
packages/paperclip-runner/docs/tutorials/phase-06-thin-paperclip-adapter.md
packages/paperclip-runner/knowledge/evidence/2026-08-09-phase-06-verification.md
packages/paperclip-runner/knowledge/evidence/index.md
packages/paperclip-runner/knowledge/journal/2026-08-09-phase-06-design.md
packages/paperclip-runner/knowledge/journal/2026-08-09-phase-06-implementation.md
packages/paperclip-runner/knowledge/journal/index.md
packages/paperclip-runner/knowledge/log.md
packages/paperclip-runner/package.json
packages/paperclip-runner/spec/fixtures/status-authority-phase5.json
packages/paperclip-runner/spec/paperclip-native-runner-implementation-plan.md
packages/paperclip-runner/spec/paperclip-native-runner-spike-spec.md
packages/paperclip-runner/src/backends/codex-native-backend.ts
packages/paperclip-runner/src/backends/harness-driver-backend.test.ts
packages/paperclip-runner/src/backends/harness-driver-backend.ts
packages/paperclip-runner/src/cli/phase6-paperclip.ts
packages/paperclip-runner/src/conformance/control-plane-port.test.ts
packages/paperclip-runner/src/conformance/control-plane-port.ts
packages/paperclip-runner/src/contracts/control-plane-port.ts
packages/paperclip-runner/src/contracts/native-execution.test.ts
packages/paperclip-runner/src/contracts/native-execution.ts
packages/paperclip-runner/src/contracts/native-session-backend.ts
packages/paperclip-runner/src/index.ts
packages/paperclip-runner/src/mock-core/mock-control-plane-adapter.ts
packages/paperclip-runner/src/native-session-runtime.test.ts
packages/paperclip-runner/src/native-session-runtime.ts
packages/shared/src/feature-catalog.ts
packages/shared/src/index.ts
packages/shared/src/types/index.ts
packages/shared/src/types/instance.ts
packages/shared/src/types/native-finalization.ts
packages/shared/src/validators/index.ts
packages/shared/src/validators/instance.ts
packages/shared/src/validators/native-finalization.ts
scripts/check-runner-phase5-spec.mjs
server/package.json
server/src/__tests__/heartbeat-native-runner-cancellation.test.ts
server/src/__tests__/heartbeat-native-runner-selection.test.ts
server/src/__tests__/heartbeat-run-event-sequencing.test.ts
server/src/__tests__/legacy-finalization-regression.test.ts
server/src/__tests__/native-finalization-migration.test.ts
server/src/__tests__/native-finalization-recovery.test.ts
server/src/__tests__/native-interaction-bridge.test.ts
server/src/__tests__/native-run-finalizer.test.ts
server/src/__tests__/native-runner-input-boundary.test.ts
server/src/__tests__/native-runner-phase6.integration.test.ts
server/src/__tests__/native-session-resumption.test.ts
server/src/__tests__/native-status-arbiter-corpus.test.ts
server/src/services/heartbeat-run-events.ts
server/src/services/heartbeat.ts
server/src/services/instance-settings.ts
server/src/services/issues.ts
server/src/services/native-runtime/canonical.ts
server/src/services/native-runtime/completion-contracts.ts
server/src/services/native-runtime/evidence-classifier.test.ts
server/src/services/native-runtime/evidence-classifier.ts
server/src/services/native-runtime/index.ts
server/src/services/native-runtime/native-execution-input.ts
server/src/services/native-runtime/native-finalization-reconciler.ts
server/src/services/native-runtime/native-interaction-bridge.ts
server/src/services/native-runtime/native-result-ingestion.ts
server/src/services/native-runtime/native-run-finalizer.ts
server/src/services/native-runtime/native-session-executor.test.ts
server/src/services/native-runtime/native-session-executor.ts
server/src/services/native-runtime/native-workspace-finalizer.ts
server/src/services/native-runtime/paperclip-control-plane-port.test.ts
server/src/services/native-runtime/paperclip-control-plane-port.ts
server/src/services/native-runtime/runtime-mode.test.ts
server/src/services/native-runtime/runtime-mode.ts
server/src/services/native-runtime/status-arbiter.test.ts
server/src/services/native-runtime/status-arbiter.ts
server/src/services/native-runtime/status-decision-committer.ts
server/src/services/native-runtime/work-assessments.ts
server/src/services/recovery/service.ts
```
## Commands the implementation must make runnable
These commands are the acceptance contract for the implementation issue. They
@@ -90,7 +90,8 @@ fail closed; no credentials enter the model envelope.
The deterministic package suite and the embedded-PostgreSQL Paperclip suite
prove selection, replay/conflict handling, authoritative evidence,
status/liveness atomicity, migration repair, bounded cancellation/retry, and a
byte-equivalent legacy read snapshot. The database suite is the internal
byte-equivalent legacy read snapshot. The thirteen-file database gate contains
46 tests with zero skips. The database suite is the internal
native canary: it executes one selected task through the public package session
contract and applies one server-owned decision. Its post-kill-switch legacy
case has zero native history rows.
@@ -119,7 +120,12 @@ entrypoint. Accepted attention starts at the immutable package-result row:
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.
Duplicate and stale targets finish as committed audit-only coordinators with
zero decisions, wakes, notifications, or issue status/version changes. Their
run receipt names the exact interaction target, and replay neither adds an
assessment nor updates that interaction again. Missing and cross-company audit
targets fail closed. `routePersistedNativeResultAttention` and
`routeNativeAttention` are internal helpers, not operational test ingresses.
Reconciliation selects the persisted
status/evidence/policy branches and executes workspace recovery through the
workspace operation recorder. The rollout kill switch is only the global
@@ -131,12 +137,14 @@ profile reset. Pending replay
only acknowledges its original decision-scoped target after verifying that the
target still exists.
Direct resolver calls are explicitly pure policy/read-model tests. They do not
Direct resolver and persisted-router calls are explicitly policy/internal
helper 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.
and concrete durable target. Sabotage removes the finalizer-owned accepted-
attention path for the duplicate and stale fixtures, 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
@@ -60,7 +60,7 @@ pnpm --filter @paperclipai/server exec vitest run \
```
This runs the unchanged package conformance suite against the real Paperclip
port. Forty-five targeted tests check company binding, duplicate/gap replay,
port. Forty-six 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,
@@ -84,6 +84,14 @@ target-state mutation, and pending replay may only acknowledge its original
decision-scoped target. Sabotage tests remove each live action and the replay
target and require failure even while the pure resolver label remains correct.
Duplicate and stale audit-only cases start at
`PaperclipControlPlanePort.completeRun`, continue through `finalizeNativeRun`,
and commit the coordinator with no status decision. The run succeeds, the issue
status/version remains unchanged, and the receipt names the exact mutated
interaction. Replaying the finalizer does not create an assessment or update
the interaction again. A mixed duplicate/stale list has the same behavior;
missing and cross-company interaction bindings fail closed.
All eleven expected fields and all 70 matrix-row joins are still checked, and
an unknown-effect case proves the status transaction fails closed without
partial rows. The recovery case uses a scripted provider only at the backend
@@ -4,7 +4,7 @@ title: Phase 6 thin Paperclip adapter verification
description: Targeted package, database-port, finalization, security-boundary, feature-selection, and legacy fallback evidence.
tags: [native-runner, phase-6, verification, paperclip, replay, finalization]
status: stable
generated: { by: openai/gpt-5.6, at: 2026-08-09T05:52:00Z }
generated: { by: openai/gpt-5.6, at: 2026-08-09T07:42:08Z }
---
# Scope
@@ -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 45 tests in thirteen files against
The complete acceptance gate below passed 46 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
@@ -75,6 +75,11 @@ The focused corpus also proved:
authority creates an issue-thread interaction, and cross-company targeting
persists a rejection decision, recovery action, failed finalization, and
audit receipt;
- valid duplicate and stale targets, including a mixed list, enter through
`PaperclipControlPlanePort.completeRun -> finalizeNativeRun`, commit a
replay-stable zero-decision coordinator, project a successful run, preserve
issue status/version, create no wake or notification, and record the exact
durable interaction target; missing and cross-company bindings fail closed;
- 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`;
@@ -87,7 +92,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, 45-test gate:
The acceptance-matrix entry points passed as one 13-file, 46-test gate:
```sh
pnpm --filter @paperclipai/server exec vitest run \
@@ -111,9 +116,11 @@ 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 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
`cancelNativeSession`; attention enters from the accepted result through
`finalizeNativeRun`, which owns the persisted-result router, then resolves a
same-company eligible target through the issue service. The mapped duplicate
and stale fixtures require that finalizer receipt and fail sabotage that leaves
only the pure resolver/internal router. 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 production heartbeat selector using the global flag:
@@ -124,7 +131,8 @@ 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, changes the real global-flag
it. Zero-decision audit replay retains the coordinator and named interaction
without another mutation. 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
@@ -133,6 +141,33 @@ and recovery canaries remain scripted only at the provider boundary, while the
production Paperclip persistence/finalization paths and the flag-off legacy
adapter/finalizer path execute for real.
# Remediation 7 rerun and call-graph self-review
The final rerun on 2026-08-09 produced:
- thirteen server files, 46 tests passed, zero skipped;
- Section 18.13 checker: 19 checks passed, zero failed;
- runner documentation validation: 58 links and the 25-concept/four-index OKF
bundle passed;
- server TypeScript and runner TypeScript/protocol typechecks passed;
- the design's authoritative 93-file allowlist matched the Phase 6 diff exactly;
- `git diff --check` passed.
The final owner review traced the successful audit-only path as
`PaperclipControlPlanePort.completeRun -> native_run_results ->
finalizeNativeRun -> routePersistedNativeResultAttention ->
recordNativeAttentionAssessment -> routeNativeAttention ->
applyNativeAttentionStatusDecision -> native_run_finalizations/heartbeat_runs`.
The public port owns immutable result validation and persistence; the finalizer
owns coordinator lease/terminal projection; the internal router derives all
identity from persisted bindings; and the audit materializer requires the exact
same-company issue interaction. The finalizer accepts a null decision only when
every receipt is `attention_duplicate_suppressed` and names a durable target.
Committed replay validates those receipts before short-circuiting, so it cannot
create another assessment or mutate the interaction timestamp. Missing or
cross-company targets throw before the coordinator commits and enter named
retryable recovery with the issue status/version unchanged.
# Compile and migration checks
```sh
@@ -4,7 +4,7 @@ title: Phase 6 feature-flagged Paperclip adapter implementation
description: Implementation decisions and evidence for the default-off native runner seam, durable replay/finalization, and legacy kill switch.
tags: [native-runner, phase-6, implementation, paperclip, security, replay]
status: stable
generated: { by: openai/gpt-5.6, at: 2026-08-09T03:01:00Z }
generated: { by: openai/gpt-5.6, at: 2026-08-09T07:42:08Z }
entry_kind: phase
phase: "6"
---
@@ -73,6 +73,11 @@ existing server authority and keep legacy execution as the default.
native runs retain native recovery after flag-off; fresh unresolved runs
select legacy. Never rewrite the agent profile or persist a second rollback
control.
21. Treat duplicate and stale attention as terminal audit-only finalization.
The public port persists the result, the finalizer binds the exact durable
interaction target and commits a null-decision coordinator, and replay
short-circuits without another assessment or interaction mutation. Missing
and cross-company targets remain named fail-closed recovery.
# Evidence
@@ -80,7 +85,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-five tests passed
- Phase 6 acceptance-matrix entry points: thirteen files, forty-six tests passed
with zero skips, including the database-backed selected-task canary, six
atomic-liveness failpoints, migration, sequencing, bounded recovery, and
legacy compatibility.
@@ -88,7 +93,8 @@ existing server authority and keep legacy execution as the default.
applied the complete migration; the actual flag-off heartbeat executed the
legacy adapter/finalizer and created zero native rows.
- Section 18.13 source corpus: 52 fixture-specific database executions passed
through named production consumers. Consumer returns and persisted rows
through named production consumers. The duplicate/stale rows enter through
the real finalizer owner rather than the persisted router directly. Consumer returns and persisted rows
supplied all eleven assertions: run status, status/preserve action, reason,
required/forbidden effects, live path, claim preservation, native-record
behavior, decision count, maximum wake count, and maximum notification
@@ -99,8 +105,8 @@ 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 and the persisted
attention ingress, change the global flag input, remove reconciliation/
- Negative entrypoint proofs remove cancellation auditing and the finalizer-
owned 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.
@@ -111,6 +117,16 @@ existing server authority and keep legacy execution as the default.
turn, or legacy call. The provider boundary remains scripted, not live Codex.
- Runner, shared, server, and database typechecks passed; migration safety
passed.
- The remediation-7 rerun passed the 13-file/46-test zero-skip matrix, all 19
runner-spec checks, runner docs validation, relevant TypeScript checks, and
`git diff --check`. A final call-graph review confirmed the public port owns
result persistence, the finalizer alone commits the zero-decision outcome,
the internal router binds the exact same-company interaction, and committed
replay short-circuits without another assessment or target mutation.
- The design's authoritative 93-file allowlist was mechanically compared with
the Phase 6 branch diff and had no missing or extra path, including the README,
spec checker, canonical helper, runtime-mode test, and database-backed port
test paths.
# Failures
+25
View File
@@ -224,6 +224,31 @@ check("duplicate attention creates no status decision or unbounded loop", () =>
return `${duplicates.length} duplicate fixture(s) bounded at zero decisions/wakes/notifications`;
});
check("audit-only duplicate and stale fixtures retain exact durable targets", () => {
const expected = new Map([
["duplicate-and-fresh-key-question", ["equivalent_attention_family", "link_canonical_request"]],
["stale-attention-response", ["response_after_supersession", "record_stale_response"]],
]);
for (const [id, [completionState, effect]] of expected) {
const fixture = corpus.fixtures.find((candidate) => candidate.id === id);
if (!fixture) throw new Error(`missing ${id}`);
if (
fixture.mode !== "native"
|| fixture.given.completionState !== completionState
|| fixture.expected.runStatus !== "succeeded"
|| fixture.expected.statusAction !== "preserve"
|| fixture.expected.reasonCode !== "attention_duplicate_suppressed"
|| fixture.expected.decisionCount !== 0
|| fixture.expected.maxWakeCount !== 0
|| fixture.expected.maxNotificationCount !== 0
|| !fixture.expected.requiredEffects.includes(effect)
) {
throw new Error(`${id} does not pin the zero-decision ${effect} outcome`);
}
}
return "duplicate and stale targets are zero-decision audit outcomes";
});
check("completion-gating human judgment is explicit", () => {
const fixture = corpus.fixtures.find((candidate) => candidate.id === "human-authority-required");
if (!fixture) throw new Error("missing human-authority-required fixture");
@@ -1,6 +1,8 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { randomUUID } from "node:crypto";
import {
activityLog,
agentWakeupRequests,
agents,
companies,
completionContracts,
@@ -378,4 +380,317 @@ describe("P6-19 native interaction bridge", () => {
]);
}
}, 30_000);
it("commits duplicate and stale attention as replay-stable zero-decision audit outcomes", async () => {
const auditSummary = "Response retained for audit after native supersession.";
const seedAuditRun = async (input: {
label: string;
requests: Record<string, unknown>[];
interactions: Array<typeof issueThreadInteractions.$inferInsert>;
}) => {
const localIssueId = randomUUID();
const localRunId = randomUUID();
const localContractId = randomUUID();
const contractSha = `audit-contract:${input.label}`;
await db.insert(issues).values({
id: localIssueId,
companyId,
title: `Audit-only native attention ${input.label}`,
status: "in_progress",
assigneeAgentId: agentId,
workMode: "standard",
});
await db.insert(completionContracts).values({
id: localContractId,
companyId,
issueId: localIssueId,
revision: 1,
schemaVersion: "paperclip.completion-contract.v1",
policyVersion: "phase6-v1",
risk: "standard",
completionAuthority: "server_arbiter",
incompleteCriteriaPolicy: "preserve_non_terminal",
contractJson: {
revision: "audit-v1",
objective: "Retain superseded attention for audit",
criteria: [{ id: "objective", requirement: "Preserve the authoritative issue state" }],
},
canonicalSha256: contractSha,
createdByActorType: "system",
createdByActorId: "test",
});
await db.insert(heartbeatRuns).values({
id: localRunId,
companyId,
agentId,
status: "running",
runtimeMode: "native",
runtimeModeResolverVersion: "phase6-v1",
runtimeModeReason: "eligible_opt_in",
runtimeModeResolvedAt: new Date(),
completionContractId: localContractId,
completionContractSha256: contractSha,
contextSnapshot: { issueId: localIssueId },
});
if (input.interactions.length > 0) {
await db.insert(issueThreadInteractions).values(input.interactions.map((interaction) => ({
...interaction,
companyId: interaction.companyId ?? companyId,
issueId: interaction.issueId ?? localIssueId,
})));
}
const port = new PaperclipControlPlanePort(db, {
companyId,
issueId: localIssueId,
runId: localRunId,
agentId,
completionContractId: localContractId,
completionContractSha256: contractSha,
sourceInstanceId: `audit-runner:${input.label}`,
controlPlaneSourceInstanceId: `audit-control:${input.label}`,
});
await port.completeRun({
result: {
schema: "paperclip.run_result.v1",
reportedWorkDisposition: "yielded",
summary: `Audit-only native attention ${input.label}`,
completionClaim: {
contractRevision: "audit-v1",
objectiveSatisfied: false,
criteria: [{ criterionId: "objective", status: "unknown", evidenceRefs: [] }],
remainingWork: [{ description: "The canonical request remains authoritative", blocksCompletion: true }],
},
evidence: [],
verification: [{ commandOrCheck: "audit-only attention", status: "not_run" }],
attentionRequests: input.requests,
artifacts: [],
continuation: {
kind: "response_wake",
summary: "Wait for the canonical request",
idempotencyKey: `audit-only:${input.label}`,
},
},
terminal: {
schema: "paperclip.prp.terminal.v1",
turnTerminalState: "completed",
runTerminalState: "succeeded",
reportedWorkDisposition: "yielded",
},
turnId: `audit-turn:${input.label}`,
});
return { issueId: localIssueId, runId: localRunId };
};
const cases = ["duplicate", "stale", "mixed"] as const;
for (const label of cases) {
const canonicalId = randomUUID();
const duplicateId = randomUUID();
const staleId = randomUUID();
const duplicateRequest = {
id: `audit:${label}:duplicate`,
requestedCapability: "duplicate",
requiredAuthority: "agent",
target: { ownerClass: "current_agent", companyId },
summary: `Duplicate attention ${label}`,
responseState: "none",
targetInteractionId: duplicateId,
canonicalRequestId: canonicalId,
};
const staleRequest = {
id: `audit:${label}:stale`,
requestedCapability: "context_lookup",
requiredAuthority: "agent",
target: { ownerClass: "current_agent", companyId },
summary: `Stale attention ${label}`,
responseState: "stale",
targetInteractionId: staleId,
};
const requests = label === "duplicate"
? [duplicateRequest]
: label === "stale" ? [staleRequest] : [duplicateRequest, staleRequest];
const interactions: Array<typeof issueThreadInteractions.$inferInsert> = [];
if (label !== "stale") {
interactions.push(
{
id: canonicalId,
kind: "request_confirmation",
status: "pending",
payload: { version: 1, prompt: `Canonical ${label}` },
},
{
id: duplicateId,
kind: "request_confirmation",
status: "expired",
resolvedAt: new Date(),
payload: { version: 1, prompt: `Duplicate ${label}` },
result: { version: 1, outcome: "superseded_by_newer_request", supersededByInteractionId: canonicalId },
},
);
}
if (label !== "duplicate") {
interactions.push({
id: staleId,
kind: "request_confirmation",
status: "expired",
resolvedAt: new Date(),
payload: { version: 1, prompt: `Stale ${label}` },
result: { version: 1, outcome: "superseded_by_comment", commentId: randomUUID() },
});
}
const seeded = await seedAuditRun({ label, requests, interactions });
const finalized = await finalizeNativeRun({
db,
runId: seeded.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
expect(finalized).toMatchObject({ phase: "committed", decisionId: null });
expect(finalized.attentionReceipts).toHaveLength(requests.length);
expect(finalized.attentionReceipts).toEqual(expect.arrayContaining(requests.map((request) =>
expect.objectContaining({
requestId: request.id,
decisionId: null,
reasonCode: "attention_duplicate_suppressed",
materializedTargets: [expect.objectContaining({
targetType: "issue_thread_interaction",
targetId: request.targetInteractionId,
})],
}),
)));
await expect(db.select().from(issues).where(eq(issues.id, seeded.issueId))).resolves.toEqual([
expect.objectContaining({ status: "in_progress", statusVersion: 0, lastStatusDecisionId: null }),
]);
await expect(db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, seeded.runId))).resolves.toEqual([
expect.objectContaining({
status: "succeeded",
nativePhase: "committed",
resultJson: expect.objectContaining({
finalizationPhase: "committed",
decisionId: null,
authoritativeDecision: null,
nativeAttentionRouting: expect.any(Array),
}),
}),
]);
await expect(db.select().from(nativeRunFinalizations)
.where(eq(nativeRunFinalizations.runId, seeded.runId))).resolves.toEqual([
expect.objectContaining({ phase: "committed", decisionId: null, failureCode: null }),
]);
await expect(db.select().from(statusDecisions).where(eq(statusDecisions.issueId, seeded.issueId))).resolves.toHaveLength(0);
await expect(db.select().from(statusDecisionEffects).where(eq(statusDecisionEffects.issueId, seeded.issueId))).resolves.toHaveLength(0);
await expect(db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, companyId))).resolves.not.toEqual(
expect.arrayContaining([expect.objectContaining({ payload: expect.objectContaining({ issueId: seeded.issueId }) })]),
);
const targetIds = requests.map((request) => request.targetInteractionId);
const beforeReplay = await db.select({
id: issueThreadInteractions.id,
summary: issueThreadInteractions.summary,
updatedAt: issueThreadInteractions.updatedAt,
}).from(issueThreadInteractions).where(and(
eq(issueThreadInteractions.issueId, seeded.issueId),
));
expect(beforeReplay.filter((interaction) => targetIds.includes(interaction.id))).toEqual(
expect.arrayContaining(requests.map((request) => expect.objectContaining({
id: request.targetInteractionId,
summary: request.responseState === "stale"
? auditSummary
: `Canonical native attention request: ${canonicalId}`,
}))),
);
const assessmentCount = await db.select().from(workAssessments)
.where(eq(workAssessments.runId, seeded.runId)).then((rows) => rows.length);
if (label === "stale") {
// Model a restart after the audit target/receipt became durable but
// before the coordinator commit. The router must rediscover the
// request without applying the interaction mutation a second time.
await db.update(nativeRunFinalizations).set({
phase: "arbitrating",
decisionId: null,
leaseOwner: null,
leaseExpiresAt: null,
}).where(eq(nativeRunFinalizations.runId, seeded.runId));
}
await expect(finalizeNativeRun({
db,
runId: seeded.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
})).resolves.toMatchObject({ phase: "committed", decisionId: null });
const afterReplay = await db.select({
id: issueThreadInteractions.id,
summary: issueThreadInteractions.summary,
updatedAt: issueThreadInteractions.updatedAt,
}).from(issueThreadInteractions).where(eq(issueThreadInteractions.issueId, seeded.issueId));
expect(afterReplay).toEqual(beforeReplay);
await expect(db.select().from(workAssessments)
.where(eq(workAssessments.runId, seeded.runId)).then((rows) => rows.length)).resolves.toBe(assessmentCount);
const coordinatorBeforeCommittedReplay = await db.select({ attempt: nativeRunFinalizations.attempt })
.from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, seeded.runId))
.then((rows) => rows[0] ?? null);
await expect(finalizeNativeRun({
db,
runId: seeded.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
})).resolves.toMatchObject({ phase: "committed", decisionId: null });
await expect(db.select({ attempt: nativeRunFinalizations.attempt })
.from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, seeded.runId))
.then((rows) => rows[0] ?? null)).resolves.toEqual(coordinatorBeforeCommittedReplay);
}
const outsideCompanyId = randomUUID();
const outsideIssueId = randomUUID();
const outsideInteractionId = randomUUID();
await db.insert(companies).values({ id: outsideCompanyId, name: "Outside audit target", issuePrefix: "OAT" });
await db.insert(issues).values({
id: outsideIssueId,
companyId: outsideCompanyId,
title: "Outside audit target",
status: "in_progress",
workMode: "standard",
});
await db.insert(issueThreadInteractions).values({
id: outsideInteractionId,
companyId: outsideCompanyId,
issueId: outsideIssueId,
kind: "request_confirmation",
status: "expired",
resolvedAt: new Date(),
payload: { version: 1, prompt: "Outside stale response" },
result: { version: 1, outcome: "superseded_by_comment", commentId: randomUUID() },
});
for (const [label, targetInteractionId] of [
["missing", randomUUID()],
["cross-company", outsideInteractionId],
] as const) {
const seeded = await seedAuditRun({
label: `invalid-${label}`,
requests: [{
id: `audit:invalid:${label}`,
requestedCapability: "context_lookup",
requiredAuthority: "agent",
target: { ownerClass: "current_agent", companyId },
summary: `Invalid audit target ${label}`,
responseState: "stale",
targetInteractionId,
}],
interactions: [],
});
await expect(finalizeNativeRun({
db,
runId: seeded.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
})).resolves.toMatchObject({ phase: "retryable_failure", failureCode: "side_effect_planning_failed" });
await expect(db.select().from(issues).where(eq(issues.id, seeded.issueId))).resolves.toEqual([
expect.objectContaining({ status: "in_progress", statusVersion: 0, lastStatusDecisionId: null }),
]);
await expect(db.select().from(statusDecisions).where(eq(statusDecisions.issueId, seeded.issueId))).resolves.toHaveLength(0);
}
await expect(db.select({ summary: issueThreadInteractions.summary })
.from(issueThreadInteractions)
.where(eq(issueThreadInteractions.id, outsideInteractionId))).resolves.toEqual([{ summary: null }]);
}, 30_000);
});
@@ -27,7 +27,6 @@ import {
applyNativeAttentionStatusDecision,
materializeNativeInteractionResponses,
rejectUnsupportedNativeRuntimeRequest,
routePersistedNativeResultAttention,
resolveNativeAttentionStatus,
} from "../services/native-runtime/native-interaction-bridge.js";
import {
@@ -55,6 +54,7 @@ import {
import {
projectNativeTerminalRunStatus,
recordNativeFinalizationFailure,
finalizeNativeRun,
resolveNativeFinalizerStatus,
} from "../services/native-runtime/native-run-finalizer.js";
import {
@@ -706,7 +706,13 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
},
},
}).where(eq(nativeRunResults.id, seeded.resultId!));
const [receipt] = await routePersistedNativeResultAttention({ db, runId: seeded.runId });
const finalized = await finalizeNativeRun({
db,
runId: seeded.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
const [receipt] = "attentionReceipts" in finalized ? finalized.attentionReceipts : [];
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))
@@ -728,14 +734,43 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
liveEntrypointCommitted = true;
for (const effect of routedDecision.effects) materializedEffects.add(effect.kind);
consumerExecutions.push({
consumer: "native-attention-entrypoint",
consumer: "native-attention-finalizer",
observed: {
phase: finalized.phase,
decisionId: receipt.decisionId,
resolvedTargetAgentId: receipt.resolvedTargetAgentId,
reasonCode: receipt.reasonCode,
materializedTargets: receipt.materializedTargets,
persistedResultId: seeded.resultId,
},
});
if (receipt.decisionId === null) {
const beforeReplay = liveAttentionInteractionId
? await db.select({
summary: issueThreadInteractions.summary,
updatedAt: issueThreadInteractions.updatedAt,
}).from(issueThreadInteractions).where(eq(issueThreadInteractions.id, liveAttentionInteractionId))
.then((rows) => rows[0] ?? null)
: null;
const assessmentCount = await db.select().from(workAssessments)
.where(eq(workAssessments.runId, seeded.runId)).then((rows) => rows.length);
const replayed = await finalizeNativeRun({
db,
runId: seeded.runId,
workspaceFinalizeStatus: "succeeded",
projectRunStatus: true,
});
expect(replayed, `${fixture.id}:audit replay coordinator`).toMatchObject({ phase: "committed", decisionId: null });
if (liveAttentionInteractionId) {
await expect(db.select({
summary: issueThreadInteractions.summary,
updatedAt: issueThreadInteractions.updatedAt,
}).from(issueThreadInteractions).where(eq(issueThreadInteractions.id, liveAttentionInteractionId))
.then((rows) => rows[0] ?? null)).resolves.toEqual(beforeReplay);
}
await expect(db.select().from(workAssessments)
.where(eq(workAssessments.runId, seeded.runId)).then((rows) => rows.length)).resolves.toBe(assessmentCount);
}
if (attentionFacts.route === "agent" && completionState !== "cross_company_target") {
expect(receipt.resolvedTargetAgentId, `${fixture.id}:eligible delegate`).toBe(delegateAgentId);
}
@@ -1694,9 +1729,9 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
if (
attentionFacts
&& ["attention_response", "attention_candidate", "interaction", "monitor"].includes(String(fixture.given.trigger))
&& !consumerExecutions.some((execution) => execution.consumer === "native-attention-entrypoint")
&& !consumerExecutions.some((execution) => execution.consumer === "native-attention-finalizer")
) {
throw new Error(`${fixture.id}: live attention proof missing`);
throw new Error(`${fixture.id}: live attention finalizer proof missing`);
}
if (
["REC-04", "REC-06", "REC-07", "REC-08"].some((row) => (fixture.covers.reconciliationRows ?? []).includes(row))
@@ -1832,7 +1867,13 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => {
}).reasonCode).toBe("attention_routed_to_agent");
await expect(executeFixture(byId("human-request-routed-to-agent"), {
disableLiveEntrypoint: "attention",
})).rejects.toThrow("live attention proof missing");
})).rejects.toThrow("live attention finalizer proof missing");
await expect(executeFixture(byId("duplicate-and-fresh-key-question"), {
disableLiveEntrypoint: "attention",
})).rejects.toThrow("live attention finalizer proof missing");
await expect(executeFixture(byId("stale-attention-response"), {
disableLiveEntrypoint: "attention",
})).rejects.toThrow("live attention finalizer proof missing");
expect(resolveNativeReconciliationStatus({
facts: { workspaceOperationPending: true },
@@ -66,7 +66,10 @@ export async function applyNativeAttentionStatusDecision(input: {
if (effect.kind === "link_canonical_request") {
const boundIds = [input.canonicalRequestId, input.targetInteractionId]
.filter((id): id is string => typeof id === "string");
const interactions = await tx.select({ id: issueThreadInteractions.id })
const interactions = await tx.select({
id: issueThreadInteractions.id,
summary: issueThreadInteractions.summary,
})
.from(issueThreadInteractions).where(and(
eq(issueThreadInteractions.companyId, input.companyId),
eq(issueThreadInteractions.issueId, input.issueId),
@@ -81,25 +84,34 @@ export async function applyNativeAttentionStatusDecision(input: {
if (!canonical || !duplicate || canonical.id === duplicate.id) {
throw new NativeInteractionBridgeError("native_attention_canonical_pair_missing", "Canonical and duplicate requests are required");
}
await tx.update(issueThreadInteractions).set({
summary: `Canonical native attention request: ${canonical.id}`,
updatedAt: new Date(),
}).where(eq(issueThreadInteractions.id, duplicate.id));
const summary = `Canonical native attention request: ${canonical.id}`;
if (duplicate.summary !== summary) {
await tx.update(issueThreadInteractions).set({
summary,
updatedAt: new Date(),
}).where(eq(issueThreadInteractions.id, duplicate.id));
}
targets.push({ effectKind: effect.kind, targetType: "issue_thread_interaction", targetId: duplicate.id });
continue;
}
if (effect.kind === "record_stale_response") {
const interaction = await tx.select({ id: issueThreadInteractions.id })
const interaction = await tx.select({
id: issueThreadInteractions.id,
summary: issueThreadInteractions.summary,
})
.from(issueThreadInteractions).where(and(
eq(issueThreadInteractions.companyId, input.companyId),
eq(issueThreadInteractions.issueId, input.issueId),
...(input.targetInteractionId ? [eq(issueThreadInteractions.id, input.targetInteractionId)] : []),
)).limit(1).then((rows) => rows[0] ?? null);
if (!interaction) throw new NativeInteractionBridgeError("native_stale_response_missing", "Stale response is not durable");
await tx.update(issueThreadInteractions).set({
summary: "Response retained for audit after native supersession.",
updatedAt: new Date(),
}).where(eq(issueThreadInteractions.id, interaction.id));
const summary = "Response retained for audit after native supersession.";
if (interaction.summary !== summary) {
await tx.update(issueThreadInteractions).set({
summary,
updatedAt: new Date(),
}).where(eq(issueThreadInteractions.id, interaction.id));
}
targets.push({ effectKind: effect.kind, targetType: "issue_thread_interaction", targetId: interaction.id });
continue;
}
@@ -402,9 +414,11 @@ function candidateFromPersistedRequest(request: Record<string, unknown>): Native
export type PersistedNativeAttentionReceipt = {
requestId: string;
route: NativeAttentionCandidate["route"];
assessmentId: string;
decisionId: string | null;
reasonCode: string | null;
resolvedTargetAgentId: string | null;
materializedTargets: NativeAttentionMaterializedTarget[];
replayed: boolean;
};
@@ -504,9 +518,11 @@ export async function routePersistedNativeResultAttention(input: {
receipts.push({
requestId,
route: candidate.route,
assessmentId: existingAssessment.id,
decisionId: existingDecision.id,
reasonCode: existingDecision.reasonCode,
resolvedTargetAgentId: nonEmptyText(delegation?.agentId),
materializedTargets: [],
replayed: true,
});
continue;
@@ -554,10 +570,12 @@ export async function routePersistedNativeResultAttention(input: {
receipts.push({
requestId,
route: candidate.route,
assessmentId: assessment.id,
decisionId: routed.decisionId,
reasonCode: routed.decision.reasonCode,
resolvedTargetAgentId: routed.resolvedTargetAgentId,
replayed: false,
materializedTargets: routed.targets,
replayed: existingAssessment !== null,
});
}
@@ -42,6 +42,18 @@ function authoritativeStatus(value: string): NativeAuthoritativeIssueStatus {
return value as NativeAuthoritativeIssueStatus;
}
function isCommittedAuditOnlyRouting(value: unknown) {
const receipts = record(value).nativeAttentionRouting;
return Array.isArray(receipts) && receipts.length > 0 && receipts.every((candidate) => {
const receipt = record(candidate);
const targets = receipt.materializedTargets;
return receipt.decisionId === null
&& receipt.reasonCode === "attention_duplicate_suppressed"
&& Array.isArray(targets)
&& targets.length > 0;
});
}
/** Server-owned terminal conversion used by live finalization and read models. */
export function projectNativeTerminalRunStatus(
terminalState: "succeeded" | "failed" | "cancelled" | "active",
@@ -101,7 +113,20 @@ async function claimCoordinator(input: { db: Db; runId: string }) {
.where(eq(nativeRunFinalizations.runId, input.runId)).for("update").limit(1)
.then((rows) => rows[0] ?? null);
if (!coordinator?.resultId) throw new Error("native_finalization_missing");
if (coordinator.phase === "committed" && coordinator.decisionId) {
if (coordinator.phase === "committed") {
if (!coordinator.decisionId) {
const run = await tx.select({ resultJson: heartbeatRuns.resultJson })
.from(heartbeatRuns)
.where(and(
eq(heartbeatRuns.id, coordinator.runId),
eq(heartbeatRuns.companyId, coordinator.companyId),
))
.limit(1)
.then((rows) => rows[0] ?? null);
if (!run || !isCommittedAuditOnlyRouting(run.resultJson)) {
throw new Error("native_finalization_committed_outcome_missing");
}
}
return { coordinator, leaseOwner: null };
}
if (coordinator.phase === "terminal_failure") throw new Error("native_finalization_terminal_failure");
@@ -385,12 +410,37 @@ export async function finalizeNativeRun(input: {
eq(statusDecisions.issueId, authoritativeIssue.id),
)).limit(1).then((rows) => rows[0] ?? null)
: null;
if (!latestCoordinator || !latestIssue || !latestDecision) {
const auditOnly = attentionReceipts.length > 0
&& attentionReceipts.every((receipt) =>
receipt.decisionId === null
&& receipt.reasonCode === "attention_duplicate_suppressed"
&& receipt.materializedTargets.length > 0
);
if (!latestCoordinator || !latestIssue || (!latestDecision && !auditOnly)) {
throw new Error("native_attention_routing_incomplete");
}
const committedCoordinator = auditOnly
? await input.db.update(nativeRunFinalizations).set({
phase: "committed",
assessmentId: attentionReceipts.at(-1)?.assessmentId ?? latestCoordinator.assessmentId,
decisionId: null,
leaseOwner: null,
leaseExpiresAt: null,
failureCode: null,
failureDetail: null,
nextAttemptAt: null,
updatedAt: new Date(),
}).where(and(
eq(nativeRunFinalizations.runId, input.runId),
eq(nativeRunFinalizations.companyId, run.companyId),
eq(nativeRunFinalizations.issueId, authoritativeIssue.id),
eq(nativeRunFinalizations.leaseOwner, claim.leaseOwner!),
)).returning().then((rows) => rows[0] ?? null)
: latestCoordinator;
if (!committedCoordinator) throw new Error("native_attention_audit_commit_failed");
const now = new Date();
const attentionFailed = latestCoordinator.phase === "retryable_failure"
|| latestCoordinator.phase === "terminal_failure";
const attentionFailed = committedCoordinator.phase === "retryable_failure"
|| committedCoordinator.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);
@@ -401,14 +451,14 @@ export async function finalizeNativeRun(input: {
: terminalState === "succeeded" ? "succeeded" : terminalState === "cancelled" ? "cancelled" : "failed",
finishedAt: now,
} : {}),
nativePhase: latestCoordinator.phase,
nativePhase: committedCoordinator.phase,
nativePhaseUpdatedAt: now,
resultJson: {
...record(currentRun?.resultJson),
finalizationPhase: latestCoordinator.phase,
assessmentId: latestCoordinator.assessmentId,
decisionId: latestCoordinator.decisionId,
authoritativeDecision: latestDecision.toStatus,
finalizationPhase: committedCoordinator.phase,
assessmentId: committedCoordinator.assessmentId,
decisionId: committedCoordinator.decisionId,
authoritativeDecision: latestDecision?.toStatus ?? null,
issueStatusBefore: authoritativeIssue.status,
issueStatusAfter: latestIssue.status,
statusVersionBefore: Number(authoritativeIssue.statusVersion),
@@ -418,7 +468,7 @@ export async function finalizeNativeRun(input: {
},
updatedAt: now,
}).where(eq(heartbeatRuns.id, run.id));
return { ...latestCoordinator, attentionReceipts };
return { ...committedCoordinator, attentionReceipts };
} catch (error) {
if (error instanceof NativeStatusRaceError && attempt < 2) {
supersedesAssessmentId = assessmentRow.id;