authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-06 02:21:03-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-06 02:21:03-07:00
logeb8ed80300bd719b49fc80ad8708f9839bb48c92
tree8c7f4b67996ad490851b1cb981e0614e2596808c
parent076d753ae6cb97136b6b4199fb8fc0b379282dba
signature Signed by PGP key B5690EEEBB952194

perf(server): skip plan bodies in thread summaries (#10341)


5 files changed, 280 insertions(+), 36 deletions(-)

apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts+3-2
......@@ -2697,7 +2697,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
26972697 }),
26982698 );
26992699
2700 it.effect("maintains shell summaries without reading message bodies", () =>
2700 it.effect("maintains shell summaries without decoding message or plan bodies", () =>
27012701 Effect.gen(function* () {
27022702 const projectionPipeline = yield* OrchestrationProjectionPipeline;
27032703 const eventStore = yield* OrchestrationEventStore;
......@@ -2923,12 +2923,13 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
29232923 ('summary-other-thread', 'thread-shell-summary-other', NULL, 'pending', NULL,
29242924 '2026-03-01T08:00:06.000Z', NULL)
29252925 `;
2926 // Empty markdown must not be decoded when the shell only needs plan status.
29262927 yield* sql`
29272928 INSERT INTO projection_thread_proposed_plans (
29282929 plan_id, thread_id, turn_id, plan_markdown, implemented_at,
29292930 implementation_thread_id, created_at, updated_at
29302931 ) VALUES (
2931 'summary-plan', 'thread-shell-summary', 'turn-shell-summary-1', '# Plan', NULL,
2932 'summary-plan', 'thread-shell-summary', 'turn-shell-summary-1', '', NULL,
29322933 NULL, '2026-03-01T08:00:06.000Z', '2026-03-01T08:00:06.000Z'
29332934 )
29342935 `;
apps/server/src/orchestration/Layers/ProjectionPipeline.ts+5-33
......@@ -197,33 +197,6 @@ function derivePendingUserInputCountFromActivities(
197197 return openRequestIds.size;
198198}
199199
200function deriveHasActionableProposedPlan(input: {
201 readonly latestTurnId: string | null;
202 readonly proposedPlans: ReadonlyArray<ProjectionThreadProposedPlan>;
203}): boolean {
204 const sorted = [...input.proposedPlans].toSorted(
205 (left, right) =>
206 left.updatedAt.localeCompare(right.updatedAt) || left.planId.localeCompare(right.planId),
207 );
208
209 let latestForTurn: ProjectionThreadProposedPlan | null = null;
210 if (input.latestTurnId !== null) {
211 for (let index = sorted.length - 1; index >= 0; index -= 1) {
212 const plan = sorted[index];
213 if (plan?.turnId === input.latestTurnId) {
214 latestForTurn = plan;
215 break;
216 }
217 }
218 }
219 if (latestForTurn !== null) {
220 return latestForTurn.implementedAt === null;
221 }
222
223 const latestPlan = sorted.at(-1) ?? null;
224 return latestPlan !== null && latestPlan.implementedAt === null;
225}
226
227200function retainProjectionMessagesAfterRevert(
228201 messages: ReadonlyArray<ProjectionThreadMessage>,
229202 turns: ReadonlyArray<ProjectionTurn>,
......@@ -595,19 +568,18 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
595568 return;
596569 }
597570
598 const [latestUserMessageAt, proposedPlans, activities, pendingApprovalCount] =
571 const [latestUserMessageAt, hasActionableProposedPlan, activities, pendingApprovalCount] =
599572 yield* Effect.all([
600573 projectionThreadMessageRepository.getLatestUserMessageAt({ threadId }),
601 projectionThreadProposedPlanRepository.listByThreadId({ threadId }),
574 projectionThreadProposedPlanRepository.hasActionableByThreadId({
575 threadId,
576 latestTurnId: existingRow.value.latestTurnId,
577 }),
602578 projectionThreadActivityRepository.listUserInputLifecycleByThreadId({ threadId }),
603579 projectionPendingApprovalRepository.countPendingByThreadId({ threadId }),
604580 ]);
605581
606582 const pendingUserInputCount = derivePendingUserInputCountFromActivities(activities);
607 const hasActionableProposedPlan = deriveHasActionableProposedPlan({
608 latestTurnId: existingRow.value.latestTurnId,
609 proposedPlans,
610 });
611583
612584 yield* projectionThreadRepository.upsert({
613585 ...existingRow.value,
apps/server/src/persistence/Layers/ProjectionRepositories.test.ts+210-1
......@@ -1,25 +1,234 @@
1import { ProjectId, ThreadId, ProviderInstanceId } from "@t3tools/contracts";
1import { ProjectId, ThreadId, TurnId, ProviderInstanceId } from "@t3tools/contracts";
22import { assert, it } from "@effect/vitest";
33import * as Effect from "effect/Effect";
44import * as Layer from "effect/Layer";
55import * as Option from "effect/Option";
66import * as SqlClient from "effect/unstable/sql/SqlClient";
7import * as Statement from "effect/unstable/sql/Statement";
78
89import { SqlitePersistenceMemory } from "./Sqlite.ts";
910import { ProjectionProjectRepositoryLive } from "./ProjectionProjects.ts";
1011import { ProjectionThreadRepositoryLive } from "./ProjectionThreads.ts";
12import { ProjectionThreadProposedPlanRepositoryLive } from "./ProjectionThreadProposedPlans.ts";
1113import { ProjectionProjectRepository } from "../Services/ProjectionProjects.ts";
1214import { ProjectionThreadRepository } from "../Services/ProjectionThreads.ts";
15import { ProjectionThreadProposedPlanRepository } from "../Services/ProjectionThreadProposedPlans.ts";
1316
1417const projectionRepositoriesLayer = it.layer(
1518 Layer.mergeAll(
1619 ProjectionProjectRepositoryLive.pipe(Layer.provideMerge(SqlitePersistenceMemory)),
1720 ProjectionThreadRepositoryLive.pipe(Layer.provideMerge(SqlitePersistenceMemory)),
21 ProjectionThreadProposedPlanRepositoryLive.pipe(Layer.provideMerge(SqlitePersistenceMemory)),
1822 SqlitePersistenceMemory,
1923 ),
2024);
2125
2226projectionRepositoriesLayer("Projection repositories", (it) => {
27 it.effect("selects the latest-turn plan before checking implementation status", () =>
28 Effect.gen(function* () {
29 const plans = yield* ProjectionThreadProposedPlanRepository;
30 const threadId = ThreadId.make("thread-plan-status");
31 const latestTurnId = TurnId.make("turn-plan-status-current");
32 const firstPlan = {
33 planId: "plan-status-first",
34 threadId,
35 turnId: latestTurnId,
36 planMarkdown: "# First plan",
37 implementedAt: null,
38 implementationThreadId: null,
39 createdAt: "2026-03-24T00:00:01.000Z",
40 updatedAt: "2026-03-24T00:00:01.000Z",
41 };
42 yield* plans.upsert(firstPlan);
43 yield* plans.upsert({
44 ...firstPlan,
45 planId: "plan-status-implemented",
46 implementedAt: "2026-03-24T00:00:02.000Z",
47 createdAt: "2026-03-24T00:00:02.000Z",
48 updatedAt: "2026-03-24T00:00:02.000Z",
49 });
50 yield* plans.upsert({
51 ...firstPlan,
52 planId: "plan-status-other-turn",
53 turnId: TurnId.make("turn-plan-status-old"),
54 updatedAt: "2026-03-24T00:00:10.000Z",
55 });
56
57 assert.isFalse(yield* plans.hasActionableByThreadId({ threadId, latestTurnId }));
58 assert.isTrue(yield* plans.hasActionableByThreadId({ threadId, latestTurnId: null }));
59
60 yield* plans.upsert({ ...firstPlan, updatedAt: "2026-03-24T00:00:03.000Z" });
61 assert.isTrue(yield* plans.hasActionableByThreadId({ threadId, latestTurnId }));
62 }),
63 );
64
65 it.effect("falls back within the thread when the latest turn has no plan", () =>
66 Effect.gen(function* () {
67 const plans = yield* ProjectionThreadProposedPlanRepository;
68 const threadId = ThreadId.make("thread-plan-fallback");
69 const latestTurnId = TurnId.make("turn-plan-fallback-missing");
70 assert.isFalse(yield* plans.hasActionableByThreadId({ threadId, latestTurnId }));
71 assert.isFalse(yield* plans.hasActionableByThreadId({ threadId, latestTurnId: null }));
72
73 const firstPlan = {
74 planId: "plan-fallback-without-turn",
75 threadId,
76 turnId: null,
77 planMarkdown: "# Old plan",
78 implementedAt: "2026-03-24T00:00:01.000Z",
79 implementationThreadId: null,
80 createdAt: "2026-03-24T00:00:01.000Z",
81 updatedAt: "2026-03-24T00:00:01.000Z",
82 };
83 yield* plans.upsert(firstPlan);
84 yield* plans.upsert({
85 ...firstPlan,
86 planId: "plan-fallback-with-turn",
87 turnId: TurnId.make("turn-plan-fallback-old"),
88 implementedAt: null,
89 updatedAt: "2026-03-24T00:00:02.000Z",
90 });
91 yield* plans.upsert({
92 ...firstPlan,
93 planId: "plan-fallback-other-thread",
94 threadId: ThreadId.make("thread-plan-fallback-other"),
95 turnId: latestTurnId,
96 updatedAt: "2026-03-24T00:00:03.000Z",
97 });
98
99 assert.isTrue(yield* plans.hasActionableByThreadId({ threadId, latestTurnId }));
100 assert.isTrue(yield* plans.hasActionableByThreadId({ threadId, latestTurnId: null }));
101 }),
102 );
103
104 it.effect("preserves locale ordering and stable ties when selecting plan status", () =>
105 Effect.gen(function* () {
106 const plans = yield* ProjectionThreadProposedPlanRepository;
107 const timestamp = "2026-03-24T00:00:00.000Z";
108 const cases = [
109 {
110 name: "mixed-case-ids",
111 expected: "plan-A".localeCompare("plan-a") > 0,
112 rows: [
113 {
114 planId: "plan-a",
115 implementedAt: timestamp,
116 createdAt: timestamp,
117 updatedAt: timestamp,
118 },
119 { planId: "plan-A", implementedAt: null, createdAt: timestamp, updatedAt: timestamp },
120 ],
121 },
122 {
123 name: "equivalent-ids",
124 expected: true,
125 rows: [
126 {
127 planId: "plan-\u00e9",
128 implementedAt: timestamp,
129 createdAt: timestamp,
130 updatedAt: timestamp,
131 },
132 {
133 planId: "plan-e\u0301",
134 implementedAt: null,
135 createdAt: "2026-03-24T00:00:01.000Z",
136 updatedAt: timestamp,
137 },
138 ],
139 },
140 {
141 name: "timestamp-formats",
142 expected: "2026-03-24T00:00:00+00:00".localeCompare("2026-03-24T00:00:00-01:00") > 0,
143 rows: [
144 {
145 planId: "plan-minus",
146 implementedAt: timestamp,
147 createdAt: timestamp,
148 updatedAt: "2026-03-24T00:00:00-01:00",
149 },
150 {
151 planId: "plan-plus",
152 implementedAt: null,
153 createdAt: timestamp,
154 updatedAt: "2026-03-24T00:00:00+00:00",
155 },
156 ],
157 },
158 ];
159 for (const testCase of cases) {
160 const threadId = ThreadId.make(`thread-plan-order-${testCase.name}`);
161 const latestTurnId = TurnId.make(`turn-plan-order-${testCase.name}`);
162 for (const plan of testCase.rows) {
163 yield* plans.upsert({
164 ...plan,
165 planId: `${testCase.name}-${plan.planId}`,
166 threadId,
167 turnId: latestTurnId,
168 planMarkdown: "# Plan",
169 implementationThreadId: null,
170 });
171 }
172 assert.strictEqual(
173 yield* plans.hasActionableByThreadId({ threadId, latestTurnId }),
174 testCase.expected,
175 testCase.name,
176 );
177 assert.strictEqual(
178 yield* plans.hasActionableByThreadId({ threadId, latestTurnId: null }),
179 testCase.expected,
180 testCase.name,
181 );
182 }
183 }),
184 );
185
186 it.effect("returns only current-turn status metadata when old plans have large bodies", () =>
187 Effect.gen(function* () {
188 const plans = yield* ProjectionThreadProposedPlanRepository;
189 const sql = yield* SqlClient.SqlClient;
190 const threadId = ThreadId.make("thread-plan-metadata");
191 const latestTurnId = TurnId.make("turn-plan-metadata-current");
192 const updatedAt = "2026-03-24T00:00:00.000Z";
193 yield* sql`
194 WITH RECURSIVE history(n) AS (
195 SELECT 1 UNION ALL SELECT n + 1 FROM history WHERE n < 256
196 )
197 INSERT INTO projection_thread_proposed_plans (
198 plan_id, thread_id, turn_id, plan_markdown, implemented_at,
199 implementation_thread_id, created_at, updated_at
200 )
201 SELECT 'plan-metadata-old-' || n, ${threadId}, 'turn-plan-metadata-old',
202 ${"# Old plan\n".repeat(1024)}, ${updatedAt}, NULL, ${updatedAt}, ${updatedAt}
203 FROM history
204 `;
205 yield* plans.upsert({
206 planId: "plan-metadata-current",
207 threadId,
208 turnId: latestTurnId,
209 planMarkdown: "# Current plan",
210 implementedAt: null,
211 implementationThreadId: null,
212 createdAt: updatedAt,
213 updatedAt,
214 });
215 const statements: Array<Statement.Statement<unknown>> = [];
216 const actionable = yield* plans.hasActionableByThreadId({ threadId, latestTurnId }).pipe(
217 Effect.provideService(Statement.CurrentTransformer, (statement) => {
218 statements.push(statement);
219 return Effect.succeed(statement);
220 }),
221 );
222 assert.isTrue(actionable);
223 assert.strictEqual(statements.length, 1);
224 const statement = statements[0];
225 if (statement === undefined) return yield* Effect.die("Expected a plan status query.");
226 assert.deepEqual(yield* statement, [
227 { planId: "plan-metadata-current", implementedAt: null, updatedAt },
228 ]);
229 }),
230 );
231
23232 it.effect("stores SQL NULL for missing project model options", () =>
24233 Effect.gen(function* () {
25234 const projects = yield* ProjectionProjectRepository;
apps/server/src/persistence/Layers/ProjectionThreadProposedPlans.ts+52
......@@ -1,11 +1,13 @@
11import * as Effect from "effect/Effect";
22import * as Layer from "effect/Layer";
3import * as Schema from "effect/Schema";
34import * as SqlClient from "effect/unstable/sql/SqlClient";
45import * as SqlSchema from "effect/unstable/sql/SqlSchema";
56
67import { toPersistenceSqlError } from "../Errors.ts";
78import {
89 DeleteProjectionThreadProposedPlansInput,
10 HasActionableProjectionThreadProposedPlanInput,
911 ListProjectionThreadProposedPlansInput,
1012 ProjectionThreadProposedPlan,
1113 ProjectionThreadProposedPlanRepository,
......@@ -77,6 +79,55 @@ const makeProjectionThreadProposedPlanRepository = Effect.gen(function* () {
7779 `,
7880 });
7981
82 const listPlanStatusCandidates = SqlSchema.findAll({
83 Request: HasActionableProjectionThreadProposedPlanInput,
84 Result: Schema.Struct({
85 planId: ProjectionThreadProposedPlan.fields.planId,
86 implementedAt: ProjectionThreadProposedPlan.fields.implementedAt,
87 updatedAt: ProjectionThreadProposedPlan.fields.updatedAt,
88 }),
89 execute: ({ threadId, latestTurnId }) => sql`
90 SELECT
91 plan_id AS "planId",
92 implemented_at AS "implementedAt",
93 updated_at AS "updatedAt"
94 FROM projection_thread_proposed_plans
95 WHERE thread_id = ${threadId}
96 AND (
97 turn_id = ${latestTurnId}
98 OR NOT EXISTS (
99 SELECT 1 FROM projection_thread_proposed_plans
100 WHERE thread_id = ${threadId} AND turn_id = ${latestTurnId}
101 )
102 )
103 ORDER BY created_at ASC, plan_id ASC
104 `,
105 });
106
107 const hasActionableByThreadId = Effect.fn(
108 "ProjectionThreadProposedPlanRepository.hasActionableByThreadId",
109 )(
110 function* (input: HasActionableProjectionThreadProposedPlanInput) {
111 const candidates = yield* listPlanStatusCandidates(input);
112 let selected: (typeof candidates)[number] | undefined;
113 // Timestamps and IDs use localeCompare, not SQLite byte order. Replace
114 // equal candidates to preserve the stable order of listByThreadId.
115 for (const candidate of candidates) {
116 if (
117 selected === undefined ||
118 (candidate.updatedAt.localeCompare(selected.updatedAt) ||
119 candidate.planId.localeCompare(selected.planId)) >= 0
120 ) {
121 selected = candidate;
122 }
123 }
124 return selected?.implementedAt === null;
125 },
126 Effect.mapError(
127 toPersistenceSqlError("ProjectionThreadProposedPlanRepository.hasActionableByThreadId:query"),
128 ),
129 );
130
80131 const upsert: ProjectionThreadProposedPlanRepositoryShape["upsert"] = (row) =>
81132 upsertProjectionThreadProposedPlanRow(row).pipe(
82133 Effect.mapError(toPersistenceSqlError("ProjectionThreadProposedPlanRepository.upsert:query")),
......@@ -101,6 +152,7 @@ const makeProjectionThreadProposedPlanRepository = Effect.gen(function* () {
101152 return {
102153 upsert,
103154 listByThreadId,
155 hasActionableByThreadId,
104156 deleteByThreadId,
105157 } satisfies ProjectionThreadProposedPlanRepositoryShape;
106158});
apps/server/src/persistence/Services/ProjectionThreadProposedPlans.ts+10
......@@ -29,6 +29,13 @@ export const ListProjectionThreadProposedPlansInput = Schema.Struct({
2929export type ListProjectionThreadProposedPlansInput =
3030 typeof ListProjectionThreadProposedPlansInput.Type;
3131
32export const HasActionableProjectionThreadProposedPlanInput = Schema.Struct({
33 threadId: ThreadId,
34 latestTurnId: Schema.NullOr(TurnId),
35});
36export type HasActionableProjectionThreadProposedPlanInput =
37 typeof HasActionableProjectionThreadProposedPlanInput.Type;
38
3239export const DeleteProjectionThreadProposedPlansInput = Schema.Struct({
3340 threadId: ThreadId,
3441});
......@@ -42,6 +49,9 @@ export interface ProjectionThreadProposedPlanRepositoryShape {
4249 readonly listByThreadId: (
4350 input: ListProjectionThreadProposedPlansInput,
4451 ) => Effect.Effect<ReadonlyArray<ProjectionThreadProposedPlan>, ProjectionRepositoryError>;
52 readonly hasActionableByThreadId: (
53 input: HasActionableProjectionThreadProposedPlanInput,
54 ) => Effect.Effect<boolean, ProjectionRepositoryError>;
4555 readonly deleteByThreadId: (
4656 input: DeleteProjectionThreadProposedPlansInput,
4757 ) => Effect.Effect<void, ProjectionRepositoryError>;