@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 +1 -1
- package/src/gateway-methods.js +29 -0
- package/src/plugin-definition.js +10 -0
- package/src/state-store.js +189 -393
package/package.json
CHANGED
package/src/gateway-methods.js
CHANGED
|
@@ -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) {
|
package/src/plugin-definition.js
CHANGED
|
@@ -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
|
}
|
package/src/state-store.js
CHANGED
|
@@ -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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
.
|
|
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}
|
|
771
|
+
`[mobcode-store] pageSessionMessages session=${normalizedSessionKey} transcriptMessages=${transcriptMessages.length} limit=${normalizedLimit} beforeId=${normalizedBeforeId ?? "-"}`
|
|
1037
772
|
);
|
|
1038
773
|
const endExclusive = normalizedBeforeId == null
|
|
1039
|
-
?
|
|
1040
|
-
: Math.max(0, Math.min(
|
|
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 =
|
|
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:
|
|
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();
|