Skip to content

feat(sessions): static-SK write path — born static, self-migrate, no rotation (issue #175 Phase 1b)#673

Merged
philmerrell merged 1 commit into
developfrom
feature/session-metadata-static-sk-phase1b
Jul 17, 2026
Merged

feat(sessions): static-SK write path — born static, self-migrate, no rotation (issue #175 Phase 1b)#673
philmerrell merged 1 commit into
developfrom
feature/session-metadata-static-sk-phase1b

Conversation

@philmerrell

Copy link
Copy Markdown
Contributor

What & why

Phase 1b (migrate writes) of the session-metadata static-sort-key migration (issue #175). This turns on the write side: sessions stop encoding lastMessageAt in the sort key, so the row never moves and the ghost-row race that produces Failed to parse session item warnings is structurally eliminated for every migrated row. It also closes the first-turn duplicate-row race.

Follows #666 (GSI), #667 (union read), #669 (fallback fix) — all deployed to prod and healthy. Full design: docs/specs/session-metadata-static-sort-key.md.

Changes (all resolve the row via GSI — SK-scheme-agnostic)

  • ensure_session_metadata_exists — new sessions born at static S#{id} + GSI4 keys, with a real attribute_not_exists(PK) conditional put. The deterministic SK makes the guard meaningful, closing the first-turn duplicate-row race the old timestamped SK made impossible to gate.
  • update_session_activity — drops the per-turn Phase-B rotation. Static rows update in place (SET GSI4_SK re-positions the sparse recency index — no row move); a still-legacy row does its one-time final rotation to the static SK, carrying any concurrent write.
  • _store_session_metadata_cloud — static SK; migrate legacy→static on move; SET GSI4 for active, REMOVE for deleted; never un-migrates a static row.
  • session_service.delete_session — resolves the raw SK via _get_session_by_gsi instead of reconstructing S#ACTIVE#{lastMessageAt}#{id} (which misses migrated rows). Non-rotating soft-delete: SET status=deleted + REMOVE GSI4 in place, or migrate a legacy row to a static tombstone. Drops the S#DELETED# prefix (nothing reads it).

The ~10 other writers resolve-then-update-in-place on the current SK and need no change — they already work on a static SK and never rotate.

Verification

  • New TestWriteSideMigration (7): born-static, one-time migrate, no-rotation, soft-delete in-place & legacy, end-to-end create→activity→list→delete, plus the real ConditionalCheckFailedException contract (moto raises it for a failed conditional put — pinning real-AWS behavior after the 1a moto-masking lesson).
  • Updated 3 tests that encoded the old rotation contract.
  • Full tests/shared + tests/routes: 1689 passed.

Deploy ordering & expectations

  • Gate met: 1a is fully rolled out in prod and the SessionRecencyIndex GSI is ACTIVE there. 1b is safe to ship as its own release now.
  • Ships via backend.yml (backend code only).
  • Ghost warnings won't instantly hit zero. During rollout each active session does one migration rotation, which still has a small one-time-per-row race window (vs today's every-turn rotation — strictly fewer). Failed to parse session item warnings taper as sessions migrate and stop fully after Phase 2 backfill, which also deletes existing ghosts. Expected, not a regression.

Next

Phase 2 (backfill cold rows + delete ghosts + set the migration-complete marker), after 1b is deployed and rolled out.

🤖 Generated with Claude Code

…rotation (issue #175 Phase 1b)

Turns on the write side of the static-sort-key migration. Sessions stop encoding
lastMessageAt in the sort key, so the row never moves and the ghost-row race that
produced "Failed to parse session item" warnings is structurally eliminated for
every migrated row.

Changed (all resolve the row via GSI, which is SK-scheme-agnostic):
- ensure_session_metadata_exists: new sessions born at static SK S#{id} + GSI4
  keys, with a real attribute_not_exists(PK) conditional put. The deterministic SK
  makes the guard meaningful, closing the first-turn duplicate-row race the old
  timestamped SK made impossible to gate.
- update_session_activity: drops the per-turn Phase-B rotation. Static rows update
  in place (SET GSI4_SK re-positions the sparse recency index — no row move); a
  still-legacy row does its one-time final rotation to the static SK, carrying any
  concurrent write.
- _store_session_metadata_cloud: static SK; migrate legacy->static on move; SET
  GSI4 for active, REMOVE for deleted; never un-migrates a static row.
- session_service.delete_session: resolves the raw SK via _get_session_by_gsi
  instead of reconstructing S#ACTIVE#{lastMessageAt}#{id} (which misses migrated
  rows). Non-rotating soft-delete: SET status=deleted + REMOVE GSI4 in place, or
  migrate a legacy row to a static tombstone. Drops the S#DELETED# prefix (nothing
  reads it).

The ~10 other writers resolve-then-update-in-place on the current SK and need no
change — they already work on a static SK and never rotate.

Tests: TestWriteSideMigration (born-static, one-time migrate, no rotation,
soft-delete in-place/legacy, end-to-end create->activity->list->delete) plus the
real ConditionalCheckFailedException contract (moto raises it for a failed
conditional put). Updated three tests that encoded the old rotation contract. Full
shared+routes suites: 1689 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@philmerrell
philmerrell merged commit 097b981 into develop Jul 17, 2026
4 checks passed
@philmerrell
philmerrell deleted the feature/session-metadata-static-sk-phase1b branch July 17, 2026 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant