Repository navigation
BUGFIX: Muse yielded commands hold the thread and Stop ends them #25810
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
90e89cb
5c159df
42a9245
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,7 +89,7 @@ import { | |
| subagentThreadTitle, | ||
| } from "@t3tools/provider-core/server/subagentProjection"; | ||
| import { turnScopedSelectionTransition } from "@t3tools/provider-core/server/selectionTransition"; | ||
| import { museItemStatus, museToolPresentation } from "./itemPresentation.ts"; | ||
| import { museCommandLabel, museItemStatus, museToolPresentation } from "./itemPresentation.ts"; | ||
|
|
||
| const MUSE_PROVIDER = ProviderDriverKind.make("muse"); | ||
| const isOpenSessionError = Schema.is(ProviderAdapter.ProviderAdapterOpenSessionError); | ||
|
|
@@ -256,6 +256,20 @@ const INFORMATIONAL_NOTIFICATIONS = new Set([ | |
| "usage/changed", | ||
| ]); | ||
| const TOOLS_WITH_NATIVE_ROWS = new Set(["write_todos", "request_user_input", "workflow"]); | ||
| // Muse returns a command to the model once `yield_time_ms` passes, keeps it running, and wakes | ||
| // the agent when it exits. It marks the call with this status line. | ||
| const isYieldedCommand = (item: MuseItem) => | ||
| item.kind === "toolCall" && item.statusLine === "terminal process running"; | ||
| const backgroundKind = (item: MuseItem | undefined) => | ||
| item?.kind === "subagent" | ||
| ? ("subagent" as const) | ||
| : item?.kind === "toolCall" | ||
| ? ("command" as const) | ||
| : ("background_task" as const); | ||
| const backgroundLabel = (item: MuseItem | undefined) => | ||
| item?.kind === "toolCall" | ||
| ? museCommandLabel(item) | ||
| : (item?.objective ?? item?.entryId ?? item?.fallbackText); | ||
| const responseAnswerSchema = Schema.Union([Schema.String, Schema.Array(Schema.String)]); | ||
|
|
||
| export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( | ||
|
|
@@ -345,6 +359,8 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( | |
| let wake: | ||
| | { readonly nativeId: string; readonly events: Array<[string, unknown]> } | ||
| | undefined; | ||
| // Muse wakes the agent to report work a user Stop ended; until the next run, end those wakes. | ||
| let muteWakes = false; | ||
| const emit = (event: ProviderAdapter.ProviderAdapterV2Event) => | ||
| Queue.offer(events, event).pipe(Effect.asVoid); | ||
| const decode = <A, I>(schema: Schema.Codec<A, I>, data: unknown) => | ||
|
|
@@ -675,12 +691,14 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( | |
| yield* updateThread({ | ||
| pendingBackgroundTasks: [...observedChildren].flatMap(([itemId, owner]) => { | ||
| const item = owner.items.get(itemId); | ||
| const description = item?.objective ?? item?.entryId ?? item?.fallbackText; | ||
| const kind = backgroundKind(item); | ||
| const description = backgroundLabel(item); | ||
| return [ | ||
| { | ||
| taskId: itemId, | ||
| kind: | ||
| item?.kind === "subagent" ? ("subagent" as const) : ("background_task" as const), | ||
| kind, | ||
| // Muse always wakes the agent when a yielded command exits. | ||
| ...(kind === "command" ? { awaited: true as const } : {}), | ||
| ...(description ? { description } : {}), | ||
| }, | ||
| ]; | ||
|
|
@@ -703,13 +721,19 @@ 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 ( | ||
| item.kind === "subagent" || | ||
| item.kind === "workflow" || | ||
| isYieldedCommand(item) || | ||
| observedChildren.has(item.itemId) | ||
| ) { | ||
| const wasRunning = observedChildren.has(item.itemId); | ||
| if (status === "running") observedChildren.set(item.itemId, turn); | ||
| else if (observedChildren.delete(item.itemId) && active !== turn) | ||
| // Work that ends during another run reaches that run, not a later wake. | ||
| else if (observedChildren.delete(item.itemId) && active === undefined) | ||
| finishedBackground.push({ | ||
| kind: item.kind === "subagent" ? "subagent" : "background_task", | ||
| label: item.objective ?? item.entryId ?? item.fallbackText, | ||
| kind: backgroundKind(item), | ||
| label: backgroundLabel(item), | ||
| outcome: | ||
| status === "completed" | ||
| ? "completed" | ||
|
|
@@ -946,7 +970,9 @@ 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") && | ||
| (item.kind === "subagent" || | ||
| item.kind === "workflow" || | ||
| observedChildren.has(item.itemId)) && | ||
| item.status === "inProgress" && | ||
| status === "completed" && | ||
| disposition === "reusable" | ||
|
|
@@ -1060,6 +1086,19 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( | |
| }); | ||
| const owns = (turn: ActiveTurn, turnId: string) => | ||
| turnId === turn.nativeId || turn.joined.has(turnId); | ||
| const stopBackgroundWork = Effect.fnUntraced(function* () { | ||
| muteWakes = true; | ||
| if (wake) yield* request("turn/interrupt", { turnId: wake.nativeId }).pipe(Effect.ignore); | ||
| yield* request("task/stopAll", {}); | ||
| }); | ||
| const settleStoppedWork = eventPermit.withPermits(1)( | ||
| Effect.gen(function* () { | ||
| wake = undefined; | ||
| yield* settleObservedChildren("interrupted"); | ||
| // Reports from stopped work must not name a later wake. | ||
| finishedBackground.length = 0; | ||
| }), | ||
| ); | ||
| const decideApproval = Effect.fnUntraced(function* ( | ||
| entry: PendingRequest, | ||
| decision: ProviderApprovalDecision | undefined, | ||
|
|
@@ -1328,7 +1367,12 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( | |
| : undefined; | ||
| if (itemEvent) { | ||
| const owner = observedChildren.get(itemEvent.itemId); | ||
| if (owner && owner !== active && itemEvent.turnId === owner.nativeId) { | ||
| if ( | ||
| owner && | ||
| owner !== active && | ||
| typeof itemEvent.turnId === "string" && | ||
| owns(owner, itemEvent.turnId) | ||
|
Comment on lines
+1373
to
+1374
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| ) { | ||
| const previous = owner.items.get(itemEvent.itemId); | ||
| if (!previous || itemEvent.revision > previous.revision) { | ||
| owner.items.set(itemEvent.itemId, itemEvent); | ||
|
|
@@ -1346,7 +1390,12 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( | |
| params.turnId !== active?.nativeId && | ||
| params.turnId !== wake?.nativeId | ||
| ) { | ||
| if (active && !active.compact) active.joined.add(params.turnId); | ||
| if (muteWakes) | ||
| yield* request("turn/interrupt", { turnId: params.turnId }).pipe( | ||
| Effect.ignore, | ||
| Effect.forkIn(scope), | ||
| ); | ||
| else if (active && !active.compact) active.joined.add(params.turnId); | ||
| else if (!active && !wake) { | ||
| const held = { nativeId: params.turnId, events: [] }; | ||
| wake = held; | ||
|
|
@@ -2021,6 +2070,7 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( | |
| yield* Effect.gen(function* () { | ||
| thread = turnInput.providerThread; | ||
| active = turn; | ||
| muteWakes = false; | ||
| yield* emit({ | ||
| type: "provider_turn.updated", | ||
| driver: MUSE_PROVIDER, | ||
|
|
@@ -2149,7 +2199,7 @@ 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. | ||
| // Running background work or a held Muse turn keeps the host: idle release must wait. | ||
| hasPendingBackgroundWork: Effect.sync( | ||
| () => observedChildren.size > 0 || wake !== undefined, | ||
| ), | ||
|
|
@@ -2233,6 +2283,15 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( | |
| interruptTurn: Effect.fn("MuseAdapter.interruptTurn")( | ||
| function* (args) { | ||
| const turn = active; | ||
| // Stop also reaches settled turns, for the background work Muse still runs. | ||
| if ( | ||
| !turn && | ||
| args.requestRuntimeRestart === true && | ||
| validateThread(args.providerThread) | ||
| ) { | ||
| yield* stopBackgroundWork(); | ||
| return yield* settleStoppedWork; | ||
| } | ||
| if ( | ||
| !turn || | ||
| turn.providerTurn.id !== args.providerTurnId || | ||
|
|
@@ -2262,11 +2321,14 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( | |
| for (const joined of turn.joined) | ||
| yield* request("turn/interrupt", { turnId: joined }).pipe(Effect.ignore); | ||
| yield* request("turn/interrupt", { turnId: turn.nativeId }); | ||
| // User Stop also ends commands Muse yielded to the background. | ||
| if (args.requestRuntimeRestart === true) yield* stopBackgroundWork(); | ||
| yield* Deferred.await(turn.done).pipe( | ||
| Effect.timeout(options.requestTimeoutMs ?? 30_000), | ||
| Effect.mapError((cause) => protocolError("Muse did not confirm interruption", cause)), | ||
| Effect.onError((cause) => eventPermit.withPermits(1)(failHost(cause))), | ||
| ); | ||
| if (args.requestRuntimeRestart === true) yield* settleStoppedWork; | ||
| }, | ||
| (effect, args) => | ||
| effect.pipe( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: pingdotgg/t3code
Length of output: 8125
Script executed:
Repository: pingdotgg/t3code
Length of output: 42069
Script executed:
Repository: pingdotgg/t3code
Length of output: 41665
Script executed:
Repository: pingdotgg/t3code
Length of output: 22324
Preserve cross-turn completion reports for yielded commands.
When a yielded command completes through the owner-routing branch,
publishItem(owner, itemEvent)handles it while another turn is active. The newactive === undefinedcondition removes the command fromobservedChildrenbut does not add a report tofinishedBackground. The later continuation cannot produce the command completion notification.Use the owner comparison so cross-turn completions remain queued.
Suggested fix
Committable suggestion