Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Prev Previous commit
Next Next commit
fix: preserve agent mode during noReply tool calls
When MCP tools call session.prompt({ noReply: true }), the agent mode
was incorrectly switching from "plan" to "build" because the current
executing agent wasn't tracked.

Changes:
- Add currentAgent field to Session schema to track active agent
- Update loop() to set currentAgent before tool execution
- Update createUserMessage() fallback chain to use currentAgent
- Clear currentAgent on loop completion
- Optimise to only update session when agent changes

Fixes mode-switching bug during MCP tool execution.
  • Loading branch information
arsham committed Dec 31, 2025
commit f123aa72db254964f31c5283a9557149f7ce0b56
1 change: 1 addition & 0 deletions 1 packages/opencode/src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export namespace Session {
})
.optional(),
title: z.string(),
currentAgent: z.string().optional(),
version: z.string(),
time: z.object({
created: z.number(),
Expand Down
53 changes: 36 additions & 17 deletions 53 packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ export namespace SessionPrompt {
noReply: z.boolean().optional(),
tools: z.record(z.string(), z.boolean()).optional(),
system: z.string().optional(),
variant: z.string().optional(),
parts: z.array(
z.discriminatedUnion("type", [
MessageV2.TextPart.omit({
Expand Down Expand Up @@ -255,7 +254,13 @@ export namespace SessionPrompt {
}

using _lock = lock(sessionID)
using _ = defer(() => cancel(sessionID))
using _ = defer(() => {
cancel(sessionID)
// Clear currentAgent when loop completes
Session.update(sessionID, (draft) => {
draft.currentAgent = undefined
}).catch(() => {})
})

let step = 0
while (true) {
Expand Down Expand Up @@ -478,7 +483,7 @@ export namespace SessionPrompt {
if (
lastFinished &&
lastFinished.summary !== true &&
(await SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model }))
SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model })
) {
await SessionCompaction.create({
sessionID,
Expand All @@ -491,6 +496,13 @@ export namespace SessionPrompt {

// normal processing
const agent = await Agent.get(lastUser.agent)
// Track current agent in session state (only if changed)
const session = await Session.get(sessionID)
if (session.currentAgent !== agent.name) {
await Session.update(sessionID, (draft) => {
draft.currentAgent = agent.name
})
}
const maxSteps = agent.maxSteps ?? Infinity
const isLastStep = step >= maxSteps
msgs = insertReminders({
Expand Down Expand Up @@ -602,7 +614,7 @@ export namespace SessionPrompt {
mergeDeep(await ToolRegistry.enabled(input.agent)),
mergeDeep(input.tools ?? {}),
)
for (const item of await ToolRegistry.tools(input.model.providerID, input.agent)) {
for (const item of await ToolRegistry.tools(input.model.providerID)) {
if (Wildcard.all(item.id, enabledTools) === false) continue
const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
tools[item.id] = tool({
Expand Down Expand Up @@ -734,7 +746,19 @@ export namespace SessionPrompt {
}

async function createUserMessage(input: PromptInput) {
const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent()))
const session = await Session.get(input.sessionID)

// If no agent specified, infer from last assistant message's mode
let agentName = input.agent
if (!agentName) {
const msgs = await MessageV2.filterCompacted(MessageV2.stream(input.sessionID))
const lastAssistant = msgs.findLast((m) => m.info.role === "assistant") as MessageV2.Assistant | undefined
// Updated fallback chain - add session.currentAgent before session.agent
agentName = lastAssistant?.mode ?? session.currentAgent ?? session.agent ?? "general"
}

const agent = (await Agent.get(agentName)) || (await Agent.get("general"))
if (!agent) throw new Error(`Agent not found: ${agentName}`)
const info: MessageV2.Info = {
id: input.messageID ?? Identifier.ascending("message"),
role: "user",
Expand All @@ -746,7 +770,6 @@ export namespace SessionPrompt {
agent: agent.name,
model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
system: input.system,
variant: input.variant,
}

const parts = await Promise.all(
Expand Down Expand Up @@ -847,7 +870,7 @@ export namespace SessionPrompt {
const result = await t.execute(args, {
sessionID: input.sessionID,
abort: new AbortController().signal,
agent: input.agent!,
agent: agent.name,
messageID: info.id,
extra: { bypassCwdCheck: true, model },
metadata: async () => {},
Expand Down Expand Up @@ -907,7 +930,7 @@ export namespace SessionPrompt {
t.execute(args, {
sessionID: input.sessionID,
abort: new AbortController().signal,
agent: input.agent!,
agent: agent.name,
messageID: info.id,
extra: { bypassCwdCheck: true },
metadata: async () => {},
Expand Down Expand Up @@ -1287,7 +1310,6 @@ export namespace SessionPrompt {
model: z.string().optional(),
arguments: z.string(),
command: z.string(),
variant: z.string().optional(),
})
export type CommandInput = z.infer<typeof CommandInput>
const bashRegex = /!`([^`]+)`/g
Expand All @@ -1303,22 +1325,20 @@ export namespace SessionPrompt {
export async function command(input: CommandInput) {
log.info("command", input)
const command = await Command.get(input.command)
const agentName = command.agent ?? input.agent ?? (await Agent.defaultAgent())
const agentName = command.agent ?? input.agent ?? "build"

const raw = input.arguments.match(argsRegex) ?? []
const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))

const templateCommand = await command.template

const placeholders = templateCommand.match(placeholderRegex) ?? []
const placeholders = command.template.match(placeholderRegex) ?? []
let last = 0
for (const item of placeholders) {
const value = Number(item.slice(1))
if (value > last) last = value
}

// Let the final placeholder swallow any extra arguments so prompts read naturally
const withArgs = templateCommand.replaceAll(placeholderRegex, (_, index) => {
const withArgs = command.template.replaceAll(placeholderRegex, (_, index) => {
const position = Number(index)
const argIndex = position - 1
if (argIndex >= args.length) return ""
Expand All @@ -1332,7 +1352,7 @@ export namespace SessionPrompt {
const results = await Promise.all(
shell.map(async ([, cmd]) => {
try {
return await $`${{ raw: cmd }}`.quiet().nothrow().text()
return await $`${{ raw: cmd }}`.nothrow().text()
} catch (error) {
return `Error executing command: ${error instanceof Error ? error.message : String(error)}`
}
Expand Down Expand Up @@ -1392,7 +1412,6 @@ export namespace SessionPrompt {
model,
agent: agentName,
parts,
variant: input.variant,
})) as MessageV2.WithParts

Bus.publish(Command.Event.Executed, {
Expand Down Expand Up @@ -1449,7 +1468,7 @@ export namespace SessionPrompt {
time: {
created: Date.now(),
},
agent: input.message.info.role === "user" ? input.message.info.agent : await Agent.defaultAgent(),
agent: input.message.info.role === "user" ? input.message.info.agent : "build",
model: {
providerID: input.providerID,
modelID: input.modelID,
Expand Down
Morty Proxy This is a proxified and sanitized view of the page, visit original site.