# Issue 1782 Connector Push Durable Protocol

Date: 2026-06-04

Status: design contract for #1782; implementation consumers are #1784
greenfield runtime and #1783 migration/backfill.

## Scope

This document defines the v2 durable outbound connector protocol for
`PushToConnector`. It is protocol only. It does not add schema, runtime code,
workers, migrations, pack payload, or vendor-specific behavior.

The design consumes:

- #1781: outbound source values use the originating user's object, record/RLS,
  and field-level read authority. Pack trust authorizes connector metadata and
  connector callout credentials only.
- #1726 / v1a: current runtime is synchronous PATCH-existing only. It has no
  POST/create, local writeback, durable link table, durable attempt table,
  outbox retry, checkpoint, or migration behavior.

The protocol is generic L0 integration substrate. It cannot branch on CRM object
names, tenant-specific fields, profile literals, or vendor-specific endpoints.

## Governing Invariant

One logical connector action on one local source record has one durable
remote-link owner. Pending creates and finalized links are found by that durable
owner, never by scanning the current record's materialized remote-id fields.

## Design Options

Option A: record-field provenance remains authoritative.

Rejected. This is the PR #1730 failure mode: changing the configured
remote-id/provenance field pair can hide an existing link and make the same
local record look unsynced.

Option B: vendor idempotency key is the only create-attempt identity.

Rejected. It helps only vendors that support idempotent create. It also cannot
define PATCH identity, migration adoption, or materialized-field drift inside
NexusCRM.

Option C: dedicated durable link plus durable create attempt.

Selected. `_System_ConnectorRemoteLink` is the SSoT for finalized remote-link
identity. `_System_ConnectorPushAttempt` is the SSoT for unfinalized create
attempts and ambiguous POST recovery. Record-level remote-id/provenance fields
become compatibility mirrors after v2 cutover, not authority.

V2 also moves create/update route templates into pack-owned action config for
durable execution. V1a may accept immediate routing inputs for synchronous
PATCH-only execution; durable create/PATCH retry must not let mutable source
record fields redefine the remote URL after an attempt has been reserved.

## Protocol Objects

The table names below are protocol names for #1784. This document does not
create them.

`_System_ConnectorRemoteLink` represents a finalized or suspended relationship
between one local source row and one remote destination slot.

`_System_ConnectorPushAttempt` represents one durable create attempt for a link
key that does not yet have a finalized remote id. It owns request identity,
request snapshots, retry state, ambiguous remote-effect state, and writeback
recovery.

## Identity Model

### Stable Slot Keys

The logical connector action and destination must be stable across Flow version
activation, pack reapply, and metadata rename/rebind when the same pack keeps
the same logical connector slot. Durable lookup must not derive identity from
mutable developer names, profile names, target object names, or display
namespaces alone.

Canonical stable identity fields:

| Field | Meaning |
|---|---|
| `ConnectorOwnerKey` | Immutable connector/owner-pack identity. It is not a display namespace string and must survive namespace/display rename for the same pack lineage. |
| `ActionSlotId` | Immutable pack-authored Flow action slot identity. It is not `FlowDeveloperName`, `FlowVersionId`, or the action element name. |
| `DestinationSlotId` | Immutable pack-authored remote destination slot identity for the target/profile/route family. It is not `TargetObject` or `ProfileName`. |
| `ActionType` | `PushToConnector`. |
| `SourceObject` | Local source SObject API name. |

Human-readable metadata fields remain required snapshots for drift validation:
`ConnectorNamespace`, `FlowDeveloperName`, `FlowActionName`, `TargetObject`, and
`ProfileName`. They are not primary lookup keys. If #1784 cannot resolve stable
owner/action/destination slot identities for a candidate send, it must fail
closed before POST instead of falling back to mutable names.

Renaming a Flow action, profile, target object, or namespace is benign only when
the stable slot ids prove the same logical slot. If a pack removes/recreates a
slot with new stable ids, the runtime must treat that as key-changing drift:
find any prior rows for the old slot lineage through explicit rebind/migration
metadata, or fail closed until #1783/#1784 retires, supersedes, or rebinds the
old ownership. It must never reserve a fresh create merely because recomputing a
name-derived key found no rows.

The active `PackInstallId` is not part of the logical action slot key. It is a
frozen authority snapshot on links and attempts. If the active install
incarnation changes, existing rows are still discoverable by the stable slot key
and must fail closed or be explicitly revalidated by #1784/#1783 rather than
silently becoming invisible and creating duplicates.

