History

perf(server): avoid full thread reads on turn start (#10108)

Theo Browne committed 2 months ago

Commit 17490c01 parent 281b92b12 files · +291 −8Browse files

Unified · Split

12 files
  1. apps/server/src/checkpointing/CheckpointDiffQuery.test.ts +5 −0
  2. apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts +1 −0
  3. apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts +141 −0
  4. apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts +68 −0
  5. apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +42 −0
  6. apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +9 −8
  7. apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts +17 −0
  8. apps/server/src/project/AgentSessionScanner.test.ts +1 −0
  9. apps/server/src/project/ProjectSetupScriptRunner.test.ts +1 −0
  10. apps/server/src/provider/Layers/ProviderService.test.ts +1 −0
  11. apps/server/src/provider/Layers/ProviderSessionReaper.test.ts +1 −0
  12. apps/server/src/serverRuntimeStartup.test.ts +4 −0
apps/server/src/checkpointing/CheckpointDiffQuery.test.ts+5−0
@@ -109,6 +109,7 @@ describe("CheckpointDiffQuery.layer", () => { · 108 unmodified lines
109109 });
110110 }),
111111 getThreadRuntimeContext: () => Effect.die("unused"),
112+ getTurnStartMessage: () => Effect.die("unused"),
112113 getThreadShellById: () => Effect.succeed(Option.none()),
113114 getThreadDetailById: () => Effect.succeed(Option.none()),
114115 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
@@ -207,6 +208,7 @@ describe("CheckpointDiffQuery.layer", () => { · 92 unmodified lines
207208 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
208209 getFullThreadDiffContext: () => Effect.die("unused"),
209210 getThreadRuntimeContext: () => Effect.die("unused"),
211+ getTurnStartMessage: () => Effect.die("unused"),
210212 getThreadShellById: () => Effect.succeed(Option.none()),
211213 getThreadDetailById: () => Effect.succeed(Option.none()),
212214 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
@@ -295,6 +297,7 @@ describe("CheckpointDiffQuery.layer", () => { · 82 unmodified lines
295297 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
296298 getFullThreadDiffContext: () => Effect.die("unused"),
297299 getThreadRuntimeContext: () => Effect.die("unused"),
300+ getTurnStartMessage: () => Effect.die("unused"),
298301 getThreadShellById: () => Effect.succeed(Option.none()),
299302 getThreadDetailById: () => Effect.succeed(Option.none()),
300303 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
@@ -368,6 +371,7 @@ describe("CheckpointDiffQuery.layer", () => { · 67 unmodified lines
368371 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
369372 getFullThreadDiffContext: () => Effect.die("unused"),
370373 getThreadRuntimeContext: () => Effect.die("unused"),
374+ getTurnStartMessage: () => Effect.die("unused"),
371375 getThreadShellById: () => Effect.succeed(Option.none()),
372376 getThreadDetailById: () => Effect.succeed(Option.none()),
373377 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
@@ -426,6 +430,7 @@ describe("CheckpointDiffQuery.layer", () => { · 52 unmodified lines
426430 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
427431 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
428432 getThreadRuntimeContext: () => Effect.die("unused"),
433+ getTurnStartMessage: () => Effect.die("unused"),
429434 getThreadShellById: () => Effect.succeed(Option.none()),
430435 getThreadDetailById: () => Effect.succeed(Option.none()),
431436 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+1−0
@@ -422,6 +422,7 @@ describe("OrchestrationEngine", () => { · 421 unmodified lines
422422 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
423423 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
424424 getThreadRuntimeContext: () => Effect.die("unused"),
425+ getTurnStartMessage: () => Effect.die("unused"),
425426 getThreadShellById: () => Effect.succeed(Option.none()),
426427 getThreadDetailById: () => Effect.succeed(Option.none()),
427428 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts+141−0
@@ -1,5 +1,6 @@
11 import {
22 type AgentSessionImportSource,
3+ ChatAttachment,
34 CheckpointRef,
45 EventId,
56 MessageId,
@@ -12,6 +13,7 @@ import { assert, it } from "@effect/vitest"; · 6 unmodified lines
1213 import * as NodeServices from "@effect/platform-node/NodeServices";
1314 import * as Effect from "effect/Effect";
1415 import * as Layer from "effect/Layer";
16+import * as Option from "effect/Option";
1517 import * as Schema from "effect/Schema";
1618 import * as SqlClient from "effect/unstable/sql/SqlClient";
1719
@@ -31,6 +33,9 @@ const asTurnId = (value: string): TurnId => TurnId.make(value); · 13 unmodified lines
3133 const asMessageId = (value: string): MessageId => MessageId.make(value);
3234 const asEventId = (value: string): EventId => EventId.make(value);
3335 const asCheckpointRef = (value: string): CheckpointRef => CheckpointRef.make(value);
36+const encodeChatAttachments = Schema.encodeEffect(
37+ Schema.fromJsonString(Schema.Array(ChatAttachment)),
38+);
3439
3540 const projectionSnapshotLayer = it.layer(
3641 OrchestrationProjectionSnapshotQueryLive.pipe(
@@ -598,6 +603,142 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { · 561 unmodified lines
598603 }),
599604 );
600605
606+ it.effect("reads one turn-start message without decoding unrelated history", () =>
607+ Effect.gen(function* () {
608+ const query = yield* ProjectionSnapshotQuery;
609+ const sql = yield* SqlClient.SqlClient;
610+ const threadId = ThreadId.make("thread-turn-start-read");
611+ const messageId = MessageId.make("message-turn-start-read");
612+ const createdAt = "2026-09-05T00:00:00.000Z";
613+ const attachments = [
614+ {
615+ type: "file" as const,
616+ id: "notes",
617+ name: "notes.txt",
618+ mimeType: "text/plain",
619+ sizeBytes: 8,
620+ },
621+ ];
622+ const attachmentsJson = yield* encodeChatAttachments(attachments);
623+ yield* sql`
624+ WITH RECURSIVE history(n) AS (
625+ VALUES (1) UNION ALL SELECT n + 1 FROM history WHERE n < 2000
626+ )
627+ INSERT INTO projection_thread_messages (
628+ message_id, thread_id, turn_id, role, text, attachments_json,
629+ is_streaming, created_at, updated_at
630+ )
631+ SELECT 'turn-start-history:' || n, ${threadId}, 'old-turn:' || n, 'assistant',
632+ 'Unrelated assistant output', 'not-json', 0, ${createdAt}, ${createdAt}
633+ FROM history
634+ `;
635+ yield* sql`
636+ INSERT INTO projection_thread_messages (
637+ message_id, thread_id, role, text, attachments_json, is_streaming, created_at, updated_at
638+ ) VALUES (${messageId}, ${threadId}, 'user', 'Read these notes',
639+ ${attachmentsJson}, 0, ${createdAt}, ${createdAt})
640+ `;
641+ yield* sql`
642+ INSERT INTO projection_thread_messages (
643+ message_id, thread_id, role, text, attachments_json, is_streaming, created_at, updated_at
644+ ) VALUES ('turn-start-unrelated-user', 'thread-turn-start-unrelated', 'user', 'Unrelated prompt',
645+ 'not-json', 0, ${createdAt}, ${createdAt})
646+ `;
647+
648+ const counter = makeSqlStatementCounter();
649+ const context = yield* query
650+ .getTurnStartMessage({ threadId, messageId })
651+ .pipe(Effect.withTracer(counter.tracer));
652+ assert.equal(counter.count(), 1);
653+ assert.deepEqual(
654+ context,
655+ Option.some({
656+ message: {
657+ id: messageId,
658+ role: "user",
659+ text: "Read these notes",
660+ turnId: null,
661+ streaming: false,
662+ createdAt,
663+ updatedAt: createdAt,
664+ attachments,
665+ },
666+ hasOtherUserMessages: false,
667+ }),
668+ );
669+ assert.equal(
670+ (yield* query.getTurnStartMessage({
671+ threadId: ThreadId.make("thread-turn-start-unrelated"),
672+ messageId,
673+ }))._tag,
674+ "None",
675+ );
676+ assert.equal(
677+ (yield* query.getTurnStartMessage({ threadId, messageId: MessageId.make("missing") }))._tag,
678+ "None",
679+ );
680+ }).pipe(
681+ Effect.ensuring(
682+ Effect.gen(function* () {
683+ const sql = yield* SqlClient.SqlClient;
684+ yield* sql`
685+ DELETE FROM projection_thread_messages
686+ WHERE thread_id IN ('thread-turn-start-read', 'thread-turn-start-unrelated')
687+ `;
688+ }).pipe(Effect.orDie),
689+ ),
690+ ),
691+ );
692+
693+ it.effect("keeps compaction and queued-message eligibility in the turn-start query", () =>
694+ Effect.gen(function* () {
695+ const query = yield* ProjectionSnapshotQuery;
696+ const sql = yield* SqlClient.SqlClient;
697+ const threadId = ThreadId.make("thread-turn-start-eligibility");
698+ const messageId = MessageId.make("message-turn-start-eligibility");
699+ const createdAt = "2026-09-05T00:00:00.000Z";
700+ yield* sql`
701+ INSERT INTO projection_thread_messages (
702+ message_id, thread_id, role, text, is_streaming, created_at, updated_at
703+ ) VALUES (${messageId}, ${threadId}, 'user', 'Start a turn', 0, ${createdAt}, ${createdAt})
704+ `;
705+ yield* sql`
706+ INSERT INTO projection_thread_messages (
707+ message_id, thread_id, role, text, attachments_json, is_streaming, created_at, updated_at
708+ ) VALUES ('turn-start-other-user', ${threadId}, 'user', '/compact', NULL, 0,
709+ '2026-09-05T00:00:01.000Z', '2026-09-05T00:00:01.000Z')
710+ `;
711+
712+ for (const { text, attachments, hasOtherUserMessages } of [
713+ { text: "/compact", attachments: null, hasOtherUserMessages: false },
714+ {
715+ text: "\t\n\r /CoMpAcT\u00a0\u2028\ufeff",
716+ attachments: "[ ]",
717+ hasOtherUserMessages: false,
718+ },
719+ { text: "/compact keep recent errors", attachments: "[]", hasOtherUserMessages: true },
720+ { text: "", attachments: null, hasOtherUserMessages: true },
721+ { text: "Queued prompt", attachments: null, hasOtherUserMessages: true },
722+ {
723+ text: "/compact",
724+ attachments:
725+ '[{"type":"file","id":"notes","name":"notes.txt","mimeType":"text/plain","sizeBytes":8}]',
726+ hasOtherUserMessages: true,
727+ },
728+ ]) {
729+ yield* sql`
730+ UPDATE projection_thread_messages SET text = ${text}, attachments_json = ${attachments}
731+ WHERE message_id = 'turn-start-other-user'
732+ `;
733+ const context = yield* query.getTurnStartMessage({ threadId, messageId });
734+ assert.equal(context._tag, "Some");
735+ if (context._tag === "Some") {
736+ assert.equal(context.value.hasOtherUserMessages, hasOtherUserMessages);
737+ }
738+ }
739+ }),
740+ );
741+
601742 it.effect("keeps archived threads out of the main shell snapshot", () =>
602743 Effect.gen(function* () {
603744 const snapshotQuery = yield* ProjectionSnapshotQuery;
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts+68−0
@@ -91,6 +91,9 @@ const THREAD_DETAIL_ACTIVITY_LIMIT = 500; · 90 unmodified lines
9191 // Snapshot payloads are decoded and projected in small sequential batches so
9292 // one client read does not retain the raw payloads for the full activity window.
9393 const THREAD_DETAIL_ACTIVITY_PAYLOAD_BATCH_SIZE = 25;
94+// SQLite trim defaults to spaces. Match the whitespace removed by String.trim.
95+const MESSAGE_TRIM_WHITESPACE =
96+ "\t\n\v\f\r \u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff";
9497 const ProjectionProjectDbRowSchema = ProjectionProject.mapFields(
9598 Struct.assign({
9699 defaultModelSelection: Schema.NullOr(Schema.fromJsonString(ModelSelection)),
@@ -105,6 +108,9 @@ const ProjectionThreadMessageDbRowSchema = ProjectionThreadMessage.mapFields( · 8 unmodified lines
105108 attachments: Schema.NullOr(Schema.fromJsonString(Schema.Array(ChatAttachment))),
106109 }),
107110 );
111+const ProjectionTurnStartMessageDbRowSchema = ProjectionThreadMessageDbRowSchema.mapFields(
112+ Struct.assign({ hasOtherUserMessages: Schema.Number }),
113+);
108114 const ProjectionThreadProposedPlanDbRowSchema = ProjectionThreadProposedPlan;
109115 const ProjectionThreadDbRowSchema = ProjectionThread.mapFields(
110116 Struct.assign({
@@ -180,6 +186,10 @@ const ProjectionImportedAgentSessionSourcesRowSchema = Schema.Struct({ · 69 unmodified lines
180186 const ThreadIdLookupInput = Schema.Struct({
181187 threadId: ThreadId,
182188 });
189+const TurnStartMessageLookupInput = Schema.Struct({
190+ threadId: ThreadId,
191+ messageId: MessageId,
192+});
183193 const ThreadActivityKindsLookupInput = Schema.Struct({
184194 threadId: ThreadId,
185195 activityKinds: Schema.Array(Schema.String),
@@ -1117,6 +1127,37 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { · 931 unmodified lines
11171127 ),
11181128 });
11191129
1130+ const getTurnStartMessageRow = SqlSchema.findOneOption({
1131+ Request: TurnStartMessageLookupInput,
1132+ Result: ProjectionTurnStartMessageDbRowSchema,
1133+ execute: ({ threadId, messageId }) => sql`
1134+ SELECT
1135+ message_id AS "messageId",
1136+ thread_id AS "threadId",
1137+ turn_id AS "turnId",
1138+ role,
1139+ text,
1140+ attachments_json AS "attachments",
1141+ is_streaming AS "isStreaming",
1142+ created_at AS "createdAt",
1143+ updated_at AS "updatedAt",
1144+ EXISTS (
1145+ SELECT 1
1146+ FROM projection_thread_messages AS other
1147+ WHERE other.thread_id = ${threadId}
1148+ AND other.message_id != ${messageId}
1149+ AND other.role = 'user'
1150+ AND (
1151+ LOWER(TRIM(other.text, ${MESSAGE_TRIM_WHITESPACE})) != '/compact'
1152+ OR COALESCE(json_array_length(other.attachments_json), 0) > 0
1153+ )
1154+ ) AS "hasOtherUserMessages"
1155+ FROM projection_thread_messages
1156+ WHERE thread_id = ${threadId} AND message_id = ${messageId}
1157+ LIMIT 1
1158+ `,
1159+ });
1160+
11201161 const listThreadMessageRowsByThread = SqlSchema.findAll({
11211162 Request: ThreadIdLookupInput,
11221163 Result: ProjectionThreadMessageDbRowSchema,
@@ -2887,6 +2928,32 @@ pending_approval_requests AS ( · 1764 unmodified lines
28872928 }));
28882929 });
28892930
2931+ const getTurnStartMessage: ProjectionSnapshotQueryShape["getTurnStartMessage"] = Effect.fn(
2932+ "ProjectionSnapshotQuery.getTurnStartMessage",
2933+ )(function* (input) {
2934+ const message = yield* getTurnStartMessageRow(input).pipe(
2935+ Effect.mapError(
2936+ toPersistenceSqlOrDecodeError(
2937+ "ProjectionSnapshotQuery.getTurnStartMessage:query",
2938+ "ProjectionSnapshotQuery.getTurnStartMessage:decodeRow",
2939+ ),
2940+ ),
2941+ );
2942+ return Option.map(message, (row) => ({
2943+ message: {
2944+ id: row.messageId,
2945+ role: row.role,
2946+ text: row.text,
2947+ turnId: row.turnId,
2948+ streaming: row.isStreaming === 1,
2949+ createdAt: row.createdAt,
2950+ updatedAt: row.updatedAt,
2951+ ...(row.attachments !== null ? { attachments: row.attachments } : {}),
2952+ },
2953+ hasOtherUserMessages: row.hasOtherUserMessages === 1,
2954+ }));
2955+ });
2956+
28902957 // Contiguous turn range bounding a windowed detail read; undefined loads the
28912958 // full thread. Resolved from a window request inside the snapshot
28922959 // transaction (see getThreadDetailSnapshot).
@@ -3326,6 +3393,7 @@ pending_approval_requests AS ( · 433 unmodified lines
33263393 getFullThreadDiffContext,
33273394 getThreadShellById,
33283395 getThreadRuntimeContext,
3396+ getTurnStartMessage,
33293397 getThreadDetailById,
33303398 getThreadDetailSnapshot,
33313399 } satisfies ProjectionSnapshotQueryShape;
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+42−0
@@ -909,6 +909,48 @@ describe("ProviderCommandReactor", () => { · 908 unmodified lines
909909 }),
910910 );
911911
912+ effectIt.effect("starts a turn and generates its title without loading old message bodies", () =>
913+ Effect.gen(function* () {
914+ const started = yield* Deferred.make<void>();
915+ const titleGenerated = yield* Deferred.make<void>();
916+ const harness = yield* Effect.promise(() =>
917+ createHarness({
918+ unreadableHistory: true,
919+ startSessionEffect: (session) =>
920+ Deferred.succeed(started, undefined).pipe(Effect.as(session)),
921+ }),
922+ );
923+ harness.generateThreadTitle.mockReturnValue(
924+ Deferred.succeed(titleGenerated, undefined).pipe(Effect.as({ title: "Generated title" })),
925+ );
926+ yield* harness.engine.dispatch({
927+ type: "thread.turn.start",
928+ commandId: CommandId.make("cmd-turn-start-with-old-history"),
929+ threadId: ThreadId.make("thread-1"),
930+ message: {
931+ messageId: MessageId.make("message-turn-start-with-old-history"),
932+ role: "user",
933+ text: "Use the current message",
934+ attachments: [],
935+ },
936+ titleSeed: "Thread",
937+ interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
938+ runtimeMode: "approval-required",
939+ createdAt: "2026-01-01T00:00:01.000Z",
940+ });
941+ yield* Deferred.await(started);
942+ yield* Deferred.await(titleGenerated);
943+ yield* Effect.promise(() => harness.drain());
944+
945+ expect(harness.sendTurn).toHaveBeenCalledWith(
946+ expect.objectContaining({ input: "Use the current message" }),
947+ );
948+ expect(harness.generateThreadTitle).toHaveBeenCalledWith(
949+ expect.objectContaining({ message: "Use the current message" }),
950+ );
951+ }),
952+ );
953+
912954 effectIt.effect("rejects /compact without conversation context", () =>
913955 Effect.gen(function* () {
914956 const harness = yield* Effect.promise(() => createHarness());
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+9−8
@@ -1188,12 +1188,15 @@ const make = Effect.gen(function* () { · 1187 unmodified lines
11881188 return;
11891189 }
11901190
1191− const thread = yield* resolveThreadDetail(event.payload.threadId);
1191+ const thread = yield* resolveThreadShell(event.payload.threadId);
11921192 if (!thread) {
11931193 return;
11941194 }
1195− const message = thread.messages.find((entry) => entry.id === event.payload.messageId);
1196− if (!message || message.role !== "user") {
1195+ const turnStart = yield* projectionSnapshotQuery.getTurnStartMessage({
1196+ threadId: thread.id,
1197+ messageId: event.payload.messageId,
1198+ });
1199+ if (Option.isNone(turnStart) || turnStart.value.message.role !== "user") {
11971200 yield* appendProviderFailureActivity({
11981201 threadId: event.payload.threadId,
11991202 kind: "provider.turn.start.failed",
@@ -1205,6 +1208,7 @@ const make = Effect.gen(function* () { · 5 unmodified lines
12051208 });
12061209 return;
12071210 }
1211+ const { message, hasOtherUserMessages } = turnStart.value;
12081212 const appendTurnStartFailure = (summary: string, detail: string) =>
12091213 appendProviderFailureActivity({
12101214 threadId: event.payload.threadId,
@@ -1297,10 +1301,7 @@ const make = Effect.gen(function* () { · 86 unmodified lines
12971301 yield* ensureThreadWorktree(thread);
12981302
12991303 const isCompactCommand = isCompactCommandMessage(message);
1300− const nonCompactUserMessageCount = thread.messages.filter(
1301− (entry) => entry.role === "user" && !isCompactCommandMessage(entry),
1302− ).length;
1303− if (nonCompactUserMessageCount === 1 && !isCompactCommand) {
1304+ if (!hasOtherUserMessages && !isCompactCommand) {
13041305 const project = yield* resolveProject(thread.projectId);
13051306 const generationCwd =
13061307 resolveThreadWorkspaceCwd({
@@ -1371,7 +1372,7 @@ const make = Effect.gen(function* () { · 64 unmodified lines
13711372 ),
13721373 );
13731374 if (isCompactCommand) {
1374− if (nonCompactUserMessageCount === 0) {
1375+ if (!hasOtherUserMessages) {
13751376 return yield* appendTurnStartFailure(
13761377 "Context compaction failed",
13771378 "Context compaction requires an existing conversation.",
apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts+17−0
@@ -10,7 +10,9 @@ import type { · 9 unmodified lines
1010 AgentSessionImportSource,
1111 ApprovalRequestId,
1212 CheckpointRef,
13+ MessageId,
1314 OrchestrationCheckpointSummary,
15+ OrchestrationMessage,
1416 OrchestrationProject,
1517 OrchestrationProjectShell,
1618 OrchestrationReadModel,
@@ -211,6 +213,21 @@ export interface ProjectionSnapshotQueryShape { · 194 unmodified lines
211213 ProjectionRepositoryError
212214 >;
213215
216+ /**
217+ * Read one requested message and whether another non-compaction user message exists.
218+ * Newer queued messages count too, preserving first-turn title eligibility.
219+ */
220+ readonly getTurnStartMessage: (input: {
221+ readonly threadId: ThreadId;
222+ readonly messageId: MessageId;
223+ }) => Effect.Effect<
224+ Option.Option<{
225+ readonly message: OrchestrationMessage;
226+ readonly hasOtherUserMessages: boolean;
227+ }>,
228+ ProjectionRepositoryError
229+ >;
230+
214231 /**
215232 * Read a single active thread detail snapshot by id.
216233 */
apps/server/src/project/AgentSessionScanner.test.ts+1−0
@@ -58,6 +58,7 @@ const makeProjectionSnapshotQueryLayer = (importedWorkspaceRoots: ReadonlyArray< · 57 unmodified lines
5858 getFullThreadDiffContext: () => Effect.die("unused"),
5959 getThreadShellById: () => Effect.die("unused"),
6060 getThreadRuntimeContext: () => Effect.die("unused"),
61+ getTurnStartMessage: () => Effect.die("unused"),
6162 getThreadDetailById: () => Effect.die("unused"),
6263 getThreadDetailSnapshot: () => Effect.die("unused"),
6364 searchThreads: () => Effect.die("unused"),
apps/server/src/project/ProjectSetupScriptRunner.test.ts+1−0
@@ -46,6 +46,7 @@ const makeProjectionSnapshotQueryLayer = (project: OrchestrationProject) => · 45 unmodified lines
4646 getThreadCheckpointContext: () => Effect.die("unused"),
4747 getFullThreadDiffContext: () => Effect.die("unused"),
4848 getThreadRuntimeContext: () => Effect.die("unused"),
49+ getTurnStartMessage: () => Effect.die("unused"),
4950 getThreadShellById: () => Effect.die("unused"),
5051 getThreadDetailById: () => Effect.die("unused"),
5152 getThreadDetailSnapshot: () => Effect.die("unused"),
apps/server/src/provider/Layers/ProviderService.test.ts+1−0
@@ -4330,6 +4330,7 @@ describe("agent browser access", () => { · 4329 unmodified lines
43304330 Layer.provide(runtimeRepositoryLayer),
43314331 );
43324332 const projectionLayer = Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
4333+ getTurnStartMessage: () => Effect.die("unused"),
43334334 getImportedAgentSessionSources: () => Effect.die("unused"),
43344335 getUserInputActivity: () => Effect.die("unused"),
43354336 getCommandReadModel: () => Effect.die("unused"),
apps/server/src/provider/Layers/ProviderSessionReaper.test.ts+1−0
@@ -221,6 +221,7 @@ describe("ProviderSessionReaper", () => { · 220 unmodified lines
221221 getThreadCheckpointContext: () => Effect.die("unused"),
222222 getFullThreadDiffContext: () => Effect.die("unused"),
223223 getThreadRuntimeContext: () => Effect.die("unused"),
224+ getTurnStartMessage: () => Effect.die("unused"),
224225 getThreadShellById: (threadId) =>
225226 Effect.succeed(
226227 input.readModel.threads.find((thread) => thread.id === threadId)
apps/server/src/serverRuntimeStartup.test.ts+4−0
@@ -171,6 +171,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa · 170 unmodified lines
171171 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
172172 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
173173 getThreadRuntimeContext: () => Effect.die("unused"),
174+ getTurnStartMessage: () => Effect.die("unused"),
174175 getThreadShellById: () => Effect.die("unused"),
175176 getThreadDetailById: () => Effect.die("unused"),
176177 getThreadDetailSnapshot: () => Effect.die("unused"),
@@ -257,6 +258,7 @@ it.effect.each([ · 80 unmodified lines
257258 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
258259 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
259260 getThreadRuntimeContext: () => Effect.die("unused"),
261+ getTurnStartMessage: () => Effect.die("unused"),
260262 getThreadShellById: () => Effect.die("unused"),
261263 getThreadDetailById: () => Effect.die("unused"),
262264 getThreadDetailSnapshot: () => Effect.die("unused"),
@@ -325,6 +327,7 @@ it.effect( · 62 unmodified lines
325327 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
326328 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
327329 getThreadRuntimeContext: () => Effect.die("unused"),
330+ getTurnStartMessage: () => Effect.die("unused"),
328331 getThreadShellById: () => Effect.die("unused"),
329332 getThreadDetailById: () => Effect.die("unused"),
330333 getThreadDetailSnapshot: () => Effect.die("unused"),
@@ -386,6 +389,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa · 55 unmodified lines
386389 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
387390 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
388391 getThreadRuntimeContext: () => Effect.die("unused"),
392+ getTurnStartMessage: () => Effect.die("unused"),
389393 getThreadShellById: () => Effect.die("unused"),
390394 getThreadDetailById: () => Effect.die("unused"),
391395 getThreadDetailSnapshot: () => Effect.die("unused"),