@xfxstudio/claworld 0.1.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.
Files changed (69) hide show
  1. package/README.md +60 -0
  2. package/bin/claworld.mjs +9 -0
  3. package/index.js +51 -0
  4. package/openclaw.plugin.json +470 -0
  5. package/package.json +76 -0
  6. package/setup-entry.js +6 -0
  7. package/src/lib/accepted-chat-kickoff.js +192 -0
  8. package/src/lib/agent-address.js +46 -0
  9. package/src/lib/agent-profile.js +69 -0
  10. package/src/lib/http-auth.js +151 -0
  11. package/src/lib/policy.js +118 -0
  12. package/src/lib/runtime-errors.js +149 -0
  13. package/src/lib/runtime-guidance.js +458 -0
  14. package/src/openclaw/index.js +53 -0
  15. package/src/openclaw/installer/cli.js +349 -0
  16. package/src/openclaw/installer/constants.js +6 -0
  17. package/src/openclaw/installer/core.js +1548 -0
  18. package/src/openclaw/installer/doctor.js +690 -0
  19. package/src/openclaw/installer/workspace-contract.js +403 -0
  20. package/src/openclaw/plugin/account-identity.js +66 -0
  21. package/src/openclaw/plugin/claworld-channel-plugin.js +3118 -0
  22. package/src/openclaw/plugin/config-schema.js +464 -0
  23. package/src/openclaw/plugin/lifecycle.js +114 -0
  24. package/src/openclaw/plugin/managed-config.js +648 -0
  25. package/src/openclaw/plugin/onboarding.js +291 -0
  26. package/src/openclaw/plugin/register.js +961 -0
  27. package/src/openclaw/plugin/relay-client.js +783 -0
  28. package/src/openclaw/plugin/runtime.js +12 -0
  29. package/src/openclaw/protocol/relay-event-protocol.js +31 -0
  30. package/src/openclaw/runtime/canonical-result-builder.js +116 -0
  31. package/src/openclaw/runtime/demo-session-bootstrap.js +37 -0
  32. package/src/openclaw/runtime/feedback-helper.js +145 -0
  33. package/src/openclaw/runtime/inbound-session-router.js +36 -0
  34. package/src/openclaw/runtime/outbound-session-bridge.js +17 -0
  35. package/src/openclaw/runtime/product-shell-helper.js +1712 -0
  36. package/src/openclaw/runtime/runtime-path.js +19 -0
  37. package/src/openclaw/runtime/system-message-orchestrator.js +1 -0
  38. package/src/openclaw/runtime/tool-contracts.js +714 -0
  39. package/src/openclaw/runtime/tool-inventory.js +92 -0
  40. package/src/openclaw/runtime/world-moderation-helper.js +415 -0
  41. package/src/openclaw/runtime/world-session-startup.js +1 -0
  42. package/src/product-shell/catalog/default-world-catalog.js +296 -0
  43. package/src/product-shell/contracts/candidate-feed.js +330 -0
  44. package/src/product-shell/contracts/chat-request-approval-policy.js +98 -0
  45. package/src/product-shell/contracts/world-manifest.js +435 -0
  46. package/src/product-shell/contracts/world-orchestration.js +1024 -0
  47. package/src/product-shell/feedback/feedback-contract.js +13 -0
  48. package/src/product-shell/feedback/feedback-routes.js +98 -0
  49. package/src/product-shell/feedback/feedback-service.js +254 -0
  50. package/src/product-shell/index.js +163 -0
  51. package/src/product-shell/matching/matchmaking-service.js +340 -0
  52. package/src/product-shell/membership/membership-service.js +277 -0
  53. package/src/product-shell/onboarding/onboarding-routes.js +37 -0
  54. package/src/product-shell/onboarding/onboarding-service.js +230 -0
  55. package/src/product-shell/orchestration/session-orchestrator.js +38 -0
  56. package/src/product-shell/results/result-service.js +15 -0
  57. package/src/product-shell/search/search-service.js +359 -0
  58. package/src/product-shell/social/chat-request-approval-policy.js +332 -0
  59. package/src/product-shell/social/chat-request-routes.js +108 -0
  60. package/src/product-shell/social/chat-request-service.js +632 -0
  61. package/src/product-shell/social/friend-routes.js +82 -0
  62. package/src/product-shell/social/friend-service.js +560 -0
  63. package/src/product-shell/social/social-routes.js +21 -0
  64. package/src/product-shell/social/social-service.js +140 -0
  65. package/src/product-shell/worlds/world-admin-service.js +705 -0
  66. package/src/product-shell/worlds/world-authorization.js +135 -0
  67. package/src/product-shell/worlds/world-broadcast-service.js +299 -0
  68. package/src/product-shell/worlds/world-routes.js +410 -0
  69. package/src/product-shell/worlds/world-service.js +89 -0