This is deliberate. A key that includes `PackInstallId` protects against
same-name pack rebirth contamination only by making old links disappear from
normal lookup. Disappearing links are unsafe for outbound create because the next
source save could reserve a fresh create for the same logical slot. V2 instead
finds the stable link first, compares its frozen install/profile snapshots with
the active install, and suspends or revalidates explicitly.

### Link Key

The durable link key identifies the remote-link owner:

```text
link_key = hash_v1(
  ConnectorOwnerKey,
  ActionSlotId,
  ActionType,
  SourceObject,
  SourceRecordId,
  DestinationSlotId
)
```

The link row stores, but does not key by:

- `CreatedPackInstallId` and `LastValidatedPackInstallId`.
- the human-readable action/destination snapshots: `ConnectorNamespace`,
  `FlowDeveloperName`, `FlowActionName`, `TargetObject`, and `ProfileName`.
- the active connector profile snapshot used to create or last validate the
  link: `CredentialRef`, `EndpointClass`, retry policy identity, endpoint
  binding generation when available, and profile payload hash.
- the action config snapshot: create path template, update path template,
  responseIdPath, remote-id field tuple, and mapping/body fingerprints.
- the finalized `RemoteId`.
- the compatibility mirror tuple: source object, remote-id field, provenance
  field, and last mirror status.

The greenfield implementation must also enforce a secondary remote-locator
uniqueness constraint after a remote id is known: within one
`ConnectorOwnerKey`, `DestinationSlotId`, remote destination scope, and
canonical remote id, one remote locator may belong to only one non-retired link.
The implementation must make this a hard uniqueness/index rule for finalized
links, not advisory validation. Target object and profile names are validation
snapshots for that destination slot; they are not part of the uniqueness scope
that proves a remote id is unclaimed. This rule prevents two local source rows
from claiming the same remote record, while the primary `link_key` still remains
the local-slot identity that can exist before create succeeds.

### Attempt Key

An open create attempt is unique by `link_key`. There must never be two active
attempt rows for the same link key.

The attempt row id is a durable generated identity, but all retry and recovery
lookup starts from:

```text
attempt_owner_key = link_key where status in active_create_statuses
```

The create gate also treats any unsuperseded terminal attempt for the same
`link_key` as an occupied create slot. A later source save must fail closed when
it finds `terminal_failed` without an explicit `superseded` transition, even
though that row is no longer active. The only protocol-compliant path to a new
POST after retry exhaustion is `terminal_failed -> superseded -> new attempt`,
performed by explicit operator or migration policy.

The attempt stores:

- the `link_key`, stable slot fields, and human-readable action/destination
  snapshots;
- a closed `DispatchLane`: existing and operator-enabled async work uses
  `configured`, while a create whose create-inline association target is not
  finalized after save-time source authorization uses `deferred_association`;
- nullable `RemoteIOStartedAt`, written under the owned processing claim after
  all validation/freezing and immediately before the worker begins remote I/O;
- `AttemptOrdinal`, starting at 1 and increasing only after a terminal attempt
  is explicitly superseded by policy;
- `RequestIdempotencyKey`, generated before any remote POST is attempted;
- frozen create request snapshot: method, relative path, headers safe to replay,
  body, body hash, target destination snapshot, credential/profile snapshot,
  and responseIdPath;
- frozen #1781 authority snapshot: originating user id, source object, source
  record id, allowed source fields, authority decision timestamp, and mapping
  fingerprint used to build the body;
- a position-aligned snapshot of every association contract's save-time source
  record id, including blank entries, so a delayed create never re-reads a
  mutable lookup field to choose a different remote association target;
- POST response snapshot when available: status, response headers needed for
  recovery, response body hash, and extracted remote id;
- writeback snapshot: record source object/id, compatibility remote-id field,
  compatibility provenance field, and expected empty/current values.

The request payload/body fingerprint is not part of the active attempt key.
Source changes while an active create attempt exists must not create another
POST attempt. They are handled after finalization through PATCH or fail-closed
parking, according to #1784.

## Source Of Truth

