@@ -54,18 +54,21 @@ import {
5454} from "@magic-context/core/features/magic-context/scheduler" ;
5555import { recordSessionProjectIdentity } from "@magic-context/core/features/magic-context/session-project-storage" ;
5656import {
57- adoptFallbackTagMessageId ,
57+ adoptPiFallbackMessageTag ,
58+ adoptPiFallbackToolOwnerTag ,
5859 type ContextDatabase ,
5960 casChannel2NudgeState ,
6061 clearPendingPiCompactionMarkerStateIf ,
6162 findAdoptableFallbackTags ,
63+ findPiFallbackToolOwnerTags ,
6264 getActiveTagsBySession ,
6365 getActiveTagTokenAggregate ,
6466 getHistorianFailureState ,
6567 getOldestActiveUnprotectedToolTags ,
6668 getPendingOps ,
6769 getPendingPiCompactionMarkerState ,
6870 getTagsByNumbers ,
71+ hasPiFallbackToolOwnerTags ,
6972 setSessionWorkMetrics ,
7073 updateSessionMeta ,
7174} from "@magic-context/core/features/magic-context/storage" ;
@@ -241,6 +244,7 @@ export const __test = {
241244 adoptPiFallbackTags,
242245 applyForwardPressureFloor,
243246 buildEntryFingerprintMap,
247+ buildPiToolOwnerMap,
244248} ;
245249
246250/**
@@ -1294,59 +1298,219 @@ function buildEntryFingerprintMap(
12941298 return map ;
12951299}
12961300
1301+ function piToolOwnerMapKey ( timestamp : number , callId : string ) : string {
1302+ return `${ timestamp } \x00${ callId } ` ;
1303+ }
1304+
1305+ function buildPiToolOwnerMap (
1306+ messages : readonly PiAgentMessage [ ] ,
1307+ resolveStableId : ( msg : unknown , index : number ) => string | undefined ,
1308+ ) : Map < string , Set < string > > {
1309+ const map = new Map < string , Set < string > > ( ) ;
1310+ for ( let i = 0 ; i < messages . length ; i ++ ) {
1311+ const message = messages [ i ] ;
1312+ if ( ! message || typeof message !== "object" ) continue ;
1313+ const msg = message as {
1314+ role ?: unknown ;
1315+ content ?: unknown ;
1316+ timestamp ?: unknown ;
1317+ } ;
1318+ if ( msg . role !== "assistant" ) continue ;
1319+ if ( typeof msg . timestamp !== "number" || ! Number . isFinite ( msg . timestamp ) ) {
1320+ continue ;
1321+ }
1322+ if ( ! Array . isArray ( msg . content ) ) continue ;
1323+ const ownerRealId = resolveStableId ( message , i ) ;
1324+ if ( ! ownerRealId || ownerRealId . startsWith ( "pi-msg-" ) ) continue ;
1325+ for ( const part of msg . content ) {
1326+ if ( ! part || typeof part !== "object" ) continue ;
1327+ const p = part as { type ?: unknown ; id ?: unknown } ;
1328+ if ( p . type !== "toolCall" ) continue ;
1329+ if ( typeof p . id !== "string" || p . id . length === 0 ) continue ;
1330+ const key = piToolOwnerMapKey ( msg . timestamp , p . id ) ;
1331+ let owners = map . get ( key ) ;
1332+ if ( ! owners ) {
1333+ owners = new Set < string > ( ) ;
1334+ map . set ( key , owners ) ;
1335+ }
1336+ owners . add ( ownerRealId ) ;
1337+ }
1338+ }
1339+ return map ;
1340+ }
1341+
1342+ function parsePiFallbackToolOwnerId (
1343+ ownerMsgId : string ,
1344+ ) : { timestamp : number ; role : string } | null {
1345+ const match = / ^ p i - m s g - \d + - ( \d + ) - ( .+ ) $ / . exec ( ownerMsgId ) ;
1346+ if ( ! match ) return null ;
1347+ const timestamp = Number ( match [ 1 ] ) ;
1348+ if ( ! Number . isFinite ( timestamp ) ) return null ;
1349+ return { timestamp, role : match [ 2 ] ?? "" } ;
1350+ }
1351+
1352+ function databaseIsInTransaction ( db : ContextDatabase ) : boolean {
1353+ const state = db as unknown as {
1354+ inTransaction ?: unknown ;
1355+ isTransaction ?: unknown ;
1356+ } ;
1357+ return state . inTransaction === true || state . isTransaction === true ;
1358+ }
1359+
1360+ function runImmediateTransaction < T > ( db : ContextDatabase , fn : ( ) => T ) : T {
1361+ if ( databaseIsInTransaction ( db ) ) {
1362+ return db . transaction ( fn ) ( ) ;
1363+ }
1364+ db . exec ( "BEGIN IMMEDIATE" ) ;
1365+ try {
1366+ const result = fn ( ) ;
1367+ db . exec ( "COMMIT" ) ;
1368+ return result ;
1369+ } catch ( error ) {
1370+ db . exec ( "ROLLBACK" ) ;
1371+ throw error ;
1372+ }
1373+ }
1374+
1375+ interface AdoptPiFallbackTagsOptions {
1376+ messages ?: readonly PiAgentMessage [ ] ;
1377+ resolveStableId ?: ( msg : unknown , index : number ) => string | undefined ;
1378+ stampStableIdScheme ?: number ;
1379+ }
1380+
1381+ function hasAdoptablePiFallbackMessageTags (
1382+ db : ContextDatabase ,
1383+ sessionId : string ,
1384+ fingerprintById : ReadonlyMap < string , string > ,
1385+ ) : boolean {
1386+ for ( const [ realMessageId , fingerprint ] of fingerprintById ) {
1387+ if ( realMessageId . startsWith ( "pi-msg-" ) ) continue ;
1388+ if ( findAdoptableFallbackTags ( db , sessionId , fingerprint ) . length > 0 ) {
1389+ return true ;
1390+ }
1391+ }
1392+ return false ;
1393+ }
1394+
12971395/**
1298- * Pi fallback-tag adoption pre-pass. Runs BEFORE tagging. For each message that
1299- * now resolves to a REAL SessionEntry id this pass, find the tag(s) created for
1300- * the same message under its earlier `pi-msg-*` fallback id (matched by raw
1301- * fingerprint) and migrate them onto the real id — so the message keeps its
1302- * tag_number (hence §N§ and all per-tag state) instead of getting a fresh tag
1303- * and drifting. Per-part, uniqueness-guarded, race-safe; any miss degrades to a
1304- * fresh allocation in `tagTranscript`. No-op for messages that are already on a
1305- * real id or have no fallback predecessor.
1396+ * Pi fallback-tag adoption pre-pass. Runs BEFORE tagging. Message text tags are
1397+ * matched by raw-message fingerprint; tool tags are owner-driven from stored
1398+ * `pi-msg-*` owners to the current real assistant entry id by `(timestamp,
1399+ * callId)`. Collision folds keep the synthetic row's tag number/drop metadata,
1400+ * merge size/token accounting by MAX, retarget pending ops, and update the
1401+ * tagger's in-memory aliases before `tagTranscript` looks anything up.
13061402 */
13071403function adoptPiFallbackTags (
13081404 db : ContextDatabase ,
13091405 sessionId : string ,
13101406 tagger : Tagger ,
13111407 fingerprintById : ReadonlyMap < string , string > ,
1408+ options : AdoptPiFallbackTagsOptions = { } ,
13121409) : void {
1313- for ( const [ realMessageId , fingerprint ] of fingerprintById ) {
1314- // Only real ids can be adoption targets; a pi-msg-* id has no fallback
1315- // predecessor to migrate from.
1316- if ( realMessageId . startsWith ( "pi-msg-" ) ) continue ;
1317- const candidates = findAdoptableFallbackTags ( db , sessionId , fingerprint ) ;
1318- if ( candidates . length === 0 ) continue ;
1319- // Group candidates by their fallback message base id (strip the :pN
1320- // suffix). A unique base means exactly one fallback message carried this
1321- // fingerprint → safe to adopt; duplicates (same fingerprint on >1
1322- // fallback message) are ambiguous → skip, let tagTranscript allocate
1323- // fresh.
1324- const baseIds = new Set < string > ( ) ;
1325- for ( const c of candidates ) {
1326- const m = / ^ ( .* ) : p \d + $ / . exec ( c . messageId ) ;
1327- baseIds . add ( m ? m [ 1 ] : c . messageId ) ;
1410+ const shouldRunMessageMigration =
1411+ options . stampStableIdScheme !== undefined ||
1412+ hasAdoptablePiFallbackMessageTags ( db , sessionId , fingerprintById ) ;
1413+ const shouldRunToolOwnerMigration = Boolean (
1414+ options . messages &&
1415+ options . resolveStableId &&
1416+ hasPiFallbackToolOwnerTags ( db , sessionId ) ,
1417+ ) ;
1418+ if ( ! shouldRunMessageMigration && ! shouldRunToolOwnerMigration ) return ;
1419+
1420+ runImmediateTransaction ( db , ( ) => {
1421+ if ( shouldRunMessageMigration ) {
1422+ for ( const [ realMessageId , fingerprint ] of fingerprintById ) {
1423+ // Only real ids can be adoption targets; a pi-msg-* id has no fallback
1424+ // predecessor to migrate from.
1425+ if ( realMessageId . startsWith ( "pi-msg-" ) ) continue ;
1426+ const candidates = findAdoptableFallbackTags (
1427+ db ,
1428+ sessionId ,
1429+ fingerprint ,
1430+ ) ;
1431+ if ( candidates . length === 0 ) continue ;
1432+ // Group candidates by their fallback message base id (strip the :pN
1433+ // suffix). A unique base means exactly one fallback message carried this
1434+ // fingerprint → safe to adopt; duplicates (same fingerprint on >1
1435+ // fallback message) are ambiguous → skip, let tagTranscript allocate
1436+ // fresh.
1437+ const baseIds = new Set < string > ( ) ;
1438+ for ( const c of candidates ) {
1439+ const m = / ^ ( .* ) : p \d + $ / . exec ( c . messageId ) ;
1440+ baseIds . add ( m ? m [ 1 ] : c . messageId ) ;
1441+ }
1442+ if ( baseIds . size !== 1 ) continue ;
1443+ for ( const c of candidates ) {
1444+ const ordinalMatch = / : p ( \d + ) $ / . exec ( c . messageId ) ;
1445+ if ( ! ordinalMatch ) continue ;
1446+ const realContentId = `${ realMessageId } :p${ ordinalMatch [ 1 ] } ` ;
1447+ const adoption = adoptPiFallbackMessageTag (
1448+ db ,
1449+ sessionId ,
1450+ c . tagNumber ,
1451+ c . messageId ,
1452+ realContentId ,
1453+ ) ;
1454+ if ( adoption . action !== "skipped" ) {
1455+ // Drop stale fallback and collision aliases, then bind the survivor
1456+ // under the real key so the same-pass exact lookup hits it.
1457+ tagger . unbindTag ( sessionId , c . messageId ) ;
1458+ if ( adoption . action === "folded" ) {
1459+ tagger . unbindTag ( sessionId , realContentId ) ;
1460+ }
1461+ tagger . bindTag ( sessionId , realContentId , adoption . tagNumber ) ;
1462+ }
1463+ }
1464+ }
13281465 }
1329- if ( baseIds . size !== 1 ) continue ;
1330- for ( const c of candidates ) {
1331- const ordinalMatch = / : p ( \d + ) $ / . exec ( c . messageId ) ;
1332- if ( ! ordinalMatch ) continue ;
1333- const realContentId = `${ realMessageId } :p${ ordinalMatch [ 1 ] } ` ;
1334- const migrated = adoptFallbackTagMessageId (
1335- db ,
1336- sessionId ,
1337- c . tagNumber ,
1338- c . messageId ,
1339- realContentId ,
1466+
1467+ if (
1468+ shouldRunToolOwnerMigration &&
1469+ options . messages &&
1470+ options . resolveStableId
1471+ ) {
1472+ const ownerMap = buildPiToolOwnerMap (
1473+ options . messages ,
1474+ options . resolveStableId ,
13401475 ) ;
1341- if ( migrated ) {
1342- // Drop the stale fallback alias, bind the real key — so the
1343- // subsequent tagTranscript exact-key lookup hits the migrated
1344- // tag and does NOT allocate a fresh one.
1345- tagger . unbindTag ( sessionId , c . messageId ) ;
1346- tagger . bindTag ( sessionId , realContentId , c . tagNumber ) ;
1476+ for ( const row of findPiFallbackToolOwnerTags ( db , sessionId ) ) {
1477+ const parsed = parsePiFallbackToolOwnerId ( row . toolOwnerMessageId ) ;
1478+ if ( parsed ?. role !== "assistant" ) continue ;
1479+ const owners = ownerMap . get (
1480+ piToolOwnerMapKey ( parsed . timestamp , row . callId ) ,
1481+ ) ;
1482+ if ( owners ?. size !== 1 ) continue ;
1483+ const [ realOwnerId ] = owners ;
1484+ if ( ! realOwnerId || realOwnerId . startsWith ( "pi-msg-" ) ) continue ;
1485+ const adoption = adoptPiFallbackToolOwnerTag (
1486+ db ,
1487+ sessionId ,
1488+ row . tagNumber ,
1489+ row . callId ,
1490+ row . toolOwnerMessageId ,
1491+ realOwnerId ,
1492+ ) ;
1493+ if ( adoption . action !== "skipped" ) {
1494+ tagger . unbindToolTag ( sessionId , row . toolOwnerMessageId , row . callId ) ;
1495+ if ( adoption . action === "folded" ) {
1496+ tagger . unbindToolTag ( sessionId , realOwnerId , row . callId ) ;
1497+ }
1498+ tagger . bindToolTag (
1499+ sessionId ,
1500+ row . callId ,
1501+ realOwnerId ,
1502+ adoption . tagNumber ,
1503+ ) ;
1504+ }
13471505 }
13481506 }
1349- }
1507+
1508+ if ( options . stampStableIdScheme !== undefined ) {
1509+ updateSessionMeta ( db , sessionId , {
1510+ piStableIdScheme : options . stampStableIdScheme ,
1511+ } ) ;
1512+ }
1513+ } ) ;
13501514}
13511515
13521516/**
@@ -1777,8 +1941,9 @@ export function registerPiContextHandler(
17771941 // (rather than an uncontrolled defer-pass bust that could leak
17781942 // full-size content). Also clear stripped_placeholder_ids so the
17791943 // forced pass rediscovers placeholders under the new scheme. The new
1780- // scheme is stamped only AFTER the pass succeeds (end of runPipeline),
1781- // so a mid-pass failure retries instead of skipping the cutover.
1944+ // scheme is stamped atomically with fallback tag adoption immediately
1945+ // before tagging, so a session can keep resolving any remaining pi-msg-*
1946+ // tool owners on later post-stamp passes via the cheap stale-owner gate.
17821947 const storedStableIdScheme = sessionMeta . piStableIdScheme ?? 0 ;
17831948 // Only activate the cutover when REAL SessionEntry ids are available this
17841949 // pass. The cutover re-keys persisted state from pi-msg-* index ids to
@@ -2111,52 +2276,11 @@ export function registerPiContextHandler(
21112276 } ) ;
21122277 }
21132278
2114- // Stamp the new stable-id scheme ONLY after a successful cutover pass
2115- // that actually executed (re-tagged + re-dropped under the new scheme).
2116- // Stamp the new scheme only when the cutover's ESSENTIAL work completed.
2117- // Re-keying tag identity (pi-msg-* → entry-id) orphans the old drop
2118- // state, so previously-dropped tools resurface as fresh active tags;
2119- // the cutover is not "done" until heuristic cleanup actually re-dropped
2120- // them. `executedWorkThisPass` is too loose — it's set true even when
2121- // the heuristics try-block THREW (the empty-pending-ops path also sets
2122- // it), which would stamp the scheme while the re-drop never happened,
2123- // leaving resurrected tools in context permanently. Gate on
2124- // `heuristicsExecuted` (the re-drop ran without throwing) when heuristics
2125- // are enabled; when disabled there are no drops to redo, so a completed
2126- // pipeline (executedWorkThisPass) is sufficient. The forced pass reliably
2127- // runs heuristics (shouldRunHeuristics is true under the forced
2128- // execute+materialize), so a genuine throw defers the stamp and the next
2129- // pass retries the cutover rather than skipping it — no infinite loop
2130- // unless heuristics throws every pass, which is a separate bug that this
2131- // gate correctly refuses to paper over.
2132- const cutoverWorkComplete =
2133- options . heuristics === undefined
2134- ? result . executedWorkThisPass
2135- : result . heuristicsExecuted ;
2136- // `stableIdSchemeCutover` is already gated on realEntryIdsAvailable at
2137- // activation (see the cutover-detection block above), so reaching here
2138- // with it true guarantees real entry ids were used for the re-key. The
2139- // remaining gate is cutoverWorkComplete (the re-drop actually ran).
2140- if ( stableIdSchemeCutover && cutoverWorkComplete ) {
2141- try {
2142- updateSessionMeta ( options . db , sessionId , {
2143- piStableIdScheme : PI_STABLE_ID_SCHEME ,
2144- } ) ;
2145- invalidateTrueRawTokenCache ( {
2146- sessionId,
2147- reason : "pi.stable-id-scheme.changed" ,
2148- } ) ;
2149- sessionLog (
2150- sessionId ,
2151- `stable-id scheme cutover complete — stamped scheme=${ PI_STABLE_ID_SCHEME } ` ,
2152- ) ;
2153- } catch ( err ) {
2154- sessionLog (
2155- sessionId ,
2156- `stable-id cutover: failed to stamp scheme (will retry next pass): ${ err instanceof Error ? err . message : String ( err ) } ` ,
2157- ) ;
2158- }
2159- }
2279+ // Stable-id scheme stamping now happens inside `adoptPiFallbackTags`, in
2280+ // the same BEGIN IMMEDIATE transaction as fallback tag rekeys/folds. Keep
2281+ // this post-transform slot free of DB writes so post-stamp passes can still
2282+ // run the cheap stale-owner gate and late-resolve any surviving pi-msg-* tool
2283+ // owners on future passes.
21602284
21612285 // Step 4b.4: nudge + note-nudge + auto-search hint. All three
21622286 // run AFTER tagging/drops finish so they see the post-mutation
@@ -3479,7 +3603,24 @@ async function runPipeline(args: RunPipelineArgs): Promise<RunPipelineResult> {
34793603 args . sessionId ,
34803604 args . tagger ,
34813605 entryFingerprintByMessageId ,
3606+ {
3607+ messages : args . messages as PiAgentMessage [ ] ,
3608+ resolveStableId : stableIdResolver ,
3609+ stampStableIdScheme : args . stableIdSchemeCutover
3610+ ? PI_STABLE_ID_SCHEME
3611+ : undefined ,
3612+ } ,
34823613 ) ;
3614+ if ( args . stableIdSchemeCutover === true ) {
3615+ invalidateTrueRawTokenCache ( {
3616+ sessionId : args . sessionId ,
3617+ reason : "pi.stable-id-scheme.changed" ,
3618+ } ) ;
3619+ sessionLog (
3620+ args . sessionId ,
3621+ `stable-id scheme cutover complete — stamped scheme=${ PI_STABLE_ID_SCHEME } ` ,
3622+ ) ;
3623+ }
34833624 const tTag = performance . now ( ) ;
34843625 const { targets } = tagTranscript (
34853626 args . sessionId ,
0 commit comments