@kaeawc/auto-mobile 0.0.85 → 0.0.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +3 -3
  2. package/README.md.backup +3 -3
  3. package/dist/build-info.json +10 -10
  4. package/dist/src/index.js +1173 -840
  5. package/dist/src/utils/workers/iosBundleExtractWorker.js +3 -3
  6. package/node_modules/strtok3/lib/stream/StreamReader.js +6 -1
  7. package/node_modules/strtok3/package.json +1 -1
  8. package/package.json +21 -17
  9. package/dist/schemas/managed-slot-contract.json +0 -142
  10. package/dist/src/db/eventTables.ts +0 -8
  11. package/dist/src/db/migrations/2025_12_28_000_initial_schema.ts +0 -28
  12. package/dist/src/db/migrations/2025_12_30_000_performance_thresholds.ts +0 -85
  13. package/dist/src/db/migrations/2025_12_30_001_navigation_graph.ts +0 -223
  14. package/dist/src/db/migrations/2025_12_31_000_accessibility_baselines.ts +0 -34
  15. package/dist/src/db/migrations/2025_12_31_001_memory_audit.ts +0 -120
  16. package/dist/src/db/migrations/2026_01_01_000_recomposition_metrics.ts +0 -44
  17. package/dist/src/db/migrations/2026_01_02_000_prediction_history.ts +0 -87
  18. package/dist/src/db/migrations/2026_01_03_000_feature_flags.ts +0 -17
  19. package/dist/src/db/migrations/2026_01_03_000_test_executions.ts +0 -45
  20. package/dist/src/db/migrations/2026_01_05_000_tool_calls.ts +0 -31
  21. package/dist/src/db/migrations/2026_01_08_000_test_coverage.ts +0 -109
  22. package/dist/src/db/migrations/2026_01_09_000_video_recordings.ts +0 -66
  23. package/dist/src/db/migrations/2026_01_10_000_device_snapshots.ts +0 -54
  24. package/dist/src/db/migrations/2026_01_11_000_installed_apps.ts +0 -33
  25. package/dist/src/db/migrations/2026_01_11_000_video_recording_highlights.ts +0 -24
  26. package/dist/src/db/migrations/2026_01_13_000_installed_apps_session_tracking.ts +0 -27
  27. package/dist/src/db/migrations/2026_01_14_000_appearance_config.ts +0 -16
  28. package/dist/src/db/migrations/2026_01_25_000_navigation_screenshots.ts +0 -27
  29. package/dist/src/db/migrations/2026_01_25_001_test_run_details.ts +0 -97
  30. package/dist/src/db/migrations/2026_01_27_000_crash_anr_monitoring.ts +0 -204
  31. package/dist/src/db/migrations/2026_01_27_000_failures.ts +0 -217
  32. package/dist/src/db/migrations/2026_01_29_000_named_nodes.ts +0 -88
  33. package/dist/src/db/migrations/2026_01_30_000_performance_live_metrics.ts +0 -108
  34. package/dist/src/db/migrations/2026_03_15_000_telemetry_events.ts +0 -184
  35. package/dist/src/db/migrations/2026_03_18_000_navigation_events.ts +0 -36
  36. package/dist/src/db/migrations/2026_03_19_000_storage_events.ts +0 -37
  37. package/dist/src/db/migrations/2026_03_19_001_layout_events.ts +0 -39
  38. package/dist/src/db/migrations/2026_03_19_002_storage_events_previous_value.ts +0 -23
  39. package/dist/src/db/migrations/2026_03_19_003_layout_events_screen_name.ts +0 -23
  40. package/dist/src/db/migrations/2026_03_20_000_network_event_details.ts +0 -32
  41. package/dist/src/db/migrations/2026_04_01_000_drop_custom_events.ts +0 -41
  42. package/dist/src/db/migrations/2026_04_02_000_device_sessions.ts +0 -51
  43. package/dist/src/db/migrations/2026_07_01_000_failure_groups_signature_unique.ts +0 -162
  44. package/dist/src/db/migrations/2026_07_02_000_event_composite_indexes.ts +0 -62
  45. package/dist/src/db/migrations/2026_07_03_000_drop_redundant_device_indexes.ts +0 -66
  46. package/dist/src/db/migrations/2026_07_03_000_repair_datetime_now_defaults.ts +0 -278
  47. package/dist/src/db/migrations/2026_07_04_000_storage_events_key_lookup.ts +0 -60
  48. package/dist/src/db/migrations/2026_07_05_000_repair_updated_at_defaults.ts +0 -239
  49. package/dist/src/db/migrations/2026_07_24_000_device_locks.ts +0 -27
  50. package/dist/src/db/migrations/2026_07_27_000_session_tool_capabilities.ts +0 -17
  51. package/dist/src/db/migrations/2026_07_31_000_video_recordings_owner_session.ts +0 -39
  52. package/dist/src/db/migrations/2026_08_02_000_navigation_provenance.ts +0 -159
  53. package/dist/src/db/migrations/2026_08_17_000_emulator_loss_incidents.ts +0 -39
  54. package/dist/src/db/migrations/2026_08_22_000_provision_device_operations.ts +0 -21
  55. package/dist/src/db/migrations/2026_08_22_001_session_tool_overrides.ts +0 -106
  56. package/dist/src/db/migrations/2026_08_22_002_navigation_observation_eviction_index.ts +0 -67
  57. package/dist/src/db/migrations/2026_08_22_003_device_teardown_operations.ts +0 -19
  58. package/dist/src/db/migrations/2026_08_23_000_device_teardown_operation_owner.ts +0 -19
  59. package/dist/src/db/migrations/2026_08_23_001_default_existing_delete_device_disabled.ts +0 -26
  60. package/dist/src/db/migrations/2026_08_24_000_installed_apps_profile_type.ts +0 -15
  61. package/dist/src/db/migrations/2026_09_06_000_tool_selection_profile_provenance.ts +0 -36
  62. package/dist/src/db/migrations/2026_09_08_000_provision_device_operations_expiry.ts +0 -25
  63. package/dist/src/db/migrations/2026_09_10_000_provision_device_operation_attempt.ts +0 -25
  64. package/dist/src/db/migrations/2026_09_10_001_device_session_retention_index.ts +0 -14
  65. package/dist/src/db/migrations/2026_09_13_000_device_snapshot_vm_reclaim.ts +0 -63
  66. package/dist/src/db/migrations/2026_09_14_000_device_session_stable_identity.ts +0 -9
  67. package/dist/src/db/migrations/2026_09_14_001_device_session_identity_writer_fence.ts +0 -45
  68. package/dist/src/db/migrations/2026_09_16_000_device_session_liveness_owner.ts +0 -9
  69. package/dist/src/db/migrations/2026_09_16_001_device_session_liveness_contract.ts +0 -37
  70. package/dist/src/db/migrations/2026_09_17_000_device_session_liveness_writer_fence.ts +0 -48
  71. package/dist/src/db/migrations/2026_09_17_001_device_session_identity_fence_same_serial.ts +0 -53
  72. package/dist/src/db/migrations/2026_09_20_000_provision_device_lifecycle_outcome.ts +0 -32
  73. package/dist/src/db/migrations/2026_09_29_000_navigation_suggestion_provenance.ts +0 -41
  74. package/dist/src/db/migrations/2026_09_30_000_video_recording_display.ts +0 -17
  75. package/dist/src/db/migrations/2026_09_30_001_network_capture_fields.ts +0 -30
  76. package/dist/src/db/migrations/2026_10_05_000_device_locks_device_identity.ts +0 -28
  77. package/dist/src/db/migrations/2026_10_05_001_storage_events_key_lookup_insertion_order.ts +0 -38
  78. package/dist/src/db/migrations/2026_10_05_002_test_edge_coverage_edge_id_index.ts +0 -19
  79. package/dist/src/db/migrations/2026_10_09_000_device_resource_applications.ts +0 -22
  80. package/dist/src/db/migrations/2026_10_09_001_drop_provision_device_operations.ts +0 -32
  81. package/dist/src/db/migrations/2026_10_09_002_drop_device_teardown_operations.ts +0 -28
  82. package/dist/src/db/migrations/2026_10_12_000_rename_recording_tool_names.ts +0 -44
  83. package/dist/src/db/migrations/2026_10_12_001_video_recording_capture_teardown.ts +0 -17
  84. package/dist/src/db/migrations/2026_10_13_000_video_recording_recorder_daemon.ts +0 -17
  85. package/dist/src/index.js.map +0 -1
  86. package/dist/src/utils/workers/iosBundleExtractWorker.js.map +0 -1
  87. package/schemas/managed-slot-contract.json +0 -142