| Fact | SSoT after v2 cutover | Notes |
|---|---|---|
| Final remote id for a source/action/destination | `_System_ConnectorRemoteLink.RemoteId` | Record fields are compatibility mirrors only. |
| Whether create is pending or ambiguous | `_System_ConnectorPushAttempt` | Current record fields must not infer this. |
| Outbound source-value authority | #1781 authority snapshot on the attempt, sourced from permissions/FLS at enqueue | Retry cannot widen by re-reading current permissions. |
| Mapping/payload construction for a new attempt | pack-authored outbound mapping SSoT plus #1781 projection | The attempt freezes the produced request snapshot. |
| Create/update route templates | pack-owned action config | Durable retry does not read route templates from source-record fields. |
| Retry body for an existing attempt | `_System_ConnectorPushAttempt` request snapshot | Do not rebuild from live mapping/source fields. |
| PATCH target identity | `_System_ConnectorRemoteLink` plus drift validation | Do not trust the materialized record remote-id field. |
| Compatibility remote-id/provenance fields | record fields mirrored from link state | Useful for legacy UX/API compatibility, not occupancy authority. |

## Lifecycle

### Remote Link State

| State | Meaning | Allowed next states |
|---|---|---|
| `finalized` | A remote id is durably known for the link key. | `suspended_drift`, `retired` |
| `suspended_drift` | Link exists, but active metadata/profile/credential/path drift blocks runtime use. | `finalized` after explicit revalidation, `retired` |
| `retired` | Link is no longer eligible for runtime send. | none |

There is no `pending` remote-link state. Pending create is represented by
`_System_ConnectorPushAttempt` so link and attempt ownership do not blur.

### Create Attempt State

| State | Meaning | Allowed next states |
|---|---|---|
| `pending` | Attempt is durably reserved before POST. | `processing`, `terminal_failed` |
| `processing` | A worker owns a lease for local validation or POST. | `post_sent`, `writeback_pending`, `ambiguous_remote_effect`, `terminal_failed`, `pending` after lease expiry |
| `post_sent` | Remote POST was sent and response is being interpreted. | `writeback_pending`, `ambiguous_remote_effect`, `terminal_failed` |
| `ambiguous_remote_effect` | POST may have succeeded but no recoverable remote id is known. | `writeback_pending` only through explicit reconciliation; otherwise no more POST |
| `writeback_pending` | Remote id is known and link finalization/mirror writeback is pending or partially failed. | `finalized`, `terminal_failed` |
| `finalized` | Link row has the remote id; compatibility mirror is written or recorded as skipped. | none |
| `terminal_failed` | Attempt cannot proceed automatically. | `superseded` only by explicit operator/migration policy |
| `superseded` | Closed by an explicit later attempt. | none |

### Deferred Association Dispatch Lane

`deferred_association` is a narrow pre-dispatch continuation of `pending_create`,
not a second create protocol and not a generic backlog worker. It is assigned
only when the ordinary synchronous request path has completed association
source resolution and user authorization, the local record is a genuine create
candidate, and a create-inline association target lacks a finalized durable
link. The request thread freezes the full contract-aligned association source-id
vector, reserves one POST attempt, and performs zero remote I/O.

The runtime may run one always-on narrow worker when the operator-enabled
connector push worker is disabled. That worker must apply
`DispatchLane = 'deferred_association'` in the SQL claim before mutating rows.
It may recover and claim both POST and follow-up PATCH work in that lane, but it
must not claim or recover `configured` attempts, including historical PATCH
leases, or claim `writeback_pending` rows. The schema default for
`DispatchLane` is `configured`, so rows created before this lane existed remain
ineligible and no deployment implicitly replays or backfills them.

Expired `deferred_association` POST claims are recovered by any worker claim
that is authorized to consume that lane: the narrow lane when the general
worker is disabled, or the general claim when it is enabled. A `processing` row
with `RemoteIOStartedAt IS NULL` is provably pre-I/O and returns to `pending`; a
row with the checkpoint set, or a `post_sent` row, becomes
`ambiguous_remote_effect` and is never replayed. Request-thread re-triggers
observe active deferred attempts without performing either recovery or remote
I/O, so worker claim remains the single recovery owner in either mode.

Expired `deferred_association` PATCH claims follow the existing PATCH recovery
rules under the same lane-scoped claim. A replay-safe `processing` PATCH whose
frozen dispatch intent and originating-user authority remain present returns to
`pending` with its retry count advanced only when `RemoteIOStartedAt` is null.
Immediately before the first PATCH or association callout, the worker persists
that checkpoint under its owned claim. Any expired `processing` PATCH with the
checkpoint set, any PATCH without replay authority, or any expired `post_sent`
PATCH becomes `ambiguous_remote_effect`; none is replayed.

