Skip to content

Commit a7ce771

Browse files
committed
fix(server): recheck Claude outcomes after Stop registration
1 parent 3418487 commit a7ce771

2 files changed

Lines changed: 57 additions & 0 deletions

File tree

‎apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import * as Option from "effect/Option";
6060
import * as Path from "effect/Path";
6161
import * as Queue from "effect/Queue";
6262
import * as Schema from "effect/Schema";
63+
import * as Scheduler from "effect/Scheduler";
6364
import * as Scope from "effect/Scope";
6465
import * as Stream from "effect/Stream";
6566
import * as TestClock from "effect/testing/TestClock";
@@ -9002,6 +9003,53 @@ describe("ClaudeAdapterV2 background wake turns", () => {
90029003
),
90039004
);
90049005

9006+
it.effect.each([1, 2, 3, 4, 5, 6, 7, 8])(
9007+
"accepts completion during Stop at scheduler step %s",
9008+
(pauseAt) =>
9009+
Effect.scoped(
9010+
Effect.gen(function* () {
9011+
const fixture = yield* makeStopFixture();
9012+
const resumptions = yield* Queue.unbounded<() => void>();
9013+
const scheduler = yield* Scheduler.Scheduler;
9014+
const dispatcher = scheduler.makeDispatcher();
9015+
let checks = 0;
9016+
// Pause Stop before its SDK request and deliver the outcome while it is suspended.
9017+
const stopping = yield* fixture.stop().pipe(
9018+
Effect.provideService(Scheduler.Scheduler, {
9019+
executionMode: scheduler.executionMode,
9020+
shouldYield: () => ++checks === pauseAt,
9021+
makeDispatcher: () => ({
9022+
scheduleTask: (task, priority) => {
9023+
if (checks === pauseAt) Queue.offerUnsafe(resumptions, task);
9024+
else dispatcher.scheduleTask(task, priority);
9025+
},
9026+
flush: () => dispatcher.flush(),
9027+
}),
9028+
}),
9029+
Effect.exit,
9030+
Effect.forkScoped,
9031+
);
9032+
const resume = yield* Queue.take(resumptions);
9033+
yield* fixture.notify("completed");
9034+
const completed = yield* Queue.take(fixture.subagentReceipts);
9035+
yield* Effect.sync(resume);
9036+
yield* TestClock.adjust("2 seconds");
9037+
assert.deepEqual(
9038+
yield* Fiber.join(stopping),
9039+
Exit.succeed({
9040+
status: "completed" as const,
9041+
completedAt: completed.subagent.completedAt!,
9042+
result: "Native outcome.",
9043+
}),
9044+
);
9045+
}).pipe(
9046+
Effect.provide(
9047+
Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
9048+
),
9049+
),
9050+
),
9051+
);
9052+
90059053
it.effect("retains native confirmation after the synchronous Agent tool result", () =>
90069054
Effect.scoped(
90079055
Effect.gen(function* () {

‎apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9115,6 +9115,15 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
91159115
subagentStopOutcomes.get(input.nativeTaskId) ??
91169116
(yield* Deferred.make<ProviderAdapter.ProviderAdapterV2SubagentOutcome>());
91179117
subagentStopOutcomes.set(input.nativeTaskId, outcome);
9118+
const recorded = (yield* Ref.get(sessionSubagentsByTaskId)).get(input.nativeTaskId);
9119+
if (
9120+
recorded?.task.startedAt === subagent.task.startedAt &&
9121+
recorded.confirmedOutcome !== undefined
9122+
) {
9123+
if (subagentStopOutcomes.get(input.nativeTaskId) === outcome)
9124+
subagentStopOutcomes.delete(input.nativeTaskId);
9125+
return recorded.confirmedOutcome;
9126+
}
91189127
const confirmedOutcome = yield* existing.query.stopTask(input.nativeTaskId).pipe(
91199128
Effect.andThen(Deferred.await(outcome)),
91209129
Effect.timeout("2 seconds"),

0 commit comments

Comments
 (0)