# Connector Inbound — pre-R22 rows migration runbook

Before PR #1591 introduced the typed `QueueEnvelope` wire format and immutable
`RouteVersion` snapshots, the `_System_ConnectorEvent` queue used a
hidden-key payload shape (`__objectKind` / `__sourceID` injected into the
raw user payload) and the dispatcher did not snapshot a route version onto
the queue row. After this PR ships, the worker requires:

1. Payload bytes that decode as `QueueEnvelope` (typed `ObjectKind`,
   `SourceID`, `Payload`).
2. `RouteVersion >= 1` on the queue row (immutable snapshot from dispatcher
   resolve).

Any row that violates either requirement is `failTerminal` at worker
processing time — the worker does NOT carry permanent legacy-shape decode
or live-route fallback paths. This is a CLAUDE.md "no historical cleanup
in runtime" requirement; the runtime fallback approach in R23 was rejected
by codex on those grounds, and R24 replaces it with this explicit runbook.

## When this runbook applies

- The cluster ran a pre-`#1591` build that registered an
  `EnvelopeParserHandler` AND received inbound webhook deliveries that
  produced rows in `_System_ConnectorEvent`.
- At `#1591` merge time, no production `EnvelopeParser` registrations exist
  (verified: `grep -rn 'RegisterEnvelopeParser' backend/ --include='*.go'`
  outside test files returns zero non-registry hits). No production legacy
  rows are expected; this runbook is largely defensive.

## Identifying affected rows

Connect to the runtime DB and run:

```sql
SELECT Id, Namespace, EventKind, IdempotencyKey, Status, CreatedDate
  FROM _System_ConnectorEvent
 WHERE COALESCE(IsDeleted, 0) = 0
   AND Status IN ('pending', 'processing', 'failed')
   AND (
        RouteVersion IS NULL
     OR RouteVersion = 0
     OR JSON_EXTRACT(Payload, '$.objectKind') IS NULL
   );
```

(`JSON_EXTRACT($.objectKind)` returns NULL for legacy raw-payload rows;
new `QueueEnvelope` rows have a top-level `objectKind` field.)

## Resolution paths

Choose ONE per identified row:

### Option A — mark terminal (default for greenfield)

If the row's business intent is no longer relevant (typical at greenfield
upgrade), mark it terminal so the worker never tries to process it:

```sql
UPDATE _System_ConnectorEvent
   SET Status = 'failed',
       LastError = 'pre-R22 schema; marked terminal by upgrade runbook',
       LastModifiedDate = NOW()
 WHERE Id IN (...);  -- IDs from the identification query
```

### Option B — backfill into typed envelope + concrete RouteVersion

Only if the row's business intent must survive. Per-row admin script:

1. Parse the legacy payload bytes; extract `__objectKind`, `__sourceID`.
2. Resolve the route via `ResolveFlow(ctx, namespace, InstalledPackId, EventKind)`
   to obtain the current `(version, flowName)` for that triple.
3. Re-encode the user payload (legacy payload minus `__objectKind`/`__sourceID`)
   into a `QueueEnvelope` via `EncodeQueueEnvelope(objectKind, sourceID, payloadJSON)`.
4. `UPDATE _System_ConnectorEvent SET Payload = ?, RouteVersion = ? WHERE Id = ?`.

A helper CLI for option B may be added in a follow-up PR if real legacy
rows are ever encountered. None are expected at `#1591` merge time.

## Verification after migration

Re-run the identification query; expect zero rows. The worker can then
drain the queue safely under the typed-envelope contract.
