Skip to content

Commit 5fd440d

Browse files
committed
mason: cut over Pi tool tag owners
1 parent b74e237 commit 5fd440d

9 files changed

Lines changed: 1265 additions & 91 deletions

File tree

packages/pi-plugin/src/context-handler.test.ts

Lines changed: 488 additions & 0 deletions
Large diffs are not rendered by default.

packages/pi-plugin/src/context-handler.ts

Lines changed: 231 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,21 @@ import {
5454
} from "@magic-context/core/features/magic-context/scheduler";
5555
import { recordSessionProjectIdentity } from "@magic-context/core/features/magic-context/session-project-storage";
5656
import {
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 = /^pi-msg-\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
*/
13071403
function 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

Comments
 (0)