Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";

// T3-CUSTOM(expbkt3): one bounded seam keeps the upstream worker model intact.
import { runProviderCommandWithinLaneDeadline } from "../providerCommandLane.expbkt3.ts";
import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";
import { decideWorktreeRecovery, describeWorktreeRecreation } from "../threadWorktreeRecovery.ts";
import {
Expand Down Expand Up @@ -2981,7 +2983,12 @@ const make = Effect.gen(function* () {
});

const processDomainEventSafely = (event: ProviderIntentEvent) =>
processDomainEvent(event).pipe(
// T3-CUSTOM(expbkt3): bound the one global lane without replacing it.
runProviderCommandWithinLaneDeadline(processDomainEvent(event), {
eventType: event.type,
threadId: event.payload.threadId,
commandId: event.commandId,
}).pipe(
Effect.catchCause((cause) => {
if (Cause.hasInterruptsOnly(cause)) {
return Effect.interrupt;
Expand Down
39 changes: 39 additions & 0 deletions apps/server/src/orchestration/providerCommandLane.expbkt3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/** T3-CUSTOM(expbkt3): regression for the 2026-08-20 global prompt-queue outage. */
import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
import { assert, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Ref from "effect/Ref";
import * as TestClock from "effect/testing/TestClock";

import { runProviderCommandWithinLaneDeadline } from "./providerCommandLane.expbkt3.ts";

it.effect("lets the next worker item run when a provider command is uninterruptible", () =>
Effect.gen(function* () {
const firstStarted = yield* Deferred.make<void>();
const secondRan = yield* Ref.make(false);
const worker = yield* makeDrainableWorker((item: "wedged" | "next") =>
runProviderCommandWithinLaneDeadline(
item === "wedged"
? Deferred.succeed(firstStarted, undefined).pipe(
Effect.andThen(Effect.uninterruptible(Effect.never)),
)
: Ref.set(secondRan, true),
{
eventType: `test.${item}`,
threadId: item,
commandId: item,
timeout: "50 millis",
},
),
);

yield* worker.enqueue("wedged");
yield* Deferred.await(firstStarted);
yield* worker.enqueue("next");
yield* TestClock.adjust("60 millis");
yield* worker.drain;

assert.isTrue(yield* Ref.get(secondRan));
}).pipe(Effect.scoped),
);
43 changes: 43 additions & 0 deletions apps/server/src/orchestration/providerCommandLane.expbkt3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* T3-CUSTOM(expbkt3): Non-invasive deadline for the upstream provider-command lane.
*
* ProviderCommandReactor deliberately uses one ordered DrainableWorker. Keep
* that upstream model, but never join one provider operation forever: its
* finalizer may be uninterruptible while an SDK callback is pending. The
* command runs detached so timing out the join releases the lane immediately;
* cleanup is then requested without awaiting the same stuck finalizer.
*/
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as Option from "effect/Option";

export const DEFAULT_PROVIDER_COMMAND_LANE_DEADLINE: Duration.Input = "30 seconds";

export interface ProviderCommandLaneDeadlineInput {
readonly eventType: string;
readonly threadId: string;
readonly commandId: string | null;
readonly timeout?: Duration.Input;
}

export const runProviderCommandWithinLaneDeadline = <E, R>(
command: Effect.Effect<void, E, R>,
input: ProviderCommandLaneDeadlineInput,
) =>
Effect.gen(function* () {
// Start in the current scheduler turn so healthy commands retain the
// upstream worker's observable ordering; only their lifetime is detached.
const commandFiber = yield* command.pipe(Effect.forkDetach({ startImmediately: true }));
const timeout = input.timeout ?? DEFAULT_PROVIDER_COMMAND_LANE_DEADLINE;
const completed = yield* Fiber.join(commandFiber).pipe(Effect.timeoutOption(timeout));
if (Option.isSome(completed)) return;

yield* Fiber.interrupt(commandFiber).pipe(Effect.forkDetach, Effect.asVoid);
yield* Effect.logWarning("provider command timed out and released the reactor lane", {
eventType: input.eventType,
threadId: input.threadId,
commandId: input.commandId,
timeout: String(timeout),
});
});
Loading