@xfxstudio/claworld 2026.4.30-testing.3 → 2026.5.3-testing.1

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.
@@ -18,6 +18,7 @@ import {
18
18
  buildClaworldToolMaintenanceEvent,
19
19
  ensureClaworldWorkingMemory,
20
20
  resolveClaworldBootstrapTarget,
21
+ updateClaworldSessionDirectory,
21
22
  } from '../runtime/working-memory.js';
22
23
  import { resolveOpenClawWorkspaceRoot } from '../runtime/workspace-resolver.js';
23
24
  import { setClaworldRuntime } from './runtime.js';
@@ -142,6 +143,33 @@ const CHAT_INBOX_FILTER_STATUSES = Object.freeze([
142
143
  'kickoff_failed',
143
144
  'ended',
144
145
  ]);
146
+ const CHAT_INBOX_FILTER_KEYS = Object.freeze([
147
+ 'direction',
148
+ 'mode',
149
+ 'status',
150
+ 'worldId',
151
+ 'chatRequestId',
152
+ 'conversationKey',
153
+ 'localSessionKey',
154
+ 'counterpartyAgentId',
155
+ ]);
156
+ const CHAT_INBOX_FILTER_KEY_SET = new Set(CHAT_INBOX_FILTER_KEYS);
157
+ const MANAGE_CONVERSATION_REQUEST_ONLY_QUERY_FIELDS = Object.freeze([
158
+ 'displayName',
159
+ 'agentCode',
160
+ 'openingMessage',
161
+ ]);
162
+ const MANAGE_CONVERSATION_FILTER_ONLY_TOP_LEVEL_FIELDS = Object.freeze([
163
+ 'mode',
164
+ 'status',
165
+ 'worldId',
166
+ 'counterpartyAgentId',
167
+ ]);
168
+ const MANAGE_CONVERSATION_GET_STATE_TARGET_FIELDS = Object.freeze([
169
+ 'chatRequestId',
170
+ 'conversationKey',
171
+ 'localSessionKey',
172
+ ]);
145
173
 
