@truefoundry/assistant-ui-runtime 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1304,6 +1304,280 @@ function replaceSessionSnapshot(snapshot, patch) {
1304
1304
  // src/convertTurnMessages.ts
1305
1305
  var TURN_EVENTS_PAGE_SIZE = 25;
1306
1306
  var SESSION_EVENTS_PAGE_SIZE = 100;
1307
+ var MAX_HISTORY_BOUNDARY_PAGES = 10;
1308
+ function trimIncompleteLeadingEvents(itemsAsc) {
1309
+ const start = itemsAsc.findIndex((item) => item.event.type === "turn.created");
1310
+ if (start === -1) {
1311
+ return [];
1312
+ }
1313
+ return itemsAsc.slice(start);
1314
+ }
1315
+ function oldestCompleteTurnGroupState(itemsAsc) {
1316
+ const trimmed = trimIncompleteLeadingEvents(itemsAsc);
1317
+ if (trimmed.length === 0) {
1318
+ return "incomplete";
1319
+ }
1320
+ const created = trimmed[0];
1321
+ if (created?.event.type !== "turn.created") {
1322
+ return "incomplete";
1323
+ }
1324
+ const hasDone = trimmed.some(
1325
+ (item) => item.turnId === created.turnId && item.event.type === "turn.done"
1326
+ );
1327
+ if (!hasDone) {
1328
+ return "incomplete";
1329
+ }
1330
+ return extractTurnUserText(created.event.input) != null ? "user-group" : "continuation";
1331
+ }
1332
+ async function fetchSessionEventsPage(session, options) {
1333
+ const page = await session.listEvents({
1334
+ limit: SESSION_EVENTS_PAGE_SIZE,
1335
+ ...options?.lastTurnId != null ? { lastTurnId: options.lastTurnId } : {},
1336
+ ...options?.pageToken != null ? { pageToken: options.pageToken } : {}
1337
+ });
1338
+ const response = page.response;
1339
+ const olderPageToken = response.pagination?.nextPageToken;
1340
+ const hasOlder = typeof page.hasNextPage === "function" ? page.hasNextPage() : olderPageToken != null && olderPageToken !== "";
1341
+ return {
1342
+ itemsNewestFirst: page.data,
1343
+ ...olderPageToken != null && olderPageToken !== "" ? { olderPageToken } : {},
1344
+ hasOlder
1345
+ };
1346
+ }
1347
+ async function fetchSessionEventsWindow(session, options) {
1348
+ let itemsNewestFirst = [];
1349
+ let pageToken = options?.pageToken;
1350
+ let olderPageToken;
1351
+ let hasOlder = false;
1352
+ for (let pageCount = 0; pageCount < MAX_HISTORY_BOUNDARY_PAGES; pageCount++) {
1353
+ const page = await fetchSessionEventsPage(session, {
1354
+ ...options,
1355
+ ...pageToken != null ? { pageToken } : {}
1356
+ });
1357
+ itemsNewestFirst = [...itemsNewestFirst, ...page.itemsNewestFirst];
1358
+ olderPageToken = page.olderPageToken;
1359
+ hasOlder = page.hasOlder;
1360
+ const itemsAsc2 = trimIncompleteLeadingEvents([...itemsNewestFirst].reverse());
1361
+ const groupState = oldestCompleteTurnGroupState(itemsAsc2);
1362
+ if (groupState === "user-group" || !hasOlder) {
1363
+ return {
1364
+ itemsAsc: itemsAsc2,
1365
+ ...olderPageToken != null ? { olderPageToken } : {},
1366
+ hasOlder
1367
+ };
1368
+ }
1369
+ if (olderPageToken == null) {
1370
+ return { itemsAsc: itemsAsc2, hasOlder: false };
1371
+ }
1372
+ pageToken = olderPageToken;
1373
+ }
1374
+ const itemsAsc = trimIncompleteLeadingEvents([...itemsNewestFirst].reverse());
1375
+ return {
1376
+ itemsAsc,
1377
+ ...olderPageToken != null ? { olderPageToken } : {},
1378
+ hasOlder
1379
+ };
1380
+ }
1381
+ async function fetchAllSessionEvents(session, options) {
1382
+ const items = [];
1383
+ for await (const item of await session.listEvents({
1384
+ limit: SESSION_EVENTS_PAGE_SIZE,
1385
+ ...options?.lastTurnId != null ? { lastTurnId: options.lastTurnId } : {}
1386
+ })) {
1387
+ items.push(item);
1388
+ }
1389
+ items.reverse();
1390
+ return items;
1391
+ }
1392
+ function cloneThreadBucket(bucket) {
1393
+ return {
1394
+ events: new Map(bucket.events),
1395
+ modelMessageIds: [...bucket.modelMessageIds],
1396
+ toolResults: new Map(bucket.toolResults),
1397
+ pendingApprovals: new Map(bucket.pendingApprovals),
1398
+ approvalDecisions: new Map(bucket.approvalDecisions),
1399
+ pendingResponses: new Map(bucket.pendingResponses),
1400
+ done: bucket.done,
1401
+ ...bucket.title != null ? { title: bucket.title } : {},
1402
+ ...bucket.agentInfo != null ? { agentInfo: bucket.agentInfo } : {}
1403
+ };
1404
+ }
1405
+ function prependFoldState(older, newer) {
1406
+ const result = new PeerThreadFoldState();
1407
+ const threadIds = /* @__PURE__ */ new Set([...older.threads.keys(), ...newer.threads.keys()]);
1408
+ for (const threadId of threadIds) {
1409
+ const olderBucket = older.threads.get(threadId);
1410
+ const newerBucket = newer.threads.get(threadId);
1411
+ if (olderBucket == null && newerBucket != null) {
1412
+ result.threads.set(threadId, cloneThreadBucket(newerBucket));
1413
+ continue;
1414
+ }
1415
+ if (newerBucket == null && olderBucket != null) {
1416
+ result.threads.set(threadId, cloneThreadBucket(olderBucket));
1417
+ continue;
1418
+ }
1419
+ if (olderBucket == null || newerBucket == null) {
1420
+ continue;
1421
+ }
1422
+ result.threads.set(threadId, {
1423
+ events: new Map([...olderBucket.events, ...newerBucket.events]),
1424
+ modelMessageIds: [
1425
+ ...olderBucket.modelMessageIds,
1426
+ ...newerBucket.modelMessageIds
1427
+ ],
1428
+ toolResults: new Map([
1429
+ ...olderBucket.toolResults,
1430
+ ...newerBucket.toolResults
1431
+ ]),
1432
+ pendingApprovals: new Map([
1433
+ ...olderBucket.pendingApprovals,
1434
+ ...newerBucket.pendingApprovals
1435
+ ]),
1436
+ approvalDecisions: new Map([
1437
+ ...olderBucket.approvalDecisions,
1438
+ ...newerBucket.approvalDecisions
1439
+ ]),
1440
+ pendingResponses: new Map([
1441
+ ...olderBucket.pendingResponses,
1442
+ ...newerBucket.pendingResponses
1443
+ ]),
1444
+ done: newerBucket.done || olderBucket.done,
1445
+ ...newerBucket.title != null || olderBucket.title != null ? { title: newerBucket.title ?? olderBucket.title } : {},
1446
+ ...newerBucket.agentInfo != null || olderBucket.agentInfo != null ? { agentInfo: newerBucket.agentInfo ?? olderBucket.agentInfo } : {}
1447
+ });
1448
+ }
1449
+ for (const [threadId, link] of older.threadParents) {
1450
+ result.threadParents.set(threadId, link);
1451
+ }
1452
+ for (const [threadId, link] of newer.threadParents) {
1453
+ result.threadParents.set(threadId, link);
1454
+ }
1455
+ return result;
1456
+ }
1457
+ function ingestSessionEventsIntoSnapshot(snapshot, items, onTurnComplete) {
1458
+ let currentTurnId = null;
1459
+ let currentCreatedEvent = null;
1460
+ let currentContentEvents = [];
1461
+ let beforeCount = 0;
1462
+ for (const item of items) {
1463
+ const { turnId, event } = item;
1464
+ if (event.type === "turn.created") {
1465
+ currentTurnId = turnId;
1466
+ currentCreatedEvent = event;
1467
+ currentContentEvents = [];
1468
+ beforeCount = snapshot.fold.threads.get(ROOT_THREAD_ID)?.modelMessageIds.length ?? 0;
1469
+ } else if (event.type === "turn.done") {
1470
+ if (currentTurnId == null || currentCreatedEvent == null) {
1471
+ continue;
1472
+ }
1473
+ const afterBucket = snapshot.fold.threads.get(ROOT_THREAD_ID);
1474
+ const rootModelMessageIds = (afterBucket?.modelMessageIds ?? []).slice(
1475
+ beforeCount
1476
+ );
1477
+ const sandboxEvent = currentContentEvents.find(
1478
+ (ev) => ev.type === "sandbox.created"
1479
+ );
1480
+ applyUserToolResponsesToFold(
1481
+ snapshot.fold,
1482
+ currentCreatedEvent.input ?? []
1483
+ );
1484
+ snapshot.turns.push(
1485
+ sessionEventsToSessionRecord(
1486
+ currentTurnId,
1487
+ currentCreatedEvent,
1488
+ event,
1489
+ rootModelMessageIds,
1490
+ sandboxEvent?.sandboxId
1491
+ )
1492
+ );
1493
+ onTurnComplete?.(replaceSessionSnapshot(snapshot, {}));
1494
+ currentTurnId = null;
1495
+ currentCreatedEvent = null;
1496
+ currentContentEvents = [];
1497
+ } else {
1498
+ if (currentTurnId != null) {
1499
+ ingestTurnEvent(snapshot.fold, event);
1500
+ currentContentEvents.push(event);
1501
+ }
1502
+ }
1503
+ }
1504
+ }
1505
+ function attachRunningTurn(snapshot, runningTurn) {
1506
+ if (runningTurn == null) {
1507
+ return snapshot;
1508
+ }
1509
+ const pendingUserText = extractTurnUserText(runningTurn.input);
1510
+ return replaceSessionSnapshot(snapshot, {
1511
+ runningTurn,
1512
+ unstable_resume: true,
1513
+ groupRootBaseline: computeGroupRootBaseline(snapshot.turns),
1514
+ ...pendingUserText ? {
1515
+ pendingUser: {
1516
+ turnId: runningTurn.id,
1517
+ content: extractTurnUserMessageContent(runningTurn.input),
1518
+ createdAt: new Date(runningTurn.createdAt)
1519
+ }
1520
+ } : {}
1521
+ });
1522
+ }
1523
+ async function buildSnapshotFromSessionEvents(session, onProgress) {
1524
+ const turnsPage = await session.listTurns({ limit: 1 });
1525
+ const newestTurn = turnsPage.data[0];
1526
+ const runningTurn = newestTurn?.state?.status === "running" ? newestTurn : void 0;
1527
+ const window = await fetchSessionEventsWindow(session);
1528
+ const historyPagination = {
1529
+ hasOlder: window.hasOlder,
1530
+ ...window.olderPageToken != null ? { olderPageToken: window.olderPageToken } : {}
1531
+ };
1532
+ const snapshot = createEmptySessionSnapshot();
1533
+ ingestSessionEventsIntoSnapshot(snapshot, window.itemsAsc, onProgress);
1534
+ const withHistory = replaceSessionSnapshot(snapshot, {
1535
+ historyEvents: window.itemsAsc,
1536
+ historyPagination
1537
+ });
1538
+ return attachRunningTurn(withHistory, runningTurn);
1539
+ }
1540
+ async function prependOlderSessionHistory(session, snapshot) {
1541
+ const pagination = snapshot.historyPagination;
1542
+ if (pagination?.hasOlder !== true || pagination.olderPageToken == null) {
1543
+ return snapshot;
1544
+ }
1545
+ const window = await fetchSessionEventsWindow(session, {
1546
+ pageToken: pagination.olderPageToken
1547
+ });
1548
+ if (window.itemsAsc.length === 0) {
1549
+ return replaceSessionSnapshot(snapshot, {
1550
+ historyPagination: { hasOlder: false }
1551
+ });
1552
+ }
1553
+ const olderSnap = createEmptySessionSnapshot();
1554
+ ingestSessionEventsIntoSnapshot(olderSnap, window.itemsAsc);
1555
+ const existingIds = new Set(snapshot.turns.map((turn) => turn.id));
1556
+ const olderTurns = olderSnap.turns.filter((turn) => !existingIds.has(turn.id));
1557
+ const mergedFold = prependFoldState(olderSnap.fold, snapshot.fold);
1558
+ const historyEvents = [
1559
+ ...window.itemsAsc,
1560
+ ...snapshot.historyEvents ?? []
1561
+ ];
1562
+ const olderRootIds = olderTurns.flatMap(
1563
+ (turn) => turn.rootModelMessageIds ?? []
1564
+ );
1565
+ return replaceSessionSnapshot(snapshot, {
1566
+ fold: mergedFold,
1567
+ turns: [...olderTurns, ...snapshot.turns],
1568
+ historyEvents,
1569
+ historyPagination: {
1570
+ hasOlder: window.hasOlder,
1571
+ ...window.olderPageToken != null ? { olderPageToken: window.olderPageToken } : {}
1572
+ },
1573
+ ...snapshot.groupRootBaseline != null ? {
1574
+ groupRootBaseline: [
1575
+ ...olderRootIds,
1576
+ ...snapshot.groupRootBaseline
1577
+ ]
1578
+ } : {}
1579
+ });
1580
+ }
1307
1581
  function assistantStatusFromTurnState(state) {
1308
1582
  switch (state.status) {
1309
1583
  case "done":
@@ -1710,92 +1984,6 @@ function projectSessionMessages(snapshot, options) {
1710
1984
  );
1711
1985
  }
1712
1986
  var DEFAULT_LIST_EVENTS_CONCURRENCY = 5;
1713
- async function fetchAllSessionEvents(session, options) {
1714
- const items = [];
1715
- for await (const item of await session.listEvents({
1716
- limit: SESSION_EVENTS_PAGE_SIZE,
1717
- ...options?.lastTurnId != null ? { lastTurnId: options.lastTurnId } : {}
1718
- })) {
1719
- items.push(item);
1720
- }
1721
- items.reverse();
1722
- return items;
1723
- }
1724
- function ingestSessionEventsIntoSnapshot(snapshot, items, onTurnComplete) {
1725
- let currentTurnId = null;
1726
- let currentCreatedEvent = null;
1727
- let currentContentEvents = [];
1728
- let beforeCount = 0;
1729
- for (const item of items) {
1730
- const { turnId, event } = item;
1731
- if (event.type === "turn.created") {
1732
- currentTurnId = turnId;
1733
- currentCreatedEvent = event;
1734
- currentContentEvents = [];
1735
- beforeCount = snapshot.fold.threads.get(ROOT_THREAD_ID)?.modelMessageIds.length ?? 0;
1736
- } else if (event.type === "turn.done") {
1737
- if (currentTurnId == null || currentCreatedEvent == null) {
1738
- continue;
1739
- }
1740
- const afterBucket = snapshot.fold.threads.get(ROOT_THREAD_ID);
1741
- const rootModelMessageIds = (afterBucket?.modelMessageIds ?? []).slice(
1742
- beforeCount
1743
- );
1744
- const sandboxEvent = currentContentEvents.find(
1745
- (ev) => ev.type === "sandbox.created"
1746
- );
1747
- applyUserToolResponsesToFold(
1748
- snapshot.fold,
1749
- currentCreatedEvent.input ?? []
1750
- );
1751
- snapshot.turns.push(
1752
- sessionEventsToSessionRecord(
1753
- currentTurnId,
1754
- currentCreatedEvent,
1755
- event,
1756
- rootModelMessageIds,
1757
- sandboxEvent?.sandboxId
1758
- )
1759
- );
1760
- onTurnComplete?.(replaceSessionSnapshot(snapshot, {}));
1761
- currentTurnId = null;
1762
- currentCreatedEvent = null;
1763
- currentContentEvents = [];
1764
- } else {
1765
- if (currentTurnId != null) {
1766
- ingestTurnEvent(snapshot.fold, event);
1767
- currentContentEvents.push(event);
1768
- }
1769
- }
1770
- }
1771
- }
1772
- async function buildSnapshotFromSessionEvents(session, onProgress) {
1773
- let runningTurn;
1774
- for await (const turn of await session.listTurns({ limit: 1 })) {
1775
- if (turn.state?.status === "running") {
1776
- runningTurn = turn;
1777
- }
1778
- }
1779
- const items = await fetchAllSessionEvents(session);
1780
- const snapshot = createEmptySessionSnapshot();
1781
- ingestSessionEventsIntoSnapshot(snapshot, items, onProgress);
1782
- if (runningTurn == null) {
1783
- return snapshot;
1784
- }
1785
- const pendingUserText = extractTurnUserText(runningTurn.input);
1786
- return replaceSessionSnapshot(snapshot, {
1787
- runningTurn,
1788
- unstable_resume: true,
1789
- groupRootBaseline: computeGroupRootBaseline(snapshot.turns),
1790
- ...pendingUserText ? {
1791
- pendingUser: {
1792
- turnId: runningTurn.id,
1793
- content: extractTurnUserMessageContent(runningTurn.input),
1794
- createdAt: new Date(runningTurn.createdAt)
1795
- }
1796
- } : {}
1797
- });
1798
- }
1799
1987
  function ingestTurnsIntoSnapshot(snapshot, turns, eventArrays) {
1800
1988
  let runningTurn;
1801
1989
  for (let i = 0; i < turns.length; i++) {
@@ -1834,9 +2022,18 @@ async function buildSnapshotFromSession(session, concurrency = DEFAULT_LIST_EVEN
1834
2022
  return replaceSessionSnapshot(snapshot, {
1835
2023
  runningTurn: turn,
1836
2024
  unstable_resume: true,
1837
- groupRootBaseline: computeGroupRootBaseline(snapshot.turns)
2025
+ groupRootBaseline: computeGroupRootBaseline(snapshot.turns),
2026
+ pendingUser: void 0
1838
2027
  });
1839
2028
  }
2029
+ async function buildSnapshotBeforeTurn(session, beforeTurnId, concurrency = DEFAULT_LIST_EVENTS_CONCURRENCY) {
2030
+ const turns = await listSessionTurnsOrdered(session);
2031
+ const beforeIndex = turns.findIndex((turn) => turn.id === beforeTurnId);
2032
+ if (beforeIndex === -1) {
2033
+ throw new Error(`Turn ${beforeTurnId} not found in session`);
2034
+ }
2035
+ return buildSnapshotBeforeTurnIndex(session, beforeIndex, concurrency, turns);
2036
+ }
1840
2037
  async function buildSnapshotBeforeTurnIndex(session, turnIndex, _concurrency = DEFAULT_LIST_EVENTS_CONCURRENCY, orderedTurns) {
1841
2038
  if (turnIndex <= 0) {
1842
2039
  return createEmptySessionSnapshot();
@@ -1859,6 +2056,11 @@ async function resolveGatewayBranchPreviousTurnId(session, turnIndex, orderedTur
1859
2056
  const turns = orderedTurns ?? await listSessionTurnsOrdered(session);
1860
2057
  return turns[turnIndex - 1]?.id ?? null;
1861
2058
  }
2059
+ async function resolveGatewayBranchPreviousTurnIdForTurn(session, turnId) {
2060
+ const turns = await listSessionTurnsOrdered(session);
2061
+ const turnIndex = turns.findIndex((turn) => turn.id === turnId);
2062
+ return resolveGatewayBranchPreviousTurnId(session, turnIndex, turns);
2063
+ }
1862
2064
  async function listSessionTurnsOrdered(session) {
1863
2065
  const turns = [];
1864
2066
  for await (const turn of await session.listTurns()) {
@@ -2125,6 +2327,7 @@ function repositoryItemsFromMessages(messages) {
2125
2327
  }
2126
2328
 
2127
2329
  // src/private/draftSessionBridge.ts
2330
+ var DRAFT_SESSION_LAST_UPDATED_AT_HEADER = "x-tfy-session-last-updated-at";
2128
2331
  function createDraftSessionBridge(gateway) {
2129
2332
  async function getDraft(draftSessionId) {
2130
2333
  const response = await gateway.agents.private.draftSessions.get(draftSessionId);
@@ -2136,7 +2339,11 @@ function createDraftSessionBridge(gateway) {
2136
2339
  return draft.agentSpec;
2137
2340
  },
2138
2341
  async syncAgentSpec(draftSessionId, agentSpec) {
2139
- await gateway.agents.private.draftSessions.update(draftSessionId, { agentSpec });
2342
+ const response = await gateway.agents.private.draftSessions.update(
2343
+ draftSessionId,
2344
+ { agentSpec }
2345
+ );
2346
+ return response.data.updatedAt;
2140
2347
  }
2141
2348
  };
2142
2349
  }
@@ -3392,6 +3599,29 @@ function useDraftAgentSpec({
3392
3599
  const syncGenerationRef = useRef(0);
3393
3600
  const loadedDraftIdRef = useRef(void 0);
3394
3601
  const localDirtyRef = useRef(false);
3602
+ const lastUpdatedAtRef = useRef(void 0);
3603
+ const pendingFlushRef = useRef(void 0);
3604
+ const inFlightFlushRef = useRef(void 0);
3605
+ const activeDraftIdRef = useRef(draftSessionId);
3606
+ useEffect(() => {
3607
+ const previousDraftId = activeDraftIdRef.current;
3608
+ if (previousDraftId === draftSessionId) {
3609
+ return;
3610
+ }
3611
+ activeDraftIdRef.current = draftSessionId;
3612
+ syncGenerationRef.current++;
3613
+ if (syncTimeoutRef.current != null) {
3614
+ clearTimeout(syncTimeoutRef.current);
3615
+ syncTimeoutRef.current = void 0;
3616
+ }
3617
+ pendingFlushRef.current = void 0;
3618
+ inFlightFlushRef.current = void 0;
3619
+ lastUpdatedAtRef.current = void 0;
3620
+ setIsSpecSyncing(false);
3621
+ if (previousDraftId != null) {
3622
+ localDirtyRef.current = false;
3623
+ }
3624
+ }, [draftSessionId]);
3395
3625
  useEffect(() => {
3396
3626
  if (!enabled || draftBridge == null) {
3397
3627
  return;
@@ -3440,10 +3670,11 @@ function useDraftAgentSpec({
3440
3670
  }
3441
3671
  setIsSpecSyncing(true);
3442
3672
  try {
3443
- await draftBridge.syncAgentSpec(draftId, spec);
3673
+ const updatedAt = await draftBridge.syncAgentSpec(draftId, spec);
3444
3674
  if (generation !== syncGenerationRef.current) {
3445
3675
  return;
3446
3676
  }
3677
+ lastUpdatedAtRef.current = updatedAt || (/* @__PURE__ */ new Date()).toISOString();
3447
3678
  setSpecError(null);
3448
3679
  onAgentSpecChange?.(spec);
3449
3680
  } catch (error) {
@@ -3465,14 +3696,47 @@ function useDraftAgentSpec({
3465
3696
  clearTimeout(syncTimeoutRef.current);
3466
3697
  }
3467
3698
  const generation = ++syncGenerationRef.current;
3699
+ const flush = () => {
3700
+ pendingFlushRef.current = void 0;
3701
+ syncTimeoutRef.current = void 0;
3702
+ const promise = flushSpecSync(draftId, spec, generation).finally(() => {
3703
+ if (inFlightFlushRef.current === promise) {
3704
+ inFlightFlushRef.current = void 0;
3705
+ }
3706
+ });
3707
+ inFlightFlushRef.current = promise;
3708
+ return promise;
3709
+ };
3710
+ pendingFlushRef.current = flush;
3468
3711
  syncTimeoutRef.current = setTimeout(() => {
3469
- void flushSpecSync(draftId, spec, generation);
3712
+ void flush();
3470
3713
  }, SPEC_SYNC_DEBOUNCE_MS);
3471
3714
  },
3472
3715
  [flushSpecSync]
3473
3716
  );
3474
3717
  const scheduleSpecSyncRef = useRef(scheduleSpecSync);
3475
3718
  scheduleSpecSyncRef.current = scheduleSpecSync;
3719
+ const flushPendingSpecSyncNow = useCallback(async () => {
3720
+ if (syncTimeoutRef.current != null) {
3721
+ clearTimeout(syncTimeoutRef.current);
3722
+ syncTimeoutRef.current = void 0;
3723
+ }
3724
+ const pending = pendingFlushRef.current;
3725
+ if (pending != null) {
3726
+ pendingFlushRef.current = void 0;
3727
+ await pending();
3728
+ return;
3729
+ }
3730
+ if (inFlightFlushRef.current != null) {
3731
+ await inFlightFlushRef.current;
3732
+ }
3733
+ }, []);
3734
+ const takeTurnHeaderTimestamp = useCallback(async () => {
3735
+ await flushPendingSpecSyncNow();
3736
+ const updatedAt = lastUpdatedAtRef.current;
3737
+ lastUpdatedAtRef.current = void 0;
3738
+ return updatedAt;
3739
+ }, [flushPendingSpecSyncNow]);
3476
3740
  useEffect(
3477
3741
  () => () => {
3478
3742
  if (syncTimeoutRef.current != null) {
@@ -3501,7 +3765,8 @@ function useDraftAgentSpec({
3501
3765
  draftSessionId: enabled ? draftSessionId : void 0,
3502
3766
  isSpecSyncing: enabled ? isSpecSyncing : false,
3503
3767
  specError: enabled ? specError : null,
3504
- updateAgentSpec
3768
+ updateAgentSpec,
3769
+ takeTurnHeaderTimestamp
3505
3770
  }),
3506
3771
  [
3507
3772
  agentSpec,
@@ -3509,6 +3774,7 @@ function useDraftAgentSpec({
3509
3774
  enabled,
3510
3775
  isSpecSyncing,
3511
3776
  specError,
3777
+ takeTurnHeaderTimestamp,
3512
3778
  updateAgentSpec
3513
3779
  ]
3514
3780
  );
@@ -3593,7 +3859,13 @@ async function* streamTurnContent(session, foldState, options, abortSignal, grou
3593
3859
  }
3594
3860
  try {
3595
3861
  yield* streamTurnEvents(
3596
- turn.execute({ stream: true }, { abortSignal }),
3862
+ turn.execute(
3863
+ { stream: true },
3864
+ {
3865
+ abortSignal,
3866
+ ...options.headers != null ? { headers: options.headers } : {}
3867
+ }
3868
+ ),
3597
3869
  foldState,
3598
3870
  groupRootBaseline
3599
3871
  );
@@ -3745,16 +4017,6 @@ async function resolveActiveSessionId(remoteId, resolveConversationSessionId) {
3745
4017
  }
3746
4018
  return remoteId;
3747
4019
  }
3748
- function findTurnIndex(snapshot, turnId) {
3749
- const committedIndex = snapshot.turns.findIndex((turn) => turn.id === turnId);
3750
- if (committedIndex !== -1) {
3751
- return committedIndex;
3752
- }
3753
- if (snapshot.pendingUser?.turnId === turnId) {
3754
- return snapshot.turns.length;
3755
- }
3756
- throw new Error(`Turn ${turnId} not found in session snapshot`);
3757
- }
3758
4020
  function resolveTurnInput(snapshot, turnId) {
3759
4021
  const turnRecord = snapshot.turns.find((turn) => turn.id === turnId);
3760
4022
  if (turnRecord?.input != null) {
@@ -3773,7 +4035,8 @@ function useTrueFoundryAgentMessages({
3773
4035
  onError,
3774
4036
  initializeSession,
3775
4037
  resolveConversationSessionId,
3776
- draftGateway
4038
+ draftGateway,
4039
+ getTurnHeaders
3777
4040
  }) {
3778
4041
  const sessionOptions = useMemo2(
3779
4042
  () => draftGateway != null ? { draftGateway } : void 0,
@@ -3782,15 +4045,19 @@ function useTrueFoundryAgentMessages({
3782
4045
  const [snapshot, setSnapshot] = useState2(createEmptySessionSnapshot);
3783
4046
  const [isRunning, setIsRunning] = useState2(false);
3784
4047
  const [isLoading, setIsLoading] = useState2(false);
4048
+ const [isLoadingOlderHistory, setIsLoadingOlderHistory] = useState2(false);
3785
4049
  const [loadRetryTrigger, setLoadRetryTrigger] = useState2(0);
3786
4050
  const snapshotRef = useRef2(snapshot);
3787
4051
  snapshotRef.current = snapshot;
4052
+ const loadOlderInflightRef = useRef2(null);
3788
4053
  const onErrorRef = useRef2(onError);
3789
4054
  onErrorRef.current = onError;
3790
4055
  const resolveConversationSessionIdRef = useRef2(resolveConversationSessionId);
3791
4056
  resolveConversationSessionIdRef.current = resolveConversationSessionId;
3792
4057
  const initializeSessionRef = useRef2(initializeSession);
3793
4058
  initializeSessionRef.current = initializeSession;
4059
+ const getTurnHeadersRef = useRef2(getTurnHeaders);
4060
+ getTurnHeadersRef.current = getTurnHeaders;
3794
4061
  const createdAtByMessageIdRef = useRef2(/* @__PURE__ */ new Map());
3795
4062
  const abortControllerRef = useRef2(null);
3796
4063
  const activeRunRef = useRef2(null);
@@ -3920,9 +4187,11 @@ function useTrueFoundryAgentMessages({
3920
4187
  const generation = ++loadGenerationRef.current;
3921
4188
  ++streamGenerationRef.current;
3922
4189
  abortControllerRef.current?.abort();
4190
+ loadOlderInflightRef.current = null;
3923
4191
  createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
3924
4192
  setSnapshot(createEmptySessionSnapshot());
3925
4193
  setIsLoading(true);
4194
+ setIsLoadingOlderHistory(false);
3926
4195
  try {
3927
4196
  const conversationSessionId = await resolveActiveSessionId(
3928
4197
  sessionId,
@@ -3990,6 +4259,8 @@ function useTrueFoundryAgentMessages({
3990
4259
  resolveConversationSessionIdRef.current
3991
4260
  );
3992
4261
  const session = await getSession(client, conversationSessionId, sessionOptions);
4262
+ const turnHeaders = await getTurnHeadersRef.current?.();
4263
+ const streamHeaders = turnHeaders != null ? { headers: turnHeaders } : {};
3993
4264
  const isContinuation = "inputs" in options || "resumeMcpAuth" in options && options.resumeMcpAuth === true;
3994
4265
  const continuationTurnId = snapshotRef.current.activeStream?.turnId;
3995
4266
  const turnId = isContinuation && continuationTurnId != null ? continuationTurnId : generateId();
@@ -4048,7 +4319,7 @@ function useTrueFoundryAgentMessages({
4048
4319
  return streamTurnContent(
4049
4320
  session,
4050
4321
  snapshotRef.current.fold,
4051
- { inputs: options.inputs },
4322
+ { inputs: options.inputs, ...streamHeaders },
4052
4323
  signal,
4053
4324
  groupRootBaseline
4054
4325
  );
@@ -4057,7 +4328,7 @@ function useTrueFoundryAgentMessages({
4057
4328
  return streamTurnContent(
4058
4329
  session,
4059
4330
  snapshotRef.current.fold,
4060
- { resumeMcpAuth: true },
4331
+ { resumeMcpAuth: true, ...streamHeaders },
4061
4332
  signal,
4062
4333
  groupRootBaseline
4063
4334
  );
@@ -4067,7 +4338,8 @@ function useTrueFoundryAgentMessages({
4067
4338
  snapshotRef.current.fold,
4068
4339
  {
4069
4340
  userMessage: options.userMessage,
4070
- ...options.previousTurnId !== void 0 ? { previousTurnId: options.previousTurnId } : {}
4341
+ ...options.previousTurnId !== void 0 ? { previousTurnId: options.previousTurnId } : {},
4342
+ ...streamHeaders
4071
4343
  },
4072
4344
  signal,
4073
4345
  groupRootBaseline
@@ -4171,20 +4443,19 @@ function useTrueFoundryAgentMessages({
4171
4443
  }
4172
4444
  const committed = commitActiveStream(snapshotRef.current);
4173
4445
  setSnapshot(committed);
4174
- const turnIndex = findTurnIndex(committed, turnId);
4175
4446
  await cancel();
4176
4447
  const conversationSessionId = await resolveActiveSessionId(
4177
4448
  activeSessionId,
4178
4449
  resolveConversationSessionIdRef.current
4179
4450
  );
4180
4451
  const session = await getSession(client, conversationSessionId, sessionOptions);
4181
- const previousTurnId = await resolveGatewayBranchPreviousTurnId(
4452
+ const previousTurnId = await resolveGatewayBranchPreviousTurnIdForTurn(
4182
4453
  session,
4183
- turnIndex
4454
+ turnId
4184
4455
  );
4185
- const rewound = await buildSnapshotBeforeTurnIndex(
4456
+ const rewound = await buildSnapshotBeforeTurn(
4186
4457
  session,
4187
- turnIndex,
4458
+ turnId,
4188
4459
  listEventsConcurrency
4189
4460
  );
4190
4461
  createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
@@ -4235,10 +4506,64 @@ function useTrueFoundryAgentMessages({
4235
4506
  const retryLoad = useCallback2(() => {
4236
4507
  setLoadRetryTrigger((n) => n + 1);
4237
4508
  }, []);
4509
+ const hasOlderHistory = snapshot.historyPagination?.hasOlder === true;
4510
+ const loadOlderHistory = useCallback2(async () => {
4511
+ if (sessionId == null || isMain === false) {
4512
+ return;
4513
+ }
4514
+ if (loadOlderInflightRef.current != null) {
4515
+ return loadOlderInflightRef.current;
4516
+ }
4517
+ const current = snapshotRef.current;
4518
+ if (current.historyPagination?.hasOlder !== true) {
4519
+ return;
4520
+ }
4521
+ if (current.historyPagination.olderPageToken == null) {
4522
+ return;
4523
+ }
4524
+ const generation = loadGenerationRef.current;
4525
+ setIsLoadingOlderHistory(true);
4526
+ const run = (async () => {
4527
+ try {
4528
+ const conversationSessionId = await resolveActiveSessionId(
4529
+ sessionId,
4530
+ resolveConversationSessionIdRef.current
4531
+ );
4532
+ const session = await getSession(
4533
+ client,
4534
+ conversationSessionId,
4535
+ sessionOptions
4536
+ );
4537
+ const next = await prependOlderSessionHistory(
4538
+ session,
4539
+ snapshotRef.current
4540
+ );
4541
+ if (generation !== loadGenerationRef.current) {
4542
+ return;
4543
+ }
4544
+ setSnapshot(next);
4545
+ } catch (error) {
4546
+ if (generation === loadGenerationRef.current) {
4547
+ onErrorRef.current?.(error);
4548
+ }
4549
+ throw error;
4550
+ } finally {
4551
+ if (generation === loadGenerationRef.current) {
4552
+ setIsLoadingOlderHistory(false);
4553
+ }
4554
+ loadOlderInflightRef.current = null;
4555
+ }
4556
+ })();
4557
+ loadOlderInflightRef.current = run;
4558
+ return run;
4559
+ }, [client, isMain, sessionId, sessionOptions]);
4238
4560
  return {
4239
4561
  messages,
4240
4562
  isRunning,
4241
4563
  isLoading,
4564
+ isLoadingOlderHistory,
4565
+ hasOlderHistory,
4566
+ loadOlderHistory,
4242
4567
  retryLoad,
4243
4568
  sendTurn,
4244
4569
  cancel,
@@ -4279,6 +4604,18 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
4279
4604
  onAgentSpecChange: agent.mode === "draft" ? agent.onAgentSpecChange : void 0,
4280
4605
  onError
4281
4606
  });
4607
+ const takeTurnHeaderTimestampRef = useRef3(draftSpec.takeTurnHeaderTimestamp);
4608
+ takeTurnHeaderTimestampRef.current = draftSpec.takeTurnHeaderTimestamp;
4609
+ const getTurnHeaders = useCallback3(async () => {
4610
+ if (agent.mode !== "draft") {
4611
+ return void 0;
4612
+ }
4613
+ const updatedAt = await takeTurnHeaderTimestampRef.current();
4614
+ if (updatedAt == null) {
4615
+ return void 0;
4616
+ }
4617
+ return { [DRAFT_SESSION_LAST_UPDATED_AT_HEADER]: updatedAt };
4618
+ }, [agent.mode]);
4282
4619
  const aui = useAui();
4283
4620
  const initializeSession = useCallback3(
4284
4621
  () => aui.threadListItem().initialize(),
@@ -4290,6 +4627,9 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
4290
4627
  messages,
4291
4628
  isRunning,
4292
4629
  isLoading,
4630
+ isLoadingOlderHistory,
4631
+ hasOlderHistory,
4632
+ loadOlderHistory,
4293
4633
  sendTurn,
4294
4634
  cancel,
4295
4635
  respondToToolApproval,
@@ -4305,7 +4645,8 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
4305
4645
  listEventsConcurrency,
4306
4646
  onError,
4307
4647
  initializeSession,
4308
- draftGateway: agent.mode === "draft" ? gateway : void 0
4648
+ draftGateway: agent.mode === "draft" ? gateway : void 0,
4649
+ getTurnHeaders: agent.mode === "draft" ? getTurnHeaders : void 0
4309
4650
  });
4310
4651
  if (agent.mode === "draft" && draftSpec.agentSpec != null) {
4311
4652
  pendingAgentSpecRef.current = draftSpec.agentSpec;
@@ -4374,6 +4715,9 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
4374
4715
  onError?.(error);
4375
4716
  }),
4376
4717
  reload: retryLoad,
4718
+ hasOlderHistory,
4719
+ isLoadingOlderHistory,
4720
+ loadOlderHistory,
4377
4721
  draft: draftExtras
4378
4722
  }),
4379
4723
  unstable_enableToolInvocations: true,
@@ -4522,6 +4866,19 @@ var useTrueFoundryReload = () => {
4522
4866
  const aui = useAui2();
4523
4867
  return () => trueFoundryExtras.get(aui).reload();
4524
4868
  };
4869
+ var useTrueFoundryHistoryPagination = () => {
4870
+ const extras = trueFoundryExtras.use((e) => e, void 0);
4871
+ return useMemo4(
4872
+ () => ({
4873
+ hasOlderHistory: extras?.hasOlderHistory ?? false,
4874
+ isLoadingOlderHistory: extras?.isLoadingOlderHistory ?? false,
4875
+ loadOlderHistory: extras?.loadOlderHistory ?? (async () => {
4876
+ throw new Error("TrueFoundry runtime is not ready yet");
4877
+ })
4878
+ }),
4879
+ [extras]
4880
+ );
4881
+ };
4525
4882
  var useTrueFoundryResetFromTurn = () => {
4526
4883
  const aui = useAui2();
4527
4884
  return (turnId) => trueFoundryExtras.get(aui).resetFromTurn(turnId);
@@ -4619,6 +4976,7 @@ export {
4619
4976
  useTrueFoundryApprovals,
4620
4977
  useTrueFoundryCancel,
4621
4978
  useTrueFoundryDownloadSandboxFile,
4979
+ useTrueFoundryHistoryPagination,
4622
4980
  useTrueFoundryMcpAuth,
4623
4981
  useTrueFoundryReload,
4624
4982
  useTrueFoundryResetFromTurn,