authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-10-04 03:29:05+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-03 17:29:05-07:00
log88744f3ddba9d3883ba631f0f4801f1a97fe77ce
treeb69e0cdc909ce5675659a7f7e38b94068e821d29
parent2d81e4c27559bbd78564bc8cc3b38a0011814361
signature Signed by PGP key B5690EEEBB952194

fix(orchestration-v2): restore earlier app agent transcript pages (#14104)


4 files changed, 65 insertions(+), 5 deletions(-)

apps/server/src/orchestration-v2/ProjectionStore.test.ts+30
...@@ -1377,6 +1377,36 @@ it.layer(TestLayer)("ProjectionStoreV2", (it) => {...@@ -1377,6 +1377,36 @@ it.layer(TestLayer)("ProjectionStoreV2", (it) => {
1377 (row) => row.sourceItemId === interruptResultId,1377 (row) => row.sourceItemId === interruptResultId,
1378 ),1378 ),
1379 );1379 );
1380
1381 yield* sql`
1382 UPDATE orchestration_v2_projection_turn_items
1383 SET payload_json = json_set(payload_json, '$.createdBy', 'agent')
1384 WHERE thread_id = ${threadId} AND type = 'user_message'
1385 `;
1386 const agentPromptId = TurnItemId.make("turn-item:bounded-sql-history:interrupt-filler:1281");
1387 yield* sql`
1388 UPDATE orchestration_v2_projection_turn_items
1389 SET type = 'user_message',
1390 payload_json = json_set(payload_json,
1391 '$.type', 'user_message', '$.inputIntent', 'turn_start',
1392 '$.createdBy', 'agent', '$.creationSource', 'provider',
1393 '$.messageId', 'message:bounded-sql-history:agent-prompt',
1394 '$.text', 'Continue the child task', '$.attachments', json('[]'))
1395 WHERE turn_item_id = ${agentPromptId}
1396 `;
1397 const agentWindow = yield* projectionStore.getThreadSnapshotWindow(threadId, {
1398 rowLimit: sqlPageLimit,
1399 userTurnLimit: THREAD_HISTORY_PAGE_POLICY.maxUserTurns,
1400 });
1401 assert.lengthOf(
1402 agentWindow.projection.visibleTurnItems.filter(
1403 (row) => row.item.type !== "run_interrupt_request",
1404 ),
1405 sqlPageLimit,
1406 );
1407 assert.isTrue(
1408 agentWindow.projection.visibleTurnItems.some((row) => row.sourceItemId === agentPromptId),
1409 );
1380 }),1410 }),
1381 );1411 );
13821412
apps/server/src/orchestration-v2/ProjectionStore.ts+4-4
...@@ -2676,7 +2676,7 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =...@@ -2676,7 +2676,7 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
2676 WHEN (SELECT COUNT(*) FROM turn_anchors) >= ${THREAD_HISTORY_MAX_RAW_TURNS + 2}2676 WHEN (SELECT COUNT(*) FROM turn_anchors) >= ${THREAD_HISTORY_MAX_RAW_TURNS + 2}
2677 THEN (SELECT MIN(ordinal) FROM turn_anchors)2677 THEN (SELECT MIN(ordinal) FROM turn_anchors)
2678 ELSE 02678 ELSE 0
2679 END AS ordinal, (SELECT COUNT(*) FROM turn_anchors) AS anchors2679 END AS ordinal
2680 FROM user_anchors2680 FROM user_anchors
2681 ), selected AS (2681 ), selected AS (
2682 SELECT payload_json, ordinal, turn_item_id, run_id, type2682 SELECT payload_json, ordinal, turn_item_id, run_id, type
...@@ -2685,7 +2685,7 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =...@@ -2685,7 +2685,7 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
2685 ORDER BY ordinal DESC, turn_item_id DESC2685 ORDER BY ordinal DESC, turn_item_id DESC
2686 LIMIT CASE2686 LIMIT CASE
2687 WHEN ${window.rowLimit} = 0 THEN 02687 WHEN ${window.rowLimit} = 0 THEN 0
2688 WHEN (SELECT anchors FROM boundary) > 0 THEN -12688 WHEN (SELECT COUNT(*) FROM user_anchors) > 0 THEN -1
2689 ELSE ${window.rowLimit}2689 ELSE ${window.rowLimit}
2690 END2690 END
2691 ), retained AS (2691 ), retained AS (
...@@ -3165,7 +3165,7 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =...@@ -3165,7 +3165,7 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
3165 localWindow !== undefined &&3165 localWindow !== undefined &&
3166 localWindow.rowLimit > 0 &&3166 localWindow.rowLimit > 0 &&
3167 projection.turnItems.length >= localWindow.rowLimit &&3167 projection.turnItems.length >= localWindow.rowLimit &&
3168 !projection.turnItems.some(isThreadHistoryTurnStart)3168 !projection.turnItems.some(isThreadHistoryUserTurn)
3169 ) {3169 ) {
3170 return withLocalVisibleTurnItems(projection);3170 return withLocalVisibleTurnItems(projection);
3171 }3171 }
...@@ -6119,7 +6119,7 @@ export const layerMemory: Layer.Layer<ProjectionStoreV2> = Layer.effect(...@@ -6119,7 +6119,7 @@ export const layerMemory: Layer.Layer<ProjectionStoreV2> = Layer.effect(
6119 );6119 );
6120 const anchorLimit = (options.userTurnLimit ?? 0) + 2;6120 const anchorLimit = (options.userTurnLimit ?? 0) + 2;
6121 const start =6121 const start =
6122 turnAnchors.length > 06122 anchors.length > 0
6123 ? anchors.length < anchorLimit6123 ? anchors.length < anchorLimit
6124 ? rawStart6124 ? rawStart
6125 : anchors.at(-anchorLimit)!6125 : anchors.at(-anchorLimit)!
apps/server/src/orchestration-v2/threadHistoryPaging.test.ts+30
...@@ -209,6 +209,36 @@ describe("threadHistoryPaging", () => {...@@ -209,6 +209,36 @@ describe("threadHistoryPaging", () => {
209 );209 );
210 });210 });
211211
212 it("pages agent-only child transcripts instead of dropping their earlier activity", () => {
213 const commandRows = Array.from({ length: 90 }, (_, index) => makeRow(index + 1));
214 const first = makeRow(0);
215 const prompt = {
216 ...first,
217 item: {
218 ...first.item,
219 type: "user_message" as const,
220 createdBy: "agent" as const,
221 creationSource: "provider" as const,
222 inputIntent: "turn_start" as const,
223 messageId: MessageId.make("child-prompt"),
224 text: "Inspect this project",
225 attachments: [],
226 },
227 } as OrchestrationV2ProjectedTurnItem;
228 const items = [prompt, ...commandRows];
229 const recent = selectRecentTimelineWindow({ items, snapshotSequence: 1 });
230
231 expect(recent.items).toHaveLength(THREAD_HISTORY_PAGE_POLICY.maxItems);
232 expect(recent.hasMoreHistory).toBe(true);
233 const older = selectHistoryPageFromCursor({
234 items,
235 cursor: recent.nextCursor!,
236 snapshotSequence: 1,
237 });
238 expect(older.items[0]?.item.type).toBe("user_message");
239 expect([...older.items, ...recent.items]).toHaveLength(items.length);
240 });
241
212 it("encodes opaque cursors with stable source identity", () => {242 it("encodes opaque cursors with stable source identity", () => {
213 const cursor = encodeThreadHistoryCursor({243 const cursor = encodeThreadHistoryCursor({
214 snapshotSequence: 9,244 snapshotSequence: 9,
apps/server/src/orchestration-v2/threadHistoryPaging.ts+1-1
...@@ -185,7 +185,7 @@ function selectOlderTimelinePage(input: {...@@ -185,7 +185,7 @@ function selectOlderTimelinePage(input: {
185 let encodedBytes = 0;185 let encodedBytes = 0;
186 let userTurns = 0;186 let userTurns = 0;
187 let rawTurns = 0;187 let rawTurns = 0;
188 const turnLimit = input.items.slice(0, end).some((row) => isThreadHistoryTurnStart(row.item))188 const turnLimit = input.items.slice(0, end).some((row) => isThreadHistoryUserTurn(row.item))
189 ? policy.maxUserTurns189 ? policy.maxUserTurns
190 : undefined;190 : undefined;
191 for (let index = end - 1; index >= 0; index -= 1) {191 for (let index = end - 1; index >= 0; index -= 1) {