@alfe.ai/openclaw-chat 0.0.4 → 0.0.6

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
@@ -1,4 +1,4 @@
1
- import { a as AlfeResolvedAccount, c as OpenClawConfig, d as createAlfeChannelPlugin, i as AlfePluginConfig, l as OpenClawModule, n as AlfeChannelAccountConfig, o as IPCClient, r as AlfeChannelConfig, s as Logger, t as plugin, u as OpenClawPluginApi } from "./plugin.js";
1
+ import { a as AlfeResolvedAccount, i as AlfePluginConfig, n as createAlfeChannelPlugin, r as AlfeChannelConfig, t as plugin } from "./plugin.js";
2
2
 
3
3
  //#region src/session-store.d.ts
4
4
 
@@ -10,6 +10,7 @@ import { a as AlfeResolvedAccount, c as OpenClawConfig, d as createAlfeChannelPl
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';
@@ -36,4 +37,4 @@ interface SessionSummary {
36
37
  messageCount: number;
37
38
  }
38
39
  //#endregion
39
- export { AlfeChannelAccountConfig, AlfeChannelConfig, AlfePluginConfig, AlfeResolvedAccount, type ChatMessage, IPCClient, Logger, OpenClawConfig, OpenClawModule, OpenClawPluginApi, type SessionData, type SessionSummary, createAlfeChannelPlugin, plugin as default, plugin };
40
+ export { type AlfeChannelConfig, type AlfePluginConfig, type AlfeResolvedAccount, type ChatMessage, type SessionData, type SessionSummary, createAlfeChannelPlugin, plugin as default };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  import { n as createAlfeChannelPlugin, t as plugin } from "./plugin2.js";
2
- export { createAlfeChannelPlugin, plugin as default, plugin };
2
+ export { createAlfeChannelPlugin, plugin as default };
package/dist/plugin.d.ts CHANGED
@@ -1,5 +1,58 @@
1
+ //#region src/types.d.ts
2
+ /**
3
+ * Types for the Alfe chat channel plugin.
4
+ *
5
+ * SDK types (PluginRuntime, OpenClawPluginApi, OpenClawConfig, etc.)
6
+ * are imported from openclaw/plugin-sdk at usage sites.
7
+ * This file only contains Alfe-specific domain types.
8
+ */
9
+ interface AlfeChannelAccountConfig {
10
+ /** Whether this account is enabled. */
11
+ enabled?: boolean;
12
+ /** Allowed sender identifiers (user IDs, email addresses). */
13
+ allowFrom?: string | string[];
14
+ /** Default delivery target. */
15
+ defaultTo?: string;
16
+ /** DM policy (open, allowlist, etc.). */
17
+ dmPolicy?: string;
18
+ }
19
+ interface AlfeChannelConfig {
20
+ /** Whether the Alfe channel is enabled. */
21
+ enabled?: boolean;
22
+ /** Allowed sender identifiers. */
23
+ allowFrom?: string | string[];
24
+ /** Default delivery target for outbound messages. */
25
+ defaultTo?: string;
26
+ /** DM policy. */
27
+ dmPolicy?: string;
28
+ /** Named accounts (multi-account support). */
29
+ accounts?: Record<string, AlfeChannelAccountConfig>;
30
+ }
31
+ interface AlfeResolvedAccount {
32
+ accountId: string;
33
+ enabled: boolean;
34
+ allowFrom: string[];
35
+ defaultTo?: string;
36
+ dmPolicy?: string;
37
+ }
38
+ interface AlfePluginConfig {
39
+ /** Agent ID this plugin is associated with. */
40
+ agentId?: string;
41
+ /** Chat service WebSocket URL (e.g. wss://chat.dev.alfe.ai/ws) */
42
+ chatWsUrl?: string;
43
+ /** API key for chat service auth */
44
+ apiKey?: string;
45
+ }
46
+ //#endregion
1
47
  //#region src/alfe-channel.d.ts
2
-
48
+ /** OpenClaw config shape — inline to avoid runtime dependency on openclaw package */
49
+ interface OpenClawConfig {
50
+ channels?: {
51
+ alfe?: AlfeChannelConfig;
52
+ [key: string]: unknown;
53
+ };
54
+ [key: string]: unknown;
55
+ }
3
56
  /**
4
57
  * Creates the Alfe ChannelPlugin object for registration with OpenClaw.
5
58
  *
@@ -108,106 +161,64 @@ declare function createAlfeChannelPlugin(): {
108
161
  };
109
162
  };
110
163
  //#endregion
111
- //#region src/types.d.ts
112
- /**
113
- * Types for the Alfe chat channel plugin.
114
- *
115
- * The Alfe channel registers with OpenClaw as a first-class channel,
116
- * allowing web and mobile clients to share conversation sessions.
117
- */
118
- interface AlfeChannelAccountConfig {
119
- /** Whether this account is enabled. */
120
- enabled?: boolean;
121
- /** Allowed sender identifiers (user IDs, email addresses). */
122
- allowFrom?: string | string[];
123
- /** Default delivery target. */
124
- defaultTo?: string;
125
- /** DM policy (open, allowlist, etc.). */
126
- dmPolicy?: string;
127
- }
128
- interface AlfeChannelConfig {
129
- /** Whether the Alfe channel is enabled. */
130
- enabled?: boolean;
131
- /** Allowed sender identifiers. */
132
- allowFrom?: string | string[];
133
- /** Default delivery target for outbound messages. */
134
- defaultTo?: string;
135
- /** DM policy. */
136
- dmPolicy?: string;
137
- /** Named accounts (multi-account support). */
138
- accounts?: Record<string, AlfeChannelAccountConfig>;
139
- }
140
- interface AlfeResolvedAccount {
141
- accountId: string;
142
- enabled: boolean;
143
- allowFrom: string[];
144
- defaultTo?: string;
145
- dmPolicy?: string;
146
- }
147
- interface AlfePluginConfig {
148
- /** Alfe daemon IPC socket path override. */
149
- daemonSocket?: string;
150
- /** Agent ID this plugin is associated with. */
151
- agentId?: string;
152
- /** Chat service WebSocket URL (e.g. wss://chat.dev.alfe.ai/ws) */
153
- chatWsUrl?: string;
154
- /** API key for chat service auth */
155
- apiKey?: string;
156
- /** OpenClaw local gateway URL override (default: ws://127.0.0.1:18789) */
157
- openclawGatewayUrl?: string;
158
- }
159
- interface Logger {
164
+ //#region src/plugin.d.ts
165
+ interface PluginLogger {
160
166
  info(msg: string, ...args: unknown[]): void;
161
167
  warn(msg: string, ...args: unknown[]): void;
162
168
  error(msg: string, ...args: unknown[]): void;
163
169
  debug(msg: string, ...args: unknown[]): void;
164
170
  }
165
- interface OpenClawConfig {
166
- channels?: {
167
- alfe?: AlfeChannelConfig;
168
- [key: string]: unknown;
171
+ interface PluginRuntime {
172
+ config: {
173
+ loadConfig(): Record<string, unknown>;
174
+ };
175
+ events: {
176
+ onAgentEvent(listener: (evt: AgentEventPayload) => void): () => void;
169
177
  };
170
- plugins?: {
171
- entries?: Record<string, {
172
- config?: AlfePluginConfig;
173
- [key: string]: unknown;
178
+ subagent: {
179
+ run(params: {
180
+ sessionKey: string;
181
+ message: string;
182
+ idempotencyKey?: string;
183
+ deliver?: boolean;
184
+ }): Promise<{
185
+ runId: string;
186
+ }>;
187
+ waitForRun(params: {
188
+ runId: string;
189
+ timeoutMs?: number;
190
+ }): Promise<{
191
+ status: string;
192
+ error?: string;
174
193
  }>;
175
- [key: string]: unknown;
176
194
  };
177
- [key: string]: unknown;
195
+ channel: unknown;
196
+ }
197
+ interface AgentEventPayload {
198
+ runId: string;
199
+ seq: number;
200
+ stream: string;
201
+ ts: number;
202
+ data: Record<string, unknown>;
203
+ sessionKey?: string;
178
204
  }
179
- interface OpenClawPluginApi {
180
- logger: Logger;
181
- config?: OpenClawConfig;
205
+ interface PluginApi {
206
+ logger: PluginLogger;
207
+ config?: Record<string, unknown>;
208
+ runtime?: PluginRuntime;
182
209
  registerChannel(channel: ReturnType<typeof createAlfeChannelPlugin>): void;
183
210
  registerGatewayMethod?(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
184
211
  on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: {
185
212
  priority?: number;
186
213
  }): void;
187
214
  }
188
- interface IPCClient {
189
- on(event: string, handler: (...args: unknown[]) => void | Promise<void>): void;
190
- start(): void;
191
- stop(): void;
192
- request(method: string, params: Record<string, unknown>): Promise<{
193
- ok: boolean;
194
- error?: {
195
- message: string;
196
- };
197
- }>;
198
- }
199
- interface OpenClawModule {
200
- IPCClient: new (socketPath: string, log: Logger) => IPCClient;
201
- }
202
- //#endregion
203
- //#region src/plugin.d.ts
204
215
  declare const plugin: {
205
216
  id: string;
206
217
  name: string;
207
218
  description: string;
208
219
  version: string;
209
- activate(api: OpenClawPluginApi): Promise<void>;
210
- deactivate(api: OpenClawPluginApi): void;
220
+ activate(api: PluginApi): void;
221
+ deactivate(api: PluginApi): void;
211
222
  };
212
223
  //#endregion
213
- export { AlfeResolvedAccount as a, OpenClawConfig as c, createAlfeChannelPlugin as d, AlfePluginConfig as i, OpenClawModule as l, AlfeChannelAccountConfig as n, IPCClient as o, AlfeChannelConfig as r, Logger as s, plugin as t, OpenClawPluginApi as u };
224
+ export { AlfeResolvedAccount as a, AlfePluginConfig as i, createAlfeChannelPlugin as n, AlfeChannelConfig as r, plugin as t };
package/dist/plugin2.js CHANGED
@@ -1,9 +1,7 @@
1
+ import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
2
+ import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
1
3
  import { join } from "node:path";
2
4
  import { homedir } from "node:os";
3
- import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
4
- import WebSocket from "ws";
5
- import { randomUUID } from "node:crypto";
6
- import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
7
5
  import { existsSync } from "node:fs";
8
6
  //#region src/alfe-channel.ts
9
7
  const CHANNEL_ID = "alfe";
@@ -137,238 +135,36 @@ function createAlfeChannelPlugin() {
137
135
  };
138
136
  }
139
137
  //#endregion
140
- //#region src/runtime-relay.ts
138
+ //#region src/session-keys.ts
141
139
  /**
142
- * RuntimeRelay bridges the chat service to the local OpenClaw runtime.
140
+ * Session key helpers handles both raw and prefixed formats.
143
141
  *
144
- * Connects to OpenClaw's local WebSocket server (typically ws://127.0.0.1:18789),
145
- * performs the protocol handshake, and forwards RPC requests from the chat
146
- * service to the runtime. Responses and streaming events are routed back
147
- * via callbacks.
142
+ * Raw format: "chat-{tenantId}-{agentId}-{suffix}"
143
+ * Prefixed format: "agent:{agentId}:chat-{tenantId}-{agentId}-{suffix}"
148
144
  *
149
- * Modeled after the voice relay's LocalGatewayClient pattern.
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.
150
148
  */
151
- const RECONNECT_DELAYS = [
152
- 500,
153
- 1e3,
154
- 2e3,
155
- 4e3,
156
- 8e3,
157
- 15e3,
158
- 3e4
159
- ];
160
- var RuntimeRelay = class {
161
- ws = null;
162
- pending = /* @__PURE__ */ new Map();
163
- connected = false;
164
- stopped = false;
165
- retryCount = 0;
166
- retryTimer = null;
167
- connectSent = false;
168
- wsUrl;
169
- token;
170
- log;
171
- constructor(options) {
172
- this.options = options;
173
- this.wsUrl = options.wsUrl ?? "ws://127.0.0.1:18789";
174
- this.token = options.token ?? "";
175
- this.log = options.logger;
176
- }
177
- get isConnected() {
178
- return this.connected && this.ws?.readyState === WebSocket.OPEN;
179
- }
180
- start() {
181
- this.log.debug("RuntimeRelay starting...");
182
- this.stopped = false;
183
- this.doConnect();
184
- }
185
- stop() {
186
- this.log.debug("RuntimeRelay stopping...");
187
- this.stopped = true;
188
- if (this.retryTimer) {
189
- clearTimeout(this.retryTimer);
190
- this.retryTimer = null;
191
- }
192
- if (this.ws) {
193
- try {
194
- this.ws.close(1e3);
195
- } catch {}
196
- this.ws = null;
197
- }
198
- this.connected = false;
199
- this.flushPending(/* @__PURE__ */ new Error("RuntimeRelay stopped"));
200
- }
201
- /**
202
- * Forward an RPC request from the chat service to the runtime.
203
- * The response routes back via the callback.
204
- */
205
- forwardRequest(msg, onResponse) {
206
- if (this.ws?.readyState !== WebSocket.OPEN) {
207
- onResponse({
208
- id: msg.id,
209
- ok: false,
210
- error: { message: "Runtime not connected" }
211
- });
212
- return;
213
- }
214
- const timeoutMs = msg.method === "agent" ? 13e4 : 3e4;
215
- const timer = setTimeout(() => {
216
- this.pending.delete(msg.id);
217
- onResponse({
218
- id: msg.id,
219
- ok: false,
220
- error: { message: `'${msg.method}' timed out` }
221
- });
222
- }, timeoutMs);
223
- this.pending.set(msg.id, {
224
- resolve: (payload) => {
225
- clearTimeout(timer);
226
- onResponse({
227
- id: msg.id,
228
- ok: true,
229
- payload
230
- });
231
- },
232
- reject: (err) => {
233
- clearTimeout(timer);
234
- onResponse({
235
- id: msg.id,
236
- ok: false,
237
- error: { message: err.message }
238
- });
239
- },
240
- expectFinal: msg.method === "agent"
241
- });
242
- this.ws.send(JSON.stringify({
243
- type: "req",
244
- id: msg.id,
245
- method: msg.method,
246
- params: msg.params
247
- }));
248
- }
249
- doConnect() {
250
- if (this.stopped) return;
251
- this.log.info(`Connecting to runtime at ${this.wsUrl}...`);
252
- this.ws = new WebSocket(this.wsUrl, { maxPayload: 25 * 1024 * 1024 });
253
- this.ws.on("open", () => {
254
- this.log.info("Connected to runtime — authenticating...");
255
- this.retryCount = 0;
256
- setTimeout(() => {
257
- this.sendConnect();
258
- }, 750);
259
- });
260
- this.ws.on("message", (data) => {
261
- const text = Buffer.isBuffer(data) ? data.toString("utf-8") : Buffer.from(data).toString("utf-8");
262
- this.handleMessage(text);
263
- });
264
- this.ws.on("close", (code) => {
265
- this.log.warn(`Runtime disconnected (${String(code)})`);
266
- this.connected = false;
267
- this.flushPending(/* @__PURE__ */ new Error(`Runtime closed (${String(code)})`));
268
- this.scheduleReconnect();
269
- });
270
- this.ws.on("error", (err) => {
271
- this.log.error(`Runtime WS error: ${err.message}`);
272
- });
273
- }
274
- async sendConnect() {
275
- if (this.connectSent) return;
276
- this.connectSent = true;
277
- const id = randomUUID();
278
- const timeoutMs = 1e4;
279
- try {
280
- await new Promise((resolve, reject) => {
281
- const timer = setTimeout(() => {
282
- this.pending.delete(id);
283
- reject(/* @__PURE__ */ new Error("Connect handshake timed out"));
284
- }, timeoutMs);
285
- this.pending.set(id, {
286
- resolve: () => {
287
- clearTimeout(timer);
288
- resolve();
289
- },
290
- reject: (err) => {
291
- clearTimeout(timer);
292
- reject(err);
293
- },
294
- expectFinal: false
295
- });
296
- this.ws?.send(JSON.stringify({
297
- type: "req",
298
- id,
299
- method: "connect",
300
- params: {
301
- minProtocol: 3,
302
- maxProtocol: 3,
303
- client: {
304
- id: "chat-relay",
305
- displayName: "Alfe Chat Relay",
306
- version: "1.0.0",
307
- platform: process.platform,
308
- mode: "backend",
309
- instanceId: `chat-${Date.now().toString(36)}`
310
- },
311
- caps: [],
312
- role: "operator",
313
- scopes: ["operator.admin"],
314
- auth: this.token ? { token: this.token } : void 0
315
- }
316
- }));
317
- });
318
- this.connected = true;
319
- this.log.info("Runtime authenticated — relay active");
320
- } catch (err) {
321
- const errMsg = err instanceof Error ? err.message : String(err);
322
- this.log.error(`Runtime connect failed: ${errMsg}`);
323
- this.ws?.close(1008, "connect failed");
324
- }
325
- }
326
- handleMessage(raw) {
327
- try {
328
- const parsed = JSON.parse(raw);
329
- if (parsed.event) {
330
- const payload = parsed.payload;
331
- if (parsed.event === "connect.challenge" && payload?.nonce) {
332
- this.connectSent = false;
333
- this.sendConnect();
334
- return;
335
- }
336
- if (payload) this.options.onEvent(parsed.event, payload);
337
- return;
338
- }
339
- if ("id" in parsed && "ok" in parsed) {
340
- const pending = this.pending.get(parsed.id);
341
- if (!pending) return;
342
- const payload = parsed.payload;
343
- if (pending.expectFinal && payload?.status === "accepted") return;
344
- this.pending.delete(parsed.id);
345
- if (parsed.ok) pending.resolve(payload);
346
- else {
347
- const error = parsed.error;
348
- pending.reject(new Error(error?.message ?? "unknown error"));
349
- }
350
- }
351
- } catch (err) {
352
- const errMsg = err instanceof Error ? err.message : String(err);
353
- this.log.error(`Runtime message parse error: ${errMsg}`);
354
- }
355
- }
356
- scheduleReconnect() {
357
- if (this.stopped) return;
358
- const delay = RECONNECT_DELAYS[Math.min(this.retryCount, RECONNECT_DELAYS.length - 1)];
359
- this.retryCount++;
360
- this.connectSent = false;
361
- this.log.info(`Reconnecting to runtime in ${String(delay)}ms (attempt ${String(this.retryCount)})...`);
362
- this.retryTimer = setTimeout(() => {
363
- this.retryTimer = null;
364
- this.doConnect();
365
- }, delay);
366
- }
367
- flushPending(err) {
368
- for (const [, p] of this.pending) p.reject(err);
369
- this.pending.clear();
370
- }
371
- };
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
+ }
372
168
  //#endregion
373
169
  //#region src/session-store.ts
374
170
  /**
@@ -379,14 +175,53 @@ var RuntimeRelay = class {
379
175
  *
380
176
  * Each session file contains metadata and the full message history.
381
177
  * Sessions are written on every message to ensure durability.
178
+ * Old sessions are cleaned up automatically (30-day TTL, 1000 max).
382
179
  */
383
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;
384
185
  async function ensureDir() {
385
186
  if (!existsSync(SESSIONS_DIR)) await mkdir(SESSIONS_DIR, { recursive: true });
386
187
  }
387
188
  function sessionPath(sessionId) {
388
189
  return join(SESSIONS_DIR, `${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
389
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
+ }
390
225
  async function getSession(sessionId) {
391
226
  try {
392
227
  const data = await readFile(sessionPath(sessionId), "utf-8");
@@ -413,6 +248,7 @@ async function createSession(sessionId, agentId, channel, tenantId, userId) {
413
248
  messages: []
414
249
  };
415
250
  await saveSession(session);
251
+ cleanupOldSessions();
416
252
  return session;
417
253
  }
418
254
  async function addMessage(sessionId, role, content) {
@@ -425,7 +261,7 @@ async function addMessage(sessionId, role, content) {
425
261
  });
426
262
  await saveSession(session);
427
263
  }
428
- async function listSessions(filters) {
264
+ async function listSessions(filters, limit = 50) {
429
265
  await ensureDir();
430
266
  let files;
431
267
  try {
@@ -456,62 +292,65 @@ async function listSessions(filters) {
456
292
  const aTime = a.lastMessageAt ?? a.createdAt;
457
293
  return (b.lastMessageAt ?? b.createdAt).localeCompare(aTime);
458
294
  });
459
- return summaries;
295
+ return summaries.slice(0, limit);
460
296
  }
461
297
  //#endregion
462
298
  //#region src/plugin.ts
463
299
  /**
464
300
  * @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
465
301
  *
466
- * Registers the 'alfe' channel with OpenClaw, creating a first-class
467
- * channel for Alfe conversations. Web and mobile clients share the
468
- * same channel and conversation sessions.
302
+ * Registers the 'alfe' channel with OpenClaw. Messages are dispatched
303
+ * in-process via runtime.subagent.run() no separate WebSocket to the
304
+ * runtime needed.
469
305
  *
470
- * This follows the same pattern as the voice-gateway plugin:
471
- * - Registers channel via api.registerChannel()
472
- * - Connects to the Alfe daemon IPC for capability registration
473
- * - Registers gateway RPC methods for message delivery and session queries
474
- * - Hooks into session lifecycle events
475
- * - Persists chat sessions to ~/.alfe/sessions/chat/
476
- * - Gracefully degrades if the daemon is unavailable
306
+ * Architecture:
307
+ * Chat Service (Fly.io) ←WS→ ChatServiceClient → runtime.subagent.run() → Agent (in-process)
308
+ * onAgentEvent streaming
477
309
  */
478
- const DEFAULT_SOCKET_PATH = join(homedir(), ".alfe", "gateway.sock");
479
- const CHAT_CAPABILITIES = [
480
- "chat.web",
481
- "chat.mobile",
482
- "chat.sessions"
483
- ];
484
- let daemonIpcClient = null;
310
+ let pluginRuntime = null;
485
311
  let chatClient = null;
486
- let runtimeRelay = null;
487
- /**
488
- * Attempt to connect to the Alfe daemon IPC socket.
489
- * Returns null if @alfe.ai/openclaw is not available or daemon is unreachable.
490
- */
491
- async function connectToDaemon(socketPath, log) {
312
+ async function handleAgentRequest(request, log) {
313
+ const runtime = pluginRuntime;
314
+ if (!runtime) {
315
+ chatClient?.sendResponse(request.id, false, { message: "Plugin runtime not initialized" });
316
+ return;
317
+ }
318
+ const { message, sessionKey } = request.params;
319
+ if (!message || !sessionKey) {
320
+ chatClient?.sendResponse(request.id, false, { message: "Missing message or sessionKey" });
321
+ return;
322
+ }
323
+ const unsubscribe = runtime.events.onAgentEvent((evt) => {
324
+ if (evt.sessionKey !== sessionKey) return;
325
+ if (evt.stream === "assistant") chatClient?.sendEvent("chat", {
326
+ runId: evt.runId,
327
+ sessionKey,
328
+ seq: evt.seq,
329
+ state: "delta",
330
+ message: evt.data
331
+ });
332
+ });
492
333
  try {
493
- const IPCClientCtor = (await import("@alfe.ai/openclaw")).IPCClient;
494
- const client = new IPCClientCtor(socketPath, log);
495
- client.on("connected", async () => {
496
- log.info("Connected to Alfe daemon — registering chat capabilities...");
497
- const response = await client.request("capability.register", {
498
- plugin: "@alfe.ai/openclaw-chat",
499
- capabilities: [...CHAT_CAPABILITIES]
500
- });
501
- if (response.ok) log.info("Chat capabilities registered with daemon");
502
- else log.warn(`Failed to register chat capabilities: ${response.error?.message ?? "unknown"}`);
334
+ const { runId } = await runtime.subagent.run({
335
+ sessionKey,
336
+ message,
337
+ deliver: true
503
338
  });
504
- client.on("disconnected", (reason) => {
505
- log.warn(`Disconnected from Alfe daemon: ${String(reason)}`);
339
+ const result = await runtime.subagent.waitForRun({
340
+ runId,
341
+ timeoutMs: 12e4
506
342
  });
507
- client.on("error", (err) => {
508
- log.debug(`Daemon IPC error: ${err.message}`);
343
+ if (result.status === "ok") chatClient?.sendResponse(request.id, true, {
344
+ text: "",
345
+ sessionKey
509
346
  });
510
- client.start();
511
- return client;
512
- } catch {
513
- log.info("Alfe daemon not available — chat plugin running standalone");
514
- return null;
347
+ else chatClient?.sendResponse(request.id, false, { message: result.error ?? `Agent run ${result.status}` });
348
+ } catch (err) {
349
+ const errMsg = err instanceof Error ? err.message : String(err);
350
+ log.error(`Agent dispatch failed: ${errMsg}`);
351
+ chatClient?.sendResponse(request.id, false, { message: errMsg });
352
+ } finally {
353
+ unsubscribe();
515
354
  }
516
355
  }
517
356
  const plugin = {
@@ -519,80 +358,56 @@ const plugin = {
519
358
  name: "Alfe Chat Plugin",
520
359
  description: "Alfe conversation channel — web widget and mobile app share unified chat sessions",
521
360
  version: "0.3.0",
522
- async activate(api) {
361
+ activate(api) {
523
362
  if (globalThis.__alfeChatPluginActivated) {
524
363
  api.logger.debug("Alfe Chat plugin already activated, skipping re-init");
525
364
  return;
526
365
  }
527
366
  globalThis.__alfeChatPluginActivated = true;
367
+ pluginRuntime = api.runtime ?? null;
528
368
  const log = api.logger;
529
- log.info("Alfe Chat plugin activating...");
530
- const pluginConfig = (api.config ?? {}).plugins?.entries?.["@alfe.ai/openclaw-chat"]?.config ?? {};
369
+ log.info("Alfe Chat plugin registering...");
531
370
  const alfeChannel = createAlfeChannelPlugin();
532
371
  api.registerChannel(alfeChannel);
533
- log.info(`Registered channel: ${alfeChannel.id} (${alfeChannel.meta.label})`);
534
- daemonIpcClient = await connectToDaemon(pluginConfig.daemonSocket ?? process.env.ALFE_GATEWAY_SOCKET ?? DEFAULT_SOCKET_PATH, log);
535
- const { apiKey, chatWsUrl } = await resolveAlfeChat({
536
- apiKey: pluginConfig.apiKey,
537
- chatWsUrl: pluginConfig.chatWsUrl
538
- });
539
- const runtimeUrl = pluginConfig.openclawGatewayUrl ?? process.env.OPENCLAW_GATEWAY_URL ?? "ws://127.0.0.1:18789";
540
- const runtimeToken = process.env.OPENCLAW_GATEWAY_TOKEN ?? "";
541
- if (chatWsUrl && apiKey) {
542
- log.info(`Connecting to chat service: ${chatWsUrl}`);
543
- runtimeRelay = new RuntimeRelay({
544
- wsUrl: runtimeUrl,
545
- token: runtimeToken,
546
- onEvent: (event, payload) => {
547
- chatClient?.sendEvent(event, payload);
548
- },
549
- logger: log
550
- });
551
- chatClient = new ChatServiceClient({
552
- wsUrl: chatWsUrl,
553
- apiKey,
554
- onRequest: (request) => {
555
- if (!runtimeRelay?.isConnected) {
556
- chatClient?.sendResponse(request.id, false, { message: "Runtime not connected" });
557
- return;
558
- }
559
- runtimeRelay.forwardRequest(request, (response) => {
560
- chatClient?.send(response);
372
+ log.info(`Registered channel: ${alfeChannel.id}`);
373
+ const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
374
+ (async () => {
375
+ try {
376
+ const { apiKey, chatWsUrl } = await resolveAlfeChat({
377
+ apiKey: pluginConfig.apiKey,
378
+ chatWsUrl: pluginConfig.chatWsUrl
379
+ });
380
+ if (chatWsUrl && apiKey) {
381
+ log.info(`Connecting to chat service: ${chatWsUrl}`);
382
+ chatClient = new ChatServiceClient({
383
+ wsUrl: chatWsUrl,
384
+ apiKey,
385
+ onRequest: (request) => {
386
+ if (request.method === "agent") handleAgentRequest(request, log);
387
+ },
388
+ onConnectionChange: (connected) => {
389
+ log.info(`Chat service connection: ${connected ? "connected" : "disconnected"}`);
390
+ },
391
+ logger: log
561
392
  });
562
- },
563
- onConnectionChange: (connected) => {
564
- log.info(`Chat service connection: ${connected ? "connected" : "disconnected"}`);
565
- },
566
- logger: log
567
- });
568
- runtimeRelay.start();
569
- chatClient.start();
570
- log.info("Chat service relay started");
571
- } else log.info("Chat service URL not configured — running without chat service relay");
393
+ chatClient.start();
394
+ log.info("Chat service relay started");
395
+ } else log.info("Chat service URL not configured running without chat service relay");
396
+ } catch (err) {
397
+ log.error(`Failed to initialize chat service: ${err instanceof Error ? err.message : String(err)}`);
398
+ }
399
+ })();
572
400
  if (typeof api.registerGatewayMethod === "function") {
573
- api.registerGatewayMethod("chat.send", (...args) => {
574
- const { sessionId, content, clientType } = args[0];
575
- log.info(`chat.send RPC: session=${sessionId}, client=${clientType ?? "unknown"}, content=${content.slice(0, 50)}...`);
576
- return Promise.resolve({
577
- ok: true,
578
- sessionId,
579
- channel: "alfe"
580
- });
581
- });
582
- log.info("Registered gateway RPC method: chat.send");
583
401
  api.registerGatewayMethod("sessions.list", async (...args) => {
584
402
  const params = args[0];
585
- log.info(`sessions.list RPC: agentId=${params.agentId ?? "*"}, channel=${params.channel ?? "*"}`);
586
403
  return { sessions: await listSessions({
587
404
  agentId: params.agentId,
588
405
  channel: params.channel,
589
406
  tenantId: params.tenantId
590
407
  }) };
591
408
  });
592
- log.info("Registered gateway RPC method: sessions.list");
593
409
  api.registerGatewayMethod("sessions.get", async (...args) => {
594
410
  const params = args[0];
595
- log.info(`sessions.get RPC: sessionId=${params.sessionId}`);
596
411
  const session = await getSession(params.sessionId);
597
412
  if (!session) return {
598
413
  ok: false,
@@ -611,30 +426,27 @@ const plugin = {
611
426
  }))
612
427
  };
613
428
  });
614
- log.info("Registered gateway RPC method: sessions.get");
429
+ log.info("Registered gateway RPC methods: sessions.list, sessions.get");
615
430
  }
616
431
  api.on("session_start", async (...eventArgs) => {
617
432
  const key = eventArgs[0].sessionKey;
618
- if (!key) return;
619
- if (!(key.startsWith("chat-") || key.includes("alfe:") || key.includes(":alfe:"))) return;
433
+ if (!key || !isAlfeSessionKey(key)) return;
620
434
  log.info(`Alfe chat session starting: ${key}`);
621
- const parts = key.split("-");
622
- 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);
623
437
  }, { priority: 50 });
624
438
  api.on("message", async (...eventArgs) => {
625
439
  const event = eventArgs[0];
626
440
  const key = event.sessionKey;
627
- if (!key) return;
628
- if (!(key.startsWith("chat-") || key.includes("alfe:") || key.includes(":alfe:"))) return;
441
+ if (!key || !isAlfeSessionKey(key)) return;
629
442
  await addMessage(key, event.role, event.content);
630
443
  });
631
444
  api.on("session_end", (...eventArgs) => {
632
445
  const key = eventArgs[0].sessionKey;
633
- if (!key) return;
634
- if (!(key.startsWith("chat-") || key.includes("alfe:") || key.includes(":alfe:"))) return;
446
+ if (!key || !isAlfeSessionKey(key)) return;
635
447
  log.info(`Alfe chat session ending: ${key}`);
636
448
  });
637
- log.info("Alfe Chat plugin activated");
449
+ log.info("Alfe Chat plugin registered");
638
450
  },
639
451
  deactivate(api) {
640
452
  globalThis.__alfeChatPluginActivated = false;
@@ -645,20 +457,7 @@ const plugin = {
645
457
  chatClient = null;
646
458
  log.info("Chat service client stopped");
647
459
  }
648
- if (runtimeRelay) {
649
- runtimeRelay.stop();
650
- runtimeRelay = null;
651
- log.info("Runtime relay stopped");
652
- }
653
- if (daemonIpcClient) {
654
- try {
655
- daemonIpcClient.stop();
656
- log.info("Disconnected from Alfe daemon");
657
- } catch (err) {
658
- log.debug(`Error disconnecting from daemon: ${err.message}`);
659
- }
660
- daemonIpcClient = null;
661
- }
460
+ pluginRuntime = null;
662
461
  log.info("Alfe Chat plugin deactivated");
663
462
  }
664
463
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-chat",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
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,12 +25,8 @@
25
25
  "openclaw.plugin.json"
26
26
  ],
27
27
  "dependencies": {
28
- "ws": "^8.18.0",
29
28
  "@alfe.ai/chat": "^0.0.2"
30
29
  },
31
- "devDependencies": {
32
- "@types/ws": "^8.5.13"
33
- },
34
30
  "license": "UNLICENSED",
35
31
  "scripts": {
36
32
  "build": "tsdown",