Skip to content

Commit 474cc5f

Browse files
authored
fix(server): ignore Claude command lifecycle messages (#6606)
1 parent e204f5a commit 474cc5f

2 files changed

Lines changed: 76 additions & 0 deletions

File tree

apps/server/src/provider/Layers/ClaudeAdapter.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2287,6 +2287,77 @@ describe("ClaudeAdapterLive", () => {
22872287
);
22882288
});
22892289

2290+
it.effect("consumes Claude command lifecycle notifications silently", () => {
2291+
const harness = makeHarness();
2292+
return Effect.gen(function* () {
2293+
const adapter = yield* ClaudeAdapter;
2294+
const sessionId = "6e81554e-5cff-4b37-8a39-f3a9051ac234";
2295+
2296+
yield* adapter.startSession({
2297+
threadId: THREAD_ID,
2298+
provider: ProviderDriverKind.make("claudeAgent"),
2299+
runtimeMode: "full-access",
2300+
});
2301+
2302+
const readyMessage = "command lifecycle test ready";
2303+
const readyFiber = yield* Stream.takeUntil(
2304+
adapter.streamEvents,
2305+
(event) => event.type === "runtime.warning" && event.payload.message === readyMessage,
2306+
).pipe(Stream.runDrain, Effect.forkChild);
2307+
harness.query.emit({
2308+
type: "system",
2309+
subtype: "notification",
2310+
key: "command-lifecycle-ready",
2311+
text: readyMessage,
2312+
priority: "high",
2313+
session_id: sessionId,
2314+
uuid: "command-lifecycle-ready",
2315+
} as unknown as SDKMessage);
2316+
yield* Fiber.join(readyFiber);
2317+
2318+
const processedMessage = "command lifecycle messages processed";
2319+
const runtimeEventsFiber = yield* Stream.takeUntil(
2320+
adapter.streamEvents,
2321+
(event) => event.type === "runtime.warning" && event.payload.message === processedMessage,
2322+
).pipe(Stream.runCollect, Effect.forkChild);
2323+
for (const [state, uuid] of [
2324+
["started", "command-started"],
2325+
["completed", "command-completed"],
2326+
]) {
2327+
harness.query.emit({
2328+
type: "command_lifecycle",
2329+
command_uuid: "4cd8e8a3-df7a-425d-b6c9-4053abc0b8fd",
2330+
state,
2331+
session_id: sessionId,
2332+
uuid,
2333+
} as unknown as SDKMessage);
2334+
}
2335+
harness.query.emit({
2336+
type: "system",
2337+
subtype: "notification",
2338+
key: "command-lifecycle-processed",
2339+
text: processedMessage,
2340+
priority: "high",
2341+
session_id: sessionId,
2342+
uuid: "command-lifecycle-processed",
2343+
} as unknown as SDKMessage);
2344+
2345+
const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber));
2346+
assert.deepEqual(
2347+
runtimeEvents.map((event) => event.type),
2348+
["runtime.warning"],
2349+
);
2350+
const warning = runtimeEvents[0];
2351+
assert.equal(warning?.type, "runtime.warning");
2352+
if (warning?.type === "runtime.warning") {
2353+
assert.equal(warning.payload.message, processedMessage);
2354+
}
2355+
}).pipe(
2356+
Effect.provideService(Random.Random, makeDeterministicRandomService()),
2357+
Effect.provide(harness.layer),
2358+
);
2359+
});
2360+
22902361
it.effect("emits thread token usage updates from Claude task progress", () => {
22912362
const harness = makeHarness();
22922363
return Effect.gen(function* () {

apps/server/src/provider/Layers/ClaudeAdapter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3490,6 +3490,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
34903490
yield* logNativeSdkMessage(context, message);
34913491
yield* ensureThreadId(context, message);
34923492

3493+
// Wire-only command bookkeeping has no user-facing T3 lifecycle.
3494+
if (sdkMessageType(message) === "command_lifecycle") {
3495+
return;
3496+
}
3497+
34933498
switch (message.type) {
34943499
case "stream_event":
34953500
yield* handleStreamEvent(context, message);

0 commit comments

Comments
 (0)