| ... | @@ -57,6 +57,7 @@ import { | ... | @@ -57,6 +57,7 @@ import { |
| 57 | decodeThreadDetailPageCursor, | 57 | decodeThreadDetailPageCursor, |
| 58 | encodeThreadDetailPageCursor, | 58 | encodeThreadDetailPageCursor, |
| 59 | } from "../threadDetailCursor.ts"; | 59 | } from "../threadDetailCursor.ts"; |
| | 60 | import { projectActivityPayload } from "../ActivityPayloadProjection.ts"; |
| 60 | import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts"; | 61 | import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts"; |
| 61 | import { ORCHESTRATION_PROJECTOR_NAMES } from "./ProjectionPipeline.ts"; | 62 | import { ORCHESTRATION_PROJECTOR_NAMES } from "./ProjectionPipeline.ts"; |
| 62 | import { | 63 | import { |
| ... | @@ -75,6 +76,9 @@ const decodeThread = Schema.decodeUnknownEffect(OrchestrationThread); | ... | @@ -75,6 +76,9 @@ const decodeThread = Schema.decodeUnknownEffect(OrchestrationThread); |
| 75 | // activity window. Applying the limit in SQL avoids decoding an unbounded | 76 | // activity window. Applying the limit in SQL avoids decoding an unbounded |
| 76 | // payload_json set before the projector can enforce that invariant. | 77 | // payload_json set before the projector can enforce that invariant. |
| 77 | const THREAD_DETAIL_ACTIVITY_LIMIT = 500; | 78 | const THREAD_DETAIL_ACTIVITY_LIMIT = 500; |
| | 79 | // Snapshot payloads are decoded and projected in small sequential batches so |
| | 80 | // one client read does not retain the raw payloads for the full activity window. |
| | 81 | const THREAD_DETAIL_ACTIVITY_PAYLOAD_BATCH_SIZE = 25; |
| 78 | const ProjectionProjectDbRowSchema = ProjectionProject.mapFields( | 82 | const ProjectionProjectDbRowSchema = ProjectionProject.mapFields( |
| 79 | Struct.assign({ | 83 | Struct.assign({ |
| 80 | defaultModelSelection: Schema.NullOr(Schema.fromJsonString(ModelSelection)), | 84 | defaultModelSelection: Schema.NullOr(Schema.fromJsonString(ModelSelection)), |
| ... | @@ -100,6 +104,9 @@ const ProjectionThreadActivityDbRowSchema = ProjectionThreadActivity.mapFields( | ... | @@ -100,6 +104,9 @@ const ProjectionThreadActivityDbRowSchema = ProjectionThreadActivity.mapFields( |
| 100 | sequence: Schema.NullOr(NonNegativeInt), | 104 | sequence: Schema.NullOr(NonNegativeInt), |
| 101 | }), | 105 | }), |
| 102 | ); | 106 | ); |
| | 107 | const ProjectionThreadActivityIdRowSchema = Schema.Struct({ |
| | 108 | activityId: ProjectionThreadActivity.fields.activityId, |
| | 109 | }); |
| 103 | const ProjectionThreadSessionDbRowSchema = ProjectionThreadSession; | 110 | const ProjectionThreadSessionDbRowSchema = ProjectionThreadSession; |
| 104 | const ProjectionCheckpointDbRowSchema = ProjectionCheckpoint.mapFields( | 111 | const ProjectionCheckpointDbRowSchema = ProjectionCheckpoint.mapFields( |
| 105 | Struct.assign({ | 112 | Struct.assign({ |
| ... | @@ -146,6 +153,9 @@ const ThreadActivityKindsLookupInput = Schema.Struct({ | ... | @@ -146,6 +153,9 @@ const ThreadActivityKindsLookupInput = Schema.Struct({ |
| 146 | threadId: ThreadId, | 153 | threadId: ThreadId, |
| 147 | activityKinds: Schema.Array(Schema.String), | 154 | activityKinds: Schema.Array(Schema.String), |
| 148 | }); | 155 | }); |
| | 156 | const ThreadActivityIdsLookupInput = Schema.Struct({ |
| | 157 | activityIds: Schema.Array(ProjectionThreadActivity.fields.activityId), |
| | 158 | }); |
| 149 | // Windowed reads order turns by the stable keyset (anchor, turn key), where | 159 | // Windowed reads order turns by the stable keyset (anchor, turn key), where |
| 150 | // anchor is requested_at and turn key is | 160 | // anchor is requested_at and turn key is |
| 151 | // COALESCE(turn_id, ''). Both are event-derived, so cursors survive the | 161 | // COALESCE(turn_id, ''). Both are event-derived, so cursors survive the |
| ... | @@ -349,6 +359,21 @@ function mapProposedPlanRow( | ... | @@ -349,6 +359,21 @@ function mapProposedPlanRow( |
| 349 | }; | 359 | }; |
| 350 | } | 360 | } |
| 351 | | 361 | |
| | 362 | function mapThreadActivityRow( |
| | 363 | row: Schema.Schema.Type<typeof ProjectionThreadActivityDbRowSchema>, |
| | 364 | ): OrchestrationThreadActivity { |
| | 365 | return { |
| | 366 | id: row.activityId, |
| | 367 | tone: row.tone, |
| | 368 | kind: row.kind, |
| | 369 | summary: row.summary, |
| | 370 | payload: row.payload, |
| | 371 | turnId: row.turnId, |
| | 372 | createdAt: row.createdAt, |
| | 373 | ...(row.sequence !== null ? { sequence: row.sequence } : {}), |
| | 374 | }; |
| | 375 | } |
| | 376 | |
| 352 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { | 377 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { |
| 353 | return (cause: unknown): ProjectionRepositoryError => | 378 | return (cause: unknown): ProjectionRepositoryError => |
| 354 | Schema.isSchemaError(cause) | 379 | Schema.isSchemaError(cause) |
| ... | @@ -1060,6 +1085,44 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -1060,6 +1085,44 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 1060 | `, | 1085 | `, |
| 1061 | }); | 1086 | }); |
| 1062 | | 1087 | |
| | 1088 | const listThreadActivityIdsByThread = SqlSchema.findAll({ |
| | 1089 | Request: ThreadIdLookupInput, |
| | 1090 | Result: ProjectionThreadActivityIdRowSchema, |
| | 1091 | execute: ({ threadId }) => |
| | 1092 | sql` |
| | 1093 | SELECT activity_id AS "activityId" |
| | 1094 | FROM projection_thread_activities |
| | 1095 | WHERE thread_id = ${threadId} |
| | 1096 | ORDER BY |
| | 1097 | sequence DESC, |
| | 1098 | created_at DESC, |
| | 1099 | activity_id DESC |
| | 1100 | LIMIT ${THREAD_DETAIL_ACTIVITY_LIMIT} |
| | 1101 | `, |
| | 1102 | }); |
| | 1103 | |
| | 1104 | const listThreadActivityRowsByIds = SqlSchema.findAll({ |
| | 1105 | Request: ThreadActivityIdsLookupInput, |
| | 1106 | Result: ProjectionThreadActivityDbRowSchema, |
| | 1107 | execute: ({ activityIds }) => |
| | 1108 | sql` |
| | 1109 | SELECT |
| | 1110 | activity_id AS "activityId", |
| | 1111 | thread_id AS "threadId", |
| | 1112 | turn_id AS "turnId", |
| | 1113 | tone, |
| | 1114 | kind, |
| | 1115 | summary, |
| | 1116 | payload_json AS "payload", |
| | 1117 | sequence, |
| | 1118 | created_at AS "createdAt" |
| | 1119 | FROM projection_thread_activities |
| | 1120 | -- The selectors already scoped these globally unique ids to the |
| | 1121 | -- thread inside this transaction. Keep this as a primary-key lookup. |
| | 1122 | WHERE ${sql.in("activity_id", activityIds)} |
| | 1123 | `, |
| | 1124 | }); |
| | 1125 | |
| 1063 | const listThreadActivityRowsByThreadAndKinds = SqlSchema.findAll({ | 1126 | const listThreadActivityRowsByThreadAndKinds = SqlSchema.findAll({ |
| 1064 | Request: ThreadActivityKindsLookupInput, | 1127 | Request: ThreadActivityKindsLookupInput, |
| 1065 | Result: ProjectionThreadActivityDbRowSchema, | 1128 | Result: ProjectionThreadActivityDbRowSchema, |
| ... | @@ -1310,15 +1373,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -1310,15 +1373,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 1310 | `, | 1373 | `, |
| 1311 | }); | 1374 | }); |
| 1312 | | 1375 | |
| 1313 | // Blocking request payloads must remain available even if they predate the | 1376 | const pinnedThreadActivityIdsCte = (threadId: string) => sql` |
| 1314 | // recent activity window. Each CTE returns at most one unresolved row per | 1377 | pending_approval_requests AS ( |
| 1315 | // request, so the merge below stays bounded by actionable work. | | |
| 1316 | const listPinnedThreadActivityRowsByThread = SqlSchema.findAll({ | | |
| 1317 | Request: ThreadIdLookupInput, | | |
| 1318 | Result: ProjectionThreadActivityDbRowSchema, | | |
| 1319 | execute: ({ threadId }) => | | |
| 1320 | sql` | | |
| 1321 | WITH pending_approval_requests AS ( | | |
| 1322 | SELECT request_id, thread_id | 1378 | SELECT request_id, thread_id |
| 1323 | FROM projection_pending_approvals | 1379 | FROM projection_pending_approvals |
| 1324 | WHERE thread_id = ${threadId} | 1380 | WHERE thread_id = ${threadId} |
| ... | @@ -1382,6 +1438,17 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -1382,6 +1438,17 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 1382 | WHERE request_order = 1 | 1438 | WHERE request_order = 1 |
| 1383 | AND kind = 'user-input.requested' | 1439 | AND kind = 'user-input.requested' |
| 1384 | ) | 1440 | ) |
| | 1441 | `; |
| | 1442 | |
| | 1443 | // Blocking request payloads must remain available even if they predate the |
| | 1444 | // recent activity window. Each CTE returns at most one unresolved row per |
| | 1445 | // request, so the merge below stays bounded by actionable work. |
| | 1446 | const listPinnedThreadActivityRowsByThread = SqlSchema.findAll({ |
| | 1447 | Request: ThreadIdLookupInput, |
| | 1448 | Result: ProjectionThreadActivityDbRowSchema, |
| | 1449 | execute: ({ threadId }) => |
| | 1450 | sql` |
| | 1451 | WITH ${pinnedThreadActivityIdsCte(threadId)} |
| 1385 | SELECT | 1452 | SELECT |
| 1386 | activity.activity_id AS "activityId", | 1453 | activity.activity_id AS "activityId", |
| 1387 | activity.thread_id AS "threadId", | 1454 | activity.thread_id AS "threadId", |
| ... | @@ -1399,6 +1466,17 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -1399,6 +1466,17 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 1399 | `, | 1466 | `, |
| 1400 | }); | 1467 | }); |
| 1401 | | 1468 | |
| | 1469 | const listPinnedThreadActivityIdsByThread = SqlSchema.findAll({ |
| | 1470 | Request: ThreadIdLookupInput, |
| | 1471 | Result: ProjectionThreadActivityIdRowSchema, |
| | 1472 | execute: ({ threadId }) => |
| | 1473 | sql` |
| | 1474 | WITH ${pinnedThreadActivityIdsCte(threadId)} |
| | 1475 | SELECT activity_id AS "activityId" |
| | 1476 | FROM pinned_activity_ids |
| | 1477 | `, |
| | 1478 | }); |
| | 1479 | |
| 1402 | const listThreadActivityRowsByThreadWindow = SqlSchema.findAll({ | 1480 | const listThreadActivityRowsByThreadWindow = SqlSchema.findAll({ |
| 1403 | Request: ThreadTurnRangeLookupInput, | 1481 | Request: ThreadTurnRangeLookupInput, |
| 1404 | Result: ProjectionThreadActivityDbRowSchema, | 1482 | Result: ProjectionThreadActivityDbRowSchema, |
| ... | @@ -1466,6 +1544,48 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -1466,6 +1544,48 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 1466 | `, | 1544 | `, |
| 1467 | }); | 1545 | }); |
| 1468 | | 1546 | |
| | 1547 | const listThreadActivityIdsByThreadWindow = SqlSchema.findAll({ |
| | 1548 | Request: ThreadTurnRangeLookupInput, |
| | 1549 | Result: ProjectionThreadActivityIdRowSchema, |
| | 1550 | execute: ({ threadId, minAnchorAt, minTurnKey, beforeAnchorAt, beforeTurnKey }) => |
| | 1551 | sql` |
| | 1552 | SELECT activity_id AS "activityId" |
| | 1553 | FROM projection_thread_activities |
| | 1554 | WHERE thread_id = ${threadId} |
| | 1555 | AND ( |
| | 1556 | turn_id IN ( |
| | 1557 | SELECT turn_id FROM projection_turns |
| | 1558 | WHERE thread_id = ${threadId} |
| | 1559 | AND turn_id IS NOT NULL |
| | 1560 | AND ( |
| | 1561 | requested_at > ${minAnchorAt} |
| | 1562 | OR ( |
| | 1563 | requested_at = ${minAnchorAt} |
| | 1564 | AND turn_id >= ${minTurnKey} |
| | 1565 | ) |
| | 1566 | ) |
| | 1567 | AND ( |
| | 1568 | requested_at < ${beforeAnchorAt} |
| | 1569 | OR ( |
| | 1570 | requested_at = ${beforeAnchorAt} |
| | 1571 | AND turn_id < ${beforeTurnKey} |
| | 1572 | ) |
| | 1573 | ) |
| | 1574 | ) |
| | 1575 | OR ( |
| | 1576 | turn_id IS NULL |
| | 1577 | AND created_at >= ${minAnchorAt} |
| | 1578 | AND created_at < ${beforeAnchorAt} |
| | 1579 | ) |
| | 1580 | ) |
| | 1581 | ORDER BY |
| | 1582 | sequence DESC, |
| | 1583 | created_at DESC, |
| | 1584 | activity_id DESC |
| | 1585 | LIMIT ${THREAD_DETAIL_ACTIVITY_LIMIT} |
| | 1586 | `, |
| | 1587 | }); |
| | 1588 | |
| 1469 | const getFullThreadDiffContextRow = SqlSchema.findOneOption({ | 1589 | const getFullThreadDiffContextRow = SqlSchema.findOneOption({ |
| 1470 | Request: FullThreadDiffContextLookupInput, | 1590 | Request: FullThreadDiffContextLookupInput, |
| 1471 | Result: ProjectionFullThreadDiffContextRowSchema, | 1591 | Result: ProjectionFullThreadDiffContextRowSchema, |
| ... | @@ -2571,49 +2691,136 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -2571,49 +2691,136 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 2571 | readonly beforeTurnKey: string; | 2691 | readonly beforeTurnKey: string; |
| 2572 | } | 2692 | } |
| 2573 | | 2693 | |
| 2574 | const getThreadDetailByIdBounded = ( | 2694 | type ThreadDetailActivityRead = |
| 2575 | threadId: ThreadId, | 2695 | | { |
| 2576 | bounds: ThreadDetailBounds | undefined, | 2696 | readonly mode: "raw"; |
| 2577 | query: ProjectionThreadDetailQuery | undefined = undefined, | 2697 | readonly query?: ProjectionThreadDetailQuery; |
| 2578 | ) => | 2698 | } |
| 2579 | Effect.gen(function* () { | 2699 | | { |
| 2580 | const activityRowsEffect = ( | 2700 | readonly mode: "client"; |
| 2581 | query?.activityKinds === undefined | 2701 | }; |
| 2582 | ? bounds === undefined | 2702 | |
| 2583 | ? listThreadActivityRowsByThread({ threadId }) | 2703 | const listProjectedThreadActivities = Effect.fn( |
| 2584 | : listThreadActivityRowsByThreadWindow({ threadId, ...bounds }) | 2704 | "ProjectionSnapshotQuery.listProjectedThreadActivities", |
| 2585 | : query.activityKinds.length === 0 | 2705 | )(function* (threadId: ThreadId, bounds: ThreadDetailBounds | undefined) { |
| 2586 | ? Effect.succeed([]) | 2706 | const [activityIdRows, pinnedActivityIdRows] = yield* Effect.all([ |
| 2587 | : listThreadActivityRowsByThreadAndKinds({ | 2707 | (bounds === undefined |
| 2588 | threadId, | 2708 | ? listThreadActivityIdsByThread({ threadId }) |
| 2589 | activityKinds: query.activityKinds, | 2709 | : listThreadActivityIdsByThreadWindow({ threadId, ...bounds }) |
| 2590 | }) | | |
| 2591 | ).pipe( | 2710 | ).pipe( |
| 2592 | Effect.mapError( | 2711 | Effect.mapError( |
| 2593 | toPersistenceSqlOrDecodeError( | 2712 | toPersistenceSqlOrDecodeError( |
| 2594 | "ProjectionSnapshotQuery.getThreadDetailById:listActivities:query", | 2713 | "ProjectionSnapshotQuery.getThreadDetailById:listActivityIds:query", |
| 2595 | "ProjectionSnapshotQuery.getThreadDetailById:listActivities:decodeRows", | 2714 | "ProjectionSnapshotQuery.getThreadDetailById:listActivityIds:decodeRows", |
| 2596 | ), | 2715 | ), |
| 2597 | ), | 2716 | ), |
| | 2717 | ), |
| | 2718 | listPinnedThreadActivityIdsByThread({ threadId }).pipe( |
| | 2719 | Effect.mapError( |
| | 2720 | toPersistenceSqlOrDecodeError( |
| | 2721 | "ProjectionSnapshotQuery.getThreadDetailById:listPinnedActivityIds:query", |
| | 2722 | "ProjectionSnapshotQuery.getThreadDetailById:listPinnedActivityIds:decodeRows", |
| | 2723 | ), |
| | 2724 | ), |
| | 2725 | ), |
| | 2726 | ]); |
| | 2727 | const activityIds = [ |
| | 2728 | ...new Set([...activityIdRows, ...pinnedActivityIdRows].map(({ activityId }) => activityId)), |
| | 2729 | ]; |
| | 2730 | const activities: OrchestrationThreadActivity[] = []; |
| | 2731 | |
| | 2732 | for ( |
| | 2733 | let offset = 0; |
| | 2734 | offset < activityIds.length; |
| | 2735 | offset += THREAD_DETAIL_ACTIVITY_PAYLOAD_BATCH_SIZE |
| | 2736 | ) { |
| | 2737 | const batchIds = activityIds.slice( |
| | 2738 | offset, |
| | 2739 | offset + THREAD_DETAIL_ACTIVITY_PAYLOAD_BATCH_SIZE, |
| 2598 | ); | 2740 | ); |
| 2599 | const pinnedActivityRowsEffect = | 2741 | const batchRows = yield* listThreadActivityRowsByIds({ activityIds: batchIds }).pipe( |
| 2600 | query?.activityKinds === undefined | 2742 | Effect.mapError( |
| 2601 | ? listPinnedThreadActivityRowsByThread({ threadId }).pipe( | 2743 | toPersistenceSqlOrDecodeError( |
| 2602 | Effect.mapError( | 2744 | "ProjectionSnapshotQuery.getThreadDetailById:listActivityPayloadBatch:query", |
| 2603 | toPersistenceSqlOrDecodeError( | 2745 | "ProjectionSnapshotQuery.getThreadDetailById:listActivityPayloadBatch:decodeRows", |
| 2604 | "ProjectionSnapshotQuery.getThreadDetailById:listPinnedActivities:query", | 2746 | ), |
| 2605 | "ProjectionSnapshotQuery.getThreadDetailById:listPinnedActivities:decodeRows", | 2747 | ), |
| | 2748 | ); |
| | 2749 | for (const row of batchRows) { |
| | 2750 | activities.push(projectActivityPayload(mapThreadActivityRow(row))); |
| | 2751 | } |
| | 2752 | } |
| | 2753 | |
| | 2754 | return activities.toSorted( |
| | 2755 | (left, right) => |
| | 2756 | (left.sequence ?? -1) - (right.sequence ?? -1) || |
| | 2757 | left.createdAt.localeCompare(right.createdAt) || |
| | 2758 | left.id.localeCompare(right.id), |
| | 2759 | ); |
| | 2760 | }); |
| | 2761 | |
| | 2762 | const getThreadDetailByIdBounded = ( |
| | 2763 | threadId: ThreadId, |
| | 2764 | bounds: ThreadDetailBounds | undefined, |
| | 2765 | activityRead: ThreadDetailActivityRead = { mode: "raw" }, |
| | 2766 | ) => |
| | 2767 | Effect.gen(function* () { |
| | 2768 | const activitiesEffect = |
| | 2769 | activityRead.mode === "client" |
| | 2770 | ? listProjectedThreadActivities(threadId, bounds) |
| | 2771 | : Effect.all([ |
| | 2772 | (activityRead.query?.activityKinds === undefined |
| | 2773 | ? bounds === undefined |
| | 2774 | ? listThreadActivityRowsByThread({ threadId }) |
| | 2775 | : listThreadActivityRowsByThreadWindow({ threadId, ...bounds }) |
| | 2776 | : activityRead.query.activityKinds.length === 0 |
| | 2777 | ? Effect.succeed([]) |
| | 2778 | : listThreadActivityRowsByThreadAndKinds({ |
| | 2779 | threadId, |
| | 2780 | activityKinds: activityRead.query.activityKinds, |
| | 2781 | }) |
| | 2782 | ).pipe( |
| | 2783 | Effect.mapError( |
| | 2784 | toPersistenceSqlOrDecodeError( |
| | 2785 | "ProjectionSnapshotQuery.getThreadDetailById:listActivities:query", |
| | 2786 | "ProjectionSnapshotQuery.getThreadDetailById:listActivities:decodeRows", |
| | 2787 | ), |
| 2606 | ), | 2788 | ), |
| 2607 | ), | 2789 | ), |
| 2608 | ) | 2790 | activityRead.query?.activityKinds === undefined |
| 2609 | : Effect.succeed([]); | 2791 | ? listPinnedThreadActivityRowsByThread({ threadId }).pipe( |
| | 2792 | Effect.mapError( |
| | 2793 | toPersistenceSqlOrDecodeError( |
| | 2794 | "ProjectionSnapshotQuery.getThreadDetailById:listPinnedActivities:query", |
| | 2795 | "ProjectionSnapshotQuery.getThreadDetailById:listPinnedActivities:decodeRows", |
| | 2796 | ), |
| | 2797 | ), |
| | 2798 | ) |
| | 2799 | : Effect.succeed([]), |
| | 2800 | ]).pipe( |
| | 2801 | Effect.map(([activityRows, pinnedActivityRows]) => |
| | 2802 | [ |
| | 2803 | ...new Map( |
| | 2804 | [...activityRows, ...pinnedActivityRows].map( |
| | 2805 | (row) => [row.activityId, row] as const, |
| | 2806 | ), |
| | 2807 | ).values(), |
| | 2808 | ] |
| | 2809 | .toSorted( |
| | 2810 | (left, right) => |
| | 2811 | (left.sequence ?? -1) - (right.sequence ?? -1) || |
| | 2812 | left.createdAt.localeCompare(right.createdAt) || |
| | 2813 | left.activityId.localeCompare(right.activityId), |
| | 2814 | ) |
| | 2815 | .map(mapThreadActivityRow), |
| | 2816 | ), |
| | 2817 | ); |
| 2610 | | 2818 | |
| 2611 | const [ | 2819 | const [ |
| 2612 | threadRow, | 2820 | threadRow, |
| 2613 | messageRows, | 2821 | messageRows, |
| 2614 | proposedPlanRows, | 2822 | proposedPlanRows, |
| 2615 | activityRows, | 2823 | activities, |
| 2616 | pinnedActivityRows, | | |
| 2617 | checkpointRows, | 2824 | checkpointRows, |
| 2618 | latestTurnRow, | 2825 | latestTurnRow, |
| 2619 | sessionRow, | 2826 | sessionRow, |
| ... | @@ -2645,8 +2852,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -2645,8 +2852,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 2645 | ), | 2852 | ), |
| 2646 | ), | 2853 | ), |
| 2647 | ), | 2854 | ), |
| 2648 | activityRowsEffect, | 2855 | activitiesEffect, |
| 2649 | pinnedActivityRowsEffect, | | |
| 2650 | listCheckpointRowsByThread({ threadId }).pipe( | 2856 | listCheckpointRowsByThread({ threadId }).pipe( |
| 2651 | Effect.mapError( | 2857 | Effect.mapError( |
| 2652 | toPersistenceSqlOrDecodeError( | 2858 | toPersistenceSqlOrDecodeError( |
| ... | @@ -2677,17 +2883,6 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -2677,17 +2883,6 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 2677 | return Option.none<OrchestrationThread>(); | 2883 | return Option.none<OrchestrationThread>(); |
| 2678 | } | 2884 | } |
| 2679 | | 2885 | |
| 2680 | const selectedActivityRows = [ | | |
| 2681 | ...new Map( | | |
| 2682 | [...activityRows, ...pinnedActivityRows].map((row) => [row.activityId, row] as const), | | |
| 2683 | ).values(), | | |
| 2684 | ].toSorted( | | |
| 2685 | (left, right) => | | |
| 2686 | (left.sequence ?? -1) - (right.sequence ?? -1) || | | |
| 2687 | left.createdAt.localeCompare(right.createdAt) || | | |
| 2688 | left.activityId.localeCompare(right.activityId), | | |
| 2689 | ); | | |
| 2690 | | | |
| 2691 | const thread = { | 2886 | const thread = { |
| 2692 | id: threadRow.value.threadId, | 2887 | id: threadRow.value.threadId, |
| 2693 | projectId: threadRow.value.projectId, | 2888 | projectId: threadRow.value.projectId, |
| ... | @@ -2729,21 +2924,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -2729,21 +2924,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 2729 | return message; | 2924 | return message; |
| 2730 | }), | 2925 | }), |
| 2731 | proposedPlans: proposedPlanRows.map(mapProposedPlanRow), | 2926 | proposedPlans: proposedPlanRows.map(mapProposedPlanRow), |
| 2732 | activities: selectedActivityRows.map((row) => { | 2927 | activities, |
| 2733 | const activity = { | | |
| 2734 | id: row.activityId, | | |
| 2735 | tone: row.tone, | | |
| 2736 | kind: row.kind, | | |
| 2737 | summary: row.summary, | | |
| 2738 | payload: row.payload, | | |
| 2739 | turnId: row.turnId, | | |
| 2740 | createdAt: row.createdAt, | | |
| 2741 | }; | | |
| 2742 | if (row.sequence !== null) { | | |
| 2743 | return Object.assign(activity, { sequence: row.sequence }); | | |
| 2744 | } | | |
| 2745 | return activity; | | |
| 2746 | }), | | |
| 2747 | checkpoints: checkpointRows.map((row) => ({ | 2928 | checkpoints: checkpointRows.map((row) => ({ |
| 2748 | turnId: row.turnId, | 2929 | turnId: row.turnId, |
| 2749 | checkpointTurnCount: row.checkpointTurnCount, | 2930 | checkpointTurnCount: row.checkpointTurnCount, |
| ... | @@ -2768,7 +2949,11 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -2768,7 +2949,11 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 2768 | const getThreadDetailById: ProjectionSnapshotQueryShape["getThreadDetailById"] = ( | 2949 | const getThreadDetailById: ProjectionSnapshotQueryShape["getThreadDetailById"] = ( |
| 2769 | threadId, | 2950 | threadId, |
| 2770 | query, | 2951 | query, |
| 2771 | ) => getThreadDetailByIdBounded(threadId, undefined, query); | 2952 | ) => |
| | 2953 | getThreadDetailByIdBounded(threadId, undefined, { |
| | 2954 | mode: "raw", |
| | 2955 | ...(query === undefined ? {} : { query }), |
| | 2956 | }); |
| 2772 | | 2957 | |
| 2773 | // Bounds pathological fan-out: one user turn that spawned hundreds of | 2958 | // Bounds pathological fan-out: one user turn that spawned hundreds of |
| 2774 | // subagent turns still pages in bounded chunks, at the cost of splitting the | 2959 | // subagent turns still pages in bounded chunks, at the cost of splitting the |
| ... | @@ -2792,7 +2977,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -2792,7 +2977,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 2792 | .withTransaction( | 2977 | .withTransaction( |
| 2793 | Effect.gen(function* () { | 2978 | Effect.gen(function* () { |
| 2794 | if (window?.turnLimit === undefined) { | 2979 | if (window?.turnLimit === undefined) { |
| 2795 | const thread = yield* getThreadDetailById(threadId); | 2980 | const thread = yield* getThreadDetailByIdBounded(threadId, undefined, { |
| | 2981 | mode: "client", |
| | 2982 | }); |
| 2796 | if (Option.isNone(thread)) { | 2983 | if (Option.isNone(thread)) { |
| 2797 | return Option.none<OrchestrationThreadDetailSnapshot>(); | 2984 | return Option.none<OrchestrationThreadDetailSnapshot>(); |
| 2798 | } | 2985 | } |
| ... | @@ -2845,7 +3032,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -2845,7 +3032,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 2845 | ? { minAnchorAt: "", minTurnKey: "", beforeAnchorAt: "", beforeTurnKey: "" } | 3032 | ? { minAnchorAt: "", minTurnKey: "", beforeAnchorAt: "", beforeTurnKey: "" } |
| 2846 | : undefined; | 3033 | : undefined; |
| 2847 | | 3034 | |
| 2848 | const thread = yield* getThreadDetailByIdBounded(threadId, emptyBounds ?? bounds); | 3035 | const thread = yield* getThreadDetailByIdBounded(threadId, emptyBounds ?? bounds, { |
| | 3036 | mode: "client", |
| | 3037 | }); |
| 2849 | if (Option.isNone(thread)) { | 3038 | if (Option.isNone(thread)) { |
| 2850 | return Option.none<OrchestrationThreadDetailSnapshot>(); | 3039 | return Option.none<OrchestrationThreadDetailSnapshot>(); |
| 2851 | } | 3040 | } |