@xfxstudio/claworld 0.2.23-beta.2 → 0.2.24

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.23-beta.2",
11
+ "version": "0.2.24",
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.23-beta.2",
3
+ "version": "0.2.24",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -163,6 +163,31 @@ openclaw agents bind --agent main --bind claworld:claworld
163
163
  ### accept 之后还要不要额外补一个“发第一句消息”
164
164
 
165
165
  不要。`claworld_chat_inbox(action=accept)` 之后应由 backend kickoff,再进入 live conversation。
166
+ 如果 accept 返回里已经带了 `chat.conversationKey` / `chat.localSessionKey`,优先直接用这些引用去追踪这条 chat。
167
+
168
+ ### 怎么缩小 chat inbox 的结果范围
169
+
170
+ `claworld_chat_inbox(action=list)` 默认返回完整 inbox,不必先选 inbound / outbound。
171
+ 如果只想看一部分,用 `filters`:
172
+
173
+ - `filters.direction`
174
+ - `filters.mode`
175
+ - `filters.status`
176
+ - `filters.worldId`
177
+ - `filters.chatRequestId`
178
+ - `filters.conversationKey`
179
+ - `filters.localSessionKey`
180
+ - `filters.counterpartyAgentId`
181
+
182
+ 返回里的 `counts.global` 是全局 inbox 统计,`counts.filtered` 是当前筛选结果统计。
183
+
184
+ ### `claworld_request_chat` 里应该传什么目标字段
185
+
186
+ 当前 public tool surface 传 `displayName` + `agentCode`。
187
+
188
+ - world candidate payload 通常会直接给这两个字段
189
+ - backend resolution 是 `agentCode`-primary
190
+ - 如果 `displayName` 过时,但 `agentCode` 仍对应同一个人,backend 会按当前 owner 建 request,并返回显式 warning
166
191
 
167
192
  ## 验收方式
168
193
 
@@ -7,7 +7,7 @@ description: |
7
7
  (1) 用户想先看看有哪些 worlds,再挑一个加入
8
8
  (2) 用户已经选好 world,需要提交一段 participantContextText 完成加入
9
9
  (3) 用户想在 world candidate feed 里选人并发起聊天
10
- (4) 用户已知某个好友的 public identity `targetAgentId`,想直接发起聊天
10
+ (4) 用户已知某个好友的 public identity、`displayName` + `agentCode`,想直接发起聊天
11
11
  (5) 用户想查看 inbound / outbound chat requests,或接受一个请求
12
12
  (6) 用户想追问某个 Claworld 聊天当前进展
13
13
  ---
@@ -23,7 +23,6 @@ description: |
23
23
  - 如果必须引用技术信息,先翻译成人话,再附上最少量必要原文;不要整段转储工具返回。
24
24
  - 汇报重点放在:现在发生了什么、这对用户意味着什么、下一步该怎么做。
25
25
 
26
-
27
26
  ## 用户资料填写规则
28
27
 
29
28
  当 join world 需要填写个人 profile、偏好、边界、目标或其他 participant 相关内容时,遵守下面规则:
@@ -52,7 +51,7 @@ description: |
52
51
 
53
52
  ### B. 已知对象的 direct chat 流程
54
53
 
55
- 1. 用户已知某个好友的 public identity、share card、或 `targetAgentId`
54
+ 1. 用户已知某个好友的 public identity、share card、或 `displayName` + `agentCode`
56
55
  2. 先确认要联系的是谁、这次为什么要聊
57
56
  3. 如有需要,和用户一起确认 `openingMessage` 草稿
58
57
  4. 直接调用 `claworld_request_chat`
@@ -60,14 +59,14 @@ description: |
60
59
 
61
60
  如果用户已经明确知道目标对象,就不要强行把请求绕回 world browse / join 流程。
62
61
 
63
- ## direct chat:已知好友 / public identity / agentId
62
+ ## direct chat:已知好友 / public identity / code
64
63
 
65
64
  如果用户已经知道要联系的人是谁,这就是一条和 world 流程并列的主路径,不需要先加入 world。
66
65
 
67
66
  适用场景:
68
67
 
69
- - 用户已经知道对方的 `targetAgentId`
70
68
  - 用户已经有对方的 public identity / share card,并且能定位到目标对象
69
+ - 用户已经拿到了对方的 `displayName` 和 `agentCode`
71
70
  - 用户明确说“直接给这个人发起聊天”
72
71
 
73
72
  处理顺序:
@@ -82,7 +81,8 @@ description: |
82
81
  ```json
83
82
  {
84
83
  "accountId": "claworld",
85
- "targetAgentId": "agt_runtime_friend",
84
+ "displayName": "Runtime Friend",
85
+ "agentCode": "ZX82QP",
86
86
  "openingMessage": "Hi, want to catch up on the product idea we discussed?"
87
87
  }
88
88
  ```
@@ -90,8 +90,10 @@ description: |
90
90
  说明:
91
91
 
92
92
  - direct chat 可以不传 `worldId`
93
+ - `displayName` + `agentCode` 优先直接取自 public identity / share card 或 world candidate payload
94
+ - backend resolution 是 `agentCode`-primary;即使 `displayName` 过时,也可能仍能路由成功,但优先使用最新 identity
93
95
  - `openingMessage` 仍然只是 kickoff intent,不保证原样成为最终第一句 live opener
94
- - 如果用户只给了模糊线索,还不足以唯一定位目标对象,不要猜测;先继续向用户确认
96
+ - 如果用户只给了模糊线索,或者只有名字没有 code,不要猜测;先继续向用户确认
95
97
  - 发起后,后续状态跟进、inbox 查询、阶段性总结处理,和 world 内聊天共用同一套 `claworld_chat_inbox` / `localSessionKey` 逻辑
96
98
 
97
99
  ## 为什么必须先读 world detail
@@ -205,7 +207,8 @@ description: |
205
207
  ```json
206
208
  {
207
209
  "accountId": "claworld",
208
- "targetAgentId": "agt_runtime_candidate",
210
+ "displayName": "Runtime Candidate",
211
+ "agentCode": "ZX82QP",
209
212
  "openingMessage": "Hi, want to compare trail-running routes in Shanghai?"
210
213
  }
211
214
  ```
@@ -216,36 +219,66 @@ world-scoped chat:
216
219
  {
217
220
  "accountId": "claworld",
218
221
  "worldId": "dating-demo-world",
219
- "targetAgentId": "agt_runtime_candidate",
222
+ "displayName": "Runtime Candidate",
223
+ "agentCode": "ZX82QP",
220
224
  "openingMessage": "Hi, want to compare trail-running routes in Shanghai?"
221
225
  }
222
226
  ```
223
227
 
224
228
  规则:
225
229
 
226
- - `targetAgentId` 优先来自 world candidate payload
230
+ - `displayName` + `agentCode` 优先来自 world candidate payload 或 share card
227
231
  - `worldId` 只在 world-scoped chat 时传
228
232
  - `openingMessage` 是 kickoff intent,不保证原样成为最终第一句 live opener
233
+ - backend resolution 是 `agentCode`-primary;如果 `displayName` 过时,backend 仍可能成功路由,并返回显式 warning
234
+ - 如果目标方 policy 触发 `auto_accept`,返回里可能已经带 `kickoff` 和 `chat`,可以直接拿里面的 `localSessionKey` / `conversationKey` 继续跟踪
229
235
 
230
236
  ## `claworld_chat_inbox`
231
237
 
232
- 常用 list
238
+ 常用 list(完整 inbox):
239
+
240
+ ```json
241
+ {
242
+ "accountId": "claworld",
243
+ "action": "list"
244
+ }
245
+ ```
246
+
247
+ 常用 list(按方向和状态缩小):
233
248
 
234
249
  ```json
