@alfe.ai/openclaw-chat 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ import { a as AlfeResolvedAccount, i as AlfePluginConfig, n as createAlfeChannel
10
10
  *
11
11
  * Each session file contains metadata and the full message history.
12
12
  * Sessions are written on every message to ensure durability.
13
+ * Old sessions are cleaned up automatically (30-day TTL, 1000 max).
13
14
  */
14
15
  interface ChatMessage {
15
16
  role: 'user' | 'assistant';
package/dist/plugin.d.ts CHANGED
@@ -205,6 +205,7 @@ interface AgentEventPayload {
205
205
  interface PluginApi {
206
206
  logger: PluginLogger;
207
207
  config?: Record<string, unknown>;
208
+ runtime?: PluginRuntime;
208
209
  registerChannel(channel: ReturnType<typeof createAlfeChannelPlugin>): void;
209
210
  registerGatewayMethod?(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
210
211
  on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: {
@@ -216,11 +217,6 @@ declare const plugin: {
216
217
  name: string;
217
218
  description: string;
218
219
  version: string;
219
- /**
220
- * Called by OpenClaw to inject the in-process runtime.
221
- * This replaces the need for a separate WS connection.
222
- */
223
- setRuntime(runtime: PluginRuntime): void;
224
220
  activate(api: PluginApi): void;
225
221
  deactivate(api: PluginApi): void;
226
222
  };
package/dist/plugin2.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
2
- import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { existsSync } from "node:fs";
@@ -135,6 +135,37 @@ function createAlfeChannelPlugin() {
135
135
  };
136
136
  }
137
137
  //#endregion
138
+ //#region src/session-keys.ts
139
+ /**
140
+ * Session key helpers — handles both raw and prefixed formats.
141
+ *
142
+ * Raw format: "chat-{tenantId}-{agentId}-{suffix}"
143
+ * Prefixed format: "agent:{agentId}:chat-{tenantId}-{agentId}-{suffix}"
144
+ *
145
+ * The chat adapter wraps keys with "agent:{agentId}:" before sending
146
+ * to the daemon. The plugin may receive either format depending on
147
+ * which OpenClaw event fires.
148
+ */
149
+ /**
150
+ * Check if a session key belongs to the Alfe chat channel.
151
+ * Handles both raw and prefixed formats.
152
+ */
153
+ function isAlfeSessionKey(key) {
154
+ return key.includes("chat-") || key.includes("alfe:") || key.includes(":alfe:");
155
+ }
156
+ /**
157
+ * Strip the "agent:{agentId}:" prefix and extract tenantId + agentId.
158
+ * Returns empty strings if the key doesn't match the expected format.
159
+ */
160
+ function parseAlfeSessionKey(key) {
161
+ const rawKey = key.includes(":") ? key.slice(key.lastIndexOf(":") + 1) : key;
162
+ const match = /^chat-([^-]+)-([^-]+)/.exec(rawKey);
163
+ return {
164
+ tenantId: match?.[1] ?? "",
165
+ agentId: match?.[2] ?? ""
166
+ };
167
+ }
168
+ //#endregion
138
169
  //#region src/session-store.ts
139
170
  /**
140
171
  * Session Store — persists chat sessions to the local filesystem.
@@ -144,14 +175,53 @@ function createAlfeChannelPlugin() {
144
175
  *
145
176
  * Each session file contains metadata and the full message history.
146
177
  * Sessions are written on every message to ensure durability.
178
+ * Old sessions are cleaned up automatically (30-day TTL, 1000 max).
147
179
  */
148
180
  const SESSIONS_DIR = join(homedir(), ".alfe", "sessions", "chat");
181
+ const MAX_SESSIONS = 1e3;
182
+ const MAX_AGE_MS = 720 * 60 * 60 * 1e3;
183
+ const CLEANUP_INTERVAL_MS = 36e5;
184
+ let lastCleanupAt = 0;
149
185
  async function ensureDir() {
150
186
  if (!existsSync(SESSIONS_DIR)) await mkdir(SESSIONS_DIR, { recursive: true });
151
187
  }
152
188
  function sessionPath(sessionId) {
153
189
  return join(SESSIONS_DIR, `${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
154
190
  }
191
+ async function cleanupOldSessions() {
192
+ if (Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return;
193
+ lastCleanupAt = Date.now();
194
+ try {
195
+ const jsonFiles = (await readdir(SESSIONS_DIR)).filter((f) => f.endsWith(".json"));
196
+ if (jsonFiles.length <= MAX_SESSIONS) {
197
+ const now = Date.now();
198
+ for (const file of jsonFiles) try {
199
+ const filePath = join(SESSIONS_DIR, file);
200
+ if (now - (await stat(filePath)).mtimeMs > MAX_AGE_MS) await unlink(filePath);
201
+ } catch {}
202
+ return;
203
+ }
204
+ const fileStats = [];
205
+ for (const file of jsonFiles) try {
206
+ const filePath = join(SESSIONS_DIR, file);
207
+ const fileStat = await stat(filePath);
208
+ fileStats.push({
209
+ path: filePath,
210
+ mtimeMs: fileStat.mtimeMs
211
+ });
212
+ } catch {}
213
+ fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
214
+ const now = Date.now();
215
+ let remaining = fileStats.length;
216
+ for (const entry of fileStats) {
217
+ if (!(now - entry.mtimeMs > MAX_AGE_MS) && !(remaining > MAX_SESSIONS)) break;
218
+ try {
219
+ await unlink(entry.path);
220
+ remaining--;
221
+ } catch {}
222
+ }
223
+ } catch {}
224
+ }
155
225
  async function getSession(sessionId) {
156
226
  try {
157
227
  const data = await readFile(sessionPath(sessionId), "utf-8");
@@ -178,6 +248,7 @@ async function createSession(sessionId, agentId, channel, tenantId, userId) {
178
248
  messages: []
179
249
  };
180
250
  await saveSession(session);
251
+ cleanupOldSessions();
181
252
  return session;
182
253
  }
183
254
  async function addMessage(sessionId, role, content) {
@@ -190,7 +261,7 @@ async function addMessage(sessionId, role, content) {
190
261
  });
191
262
  await saveSession(session);
192
263
  }
193
- async function listSessions(filters) {
264
+ async function listSessions(filters, limit = 50) {
194
265
  await ensureDir();
195
266
  let files;
196
267
  try {
@@ -221,7 +292,7 @@ async function listSessions(filters) {
221
292
  const aTime = a.lastMessageAt ?? a.createdAt;
222
293
  return (b.lastMessageAt ?? b.createdAt).localeCompare(aTime);
223
294
  });
224
- return summaries;
295
+ return summaries.slice(0, limit);
225
296
  }
226
297
  //#endregion
227
298
  //#region src/plugin.ts
@@ -287,15 +358,13 @@ const plugin = {
287
358
  name: "Alfe Chat Plugin",
288
359
  description: "Alfe conversation channel — web widget and mobile app share unified chat sessions",
289
360
  version: "0.3.0",
290
- setRuntime(runtime) {
291
- pluginRuntime = runtime;
292
- },
293
361
  activate(api) {
294
362
  if (globalThis.__alfeChatPluginActivated) {
295
363
  api.logger.debug("Alfe Chat plugin already activated, skipping re-init");
296
364
  return;
297
365
  }
298
366
  globalThis.__alfeChatPluginActivated = true;
367
+ pluginRuntime = api.runtime ?? null;
299
368
  const log = api.logger;
300
369
  log.info("Alfe Chat plugin registering...");
301
370
  const alfeChannel = createAlfeChannelPlugin();
@@ -361,23 +430,20 @@ const plugin = {
361
430
  }
362
431
  api.on("session_start", async (...eventArgs) => {
363
432
  const key = eventArgs[0].sessionKey;
364
- if (!key) return;
365
- if (!(key.startsWith("chat-") || key.includes("alfe:") || key.includes(":alfe:"))) return;
433
+ if (!key || !isAlfeSessionKey(key)) return;
366
434
  log.info(`Alfe chat session starting: ${key}`);
367
- const parts = key.split("-");
368
- await createSession(key, parts.length >= 3 ? parts[2] : "", "alfe", parts.length >= 2 ? parts[1] : "");
435
+ const { tenantId, agentId: parsedAgentId } = parseAlfeSessionKey(key);
436
+ await createSession(key, parsedAgentId, "alfe", tenantId);
369
437
  }, { priority: 50 });
370
438
  api.on("message", async (...eventArgs) => {
371
439
  const event = eventArgs[0];
372
440
  const key = event.sessionKey;
373
- if (!key) return;
374
- if (!(key.startsWith("chat-") || key.includes("alfe:") || key.includes(":alfe:"))) return;
441
+ if (!key || !isAlfeSessionKey(key)) return;
375
442
  await addMessage(key, event.role, event.content);
376
443
  });
377
444
  api.on("session_end", (...eventArgs) => {
378
445
  const key = eventArgs[0].sessionKey;
379
- if (!key) return;
380
- if (!(key.startsWith("chat-") || key.includes("alfe:") || key.includes(":alfe:"))) return;
446
+ if (!key || !isAlfeSessionKey(key)) return;
381
447
  log.info(`Alfe chat session ending: ${key}`);
382
448
  });
383
449
  log.info("Alfe Chat plugin registered");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-chat",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin.js",
@@ -25,7 +25,7 @@
25
25
  "openclaw.plugin.json"
26
26
  ],
27
27
  "dependencies": {
28
- "@alfe.ai/chat": "^0.0.2"
28
+ "@alfe.ai/chat": "^0.0.3"
29
29
  },
30
30
  "license": "UNLICENSED",
31
31
  "scripts": {