authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-27 12:22:26-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
log58427d565d91c27f1f37b3e0d1a396348c8afe6a
treecfe7379e1046ead98ddcbf117ba69ffdb1837da4
parent9b3dbada17dcbd0b96ca7eb6f79af7f7e0a416db

Refactor ClaudeCodeAdapter session flow and stream handling

- Replace manual prompt async iterator with `Stream.fromQueue(...).toAsyncIterable` - Use `Ref` for shared session context in tool approval callbacks - Simplify timestamp/ID generation and close sessions via queue shutdown

1 files changed, 32 insertions(+), 62 deletions(-)

apps/server/src/provider/Layers/ClaudeCodeAdapter.ts+32-62
...@@ -31,7 +31,7 @@ import {...@@ -31,7 +31,7 @@ import {
31 ProviderThreadId,31 ProviderThreadId,
32 ProviderTurnId,32 ProviderTurnId,
33} from "@t3tools/contracts";33} from "@t3tools/contracts";
34import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Stream } from "effect";34import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Ref, Stream } from "effect";
3535
36import {36import {
37 ProviderAdapterProcessError,37 ProviderAdapterProcessError,
...@@ -312,25 +312,12 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -312,25 +312,12 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
312 readonly options: ClaudeQueryOptions;312 readonly options: ClaudeQueryOptions;
313 }) => query({ prompt: input.prompt, options: input.options }) as ClaudeQueryRuntime);313 }) => query({ prompt: input.prompt, options: input.options }) as ClaudeQueryRuntime);
314314
315 const sessions = new Map<315 const sessions = new Map<ProviderSessionId, ClaudeSessionContext>();
316 ReturnType<typeof ProviderSessionId.makeUnsafe>,
317 ClaudeSessionContext
318 >();
319 const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>();316 const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>();
320317
321 const now = (): Effect.Effect<string> =>318 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
322 DateTime.now.pipe(Effect.map(DateTime.formatIso));319 const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.makeUnsafe(id));
323 const randomId = (): Effect.Effect<string> => Random.nextUUIDv4;320 const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso });
324 const makeEventId = (): Effect.Effect<ReturnType<typeof EventId.makeUnsafe>> =>
325 randomId().pipe(Effect.map((id) => EventId.makeUnsafe(id)));
326 const makeEventStamp = (): Effect.Effect<{
327 readonly eventId: ReturnType<typeof EventId.makeUnsafe>;
328 readonly createdAt: string;
329 }> =>
330 Effect.all({
331 eventId: makeEventId(),
332 createdAt: now(),
333 });
334321
335 const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>322 const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>
336 Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid);323 Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid);
...@@ -347,7 +334,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -347,7 +334,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
347 Effect.gen(function* () {334 Effect.gen(function* () {
348 const threadId =335 const threadId =
349 context.session.threadId ??336 context.session.threadId ??
350 ProviderThreadId.makeUnsafe(`claude-thread-${yield* randomId()}`);337 ProviderThreadId.makeUnsafe(`claude-thread-${yield* Random.nextUUIDv4}`);
351 return {338 return {
352 threadId,339 threadId,
353 turns: context.turns.map((turn) => ({340 turns: context.turns.map((turn) => ({
...@@ -360,9 +347,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -360,9 +347,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
360 const updateResumeCursor = (context: ClaudeSessionContext): Effect.Effect<void> =>347 const updateResumeCursor = (context: ClaudeSessionContext): Effect.Effect<void> =>
361 Effect.gen(function* () {348 Effect.gen(function* () {
362 const threadId = context.session.threadId;349 const threadId = context.session.threadId;
363 if (!threadId) {350 if (!threadId) return;
364 return;
365 }
366351
367 const resumeCursor = {352 const resumeCursor = {
368 threadId,353 threadId,
...@@ -374,7 +359,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -374,7 +359,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
374 context.session = {359 context.session = {
375 ...context.session,360 ...context.session,
376 resumeCursor,361 resumeCursor,
377 updatedAt: yield* now(),362 updatedAt: yield* nowIso,
378 };363 };
379 });364 });
380365
...@@ -387,7 +372,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -387,7 +372,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
387 const changed = context.session.threadId !== nextThreadId;372 const changed = context.session.threadId !== nextThreadId;
388373
389 if (changed) {374 if (changed) {
390 const updatedAt = yield* now();375 const updatedAt = yield* nowIso;
391 context.session = {376 context.session = {
392 ...context.session,377 ...context.session,
393 threadId: nextThreadId,378 threadId: nextThreadId,
...@@ -487,7 +472,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -487,7 +472,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
487 ...(errorMessage ? { errorMessage } : {}),472 ...(errorMessage ? { errorMessage } : {}),
488 });473 });
489474
490 const updatedAt = yield* now();475 const updatedAt = yield* nowIso;
491 context.turnState = undefined;476 context.turnState = undefined;
492 context.session = {477 context.session = {
493 ...context.session,478 ...context.session,
...@@ -697,9 +682,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -697,9 +682,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
697 options?: { readonly emitExitEvent?: boolean },682 options?: { readonly emitExitEvent?: boolean },
698 ): Effect.Effect<void> =>683 ): Effect.Effect<void> =>
699 Effect.gen(function* () {684 Effect.gen(function* () {
700 if (context.stopped) {685 if (context.stopped) return;
701 return;
702 }
703686
704 context.stopped = true;687 context.stopped = true;
705688
...@@ -725,11 +708,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -725,11 +708,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
725 yield* completeTurn(context, "interrupted", "Session stopped.");708 yield* completeTurn(context, "interrupted", "Session stopped.");
726 }709 }
727710
728 yield* Queue.offer(context.promptQueue, { type: "terminate" }).pipe(Effect.ignore);711 yield* Queue.shutdown(context.promptQueue);
729712
730 yield* Effect.sync(() => context.query.close()).pipe(Effect.ignoreCause);713 context.query.close();
731714
732 const updatedAt = yield* now();715 const updatedAt = yield* nowIso;
733 context.session = {716 context.session = {
734 ...context.session,717 ...context.session,
735 status: "closed",718 status: "closed",
...@@ -786,44 +769,31 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -786,44 +769,31 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
786 });769 });
787 }770 }
788771
789 const startedAt = yield* now();772 const startedAt = yield* nowIso;
790 const sessionId = ProviderSessionId.makeUnsafe(`claude-session-${yield* randomId()}`);773 const sessionId = ProviderSessionId.makeUnsafe(
774 `claude-session-${yield* Random.nextUUIDv4}`,
775 );
791 const resumeState = readClaudeResumeState(input.resumeCursor);776 const resumeState = readClaudeResumeState(input.resumeCursor);
792 const threadId =777 const threadId =
793 resumeState?.threadId ??778 resumeState?.threadId ??
794 ProviderThreadId.makeUnsafe(`claude-thread-${yield* randomId()}`);779 ProviderThreadId.makeUnsafe(`claude-thread-${yield* Random.nextUUIDv4}`);
795780
796 const promptQueue = yield* Queue.unbounded<PromptQueueItem>();781 const promptQueue = yield* Queue.unbounded<PromptQueueItem>();
797 const prompt: AsyncIterable<SDKUserMessage> = {782 const prompt = Stream.fromQueue(promptQueue).pipe(
798 [Symbol.asyncIterator]() {783 Stream.filter((item) => item.type === "message"),
799 return {784 Stream.map((item) => item.message),
800 next: async (): Promise<IteratorResult<SDKUserMessage>> => {785 Stream.toAsyncIterable,
801 const item = await Effect.runPromise(Queue.take(promptQueue));786 );
802 if (item.type === "terminate") {
803 return {
804 done: true,
805 value: undefined,
806 };
807 }
808
809 return {
810 done: false,
811 value: item.message,
812 };
813 },
814 };
815 },
816 };
817787
818 const pendingApprovals = new Map<ApprovalRequestId, PendingApproval>();788 const pendingApprovals = new Map<ApprovalRequestId, PendingApproval>();
819 const inFlightTools = new Map<number, ToolInFlight>();789 const inFlightTools = new Map<number, ToolInFlight>();
820790
821 let contextRef: ClaudeSessionContext | undefined;791 const contextRef = yield* Ref.make<ClaudeSessionContext | undefined>(undefined);
822792
823 const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) =>793 const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) =>
824 Effect.runPromise(794 Effect.runPromise(
825 Effect.gen(function* () {795 Effect.gen(function* () {
826 const context = contextRef;796 const context = yield* Ref.get(contextRef);
827 if (!context) {797 if (!context) {
828 return {798 return {
829 behavior: "deny",799 behavior: "deny",
...@@ -839,7 +809,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -839,7 +809,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
839 } satisfies PermissionResult;809 } satisfies PermissionResult;
840 }810 }
841811
842 const requestId = ApprovalRequestId.makeUnsafe(yield* randomId());812 const requestId = ApprovalRequestId.makeUnsafe(yield* Random.nextUUIDv4);
843 const requestKind = classifyRequestKind(toolName);813 const requestKind = classifyRequestKind(toolName);
844 const detail = summarizeToolRequest(toolName, toolInput);814 const detail = summarizeToolRequest(toolName, toolInput);
845 const decisionDeferred = yield* Deferred.make<ProviderApprovalDecision>();815 const decisionDeferred = yield* Deferred.make<ProviderApprovalDecision>();
...@@ -988,7 +958,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -988,7 +958,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
988 lastThreadStartedId: undefined,958 lastThreadStartedId: undefined,
989 stopped: false,959 stopped: false,
990 };960 };
991 contextRef = context;961 yield* Ref.set(contextRef, context);
992 sessions.set(sessionId, context);962 sessions.set(sessionId, context);
993963
994 const sessionStartedStamp = yield* makeEventStamp();964 const sessionStartedStamp = yield* makeEventStamp();
...@@ -1027,16 +997,16 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -1027,16 +997,16 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
1027 });997 });
1028 }998 }
1029999
1030 const turnId = ProviderTurnId.makeUnsafe(`claude-turn-${yield* randomId()}`);1000 const turnId = ProviderTurnId.makeUnsafe(`claude-turn-${yield* Random.nextUUIDv4}`);
1031 const turnState: ClaudeTurnState = {1001 const turnState: ClaudeTurnState = {
1032 turnId,1002 turnId,
1033 assistantItemId: `claude-message-${yield* randomId()}`,1003 assistantItemId: `claude-message-${yield* Random.nextUUIDv4}`,
1034 startedAt: yield* now(),1004 startedAt: yield* nowIso,
1035 items: [],1005 items: [],
1036 messageCompleted: false,1006 messageCompleted: false,
1037 };1007 };
10381008
1039 const updatedAt = yield* now();1009 const updatedAt = yield* nowIso;
1040 context.turnState = turnState;1010 context.turnState = turnState;
1041 context.session = {1011 context.session = {
1042 ...context.session,1012 ...context.session,