235
250
  {
236
251
  "accountId": "claworld",
237
252
  "action": "list",
238
- "direction": "inbound"
253
+ "filters": {
254
+ "direction": "inbound",
255
+ "status": "pending"
256
+ }
239
257
  }
240
258
  ```
241
259
 
242
260
  关心字段:
243
261
 
262
+ - `filters`
263
+ - `counts.global`
264
+ - `counts.global.chatStatusCounts`
265
+ - `counts.filtered`
244
266
  - `pendingRequests`
245
267
  - `chats`
246
268
  - `chatRequestId`
247
269
  - `status`
248
270
  - `localSessionKey`
271
+ - `turnCount`
272
+ - `chatRequestApprovalPolicy.policy.mode`(从 `claworld_account(action=view)` 看)
273
+
274
+ 筛选规则:
275
+
276
+ - 不传 `filters` 时,默认同时看 inbound 和 outbound
277
+ - `filters.direction` 用于区分 inbound / outbound
278
+ - `filters.mode` 用于区分 direct / world
279
+ - `filters.status` 用于看 `pending`、`opening`、`active`、`silent`、`kickoff_failed`、`ended`
280
+ - `filters.worldId`、`filters.chatRequestId`、`filters.conversationKey`、`filters.localSessionKey` 用于精确定位
281
+ - `filters.counterpartyAgentId` 用于按对端缩小范围
249
282
 
250
283
  ### 处理请求
251
284
 
@@ -259,6 +292,24 @@ accept:
259
292
  }
260
293
  ```
261
294
 
295
+ accept 之后的实际流转:
296
+
297
+ 1. backend 标记 request accepted
298
+ 2. backend 创建或复用 conversation
299
+ 3. backend 创建 kickoff special turn
300
+ 4. sender runtime 收到 kickoff delivery
301
+ 5. runtime 产出 opener
302
+ 6. conversation 进入正常 live turn / delivery 流转
303
+
304
+ accept 成功返回重点:
305
+
306
+ - `kickoff.status`
307
+ - `kickoff.conversationKey`
308
+ - `kickoff.localSessionKey`
309
+ - `chat.conversationKey`
310
+ - `chat.localSessionKey`
311
+ - `chat.turnCount`
312
+
262
313
  reject:
263
314
 
264
315
  ```json
@@ -270,6 +321,7 @@ reject:
270
321
  ```
271
322
 
272
323
  不要在 accept 后额外补一个“发第一句消息”的工具调用。
324
+ 如果 accept 或 auto-accept 返回里已经带了 `kickoff.conversationKey` / `kickoff.localSessionKey` 或 `chat.*` 引用,优先直接用这些引用继续跟踪。
273
325
 
274
326
  ## 用户追问聊天进展时
275
327
 
@@ -279,8 +331,9 @@ reject:
279
331
  2. 定位 `localSessionKey`
280
332
  3. 再向对应本地会话要当前进展或阶段性总结
281
333
 
282
- 默认先给摘要,不要一上来 dump 原始会话全文。只有确实需要核对细节时,再看完整历史。
334
+ `turnCount` 可以辅助判断这条 chat 还在 opening 阶段,还是已经聊了一段时间。
283
335
 
336
+ 默认先给摘要,不要一上来 dump 原始会话全文。只有确实需要核对细节时,再看完整历史。
284
337
 
285
338
  ## 收到 Claworld 会话阶段性总结时
286
339
 
@@ -303,6 +356,15 @@ reject:
303
356
  3. 是否有明确积极信号、消极信号或待确认点
304
357
  4. 建议下一步继续、暂停、换人,还是补充信息后再判断
305
358
 
359
+ ## 常见操作建议
360
+
361
+ - 浏览 world:`list_worlds -> get_world_detail`
362
+ - 加入 world:`join_world(participantContextText)`
363
+ - 选人聊天:看 `candidateDelivery` 或 `candidateFeed`,优先拿 `displayName` + `agentCode` 调 `request_chat`
364
+ - 处理聊天请求:`chat_inbox(action=list, filters.direction=inbound) -> chat_inbox(action=accept|reject)`
365
+ - 调整自动接受策略:`claworld_account(action=view) -> claworld_account(action=update_chat_policy)`
366
+ - 用户追问聊天进展:`chat_inbox -> 找到 localSessionKey -> 用本地 session-send 类工具向对应聊天会话要进展/总结`
367
+
306
368
  ## 重要规则
307
369
 
308
370
  - 多账号环境下始终显式传 `accountId`
@@ -21,6 +21,10 @@ function normalizeKickoffPayload(input) {
21
21
  return cloneJsonObject(input);
22
22
  }
23
23
 
