diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/muse_transcript.ndjson index 78f74648311d..e36ec84eaeea 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/muse_transcript.ndjson @@ -28,6 +28,8 @@ {"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:15","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"537e3fcf-361b-4956-ba2b-313a175666eb","sequence":71},"last":{"id":"3ccbc753-f023-4b12-94f2-d1477c0400d1","sequence":74}},"item":{"itemId":"537e3fcf-361b-4956-ba2b-313a175666eb","kind":"reminderChild","turnId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4","revision":2,"status":"completed","recordedAt":"2026-10-08T07:30:21.003178Z","fallbackText":"Reminder child session","childSessionId":"00dc6e2b-34de-4607-8156-9a033b3eed7d","reminderAgentId":"verify-reminder","generationId":1,"taskId":"01a11a6b-8c06-7953-856f-7eaea4ce7c19","childSessionLogPath":"subagent/00dc6e2b-34de-4607-8156-9a033b3eed7d/session.jsonl","blocking":true}},"emittedAtMs":1791444621008}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","status":"idle","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:16"},"emittedAtMs":1791444621046}} {"type":"emit_inbound","label":"turn/completed","frame":{"jsonrpc":"2.0","method":"turn/completed","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:16","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"688a69c5-e353-4db9-bd8c-1beb033902ba","sequence":79},"last":{"id":"688a69c5-e353-4db9-bd8c-1beb033902ba","sequence":79}},"turnId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4","terminal":"completed","durationMs":9378,"timeToFirstTokenMs":6406},"emittedAtMs":1791444621047}} +{"type":"expect_outbound","label":"task/list","frame":{"jsonrpc":"2.0","id":5,"method":"task/list","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0"}}} +{"type":"emit_inbound","label":"response:task/list","frame":{"jsonrpc":"2.0","id":5,"result":{"tasks":[]}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:17","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"36753b8a-26ce-4935-8127-bc9191cb328b","sequence":80},"last":{"id":"36753b8a-26ce-4935-8127-bc9191cb328b","sequence":80}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791444621054}} {"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":4,"method":"turn/start","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images and videos in your response using Markdown with absolute file paths.\n\n\nWhen the t3-code MCP server exposes link_pull_request, you must use it to register every pull request you create or work on for this thread. Call link_pull_request with the full PR URL immediately after creating a PR or starting work on an existing PR. For a stack, call it for every layer, not just the current branch or the top PR. This applies when creating or updating PRs through gh, gh stack, another CLI, or the host API: those operations do not register the PRs with this thread. Linking an already-linked PR is safe. Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing. Do not link unrelated PRs mentioned only as background. If a linking call fails, report that failure instead of claiming the PR is linked. When asked to monitor, watch, or babysit a PR and watch_pull_request is available, call it and end your turn: T3 Code wakes you when checks finish, someone else comments, or the branch conflicts, so do not poll or run your own watcher. When you hand the work back to the user, call unwatch_pull_request first so the thread returns to their inbox.\n"},{"type":"text","text":"Respond with exactly: second fixture turn complete"}],"displayText":"Respond with exactly: second fixture turn complete","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6b-97ec-7000-bd1b-d7afbea11045"}}} {"type":"emit_inbound","label":"response:turn/start","frame":{"jsonrpc":"2.0","id":4,"result":{"commandId":"01a11a6b-97ec-7000-bd1b-d7afbea11045","status":"accepted","turnId":"01a11a6b-97ec-7000-bd1b-d7afbea11045","startedNewTurn":true,"disposition":"started"}}} @@ -49,5 +51,7 @@ {"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:30","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"396f33da-f205-4062-9082-0a24038b9156","sequence":136},"last":{"id":"b10fa520-7ed1-487e-86ac-92927f5dc02b","sequence":140}},"item":{"itemId":"396f33da-f205-4062-9082-0a24038b9156","kind":"reminderChild","turnId":"01a11a6b-97ec-7000-bd1b-d7afbea11045","revision":2,"status":"completed","recordedAt":"2026-10-08T07:30:25.692766Z","fallbackText":"Reminder child session","childSessionId":"4a318cd8-2669-4e76-9202-ee12d5858e0a","reminderAgentId":"verify-reminder","generationId":1,"taskId":"01a11a6b-9f69-73e0-ba18-a0d873a1c0dd","childSessionLogPath":"subagent/4a318cd8-2669-4e76-9202-ee12d5858e0a/session.jsonl","blocking":true}},"emittedAtMs":1791444625704}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","status":"idle","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:31"},"emittedAtMs":1791444625758}} {"type":"emit_inbound","label":"turn/completed","frame":{"jsonrpc":"2.0","method":"turn/completed","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:31","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"aacefdf9-52b1-4d5d-8648-58ec64cc2925","sequence":148},"last":{"id":"aacefdf9-52b1-4d5d-8648-58ec64cc2925","sequence":148}},"turnId":"01a11a6b-97ec-7000-bd1b-d7afbea11045","terminal":"completed","durationMs":4428,"timeToFirstTokenMs":1805},"emittedAtMs":1791444625759}} +{"type":"expect_outbound","label":"task/list","frame":{"jsonrpc":"2.0","id":6,"method":"task/list","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0"}}} +{"type":"emit_inbound","label":"response:task/list","frame":{"jsonrpc":"2.0","id":6,"result":{"tasks":[]}}} {"type":"emit_inbound","label":"session/closed","frame":{"jsonrpc":"2.0","method":"session/closed","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","reason":"hostShutdown","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:31"},"emittedAtMs":1791444625952}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","status":"notLoaded","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:31"},"emittedAtMs":1791444625952}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/muse_permission/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/muse_permission/muse_transcript.ndjson index 33bc408411cd..20170758760b 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/muse_permission/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/muse_permission/muse_transcript.ndjson @@ -41,6 +41,8 @@ {"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:22","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"25bcb5c8-867e-480f-91ea-70369e4ea395","sequence":112},"last":{"id":"12072a5e-75cc-4e9d-98a3-ca6f8103fb20","sequence":115}},"item":{"itemId":"25bcb5c8-867e-480f-91ea-70369e4ea395","kind":"reminderChild","turnId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6","revision":2,"status":"completed","recordedAt":"2026-10-08T07:32:27.344136Z","fallbackText":"Reminder child session","childSessionId":"a5bc3f6b-6d7a-4b8c-8d01-338193f4a413","reminderAgentId":"verify-reminder","generationId":1,"taskId":"01a11a6d-4edf-7f73-9ba4-3793a7dfda49","childSessionLogPath":"subagent/a5bc3f6b-6d7a-4b8c-8d01-338193f4a413/session.jsonl","blocking":true}},"emittedAtMs":1791444747348}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","status":"idle","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:23"},"emittedAtMs":1791444747386}} {"type":"emit_inbound","label":"turn/completed","frame":{"jsonrpc":"2.0","method":"turn/completed","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:23","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"7c29ac76-7e09-48c1-a18f-f336126e27d1","sequence":120},"last":{"id":"7c29ac76-7e09-48c1-a18f-f336126e27d1","sequence":120}},"turnId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6","terminal":"completed","durationMs":18608,"timeToFirstTokenMs":4791},"emittedAtMs":1791444747387}} +{"type":"expect_outbound","label":"task/list","frame":{"jsonrpc":"2.0","id":7,"method":"task/list","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11"}}} +{"type":"emit_inbound","label":"response:task/list","frame":{"jsonrpc":"2.0","id":7,"result":{"tasks":[]}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:24","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"1e4afe30-d924-444c-8665-5e763f524e70","sequence":121},"last":{"id":"1e4afe30-d924-444c-8665-5e763f524e70","sequence":121}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791444747394}} {"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":5,"method":"turn/start","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images and videos in your response using Markdown with absolute file paths.\n\n\nWhen the t3-code MCP server exposes link_pull_request, you must use it to register every pull request you create or work on for this thread. Call link_pull_request with the full PR URL immediately after creating a PR or starting work on an existing PR. For a stack, call it for every layer, not just the current branch or the top PR. This applies when creating or updating PRs through gh, gh stack, another CLI, or the host API: those operations do not register the PRs with this thread. Linking an already-linked PR is safe. Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing. Do not link unrelated PRs mentioned only as background. If a linking call fails, report that failure instead of claiming the PR is linked. When asked to monitor, watch, or babysit a PR and watch_pull_request is available, call it and end your turn: T3 Code wakes you when checks finish, someone else comments, or the branch conflicts, so do not poll or run your own watcher. When you hand the work back to the user, call unwatch_pull_request first so the thread returns to their inbox.\n"},{"type":"text","text":"Run the shell command `touch muse-declined.txt` with your shell tool once. If it is rejected, do not retry; reply REJECTED."}],"displayText":"Run the shell command `touch muse-declined.txt` with your shell tool once. If it is rejected, do not retry; reply REJECTED.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6d-8566-7000-8ad1-6269ac187a29"}}} {"type":"emit_inbound","label":"response:turn/start","frame":{"jsonrpc":"2.0","id":5,"result":{"commandId":"01a11a6d-8566-7000-8ad1-6269ac187a29","status":"accepted","turnId":"01a11a6d-8566-7000-8ad1-6269ac187a29","startedNewTurn":true,"disposition":"started"}}} @@ -74,5 +76,7 @@ {"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:43","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"36e755a6-30e8-4798-a039-5d89a4c8d180","sequence":217},"last":{"id":"99de4c3b-df37-4b82-9b3c-095173ebe05c","sequence":221}},"item":{"itemId":"36e755a6-30e8-4798-a039-5d89a4c8d180","kind":"reminderChild","turnId":"01a11a6d-8566-7000-8ad1-6269ac187a29","revision":2,"status":"completed","recordedAt":"2026-10-08T07:32:44.524269Z","fallbackText":"Reminder child session","childSessionId":"95821451-6876-43e7-ae4c-25c75316a3e3","reminderAgentId":"verify-reminder","generationId":1,"taskId":"01a11a6d-96c9-74b1-8e7e-505b70ec091c","childSessionLogPath":"subagent/95821451-6876-43e7-ae4c-25c75316a3e3/session.jsonl","blocking":true}},"emittedAtMs":1791444764535}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","status":"idle","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:44"},"emittedAtMs":1791444764581}} {"type":"emit_inbound","label":"turn/completed","frame":{"jsonrpc":"2.0","method":"turn/completed","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:44","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"67ced310-34c3-44ad-9b6c-619649d52af2","sequence":229},"last":{"id":"67ced310-34c3-44ad-9b6c-619649d52af2","sequence":229}},"turnId":"01a11a6d-8566-7000-8ad1-6269ac187a29","terminal":"completed","durationMs":16926,"timeToFirstTokenMs":4222},"emittedAtMs":1791444764582}} +{"type":"expect_outbound","label":"task/list","frame":{"jsonrpc":"2.0","id":8,"method":"task/list","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11"}}} +{"type":"emit_inbound","label":"response:task/list","frame":{"jsonrpc":"2.0","id":8,"result":{"tasks":[]}}} {"type":"emit_inbound","label":"session/closed","frame":{"jsonrpc":"2.0","method":"session/closed","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","reason":"hostShutdown","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:44"},"emittedAtMs":1791444764791}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","status":"notLoaded","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:44"},"emittedAtMs":1791444764791}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/muse_subagent_approval/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/muse_subagent_approval/muse_transcript.ndjson index 21b68a4ba72e..cb1dc83f0675 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/muse_subagent_approval/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/muse_subagent_approval/muse_transcript.ndjson @@ -64,6 +64,8 @@ {"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:37","sourceRange":{"stream":{"kind":"session","id":"01a12d1d-a1b7-7000-9697-d430c2e2afd8"},"first":{"id":"de85ff78-ce8a-4237-9b4e-ab74ac911fa2","sequence":200},"last":{"id":"7c0fc5d8-b5d4-4200-83e9-7a9f73f762b7","sequence":203}},"item":{"itemId":"de85ff78-ce8a-4237-9b4e-ab74ac911fa2","kind":"reminderChild","turnId":"01a12d1d-a348-7000-ae97-9d27b5f80bce","revision":2,"status":"completed","recordedAt":"2026-10-11T22:38:31.96353Z","fallbackText":"Reminder child session","childSessionId":"9f805ae2-dd39-45f0-80b1-8cfa7e101f97","reminderAgentId":"verify-reminder","generationId":1,"taskId":"01a12d1d-e699-7410-98e0-74293e74c0fe","blocking":true}},"emittedAtMs":1791758311972}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","status":"idle","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:38"},"emittedAtMs":1791758312007}} {"type":"emit_inbound","label":"turn/completed","frame":{"jsonrpc":"2.0","method":"turn/completed","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:38","sourceRange":{"stream":{"kind":"session","id":"01a12d1d-a1b7-7000-9697-d430c2e2afd8"},"first":{"id":"567c1e33-4fb4-4f52-b8a1-2f8a6f114404","sequence":208},"last":{"id":"567c1e33-4fb4-4f52-b8a1-2f8a6f114404","sequence":208}},"turnId":"01a12d1d-a348-7000-ae97-9d27b5f80bce","terminal":"completed","durationMs":32467,"timeToFirstTokenMs":2945},"emittedAtMs":1791758312007}} +{"type":"expect_outbound","label":"task/list","frame":{"jsonrpc":"2.0","id":5,"method":"task/list","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8"}}} +{"type":"emit_inbound","label":"response:task/list","frame":{"jsonrpc":"2.0","id":5,"result":{"tasks":[]}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:39","sourceRange":{"stream":{"kind":"session","id":"01a12d1d-a1b7-7000-9697-d430c2e2afd8"},"first":{"id":"22e714fa-df1b-4554-978b-253a71130a55","sequence":209},"last":{"id":"22e714fa-df1b-4554-978b-253a71130a55","sequence":209}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791758312014}} {"type":"emit_inbound","label":"session/closed","frame":{"jsonrpc":"2.0","method":"session/closed","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","reason":"hostShutdown","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:39"},"emittedAtMs":1791758312288}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","status":"notLoaded","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:39"},"emittedAtMs":1791758312288}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/muse_workflow/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/muse_workflow/muse_transcript.ndjson index bf8a2da7960e..7f5d7ee52a98 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/muse_workflow/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/muse_workflow/muse_transcript.ndjson @@ -28,6 +28,8 @@ {"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:14","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"95574bfe-853f-44cc-81f6-88b05d6c8c16","sequence":38},"last":{"id":"b5c169b1-4200-4546-bb40-16513a743e61","sequence":80}},"item":{"itemId":"95574bfe-853f-44cc-81f6-88b05d6c8c16","kind":"reminderChild","turnId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5","revision":2,"status":"completed","recordedAt":"2026-10-11T23:47:36.008161Z","fallbackText":"Reminder child session","childSessionId":"b16dbb52-7f62-4a5a-84c2-f68d7ea22b18","reminderAgentId":"skill-reminder","generationId":1,"taskId":"01a12d5d-4375-7d72-b5b2-4b096d0df507"}},"emittedAtMs":1791762456016}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","status":"idle","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:15"},"emittedAtMs":1791762456056}} {"type":"emit_inbound","label":"turn/completed","frame":{"jsonrpc":"2.0","method":"turn/completed","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:15","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"a70694d6-83fb-43c9-b3c8-6b4e3083b1a7","sequence":85},"last":{"id":"a70694d6-83fb-43c9-b3c8-6b4e3083b1a7","sequence":85}},"turnId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5","terminal":"completed","durationMs":6948,"timeToFirstTokenMs":2452},"emittedAtMs":1791762456057}} +{"type":"expect_outbound","label":"task/list","frame":{"jsonrpc":"2.0","id":4,"method":"task/list","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23"}}} +{"type":"emit_inbound","label":"response:task/list","frame":{"jsonrpc":"2.0","id":4,"result":{"tasks":[]}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:16","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"f8defab1-74c6-4884-a29a-7cf2241cd4b7","sequence":86},"last":{"id":"f8defab1-74c6-4884-a29a-7cf2241cd4b7","sequence":86}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791762456064}} {"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:17","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"e1f5bbdc-9a2d-52c5-8501-9b6ef280aa94","sequence":70},"last":{"id":"95bd9a14-043c-4cd0-9811-c8a2f95a8f54","sequence":90}},"item":{"itemId":"e1f5bbdc-9a2d-52c5-8501-9b6ef280aa94","kind":"workflow","turnId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5","revision":2,"status":"inProgress","recordedAt":"2026-10-11T23:47:36.149221Z","fallbackText":"Workflow: model-chosen generated workflow","workflowRunId":"workflow-run-model-tool-call_01a12d5d5b7976179c1800af8310d1be","entryId":"ping-pong","scriptId":"generated.workflow.ping-pong","triggerSource":"guidanceAuto","children":[{"childId":"01a12d5d-5e54-7f10-a55a-9537347f2ad8","attempt":1,"status":"scheduled","label":"ping"}],"stopAvailable":true}},"emittedAtMs":1791762456153}} {"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:18","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"e1f5bbdc-9a2d-52c5-8501-9b6ef280aa94","sequence":70},"last":{"id":"f6d87a32-b02b-432f-8f8e-752e23a41c4a","sequence":103}},"item":{"itemId":"e1f5bbdc-9a2d-52c5-8501-9b6ef280aa94","kind":"workflow","turnId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5","revision":3,"status":"inProgress","recordedAt":"2026-10-11T23:47:36.309016Z","fallbackText":"Workflow: model-chosen generated workflow","workflowRunId":"workflow-run-model-tool-call_01a12d5d5b7976179c1800af8310d1be","entryId":"ping-pong","scriptId":"generated.workflow.ping-pong","triggerSource":"guidanceAuto","children":[{"childId":"01a12d5d-5e54-7f10-a55a-9537347f2ad8","attempt":1,"status":"started"}],"stopAvailable":true}},"emittedAtMs":1791762456316}} @@ -59,5 +61,7 @@ {"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:38","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"fed904c1-8f75-4661-a422-16d183abcad0","sequence":189},"last":{"id":"0337115b-c1f0-44a0-a99e-35332b7bca1e","sequence":192}},"item":{"itemId":"fed904c1-8f75-4661-a422-16d183abcad0","kind":"reminderChild","turnId":"01a12d5d-6ba0-75d1-aa8f-0d40852d8721","revision":2,"status":"completed","recordedAt":"2026-10-11T23:47:46.293256Z","fallbackText":"Reminder child session","childSessionId":"d54b1aec-3e73-4d33-a3ce-429940c6ae26","reminderAgentId":"verify-reminder","generationId":1,"taskId":"01a12d5d-7604-77e1-b7e9-1e5b03eb471c","childSessionLogPath":"subagent/d54b1aec-3e73-4d33-a3ce-429940c6ae26/session.jsonl","blocking":true}},"emittedAtMs":1791762466297}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","status":"idle","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:39"},"emittedAtMs":1791762466332}} {"type":"emit_inbound","label":"turn/completed","frame":{"jsonrpc":"2.0","method":"turn/completed","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:39","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"b8de98c6-4b19-4998-9daf-545f96b65ecc","sequence":197},"last":{"id":"b8de98c6-4b19-4998-9daf-545f96b65ecc","sequence":197}},"turnId":"01a12d5d-6ba0-75d1-aa8f-0d40852d8721","terminal":"completed","durationMs":6759,"timeToFirstTokenMs":2531},"emittedAtMs":1791762466333}} +{"type":"expect_outbound","label":"task/list","frame":{"jsonrpc":"2.0","id":5,"method":"task/list","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23"}}} +{"type":"emit_inbound","label":"response:task/list","frame":{"jsonrpc":"2.0","id":5,"result":{"tasks":[]}}} {"type":"emit_inbound","label":"session/closed","frame":{"jsonrpc":"2.0","method":"session/closed","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","reason":"hostShutdown","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:39"},"emittedAtMs":1791762466580}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","status":"notLoaded","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:39"},"emittedAtMs":1791762466580}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/simple/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/simple/muse_transcript.ndjson index 593a6f98ab53..e73d7db7ae85 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/simple/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/simple/muse_transcript.ndjson @@ -28,6 +28,8 @@ {"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:15","sourceRange":{"stream":{"kind":"session","id":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5"},"first":{"id":"720d1294-2aa3-492f-b602-c3f892bdf7d3","sequence":67},"last":{"id":"4c252852-689b-457c-8b30-af557b2b7acf","sequence":71}},"item":{"itemId":"720d1294-2aa3-492f-b602-c3f892bdf7d3","kind":"reminderChild","turnId":"01a11a6a-be41-7000-8db9-1d84c1b8be48","revision":2,"status":"completed","recordedAt":"2026-10-08T07:29:39.338747Z","fallbackText":"Reminder child session","childSessionId":"928f5153-7ec9-4cf3-9c9a-4e8c3ab74de8","reminderAgentId":"verify-reminder","generationId":1,"taskId":"01a11a6a-c6f1-7d82-afc3-099dfc7a8968","childSessionLogPath":"subagent/928f5153-7ec9-4cf3-9c9a-4e8c3ab74de8/session.jsonl","blocking":true}},"emittedAtMs":1791444579350}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","status":"idle","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:16"},"emittedAtMs":1791444579409}} {"type":"emit_inbound","label":"turn/completed","frame":{"jsonrpc":"2.0","method":"turn/completed","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:16","sourceRange":{"stream":{"kind":"session","id":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5"},"first":{"id":"186e95f3-f1f1-46e3-9470-0154904b2eae","sequence":79},"last":{"id":"186e95f3-f1f1-46e3-9470-0154904b2eae","sequence":79}},"turnId":"01a11a6a-be41-7000-8db9-1d84c1b8be48","terminal":"completed","durationMs":13778,"timeToFirstTokenMs":2084},"emittedAtMs":1791444579410}} +{"type":"expect_outbound","label":"task/list","frame":{"jsonrpc":"2.0","id":4,"method":"task/list","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5"}}} +{"type":"emit_inbound","label":"response:task/list","frame":{"jsonrpc":"2.0","id":4,"result":{"tasks":[]}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:17","sourceRange":{"stream":{"kind":"session","id":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5"},"first":{"id":"4cb68dd8-75e6-4320-bcb8-ccdae87408e3","sequence":80},"last":{"id":"4cb68dd8-75e6-4320-bcb8-ccdae87408e3","sequence":80}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791444579437}} {"type":"emit_inbound","label":"session/closed","frame":{"jsonrpc":"2.0","method":"session/closed","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","reason":"hostShutdown","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:17"},"emittedAtMs":1791444579675}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","status":"notLoaded","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:17"},"emittedAtMs":1791444579675}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent_v2/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_v2/muse_transcript.ndjson index 3b8ebe4065b0..6fb62f13588f 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/subagent_v2/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_v2/muse_transcript.ndjson @@ -52,6 +52,8 @@ {"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:37","sourceRange":{"stream":{"kind":"session","id":"01a12d17-75fa-7000-9157-e8365ce94492"},"first":{"id":"885bd855-b9bf-436c-956d-29e676850f2e","sequence":192},"last":{"id":"dbf2b107-a483-4612-a387-2f6dd21436a9","sequence":196}},"item":{"itemId":"885bd855-b9bf-436c-956d-29e676850f2e","kind":"reminderChild","turnId":"01a12d17-7790-7000-967d-3f2246f38a60","revision":2,"status":"completed","recordedAt":"2026-10-11T22:31:29.932949Z","fallbackText":"Reminder child session","childSessionId":"a88d2634-79b3-409b-bfc8-9a10989dab18","reminderAgentId":"verify-reminder","generationId":1,"taskId":"01a12d17-97b8-7e31-abb4-b55e1490a992","blocking":true}},"emittedAtMs":1791757889941}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","status":"idle","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:38"},"emittedAtMs":1791757889997}} {"type":"emit_inbound","label":"turn/completed","frame":{"jsonrpc":"2.0","method":"turn/completed","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:38","sourceRange":{"stream":{"kind":"session","id":"01a12d17-75fa-7000-9157-e8365ce94492"},"first":{"id":"7b037a6e-c614-47a3-9828-b897a489e167","sequence":204},"last":{"id":"7b037a6e-c614-47a3-9828-b897a489e167","sequence":204}},"turnId":"01a12d17-7790-7000-967d-3f2246f38a60","terminal":"completed","durationMs":14869,"timeToFirstTokenMs":8109},"emittedAtMs":1791757889998}} +{"type":"expect_outbound","label":"task/list","frame":{"jsonrpc":"2.0","id":4,"method":"task/list","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492"}}} +{"type":"emit_inbound","label":"response:task/list","frame":{"jsonrpc":"2.0","id":4,"result":{"tasks":[]}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:39","sourceRange":{"stream":{"kind":"session","id":"01a12d17-75fa-7000-9157-e8365ce94492"},"first":{"id":"b910ce96-80f4-46e9-ace5-327b2a8f7bc3","sequence":205},"last":{"id":"b910ce96-80f4-46e9-ace5-327b2a8f7bc3","sequence":205}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791757890009}} {"type":"emit_inbound","label":"session/closed","frame":{"jsonrpc":"2.0","method":"session/closed","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","reason":"hostShutdown","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:39"},"emittedAtMs":1791757890237}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","status":"notLoaded","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:39"},"emittedAtMs":1791757890237}} diff --git a/packages/provider-muse/src/server/adapter.test.ts b/packages/provider-muse/src/server/adapter.test.ts index 7571c20995b7..e9a83eff7b6d 100644 --- a/packages/provider-muse/src/server/adapter.test.ts +++ b/packages/provider-muse/src/server/adapter.test.ts @@ -30,6 +30,7 @@ import * as Layer from "effect/Layer"; import * as Queue from "effect/Queue"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; +import * as TestClock from "effect/testing/TestClock"; import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost"; import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; @@ -118,6 +119,8 @@ const makeFakeMuse = Effect.fnUntraced(function* (idPrefix = "native") { return { turnId: call.params.expectedTurnId }; case "session/compact": return { status: "accepted" }; + case "task/list": + return { tasks: [] }; default: return {}; } @@ -350,6 +353,22 @@ const approval = (turnId: string) => ({ subject: { kind: "shell", command: "touch result.txt" }, }); +const toolCall = ( + turnId: string, + itemId: string, + revision: number, + status: string, + extra = {}, +) => ({ + itemId, + kind: "toolCall", + revision, + status, + turnId, + tool: "shell", + ...extra, +}); + describe("MuseAdapterV2", () => { it.effect("connects the thread's MCP credential on native start and resume", () => Effect.gen(function* () { @@ -1262,4 +1281,214 @@ describe("MuseAdapterV2", () => { assert.strictEqual(child.subagent.result, "budget_exhausted"); }).pipe(Effect.scoped, Effect.provide(testLayer)), ); + it.effect("keeps a monitor from Muse's task list pending until the list drops it", () => + Effect.gen(function* () { + const fake = yield* makeFakeMuse(); + const harness = yield* makeHarness(fake); + const { runtime, providerThread } = harness; + const turn = yield* startConversation(harness, fake); + const monitor = { taskId: "monitor-1", kind: "monitor", status: "inProgress" }; + // The monitor tool returns at once; only the task list shows the monitor running. + yield* fake.emit("item/completed", { + turnId: turn.nativeId, + item: toolCall(turn.nativeId, "launch-1", 1, "completed", { tool: "monitor" }), + }); + fake.queueResponse("task/list", { tasks: [monitor] }); + yield* fake.emit("turn/completed", { turnId: turn.nativeId, terminal: "completed" }); + const terminal = yield* harness.takeEvent("turn.terminal"); + // The rows reach the thread before the turn ends, so the run records them. + const listed = harness.allEvents + .slice(0, harness.allEvents.indexOf(terminal)) + .findLast((event) => event.type === "provider_thread.updated"); + assert.deepStrictEqual( + listed?.type === "provider_thread.updated" + ? listed.providerThread.pendingBackgroundTasks + : undefined, + [{ taskId: "monitor-1", kind: "background_task", description: "monitor" }], + ); + assert.isTrue(yield* runtime.hasPendingBackgroundWorkForThread!(providerThread)); + // Idle release asks Muse again. A failed or stalled read keeps the monitor and the host. + fake.queueResponse("task/list", { tasks: [monitor] }); + assert.isTrue(yield* runtime.hasPendingBackgroundWork!); + fake.queueResponse("task/list", new Error("Muse is busy")); + assert.isTrue(yield* runtime.hasPendingBackgroundWork!); + fake.queueResponse("task/list", pendingPromise>().promise); + const stalled = yield* runtime.hasPendingBackgroundWork!.pipe(Effect.forkScoped); + yield* fake.takeCall("task/list"); + yield* TestClock.adjust("31 seconds"); + assert.isTrue(yield* Fiber.join(stalled)); + assert.strictEqual(fake.closeCount(), 0); + fake.queueResponse("task/list", { tasks: [] }); + assert.isFalse(yield* runtime.hasPendingBackgroundWork!); + // A run that has not read the empty update yet keeps its stream open for it. + const stale = listed?.type === "provider_thread.updated" ? listed.providerThread : undefined; + assert.isTrue(yield* runtime.hasPendingBackgroundWorkForThread!(stale!)); + const cleared = yield* harness.takeEvent( + "provider_thread.updated", + (event) => event.providerThread.pendingBackgroundTasks?.length === 0, + ); + assert.isFalse(yield* runtime.hasPendingBackgroundWorkForThread!(cleared.providerThread)); + }).pipe(Effect.scoped, Effect.provide(testLayer)), + ); + + it.effect("lists a running monitor again when a turn starts from an older thread snapshot", () => + Effect.gen(function* () { + const fake = yield* makeFakeMuse(); + const harness = yield* makeHarness(fake); + const monitor = { taskId: "monitor-1", kind: "monitor", status: "inProgress" }; + const first = yield* startConversation(harness, fake); + fake.queueResponse("task/list", { tasks: [monitor] }); + yield* fake.emit("turn/completed", { turnId: first.nativeId, terminal: "completed" }); + yield* harness.takeEvent("turn.terminal"); + // harness.providerThread predates the monitor, so the second turn starts without its row. + const second = yield* startConversation(harness, fake, 2); + fake.queueResponse("task/list", { tasks: [monitor] }); + yield* fake.emit("turn/completed", { turnId: second.nativeId, terminal: "completed" }); + const terminal = yield* harness.takeEvent("turn.terminal"); + const listed = harness.allEvents + .slice(0, harness.allEvents.indexOf(terminal)) + .findLast((event) => event.type === "provider_thread.updated"); + assert.deepStrictEqual( + listed?.type === "provider_thread.updated" + ? listed.providerThread.pendingBackgroundTasks?.map((task) => task.taskId) + : undefined, + ["monitor-1"], + ); + }).pipe(Effect.scoped, Effect.provide(testLayer)), + ); + + it.effect("keeps background tool calls pending after their turn until each one ends", () => + Effect.gen(function* () { + const fake = yield* makeFakeMuse(); + const harness = yield* makeHarness(fake); + const { runtime, providerThread } = harness; + const { nativeId } = yield* startConversation(harness, fake); + for (const itemId of ["background-1", "background-2"]) + yield* fake.emit("item/started", { + turnId: nativeId, + item: toolCall(nativeId, itemId, 1, "inProgress", { background: true }), + }); + yield* fake.emit("item/started", { + turnId: nativeId, + item: toolCall(nativeId, "foreground-1", 1, "inProgress"), + }); + yield* fake.emit("turn/completed", { turnId: nativeId, terminal: "completed" }); + yield* harness.takeEvent("turn.terminal"); + assert.isTrue(yield* runtime.hasPendingBackgroundWork!); + assert.isTrue(yield* runtime.hasPendingBackgroundWorkForThread!(providerThread)); + const running = harness.allEvents.findLast( + (event) => event.type === "provider_thread.updated", + ); + assert.deepStrictEqual( + running?.type === "provider_thread.updated" + ? running.providerThread.pendingBackgroundTasks?.map((task) => task.taskId) + : undefined, + ["background-1", "background-2"], + ); + yield* fake.emit("item/completed", { + turnId: nativeId, + item: toolCall(nativeId, "background-1", 2, "completed", { background: true }), + }); + const remaining = yield* harness.takeEvent( + "provider_thread.updated", + (event) => event.providerThread.pendingBackgroundTasks?.length === 1, + ); + assert.strictEqual( + remaining.providerThread.pendingBackgroundTasks?.[0]?.taskId, + "background-2", + ); + assert.isTrue(yield* runtime.hasPendingBackgroundWork!); + yield* fake.emit("item/completed", { + turnId: nativeId, + item: toolCall(nativeId, "background-2", 2, "completed", { background: true }), + }); + yield* harness.takeEvent( + "provider_thread.updated", + (event) => event.providerThread.pendingBackgroundTasks?.length === 0, + ); + assert.isFalse(yield* runtime.hasPendingBackgroundWork!); + assert.isFalse(yield* runtime.hasPendingBackgroundWorkForThread!(providerThread)); + }).pipe(Effect.scoped, Effect.provide(testLayer)), + ); + + it.effect("settles a tracked background call when later revisions omit the flag", () => + Effect.gen(function* () { + const fake = yield* makeFakeMuse(); + const harness = yield* makeHarness(fake); + const { runtime, providerThread } = harness; + const { nativeId } = yield* startConversation(harness, fake); + yield* fake.emit("item/started", { + turnId: nativeId, + item: toolCall(nativeId, "background-1", 1, "inProgress", { background: true }), + }); + yield* fake.emit("item/updated", { + turnId: nativeId, + item: toolCall(nativeId, "background-1", 2, "inProgress"), + }); + yield* fake.emit("turn/completed", { turnId: nativeId, terminal: "completed" }); + yield* harness.takeEvent("turn.terminal"); + assert.isTrue(yield* runtime.hasPendingBackgroundWork!); + assert.isTrue(yield* runtime.hasPendingBackgroundWorkForThread!(providerThread)); + assert.isFalse( + harness.allEvents.some( + (event) => + event.type === "turn_item.updated" && + event.turnItem.nativeItemRef?.nativeId === "background-1" && + event.turnItem.status === "completed", + ), + ); + yield* fake.emit("item/completed", { + turnId: nativeId, + item: toolCall(nativeId, "background-1", 3, "completed"), + }); + yield* harness.takeEvent( + "turn_item.updated", + (event) => event.turnItem.status === "completed", + ); + const settled = harness.allEvents.findLast( + (event) => event.type === "provider_thread.updated", + ); + assert.deepStrictEqual( + settled?.type === "provider_thread.updated" + ? settled.providerThread.pendingBackgroundTasks + : undefined, + [], + ); + assert.isFalse(yield* runtime.hasPendingBackgroundWork!); + assert.isFalse(yield* runtime.hasPendingBackgroundWorkForThread!(providerThread)); + }).pipe(Effect.scoped, Effect.provide(testLayer)), + ); + + it.effect("clears background tool calls and task list rows when the host fails", () => + Effect.gen(function* () { + const fake = yield* makeFakeMuse(); + const harness = yield* makeHarness(fake); + const { nativeId } = yield* startConversation(harness, fake); + yield* fake.emit("item/started", { + turnId: nativeId, + item: toolCall(nativeId, "background-1", 1, "inProgress", { background: true }), + }); + const monitor = { taskId: "monitor-1", kind: "monitor", status: "inProgress" }; + fake.queueResponse("task/list", { tasks: [monitor] }); + yield* fake.emit("turn/completed", { turnId: nativeId, terminal: "completed" }); + yield* harness.takeEvent("turn.terminal"); + fake.queueResponse("task/list", { tasks: [monitor] }); + assert.isTrue(yield* harness.runtime.hasPendingBackgroundWork!); + const tracked = harness.allEvents.findLast( + (event) => event.type === "provider_thread.updated", + ); + assert.strictEqual( + tracked?.type === "provider_thread.updated" + ? tracked.providerThread.pendingBackgroundTasks?.length + : undefined, + 2, + ); + yield* fake.disconnect(); + yield* harness.takeEvent( + "provider_thread.updated", + (event) => event.providerThread.pendingBackgroundTasks?.length === 0, + ); + assert.isFalse(yield* harness.runtime.hasPendingBackgroundWork!); + }).pipe(Effect.scoped, Effect.provide(testLayer)), + ); }); diff --git a/packages/provider-muse/src/server/adapter.ts b/packages/provider-muse/src/server/adapter.ts index bad108c6c352..106cb18bb016 100644 --- a/packages/provider-muse/src/server/adapter.ts +++ b/packages/provider-muse/src/server/adapter.ts @@ -50,6 +50,7 @@ import { MuseDelta, MuseItemEvent, MuseSessionResult, + MuseTaskList, MuseTokenUsageEvent, MuseTodoList, MuseTurnRetryScheduled, @@ -334,6 +335,8 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( const pending = new Map(); const seenTerminals = new Set(); const observedChildren = new Map(); + // Other running tasks from Muse's task list (a `monitor` has no item), by task id. + const registryTasks = new Map(); const childrenByItemId = new Map(); const agentTitles = new Map(); const finishedBackground: Array = []; @@ -666,22 +669,57 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( } } }); + // Later revisions can omit the flag; keep tracking the call until it settles. + const isBackgroundItem = (item: MuseItem) => + observedChildren.has(item.itemId) || + item.kind === "subagent" || + item.kind === "workflow" || + (item.kind === "toolCall" && item.background === true); const syncBackground = Effect.fnUntraced(function* () { yield* updateThread({ - pendingBackgroundTasks: [...observedChildren].flatMap(([itemId, owner]) => { - const item = owner.items.get(itemId); - const description = item?.objective ?? item?.entryId ?? item?.fallbackText; - return [ - { + pendingBackgroundTasks: [ + ...[...observedChildren].map(([itemId, owner]) => { + const item = owner.items.get(itemId); + const description = + item?.objective ?? item?.entryId ?? item?.fallbackText ?? item?.tool; + return { taskId: itemId, kind: item?.kind === "subagent" ? ("subagent" as const) : ("background_task" as const), ...(description ? { description } : {}), - }, - ]; - }), + }; + }), + ...[...registryTasks].map(([taskId, description]) => ({ + taskId, + kind: "background_task" as const, + description, + })), + ], }); }); + /** + * Muse sends no event when a registry task such as a `monitor` ends, so its task list is + * read when a turn completes and when idle release asks. Callers hold the event permit, so + * item events apply in order around the read. A failed read keeps the last rows. + */ + const refreshTasks = Effect.tryPromise(() => + host.connection.request("task/list", { sessionId: nativeSessionId }), + ).pipe( + Effect.timeout(options.requestTimeoutMs ?? 30_000), + Effect.flatMap((value) => decode(MuseTaskList, value)), + Effect.flatMap(({ tasks }) => { + registryTasks.clear(); + for (const task of tasks) + if (!observedChildren.has(task.taskId)) + registryTasks.set(task.taskId, task.tool ?? task.kind); + // Compares with the thread's rows, not the last list: a turn starts from the + // orchestrator's copy of the thread, which can predate the rows published since. + const listed = (thread?.pendingBackgroundTasks ?? []).map((task) => task.taskId); + const running = [...observedChildren.keys(), ...registryTasks.keys()]; + return listed.join("\n") === running.join("\n") ? Effect.void : syncBackground(); + }), + Effect.catch((cause) => Effect.logWarning("Could not read Muse's background tasks", cause)), + ); const publishItem = Effect.fnUntraced(function* ( turn: ActiveTurn, item: MuseItem, @@ -698,13 +736,13 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( return; const time = yield* DateTime.now; const status = terminal ?? itemStatus(item); - if (item.kind === "subagent" || item.kind === "workflow") { + if (isBackgroundItem(item)) { const wasRunning = observedChildren.has(item.itemId); if (status === "running") observedChildren.set(item.itemId, turn); else if (observedChildren.delete(item.itemId) && active !== turn) finishedBackground.push({ kind: item.kind === "subagent" ? "subagent" : "background_task", - label: item.objective ?? item.entryId ?? item.fallbackText, + label: item.objective ?? item.entryId ?? item.fallbackText ?? item.tool, outcome: status === "completed" ? "completed" @@ -893,6 +931,7 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( if (item) yield* publishItem(owner, item, status); } observedChildren.clear(); + registryTasks.clear(); if (thread?.pendingBackgroundTasks?.length) yield* syncBackground(); for (const entry of pending.values()) yield* resolvePending(entry, "cancelled"); }); @@ -940,7 +979,7 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( const completedAt = yield* DateTime.now; for (const item of turn.items.values()) { if ( - (item.kind === "subagent" || item.kind === "workflow") && + isBackgroundItem(item) && item.status === "inProgress" && status === "completed" && disposition === "reusable" @@ -1416,6 +1455,7 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( ? "interrupted" : "cancelled" : "failed"; + if (status === "completed") yield* refreshTasks; yield* finish(turn, status, result.error?.message ?? result.reason); break; } @@ -2057,10 +2097,21 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( providerSession: session, events: Stream.fromQueue(events), backgroundTasksEndWithProcess: true, - // A running workflow or a held Muse turn keeps the host: idle release must wait. - hasPendingBackgroundWork: Effect.sync( - () => observedChildren.size > 0 || wake !== undefined, - ), + // Running background work or a held Muse turn keeps the host: idle release must wait. + hasPendingBackgroundWork: Effect.gen(function* () { + if (registryTasks.size > 0) yield* eventPermit.withPermits(1)(refreshTasks); + return observedChildren.size > 0 || wake !== undefined || registryTasks.size > 0; + }), + // Holds the run's event stream open until the run has read the update that clears the + // rows, even when the adapter cleared them first. + hasPendingBackgroundWorkForThread: (providerThread) => + Effect.sync( + () => + validateThread(providerThread) && + (observedChildren.size > 0 || + registryTasks.size > 0 || + (providerThread.pendingBackgroundTasks?.length ?? 0) > 0), + ), ensureThread: (args) => register(args).pipe( commands.withPermits(1), diff --git a/packages/provider-muse/src/server/protocol.ts b/packages/provider-muse/src/server/protocol.ts index 08d1507de91f..7af5d8013623 100644 --- a/packages/provider-muse/src/server/protocol.ts +++ b/packages/provider-muse/src/server/protocol.ts @@ -38,6 +38,8 @@ export const MuseItem = Schema.Struct({ text: Schema.optional(Schema.String), summary: Schema.optional(Schema.Array(Schema.String)), tool: Schema.optional(Schema.String), + /** `toolCall`: true once Muse moved the call to the background. */ + background: Schema.optional(Schema.Boolean), args: Schema.optional(Schema.String), visibleOutput: Schema.optional(Schema.String), truncated: Schema.optional(Schema.Boolean), @@ -162,6 +164,16 @@ export const MuseTurnRetryScheduled = Schema.Struct({ reason: NonEmptyString, retryDelayMs: Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)), }); +/** Muse's running background work, including registry tasks such as a `monitor` that have no item. */ +export const MuseTaskList = Schema.Struct({ + tasks: Schema.Array( + Schema.Struct({ + taskId: NonEmptyString, + kind: Schema.String, + tool: Schema.optional(Schema.String), + }), + ), +}); export const MuseCompactResult = Schema.Struct({ status: Schema.String, reason: Schema.optional(Schema.String),