@@ -0,0 +1,464 @@
1
+ import {
2
+ applyRuntimeIdentity,
3
+ normalizeRuntimeRegistration,
4
+ resolveRuntimeAppToken,
5
+ } from './account-identity.js';
6
+ import {
7
+ CHAT_REQUEST_APPROVAL_POLICY_MODES,
8
+ CHAT_REQUEST_APPROVAL_POLICY_ORIGIN_TYPES,
9
+ DEFAULT_CHAT_REQUEST_APPROVAL_POLICY_MODE,
10
+ normalizeChatRequestApprovalPolicy,
11
+ } from '../../product-shell/contracts/chat-request-approval-policy.js';
12
+
13
+ const REQUIRED_KEYS = ['enabled', 'serverUrl', 'apiKey', 'accountId'];
14
+
15
+ export const CLAWORLD_CHANNEL_ID = 'claworld';
16
+ const LOCAL_AGENT_CODE_PATTERN = '^[A-Za-z0-9._:+~-]+(?:@[A-Za-z0-9._:+~-]+)?$';
17
+ const LOCAL_AGENT_CODE_REGEX = new RegExp(LOCAL_AGENT_CODE_PATTERN, 'i');
18
+
19
+ const AGENT_REGISTRATION_SCHEMA = {
20
+ type: 'object',
21
+ additionalProperties: false,
22
+ properties: {
23
+ enabled: {
24
+ type: 'boolean',
25
+ description: 'Enable relay agent registration when this account does not already have an app token.',
26
+ default: false,
27
+ },
28
+ agentCode: {
29
+ type: 'string',
30
+ minLength: 1,
31
+ pattern: LOCAL_AGENT_CODE_PATTERN,
32
+ description: 'Unique Claworld identity handle. Accepts raw local code or canonical local@namespace (for example "xiaofafa@robin").',
33
+ },
34
+ displayName: {
35
+ type: 'string',
36
+ minLength: 1,
37
+ description: 'Display name to use when the relay agent is created or refreshed.',
38
+ },
39
+ },
40
+ };
41
+
42
+ export const LOCAL_AGENT_BOOTSTRAP_SCHEMA = AGENT_REGISTRATION_SCHEMA;
43
+ export const LOCAL_AGENT_BOOTSTRAP_REQUIRED = ['agentCode'];
44
+
45
+ export const MANUAL_RELAY_BINDING_SCHEMA = {
46
+ type: 'object',
47
+ additionalProperties: false,
48
+ properties: {
49
+ appToken: {
50
+ type: 'string',
51
+ minLength: 1,
52
+ description: 'Canonical Claworld app token for this account. The runtime resolves the bound relay agent from this token.',
53
+ },
54
+ agentId: {
55
+ type: 'string',
56
+ minLength: 1,
57
+ description: 'Legacy relay agent id hint. Canonical flow resolves the binding from appToken at runtime.',
58
+ },
59
+ credentialToken: {
60
+ type: 'string',
61
+ minLength: 1,
62
+ description: 'Legacy alias for appToken.',
63
+ },
64
+ defaultToAddress: {
65
+ type: 'string',
66
+ minLength: 1,
67
+ description: 'Default relay target address (for example alice@robin) for minimal outbound testing.',
68
+ },
69
+ },
70
+ };
71
+
72
+ const SINGLE_ACCOUNT_PROPERTIES = {
73
+ name: {
74
+ type: 'string',
75
+ minLength: 1,
76
+ description: 'Optional operator-facing name for this local Claworld account.',
77
+ },
78
+ enabled: { type: 'boolean', description: 'Enable the Claworld channel plugin.' },
79
+ serverUrl: {
80
+ type: 'string',
81
+ minLength: 1,
82
+ description: 'Relay backend base URL or websocket URL (http/https/ws/wss).',
83
+ },
84
+ apiKey: {
85
+ type: 'string',
86
+ minLength: 1,
87
+ description: 'Plugin/backend API key for future backend-authenticated control paths.',
88
+ },
89
+ appToken: {
90
+ type: 'string',
91
+ minLength: 1,
92
+ description: 'Canonical Claworld app token for this channel account.',
93
+ },
94
+ accountId: {
95
+ type: 'string',
96
+ minLength: 1,
97
+ description: 'Local OpenClaw-facing account id bound to this channel instance.',
98
+ },
99
+ toolProfile: {
100
+ type: 'string',
101
+ enum: ['minimal', 'default', 'world', 'full'],
102
+ description: 'Managed Claworld tool visibility profile. Legacy "world" normalizes to "default".',
103
+ },
104
+ heartbeatSeconds: {
105
+ type: 'integer',
106
+ minimum: 1,
107
+ description: 'Heartbeat cadence for the relay websocket client.',
108
+ default: 15,
109
+ },
110
+ reconnect: {
111
+ type: 'boolean',
112
+ description: 'Whether reconnect attempts are allowed after disconnect.',
113
+ default: true,
114
+ },
115
+ routing: {
116
+ type: 'object',
117
+ additionalProperties: false,
118
+ properties: {
119
+ sessionTarget: {
120
+ type: 'string',
121
+ enum: ['subagent', 'mainagent'],
122
+ default: 'subagent',
123
+ },
124
+ fallbackTarget: {
125
+ type: 'string',
126
+ enum: ['mainagent', 'human(optional)'],
127
+ default: 'mainagent',
128
+ },
129
+ allowHumanInterrupt: {
130
+ type: 'boolean',
131
+ default: true,
132
+ },
133
+ },
134
+ },
135
+ approval: {
136
+ type: 'object',
137
+ additionalProperties: false,
138
+ properties: {
139
+ mode: {
140
+ type: 'string',
141
+ enum: [...CHAT_REQUEST_APPROVAL_POLICY_MODES],
142
+ description: 'Declarative inbound chat-request approval policy for this account.',
143
+ default: DEFAULT_CHAT_REQUEST_APPROVAL_POLICY_MODE,
144
+ },
145
+ blocks: {
146
+ type: 'object',
147
+ additionalProperties: false,
148
+ properties: {
149
+ originTypes: {
150
+ type: 'array',
151
+ items: {
152
+ type: 'string',
153
+ enum: [...CHAT_REQUEST_APPROVAL_POLICY_ORIGIN_TYPES],
154
+ },
155
+ description: 'Optional canonical chat-request origin types that should be rejected by backend policy.',
156
+ },
157
+ worldIds: {
158
+ type: 'array',
159
+ items: {
160
+ type: 'string',
161
+ minLength: 1,
162
+ },
163
+ description: 'Optional world ids that should be rejected by backend policy.',
164
+ },
165
+ },
166
+ },
167
+ autoAccept: {
168
+ type: 'boolean',
169
+ description: 'Legacy alias. `true` maps to `approval.mode = "open"` and `false` maps to `approval.mode = "manual_review"`.',
170
+ default: false,
171
+ },
172
+ maxTurns: {
173
+ type: 'integer',
174
+ minimum: 1,
175
+ description: 'Legacy ignored field. Approval policy no longer carries session turn controls.',
176
+ default: 4,
177
+ },
178
+ turnTimeoutMs: {
179
+ type: 'integer',
180
+ minimum: 1000,
181
+ description: 'Legacy ignored field. Approval policy no longer carries session timeout controls.',
182
+ default: 30000,
183
+ },
184
+ },
185
+ },
186
+ testing: {
187
+ type: 'object',
188
+ additionalProperties: false,
189
+ properties: {
190
+ allowBridgedCommandDispatch: {
191
+ type: 'boolean',
192
+ description: 'Test-only switch that allows bridged relay turns beginning with slash commands to use the OpenClaw command fast-path.',
193
+ default: false,
194
+ },
195
+ },
196
+ },
197
+ registration: AGENT_REGISTRATION_SCHEMA,
198
+ localAgent: LOCAL_AGENT_BOOTSTRAP_SCHEMA,
199
+ relay: MANUAL_RELAY_BINDING_SCHEMA,
200
+ };
201
+
202
+ export const claworldChannelConfigJsonSchema = {
203
+ type: 'object',
204
+ additionalProperties: false,
205
+ properties: {
206
+ ...SINGLE_ACCOUNT_PROPERTIES,
207
+ defaultAccount: {
208
+ type: 'string',
209
+ minLength: 1,
210
+ description: 'Default account id to use when multiple claworld accounts are configured.',
211
+ },
212
+ accounts: {
213
+ type: 'object',
214
+ minProperties: 1,
215
+ additionalProperties: {
216
+ type: 'object',
217
+ additionalProperties: false,
218
+ required: REQUIRED_KEYS,
219
+ properties: SINGLE_ACCOUNT_PROPERTIES,
220
+ },
221
+ },
222
+ },
223
+ };
224
+
225
+ export const claworldChannelConfigSchema = {
226
+ channelId: CLAWORLD_CHANNEL_ID,
227
+ required: REQUIRED_KEYS,
228
+ optional: ['name', 'heartbeatSeconds', 'reconnect', 'routing', 'approval', 'testing', 'appToken', 'registration', 'localAgent', 'relay', 'toolProfile', 'defaultAccount', 'accounts'],
229
+ jsonSchema: claworldChannelConfigJsonSchema,
230
+ description:
231
+ '最小 OpenClaw claworld channel 配置;支持单账号或 accounts.<id> 多账号模式。canonical flow uses appToken + registration, while relay/localAgent fields remain compatibility aliases.',
232
+ routingShape: {
233
+ sessionTarget: 'subagent',
234
+ fallbackTarget: 'mainagent',
235
+ allowHumanInterrupt: true,
236
+ },
237
+ };
238
+
239
+ function normalizeText(value, fallback = null) {
240
+ if (value == null) return fallback;
241
+ const normalized = String(value).trim();
242
+ return normalized || fallback;
243
+ }
244
+
245
+ function readRawClaworldRoot(config = {}) {
246
+ if (config?.channels?.[CLAWORLD_CHANNEL_ID]) return config.channels[CLAWORLD_CHANNEL_ID];
247
+ if (config?.[CLAWORLD_CHANNEL_ID]) return config[CLAWORLD_CHANNEL_ID];
248
+ return config;
249
+ }
250
+
251
+ function listConfiguredClaworldAccountIds(config = {}) {
252
+ const root = readRawClaworldRoot(config);
253
+ if (root?.accounts && typeof root.accounts === 'object') {
254
+ return Object.keys(root.accounts).filter(Boolean);
255
+ }
256
+ const single = root;
257
+ if (single?.accountId) return [single.accountId];
258
+ return [];
259
+ }
260
+
261
+ function readDefaultAccountId(config = {}) {
262
+ const root = readRawClaworldRoot(config);
263
+ const configuredIds = listConfiguredClaworldAccountIds(config);
264
+ const explicit = String(root?.defaultAccount || '').trim();
265
+ if (explicit && configuredIds.includes(explicit)) return explicit;
266
+ if (root?.accounts?.default) return 'default';
267
+ return configuredIds[0] || null;
268
+ }
269
+
270
+ function readClaworldConfigSection(config = {}, accountId = null) {
271
+ const root = readRawClaworldRoot(config);
272
+ if (root?.accounts && typeof root.accounts === 'object') {
273
+ const normalizedAccountId = String(accountId || '').trim();
274
+ if (normalizedAccountId && root.accounts[normalizedAccountId]) return root.accounts[normalizedAccountId];
275
+
276
+ const defaultAccountId = readDefaultAccountId(config);
277
+ if (defaultAccountId && root.accounts[defaultAccountId]) return root.accounts[defaultAccountId];
278
+
279
+ const [firstAccount] = Object.values(root.accounts);
280
+ if (firstAccount) return firstAccount;
281
+ }
282
+ return root;
283
+ }
284
+
285
+ function determineCredentialStatus(candidate = {}) {
286
+ if (resolveRuntimeAppToken(candidate)) {
287
+ return { tokenSource: 'config', tokenStatus: 'available' };
288
+ }
289
+ if (normalizeRuntimeRegistration(candidate).enabled) {
290
+ return { tokenSource: 'registration', tokenStatus: 'registration_required' };
291
+ }
292
+ return { tokenSource: 'none', tokenStatus: 'missing' };
293
+ }
294
+
295
+ function determineBindingStatus(candidate = {}) {
296
+ if (resolveRuntimeAppToken(candidate)) return 'bound';
297
+ if (normalizeRuntimeRegistration(candidate).enabled) return 'registration_pending';
298
+ return 'unbound';
299
+ }
300
+
301
+ export function validateClaworldChannelConfig(config = {}, accountId = null) {
302
+ const root = readRawClaworldRoot(config);
303
+ const candidate = readClaworldConfigSection(config, accountId) || {};
304
+ const missing = REQUIRED_KEYS.filter((key) => candidate[key] == null || candidate[key] === '');
305
+ const errors = [];
306
+ const registration = normalizeRuntimeRegistration(candidate);
307
+ const appToken = resolveRuntimeAppToken(candidate);
308
+ const approval = normalizeChatRequestApprovalPolicy(candidate.approval, {
309
+ legacyAutoAccept: typeof candidate.approval?.autoAccept === 'boolean'
310
+ ? candidate.approval.autoAccept
311
+ : null,
312
+ });
313
+
314
+ const configuredIds = listConfiguredClaworldAccountIds(config);
315
+ const hasMultipleAccounts = configuredIds.length > 1;
316
+ const explicitDefaultAccount = String(root?.defaultAccount || '').trim();
317
+
318
+ if (hasMultipleAccounts && !explicitDefaultAccount && !String(accountId || '').trim()) {
319
+ errors.push({ code: 'missing_default_account' });
320
+ }
321
+ if (explicitDefaultAccount && root?.accounts && !root.accounts[explicitDefaultAccount]) {
322
+ errors.push({ code: 'invalid_default_account', value: explicitDefaultAccount });
323
+ }
324
+
325
+ if (missing.length > 0) {
326
+ errors.push({ code: 'missing_required_keys', keys: missing });
327
+ }
328
+
329
+ if (candidate.serverUrl != null) {
330
+ try {
331
+ const parsed = new URL(candidate.serverUrl);
332
+ if (!['ws:', 'wss:', 'http:', 'https:'].includes(parsed.protocol)) {
333
+ errors.push({ code: 'invalid_server_url_protocol', value: parsed.protocol });
334
+ }
335
+ } catch {
336
+ errors.push({ code: 'invalid_server_url', value: candidate.serverUrl });
337
+ }
338
+ }
339
+
340
+ if (
341
+ candidate.heartbeatSeconds != null
342
+ && (!Number.isFinite(Number(candidate.heartbeatSeconds)) || Number(candidate.heartbeatSeconds) <= 0)
343
+ ) {
344
+ errors.push({ code: 'invalid_heartbeat_seconds', value: candidate.heartbeatSeconds });
345
+ }
346
+
347
+ const sessionTarget = candidate.routing?.sessionTarget || 'subagent';
348
+ const fallbackTarget = candidate.routing?.fallbackTarget || 'mainagent';
349
+ if (!['subagent', 'mainagent'].includes(sessionTarget)) {
350
+ errors.push({ code: 'invalid_session_target', value: sessionTarget });
351
+ }
352
+ if (!['mainagent', 'human(optional)'].includes(fallbackTarget)) {
353
+ errors.push({ code: 'invalid_fallback_target', value: fallbackTarget });
354
+ }
355
+
356
+ if (registration.enabled && !registration.agentCode) {
357
+ errors.push({ code: 'missing_local_agent_code' });
358
+ }
359
+ if (registration.enabled && registration.agentCode && !LOCAL_AGENT_CODE_REGEX.test(String(registration.agentCode).trim())) {
360
+ errors.push({ code: 'invalid_local_agent_code', value: registration.agentCode });
361
+ }
362
+
363
+ if (candidate.relay?.agentId && !appToken) {
364
+ errors.push({ code: 'missing_relay_app_token' });
365
+ }
366
+
367
+ const runtimeIdentity = applyRuntimeIdentity({
368
+ enabled: Boolean(candidate.enabled),
369
+ serverUrl: candidate.serverUrl || null,
370
+ apiKey: candidate.apiKey || null,
371
+ appToken,
372
+ accountId: candidate.accountId || null,
373
+ defaultAccount: explicitDefaultAccount || null,
374
+ heartbeatSeconds: candidate.heartbeatSeconds == null ? 15 : Math.floor(Number(candidate.heartbeatSeconds)),
375
+ reconnect: candidate.reconnect !== false,
376
+ routing: {
377
+ sessionTarget,
378
+ fallbackTarget,
379
+ allowHumanInterrupt: candidate.routing?.allowHumanInterrupt !== false,
380
+ },
381
+ approval,
382
+ testing: {
383
+ allowBridgedCommandDispatch: candidate.testing?.allowBridgedCommandDispatch === true,
384
+ },
385
+ registration,
386
+ relay: {
387
+ agentId: candidate.relay?.agentId || null,
388
+ appToken,
389
+ credentialToken: appToken,
390
+ defaultToAddress: candidate.relay?.defaultToAddress || null,
391
+ },
392
+ });
393
+
394
+ return {
395
+ ok: errors.length === 0,
396
+ errors,
397
+ normalized: runtimeIdentity,
398
+ };
399
+ }
400
+
401
+ export function inspectClaworldChannelAccount(config = {}, accountId = null) {
402
+ const result = validateClaworldChannelConfig(config, accountId);
403
+ const normalized = result.normalized;
404
+ const configuredIds = listConfiguredClaworldAccountIds(config);
405
+ const { tokenSource, tokenStatus } = determineCredentialStatus(normalized);
406
+ const bindingStatus = determineBindingStatus(normalized);
407
+ const configured = Boolean(normalized.serverUrl && normalized.apiKey && normalized.accountId);
408
+ return {
409
+ accountId: normalized.accountId || accountId || readDefaultAccountId(config) || configuredIds[0] || 'default',
410
+ name: normalizeText(normalized.name, normalizeText(normalized.registration?.displayName, null)),
411
+ enabled: normalized.enabled,
412
+ configured,
413
+ configuredStatus: configured ? 'configured' : 'missing_required_config',
414
+ serverUrl: normalized.serverUrl,
415
+ heartbeatSeconds: normalized.heartbeatSeconds,
416
+ reconnect: normalized.reconnect,
417
+ routing: normalized.routing,
418
+ approval: normalized.approval,
419
+ testing: normalized.testing,
420
+ appToken: normalized.appToken || null,
421
+ registration: normalized.registration,
422
+ localAgent: normalized.localAgent,
423
+ relay: normalized.relay,
424
+ defaultAccount: normalized.defaultAccount,
425
+ bindingStatus,
426
+ tokenSource,
427
+ tokenStatus,
428
+ issues: result.errors,
429
+ };
430
+ }
431
+
432
+ export function resolveClaworldRuntimeConfig(config = {}, accountId = null) {
433
+ const result = validateClaworldChannelConfig(config, accountId);
434
+ if (!result.ok) {
435
+ const detail = result.errors.map((error) => error.code).join(', ') || 'invalid_config';
436
+ throw new Error(`invalid claworld config: ${detail}`);
437
+ }
438
+ return {
439
+ ...result.normalized,
440
+ accountId: result.normalized.accountId || accountId || readDefaultAccountId(config) || 'default',
441
+ approval: result.normalized.approval,
442
+ localAgent: result.normalized.localAgent,
443
+ relay: result.normalized.relay,
444
+ };
445
+ }
446
+
447
+ export function resolveClaworldChannelAccount(config = {}, accountId = null) {
448
+ const runtimeConfig = resolveClaworldRuntimeConfig(config, accountId);
449
+ const inspection = inspectClaworldChannelAccount(config, accountId);
450
+ return {
451
+ ...inspection,
452
+ runtimeReady: true,
453
+ resolvedFrom: accountId ? 'requested_account' : 'default_account',
454
+ runtimeConfig,
455
+ };
456
+ }
457
+
458
+ export function listClaworldAccountIds(config = {}) {
459
+ return listConfiguredClaworldAccountIds(config);
460
+ }
461
+
462
+ export function defaultClaworldAccountId(config = {}) {
463
+ return readDefaultAccountId(config) || 'default';
464
+ }
@@ -0,0 +1,114 @@
1
+ import {
2
+ logRuntimeBoundary,
3
+ serializeRuntimeBoundaryError,
4
+ } from '../../lib/runtime-errors.js';
5
+
6
+ export function createClaworldLifecycleManager({ connect, disconnect, logger = console } = {}) {
7
+ let started = false;
8
+ let connection = null;
9
+ let lastStartError = null;
10
+ let lastStartFailure = null;
11
+ let lastStopReason = null;
12
+
13
+ return {
14
+ async start(context = {}) {
15
+ if (started) {
16
+ logger.warn?.('[openclaw:lifecycle] start ignored: already started');
17
+ return {
18
+ started: true,
19
+ reused: true,
20
+ connection,
21
+ lastStartError,
22
+ lastStartFailure,
23
+ lastStopReason,
24
+ };
25
+ }
26
+
27
+ started = true;
28
+ lastStartError = null;
29
+ lastStartFailure = null;
30
+ lastStopReason = null;
31
+
32
+ try {
33
+ connection = (await connect?.(context)) || null;
34
+ logger.info?.('[openclaw:lifecycle] started');
35
+ return {
36
+ started: true,
37
+ reused: false,
38
+ connection,
39
+ lastStartError,
40
+ lastStartFailure,
41
+ lastStopReason,
42
+ };
43
+ } catch (error) {
44
+ started = false;
45
+ connection = null;
46
+ const normalized = logRuntimeBoundary(logger, '[openclaw:lifecycle] start failed', error, null, {
47
+ includeStack: false,
48
+ fallback: {
49
+ code: 'openclaw_lifecycle_start_failed',
50
+ category: 'bootstrap',
51
+ publicMessage: 'OpenClaw Claworld lifecycle start failed',
52
+ recoverable: true,
53
+ },
54
+ });
55
+ lastStartError = normalized.message;
56
+ lastStartFailure = serializeRuntimeBoundaryError(normalized);
57
+ throw normalized;
58
+ }
59
+ },
60
+ async stop(reason = 'manual_stop') {
61
+ if (!started) {
62
+ return {
63
+ started: false,
64
+ stopped: false,
65
+ reason: 'not_started',
66
+ lastStartError,
67
+ lastStartFailure,
68
+ lastStopReason,
69
+ };
70
+ }
71
+
72
+ started = false;
73
+ try {
74
+ await disconnect?.({ reason, connection });
75
+ } catch (error) {
76
+ connection = null;
77
+ const normalized = logRuntimeBoundary(logger, '[openclaw:lifecycle] stop failed', error, { reason }, {
78
+ includeStack: false,
79
+ fallback: {
80
+ code: 'openclaw_lifecycle_stop_failed',
81
+ category: 'runtime',
82
+ publicMessage: 'OpenClaw Claworld lifecycle stop failed',
83
+ recoverable: true,
84
+ },
85
+ });
86
+ throw normalized;
87
+ }
88
+ connection = null;
89
+ lastStopReason = reason;
90
+ logger.info?.(`[openclaw:lifecycle] stopped (${reason})`);
91
+ return {
92
+ started: false,
93
+ stopped: true,
94
+ reason,
95
+ lastStartError,
96
+ lastStartFailure,
97
+ lastStopReason,
98
+ };
99
+ },
100
+ async reconnect(context = {}) {
101
+ await this.stop('reconnect');
102
+ return this.start(context);
103
+ },
104
+ snapshot() {
105
+ return {
106
+ started,
107
+ hasConnection: Boolean(connection),
108
+ lastStartError,
109
+ lastStartFailure,
110
+ lastStopReason,
111
+ };
112
+ },
113
+ };
114
+ }