diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 467d58702df01..9e13faa56e44c 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -3444,6 +3444,51 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); + it.effect("rosters an artifact live-updates watcher as a monitor, not a subagent", () => + Effect.scoped( + Effect.gen(function* () { + const harness = yield* makeWakeHarness; + const now = yield* DateTime.now; + const taskId = "artifact-watch-1"; + const description = "live updates for artifact https://claude.ai/artifact/abc"; + yield* harness.runtime.startTurn( + makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now, + attemptId: RunAttemptId.make("attempt-claude-artifact-watch"), + text: "Publish the report.", + attachments: [], + }), + ); + yield* Queue.offer( + harness.sdkMessages, + claudeSdkFrame({ + type: "system", + subtype: "task_started", + task_id: taskId, + tool_use_id: "toolu_artifact_1", + description, + task_type: "monitor_ws", + uuid: "00000000-0000-4000-8000-000000000901", + session_id: WAKE_NATIVE_SESSION, + }), + ); + yield* Queue.offer( + harness.sdkMessages, + makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000902", result: "Published." }), + ); + yield* awaitUntil(() => harness.terminalEvents().length === 1, "turn terminal"); + + assert.deepEqual( + providerThreadRosterEvents(harness.events).at(-1)?.providerThread.pendingBackgroundTasks, + [{ taskId, kind: "monitor", description }], + ); + assert.isFalse(harness.events.some((event) => event.type === "subagent.updated")); + }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), + ), + ); + it.effect("stops background work after the turn settled", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 48deeb4ae38fc..e24bbb7da7b52 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -1674,11 +1674,16 @@ function commandInputFromClaudeTool(toolName: string, input: ClaudeNativeToolInp // Opaque non-subagent background work admitted onto the Waiting roster, by // Claude SDK `task_type`, and the kind the roster names it by. Subagents // project through the normal subagent lifecycle and must not be -// double-counted when background_tasks_changed includes them. +// double-counted when background_tasks_changed includes them. monitor_ws is +// the websocket watch Claude Code arms for a published artifact; it runs no +// model, so it is a monitor, not a subagent. const CLAUDE_OPAQUE_BACKGROUND_TASK_KINDS: ReadonlyMap< string, Exclude -> = new Map([["local_bash", "command"]]); +> = new Map([ + ["local_bash", "command"], + ["monitor_ws", "monitor"], +]); function isClaudeOpaqueBackgroundTaskType(taskType: string | null | undefined): boolean { return typeof taskType === "string" && CLAUDE_OPAQUE_BACKGROUND_TASK_KINDS.has(taskType);