From c9c02e0525c9e11763d3b06ca9f94abdc36bdd4d Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 14 Feb 2026 17:50:08 +0900 Subject: [PATCH] refactor(shared): consolidate 13+ getMessageDir copies into single shared function --- src/features/background-agent/message-dir.ts | 2 +- .../message-storage-locator.ts | 18 +-- .../parent-session-context-resolver.ts | 2 +- .../background-agent/result-handler.ts | 2 +- .../message-storage-directory.ts | 1 + .../pruning-deduplication.ts | 20 +--- .../pruning-tool-output-truncation.ts | 16 +-- src/hooks/atlas/recent-model-resolver.ts | 2 +- src/hooks/atlas/session-last-agent.ts | 2 +- .../prometheus-md-only/agent-resolution.ts | 19 +--- .../ralph-loop/message-storage-directory.ts | 17 +-- .../message-directory.ts | 19 +--- src/shared/index.ts | 2 +- src/shared/opencode-message-dir.test.ts | 104 ++++++++++-------- src/shared/opencode-message-dir.ts | 5 +- src/tools/background-task/message-dir.ts | 18 +-- src/tools/background-task/modules/utils.ts | 18 +-- src/tools/call-omo-agent/message-dir.ts | 19 +--- .../message-storage-directory.ts | 19 +--- .../delegate-task/parent-context-resolver.ts | 2 +- src/tools/delegate-task/sync-continuation.ts | 2 +- 21 files changed, 86 insertions(+), 223 deletions(-) diff --git a/src/features/background-agent/message-dir.ts b/src/features/background-agent/message-dir.ts index 138f5dab..cf8b56ed 100644 --- a/src/features/background-agent/message-dir.ts +++ b/src/features/background-agent/message-dir.ts @@ -1 +1 @@ -export { getMessageDir } from "./message-storage-locator" +export { getMessageDir } from "../../shared" diff --git a/src/features/background-agent/message-storage-locator.ts b/src/features/background-agent/message-storage-locator.ts index ceecd329..f9cb8cfd 100644 --- a/src/features/background-agent/message-storage-locator.ts +++ b/src/features/background-agent/message-storage-locator.ts @@ -1,17 +1 @@ -import { existsSync, readdirSync } from "node:fs" -import { join } from "node:path" -import { MESSAGE_STORAGE } from "../hook-message-injector" - -export 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 -} +import { getMessageDir } from "../../shared" diff --git a/src/features/background-agent/parent-session-context-resolver.ts b/src/features/background-agent/parent-session-context-resolver.ts index d27dd375..2eff0b7e 100644 --- a/src/features/background-agent/parent-session-context-resolver.ts +++ b/src/features/background-agent/parent-session-context-resolver.ts @@ -1,7 +1,7 @@ import type { OpencodeClient } from "./constants" import type { BackgroundTask } from "./types" import { findNearestMessageWithFields } from "../hook-message-injector" -import { getMessageDir } from "./message-storage-locator" +import { getMessageDir } from "../../shared" type AgentModel = { providerID: string; modelID: string } diff --git a/src/features/background-agent/result-handler.ts b/src/features/background-agent/result-handler.ts index ccc365c8..3f9f9a7a 100644 --- a/src/features/background-agent/result-handler.ts +++ b/src/features/background-agent/result-handler.ts @@ -1,6 +1,6 @@ export type { ResultHandlerContext } from "./result-handler-context" export { formatDuration } from "./duration-formatter" -export { getMessageDir } from "./message-storage-locator" +export { getMessageDir } from "../../shared" export { checkSessionTodos } from "./session-todo-checker" export { validateSessionHasOutput } from "./session-output-validator" export { tryCompleteTask } from "./background-task-completer" diff --git a/src/hooks/anthropic-context-window-limit-recovery/message-storage-directory.ts b/src/hooks/anthropic-context-window-limit-recovery/message-storage-directory.ts index 8cb0463c..80bc6f11 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/message-storage-directory.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/message-storage-directory.ts @@ -1,3 +1,4 @@ +import { existsSync, readdirSync } from "node:fs" import { getMessageDir } from "../../shared/opencode-message-dir" export { getMessageDir } diff --git a/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.ts b/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.ts index b3e8b520..1598052c 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.ts @@ -1,9 +1,9 @@ -import { existsSync, readdirSync, readFileSync } from "node:fs" +import { readdirSync, readFileSync } from "node:fs" import { join } from "node:path" import type { PruningState, ToolCallSignature } from "./pruning-types" import { estimateTokens } from "./pruning-types" import { log } from "../../shared/logger" -import { MESSAGE_STORAGE } from "../../features/hook-message-injector" +import { getMessageDir } from "../../shared/opencode-message-dir" export interface DeduplicationConfig { enabled: boolean @@ -43,20 +43,6 @@ function sortObject(obj: unknown): unknown { return sorted } -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 readMessages(sessionID: string): MessagePart[] { const messageDir = getMessageDir(sessionID) if (!messageDir) return [] @@ -64,7 +50,7 @@ function readMessages(sessionID: string): MessagePart[] { const messages: MessagePart[] = [] try { - const files = readdirSync(messageDir).filter(f => f.endsWith(".json")) + const files = readdirSync(messageDir).filter((f: string) => f.endsWith(".json")) for (const file of files) { const content = readFileSync(join(messageDir, file), "utf-8") const data = JSON.parse(content) diff --git a/src/hooks/anthropic-context-window-limit-recovery/pruning-tool-output-truncation.ts b/src/hooks/anthropic-context-window-limit-recovery/pruning-tool-output-truncation.ts index 0481e94c..e9294633 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/pruning-tool-output-truncation.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/pruning-tool-output-truncation.ts @@ -3,6 +3,7 @@ import { join } from "node:path" import { getOpenCodeStorageDir } from "../../shared/data-path" import { truncateToolResult } from "./storage" import { log } from "../../shared/logger" +import { getMessageDir } from "../../shared/opencode-message-dir" interface StoredToolPart { type?: string @@ -21,21 +22,6 @@ function getPartStorage(): string { return join(getOpenCodeStorageDir(), "part") } -function getMessageDir(sessionID: string): string | null { - const messageStorage = getMessageStorage() - if (!existsSync(messageStorage)) return null - - const directPath = join(messageStorage, sessionID) - if (existsSync(directPath)) return directPath - - for (const dir of readdirSync(messageStorage)) { - const sessionPath = join(messageStorage, dir, sessionID) - if (existsSync(sessionPath)) return sessionPath - } - - return null -} - function getMessageIds(sessionID: string): string[] { const messageDir = getMessageDir(sessionID) if (!messageDir) return [] diff --git a/src/hooks/atlas/recent-model-resolver.ts b/src/hooks/atlas/recent-model-resolver.ts index 814e6af8..ccaed01c 100644 --- a/src/hooks/atlas/recent-model-resolver.ts +++ b/src/hooks/atlas/recent-model-resolver.ts @@ -1,6 +1,6 @@ import type { PluginInput } from "@opencode-ai/plugin" import { findNearestMessageWithFields } from "../../features/hook-message-injector" -import { getMessageDir } from "../../shared/session-utils" +import { getMessageDir } from "../../shared" import type { ModelInfo } from "./types" export async function resolveRecentModelForSession( diff --git a/src/hooks/atlas/session-last-agent.ts b/src/hooks/atlas/session-last-agent.ts index 341eda6f..4afbf3e4 100644 --- a/src/hooks/atlas/session-last-agent.ts +++ b/src/hooks/atlas/session-last-agent.ts @@ -1,5 +1,5 @@ import { findNearestMessageWithFields } from "../../features/hook-message-injector" -import { getMessageDir } from "../../shared/session-utils" +import { getMessageDir } from "../../shared" export function getLastAgentFromSession(sessionID: string): string | null { const messageDir = getMessageDir(sessionID) diff --git a/src/hooks/prometheus-md-only/agent-resolution.ts b/src/hooks/prometheus-md-only/agent-resolution.ts index b59c5a3a..c6adf2e8 100644 --- a/src/hooks/prometheus-md-only/agent-resolution.ts +++ b/src/hooks/prometheus-md-only/agent-resolution.ts @@ -1,22 +1,7 @@ -import { existsSync, readdirSync } from "node:fs" -import { join } from "node:path" -import { findNearestMessageWithFields, findFirstMessageWithAgent, MESSAGE_STORAGE } from "../../features/hook-message-injector" +import { findNearestMessageWithFields, findFirstMessageWithAgent } from "../../features/hook-message-injector" import { getSessionAgent } from "../../features/claude-code-session-state" import { readBoulderState } from "../../features/boulder-state" - -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 -} +import { getMessageDir } from "../../shared/opencode-message-dir" function getAgentFromMessageFiles(sessionID: string): string | undefined { const messageDir = getMessageDir(sessionID) diff --git a/src/hooks/ralph-loop/message-storage-directory.ts b/src/hooks/ralph-loop/message-storage-directory.ts index 7d4caca1..a9111f43 100644 --- a/src/hooks/ralph-loop/message-storage-directory.ts +++ b/src/hooks/ralph-loop/message-storage-directory.ts @@ -1,16 +1 @@ -import { existsSync, readdirSync } from "node:fs" -import { join } from "node:path" -import { MESSAGE_STORAGE } from "../../features/hook-message-injector" - -export 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 { getMessageDir } from "../../shared/opencode-message-dir" diff --git a/src/hooks/todo-continuation-enforcer/message-directory.ts b/src/hooks/todo-continuation-enforcer/message-directory.ts index 85e68242..a9111f43 100644 --- a/src/hooks/todo-continuation-enforcer/message-directory.ts +++ b/src/hooks/todo-continuation-enforcer/message-directory.ts @@ -1,18 +1 @@ -import { existsSync, readdirSync } from "node:fs" -import { join } from "node:path" - -import { MESSAGE_STORAGE } from "../../features/hook-message-injector" - -export 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 { getMessageDir } from "../../shared/opencode-message-dir" diff --git a/src/shared/index.ts b/src/shared/index.ts index 4b135520..54bcf679 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -37,7 +37,7 @@ export { resolveModelPipeline } from "./model-resolution-pipeline" export type { ModelResolutionRequest, ModelResolutionProvenance, - ModelResolutionPipelineResult, + ModelResolutionResult, } from "./model-resolution-types" export * from "./model-availability" export * from "./connected-providers-cache" diff --git a/src/shared/opencode-message-dir.test.ts b/src/shared/opencode-message-dir.test.ts index 251a1f4d..a47d61db 100644 --- a/src/shared/opencode-message-dir.test.ts +++ b/src/shared/opencode-message-dir.test.ts @@ -1,83 +1,95 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" -import { existsSync, readdirSync } from "node:fs" -import { join } from "node:path" -import { getMessageDir } from "./opencode-message-dir" +declare const require: (name: string) => any +const { describe, it, expect, beforeEach, afterEach, beforeAll, mock } = require("bun:test") -// Mock the constants -vi.mock("../tools/session-manager/constants", () => ({ - MESSAGE_STORAGE: "/mock/message/storage", -})) +let getMessageDir: (sessionID: string) => string | null -vi.mock("node:fs", () => ({ - existsSync: vi.fn(), - readdirSync: vi.fn(), -})) +beforeAll(async () => { + // Mock the data-path module + mock.module("./data-path", () => ({ + getOpenCodeStorageDir: () => "/mock/opencode/storage", + })) -vi.mock("node:path", () => ({ - join: vi.fn(), -})) + // Mock fs functions + mock.module("node:fs", () => ({ + existsSync: mock(() => false), + readdirSync: mock(() => []), + })) -const mockExistsSync = vi.mocked(existsSync) -const mockReaddirSync = vi.mocked(readdirSync) -const mockJoin = vi.mocked(join) + mock.module("node:path", () => ({ + join: mock((...args: string[]) => args.join("/")), + })) + + ;({ getMessageDir } = await import("./opencode-message-dir")) +}) describe("getMessageDir", () => { beforeEach(() => { - vi.clearAllMocks() - mockJoin.mockImplementation((...args) => args.join("/")) + // Reset mocks + mock.restore() }) - afterEach(() => { - vi.restoreAllMocks() + it("returns null when sessionID does not start with ses_", () => { + // given + // no mocks needed + + // when + const result = getMessageDir("invalid") + + // then + expect(result).toBe(null) }) it("returns null when MESSAGE_STORAGE does not exist", () => { // given - mockExistsSync.mockReturnValue(false) + mock.module("node:fs", () => ({ + existsSync: mock(() => false), + readdirSync: mock(() => []), + })) // when - const result = getMessageDir("session123") + const result = getMessageDir("ses_123") // then expect(result).toBe(null) - expect(mockExistsSync).toHaveBeenCalledWith("/mock/message/storage") }) it("returns direct path when session exists directly", () => { // given - mockExistsSync.mockImplementation((path) => path === "/mock/message/storage" || path === "/mock/message/storage/session123") + mock.module("node:fs", () => ({ + existsSync: mock((path: string) => path === "/mock/opencode/storage/message" || path === "/mock/opencode/storage/message/ses_123"), + readdirSync: mock(() => []), + })) // when - const result = getMessageDir("session123") + const result = getMessageDir("ses_123") // then - expect(result).toBe("/mock/message/storage/session123") - expect(mockExistsSync).toHaveBeenCalledWith("/mock/message/storage") - expect(mockExistsSync).toHaveBeenCalledWith("/mock/message/storage/session123") + expect(result).toBe("/mock/opencode/storage/message/ses_123") }) it("returns subdirectory path when session exists in subdirectory", () => { // given - mockExistsSync.mockImplementation((path) => { - return path === "/mock/message/storage" || path === "/mock/message/storage/subdir/session123" - }) - mockReaddirSync.mockReturnValue(["subdir"]) + mock.module("node:fs", () => ({ + existsSync: mock((path: string) => path === "/mock/opencode/storage/message" || path === "/mock/opencode/storage/message/subdir/ses_123"), + readdirSync: mock(() => ["subdir"]), + })) // when - const result = getMessageDir("session123") + const result = getMessageDir("ses_123") // then - expect(result).toBe("/mock/message/storage/subdir/session123") - expect(mockReaddirSync).toHaveBeenCalledWith("/mock/message/storage") + expect(result).toBe("/mock/opencode/storage/message/subdir/ses_123") }) it("returns null when session not found anywhere", () => { // given - mockExistsSync.mockImplementation((path) => path === "/mock/message/storage") - mockReaddirSync.mockReturnValue(["subdir1", "subdir2"]) + mock.module("node:fs", () => ({ + existsSync: mock((path: string) => path === "/mock/opencode/storage/message"), + readdirSync: mock(() => ["subdir1", "subdir2"]), + })) // when - const result = getMessageDir("session123") + const result = getMessageDir("ses_123") // then expect(result).toBe(null) @@ -85,13 +97,15 @@ describe("getMessageDir", () => { it("returns null when readdirSync throws", () => { // given - mockExistsSync.mockImplementation((path) => path === "/mock/message/storage") - mockReaddirSync.mockImplementation(() => { - throw new Error("Permission denied") - }) + mock.module("node:fs", () => ({ + existsSync: mock((path: string) => path === "/mock/opencode/storage/message"), + readdirSync: mock(() => { + throw new Error("Permission denied") + }), + })) // when - const result = getMessageDir("session123") + const result = getMessageDir("ses_123") // then expect(result).toBe(null) diff --git a/src/shared/opencode-message-dir.ts b/src/shared/opencode-message-dir.ts index f2b81594..080eadc8 100644 --- a/src/shared/opencode-message-dir.ts +++ b/src/shared/opencode-message-dir.ts @@ -1,8 +1,11 @@ import { existsSync, readdirSync } from "node:fs" import { join } from "node:path" -import { MESSAGE_STORAGE } from "../tools/session-manager/constants" +import { getOpenCodeStorageDir } from "./data-path" + +const MESSAGE_STORAGE = join(getOpenCodeStorageDir(), "message") export function getMessageDir(sessionID: string): string | null { + if (!sessionID.startsWith("ses_")) return null if (!existsSync(MESSAGE_STORAGE)) return null const directPath = join(MESSAGE_STORAGE, sessionID) diff --git a/src/tools/background-task/message-dir.ts b/src/tools/background-task/message-dir.ts index 74c49607..a9111f43 100644 --- a/src/tools/background-task/message-dir.ts +++ b/src/tools/background-task/message-dir.ts @@ -1,17 +1 @@ -import { existsSync, readdirSync } from "node:fs" -import { join } from "node:path" -import { MESSAGE_STORAGE } from "../../features/hook-message-injector" - -export 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 { getMessageDir } from "../../shared/opencode-message-dir" diff --git a/src/tools/background-task/modules/utils.ts b/src/tools/background-task/modules/utils.ts index bfc14c63..907f8eaf 100644 --- a/src/tools/background-task/modules/utils.ts +++ b/src/tools/background-task/modules/utils.ts @@ -1,20 +1,6 @@ -import { existsSync, readdirSync } from "node:fs" -import { join } from "node:path" -import { MESSAGE_STORAGE } from "../../../features/hook-message-injector" +import { getMessageDir } from "../../../shared" -export 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 { getMessageDir } export function formatDuration(start: Date, end?: Date): string { const duration = (end ?? new Date()).getTime() - start.getTime() diff --git a/src/tools/call-omo-agent/message-dir.ts b/src/tools/call-omo-agent/message-dir.ts index 01fa68fc..a9111f43 100644 --- a/src/tools/call-omo-agent/message-dir.ts +++ b/src/tools/call-omo-agent/message-dir.ts @@ -1,18 +1 @@ -import { existsSync, readdirSync } from "node:fs" -import { join } from "node:path" -import { MESSAGE_STORAGE } from "../../features/hook-message-injector" - -export function getMessageDir(sessionID: string): string | null { - if (!sessionID.startsWith("ses_")) return 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 { getMessageDir } from "../../shared/opencode-message-dir" diff --git a/src/tools/call-omo-agent/message-storage-directory.ts b/src/tools/call-omo-agent/message-storage-directory.ts index 30fecd6e..cf8b56ed 100644 --- a/src/tools/call-omo-agent/message-storage-directory.ts +++ b/src/tools/call-omo-agent/message-storage-directory.ts @@ -1,18 +1 @@ -import { existsSync, readdirSync } from "node:fs" -import { join } from "node:path" -import { MESSAGE_STORAGE } from "../../features/hook-message-injector" - -export function getMessageDir(sessionID: string): string | null { - if (!sessionID.startsWith("ses_")) return 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 { getMessageDir } from "../../shared" diff --git a/src/tools/delegate-task/parent-context-resolver.ts b/src/tools/delegate-task/parent-context-resolver.ts index cf231783..1eea7b7a 100644 --- a/src/tools/delegate-task/parent-context-resolver.ts +++ b/src/tools/delegate-task/parent-context-resolver.ts @@ -3,7 +3,7 @@ import type { ParentContext } from "./executor-types" import { findNearestMessageWithFields, findFirstMessageWithAgent } from "../../features/hook-message-injector" import { getSessionAgent } from "../../features/claude-code-session-state" import { log } from "../../shared/logger" -import { getMessageDir } from "../../shared/session-utils" +import { getMessageDir } from "../../shared" export function resolveParentContext(ctx: ToolContextWithMetadata): ParentContext { const messageDir = getMessageDir(ctx.sessionID) diff --git a/src/tools/delegate-task/sync-continuation.ts b/src/tools/delegate-task/sync-continuation.ts index 72355982..0a72a454 100644 --- a/src/tools/delegate-task/sync-continuation.ts +++ b/src/tools/delegate-task/sync-continuation.ts @@ -4,7 +4,7 @@ import { isPlanFamily } from "./constants" import { storeToolMetadata } from "../../features/tool-metadata-store" import { getTaskToastManager } from "../../features/task-toast-manager" import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions" -import { getMessageDir } from "../../shared/session-utils" +import { getMessageDir } from "../../shared" import { promptWithModelSuggestionRetry } from "../../shared/model-suggestion-retry" import { findNearestMessageWithFields } from "../../features/hook-message-injector" import { formatDuration } from "./time-formatter"