@mobcode/openclaw-plugin 0.1.16 → 0.1.18

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mobcode/openclaw-plugin",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "MobCode integration plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -122,6 +122,35 @@ export function registerMobcodeGatewayMethods({
122
122
  });
123
123
  });
124
124
 
125
+ api.registerGatewayMethod("mobcode.tool_result.get", async ({ params, respond }) => {
126
+ const sessionKey = typeof params?.sessionKey === "string" ? params.sessionKey.trim() : "";
127
+ const toolCallId = typeof params?.toolCallId === "string" ? params.toolCallId.trim() : "";
128
+ if (!sessionKey || !toolCallId) {
129
+ respond(false, { error: "sessionKey and toolCallId are required" });
130
+ return;
131
+ }
132
+ const timeoutMs =
133
+ typeof params?.waitMs === "number"
134
+ ? params.waitMs
135
+ : typeof params?.timeoutMs === "number"
136
+ ? params.timeoutMs
137
+ : 0;
138
+ const result = await store.waitForToolResult(sessionKey, toolCallId, {
139
+ timeoutMs,
140
+ });
141
+ if (!result?.message) {
142
+ respond(false, { error: "tool result not found" });
143
+ return;
144
+ }
145
+ respond(true, {
146
+ ok: true,
147
+ sessionKey,
148
+ toolCallId,
149
+ messageId: result.messageId,
150
+ message: result.message,
151
+ });
152
+ });
153
+
125
154
  api.registerGatewayMethod("mobcode.client.register", async ({ params, respond }) => {
126
155
  const clientId = typeof params?.clientId === "string" ? params.clientId.trim() : "";
127
156
  if (!clientId) {
@@ -157,122 +157,6 @@ function decodeMobcodePayload(value) {
157
157
  return null;
158
158
  }
159
159
 
160
- function extractMobcodeContentBlocksFromToolResult(rawResult) {
161
- const payload = decodeMobcodePayload(rawResult);
162
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
163
- return [];
164
- }
165
- return Array.isArray(payload.content_blocks)
166
- ? cloneJson(payload.content_blocks) ?? []
167
- : [];
168
- }
169
-
170
- function timelineTextPreview(timeline) {
171
- if (!Array.isArray(timeline) || timeline.length === 0) {
172
- return "";
173
- }
174
- const parts = [];
175
- for (const item of timeline) {
176
- if (!item || typeof item !== "object") continue;
177
- const kind = String(item.kind ?? "").trim().toLowerCase();
178
- if (kind === "text") {
179
- const text = String(item.text ?? "");
180
- if (text.trim()) parts.push(text);
181
- continue;
182
- }
183
- if (kind === "step") {
184
- const title = String(item.title ?? "");
185
- const detail = String(item.detail ?? "");
186
- if (title.trim()) parts.push(title);
187
- if (detail.trim()) parts.push(detail);
188
- continue;
189
- }
190
- if (kind === "artifact") {
191
- const title = String(item?.block?.title ?? "");
192
- if (title.trim()) parts.push(title);
193
- continue;
194
- }
195
- if (kind === "question") {
196
- const title = String(item.title ?? item?.payload?.title ?? "");
197
- if (title.trim()) parts.push(title);
198
- }
199
- }
200
- return parts.join("\n");
201
- }
202
-
203
- function stableId(prefix, seed) {
204
- const digest = crypto.createHash("sha1").update(`${prefix}\n${seed}`).digest("hex");
205
- return `${prefix}-${digest.slice(0, 12)}`;
206
- }
207
-
208
- function createTextTimelineItem({ id, text, status = "done" }) {
209
- return {
210
- id,
211
- kind: "text",
212
- status,
213
- text,
214
- };
215
- }
216
-
217
- function createToolTimelineItem({ id, toolCall }) {
218
- const status = toolCall.isError
219
- ? "error"
220
- : toolCall.phase === "result"
221
- ? "done"
222
- : "streaming";
223
- return {
224
- id,
225
- kind: "tool",
226
- status,
227
- toolCallId: toolCall.toolCallId,
228
- ...(toolCall.approvalId ? { approvalId: toolCall.approvalId } : {}),
229
- name: toolCall.name,
230
- args: cloneJson(toolCall.args),
231
- result: cloneJson(toolCall.result),
232
- isError: toolCall.isError === true,
233
- phase: toolCall.phase,
234
- };
235
- }
236
-
237
- function timelineItemFromContentBlock(block, seed) {
238
- if (!block || typeof block !== "object") {
239
- return null;
240
- }
241
- const type = String(block.type ?? "").trim().toLowerCase();
242
- if (type === "text") {
243
- const text = String(block.text ?? "");
244
- if (!text.trim()) return null;
245
- return createTextTimelineItem({
246
- id: stableId("timeline-text", `${seed}\n${text}`),
247
- text,
248
- status: "done",
249
- });
250
- }
251
- if (type === "artifact_ref") {
252
- return {
253
- id: stableId("timeline-artifact", `${seed}\n${JSON.stringify(block)}`),
254
- kind: "artifact",
255
- status: "done",
256
- block: cloneJson(block),
257
- };
258
- }
259
- if (type === "question") {
260
- return {
261
- id: stableId("timeline-question", `${seed}\n${JSON.stringify(block)}`),
262
- kind: "question",
263
- status: "done",
264
- payload: cloneJson(block),
265
- ...cloneJson(block),
266
- };
267
- }
268
- return {
269
- id: stableId("timeline-unknown", `${seed}\n${JSON.stringify(block)}`),
270
- kind: "unknown",
271
- status: "error",
272
- block: cloneJson(block),
273
- };
274
- }
275
-
276
160
  function extractArtifactDocumentFromMessage(message) {
277
161
  if (!message || typeof message !== "object" || Array.isArray(message)) {
278
162
  return null;
@@ -295,273 +179,6 @@ function extractArtifactDocumentFromMessage(message) {
295
179
  return null;
296
180
  }
297
181
 
298
- function createConversationMessage({ id, role, text = "", replyToId = null, replyPreview = null }) {
299
- return {
300
- id,
301
- role,
302
- text,
303
- status: "done",
304
- toolCalls: [],
305
- contentBlocks: [],
306
- timeline: [],
307
- ...(replyToId ? { replyToId } : {}),
308
- ...(replyPreview ? { replyPreview } : {}),
309
- };
310
- }
311
-
312
- function upsertToolCall(message, toolCall, seed) {
313
- const toolCalls = Array.isArray(message.toolCalls) ? [...message.toolCalls] : [];
314
- const toolIndex = toolCalls.findIndex((item) => item?.toolCallId === toolCall.toolCallId);
315
- if (toolIndex >= 0) {
316
- toolCalls[toolIndex] = { ...toolCalls[toolIndex], ...cloneJson(toolCall) };
317
- } else {
318
- toolCalls.push(cloneJson(toolCall));
319
- }
320
-
321
- const timeline = Array.isArray(message.timeline) ? [...message.timeline] : [];
322
- const timelineIndex = timeline.findIndex(
323
- (item) => item?.kind === "tool" && item?.toolCallId === toolCall.toolCallId,
324
- );
325
- const toolItem = createToolTimelineItem({
326
- id: timelineIndex >= 0 ? timeline[timelineIndex].id : stableId("timeline-tool", seed),
327
- toolCall,
328
- });
329
- if (timelineIndex >= 0) {
330
- timeline[timelineIndex] = toolItem;
331
- } else {
332
- timeline.push(toolItem);
333
- }
334
-
335
- return {
336
- ...message,
337
- toolCalls,
338
- timeline,
339
- text: timelineTextPreview(timeline) || message.text || "",
340
- };
341
- }
342
-
343
- function appendAssistantTextSegment(message, text, seed) {
344
- if (!String(text ?? "").trim()) {
345
- return message;
346
- }
347
- const normalizedText = String(text);
348
- const timeline = Array.isArray(message.timeline) ? [...message.timeline] : [];
349
- const last = timeline.length > 0 ? timeline[timeline.length - 1] : null;
350
- if (last?.kind === "text" && last?.status === "streaming") {
351
- timeline[timeline.length - 1] = {
352
- ...last,
353
- text: `${String(last.text ?? "")}${normalizedText}`,
354
- status: "done",
355
- };
356
- } else {
357
- timeline.push(
358
- createTextTimelineItem({
359
- id: stableId("timeline-text", `${seed}\n${timeline.length}`),
360
- text: normalizedText,
361
- status: "done",
362
- }),
363
- );
364
- }
365
- return {
366
- ...message,
367
- timeline,
368
- text: timelineTextPreview(timeline) || message.text || "",
369
- };
370
- }
371
-
372
- function appendToolResult(message, { toolCallId, result, isError }, seed) {
373
- const existingTool =
374
- (Array.isArray(message.toolCalls)
375
- ? message.toolCalls.find((item) => item?.toolCallId === toolCallId)
376
- : null) ?? {
377
- toolCallId,
378
- name: "unknown",
379
- phase: "result",
380
- result: null,
381
- isError: false,
382
- };
383
- let next = upsertToolCall(
384
- message,
385
- {
386
- ...existingTool,
387
- result: cloneJson(result),
388
- isError: isError === true,
389
- phase: "result",
390
- },
391
- `${seed}\n${toolCallId}`,
392
- );
393
-
394
- const blocks = extractMobcodeContentBlocksFromToolResult(result);
395
- if (blocks.length === 0) {
396
- return next;
397
- }
398
-
399
- const contentBlocks = Array.isArray(next.contentBlocks) ? [...next.contentBlocks] : [];
400
- const timeline = Array.isArray(next.timeline) ? [...next.timeline] : [];
401
- const seenBlockKeys = new Set(contentBlocks.map((block) => JSON.stringify(block)));
402
- const seenTimelineKeys = new Set(timeline.map((item) => JSON.stringify(item)));
403
- for (const block of blocks) {
404
- const blockKey = JSON.stringify(block);
405
- if (!seenBlockKeys.has(blockKey)) {
406
- seenBlockKeys.add(blockKey);
407
- contentBlocks.push(cloneJson(block));
408
- }
409
- const item = timelineItemFromContentBlock(block, `${seed}\n${blockKey}`);
410
- if (!item) continue;
411
- const timelineKey = JSON.stringify(item);
412
- if (!seenTimelineKeys.has(timelineKey)) {
413
- seenTimelineKeys.add(timelineKey);
414
- timeline.push(item);
415
- }
416
- }
417
- return {
418
- ...next,
419
- contentBlocks,
420
- timeline,
421
- text: timelineTextPreview(timeline) || next.text || "",
422
- };
423
- }
424
-
425
- function appendAssistantTranscriptContent(message, content, seed) {
426
- if (!Array.isArray(content)) {
427
- return appendAssistantTextSegment(
428
- message,
429
- textToDisplayForAssistant(extractMessageText({ content })),
430
- seed,
431
- );
432
- }
433
-
434
- let next = { ...message };
435
- for (const rawBlock of content) {
436
- if (!rawBlock || typeof rawBlock !== "object") continue;
437
- const block = cloneJson(rawBlock) ?? {};
438
- const type = String(block.type ?? "").trim().toLowerCase();
439
- if (type === "text" || type === "output_text" || type === "input_text") {
440
- next = appendAssistantTextSegment(
441
- next,
442
- textToDisplayForAssistant(String(block.text ?? block.content ?? "")),
443
- `${seed}\ntext`,
444
- );
445
- continue;
446
- }
447
- if (type === "toolcall" || type === "tool_call") {
448
- const toolCallId = String(block.id ?? "").trim();
449
- if (!toolCallId) continue;
450
- next = upsertToolCall(
451
- next,
452
- {
453
- toolCallId,
454
- approvalId: String(block.approvalId ?? block.approval_id ?? "").trim() || null,
455
- name: String(block.name ?? "unknown"),
456
- args: cloneJson(block.arguments ?? block.args),
457
- result: null,
458
- isError: false,
459
- phase: "start",
460
- },
461
- `${seed}\n${toolCallId}`,
462
- );
463
- continue;
464
- }
465
- if (type === "toolresult" || type === "tool_result") {
466
- const toolCallId = String(block.toolCallId ?? block.tool_call_id ?? "").trim();
467
- if (!toolCallId) continue;
468
- next = appendToolResult(
469
- next,
470
- {
471
- toolCallId,
472
- result: block.content ?? block.result ?? block.output,
473
- isError: block.isError === true || block.is_error === true,
474
- },
475
- `${seed}\n${toolCallId}`,
476
- );
477
- continue;
478
- }
479
- }
480
- return next;
481
- }
482
-
483
- function projectConversationMessages(rawMessages, sessionKey) {
484
- const projected = [];
485
- const assistantIndexByRunId = new Map();
486
-
487
- for (const rawMessage of rawMessages) {
488
- const message = normalizeMessageObject(rawMessage, rawMessage?.id ?? null);
489
- const role = String(message?.role ?? "").trim().toLowerCase();
490
- const runId = String(message?.runId ?? message?.run_id ?? "").trim();
491
- const messageId =
492
- String(message?.id ?? "").trim() ||
493
- stableId("message", `${sessionKey}\n${role}\n${JSON.stringify(message)}`);
494
-
495
- if (role === "user" || role === "system") {
496
- projected.push(
497
- createConversationMessage({
498
- id: messageId,
499
- role,
500
- text: extractMessageText(message),
501
- }),
502
- );
503
- continue;
504
- }
505
-
506
- if (role === "assistant") {
507
- const assistantIndex = runId ? assistantIndexByRunId.get(runId) : undefined;
508
- const current =
509
- assistantIndex == null
510
- ? createConversationMessage({
511
- id: messageId,
512
- role: "assistant",
513
- text: "",
514
- })
515
- : projected[assistantIndex];
516
- let next = appendAssistantTranscriptContent(
517
- {
518
- ...current,
519
- thinking: extractThinkingText(message) ?? current.thinking ?? null,
520
- },
521
- message.content,
522
- `${sessionKey}\n${messageId}`,
523
- );
524
- if (assistantIndex == null) {
525
- projected.push(next);
526
- if (runId) {
527
- assistantIndexByRunId.set(runId, projected.length - 1);
528
- }
529
- } else {
530
- projected[assistantIndex] = next;
531
- }
532
- continue;
533
- }
534
-
535
- if (role === "toolresult" || role === "tool_result") {
536
- const toolCallId = String(message?.toolCallId ?? message?.tool_call_id ?? "").trim();
537
- if (!toolCallId) {
538
- continue;
539
- }
540
- const assistantIndex = runId
541
- ? assistantIndexByRunId.get(runId)
542
- : projected.map((item) => item.role).lastIndexOf("assistant");
543
- if (assistantIndex == null || assistantIndex < 0) {
544
- continue;
545
- }
546
- projected[assistantIndex] = appendToolResult(
547
- projected[assistantIndex],
548
- {
549
- toolCallId,
550
- result: decodeMobcodePayload(message.content) ?? message.content,
551
- isError: message.isError === true || message.is_error === true,
552
- },
553
- `${sessionKey}\n${messageId}`,
554
- );
555
- }
556
- }
557
-
558
- return projected.map((item) => ({
559
- ...item,
560
- status: "done",
561
- text: timelineTextPreview(item.timeline) || item.text || "",
562
- }));
563
- }
564
-
565
182
  function createMessageSourceKey(sessionKey, messageId, normalizedMessage) {
566
183
  if (messageId) {
567
184
  return `mid:${sessionKey}:${messageId}`;
@@ -1150,15 +767,14 @@ export class MobcodeStateStore {
1150
767
  const normalizedBeforeId =
1151
768
  typeof beforeId === "number" && Number.isFinite(beforeId) ? beforeId : null;
1152
769
  const transcriptMessages = await this.pageTranscriptMessages(normalizedSessionKey);
1153
- const projected = projectConversationMessages(transcriptMessages, normalizedSessionKey);
1154
770
  this.logger?.info?.(
1155
- `[mobcode-store] pageSessionMessages session=${normalizedSessionKey} transcriptMessages=${transcriptMessages.length} projectedMessages=${projected.length} limit=${normalizedLimit} beforeId=${normalizedBeforeId ?? "-"}`
771
+ `[mobcode-store] pageSessionMessages session=${normalizedSessionKey} transcriptMessages=${transcriptMessages.length} limit=${normalizedLimit} beforeId=${normalizedBeforeId ?? "-"}`
1156
772
  );
1157
773
  const endExclusive = normalizedBeforeId == null
1158
- ? projected.length
1159
- : Math.max(0, Math.min(projected.length, normalizedBeforeId - 1));
774
+ ? transcriptMessages.length
775
+ : Math.max(0, Math.min(transcriptMessages.length, normalizedBeforeId - 1));
1160
776
  const start = Math.max(0, endExclusive - normalizedLimit);
1161
- const items = projected.slice(start, endExclusive);
777
+ const items = transcriptMessages.slice(start, endExclusive);
1162
778
  const hasMore = start > 0;
1163
779
  const nextBeforeId = hasMore ? start + 1 : null;
1164
780
 
@@ -1168,7 +784,7 @@ export class MobcodeStateStore {
1168
784
  rawMessages: transcriptMessages,
1169
785
  nextBeforeId,
1170
786
  hasMore,
1171
- total: projected.length,
787
+ total: transcriptMessages.length,
1172
788
  };
1173
789
  }
1174
790
 
@@ -1177,6 +793,67 @@ export class MobcodeStateStore {
1177
793
  return page.items;
1178
794
  }
1179
795
 
796
+ async readToolResult(sessionKey, toolCallId) {
797
+ const normalizedSessionKey = String(sessionKey ?? "").trim();
798
+ const normalizedToolCallId = String(toolCallId ?? "").trim();
799
+ if (!normalizedSessionKey || !normalizedToolCallId) {
800
+ return null;
801
+ }
802
+ const row = this._db()
803
+ .prepare(
804
+ `SELECT message_id, raw_json
805
+ FROM messages
806
+ WHERE session_key=?
807
+ AND role='toolResult'
808
+ ORDER BY id DESC`,
809
+ )
810
+ .all(normalizedSessionKey)
811
+ .find((candidate) => {
812
+ const message = fromJson(candidate.raw_json, null);
813
+ return (
814
+ message &&
815
+ typeof message === "object" &&
816
+ !Array.isArray(message) &&
817
+ String(message.toolCallId ?? message.tool_call_id ?? "").trim() ===
818
+ normalizedToolCallId
819
+ );
820
+ });
821
+ if (!row) {
822
+ return null;
823
+ }
824
+ const message = fromJson(row.raw_json, null);
825
+ if (!message || typeof message !== "object" || Array.isArray(message)) {
826
+ return null;
827
+ }
828
+ return {
829
+ messageId:
830
+ typeof row.message_id === "string" && row.message_id.trim()
831
+ ? row.message_id.trim()
832
+ : null,
833
+ message,
834
+ };
835
+ }
836
+
837
+ async waitForToolResult(sessionKey, toolCallId, { timeoutMs = 0, pollMs = 75 } = {}) {
838
+ const normalizedTimeout = Math.max(0, Number(timeoutMs) || 0);
839
+ const normalizedPoll = Math.max(25, Number(pollMs) || 75);
840
+ const deadline = Date.now() + normalizedTimeout;
841
+ while (true) {
842
+ const found = await this.readToolResult(sessionKey, toolCallId);
843
+ if (found) {
844
+ return found;
845
+ }
846
+ if (Date.now() >= deadline) {
847
+ return null;
848
+ }
849
+ const sleepMs = Math.min(normalizedPoll, Math.max(0, deadline - Date.now()));
850
+ if (sleepMs <= 0) {
851
+ return null;
852
+ }
853
+ await new Promise((resolve) => setTimeout(resolve, sleepMs));
854
+ }
855
+ }
856
+
1180
857
  async recordApprovalRequested(payload) {
1181
858
  const request = payload?.request && typeof payload.request === "object" ? payload.request : {};
1182
859
  const approvalId = String(payload?.id ?? "").trim();