@echomem/echo-memory-cloud-openclaw-plugin 0.2.2 → 0.2.3

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.
@@ -2,7 +2,7 @@
2
2
  "id": "echo-memory-cloud-openclaw-plugin",
3
3
  "name": "Echo Memory Cloud OpenClaw Plugin",
4
4
  "description": "Sync OpenClaw local markdown memory files to Echo cloud",
5
- "version": "0.2.2",
5
+ "version": "0.2.3",
6
6
  "kind": "lifecycle",
7
7
  "main": "./index.js",
8
8
  "configSchema": {
package/index.js CHANGED
@@ -25,6 +25,25 @@ import {
25
25
 
26
26
  const LOCAL_UI_RECONNECT_GRACE_MS = 4000;
27
27
  const LOCAL_UI_PRESENCE_GRACE_MS = 75000;
28
+ const COMPAT_STARTUP_DELAY_MS = 1500;
29
+ const PROCESS_STATE_KEY = Symbol.for("echo-memory-cloud-openclaw-plugin.process-state");
30
+
31
+ function getProcessState() {
32
+ const globalState = globalThis;
33
+ if (!globalState[PROCESS_STATE_KEY]) {
34
+ globalState[PROCESS_STATE_KEY] = {
35
+ backgroundActive: false,
36
+ backgroundOwnerId: null,
37
+ backgroundStartPromise: null,
38
+ browserOpenAttempted: false,
39
+ browserOpenPromise: null,
40
+ compatFallbackScheduled: false,
41
+ serviceStartObserved: false,
42
+ stopBackground: null,
43
+ };
44
+ }
45
+ return globalState[PROCESS_STATE_KEY];
46
+ }
28
47
 
29
48
  function resolveCommandLabel(channel) {
30
49
  return channel === "discord" ? "/echomemory" : "/echo-memory";
@@ -53,6 +72,8 @@ export default {
53
72
  kind: "lifecycle",
54
73
 
55
74
  register(api) {
75
+ const processState = getProcessState();
76
+ const registrationId = Symbol("echo-memory-cloud-openclaw-plugin.register");
56
77
  const cfg = buildConfig(api.pluginConfig);
57
78
  const client = createApiClient(cfg);
58
79
  const workspaceDir = path.resolve(path.dirname(cfg.memoryDir), "..");
@@ -66,14 +87,52 @@ export default {
66
87
  fallbackStateDir: legacyPluginStateDir,
67
88
  stableStateDir,
68
89
  });
69
- let startupBrowserOpenAttempted = false;
70
- let backgroundStarted = false;
71
- let serviceStartObserved = false;
72
90
 
73
91
  if (!workspaceDir || workspaceDir === "." || workspaceDir === path.sep) {
74
92
  api.logger?.warn?.("[echo-memory] workspace resolution looks unusual; compatibility fallback may be limited");
75
93
  }
76
94
 
95
+ async function maybeAutoOpenLocalUi(url, { trigger = "manual" } = {}) {
96
+ const existingPageDetected = trigger === "gateway-start"
97
+ ? await hasRecentLocalUiPresence(syncRunner, { maxAgeMs: LOCAL_UI_PRESENCE_GRACE_MS })
98
+ : false;
99
+ const existingClientDetected = existingPageDetected || (
100
+ trigger === "gateway-start"
101
+ ? await waitForLocalUiClient({ timeoutMs: LOCAL_UI_RECONNECT_GRACE_MS })
102
+ : false
103
+ );
104
+ return existingClientDetected
105
+ ? { opened: false, reason: existingPageDetected ? "existing_page_detected" : "existing_client_reconnected" }
106
+ : openUrlInDefaultBrowser(url, {
107
+ logger: api.logger,
108
+ force: trigger !== "gateway-start",
109
+ });
110
+ }
111
+
112
+ async function maybeAutoOpenStartupBrowser(url) {
113
+ if (!cfg.localUiAutoOpenOnGatewayStart || processState.browserOpenAttempted) {
114
+ return { attempted: false, opened: false, reason: "disabled_or_already_attempted" };
115
+ }
116
+ if (processState.browserOpenPromise) {
117
+ const result = await processState.browserOpenPromise;
118
+ return { attempted: false, ...result };
119
+ }
120
+
121
+ const openPromise = maybeAutoOpenLocalUi(url, { trigger: "gateway-start" })
122
+ .then((result) => {
123
+ processState.browserOpenAttempted = true;
124
+ return result;
125
+ })
126
+ .finally(() => {
127
+ if (processState.browserOpenPromise === openPromise) {
128
+ processState.browserOpenPromise = null;
129
+ }
130
+ });
131
+ processState.browserOpenPromise = openPromise;
132
+ const result = await openPromise;
133
+ return { attempted: true, ...result };
134
+ }
135
+
77
136
  async function ensureLocalUi({ openInBrowser = false, trigger = "manual" } = {}) {
78
137
  const url = await startLocalServer(workspaceDir, {
79
138
  apiClient: client,
@@ -86,20 +145,7 @@ export default {
86
145
  let openedInBrowser = false;
87
146
  let openReason = "not_requested";
88
147
  if (openInBrowser) {
89
- const existingPageDetected = trigger === "gateway-start"
90
- ? await hasRecentLocalUiPresence(syncRunner, { maxAgeMs: LOCAL_UI_PRESENCE_GRACE_MS })
91
- : false;
92
- const existingClientDetected = existingPageDetected || (
93
- trigger === "gateway-start"
94
- ? await waitForLocalUiClient({ timeoutMs: LOCAL_UI_RECONNECT_GRACE_MS })
95
- : false
96
- );
97
- const openResult = existingClientDetected
98
- ? { opened: false, reason: existingPageDetected ? "existing_page_detected" : "existing_client_reconnected" }
99
- : await openUrlInDefaultBrowser(url, {
100
- logger: api.logger,
101
- force: trigger !== "gateway-start",
102
- });
148
+ const openResult = await maybeAutoOpenLocalUi(url, { trigger });
103
149
  openedInBrowser = openResult.opened;
104
150
  openReason = openResult.reason;
105
151
  }
@@ -170,68 +216,150 @@ export default {
170
216
  }
171
217
 
172
218
  async function startBackgroundFeatures({ stateDir = null, trigger = "service" } = {}) {
173
- if (backgroundStarted) {
174
- return;
175
- }
176
- backgroundStarted = true;
177
- await syncRunner.initialize(stateDir || legacyPluginStateDir);
178
-
179
- try {
180
- const shouldOpenBrowser = cfg.localUiAutoOpenOnGatewayStart && !startupBrowserOpenAttempted;
181
- const { url, openedInBrowser, openReason } = await ensureLocalUi({
182
- openInBrowser: shouldOpenBrowser,
183
- trigger: trigger === "service" ? "gateway-start" : trigger,
184
- });
185
- api.logger?.info?.(`[echo-memory] Local workspace viewer: ${url}`);
186
- if (shouldOpenBrowser) {
187
- startupBrowserOpenAttempted = true;
188
- if (openedInBrowser) {
189
- api.logger?.info?.("[echo-memory] Opened local workspace viewer in the default browser");
190
- } else {
191
- api.logger?.info?.(`[echo-memory] Skipped browser auto-open (${openReason})`);
219
+ const effectiveTrigger = trigger === "service" ? "gateway-start" : trigger;
220
+
221
+ if (processState.backgroundActive) {
222
+ if (effectiveTrigger === "gateway-start") {
223
+ const { attempted, opened, reason } = await maybeAutoOpenStartupBrowser(
224
+ await startLocalServer(workspaceDir, {
225
+ apiClient: client,
226
+ syncRunner,
227
+ cfg,
228
+ logger: api.logger,
229
+ pluginConfig: api.pluginConfig,
230
+ }),
231
+ );
232
+ if (attempted) {
233
+ if (opened) {
234
+ api.logger?.info?.("[echo-memory] Opened local workspace viewer in the default browser");
235
+ } else {
236
+ api.logger?.info?.(`[echo-memory] Skipped browser auto-open (${reason})`);
237
+ }
192
238
  }
193
239
  }
194
- } catch (error) {
195
- api.logger?.warn?.(`[echo-memory] local server failed: ${String(error?.message ?? error)}`);
240
+ return;
196
241
  }
197
242
 
198
- if (!cfg.autoSync) {
199
- api.logger?.info?.("[echo-memory] autoSync disabled");
243
+ if (processState.backgroundStartPromise) {
244
+ await processState.backgroundStartPromise;
245
+ if (effectiveTrigger === "gateway-start") {
246
+ const { attempted, opened, reason } = await maybeAutoOpenStartupBrowser(
247
+ await startLocalServer(workspaceDir, {
248
+ apiClient: client,
249
+ syncRunner,
250
+ cfg,
251
+ logger: api.logger,
252
+ pluginConfig: api.pluginConfig,
253
+ }),
254
+ );
255
+ if (attempted) {
256
+ if (opened) {
257
+ api.logger?.info?.("[echo-memory] Opened local workspace viewer in the default browser");
258
+ } else {
259
+ api.logger?.info?.(`[echo-memory] Skipped browser auto-open (${reason})`);
260
+ }
261
+ }
262
+ }
200
263
  return;
201
264
  }
202
265
 
203
- await syncRunner.runSync(trigger === "service" ? "startup" : trigger).catch((error) => {
204
- api.logger?.warn?.(`[echo-memory] startup sync failed: ${String(error?.message ?? error)}`);
205
- });
266
+ const startPromise = (async () => {
267
+ processState.backgroundOwnerId = registrationId;
268
+ processState.stopBackground = () => {
269
+ syncRunner.stopInterval();
270
+ stopLocalServer();
271
+ };
272
+ await syncRunner.initialize(stateDir || legacyPluginStateDir);
273
+
274
+ let url = null;
275
+ try {
276
+ const localUi = await ensureLocalUi({
277
+ openInBrowser: false,
278
+ trigger: effectiveTrigger,
279
+ });
280
+ url = localUi.url;
281
+ api.logger?.info?.(`[echo-memory] Local workspace viewer: ${url}`);
282
+ } catch (error) {
283
+ api.logger?.warn?.(`[echo-memory] local server failed: ${String(error?.message ?? error)}`);
284
+ }
285
+
286
+ processState.backgroundActive = true;
287
+
288
+ if (effectiveTrigger === "gateway-start" && url) {
289
+ const { attempted, opened, reason } = await maybeAutoOpenStartupBrowser(url);
290
+ if (attempted) {
291
+ if (opened) {
292
+ api.logger?.info?.("[echo-memory] Opened local workspace viewer in the default browser");
293
+ } else {
294
+ api.logger?.info?.(`[echo-memory] Skipped browser auto-open (${reason})`);
295
+ }
296
+ }
297
+ }
298
+
299
+ if (!cfg.autoSync) {
300
+ api.logger?.info?.("[echo-memory] autoSync disabled");
301
+ return;
302
+ }
303
+
304
+ await syncRunner.runSync(trigger === "service" ? "startup" : trigger).catch((error) => {
305
+ api.logger?.warn?.(`[echo-memory] startup sync failed: ${String(error?.message ?? error)}`);
306
+ });
206
307
 
207
- syncRunner.startInterval();
308
+ syncRunner.startInterval();
309
+ })()
310
+ .catch((error) => {
311
+ if (processState.backgroundOwnerId === registrationId) {
312
+ processState.backgroundActive = false;
313
+ processState.backgroundOwnerId = null;
314
+ processState.stopBackground = null;
315
+ }
316
+ throw error;
317
+ })
318
+ .finally(() => {
319
+ if (processState.backgroundStartPromise === startPromise) {
320
+ processState.backgroundStartPromise = null;
321
+ }
322
+ });
323
+
324
+ processState.backgroundStartPromise = startPromise;
325
+ await startPromise;
208
326
  }
209
327
 
210
328
  if (typeof api.registerService === "function") {
211
329
  api.registerService({
212
330
  id: "echo-memory-cloud-openclaw-sync",
213
331
  start: async (ctx) => {
214
- serviceStartObserved = true;
332
+ processState.serviceStartObserved = true;
215
333
  await startBackgroundFeatures({ stateDir: ctx?.stateDir || null, trigger: "service" });
216
334
  },
217
335
  stop: async () => {
218
- syncRunner.stopInterval();
219
- stopLocalServer();
220
- backgroundStarted = false;
336
+ processState.stopBackground?.();
337
+ processState.backgroundActive = false;
338
+ processState.backgroundOwnerId = null;
339
+ processState.backgroundStartPromise = null;
340
+ processState.browserOpenAttempted = false;
341
+ processState.browserOpenPromise = null;
342
+ processState.compatFallbackScheduled = false;
343
+ processState.serviceStartObserved = false;
344
+ processState.stopBackground = null;
221
345
  },
222
346
  });
223
347
  }
224
348
 
225
349
  // Compatibility fallback for older hosts that discover the plugin but do not
226
350
  // reliably auto-start registered background services.
227
- queueMicrotask(() => {
228
- if (serviceStartObserved) {
229
- return;
230
- }
231
- startBackgroundFeatures({ stateDir: legacyPluginStateDir, trigger: "compat-startup" }).catch((error) => {
232
- api.logger?.warn?.(`[echo-memory] compatibility startup failed: ${String(error?.message ?? error)}`);
233
- });
234
- });
351
+ if (!processState.compatFallbackScheduled) {
352
+ processState.compatFallbackScheduled = true;
353
+ setTimeout(() => {
354
+ processState.compatFallbackScheduled = false;
355
+ if (processState.serviceStartObserved || processState.backgroundActive || processState.backgroundStartPromise) {
356
+ return;
357
+ }
358
+ startBackgroundFeatures({ stateDir: legacyPluginStateDir, trigger: "compat-startup" }).catch((error) => {
359
+ api.logger?.warn?.(`[echo-memory] compatibility startup failed: ${String(error?.message ?? error)}`);
360
+ });
361
+ }, COMPAT_STARTUP_DELAY_MS);
362
+ }
235
363
 
236
364
  if (typeof api.registerCommand === "function") {
237
365
  api.registerCommand({
@@ -2,7 +2,7 @@
2
2
  "id": "echo-memory-cloud-openclaw-plugin",
3
3
  "name": "Echo Memory Cloud OpenClaw Plugin",
4
4
  "description": "Sync OpenClaw local markdown memory files to Echo cloud",
5
- "version": "0.2.2",
5
+ "version": "0.2.3",
6
6
  "kind": "lifecycle",
7
7
  "main": "./index.js",
8
8
  "configSchema": {
@@ -2,7 +2,7 @@
2
2
  "id": "echo-memory-cloud-openclaw-plugin",
3
3
  "name": "Echo Memory Cloud OpenClaw Plugin",
4
4
  "description": "Sync OpenClaw local markdown memory files to Echo cloud",
5
- "version": "0.2.2",
5
+ "version": "0.2.3",
6
6
  "kind": "lifecycle",
7
7
  "main": "./index.js",
8
8
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/echo-memory-cloud-openclaw-plugin",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "OpenClaw plugin for syncing local markdown memory files to Echo cloud",
5
5
  "type": "module",
6
6
  "main": "./index.js",