The general worker is also closed to the two recognized lanes: `configured`
and `deferred_association`. Its attempt claim, writeback claim, writeback CAS,
and every PATCH lease-recovery mutation carry that explicit lane set. A row
with any unknown `DispatchLane` is neither recovered nor claimed; it remains
untouched for explicit operator repair instead of being normalized implicitly
by a broad worker scan or starving valid writebacks through scan rollback.

The same lane authority applies after a successful deferred POST. If the
compatibility mirror fails, the worker activates `writeback_pending` under a
bounded non-cancelled context. A crash between durable POST-result commit and
that activation is recovered by promoting only `deferred_association` POST
rows from `writeback_commit_pending` whose remote-I/O checkpoint and remote ID
are both present; the narrow worker then claims only deferred writebacks.
Historical `configured` writebacks remain ineligible.

Configured async POST attempts created before the association-source snapshot
column existed retain their historical live-lookup recovery semantics when the
persisted vector is empty. This compatibility exception applies only to the
pre-existing `configured` lane; a `deferred_association` attempt without the
full contract-aligned vector fails closed and is never reconstructed from a
later lookup value. Historical configured POST recovery resolves and waits only
for create-inline association targets, preserving the pre-column behavior; an
unready update-only target cannot hold that POST in retry.

Request-thread re-triggers also leave deferred `writeback_commit_pending` and
`writeback_pending` rows unchanged after validating the frozen metadata and
known remote id. Link finalization and compatibility mirror retry remain owned
by the lane-scoped worker; the request path performs no fallback mirror write,
state transition, or remote callout.

After the association target link finalizes, the narrow worker reuses the same
attempt, resolves the target link from the frozen save-time source record id,
freezes the association-complete POST body under its processing claim,
and follows the existing create state machine. Missing update-only association
targets still fail admission even when an earlier create-inline target is also
pending. A later record trigger that finds a pending `deferred_association`
attempt validates its metadata and leaves it worker-owned; it never resumes the
POST on the request thread. PATCH behavior, parent-record authorization, and the
operator-enabled generic worker remain unchanged.

## State Transitions

### Before POST

Runtime must fail closed before remote POST when any of these are true:

- no trusted pack-owned Flow/action slot can be resolved;
- stable `ConnectorOwnerKey`, `ActionSlotId`, or `DestinationSlotId` cannot be
  resolved from trusted pack metadata;
- no originating user authority snapshot can be produced under #1781;
- active pack/profile cannot be resolved to exactly one usable active
  incarnation;
- pack-authored outbound mappings are absent, customized, duplicated, or
  produce no body;
- create/update path templates are missing from trusted pack-owned action
  config or are sourced from mutable record fields;
- source record read or source-field FLS fails;
- the source record no longer exists before the first POST;
- a finalized or suspended remote link already exists for the link key;
- an active create attempt already exists for the link key;
- an unsuperseded terminal create attempt exists for the link key;
- key-changing metadata drift indicates prior link/attempt rows may exist under
  old slot lineage and they have not been explicitly revalidated, retired, or
  superseded;
- create path, responseIdPath, target object, profile, credential, endpoint
  class, or mapping snapshot cannot be frozen.

The attempt row must commit before the first POST. A process crash before POST
leaves a `pending` attempt and is safe to retry.

### After POST

If POST returns a recoverable remote id:

1. persist the response snapshot and extracted remote id;
2. create or update `_System_ConnectorRemoteLink` to `finalized` in the same
   durable transition that moves the attempt to `writeback_pending` or
   `finalized`;
3. write the compatibility mirror id/provenance only after link finalization.

If local mirror writeback fails after the remote id is known, runtime must not
POST again. If the source record still exists, it retries only the mirror
writeback or records the mirror as skipped/failed while preserving the
authoritative link row. If the source record was deleted before writeback,
runtime preserves the known link/attempt evidence only for reconciliation,
retire, or migration policy. It must not recreate the local source row, create a
replacement remote row, or treat the deleted local record as a fresh create
candidate.