@@ -1,278 +0,0 @@
1
- import { type Kysely, sql } from "kysely";
2
-
3
- /**
4
- * #2895 — repair the string-literal timestamp-default bug on already-migrated
5
- * databases.
6
- *
7
- * Every migration column that passed a plain SQL time-expression string to
8
- * `col.defaultTo(...)` — `"datetime('now')"` or `"CURRENT_TIMESTAMP"` — bound the
9
- * argument as a VALUE, not raw SQL, so its DDL emitted `DEFAULT 'datetime(''now'')'`
10
- * / `DEFAULT 'CURRENT_TIMESTAMP'`. Any row inserted without an explicit value for
11
- * such a column stored the useless literal text instead of a timestamp.
12
- *
13
- * The DDL is fixed FORWARD (all columns now use `defaultTo(sql`(datetime('now'))`)`),
14
- * so fresh databases are already correct. But a database that already ran the
15
- * buggy migrations keeps the broken `DEFAULT` in its stored schema — editing a
16
- * historical migration does NOT re-run it (Kysely tracks migrations by name, no
17
- * content checksum). So on an upgraded DB this migration must do TWO things:
18
- *
19
- * 1. REBUILD each column whose live default is the broken string literal, so
20
- * FUTURE inserts that omit the column (e.g. `getOrCreateApp`, which omits
21
- * `created_at`) stop re-poisoning it. SQLite cannot alter a column default
22
- * in place, and bun:sqlite forbids editing `sqlite_master` directly, so we
23
- * use the standard table-rebuild: create a twin table with the corrected
24
- * default, copy the rows, drop the original, rename the twin back, recreate
25
- * its indexes/triggers, and restore its `sqlite_sequence` AUTOINCREMENT
26
- * high-water mark (so a rebuilt table cannot reuse a deleted top id). FK
27
- * enforcement is toggled OFF around the
28
- * rebuild (see `up`) so dropping a referenced parent cannot cascade-delete
29
- * its children, then `PRAGMA foreign_key_check` verifies integrity before it
30
- * is restored.
31
- * 2. REPAIR the existing poisoned rows in those columns to an evaluated
32
- * timestamp.
33
- *
34
- * TARGETING — the fix keys off `pragma_table_info(...).dflt_value`, which reports
35
- * a column's stored default. Only the two broken string-literal forms match
36
- * ({@link BROKEN_DEFAULTS}); a corrected `(datetime('now'))` default, a bare
37
- * `CURRENT_TIMESTAMP` keyword, and legitimate value defaults (`'{}'`, `'success'`)
38
- * all report a different `dflt_value` and are skipped. This is deliberately
39
- * NARROWER than "rewrite any TEXT cell equal to the literal": columns like
40
- * `storage_events.value` persist arbitrary app data (an app could legitimately
41
- * store the string `datetime('now')`), so restricting the row repair to columns
42
- * that actually carry the broken default is what keeps it from corrupting real
43
- * data.
44
- *
45
- * The evaluated `datetime('now')` written for repaired rows is the best available
46
- * timestamp for a row that never had a real one (its true creation time is
47
- * unrecoverable). It is stored in SQLite's `YYYY-MM-DD HH:MM:SS` format — the same
48
- * format the fixed column defaults now emit and the codebase's other
49
- * server-evaluated-now writes use (`sql`datetime('now')`` in ThresholdManager /
50
- * MemoryThresholdManager) — rather than the app's ISO-8601. This divergence is
51
- * deliberate and harmless: no column is both defaulted AND written explicit ISO at
52
- * an ordered/compared read site (audited during #2895 review), and every TTL
53
- * comparison wraps the value in `datetime(...)`, which normalizes both formats.
54
- *
55
- * Safe on a fresh DB and idempotent: fresh/replayed schemas carry the corrected
56
- * default, so no column matches {@link BROKEN_DEFAULTS} and the whole pass is a
57
- * no-op. A second run finds nothing left to rebuild or repair.
58
- */
59
-
60
- /**
61
- * A column's stored default (as reported by `pragma_table_info(...).dflt_value`)
62
- * mapped to the corrected default expression it should carry and the literal
63
- * value its poisoned rows hold. The `dflt_value` keys are the exact broken
64
- * string-literal forms — a correct `datetime('now')` / `CURRENT_TIMESTAMP`
65
- * default reports differently and never matches.
66
- */
67
- const BROKEN_DEFAULTS: Record<string, { corrected: string; poisonedValue: string }> = {
68
- "'datetime(''now'')'": { corrected: "(datetime('now'))", poisonedValue: "datetime('now')" },
69
- "'CURRENT_TIMESTAMP'": { corrected: "CURRENT_TIMESTAMP", poisonedValue: "CURRENT_TIMESTAMP" },
70
- };
71
-
72
- interface NamedRow {
73
- name: string;
74
- }
75
-
76
- interface ColumnInfoRow {
77
- name: string;
78
- dflt_value: string | null;
79
- }
80
-
81
- interface SqlRow {
82
- sql: string | null;
83
- }
84
-
85
- export async function up(db: Kysely<unknown>): Promise<void> {
86
- // Fast path: on a fresh/already-fixed schema no column carries a broken default,
87
- // so there is nothing to rebuild. Return WITHOUT touching PRAGMA foreign_keys so
88
- // the common case (every existing DB and test) leaves the connection's FK state
89
- // exactly as the caller set it. Only the rare upgraded-DB path below toggles it.
90
- const brokenTables = await findTablesWithBrokenDefaults(db);
91
- if (brokenTables.length === 0) {
92
- return;
93
- }
94
-
95
- // Rebuilding a table that OTHER tables reference by foreign key requires FK
96
- // enforcement OFF: `DROP TABLE` performs an implicit DELETE that would otherwise
97
- // fire `ON DELETE CASCADE` and wipe the child rows (deferral only delays the
98
- // constraint *check*, not the cascade *action*). `PRAGMA foreign_keys` is a
99
- // no-op inside a transaction, so it is toggled on the connection around the
100
- // transaction — safe because the migrator does not wrap SQLite migrations in a
101
- // transaction and the run is serialized by the migration lock. It is restored to
102
- // ON afterwards: this branch only runs on an upgraded production DB, whose
103
- // connection is configured FK-ON (`configureSqliteDatabase`), which is also the
104
- // state the rest of the migration chain expects.
105
- await sql`PRAGMA foreign_keys = OFF`.execute(db);
106
- try {
107
- // One transaction so a mid-rebuild failure cannot leave a half-swapped schema.
108
- await db.transaction().execute(async (trx) => {
109
- await rebuildAndRepair(trx, brokenTables);
110
- });
111
- } finally {
112
- await sql`PRAGMA foreign_keys = ON`.execute(db);
113
- }
114
- }
115
-
116
- /**
117
- * Return the tables that have at least one column whose stored default is a
118
- * broken string literal. Uses a `SELECT`-shaped `pragma_table_info(...)` read so
119
- * the bunSqliteDialect returns rows (a bare `PRAGMA x` value read does not).
120
- */
121
- async function findTablesWithBrokenDefaults(db: Kysely<unknown>): Promise<string[]> {
122
- const tables = await sql<NamedRow>`
123
- SELECT name FROM sqlite_master
124
- WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
125
- `.execute(db);
126
-
127
- const broken: string[] = [];
128
- for (const { name: table } of tables.rows) {
129
- // Inline the table name as a literal (sql.lit), not a bound parameter: a
130
- // parameterized pragma_table_info(?) was observed to intermittently return a
131
- // null dflt_value under parallel-file load (bun:sqlite quirk, see #2922's
132
- // test). Here a spurious null on a genuinely-poisoned column would fail the
133
- // `!== null` guard below, so the column would never be added to the broken set
134
- // and its poisoned default would be silently skipped and never rebuilt (#3612).
135
- // Matches 2026_07_05_000_repair_updated_at_defaults.ts.
136
- const columns = await sql<ColumnInfoRow>`
137
- SELECT dflt_value FROM pragma_table_info(${sql.lit(table)})
138
- `.execute(db);
139
- if (
140
- columns.rows.some(
141
- (column) => column.dflt_value !== null && column.dflt_value in BROKEN_DEFAULTS,
142
- )
143
- ) {
144
- broken.push(table);
145
- }
146
- }
147
- return broken;
148
- }
149
-
150
- async function rebuildAndRepair(trx: Kysely<unknown>, tables: string[]): Promise<void> {
151
- for (const table of tables) {
152
- // sql.lit (not a bound param) for the same bun:sqlite null-dflt_value quirk
153
- // guarded in findTablesWithBrokenDefaults above (#2922 / #3612).
154
- const columns = await sql<ColumnInfoRow>`
155
- SELECT name, dflt_value FROM pragma_table_info(${sql.lit(table)})
156
- `.execute(trx);
157
-
158
- const brokenColumns = columns.rows.filter(
159
- (column) => column.dflt_value !== null && column.dflt_value in BROKEN_DEFAULTS,
160
- );
161
- if (brokenColumns.length === 0) {
162
- continue;
163
- }
164
-
165
- await rebuildTableWithCorrectedDefaults(trx, table);
166
-
167
- // Rows copied verbatim during the rebuild still hold the literal; heal them
168
- // now, restricted to the columns that actually carried the broken default.
169
- for (const column of brokenColumns) {
170
- const { poisonedValue } = BROKEN_DEFAULTS[column.dflt_value as string];
171
- await sql`
172
- UPDATE ${sql.ref(table)}
173
- SET ${sql.ref(column.name)} = datetime('now')
174
- WHERE ${sql.ref(column.name)} = ${poisonedValue}
175
- `.execute(trx);
176
- }
177
- }
178
- }
179
-
180
- /**
181
- * Rebuild `table` so every broken string-literal default becomes its corrected
182
- * raw-SQL form, preserving data, indexes, and triggers. Only the `DEFAULT`
183
- * clauses change; column set and order are untouched, so `INSERT ... SELECT *`
184
- * lines the rows up 1:1.
185
- */
186
- async function rebuildTableWithCorrectedDefaults(
187
- trx: Kysely<unknown>,
188
- table: string,
189
- ): Promise<void> {
190
- const createRow = await sql<SqlRow>`
191
- SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ${table}
192
- `.execute(trx);
193
- const createSql = createRow.rows[0]?.sql;
194
- if (!createSql) {
195
- return;
196
- }
197
-
198
- const tempTable = `__rebuild_${table}`;
199
- let correctedSql = replaceFirstTableName(createSql, table, tempTable);
200
- for (const brokenDefault of Object.keys(BROKEN_DEFAULTS)) {
201
- // The broken literal only appears in the schema as a column default, so a
202
- // global replace cannot touch anything else.
203
- correctedSql = correctedSql.split(brokenDefault).join(BROKEN_DEFAULTS[brokenDefault].corrected);
204
- }
205
-
206
- // Capture the table's own indexes and triggers before the drop — auto-indexes
207
- // backing PRIMARY KEY / UNIQUE constraints have a NULL `sql` and are recreated
208
- // by the CREATE TABLE itself, so only the explicit ones need replaying.
209
- const auxiliary = await sql<SqlRow>`
210
- SELECT sql FROM sqlite_master
211
- WHERE tbl_name = ${table} AND type IN ('index', 'trigger') AND sql IS NOT NULL
212
- `.execute(trx);
213
-
214
- // Capture the AUTOINCREMENT high-water mark. `sqlite_sequence` records the
215
- // highest rowid EVER used so AUTOINCREMENT never reuses a deleted id; the
216
- // copy/drop/rename below would otherwise reset it to `max(current id)` and let a
217
- // deleted top id be handed out again — aliasing ids that other tables reference
218
- // as plain integers. Restored after the rename.
219
- const originalSequence = await captureSequence(trx, table);
220
-
221
- await sql.raw(correctedSql).execute(trx);
222
- await sql`INSERT INTO ${sql.ref(tempTable)} SELECT * FROM ${sql.ref(table)}`.execute(trx);
223
- await sql`DROP TABLE ${sql.ref(table)}`.execute(trx);
224
- await sql`ALTER TABLE ${sql.ref(tempTable)} RENAME TO ${sql.ref(table)}`.execute(trx);
225
- for (const { sql: auxSql } of auxiliary.rows) {
226
- if (auxSql) {
227
- await sql.raw(auxSql).execute(trx);
228
- }
229
- }
230
-
231
- if (originalSequence !== undefined) {
232
- await sql`DELETE FROM sqlite_sequence WHERE name = ${table}`.execute(trx);
233
- await sql`INSERT INTO sqlite_sequence (name, seq) VALUES (${table}, ${originalSequence})`.execute(
234
- trx,
235
- );
236
- }
237
- }
238
-
239
- /**
240
- * Read a table's `sqlite_sequence` high-water mark, or `undefined` when the table
241
- * is not AUTOINCREMENT / has never been inserted into (no sequence row) or the
242
- * `sqlite_sequence` table does not exist (no AUTOINCREMENT table in the DB).
243
- */
244
- async function captureSequence(trx: Kysely<unknown>, table: string): Promise<number | undefined> {
245
- const hasSequenceTable = await sql<NamedRow>`
246
- SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'
247
- `.execute(trx);
248
- if (hasSequenceTable.rows.length === 0) {
249
- return undefined;
250
- }
251
- const seqRow = await sql<{ seq: number }>`
252
- SELECT seq FROM sqlite_sequence WHERE name = ${table}
253
- `.execute(trx);
254
- return seqRow.rows[0]?.seq;
255
- }
256
-
257
- /**
258
- * Replace the table name in a `CREATE TABLE` statement, tolerating the double-
259
- * quoted (Kysely) and bare forms and arbitrary whitespace after the keyword.
260
- * Only the first occurrence — the table being declared — is rewritten; a later
261
- * self-reference (e.g. a table-level FK) keeps pointing at the real name so the
262
- * copy still works.
263
- */
264
- function replaceFirstTableName(createSql: string, from: string, to: string): string {
265
- // Match the declared table name as either a double-quoted identifier (Kysely's
266
- // form) or a bare word, and rewrite only when it is exactly `from` so a later
267
- // self-reference in the body is left untouched.
268
- return createSql.replace(
269
- /^(\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?)(?:"([^"]+)"|(\w+))/i,
270
- (match, prefix: string, quotedName: string | undefined, bareName: string | undefined) =>
271
- (quotedName ?? bareName) === from ? `${prefix}"${to}"` : match,
272
- );
273
- }
274
-
275
- export async function down(): Promise<void> {
276
- // Irreversible repair: the corrected defaults and healed rows carry no
277
- // information worth reverting, so the down migration is intentionally a no-op.
278
- }
@@ -1,60 +0,0 @@
1
- import type { Kysely } from "kysely";
2
-
3
- /**
4
- * Seek-reduction index for `recordStorageEvent`'s per-insert previous-value
5
- * lookup (#2798).
6
- *
7
- * On every storage telemetry insert that doesn't already carry `previousValue`,
8
- * `storageEventRepository.recordStorageEvent` runs:
9
- *
10
- * SELECT value FROM storage_events
11
- * WHERE device_id=? AND file_name=? AND key=?
12
- * ORDER BY timestamp DESC LIMIT 1
13
- *
14
- * #2798 was filed against the pre-#2788 schema, where the only usable index was
15
- * the standalone `idx_storage_events_device` and the plan showed
16
- * `USE TEMP B-TREE FOR ORDER BY`. That temp B-tree is ALREADY gone: PR #2890
17
- * (#2788) added `idx_storage_events_device_timestamp (device_id, timestamp)` and
18
- * PR #2904 (#2893) dropped the standalone device index, so the composite's
19
- * `device_id` prefix now supplies the DESC order for free.
20
- *
21
- * What remains — and what this index fixes — is the SEEK width. With only the
22
- * `(device_id, timestamp)` composite the planner seeks on `device_id=?` and then
23
- * scans every row for that device in timestamp order, applying the `file_name=?`
24
- * and `key=?` filters row-by-row until the first match. A device with many
25
- * distinct keys makes that scan arbitrarily long. `(device_id, file_name, key,
26
- * timestamp)` turns the three equality predicates into a single prefix seek that
27
- * descends straight to the target key's rows, and the trailing `timestamp` column
28
- * still yields the newest row without a sort (SQLite walks the ascending index
29
- * backward for `ORDER BY timestamp DESC`).
30
- *
31
- * NOT a covering index (per Mira Kessler's review on #2798): the query selects
32
- * `value`, which is deliberately NOT in the index — `value` is an arbitrarily
33
- * large TEXT blob, and widening the index to make the read index-only is a bad
34
- * trade. The plan is a prefix seek + one rowid lookup for `value`; the name is
35
- * `_key_lookup` (a seek-reduction index), NOT `_covering`.
36
- *
37
- * Distinct from `idx_storage_events_device_timestamp` (#2788), not a
38
- * planner-redundant near-duplicate: that composite serves the device-scoped
39
- * getter (`WHERE device_id=? ORDER BY timestamp DESC`) and cannot serve this
40
- * three-equality predicate as a prefix seek; this index cannot serve the
41
- * device-only getter's ordering as efficiently. The planner picks each for its
42
- * own shape.
43
- *
44
- * Additive and forward-only, `.ifNotExists()` / `.ifExists()` so the migration
45
- * survives #2785's destructive-recovery replay (drop-all then replay every
46
- * migration on an empty DB) in unusual orders. Sorts after
47
- * `2026_07_03_*`, so `storage_events` exists when it runs during a full replay.
48
- */
49
- export async function up(db: Kysely<unknown>): Promise<void> {
50
- await db.schema
51
- .createIndex("idx_storage_events_key_lookup")
52
- .ifNotExists()
53
- .on("storage_events")
54
- .columns(["device_id", "file_name", "key", "timestamp"])
55
- .execute();
56
- }
57
-
58
- export async function down(db: Kysely<unknown>): Promise<void> {
59
- await db.schema.dropIndex("idx_storage_events_key_lookup").ifExists().execute();
60
- }
@@ -1,239 +0,0 @@
1
- import { type Kysely, sql } from "kysely";
2
-
3
- /**
4
- * #2937 — give eight tables' `updated_at` columns their `(datetime('now'))`
5
- * default on already-upgraded databases.
6
- *
7
- * PR #2922 (issue #2913) reconciled the schema by adding
8
- * `.defaultTo(sql`(datetime('now'))`)` to these eight tables' `updated_at`
9
- * migration declarations, matching their `created_at` sibling. But editing a
10
- * historical migration body does NOT re-run it — Kysely tracks executed
11
- * migrations by name, with no content checksum — so the added default lands only
12
- * on FRESH/replayed databases. A database that already ran the pre-#2922
13
- * migrations keeps the old `updated_at TEXT NOT NULL` column with no default,
14
- * and a defaulted insert (one that omits `updated_at`) fails its NOT NULL
15
- * constraint there.
16
- *
17
- * This mirrors the `created_at` situation before #2915, minus the live bug:
18
- * `created_at` had a broken string-literal default actively poisoning new rows,
19
- * which the {@link file://./2026_07_03_000_repair_datetime_now_defaults.ts}
20
- * rebuild repaired. `updated_at` has no such poison — every current writer
21
- * supplies it explicitly — so this migration only needs to REBUILD each column's
22
- * default; the existing rows already hold real, explicitly-written timestamps and
23
- * are copied through untouched (no row repair).
24
- *
25
- * REBUILD MECHANICS — identical to the #2915 repair: SQLite cannot alter a column
26
- * default in place, and bun:sqlite forbids editing `sqlite_master` directly, so
27
- * each table is rebuilt via a twin: create a twin whose `updated_at` carries the
28
- * corrected default, copy the rows with `INSERT ... SELECT *`, drop the original,
29
- * rename the twin back, replay the table's explicit indexes/triggers, and restore
30
- * its `sqlite_sequence` AUTOINCREMENT high-water mark (so a rebuilt table cannot
31
- * reuse a deleted top id). FK enforcement is toggled OFF around the transaction so
32
- * dropping a referenced parent (e.g. `navigation_apps`) cannot cascade-delete its
33
- * children.
34
- *
35
- * TARGETING — the eight tables are enumerated in {@link TABLES_WITH_DEFAULTED_UPDATED_AT}
36
- * (exactly the set #2922 edited), and each is rebuilt only when its live
37
- * `updated_at` default is absent. Detection reads
38
- * `pragma_table_info(...).dflt_value`: `NULL` means the no-default upgraded column
39
- * (rebuild it); the corrected default reports as `datetime('now')` and is skipped.
40
- * `device_sessions` / `failure_groups` are deliberately NOT listed — their
41
- * `updated_at` default has existed on every database since #2915 repaired the
42
- * broken literal they originally shipped, so they need no rebuild.
43
- *
44
- * Safe on a fresh DB and idempotent: a fresh/replayed or already-rebuilt schema
45
- * carries the default, so nothing matches and the whole pass is a no-op that never
46
- * touches FK state. A second run finds every column already defaulted.
47
- */
48
-
49
- /**
50
- * The eight tables whose `updated_at` default was added by PR #2922 (a migration
51
- * edit that does not replay), so an upgraded DB's copy still lacks it. Ordered
52
- * arbitrarily; each is independently rebuilt.
53
- */
54
- const TABLES_WITH_DEFAULTED_UPDATED_AT = [
55
- "device_configs",
56
- "navigation_apps",
57
- "prediction_transition_stats",
58
- "accessibility_baselines",
59
- "feature_flags",
60
- "video_recording_configs",
61
- "device_snapshot_configs",
62
- "appearance_configs",
63
- ] as const;
64
-
65
- /** The corrected default expression `updated_at` should carry, as raw SQL. */
66
- const CORRECTED_DEFAULT = "(datetime('now'))";
67
-
68
- interface ColumnInfoRow {
69
- name: string;
70
- dflt_value: string | null;
71
- }
72
-
73
- interface SqlRow {
74
- sql: string | null;
75
- }
76
-
77
- export async function up(db: Kysely<unknown>): Promise<void> {
78
- // Fast path: only the tables whose `updated_at` column exists AND has no
79
- // default need a rebuild. On a fresh/already-fixed schema this is empty, so we
80
- // return WITHOUT touching PRAGMA foreign_keys — leaving the connection's FK
81
- // state exactly as the caller set it, as the common case (every existing DB and
82
- // test) requires. Only the rare upgraded-DB path below toggles it.
83
- const tablesToRebuild: string[] = [];
84
- for (const table of TABLES_WITH_DEFAULTED_UPDATED_AT) {
85
- if (await updatedAtLacksDefault(db, table)) {
86
- tablesToRebuild.push(table);
87
- }
88
- }
89
- if (tablesToRebuild.length === 0) {
90
- return;
91
- }
92
-
93
- // Rebuilding a table that OTHER tables reference by foreign key requires FK
94
- // enforcement OFF: `DROP TABLE` performs an implicit DELETE that would otherwise
95
- // fire `ON DELETE CASCADE` and wipe the child rows (deferral only delays the
96
- // constraint *check*, not the cascade *action*). `PRAGMA foreign_keys` is a
97
- // no-op inside a transaction, so it is toggled on the connection around the
98
- // transaction — safe because the migrator does not wrap SQLite migrations in a
99
- // transaction and the run is serialized by the migration lock. Restored to ON
100
- // afterwards, the state `configureSqliteDatabase` and the rest of the chain
101
- // expect on the production connection.
102
- await sql`PRAGMA foreign_keys = OFF`.execute(db);
103
- try {
104
- // One transaction so a mid-rebuild failure cannot leave a half-swapped schema.
105
- await db.transaction().execute(async (trx) => {
106
- for (const table of tablesToRebuild) {
107
- await rebuildTableWithUpdatedAtDefault(trx, table);
108
- }
109
- });
110
- } finally {
111
- await sql`PRAGMA foreign_keys = ON`.execute(db);
112
- }
113
- }
114
-
115
- /**
116
- * True when `table` exists and its `updated_at` column is present with NO stored
117
- * default (the pre-#2922 upgraded-DB shape). A corrected `(datetime('now'))`
118
- * default reports its `dflt_value` as `datetime('now')` (non-null) and is skipped;
119
- * a missing table or column yields no matching row and is also skipped.
120
- *
121
- * The table name is inlined as a literal (`sql.lit`) rather than a bound
122
- * parameter: a parameterized `pragma_table_info(?)` read was observed to
123
- * intermittently return a null `dflt_value` under parallel-file load (a bun:sqlite
124
- * quirk, see #2922's test), which here — where null is the very signal that
125
- * triggers a rebuild — would spuriously rebuild a fresh table.
126
- */
127
- async function updatedAtLacksDefault(db: Kysely<unknown>, table: string): Promise<boolean> {
128
- const columns = await sql<ColumnInfoRow>`
129
- SELECT name, dflt_value FROM pragma_table_info(${sql.lit(table)})
130
- `.execute(db);
131
- const updatedAt = columns.rows.find((column) => column.name === "updated_at");
132
- return updatedAt !== undefined && updatedAt.dflt_value === null;
133
- }
134
-
135
- /**
136
- * Rebuild `table` so its `updated_at` column gains the `(datetime('now'))`
137
- * default, preserving data, indexes, and triggers. Only the `updated_at` column's
138
- * `DEFAULT` clause changes; the column set and order are untouched, so
139
- * `INSERT ... SELECT *` lines the rows up 1:1.
140
- */
141
- async function rebuildTableWithUpdatedAtDefault(
142
- trx: Kysely<unknown>,
143
- table: string,
144
- ): Promise<void> {
145
- const createRow = await sql<SqlRow>`
146
- SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ${table}
147
- `.execute(trx);
148
- const createSql = createRow.rows[0]?.sql;
149
- if (!createSql) {
150
- return;
151
- }
152
-
153
- const tempTable = `__rebuild_${table}`;
154
- const correctedSql = addUpdatedAtDefault(replaceFirstTableName(createSql, table, tempTable));
155
-
156
- // Capture the table's own indexes and triggers before the drop — auto-indexes
157
- // backing PRIMARY KEY / UNIQUE constraints have a NULL `sql` and are recreated
158
- // by the CREATE TABLE itself, so only the explicit ones need replaying.
159
- const auxiliary = await sql<SqlRow>`
160
- SELECT sql FROM sqlite_master
161
- WHERE tbl_name = ${table} AND type IN ('index', 'trigger') AND sql IS NOT NULL
162
- `.execute(trx);
163
-
164
- // Capture the AUTOINCREMENT high-water mark. `sqlite_sequence` records the
165
- // highest rowid EVER used so AUTOINCREMENT never reuses a deleted id; the
166
- // copy/drop/rename below would otherwise reset it to `max(current id)` and let a
167
- // deleted top id be handed out again. Restored after the rename.
168
- const originalSequence = await captureSequence(trx, table);
169
-
170
- await sql.raw(correctedSql).execute(trx);
171
- await sql`INSERT INTO ${sql.ref(tempTable)} SELECT * FROM ${sql.ref(table)}`.execute(trx);
172
- await sql`DROP TABLE ${sql.ref(table)}`.execute(trx);
173
- await sql`ALTER TABLE ${sql.ref(tempTable)} RENAME TO ${sql.ref(table)}`.execute(trx);
174
- for (const { sql: auxSql } of auxiliary.rows) {
175
- if (auxSql) {
176
- await sql.raw(auxSql).execute(trx);
177
- }
178
- }
179
-
180
- if (originalSequence !== undefined) {
181
- await sql`DELETE FROM sqlite_sequence WHERE name = ${table}`.execute(trx);
182
- await sql`INSERT INTO sqlite_sequence (name, seq) VALUES (${table}, ${originalSequence})`.execute(
183
- trx,
184
- );
185
- }
186
- }
187
-
188
- /**
189
- * Read a table's `sqlite_sequence` high-water mark, or `undefined` when the table
190
- * is not AUTOINCREMENT / has never been inserted into (no sequence row) or the
191
- * `sqlite_sequence` table does not exist (no AUTOINCREMENT table in the DB).
192
- */
193
- async function captureSequence(trx: Kysely<unknown>, table: string): Promise<number | undefined> {
194
- const hasSequenceTable = await sql<{ name: string }>`
195
- SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'
196
- `.execute(trx);
197
- if (hasSequenceTable.rows.length === 0) {
198
- return undefined;
199
- }
200
- const seqRow = await sql<{ seq: number }>`
201
- SELECT seq FROM sqlite_sequence WHERE name = ${table}
202
- `.execute(trx);
203
- return seqRow.rows[0]?.seq;
204
- }
205
-
206
- /**
207
- * Inject `default (datetime('now'))` into the `updated_at` column definition of a
208
- * `CREATE TABLE` statement, right after its type keyword. Matches only when the
209
- * column has no existing default (the type is immediately followed by `not null`),
210
- * so it is a no-op on a schema that already carries the default and never
211
- * double-adds. Only the first (and only) `"updated_at"` column definition is
212
- * rewritten.
213
- */
214
- function addUpdatedAtDefault(createSql: string): string {
215
- return createSql.replace(
216
- /("updated_at"\s+\w+)(\s+not\s+null)/i,
217
- `$1 default ${CORRECTED_DEFAULT}$2`,
218
- );
219
- }
220
-
221
- /**
222
- * Replace the table name in a `CREATE TABLE` statement, tolerating the double-
223
- * quoted (Kysely) and bare forms and arbitrary whitespace after the keyword.
224
- * Only the first occurrence — the table being declared — is rewritten; a later
225
- * self-reference (e.g. a table-level FK) keeps pointing at the real name so the
226
- * copy still works.
227
- */
228
- function replaceFirstTableName(createSql: string, from: string, to: string): string {
229
- return createSql.replace(
230
- /^(\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?)(?:"([^"]+)"|(\w+))/i,
231
- (match, prefix: string, quotedName: string | undefined, bareName: string | undefined) =>
232
- (quotedName ?? bareName) === from ? `${prefix}"${to}"` : match,
233
- );
234
- }
235
-
236
- export async function down(): Promise<void> {
237
- // Irreversible repair: the corrected default carries no information worth
238
- // reverting, so the down migration is intentionally a no-op.
239
- }
@@ -1,27 +0,0 @@
1
- import { type Kysely, sql } from "kysely";
2
-
3
- /**
4
- * Remember how to unlock a device, keyed by device (issue #4360).
5
- *
6
- * A dedicated table rather than columns on `device_sessions`: a session row only
7
- * exists when device-pool autolock is enabled, and never during boot (which runs
8
- * before any session), so session-scoped storage cannot deliver
9
- * remember-then-reuse in the default config or at boot. `lock_credential` is
10
- * stored as-is in the local `~/.auto-mobile` DB — a single-user, on-disk
11
- * automation store — so a locked dev device does not have to be unlocked by hand
12
- * every session.
13
- */
14
- export async function up(db: Kysely<unknown>): Promise<void> {
15
- await db.schema
16
- .createTable("device_locks")
17
- .ifNotExists()
18
- .addColumn("device_id", "text", (col) => col.primaryKey())
19
- .addColumn("lock_type", "text", (col) => col.notNull())
20
- .addColumn("lock_credential", "text")
21
- .addColumn("updated_at", "text", (col) => col.notNull().defaultTo(sql`(datetime('now'))`))
22
- .execute();
23
- }
24
-
25
- export async function down(db: Kysely<unknown>): Promise<void> {
26
- await db.schema.dropTable("device_locks").execute();
27
- }
@@ -1,17 +0,0 @@
1
- import { type Kysely, sql } from "kysely";
2
-
3
- export async function up(db: Kysely<unknown>): Promise<void> {
4
- await db.schema
5
- .createTable("session_tool_capabilities")
6
- .ifNotExists()
7
- .addColumn("session_uuid", "text", (col) => col.notNull())
8
- .addColumn("capability", "text", (col) => col.notNull())
9
- .addColumn("enabled", "integer", (col) => col.notNull())
10
- .addColumn("updated_at", "text", (col) => col.notNull().defaultTo(sql`(datetime('now'))`))
11
- .addPrimaryKeyConstraint("session_tool_capabilities_pk", ["session_uuid", "capability"])
12
- .execute();
13
- }
14
-
15
- export async function down(db: Kysely<unknown>): Promise<void> {
16
- await db.schema.dropTable("session_tool_capabilities").ifExists().execute();
17
- }
@@ -1,39 +0,0 @@
1
- import { sql, type Kysely } from "kysely";
2
-
3
- /**
4
- * Session-scope the video recording archive (issue #4752).
5
- *
6
- * Adds a nullable `owner_session_uuid` column recording the daemon session that
7
- * started a recording. The column is deliberately nullable: rows that predate
8
- * this migration have no known owner and keep a NULL owner. The read policy
9
- * (see `VideoRecordingRepository`) treats a NULL-owner row as legacy/unowned and
10
- * readable by any session, while a row with a non-null owner is only returned to
11
- * that owner — so this migration hardens new recordings without orphaning the
12
- * existing archive.
13
- */
14
- export async function up(db: Kysely<unknown>): Promise<void> {
15
- const existingColumn = await sql<{ name: string }>`
16
- SELECT name FROM pragma_table_info('video_recordings')
17
- WHERE name = 'owner_session_uuid'
18
- `.execute(db);
19
-
20
- if (existingColumn.rows.length === 0) {
21
- await db.schema
22
- .alterTable("video_recordings")
23
- .addColumn("owner_session_uuid", "text")
24
- .execute();
25
- }
26
-
27
- await db.schema
28
- .createIndex("idx_video_recordings_owner_session")
29
- .ifNotExists()
30
- .on("video_recordings")
31
- .column("owner_session_uuid")
32
- .execute();
33
- }
34
-
35
- export async function down(db: Kysely<unknown>): Promise<void> {
36
- await db.schema.dropIndex("idx_video_recordings_owner_session").execute();
37
-
38
- await db.schema.alterTable("video_recordings").dropColumn("owner_session_uuid").execute();
39
- }