@@ -373,7 +373,7 @@ function injectHistoryBlockIntoFirstUserMessage(
373373) : boolean {
374374 for ( let i = 0 ; i < piMessages . length ; i ++ ) {
375375 const msg = piMessages [ i ] ;
376- if ( ! msg || msg . role !== "user" ) continue ;
376+ if ( msg ? .role !== "user" ) continue ;
377377
378378 const userMsg = msg as PiUserMessage ;
379379 if ( typeof userMsg . content === "string" ) {
@@ -610,6 +610,9 @@ export interface PiM0SnapshotMarkers {
610610 // Captured from PiM0HardSignals at the injection call site.
611611 systemHash : string ;
612612 modelKey : string ;
613+ // Pi sessions can switch projects in-process (`/cd`). NULL on legacy cached
614+ // rows means unknown/lazy-adopted and must not force a HARD fold by itself.
615+ projectIdentity : string | null ;
613616}
614617
615618/**
@@ -770,6 +773,7 @@ function getCachedMarkers(
770773 lastBaselineEndMessageId : cachedBoundary ,
771774 systemHash : meta . cachedM0SystemHash ?? "" ,
772775 modelKey : meta . cachedM0ModelKey ?? "" ,
776+ projectIdentity : meta . cachedM0ProjectIdentity ?? null ,
773777 } ;
774778}
775779
@@ -859,6 +863,7 @@ function readCurrentMarkersFromCompartments(
859863 lastBaselineEndMessageId : lastBaselineEndMessageId ( compartments ) ,
860864 systemHash : ( state . hardSignals ?? EMPTY_PI_HARD_SIGNALS ) . systemHash ,
861865 modelKey : ( state . hardSignals ?? EMPTY_PI_HARD_SIGNALS ) . modelKey ,
866+ projectIdentity : state . projectIdentity ,
862867 } ;
863868}
864869
@@ -907,6 +912,15 @@ export function mustMaterializePi(
907912 ) {
908913 return { value : true , reason : "system_hash" } ;
909914 }
915+ // Pi can switch projects within the same session (`/cd`). Legacy cached rows
916+ // have a NULL marker: treat that as unknown/MATCH for lazy adoption so the
917+ // first post-upgrade no-switch pass does not HARD-fold existing sessions.
918+ if (
919+ meta . cachedM0ProjectIdentity !== null &&
920+ meta . cachedM0ProjectIdentity !== state . projectIdentity
921+ ) {
922+ return { value : true , reason : "project_change" } ;
923+ }
910924 // Idle > TTL: self-consuming guard via cachedM0MaterializedAt (parity with
911925 // OpenCode). cacheExpired stays true every pass until lastResponseTime
912926 // updates, so fold only when the last response is newer than the last
@@ -1227,6 +1241,7 @@ function readFrozenM0InputsPi(
12271241 lastBaselineEndMessageId : lastBaselineEndMessageId ( compartments ) ,
12281242 systemHash : ( state . hardSignals ?? EMPTY_PI_HARD_SIGNALS ) . systemHash ,
12291243 modelKey : ( state . hardSignals ?? EMPTY_PI_HARD_SIGNALS ) . modelKey ,
1244+ projectIdentity : state . projectIdentity ,
12301245 } ;
12311246 return { docs, markers, compartments, memories, userProfile, workspace } ;
12321247 } ) ;
@@ -1391,6 +1406,7 @@ export function materializeM0Pi(
13911406 current . maxCompartmentSeq !== snapshotMarkers . maxCompartmentSeq ||
13921407 current . maxMutationId !== snapshotMarkers . maxMutationId ||
13931408 current . maxMemoryMutationId !== snapshotMarkers . maxMemoryMutationId ||
1409+ current . projectIdentity !== snapshotMarkers . projectIdentity ||
13941410 // Inert today (both harnesses pin sessionFactsVersion to 0 — facts are
13951411 // retired in v2), but kept for structural parity with OpenCode
13961412 // materializeM0 so the two stale checks can't silently drift if either
@@ -1428,6 +1444,7 @@ export function materializeM0Pi(
14281444 upgradeState : snapshotMarkers . upgradeState ,
14291445 systemHash : snapshotMarkers . systemHash ,
14301446 modelKey : snapshotMarkers . modelKey ,
1447+ projectIdentity : snapshotMarkers . projectIdentity ,
14311448 } ) ;
14321449 // Persist the rendered-memory identity in the SAME transaction as the m[0]
14331450 // snapshot (parity with OpenCode materializeM0). `memory_block_ids` /
@@ -1743,6 +1760,7 @@ interface CachedPiM0M1Row {
17431760 cached_m0_upgrade_state : string | null ;
17441761 cached_m0_system_hash : string | null ;
17451762 cached_m0_model_key : string | null ;
1763+ cached_m0_project_identity : string | null ;
17461764 cached_m0_last_baseline_end_message_id : string | null ;
17471765 memory_block_ids : string | null ;
17481766}
@@ -1792,6 +1810,7 @@ function readCachedPiM0M1Row(
17921810 cached_m0_upgrade_state,
17931811 cached_m0_system_hash,
17941812 cached_m0_model_key,
1813+ cached_m0_project_identity,
17951814 cached_m0_last_baseline_end_message_id,
17961815 memory_block_ids
17971816 FROM session_meta
@@ -1836,6 +1855,7 @@ function markersFromCachedPiRow(
18361855 : null ,
18371856 systemHash : row . cached_m0_system_hash ?? "" ,
18381857 modelKey : row . cached_m0_model_key ?? "" ,
1858+ projectIdentity : row . cached_m0_project_identity ?? null ,
18391859 } ;
18401860}
18411861
@@ -1870,6 +1890,8 @@ function cachedPiRowMatchesSnapshot(args: {
18701890 // this process's cached row so the soft-refresh CAS adopts the sibling's m[0].
18711891 ( rowMarkers . systemHash ?? "" ) === ( args . markers . systemHash ?? "" ) &&
18721892 ( rowMarkers . modelKey ?? "" ) === ( args . markers . modelKey ?? "" ) &&
1893+ ( rowMarkers . projectIdentity ?? null ) ===
1894+ ( args . markers . projectIdentity ?? null ) &&
18731895 // Workspace fingerprint (parity with OpenCode cachedRowMatchesState):
18741896 // projectMemoryEpoch above only tracks THIS session's own project, but a
18751897 // FOREIGN member's epoch bump changes the workspace fingerprint without
@@ -1881,6 +1903,15 @@ function cachedPiRowMatchesSnapshot(args: {
18811903 ) ;
18821904}
18831905
1906+ function adoptCachedPiProjectIdentity (
1907+ db : ContextDatabase ,
1908+ state : PiM0M1State ,
1909+ ) : void {
1910+ db . prepare (
1911+ "UPDATE session_meta SET cached_m0_project_identity = ? WHERE session_id = ? AND cached_m0_project_identity IS NULL" ,
1912+ ) . run ( state . projectIdentity , state . sessionId ) ;
1913+ }
1914+
18841915function decodeCachedM1 ( row : CachedPiM0M1Row , sessionId : string ) : string {
18851916 if ( ! row . cached_m1_bytes ) {
18861917 throw new PiMaterializeContentionError (
@@ -2168,6 +2199,10 @@ export function injectM0M1Pi(
21682199 `missing m[0] markers for ${ state . sessionId } ` ,
21692200 ) ;
21702201 }
2202+ if ( ! materialized && markers . projectIdentity === null ) {
2203+ adoptCachedPiProjectIdentity ( db , state ) ;
2204+ markers = { ...markers , projectIdentity : state . projectIdentity } ;
2205+ }
21712206
21722207 if ( materialized ) {
21732208 // m[1] was rendered and persisted atomically inside materializeM0Pi.
0 commit comments