If POST may have succeeded but no remote id was durably captured, runtime must
not issue a second POST unless the connector profile declares a replay-safe
remote idempotency/reconciliation contract and the retry uses the same
`RequestIdempotencyKey` and frozen request snapshot. Otherwise the attempt moves
to `ambiguous_remote_effect` and blocks duplicate creates until explicit
operator reconciliation or #1783 migration/backfill policy resolves it.

### Before PATCH

Runtime must resolve a finalized link by `link_key` before PATCH. It must fail
closed before outbound PATCH when:

- no finalized link exists;
- an active create attempt exists for the same key;
- active pack/profile/mapping/path drift is classified as fail-closed by the
  drift matrix;
- the link has `suspended_drift` or `retired` status;
- current source record read/FLS under #1781 fails for fields needed by the
  PATCH body;
- update path rendering from the durable remote id fails;
- remote validation, remote PATCH response, or remote-locator reconciliation
  proves the finalized link is no longer safely patchable.

The materialized record remote-id field is not used as PATCH authority. It may
be checked as a compatibility mirror signal only after the link row has already
identified the remote target.

Remote record-state checks are connector-profile contracts, not vendor-specific
branches in the generic runtime. A profile may declare a safe precondition,
version, ETag, or reconciliation mechanism. Without that explicit contract,
remote 404/deleted, out-of-band changed, and conflicting remote-locator signals
fail closed by suspending the link or parking the attempt for operator or #1783
reconciliation. They never silently create a replacement remote record or PATCH
a stale target.

## Drift Matrix

