@truefoundry/assistant-ui-runtime 0.1.15 → 0.1.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truefoundry/assistant-ui-runtime",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "TrueFoundry Gateway agent runtime adapter for assistant-ui",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -18,8 +18,8 @@ import type {
18
18
  import { ROOT_THREAD_ID } from "./constants.js";
19
19
  import { collectPendingToolResponses } from "./collectPending.js";
20
20
  import {
21
- buildSnapshotBeforeTurnIndex,
22
21
  buildSnapshotFromSessionEvents,
22
+ buildSnapshotThroughTurn,
23
23
  buildTurnAssistantContent,
24
24
  buildUserMessageContent,
25
25
  buildUserMessageFromTurnInput,
@@ -28,6 +28,7 @@ import {
28
28
  prependOlderSessionHistory,
29
29
  projectSessionMessages,
30
30
  repositoryItemsFromMessages,
31
+ resolveGatewayBranchPreviousTurnIdForTurn,
31
32
  streamTurnEvents,
32
33
  turnStreamUpdateToAssistantMessage,
33
34
  } from "./convertTurnMessages.js";
@@ -135,22 +136,68 @@ function mockTurn(
135
136
  };
136
137
  }
137
138
 
139
+ /** Mirror runtime listTurns normalization: ASC stays, DESC (or tied) reverses. */
140
+ function toOldestFirstTurns(turns: TurnFixture[]): TurnFixture[] {
141
+ const chain = turns.filter((turn) => turn.state.status !== "running");
142
+ let newestFirst = chain.length > 1;
143
+ for (let i = 1; i < chain.length; i++) {
144
+ const newer = chain[i];
145
+ const older = chain[i - 1];
146
+ if (newer == null || older == null) {
147
+ continue;
148
+ }
149
+ const delta = Date.parse(newer.createdAt) - Date.parse(older.createdAt);
150
+ if (delta < 0) {
151
+ newestFirst = true;
152
+ break;
153
+ }
154
+ if (delta > 0) {
155
+ newestFirst = false;
156
+ break;
157
+ }
158
+ }
159
+ return newestFirst ? [...chain].reverse() : [...chain];
160
+ }
161
+
162
+ /** Mirrors the host's ancestor walk: parent pointers only, sibling branches excluded. */
163
+ function ancestorChainFromTurns(
164
+ turns: TurnFixture[],
165
+ anchorTurnId: string,
166
+ ): TurnFixture[] {
167
+ const turnsById = new Map(turns.map((turn) => [turn.id, turn]));
168
+ const chain: TurnFixture[] = [];
169
+ const seen = new Set<string>();
170
+ let cursor: string | null | undefined = anchorTurnId;
171
+ while (cursor != null && !seen.has(cursor)) {
172
+ seen.add(cursor);
173
+ const turn = turnsById.get(cursor);
174
+ if (turn == null) {
175
+ break;
176
+ }
177
+ if (turn.state.status !== "running") {
178
+ chain.unshift(turn);
179
+ }
180
+ cursor = turn.previousTurnId;
181
+ }
182
+ return chain;
183
+ }
184
+
138
185
  /**
139
- * Builds session-level event items from per-turn mocks — newest-first turns,
140
- * running turns excluded (matches session.listEvents API contract).
186
+ * Builds session-level event items from per-turn mocks — accepts ASC or DESC
187
+ * listTurns order; running turns excluded (matches session.listEvents). An
188
+ * anchor selects that turn's ancestor chain, not a chronological prefix.
141
189
  */
