| author | |
| committer | |
| log | 88744f3ddba9d3883ba631f0f4801f1a97fe77ce |
| tree | b69e0cdc909ce5675659a7f7e38b94068e821d29 |
| parent | 2d81e4c27559bbd78564bc8cc3b38a0011814361 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 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 | ); |
| 1382 | 1412 |
apps/server/src/orchestration-v2/ProjectionStore.ts+4-4| ... | ... | @@ -2676,7 +2676,7 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> = |
| 2676 | 2676 | WHEN (SELECT COUNT(*) FROM turn_anchors) >= ${THREAD_HISTORY_MAX_RAW_TURNS + 2} |
| 2677 | 2677 | THEN (SELECT MIN(ordinal) FROM turn_anchors) |
| 2678 | 2678 | ELSE 0 |
| 2679 | END AS ordinal, (SELECT COUNT(*) FROM turn_anchors) AS anchors | |
| 2679 | END AS ordinal | |
| 2680 | 2680 | FROM user_anchors |
| 2681 | 2681 | ), selected AS ( |
| 2682 | 2682 | SELECT payload_json, ordinal, turn_item_id, run_id, type |
| ... | ... | @@ -2685,7 +2685,7 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> = |
| 2685 | 2685 | ORDER BY ordinal DESC, turn_item_id DESC |
| 2686 | 2686 | LIMIT CASE |
| 2687 | 2687 | WHEN ${window.rowLimit} = 0 THEN 0 |
| 2688 | WHEN (SELECT anchors FROM boundary) > 0 THEN -1 | |
| 2688 | WHEN (SELECT COUNT(*) FROM user_anchors) > 0 THEN -1 | |
| 2689 | 2689 | ELSE ${window.rowLimit} |
| 2690 | 2690 | END |
| 2691 | 2691 | ), retained AS ( |
| ... | ... | @@ -3165,7 +3165,7 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> = |
| 3165 | 3165 | localWindow !== undefined && |
| 3166 | 3166 | localWindow.rowLimit > 0 && |
| 3167 | 3167 | projection.turnItems.length >= localWindow.rowLimit && |
| 3168 | !projection.turnItems.some(isThreadHistoryTurnStart) | |
| 3168 | !projection.turnItems.some(isThreadHistoryUserTurn) | |
| 3169 | 3169 | ) { |
| 3170 | 3170 | return withLocalVisibleTurnItems(projection); |
| 3171 | 3171 | } |
| ... | ... | @@ -6119,7 +6119,7 @@ export const layerMemory: Layer.Layer<ProjectionStoreV2> = Layer.effect( |
| 6119 | 6119 | ); |
| 6120 | 6120 | const anchorLimit = (options.userTurnLimit ?? 0) + 2; |
| 6121 | 6121 | const start = |
| 6122 | turnAnchors.length > 0 | |
| 6122 | anchors.length > 0 | |
| 6123 | 6123 | ? anchors.length < anchorLimit |
| 6124 | 6124 | ? rawStart |
| 6125 | 6125 | : anchors.at(-anchorLimit)! |
apps/server/src/orchestration-v2/threadHistoryPaging.test.ts+30| ... | ... | @@ -209,6 +209,36 @@ describe("threadHistoryPaging", () => { |
| 209 | 209 | ); |
| 210 | 210 | }); |
| 211 | 211 | |
| 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 | 242 | it("encodes opaque cursors with stable source identity", () => { |
| 213 | 243 | const cursor = encodeThreadHistoryCursor({ |
| 214 | 244 | snapshotSequence: 9, |
apps/server/src/orchestration-v2/threadHistoryPaging.ts+1-1| ... | ... | @@ -185,7 +185,7 @@ function selectOlderTimelinePage(input: { |
| 185 | 185 | let encodedBytes = 0; |
| 186 | 186 | let userTurns = 0; |
| 187 | 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 | 189 | ? policy.maxUserTurns |
| 190 | 190 | : undefined; |
| 191 | 191 | for (let index = end - 1; index >= 0; index -= 1) { |