| Surface | Stored on link/attempt | Existing finalized link | Active create attempt | Migration/backfill implication |
|---|---|---|---|---|
| Flow version activation with same stable `ActionSlotId` | action slot key, FlowVersion id and Flow names as snapshots only | benign; revalidate current action config before new PATCH | retry uses frozen request; no rebuild | none |
| Flow action renamed with same `ActionSlotId` | `ActionSlotId`, old/new FlowDeveloperName and FlowActionName snapshots | benign only after drift validation; link remains discoverable by stable key | retry uses frozen request; no rebuild from renamed metadata | none |
| Flow action removed/recreated with new or missing `ActionSlotId` | stable slot id plus old name snapshots | fail closed; link becomes `suspended_drift` until explicit rebind/retire | no new attempt until old slot lineage is revalidated, superseded, or retired | migration/rebind may map old slot to new slot |
| connector namespace / owner pack renamed with same `ConnectorOwnerKey` | ConnectorOwnerKey and namespace snapshot | discover old link by stable owner key, then revalidate or fail closed | retry only frozen attempt; no name-derived new key | none |
| connector owner identity missing/changed | ConnectorOwnerKey | fail closed; do not auto-adopt by namespace string | fail closed unless explicit rebind proves same owner lineage | migration-only rebind |
| active PackInstallId changes | Created/last validated PackInstallId | discover old link by stable key, then fail closed until explicit revalidation | retry only frozen attempt; no new POST under new install | #1783 may validate/rebind |
| connector profile renamed with same `DestinationSlotId` | DestinationSlotId, ProfileName, profile hash | benign only after profile/destination drift validation | retry uses frozen profile snapshot; no rebuild | none |
| connector profile deleted/inactive or recreated with new `DestinationSlotId` | DestinationSlotId, ProfileName, profile hash | fail closed before PATCH | fail closed unless attempt is already in writeback-only recovery; no new create under name-derived key | no adoption without #1783 |
| credential reference changes | CredentialRef hash/generation | fail closed before PATCH unless profile declares credential rotation as same remote destination | active POST retry fails closed; writeback-only recovery can continue | may require operator validation |
| endpoint class or endpoint binding changes | EndpointClass and endpoint binding hash/generation | fail closed before PATCH unless declared benign by profile contract | active POST retry fails closed; writeback-only recovery can continue | migration/revalidation required |
| credential secret or auth generation changes | CredentialRef, auth type, and credential generation/hash when available | fail closed before PATCH unless the credential contract declares same remote tenant/destination | active POST retry fails closed unless frozen idempotency/recovery remains valid; writeback-only recovery can continue | operator validation or #1783 revalidation |
| retry policy changes | retry policy hash | benign for future PATCH attempts; does not change link identity | active create uses frozen retry/idempotency policy | none |
| outbound mapping rows change | mapping fingerprint and body fingerprint | fail closed before PATCH until payload SSoT revalidates; do not mutate link key | active create uses frozen body; new source changes do not create another POST | #1783/#1784 may provide revalidation tooling |
| source field FLS/RLS changes | authority snapshot and current authority check | current PATCH must pass current #1781 authority; link remains authoritative | create retry uses frozen request only; new body rebuild requires fresh authority and must not POST while attempt active | none |
| create path changes | create path template hash | no effect on finalized PATCH, but blocks new create for same slot until validated | active create uses frozen path only; changed path cannot start a second attempt | migration/revalidation if old attempts remain |
| update path changes | update path template hash | fail closed before PATCH unless old and new render to same remote route under explicit profile contract | no effect until finalized link PATCH | revalidate links |
| responseIdPath changes | responseIdPath hash | no effect on finalized PATCH | active create uses frozen responseIdPath; changed path cannot reinterpret old response | no migration unless ambiguous attempts need recovery |
| remote-id/provenance field pair changes | mirror tuple | link remains authoritative; mirror writeback to new tuple is blocked until policy says how to mirror | active writeback uses frozen tuple; changed tuple cannot hide the attempt | #1783 owns adoption/mirror migration |
| target object renamed/rebound with same `DestinationSlotId` | DestinationSlotId and TargetObject snapshot | fail closed until destination contract proves same remote target | active attempt uses frozen target; no second attempt | revalidation or migration/rebind |
| target object changes with new/missing `DestinationSlotId` | DestinationSlotId and TargetObject snapshot | fail closed; this is a different remote destination | active attempt uses frozen target; no second attempt and no name-derived replacement attempt | migration/rebind only |
| vendor-specific idempotency support changes | profile recovery contract | no effect on finalized links | ambiguous POST recovery cannot widen; use frozen support declaration | operator/migration policy only |
| local source deleted before first POST | source record id and #1781 authority snapshot | no finalized link yet | fail closed before POST; move to terminal/parked state for explicit policy, never create an orphan remote row | #1783 may retire/adopt only with explicit evidence |
| local source deleted after remote id is known but before mirror writeback | source record id, writeback tuple, extracted remote id | preserve link evidence only for reconciliation or retire; do not recreate local row | continue writeback only if source still exists; otherwise park/terminal-fail without another POST | #1783 owns retire/reconcile policy |
| remote 404 or remote-deleted validation/PATCH response | remote id, target object, profile recovery contract | suspend/fail closed; no automatic create replacement | no effect before finalization except recovery evidence | #1783 may retire/rebind only with explicit operator evidence |
| remote changed out-of-band | profile precondition/conflict contract and last validated remote snapshot when available | fail closed unless the profile declares a safe precondition/reconciliation contract; otherwise suspend link | active create retry cannot widen its frozen request into a conflict repair | operator or #1783 reconciliation only |
| stale remote-locator conflict | finalized remote-locator uniqueness tuple and remote id | suspend/fail closed; never silently PATCH two links to one remote target | finalization must fail the hard remote-locator uniqueness constraint | migration/backfill must resolve ambiguity before adoption |
| attempt orphaned by crash or lost worker lease | attempt id, status, lease/version, request snapshot | no direct effect on finalized links | resume only through the durable attempt state machine; ambiguous effects remain parked until reconciliation | no migration adoption without durable attempt evidence |

## Retry And Recovery Semantics

1. Retrying an active create attempt reuses the attempt's frozen request and
   association-source snapshots. It does not re-read live mapping rows or
   mutable lookup fields, rebuild the body from the current source row, or
   re-evaluate current FLS to widen authority.
2. A fresh source-record save while an active attempt exists must not create a
   second POST. It may enqueue a follow-up PATCH only after the link finalizes,
   or park/fail closed according to #1784.
3. A crash before POST leaves `pending`; retry is safe.
4. A crash after POST but before response persistence is ambiguous. Retry POST
   only when the frozen profile contract proves remote idempotency/recovery for
   the same request key. Otherwise move to `ambiguous_remote_effect`.
5. A crash after remote id persistence but before mirror writeback never retries
   POST. It resumes link finalization or compatibility mirror writeback.
6. Retry exhaustion is durable and operator-visible. It must not delete the
   link or active attempt in a way that permits a later save to POST again.
7. A delayed create attempt must re-check that the local source record still
   exists before the first POST. If the source is gone, it fails closed rather
   than creating an orphan remote row from the frozen snapshot.
