@convex-dev/agent 0.2.8-alpha.2 → 0.2.8-alpha.4
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/dist/UIMessages.d.ts +2 -1
- package/dist/UIMessages.d.ts.map +1 -1
- package/dist/UIMessages.js +12 -17
- package/dist/UIMessages.js.map +1 -1
- package/dist/client/createTool.d.ts +1 -1
- package/dist/client/createTool.d.ts.map +1 -1
- package/dist/client/createTool.js +1 -1
- package/dist/client/createTool.js.map +1 -1
- package/dist/client/mockModel.d.ts +10 -0
- package/dist/client/mockModel.d.ts.map +1 -1
- package/dist/client/mockModel.js +73 -62
- package/dist/client/mockModel.js.map +1 -1
- package/dist/component/streams.d.ts +8 -8
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +18 -21
- package/dist/component/streams.js.map +1 -1
- package/dist/deltas.d.ts.map +1 -1
- package/dist/deltas.js +1 -0
- package/dist/deltas.js.map +1 -1
- package/dist/react/types.d.ts +1 -1
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts +2 -2
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -1
- package/dist/react/useStreamingUIMessages.js +22 -18
- package/dist/react/useStreamingUIMessages.js.map +1 -1
- package/dist/react/useThreadMessages.d.ts +2 -2
- package/dist/react/useThreadMessages.d.ts.map +1 -1
- package/dist/react/useThreadMessages.js +3 -2
- package/dist/react/useThreadMessages.js.map +1 -1
- package/dist/react/useUIMessages.d.ts +10 -6
- package/dist/react/useUIMessages.d.ts.map +1 -1
- package/dist/react/useUIMessages.js +24 -34
- package/dist/react/useUIMessages.js.map +1 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +7 -4
- package/dist/shared.js.map +1 -1
- package/package.json +1 -1
- package/src/UIMessages.ts +16 -21
- package/src/client/createTool.ts +2 -2
- package/src/client/mockModel.ts +108 -76
- package/src/component/streams.ts +20 -21
- package/src/deltas.test.ts +311 -97
- package/src/deltas.ts +4 -0
- package/src/react/types.ts +1 -1
- package/src/react/useStreamingUIMessages.ts +36 -28
- package/src/react/useThreadMessages.ts +13 -8
- package/src/react/useUIMessages.test.ts +255 -0
- package/src/react/useUIMessages.ts +38 -52
- package/src/shared.ts +6 -4
- package/src/toUIMessages.test.ts +1 -1
package/src/UIMessages.ts
CHANGED
|
@@ -27,6 +27,8 @@ import type {
|
|
|
27
27
|
} from "./validators.js";
|
|
28
28
|
import { omit, pick } from "convex-helpers";
|
|
29
29
|
|
|
30
|
+
export type UIStatus = "streaming" | MessageStatus;
|
|
31
|
+
|
|
30
32
|
export type UIMessage<
|
|
31
33
|
METADATA = unknown,
|
|
32
34
|
DATA_PARTS extends UIDataTypes = UIDataTypes,
|
|
@@ -35,7 +37,7 @@ export type UIMessage<
|
|
|
35
37
|
key: string;
|
|
36
38
|
order: number;
|
|
37
39
|
stepOrder: number;
|
|
38
|
-
status:
|
|
40
|
+
status: UIStatus;
|
|
39
41
|
agentName?: string;
|
|
40
42
|
text: string;
|
|
41
43
|
_creationTime: number;
|
|
@@ -160,7 +162,7 @@ export function toUIMessages<
|
|
|
160
162
|
messages: (MessageDoc & ExtraFields<METADATA>)[],
|
|
161
163
|
): UIMessage<METADATA, DATA_PARTS, TOOLS>[] {
|
|
162
164
|
// Group assistant and tool messages together
|
|
163
|
-
const assistantGroups = groupAssistantMessages(messages);
|
|
165
|
+
const assistantGroups = groupAssistantMessages(sorted(messages));
|
|
164
166
|
|
|
165
167
|
const uiMessages: UIMessage<METADATA, DATA_PARTS, TOOLS>[] = [];
|
|
166
168
|
for (const group of assistantGroups) {
|
|
@@ -196,13 +198,10 @@ function groupAssistantMessages<METADATA = unknown>(
|
|
|
196
198
|
): Group<METADATA>[] {
|
|
197
199
|
const groups: Group<METADATA>[] = [];
|
|
198
200
|
|
|
199
|
-
// Sort messages by order and stepOrder first to handle out-of-order arrivals
|
|
200
|
-
const sortedMessages = sorted(messages);
|
|
201
|
-
|
|
202
201
|
let currentAssistantGroup: (MessageDoc & ExtraFields<METADATA>)[] = [];
|
|
203
202
|
let currentOrder: number | undefined;
|
|
204
203
|
|
|
205
|
-
for (const message of
|
|
204
|
+
for (const message of messages) {
|
|
206
205
|
const coreMessage = message.message && deserializeMessage(message.message);
|
|
207
206
|
if (!coreMessage) continue;
|
|
208
207
|
|
|
@@ -239,7 +238,6 @@ function groupAssistantMessages<METADATA = unknown>(
|
|
|
239
238
|
currentAssistantGroup.push(message);
|
|
240
239
|
|
|
241
240
|
// End group if this is an assistant message without tool calls
|
|
242
|
-
// But only if we're processing messages in order (which we are now due to sorting)
|
|
243
241
|
if (coreMessage.role === "assistant" && !message.tool) {
|
|
244
242
|
groups.push({
|
|
245
243
|
role: "assistant",
|
|
@@ -294,7 +292,7 @@ function createSystemUIMessage<
|
|
|
294
292
|
|
|
295
293
|
function extractTextFromMessageDoc(message: MessageDoc): string {
|
|
296
294
|
return (
|
|
297
|
-
|
|
295
|
+
(message.message && extractText(message.message)) || message.text || ""
|
|
298
296
|
);
|
|
299
297
|
}
|
|
300
298
|
|
|
@@ -373,9 +371,9 @@ function createAssistantUIMessage<
|
|
|
373
371
|
|
|
374
372
|
// Concatenate text from all messages in group
|
|
375
373
|
const allText = group
|
|
376
|
-
.map((msg) =>
|
|
374
|
+
.map((msg) => extractTextFromMessageDoc(msg))
|
|
377
375
|
.filter(Boolean)
|
|
378
|
-
.join("");
|
|
376
|
+
.join(" ");
|
|
379
377
|
|
|
380
378
|
// Get status from last message
|
|
381
379
|
const lastMessage = group[group.length - 1];
|
|
@@ -393,8 +391,7 @@ function createAssistantUIMessage<
|
|
|
393
391
|
const content = coreMessage.content;
|
|
394
392
|
const nonStringContent =
|
|
395
393
|
content && typeof content !== "string" ? content : [];
|
|
396
|
-
const text =
|
|
397
|
-
message.text || ((message.message && extractText(message.message)) ?? "");
|
|
394
|
+
const text = extractTextFromMessageDoc(message);
|
|
398
395
|
|
|
399
396
|
const partCommon = {
|
|
400
397
|
state: message.streaming ? ("streaming" as const) : ("done" as const),
|
|
@@ -465,6 +462,10 @@ function createAssistantUIMessage<
|
|
|
465
462
|
break;
|
|
466
463
|
}
|
|
467
464
|
case "tool-result": {
|
|
465
|
+
const output =
|
|
466
|
+
contentPart.output?.type === "json"
|
|
467
|
+
? contentPart.output.value
|
|
468
|
+
: contentPart.output;
|
|
468
469
|
const call = allParts.find(
|
|
469
470
|
(part) =>
|
|
470
471
|
part.type === `tool-${contentPart.toolName}` &&
|
|
@@ -475,13 +476,10 @@ function createAssistantUIMessage<
|
|
|
475
476
|
if (message.error) {
|
|
476
477
|
call.state = "output-error";
|
|
477
478
|
call.errorText = message.error;
|
|
478
|
-
call.output =
|
|
479
|
+
call.output = output;
|
|
479
480
|
} else {
|
|
480
481
|
call.state = "output-available";
|
|
481
|
-
call.output =
|
|
482
|
-
contentPart.output?.type === "json"
|
|
483
|
-
? contentPart.output.value
|
|
484
|
-
: contentPart.output;
|
|
482
|
+
call.output = output;
|
|
485
483
|
}
|
|
486
484
|
} else {
|
|
487
485
|
console.warn(
|
|
@@ -503,10 +501,7 @@ function createAssistantUIMessage<
|
|
|
503
501
|
toolCallId: contentPart.toolCallId,
|
|
504
502
|
state: "output-available",
|
|
505
503
|
input: undefined,
|
|
506
|
-
output
|
|
507
|
-
contentPart.output?.type === "json"
|
|
508
|
-
? contentPart.output.value
|
|
509
|
-
: contentPart.output,
|
|
504
|
+
output,
|
|
510
505
|
callProviderMetadata: message.providerMetadata,
|
|
511
506
|
} satisfies ToolUIPart<TOOLS>);
|
|
512
507
|
}
|
package/src/client/createTool.ts
CHANGED
|
@@ -44,7 +44,7 @@ export function createTool<INPUT, OUTPUT, Ctx extends ToolCtx = ToolCtx>(def: {
|
|
|
44
44
|
ctx: Ctx,
|
|
45
45
|
args: INPUT,
|
|
46
46
|
options: ToolCallOptions,
|
|
47
|
-
) => PromiseLike<OUTPUT>;
|
|
47
|
+
) => PromiseLike<OUTPUT> | AsyncIterable<OUTPUT>;
|
|
48
48
|
/**
|
|
49
49
|
* Provide the context to use, e.g. when defining the tool at runtime.
|
|
50
50
|
*/
|
|
@@ -85,7 +85,7 @@ export function createTool<INPUT, OUTPUT, Ctx extends ToolCtx = ToolCtx>(def: {
|
|
|
85
85
|
ctx: def.ctx,
|
|
86
86
|
description: def.description,
|
|
87
87
|
inputSchema: def.args,
|
|
88
|
-
|
|
88
|
+
execute(args: INPUT, options: ToolCallOptions) {
|
|
89
89
|
if (!getCtx(this)) {
|
|
90
90
|
throw new Error(
|
|
91
91
|
"To use a Convex tool, you must either provide the ctx" +
|
package/src/client/mockModel.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
LanguageModelV2StreamPart,
|
|
5
5
|
} from "@ai-sdk/provider";
|
|
6
6
|
import { simulateReadableStream, type ProviderMetadata } from "ai";
|
|
7
|
-
import { pick } from "convex-helpers";
|
|
7
|
+
import { assert, pick } from "convex-helpers";
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_TEXT = `
|
|
10
10
|
A A A A A A A A A A A A A A A
|
|
@@ -22,8 +22,18 @@ export type MockModelArgs = {
|
|
|
22
22
|
| (() => LanguageModelV2["supportedUrls"]);
|
|
23
23
|
chunkDelayInMs?: number;
|
|
24
24
|
initialDelayInMs?: number;
|
|
25
|
-
|
|
25
|
+
/** A list of the responses for multiple steps.
|
|
26
|
+
* For tool calls, the first list would include a tool call part,
|
|
27
|
+
* then the next list would be after the tool response or another tool call.
|
|
28
|
+
* Tool responses come from actual tool calls!
|
|
29
|
+
*/
|
|
30
|
+
contentSteps?: LanguageModelV2Content[][];
|
|
31
|
+
/** A single list of content responded from each step.
|
|
32
|
+
* Provide contentSteps instead if you want to do multi-step responses with
|
|
33
|
+
* tool calls.
|
|
34
|
+
*/
|
|
26
35
|
content?: LanguageModelV2Content[];
|
|
36
|
+
// provide either content, contentResponses or doGenerate & doStream
|
|
27
37
|
doGenerate?: LanguageModelV2["doGenerate"];
|
|
28
38
|
doStream?: LanguageModelV2["doStream"];
|
|
29
39
|
providerMetadata?: ProviderMetadata;
|
|
@@ -35,6 +45,10 @@ export type MockModelArgs = {
|
|
|
35
45
|
};
|
|
36
46
|
};
|
|
37
47
|
|
|
48
|
+
function atMostOneOf(...args: unknown[]) {
|
|
49
|
+
return args.filter(Boolean).length <= 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
export function mockModel(args?: MockModelArgs): LanguageModelV2 {
|
|
39
53
|
return new MockLanguageModel(args ?? {});
|
|
40
54
|
}
|
|
@@ -54,10 +68,19 @@ export class MockLanguageModel implements LanguageModelV2 {
|
|
|
54
68
|
doStreamCalls: Parameters<LanguageModelV2["doStream"]>[0][] = [];
|
|
55
69
|
|
|
56
70
|
constructor(args: MockModelArgs) {
|
|
71
|
+
assert(
|
|
72
|
+
atMostOneOf(
|
|
73
|
+
args.content,
|
|
74
|
+
args.contentSteps,
|
|
75
|
+
args.doGenerate && args.doStream,
|
|
76
|
+
),
|
|
77
|
+
"Expected only one of content, contentSteps, or doGenerate and doStream",
|
|
78
|
+
);
|
|
57
79
|
this.provider = args.provider || "mock-provider";
|
|
58
80
|
this.modelId = args.modelId || "mock-model-id";
|
|
59
81
|
const {
|
|
60
82
|
content = [{ type: "text", text: DEFAULT_TEXT }],
|
|
83
|
+
contentSteps = [content],
|
|
61
84
|
chunkDelayInMs = 0,
|
|
62
85
|
initialDelayInMs = 0,
|
|
63
86
|
supportedUrls = {},
|
|
@@ -72,77 +95,83 @@ export class MockLanguageModel implements LanguageModelV2 {
|
|
|
72
95
|
"Mock error message";
|
|
73
96
|
const metadata = pick(args, ["providerMetadata"]);
|
|
74
97
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
type
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
(
|
|
95
|
-
(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
type
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
(
|
|
117
|
-
(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
98
|
+
const chunkResponses: LanguageModelV2StreamPart[][] = contentSteps.map(
|
|
99
|
+
(content) => {
|
|
100
|
+
const chunks: LanguageModelV2StreamPart[] = [
|
|
101
|
+
{ type: "stream-start", warnings: [] },
|
|
102
|
+
];
|
|
103
|
+
chunks.push(
|
|
104
|
+
...content.flatMap((c, ci): LanguageModelV2StreamPart[] => {
|
|
105
|
+
if (c.type !== "text" && c.type !== "reasoning") {
|
|
106
|
+
return [c];
|
|
107
|
+
}
|
|
108
|
+
const metadata = pick(c, ["providerMetadata"]);
|
|
109
|
+
const deltas = c.text.split(" ");
|
|
110
|
+
const parts: LanguageModelV2StreamPart[] = [];
|
|
111
|
+
if (c.type === "reasoning") {
|
|
112
|
+
parts.push({
|
|
113
|
+
type: "reasoning-start",
|
|
114
|
+
id: `reasoning-${ci}`,
|
|
115
|
+
...metadata,
|
|
116
|
+
});
|
|
117
|
+
parts.push(
|
|
118
|
+
...deltas.map(
|
|
119
|
+
(delta, di) =>
|
|
120
|
+
({
|
|
121
|
+
type: "reasoning-delta",
|
|
122
|
+
delta: (di ? " " : "") + delta,
|
|
123
|
+
id: `reasoning-${ci}`,
|
|
124
|
+
...metadata,
|
|
125
|
+
}) satisfies LanguageModelV2StreamPart,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
parts.push({
|
|
129
|
+
type: "reasoning-end",
|
|
130
|
+
id: `reasoning-${ci}`,
|
|
131
|
+
...metadata,
|
|
132
|
+
});
|
|
133
|
+
} else if (c.type === "text") {
|
|
134
|
+
parts.push({
|
|
135
|
+
type: "text-start",
|
|
136
|
+
id: `txt-${ci}`,
|
|
137
|
+
...metadata,
|
|
138
|
+
});
|
|
139
|
+
parts.push(
|
|
140
|
+
...deltas.map(
|
|
141
|
+
(delta, di) =>
|
|
142
|
+
({
|
|
143
|
+
type: "text-delta",
|
|
144
|
+
delta: (di ? " " : "") + delta,
|
|
145
|
+
id: `txt-${ci}`,
|
|
146
|
+
...metadata,
|
|
147
|
+
}) satisfies LanguageModelV2StreamPart,
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
parts.push({
|
|
151
|
+
type: "text-end",
|
|
152
|
+
id: `txt-${ci}`,
|
|
153
|
+
...metadata,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return parts;
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
if (fail) {
|
|
160
|
+
chunks.push({
|
|
161
|
+
type: "error",
|
|
162
|
+
error,
|
|
129
163
|
});
|
|
130
164
|
}
|
|
131
|
-
|
|
132
|
-
|
|
165
|
+
chunks.push({
|
|
166
|
+
type: "finish",
|
|
167
|
+
finishReason: fail ? "error" : "stop",
|
|
168
|
+
usage: DEFAULT_USAGE,
|
|
169
|
+
...metadata,
|
|
170
|
+
});
|
|
171
|
+
return chunks;
|
|
172
|
+
},
|
|
133
173
|
);
|
|
134
|
-
|
|
135
|
-
chunks.push({
|
|
136
|
-
type: "error",
|
|
137
|
-
error,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
chunks.push({
|
|
141
|
-
type: "finish",
|
|
142
|
-
finishReason: fail ? "error" : "stop",
|
|
143
|
-
usage: DEFAULT_USAGE,
|
|
144
|
-
...metadata,
|
|
145
|
-
});
|
|
174
|
+
let callIndex = 0;
|
|
146
175
|
this.doGenerate = async (options) => {
|
|
147
176
|
this.doGenerateCalls.push(options);
|
|
148
177
|
|
|
@@ -153,14 +182,16 @@ export class MockLanguageModel implements LanguageModelV2 {
|
|
|
153
182
|
return args.doGenerate(options);
|
|
154
183
|
} else if (Array.isArray(args.doGenerate)) {
|
|
155
184
|
return args.doGenerate[this.doGenerateCalls.length];
|
|
156
|
-
} else if (
|
|
157
|
-
|
|
158
|
-
content,
|
|
159
|
-
finishReason: "stop",
|
|
185
|
+
} else if (contentSteps.length) {
|
|
186
|
+
const result = {
|
|
187
|
+
content: contentSteps[callIndex % contentSteps.length],
|
|
188
|
+
finishReason: "stop" as const,
|
|
160
189
|
usage: DEFAULT_USAGE,
|
|
161
190
|
...metadata,
|
|
162
191
|
warnings: [],
|
|
163
192
|
};
|
|
193
|
+
callIndex++;
|
|
194
|
+
return result;
|
|
164
195
|
} else {
|
|
165
196
|
throw new Error("Unexpected: no content or doGenerate");
|
|
166
197
|
}
|
|
@@ -176,12 +207,13 @@ export class MockLanguageModel implements LanguageModelV2 {
|
|
|
176
207
|
return args.doStream(options);
|
|
177
208
|
} else if (Array.isArray(args.doStream)) {
|
|
178
209
|
return args.doStream[this.doStreamCalls.length];
|
|
179
|
-
} else if (
|
|
210
|
+
} else if (contentSteps) {
|
|
180
211
|
const stream = simulateReadableStream({
|
|
181
|
-
chunks,
|
|
212
|
+
chunks: chunkResponses[callIndex % chunkResponses.length],
|
|
182
213
|
initialDelayInMs,
|
|
183
214
|
chunkDelayInMs,
|
|
184
215
|
});
|
|
216
|
+
callIndex++;
|
|
185
217
|
|
|
186
218
|
if (options.abortSignal) {
|
|
187
219
|
options.abortSignal.addEventListener("abort", () => {
|
package/src/component/streams.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { v } from "convex/values";
|
|
|
3
3
|
import {
|
|
4
4
|
type MessageWithMetadataInternal,
|
|
5
5
|
type StreamDelta,
|
|
6
|
+
type StreamMessage,
|
|
6
7
|
vStreamDelta,
|
|
7
8
|
vStreamMessage,
|
|
8
9
|
} from "../validators.js";
|
|
@@ -136,23 +137,27 @@ export const list = query({
|
|
|
136
137
|
["order", "stepOrder"],
|
|
137
138
|
).take(100);
|
|
138
139
|
|
|
139
|
-
return messages.map((m) => (
|
|
140
|
-
streamId: m._id,
|
|
141
|
-
status: m.state.kind,
|
|
142
|
-
...pick(m, [
|
|
143
|
-
"format",
|
|
144
|
-
"order",
|
|
145
|
-
"stepOrder",
|
|
146
|
-
"userId",
|
|
147
|
-
"agentName",
|
|
148
|
-
"model",
|
|
149
|
-
"provider",
|
|
150
|
-
"providerOptions",
|
|
151
|
-
]),
|
|
152
|
-
}));
|
|
140
|
+
return messages.map((m) => publicStreamMessage(m));
|
|
153
141
|
},
|
|
154
142
|
});
|
|
155
143
|
|
|
144
|
+
function publicStreamMessage(m: Doc<"streamingMessages">): StreamMessage {
|
|
145
|
+
return {
|
|
146
|
+
streamId: m._id,
|
|
147
|
+
status: m.state.kind,
|
|
148
|
+
...pick(m, [
|
|
149
|
+
"format",
|
|
150
|
+
"order",
|
|
151
|
+
"stepOrder",
|
|
152
|
+
"userId",
|
|
153
|
+
"agentName",
|
|
154
|
+
"model",
|
|
155
|
+
"provider",
|
|
156
|
+
"providerOptions",
|
|
157
|
+
]),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
156
161
|
export const abortByOrder = mutation({
|
|
157
162
|
args: { threadId: v.id("threads"), order: v.number(), reason: v.string() },
|
|
158
163
|
returns: v.boolean(),
|
|
@@ -526,13 +531,7 @@ export async function getStreamingMessagesWithMetadata(
|
|
|
526
531
|
.take(1000);
|
|
527
532
|
const uiMessages = await deriveUIMessagesFromDeltas(
|
|
528
533
|
threadId,
|
|
529
|
-
[
|
|
530
|
-
{
|
|
531
|
-
...streamingMessage,
|
|
532
|
-
status: "streaming",
|
|
533
|
-
streamId: streamingMessage._id,
|
|
534
|
-
},
|
|
535
|
-
],
|
|
534
|
+
[publicStreamMessage(streamingMessage)],
|
|
536
535
|
deltas,
|
|
537
536
|
);
|
|
538
537
|
// We don't save messages that have already been saved
|