@phronesis-io/openclaw-eigenflux 0.0.1 → 0.0.2

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/src/logger.ts DELETED
@@ -1,27 +0,0 @@
1
- /**
2
- * Logger wrapper for consistent logging
3
- */
4
-
5
- export class Logger {
6
- private baseLogger: any;
7
-
8
- constructor(baseLogger: any) {
9
- this.baseLogger = baseLogger;
10
- }
11
-
12
- info(message: string, ...args: any[]): void {
13
- this.baseLogger?.info(`[EigenFlux] ${message}`, ...args);
14
- }
15
-
16
- warn(message: string, ...args: any[]): void {
17
- this.baseLogger?.warn(`[EigenFlux] ${message}`, ...args);
18
- }
19
-
20
- error(message: string, ...args: any[]): void {
21
- this.baseLogger?.error(`[EigenFlux] ${message}`, ...args);
22
- }
23
-
24
- debug(message: string, ...args: any[]): void {
25
- this.baseLogger?.debug?.(`[EigenFlux] ${message}`, ...args);
26
- }
27
- }
@@ -1,136 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as os from 'os';
3
- import * as path from 'path';
4
- import { Logger } from './logger';
5
- import { resolveNotificationRoute } from './notification-route-resolver';
6
- import { writeStoredNotificationRoute } from './session-route-memory';
7
-
8
- function createLogger(): Logger {
9
- return new Logger({
10
- info: jest.fn(),
11
- warn: jest.fn(),
12
- error: jest.fn(),
13
- debug: jest.fn(),
14
- });
15
- }
16
-
17
- describe('resolveNotificationRoute', () => {
18
- test('prefers remembered session route over dynamically fresher session when config is automatic', () => {
19
- const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'eigenflux-route-memory-'));
20
- const sessionStorePath = path.join(workdir, 'sessions.json');
21
-
22
- fs.writeFileSync(
23
- sessionStorePath,
24
- JSON.stringify({
25
- 'agent:main:main': {
26
- updatedAt: 100,
27
- deliveryContext: { channel: 'webchat' },
28
- },
29
- 'agent:main:feishu:group:oc_newer': {
30
- updatedAt: 400,
31
- deliveryContext: {
32
- channel: 'feishu',
33
- to: 'chat:oc_newer',
34
- accountId: 'default',
35
- },
36
- },
37
- 'agent:mengtian:feishu:direct:ou_saved': {
38
- updatedAt: 200,
39
- deliveryContext: {
40
- channel: 'feishu',
41
- to: 'user:ou_saved',
42
- accountId: 'default',
43
- },
44
- },
45
- }),
46
- 'utf-8'
47
- );
48
-
49
- writeStoredNotificationRoute(
50
- workdir,
51
- {
52
- sessionKey: 'agent:mengtian:feishu:direct:ou_saved',
53
- agentId: 'mengtian',
54
- replyChannel: 'feishu',
55
- replyTo: 'user:ou_saved',
56
- replyAccountId: 'default',
57
- },
58
- createLogger()
59
- );
60
-
61
- const route = resolveNotificationRoute(
62
- {
63
- sessionKey: 'main',
64
- agentId: 'main',
65
- sessionStorePath,
66
- workdir,
67
- routeOverrides: {
68
- sessionKey: false,
69
- agentId: false,
70
- replyChannel: false,
71
- replyTo: false,
72
- replyAccountId: false,
73
- },
74
- },
75
- createLogger()
76
- );
77
-
78
- expect(route).toEqual({
79
- sessionKey: 'agent:mengtian:feishu:direct:ou_saved',
80
- agentId: 'mengtian',
81
- replyChannel: 'feishu',
82
- replyTo: 'user:ou_saved',
83
- replyAccountId: 'default',
84
- });
85
-
86
- fs.rmSync(workdir, { recursive: true, force: true });
87
- });
88
-
89
- test('respects explicit route overrides while still enriching exact session metadata', () => {
90
- const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'eigenflux-route-explicit-'));
91
- const sessionStorePath = path.join(workdir, 'sessions.json');
92
-
93
- fs.writeFileSync(
94
- sessionStorePath,
95
- JSON.stringify({
96
- 'agent:main:feishu:direct:ou_explicit': {
97
- updatedAt: 300,
98
- deliveryContext: {
99
- channel: 'feishu',
100
- to: 'user:ou_explicit',
101
- accountId: 'default',
102
- },
103
- },
104
- }),
105
- 'utf-8'
106
- );
107
-
108
- const route = resolveNotificationRoute(
109
- {
110
- sessionKey: 'agent:main:feishu:direct:ou_explicit',
111
- agentId: 'main',
112
- replyChannel: 'feishu',
113
- sessionStorePath,
114
- workdir,
115
- routeOverrides: {
116
- sessionKey: true,
117
- agentId: true,
118
- replyChannel: true,
119
- replyTo: false,
120
- replyAccountId: false,
121
- },
122
- },
123
- createLogger()
124
- );
125
-
126
- expect(route).toEqual({
127
- sessionKey: 'agent:main:feishu:direct:ou_explicit',
128
- agentId: 'main',
129
- replyChannel: 'feishu',
130
- replyTo: 'user:ou_explicit',
131
- replyAccountId: 'default',
132
- });
133
-
134
- fs.rmSync(workdir, { recursive: true, force: true });
135
- });
136
- });
@@ -1,430 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as os from 'os';
3
- import * as path from 'path';
4
- import { type NotificationRouteOverrides } from './config';
5
- import { Logger } from './logger';
6
- import { readStoredNotificationRoute } from './session-route-memory';
7
-
8
- const INTERNAL_CHANNELS = new Set(['webchat']);
9
-
10
- function getDefaultOpenClawStateDir(): string {
11
- return path.join(os.homedir(), '.openclaw');
12
- }
13
-
14
- type DeliveryContextLike = {
15
- channel?: unknown;
16
- to?: unknown;
17
- accountId?: unknown;
18
- };
19
-
20
- type SessionOriginLike = {
21
- provider?: unknown;
22
- to?: unknown;
23
- accountId?: unknown;
24
- };
25
-
26
- type SessionStoreEntry = {
27
- updatedAt?: unknown;
28
- deliveryContext?: DeliveryContextLike;
29
- lastTo?: unknown;
30
- lastAccountId?: unknown;
31
- origin?: SessionOriginLike;
32
- };
33
-
34
- type SessionStoreSnapshot = {
35
- path: string;
36
- store: Record<string, SessionStoreEntry>;
37
- };
38
-
39
- export type NotificationRouteConfig = {
40
- sessionKey: string;
41
- agentId: string;
42
- replyChannel?: string;
43
- replyTo?: string;
44
- replyAccountId?: string;
45
- sessionStorePath?: string;
46
- workdir?: string;
47
- routeOverrides?: NotificationRouteOverrides;
48
- };
49
-
50
- export type ResolvedNotificationRoute = {
51
- sessionKey: string;
52
- agentId: string;
53
- replyChannel?: string;
54
- replyTo?: string;
55
- replyAccountId?: string;
56
- };
57
-
58
- type PreferredRoute = {
59
- channel?: string;
60
- to?: string;
61
- accountId?: string;
62
- };
63
-
64
- type RouteSelection = {
65
- route: ResolvedNotificationRoute;
66
- updatedAt: number;
67
- };
68
-
69
- function readNonEmptyString(value: unknown): string | undefined {
70
- if (typeof value !== 'string') {
71
- return undefined;
72
- }
73
- const trimmed = value.trim();
74
- return trimmed.length > 0 ? trimmed : undefined;
75
- }
76
-
77
- function normalizeChannel(value: unknown): string | undefined {
78
- return readNonEmptyString(value)?.toLowerCase();
79
- }
80
-
81
- function normalizeUpdatedAt(value: unknown): number {
82
- if (typeof value === 'number' && Number.isFinite(value)) {
83
- return value;
84
- }
85
- return 0;
86
- }
87
-
88
- function createRouteOverrides(
89
- overrides: NotificationRouteConfig['routeOverrides']
90
- ): NotificationRouteOverrides {
91
- return {
92
- sessionKey: overrides?.sessionKey === true,
93
- agentId: overrides?.agentId === true,
94
- replyChannel: overrides?.replyChannel === true,
95
- replyTo: overrides?.replyTo === true,
96
- replyAccountId: overrides?.replyAccountId === true,
97
- };
98
- }
99
-
100
- function isAnyRouteOverrideEnabled(overrides: NotificationRouteOverrides): boolean {
101
- return Object.values(overrides).some(Boolean);
102
- }
103
-
104
- function isInternalSessionKey(sessionKey: string): boolean {
105
- const trimmed = readNonEmptyString(sessionKey);
106
- if (!trimmed) {
107
- return true;
108
- }
109
- if (trimmed === 'main') {
110
- return true;
111
- }
112
-
113
- const parts = trimmed.split(':').filter((part) => part.length > 0);
114
- return parts[0]?.toLowerCase() === 'agent' && parts[2]?.toLowerCase() === 'main';
115
- }
116
-
117
- function isExternalChannel(channel: string | undefined): boolean {
118
- return Boolean(channel && !INTERNAL_CHANNELS.has(channel));
119
- }
120
-
121
- function routeTargetMatches(actual: string | undefined, expected: string | undefined): boolean {
122
- if (!expected) {
123
- return true;
124
- }
125
- if (!actual) {
126
- return false;
127
- }
128
- return actual === expected || actual.endsWith(`:${expected}`) || expected.endsWith(`:${actual}`);
129
- }
130
-
131
- function routeMatchesPreferred(
132
- route: ResolvedNotificationRoute,
133
- preferred: PreferredRoute | undefined
134
- ): boolean {
135
- if (!preferred) {
136
- return true;
137
- }
138
- if (preferred.channel && route.replyChannel !== preferred.channel) {
139
- return false;
140
- }
141
- if (!routeTargetMatches(route.replyTo, preferred.to)) {
142
- return false;
143
- }
144
- if (preferred.accountId && route.replyAccountId !== preferred.accountId) {
145
- return false;
146
- }
147
- return true;
148
- }
149
-
150
- function deriveAgentIdFromSessionKey(sessionKey: string | undefined): string | undefined {
151
- const trimmed = readNonEmptyString(sessionKey);
152
- if (!trimmed) {
153
- return undefined;
154
- }
155
- const parts = trimmed.split(':').filter((part) => part.length > 0);
156
- if (parts[0]?.toLowerCase() !== 'agent') {
157
- return undefined;
158
- }
159
- return readNonEmptyString(parts[1]);
160
- }
161
-
162
- function extractExternalRoute(
163
- sessionKey: string,
164
- entry: SessionStoreEntry | undefined
165
- ): ResolvedNotificationRoute | undefined {
166
- if (!entry) {
167
- return undefined;
168
- }
169
-
170
- const replyChannel =
171
- normalizeChannel(entry.deliveryContext?.channel) ?? normalizeChannel(entry.origin?.provider);
172
- const replyTo =
173
- readNonEmptyString(entry.deliveryContext?.to) ??
174
- readNonEmptyString(entry.lastTo) ??
175
- readNonEmptyString(entry.origin?.to);
176
- const replyAccountId =
177
- readNonEmptyString(entry.deliveryContext?.accountId) ??
178
- readNonEmptyString(entry.lastAccountId) ??
179
- readNonEmptyString(entry.origin?.accountId);
180
-
181
- if (!isExternalChannel(replyChannel) || !replyTo) {
182
- return undefined;
183
- }
184
-
185
- return {
186
- sessionKey,
187
- agentId: deriveAgentIdFromSessionKey(sessionKey) ?? 'main',
188
- replyChannel,
189
- replyTo,
190
- replyAccountId,
191
- };
192
- }
193
-
194
- function tryDeriveAgentIdFromStorePath(sessionStorePath: string): string | undefined {
195
- const normalized = path.normalize(sessionStorePath);
196
- const parts = normalized.split(path.sep).filter(Boolean);
197
- const agentsIndex = parts.lastIndexOf('agents');
198
- if (agentsIndex === -1) {
199
- return undefined;
200
- }
201
- return readNonEmptyString(parts[agentsIndex + 1]);
202
- }
203
-
204
- function listSessionStorePaths(explicitPath: string | undefined, baseAgentId: string): string[] {
205
- const candidates: string[] = [];
206
- const seen = new Set<string>();
207
-
208
- const addPath = (candidate: string | undefined) => {
209
- const trimmed = readNonEmptyString(candidate);
210
- if (!trimmed) {
211
- return;
212
- }
213
- const normalized = path.normalize(trimmed);
214
- if (seen.has(normalized)) {
215
- return;
216
- }
217
- seen.add(normalized);
218
- candidates.push(normalized);
219
- };
220
-
221
- addPath(explicitPath);
222
- if (candidates.length > 0) {
223
- return candidates;
224
- }
225
-
226
- const defaultOpenClawStateDir = getDefaultOpenClawStateDir();
227
- addPath(path.join(defaultOpenClawStateDir, 'agents', baseAgentId, 'sessions', 'sessions.json'));
228
-
229
- const agentsRoot = path.join(defaultOpenClawStateDir, 'agents');
230
- try {
231
- if (fs.existsSync(agentsRoot)) {
232
- for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
233
- if (!entry.isDirectory()) {
234
- continue;
235
- }
236
- addPath(path.join(agentsRoot, entry.name, 'sessions', 'sessions.json'));
237
- }
238
- }
239
- } catch {
240
- // Ignore directory scan failures; we will still try explicit/default paths.
241
- }
242
-
243
- return candidates;
244
- }
245
-
246
- function readSessionStore(
247
- sessionStorePath: string,
248
- logger: Logger
249
- ): Record<string, SessionStoreEntry> | undefined {
250
- try {
251
- if (!fs.existsSync(sessionStorePath)) {
252
- return undefined;
253
- }
254
- const raw = fs.readFileSync(sessionStorePath, 'utf-8');
255
- const parsed = JSON.parse(raw) as unknown;
256
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
257
- return undefined;
258
- }
259
- return parsed as Record<string, SessionStoreEntry>;
260
- } catch (error) {
261
- logger.debug(
262
- `Failed to read session store ${sessionStorePath}: ${error instanceof Error ? error.message : String(error)}`
263
- );
264
- return undefined;
265
- }
266
- }
267
-
268
- function readSessionStores(
269
- sessionStorePath: string | undefined,
270
- baseAgentId: string,
271
- logger: Logger
272
- ): SessionStoreSnapshot[] {
273
- const snapshots: SessionStoreSnapshot[] = [];
274
- for (const candidate of listSessionStorePaths(sessionStorePath, baseAgentId)) {
275
- const store = readSessionStore(candidate, logger);
276
- if (store) {
277
- snapshots.push({ path: candidate, store });
278
- }
279
- }
280
- return snapshots;
281
- }
282
-
283
- function buildPreferredRoute(route: ResolvedNotificationRoute): PreferredRoute | undefined {
284
- if (!route.replyChannel && !route.replyTo && !route.replyAccountId) {
285
- return undefined;
286
- }
287
- return {
288
- channel: route.replyChannel,
289
- to: route.replyTo,
290
- accountId: route.replyAccountId,
291
- };
292
- }
293
-
294
- function mergeRoute(
295
- base: ResolvedNotificationRoute,
296
- resolved: ResolvedNotificationRoute,
297
- overrides: NotificationRouteOverrides,
298
- allowSessionOverride: boolean
299
- ): ResolvedNotificationRoute {
300
- const nextSessionKey =
301
- allowSessionOverride && !overrides.sessionKey ? resolved.sessionKey : base.sessionKey;
302
-
303
- return {
304
- sessionKey: nextSessionKey,
305
- agentId:
306
- overrides.agentId === true
307
- ? base.agentId
308
- : resolved.agentId ?? deriveAgentIdFromSessionKey(nextSessionKey) ?? base.agentId,
309
- replyChannel:
310
- overrides.replyChannel === true ? base.replyChannel : resolved.replyChannel ?? base.replyChannel,
311
- replyTo: overrides.replyTo === true ? base.replyTo : resolved.replyTo ?? base.replyTo,
312
- replyAccountId:
313
- overrides.replyAccountId === true
314
- ? base.replyAccountId
315
- : resolved.replyAccountId ?? base.replyAccountId,
316
- };
317
- }
318
-
319
- function selectExactRoute(
320
- snapshots: SessionStoreSnapshot[],
321
- sessionKey: string
322
- ): RouteSelection | undefined {
323
- let best: RouteSelection | undefined;
324
-
325
- for (const snapshot of snapshots) {
326
- const entry = snapshot.store[sessionKey];
327
- const route = extractExternalRoute(sessionKey, entry);
328
- if (!route) {
329
- continue;
330
- }
331
- const updatedAt = normalizeUpdatedAt(entry?.updatedAt);
332
- if (!best || updatedAt > best.updatedAt) {
333
- best = { route, updatedAt };
334
- }
335
- }
336
-
337
- return best;
338
- }
339
-
340
- function selectLatestExternalRoute(
341
- snapshots: SessionStoreSnapshot[],
342
- preferred: PreferredRoute | undefined,
343
- preferredAgentId?: string
344
- ): RouteSelection | undefined {
345
- let best: RouteSelection | undefined;
346
-
347
- for (const snapshot of snapshots) {
348
- const pathAgentId = tryDeriveAgentIdFromStorePath(snapshot.path);
349
- for (const [sessionKey, entry] of Object.entries(snapshot.store)) {
350
- if (sessionKey.includes(':subagent:')) {
351
- continue;
352
- }
353
-
354
- const route = extractExternalRoute(sessionKey, entry);
355
- if (!route || !routeMatchesPreferred(route, preferred)) {
356
- continue;
357
- }
358
-
359
- if (preferredAgentId && route.agentId !== preferredAgentId && pathAgentId !== preferredAgentId) {
360
- continue;
361
- }
362
-
363
- const updatedAt = normalizeUpdatedAt(entry.updatedAt);
364
- if (!best || updatedAt > best.updatedAt) {
365
- best = { route, updatedAt };
366
- }
367
- }
368
- }
369
-
370
- return best;
371
- }
372
-
373
- export function resolveNotificationRoute(
374
- config: NotificationRouteConfig,
375
- logger: Logger
376
- ): ResolvedNotificationRoute {
377
- const overrides = createRouteOverrides(config.routeOverrides);
378
-
379
- let resolved: ResolvedNotificationRoute = {
380
- sessionKey: readNonEmptyString(config.sessionKey) ?? 'main',
381
- agentId:
382
- readNonEmptyString(config.agentId) ??
383
- deriveAgentIdFromSessionKey(config.sessionKey) ??
384
- 'main',
385
- replyChannel: normalizeChannel(config.replyChannel),
386
- replyTo: readNonEmptyString(config.replyTo),
387
- replyAccountId: readNonEmptyString(config.replyAccountId),
388
- };
389
-
390
- const snapshots = readSessionStores(config.sessionStorePath, resolved.agentId, logger);
391
-
392
- const exactRoute = selectExactRoute(snapshots, resolved.sessionKey);
393
- if (exactRoute) {
394
- resolved = mergeRoute(resolved, exactRoute.route, overrides, false);
395
- }
396
-
397
- if (!isAnyRouteOverrideEnabled(overrides)) {
398
- const rememberedRoute = readStoredNotificationRoute(config.workdir, logger);
399
- if (rememberedRoute) {
400
- resolved = mergeRoute(resolved, rememberedRoute, overrides, true);
401
- const rememberedExact = selectExactRoute(snapshots, resolved.sessionKey);
402
- if (rememberedExact) {
403
- resolved = mergeRoute(resolved, rememberedExact.route, overrides, false);
404
- }
405
- logger.debug(
406
- `Resolved notification route via remembered session: session_key=${resolved.sessionKey}, channel=${resolved.replyChannel ?? 'unknown'}, to=${resolved.replyTo ?? 'unknown'}`
407
- );
408
- }
409
- }
410
-
411
- if (!isInternalSessionKey(resolved.sessionKey)) {
412
- return resolved;
413
- }
414
-
415
- const preferred = buildPreferredRoute(resolved);
416
- const latestRoute = selectLatestExternalRoute(
417
- snapshots,
418
- preferred,
419
- overrides.agentId || overrides.sessionKey ? resolved.agentId : undefined
420
- );
421
- if (!latestRoute) {
422
- return resolved;
423
- }
424
-
425
- resolved = mergeRoute(resolved, latestRoute.route, overrides, true);
426
- logger.debug(
427
- `Resolved notification route via session store: session_key=${resolved.sessionKey}, channel=${resolved.replyChannel ?? 'unknown'}, to=${resolved.replyTo ?? 'unknown'}`
428
- );
429
- return resolved;
430
- }