146
174
  const TERMINAL_ACCOUNT_ACTIONS = Object.freeze([
147
175
  'view_account',
@@ -292,6 +320,117 @@ function normalizeChatInboxListFiltersInput(params = {}) {
292
320
  );
293
321
  }
294
322
 
323
+ function hasProvidedToolParam(params = {}, fieldId) {
324
+ if (!params || typeof params !== 'object') return false;
325
+ if (!Object.prototype.hasOwnProperty.call(params, fieldId)) return false;
326
+ const value = params[fieldId];
327
+ if (typeof value === 'string') return normalizeText(value, null) != null;
328
+ return value != null;
329
+ }
330
+
331
+ function buildChatInboxFiltersParam({ description, worldIdProperty } = {}) {
332
+ return objectParam({
333
+ description,
334
+ additionalProperties: false,
335
+ properties: {
336
+ direction: stringParam({
337
+ description: 'Filter from the current account perspective.',
338
+ enumValues: CHAT_INBOX_FILTER_DIRECTIONS,
339
+ examples: ['outbound'],
340
+ }),
341
+ mode: stringParam({
342
+ description: 'Filter to direct or world-scoped chat items.',
343
+ enumValues: CHAT_INBOX_FILTER_MODES,
344
+ examples: ['world'],
345
+ }),
346
+ status: stringParam({
347
+ description: 'Filter to pending or terminal requests, or to chats by current status.',
348
+ enumValues: CHAT_INBOX_FILTER_STATUSES,
349
+ examples: ['active'],
350
+ }),
351
+ worldId: worldIdProperty,
352
+ chatRequestId: stringParam({
353
+ description: 'Filter to one canonical chat request id.',
354
+ minLength: 1,
355
+ examples: ['req_demo_1'],
356
+ }),
357
+ conversationKey: stringParam({
358
+ description: 'Filter to one canonical conversation key.',
359
+ minLength: 1,
360
+ examples: ['pair:agt_alice::agt_moza:world:dating-demo-world'],
361
+ }),
362
+ localSessionKey: stringParam({
363
+ description: 'Filter to one local Claworld session reference for internal tracking, summaries, or orchestration only. Not a transport address for sending a user message to the peer.',
364
+ minLength: 1,
365
+ examples: ['conversation:pair:agt_alice::agt_moza:world:dating-demo-world'],
366
+ }),
367
+ counterpartyAgentId: stringParam({
368
+ description: 'Filter to one counterparty agentId.',
369
+ minLength: 1,
370
+ examples: ['agt_alice'],
371
+ }),
372
+ },
373
+ });
374
+ }
375
+
376
+ function validateChatInboxFilterInput(filters = {}, action) {
377
+ const source = normalizeObject(filters, {}) || {};
378
+ for (const key of Object.keys(source)) {
379
+ if (CHAT_INBOX_FILTER_KEY_SET.has(key)) continue;
380
+ requireManageWorldField(`filters.${key}`, `filters.${key} is not supported for action=${action}`);
381
+ }
382
+ return source;
383
+ }
384
+
385
+ function normalizeManageConversationInboxQuery(params = {}, action) {
386
+ const normalizedAction = normalizeTerminalConversationAction(action, 'list_related');
387
+ const filters = validateChatInboxFilterInput(params.filters, normalizedAction);
388
+
389
+ const requestOnlyField = MANAGE_CONVERSATION_REQUEST_ONLY_QUERY_FIELDS.find((fieldId) => hasProvidedToolParam(params, fieldId));
390
+ if (requestOnlyField) {
391
+ requireManageWorldField(requestOnlyField, `${requestOnlyField} is only supported for action=request`);
392
+ }
393
+ if (hasProvidedToolParam(params, 'limit')) {
394
+ requireManageWorldField('limit', `limit is not supported for action=${normalizedAction}`);
395
+ }
396
+
397
+ const filterOnlyField = MANAGE_CONVERSATION_FILTER_ONLY_TOP_LEVEL_FIELDS.find((fieldId) => hasProvidedToolParam(params, fieldId));
398
+ if (filterOnlyField) {
399
+ requireManageWorldField(
400
+ filterOnlyField,
401
+ `${filterOnlyField} must be passed as filters.${filterOnlyField} for action=${normalizedAction}`,
402
+ );
403
+ }
404
+
405
+ if (normalizedAction !== 'get_state') {
406
+ const getStateOnlyField = MANAGE_CONVERSATION_GET_STATE_TARGET_FIELDS.find((fieldId) => hasProvidedToolParam(params, fieldId));
407
+ if (getStateOnlyField) {
408
+ requireManageWorldField(
409
+ getStateOnlyField,
410
+ `${getStateOnlyField} must be passed as filters.${getStateOnlyField} for action=${normalizedAction}`,
411
+ );
412
+ }
413
+ }
414
+
415
+ const mergedFilters = {
416
+ ...filters,
417
+ ...(!Object.prototype.hasOwnProperty.call(filters, 'direction') && hasProvidedToolParam(params, 'direction')
418
+ ? { direction: params.direction }
419
+ : {}),
420
+ ...(normalizedAction === 'get_state'
421
+ ? Object.fromEntries(
422
+ MANAGE_CONVERSATION_GET_STATE_TARGET_FIELDS
423
+ .filter((fieldId) => (
424
+ !Object.prototype.hasOwnProperty.call(filters, fieldId)
425
+ && hasProvidedToolParam(params, fieldId)
426
+ ))
427
+ .map((fieldId) => [fieldId, params[fieldId]]),
428
+ )
429
+ : {}),
430
+ };
431
+ return normalizeChatInboxListFiltersInput({ filters: mergedFilters });
432
+ }
433
+
295
434
  function parseToolResultPayload(result = null) {
296
435
  const text = result?.content?.[0]?.text;
297
436
  if (typeof text !== 'string') return null;
@@ -924,10 +1063,27 @@ function createTerminalToolAdapters(api, plugin, internalTools) {
924
1063
  agentCode: stringParam({ description: 'Target public agent code for request.', minLength: 1 }),
925
1064
  openingMessage: stringParam({ description: 'Request/re-engagement kickoff message.', minLength: 1 }),
926
1065
  worldId: worldIdProperty,
927
- filters: objectParam({ description: 'List filters.', additionalProperties: true }),
928
- chatRequestId: stringParam({ description: 'Request id for accept/reject.', minLength: 1 }),
929
- conversationKey: stringParam({ description: 'Conversation key for get_state/close.', minLength: 1 }),
930
- localSessionKey: stringParam({ description: 'Local conversation session key for get_state/close.', minLength: 1 }),
1066
+ direction: stringParam({
1067
+ description: 'Top-level alias for filters.direction on action=list_related/get_state.',
1068
+ enumValues: CHAT_INBOX_FILTER_DIRECTIONS,
1069
+ examples: ['outbound'],
1070
+ }),
1071
+ filters: buildChatInboxFiltersParam({
1072
+ description: 'Inbox filters for action=list_related/get_state.',
1073
+ worldIdProperty,
1074
+ }),
1075
+ chatRequestId: stringParam({
1076
+ description: 'Request id for action=accept/reject, or a top-level get_state convenience target that normalizes to filters.chatRequestId.',
1077
+ minLength: 1,
1078
+ }),
1079
+ conversationKey: stringParam({
1080
+ description: 'Conversation key for action=close, or a top-level get_state convenience target that normalizes to filters.conversationKey.',
1081
+ minLength: 1,
1082
+ }),
1083
+ localSessionKey: stringParam({
1084
+ description: 'Local conversation session key for action=close, or a top-level get_state convenience target that normalizes to filters.localSessionKey.',
1085
+ minLength: 1,
1086
+ }),
931
1087
  },
932
1088
  }),