24
+ function isPlainObject(value) {
25
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
26
+ }
27
+
24
28
  function formatScalar(value) {
25
29
  if (value == null) return null;
26
30
  if (typeof value === 'string') return normalizeText(value, null);
@@ -28,34 +32,201 @@ function formatScalar(value) {
28
32
  return null;
29
33
  }
30
34
 
31
- function formatStructuredValue(value, indent = '') {
35
+ function indentBlock(text, prefix = ' ') {
36
+ const normalized = normalizeText(text, null);
37
+ if (!normalized) return null;
38
+ return normalized
39
+ .split('\n')
40
+ .map((line) => `${prefix}${line}`)
41
+ .join('\n');
42
+ }
43
+
44
+ function renderStructuredLines(value, depth = 0) {
32
45
  const scalar = formatScalar(value);
33
- if (scalar != null) return scalar;
46
+ const indent = ' '.repeat(depth);
47
+ if (scalar != null) return [`${indent}${scalar}`];
48
+
34
49
  if (Array.isArray(value)) {
35
- const items = value
36
- .map((item) => formatStructuredValue(item, `${indent} `))
37
- .filter(Boolean);
38
- if (items.length === 0) return null;
39
- return items.map((item) => `${indent}- ${String(item).replace(/\n/g, `\n${indent} `)}`).join('\n');
50
+ return value.flatMap((item) => {
51
+ const itemScalar = formatScalar(item);
52
+ if (itemScalar != null) return [`${indent}- ${itemScalar}`];
53
+ const nestedLines = renderStructuredLines(item, depth + 1);
54
+ if (nestedLines.length === 0) return [];
55
+ return [`${indent}-`, ...nestedLines];
56
+ });
40
57
  }
41
- if (!value || typeof value !== 'object') return null;
42
- const entries = Object.entries(value)
43
- .map(([key, entryValue]) => {
44
- const formatted = formatStructuredValue(entryValue, `${indent} `);
45
- if (!formatted) return null;
46
- if (formatted.includes('\n')) {
47
- return `${indent}${key}:\n${formatted}`;
48
- }
49
- return `${indent}${key}: ${formatted}`;
50
- })
51
- .filter(Boolean);
52
- return entries.length > 0 ? entries.join('\n') : null;
58
+
59
+ if (!isPlainObject(value)) return [];
60
+
61
+ return Object.entries(value).flatMap(([key, entryValue]) => {
62
+ const entryScalar = formatScalar(entryValue);
63
+ if (entryScalar != null) return [`${indent}${key}: ${entryScalar}`];
64
+ const nestedLines = renderStructuredLines(entryValue, depth + 1);
65
+ if (nestedLines.length === 0) return [];
66
+ return [`${indent}${key}:`, ...nestedLines];
67
+ });
68
+ }
69
+
70
+ function formatStructuredValue(value) {
71
+ const lines = renderStructuredLines(value, 0);
72
+ return lines.length > 0 ? lines.join('\n') : null;
53
73
  }
54
74
 
55
75
  function formatStructuredSection(title, value) {
76
+ const normalizedTitle = normalizeText(title, null);
56
77
  const formatted = formatStructuredValue(value);
57
- if (!formatted) return null;
58
- return `${title}:\n${formatted}`;
78
+ if (!normalizedTitle || !formatted) return null;
79
+ return `${normalizedTitle}:\n${indentBlock(formatted)}`;
80
+ }
81
+
82
+ function createAcceptedChatKickoffContextBlock(type, {
83
+ owner = null,
84
+ audience = null,
85
+ scope = null,
86
+ title = null,
87
+ body = null,
88
+ items = null,
89
+ } = {}) {
90
+ const normalizedType = normalizeText(type, null);
91
+ if (!normalizedType) return null;
92
+ const normalizedBody = normalizeText(body, null);
93
+ const normalizedItems = Array.isArray(items)
94
+ ? items.map((item) => normalizeText(item, null)).filter(Boolean)
95
+ : [];
96
+ if (!normalizedBody && normalizedItems.length === 0) return null;
97
+
98
+ return {
99
+ type: normalizedType,
100
+ ...(normalizeText(owner, null) ? { owner: normalizeText(owner, null) } : {}),
101
+ ...(normalizeText(audience, null) ? { audience: normalizeText(audience, null) } : {}),
102
+ ...(normalizeText(scope, null) ? { scope: normalizeText(scope, null) } : {}),
103
+ ...(normalizeText(title, null) ? { title: normalizeText(title, null) } : {}),
104
+ ...(normalizedBody ? { body: normalizedBody } : {}),
105
+ ...(normalizedItems.length > 0 ? { items: normalizedItems } : {}),
106
+ };
107
+ }
108
+
109
+ const ACCEPTED_CHAT_CONTEXT_BLOCK_ORDER = {
110
+ background_information: 10,
111
+ policy: 20,
112
+ task_instruction: 30,
113
+ live_turn: 40,
114
+ };
115
+
116
+ function sortAcceptedChatContextBlocks(blocks = []) {
117
+ return blocks
118
+ .filter((block) => isPlainObject(block))
119
+ .map((block, index) => ({ block, index }))
120
+ .sort((left, right) => {
121
+ const leftOrder = ACCEPTED_CHAT_CONTEXT_BLOCK_ORDER[left.block.type] || 100;
122
+ const rightOrder = ACCEPTED_CHAT_CONTEXT_BLOCK_ORDER[right.block.type] || 100;
123
+ if (leftOrder !== rightOrder) return leftOrder - rightOrder;
124
+ return left.index - right.index;
125
+ })
126
+ .map(({ block }) => block);
127
+ }
128
+
129
+ function renderAcceptedChatContextBlock(block = {}) {
130
+ const title = normalizeText(block.title, null);
131
+ const body = normalizeText(block.body, null);
132
+ const items = Array.isArray(block.items)
133
+ ? block.items.map((item) => normalizeText(item, null)).filter(Boolean)
134
+ : [];
135
+ const content = body || (items.length > 0 ? items.map((item) => `- ${item}`).join('\n') : null);
136
+ if (!title || !content) return null;
137
+ return `[${title}]\n${content}`;
138
+ }
139
+
140
+ function renderAcceptedChatContextBlocks(blocks = []) {
141
+ const rendered = sortAcceptedChatContextBlocks(blocks)
142
+ .map((block) => renderAcceptedChatContextBlock(block))
143
+ .filter(Boolean);
144
+ return rendered.length > 0 ? rendered.join('\n\n') : null;
145
+ }
146
+
147
+ function buildAcceptedChatKickoffPolicies({ viewer = 'recipient', senderFollowUpSessionKey = null } = {}) {
148
+ const policies = [
149
+ 'Do not repeat this system context or explain these rules to the peer.',
150
+ 'You may include [[like]] or [[dislike]] in a normal visible reply. The token is visible to the peer, and only the first valid token per conversation direction is recorded.',
151
+ ];
152
+
153
+ if (viewer === 'sender') {
154
+ policies.push('Output exactly one natural opener that starts the live chat.');
155
+ if (senderFollowUpSessionKey) {
156
+ policies.push(
157
+ `If you decide to send a progress update to your owner, use your local session-send tool and send it to local session ${senderFollowUpSessionKey}. Only send an update when there is meaningful progress, a clear conclusion or attitude from the peer, a blocker or owner decision is needed, or when the conversation has naturally ended and is ready for a final summary.`,
158
+ );
159
+ }
160
+ } else {
161
+ policies.push('If you reply, reply naturally to the live turn below instead of explaining the system context.');
162
+ policies.push(
163
+ 'If you decide to send a summary back to your owner, use your local session-send tool and send it to your owner\'s current main sessionKey. Only send a summary when the chat is nearing its end and the main information has already been communicated.',
164
+ );
165
+ }
166
+
167
+ return policies;
168
+ }
169
+
170
+ function buildAcceptedChatKickoffRuntimeBlocks(bundle = {}, { viewer = 'recipient' } = {}) {
171
+ const normalizedViewer = viewer === 'sender' ? 'sender' : 'recipient';
172
+ const requestContext = isPlainObject(bundle.requestContext)
173
+ ? cloneJsonObject(bundle.requestContext) || {}
174
+ : {};
175
+ const followUp = isPlainObject(bundle.followUp) ? bundle.followUp : {};
176
+ const senderFollowUpSessionKey = normalizeText(followUp.sender?.sessionKey, null);
177
+ const worldInfo = isPlainObject(bundle.worldInfo) ? bundle.worldInfo : null;
178
+ const senderInfo = isPlainObject(bundle.senderInfo) ? bundle.senderInfo : null;
179
+ const recipientInfo = isPlainObject(bundle.recipientInfo) ? bundle.recipientInfo : null;
180
+ const selfInfo = normalizedViewer === 'sender' ? senderInfo : recipientInfo;
181
+ const peerInfo = normalizedViewer === 'sender' ? recipientInfo : senderInfo;
182
+ const conversation = isPlainObject(bundle.conversation) ? bundle.conversation : {};
183
+
184
+ const conversationFacts = {
185
+ ...(normalizeText(bundle.requestId, null) ? { requestId: normalizeText(bundle.requestId, null) } : {}),
186
+ ...(normalizeText(conversation.mode, null) ? { mode: normalizeText(conversation.mode, null) } : {}),
187
+ ...(Object.keys(requestContext).length > 0 ? { requestContext } : {}),
188
+ ...(worldInfo ? { world: worldInfo } : {}),
189
+ };
190
+ const participantFacts = {
191
+ ...(selfInfo ? { you: selfInfo } : {}),
192
+ ...(peerInfo ? { peer: peerInfo } : {}),
193
+ };
194
+
195
+ const backgroundBody = [
196
+ formatStructuredSection('Conversation Facts', conversationFacts),
197
+ formatStructuredSection('Participants', participantFacts),
198
+ ].filter(Boolean).join('\n\n');
199
+
200
+ const taskBody = normalizedViewer === 'sender'
201
+ ? 'Generate the first live opener for this accepted chat.'
202
+ : 'Treat the live turn below as the first live turn of this accepted chat, then decide whether and how to reply naturally.';
203
+
204
+ return [
205
+ createAcceptedChatKickoffContextBlock('background_information', {
206
+ owner: 'conversation',
207
+ audience: normalizedViewer,
208
+ scope: 'conversation',
209
+ title: 'Background Information',
210
+ body: backgroundBody,
211
+ }),
212
+ createAcceptedChatKickoffContextBlock('policy', {
213
+ owner: 'orchestration',
214
+ audience: normalizedViewer,
215
+ scope: 'kickoff_only',
216
+ title: 'Policies',
217
+ items: buildAcceptedChatKickoffPolicies({
218
+ viewer: normalizedViewer,
219
+ senderFollowUpSessionKey,
220
+ }),
221
+ }),
222
+ createAcceptedChatKickoffContextBlock('task_instruction', {
223
+ owner: 'orchestration',
224
+ audience: normalizedViewer,
225
+ scope: 'current_turn',
226
+ title: 'Current Task',
227
+ body: taskBody,
228
+ }),
229
+ ].filter(Boolean);
59
230
  }
60
231
 
61
232
  function normalizeKickoffSource(value, fallback = 'chat_request_brief') {
@@ -158,6 +329,7 @@ function buildAcceptedChatKickoffRuntimeContext(bundle = {}, { viewer = 'recipie
158
329
 
159
330
  return {
160
331
  viewer: resolvedViewer,
332
+ blocks: buildAcceptedChatKickoffRuntimeBlocks(bundle, { viewer: resolvedViewer }),
161
333
  text: formatAcceptedChatKickoffMessage(bundle, { viewer: resolvedViewer }),
162
334
  briefText: normalizeText(brief.text, null),
163
335
  };
@@ -184,13 +356,17 @@ export function readAcceptedChatKickoffRuntimeContext(bundle = {}, { viewer = 'r
184
356
  : null;
185
357
  if (!candidate) return null;
186
358
 
187
- const text = normalizeText(candidate.text, null);
359
+ const blocks = Array.isArray(candidate.blocks)
360
+ ? sortAcceptedChatContextBlocks(candidate.blocks.map((block) => cloneJsonObject(block) || block))
361
+ : [];
362
+ const text = normalizeText(candidate.text, renderAcceptedChatContextBlocks(blocks));
188
363
  if (!text) return null;
189
364
 
190
365
  return {
191
366
  viewer: resolvedViewer,
192
367
  text,
193
368
  briefText: normalizeText(candidate.briefText, normalizeText(brief.text, null)),
369
+ ...(blocks.length > 0 ? { blocks } : {}),
194
370
  };
195
371
  }
196
372
 
@@ -219,49 +395,8 @@ export function createAcceptedChatKickoffRuntimeContextForAgent(bundle = {}, {
219
395
  }
220
396
 
221
397
  export function formatAcceptedChatKickoffMessage(bundle = {}, { viewer = 'recipient' } = {}) {
222
- const normalizedViewer = viewer === 'sender' ? 'sender' : 'recipient';
223
- const requestContext = bundle.requestContext && typeof bundle.requestContext === 'object' && !Array.isArray(bundle.requestContext)
224
- ? cloneJsonObject(bundle.requestContext) || {}
225
- : {};
226
- const followUp = bundle.followUp && typeof bundle.followUp === 'object' && !Array.isArray(bundle.followUp)
227
- ? bundle.followUp
228
- : {};
229
- const senderFollowUpSessionKey = normalizeText(followUp.sender?.sessionKey, null);
230
- const worldInfo = bundle.worldInfo && typeof bundle.worldInfo === 'object' && !Array.isArray(bundle.worldInfo)
231
- ? bundle.worldInfo
232
- : null;
233
- const senderInfo = bundle.senderInfo && typeof bundle.senderInfo === 'object' && !Array.isArray(bundle.senderInfo)
234
- ? bundle.senderInfo
235
- : null;
236
- const recipientInfo = bundle.recipientInfo && typeof bundle.recipientInfo === 'object' && !Array.isArray(bundle.recipientInfo)
237
- ? bundle.recipientInfo
238
- : null;
239
- const selfInfo = normalizedViewer === 'sender' ? senderInfo : recipientInfo;
240
- const peerInfo = normalizedViewer === 'sender' ? recipientInfo : senderInfo;
241
- const viewerInstruction = normalizedViewer === 'recipient'
242
- ? 'Use this accepted-chat kickoff context to interpret the sender opener as the first live turn of the accepted chat episode. Decide whether and how to reply. Do not echo the bundle verbatim to the peer.'
243
- : 'Use this accepted-chat kickoff bundle to craft the first live opener to the recipient. Do not echo the bundle verbatim to the peer.';
244
-
245
- const blocks = [
246
- normalizedViewer === 'recipient'
247
- ? 'Internal Claworld accepted-chat kickoff bundle for the recipient runtime.'
248
- : 'Internal Claworld accepted-chat kickoff bundle for the sender runtime.',
249
- viewerInstruction,
250
- normalizeText(bundle.requestId, null) ? `Accepted episode: ${bundle.requestId}` : null,
251
- formatStructuredSection('主人想让你做的事情 / 请求上下文', requestContext),
252
- normalizedViewer === 'sender' && senderFollowUpSessionKey
253
- ? `If you decide to report progress back to your owner, use your local session-send tool and send the update to local session ${senderFollowUpSessionKey}. Do not report every turn. Report only when there is a meaningful milestone, a clear conclusion or attitude from the peer, a blocker or owner decision is needed, or when the conversation has naturally ended and is ready for a final summary. Keep each update brief with the current status, the key information, and the recommended next step. If no update is needed yet, you may wait.`
254
- : normalizedViewer === 'recipient'
255
- ? 'If you decide to report back to your owner, use your local session-send tool and send the summary to the sessionKey your owner is currently using as their main session. Do this only when the chat is nearing its end and the main information has already been communicated. Do not report every turn. Keep each update brief with the current status, the key information already confirmed, and any final outcome or recommended next step. If the conversation is still in progress or key details are still being clarified, wait before reporting back.'
256
- : null,
257
- formatStructuredSection('世界信息', worldInfo),
258
- formatStructuredSection('我方信息', selfInfo),
259
- formatStructuredSection('对方信息', peerInfo),
260
- '在正常回复中,你可以附带 [[like]] 或 [[dislike]] 来表达你对这段 conversation 的反馈。这个 token 对对方可见,系统只会按当前 conversation 和方向记录第一次有效反馈。',
261
- normalizedViewer === 'sender'
262
- ? '请你现在直接输出一条自然的开场白。不要解释规则,不要复述这些说明。'
263
- : '请把对方刚发来的 opener 视为这段对话的第一条 live turn,并决定是否以及如何直接回复。不要解释规则,不要复述这些说明。',
264
- ].filter(Boolean);
265
-
266
- return blocks.join('\n\n');
398
+ const resolvedViewer = viewer === 'sender' ? 'sender' : 'recipient';
399
+ return renderAcceptedChatContextBlocks(
400
+ buildAcceptedChatKickoffRuntimeBlocks(bundle, { viewer: resolvedViewer }),
401
+ );
267
402
  }
@@ -117,6 +117,101 @@ function resolveNormalizedText(value, fallback = null) {
117
117
  return normalizeClaworldText(value, fallback);
118
118
  }
119
119
 
120
+ function isAgentScopedSessionKey(sessionKey) {
121
+ return /^agent:[^:]+:/i.test(String(sessionKey || ''));
122
+ }
123
+
124
+ function buildAgentScopedLocalSessionKey({ sessionKey, localAgentId } = {}) {
125
+ const normalizedSessionKey = resolveNormalizedText(sessionKey, null);
126
+ if (!normalizedSessionKey) return null;
127
+ if (isAgentScopedSessionKey(normalizedSessionKey)) {
128
+ return normalizedSessionKey;
129
+ }
130
+ const normalizedLocalAgentId = resolveNormalizedText(localAgentId, null);
131
+ if (!normalizedLocalAgentId) {
132
+ return normalizedSessionKey;
133
+ }
134
+ return `agent:${normalizedLocalAgentId}:${normalizedSessionKey}`;
135
+ }
136
+
137
+ function stripAgentScopedLocalSessionKey({ sessionKey, localAgentId } = {}) {
138
+ const normalizedSessionKey = resolveNormalizedText(sessionKey, null);
139
+ if (!normalizedSessionKey) return null;
140
+ const normalizedLocalAgentId = resolveNormalizedText(localAgentId, null);
141
+ if (!normalizedLocalAgentId) {
142
+ return normalizedSessionKey;
143
+ }
144
+ const prefix = `agent:${normalizedLocalAgentId}:`;
145
+ if (normalizedSessionKey.startsWith(prefix)) {
146
+ return normalizedSessionKey.slice(prefix.length) || null;
147
+ }
148
+ return normalizedSessionKey;
149
+ }
150
+
151
+ function normalizeLocalSessionKeyFields(record = null, { localAgentId = null } = {}) {
152
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
153
+ return record;
154
+ }
155
+ const nextRecord = { ...record };
156
+ const normalizedLocalSessionKey = buildAgentScopedLocalSessionKey({
157
+ sessionKey: resolveNormalizedText(record.localSessionKey, resolveNormalizedText(record.sessionKey, null)),
158
+ localAgentId,
159
+ });
160
+ if (normalizedLocalSessionKey) {
161
+ nextRecord.localSessionKey = normalizedLocalSessionKey;
162
+ }
163
+ return nextRecord;
164
+ }
165
+
166
+ function normalizeChatInboxPayloadSessionKeys(payload = null, { localAgentId = null } = {}) {
167
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
168
+ return payload;
169
+ }
170
+ const nextPayload = { ...payload };
171
+ if (payload.filters && typeof payload.filters === 'object' && !Array.isArray(payload.filters)) {
172
+ const normalizedFilterLocalSessionKey = buildAgentScopedLocalSessionKey({
173
+ sessionKey: payload.filters.localSessionKey,
174
+ localAgentId,
175
+ });
176
+ nextPayload.filters = {
177
+ ...payload.filters,
178
+ ...(normalizedFilterLocalSessionKey ? { localSessionKey: normalizedFilterLocalSessionKey } : {}),
179
+ };
180
+ }
181
+ if (Array.isArray(payload.chats)) {
182
+ nextPayload.chats = payload.chats.map((chat) => normalizeLocalSessionKeyFields(chat, { localAgentId }));
183
+ }
184
+ if (payload.kickoff && typeof payload.kickoff === 'object' && !Array.isArray(payload.kickoff)) {
185
+ nextPayload.kickoff = normalizeLocalSessionKeyFields(payload.kickoff, { localAgentId });
186
+ }
187
+ if (payload.chat && typeof payload.chat === 'object' && !Array.isArray(payload.chat)) {
188
+ nextPayload.chat = normalizeLocalSessionKeyFields(payload.chat, { localAgentId });
189
+ }
190
+ return nextPayload;
191
+ }
192
+
193
+ function resolveRelaySessionKeyFromOutboundContext(outboundContext = {}) {
194
+ const metadata = outboundContext?.metadata && typeof outboundContext.metadata === 'object' && !Array.isArray(outboundContext.metadata)
195
+ ? outboundContext.metadata
196
+ : {};
197
+ return normalizeClaworldText(
198
+ outboundContext.relaySessionKey,
199
+ normalizeClaworldText(
200
+ outboundContext.RelaySessionKey,
201
+ normalizeClaworldText(
202
+ metadata.relaySessionKey,
203
+ normalizeClaworldText(
204
+ metadata.sessionKey,
205
+ normalizeClaworldText(
206
+ outboundContext.sessionKey,
207
+ normalizeClaworldText(outboundContext.SessionKey, null),
208
+ ),
209
+ ),
210
+ ),
211
+ ),
212
+ );
213
+ }
214
+
120
215
  function normalizeClaworldInteger(value, fallback = null) {
121
216
  const normalized = Number(value);
122
217
  if (!Number.isFinite(normalized)) return fallback;
@@ -410,6 +505,7 @@ async function deliverRelayMessage({ runtimeConfig, to, text, fetchImpl, logger,
410
505
  const clientMessageId = normalizePluginOptionalText(
411
506
  outboundContext.clientMessageId || outboundContext.metadata?.clientMessageId || null
412
507
  ) || buildGeneratedClientMessageId();
508
+ const relaySessionKey = resolveRelaySessionKeyFromOutboundContext(outboundContext);
413
509
 
414
510
  const baseUrl = normalizeRelayHttpBaseUrl(runtimeConfig.serverUrl);
415
511
  const result = await fetchJson(fetchImpl, `${baseUrl}/v1/messages`, {
@@ -430,7 +526,7 @@ async function deliverRelayMessage({ runtimeConfig, to, text, fetchImpl, logger,
430
526
  scope: outboundContext.scope || outboundContext.metadata?.scope || null,
431
527
  conversationId: outboundContext.conversationId || outboundContext.metadata?.conversationId || null,
432
528
  threadId: outboundContext.threadId || outboundContext.metadata?.threadId || null,
433
- sessionKey: outboundContext.sessionKey || outboundContext.SessionKey || null,
529
+ sessionKey: relaySessionKey,
434
530
  },
435
531
  }),
436
532
  });
@@ -460,7 +556,7 @@ async function deliverRelayMessage({ runtimeConfig, to, text, fetchImpl, logger,
460
556
  timestamp: Date.now(),
461
557
  meta: {
462
558
  clientMessageId,
463
- sessionKey: result.body?.delivery?.sessionKey || outboundContext.sessionKey || outboundContext.SessionKey || null,
559
+ sessionKey: result.body?.delivery?.sessionKey || relaySessionKey,
464
560
  turnId: result.body?.turn?.turnId || null,
465
561
  conversationKey: result.body?.conversationKey || null,
466
562
  targetAgentId,
@@ -563,13 +659,29 @@ async function createChatRequest({
563
659
  async function listChatInbox({
564
660
  runtimeConfig,
565
661
  agentId,
662
+ localAgentId = null,
663
+ filters = null,
566
664
  direction = null,
567
665
  fetchImpl,
568
666
  }) {
667
+ const normalizedFilters = filters && typeof filters === 'object' && !Array.isArray(filters)
668
+ ? filters
669
+ : {};
670
+ const relayLocalSessionKey = stripAgentScopedLocalSessionKey({
671
+ sessionKey: normalizedFilters.localSessionKey,
672
+ localAgentId,
673
+ });
569
674
  const baseUrl = normalizeRelayHttpBaseUrl(runtimeConfig.serverUrl);
570
675
  const path = buildRelayJsonPath('/v1/chat-requests', {
571
676
  agentId,
572
- direction,
677
+ direction: normalizedFilters.direction || direction,
678
+ mode: normalizedFilters.mode,
679
+ status: normalizedFilters.status,
680
+ worldId: normalizedFilters.worldId,
681
+ chatRequestId: normalizedFilters.chatRequestId,
682
+ conversationKey: normalizedFilters.conversationKey,
683
+ localSessionKey: relayLocalSessionKey,
684
+ counterpartyAgentId: normalizedFilters.counterpartyAgentId,
573
685
  });
574
686
  const result = await fetchJson(fetchImpl, `${baseUrl}${path}`, {
575
687
  method: 'GET',
@@ -584,16 +696,24 @@ async function listChatInbox({
584
696
  runtimeConfig,
585
697
  code: 'chat_request_list_failed',
586
698
  publicMessage: 'failed to list chat requests',
587
- context: { agentId, direction },
699
+ context: {
700
+ agentId,
701
+ direction: normalizedFilters.direction || direction,
702
+ mode: normalizedFilters.mode || null,
703
+ status: normalizedFilters.status || null,
704
+ worldId: normalizedFilters.worldId || null,
705
+ chatRequestId: normalizedFilters.chatRequestId || null,
706
+ },
588
707
  });
589
708
  }
590
- return result.body || {};
709
+ return normalizeChatInboxPayloadSessionKeys(result.body || {}, { localAgentId });
591
710
  }
592
711
 
593
712
  async function acceptChatRequest({
594
713
  runtimeConfig,
595
714
  actorAgentId,
596
715
  chatRequestId,
716
+ localAgentId = null,
597
717
  fetchImpl,
598
718
  }) {
599
719
  const baseUrl = normalizeRelayHttpBaseUrl(runtimeConfig.serverUrl);
@@ -615,7 +735,7 @@ async function acceptChatRequest({
615
735
  context: { actorAgentId, chatRequestId },
616
736
  });
617
737
  }
618
- return result.body || {};
738
+ return normalizeChatInboxPayloadSessionKeys(result.body || {}, { localAgentId });
619
739
  }
620
740
 
621
741
  async function rejectChatRequest({
@@ -1236,6 +1356,7 @@ function buildDeliveryInboundEnvelope({
1236
1356
  timestamp = null,
1237
1357
  deliveryId,
1238
1358
  sessionKey,
1359
+ localSessionKey = null,
1239
1360
  worldId = null,
1240
1361
  conversationKey = null,
1241
1362
  untrustedContext = [],
@@ -1256,7 +1377,8 @@ function buildDeliveryInboundEnvelope({
1256
1377
  `[claworld peer ${remoteLabel}]`,
1257
1378
  ...(worldId ? [`[claworld world ${worldId}]`] : []),
1258
1379
  ...(conversationKey ? [`[claworld conversation ${conversationKey}]`] : []),
1259
- `[claworld session ${sessionKey}]`,
1380
+ ...(localSessionKey && localSessionKey !== sessionKey ? [`[claworld local session ${localSessionKey}]`] : []),
1381
+ `[claworld relay session ${sessionKey}]`,
1260
1382
  `[claworld delivery ${deliveryId}]`,
1261
1383
  ], untrustedContext);
1262
1384
  const envelopeTimestamp = Number.isFinite(timestamp) ? new Date(timestamp) : new Date();
@@ -1701,7 +1823,24 @@ async function maybeBridgeRuntimeDelivery({
1701
1823
  return { skipped: true, reason: 'missing_delivery_payload' };
1702
1824
  }
1703
1825
 
1704
- const currentCfg = cfg || await runtime.config?.loadConfig?.() || {};
1826
+ const loadedCfg = await runtime.config?.loadConfig?.() || {};
1827
+ const currentCfg = {
1828
+ ...(loadedCfg && typeof loadedCfg === 'object' && !Array.isArray(loadedCfg) ? loadedCfg : {}),
1829
+ ...(cfg && typeof cfg === 'object' && !Array.isArray(cfg) ? cfg : {}),
1830
+ agents: cfg?.agents || loadedCfg?.agents,
1831
+ bindings: cfg?.bindings || loadedCfg?.bindings,
1832
+ channels: cfg?.channels || loadedCfg?.channels,
1833
+ session: cfg?.session || loadedCfg?.session,
1834
+ };
1835
+ const localAgentId = resolveBoundLocalAgentId({
1836
+ cfg: currentCfg,
1837
+ runtimeConfig,
1838
+ relayClient,
1839
+ });
1840
+ const localSessionKey = buildAgentScopedLocalSessionKey({
1841
+ sessionKey,
1842
+ localAgentId,
1843
+ });
1705
1844
  const routed = inbound?.routeInboundEvent?.(delivery, {
1706
1845
  sessionTarget: runtimeConfig.routing?.sessionTarget,
1707
1846
  fallbackTarget: runtimeConfig.routing?.fallbackTarget,
@@ -1722,6 +1861,7 @@ async function maybeBridgeRuntimeDelivery({
1722
1861
  timestamp: inboundTimestamp,
1723
1862
  deliveryId,
1724
1863
  sessionKey,
1864
+ localSessionKey,
1725
1865
  worldId,
1726
1866
  conversationKey: metadata.conversationKey || null,
1727
1867
  untrustedContext: payload.untrustedContext,
@@ -1735,7 +1875,8 @@ async function maybeBridgeRuntimeDelivery({
1735
1875
  BodyForCommands,
1736
1876
  From: `claworld:${remoteIdentity}`,
1737
1877
  To: `claworld:${localIdentity}`,
1738
- SessionKey: sessionKey,
1878
+ SessionKey: localSessionKey || sessionKey,
1879
+ RelaySessionKey: sessionKey,
1739
1880
  AccountId: runtimeConfig.accountId,
1740
1881
  OriginatingChannel: 'claworld',
1741
1882
  OriginatingFrom: remoteIdentity,
@@ -1755,11 +1896,6 @@ async function maybeBridgeRuntimeDelivery({
1755
1896
  RelayFromAgentId: fromAgentId,
1756
1897
  UntrustedContext,
1757
1898
  });
1758
- const localAgentId = resolveBoundLocalAgentId({
1759
- cfg: currentCfg,
1760
- runtimeConfig,
1761
- relayClient,
1762
- });
1763
1899
 
1764
1900
  if (runtime?.channel?.session?.recordInboundSession && runtime?.channel?.session?.resolveStorePath && localAgentId) {
1765
1901
  const storePath = runtime.channel.session.resolveStorePath(currentCfg.session?.store, {
@@ -1773,6 +1909,7 @@ async function maybeBridgeRuntimeDelivery({
1773
1909
  logger.error?.(`[claworld:${runtimeAccountId}] failed to record inbound session`, {
1774
1910
  deliveryId,
1775
1911
  sessionKey,
1912
+ localSessionKey,
1776
1913
  localAgentId,
1777
1914
  error: error?.message || String(error),
1778
1915
  });
@@ -1783,6 +1920,7 @@ async function maybeBridgeRuntimeDelivery({
1783
1920
  logger.info?.(`[claworld:${runtimeAccountId}] routing delivery into runtime session`, {
1784
1921
  deliveryId,
1785
1922
  sessionKey,
1923
+ localSessionKey,
1786
1924
  localAgentId,
1787
1925
  remoteIdentity,
1788
1926
  routeStatus: routed?.status || null,
@@ -1805,6 +1943,7 @@ async function maybeBridgeRuntimeDelivery({
1805
1943
  logger.warn?.(`[claworld:${runtimeAccountId}] delivery acceptance acknowledgement failed`, {
1806
1944
  deliveryId,
1807
1945
  sessionKey,
1946
+ localSessionKey,
1808
1947
  localAgentId,
1809
1948
  error: error?.message || String(error),
1810
1949
  });
@@ -1850,6 +1989,7 @@ async function maybeBridgeRuntimeDelivery({
1850
1989
  logger.warn?.(`[claworld:${runtimeAccountId}] kickoff delivery produced only operational notices; retrying dispatch once`, {
1851
1990
  deliveryId,
1852
1991
  sessionKey,
1992
+ localSessionKey,
1853
1993
  localAgentId,
1854
1994
  runtimeOutputSummary,
1855
1995
  });
@@ -1876,6 +2016,7 @@ async function maybeBridgeRuntimeDelivery({
1876
2016
  logger.info?.(`[claworld:${runtimeAccountId}] delivery bridge completed`, {
1877
2017
  deliveryId,
1878
2018
  sessionKey,
2019
+ localSessionKey,
1879
2020
  queuedFinal: Boolean(dispatchResult?.queuedFinal),
1880
2021
  replied,
1881
2022
  keptSilent,
@@ -1890,6 +2031,7 @@ async function maybeBridgeRuntimeDelivery({
1890
2031
  keptSilent,
1891
2032
  queuedFinal: Boolean(dispatchResult?.queuedFinal),
1892
2033
  sessionKey,
2034
+ localSessionKey,
1893
2035
  routeStatus: routed?.status || null,
1894
2036
  };
1895
2037
  }
@@ -2154,6 +2296,14 @@ export function createClaworldChannelPlugin({
2154
2296
  };
2155
2297
  }
2156
2298
 
2299
+ function resolveContextBoundLocalAgentId(context = {}) {
2300
+ return resolveBoundLocalAgentId({
2301
+ cfg: context.cfg || {},
2302
+ runtimeConfig: context.runtimeConfig || {},
2303
+ relayClient: relayClients.get(context.accountId || 'default') || null,
2304
+ });
2305
+ }
2306
+
2157
2307
  function getAccountLifecycle(accountKey = 'default') {
2158
2308
  if (lifecycles.has(accountKey)) return lifecycles.get(accountKey);
2159
2309
 
@@ -2768,6 +2918,8 @@ async function generateRuntimeProfileCard(context = {}) {
2768
2918
  return listChatInbox({
2769
2919
  runtimeConfig: resolvedContext.runtimeConfig,
2770
2920
  agentId: resolvedContext.agentId || null,
2921
+ localAgentId: resolveContextBoundLocalAgentId(resolvedContext),
2922
+ filters: context.filters || null,
2771
2923
  direction: context.direction || null,
2772
2924
  fetchImpl,
2773
2925
  });
@@ -2778,6 +2930,7 @@ async function generateRuntimeProfileCard(context = {}) {
2778
2930
  runtimeConfig: resolvedContext.runtimeConfig,
2779
2931
  actorAgentId: resolvedContext.agentId || null,
2780
2932
  chatRequestId: context.chatRequestId || null,
2933
+ localAgentId: resolveContextBoundLocalAgentId(resolvedContext),
2781
2934
  fetchImpl,
2782
2935
  });
2783
2936
  },
@@ -69,6 +69,40 @@ function buildClaworldStatusRoute(plugin) {
69
69
  };
70
70
  }
71
71
 
72
+ const CHAT_INBOX_FILTER_DIRECTIONS = Object.freeze([
73
+ 'inbound',
74
+ 'outbound',
75
+ ]);
76
+ const CHAT_INBOX_FILTER_MODES = Object.freeze([
77
+ 'direct',
78
+ 'world',
79
+ ]);
80
+ const CHAT_INBOX_FILTER_STATUSES = Object.freeze([
81
+ 'pending',
82
+ 'opening',
83
+ 'active',
84
+ 'silent',
85
+ 'kickoff_failed',
86
+ 'ended',
87
+ ]);
88
+
89
+ function normalizeChatInboxListFiltersInput(params = {}) {
90
+ const source = normalizeObject(params.filters, {}) || {};
91
+ const normalized = {
92
+ direction: normalizeText(source.direction ?? params.direction, null),
93
+ mode: normalizeText(source.mode, null),
94
+ status: normalizeText(source.status, null),
95
+ worldId: normalizeText(source.worldId, null),
96
+ chatRequestId: normalizeText(source.chatRequestId, null),
97
+ conversationKey: normalizeText(source.conversationKey, null),
98
+ localSessionKey: normalizeText(source.localSessionKey, null),
99
+ counterpartyAgentId: normalizeText(source.counterpartyAgentId, null),
100
+ };
101
+ return Object.fromEntries(
102
+ Object.entries(normalized).filter(([, value]) => value != null),
103
+ );
104
+ }
105
+
72
106
  function buildRegisteredTools(api, plugin) {
73
107
  const accountIdProperty = stringParam({
74
108
  description: 'Claworld account id to execute the tool against. In managed installs this is usually the dedicated claworld account.',
@@ -582,26 +616,39 @@ function buildRegisteredTools(api, plugin) {
582
616
  {
583
617
  name: 'claworld_chat_inbox',
584
618
  label: 'Claworld Chat Inbox',
585
- description: 'Canonical chat inbox tool. List pending requests plus current or recent Claworld chats and the local session references you can use to check progress, or accept/reject one inbox request from the same surface.',
619
+ description: 'Canonical chat inbox tool. By default it lists all pending requests plus current or recent Claworld chats and their local session references; optional filters help narrow to one world, peer, request, status, or conversation.',
586
620
  metadata: buildToolMetadata({
587
621
  category: 'chat_request',
588
622
  usageNotes: [
589
- 'Default action is list. Use it to review pending inbound requests waiting for acceptance.',
623
+ 'Default action is list. Without filters it returns the full inbox view, including both inbound and outbound items.',
590
624
  'Use to locate the relevant Claworld chat and the local sessionKey tied to it.',
625
+ 'Optional filters can narrow by direction, mode, status, worldId, chatRequestId, conversationKey, localSessionKey, or counterpartyAgentId.',
591
626
  'If the user asks about one chat, first locate it here, then use your local session-send tool to ask that local session for a progress update or short summary.',
592
627
  'Prefer asking the local chat session for a concise update before inspecting raw local transcript details.',
593
- 'Use direction=outbound when focusing on chats you initiated.',
628
+ 'Global counts stay visible even when filters are applied; filtered counts describe the current narrowed result set.',
594
629
  'After action=accept or action=reject, call action=list again to refresh the inbox view.',
595
630
  ],
596
631
  examples: [
597
632
  {
598
- title: 'Review inbound chat state',
633
+ title: 'Review the full inbox',
599
634
  input: {
600
635
  accountId: 'claworld',
601
636
  action: 'list',
602
- direction: 'inbound',
603
637
  },
604
- outcome: 'Returns pending requests plus related chats for the current account.',
638
+ outcome: 'Returns all pending requests plus related chats for the current account.',
639
+ },
640
+ {
641
+ title: 'Filter to active world chats',
642
+ input: {
643
+ accountId: 'claworld',
644
+ action: 'list',
645
+ filters: {
646
+ mode: 'world',
647
+ status: 'active',
648
+ worldId: 'dating-demo-world',
649
+ },
650
+ },
651
+ outcome: 'Returns only matching world chats while keeping global and filtered counts.',
605
652
  },
606
653
  {
607
654
  title: 'Accept one inbound request from the inbox',
@@ -624,10 +671,46 @@ function buildRegisteredTools(api, plugin) {
624
671
  enumValues: CHAT_INBOX_ACTIONS,
625
672
  examples: ['list', 'accept', 'reject'],
626
673
  }),
627
- direction: stringParam({
628
- description: 'Filter to inbound or outbound chat state from the current account perspective. Used with action=list.',
629
- enumValues: ['inbound', 'outbound'],
630
- examples: ['inbound'],
674
+ filters: objectParam({
675
+ description: 'Optional list filters. Omit to review the full inbox across inbound and outbound items.',
676
+ properties: {
677
+ direction: stringParam({
678
+ description: 'Filter from the current account perspective.',
679
+ enumValues: CHAT_INBOX_FILTER_DIRECTIONS,
680
+ examples: ['outbound'],
681
+ }),
682
+ mode: stringParam({
683
+ description: 'Filter to direct or world-scoped chat items.',
684
+ enumValues: CHAT_INBOX_FILTER_MODES,
685
+ examples: ['world'],
686
+ }),
687
+ status: stringParam({
688
+ description: 'Filter to pending requests or chats by current status.',
689
+ enumValues: CHAT_INBOX_FILTER_STATUSES,
690
+ examples: ['active'],
691
+ }),
692
+ worldId: worldIdProperty,
693
+ chatRequestId: stringParam({
694
+ description: 'Filter to one canonical chat request id.',
695
+ minLength: 1,
696
+ examples: ['req_demo_1'],
697
+ }),
698
+ conversationKey: stringParam({
699
+ description: 'Filter to one canonical conversation key.',
700
+ minLength: 1,
701
+ examples: ['pair:agt_alice::agt_moza:world:dating-demo-world'],
702
+ }),
703
+ localSessionKey: stringParam({
704
+ description: 'Filter to one local Claworld session reference.',
705
+ minLength: 1,
706
+ examples: ['conversation:pair:agt_alice::agt_moza:world:dating-demo-world'],
707
+ }),
708
+ counterpartyAgentId: stringParam({
709
+ description: 'Filter to one counterparty agentId.',
710
+ minLength: 1,
711
+ examples: ['agt_alice'],
712
+ }),
713
+ },
631
714
  }),
632
715
  chatRequestId: stringParam({
633
716
  description: 'Canonical chat request id returned by claworld_chat_inbox pendingRequests. Required for action=accept or action=reject.',
@@ -639,7 +722,9 @@ function buildRegisteredTools(api, plugin) {
639
722
  {
640
723
  accountId: 'claworld',
641
724
  action: 'list',
642
- direction: 'inbound',
725
+ filters: {
726
+ direction: 'inbound',
727
+ },
643
728
  },
644
729
  {
645
730
  accountId: 'claworld',
@@ -670,9 +755,10 @@ function buildRegisteredTools(api, plugin) {
670
755
  action,
671
756
  }));
672
757
  }
758
+ const filters = normalizeChatInboxListFiltersInput(params);
673
759
  const payload = await plugin.helpers.social.listChatInbox({
674
760
  ...context,
675
- direction: params.direction || null,
761
+ filters,
676
762
  });
677
763
  return buildToolResult(projectToolChatInboxActionResponse(payload, {
678
764
  accountId: context.accountId,
@@ -486,7 +486,7 @@ export class ClaworldRelayClient extends EventEmitter {
486
486
  config,
487
487
  agentId,
488
488
  credential = null,
489
- clientVersion = 'claworld-plugin/0.2.22',
489
+ clientVersion = 'claworld-plugin/0.2.24',
490
490
  sessionTarget,
491
491
  fallbackTarget,
492
492
  } = {}) {
@@ -451,6 +451,14 @@ function projectChatRequestKickoff(kickoff = {}) {
451
451
  openerAcceptedAt: normalizeText(normalizedKickoff.openerAcceptedAt, null),
452
452
  openerDeliveredAt: normalizeText(normalizedKickoff.openerDeliveredAt, null),
453
453
  liveChatEstablishedAt: normalizeText(normalizedKickoff.liveChatEstablishedAt, null),
454
+ conversationKey: normalizeText(normalizedKickoff.conversationKey, null),
455
+ localSessionKey: normalizeText(
456
+ normalizedKickoff.localSessionKey,
457
+ normalizeText(normalizedKickoff.sessionKey, null),
458
+ ),
459
+ turnId: normalizeText(normalizedKickoff.turnId, null),
460
+ deliveryId: normalizeText(normalizedKickoff.deliveryId, null),
461
+ created: typeof normalizedKickoff.created === 'boolean' ? normalizedKickoff.created : null,
454
462
  reason: normalizeText(normalizedKickoff.reason, null),
455
463
  };
456
464
  }
@@ -517,12 +525,52 @@ function projectChatInboxChatItem(chat = {}) {
517
525
  lastTurnAt: normalizeText(chat.lastTurnAt, null),
518
526
  conversationKey: normalizeText(chat.conversationKey, null),
519
527
  localSessionKey: normalizeText(chat.localSessionKey, normalizeText(chat.sessionKey, null)),
528
+ turnCount: normalizeInteger(chat.turnCount, null),
520
529
  counterparty: projectToolAgentSummary(chat.counterparty),
521
530
  conversation: normalizeConversationScopeDetails(chat.conversation),
522
531
  feedbackSummary: projectConversationFeedbackSummary(chat.feedbackSummary),
523
532
  };
524
533
  }
525
534
 
535
+ function projectChatInboxFilters(filters = {}) {
536
+ if (!filters || typeof filters !== 'object' || Array.isArray(filters)) return {};
537
+ const projected = {
538
+ direction: normalizeText(filters.direction, null),
539
+ mode: normalizeText(filters.mode, null),
540
+ status: normalizeText(filters.status, null),
541
+ worldId: normalizeText(filters.worldId, null),
542
+ chatRequestId: normalizeText(filters.chatRequestId, null),
543
+ conversationKey: normalizeText(filters.conversationKey, null),
544
+ localSessionKey: normalizeText(filters.localSessionKey, null),
545
+ counterpartyAgentId: normalizeText(filters.counterpartyAgentId, null),
546
+ };
547
+ return Object.fromEntries(
548
+ Object.entries(projected).filter(([, value]) => value != null),
549
+ );
550
+ }
551
+
552
+ function projectChatInboxCountBlock(counts = {}, fallback = {}) {
553
+ return {
554
+ pendingRequestCount: normalizeInteger(counts.pendingRequestCount, fallback.pendingRequestCount ?? 0),
555
+ chatCount: normalizeInteger(counts.chatCount, fallback.chatCount ?? 0),
556
+ chatStatusCounts: counts.chatStatusCounts && typeof counts.chatStatusCounts === 'object' && !Array.isArray(counts.chatStatusCounts)
557
+ ? {
558
+ opening: normalizeInteger(counts.chatStatusCounts.opening, 0),
559
+ active: normalizeInteger(counts.chatStatusCounts.active, 0),
560
+ silent: normalizeInteger(counts.chatStatusCounts.silent, 0),
561
+ kickoff_failed: normalizeInteger(counts.chatStatusCounts.kickoff_failed, 0),
562
+ ended: normalizeInteger(counts.chatStatusCounts.ended, 0),
563
+ }
564
+ : {
565
+ opening: 0,
566
+ active: 0,
567
+ silent: 0,
568
+ kickoff_failed: 0,
569
+ ended: 0,
570
+ },
571
+ };
572
+ }
573
+
526
574
  export function projectToolChatRequestMutationResponse(result = {}, { accountId = null } = {}) {
527
575
  const request = result.chatRequest && typeof result.chatRequest === 'object'
528
576
  ? result.chatRequest
@@ -531,11 +579,13 @@ export function projectToolChatRequestMutationResponse(result = {}, { accountId
531
579
  : result;
532
580
  const projectedRequest = projectChatRequestItem(request);
533
581
  const kickoff = projectChatRequestKickoff(result.kickoff || result.request?.kickoff);
582
+ const projectedChat = projectChatInboxChatItem(result.chat);
534
583
  const normalizedStatus = normalizeText(result.status, projectedRequest?.status || 'pending');
535
584
  return {
536
585
  status: normalizedStatus,
537
586
  accountId: normalizeText(accountId, null),
538
587
  chatRequest: projectedRequest,
588
+ ...(projectedChat ? { chat: projectedChat } : {}),
539
589
  kickoff,
540
590
  nextAction: normalizeText(
541
591
  result.nextAction,
@@ -563,17 +613,22 @@ export function projectToolChatInboxResponse(result = {}, { accountId = null } =
563
613
  const chats = Array.isArray(result.chats)
564
614
  ? result.chats.map((chat) => projectChatInboxChatItem(chat)).filter(Boolean)
565
615
  : [];
616
+ const projectedFilters = projectChatInboxFilters(result.filters);
617
+ const globalCounts = projectChatInboxCountBlock(result.counts?.global, {
618
+ pendingRequestCount: pendingRequests.length,
619
+ chatCount: chats.length,
620
+ });
621
+ const filteredCounts = projectChatInboxCountBlock(result.counts?.filtered, {
622
+ pendingRequestCount: pendingRequests.length,
623
+ chatCount: chats.length,
624
+ });
566
625
  return {
567
626
  accountId: normalizeText(accountId, null),
568
- counts: result.counts && typeof result.counts === 'object' && !Array.isArray(result.counts)
569
- ? {
570
- pendingRequestCount: normalizeInteger(result.counts.pendingRequestCount, pendingRequests.length),
571
- chatCount: normalizeInteger(result.counts.chatCount, chats.length),
572
- }
573
- : {
574
- pendingRequestCount: pendingRequests.length,
575
- chatCount: chats.length,
576
- },
627
+ filters: projectedFilters,
628
+ counts: {
629
+ global: globalCounts,
630
+ filtered: filteredCounts,
631
+ },
577
632
  pendingRequests,
578
633
  chats,
579
634
  };