@keychat-io/keychat-openclaw 0.1.16 → 0.1.17

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "keychat-openclaw",
3
- "channels": ["keychat"],
3
+ "channels": ["keychat-openclaw"],
4
4
  "name": "Keychat",
5
5
  "description": "Sovereign identity + E2E encrypted chat via Signal Protocol over Nostr relays. Lightning wallet support via LNURL-pay and NWC.",
6
6
  "version": "0.1.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keychat-io/keychat-openclaw",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Keychat — E2E encrypted chat + Lightning wallet for OpenClaw agents",
5
5
  "license": "AGPL-3.0",
6
6
  "repository": {
@@ -96,7 +96,7 @@ try {
96
96
  // Don't fail install — user can build manually
97
97
  }
98
98
 
99
- // Auto-initialize config if channels.keychat not set
99
+ // Auto-initialize config if channels["keychat-openclaw"] not set
100
100
  import { homedir } from "node:os";
101
101
 
102
102
  const configPath = join(homedir(), ".openclaw", "openclaw.json");
@@ -106,16 +106,23 @@ try {
106
106
  config = JSON.parse(readFileSync(configPath, "utf-8"));
107
107
  }
108
108
 
109
- if (config.channels?.keychat) {
109
+ if (config.channels?.["keychat-openclaw"] || config.channels?.keychat) {
110
110
  console.log("[keychat] Config already contains keychat settings, skipping init");
111
+ // Migrate old channels.keychat → channels.keychat-openclaw
112
+ if (config.channels?.keychat && !config.channels?.["keychat-openclaw"]) {
113
+ config.channels["keychat-openclaw"] = config.channels.keychat;
114
+ delete config.channels.keychat;
115
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
116
+ console.log("[keychat] ✅ Migrated channels.keychat → channels.keychat-openclaw");
117
+ }
111
118
  } else {
112
119
  if (!config.channels) config.channels = {};
113
- config.channels.keychat = { enabled: true, dmPolicy: "open" };
120
+ config.channels["keychat-openclaw"] = { enabled: true, dmPolicy: "open" };
114
121
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
115
- console.log("[keychat] ✅ Config initialized (channels.keychat.enabled = true)");
122
+ console.log('[keychat] ✅ Config initialized (channels.keychat-openclaw.enabled = true)');
116
123
  console.log("[keychat] Restart gateway to activate: openclaw gateway restart");
117
124
  }
118
125
  } catch (err) {
119
126
  console.warn(`[keychat] Could not auto-configure: ${err.message}`);
120
- console.warn("[keychat] Run manually: openclaw config set channels.keychat.enabled true");
127
+ console.warn('[keychat] Run manually: openclaw config set channels.keychat-openclaw.enabled true');
121
128
  }
@@ -252,6 +252,15 @@ export class KeychatBridgeClient {
252
252
  setTimeout(() => reject(new Error("health check timeout")), this.HEALTH_CHECK_TIMEOUT_MS),
253
253
  );
254
254
  await Promise.race([pingPromise, timeoutPromise]);
255
+ // Also check relay connectivity and auto-reconnect if needed
256
+ try {
257
+ const result = await this.call("relay_health_check") as { reconnected?: boolean };
258
+ if (result?.reconnected) {
259
+ console.log(`[keychat-openclaw] Relay health check: reconnected and resubscribed`);
260
+ }
261
+ } catch (relayErr) {
262
+ console.warn(`[keychat-openclaw] Relay health check failed: ${relayErr}`);
263
+ }
255
264
  } catch {
256
265
  console.error(`[keychat-openclaw] Health check failed — killing stale process`);
257
266
  try { this.process?.kill(); } catch { /* ignore */ }
package/src/channel.ts CHANGED
@@ -54,6 +54,12 @@ const pendingOutbound: PendingMessage[] = [];
54
54
  const MAX_PENDING_QUEUE = 100;
55
55
  const MAX_MESSAGE_RETRIES = 5;
56
56
 
57
+ // ═══════════════════════════════════════════════════════════════════════════
58
+ // Per-account startup mutex — prevents concurrent startAccount from corrupting state
59
+ // during rapid hot-reloads (e.g. two config changes <1s apart)
60
+ // ═══════════════════════════════════════════════════════════════════════════
61
+ const accountStartupLocks = new Map<string, Promise<void>>();
62
+
57
63
  // ═══════════════════════════════════════════════════════════════════════════
58
64
  // Pending hello messages — queued while waiting for session establishment
59
65
  // ═══════════════════════════════════════════════════════════════════════════
@@ -327,9 +333,9 @@ function bech32Decode(npub: string): string | null {
327
333
  }
328
334
 
329
335
  export const keychatPlugin: ChannelPlugin<ResolvedKeychatAccount> = {
330
- id: "keychat",
336
+ id: "keychat-openclaw",
331
337
  meta: {
332
- id: "keychat",
338
+ id: "keychat-openclaw",
333
339
  label: "Keychat",
334
340
  selectionLabel: "Keychat (E2E Encrypted)",
335
341
  docsPath: "/channels/keychat",
@@ -691,6 +697,18 @@ export const keychatPlugin: ChannelPlugin<ResolvedKeychatAccount> = {
691
697
  const runtime = getKeychatRuntime();
692
698
  const account = ctx.account;
693
699
 
700
+ // Serialize startAccount calls for the same account — wait for any in-flight
701
+ // startup/cleanup to finish before proceeding (prevents hot-reload race conditions)
702
+ const prevLock = accountStartupLocks.get(account.accountId);
703
+ if (prevLock) {
704
+ ctx.log?.info(`[${account.accountId}] Waiting for previous startup to finish...`);
705
+ await prevLock.catch(() => {}); // ignore errors from previous run
706
+ }
707
+
708
+ let lockResolve: () => void;
709
+ const lock = new Promise<void>((resolve) => { lockResolve = resolve; });
710
+ accountStartupLocks.set(account.accountId, lock);
711
+
694
712
  ctx.log?.info(`[${account.accountId}] Starting Keychat channel...`);
695
713
 
696
714
  // Clean up any existing bridge from a previous start
@@ -1139,6 +1157,10 @@ export const keychatPlugin: ChannelPlugin<ResolvedKeychatAccount> = {
1139
1157
  bridgeReadyPromises.delete(account.accountId);
1140
1158
  }
1141
1159
 
1160
+ // Release startup lock — initialization is complete, bridge is ready
1161
+ lockResolve!();
1162
+ ctx.log?.info(`[${account.accountId}] Startup lock released — channel fully initialized`);
1163
+
1142
1164
  // Keep the channel alive until abortSignal fires (OpenClaw expects startAccount
1143
1165
  // to stay pending while the channel is running — resolving triggers auto-restart)
1144
1166
  const abortSignal = (ctx as any).abortSignal as AbortSignal | undefined;
@@ -1152,15 +1174,29 @@ export const keychatPlugin: ChannelPlugin<ResolvedKeychatAccount> = {
1152
1174
  await new Promise<void>(() => {});
1153
1175
  }
1154
1176
 
1155
- // Cleanup on abort
1177
+ // Cleanup on abort — clear ALL module-level state for this account
1178
+ // to prevent stale data leaking into the next startAccount call
1156
1179
  bridge.disableAutoRestart();
1157
1180
  await bridge.disconnect();
1158
1181
  await bridge.stop();
1182
+ // Clear peerSubscribedAddresses entries for this account's peers BEFORE deleting peer sessions
1183
+ const peersToClean = peerSessionsByAccount.get(account.accountId);
1184
+ if (peersToClean) {
1185
+ for (const [peerPk] of peersToClean) {
1186
+ peerSubscribedAddresses.delete(peerPk);
1187
+ }
1188
+ }
1189
+ // Now clear all per-account state so next startup reads fresh from DB
1159
1190
  activeBridges.delete(account.accountId);
1160
1191
  accountInfoCache.delete(account.accountId);
1161
1192
  bridgeReadyPromises.delete(account.accountId);
1162
1193
  bridgeReadyResolvers.delete(account.accountId);
1163
- ctx.log?.info(`[${account.accountId}] Keychat provider stopped`);
1194
+ accountStartupLocks.delete(account.accountId);
1195
+ peerSessionsByAccount.delete(account.accountId);
1196
+ addressToPeerByAccount.delete(account.accountId);
1197
+ seenEventIdsByAccount.delete(account.accountId);
1198
+ mlsInitialized.delete(account.accountId);
1199
+ ctx.log?.info(`[${account.accountId}] Keychat provider stopped (all state cleared)`);
1164
1200
  },
1165
1201
  },
1166
1202
  };