From 355fa3565143c41b40f4de8392847778e614ce62 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 15 Dec 2025 19:02:31 +0900 Subject: [PATCH] fix(hooks): respect previous message's agent mode in message sending hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Message hooks like todo-continuation-enforcer and background-notification now preserve the agent mode from the previous message when sending follow-up prompts. This ensures that continuation messages and task completion notifications use the same agent that was active in the conversation. - Export findNearestMessageWithFields and MESSAGE_STORAGE from hook-message-injector - Add getMessageDir helper to locate session message directories - Pass agent field to session.prompt in todo-continuation-enforcer - Pass agent field to session.prompt in BackgroundManager.notifyParentSession Closes #59 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- src/features/background-agent/manager.ts | 24 ++++++++++++++++++ src/features/hook-message-injector/index.ts | 4 ++- .../hook-message-injector/injector.ts | 4 +-- src/hooks/todo-continuation-enforcer.ts | 25 +++++++++++++++++++ 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index a3e64bb..d62e8f6 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -1,9 +1,15 @@ +import { existsSync, readdirSync } from "node:fs" +import { join } from "node:path" import type { PluginInput } from "@opencode-ai/plugin" import type { BackgroundTask, LaunchInput, } from "./types" import { log } from "../../shared/logger" +import { + findNearestMessageWithFields, + MESSAGE_STORAGE, +} from "../hook-message-injector" type OpencodeClient = PluginInput["client"] @@ -24,6 +30,20 @@ interface Event { properties?: EventProperties } +function getMessageDir(sessionID: string): string | null { + if (!existsSync(MESSAGE_STORAGE)) return null + + const directPath = join(MESSAGE_STORAGE, sessionID) + if (existsSync(directPath)) return directPath + + for (const dir of readdirSync(MESSAGE_STORAGE)) { + const sessionPath = join(MESSAGE_STORAGE, dir, sessionID) + if (existsSync(sessionPath)) return sessionPath + } + + return null +} + export class BackgroundManager { private tasks: Map private notifications: Map @@ -253,9 +273,13 @@ export class BackgroundManager { setTimeout(async () => { try { + const messageDir = getMessageDir(task.parentSessionID) + const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null + await this.client.session.prompt({ path: { id: task.parentSessionID }, body: { + agent: prevMessage?.agent, parts: [{ type: "text", text: message }], }, query: { directory: this.directory }, diff --git a/src/features/hook-message-injector/index.ts b/src/features/hook-message-injector/index.ts index a131b82..2262a0b 100644 --- a/src/features/hook-message-injector/index.ts +++ b/src/features/hook-message-injector/index.ts @@ -1,2 +1,4 @@ -export { injectHookMessage } from "./injector" +export { injectHookMessage, findNearestMessageWithFields } from "./injector" +export type { StoredMessage } from "./injector" export type { MessageMeta, OriginalMessageContext, TextPart } from "./types" +export { MESSAGE_STORAGE } from "./constants" diff --git a/src/features/hook-message-injector/injector.ts b/src/features/hook-message-injector/injector.ts index a790f20..3e7b9d4 100644 --- a/src/features/hook-message-injector/injector.ts +++ b/src/features/hook-message-injector/injector.ts @@ -3,13 +3,13 @@ import { join } from "node:path" import { MESSAGE_STORAGE, PART_STORAGE } from "./constants" import type { MessageMeta, OriginalMessageContext, TextPart } from "./types" -interface StoredMessage { +export interface StoredMessage { agent?: string model?: { providerID?: string; modelID?: string } tools?: Record } -function findNearestMessageWithFields(messageDir: string): StoredMessage | null { +export function findNearestMessageWithFields(messageDir: string): StoredMessage | null { try { const files = readdirSync(messageDir) .filter((f) => f.endsWith(".json")) diff --git a/src/hooks/todo-continuation-enforcer.ts b/src/hooks/todo-continuation-enforcer.ts index 86d6c6e..e3dfb57 100644 --- a/src/hooks/todo-continuation-enforcer.ts +++ b/src/hooks/todo-continuation-enforcer.ts @@ -1,4 +1,10 @@ +import { existsSync, readdirSync } from "node:fs" +import { join } from "node:path" import type { PluginInput } from "@opencode-ai/plugin" +import { + findNearestMessageWithFields, + MESSAGE_STORAGE, +} from "../features/hook-message-injector" export interface TodoContinuationEnforcer { handler: (input: { event: { type: string; properties?: unknown } }) => Promise @@ -21,6 +27,20 @@ Incomplete tasks remain in your todo list. Continue working on the next pending - Mark each task complete when finished - Do not stop until all tasks are done` +function getMessageDir(sessionID: string): string | null { + if (!existsSync(MESSAGE_STORAGE)) return null + + const directPath = join(MESSAGE_STORAGE, sessionID) + if (existsSync(directPath)) return directPath + + for (const dir of readdirSync(MESSAGE_STORAGE)) { + const sessionPath = join(MESSAGE_STORAGE, dir, sessionID) + if (existsSync(sessionPath)) return sessionPath + } + + return null +} + function detectInterrupt(error: unknown): boolean { if (!error) return false if (typeof error === "object") { @@ -137,9 +157,14 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati } try { + // Get previous message's agent info to respect agent mode + const messageDir = getMessageDir(sessionID) + const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null + await ctx.client.session.prompt({ path: { id: sessionID }, body: { + agent: prevMessage?.agent, parts: [ { type: "text",