8. A finalized link that receives remote-deleted, remote-404, stale-locator, or
   out-of-band conflict evidence must suspend/fail closed unless the connector
   profile has an explicit safe reconciliation contract. It must not auto-create
   a replacement remote row.
9. A new create reservation must resolve stable owner/action/destination slot
   identities before it computes `link_key`. It must not use mutable
   `ConnectorNamespace`, `FlowDeveloperName`, `FlowActionName`, `TargetObject`,
   or `ProfileName` to prove absence of prior ownership. If key-changing drift
   makes old ownership uncertain, runtime fails closed until explicit rebind,
   retire, or supersede policy resolves the old rows.

## Migration Contract For #1783

#1783 must not redefine protocol semantics. It must satisfy them.

Migration/backfill may create link rows only when it can prove the same
`link_key` and remote id unambiguously from legacy materialized fields and
provenance markers. Ambiguous cases fail closed. Examples:

- multiple candidate remote-id slots for one link key;
- provenance without a recoverable remote id;
- remote id outside the current field pair;
- changed action slot where old Flow action identity cannot be mapped;
- target/profile/path drift that changes the remote destination.

After v2 cutover, legacy fields are compatibility mirrors. They do not decide
occupancy, pending-create state, PATCH target identity, or retry behavior.

## Implementation Contract For #1784

#1784 must implement this protocol without changing its ownership boundaries:

- add the greenfield durable tables and indexes;
- enforce stable `ConnectorOwnerKey`, `ActionSlotId`, and `DestinationSlotId`
  resolution before any `link_key` or attempt lookup;
- enforce the `link_key`, finalized remote-locator uniqueness constraint, and
  one-active-attempt rule without deriving absence from mutable names;
- block new create attempts while any unsuperseded terminal attempt exists for
  the same `link_key`;
- freeze request and authority snapshots before POST;
- implement the lifecycle transitions and fail-closed gates;
- make link rows the PATCH SSoT;
- make compatibility mirror writes idempotent and non-authoritative;
- provide tests for crash/restart, ambiguous POST, writeback failure, metadata
  drift, local source deletion before delayed POST, source deletion during
  writeback, remote 404/deleted PATCH, remote out-of-band conflict, stale
  remote-locator conflict, orphaned attempt recovery, key-changing metadata
  drift, action/profile/target rename with stable slot ids, missing stable slot
  ids, and duplicate-create prevention.

#1784 must not add legacy backfill/adoption heuristics. That is #1783.

## 2026-07-22 Finalized Mapping Revalidation CARVE_DOWN

This section supersedes the earlier same-day split proposal. The split added a
second effective snapshot and a later compatibility read path; review found
that shape violated the platform's one-fact/one-SSoT/one-read-path rule.

### Scope Guard Result

- Classification: design-first durable protocol.
- Stop-loss decision: `CARVE_DOWN` before commit.
- Target invariant: `MappingFingerprint` and `BodyFingerprint` remain the only
  current PATCH-validation snapshot. An explicitly approved L0 transaction may
  advance those two fields while preserving the exact before/after state in an
  append-only receipt.
- No runtime read-path change, fallback, resave, replay, attempt release, or
  external connector write is in scope.

| Axis | Disposition | Owner |
|---|---|---|
| `schema_store` | receipt plus one current-receipt pointer | L0 |
| `migration_backfill` | generic dry-run/apply transaction only | L0 mechanism; L2 supplies the concrete approved manifest |
| `finalized_link_read_path` | unchanged | existing L0 fail-closed validator |
| `metadata_authority` | unchanged | installed pack metadata |
| `pending_create` | explicit occupancy gate; never modified | existing durable protocol |
| `post_writeback_recovery` | out of scope | existing durable protocol |
| `mirror_writeback` | out of scope | existing compatibility path |
| `echo_idempotency` | out of scope | existing connector contracts |

### Single-Snapshot Contract

`_System_ConnectorRemoteLink.MappingFingerprint` and `BodyFingerprint` are the
current validated mapping/body authority. Links that have never been
revalidated still carry their creation values. After revalidation,
`LastMappingRevalidationOperationId` names the append-only receipt that moved
the authority to its current value. History lives only in
`_System_ConnectorPushLinkRevalidationReceipt`; PATCH never reads that table.

