feat(sessions): static-SK write path — born static, self-migrate, no rotation (issue #175 Phase 1b)#673
Merged
Conversation
…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>
This was referenced Jul 17, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
lastMessageAtin the sort key, so the row never moves and the ghost-row race that producesFailed to parse session itemwarnings 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 staticS#{id}+ GSI4 keys, with a realattribute_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_SKre-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;SETGSI4 for active,REMOVEfor deleted; never un-migrates a static row.session_service.delete_session— resolves the raw SK via_get_session_by_gsiinstead of reconstructingS#ACTIVE#{lastMessageAt}#{id}(which misses migrated rows). Non-rotating soft-delete:SET status=deleted+REMOVE GSI4in place, or migrate a legacy row to a static tombstone. Drops theS#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
TestWriteSideMigration(7): born-static, one-time migrate, no-rotation, soft-delete in-place & legacy, end-to-end create→activity→list→delete, plus the realConditionalCheckFailedExceptioncontract (moto raises it for a failed conditional put — pinning real-AWS behavior after the 1a moto-masking lesson).tests/shared+tests/routes: 1689 passed.Deploy ordering & expectations
SessionRecencyIndexGSI is ACTIVE there. 1b is safe to ship as its own release now.backend.yml(backend code only).Failed to parse session itemwarnings 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