@xfxstudio/claworld 0.2.21 → 0.2.23-beta.0

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.
@@ -8,7 +8,7 @@
8
8
  ],
9
9
  "name": "Claworld Persona Relay",
10
10
  "description": "Claworld relay world channel plugin for OpenClaw.",
11
- "version": "0.2.21",
11
+ "version": "0.2.23-beta.0",
12
12
  "configSchema": {
13
13
  "type": "object",
14
14
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfxstudio/claworld",
3
- "version": "0.2.21",
3
+ "version": "0.2.23-beta.0",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -17,6 +17,10 @@ import {
17
17
  resolveClaworldRuntimeConfig,
18
18
  validateClaworldChannelConfig,
19
19
  } from './config-schema.js';
20
+ import {
21
+ loadClaworldRuntimeBackup,
22
+ persistClaworldRuntimeBackup,
23
+ } from './runtime-backup.js';
20
24
  import {
21
25
  claworldOnboardingAdapter,
22
26
  claworldSetupAdapter,
@@ -1324,6 +1328,30 @@ function createDeliveryReplyDispatcher({
1324
1328
  runtimeOutputSummary.counts[kind] += 1;
1325
1329
  };
1326
1330
 
1331
+ const submitRelayReply = async (replyText) => {
1332
+ if (typeof relayClient?.submitDeliveryReply !== 'function') {
1333
+ throw new Error('relay client does not support reply submission');
1334
+ }
1335
+ return await relayClient.submitDeliveryReply({
1336
+ deliveryId,
1337
+ sessionKey,
1338
+ replyText,
1339
+ source: 'openclaw-autochain',
1340
+ });
1341
+ };
1342
+
1343
+ const submitRelayKeptSilent = async (reason) => {
1344
+ if (typeof relayClient?.submitDeliveryKeptSilent !== 'function') {
1345
+ throw new Error('relay client does not support kept_silent submission');
1346
+ }
1347
+ return await relayClient.submitDeliveryKeptSilent({
1348
+ deliveryId,
1349
+ sessionKey,
1350
+ reason,
1351
+ source: 'openclaw-autochain',
1352
+ });
1353
+ };
1354
+
1327
1355
  const flushReply = async (text) => {
1328
1356
  const normalized = String(text || '').trim();
1329
1357
  if (!normalized || replied || suppressed) return false;
@@ -1331,25 +1359,9 @@ function createDeliveryReplyDispatcher({
1331
1359
  suppressed = true;
1332
1360
  return false;
1333
1361
  }
1334
- if (typeof relayClient.sendReplyAndWaitForAck === 'function') {
1335
- const replyResult = await relayClient.sendReplyAndWaitForAck({
1336
- deliveryId,
1337
- sessionKey,
1338
- replyText: normalized,
1339
- source: 'openclaw-autochain',
1340
- });
1341
- replyTransport = replyResult?.transport || 'websocket';
1342
- replyFallbackUsed = replyResult?.fallbackUsed === true;
1343
- } else {
1344
- relayClient.sendReply({
1345
- deliveryId,
1346
- sessionKey,
1347
- replyText: normalized,
1348
- source: 'openclaw-autochain',
1349
- });
1350
- replyTransport = 'websocket-fire-and-forget';
1351
- replyFallbackUsed = false;
1352
- }
1362
+ const replyResult = await submitRelayReply(normalized);
1363
+ replyTransport = replyResult?.transport || null;
1364
+ replyFallbackUsed = replyResult?.fallbackUsed === true;
1353
1365
  replied = true;
1354
1366
  return true;
1355
1367
  };
@@ -1360,19 +1372,11 @@ function createDeliveryReplyDispatcher({
1360
1372
  suppressed = true;
1361
1373
  return false;
1362
1374
  }
1363
- if (typeof relayClient.sendKeepSilentAndWaitForAck === 'function') {
1364
- const silentResult = await relayClient.sendKeepSilentAndWaitForAck({
1365
- deliveryId,
1366
- sessionKey,
1367
- reason: normalizePluginOptionalText(reason) || 'no_renderable_reply',
1368
- source: 'openclaw-autochain',
1369
- });
1370
- keptSilentTransport = silentResult?.transport || 'websocket';
1371
- keptSilentFallbackUsed = silentResult?.fallbackUsed === true;
1372
- } else {
1373
- keptSilentTransport = 'unsupported';
1374
- keptSilentFallbackUsed = false;
1375
- }
1375
+ const silentResult = await submitRelayKeptSilent(
1376
+ normalizePluginOptionalText(reason) || 'no_renderable_reply',
1377
+ );
1378
+ keptSilentTransport = silentResult?.transport || null;
1379
+ keptSilentFallbackUsed = silentResult?.fallbackUsed === true;
1376
1380
  keptSilent = true;
1377
1381
  return true;
1378
1382
  };
@@ -1705,12 +1709,13 @@ async function maybeBridgeRuntimeDelivery({
1705
1709
  });
1706
1710
 
1707
1711
  try {
1708
- if (typeof relayClient?.sendAcceptedAndWaitForAck === 'function') {
1709
- await relayClient.sendAcceptedAndWaitForAck({
1710
- deliveryId,
1711
- sessionKey,
1712
- source: 'runtime_dispatch',
1713
- });
1712
+ const acceptedResult = await relayClient.acceptDeliveryHttp({
1713
+ deliveryId,
1714
+ sessionKey,
1715
+ source: 'runtime_dispatch',
1716
+ });
1717
+ if (acceptedResult.status < 200 || acceptedResult.status >= 300) {
1718
+ throw new Error(`failed to submit relay delivery acceptance: ${acceptedResult.status}`);
1714
1719
  }
1715
1720
  } catch (error) {
1716
1721
  logger.warn?.(`[claworld:${runtimeAccountId}] delivery acceptance acknowledgement failed`, {
@@ -1895,48 +1900,118 @@ export function createClaworldChannelPlugin({
1895
1900
  }
1896
1901
 
1897
1902
  async function persistRuntimeAppToken({ runtime, accountId, appToken, relayAgentId = null }) {
1898
- if (!runtime?.config?.loadConfig || !runtime?.config?.writeConfigFile) {
1899
- return { skipped: true, reason: 'missing_runtime_config_io' };
1900
- }
1901
1903
  if (!accountId || !appToken) {
1902
1904
  return { skipped: true, reason: 'missing_account_or_token' };
1903
1905
  }
1904
1906
 
1905
- const currentCfg = await runtime.config.loadConfig();
1906
- const nextCfg = JSON.parse(JSON.stringify(currentCfg || {}));
1907
- nextCfg.channels = nextCfg.channels && typeof nextCfg.channels === 'object' && !Array.isArray(nextCfg.channels)
1908
- ? nextCfg.channels
1909
- : {};
1910
- const claworldRoot = nextCfg.channels.claworld && typeof nextCfg.channels.claworld === 'object' && !Array.isArray(nextCfg.channels.claworld)
1911
- ? nextCfg.channels.claworld
1912
- : {};
1913
- const accounts = claworldRoot.accounts && typeof claworldRoot.accounts === 'object' && !Array.isArray(claworldRoot.accounts)
1914
- ? claworldRoot.accounts
1915
- : {};
1916
- const account = accounts[accountId] && typeof accounts[accountId] === 'object' && !Array.isArray(accounts[accountId])
1917
- ? accounts[accountId]
1918
- : {};
1907
+ let configPersistResult = { skipped: true, reason: 'missing_runtime_config_io' };
1908
+ if (runtime?.config?.loadConfig && runtime?.config?.writeConfigFile) {
1909
+ const currentCfg = await runtime.config.loadConfig();
1910
+ const nextCfg = JSON.parse(JSON.stringify(currentCfg || {}));
1911
+ nextCfg.channels = nextCfg.channels && typeof nextCfg.channels === 'object' && !Array.isArray(nextCfg.channels)
1912
+ ? nextCfg.channels
1913
+ : {};
1914
+ const claworldRoot = nextCfg.channels.claworld && typeof nextCfg.channels.claworld === 'object' && !Array.isArray(nextCfg.channels.claworld)
1915
+ ? nextCfg.channels.claworld
1916
+ : {};
1917
+ const accounts = claworldRoot.accounts && typeof claworldRoot.accounts === 'object' && !Array.isArray(claworldRoot.accounts)
1918
+ ? claworldRoot.accounts
1919
+ : {};
1920
+ const account = accounts[accountId] && typeof accounts[accountId] === 'object' && !Array.isArray(accounts[accountId])
1921
+ ? accounts[accountId]
1922
+ : {};
1923
+
1924
+ const normalizedRelayAgentId = normalizeClaworldText(relayAgentId, normalizeClaworldText(account?.relay?.agentId, null));
1925
+ const currentAppToken = normalizeClaworldText(account.appToken, null);
1926
+ const currentRelayAgentId = normalizeClaworldText(account?.relay?.agentId, null);
1927
+ if (currentAppToken === appToken && currentRelayAgentId === normalizedRelayAgentId) {
1928
+ configPersistResult = { skipped: true, reason: 'already_persisted' };
1929
+ } else {
1930
+ accounts[accountId] = {
1931
+ ...account,
1932
+ appToken,
1933
+ relay: {
1934
+ ...(account?.relay && typeof account.relay === 'object' && !Array.isArray(account.relay) ? account.relay : {}),
1935
+ ...(normalizedRelayAgentId ? { agentId: normalizedRelayAgentId } : {}),
1936
+ },
1937
+ };
1938
+ delete accounts[accountId].registration;
1939
+ claworldRoot.accounts = accounts;
1940
+ nextCfg.channels.claworld = claworldRoot;
1941
+ await runtime.config.writeConfigFile(nextCfg);
1942
+ configPersistResult = { skipped: false, ok: true };
1943
+ }
1944
+ }
1945
+
1946
+ let backupPersistResult = { skipped: true, reason: 'missing_runtime_config_loader' };
1947
+ try {
1948
+ backupPersistResult = await persistClaworldRuntimeBackup({
1949
+ runtime,
1950
+ accountId,
1951
+ });
1952
+ } catch (error) {
1953
+ backupPersistResult = {
1954
+ skipped: true,
1955
+ reason: 'backup_persist_failed',
1956
+ error: error?.message || String(error),
1957
+ };
1958
+ }
1959
+
1960
+ return {
1961
+ ...configPersistResult,
1962
+ backup: backupPersistResult,
1963
+ };
1964
+ }
1919
1965
 
1920
- const normalizedRelayAgentId = normalizeClaworldText(relayAgentId, normalizeClaworldText(account?.relay?.agentId, null));
1921
- const currentAppToken = normalizeClaworldText(account.appToken, null);
1922
- const currentRelayAgentId = normalizeClaworldText(account?.relay?.agentId, null);
1923
- if (currentAppToken === appToken && currentRelayAgentId === normalizedRelayAgentId) {
1924
- return { skipped: true, reason: 'already_persisted' };
1966
+ async function maybeRestoreRuntimeAppToken({ runtime, accountId, runtimeConfig }) {
1967
+ if (resolveRuntimeAppToken(runtimeConfig)) {
1968
+ return { restored: false, reason: 'already_configured', runtimeConfig };
1925
1969
  }
1926
1970
 
1927
- accounts[accountId] = {
1928
- ...account,
1929
- appToken,
1930
- relay: {
1931
- ...(account?.relay && typeof account.relay === 'object' && !Array.isArray(account.relay) ? account.relay : {}),
1932
- ...(normalizedRelayAgentId ? { agentId: normalizedRelayAgentId } : {}),
1933
- },
1971
+ const backupState = await loadClaworldRuntimeBackup({ accountId });
1972
+ const backup = backupState.backup;
1973
+ const backupToken = normalizeClaworldText(backup?.appToken, null);
1974
+ if (!backupToken) {
1975
+ return {
1976
+ restored: false,
1977
+ reason: 'backup_missing_app_token',
1978
+ runtimeConfig,
1979
+ };
1980
+ }
1981
+
1982
+ const backupServerUrl = normalizeClaworldText(backup?.serverUrl, null);
1983
+ const currentServerUrl = normalizeClaworldText(runtimeConfig?.serverUrl, null);
1984
+ if (backupServerUrl && currentServerUrl && normalizeRelayHttpBaseUrl(backupServerUrl) !== normalizeRelayHttpBaseUrl(currentServerUrl)) {
1985
+ return {
1986
+ restored: false,
1987
+ reason: 'backup_server_mismatch',
1988
+ runtimeConfig,
1989
+ };
1990
+ }
1991
+
1992
+ const restoredRuntimeConfig = applyRuntimeIdentity(runtimeConfig, {
1993
+ appToken: backupToken,
1994
+ });
1995
+
1996
+ try {
1997
+ await persistRuntimeAppToken({
1998
+ runtime,
1999
+ accountId,
2000
+ appToken: backupToken,
2001
+ });
2002
+ } catch (error) {
2003
+ logger.warn?.(`[claworld:${accountId || 'default'}] failed to persist restored runtime appToken`, {
2004
+ error: error?.message || String(error),
2005
+ });
2006
+ }
2007
+
2008
+ return {
2009
+ restored: true,
2010
+ reason: 'installer_state_backup',
2011
+ runtimeConfig: restoredRuntimeConfig,
2012
+ backup,
2013
+ installerStatePath: backupState.installerStatePath,
1934
2014
  };
1935
- delete accounts[accountId].registration;
1936
- claworldRoot.accounts = accounts;
1937
- nextCfg.channels.claworld = claworldRoot;
1938
- await runtime.config.writeConfigFile(nextCfg);
1939
- return { skipped: false, ok: true };
1940
2015
  }
1941
2016
 
1942
2017
  function resolveConfiguredRuntimeContext(context = {}) {
@@ -1959,6 +2034,19 @@ export function createClaworldChannelPlugin({
1959
2034
  const cfg = configuredContext.cfg || {};
1960
2035
  const accountId = configuredContext.accountId || null;
1961
2036
  let runtimeConfig = configuredContext.runtimeConfig;
2037
+ const runtimeResolution = resolvePluginRuntimeCandidate(context.runtime || null);
2038
+ const restoredBinding = await maybeRestoreRuntimeAppToken({
2039
+ runtime: runtimeResolution.runtime,
2040
+ accountId,
2041
+ runtimeConfig,
2042
+ });
2043
+ if (restoredBinding.restored) {
2044
+ runtimeConfig = restoredBinding.runtimeConfig;
2045
+ logger.info?.(`[claworld:${accountId || 'default'}] restored runtime binding from installer state`, {
2046
+ installerStatePath: restoredBinding.installerStatePath || null,
2047
+ relayAgentId: runtimeConfig?.relay?.agentId || null,
2048
+ });
2049
+ }
1962
2050
  const runtimeContext = accountRuntimeContexts.get(accountId || 'default') || null;
1963
2051
  if (runtimeContext?.runtimeConfig && !runtimeContext?.deferredFailure) {
1964
2052
  return {
@@ -2009,6 +2097,20 @@ export function createClaworldChannelPlugin({
2009
2097
  runtimeConfigShape: summarizeObjectShape(runtimeConfig),
2010
2098
  });
2011
2099
 
2100
+ const runtimeResolution = resolvePluginRuntimeCandidate(context.runtime || null);
2101
+ const restoredBinding = await maybeRestoreRuntimeAppToken({
2102
+ runtime: runtimeResolution.runtime,
2103
+ accountId: runtimeAccountId,
2104
+ runtimeConfig,
2105
+ });
2106
+ if (restoredBinding.restored) {
2107
+ runtimeConfig = restoredBinding.runtimeConfig;
2108
+ logger.info?.(`[claworld:${runtimeAccountId}] restored runtime binding from installer state`, {
2109
+ installerStatePath: restoredBinding.installerStatePath || null,
2110
+ relayAgentId: runtimeConfig?.relay?.agentId || null,
2111
+ });
2112
+ }
2113
+
2012
2114
  const validation = validateClaworldChannelConfig(configSource, context.accountId);
2013
2115
  if (!validation.ok && sourceType !== 'root_cfg') {
2014
2116
  logger.warn?.(`[claworld:${runtimeAccountId}] non-root runtime source would not validate as full cfg`, {
@@ -2084,7 +2186,6 @@ export function createClaworldChannelPlugin({
2084
2186
  return { startedDeferred: true, reason: 'missing_runtime_context', runtimeConfig };
2085
2187
  }
2086
2188
 
2087
- const runtimeResolution = resolvePluginRuntimeCandidate(context.runtime || null);
2088
2189
  const pluginRuntime = runtimeResolution.runtime;
2089
2190
  const runtimeSource = runtimeResolution.runtimeSource;
2090
2191
 
@@ -2092,24 +2193,24 @@ export function createClaworldChannelPlugin({
2092
2193
  runtimeSource,
2093
2194
  });
2094
2195
 
2095
- if (binding.bindingSource !== 'configured_app_token') {
2096
- try {
2097
- const persisted = await persistRuntimeAppToken({
2098
- runtime: pluginRuntime,
2196
+ try {
2197
+ const persisted = await persistRuntimeAppToken({
2198
+ runtime: pluginRuntime,
2199
+ accountId: runtimeConfig.accountId,
2200
+ appToken: resolveRuntimeAppToken(runtimeConfig),
2201
+ relayAgentId: runtimeConfig.relay?.agentId || null,
2202
+ });
2203
+ if (!persisted.skipped || persisted.backup?.skipped === false) {
2204
+ logger.info?.(`[claworld:${runtimeAccountId}] persisted runtime binding state`, {
2099
2205
  accountId: runtimeConfig.accountId,
2100
- appToken: resolveRuntimeAppToken(runtimeConfig),
2101
- relayAgentId: runtimeConfig.relay?.agentId || null,
2102
- });
2103
- if (!persisted.skipped) {
2104
- logger.info?.(`[claworld:${runtimeAccountId}] persisted runtime appToken`, {
2105
- accountId: runtimeConfig.accountId,
2106
- });
2107
- }
2108
- } catch (error) {
2109
- logger.warn?.(`[claworld:${runtimeAccountId}] failed to persist runtime appToken`, {
2110
- error: error?.message || String(error),
2206
+ configSkipped: persisted.skipped === true,
2207
+ backupSkipped: persisted.backup?.skipped === true,
2111
2208
  });
2112
2209
  }
2210
+ } catch (error) {
2211
+ logger.warn?.(`[claworld:${runtimeAccountId}] failed to persist runtime appToken`, {
2212
+ error: error?.message || String(error),
2213
+ });
2113
2214
  }
2114
2215
 
2115
2216
  accountRuntimeContexts.set(accountKey, {
@@ -29,6 +29,21 @@ import {
29
29
  TERMINAL_CLOSE_REASONS,
30
30
  } from './relay-client-shared.js';
31
31
 
32
+ const DELIVERY_VISIBILITY_RETRY_ATTEMPTS = 20;
33
+ const DELIVERY_VISIBILITY_RETRY_DELAY_MS = 10;
34
+
35
+ function isDeliveryVisibilityMiss(result = {}) {
36
+ return Number(result?.status) === 404
37
+ && normalizeOptionalText(result?.body?.error) === 'delivery_not_found';
38
+ }
39
+
40
+ async function waitForDeliveryVisibilityRetry() {
41
+ await new Promise((resolve) => {
42
+ const timer = setTimeout(resolve, DELIVERY_VISIBILITY_RETRY_DELAY_MS);
43
+ if (typeof timer?.unref === 'function') timer.unref();
44
+ });
45
+ }
46
+
32
47
  export class ClaworldRelayClient extends EventEmitter {
33
48
  constructor({
34
49
  logger = console,
@@ -244,6 +259,18 @@ export class ClaworldRelayClient extends EventEmitter {
244
259
  return { status: response.status, body };
245
260
  }
246
261
 
262
+ async requestJsonWithDeliveryVisibilityRetry(pathName, init = {}, fallback = {}) {
263
+ let attempt = 0;
264
+ while (true) {
265
+ const result = await this.requestJson(pathName, init, fallback);
266
+ if (!isDeliveryVisibilityMiss(result) || attempt >= DELIVERY_VISIBILITY_RETRY_ATTEMPTS - 1) {
267
+ return result;
268
+ }
269
+ attempt += 1;
270
+ await waitForDeliveryVisibilityRetry();
271
+ }
272
+ }
273
+
247
274
  async openSocket({
248
275
  wsUrl,
249
276
  agentId,
@@ -459,7 +486,7 @@ export class ClaworldRelayClient extends EventEmitter {
459
486
  config,
460
487
  agentId,
461
488
  credential = null,
462
- clientVersion = 'claworld-plugin/0.2.21',
489
+ clientVersion = 'claworld-plugin/0.2.22',
463
490
  sessionTarget,
464
491
  fallbackTarget,
465
492
  } = {}) {
@@ -879,7 +906,7 @@ export class ClaworldRelayClient extends EventEmitter {
879
906
  replyText,
880
907
  source,
881
908
  });
882
- const result = await this.requestJson(`/v1/deliveries/${encodeURIComponent(envelope.deliveryId)}/reply`, {
909
+ const result = await this.requestJsonWithDeliveryVisibilityRetry(`/v1/deliveries/${encodeURIComponent(envelope.deliveryId)}/reply`, {
883
910
  method: 'POST',
884
911
  headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
885
912
  body: JSON.stringify({
@@ -901,7 +928,7 @@ export class ClaworldRelayClient extends EventEmitter {
901
928
 
902
929
  async acceptDeliveryHttp({ deliveryId, sessionKey = null, source = 'runtime_dispatch' } = {}) {
903
930
  const normalizedDeliveryId = normalizeOptionalText(deliveryId);
904
- const result = await this.requestJson(`/v1/deliveries/${encodeURIComponent(normalizedDeliveryId)}/accepted`, {
931
+ const result = await this.requestJsonWithDeliveryVisibilityRetry(`/v1/deliveries/${encodeURIComponent(normalizedDeliveryId)}/accepted`, {
905
932
  method: 'POST',
906
933
  headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
907
934
  body: JSON.stringify({
@@ -932,6 +959,10 @@ export class ClaworldRelayClient extends EventEmitter {
932
959
  timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
933
960
  httpFallback = true,
934
961
  } = {}) {
962
+ const ackPromise = this.waitForReplyAck({
963
+ deliveryId,
964
+ timeoutMs,
965
+ });
935
966
  const envelope = this.sendReply({
936
967
  deliveryId,
937
968
  sessionKey,
@@ -940,10 +971,7 @@ export class ClaworldRelayClient extends EventEmitter {
940
971
  });
941
972
 
942
973
  try {
943
- const ack = await this.waitForReplyAck({
944
- deliveryId: envelope.deliveryId,
945
- timeoutMs,
946
- });
974
+ const ack = await ackPromise;
947
975
  return {
948
976
  ok: true,
949
977
  envelope,
@@ -1011,6 +1039,24 @@ export class ClaworldRelayClient extends EventEmitter {
1011
1039
  }
1012
1040
  }
1013
1041
 
1042
+ async submitDeliveryReply({
1043
+ deliveryId,
1044
+ sessionKey,
1045
+ replyText,
1046
+ source = 'subagent',
1047
+ timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
1048
+ httpFallback = true,
1049
+ } = {}) {
1050
+ return await this.sendReplyAndWaitForAck({
1051
+ deliveryId,
1052
+ sessionKey,
1053
+ replyText,
1054
+ source,
1055
+ timeoutMs,
1056
+ httpFallback,
1057
+ });
1058
+ }
1059
+
1014
1060
  async sendAcceptedAndWaitForAck({
1015
1061
  deliveryId,
1016
1062
  sessionKey,
@@ -1018,6 +1064,10 @@ export class ClaworldRelayClient extends EventEmitter {
1018
1064
  timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
1019
1065
  httpFallback = true,
1020
1066
  } = {}) {
1067
+ const ackPromise = this.waitForAcceptedAck({
1068
+ deliveryId,
1069
+ timeoutMs,
1070
+ });
1021
1071
  const envelope = this.sendAccepted({
1022
1072
  deliveryId,
1023
1073
  sessionKey,
@@ -1025,10 +1075,7 @@ export class ClaworldRelayClient extends EventEmitter {
1025
1075
  });
1026
1076
 
1027
1077
  try {
1028
- const ack = await this.waitForAcceptedAck({
1029
- deliveryId: envelope.deliveryId,
1030
- timeoutMs,
1031
- });
1078
+ const ack = await ackPromise;
1032
1079
  return {
1033
1080
  ok: true,
1034
1081
  envelope,
@@ -1082,7 +1129,7 @@ export class ClaworldRelayClient extends EventEmitter {
1082
1129
 
1083
1130
  async keepDeliverySilentHttp({ deliveryId, reason = null, source = 'openclaw-autochain' } = {}) {
1084
1131
  const normalizedDeliveryId = normalizeOptionalText(deliveryId);
1085
- const result = await this.requestJson(`/v1/deliveries/${encodeURIComponent(normalizedDeliveryId)}/kept-silent`, {
1132
+ const result = await this.requestJsonWithDeliveryVisibilityRetry(`/v1/deliveries/${encodeURIComponent(normalizedDeliveryId)}/kept-silent`, {
1086
1133
  method: 'POST',
1087
1134
  headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
1088
1135
  body: JSON.stringify({
@@ -1113,6 +1160,10 @@ export class ClaworldRelayClient extends EventEmitter {
1113
1160
  timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
1114
1161
  httpFallback = true,
1115
1162
  } = {}) {
1163
+ const ackPromise = this.waitForKeepSilentAck({
1164
+ deliveryId,
1165
+ timeoutMs,
1166
+ });
1116
1167
  const envelope = this.sendKeepSilent({
1117
1168
  deliveryId,
1118
1169
  sessionKey,
@@ -1121,10 +1172,7 @@ export class ClaworldRelayClient extends EventEmitter {
1121
1172
  });
1122
1173
 
1123
1174
  try {
1124
- const ack = await this.waitForKeepSilentAck({
1125
- deliveryId: envelope.deliveryId,
1126
- timeoutMs,
1127
- });
1175
+ const ack = await ackPromise;
1128
1176
  return {
1129
1177
  ok: true,
1130
1178
  envelope,
@@ -1192,6 +1240,24 @@ export class ClaworldRelayClient extends EventEmitter {
1192
1240
  }
1193
1241
  }
1194
1242
 
1243
+ async submitDeliveryKeptSilent({
1244
+ deliveryId,
1245
+ sessionKey,
1246
+ reason = null,
1247
+ source = 'openclaw-autochain',
1248
+ timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
1249
+ httpFallback = true,
1250
+ } = {}) {
1251
+ return await this.sendKeepSilentAndWaitForAck({
1252
+ deliveryId,
1253
+ sessionKey,
1254
+ reason,
1255
+ source,
1256
+ timeoutMs,
1257
+ httpFallback,
1258
+ });
1259
+ }
1260
+
1195
1261
  async createChatRequest({ fromAgentId, displayName, agentCode, requestContext = {} } = {}) {
1196
1262
  const normalized = normalizeChatRequestInput({ requestContext, source: 'direct_lookup' });
1197
1263
  const normalizedDisplayName = normalizeOptionalText(displayName);
@@ -0,0 +1,105 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import {
6
+ DEFAULT_CLAWORLD_ACCOUNT_ID,
7
+ findClaworldManagedRuntimeBackup,
8
+ setClaworldManagedRuntimeBackupState,
9
+ stripClaworldManagedRuntimeConfig,
10
+ } from './managed-config.js';
11
+
12
+ function normalizeText(value, fallback = null) {
13
+ if (value == null) return fallback;
14
+ const normalized = String(value).trim();
15
+ return normalized || fallback;
16
+ }
17
+
18
+ export function resolveDefaultOpenClawConfigPath() {
19
+ return path.resolve(normalizeText(
20
+ process.env.OPENCLAW_CONFIG_PATH,
21
+ path.join(os.homedir(), '.openclaw', 'openclaw.json'),
22
+ ));
23
+ }
24
+
25
+ export function resolveClaworldInstallerStatePath(configPath = resolveDefaultOpenClawConfigPath()) {
26
+ const resolvedConfigPath = path.resolve(String(configPath));
27
+ return path.join(path.dirname(resolvedConfigPath), '.claworld-installer-state.json');
28
+ }
29
+
30
+ async function loadInstallerStateFromDisk(installerStatePath) {
31
+ try {
32
+ const raw = await fs.readFile(installerStatePath, 'utf8');
33
+ const parsed = JSON.parse(raw);
34
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
35
+ } catch (error) {
36
+ if (error?.code === 'ENOENT') return {};
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ async function writeInstallerStateToDisk(installerStatePath, installerState) {
42
+ const nextState = installerState && typeof installerState === 'object' && !Array.isArray(installerState)
43
+ ? installerState
44
+ : {};
45
+ if (Object.keys(nextState).length === 0) {
46
+ await fs.rm(installerStatePath, { force: true });
47
+ return;
48
+ }
49
+ await fs.mkdir(path.dirname(installerStatePath), { recursive: true });
50
+ await fs.writeFile(installerStatePath, `${JSON.stringify(nextState, null, 2)}\n`, 'utf8');
51
+ }
52
+
53
+ export async function loadClaworldRuntimeBackup({
54
+ accountId = DEFAULT_CLAWORLD_ACCOUNT_ID,
55
+ configPath = resolveDefaultOpenClawConfigPath(),
56
+ } = {}) {
57
+ const installerStatePath = resolveClaworldInstallerStatePath(configPath);
58
+ const installerState = await loadInstallerStateFromDisk(installerStatePath);
59
+ return {
60
+ installerStatePath,
61
+ installerState,
62
+ backup: findClaworldManagedRuntimeBackup(installerState, accountId),
63
+ };
64
+ }
65
+
66
+ export async function persistClaworldRuntimeBackup({
67
+ runtime = null,
68
+ accountId = DEFAULT_CLAWORLD_ACCOUNT_ID,
69
+ configPath = resolveDefaultOpenClawConfigPath(),
70
+ } = {}) {
71
+ if (!runtime?.config?.loadConfig) {
72
+ return { skipped: true, reason: 'missing_runtime_config_loader' };
73
+ }
74
+
75
+ const currentConfig = await runtime.config.loadConfig();
76
+ const { backup } = stripClaworldManagedRuntimeConfig(currentConfig, {
77
+ accountId,
78
+ preserveBackup: true,
79
+ });
80
+
81
+ if (!backup?.appToken) {
82
+ return { skipped: true, reason: 'missing_app_token', backup: null };
83
+ }
84
+
85
+ const installerStatePath = resolveClaworldInstallerStatePath(configPath);
86
+ const installerState = await loadInstallerStateFromDisk(installerStatePath);
87
+ const previousBackup = findClaworldManagedRuntimeBackup(installerState, accountId);
88
+ if (JSON.stringify(previousBackup || null) === JSON.stringify(backup)) {
89
+ return {
90
+ skipped: true,
91
+ reason: 'already_persisted',
92
+ installerStatePath,
93
+ backup,
94
+ };
95
+ }
96
+
97
+ setClaworldManagedRuntimeBackupState(installerState, accountId, backup);
98
+ await writeInstallerStateToDisk(installerStatePath, installerState);
99
+ return {
100
+ skipped: false,
101
+ ok: true,
102
+ installerStatePath,
103
+ backup,
104
+ };
105
+ }