142
190
  function sessionEventItemsFromTurns(
143
- turnsNewestFirst: TurnFixture[],
191
+ turns: TurnFixture[],
144
192
  lastTurnId?: string,
145
193
  ): SessionEventItem[] {
146
- let chain = turnsNewestFirst.filter((turn) => turn.state.status !== "running");
147
- if (lastTurnId != null) {
148
- const anchorIndex = chain.findIndex((turn) => turn.id === lastTurnId);
149
- chain = anchorIndex === -1 ? [] : chain.slice(anchorIndex);
150
- }
194
+ const chain =
195
+ lastTurnId == null
196
+ ? toOldestFirstTurns(turns)
197
+ : ancestorChainFromTurns(turns, lastTurnId);
151
198
 
152
199
  const items: SessionEventItem[] = [];
153
- for (const turn of [...chain].reverse()) {
200
+ for (const turn of chain) {
154
201
  items.push({
155
202
  turnId: turn.id,
156
203
  event: {
@@ -160,6 +207,9 @@ function sessionEventItemsFromTurns(
160
207
  input: turn.input,
161
208
  state: { status: "running" },
162
209
  createdAt: turn.createdAt,
210
+ ...(turn.previousTurnId === undefined
211
+ ? {}
212
+ : { previousTurnId: turn.previousTurnId }),
163
213
  },
164
214
  });
165
215
  for (const event of turn.events) {
@@ -180,10 +230,18 @@ function sessionEventItemsFromTurns(
180
230
 
181
231
  function mockServerWithTurns(turns: TurnFixture[]): AgentChatServer {
182
232
  const eventsByTurnId = new Map(turns.map((turn) => [turn.id, turn.events]));
233
+ const turnsById = new Map(turns.map((turn) => [turn.id, turn]));
183
234
  return {
184
235
  listTurns: async ({ limit }: { limit?: number } = {}) => ({
185
236
  data: limit != null ? turns.slice(0, limit) : turns,
186
237
  }),
238
+ getTurn: async ({ turnId }: { turnId: string }) => {
239
+ const turn = turnsById.get(turnId);
240
+ if (turn == null) {
241
+ throw new Error(`Turn ${turnId} not found`);
242
+ }
243
+ return turn;
244
+ },
187
245
  listEvents: async (opts: { lastTurnId?: string } = {}) => {
188
246
  const items = sessionEventItemsFromTurns(turns, opts.lastTurnId);
189
247
  const newestFirst = [...items].reverse();
@@ -2824,40 +2882,46 @@ describe("buildSnapshotFromSessionEvents", () => {
2824
2882
  });
2825
2883
  });
2826
2884
 
2827
- describe("buildSnapshotBeforeTurnIndex", () => {
2828
- it("rewinds via session.listEvents({ lastTurnId }) excluding the branch turn", async () => {
2829
- const t1 = mockTurn({
2830
- id: "t1",
2831
- createdAt,
2832
- input: [{ type: "user.message", content: "first" }],
2833
- events: [
2834
- modelMessage({ id: "m1", threadId: ROOT_THREAD_ID, content: "reply 1" }),
2885
+ describe("buildSnapshotThroughTurn", () => {
2886
+ /** Linear session t1 -> t2 -> t3, oldest-first. */
2887
+ function linearTurns(): TurnFixture[] {
2888
+ return [
2889
+ mockTurn({
2890
+ id: "t1",
2891
+ createdAt: "2026-01-01T00:00:00.000Z",
2892
+ previousTurnId: null,
2893
+ input: [{ type: "user.message", content: "first" }],
2894
+ events: [
2895
+ modelMessage({ id: "m1", threadId: ROOT_THREAD_ID, content: "reply 1" }),
2835
2896
  ],
2836
- });
2837
- const t2 = mockTurn({
2838
- id: "t2",
2839
- createdAt,
2840
- input: [{ type: "user.message", content: "second" }],
2841
- events: [
2842
- modelMessage({ id: "m2", threadId: ROOT_THREAD_ID, content: "reply 2" }),
2897
+ }),
2898
+ mockTurn({
2899
+ id: "t2",
2900
+ createdAt: "2026-01-01T00:01:00.000Z",
2901
+ previousTurnId: "t1",
2902
+ input: [{ type: "user.message", content: "second" }],
2903
+ events: [
2904
+ modelMessage({ id: "m2", threadId: ROOT_THREAD_ID, content: "reply 2" }),
2843
2905
  ],
2844
- });
2845
- const t3 = mockTurn({
2846
- id: "t3",
2847
- createdAt,
2848
- input: [{ type: "user.message", content: "third" }],
2849
- events: [
2850
- modelMessage({ id: "m3", threadId: ROOT_THREAD_ID, content: "reply 3" }),
2906
+ }),
2907
+ mockTurn({
2908
+ id: "t3",
2909
+ createdAt: "2026-01-01T00:02:00.000Z",
2910
+ previousTurnId: "t2",
2911
+ input: [{ type: "user.message", content: "third" }],
2912
+ events: [
2913
+ modelMessage({ id: "m3", threadId: ROOT_THREAD_ID, content: "reply 3" }),
2851
2914
  ],
2852
- });
2915
+ }),
2916
+ ];
2917
+ }
2853
2918
 
2854
- // listTurns is newest-first.
2855
- const server = mockServerWithTurns([t3, t2, t1]);
2856
- const snapshot = await buildSnapshotBeforeTurnIndex(server, SESSION_ID, 2);
2919
+ it("rewinds to the anchor's ancestor chain, inclusive", async () => {
2920
+ const server = mockServerWithTurns(linearTurns());
2921
+ const snapshot = await buildSnapshotThroughTurn(server, SESSION_ID, "t2");
2857
2922
 
2858
2923
  expect(snapshot.turns.map((turn) => turn.id)).toEqual(["t1", "t2"]);
2859
- const messages = projectSessionMessages(snapshot);
2860
- expect(messages.map((message) => message.id)).toEqual([
2924
+ expect(projectSessionMessages(snapshot).map((message) => message.id)).toEqual([
2861
2925
  "t1-user",
2862
2926
  "t1-assistant",
2863
2927
  "t2-user",
@@ -2865,12 +2929,116 @@ describe("buildSnapshotBeforeTurnIndex", () => {
2865
2929
  ]);
2866
2930
  });
2867
2931
 
2868
- it("returns an empty snapshot when branching from the first turn", async () => {
2869
- const server = mockServerWithTurns([
2870
- mockTurn({ id: "t1", createdAt }),
2871
- ]);
2872
- const snapshot = await buildSnapshotBeforeTurnIndex(server, SESSION_ID, 0);
2932
+ it("returns an empty snapshot for a null anchor (branching from a root turn)", async () => {
2933
+ const server = mockServerWithTurns(linearTurns());
2934
+ const snapshot = await buildSnapshotThroughTurn(server, SESSION_ID, null);
2873
2935
  expect(snapshot.turns).toHaveLength(0);
2874
2936
  });
2937
+
2938
+ it("ignores listTurns page order", async () => {
2939
+ const asc = linearTurns();
2940
+ const ascSnapshot = await buildSnapshotThroughTurn(
2941
+ mockServerWithTurns(asc),
2942
+ SESSION_ID,
2943
+ "t2",
2944
+ );
2945
+ const descSnapshot = await buildSnapshotThroughTurn(
2946
+ mockServerWithTurns([...asc].reverse()),
2947
+ SESSION_ID,
2948
+ "t2",
2949
+ );
2950
+
2951
+ expect(descSnapshot.turns.map((turn) => turn.id)).toEqual(
2952
+ ascSnapshot.turns.map((turn) => turn.id),
2953
+ );
2954
+ expect(descSnapshot.turns.map((turn) => turn.id)).toEqual(["t1", "t2"]);
2955
+ });
2956
+
2957
+ it("drops sibling turns left behind by an earlier rerun", async () => {
2958
+ // m2/m3 were rerun once already: t2/t3 are orphans, t2p/t3p are live.
2959
+ const [t1, t2, t3] = linearTurns();
2960
+ const t2p = mockTurn({
2961
+ id: "t2p",
2962
+ createdAt: "2026-01-01T00:03:00.000Z",
2963
+ previousTurnId: "t1",
2964
+ input: [{ type: "user.message", content: "second again" }],
2965
+ events: [
2966
+ modelMessage({ id: "m2p", threadId: ROOT_THREAD_ID, content: "reply 2p" }),
2967
+ ],
2968
+ });
2969
+ const t3p = mockTurn({
2970
+ id: "t3p",
2971
+ createdAt: "2026-01-01T00:04:00.000Z",
2972
+ previousTurnId: "t2p",
2973
+ input: [{ type: "user.message", content: "third again" }],
2974
+ events: [
2975
+ modelMessage({ id: "m3p", threadId: ROOT_THREAD_ID, content: "reply 3p" }),
2976
+ ],
2977
+ });
2978
+ const server = mockServerWithTurns([t1!, t2!, t3!, t2p, t3p]);
2979
+
2980
+ // Rerunning the middle live turn rewinds to its parent, not to the
2981
+ // chronologically preceding turn (t3), whose chain still holds t2/t3.
2982
+ const previousTurnId = await resolveGatewayBranchPreviousTurnIdForTurn(
2983
+ server,
2984
+ SESSION_ID,
2985
+ "t2p",
2986
+ );
2987
+ expect(previousTurnId).toBe("t1");
2988
+
2989
+ const snapshot = await buildSnapshotThroughTurn(server, SESSION_ID, previousTurnId);
2990
+ expect(snapshot.turns.map((turn) => turn.id)).toEqual(["t1"]);
2991
+
2992
+ // The orphan chain is still reachable, so the assertion above is not
2993
+ // passing by accident.
2994
+ const orphanSnapshot = await buildSnapshotThroughTurn(server, SESSION_ID, "t3");
2995
+ expect(orphanSnapshot.turns.map((turn) => turn.id)).toEqual(["t1", "t2", "t3"]);
2996
+ });
2997
+ });
2998
+
2999
+ describe("resolveGatewayBranchPreviousTurnIdForTurn", () => {
3000
+ it("uses the turn parent for subsequent turns and none for roots (ASC listTurns)", async () => {
3001
+ const t1 = mockTurn({
3002
+ id: "t1",
3003
+ createdAt: "2026-01-01T00:00:00.000Z",
3004
+ previousTurnId: null,
3005
+ });
3006
+ const t2 = mockTurn({
3007
+ id: "t2",
3008
+ createdAt: "2026-01-01T00:01:00.000Z",
3009
+ previousTurnId: "t1",
3010
+ });
3011
+ const t3 = mockTurn({
3012
+ id: "t3",
3013
+ createdAt: "2026-01-01T00:02:00.000Z",
3014
+ previousTurnId: "t2",
3015
+ });
3016
+ const server = mockServerWithTurns([t1, t2, t3]);
3017
+
3018
+ await expect(
3019
+ resolveGatewayBranchPreviousTurnIdForTurn(server, SESSION_ID, "t1"),
3020
+ ).resolves.toBe("none");
3021
+ await expect(
3022
+ resolveGatewayBranchPreviousTurnIdForTurn(server, SESSION_ID, "t3"),
3023
+ ).resolves.toBe("t2");
3024
+ });
3025
+
3026
+ it("uses the turn parent when listTurns is newest-first", async () => {
3027
+ const t1 = mockTurn({
3028
+ id: "t1",
3029
+ createdAt: "2026-01-01T00:00:00.000Z",
3030
+ previousTurnId: null,
3031
+ });
3032
+ const t2 = mockTurn({
3033
+ id: "t2",
3034
+ createdAt: "2026-01-01T00:01:00.000Z",
3035
+ previousTurnId: "t1",
3036
+ });
3037
+ const server = mockServerWithTurns([t2, t1]);
3038
+
3039
+ await expect(
3040
+ resolveGatewayBranchPreviousTurnIdForTurn(server, SESSION_ID, "t2"),
3041
+ ).resolves.toBe("t1");
3042
+ });
2875
3043
  });
2876
3044
 
@@ -1379,93 +1379,38 @@ export async function buildSnapshotFromSession(
1379
1379
  });
1380
1380
  }
1381
1381
 
1382
- /** Rebuilds session state from turns strictly before `beforeTurnId` (excludes that turn). */
1383
- export async function buildSnapshotBeforeTurn(
1384
- server: AgentChatServer,
1385
- sessionId: string,
1386
- beforeTurnId: string,
1387
- concurrency: number = DEFAULT_LIST_EVENTS_CONCURRENCY,
1388
- ): Promise<SessionSnapshot> {
1389
- const turns = await listSessionTurnsOrdered(server, sessionId);
1390
-
1391
- const beforeIndex = turns.findIndex((turn) => turn.id === beforeTurnId);
1392
- if (beforeIndex === -1) {
1393
- throw new Error(`Turn ${beforeTurnId} not found in session`);
1394
- }
1395
-
1396
- return buildSnapshotBeforeTurnIndex(
1397
- server,
1398
- sessionId,
1399
- beforeIndex,
1400
- concurrency,
1401
- turns,
1402
- );
1403
- }
1404
-
1405
- /** Rebuilds session state from the first `turnIndex` turns (excludes that turn). */
1406
- export async function buildSnapshotBeforeTurnIndex(
1382
+ /**
1383
+ * Rebuilds the conversation through `anchorTurnId`, including that turn.
1384
+ * The server follows parent links from the anchor, so turns from abandoned
1385
+ * branches are excluded. A null anchor represents an empty conversation.
1386
+ */
1387
+ export async function buildSnapshotThroughTurn(
1407
1388
  server: AgentChatServer,
1408
1389
  sessionId: string,
1409
- turnIndex: number,
1410
- _concurrency: number = DEFAULT_LIST_EVENTS_CONCURRENCY,
1411
- orderedTurns?: Turn[],
1390
+ anchorTurnId: string | null,
1412
1391
  ): Promise<SessionSnapshot> {
1413
- if (turnIndex <= 0) {
1392
+ if (anchorTurnId == null) {
1414
1393
  return createEmptySessionSnapshot();
1415
1394
  }
1416
-
1417
- const turns = orderedTurns ?? (await listSessionTurnsOrdered(server, sessionId));
1418
- const turnsToInclude = turns.slice(0, turnIndex);
1419
- const lastTurnId = turnsToInclude.at(-1)?.id;
1420
- if (lastTurnId == null) {
1421
- return createEmptySessionSnapshot();
1422
- }
1423
-
1424
- // Anchor the session events window at the newest included turn so the
1425
- // ancestor chain matches `[turns[0], …, turns[turnIndex - 1]]`.
1426
- const items = await fetchAllSessionEvents(server, sessionId, { lastTurnId });
1395
+ const items = await fetchAllSessionEvents(server, sessionId, {
1396
+ lastTurnId: anchorTurnId,
1397
+ });
1427
1398
  const snapshot = createEmptySessionSnapshot();
1428
1399
  ingestSessionEventsIntoSnapshot(snapshot, items);
1429
1400
  return snapshot;
1430
1401
  }
1431
1402
 
1432
- /** Turn id to branch from when resubmitting at `turnIndex` (`"none"` for first turn). */
1433
- export async function resolveGatewayBranchPreviousTurnId(
1434
- server: AgentChatServer,
1435
- sessionId: string,
1436
- turnIndex: number,
1437
- orderedTurns?: Turn[],
1438
- ): Promise<string> {
1439
- if (turnIndex <= 0) {
1440
- return "none";
1441
- }
1442
- const turns = orderedTurns ?? (await listSessionTurnsOrdered(server, sessionId));
1443
- return turns[turnIndex - 1]?.id ?? "none";
1444
- }
1445
-
1446
- /** Resolves `previousTurnId` by turn id so partial history windows stay correct. */
1403
+ /**
1404
+ * Resolves `previousTurnId` for edit/retry of `turnId` from the turn's own
1405
+ * parent pointer (`"none"` for roots). Independent of listTurns order.
1406
+ */
1447
1407
  export async function resolveGatewayBranchPreviousTurnIdForTurn(
1448
1408
  server: AgentChatServer,
1449
1409
  sessionId: string,
1450
1410
  turnId: string,
1451
1411
  ): Promise<string> {
1452
- const turns = await listSessionTurnsOrdered(server, sessionId);
1453
- const turnIndex = turns.findIndex((turn) => turn.id === turnId);
1454
- return resolveGatewayBranchPreviousTurnId(server, sessionId, turnIndex, turns);
1455
- }
1456
-
1457
- async function listSessionTurnsOrdered(
1458
- server: AgentChatServer,
1459
- sessionId: string,
1460
- ): Promise<Turn[]> {
1461
- const turns = await drainListPages((pageToken) =>
1462
- server.listTurns({
1463
- sessionId,
1464
- ...(pageToken != null ? { pageToken } : {}),
1465
- }),
1466
- );
1467
- turns.reverse();
1468
- return turns;
1412
+ const turn = await server.getTurn({ sessionId, turnId });
1413
+ return turn.previousTurnId ?? "none";
1469
1414
  }
1470
1415
 
1471
1416
  export async function buildTurnAssistantContent(
package/src/types.ts CHANGED
@@ -28,7 +28,6 @@ type TrueFoundryAgentRuntimeBaseOptions = ExternalStoreSharedOptions & {
28
28
  threadId?: string | undefined;
29
29
  onThreadIdChange?: ((threadId: string | undefined) => void) | undefined;
30
30
  onError?: ((error: unknown) => void) | undefined;
31
- listEventsConcurrency?: number | undefined;
32
31
  /**
33
32
  * Optional filter forwarded to `listSessions({ agentId })`.
34
33
  * Omit for all chats; hosts that key agents by name pass that name as the id.
@@ -47,6 +47,7 @@ vi.mock("./convertTurnMessages.js", async (importOriginal) => {
47
47
  const mockServer = {
48
48
  cancelSession: vi.fn().mockResolvedValue(undefined),
49
49
  listTurns: vi.fn(),
50
+ getTurn: vi.fn(),
50
51
  // Present so resume-capable paths are exercised; resumeTurnStream is mocked.
51
52
  subscribeToTurn: vi.fn(),
52
53
  } as unknown as AgentChatServer;
@@ -429,6 +430,7 @@ describe("useTrueFoundryAgentMessages", () => {
429
430
  expect.any(PeerThreadFoldState),
430
431
  {
431
432
  userMessage: "first",
433
+ previousTurnId: "none",
432
434
  headers: {
433
435
  "x-tfy-session-last-updated-at": "2026-06-30T12:00:00.000Z",
434
436
  },
@@ -689,21 +691,22 @@ describe("useTrueFoundryAgentMessages", () => {
689
691
 
690
692
  it("editFromTurn drops prior turns before showing the edited user message", async () => {
691
693
  const createdAt = new Date().toISOString();
694
+ const rootTurn = {
695
+ id: "turn-1",
696
+ sessionId: "session-1",
697
+ createdAt,
698
+ previousTurnId: null,
699
+ state: {
700
+ status: "done" as const,
701
+ requiredActions: [],
702
+ completedAt: createdAt,
703
+ },
704
+ input: [{ type: "user.message" as const, content: "Hello" }],
705
+ } as Turn;
692
706
  vi.mocked(mockServer.listTurns).mockResolvedValue({
693
- data: [
694
- {
695
- id: "turn-1",
696
- sessionId: "session-1",
697
- createdAt,
698
- state: {
699
- status: "done",
700
- requiredActions: [],
701
- completedAt: createdAt,
702
- },
703
- input: [{ type: "user.message", content: "Hello" }],
704
- } as Turn,
705
- ],
707
+ data: [rootTurn],
706
708
  });
709
+ vi.mocked(mockServer.getTurn).mockResolvedValue(rootTurn);
707
710
  const fold = new PeerThreadFoldState();
708
711
  ingestTurnEvent(fold, {
709
712
  type: "model.message",
@@ -805,21 +808,22 @@ describe("useTrueFoundryAgentMessages", () => {
805
808
  const onError = vi.fn();
806
809
  const original = snapshotWithUserTurn("Hello");
807
810
  vi.mocked(loadSessionSnapshot).mockResolvedValue(original);
811
+ const rootTurn = {
812
+ id: "turn-1",
813
+ sessionId: "session-1",
814
+ createdAt,
815
+ previousTurnId: null,
816
+ state: {
817
+ status: "done" as const,
818
+ requiredActions: [],
819
+ completedAt: createdAt,
820
+ },
821
+ input: [{ type: "user.message" as const, content: "Hello" }],
822
+ } as Turn;
808
823
  vi.mocked(mockServer.listTurns).mockResolvedValue({
809
- data: [
810
- {
811
- id: "turn-1",
812
- sessionId: "session-1",
813
- createdAt,
814
- state: {
815
- status: "done",
816
- requiredActions: [],
817
- completedAt: createdAt,
818
- },
819
- input: [{ type: "user.message", content: "Hello" }],
820
- } as Turn,
821
- ],
824
+ data: [rootTurn],
822
825
  });
826
+ vi.mocked(mockServer.getTurn).mockResolvedValue(rootTurn);
823
827
  vi.mocked(streamTurnContent).mockImplementation(async function* () {
824
828
  throw new Error("Turn preparation failed");
825
829
  });
@@ -15,7 +15,7 @@ import type { AgentChatServer } from "./server/types.js";
15
15
  import { ROOT_THREAD_ID } from "./constants.js";
16
16
  import {
17
17
  buildEditedUserMessageContent,
18
- buildSnapshotBeforeTurn,
18
+ buildSnapshotThroughTurn,
19
19
  computeGroupRootBaseline,
20
20
  extractTurnUserMessageContent,
21
21
  prependOlderSessionHistory,
@@ -60,7 +60,6 @@ export type UseTrueFoundryAgentMessagesOptions = {
60
60
  isMain?: boolean | undefined;
61
61
  /** URL-selected session may load before the thread list marks it as main. */
62
62
  isInitialSession?: boolean | undefined;
63
- listEventsConcurrency?: number | undefined;
64
63
  onError?: ((error: unknown) => void) | undefined;
65
64
  initializeSession?: () => Promise<{
66
65
  remoteId: string;
@@ -293,7 +292,6 @@ export function useTrueFoundryAgentMessages({
293
292
  sessionId,
294
293
  isMain,
295
294
  isInitialSession,
296
- listEventsConcurrency,
297
295
  onError,
298
296
  initializeSession,
299
297
  resolveConversationSessionId,
@@ -975,11 +973,12 @@ export function useTrueFoundryAgentMessages({
975
973
  conversationSessionId,
976
974
  turnId,
977
975
  );
978
- rewound = await buildSnapshotBeforeTurn(
976
+ // Rewind to the exact parent used for the new branch. Using the
977
+ // previous item from listTurns could select an abandoned branch.
978
+ rewound = await buildSnapshotThroughTurn(
979
979
  server,
980
980
  conversationSessionId,
981
- turnId,
982
- listEventsConcurrency,
981
+ previousTurnId === "none" ? null : previousTurnId,
983
982
  );
984
983
  createdAtByMessageIdRef.current = new Map();
985
984
  // Keep the ref aligned before awaiting sendTurn so any intermediate
@@ -1000,13 +999,7 @@ export function useTrueFoundryAgentMessages({
1000
999
  branchRollbackSnapshot: committed,
1001
1000
  });
1002
1001
  },
1003
- [
1004
- cancel,
1005
- server,
1006
- listEventsConcurrency,
1007
- sendTurn,
1008
- sessionId,
1009
- ],
1002
+ [cancel, server, sendTurn, sessionId],
1010
1003
  );
1011
1004
 
1012
1005
  const resetFromTurn = useCallback(
@@ -49,7 +49,6 @@ function useTrueFoundryAgentRuntimeImpl(
49
49
  agent,
50
50
  adapters,
51
51
  onError,
52
- listEventsConcurrency,
53
52
  ...sharedOptions
54
53
  } = options;
55
54
 
@@ -126,7 +125,6 @@ function useTrueFoundryAgentRuntimeImpl(
126
125
  sessionId,
127
126
  isMain,
128
127
  isInitialSession,
129
- listEventsConcurrency,
130
128
  onError,
131
129
  initializeSession,
132
130
  getTurnHeaders: agent.mode === "draft" ? getTurnHeaders : undefined,