You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A local append-only log of semantic removals that no table row can express on its own, so the server applies the same durable state transition. Most begin with a Rider intent; some are automatic, such as clearing a Board Warning after a clean detector evaluation.
A row that has been deleted cannot carry a Change Timestamp saying so. Sync Actions are that missing signal. The log is typed — delete is the only type today — and named for what it is so a later intent does not need a second mechanism. The server side is already built in KacperKozak/vescape-server#12; this is the phone's half.
Two shapes, and the difference matters:
Tombstoned entities. A Board syncs its deleted_at as an ordinary upsert and emits a Sync Action. The upsert stays a dumb upsert; the cascade over the Board's configuration stays an explicit, replay-safe action. An upsert that quietly deletes rows in four other tables is exactly the kind of hidden side effect that bites later.
Removed entities. Alert Rules, Tune Profiles, Privacy Zones, Favorites, app settings, board settings and board warnings leave no row behind, so the action is the only signal there is. Deleting a Favorite emits one Favorite action; its favorite_media manifest rows are a raw parent-covered cascade, matching the future server cascade.
The boundary is semantic removal, not a particular public function. If a future restore must preserve a row's absence, that removal emits a Sync Action. This covers explicit deletes, reset-to-default storage, replacement flows that remove old rows, and automatic current-state clears.
Maintenance has explicit no-action paths: cursor-gated retention, schema migrations, parent-covered cascades, and the confirmed Account reset. A DeleteTarget enum has cases only for configuration/current-state kinds, so retention cannot name a telemetry frame or minute bucket. No database triggers — intent cannot be inferred from SQL alone.
Action insertion and the corresponding delete or tombstone happen in one local database transaction. The log is transport state rather than durable truth. Its cursor is its own AUTOINCREMENT key; after upload the accepted action cursor commits first and pruning happens afterwards. Any crash therefore fails toward re-sending.
Likely files
modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryEntities.kt - the new entity and the target enum
modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryDao.kt - transactional semantic-delete APIs, raw cascade/maintenance deletes, replacement paths and silent retention
modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryDatabase.kt - Room version and migration
modules/vescape-core/ios/telemetry/TelemetryDatabase.swift, TelemetryDao.swift, AppDataRepository.swift, TuneProfileStore.swift and warnings/BoardWarningStore.swift - the GRDB peers and their distributed delete paths
modules/vescape-core/src/index.ts - the TS mirror of the target enum, which needs @parity lines to both platforms
docs/adr/0027-boards-are-tombstoned-never-deleted.md - why a Board delete is both a column and an action
Implementation hints
Start with an inventory of every DELETE touching a syncable table on each platform and classify it explicitly:
Semantic:deleteAlertRule, deleteTuneProfileSafe, deletePrivacyZone, deleteAppSetting, deleteBoardSetting, deleteBoardWarning; the deleted-key side of Board edits; reset-to-default and legalPolicy = null; Board Warning clean-evaluation; preset-rule regeneration. Each removed row gets an action.
Parent-covered cascade: Board configuration removed by deleteBoardWithSettings, and Tune History removed with a Tune Profile. Raw deletes, covered by one parent action.
Maintenance: cursor-gated retention, migrations and confirmed Account reset. Raw deletes, no actions.
Corrupt-setting cleanup must also be classified rather than inheriting behavior accidentally. Default: semantic deletion, so restore cannot resurrect the corrupt stored value; changing that requires an explicit documented exception.
Keep raw delete primitives private to the transaction-owning repository/store. Expose intent-specific operations rather than letting callers choose whether to append an action:
deleteForSync(target, identity) -> read + action + delete in one transaction
replaceForSync(...) -> actions for removed rows + replacement in one transaction
deleteCoveredByParent(...) -> raw delete inside parent transaction
deleteForMaintenance(...) -> raw delete, no action
A deletion is stamped from the row being removed as max(now, row.updated_at); Board Warnings use last_detected_at. A plain now on a rewound clock produces an action the server treats as a no-op and cannot self-heal because its row is gone. A Board tombstone uses the newly ratcheted Board timestamp for both the row and action.
The server applies actions after every upsert in a batch, so a batch that deletes and re-creates the same identifier has already written the newer row. The action still has its own cursor position and travels in dependency order.
The target enum is a TS-to-native mirror, so it carries two @parity lines per AGENTS.md, and the native nodes carry back-pointers to it.
Acceptance criteria
A local Sync Action log exists on both platforms, append-only, keyed by AUTOINCREMENT
Every semantic removal appends its action and deletes/tombstones the row in the same transaction
Explicit deletes of Alert Rules, Tune Profiles, Privacy Zones, Favorites, App Settings, Board Settings and Board Warnings emit one action per removed row
Deleting a Favorite emits one Favorite action; its Favorite Media manifest rows emit none because the parent action covers them
Parent
What to build
A local append-only log of semantic removals that no table row can express on its own, so the server applies the same durable state transition. Most begin with a Rider intent; some are automatic, such as clearing a Board Warning after a clean detector evaluation.
A row that has been deleted cannot carry a Change Timestamp saying so. Sync Actions are that missing signal. The log is typed —
deleteis the only type today — and named for what it is so a later intent does not need a second mechanism. The server side is already built inKacperKozak/vescape-server#12; this is the phone's half.Two shapes, and the difference matters:
deleted_atas an ordinary upsert and emits a Sync Action. The upsert stays a dumb upsert; the cascade over the Board's configuration stays an explicit, replay-safe action. An upsert that quietly deletes rows in four other tables is exactly the kind of hidden side effect that bites later.favorite_mediamanifest rows are a raw parent-covered cascade, matching the future server cascade.The boundary is semantic removal, not a particular public function. If a future restore must preserve a row's absence, that removal emits a Sync Action. This covers explicit deletes, reset-to-default storage, replacement flows that remove old rows, and automatic current-state clears.
Maintenance has explicit no-action paths: cursor-gated retention, schema migrations, parent-covered cascades, and the confirmed Account reset. A
DeleteTargetenum has cases only for configuration/current-state kinds, so retention cannot name a telemetry frame or minute bucket. No database triggers — intent cannot be inferred from SQL alone.Action insertion and the corresponding delete or tombstone happen in one local database transaction. The log is transport state rather than durable truth. Its cursor is its own
AUTOINCREMENTkey; after upload the accepted action cursor commits first and pruning happens afterwards. Any crash therefore fails toward re-sending.Likely files
modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryEntities.kt- the new entity and the target enummodules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryDao.kt- transactional semantic-delete APIs, raw cascade/maintenance deletes, replacement paths and silent retentionmodules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryDatabase.kt- Room version and migrationmodules/vescape-core/ios/telemetry/TelemetryDatabase.swift,TelemetryDao.swift,AppDataRepository.swift,TuneProfileStore.swiftandwarnings/BoardWarningStore.swift- the GRDB peers and their distributed delete pathsmodules/vescape-core/src/index.ts- the TS mirror of the target enum, which needs@paritylines to both platformsdocs/adr/0027-boards-are-tombstoned-never-deleted.md- why a Board delete is both a column and an actionImplementation hints
Start with an inventory of every
DELETEtouching a syncable table on each platform and classify it explicitly:deleteAlertRule,deleteTuneProfileSafe,deletePrivacyZone,deleteAppSetting,deleteBoardSetting,deleteBoardWarning; the deleted-key side of Board edits; reset-to-default andlegalPolicy = null; Board Warning clean-evaluation; preset-rule regeneration. Each removed row gets an action.deleteBoardWithSettings, and Tune History removed with a Tune Profile. Raw deletes, covered by one parent action.Corrupt-setting cleanup must also be classified rather than inheriting behavior accidentally. Default: semantic deletion, so restore cannot resurrect the corrupt stored value; changing that requires an explicit documented exception.
Keep raw delete primitives private to the transaction-owning repository/store. Expose intent-specific operations rather than letting callers choose whether to append an action:
A deletion is stamped from the row being removed as
max(now, row.updated_at); Board Warnings uselast_detected_at. A plainnowon a rewound clock produces an action the server treats as a no-op and cannot self-heal because its row is gone. A Board tombstone uses the newly ratcheted Board timestamp for both the row and action.The server applies actions after every upsert in a batch, so a batch that deletes and re-creates the same identifier has already written the newer row. The action still has its own cursor position and travels in dependency order.
The target enum is a TS-to-native mirror, so it carries two
@paritylines perAGENTS.md, and the native nodes carry back-pointers to it.Acceptance criteria
AUTOINCREMENTlegalPolicy = null, preset-rule regeneration and automatic Board Warning clears emit actionsDeleteTarget, mirroring the server's structural testmax(now, row.updated_at); Board Warnings uselast_detected_at; Board tombstone row and action share the new ratcheted timestamp@paritylines to both platformsBlocked by
Related