The receipt records the immutable manifest and approval digests, fingerprint
algorithm versions, exact old/new fingerprints, old/new pack-install lineage,
and digests of the complete persisted link snapshot before and after the
transition. Those snapshot digests bind the exact transition boundary; a later
orthogonal profile revalidation may legitimately change the current full-row
digest. The next mapping transition therefore binds the then-current full-row
digest in its own manifest while proving mapping lineage through the prior
receipt pointer and exact pack/mapping/body tuple. This preserves creation and
rollback evidence without creating a second serving snapshot or coupling the
profile and mapping lifecycles.

### L0 Transaction Invariants

1. The manifest digest is recomputed from a canonical, LinkKey-sorted document;
   duplicate links, non-canonical authority strings, digest mismatch, and empty
   approval/reason fields fail before opening a transaction.
2. Every member supplies a digest over the exact finalized link id, durable
   identity, remote id, complete persisted profile/path snapshot, current
   fingerprints, and current revalidation pointer, plus explicit old/new
   fingerprints and pack lineage. L0 recomputes the full-row digest under lock;
   links are locked in stable LinkKey order.
3. For a new operation, each LinkKey is locked before its exact
   `AttemptOccupancyKey`. Any occupying attempt blocks the complete manifest;
   the transaction never clears, rebuilds, releases, or replays an attempt.
4. All member receipts and both fingerprint updates commit in one transaction.
   A zero-row CAS, mixed already-applied state, missing receipt-chain evidence,
   or any member conflict rolls back the entire manifest.
5. Retrying an operation is verify-only: every receipt and every current link
   must exactly match the requested after-state. An all-applied operation
   returns `AlreadyApplied`; a mixed or divergent operation fails closed.
6. `ValidateFinalizedLinkMappingRevalidation` executes the same locks and
   checks, returns the projected receipts, and rolls back without writes.
   `ApplyFinalizedLinkMappingRevalidation` is the only mutation entry point.

Neither entry point has access to connector callouts, Accounts, FlowAudit,
WorkItems, or attempt mutation. Consequently its external-write boundary is
zero HubSpot writes and zero L3 record writes.

### L2 Governed Migration Boundary

L0 does not decide that two mappings are semantically equivalent and does not
discover tenant members by Email, name, or remote heuristics. L2 must provide a
release-bound immutable manifest, exact old preimages or explicit semantic-owner
approval, a database backup, dry-run evidence, rollback manifest, and a bounded
cutover window. Production acceptance requires manifest/receipt DB readback,
normal API readback through the unchanged finalized-link path, and separate
evidence that no unexpected remote writes occurred. A source merge or a green
dry-run is not production remediation.

### Acceptance

- no `Effective*` mapping/body columns or alternate PATCH read path exist;
- the receipt table is blocked from generic CRUD and listed as managed system
  substrate;
- dry-run is zero-write, occupied attempts fail closed, fresh apply is atomic,
  and exact retries are readback-only;
- existing sync/async PATCH drift validation remains unchanged and fail-closed;
- no L2 manifest may run until the semantic-owner decision gate is satisfied.

## Review Checklist

- The design names a stable link key that survives Flow version activation and
  pack reapply without making old links invisible.
- The design keeps mutable namespace, Flow/action names, profile names, and
  target names out of primary ownership lookup.
- The design fails closed when key-changing metadata drift cannot prove stable
  old-row lineage before a new POST.
- The design names an attempt owner key that prevents concurrent duplicate POST
  for one link key.
- The design makes finalized remote-locator ownership a required uniqueness
  constraint, not optional guidance.
- The design blocks a new POST after retry exhaustion unless the prior terminal
  attempt is explicitly superseded.
- The design identifies `_System_ConnectorRemoteLink` as the finalized
  remote-id SSoT and `_System_ConnectorPushAttempt` as the pending-create SSoT.
- The design consumes #1781 and never reopens source-value authority.
- The design defines POST success plus local writeback failure without a second
  POST.
- The design prevents delayed create from POSTing after local source deletion.
- The design fails closed on stale finalized links whose remote target is
  deleted, changed out-of-band, or conflicts with another remote locator.
- The drift matrix covers metadata, pack install, profile, mapping, credential,
  endpoint, auth/idempotency generation, create path, update path,
  responseIdPath, remote-id field-pair drift, and local/remote record-state
  divergence.
- The design keeps runtime implementation and migration/backfill out of this
  PR.
