diff --git a/extensions/line/runtime-api.ts b/extensions/line/runtime-api.ts index 53f1be0c51c..8b4984ec244 100644 --- a/extensions/line/runtime-api.ts +++ b/extensions/line/runtime-api.ts @@ -4,7 +4,6 @@ export * from "../../src/plugin-sdk/line.js"; export { resolveExactLineGroupConfigKey } from "../../src/plugin-sdk/line-core.js"; export { - formatDocsLink, setSetupChannelEnabled, splitSetupEntries, type ChannelSetupDmPolicy, diff --git a/extensions/line/src/setup-surface.ts b/extensions/line/src/setup-surface.ts index 6f46cc92217..2f7e14b583c 100644 --- a/extensions/line/src/setup-surface.ts +++ b/extensions/line/src/setup-surface.ts @@ -1,7 +1,7 @@ import { createAllowFromSection, createTopLevelChannelDmPolicy } from "openclaw/plugin-sdk/setup"; +import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools"; import { DEFAULT_ACCOUNT_ID, - formatDocsLink, resolveLineAccount, setSetupChannelEnabled, splitSetupEntries, diff --git a/src/agents/cortex.ts b/src/agents/cortex.ts index acb09ed3fbd..ed46ff8898f 100644 --- a/src/agents/cortex.ts +++ b/src/agents/cortex.ts @@ -324,7 +324,9 @@ function buildAgentCortexConversationKey(params: { sessionId?: string; channelId?: string; }): string { - return [params.agentId, params.sessionId ?? "", params.channelId ?? ""].join(":"); + // Use NUL as separator to avoid collisions when IDs contain colons + // (e.g. "session:test" vs separate "session" + "test" tokens). + return [params.agentId, params.sessionId ?? "", params.channelId ?? ""].join("\0"); } export function getAgentCortexMemoryCaptureStatus(params: { diff --git a/src/gateway/server/health-state.ts b/src/gateway/server/health-state.ts index 9fc8c6dde74..8b3c7f7a109 100644 --- a/src/gateway/server/health-state.ts +++ b/src/gateway/server/health-state.ts @@ -1,5 +1,8 @@ import { resolveDefaultAgentId } from "../../agents/agent-scope.js"; -import { getLatestCortexCaptureHistoryEntry } from "../../agents/cortex-history.js"; +import { + getCachedLatestCortexCaptureHistoryEntry, + getLatestCortexCaptureHistoryEntry, +} from "../../agents/cortex-history.js"; import { resolveAgentCortexModeStatus, resolveCortexChannelTarget } from "../../agents/cortex.js"; import { getHealthSnapshot, type HealthSummary } from "../../commands/health.js"; import { STATE_DIR, createConfigIO, loadConfig } from "../../config/config.js"; @@ -41,12 +44,17 @@ export async function buildGatewaySnapshot(): Promise { sessionId: mainSessionEntry?.sessionId, channelId, }); + // Prefer the in-memory cache to avoid reading the full JSONL during + // WebSocket connect handshakes. Fall back to async read only when + // the cache is cold (first snapshot after restart). + const cortexHistoryParams = { + agentId: defaultAgentId, + sessionId: mainSessionEntry?.sessionId, + channelId, + }; const latestCortexCapture = cortex - ? await getLatestCortexCaptureHistoryEntry({ - agentId: defaultAgentId, - sessionId: mainSessionEntry?.sessionId, - channelId, - }).catch(() => null) + ? (getCachedLatestCortexCaptureHistoryEntry(cortexHistoryParams) ?? + (await getLatestCortexCaptureHistoryEntry(cortexHistoryParams).catch(() => null))) : null; const scope = cfg.session?.scope ?? "per-sender"; const presence = listSystemPresence(); diff --git a/src/memory/cortex-mode-overrides.ts b/src/memory/cortex-mode-overrides.ts index 9678e82838e..9d3ce51ec5a 100644 --- a/src/memory/cortex-mode-overrides.ts +++ b/src/memory/cortex-mode-overrides.ts @@ -19,7 +19,8 @@ type CortexModeOverrideStore = { }; function buildKey(agentId: string, targetId: string): string { - return `${agentId}:${targetId}`; + // Use NUL separator to avoid collisions when IDs contain colons. + return `${agentId}\0${targetId}`; } export function resolveCortexModeOverridesPath(env: NodeJS.ProcessEnv = process.env): string { diff --git a/src/memory/cortex.ts b/src/memory/cortex.ts index e6e6588e1a3..442cbce5c89 100644 --- a/src/memory/cortex.ts +++ b/src/memory/cortex.ts @@ -405,13 +405,23 @@ export async function ingestCortexMemoryFromText(params: { if (!text) { throw new Error("Cortex memory ingest requires non-empty text"); } - const status = requireCortexStatus( - await resolveCortexStatus({ + const status = await resolveCortexStatus({ + workspaceDir: params.workspaceDir, + graphPath: params.graphPath, + status: params.status, + }); + if (!status.available) { + throw new Error("Cortex CLI unavailable: " + (status.error ?? "unknown error")); + } + // Allow ingesting even if the graph does not exist yet - cortex extract + // creates the output file when -o is provided, and ensureCortexGraphInitialized + // seeds the directory below. This fixes first-time ingest failures. + if (!status.graphExists) { + await ensureCortexGraphInitialized({ workspaceDir: params.workspaceDir, graphPath: params.graphPath, - status: params.status, - }), - ); + }); + } await fs.mkdir(path.dirname(status.graphPath), { recursive: true }); const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cortex-ingest-")); const inputPath = path.join(tmpDir, "memory.txt"); diff --git a/src/plugin-sdk/channel-import-guardrails.test.ts b/src/plugin-sdk/channel-import-guardrails.test.ts index 9b481097ed6..579672d747c 100644 --- a/src/plugin-sdk/channel-import-guardrails.test.ts +++ b/src/plugin-sdk/channel-import-guardrails.test.ts @@ -121,6 +121,10 @@ const SETUP_BARREL_GUARDS: GuardedSource[] = [ path: "extensions/whatsapp/src/setup-surface.ts", forbiddenPatterns: [/\bformatCliCommand\b/, /\bformatDocsLink\b/], }, + { + path: "extensions/line/src/setup-surface.ts", + forbiddenPatterns: [/\bformatDocsLink\b/], + }, ]; const LOCAL_EXTENSION_API_BARREL_GUARDS = [ diff --git a/src/plugin-sdk/line-core.ts b/src/plugin-sdk/line-core.ts index 04b2950a50d..9b6e5e58f09 100644 --- a/src/plugin-sdk/line-core.ts +++ b/src/plugin-sdk/line-core.ts @@ -3,7 +3,6 @@ export type { LineConfig } from "../line/types.js"; export { createTopLevelChannelDmPolicy, DEFAULT_ACCOUNT_ID, - formatDocsLink, setSetupChannelEnabled, setTopLevelChannelDmPolicyWithAllowFrom, splitSetupEntries,