mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 13:24:55 +01:00
fix(native-runner): commit audit-only attention
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
c82a2142c9
commit
bacf670497
11 files changed
+688
-52
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user