import { CommandId, type OrchestrationV2ThreadProjection } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Ref from "effect/Ref"; import { IdAllocatorV2 } from "./IdAllocator.ts"; import { type ProviderContinuationRequest, ProviderContinuationRequests, } from "./ProviderContinuationRequests.ts"; import { ThreadManagementService } from "./ThreadManagementService.ts"; const CONTINUATION_MESSAGE_TEXT = "Background task completed."; function delegatedCompletionText(taskIds: ReadonlyArray): string { const taskList = taskIds.join(", "); return taskIds.length === 1 ? `Delegated task ${taskList} reached a terminal state. Use task_status with taskId ${taskList} to read the result.` : `Delegated tasks ${taskList} reached terminal states. Use task_status with each taskId to read the results.`; } function currentDelegatedCompletionDelivery( projection: OrchestrationV2ThreadProjection, completion: NonNullable, ) { const sourceRun = projection.runs.find((candidate) => candidate.id === completion.parentRunId); const delivery = sourceRun?.delegatedCompletion?.delivery; const alreadyDispatched = projection.messages.some( (message) => message.id === completion.messageId, ); if ( sourceRun?.delegatedCompletion?.disposition !== "open" || delivery === null || delivery === undefined || delivery.generation !== completion.generation || delivery.messageId !== completion.messageId || alreadyDispatched ) { return undefined; } return delivery; } function delegatedCompletionRetryKey( request: ProviderContinuationRequest, completion: NonNullable, ): string { return `${request.threadId}:${completion.parentRunId}:${completion.generation}:${completion.messageId}`; } /** * Drains ProviderContinuationRequests and dispatches an internal * message.dispatch per request so the wake turn buffered by the adapter is * ingested as a normal run. Dispatches queue_after_active, so a continuation * racing a user run simply queues behind it and drains the wake buffer once * that run finishes. */ export const workerLive = Layer.effectDiscard( Effect.gen(function* () { const ids = yield* IdAllocatorV2; const requests = yield* ProviderContinuationRequests; const threads = yield* ThreadManagementService; const retryAttempts = yield* Ref.make(new Map()); const clearRetryAttempt = (key: string) => Ref.update(retryAttempts, (current) => { if (!current.has(key)) return current; const updated = new Map(current); updated.delete(key); return updated; }); const nextRetryDelay = (key: string) => Ref.modify(retryAttempts, (current) => { const attempt = current.get(key) ?? 0; const updated = new Map(current); updated.set(key, attempt + 1); return [Math.min(100 * 2 ** Math.min(attempt, 6), 5_000), updated] as const; }); const dispatchContinuation = Effect.fn("ProviderContinuationService.dispatchContinuation")( function* (request: ProviderContinuationRequest) { const projection = yield* threads.getThreadProjection(request.threadId); if (projection.thread.archivedAt !== null || projection.thread.deletedAt !== null) { yield* Effect.logInfo("orchestration-v2.provider-continuation.thread-archived", { threadId: request.threadId, providerThreadId: request.providerThreadId, }); if (request.delegatedCompletion !== undefined) { yield* clearRetryAttempt( delegatedCompletionRetryKey(request, request.delegatedCompletion), ); } // No continuation turn will start to clear the adapter's sticky offer. if (request.clearIfCurrent !== undefined) { yield* request.clearIfCurrent(); } else if (request.dispatchIfCurrent !== undefined) { // Backward compatibility for request producers without an explicit // drop callback. yield* request.dispatchIfCurrent(Effect.void); } return; } if (request.delegatedCompletion !== undefined) { const retryKey = delegatedCompletionRetryKey(request, request.delegatedCompletion); const delivery = currentDelegatedCompletionDelivery( projection, request.delegatedCompletion, ); if (delivery === undefined) { yield* clearRetryAttempt(retryKey); return; } const commandId = yield* ids.allocate.command({ fixtureName: "delegated-completion", commandName: "dispatch", }); yield* threads.dispatch({ type: "message.dispatch", commandId, threadId: request.threadId, messageId: delivery.messageId, text: delegatedCompletionText(delivery.taskIds), attachments: [], dispatchMode: { type: "queue_after_active" }, createdBy: "agent", creationSource: "server", delegatedCompletion: { parentRunId: request.delegatedCompletion.parentRunId, generation: delivery.generation, taskIds: delivery.taskIds, }, }); yield* clearRetryAttempt(retryKey); return; } // The ordinal is display metadata only: allocate.message appends a // random UUID, so a stale projection read here cannot collide ids. const messageId = yield* ids.allocate.message({ threadId: request.threadId, ordinal: projection.messages.length + 1, }); const commandId = CommandId.make(`provider-continuation:${messageId}`); const dispatch = threads.dispatch({ type: "message.dispatch", commandId, threadId: request.threadId, messageId, text: request.detail ?? CONTINUATION_MESSAGE_TEXT, attachments: [], dispatchMode: { type: "queue_after_active" }, createdBy: "agent", // "provider" marks an adapter-buffered wake, which ClaudeAdapterV2 // detects to attach the buffered CLI output and drop this text. A // message_text wake has no buffered output, so it must not carry that // marker or the turn settles immediately having prompted nothing. creationSource: request.delivery === "message_text" ? "server" : "provider", }); if (request.dispatchIfCurrent === undefined) { yield* dispatch; return; } yield* request.dispatchIfCurrent(dispatch); }, ); yield* requests.take.pipe( Effect.flatMap((request) => dispatchContinuation(request).pipe( Effect.catchCause((cause) => Effect.gen(function* () { yield* Effect.logWarning("orchestration-v2.provider-continuation.dispatch-failed", { threadId: request.threadId, providerThreadId: request.providerThreadId, cause, }); if (request.delegatedCompletion !== undefined) { const completion = request.delegatedCompletion; const retryKey = delegatedCompletionRetryKey(request, completion); const retryDelay = yield* nextRetryDelay(retryKey); yield* Effect.gen(function* () { yield* Effect.sleep(`${retryDelay} millis`); const projection = yield* threads.getThreadProjection(request.threadId); if (currentDelegatedCompletionDelivery(projection, completion) !== undefined) { yield* requests.offer(request); } else { yield* clearRetryAttempt(retryKey); } }).pipe( Effect.catchCause((retryCause) => Effect.logWarning("orchestration-v2.provider-continuation.retry-check-failed", { threadId: request.threadId, providerThreadId: request.providerThreadId, cause: retryCause, }).pipe(Effect.andThen(requests.offer(request))), ), Effect.forkScoped, ); } }), ), ), ), Effect.forever, Effect.forkScoped, ); }), );