What happened
Updating one scheduled task reads and rewrites every scheduled task and pending fire claim. Binding an execution, advancing a native fire state, and settling a fire use the same path. Even a due scan with no changes deletes and reinserts both tables.
At main 492ff80f0f4abf3a917ce8f2c41b32fa9e3fc60c, SqliteScheduledTaskStore.mutate() loads the complete state before write() deletes both tables and reinserts every record. get(taskId) also reads both tables before finding its target.
A local reproduction using the real owner lease, store API and SQLite connection produced the following counts. Each unrelated task also has one pending fire claim; measurements exclude fixture creation.
| Unrelated tasks / claims |
JSON records returned by get(target) |
Rows changed by snooze(target) |
Rows changed by an unchanged claimNextDue() |
| 0 / 0 |
1 |
2 |
2 |
| 32 / 32 |
65 |
130 |
130 |
| 256 / 256 |
513 |
1,026 |
1,026 |
Returned JSON records measure materialization, not all internal SQLite row visits. Changed rows are measured with total_changes() on the actual store connection and include deletes plus inserts. These are work counts, not latency or WAL-volume measurements.
Expected: point operations locate the affected task/claim through existing keys and update only relevant rows. Settlement updates the current task and removes its claim in one transaction. A due scan with no state changes performs no DML.
How to reproduce
- Build source at
492ff80f0f4abf3a917ce8f2c41b32fa9e3fc60c.
- Resolve a fresh interactive storage root, acquire its owner, and open
openInteractiveScheduledTaskStoreForWrite(owner.lease).
- Through
store.create(), create a target task and 0, 32, or 256 unrelated tasks scheduled for the future. Use store.claimNow() to create one pending claim for each unrelated task.
- Acquire the observation lease through
runWithStorageRootLease() using its canonical root path, so observation shares the store's actual connection.
- After setup, measure
get(target.id), snooze(target.id, 1000, now), and claimNextDue(now). Keep now fixed before the target is due and before any task expires. Record returned JSON rows and the connection's total_changes() difference around each operation.
The unchanged poll returns { claim: null, expired: [] } but still produces the writes shown above. Inspecting the executed statements confirms the unqualified deletes and full reinserts.
Environment
- Source commit reproduced:
492ff80f0f4abf3a917ce8f2c41b32fa9e3fc60c.
- Surface: Runtime Host / SQLite scheduled-task storage, exercised through its owner-protected store API.
- Reproduction environment: macOS 26.6.2; results confirmed with Node v22.23.1 and v24.18.0; npm 11.19.0 for builds.
- No external provider or timing-dependent sleep is required. Business fixtures are created through the store API.
Logs, screenshots, or additional context
This tracks the scheduled-task whole-collection rewrite item from Discussion #4876.
Source evidence: point get, full-state read and rewrite, and existing primary/unique keys.
Proposed scope: keyed task/claim reads and transactional row operations for point mutations; due discovery writes only newly expired tasks and at most one new claim. Preserve claim uniqueness, execution identity, expiry ordering, native-delivery state and restart recovery.
Full bounded-read treatment of due discovery, catalog listing, and the Host's schedule/residency refresh remains separate work. The Host's create limit is not a storage-layer bound. This issue does not claim to close all read-path items in #4876, and does not require a schema or protocol change.
Prepared with Codex; submitted with contributor approval.
What happened
Updating one scheduled task reads and rewrites every scheduled task and pending fire claim. Binding an execution, advancing a native fire state, and settling a fire use the same path. Even a due scan with no changes deletes and reinserts both tables.
At main
492ff80f0f4abf3a917ce8f2c41b32fa9e3fc60c,SqliteScheduledTaskStore.mutate()loads the complete state beforewrite()deletes both tables and reinserts every record.get(taskId)also reads both tables before finding its target.A local reproduction using the real owner lease, store API and SQLite connection produced the following counts. Each unrelated task also has one pending fire claim; measurements exclude fixture creation.
get(target)snooze(target)claimNextDue()Returned JSON records measure materialization, not all internal SQLite row visits. Changed rows are measured with
total_changes()on the actual store connection and include deletes plus inserts. These are work counts, not latency or WAL-volume measurements.Expected: point operations locate the affected task/claim through existing keys and update only relevant rows. Settlement updates the current task and removes its claim in one transaction. A due scan with no state changes performs no DML.
How to reproduce
492ff80f0f4abf3a917ce8f2c41b32fa9e3fc60c.openInteractiveScheduledTaskStoreForWrite(owner.lease).store.create(), create a target task and 0, 32, or 256 unrelated tasks scheduled for the future. Usestore.claimNow()to create one pending claim for each unrelated task.runWithStorageRootLease()using its canonical root path, so observation shares the store's actual connection.get(target.id),snooze(target.id, 1000, now), andclaimNextDue(now). Keepnowfixed before the target is due and before any task expires. Record returned JSON rows and the connection'stotal_changes()difference around each operation.The unchanged poll returns
{ claim: null, expired: [] }but still produces the writes shown above. Inspecting the executed statements confirms the unqualified deletes and full reinserts.Environment
492ff80f0f4abf3a917ce8f2c41b32fa9e3fc60c.Logs, screenshots, or additional context
This tracks the scheduled-task whole-collection rewrite item from Discussion #4876.
Source evidence: point get, full-state read and rewrite, and existing primary/unique keys.
Proposed scope: keyed task/claim reads and transactional row operations for point mutations; due discovery writes only newly expired tasks and at most one new claim. Preserve claim uniqueness, execution identity, expiry ordering, native-delivery state and restart recovery.
Full bounded-read treatment of due discovery, catalog listing, and the Host's schedule/residency refresh remains separate work. The Host's create limit is not a storage-layer bound. This issue does not claim to close all read-path items in #4876, and does not require a schema or protocol change.
Prepared with Codex; submitted with contributor approval.