fix(hooks): respect previous message's agent mode in message sending hooks

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)
This commit is contained in:
YeonGyu-Kim
2025-12-15 19:02:31 +09:00
parent 9aab980dc7
commit 355fa35651
4 changed files with 54 additions and 3 deletions

View File

@@ -1,9 +1,15 @@
import { existsSync, readdirSync } from "node:fs"
import { join } from "node:path"
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import type { import type {
BackgroundTask, BackgroundTask,
LaunchInput, LaunchInput,
} from "./types" } from "./types"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import {
findNearestMessageWithFields,
MESSAGE_STORAGE,
} from "../hook-message-injector"
type OpencodeClient = PluginInput["client"] type OpencodeClient = PluginInput["client"]
@@ -24,6 +30,20 @@ interface Event {
properties?: EventProperties 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 { export class BackgroundManager {
private tasks: Map<string, BackgroundTask> private tasks: Map<string, BackgroundTask>
private notifications: Map<string, BackgroundTask[]> private notifications: Map<string, BackgroundTask[]>
@@ -253,9 +273,13 @@ export class BackgroundManager {
setTimeout(async () => { setTimeout(async () => {
try { try {
const messageDir = getMessageDir(task.parentSessionID)
const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
await this.client.session.prompt({ await this.client.session.prompt({
path: { id: task.parentSessionID }, path: { id: task.parentSessionID },
body: { body: {
agent: prevMessage?.agent,
parts: [{ type: "text", text: message }], parts: [{ type: "text", text: message }],
}, },
query: { directory: this.directory }, query: { directory: this.directory },

View File

@@ -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 type { MessageMeta, OriginalMessageContext, TextPart } from "./types"
export { MESSAGE_STORAGE } from "./constants"

View File

@@ -3,13 +3,13 @@ import { join } from "node:path"
import { MESSAGE_STORAGE, PART_STORAGE } from "./constants" import { MESSAGE_STORAGE, PART_STORAGE } from "./constants"
import type { MessageMeta, OriginalMessageContext, TextPart } from "./types" import type { MessageMeta, OriginalMessageContext, TextPart } from "./types"
interface StoredMessage { export interface StoredMessage {
agent?: string agent?: string
model?: { providerID?: string; modelID?: string } model?: { providerID?: string; modelID?: string }
tools?: Record<string, boolean> tools?: Record<string, boolean>
} }
function findNearestMessageWithFields(messageDir: string): StoredMessage | null { export function findNearestMessageWithFields(messageDir: string): StoredMessage | null {
try { try {
const files = readdirSync(messageDir) const files = readdirSync(messageDir)
.filter((f) => f.endsWith(".json")) .filter((f) => f.endsWith(".json"))

View File

@@ -1,4 +1,10 @@
import { existsSync, readdirSync } from "node:fs"
import { join } from "node:path"
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import {
findNearestMessageWithFields,
MESSAGE_STORAGE,
} from "../features/hook-message-injector"
export interface TodoContinuationEnforcer { export interface TodoContinuationEnforcer {
handler: (input: { event: { type: string; properties?: unknown } }) => Promise<void> handler: (input: { event: { type: string; properties?: unknown } }) => Promise<void>
@@ -21,6 +27,20 @@ Incomplete tasks remain in your todo list. Continue working on the next pending
- Mark each task complete when finished - Mark each task complete when finished
- Do not stop until all tasks are done` - 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 { function detectInterrupt(error: unknown): boolean {
if (!error) return false if (!error) return false
if (typeof error === "object") { if (typeof error === "object") {
@@ -137,9 +157,14 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
} }
try { 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({ await ctx.client.session.prompt({
path: { id: sessionID }, path: { id: sessionID },
body: { body: {
agent: prevMessage?.agent,
parts: [ parts: [
{ {
type: "text", type: "text",