diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index dc05e51..fb9ec65 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -79,12 +79,12 @@ d4d912fbec5d674a16c2c12242332335924405ebc6bdf080c904d32e08d15396 tests/test_sov 583706bc944261a63b317d75ad666604155d3a286ae99ad21b13d2321d7d7341 tests/test_sovereign_renderer_contract.mjs 6238a56aa042a20b86e3b555786d34849094c894d8473fd80d6a5255d38a5108 tests/test_sovereign_renderer_motion_upgrade.mjs 3f63626f40bebc0746521643f1fff6bc522e402218965ee327b8725b6e50dc94 tests/test_sovereign_reproduction.mjs -2fd7bc90cad5a8d5d51925ac2579267cc42505b4fee5dbbe8a8acd3ed6eb2e06 tests/test_sovereign_runtime_contract.mjs +f6c7f2623eeeeea2faebe84c30e135c6357403b3c44aee379dc5f8058064ecac tests/test_sovereign_runtime_contract.mjs 6c69813ab0bc4cb1534b82e8a7d06dc323c8a6c0458298e0a0584b7bde4d47e8 tests/test_sovereign_shell.mjs 3addb20df209d6f6ad84ff37adaa7dd67bf2c10adfc4c372559bc32543311415 tests/test_sovereign_shelter_environment.mjs 218e8e718765fa3e19aead2359ab5cec36778c6fda596176d8b016a38843da00 tests/test_sovereign_society.mjs 5357326e16f8493d08ecc0a56bae1a48773c1f2e979f830f99181054e144ce7a tests/test_sovereign_transient_physics_upgrade.mjs -f083f228454b586273016fe7265d7badd2e7910f6484be49f0c88640670d5214 worker/src/index.js +f19d6c322cd8b17bd8f65560e4602d523899f8546d70dd40a783cc8a285ab0a7 worker/src/index.js 2eac2b46fb9337eb62b4b716be26eb11f92359c58e6c6685039f3506346a09fb world/actions.js 79cdf84d53e19cb1500f2a782aad657d49281005c7809e06bfcf190f7411f294 world/artifacts.js 251ef01a7616cef68bba68b1bc81ac62d30b0bd1685a8beb9784c4ba55fa469b world/beliefs.js diff --git a/tests/test_sovereign_runtime_contract.mjs b/tests/test_sovereign_runtime_contract.mjs index 159b86c..28440c5 100644 --- a/tests/test_sovereign_runtime_contract.mjs +++ b/tests/test_sovereign_runtime_contract.mjs @@ -49,6 +49,21 @@ test('constructor never postpones a due alarm and alarm handler keeps the schedu assert.match(alarm,/finally/); assert.match(alarm,/setAlarm\(Date\.now\(\)\+ALARM_MS\)/); }); +test('runtime recovery is persisted immediately before hibernation can discard the rebase',()=>{ + const tickStart=worker.indexOf('async tick(){'); + const tickEnd=worker.indexOf('async alarm(){',tickStart); + const tick=worker.slice(tickStart,tickEnd); + + assert.match(tick,/progress\.recovered/); + assert.match(tick,/persist\(\{forceSeal:true\}\)/); + + const recoveryIndex=tick.indexOf('progress.recovered'); + const checkpointIndex=tick.indexOf('checkpointDue',recoveryIndex); + + assert.ok(recoveryIndex>=0); + assert.ok(checkpointIndex>recoveryIndex); +}); + test('canonical world persists in Durable Object SQLite with SHA-256 checkpoint seals',()=>{ assert.match(worker,/storage\.sql/); assert.match(worker,/CREATE TABLE IF NOT EXISTS world_state/); diff --git a/worker/src/index.js b/worker/src/index.js index 6920ef2..a84524a 100644 --- a/worker/src/index.js +++ b/worker/src/index.js @@ -284,7 +284,12 @@ export class SovereignWorld { this.world.clock.worldMinute-lastPersisted >= PERSIST_INTERVAL_WORLD_MINUTES; - if(checkpointDue){ + if(progress.recovered){ + // Recovery rebases realEpochMs so downtime is not counted as lived + // world time. Persist that boundary immediately; otherwise Durable + // Object hibernation can discard it and reload the pre-recovery state. + await this.persist({forceSeal:true}); + }else if(checkpointDue){ await this.persist(); }