authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-04 08:46:29-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 08:46:29-07:00
logdffb4cd3b16dc6f41aced99922950ee3083082c6
tree6d13d73e39b130a91857b12d92d5a6d12c0140f0
parent3bbbc1d9fd8b3d649c60ba0137c7dae93a6aab3f
signature Signed by PGP key B5690EEEBB952194

perf(server): use one query for buffered provider events (#9706)


10 files changed, 228 insertions(+), 65 deletions(-)

apps/server/src/checkpointing/CheckpointDiffQuery.test.ts+5
......@@ -107,6 +107,7 @@ describe("CheckpointDiffQuery.layer", () => {
107107 toCheckpointRef,
108108 });
109109 }),
110 getThreadRuntimeContext: () => Effect.die("unused"),
110111 getThreadShellById: () => Effect.succeed(Option.none()),
111112 getThreadDetailById: () => Effect.succeed(Option.none()),
112113 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
......@@ -203,6 +204,7 @@ describe("CheckpointDiffQuery.layer", () => {
203204 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
204205 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
205206 getFullThreadDiffContext: () => Effect.die("unused"),
207 getThreadRuntimeContext: () => Effect.die("unused"),
206208 getThreadShellById: () => Effect.succeed(Option.none()),
207209 getThreadDetailById: () => Effect.succeed(Option.none()),
208210 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
......@@ -289,6 +291,7 @@ describe("CheckpointDiffQuery.layer", () => {
289291 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
290292 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
291293 getFullThreadDiffContext: () => Effect.die("unused"),
294 getThreadRuntimeContext: () => Effect.die("unused"),
292295 getThreadShellById: () => Effect.succeed(Option.none()),
293296 getThreadDetailById: () => Effect.succeed(Option.none()),
294297 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
......@@ -360,6 +363,7 @@ describe("CheckpointDiffQuery.layer", () => {
360363 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
361364 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
362365 getFullThreadDiffContext: () => Effect.die("unused"),
366 getThreadRuntimeContext: () => Effect.die("unused"),
363367 getThreadShellById: () => Effect.succeed(Option.none()),
364368 getThreadDetailById: () => Effect.succeed(Option.none()),
365369 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
......@@ -416,6 +420,7 @@ describe("CheckpointDiffQuery.layer", () => {
416420 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
417421 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
418422 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
423 getThreadRuntimeContext: () => Effect.die("unused"),
419424 getThreadShellById: () => Effect.succeed(Option.none()),
420425 getThreadDetailById: () => Effect.succeed(Option.none()),
421426 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+1
......@@ -418,6 +418,7 @@ describe("OrchestrationEngine", () => {
418418 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
419419 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
420420 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
421 getThreadRuntimeContext: () => Effect.die("unused"),
421422 getThreadShellById: () => Effect.succeed(Option.none()),
422423 getThreadDetailById: () => Effect.succeed(Option.none()),
423424 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts+49
......@@ -22,6 +22,7 @@ import * as ThreadPlanProgress from "../ThreadPlanProgress.ts";
2222import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";
2323import { encodeThreadDetailPageCursor } from "../threadDetailCursor.ts";
2424import { projectThreadDetailSnapshot } from "../ActivityPayloadProjection.ts";
25import { makeSqlStatementCounter } from "../../../integration/SqlStatementCounter.integration.ts";
2526
2627const asProjectId = (value: string): ProjectId => ProjectId.make(value);
2728const asTurnId = (value: string): TurnId => TurnId.make(value);
......@@ -560,6 +561,38 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
560561 },
561562 ]);
562563 }
564
565 const counter = makeSqlStatementCounter();
566 const context = yield* snapshotQuery
567 .getThreadRuntimeContext(ThreadId.make("thread-1"))
568 .pipe(Effect.withTracer(counter.tracer));
569 assert.equal(counter.count(), 1);
570 assert.equal(context._tag, "Some");
571 if (context._tag === "Some") {
572 assert.deepEqual(context.value, {
573 id: ThreadId.make("thread-1"),
574 title: "Thread 1",
575 session: snapshot.threads[0]?.session,
576 });
577 }
578
579 yield* sql`
580 UPDATE projection_thread_sessions
581 SET status = 'starting', active_turn_id = NULL, provider_name = 'claudeAgent',
582 provider_instance_id = 'claude-secondary', last_error = 'Starting another session'
583 WHERE thread_id = 'thread-1'
584 `;
585 const changedContext = yield* snapshotQuery.getThreadRuntimeContext(
586 ThreadId.make("thread-1"),
587 );
588 assert.equal(changedContext._tag, "Some");
589 if (changedContext._tag === "Some") {
590 assert.equal(changedContext.value.session?.status, "starting");
591 assert.equal(changedContext.value.session?.activeTurnId, null);
592 assert.equal(changedContext.value.session?.providerName, "claudeAgent");
593 assert.equal(changedContext.value.session?.providerInstanceId, "claude-secondary");
594 assert.equal(changedContext.value.session?.lastError, "Starting another session");
595 }
563596 }),
564597 );
565598
......@@ -680,6 +713,22 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
680713 [ThreadId.make("thread-archived")],
681714 );
682715 assert.equal(archivedShellSnapshot.threads[0]?.archivedAt, "2026-04-06T00:00:06.000Z");
716 const activeContext = yield* snapshotQuery.getThreadRuntimeContext(
717 ThreadId.make("thread-active"),
718 );
719 assert.equal(activeContext._tag, "Some");
720 if (activeContext._tag === "Some") assert.equal(activeContext.value.session, null);
721 for (const threadId of ["thread-archived", "thread-missing"]) {
722 assert.equal(
723 (yield* snapshotQuery.getThreadRuntimeContext(ThreadId.make(threadId)))._tag,
724 "None",
725 );
726 }
727 yield* sql`UPDATE projection_threads SET deleted_at = '2026-04-06T00:00:08.000Z' WHERE thread_id = 'thread-active'`;
728 assert.equal(
729 (yield* snapshotQuery.getThreadRuntimeContext(ThreadId.make("thread-active")))._tag,
730 "None",
731 );
683732 }),
684733 );
685734
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts+57
......@@ -113,6 +113,11 @@ const ProjectionThreadActivityIdRowSchema = Schema.Struct({
113113 activityId: ProjectionThreadActivity.fields.activityId,
114114});
115115const ProjectionThreadSessionDbRowSchema = ProjectionThreadSession;
116const ProjectionThreadRuntimeContextDbRowSchema = Schema.Struct({
117 id: ThreadId,
118 title: Schema.String,
119 session: Schema.NullOr(ProjectionThreadSessionDbRowSchema),
120});
116121const ProjectionCheckpointDbRowSchema = ProjectionCheckpoint.mapFields(
117122 Struct.assign({
118123 files: Schema.fromJsonString(Schema.Array(OrchestrationCheckpointFile)),
......@@ -1038,6 +1043,40 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
10381043 `,
10391044 });
10401045
1046 const getThreadRuntimeContextRow = SqlSchema.findOneOption({
1047 Request: ThreadIdLookupInput,
1048 Result: ProjectionThreadRuntimeContextDbRowSchema,
1049 execute: ({ threadId }) =>
1050 sql`
1051 SELECT
1052 threads.thread_id AS id,
1053 threads.title,
1054 sessions.thread_id AS "threadId",
1055 sessions.status,
1056 sessions.provider_name AS "providerName",
1057 sessions.provider_instance_id AS "providerInstanceId",
1058 sessions.runtime_mode AS "runtimeMode",
1059 sessions.active_turn_id AS "activeTurnId",
1060 sessions.last_error AS "lastError",
1061 sessions.updated_at AS "updatedAt"
1062 FROM projection_threads AS threads
1063 LEFT JOIN projection_thread_sessions AS sessions
1064 ON sessions.thread_id = threads.thread_id
1065 WHERE threads.thread_id = ${threadId}
1066 AND threads.deleted_at IS NULL
1067 AND threads.archived_at IS NULL
1068 LIMIT 1
1069 `.pipe(
1070 Effect.map((rows) =>
1071 rows.map((row) => ({
1072 id: row.id,
1073 title: row.title,
1074 session: row.threadId === null ? null : row,
1075 })),
1076 ),
1077 ),
1078 });
1079
10411080 const listThreadMessageRowsByThread = SqlSchema.findAll({
10421081 Request: ThreadIdLookupInput,
10431082 Result: ProjectionThreadMessageDbRowSchema,
......@@ -2764,6 +2803,23 @@ pending_approval_requests AS (
27642803 } satisfies OrchestrationThreadShell);
27652804 });
27662805
2806 const getThreadRuntimeContext: ProjectionSnapshotQueryShape["getThreadRuntimeContext"] =
2807 Effect.fn("ProjectionSnapshotQuery.getThreadRuntimeContext")(function* (threadId) {
2808 const context = yield* getThreadRuntimeContextRow({ threadId }).pipe(
2809 Effect.mapError(
2810 toPersistenceSqlOrDecodeError(
2811 "ProjectionSnapshotQuery.getThreadRuntimeContext:query",
2812 "ProjectionSnapshotQuery.getThreadRuntimeContext:decodeRow",
2813 ),
2814 ),
2815 );
2816 return Option.map(context, (row) => ({
2817 id: row.id,
2818 title: row.title,
2819 session: row.session === null ? null : mapSessionRow(row.session),
2820 }));
2821 });
2822
27672823 // Contiguous turn range bounding a windowed detail read; undefined loads the
27682824 // full thread. Resolved from a window request inside the snapshot
27692825 // transaction (see getThreadDetailSnapshot).
......@@ -3200,6 +3256,7 @@ pending_approval_requests AS (
32003256 getThreadCheckpointContext,
32013257 getFullThreadDiffContext,
32023258 getThreadShellById,
3259 getThreadRuntimeContext,
32033260 getThreadDetailById,
32043261 getThreadDetailSnapshot,
32053262 } satisfies ProjectionSnapshotQueryShape;
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+97-62
......@@ -24,6 +24,7 @@ import {
2424 TurnId,
2525} from "@t3tools/contracts";
2626import * as Clock from "effect/Clock";
27import * as Deferred from "effect/Deferred";
2728import * as Effect from "effect/Effect";
2829import * as Exit from "effect/Exit";
2930import * as Layer from "effect/Layer";
......@@ -31,6 +32,7 @@ import * as ManagedRuntime from "effect/ManagedRuntime";
3132import * as PubSub from "effect/PubSub";
3233import * as Scope from "effect/Scope";
3334import * as Stream from "effect/Stream";
35import * as Tracer from "effect/Tracer";
3436import { it as effectIt } from "@effect/vitest";
3537import { afterEach, describe, expect, it } from "vite-plus/test";
3638
......@@ -55,6 +57,7 @@ import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts"
5557import { ServerConfig } from "../../config.ts";
5658import { ServerSettingsService } from "../../serverSettings.ts";
5759import * as NodeServices from "@effect/platform-node/NodeServices";
60import { makeSqlStatementCounter } from "../../../integration/SqlStatementCounter.integration.ts";
5861
5962function makeTestServerSettingsLayer(overrides: Partial<ServerSettings> = {}) {
6063 return ServerSettingsService.layerTest(overrides);
......@@ -98,7 +101,12 @@ function isLegacyTurnCompletedEvent(
98101}
99102
100103function createProviderServiceHarness() {
101 const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>());
104 const runtimeEventPubSub = Effect.runSync(
105 PubSub.unbounded<{
106 readonly events: ReadonlyArray<ProviderRuntimeEvent>;
107 readonly enqueued?: Deferred.Deferred<void>;
108 }>(),
109 );
102110 const runtimeSessions: ProviderSession[] = [];
103111
104112 const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never;
......@@ -129,7 +137,16 @@ function createProviderServiceHarness() {
129137 rollbackConversation: () => unsupported(),
130138 uploadFeedback: () => unsupported(),
131139 get streamEvents() {
132 return Stream.fromPubSub(runtimeEventPubSub);
140 return Stream.fromPubSub(runtimeEventPubSub).pipe(
141 Stream.flatMap(({ events, enqueued }) =>
142 Stream.concat(
143 Stream.fromIterable(events),
144 enqueued
145 ? Stream.fromEffect(Deferred.succeed(enqueued, undefined)).pipe(Stream.drain)
146 : Stream.empty,
147 ),
148 ),
149 );
133150 },
134151 };
135152
......@@ -158,12 +175,24 @@ function createProviderServiceHarness() {
158175 };
159176
160177 const emit = (event: LegacyProviderRuntimeEvent): void => {
161 Effect.runSync(PubSub.publish(runtimeEventPubSub, normalizeLegacyEvent(event)));
178 Effect.runSync(PubSub.publish(runtimeEventPubSub, { events: [normalizeLegacyEvent(event)] }));
162179 };
163180
181 const emitAndWaitForEnqueue = Effect.fnUntraced(function* (
182 events: ReadonlyArray<LegacyProviderRuntimeEvent>,
183 ) {
184 const enqueued = yield* Deferred.make<void>();
185 yield* PubSub.publish(runtimeEventPubSub, {
186 events: events.map(normalizeLegacyEvent),
187 enqueued,
188 });
189 yield* Deferred.await(enqueued);
190 });
191
164192 return {
165193 service,
166194 emit,
195 emitAndWaitForEnqueue,
167196 setSession,
168197 };
169198}
......@@ -232,6 +261,7 @@ describe("ProviderRuntimeIngestion", () => {
232261 const workspaceRoot = makeTempDir("t3-provider-project-");
233262 NodeFS.mkdirSync(NodePath.join(workspaceRoot, ".git"));
234263 const provider = createProviderServiceHarness();
264 const sqlCounter = makeSqlStatementCounter();
235265 const orchestrationLayer = OrchestrationEngineLive.pipe(
236266 Layer.provide(OrchestrationProjectionSnapshotQueryLive),
237267 Layer.provide(OrchestrationProjectionPipelineLive),
......@@ -256,15 +286,22 @@ describe("ProviderRuntimeIngestion", () => {
256286 Layer.provideMerge(makeTestServerSettingsLayer(options?.serverSettings)),
257287 Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
258288 Layer.provideMerge(NodeServices.layer),
289 Layer.provideMerge(Layer.succeed(Tracer.Tracer, sqlCounter.tracer)),
259290 );
260 runtime = ManagedRuntime.make(layer);
261 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));
262 const snapshotQuery = await runtime.runPromise(Effect.service(ProjectionSnapshotQuery));
263 const ingestion = await runtime.runPromise(Effect.service(ProviderRuntimeIngestionService));
291 const testRuntime = ManagedRuntime.make(layer);
292 runtime = testRuntime;
293 const engine = await testRuntime.runPromise(Effect.service(OrchestrationEngineService));
294 const snapshotQuery = await testRuntime.runPromise(Effect.service(ProjectionSnapshotQuery));
295 const ingestion = await testRuntime.runPromise(Effect.service(ProviderRuntimeIngestionService));
264296 scope = await Effect.runPromise(Scope.make("sequential"));
265 await Effect.runPromise(ingestion.start().pipe(Scope.provide(scope)));
266 const drain = () => Effect.runPromise(ingestion.drain);
267 const dispatch = (command: OrchestrationCommand) => Effect.runPromise(engine.dispatch(command));
297 await testRuntime.runPromise(ingestion.start().pipe(Scope.provide(scope)));
298 const drain = () => testRuntime.runPromise(ingestion.drain);
299 const dispatch = (command: OrchestrationCommand) =>
300 testRuntime.runPromise(engine.dispatch(command));
301 const emitAndDrain = (events: ReadonlyArray<LegacyProviderRuntimeEvent>) =>
302 testRuntime.runPromise(
303 provider.emitAndWaitForEnqueue(events).pipe(Effect.andThen(ingestion.drain)),
304 );
268305
269306 const createdAt = "2026-01-01T00:00:00.000Z";
270307 await dispatch({
......@@ -322,8 +359,10 @@ describe("ProviderRuntimeIngestion", () => {
322359 return {
323360 engine,
324361 dispatch,
325 readModel: () => Effect.runPromise(snapshotQuery.getSnapshot()),
362 readModel: () => testRuntime.runPromise(snapshotQuery.getSnapshot()),
326363 emit: provider.emit,
364 emitAndDrain,
365 sqlCount: sqlCounter.count,
327366 setProviderSession: provider.setSession,
328367 drain,
329368 };
......@@ -2104,39 +2143,40 @@ describe("ProviderRuntimeIngestion", () => {
21042143 expect(proposedPlan?.planMarkdown).toBe("## Buffered plan\n\n- first\n- second");
21052144 });
21062145
2107 it("buffers assistant deltas by default until completion", async () => {
2146 it("buffers assistant deltas with one lifecycle query per event until completion", async () => {
21082147 const harness = await createHarness();
21092148 const now = "2026-01-01T00:00:00.000Z";
21102149
2111 harness.emit({
2112 type: "turn.started",
2113 eventId: asEventId("evt-turn-started-buffered"),
2114 provider: ProviderDriverKind.make("codex"),
2115 createdAt: now,
2116 threadId: asThreadId("thread-1"),
2117 turnId: asTurnId("turn-buffered"),
2118 });
2119 await waitForThread(
2120 harness.readModel,
2121 (thread) =>
2122 thread.session?.status === "running" && thread.session?.activeTurnId === "turn-buffered",
2123 );
2124
2125 harness.emit({
2126 type: "content.delta",
2127 eventId: asEventId("evt-message-delta-buffered"),
2128 provider: ProviderDriverKind.make("codex"),
2129 createdAt: now,
2130 threadId: asThreadId("thread-1"),
2131 turnId: asTurnId("turn-buffered"),
2132 itemId: asItemId("item-buffered"),
2133 payload: {
2134 streamKind: "assistant_text",
2135 delta: "buffer me",
2150 await harness.emitAndDrain([
2151 {
2152 type: "turn.started",
2153 eventId: asEventId("evt-turn-started-buffered"),
2154 provider: ProviderDriverKind.make("codex"),
2155 createdAt: now,
2156 threadId: asThreadId("thread-1"),
2157 turnId: asTurnId("turn-buffered"),
21362158 },
2137 });
2159 ]);
2160
2161 const eventCount = 1_000;
2162 const before = harness.sqlCount();
2163 await harness.emitAndDrain(
2164 Array.from({ length: eventCount }, (_, index) => ({
2165 type: "content.delta",
2166 eventId: asEventId(`evt-message-delta-buffered-${index}`),
2167 provider: ProviderDriverKind.make("codex"),
2168 createdAt: now,
2169 threadId: asThreadId("thread-1"),
2170 turnId: asTurnId("turn-buffered"),
2171 itemId: asItemId("item-buffered"),
2172 payload: {
2173 streamKind: "assistant_text",
2174 delta: "a",
2175 },
2176 })),
2177 );
2178 expect(harness.sqlCount() - before).toBe(eventCount);
21382179
2139 await harness.drain();
21402180 const midReadModel = await harness.readModel();
21412181 const midThread = midReadModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"));
21422182 expect(
......@@ -2145,30 +2185,25 @@ describe("ProviderRuntimeIngestion", () => {
21452185 ),
21462186 ).toBe(false);
21472187
2148 harness.emit({
2149 type: "item.completed",
2150 eventId: asEventId("evt-message-completed-buffered"),
2151 provider: ProviderDriverKind.make("codex"),
2152 createdAt: now,
2153 threadId: asThreadId("thread-1"),
2154 turnId: asTurnId("turn-buffered"),
2155 itemId: asItemId("item-buffered"),
2156 payload: {
2157 itemType: "assistant_message",
2158 status: "completed",
2188 await harness.emitAndDrain([
2189 {
2190 type: "item.completed",
2191 eventId: asEventId("evt-message-completed-buffered"),
2192 provider: ProviderDriverKind.make("codex"),
2193 createdAt: now,
2194 threadId: asThreadId("thread-1"),
2195 turnId: asTurnId("turn-buffered"),
2196 itemId: asItemId("item-buffered"),
2197 payload: {
2198 itemType: "assistant_message",
2199 status: "completed",
2200 },
21592201 },
2160 });
2161
2162 const thread = await waitForThread(harness.readModel, (entry) =>
2163 entry.messages.some(
2164 (message: ProviderRuntimeTestMessage) =>
2165 message.id === "assistant:item-buffered" && !message.streaming,
2166 ),
2167 );
2168 const message = thread.messages.find(
2169 (entry: ProviderRuntimeTestMessage) => entry.id === "assistant:item-buffered",
2170 );
2171 expect(message?.text).toBe("buffer me");
2202 ]);
2203 const readModel = await harness.readModel();
2204 const thread = readModel.threads.find((entry) => entry.id === asThreadId("thread-1"));
2205 const message = thread?.messages.find((entry) => entry.id === "assistant:item-buffered");
2206 expect(message?.text).toBe("a".repeat(eventCount));
21722207 expect(message?.streaming).toBe(false);
21732208 });
21742209
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+5-3
......@@ -1018,9 +1018,11 @@ const make = Effect.gen(function* () {
10181018 .pipe(Effect.map(Option.getOrUndefined));
10191019 });
10201020
1021 const resolveThreadShell = Effect.fn("resolveThreadShell")(function* (threadId: ThreadId) {
1021 const resolveThreadRuntimeContext = Effect.fn("resolveThreadRuntimeContext")(function* (
1022 threadId: ThreadId,
1023 ) {
10221024 return yield* projectionSnapshotQuery
1023 .getThreadShellById(threadId)
1025 .getThreadRuntimeContext(threadId)
10241026 .pipe(Effect.map(Option.getOrUndefined));
10251027 });
10261028
......@@ -1562,7 +1564,7 @@ const make = Effect.gen(function* () {
15621564 return;
15631565 }
15641566
1565 const thread = yield* resolveThreadShell(event.threadId);
1567 const thread = yield* resolveThreadRuntimeContext(event.threadId);
15661568 if (!thread) return;
15671569
15681570 let loadedThreadDetail: OrchestrationThread | null | undefined;
apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts+8
......@@ -193,6 +193,14 @@ export interface ProjectionSnapshotQueryShape {
193193 threadId: ThreadId,
194194 ) => Effect.Effect<Option.Option<OrchestrationThreadShell>, ProjectionRepositoryError>;
195195
196 /** Read the active thread and session facts used to ingest provider events. */
197 readonly getThreadRuntimeContext: (
198 threadId: ThreadId,
199 ) => Effect.Effect<
200 Option.Option<Pick<OrchestrationThreadShell, "id" | "title" | "session">>,
201 ProjectionRepositoryError
202 >;
203
196204 /**
197205 * Read a single active thread detail snapshot by id.
198206 */
apps/server/src/project/ProjectSetupScriptRunner.test.ts+1
......@@ -43,6 +43,7 @@ const makeProjectionSnapshotQueryLayer = (project: OrchestrationProject) =>
4343 getFirstActiveThreadIdByProjectId: () => Effect.die("unused"),
4444 getThreadCheckpointContext: () => Effect.die("unused"),
4545 getFullThreadDiffContext: () => Effect.die("unused"),
46 getThreadRuntimeContext: () => Effect.die("unused"),
4647 getThreadShellById: () => Effect.die("unused"),
4748 getThreadDetailById: () => Effect.die("unused"),
4849 getThreadDetailSnapshot: () => Effect.die("unused"),
apps/server/src/provider/Layers/ProviderSessionReaper.test.ts+1
......@@ -219,6 +219,7 @@ describe("ProviderSessionReaper", () => {
219219 getFirstActiveThreadIdByProjectId: () => Effect.die("unused"),
220220 getThreadCheckpointContext: () => Effect.die("unused"),
221221 getFullThreadDiffContext: () => Effect.die("unused"),
222 getThreadRuntimeContext: () => Effect.die("unused"),
222223 getThreadShellById: (threadId) =>
223224 Effect.succeed(
224225 input.readModel.threads.find((thread) => thread.id === threadId)
apps/server/src/serverRuntimeStartup.test.ts+4
......@@ -138,6 +138,7 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa
138138 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
139139 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
140140 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
141 getThreadRuntimeContext: () => Effect.die("unused"),
141142 getThreadShellById: () => Effect.succeed(Option.none()),
142143 getThreadDetailById: () => Effect.succeed(Option.none()),
143144 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
......@@ -211,6 +212,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa
211212 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.some(bootstrapThreadId)),
212213 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
213214 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
215 getThreadRuntimeContext: () => Effect.die("unused"),
214216 getThreadShellById: () => Effect.die("unused"),
215217 getThreadDetailById: () => Effect.die("unused"),
216218 getThreadDetailSnapshot: () => Effect.die("unused"),
......@@ -265,6 +267,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when
265267 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
266268 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
267269 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
270 getThreadRuntimeContext: () => Effect.die("unused"),
268271 getThreadShellById: () => Effect.die("unused"),
269272 getThreadDetailById: () => Effect.die("unused"),
270273 getThreadDetailSnapshot: () => Effect.die("unused"),
......@@ -328,6 +331,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa
328331 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
329332 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
330333 getFullThreadDiffContext: () => Effect.succeed(Option.none()),
334 getThreadRuntimeContext: () => Effect.die("unused"),
331335 getThreadShellById: () => Effect.die("unused"),
332336 getThreadDetailById: () => Effect.die("unused"),
333337 getThreadDetailSnapshot: () => Effect.die("unused"),