UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

246 lines (244 loc) 10.3 kB
import { s as resolveStateDir } from "../../paths-CyR9Pa1R.js"; import { d as resolveAgentIdFromSessionKey } from "../../session-key-CgcjHuX_.js"; import "../../registry-BmY4gNy6.js"; import { s as resolveAgentWorkspaceDir } from "../../agent-scope-BLYwrEEA.js"; import { t as createSubsystemLogger } from "../../subsystem-B5g771Td.js"; import "../../workspace-D9IzNnST.js"; import "../../tokens-BRx7o_ZS.js"; import "../../pi-embedded-BDHP7G3o.js"; import "../../normalize-DZvZ0Eiv.js"; import "../../accounts-D7irbKbS.js"; import "../../boolean-B8-BqKGQ.js"; import "../../command-format-Ck2WauwF.js"; import "../../bindings-DWtXcdc8.js"; import "../../send-CbPQXDCf.js"; import "../../plugins-CJYXNO4s.js"; import "../../send-VMzh7zM1.js"; import "../../deliver-C5sD6Dv0.js"; import "../../diagnostic-CoT2YQH4.js"; import "../../diagnostic-session-state-DmrztgHU.js"; import "../../accounts-DSVHyeYh.js"; import "../../send-xJGV2Pmu.js"; import "../../fs-safe-B8-WkEXM.js"; import "../../model-auth-BPNt9bHS.js"; import "../../github-copilot-token-BQ98eazZ.js"; import "../../pi-model-discovery-C-yOXpma.js"; import "../../message-channel-BDlvrUZF.js"; import { ot as hasInterSessionUserProvenance } from "../../pi-embedded-helpers-BfjCe8vu.js"; import "../../config-C0k_b9gg.js"; import "../../manifest-registry-Dovhiqzt.js"; import "../../common-y9ALHdVZ.js"; import "../../chrome-DVeI6N8j.js"; import "../../frontmatter-BT4H5SZG.js"; import "../../skills-Ci_2Mah4.js"; import "../../redact-BBbIZgau.js"; import "../../errors-3KjSwJLH.js"; import "../../ssrf-B8OrDkCk.js"; import "../../store-Be0QxLPZ.js"; import "../../thinking-Bfk6_7EU.js"; import "../../accounts-C5646aU0.js"; import "../../paths-gnW-md4M.js"; import "../../image-Do9rxbj3.js"; import "../../reply-prefix-BaCBHwi7.js"; import "../../manager-D7zJM2Ly.js"; import "../../gemini-auth-DkBnXstb.js"; import "../../sqlite-DJh2xLr6.js"; import "../../retry-BxB_D6Pn.js"; import "../../chunk-TRVny4T1.js"; import "../../markdown-tables-DO9qAEKa.js"; import "../../local-roots-tMzVEQD2.js"; import "../../ir-CRTCIHIG.js"; import "../../render-CDCvpfhh.js"; import "../../commands-registry-DqNdrksb.js"; import "../../skill-commands-pI2wkJrZ.js"; import "../../runner-CLSBcNny.js"; import "../../fetch-DtI0mtzx.js"; import "../../send-hD-4IHpX.js"; import "../../outbound-attachment-BChLBLPg.js"; import "../../send-Cm_4TZEY.js"; import "../../resolve-route-ClURVRZM.js"; import "../../channel-activity-LT3oBvO7.js"; import "../../tables-xDr7OSBa.js"; import "../../proxy-CBJ1upuz.js"; import "../../replies-BMmKlXtC.js"; import { generateSlugViaLLM } from "../../llm-slug-generator.js"; import { t as resolveHookConfig } from "../../config-ClSsetRi.js"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; //#region src/hooks/bundled/session-memory/handler.ts /** * Session memory hook handler * * Saves session context to memory when /new or /reset command is triggered * Creates a new dated memory file with LLM-generated slug */ const log = createSubsystemLogger("hooks/session-memory"); /** * Read recent messages from session file for slug generation */ async function getRecentSessionContent(sessionFilePath, messageCount = 15) { try { const lines = (await fs.readFile(sessionFilePath, "utf-8")).trim().split("\n"); const allMessages = []; for (const line of lines) try { const entry = JSON.parse(line); if (entry.type === "message" && entry.message) { const msg = entry.message; const role = msg.role; if ((role === "user" || role === "assistant") && msg.content) { if (role === "user" && hasInterSessionUserProvenance(msg)) continue; const text = Array.isArray(msg.content) ? msg.content.find((c) => c.type === "text")?.text : msg.content; if (text && !text.startsWith("/")) allMessages.push(`${role}: ${text}`); } } } catch {} return allMessages.slice(-messageCount).join("\n"); } catch { return null; } } /** * Try the active transcript first; if /new already rotated it, * fallback to the latest .jsonl.reset.* sibling. */ async function getRecentSessionContentWithResetFallback(sessionFilePath, messageCount = 15) { const primary = await getRecentSessionContent(sessionFilePath, messageCount); if (primary) return primary; try { const dir = path.dirname(sessionFilePath); const resetPrefix = `${path.basename(sessionFilePath)}.reset.`; const resetCandidates = (await fs.readdir(dir)).filter((name) => name.startsWith(resetPrefix)).toSorted(); if (resetCandidates.length === 0) return primary; const latestResetPath = path.join(dir, resetCandidates[resetCandidates.length - 1]); const fallback = await getRecentSessionContent(latestResetPath, messageCount); if (fallback) log.debug("Loaded session content from reset fallback", { sessionFilePath, latestResetPath }); return fallback || primary; } catch { return primary; } } function stripResetSuffix(fileName) { const resetIndex = fileName.indexOf(".reset."); return resetIndex === -1 ? fileName : fileName.slice(0, resetIndex); } async function findPreviousSessionFile(params) { try { const files = await fs.readdir(params.sessionsDir); const fileSet = new Set(files); const baseFromReset = params.currentSessionFile ? stripResetSuffix(path.basename(params.currentSessionFile)) : void 0; if (baseFromReset && fileSet.has(baseFromReset)) return path.join(params.sessionsDir, baseFromReset); const trimmedSessionId = params.sessionId?.trim(); if (trimmedSessionId) { const canonicalFile = `${trimmedSessionId}.jsonl`; if (fileSet.has(canonicalFile)) return path.join(params.sessionsDir, canonicalFile); const topicVariants = files.filter((name) => name.startsWith(`${trimmedSessionId}-topic-`) && name.endsWith(".jsonl") && !name.includes(".reset.")).toSorted().toReversed(); if (topicVariants.length > 0) return path.join(params.sessionsDir, topicVariants[0]); } if (!params.currentSessionFile) return; const nonResetJsonl = files.filter((name) => name.endsWith(".jsonl") && !name.includes(".reset.")).toSorted().toReversed(); if (nonResetJsonl.length > 0) return path.join(params.sessionsDir, nonResetJsonl[0]); } catch {} } /** * Save session context to memory when /new or /reset command is triggered */ const saveSessionToMemory = async (event) => { const isResetCommand = event.action === "new" || event.action === "reset"; if (event.type !== "command" || !isResetCommand) return; try { log.debug("Hook triggered for reset/new command", { action: event.action }); const context = event.context || {}; const cfg = context.cfg; const agentId = resolveAgentIdFromSessionKey(event.sessionKey); const workspaceDir = cfg ? resolveAgentWorkspaceDir(cfg, agentId) : path.join(resolveStateDir(process.env, os.homedir), "workspace"); const memoryDir = path.join(workspaceDir, "memory"); await fs.mkdir(memoryDir, { recursive: true }); const now = new Date(event.timestamp); const dateStr = now.toISOString().split("T")[0]; const sessionEntry = context.previousSessionEntry || context.sessionEntry || {}; const currentSessionId = sessionEntry.sessionId; let currentSessionFile = sessionEntry.sessionFile || void 0; if (!currentSessionFile || currentSessionFile.includes(".reset.")) { const sessionsDirs = /* @__PURE__ */ new Set(); if (currentSessionFile) sessionsDirs.add(path.dirname(currentSessionFile)); sessionsDirs.add(path.join(workspaceDir, "sessions")); for (const sessionsDir of sessionsDirs) { const recoveredSessionFile = await findPreviousSessionFile({ sessionsDir, currentSessionFile, sessionId: currentSessionId }); if (!recoveredSessionFile) continue; currentSessionFile = recoveredSessionFile; log.debug("Found previous session file", { file: currentSessionFile }); break; } } log.debug("Session context resolved", { sessionId: currentSessionId, sessionFile: currentSessionFile, hasCfg: Boolean(cfg) }); const sessionFile = currentSessionFile || void 0; const hookConfig = resolveHookConfig(cfg, "session-memory"); const messageCount = typeof hookConfig?.messages === "number" && hookConfig.messages > 0 ? hookConfig.messages : 15; let slug = null; let sessionContent = null; if (sessionFile) { sessionContent = await getRecentSessionContentWithResetFallback(sessionFile, messageCount); log.debug("Session content loaded", { length: sessionContent?.length ?? 0, messageCount }); const allowLlmSlug = !(process.env.OPENCLAW_TEST_FAST === "1" || process.env.VITEST === "true" || process.env.VITEST === "1" || false) && hookConfig?.llmSlug !== false; if (sessionContent && cfg && allowLlmSlug) { log.debug("Calling generateSlugViaLLM..."); slug = await generateSlugViaLLM({ sessionContent, cfg }); log.debug("Generated slug", { slug }); } } if (!slug) { slug = now.toISOString().split("T")[1].split(".")[0].replace(/:/g, "").slice(0, 4); log.debug("Using fallback timestamp slug", { slug }); } const filename = `${dateStr}-${slug}.md`; const memoryFilePath = path.join(memoryDir, filename); log.debug("Memory file path resolved", { filename, path: memoryFilePath.replace(os.homedir(), "~") }); const timeStr = now.toISOString().split("T")[1].split(".")[0]; const sessionId = sessionEntry.sessionId || "unknown"; const source = context.commandSource || "unknown"; const entryParts = [ `# Session: ${dateStr} ${timeStr} UTC`, "", `- **Session Key**: ${event.sessionKey}`, `- **Session ID**: ${sessionId}`, `- **Source**: ${source}`, "" ]; if (sessionContent) entryParts.push("## Conversation Summary", "", sessionContent, ""); const entry = entryParts.join("\n"); await fs.writeFile(memoryFilePath, entry, "utf-8"); log.debug("Memory file written successfully"); const relPath = memoryFilePath.replace(os.homedir(), "~"); log.info(`Session context saved to ${relPath}`); } catch (err) { if (err instanceof Error) log.error("Failed to save session memory", { errorName: err.name, errorMessage: err.message, stack: err.stack }); else log.error("Failed to save session memory", { error: String(err) }); } }; //#endregion export { saveSessionToMemory as default };