933
1089
  async execute(toolCallId, params = {}) {
@@ -939,10 +1095,19 @@ function createTerminalToolAdapters(api, plugin, internalTools) {
939
1095
  });
940
1096
  return rewriteToolResultName(result, manageConversationsTool, action);
941
1097
  }
942
- if (['list_related', 'get_state', 'accept', 'reject'].includes(action)) {
1098
+ if (action === 'list_related' || action === 'get_state') {
1099
+ const filters = normalizeManageConversationInboxQuery(params, action);
943
1100
  const result = await requireTerminalTool(internalTools, 'claworld_chat_inbox').execute(toolCallId, {
944
1101
  ...params,
945
- action: ['list_related', 'get_state'].includes(action) ? 'list' : action,
1102
+ action: 'list',
1103
+ ...(Object.keys(filters).length > 0 ? { filters } : {}),
1104
+ });
1105
+ return rewriteToolResultName(result, manageConversationsTool, action);
1106
+ }
1107
+ if (action === 'accept' || action === 'reject') {
1108
+ const result = await requireTerminalTool(internalTools, 'claworld_chat_inbox').execute(toolCallId, {
1109
+ ...params,
1110
+ action,
946
1111
  });
947
1112
  return rewriteToolResultName(result, manageConversationsTool, action);
948
1113
  }
@@ -1690,45 +1855,10 @@ function buildRegisteredTools(api, plugin) {
1690
1855
  examples: ['list', 'accept', 'reject'],
1691
1856
  }),
1692
1857
  filters: objectParam({
1693
- description: 'Optional list filters for query mode. Omit to review the full inbox across inbound and outbound items.',
1694
- properties: {
1695
- direction: stringParam({
1696
- description: 'Filter from the current account perspective.',
1697
- enumValues: CHAT_INBOX_FILTER_DIRECTIONS,
1698
- examples: ['outbound'],
1699
- }),
1700
- mode: stringParam({
1701
- description: 'Filter to direct or world-scoped chat items.',
1702
- enumValues: CHAT_INBOX_FILTER_MODES,
1703
- examples: ['world'],
1704
- }),
1705
- status: stringParam({
1706
- description: 'Filter to pending or terminal requests, or to chats by current status.',
1707
- enumValues: CHAT_INBOX_FILTER_STATUSES,
1708
- examples: ['active'],
1709
- }),
1710
- worldId: worldIdProperty,
1711
- chatRequestId: stringParam({
1712
- description: 'Filter to one canonical chat request id.',
1713
- minLength: 1,
1714
- examples: ['req_demo_1'],
1715
- }),
1716
- conversationKey: stringParam({
1717
- description: 'Filter to one canonical conversation key.',
1718
- minLength: 1,
1719
- examples: ['pair:agt_alice::agt_moza:world:dating-demo-world'],
1720
- }),
1721
- localSessionKey: stringParam({
1722
- description: 'Filter to one local Claworld session reference for internal tracking, summaries, or orchestration only. Not a transport address for sending a user message to the peer.',
1723
- minLength: 1,
1724
- examples: ['conversation:pair:agt_alice::agt_moza:world:dating-demo-world'],
1725
- }),
1726
- counterpartyAgentId: stringParam({
1727
- description: 'Filter to one counterparty agentId.',
1728
- minLength: 1,
1729
- examples: ['agt_alice'],
1730
- }),
1731
- },
1858
+ ...buildChatInboxFiltersParam({
1859
+ description: 'Optional list filters for query mode. Omit to review the full inbox across inbound and outbound items.',
1860
+ worldIdProperty,
1861
+ }),
1732
1862
  }),
