From 51b98e02a5fb0a46a7304113ca8cd252bc207599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=BE=D1=80=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=A0=D0=BE?= =?UTF-8?q?=D0=BC=D0=B0=D0=BD?= Date: Fri, 25 Sep 2026 23:24:12 +0300 Subject: [PATCH] refactor: remove V1 code, plugin is V2-only now - Remove isV2Context() detection and all V1 branches - Remove sdkClient parameter and V1 SDK polling logic - Remove extractContent/formatMessage helpers (V1-only) - Remove sentMessageIds dedup map (V1-only) - Remove v1Server() entry point and server() export - Remove encodeSessionId() from session-manager - Remove crypto dependency - Simplify help text (no mode info) - Session ID now uses roomId:threadRootId directly --- index.ts | 302 ++++++--------------------------------------- session-manager.ts | 6 - 2 files changed, 40 insertions(+), 268 deletions(-) diff --git a/index.ts b/index.ts index f59ca50..6cb5196 100644 --- a/index.ts +++ b/index.ts @@ -1,32 +1,23 @@ import fs from "fs" import { MatrixBotClient } from "./matrix-client.js" -import { SessionManager, encodeSessionId } from "./session-manager.js" +import { SessionManager } from "./session-manager.js" import { loadConfig } from "./config-loader.js" import { log } from "./logger.js" log("Plugin module loaded") // ============================================================================= -// V2 Support Detection +// Plugin Setup // ============================================================================= -function isV2Context(ctx: any): boolean { - return !!(ctx?.session?.prompt && ctx?.event?.subscribe && ctx?.storage) -} - -// ============================================================================= -// Shared initialization logic -// ============================================================================= - -async function runPlugin(ctx: any, options: any, sdkClient?: any) { - const isV2 = isV2Context(ctx) +async function runPlugin(ctx: any, options: any) { const eventController = new AbortController() let cleanup: (() => void) | undefined try { const directory = ctx?.location?.directory || process.cwd() - fs.appendFileSync("/tmp/opencode-matrix-plugin/setup-started.log", `[${new Date().toISOString()}] setup() called, dir=${directory}, v2=${isV2}, sdk=${!!sdkClient}\n`) - log(`Plugin setup started, directory=${directory}, v2=${isV2}, sdk=${!!sdkClient}`) + fs.appendFileSync("/tmp/opencode-matrix-plugin/setup-started.log", `[${new Date().toISOString()}] setup() called, dir=${directory}\n`) + log(`Plugin setup started, directory=${directory}`) if (!options.homeserver) { log("Plugin disabled (no homeserver configured)") @@ -38,81 +29,30 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) { const matrix = new MatrixBotClient(options) const sessionManager = new SessionManager(options.rateLimitSeconds || 5) - // Track pending tool calls per session: sessionID -> Map - const pendingToolCalls = new Map>() - - // Track sent messages per session to avoid duplicates - const sentMessageIds = new Map>() - // Track Matrix context per OpenCode session const sessionContextMap = new Map() - // Helper: send a message to Matrix - async function sendMatrixMessage(roomId: string, threadRootId: string, context: any, text: string): Promise { - if (!text.trim()) return - await matrix.sendReply(context, text) - log(`Matrix: Sent to ${roomId}:${threadRootId} (${text.length} chars)`) - } - - // Helper: extract content from parts - function extractContent(parts: any[]): { reasoningText: string; mainText: string; toolCallText: string } { - let reasoningText = "" - let mainText = "" - let toolCallText = "" - const reasoningTypes = new Set(["reasoning", "thinking", "redwood", "reason"]) - - for (const p of parts) { - const content = p?.content || p?.text || "" - const pType = p?.type || "" - if (reasoningTypes.has(pType)) { - reasoningText += content - } else if (pType === "tool_call" || pType === "tool") { - toolCallText += content - } else { - mainText += content - } - } - - return { reasoningText, mainText, toolCallText } - } - - // Helper: format message text - function formatMessage(toolCallText: string, reasoningText: string, mainText: string): string { - if (toolCallText.trim()) { - return `Tool call: ${toolCallText.trim()}` - } - - let responseText = mainText - if (reasoningText.trim()) { - responseText = `
Thoughts\n\n${reasoningText.trim()}\n\n
\n\n---\n\n${mainText}` - } - - return responseText - } - // V2 Event subscription for response streaming - if (isV2) { - void (async () => { - try { - for await (const event of ctx.event.subscribe({ signal: eventController.signal })) { - const eventType = typeof event === "object" && event !== null && "type" in event ? (event as { type: string }).type : "" - if (eventType === "session.updated") { - const sessionId = (event as { sessionID?: string }).sessionID - if (!sessionId) continue - const update = (event as { update?: { type: string; content?: string; toolName?: string } }).update - if (!update) continue - log(`V2 event: session=${sessionId}, update.type=${update.type}`) - } - } - } catch (err) { - if (String(err).includes("AbortError") || (err as { name?: string }).name === "AbortError") { - // expected during cleanup - } else { - log(`Event subscription error: ${String(err)}`) + void (async () => { + try { + for await (const event of ctx.event.subscribe({ signal: eventController.signal })) { + const eventType = typeof event === "object" && event !== null && "type" in event ? (event as { type: string }).type : "" + if (eventType === "session.updated") { + const sessionId = (event as { sessionID?: string }).sessionID + if (!sessionId) continue + const update = (event as { update?: { type: string; content?: string; toolName?: string } }).update + if (!update) continue + log(`V2 event: session=${sessionId}, update.type=${update.type}`) } } - })() - } + } catch (err) { + if (String(err).includes("AbortError") || (err as { name?: string }).name === "AbortError") { + // expected during cleanup + } else { + log(`Event subscription error: ${String(err)}`) + } + } + })() // Message handler matrix.on(async (data: any) => { @@ -147,18 +87,10 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) { if (!opencodeSessionId) { log(`Creating new OpenCode session for ${roomId}:${threadRootId}`) try { - if (isV2 && ctx?.session?.create) { - // V2: can pass custom ID - const encodedId = encodeSessionId(roomId, threadRootId) - const createResult = await ctx.session.create({ id: encodedId, title: `Matrix: ${roomId.slice(0, 30)}...` }) - opencodeSessionId = createResult?.id || createResult?.data?.id || encodedId - log(`V2: Created OpenCode session ${opencodeSessionId} (custom ID: ${encodedId})`) - } else if (sdkClient) { - // V1: cannot pass custom ID, need to save mapping - const createResult = await sdkClient.session.create({ body: { title: `Matrix: ${roomId.slice(0, 30)}...` } }) - opencodeSessionId = createResult?.data?.id || createResult?.id || "" - log(`V1: Created OpenCode session ${opencodeSessionId}`) - } + const encodedId = `${roomId}:${threadRootId}` + const createResult = await ctx.session.create({ id: encodedId, title: `Matrix: ${roomId.slice(0, 30)}...` }) + opencodeSessionId = createResult?.id || createResult?.data?.id || encodedId + log(`Created OpenCode session ${opencodeSessionId} (ID: ${encodedId})`) session.opencodeSessionId = opencodeSessionId sessionManager.saveSession(roomId, threadRootId, opencodeSessionId) sessionContextMap.set(opencodeSessionId, { roomId, threadRootId, context }) @@ -173,112 +105,13 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) { } try { - if (isV2) { - // V2: use ctx.session.prompt() - const result = await ctx.session.prompt({ - sessionID: opencodeSessionId, - text: query, - }) - const responseText = result ? String(result) : "No response" - session.outputChars += responseText.length - await matrix.sendReply(context, responseText) - } else if (sdkClient) { - // V1: use SDK client.session.prompt() - log(`V1: Sending prompt to session ${opencodeSessionId}: ${query}`) - - // Start polling for new messages - const pollInterval = setInterval(async () => { - if (!session.isActive) { - clearInterval(pollInterval) - return - } - - try { - const existingIds = sentMessageIds.get(opencodeSessionId) || new Set() - const messagesResult = await sdkClient.session.messages({ - path: { id: opencodeSessionId }, - }) - - const messages = messagesResult?.data || [] - if (Array.isArray(messages)) { - for (const msg of messages) { - const msgId = msg?.info?.id || msg?.id - if (!msgId || existingIds.has(msgId)) continue - - const role = msg?.info?.role || "" - if (role !== "assistant" && role !== "tool_call") continue - - const parts = msg?.parts || msg?.info?.parts || [] - const { reasoningText, mainText, toolCallText } = extractContent(parts) - const responseText = formatMessage(toolCallText, reasoningText, mainText) - - if (responseText.trim()) { - await matrix.sendReply(context, responseText) - log(`V1: Sent message ${msgId} (role=${role}, ${responseText.length} chars)`) - existingIds.add(msgId) - } - } - } - - sentMessageIds.set(opencodeSessionId, existingIds) - } catch (err) { - log(`V1: Error during polling: ${String(err)}`) - } - }, 1000) - - try { - const result = await sdkClient.session.prompt({ - path: { id: opencodeSessionId }, - body: { parts: [{ type: "text", text: query }] }, - }) - - // Stop polling - clearInterval(pollInterval) - - // Send any remaining messages - try { - const existingIds = sentMessageIds.get(opencodeSessionId) || new Set() - const messagesResult = await sdkClient.session.messages({ - path: { id: opencodeSessionId }, - }) - - const messages = messagesResult?.data || [] - if (Array.isArray(messages)) { - for (const msg of messages) { - const msgId = msg?.info?.id || msg?.id - if (!msgId || existingIds.has(msgId)) continue - - const role = msg?.info?.role || "" - if (role !== "assistant" && role !== "tool_call") continue - - const parts = msg?.parts || msg?.info?.parts || [] - const { reasoningText, mainText, toolCallText } = extractContent(parts) - const responseText = formatMessage(toolCallText, reasoningText, mainText) - - if (responseText.trim()) { - await matrix.sendReply(context, responseText) - log(`V1: Sent message ${msgId} (role=${role}, ${responseText.length} chars)`) - existingIds.add(msgId) - } - } - } - - sentMessageIds.set(opencodeSessionId, existingIds) - } catch (err) { - log(`V1: Error sending final messages: ${String(err)}`) - } - } catch (promptErr: any) { - log(`V1 prompt error: ${String(promptErr)}`) - const errMsg = promptErr?.response?.error?.message || promptErr?.error?.message || String(promptErr) - await matrix.sendNotice(context, `Error: ${errMsg.slice(0, 200)}`) - } - } else { - // No SDK client available - log(`V1 mode without SDK client: Would send prompt to session ${opencodeSessionId}: ${query}`) - const responseText = "Matrix plugin running in V1 mode — prompt forwarding requires OpenCode SDK client" - session.outputChars += responseText.length - await matrix.sendReply(context, responseText) - } + const result = await ctx.session.prompt({ + sessionID: opencodeSessionId, + text: query, + }) + const responseText = result ? String(result) : "No response" + session.outputChars += responseText.length + await matrix.sendReply(context, responseText) } catch (err: any) { log(`Error processing message: ${String(err)}`) const errMsg = err?.response?.error?.message || String(err) @@ -322,7 +155,6 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) { case "h": { const trigger = options.triggerPatterns?.[0] || "!oc " const botName = options.botName || "opencode" - const mode = isV2 ? "OpenCode V2 (full)" : sdkClient ? "OpenCode V1 (full via SDK)" : "OpenCode V1 (limited — requires SDK client)" const msg = [ `${botName} - OpenCode Matrix Plugin`, "", @@ -334,8 +166,6 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) { `Usage: ${trigger} `, `Or @${botName} `, `Or reply in a thread`, - "", - `Mode: ${mode}`, ].join("\n") await sendFn(msg) break @@ -368,8 +198,8 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) { } }) - // V2 Storage - defensive access - if (isV2 && ctx?.storage) { + // V2 Storage + if (ctx?.storage) { await ctx.storage.set("matrix/plugin_version", "1.0.0") await ctx.storage.set("matrix/started_at", new Date().toISOString()) } @@ -377,7 +207,7 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) { // Start Matrix client await matrix.start() - log(`Plugin setup completed successfully (V2=${isV2}, sdk=${!!sdkClient})`) + log(`Plugin setup completed successfully`) cleanup = () => { log("Plugin cleaning up...") @@ -395,47 +225,6 @@ async function runPlugin(ctx: any, options: any, sdkClient?: any) { } } -// ============================================================================= -// V1 Server Entry Point — receives PluginInput with SDK client -// ============================================================================= - -async function v1Server(input: any, options?: any) { - try { - log("V1 server entry point called") - - const { client, directory, worktree, project } = input - const sdkClient = client - - if (!sdkClient) { - log("V1: No SDK client in PluginInput") - return {} - } - - log(`V1: SDK client available from PluginInput`) - - // Load config using loadConfig (reads from matrix.json or env vars) - const pluginOptions = options || {} - const { options: configOptions } = await loadConfig(pluginOptions) - - // Create a context that runPlugin can use - const v1Ctx = { - location: { directory: directory || process.cwd(), project: project || { id: "v1" } }, - storage: { - set: async (_key: string, _value: any) => { /* V1 storage not available */ }, - get: async (_key: string) => null, - }, - session: {}, // Not V2 context - event: {}, // Not V2 context - } - - await runPlugin(v1Ctx as any, configOptions, sdkClient) - return {} - } catch (err) { - log(`V1 server error: ${String(err)}`) - return {} - } -} - // ============================================================================= // V2 Setup Function // ============================================================================= @@ -452,24 +241,13 @@ async function v2Setup(ctx: any) { } // ============================================================================= -// Dual V1/V2 Export +// Export // ============================================================================= -// OpenCode 1.18.x calls server(PluginInput, options) where PluginInput contains SDK client. -// V2 reads id + setup() and ignores server(). export default { id: "matrix-plugin", async setup(ctx: any) { - const isV2 = isV2Context(ctx) - if (isV2) { - log("Using V2 path (full context)") - return await v2Setup(ctx) - } - log("Using V1 path (server() will be called by OpenCode)") - return await v1Server({}, {}) - }, - async server(input: any, options?: any) { - log("V1 server() called by OpenCode") - return await v1Server(input, options) + log("Using V2 path (full context)") + return await v2Setup(ctx) }, } diff --git a/session-manager.ts b/session-manager.ts index 627e587..4bcd08a 100644 --- a/session-manager.ts +++ b/session-manager.ts @@ -1,4 +1,3 @@ -import crypto from "crypto" import fs from "fs" import path from "path" @@ -15,11 +14,6 @@ interface Session { lastEventIds: Map } -export function encodeSessionId(matrixRoomId: string, threadRootId: string): string { - const hash = crypto.createHash("md5").update(`${matrixRoomId}:${threadRootId}`).digest("hex").slice(0, 16) - return `ses_${hash}` -} - const SESSION_MAP_FILE = path.join(process.env.HOME || "/tmp", ".opencode-matrix-sessions.json") console.log(`[SessionManager] SESSION_MAP_FILE=${SESSION_MAP_FILE}`)