@@ -324,28 +324,24 @@ pub enum WmKind {
324324/// used to destroy (working memory, freshest full result, act fingerprints,
325325/// the receipt counter). Written to `~/.continuum/personas/<id>/volatile.json`
326326/// at shutdown / on tick write-through, restored at spawn. Deliberately
327- /// EXCLUDES engrams (already durable in sqlite) and dispatched handles (their
328- /// processes died with the old core — restoring them would fabricate
329- /// in-flight work).
327+ /// EXCLUDES engrams (already durable in sqlite) and dispatched handles (the
328+ /// checkpoint cannot establish whether their operations are still in flight).
330329#[ derive( Debug , Clone , serde:: Serialize , serde:: Deserialize ) ]
331330pub struct VolatileSnapshot {
332331 pub entries : Vec < WmEntry > ,
333332 pub last_action : Option < ( u64 , String ) > ,
334333 pub action_fps : Vec < String > ,
335334 pub next_action_seq : u64 ,
336- /// Wall-clock when this snapshot was written — lets restore render the
337- /// interruption GAP ("~N minutes ago") as a perceivable fact instead of
338- /// an invisible discontinuity. `0` on snapshots from before this field
339- /// (serde default): restore then omits the gap, never guesses it.
335+ /// Wall-clock when this snapshot was written — lets restore render its age,
336+ /// not the time of an interruption. `0` on snapshots from before this field
337+ /// (serde default): restore then reports the save time as unknown.
340338 #[ serde( default ) ]
341339 pub saved_at_ms : u64 ,
342340 /// LABELS (never handles) of dispatched commands still `Running` at
343- /// save time. Their processes die with the old core, so the handles are
344- /// deliberately NOT restored (that would fabricate in-flight work) —
345- /// but the persona must KNOW what was cut off so she can repeat it in
346- /// one motion (Joel 2026-07-13: an interruption should be like closing
347- /// a laptop — reopen, see what didn't finish, redo it easily). Restore
348- /// renders these into a `[resumed]` fact marked safe-to-repeat.
341+ /// save time. Labels alone establish neither completion nor side effects,
342+ /// so restore reports those outcomes as unknown instead of restoring handles
343+ /// or asserting that repeating the operation is safe. The field name stays
344+ /// unchanged for compatibility with existing checkpoints.
349345 #[ serde( default ) ]
350346 pub interrupted_dispatches : Vec < String > ,
351347 /// Build the receipts in this snapshot were RECORDED against.
@@ -824,13 +820,10 @@ impl WorkingMemory {
824820 /// mid-thought instead of blank. Capacity re-clamps on the way in so a
825821 /// snapshot from a larger-capacity life never overflows this one.
826822 ///
827- /// The laptop-lid contract (Joel 2026-07-13): the interruption itself
828- /// becomes a PERCEIVABLE fact — how long the lid was closed, and exactly
829- /// which dispatched commands were cut off mid-flight (their processes
830- /// died with the old core; the work did NOT complete and is safe to
831- /// repeat). Without this, a killed dispatch is indistinguishable from a
832- /// finished one, and she either re-does completed work or trusts work
833- /// that never happened.
823+ /// The checkpoint becomes a PERCEIVABLE fact: its save age and the dispatch
824+ /// labels it recorded as pending. It does not establish when an interruption
825+ /// happened, whether those operations completed, or what side effects they
826+ /// produced. The persona decides what to do with that uncertainty.
834827 pub fn restore ( & self , snap : VolatileSnapshot ) {
835828 {
836829 let mut e = self . entries . lock ( ) ;
@@ -877,10 +870,9 @@ impl WorkingMemory {
877870 self . next_action_seq
878871 . store ( snap. next_action_seq . max ( 1 ) , Ordering :: Relaxed ) ;
879872
880- // Render the interruption as a fact AFTER the entries land, so it is
881- // the NEWEST thing in her window when she wakes. A fact, never an
882- // instruction — she decides whether the cut-off work still matters.
883- let gap = ( snap. saved_at_ms > 0 )
873+ // Render the checkpoint evidence AFTER the entries land, so it is the
874+ // NEWEST thing in her window when she wakes. A fact, never an instruction.
875+ let checkpoint_age = ( snap. saved_at_ms > 0 )
884876 . then ( || {
885877 std:: time:: SystemTime :: now ( )
886878 . duration_since ( std:: time:: UNIX_EPOCH )
@@ -896,23 +888,21 @@ impl WorkingMemory {
896888 format ! ( "~{mins} min" )
897889 }
898890 } ) ;
899- let fact = match ( & gap, snap. interrupted_dispatches . is_empty ( ) ) {
900- ( Some ( g) , false ) => format ! (
901- "[resumed] your session was interrupted {g} ago and your memory restored. Cut off mid-flight and NOT completed: {} — safe to repeat if still wanted" ,
902- snap. interrupted_dispatches. join( "; " )
903- ) ,
904- ( Some ( g) , true ) => format ! (
905- "[resumed] your session was interrupted {g} ago and your memory restored; nothing was in flight"
906- ) ,
907- ( None , false ) => format ! (
908- "[resumed] your session was interrupted and your memory restored. Cut off mid-flight and NOT completed: {} — safe to repeat if still wanted" ,
891+ let saved = match checkpoint_age {
892+ Some ( age) => format ! ( "saved {age} ago" ) ,
893+ None => "with an unknown save time" . to_string ( ) ,
894+ } ;
895+ let pending = if snap. interrupted_dispatches . is_empty ( ) {
896+ "No pending dispatches were recorded in that checkpoint." . to_string ( )
897+ } else {
898+ format ! (
899+ "Dispatches recorded as pending at that save: {}. Their completion and side effects are unknown." ,
909900 snap. interrupted_dispatches. join( "; " )
910- ) ,
911- ( None , true ) => {
912- "[resumed] your session was interrupted and your memory restored; nothing was in flight" . to_string ( )
913- }
901+ )
914902 } ;
915- self . record_fact ( & fact) ;
903+ self . record_fact ( & format ! (
904+ "[resumed] your memory was restored from a checkpoint {saved}. {pending}"
905+ ) ) ;
916906
917907 // The OTHER discontinuity, and until 2026-08-07 an invisible one: the
918908 // substrate itself was rebuilt while she was away (#165).
@@ -1805,9 +1795,8 @@ mod tests {
18051795 // round-trips through the JSON snapshot losslessly: typed entries (kinds
18061796 // intact), the full last result, fingerprints, and the receipt counter
18071797 // (so post-restore receipts keep ascending numbers instead of colliding
1808- // with restored ones) — PLUS the laptop-lid contract (Joel 2026-07-13):
1809- // the interruption itself lands as the NEWEST fact, naming the gap and
1810- // any dispatched commands cut off mid-flight as safe to repeat.
1798+ // with restored ones). The NEWEST fact names the checkpoint's save age and
1799+ // recorded pending labels without inventing interruption time or outcomes.
18111800 #[ test]
18121801 fn volatile_snapshot_round_trips_and_renders_the_interruption ( ) {
18131802 let wm = WorkingMemory :: new ( 8 ) ;
@@ -1816,8 +1805,8 @@ mod tests {
18161805 wm. record_fact ( "[unfulfilled] I said I would run commands, but no tool ran" ) ;
18171806 wm. record_settlement ( "shared the plan" ) ;
18181807 wm. note_action_fingerprint ( "code/list|{\" path\" :\" .\" }" ) ;
1819- // A dispatched compile still Running at snapshot time — the process
1820- // dies with the old core; only its LABEL must survive .
1808+ // A dispatched compile still Running at snapshot time — only its LABEL
1809+ // survives, not evidence about later completion or side effects .
18211810 let handle = Uuid :: new_v4 ( ) ;
18221811 wm. record_dispatch_event (
18231812 handle,
@@ -1845,14 +1834,27 @@ mod tests {
18451834 "window identical before the marker"
18461835 ) ;
18471836 assert ! (
1848- resumed[ 0 ] . contains( "[resumed]" ) ,
1849- "interruption is perceivable: {resumed:?}"
1837+ resumed[ 0 ] . starts_with( "[resumed] your memory was restored from a checkpoint saved " )
1838+ && resumed[ 0 ] . contains( " ago." ) ,
1839+ "the age describes the checkpoint save: {resumed:?}"
18501840 ) ;
18511841 assert ! (
1852- resumed[ 0 ] . contains( "cargo build (dispatched)" )
1853- && resumed[ 0 ] . contains( "safe to repeat" ) ,
1854- "cut-off work named + marked repeatable: {resumed:?}"
1855- ) ;
1842+ resumed[ 0 ] . contains(
1843+ "Dispatches recorded as pending at that save: cargo build (dispatched)."
1844+ ) && resumed[ 0 ] . contains( "Their completion and side effects are unknown." ) ,
1845+ "pending work is named without inventing its outcome: {resumed:?}"
1846+ ) ;
1847+ for unsupported in [
1848+ "was interrupted" ,
1849+ "NOT completed" ,
1850+ "safe to repeat" ,
1851+ "nothing was in flight" ,
1852+ ] {
1853+ assert ! (
1854+ !resumed[ 0 ] . contains( unsupported) ,
1855+ "unsupported claim {unsupported:?}: {resumed:?}"
1856+ ) ;
1857+ }
18561858 assert ! (
18571859 !restored. iter( ) . any( |l| l. contains( "[rebuilt]" ) ) ,
18581860 "SAME build across the restart — no rebuild fact, or we cry wolf on every \
@@ -1874,14 +1876,42 @@ mod tests {
18741876 "counter resumed: {last:?}"
18751877 ) ;
18761878
1877- // And the quiet path: nothing in flight → the fact says so plainly.
1879+ // Missing and zero save times remain unknown even with pending labels.
1880+ for missing_saved_at in [ false , true ] {
1881+ let mut legacy: serde_json:: Value = serde_json:: from_str ( & json) . expect ( "snapshot JSON" ) ;
1882+ if missing_saved_at {
1883+ let _ = legacy
1884+ . as_object_mut ( )
1885+ . expect ( "snapshot object" )
1886+ . remove ( "saved_at_ms" ) ;
1887+ } else {
1888+ legacy[ "saved_at_ms" ] = serde_json:: json!( 0 ) ;
1889+ }
1890+ let legacy = serde_json:: from_value ( legacy) . expect ( "legacy snapshot deserializes" ) ;
1891+ let restored_legacy = WorkingMemory :: new ( 8 ) ;
1892+ restored_legacy. restore ( legacy) ;
1893+ let recent = restored_legacy. recent ( ) ;
1894+ let resumed = recent. last ( ) . expect ( "restored checkpoint fact" ) ;
1895+ assert ! (
1896+ resumed. contains( "checkpoint with an unknown save time." ) ,
1897+ "{resumed}"
1898+ ) ;
1899+ assert ! ( !resumed. contains( "ago" ) , "no fabricated save age: {resumed}" ) ;
1900+ assert ! ( resumed. contains( "cargo build (dispatched)" ) , "{resumed}" ) ;
1901+ assert ! (
1902+ resumed. contains( "Their completion and side effects are unknown." ) ,
1903+ "{resumed}"
1904+ ) ;
1905+ }
1906+
1907+ // An empty list describes this checkpoint, not everything that was running.
18781908 let quiet = WorkingMemory :: new ( 8 ) ;
18791909 quiet. restore ( VolatileSnapshot {
18801910 entries : Vec :: new ( ) ,
18811911 last_action : None ,
18821912 action_fps : Vec :: new ( ) ,
18831913 next_action_seq : 1 ,
1884- saved_at_ms : 0 , // pre-field snapshot: no gap guessed
1914+ saved_at_ms : 0 , // pre-field snapshot: no save time guessed
18851915 interrupted_dispatches : Vec :: new ( ) ,
18861916 build_sha : String :: new ( ) , // pre-field snapshot: no rebuild guessed either
18871917 receipt_heads : Vec :: new ( ) ,
@@ -1890,10 +1920,15 @@ mod tests {
18901920 } ) ;
18911921 let q = quiet. recent ( ) ;
18921922 assert_eq ! ( q. len( ) , 1 ) ;
1893- assert ! ( q[ 0 ] . contains( "nothing was in flight" ) , "{q:?}" ) ;
1923+ assert ! (
1924+ q[ 0 ] . contains( "No pending dispatches were recorded in that checkpoint." ) ,
1925+ "{q:?}"
1926+ ) ;
1927+ assert ! ( !q[ 0 ] . contains( "nothing was in flight" ) , "{q:?}" ) ;
1928+ assert ! ( q[ 0 ] . contains( "checkpoint with an unknown save time." ) , "{q:?}" ) ;
18941929 assert ! (
18951930 !q[ 0 ] . contains( "ago" ) ,
1896- "no fabricated gap on legacy snapshots: {q:?}"
1931+ "no fabricated save age on legacy snapshots: {q:?}"
18971932 ) ;
18981933 }
18991934
0 commit comments