@mobcode/openclaw-plugin 0.1.15 → 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.15",
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) {
@@ -67,6 +67,16 @@ export function createMobcodePluginDefinition() {
67
67
  api,
68
68
  store,
69
69
  });
70
+ api.on("tool_result_persist", (event, ctx) => {
71
+ store.appendMessageSync({
72
+ sessionKey: ctx?.sessionKey,
73
+ message: event?.message,
74
+ messageId:
75
+ typeof event?.message?.id === "string" && event.message.id.trim()
76
+ ? event.message.id.trim()
77
+ : undefined,
78
+ });
79
+ });
70
80
  },
71
81
  };
72
82
  }
@@ -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}`;
@@ -996,10 +613,129 @@ export class MobcodeStateStore {
996
613
  this.logger?.warn?.(
997
614
  `[mobcode-debug] appendMessage sessionKey=${sessionKey} messageId=${String(update?.messageId ?? normalizedMessage?.id ?? "").trim() || "-"} role=${String(normalizedMessage?.role ?? "").trim() || "-"} toolCallId=${String(normalizedMessage?.toolCallId ?? "").trim() || "-"} sourceKey=${createMessageSourceKey(sessionKey, String(update?.messageId ?? normalizedMessage?.id ?? "").trim() || null, normalizedMessage)}`,
998
615
  );
999
- await this.indexSessionMessages(sessionKey, [normalizedMessage]);
1000
- const row = this._db()
1001
- .prepare(`SELECT COUNT(*) AS count FROM messages WHERE session_key=?`)
1002
- .get(sessionKey);
616
+ return this._appendMessageInternal({
617
+ sessionKey,
618
+ normalizedMessage,
619
+ messageId: String(update?.messageId ?? normalizedMessage?.id ?? "").trim() || null,
620
+ });
621
+ }
622
+
623
+ appendMessageSync(update) {
624
+ const sessionKey = String(update?.sessionKey ?? "").trim();
625
+ const normalizedMessage = normalizeMessageObject(update?.message, update?.messageId);
626
+ if (!sessionKey) {
627
+ this.logger?.warn?.(
628
+ `[mobcode-store] appendMessageSync skipped rawSessionKey=${String(update?.sessionKey ?? "").trim() || "-"} messageId=${String(update?.messageId ?? normalizedMessage?.id ?? "").trim() || "-"} reason=missing_session_key`,
629
+ );
630
+ return { sessionKey: null, count: 0, skipped: true };
631
+ }
632
+ if (!this.database) {
633
+ this.logger?.warn?.(
634
+ `[mobcode-store] appendMessageSync skipped sessionKey=${sessionKey} messageId=${String(update?.messageId ?? normalizedMessage?.id ?? "").trim() || "-"} reason=store_not_initialized`,
635
+ );
636
+ return { sessionKey, count: 0, skipped: true };
637
+ }
638
+ this.logger?.warn?.(
639
+ `[mobcode-debug] appendMessageSync sessionKey=${sessionKey} messageId=${String(update?.messageId ?? normalizedMessage?.id ?? "").trim() || "-"} role=${String(normalizedMessage?.role ?? "").trim() || "-"} toolCallId=${String(normalizedMessage?.toolCallId ?? "").trim() || "-"} sourceKey=${createMessageSourceKey(sessionKey, String(update?.messageId ?? normalizedMessage?.id ?? "").trim() || null, normalizedMessage)}`,
640
+ );
641
+ return this._appendMessageInternal({
642
+ sessionKey,
643
+ normalizedMessage,
644
+ messageId: String(update?.messageId ?? normalizedMessage?.id ?? "").trim() || null,
645
+ });
646
+ }
647
+
648
+ _appendMessageInternal({ sessionKey, normalizedMessage, messageId }) {
649
+ const db = this._db();
650
+ const insert = db.prepare(
651
+ `INSERT OR IGNORE INTO messages(
652
+ session_key,
653
+ source_key,
654
+ message_id,
655
+ role,
656
+ text,
657
+ raw_json,
658
+ created_at_ms,
659
+ indexed_at
660
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?)`,
661
+ );
662
+ const touchSession = db.prepare(
663
+ `INSERT INTO indexed_sessions(session_key, updated_at)
664
+ VALUES(?, ?)
665
+ ON CONFLICT(session_key) DO UPDATE SET updated_at=excluded.updated_at`,
666
+ );
667
+ const upsertArtifact = db.prepare(
668
+ `INSERT INTO artifacts(
669
+ artifact_id,
670
+ session_key,
671
+ run_id,
672
+ kind,
673
+ title,
674
+ summary,
675
+ document_json,
676
+ created_at_ms,
677
+ updated_at_ms
678
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
679
+ ON CONFLICT(artifact_id) DO UPDATE SET
680
+ session_key=excluded.session_key,
681
+ run_id=excluded.run_id,
682
+ kind=excluded.kind,
683
+ title=excluded.title,
684
+ summary=excluded.summary,
685
+ document_json=excluded.document_json,
686
+ updated_at_ms=excluded.updated_at_ms`,
687
+ );
688
+ const now = new Date().toISOString();
689
+ const sourceKey = createMessageSourceKey(sessionKey, messageId, normalizedMessage);
690
+ const timestampRaw =
691
+ normalizedMessage?.timestamp ?? normalizedMessage?.createdAt ?? Date.now();
692
+ const createdAtMs =
693
+ typeof timestampRaw === "number"
694
+ ? timestampRaw
695
+ : Number.parseInt(String(timestampRaw), 10) || Date.now();
696
+
697
+ db.exec("BEGIN");
698
+ try {
699
+ insert.run(
700
+ sessionKey,
701
+ sourceKey,
702
+ messageId,
703
+ String(normalizedMessage?.role ?? "").trim() || null,
704
+ extractMessageText(normalizedMessage),
705
+ toJson(normalizedMessage),
706
+ createdAtMs,
707
+ now,
708
+ );
709
+ const artifactDocument = extractArtifactDocumentFromMessage(normalizedMessage);
710
+ const artifactId = String(
711
+ artifactDocument?.artifactId ?? artifactDocument?.artifact_id ?? "",
712
+ ).trim();
713
+ const artifactKind = String(artifactDocument?.kind ?? "").trim();
714
+ const artifactTitle = String(artifactDocument?.title ?? "").trim();
715
+ if (artifactId && artifactKind && artifactTitle) {
716
+ upsertArtifact.run(
717
+ artifactId,
718
+ sessionKey,
719
+ String(normalizedMessage?.runId ?? normalizedMessage?.run_id ?? "").trim() || null,
720
+ artifactKind,
721
+ artifactTitle,
722
+ String(artifactDocument?.summary ?? "").trim() || null,
723
+ toJson(artifactDocument),
724
+ createdAtMs,
725
+ createdAtMs,
726
+ );
727
+ }
728
+ touchSession.run(sessionKey, now);
729
+ db.exec("COMMIT");
730
+ } catch (error) {
731
+ try {
732
+ db.exec("ROLLBACK");
733
+ } catch {
734
+ // Ignore rollback failures so the original write error is preserved.
735
+ }
736
+ throw error;
737
+ }
738
+ const row = db.prepare(`SELECT COUNT(*) AS count FROM messages WHERE session_key=?`).get(sessionKey);
1003
739
  return { sessionKey, count: Number(row?.count ?? 0), skipped: false };
1004
740
  }
1005
741
 
@@ -1031,15 +767,14 @@ export class MobcodeStateStore {
1031
767
  const normalizedBeforeId =
1032
768
  typeof beforeId === "number" && Number.isFinite(beforeId) ? beforeId : null;
1033
769
  const transcriptMessages = await this.pageTranscriptMessages(normalizedSessionKey);
1034
- const projected = projectConversationMessages(transcriptMessages, normalizedSessionKey);
1035
770
  this.logger?.info?.(
1036
- `[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 ?? "-"}`
1037
772
  );
1038
773
  const endExclusive = normalizedBeforeId == null
1039
- ? projected.length
1040
- : Math.max(0, Math.min(projected.length, normalizedBeforeId - 1));
774
+ ? transcriptMessages.length
775
+ : Math.max(0, Math.min(transcriptMessages.length, normalizedBeforeId - 1));
1041
776
  const start = Math.max(0, endExclusive - normalizedLimit);
1042
- const items = projected.slice(start, endExclusive);
777
+ const items = transcriptMessages.slice(start, endExclusive);
1043
778
  const hasMore = start > 0;
1044
779
  const nextBeforeId = hasMore ? start + 1 : null;
1045
780
 
@@ -1049,7 +784,7 @@ export class MobcodeStateStore {
1049
784
  rawMessages: transcriptMessages,
1050
785
  nextBeforeId,
1051
786
  hasMore,
1052
- total: projected.length,
787
+ total: transcriptMessages.length,
1053
788
  };
1054
789
  }
1055
790
 
@@ -1058,6 +793,67 @@ export class MobcodeStateStore {
1058
793
  return page.items;
1059
794
  }
1060
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
+
1061
857
  async recordApprovalRequested(payload) {
1062
858
  const request = payload?.request && typeof payload.request === "object" ? payload.request : {};
1063
859
  const approvalId = String(payload?.id ?? "").trim();