1733
1863
  chatRequestId: stringParam({
1734
1864
  description: 'Canonical chat request id returned by claworld_chat_inbox pendingRequests. Required for action=accept or action=reject.',
@@ -2095,13 +2225,45 @@ export function registerClaworldPluginFull(api, plugin) {
2095
2225
  });
2096
2226
 
2097
2227
  api.on('before_tool_call', async (event, ctx) => {
2098
- if (event?.toolName !== 'claworld_manage_conversations') return;
2228
+ const toolName = normalizeText(event?.toolName, null);
2229
+ if (!toolName || !toolName.startsWith('claworld_')) return;
2099
2230
  const params = event?.params && typeof event.params === 'object' && !Array.isArray(event.params)
2100
2231
  ? event.params
2101
2232
  : {};
2102
- if (normalizeTerminalConversationAction(params.action, null) !== 'request') return;
2103
2233
  const requesterSessionKey = normalizeText(ctx?.sessionKey, null);
2104
- if (!requesterSessionKey) return;
2234
+ if (requesterSessionKey) {
2235
+ const logger = getHookLogger(api);
2236
+ try {
2237
+ const workspaceRoot = await resolveHookWorkspaceRoot(api, event, ctx);
2238
+ if (workspaceRoot) {
2239
+ await updateClaworldSessionDirectory(
2240
+ workspaceRoot,
2241
+ {
2242
+ timestamp: event?.timestamp || ctx?.timestamp || null,
2243
+ source: 'claworld_hook',
2244
+ eventType: 'before_tool_call',
2245
+ kind: toolName,
2246
+ toolName,
2247
+ relations: {
2248
+ localSessionKey: requesterSessionKey,
2249
+ sessionKey: requesterSessionKey,
2250
+ localAgentId: normalizeText(ctx?.agentId ?? ctx?.AgentId, null),
2251
+ },
2252
+ context: ctx || {},
2253
+ },
2254
+ );
2255
+ }
2256
+ } catch (error) {
2257
+ logger?.warn?.('[claworld:working-memory] unable to update session directory before tool call', error);
2258
+ }
2259
+ }
2260
+ if (
2261
+ toolName !== 'claworld_manage_conversations'
2262
+ || normalizeTerminalConversationAction(params.action, null) !== 'request'
2263
+ || !requesterSessionKey
2264
+ ) {
2265
+ return;
2266
+ }
2105
2267
  return {
2106
2268
  params: {
2107
2269
  ...params,
@@ -2123,6 +2285,7 @@ export function registerClaworldPluginFull(api, plugin) {
2123
2285
  params: event?.params || {},
2124
2286
  result: hookToolResult(event),
2125
2287
  timestamp: event?.timestamp || ctx?.timestamp || null,
2288
+ context: ctx || {},
2126
2289
  });
2127
2290
  if (!maintenanceEvent) return;
2128
2291
  await appendClaworldJournalEvent(workspaceRoot, maintenanceEvent);
@@ -95,13 +95,23 @@ export function buildInboundEnvelope(message = {}) {
95
95
  'text',
96
96
  'body',
97
97
  'notification',
98
+ 'conversationKey',
99
+ 'worldId',
98
100
  ]) {
99
101
  if (payload[key] == null && data[key] != null) payload[key] = data[key];
100
102
  }
101
103
  }
104
+ const notification = payload.notification && typeof payload.notification === 'object' && !Array.isArray(payload.notification)
105
+ ? payload.notification
106
+ : data.notification && typeof data.notification === 'object' && !Array.isArray(data.notification)
107
+ ? data.notification
108
+ : {};
102
109
  const targetAgentId = normalizeEnvelopeText(
103
110
  data.targetAgentId,
104
- normalizeEnvelopeText(payload.targetAgentId, null),
111
+ normalizeEnvelopeText(
112
+ payload.targetAgentId,
113
+ normalizeEnvelopeText(notification.targetAgentId, normalizeEnvelopeText(metadata.targetAgentId, null)),
114
+ ),
105
115
  );
106
116
  const sessionKey = normalizeEnvelopeText(
107
117
  data.sessionKey,
@@ -109,23 +119,48 @@ export function buildInboundEnvelope(message = {}) {
109
119
  payload.sessionKey,
110
120
  normalizeEnvelopeText(
111
121
  data.targetSessionKey,
112
- normalizeEnvelopeText(payload.targetSessionKey, null),
122
+ normalizeEnvelopeText(
123
+ payload.targetSessionKey,
124
+ normalizeEnvelopeText(
125
+ notification.targetSessionKey,
126
+ normalizeEnvelopeText(metadata.sessionKey, targetAgentId ? `management:${targetAgentId}` : null),
127
+ ),
128
+ ),
113
129
  ),
114
130
  ),
115
131
  );
116
132
  const isDeliveryEvent = message.event === 'delivery';
117
133
  const isRoutableEvent = Boolean(eventType && sessionKey);
118
134
  if (!isDeliveryEvent && !isRoutableEvent) return null;
135
+ const deliveryId = resolveEnvelopeMessageId(data, payload);
136
+ const eventName = normalizeEnvelopeText(
137
+ data.eventName,
138
+ normalizeEnvelopeText(payload.eventName, isDeliveryEvent ? null : normalizeEnvelopeText(message.event, null)),
139
+ );
119
140
  return {
120
141
  eventType: eventType || 'delivery',
121
- deliveryId: resolveEnvelopeMessageId(data, payload),
142
+ eventName,
143
+ eventId: deliveryId,
144
+ deliveryId,
122
145
  sessionKey,
123
146
  targetAgentId,
124
- createdAt: data.createdAt || data.availableAt || null,
125
- updatedAt: data.updatedAt || null,
147
+ conversationKey: normalizeEnvelopeText(
148
+ data.conversationKey,
149
+ normalizeEnvelopeText(payload.conversationKey, normalizeEnvelopeText(notification.relatedObjects?.conversationKey, null)),
150
+ ),
151
+ worldId: normalizeEnvelopeText(
152
+ data.worldId,
153
+ normalizeEnvelopeText(payload.worldId, normalizeEnvelopeText(notification.relatedObjects?.worldId, null)),
154
+ ),
155
+ createdAt: data.createdAt || payload.createdAt || data.availableAt || payload.availableAt || notification.createdAt || null,
156
+ updatedAt: data.updatedAt || payload.updatedAt || notification.updatedAt || null,
126
157
  turnCreatedAt: data.turnCreatedAt || null,
127
158
  payload,
128
- metadata,
159
+ metadata: {
160
+ ...metadata,
161
+ relayEvent: normalizeEnvelopeText(message.event, null),
162
+ inboxItemId: normalizeEnvelopeText(data.inboxItemId, normalizeEnvelopeText(payload.inboxItemId, null)),
163
+ },
129
164
  };
130
165
  }
131
166
 
@@ -1,6 +1,15 @@
1
1
  export const CLAWORLD_PLUGIN_BRIDGE_PROTOCOL = 'claworld.delivery_reply.v1';
2
2
 
3
3
  const DELIVERY_EVENT_TYPE = 'delivery';
4
+ const MANAGEMENT_EVENT_TYPES = new Set([
5
+ 'notification',
6
+ 'domain_notification',
7
+ 'management_wake',
8
+ 'management_tick',
9
+ 'conversation_lifecycle',
10
+ 'platform_recommendation',
11
+ 'ops_recommendation',
12
+ ]);
4
13
 
5
14
  function normalizeText(value, fallback = null) {
6
15
  if (value == null) return fallback;
@@ -16,27 +25,28 @@ function normalizePayload(payload = null) {
16
25
  export function createRelayEventProtocol() {
17
26
  return {
18
27
  version: CLAWORLD_PLUGIN_BRIDGE_PROTOCOL,
19
- eventTypes: [DELIVERY_EVENT_TYPE],
20
- requiredEnvelopeFields: ['eventType', 'deliveryId', 'sessionKey', 'payload'],
28
+ eventTypes: [DELIVERY_EVENT_TYPE, ...MANAGEMENT_EVENT_TYPES],
29
+ requiredEnvelopeFields: ['eventType', 'sessionKey', 'payload'],
21
30
  describeEvent(event = {}) {
22
31
  const payload = normalizePayload(event.payload);
23
32
  const missing = [];
24
- if (normalizeText(event.eventType, null) !== DELIVERY_EVENT_TYPE) {
33
+ const eventType = normalizeText(event.eventType, null);
34
+ if (eventType !== DELIVERY_EVENT_TYPE && !MANAGEMENT_EVENT_TYPES.has(eventType)) {
25
35
  missing.push('eventType');
26
36
  }
27
- if (!normalizeText(event.deliveryId, null)) {
37
+ if (eventType === DELIVERY_EVENT_TYPE && !normalizeText(event.deliveryId, null)) {
28
38
  missing.push('deliveryId');
29
39
  }
30
40
  if (!normalizeText(event.sessionKey, null)) {
31
41
  missing.push('sessionKey');
32
42
  }
33
- if (!normalizeText(payload.text, null)) {
43
+ if (eventType === DELIVERY_EVENT_TYPE && !normalizeText(payload.text, null)) {
34
44
  missing.push('payload.text');
35
45
  }
36
46
  return {
37
47
  ok: missing.length === 0,
38
48
  missing,
39
- role: 'delivery',
49
+ role: eventType === DELIVERY_EVENT_TYPE ? 'delivery' : 'management',
40
50
  };
41
51
  },
42
52
  };