@gestaltrun/dsh-im-runtime 0.1.0 → 0.1.6
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/README.i18n.yaml +35 -20
- package/README.md +142 -10
- package/README.zh.md +141 -9
- package/lib/index.js +140 -2
- package/lib/inline-image-DuX6vSEI.js +20 -0
- package/lib/read-receipts.js +413 -0
- package/lib/read-state-CnUZKCuY.js +1868 -0
- package/lib/runtime-Bm6iSEpv.js +10939 -0
- package/lib/tools.js +1 -1
- package/lib/types/account-placement.d.ts +22 -0
- package/lib/types/admission-annotations.d.ts +40 -0
- package/lib/types/agent-coordinator.d.ts +47 -2
- package/lib/types/agent-media.d.ts +46 -0
- package/lib/types/agent-prompt.d.ts +8 -2
- package/lib/types/checkpoint-recovery.d.ts +32 -0
- package/lib/types/checkpoint.d.ts +127 -0
- package/lib/types/consumer-chat-links.d.ts +59 -0
- package/lib/types/delivery-schema.d.ts +126 -2
- package/lib/types/delivery-sqlite.d.ts +206 -6
- package/lib/types/delivery-types.d.ts +97 -1
- package/lib/types/delivery.d.ts +29 -6
- package/lib/types/errors.d.ts +1 -1
- package/lib/types/evaluation-accounts.d.ts +86 -0
- package/lib/types/index.d.ts +34 -2
- package/lib/types/inline-image.d.ts +7 -0
- package/lib/types/input-drain.d.ts +73 -0
- package/lib/types/knowledge-image.d.ts +64 -0
- package/lib/types/media-lifetime.d.ts +22 -0
- package/lib/types/media.d.ts +32 -0
- package/lib/types/message-preview.d.ts +4 -0
- package/lib/types/message-upload.d.ts +55 -0
- package/lib/types/nas-host-lease.d.ts +33 -0
- package/lib/types/placement-slice.d.ts +30 -0
- package/lib/types/policies.d.ts +10 -0
- package/lib/types/preset-model.d.ts +50 -0
- package/lib/types/read-context.d.ts +27 -0
- package/lib/types/read-receipts.d.ts +10 -0
- package/lib/types/read-records.d.ts +30 -0
- package/lib/types/read-state.d.ts +18 -0
- package/lib/types/read-types.d.ts +28 -0
- package/lib/types/restart-recovery.d.ts +38 -0
- package/lib/types/runtime.d.ts +318 -10
- package/lib/types/schema.d.ts +13 -2
- package/lib/types/script-clock.d.ts +14 -0
- package/lib/types/sdk.d.ts +108 -0
- package/lib/types/service-types.d.ts +161 -4
- package/lib/types/session-events.d.ts +9 -9
- package/lib/types/simulation-batch.d.ts +40 -0
- package/lib/types/simulation-controller.d.ts +16 -3
- package/lib/types/simulation-operator.d.ts +19 -0
- package/lib/types/sqlite-connection.d.ts +27 -0
- package/lib/types/time.d.ts +13 -0
- package/lib/types/transport.d.ts +44 -4
- package/lib/types/transports.d.ts +7 -0
- package/lib/types/types.d.ts +22 -5
- package/lib/types/typing.d.ts +33 -0
- package/lib/types/upload-types.d.ts +121 -0
- package/lib/types.js +2 -1
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +67 -41
- package/lib/runtime-C0-S-_CC.js +0 -6758
|
@@ -0,0 +1,1868 @@
|
|
|
1
|
+
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
2
|
+
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
3
|
+
import { WorkspaceId } from "@deepseek-ai/dsh-workspace";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import { basename } from "node:path";
|
|
7
|
+
//#region src/errors.ts
|
|
8
|
+
/** Structured IM runtime failure for requests that cannot produce a durable receipt. */
|
|
9
|
+
var ImRuntimeError = class extends Error {
|
|
10
|
+
code;
|
|
11
|
+
/** @param code - stable failure code. @param message - diagnostic detail. */
|
|
12
|
+
constructor(code, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = "ImRuntimeError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/delivery-schema.ts
|
|
20
|
+
const accountId = z.string().min(1).transform((value) => brandString(value));
|
|
21
|
+
const operationId = z.string().min(1).transform((value) => brandString(value));
|
|
22
|
+
const messageId = z.string().min(1).transform((value) => brandString(value));
|
|
23
|
+
const outboundRequestId = z.string().min(1).transform((value) => brandString(value));
|
|
24
|
+
const outboundAttemptId = z.string().min(1).transform((value) => brandString(value));
|
|
25
|
+
const scopeId = z.string().min(1).transform((value) => brandString(value));
|
|
26
|
+
const providerCursorId = z.string().min(1).transform((value) => brandString(value));
|
|
27
|
+
const admissionId = z.string().min(1).transform((value) => brandString(value));
|
|
28
|
+
const agentTaskId = z.string().min(1).transform((value) => brandString(value));
|
|
29
|
+
const timestamp$1 = z.iso.datetime();
|
|
30
|
+
/** Captured platform identities; legacy observations omit the whole value. */
|
|
31
|
+
const imUploadIdentitySchema = z.object({
|
|
32
|
+
senderAccountId: z.string().min(1).nullable(),
|
|
33
|
+
senderDisplayName: z.string().min(1).nullable(),
|
|
34
|
+
senderDomain: z.string().min(1).nullable(),
|
|
35
|
+
assignedAccountId: z.string().min(1).nullable()
|
|
36
|
+
});
|
|
37
|
+
const realScope = z.object({
|
|
38
|
+
kind: z.literal("real"),
|
|
39
|
+
platform: z.enum(["dingtalk", "wangwang"]),
|
|
40
|
+
accountId,
|
|
41
|
+
conversationKind: z.enum(["direct", "group"]),
|
|
42
|
+
conversationId: z.string().min(1)
|
|
43
|
+
});
|
|
44
|
+
const simulationScope = z.object({
|
|
45
|
+
kind: z.literal("simulation"),
|
|
46
|
+
instanceId: z.string().min(1).transform((value) => brandString(value)),
|
|
47
|
+
platform: z.enum(["dingtalk", "wangwang"]),
|
|
48
|
+
accountId,
|
|
49
|
+
conversationKind: z.enum(["direct", "group"]),
|
|
50
|
+
conversationId: z.string().min(1)
|
|
51
|
+
});
|
|
52
|
+
const imDeliveryScopeSchema = z.discriminatedUnion("kind", [realScope, simulationScope]);
|
|
53
|
+
const humanDshSender = z.object({
|
|
54
|
+
kind: z.literal("human-dsh"),
|
|
55
|
+
outboundRequestId,
|
|
56
|
+
providerActorId: z.string().min(1).optional()
|
|
57
|
+
});
|
|
58
|
+
const sender = z.discriminatedUnion("kind", [
|
|
59
|
+
z.object({
|
|
60
|
+
kind: z.literal("external"),
|
|
61
|
+
senderId: z.string().min(1),
|
|
62
|
+
senderDisplayName: z.string().optional(),
|
|
63
|
+
userId: z.string().min(1).optional(),
|
|
64
|
+
openDingTalkId: z.string().min(1).optional()
|
|
65
|
+
}),
|
|
66
|
+
z.object({
|
|
67
|
+
kind: z.literal("human-native"),
|
|
68
|
+
accountId,
|
|
69
|
+
providerActorId: z.string().min(1)
|
|
70
|
+
}),
|
|
71
|
+
humanDshSender,
|
|
72
|
+
z.object({
|
|
73
|
+
kind: z.literal("ai"),
|
|
74
|
+
outboundRequestId
|
|
75
|
+
}),
|
|
76
|
+
z.object({
|
|
77
|
+
kind: z.literal("unknown"),
|
|
78
|
+
reason: z.enum([
|
|
79
|
+
"insufficient-evidence",
|
|
80
|
+
"unmatched-self",
|
|
81
|
+
"unmatched-echo",
|
|
82
|
+
"provider-unknown"
|
|
83
|
+
]),
|
|
84
|
+
observedSenderId: z.string().optional()
|
|
85
|
+
})
|
|
86
|
+
]);
|
|
87
|
+
const quote = z.object({
|
|
88
|
+
text: z.string(),
|
|
89
|
+
senderDisplayName: z.string().min(1).optional(),
|
|
90
|
+
externalMessageId: z.string().min(1).optional()
|
|
91
|
+
});
|
|
92
|
+
const forbiddenDetailKeys = /* @__PURE__ */ new Set([
|
|
93
|
+
"accesskey",
|
|
94
|
+
"accesskeyid",
|
|
95
|
+
"accesskeysecret",
|
|
96
|
+
"authorization",
|
|
97
|
+
"connectionconfig",
|
|
98
|
+
"cookie",
|
|
99
|
+
"credential",
|
|
100
|
+
"credentials",
|
|
101
|
+
"header",
|
|
102
|
+
"headers",
|
|
103
|
+
"password",
|
|
104
|
+
"secret",
|
|
105
|
+
"token",
|
|
106
|
+
"transportconfig"
|
|
107
|
+
]);
|
|
108
|
+
function unsafeDetailPath(value, path = []) {
|
|
109
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
for (const [index, entry] of value.entries()) {
|
|
112
|
+
const unsafe = unsafeDetailPath(entry, [...path, String(index)]);
|
|
113
|
+
if (unsafe !== void 0) return unsafe;
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
118
|
+
const next = [...path, key];
|
|
119
|
+
if (forbiddenDetailKeys.has(key.replaceAll("-", "").replaceAll("_", "").toLowerCase())) return next;
|
|
120
|
+
const unsafe = unsafeDetailPath(entry, next);
|
|
121
|
+
if (unsafe !== void 0) return unsafe;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const displaySafeJson = z.json().superRefine((value, context) => {
|
|
125
|
+
const path = unsafeDetailPath(value);
|
|
126
|
+
if (path !== void 0) context.addIssue({
|
|
127
|
+
code: "custom",
|
|
128
|
+
message: "unsupported-message details contain private transport configuration",
|
|
129
|
+
path: [...path]
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
const content = z.discriminatedUnion("format", [
|
|
133
|
+
z.object({
|
|
134
|
+
text: z.string(),
|
|
135
|
+
format: z.literal("media"),
|
|
136
|
+
media: z.object({
|
|
137
|
+
kind: z.enum(["image", "file"]),
|
|
138
|
+
name: z.string().min(1),
|
|
139
|
+
mime: z.string(),
|
|
140
|
+
bytes: z.number().int().nonnegative(),
|
|
141
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
142
|
+
sourceFile: z.object({
|
|
143
|
+
attachmentId: z.string().min(1).transform((value) => brandString(value)),
|
|
144
|
+
name: z.string().min(1),
|
|
145
|
+
bytes: z.number().int().nonnegative()
|
|
146
|
+
}).optional()
|
|
147
|
+
}),
|
|
148
|
+
quote: quote.optional()
|
|
149
|
+
}),
|
|
150
|
+
z.object({
|
|
151
|
+
text: z.string(),
|
|
152
|
+
format: z.enum(["text", "markdown"]),
|
|
153
|
+
quote: quote.optional()
|
|
154
|
+
}),
|
|
155
|
+
z.object({
|
|
156
|
+
text: z.string(),
|
|
157
|
+
format: z.literal("image"),
|
|
158
|
+
quote: quote.optional()
|
|
159
|
+
}),
|
|
160
|
+
z.object({
|
|
161
|
+
text: z.string(),
|
|
162
|
+
format: z.literal("unsupported"),
|
|
163
|
+
messageType: z.string().min(1),
|
|
164
|
+
details: displaySafeJson,
|
|
165
|
+
quote: quote.optional()
|
|
166
|
+
})
|
|
167
|
+
]);
|
|
168
|
+
/** Parser for provider and JSONL inbound records. */
|
|
169
|
+
const imInboundMessageInputSchema = z.object({
|
|
170
|
+
uploadIdentity: imUploadIdentitySchema.optional(),
|
|
171
|
+
externalMessageId: z.string().min(1),
|
|
172
|
+
sender,
|
|
173
|
+
content,
|
|
174
|
+
occurredAt: timestamp$1,
|
|
175
|
+
mentionedConfiguredAccount: z.boolean().optional()
|
|
176
|
+
});
|
|
177
|
+
const inboundMessage = z.object({
|
|
178
|
+
uploadIdentity: imUploadIdentitySchema.optional(),
|
|
179
|
+
messageId,
|
|
180
|
+
scopeId,
|
|
181
|
+
externalMessageId: z.string().min(1),
|
|
182
|
+
sender,
|
|
183
|
+
content,
|
|
184
|
+
origin: z.enum(["live", "jsonl-import"]),
|
|
185
|
+
stage: z.enum(["received", "submitted"]),
|
|
186
|
+
sequenceNumber: z.number().int().positive(),
|
|
187
|
+
occurredAt: timestamp$1,
|
|
188
|
+
mentionedConfiguredAccount: z.boolean().optional(),
|
|
189
|
+
receivedAt: timestamp$1,
|
|
190
|
+
submission: z.object({
|
|
191
|
+
sessionId: z.string().min(1).transform(SessionId),
|
|
192
|
+
submittedAt: timestamp$1
|
|
193
|
+
}).optional()
|
|
194
|
+
});
|
|
195
|
+
const cursor = z.object({
|
|
196
|
+
scopeId,
|
|
197
|
+
platformCursor: z.string().nullable(),
|
|
198
|
+
lastReceivedSequenceNumber: z.number().int().nonnegative(),
|
|
199
|
+
lastSubmittedSequenceNumber: z.number().int().nonnegative(),
|
|
200
|
+
pendingCount: z.number().int().nonnegative(),
|
|
201
|
+
lastSuppressedSequenceNumber: z.number().int().nonnegative().optional(),
|
|
202
|
+
lastSyncedAt: timestamp$1.optional(),
|
|
203
|
+
updatedAt: timestamp$1
|
|
204
|
+
});
|
|
205
|
+
const conversationPresentation = z.object({
|
|
206
|
+
displayName: z.string().min(1).optional(),
|
|
207
|
+
memberCount: z.number().int().positive().optional(),
|
|
208
|
+
assignedActorId: z.string().min(1).optional()
|
|
209
|
+
});
|
|
210
|
+
const pageResult = z.object({
|
|
211
|
+
kind: z.literal("page"),
|
|
212
|
+
operationId,
|
|
213
|
+
status: z.enum(["applied", "conflict"]),
|
|
214
|
+
acceptedCount: z.number().int().nonnegative(),
|
|
215
|
+
evidenceMergedCount: z.number().int().nonnegative().optional(),
|
|
216
|
+
duplicateCount: z.number().int().nonnegative(),
|
|
217
|
+
messages: z.array(inboundMessage),
|
|
218
|
+
cursor
|
|
219
|
+
});
|
|
220
|
+
const importResult = z.object({
|
|
221
|
+
kind: z.literal("jsonl-import"),
|
|
222
|
+
operationId,
|
|
223
|
+
status: z.literal("applied"),
|
|
224
|
+
fileName: z.string().min(1),
|
|
225
|
+
messageCount: z.number().int().nonnegative(),
|
|
226
|
+
importedCount: z.number().int().nonnegative(),
|
|
227
|
+
duplicateCount: z.number().int().nonnegative()
|
|
228
|
+
});
|
|
229
|
+
const routeBinding = z.object({
|
|
230
|
+
handlingEpoch: z.number().int().nonnegative().optional(),
|
|
231
|
+
routeId: z.string().min(1).transform((value) => brandString(value)),
|
|
232
|
+
routeRevision: z.string().min(1).transform((value) => brandString(value)),
|
|
233
|
+
accountRevision: z.string().min(1).transform((value) => brandString(value)),
|
|
234
|
+
workspaceId: z.string().min(1).transform(WorkspaceId)
|
|
235
|
+
});
|
|
236
|
+
const receipt = z.object({
|
|
237
|
+
providerReceiptId: z.string().optional(),
|
|
238
|
+
providerStatus: z.string().optional(),
|
|
239
|
+
errorCode: z.string().optional(),
|
|
240
|
+
observedAt: timestamp$1
|
|
241
|
+
});
|
|
242
|
+
const outbound = z.object({
|
|
243
|
+
requestId: outboundRequestId,
|
|
244
|
+
scopeId,
|
|
245
|
+
intent: z.enum(["ai", "human-manual"]),
|
|
246
|
+
content,
|
|
247
|
+
media: z.object({
|
|
248
|
+
state: z.enum(["uploading", "prepared"]),
|
|
249
|
+
imageUrl: z.url().optional()
|
|
250
|
+
}).optional(),
|
|
251
|
+
status: z.enum([
|
|
252
|
+
"pending",
|
|
253
|
+
"dispatching",
|
|
254
|
+
"pre-send-failed",
|
|
255
|
+
"sent",
|
|
256
|
+
"result-unknown",
|
|
257
|
+
"confirmed-failed"
|
|
258
|
+
]),
|
|
259
|
+
sequenceNumber: z.number().int().positive(),
|
|
260
|
+
handlingEpoch: z.number().int().nonnegative().optional(),
|
|
261
|
+
routeBinding: routeBinding.optional(),
|
|
262
|
+
sender: humanDshSender.optional(),
|
|
263
|
+
manualBinding: z.object({
|
|
264
|
+
sessionId: z.string().min(1).transform(SessionId),
|
|
265
|
+
taskId: agentTaskId
|
|
266
|
+
}).optional(),
|
|
267
|
+
retryOfRequestId: outboundRequestId.optional(),
|
|
268
|
+
preSendFailureReason: z.enum([
|
|
269
|
+
"account-not-found",
|
|
270
|
+
"platform-mismatch",
|
|
271
|
+
"account-paused",
|
|
272
|
+
"route-disabled",
|
|
273
|
+
"route-unmatched",
|
|
274
|
+
"route-changed",
|
|
275
|
+
"cancelled"
|
|
276
|
+
]).optional(),
|
|
277
|
+
attempt: z.object({
|
|
278
|
+
attemptId: outboundAttemptId,
|
|
279
|
+
startedAt: timestamp$1
|
|
280
|
+
}).optional(),
|
|
281
|
+
receipt: receipt.optional(),
|
|
282
|
+
externalMessageId: z.string().optional(),
|
|
283
|
+
replyToExternalMessageId: z.string().optional(),
|
|
284
|
+
createdAt: timestamp$1,
|
|
285
|
+
updatedAt: timestamp$1
|
|
286
|
+
});
|
|
287
|
+
/** Parser shared by the sqlite transfer journal and the external queue file. */
|
|
288
|
+
const uploadTarget = z.object({
|
|
289
|
+
serverOrigin: z.url(),
|
|
290
|
+
tenantId: z.string().min(1).transform((value) => brandString(value)),
|
|
291
|
+
shopAccountId: z.string().min(1),
|
|
292
|
+
traffic: z.enum([
|
|
293
|
+
"formal",
|
|
294
|
+
"test",
|
|
295
|
+
"simulation"
|
|
296
|
+
]),
|
|
297
|
+
testRunId: z.string().nullable()
|
|
298
|
+
});
|
|
299
|
+
/** Server V2 control-result fields shared by the journal and queue parsers. */
|
|
300
|
+
const imUploadConversationEventSchema = z.object({
|
|
301
|
+
eventId: z.string().min(1),
|
|
302
|
+
conversationKey: z.string().min(1),
|
|
303
|
+
accountId: z.string().min(1),
|
|
304
|
+
kind: z.enum([
|
|
305
|
+
"ai_to_human",
|
|
306
|
+
"hosting_disabled",
|
|
307
|
+
"hosting_resumed"
|
|
308
|
+
]),
|
|
309
|
+
occurredAt: timestamp$1,
|
|
310
|
+
controlRevision: z.string().regex(/^(0|[1-9][0-9]{0,18})$/u),
|
|
311
|
+
relatedMessageIds: z.array(z.string().min(1)).max(200),
|
|
312
|
+
sourceOperationId: z.string().min(1),
|
|
313
|
+
sourceKind: z.literal("dsh-control-result"),
|
|
314
|
+
payloadHash: z.string().regex(/^[0-9a-f]{64}$/u)
|
|
315
|
+
});
|
|
316
|
+
const imUploadObservationSchema = z.object({
|
|
317
|
+
id: z.string().min(1).transform((value) => brandString(value)),
|
|
318
|
+
target: uploadTarget,
|
|
319
|
+
scope: imDeliveryScopeSchema,
|
|
320
|
+
observedAt: timestamp$1,
|
|
321
|
+
identity: imUploadIdentitySchema.optional(),
|
|
322
|
+
message: z.discriminatedUnion("kind", [
|
|
323
|
+
z.object({
|
|
324
|
+
kind: z.literal("inbound"),
|
|
325
|
+
value: inboundMessage
|
|
326
|
+
}),
|
|
327
|
+
z.object({
|
|
328
|
+
kind: z.literal("outbound"),
|
|
329
|
+
value: outbound
|
|
330
|
+
}),
|
|
331
|
+
z.object({
|
|
332
|
+
kind: z.literal("conversation-event"),
|
|
333
|
+
value: imUploadConversationEventSchema
|
|
334
|
+
})
|
|
335
|
+
])
|
|
336
|
+
});
|
|
337
|
+
const imDeliveryAggregateSchema = z.object({
|
|
338
|
+
uploadIdentities: z.record(z.string(), imUploadIdentitySchema).optional(),
|
|
339
|
+
scope: imDeliveryScopeSchema,
|
|
340
|
+
scopeId,
|
|
341
|
+
uploadTargets: z.record(z.string(), uploadTarget).optional(),
|
|
342
|
+
uploadPending: z.array(imUploadObservationSchema).optional(),
|
|
343
|
+
uploadObservedAt: timestamp$1.optional(),
|
|
344
|
+
nextMessageSequenceNumber: z.number().int().positive(),
|
|
345
|
+
nextOutboundSequenceNumber: z.number().int().positive(),
|
|
346
|
+
cursor,
|
|
347
|
+
presentation: conversationPresentation.optional(),
|
|
348
|
+
directRecipient: z.object({
|
|
349
|
+
providerActorId: z.string().min(1),
|
|
350
|
+
userId: z.string().min(1).optional(),
|
|
351
|
+
openDingTalkId: z.string().min(1).optional()
|
|
352
|
+
}).optional(),
|
|
353
|
+
messages: z.record(z.string(), inboundMessage),
|
|
354
|
+
messageIdsByExternalId: z.record(z.string(), messageId),
|
|
355
|
+
operations: z.record(z.string(), z.object({
|
|
356
|
+
fingerprint: z.string(),
|
|
357
|
+
result: z.union([pageResult, importResult])
|
|
358
|
+
})),
|
|
359
|
+
outbounds: z.record(z.string(), outbound)
|
|
360
|
+
});
|
|
361
|
+
const providerCursorOwner = z.object({
|
|
362
|
+
platform: z.enum(["dingtalk", "wangwang"]),
|
|
363
|
+
accountId,
|
|
364
|
+
streamId: z.string().min(1)
|
|
365
|
+
});
|
|
366
|
+
const providerCursorView = z.object({
|
|
367
|
+
id: providerCursorId,
|
|
368
|
+
owner: providerCursorOwner,
|
|
369
|
+
cursor: z.string().nullable(),
|
|
370
|
+
updatedAt: timestamp$1
|
|
371
|
+
});
|
|
372
|
+
const providerCursorResult = z.object({
|
|
373
|
+
operationId,
|
|
374
|
+
status: z.enum([
|
|
375
|
+
"applied",
|
|
376
|
+
"unchanged",
|
|
377
|
+
"conflict",
|
|
378
|
+
"rejected"
|
|
379
|
+
]),
|
|
380
|
+
cursor: providerCursorView,
|
|
381
|
+
code: z.literal("IM_PROVIDER_PAGE_NOT_DURABLE").optional(),
|
|
382
|
+
message: z.string().optional()
|
|
383
|
+
});
|
|
384
|
+
/** Parser for one provider-owned feed cursor plus its CAS receipts. */
|
|
385
|
+
const imProviderCursorAggregateSchema = z.object({
|
|
386
|
+
id: providerCursorId,
|
|
387
|
+
owner: providerCursorOwner,
|
|
388
|
+
cursor: z.string().nullable(),
|
|
389
|
+
updatedAt: timestamp$1,
|
|
390
|
+
operations: z.record(z.string(), z.object({
|
|
391
|
+
fingerprint: z.string(),
|
|
392
|
+
result: providerCursorResult
|
|
393
|
+
}))
|
|
394
|
+
});
|
|
395
|
+
const triggerReason = z.enum([
|
|
396
|
+
"direct",
|
|
397
|
+
"mention",
|
|
398
|
+
"every-n",
|
|
399
|
+
"fixed-interval"
|
|
400
|
+
]);
|
|
401
|
+
/** Frozen analysis parse path, shared by JSON records and compatibility fixtures. */
|
|
402
|
+
const imAdmissionAnnotationSchema = z.discriminatedUnion("status", [z.object({
|
|
403
|
+
id: z.string().min(1).transform((value) => brandString(value)),
|
|
404
|
+
annotatorId: z.string().min(1).transform((value) => brandString(value)),
|
|
405
|
+
version: z.string().min(1),
|
|
406
|
+
status: z.literal("success"),
|
|
407
|
+
text: z.string().min(1)
|
|
408
|
+
}), z.object({
|
|
409
|
+
id: z.string().min(1).transform((value) => brandString(value)),
|
|
410
|
+
annotatorId: z.string().min(1).transform((value) => brandString(value)),
|
|
411
|
+
version: z.string().min(1),
|
|
412
|
+
status: z.enum(["timeout", "failed"])
|
|
413
|
+
})]);
|
|
414
|
+
/** Parser for the frozen batch retained until its exact Session append commits. */
|
|
415
|
+
const imPendingAdmissionSchema = z.object({
|
|
416
|
+
annotations: z.array(imAdmissionAnnotationSchema).optional(),
|
|
417
|
+
handlingEpoch: z.number().int().nonnegative().optional(),
|
|
418
|
+
admissionId,
|
|
419
|
+
messageIds: z.array(messageId).min(1),
|
|
420
|
+
triggerReasons: z.array(triggerReason).min(1),
|
|
421
|
+
createdAt: timestamp$1
|
|
422
|
+
});
|
|
423
|
+
/** Parser for one route-bound Agent task generation. */
|
|
424
|
+
const imExecutionAggregateSchema = z.object({
|
|
425
|
+
taskId: agentTaskId,
|
|
426
|
+
generation: z.number().int().positive(),
|
|
427
|
+
scope: imDeliveryScopeSchema,
|
|
428
|
+
scopeId,
|
|
429
|
+
sessionId: z.string().min(1).transform(SessionId),
|
|
430
|
+
routeId: z.string().min(1).transform((value) => brandString(value)),
|
|
431
|
+
routeRevision: z.string().min(1).transform((value) => brandString(value)),
|
|
432
|
+
accountRevision: z.string().min(1).transform((value) => brandString(value)),
|
|
433
|
+
workspaceId: z.string().min(1).transform(WorkspaceId),
|
|
434
|
+
agentPreset: z.string().min(1),
|
|
435
|
+
controller: z.string().min(1).optional(),
|
|
436
|
+
groupTrigger: z.object({
|
|
437
|
+
mention: z.boolean().optional(),
|
|
438
|
+
everyN: z.number().int().positive().optional(),
|
|
439
|
+
fixedIntervalSeconds: z.number().int().positive().optional()
|
|
440
|
+
}).optional(),
|
|
441
|
+
directRecipient: z.object({
|
|
442
|
+
providerActorId: z.string().min(1),
|
|
443
|
+
userId: z.string().min(1).optional(),
|
|
444
|
+
openDingTalkId: z.string().min(1).optional()
|
|
445
|
+
}).optional(),
|
|
446
|
+
createdAt: timestamp$1,
|
|
447
|
+
updatedAt: timestamp$1,
|
|
448
|
+
pendingAdmission: imPendingAdmissionSchema.optional()
|
|
449
|
+
});
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/delivery.ts
|
|
452
|
+
/** Durable receive, history, Session reconciliation, and outbox state. */
|
|
453
|
+
const settled = async (promise) => {
|
|
454
|
+
try {
|
|
455
|
+
await promise;
|
|
456
|
+
} catch {}
|
|
457
|
+
};
|
|
458
|
+
function timestamp() {
|
|
459
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Encode every scope component in an ordered JSON tuple before hashing.
|
|
463
|
+
* @param scope - complete real or simulation conversation identity.
|
|
464
|
+
* @returns stable scope identifier shared by history, Session sources, and outbox rows.
|
|
465
|
+
*/
|
|
466
|
+
function encodeImScopeId(scope) {
|
|
467
|
+
const tuple = scope.kind === "real" ? [
|
|
468
|
+
scope.kind,
|
|
469
|
+
scope.platform,
|
|
470
|
+
scope.accountId,
|
|
471
|
+
scope.conversationKind,
|
|
472
|
+
scope.conversationId
|
|
473
|
+
] : [
|
|
474
|
+
scope.kind,
|
|
475
|
+
scope.instanceId,
|
|
476
|
+
scope.platform,
|
|
477
|
+
scope.accountId,
|
|
478
|
+
scope.conversationKind,
|
|
479
|
+
scope.conversationId
|
|
480
|
+
];
|
|
481
|
+
return brandString(`im-scope:${createHash("sha256").update(JSON.stringify(tuple)).digest("base64url")}`);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Encode the platform account and provider feed name that jointly own a cursor.
|
|
485
|
+
* @param owner - provider cursor owner.
|
|
486
|
+
* @returns stable provider cursor identifier.
|
|
487
|
+
*/
|
|
488
|
+
function encodeImProviderCursorId(owner) {
|
|
489
|
+
return brandString(`im-provider-cursor:${createHash("sha256").update(JSON.stringify([
|
|
490
|
+
owner.platform,
|
|
491
|
+
owner.accountId,
|
|
492
|
+
owner.streamId
|
|
493
|
+
])).digest("base64url")}`);
|
|
494
|
+
}
|
|
495
|
+
function messageIdentity(scopeId, externalMessageId) {
|
|
496
|
+
return brandString(`im-message:${createHash("sha256").update(JSON.stringify([scopeId, externalMessageId])).digest("base64url")}`);
|
|
497
|
+
}
|
|
498
|
+
function existingInboundId(messages, byExternalId, input) {
|
|
499
|
+
const byId = byExternalId[input.externalMessageId];
|
|
500
|
+
if (byId !== void 0) return byId;
|
|
501
|
+
if (input.sender.kind !== "human-dsh") return void 0;
|
|
502
|
+
const requestId = input.sender.outboundRequestId;
|
|
503
|
+
return Object.values(messages).find((message) => message.sender.kind === "human-dsh" && message.sender.outboundRequestId === requestId)?.messageId;
|
|
504
|
+
}
|
|
505
|
+
function attemptIdentity() {
|
|
506
|
+
return brandString(`im-attempt:${randomUUID()}`);
|
|
507
|
+
}
|
|
508
|
+
/** @param outbound - intent that now owns this platform message id. @returns its durable sender, or absence for an intent that has no echo attribution. */
|
|
509
|
+
function echoSender(outbound) {
|
|
510
|
+
if (outbound.intent === "ai") return {
|
|
511
|
+
kind: "ai",
|
|
512
|
+
outboundRequestId: outbound.requestId
|
|
513
|
+
};
|
|
514
|
+
if (outbound.intent !== "human-manual") return void 0;
|
|
515
|
+
return outbound.sender?.kind === "human-dsh" ? outbound.sender : {
|
|
516
|
+
kind: "human-dsh",
|
|
517
|
+
outboundRequestId: outbound.requestId
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Reattribute live history that already carries this exact platform id.
|
|
522
|
+
* Rows with any other id, imported history, and buyer evidence stay as stored.
|
|
523
|
+
* @param messages - current history map.
|
|
524
|
+
* @param outbound - settled outbox row.
|
|
525
|
+
* @param externalMessageId - platform message id from the receipt.
|
|
526
|
+
* @returns the next map and the ids whose sender changed.
|
|
527
|
+
*/
|
|
528
|
+
function claimExactEcho(messages, outbound, externalMessageId) {
|
|
529
|
+
const sender = echoSender(outbound);
|
|
530
|
+
if (sender === void 0) return {
|
|
531
|
+
messages,
|
|
532
|
+
revised: []
|
|
533
|
+
};
|
|
534
|
+
let next;
|
|
535
|
+
const revised = [];
|
|
536
|
+
for (const [id, message] of Object.entries(messages)) {
|
|
537
|
+
if (message.origin !== "live" || message.externalMessageId !== externalMessageId) continue;
|
|
538
|
+
if (message.sender.kind !== "human-native" && message.sender.kind !== "unknown") continue;
|
|
539
|
+
if (JSON.stringify(message.sender) === JSON.stringify(sender)) continue;
|
|
540
|
+
const updated = {
|
|
541
|
+
...message,
|
|
542
|
+
sender
|
|
543
|
+
};
|
|
544
|
+
next ??= { ...messages };
|
|
545
|
+
next[id] = updated;
|
|
546
|
+
revised.push(updated.messageId);
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
messages: next ?? messages,
|
|
550
|
+
revised
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function assertLimit(limit) {
|
|
554
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) throw new ImRuntimeError("IM_DELIVERY_SCOPE_INVALID", "IM delivery query limit must be an integer from 1 through 1000");
|
|
555
|
+
}
|
|
556
|
+
function sameScope(left, right) {
|
|
557
|
+
return encodeImScopeId(left) === encodeImScopeId(right);
|
|
558
|
+
}
|
|
559
|
+
function initialAggregate(scope) {
|
|
560
|
+
const scopeId = encodeImScopeId(scope);
|
|
561
|
+
return {
|
|
562
|
+
scope,
|
|
563
|
+
scopeId,
|
|
564
|
+
nextMessageSequenceNumber: 1,
|
|
565
|
+
nextOutboundSequenceNumber: 1,
|
|
566
|
+
cursor: {
|
|
567
|
+
scopeId,
|
|
568
|
+
platformCursor: null,
|
|
569
|
+
lastReceivedSequenceNumber: 0,
|
|
570
|
+
lastSubmittedSequenceNumber: 0,
|
|
571
|
+
pendingCount: 0,
|
|
572
|
+
updatedAt: timestamp()
|
|
573
|
+
},
|
|
574
|
+
messages: {},
|
|
575
|
+
messageIdsByExternalId: {},
|
|
576
|
+
operations: {},
|
|
577
|
+
outbounds: {}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function sortedMessages(aggregate) {
|
|
581
|
+
return Object.values(aggregate.messages).sort((left, right) => left.sequenceNumber - right.sequenceNumber);
|
|
582
|
+
}
|
|
583
|
+
function sortedOutbounds(aggregate) {
|
|
584
|
+
return Object.values(aggregate.outbounds).sort((left, right) => left.sequenceNumber - right.sequenceNumber);
|
|
585
|
+
}
|
|
586
|
+
/** @param messages - one document's messages in sequence order. @param request - validated page bounds and origin filter. @returns the ascending page that ends before the bound. */
|
|
587
|
+
function historyPage(messages, request) {
|
|
588
|
+
const acceptedOrigins = request.origins === void 0 ? void 0 : new Set(request.origins);
|
|
589
|
+
const before = request.beforeSequenceNumber ?? Number.POSITIVE_INFINITY;
|
|
590
|
+
const candidates = messages.filter((message) => message.sequenceNumber < before && (acceptedOrigins?.has(message.origin) ?? true));
|
|
591
|
+
const offset = Math.max(0, candidates.length - request.limit);
|
|
592
|
+
const items = candidates.slice(offset);
|
|
593
|
+
return {
|
|
594
|
+
items,
|
|
595
|
+
hasMore: offset > 0,
|
|
596
|
+
...offset > 0 && items[0] !== void 0 ? { nextBeforeSequenceNumber: items[0].sequenceNumber } : {}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/** @param outbounds - one document's outbounds in sequence order. @param request - validated page bounds. @returns the ascending page that ends before the bound. */
|
|
600
|
+
function outboundPage(outbounds, request) {
|
|
601
|
+
const before = request.beforeSequenceNumber ?? Number.POSITIVE_INFINITY;
|
|
602
|
+
const candidates = outbounds.filter((outbound) => outbound.sequenceNumber < before);
|
|
603
|
+
const offset = Math.max(0, candidates.length - request.limit);
|
|
604
|
+
const items = candidates.slice(offset);
|
|
605
|
+
return {
|
|
606
|
+
items,
|
|
607
|
+
hasMore: offset > 0,
|
|
608
|
+
...offset > 0 && items[0] !== void 0 ? { nextBeforeSequenceNumber: items[0].sequenceNumber } : {}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* @param aggregate - stored or initial conversation document.
|
|
613
|
+
* @returns records over the document with the same paging as `ImDeliveryStore.queryHistory` and `queryOutbound`.
|
|
614
|
+
*/
|
|
615
|
+
function conversationRecords(aggregate) {
|
|
616
|
+
let messages;
|
|
617
|
+
let outbounds;
|
|
618
|
+
return {
|
|
619
|
+
scope: aggregate.scope,
|
|
620
|
+
cursor: aggregate.cursor,
|
|
621
|
+
queryHistory: (request) => {
|
|
622
|
+
assertLimit(request.limit);
|
|
623
|
+
messages ??= sortedMessages(aggregate);
|
|
624
|
+
return historyPage(messages, request);
|
|
625
|
+
},
|
|
626
|
+
queryOutbound: (request) => {
|
|
627
|
+
assertLimit(request.limit);
|
|
628
|
+
outbounds ??= sortedOutbounds(aggregate);
|
|
629
|
+
return outboundPage(outbounds, request);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function suppressedThrough(aggregate) {
|
|
634
|
+
return aggregate.cursor.lastSuppressedSequenceNumber ?? 0;
|
|
635
|
+
}
|
|
636
|
+
function pendingCount(aggregate) {
|
|
637
|
+
const suppressed = suppressedThrough(aggregate);
|
|
638
|
+
return Object.values(aggregate.messages).filter((message) => message.origin === "live" && message.stage === "received" && message.sequenceNumber > suppressed).length;
|
|
639
|
+
}
|
|
640
|
+
function submittedWatermark(aggregate) {
|
|
641
|
+
let watermark = 0;
|
|
642
|
+
for (const message of sortedMessages(aggregate)) {
|
|
643
|
+
if (message.origin !== "live") continue;
|
|
644
|
+
if (message.stage !== "submitted") break;
|
|
645
|
+
watermark = message.sequenceNumber;
|
|
646
|
+
}
|
|
647
|
+
return watermark;
|
|
648
|
+
}
|
|
649
|
+
function pageFingerprint(request, includePeer = true) {
|
|
650
|
+
return JSON.stringify([
|
|
651
|
+
"page",
|
|
652
|
+
request.scope,
|
|
653
|
+
request.observedCursor,
|
|
654
|
+
request.nextCursor,
|
|
655
|
+
request.presentation ?? null,
|
|
656
|
+
request.messages,
|
|
657
|
+
...!includePeer || request.directRecipient === void 0 ? [] : [request.directRecipient]
|
|
658
|
+
]);
|
|
659
|
+
}
|
|
660
|
+
function importFingerprint(request, fileName, messages) {
|
|
661
|
+
return JSON.stringify([
|
|
662
|
+
"jsonl-import",
|
|
663
|
+
request.scope,
|
|
664
|
+
fileName,
|
|
665
|
+
messages
|
|
666
|
+
]);
|
|
667
|
+
}
|
|
668
|
+
function assertOperation(existing, fingerprint, operationId) {
|
|
669
|
+
if (existing !== void 0 && existing.fingerprint !== fingerprint) throw new ImRuntimeError("IM_DELIVERY_OPERATION_REUSED", `IM delivery operation '${operationId}' was already used for a different request`);
|
|
670
|
+
}
|
|
671
|
+
function assertOutbound(existing, request) {
|
|
672
|
+
if (existing === void 0) return;
|
|
673
|
+
if (JSON.stringify([
|
|
674
|
+
existing.intent,
|
|
675
|
+
existing.content,
|
|
676
|
+
existing.sender ?? null,
|
|
677
|
+
existing.manualBinding ?? null,
|
|
678
|
+
existing.retryOfRequestId ?? null,
|
|
679
|
+
existing.replyToExternalMessageId ?? null
|
|
680
|
+
]) !== JSON.stringify([
|
|
681
|
+
request.intent,
|
|
682
|
+
request.content,
|
|
683
|
+
request.sender ?? null,
|
|
684
|
+
request.manualBinding ?? null,
|
|
685
|
+
request.retryOfRequestId ?? null,
|
|
686
|
+
request.replyToExternalMessageId ?? null
|
|
687
|
+
])) throw new ImRuntimeError("IM_OUTBOUND_REQUEST_REUSED", `IM outbound request '${request.requestId}' was already used for a different intent`);
|
|
688
|
+
}
|
|
689
|
+
function preparationUnknown(outbound, changedAt) {
|
|
690
|
+
return {
|
|
691
|
+
...outbound,
|
|
692
|
+
status: "result-unknown",
|
|
693
|
+
receipt: {
|
|
694
|
+
providerStatus: "media-preparation-unknown",
|
|
695
|
+
errorCode: "IM_MEDIA_PREPARATION_UNKNOWN",
|
|
696
|
+
observedAt: changedAt
|
|
697
|
+
},
|
|
698
|
+
updatedAt: changedAt
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
/** One-record mutations for complete conversation scopes. */
|
|
702
|
+
var ImDeliveryStore = class {
|
|
703
|
+
records;
|
|
704
|
+
host;
|
|
705
|
+
tails = /* @__PURE__ */ new Map();
|
|
706
|
+
cursorTails = /* @__PURE__ */ new Map();
|
|
707
|
+
cursors;
|
|
708
|
+
/** @param records - sqlite delivery records. @param host - account policy and post-commit publisher. */
|
|
709
|
+
constructor(records, host) {
|
|
710
|
+
this.records = records;
|
|
711
|
+
this.host = host;
|
|
712
|
+
this.cursors = records.projectScopes((value) => value.cursor);
|
|
713
|
+
}
|
|
714
|
+
/** Recover interrupted sends and unfinished file preparations before publishing the runtime, independently of provider availability. */
|
|
715
|
+
async recoverInterruptedAttempts() {
|
|
716
|
+
const unfinishedFiles = new Set(this.records.unfinishedFilePreparationIds());
|
|
717
|
+
const scopeIds = new Set(this.records.dispatchingScopeIds());
|
|
718
|
+
for (const requestId of unfinishedFiles) {
|
|
719
|
+
const owner = this.records.outboundOwner(requestId);
|
|
720
|
+
if (owner !== void 0) scopeIds.add(encodeImScopeId(owner.scope));
|
|
721
|
+
}
|
|
722
|
+
for (const scopeId of scopeIds) await this.enqueue(scopeId, async () => {
|
|
723
|
+
const current = this.require(scopeId);
|
|
724
|
+
const changedAt = timestamp();
|
|
725
|
+
let changed = false;
|
|
726
|
+
const outbounds = Object.fromEntries(Object.entries(current.outbounds).map(([id, outbound]) => {
|
|
727
|
+
if (unfinishedFiles.has(outbound.requestId) && outbound.status !== "sent") {
|
|
728
|
+
if (outbound.status === "result-unknown" && outbound.receipt?.errorCode === "IM_MEDIA_PREPARATION_UNKNOWN") return [id, outbound];
|
|
729
|
+
changed = true;
|
|
730
|
+
return [id, preparationUnknown(outbound, changedAt)];
|
|
731
|
+
}
|
|
732
|
+
if (outbound.status !== "dispatching") return [id, outbound];
|
|
733
|
+
changed = true;
|
|
734
|
+
return [id, {
|
|
735
|
+
...outbound,
|
|
736
|
+
status: "result-unknown",
|
|
737
|
+
receipt: outbound.receipt ?? {
|
|
738
|
+
providerStatus: "interrupted",
|
|
739
|
+
observedAt: changedAt
|
|
740
|
+
},
|
|
741
|
+
updatedAt: changedAt
|
|
742
|
+
}];
|
|
743
|
+
}));
|
|
744
|
+
if (changed) await this.persistMutation(current, {
|
|
745
|
+
...current,
|
|
746
|
+
outbounds
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
/** @param request - Validated page and scope. @param receiptTime - Instance script time; absent uses the ingestion clock. @returns The durable page receipt. */
|
|
751
|
+
async ingestInboundPage(request, receiptTime) {
|
|
752
|
+
if (request.scope.kind === "real") this.assertRealScope(request.scope);
|
|
753
|
+
if (request.scope.conversationKind === "direct" && request.presentation?.memberCount !== void 0) throw new ImRuntimeError("IM_DELIVERY_SCOPE_INVALID", "direct-conversation presentation cannot carry a member count");
|
|
754
|
+
const scopeId = encodeImScopeId(request.scope);
|
|
755
|
+
const fingerprint = pageFingerprint(request);
|
|
756
|
+
return this.mutate(request.scope, (current) => {
|
|
757
|
+
const existing = current.operations[request.operationId];
|
|
758
|
+
assertOperation(existing, existing !== void 0 && existing.fingerprint === pageFingerprint(request, false) ? existing.fingerprint : fingerprint, request.operationId);
|
|
759
|
+
if (existing !== void 0) {
|
|
760
|
+
if (existing.result.kind !== "page") throw new ImRuntimeError("IM_DELIVERY_OPERATION_REUSED", `IM delivery operation '${request.operationId}' is not a page operation`);
|
|
761
|
+
return {
|
|
762
|
+
aggregate: current,
|
|
763
|
+
value: existing.result,
|
|
764
|
+
changed: false
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
if (current.cursor.platformCursor !== request.observedCursor) {
|
|
768
|
+
const result = {
|
|
769
|
+
kind: "page",
|
|
770
|
+
operationId: request.operationId,
|
|
771
|
+
status: "conflict",
|
|
772
|
+
acceptedCount: 0,
|
|
773
|
+
evidenceMergedCount: 0,
|
|
774
|
+
duplicateCount: 0,
|
|
775
|
+
messages: [],
|
|
776
|
+
cursor: current.cursor
|
|
777
|
+
};
|
|
778
|
+
return {
|
|
779
|
+
aggregate: {
|
|
780
|
+
...current,
|
|
781
|
+
operations: {
|
|
782
|
+
...current.operations,
|
|
783
|
+
[request.operationId]: {
|
|
784
|
+
fingerprint,
|
|
785
|
+
result
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
value: result,
|
|
790
|
+
changed: true
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
let nextSequence = current.nextMessageSequenceNumber;
|
|
794
|
+
const messages = { ...current.messages };
|
|
795
|
+
const byExternalId = { ...current.messageIdsByExternalId };
|
|
796
|
+
const accepted = [];
|
|
797
|
+
const evidenceMerged = [];
|
|
798
|
+
let duplicateCount = 0;
|
|
799
|
+
const ordered = [...request.messages].sort((left, right) => left.occurredAt.localeCompare(right.occurredAt) || (request.scope.kind === "simulation" ? 0 : left.externalMessageId.localeCompare(right.externalMessageId)));
|
|
800
|
+
const receivedAt = receiptTime ?? timestamp();
|
|
801
|
+
for (const input of ordered) {
|
|
802
|
+
const existingId = existingInboundId(messages, byExternalId, input);
|
|
803
|
+
if (existingId !== void 0) {
|
|
804
|
+
duplicateCount++;
|
|
805
|
+
const existingMessage = messages[existingId];
|
|
806
|
+
if (existingMessage !== void 0 && existingMessage.mentionedConfiguredAccount !== true && input.mentionedConfiguredAccount === true) {
|
|
807
|
+
const enriched = {
|
|
808
|
+
...existingMessage,
|
|
809
|
+
mentionedConfiguredAccount: true
|
|
810
|
+
};
|
|
811
|
+
messages[existingId] = enriched;
|
|
812
|
+
evidenceMerged.push(enriched);
|
|
813
|
+
}
|
|
814
|
+
if (existingMessage !== void 0 && input.sender.kind !== "unknown" && JSON.stringify(existingMessage.sender) !== JSON.stringify(input.sender)) {
|
|
815
|
+
const revised = {
|
|
816
|
+
...messages[existingId],
|
|
817
|
+
sender: input.sender
|
|
818
|
+
};
|
|
819
|
+
messages[existingId] = revised;
|
|
820
|
+
evidenceMerged.push(revised);
|
|
821
|
+
}
|
|
822
|
+
if (existingMessage !== void 0 && existingMessage.externalMessageId !== input.externalMessageId) {
|
|
823
|
+
delete byExternalId[existingMessage.externalMessageId];
|
|
824
|
+
messages[existingId] = {
|
|
825
|
+
...messages[existingId],
|
|
826
|
+
externalMessageId: input.externalMessageId
|
|
827
|
+
};
|
|
828
|
+
byExternalId[input.externalMessageId] = existingId;
|
|
829
|
+
}
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const id = messageIdentity(scopeId, input.externalMessageId);
|
|
833
|
+
const message = {
|
|
834
|
+
...input,
|
|
835
|
+
messageId: id,
|
|
836
|
+
scopeId,
|
|
837
|
+
origin: "live",
|
|
838
|
+
stage: "received",
|
|
839
|
+
sequenceNumber: nextSequence++,
|
|
840
|
+
receivedAt: request.scope.kind === "simulation" ? input.occurredAt : receivedAt
|
|
841
|
+
};
|
|
842
|
+
messages[id] = message;
|
|
843
|
+
byExternalId[input.externalMessageId] = id;
|
|
844
|
+
accepted.push(message);
|
|
845
|
+
}
|
|
846
|
+
const mergedMessages = [...new Set(evidenceMerged.map((message) => message.messageId))].map((id) => messages[id]);
|
|
847
|
+
const nextMessages = {
|
|
848
|
+
...current,
|
|
849
|
+
nextMessageSequenceNumber: nextSequence,
|
|
850
|
+
messages
|
|
851
|
+
};
|
|
852
|
+
const cursor = {
|
|
853
|
+
...current.cursor,
|
|
854
|
+
platformCursor: request.nextCursor,
|
|
855
|
+
lastReceivedSequenceNumber: accepted.at(-1)?.sequenceNumber ?? current.cursor.lastReceivedSequenceNumber,
|
|
856
|
+
pendingCount: pendingCount(nextMessages),
|
|
857
|
+
lastSyncedAt: receivedAt,
|
|
858
|
+
updatedAt: receivedAt
|
|
859
|
+
};
|
|
860
|
+
const result = {
|
|
861
|
+
kind: "page",
|
|
862
|
+
operationId: request.operationId,
|
|
863
|
+
status: "applied",
|
|
864
|
+
acceptedCount: accepted.length,
|
|
865
|
+
evidenceMergedCount: mergedMessages.length,
|
|
866
|
+
duplicateCount,
|
|
867
|
+
messages: [...accepted, ...mergedMessages],
|
|
868
|
+
cursor
|
|
869
|
+
};
|
|
870
|
+
return {
|
|
871
|
+
aggregate: {
|
|
872
|
+
...current,
|
|
873
|
+
nextMessageSequenceNumber: nextSequence,
|
|
874
|
+
cursor,
|
|
875
|
+
messages,
|
|
876
|
+
messageIdsByExternalId: byExternalId,
|
|
877
|
+
...request.presentation === void 0 ? {} : { presentation: {
|
|
878
|
+
...current.presentation,
|
|
879
|
+
...request.presentation
|
|
880
|
+
} },
|
|
881
|
+
...request.directRecipient === void 0 ? {} : { directRecipient: request.directRecipient },
|
|
882
|
+
operations: {
|
|
883
|
+
...current.operations,
|
|
884
|
+
[request.operationId]: {
|
|
885
|
+
fingerprint,
|
|
886
|
+
result
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
},
|
|
890
|
+
value: result,
|
|
891
|
+
changed: true
|
|
892
|
+
};
|
|
893
|
+
}).then((result) => {
|
|
894
|
+
if (result.changed) this.host.publish({
|
|
895
|
+
scope: request.scope,
|
|
896
|
+
scopeId,
|
|
897
|
+
kind: "inbound",
|
|
898
|
+
messageIds: result.value.messages.map((message) => message.messageId)
|
|
899
|
+
});
|
|
900
|
+
return result.value;
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
queryInboundOperation(scope, operationId) {
|
|
904
|
+
const result = this.lookup(scope)?.operations[operationId]?.result;
|
|
905
|
+
return result === void 0 ? { state: "not-found" } : {
|
|
906
|
+
state: "known",
|
|
907
|
+
result
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
getConversationCursor(scope) {
|
|
911
|
+
return this.cursors.get(encodeImScopeId(scope)) ?? initialAggregate(scope).cursor;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* @param project - pure function of one conversation's records.
|
|
915
|
+
* @returns values derived at each document parse or write; a conversation without a document projects its initial records on every read.
|
|
916
|
+
*/
|
|
917
|
+
projectConversations(project) {
|
|
918
|
+
const projection = this.records.projectScopes((value) => project(conversationRecords(value)));
|
|
919
|
+
return {
|
|
920
|
+
get: (scope) => projection.get(encodeImScopeId(scope)) ?? project(conversationRecords(initialAggregate(scope))),
|
|
921
|
+
dispose: () => {
|
|
922
|
+
projection.dispose();
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/** @param scope - durable conversation. @returns last admitted presentation, including the assigned service account when known. */
|
|
927
|
+
getConversationPresentation(scope) {
|
|
928
|
+
return this.lookup(scope)?.presentation;
|
|
929
|
+
}
|
|
930
|
+
getProviderCursor(owner) {
|
|
931
|
+
const id = encodeImProviderCursorId(owner);
|
|
932
|
+
const aggregate = this.records.getCursor(id);
|
|
933
|
+
if (aggregate !== void 0) return {
|
|
934
|
+
id,
|
|
935
|
+
owner: aggregate.owner,
|
|
936
|
+
cursor: aggregate.cursor,
|
|
937
|
+
updatedAt: aggregate.updatedAt
|
|
938
|
+
};
|
|
939
|
+
return {
|
|
940
|
+
id,
|
|
941
|
+
owner,
|
|
942
|
+
cursor: null,
|
|
943
|
+
updatedAt: timestamp()
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
async commitProviderCursor(request) {
|
|
947
|
+
const account = this.host.inspectAccount(request.owner.accountId);
|
|
948
|
+
if (account === void 0) throw new ImRuntimeError("IM_ACCOUNT_NOT_FOUND", `IM account '${request.owner.accountId}' is unknown`);
|
|
949
|
+
if (account.platform !== request.owner.platform || request.owner.streamId.trim() === "") throw new ImRuntimeError("IM_DELIVERY_SCOPE_INVALID", "provider cursor owner must use the configured account platform and a non-empty streamId");
|
|
950
|
+
const id = encodeImProviderCursorId(request.owner);
|
|
951
|
+
const fingerprint = JSON.stringify([
|
|
952
|
+
"provider-cursor",
|
|
953
|
+
request.owner,
|
|
954
|
+
request.observedCursor,
|
|
955
|
+
request.nextCursor,
|
|
956
|
+
request.pages.map((page) => [page.scope, page.operationId])
|
|
957
|
+
]);
|
|
958
|
+
return this.enqueueCursor(id, async () => {
|
|
959
|
+
const current = this.records.getCursor(id) ?? {
|
|
960
|
+
id,
|
|
961
|
+
owner: request.owner,
|
|
962
|
+
cursor: null,
|
|
963
|
+
updatedAt: timestamp(),
|
|
964
|
+
operations: {}
|
|
965
|
+
};
|
|
966
|
+
const existing = current.operations[request.operationId];
|
|
967
|
+
assertOperation(existing, fingerprint, request.operationId);
|
|
968
|
+
if (existing !== void 0) return existing.result;
|
|
969
|
+
let status;
|
|
970
|
+
let code;
|
|
971
|
+
let message;
|
|
972
|
+
if (current.cursor !== request.observedCursor) status = "conflict";
|
|
973
|
+
else {
|
|
974
|
+
const missing = request.pages.find((page) => {
|
|
975
|
+
const receipt = this.lookup(page.scope)?.operations[page.operationId]?.result;
|
|
976
|
+
return receipt?.kind !== "page" || receipt.status !== "applied";
|
|
977
|
+
});
|
|
978
|
+
if (missing !== void 0) {
|
|
979
|
+
status = "rejected";
|
|
980
|
+
code = "IM_PROVIDER_PAGE_NOT_DURABLE";
|
|
981
|
+
message = `conversation page '${missing.operationId}' is not durably applied`;
|
|
982
|
+
} else status = current.cursor === request.nextCursor ? "unchanged" : "applied";
|
|
983
|
+
}
|
|
984
|
+
const updatedAt = timestamp();
|
|
985
|
+
const cursor = {
|
|
986
|
+
id,
|
|
987
|
+
owner: current.owner,
|
|
988
|
+
cursor: status === "applied" ? request.nextCursor : current.cursor,
|
|
989
|
+
updatedAt: status === "applied" ? updatedAt : current.updatedAt
|
|
990
|
+
};
|
|
991
|
+
const result = {
|
|
992
|
+
operationId: request.operationId,
|
|
993
|
+
status,
|
|
994
|
+
cursor,
|
|
995
|
+
...code === void 0 ? {} : { code },
|
|
996
|
+
...message === void 0 ? {} : { message }
|
|
997
|
+
};
|
|
998
|
+
await this.records.putCursor(id, {
|
|
999
|
+
id,
|
|
1000
|
+
owner: current.owner,
|
|
1001
|
+
cursor: cursor.cursor,
|
|
1002
|
+
updatedAt: cursor.updatedAt,
|
|
1003
|
+
operations: {
|
|
1004
|
+
...current.operations,
|
|
1005
|
+
[request.operationId]: {
|
|
1006
|
+
fingerprint,
|
|
1007
|
+
result
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
if (status === "applied") this.host.onProviderCursorApplied?.(request.owner);
|
|
1012
|
+
return result;
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
queryProviderCursorOperation(owner, operationId) {
|
|
1016
|
+
const result = this.records.getCursor(encodeImProviderCursorId(owner))?.operations[operationId]?.result;
|
|
1017
|
+
return result === void 0 ? { state: "not-found" } : {
|
|
1018
|
+
state: "known",
|
|
1019
|
+
result
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
queryHistory(request) {
|
|
1023
|
+
assertLimit(request.limit);
|
|
1024
|
+
const aggregate = this.lookup(request.scope);
|
|
1025
|
+
if (aggregate === void 0) return {
|
|
1026
|
+
items: [],
|
|
1027
|
+
hasMore: false
|
|
1028
|
+
};
|
|
1029
|
+
return historyPage(sortedMessages(aggregate), request);
|
|
1030
|
+
}
|
|
1031
|
+
pendingInbound(request) {
|
|
1032
|
+
assertLimit(request.limit);
|
|
1033
|
+
if (!this.records.hasPendingInbound(encodeImScopeId(request.scope))) return [];
|
|
1034
|
+
const aggregate = this.lookup(request.scope);
|
|
1035
|
+
if (aggregate === void 0) return [];
|
|
1036
|
+
const suppressed = suppressedThrough(aggregate);
|
|
1037
|
+
return sortedMessages(aggregate).filter((message) => message.origin === "live" && message.stage === "received" && message.sequenceNumber > suppressed).slice(0, request.limit);
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Consume the current received live batch without an Agent turn.
|
|
1041
|
+
* A later allow only admits newer sequence numbers.
|
|
1042
|
+
* @param scope - conversation whose policy just refused admission.
|
|
1043
|
+
*/
|
|
1044
|
+
suppressUnadmitted(scope) {
|
|
1045
|
+
const scopeId = encodeImScopeId(scope);
|
|
1046
|
+
return this.enqueue(scopeId, async () => {
|
|
1047
|
+
if (!this.records.hasPendingInbound(scopeId)) return;
|
|
1048
|
+
await this.applyMutation(scope, (current) => {
|
|
1049
|
+
let through = suppressedThrough(current);
|
|
1050
|
+
for (const message of Object.values(current.messages)) if (message.origin === "live" && message.stage === "received" && message.sequenceNumber > through) through = message.sequenceNumber;
|
|
1051
|
+
if (through === suppressedThrough(current)) return {
|
|
1052
|
+
aggregate: current,
|
|
1053
|
+
value: void 0,
|
|
1054
|
+
changed: false
|
|
1055
|
+
};
|
|
1056
|
+
const cursor = {
|
|
1057
|
+
...current.cursor,
|
|
1058
|
+
lastSuppressedSequenceNumber: through,
|
|
1059
|
+
pendingCount: pendingCount({
|
|
1060
|
+
...current,
|
|
1061
|
+
cursor: {
|
|
1062
|
+
...current.cursor,
|
|
1063
|
+
lastSuppressedSequenceNumber: through
|
|
1064
|
+
}
|
|
1065
|
+
}),
|
|
1066
|
+
updatedAt: timestamp()
|
|
1067
|
+
};
|
|
1068
|
+
return {
|
|
1069
|
+
aggregate: {
|
|
1070
|
+
...current,
|
|
1071
|
+
cursor
|
|
1072
|
+
},
|
|
1073
|
+
value: void 0,
|
|
1074
|
+
changed: true
|
|
1075
|
+
};
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
messageSource(scope, messageId) {
|
|
1080
|
+
const message = this.lookup(scope)?.messages[messageId];
|
|
1081
|
+
if (message === void 0) throw new ImRuntimeError("IM_MESSAGE_NOT_FOUND", `IM message '${messageId}' is unknown for scope '${encodeImScopeId(scope)}'`);
|
|
1082
|
+
return {
|
|
1083
|
+
kind: "im",
|
|
1084
|
+
scopeId: message.scopeId,
|
|
1085
|
+
messageId: message.messageId,
|
|
1086
|
+
sequenceNumber: message.sequenceNumber
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
/** @param source - durable Session message source. @returns matched scope, or absence for stale or foreign evidence. */
|
|
1090
|
+
matchMessageSource(source) {
|
|
1091
|
+
const aggregate = this.records.getScope(source.scopeId);
|
|
1092
|
+
const message = aggregate?.messages[source.messageId];
|
|
1093
|
+
if (aggregate === void 0 || message === void 0 || message.origin !== "live" || message.sequenceNumber !== source.sequenceNumber) return void 0;
|
|
1094
|
+
return {
|
|
1095
|
+
scope: aggregate.scope,
|
|
1096
|
+
messageId: message.messageId
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
/** @param scope - complete scope. @param messageId - inbound identity. @returns stored row. */
|
|
1100
|
+
getInbound(scope, messageId) {
|
|
1101
|
+
const message = this.lookup(scope)?.messages[messageId];
|
|
1102
|
+
if (message === void 0) throw new ImRuntimeError("IM_MESSAGE_NOT_FOUND", `IM message '${messageId}' is unknown for scope '${encodeImScopeId(scope)}'`);
|
|
1103
|
+
return message;
|
|
1104
|
+
}
|
|
1105
|
+
markSubmitted(request) {
|
|
1106
|
+
const scopeId = encodeImScopeId(request.scope);
|
|
1107
|
+
return this.mutate(request.scope, (current) => {
|
|
1108
|
+
const submittedAt = request.submittedAt ?? timestamp();
|
|
1109
|
+
const messages = { ...current.messages };
|
|
1110
|
+
const changed = [];
|
|
1111
|
+
for (const id of new Set(request.messageIds)) {
|
|
1112
|
+
const message = messages[id];
|
|
1113
|
+
if (message === void 0 || message.origin !== "live") throw new ImRuntimeError("IM_MESSAGE_NOT_FOUND", `live IM message '${id}' is unknown for scope '${scopeId}'`);
|
|
1114
|
+
if (message.submission !== void 0 && message.submission.sessionId !== request.sessionId) throw new ImRuntimeError("IM_MESSAGE_ALREADY_SUBMITTED", `IM message '${id}' is already submitted to Session '${message.submission.sessionId}'`);
|
|
1115
|
+
if (message.stage === "submitted") continue;
|
|
1116
|
+
const submitted = {
|
|
1117
|
+
...message,
|
|
1118
|
+
stage: "submitted",
|
|
1119
|
+
submission: {
|
|
1120
|
+
sessionId: request.sessionId,
|
|
1121
|
+
submittedAt
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
messages[id] = submitted;
|
|
1125
|
+
changed.push(submitted);
|
|
1126
|
+
}
|
|
1127
|
+
if (changed.length === 0) return {
|
|
1128
|
+
aggregate: current,
|
|
1129
|
+
value: {
|
|
1130
|
+
messages: [],
|
|
1131
|
+
cursor: current.cursor
|
|
1132
|
+
},
|
|
1133
|
+
changed: false
|
|
1134
|
+
};
|
|
1135
|
+
const next = {
|
|
1136
|
+
...current,
|
|
1137
|
+
messages
|
|
1138
|
+
};
|
|
1139
|
+
const cursor = {
|
|
1140
|
+
...current.cursor,
|
|
1141
|
+
lastSubmittedSequenceNumber: submittedWatermark(next),
|
|
1142
|
+
pendingCount: pendingCount(next),
|
|
1143
|
+
updatedAt: submittedAt
|
|
1144
|
+
};
|
|
1145
|
+
return {
|
|
1146
|
+
aggregate: {
|
|
1147
|
+
...next,
|
|
1148
|
+
cursor
|
|
1149
|
+
},
|
|
1150
|
+
value: {
|
|
1151
|
+
messages: changed,
|
|
1152
|
+
cursor
|
|
1153
|
+
},
|
|
1154
|
+
changed: true
|
|
1155
|
+
};
|
|
1156
|
+
}).then((result) => {
|
|
1157
|
+
if (result.changed) this.host.publish({
|
|
1158
|
+
scope: request.scope,
|
|
1159
|
+
scopeId,
|
|
1160
|
+
kind: "submitted",
|
|
1161
|
+
messageIds: result.value.messages.map((message) => message.messageId)
|
|
1162
|
+
});
|
|
1163
|
+
return result.value;
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
async importJsonlHistory(request) {
|
|
1167
|
+
const fileName = basename(request.fileName.trim().replaceAll("\\", "/"));
|
|
1168
|
+
if (fileName === "" || fileName === "." || fileName === "..") throw new ImRuntimeError("IM_JSONL_INVALID", "IM history import fileName must name a file");
|
|
1169
|
+
const parsed = [];
|
|
1170
|
+
for (const [index, line] of request.jsonl.split(/\r?\n/u).entries()) {
|
|
1171
|
+
if (line.trim() === "") continue;
|
|
1172
|
+
let value;
|
|
1173
|
+
try {
|
|
1174
|
+
value = JSON.parse(line);
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
throw new ImRuntimeError("IM_JSONL_INVALID", `IM JSONL line ${index + 1} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
1177
|
+
}
|
|
1178
|
+
const admitted = imInboundMessageInputSchema.safeParse(value);
|
|
1179
|
+
if (!admitted.success) throw new ImRuntimeError("IM_JSONL_INVALID", `IM JSONL line ${index + 1} is invalid: ${admitted.error.message}`);
|
|
1180
|
+
parsed.push(admitted.data);
|
|
1181
|
+
}
|
|
1182
|
+
const scopeId = encodeImScopeId(request.scope);
|
|
1183
|
+
const fingerprint = importFingerprint(request, fileName, parsed);
|
|
1184
|
+
return this.mutate(request.scope, (current) => {
|
|
1185
|
+
const existing = current.operations[request.operationId];
|
|
1186
|
+
assertOperation(existing, fingerprint, request.operationId);
|
|
1187
|
+
if (existing !== void 0) {
|
|
1188
|
+
if (existing.result.kind !== "jsonl-import") throw new ImRuntimeError("IM_DELIVERY_OPERATION_REUSED", `IM delivery operation '${request.operationId}' is not an import operation`);
|
|
1189
|
+
return {
|
|
1190
|
+
aggregate: current,
|
|
1191
|
+
value: existing.result,
|
|
1192
|
+
changed: false
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
let nextSequence = current.nextMessageSequenceNumber;
|
|
1196
|
+
const messages = { ...current.messages };
|
|
1197
|
+
const byExternalId = { ...current.messageIdsByExternalId };
|
|
1198
|
+
let importedCount = 0;
|
|
1199
|
+
let duplicateCount = 0;
|
|
1200
|
+
const receivedAt = timestamp();
|
|
1201
|
+
for (const input of parsed) {
|
|
1202
|
+
if (byExternalId[input.externalMessageId] !== void 0) {
|
|
1203
|
+
duplicateCount++;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
const id = messageIdentity(scopeId, input.externalMessageId);
|
|
1207
|
+
messages[id] = {
|
|
1208
|
+
...input,
|
|
1209
|
+
messageId: id,
|
|
1210
|
+
scopeId,
|
|
1211
|
+
origin: "jsonl-import",
|
|
1212
|
+
stage: "submitted",
|
|
1213
|
+
sequenceNumber: nextSequence++,
|
|
1214
|
+
receivedAt
|
|
1215
|
+
};
|
|
1216
|
+
byExternalId[input.externalMessageId] = id;
|
|
1217
|
+
importedCount++;
|
|
1218
|
+
}
|
|
1219
|
+
const result = {
|
|
1220
|
+
kind: "jsonl-import",
|
|
1221
|
+
operationId: request.operationId,
|
|
1222
|
+
status: "applied",
|
|
1223
|
+
fileName,
|
|
1224
|
+
messageCount: parsed.length,
|
|
1225
|
+
importedCount,
|
|
1226
|
+
duplicateCount
|
|
1227
|
+
};
|
|
1228
|
+
return {
|
|
1229
|
+
aggregate: {
|
|
1230
|
+
...current,
|
|
1231
|
+
nextMessageSequenceNumber: nextSequence,
|
|
1232
|
+
messages,
|
|
1233
|
+
messageIdsByExternalId: byExternalId,
|
|
1234
|
+
operations: {
|
|
1235
|
+
...current.operations,
|
|
1236
|
+
[request.operationId]: {
|
|
1237
|
+
fingerprint,
|
|
1238
|
+
result
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
},
|
|
1242
|
+
value: result,
|
|
1243
|
+
changed: true
|
|
1244
|
+
};
|
|
1245
|
+
}).then((result) => {
|
|
1246
|
+
if (result.changed) this.host.publish({
|
|
1247
|
+
scope: request.scope,
|
|
1248
|
+
scopeId,
|
|
1249
|
+
kind: "inbound"
|
|
1250
|
+
});
|
|
1251
|
+
return result.value;
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
/** @param request - Scoped send intent. @param frozenBinding - Route observed by the Agent task. @param clock - Time sampled when the intent is stored. @returns Durable outbox intent. */
|
|
1255
|
+
registerOutbound(request, frozenBinding, clock = Date.now) {
|
|
1256
|
+
const scopeId = encodeImScopeId(request.scope);
|
|
1257
|
+
return this.mutate(request.scope, (current) => {
|
|
1258
|
+
const existing = current.outbounds[request.requestId];
|
|
1259
|
+
assertOutbound(existing, request);
|
|
1260
|
+
if (existing !== void 0) return {
|
|
1261
|
+
aggregate: current,
|
|
1262
|
+
value: existing,
|
|
1263
|
+
changed: false
|
|
1264
|
+
};
|
|
1265
|
+
if (request.sender !== void 0 && (request.intent !== "human-manual" || request.sender.outboundRequestId !== request.requestId)) throw new ImRuntimeError("IM_MANUAL_MESSAGE_INVALID", "manual sender evidence must identify its own human-manual request");
|
|
1266
|
+
if (request.manualBinding !== void 0 && (request.intent !== "human-manual" || request.sender === void 0)) throw new ImRuntimeError("IM_MANUAL_MESSAGE_INVALID", "manual Session binding requires persisted human sender evidence");
|
|
1267
|
+
if (request.retryOfRequestId !== void 0) {
|
|
1268
|
+
const prior = current.outbounds[request.retryOfRequestId];
|
|
1269
|
+
if (request.intent !== "human-manual" || request.retryOfRequestId === request.requestId || prior?.intent !== "human-manual" || !(prior.content.format === "media" ? ["confirmed-failed", "pre-send-failed"].includes(prior.status) : prior.status === "result-unknown") || prior.content.format === "media" && this.records.hasUnfinishedFilePreparation(prior.requestId) || JSON.stringify(prior.manualBinding) !== JSON.stringify(request.manualBinding)) throw new ImRuntimeError("IM_MANUAL_RETRY_INVALID", "manual retry must name a distinct eligible human message in the same scope");
|
|
1270
|
+
}
|
|
1271
|
+
const policy = this.registrationPolicy(request, frozenBinding);
|
|
1272
|
+
const createdAt = new Date(clock()).toISOString();
|
|
1273
|
+
const outbound = {
|
|
1274
|
+
requestId: request.requestId,
|
|
1275
|
+
scopeId,
|
|
1276
|
+
intent: request.intent,
|
|
1277
|
+
content: request.content,
|
|
1278
|
+
status: policy.reason === void 0 ? "pending" : "pre-send-failed",
|
|
1279
|
+
sequenceNumber: current.nextOutboundSequenceNumber,
|
|
1280
|
+
...request.intent === "ai" ? { handlingEpoch: frozenBinding?.handlingEpoch ?? this.records.getHandlingControl(scopeId)?.handlingEpoch ?? 0 } : {},
|
|
1281
|
+
...policy.binding === void 0 ? {} : { routeBinding: policy.binding },
|
|
1282
|
+
...request.sender === void 0 ? {} : { sender: request.sender },
|
|
1283
|
+
...request.manualBinding === void 0 ? {} : { manualBinding: request.manualBinding },
|
|
1284
|
+
...request.retryOfRequestId === void 0 ? {} : { retryOfRequestId: request.retryOfRequestId },
|
|
1285
|
+
...policy.reason === void 0 ? {} : { preSendFailureReason: policy.reason },
|
|
1286
|
+
...request.replyToExternalMessageId === void 0 ? {} : { replyToExternalMessageId: request.replyToExternalMessageId },
|
|
1287
|
+
createdAt,
|
|
1288
|
+
updatedAt: createdAt
|
|
1289
|
+
};
|
|
1290
|
+
return {
|
|
1291
|
+
aggregate: {
|
|
1292
|
+
...current,
|
|
1293
|
+
nextOutboundSequenceNumber: current.nextOutboundSequenceNumber + 1,
|
|
1294
|
+
outbounds: {
|
|
1295
|
+
...current.outbounds,
|
|
1296
|
+
[request.requestId]: outbound
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1299
|
+
value: outbound,
|
|
1300
|
+
changed: true
|
|
1301
|
+
};
|
|
1302
|
+
}).then((result) => {
|
|
1303
|
+
if (result.changed) this.host.publish({
|
|
1304
|
+
scope: request.scope,
|
|
1305
|
+
scopeId,
|
|
1306
|
+
kind: "outbound",
|
|
1307
|
+
requestId: request.requestId
|
|
1308
|
+
});
|
|
1309
|
+
return result.value;
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
/** @param request - owning real outbox intent. @param media - durable upload display facts. @returns the updated row after its preparation commit. */
|
|
1313
|
+
recordMediaPreparation(request, media) {
|
|
1314
|
+
const scopeId = encodeImScopeId(request.scope);
|
|
1315
|
+
return this.mutate(request.scope, (current) => {
|
|
1316
|
+
const outbound = current.outbounds[request.requestId];
|
|
1317
|
+
if (outbound === void 0 || outbound.status !== "pending" || outbound.content.format !== "media") throw new ImRuntimeError("IM_OUTBOUND_STATE_INVALID", "media preparation requires a pending media intent");
|
|
1318
|
+
const updated = {
|
|
1319
|
+
...outbound,
|
|
1320
|
+
media,
|
|
1321
|
+
updatedAt: timestamp()
|
|
1322
|
+
};
|
|
1323
|
+
return {
|
|
1324
|
+
aggregate: {
|
|
1325
|
+
...current,
|
|
1326
|
+
outbounds: {
|
|
1327
|
+
...current.outbounds,
|
|
1328
|
+
[request.requestId]: updated
|
|
1329
|
+
}
|
|
1330
|
+
},
|
|
1331
|
+
value: updated,
|
|
1332
|
+
changed: true
|
|
1333
|
+
};
|
|
1334
|
+
}).then((result) => {
|
|
1335
|
+
this.host.publish({
|
|
1336
|
+
scope: request.scope,
|
|
1337
|
+
scopeId,
|
|
1338
|
+
kind: "outbound",
|
|
1339
|
+
requestId: request.requestId
|
|
1340
|
+
});
|
|
1341
|
+
return result.value;
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
beginOutboundAttempt(request) {
|
|
1345
|
+
const scopeId = encodeImScopeId(request.scope);
|
|
1346
|
+
return this.mutate(request.scope, (current) => {
|
|
1347
|
+
const outbound = current.outbounds[request.requestId];
|
|
1348
|
+
if (outbound === void 0) throw new ImRuntimeError("IM_OUTBOUND_NOT_FOUND", `IM outbound request '${request.requestId}' is unknown`);
|
|
1349
|
+
if (outbound.status !== "sent" && this.records.hasUnfinishedFilePreparation(request.requestId) && (outbound.status !== "result-unknown" || outbound.receipt?.errorCode !== "IM_MEDIA_PREPARATION_UNKNOWN")) {
|
|
1350
|
+
const updated = preparationUnknown(outbound, timestamp());
|
|
1351
|
+
return {
|
|
1352
|
+
aggregate: {
|
|
1353
|
+
...current,
|
|
1354
|
+
outbounds: {
|
|
1355
|
+
...current.outbounds,
|
|
1356
|
+
[request.requestId]: updated
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
value: {
|
|
1360
|
+
state: "result-unknown",
|
|
1361
|
+
outbound: updated
|
|
1362
|
+
},
|
|
1363
|
+
changed: true
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
if (outbound.status === "result-unknown") return {
|
|
1367
|
+
aggregate: current,
|
|
1368
|
+
value: {
|
|
1369
|
+
state: "result-unknown",
|
|
1370
|
+
outbound
|
|
1371
|
+
},
|
|
1372
|
+
changed: false
|
|
1373
|
+
};
|
|
1374
|
+
if (outbound.status === "dispatching") {
|
|
1375
|
+
const updated = {
|
|
1376
|
+
...outbound,
|
|
1377
|
+
status: "result-unknown",
|
|
1378
|
+
receipt: outbound.receipt ?? {
|
|
1379
|
+
providerStatus: "repeated-begin",
|
|
1380
|
+
observedAt: timestamp()
|
|
1381
|
+
},
|
|
1382
|
+
updatedAt: timestamp()
|
|
1383
|
+
};
|
|
1384
|
+
return {
|
|
1385
|
+
aggregate: {
|
|
1386
|
+
...current,
|
|
1387
|
+
outbounds: {
|
|
1388
|
+
...current.outbounds,
|
|
1389
|
+
[request.requestId]: updated
|
|
1390
|
+
}
|
|
1391
|
+
},
|
|
1392
|
+
value: {
|
|
1393
|
+
state: "result-unknown",
|
|
1394
|
+
outbound: updated
|
|
1395
|
+
},
|
|
1396
|
+
changed: true
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
if (outbound.status !== "pending") return {
|
|
1400
|
+
aggregate: current,
|
|
1401
|
+
value: {
|
|
1402
|
+
state: "blocked",
|
|
1403
|
+
outbound
|
|
1404
|
+
},
|
|
1405
|
+
changed: false
|
|
1406
|
+
};
|
|
1407
|
+
const reason = this.dispatchFailure(request.scope, outbound);
|
|
1408
|
+
if (reason !== void 0) {
|
|
1409
|
+
const updated = {
|
|
1410
|
+
...outbound,
|
|
1411
|
+
status: "pre-send-failed",
|
|
1412
|
+
preSendFailureReason: reason,
|
|
1413
|
+
updatedAt: timestamp()
|
|
1414
|
+
};
|
|
1415
|
+
return {
|
|
1416
|
+
aggregate: {
|
|
1417
|
+
...current,
|
|
1418
|
+
outbounds: {
|
|
1419
|
+
...current.outbounds,
|
|
1420
|
+
[request.requestId]: updated
|
|
1421
|
+
}
|
|
1422
|
+
},
|
|
1423
|
+
value: {
|
|
1424
|
+
state: "blocked",
|
|
1425
|
+
outbound: updated
|
|
1426
|
+
},
|
|
1427
|
+
changed: true
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
const attempt = {
|
|
1431
|
+
attemptId: attemptIdentity(),
|
|
1432
|
+
startedAt: timestamp()
|
|
1433
|
+
};
|
|
1434
|
+
const updated = {
|
|
1435
|
+
...outbound,
|
|
1436
|
+
status: "dispatching",
|
|
1437
|
+
attempt,
|
|
1438
|
+
updatedAt: attempt.startedAt
|
|
1439
|
+
};
|
|
1440
|
+
return {
|
|
1441
|
+
aggregate: {
|
|
1442
|
+
...current,
|
|
1443
|
+
outbounds: {
|
|
1444
|
+
...current.outbounds,
|
|
1445
|
+
[request.requestId]: updated
|
|
1446
|
+
}
|
|
1447
|
+
},
|
|
1448
|
+
value: {
|
|
1449
|
+
state: "ready",
|
|
1450
|
+
attemptId: attempt.attemptId,
|
|
1451
|
+
outbound: updated
|
|
1452
|
+
},
|
|
1453
|
+
changed: true
|
|
1454
|
+
};
|
|
1455
|
+
}).then((result) => {
|
|
1456
|
+
if (result.changed) this.host.publish({
|
|
1457
|
+
scope: request.scope,
|
|
1458
|
+
scopeId,
|
|
1459
|
+
kind: "outbound",
|
|
1460
|
+
requestId: request.requestId
|
|
1461
|
+
});
|
|
1462
|
+
return result.value;
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
/** @param request - Owning attempt. @returns Whether its frozen route and handling epoch still authorize an external submission. */
|
|
1466
|
+
authorizeOutboundSubmission(request) {
|
|
1467
|
+
const outbound = this.lookup(request.scope)?.outbounds[request.requestId];
|
|
1468
|
+
return outbound?.status === "dispatching" && outbound.attempt?.attemptId === request.attemptId && this.dispatchFailure(request.scope, outbound) === void 0;
|
|
1469
|
+
}
|
|
1470
|
+
settleOutboundAttempt(request) {
|
|
1471
|
+
const scopeId = encodeImScopeId(request.scope);
|
|
1472
|
+
return this.mutate(request.scope, (current) => {
|
|
1473
|
+
const outbound = current.outbounds[request.requestId];
|
|
1474
|
+
if (outbound === void 0) throw new ImRuntimeError("IM_OUTBOUND_NOT_FOUND", `IM outbound request '${request.requestId}' is unknown`);
|
|
1475
|
+
if (outbound.attempt?.attemptId !== request.attemptId) throw new ImRuntimeError("IM_OUTBOUND_ATTEMPT_MISMATCH", `IM outbound request '${request.requestId}' does not own attempt '${request.attemptId}'`);
|
|
1476
|
+
const unchanged = {
|
|
1477
|
+
outbound,
|
|
1478
|
+
revised: []
|
|
1479
|
+
};
|
|
1480
|
+
if (outbound.status === request.status && outbound.externalMessageId === request.externalMessageId && JSON.stringify(outbound.receipt) === JSON.stringify(request.receipt)) return {
|
|
1481
|
+
aggregate: current,
|
|
1482
|
+
value: unchanged,
|
|
1483
|
+
changed: false
|
|
1484
|
+
};
|
|
1485
|
+
if (outbound.status === "sent") return {
|
|
1486
|
+
aggregate: current,
|
|
1487
|
+
value: unchanged,
|
|
1488
|
+
changed: false
|
|
1489
|
+
};
|
|
1490
|
+
if (outbound.status !== "dispatching" && outbound.status !== "result-unknown") throw new ImRuntimeError("IM_OUTBOUND_STATE_INVALID", `IM outbound request '${request.requestId}' cannot settle from '${outbound.status}'`);
|
|
1491
|
+
const updated = {
|
|
1492
|
+
...outbound,
|
|
1493
|
+
status: request.status,
|
|
1494
|
+
...request.receipt === void 0 ? {} : { receipt: request.receipt },
|
|
1495
|
+
...request.externalMessageId === void 0 ? {} : { externalMessageId: request.externalMessageId },
|
|
1496
|
+
updatedAt: timestamp()
|
|
1497
|
+
};
|
|
1498
|
+
const claimed = request.status === "sent" && request.externalMessageId !== void 0 ? claimExactEcho(current.messages, updated, request.externalMessageId) : {
|
|
1499
|
+
messages: current.messages,
|
|
1500
|
+
revised: []
|
|
1501
|
+
};
|
|
1502
|
+
return {
|
|
1503
|
+
aggregate: {
|
|
1504
|
+
...current,
|
|
1505
|
+
messages: claimed.messages,
|
|
1506
|
+
outbounds: {
|
|
1507
|
+
...current.outbounds,
|
|
1508
|
+
[request.requestId]: updated
|
|
1509
|
+
}
|
|
1510
|
+
},
|
|
1511
|
+
value: {
|
|
1512
|
+
outbound: updated,
|
|
1513
|
+
revised: claimed.revised
|
|
1514
|
+
},
|
|
1515
|
+
changed: true
|
|
1516
|
+
};
|
|
1517
|
+
}).then((result) => {
|
|
1518
|
+
if (result.changed) {
|
|
1519
|
+
this.host.publish({
|
|
1520
|
+
scope: request.scope,
|
|
1521
|
+
scopeId,
|
|
1522
|
+
kind: "outbound",
|
|
1523
|
+
requestId: request.requestId
|
|
1524
|
+
});
|
|
1525
|
+
if (result.value.revised.length > 0) this.host.publish({
|
|
1526
|
+
scope: request.scope,
|
|
1527
|
+
scopeId,
|
|
1528
|
+
kind: "inbound",
|
|
1529
|
+
messageIds: result.value.revised
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
return result.value.outbound;
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
/** @param request - Local simulation intent. @param clock - Time sampled when the send settles. @returns Durable local receipt. */
|
|
1536
|
+
settleSimulationOutbound(request, clock = Date.now) {
|
|
1537
|
+
const scopeId = encodeImScopeId(request.scope);
|
|
1538
|
+
return this.mutate(request.scope, (current) => {
|
|
1539
|
+
const outbound = current.outbounds[request.requestId];
|
|
1540
|
+
if (outbound === void 0) throw new ImRuntimeError("IM_OUTBOUND_NOT_FOUND", `IM outbound request '${request.requestId}' is unknown`);
|
|
1541
|
+
if (outbound.status === "sent") return {
|
|
1542
|
+
aggregate: current,
|
|
1543
|
+
value: outbound,
|
|
1544
|
+
changed: false
|
|
1545
|
+
};
|
|
1546
|
+
if (outbound.intent === "ai" && this.handlingRevoked(request.scope, outbound)) {
|
|
1547
|
+
const updated = {
|
|
1548
|
+
...outbound,
|
|
1549
|
+
status: "pre-send-failed",
|
|
1550
|
+
preSendFailureReason: "cancelled",
|
|
1551
|
+
updatedAt: timestamp()
|
|
1552
|
+
};
|
|
1553
|
+
return {
|
|
1554
|
+
aggregate: {
|
|
1555
|
+
...current,
|
|
1556
|
+
outbounds: {
|
|
1557
|
+
...current.outbounds,
|
|
1558
|
+
[request.requestId]: updated
|
|
1559
|
+
}
|
|
1560
|
+
},
|
|
1561
|
+
value: updated,
|
|
1562
|
+
changed: true
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
if (outbound.status !== "pending") throw new ImRuntimeError("IM_OUTBOUND_STATE_INVALID", `simulation outbound request '${request.requestId}' cannot settle from '${outbound.status}'`);
|
|
1566
|
+
const sentAt = new Date(clock()).toISOString();
|
|
1567
|
+
const updated = {
|
|
1568
|
+
...outbound,
|
|
1569
|
+
status: "sent",
|
|
1570
|
+
receipt: {
|
|
1571
|
+
providerStatus: "simulation",
|
|
1572
|
+
observedAt: sentAt
|
|
1573
|
+
},
|
|
1574
|
+
updatedAt: sentAt
|
|
1575
|
+
};
|
|
1576
|
+
return {
|
|
1577
|
+
aggregate: {
|
|
1578
|
+
...current,
|
|
1579
|
+
outbounds: {
|
|
1580
|
+
...current.outbounds,
|
|
1581
|
+
[request.requestId]: updated
|
|
1582
|
+
}
|
|
1583
|
+
},
|
|
1584
|
+
value: updated,
|
|
1585
|
+
changed: true
|
|
1586
|
+
};
|
|
1587
|
+
}).then((result) => {
|
|
1588
|
+
if (result.changed) this.host.publish({
|
|
1589
|
+
scope: request.scope,
|
|
1590
|
+
scopeId,
|
|
1591
|
+
kind: "outbound",
|
|
1592
|
+
requestId: request.requestId
|
|
1593
|
+
});
|
|
1594
|
+
return result.value;
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
getOutbound(request) {
|
|
1598
|
+
return this.lookup(request.scope)?.outbounds[request.requestId];
|
|
1599
|
+
}
|
|
1600
|
+
queryOutbound(request) {
|
|
1601
|
+
assertLimit(request.limit);
|
|
1602
|
+
const aggregate = this.lookup(request.scope);
|
|
1603
|
+
if (aggregate === void 0) return {
|
|
1604
|
+
items: [],
|
|
1605
|
+
hasMore: false
|
|
1606
|
+
};
|
|
1607
|
+
return outboundPage(sortedOutbounds(aggregate), request);
|
|
1608
|
+
}
|
|
1609
|
+
cancelPendingAi(request) {
|
|
1610
|
+
const scopeId = encodeImScopeId(request.scope);
|
|
1611
|
+
return this.mutate(request.scope, (current) => {
|
|
1612
|
+
const updatedAt = timestamp();
|
|
1613
|
+
const cancelled = [];
|
|
1614
|
+
const outbounds = Object.fromEntries(Object.entries(current.outbounds).map(([id, outbound]) => {
|
|
1615
|
+
if (outbound.intent !== "ai" || outbound.status !== "pending") return [id, outbound];
|
|
1616
|
+
const updated = {
|
|
1617
|
+
...outbound,
|
|
1618
|
+
status: "pre-send-failed",
|
|
1619
|
+
preSendFailureReason: request.reason,
|
|
1620
|
+
updatedAt
|
|
1621
|
+
};
|
|
1622
|
+
cancelled.push(updated);
|
|
1623
|
+
return [id, updated];
|
|
1624
|
+
}));
|
|
1625
|
+
return {
|
|
1626
|
+
aggregate: cancelled.length === 0 ? current : {
|
|
1627
|
+
...current,
|
|
1628
|
+
outbounds
|
|
1629
|
+
},
|
|
1630
|
+
value: cancelled,
|
|
1631
|
+
changed: cancelled.length > 0
|
|
1632
|
+
};
|
|
1633
|
+
}).then((result) => {
|
|
1634
|
+
if (result.changed) this.host.publish({
|
|
1635
|
+
scope: request.scope,
|
|
1636
|
+
scopeId,
|
|
1637
|
+
kind: "outbound"
|
|
1638
|
+
});
|
|
1639
|
+
return result.value;
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
findSentOutbound(scope, externalMessageId) {
|
|
1643
|
+
return Object.values(this.lookup(scope)?.outbounds ?? {}).find((outbound) => outbound.status === "sent" && (outbound.externalMessageId === externalMessageId || outbound.receipt?.providerReceiptId === externalMessageId));
|
|
1644
|
+
}
|
|
1645
|
+
findUnknownOutbound(scope, text) {
|
|
1646
|
+
const needle = text.replace(/\s+/gu, " ").trim();
|
|
1647
|
+
if (needle === "") return void 0;
|
|
1648
|
+
return Object.values(this.lookup(scope)?.outbounds ?? {}).filter((outbound) => outbound.status === "result-unknown" && outbound.attempt !== void 0 && outbound.content.text.replace(/\s+/gu, " ").trim() === needle).sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.requestId.localeCompare(right.requestId)).at(-1);
|
|
1649
|
+
}
|
|
1650
|
+
registrationPolicy(request, frozenBinding) {
|
|
1651
|
+
const account = this.host.inspectAccount(request.scope.accountId, request.scope);
|
|
1652
|
+
if (account === void 0) return { reason: "account-not-found" };
|
|
1653
|
+
if (account.platform !== request.scope.platform) return { reason: "platform-mismatch" };
|
|
1654
|
+
if (request.intent === "human-manual") return {};
|
|
1655
|
+
const control = this.records.getHandlingControl(encodeImScopeId(request.scope));
|
|
1656
|
+
if (this.host.automaticHandlingBlocked(encodeImScopeId(request.scope)) || frozenBinding !== void 0 && (frozenBinding.handlingEpoch ?? 0) !== (control?.handlingEpoch ?? 0)) return { reason: "cancelled" };
|
|
1657
|
+
if (request.scope.kind === "simulation") return {};
|
|
1658
|
+
if (account.paused) return { reason: "account-paused" };
|
|
1659
|
+
const resolution = this.host.resolveRoute(request.scope);
|
|
1660
|
+
if (frozenBinding !== void 0) return resolution?.state === "matched" && account.revision === frozenBinding.accountRevision && resolution.route.id === frozenBinding.routeId && resolution.route.revision === frozenBinding.routeRevision && resolution.route.workspaceId === frozenBinding.workspaceId ? { binding: frozenBinding } : {
|
|
1661
|
+
binding: frozenBinding,
|
|
1662
|
+
reason: "route-changed"
|
|
1663
|
+
};
|
|
1664
|
+
if (resolution?.state === "matched") return { binding: {
|
|
1665
|
+
routeId: resolution.route.id,
|
|
1666
|
+
routeRevision: resolution.route.revision,
|
|
1667
|
+
workspaceId: resolution.route.workspaceId,
|
|
1668
|
+
accountRevision: account.revision
|
|
1669
|
+
} };
|
|
1670
|
+
if (resolution?.state === "disabled") return { reason: "route-disabled" };
|
|
1671
|
+
return { reason: "route-unmatched" };
|
|
1672
|
+
}
|
|
1673
|
+
handlingRevoked(scope, outbound) {
|
|
1674
|
+
const control = this.records.getHandlingControl(encodeImScopeId(scope));
|
|
1675
|
+
return this.host.automaticHandlingBlocked(encodeImScopeId(scope)) || (control?.handlingEpoch ?? 0) !== (outbound.handlingEpoch ?? 0);
|
|
1676
|
+
}
|
|
1677
|
+
dispatchFailure(scope, outbound) {
|
|
1678
|
+
const account = this.host.inspectAccount(scope.accountId);
|
|
1679
|
+
if (account === void 0) return "account-not-found";
|
|
1680
|
+
if (account.platform !== scope.platform) return "platform-mismatch";
|
|
1681
|
+
if (outbound.intent === "human-manual") return void 0;
|
|
1682
|
+
if (this.handlingRevoked(scope, outbound)) return "cancelled";
|
|
1683
|
+
if (account.paused) return "account-paused";
|
|
1684
|
+
const resolution = this.host.resolveRoute(scope);
|
|
1685
|
+
if (resolution?.state !== "matched" || outbound.routeBinding === void 0) return "route-changed";
|
|
1686
|
+
const binding = outbound.routeBinding;
|
|
1687
|
+
return account.revision === binding.accountRevision && resolution.route.id === binding.routeId && resolution.route.revision === binding.routeRevision && resolution.route.workspaceId === binding.workspaceId ? void 0 : "route-changed";
|
|
1688
|
+
}
|
|
1689
|
+
assertRealScope(scope) {
|
|
1690
|
+
if (scope.conversationId.trim() === "") throw new ImRuntimeError("IM_DELIVERY_SCOPE_INVALID", "IM conversationId must be non-empty");
|
|
1691
|
+
const account = this.host.inspectAccount(scope.accountId);
|
|
1692
|
+
if (account === void 0) throw new ImRuntimeError("IM_ACCOUNT_NOT_FOUND", `IM account '${scope.accountId}' is unknown`);
|
|
1693
|
+
if (account.platform !== scope.platform) throw new ImRuntimeError("IM_DELIVERY_SCOPE_INVALID", "IM delivery scope platform does not match its account");
|
|
1694
|
+
}
|
|
1695
|
+
lookup(scope) {
|
|
1696
|
+
const aggregate = this.records.getScope(encodeImScopeId(scope));
|
|
1697
|
+
if (aggregate !== void 0 && !sameScope(aggregate.scope, scope)) throw new Error("IM scope hash collision");
|
|
1698
|
+
return aggregate;
|
|
1699
|
+
}
|
|
1700
|
+
require(scopeId) {
|
|
1701
|
+
const aggregate = this.records.getScope(scopeId);
|
|
1702
|
+
if (aggregate === void 0) throw new Error(`IM delivery scope '${scopeId}' disappeared during a serialized mutation`);
|
|
1703
|
+
return aggregate;
|
|
1704
|
+
}
|
|
1705
|
+
mutate(scope, fn) {
|
|
1706
|
+
return this.enqueue(encodeImScopeId(scope), () => this.applyMutation(scope, fn));
|
|
1707
|
+
}
|
|
1708
|
+
/** Apply one transform to the stored or initial aggregate; the caller already holds the scope's queue. */
|
|
1709
|
+
async applyMutation(scope, fn) {
|
|
1710
|
+
const current = this.lookup(scope) ?? initialAggregate(scope);
|
|
1711
|
+
const result = fn(current);
|
|
1712
|
+
if (result.changed) await this.persistMutation(current, result.aggregate);
|
|
1713
|
+
return result;
|
|
1714
|
+
}
|
|
1715
|
+
/** @param queue - external durable queue. @returns completion after every journal transfer is synced and removed from SQLite. */
|
|
1716
|
+
async transferUploads(queue) {
|
|
1717
|
+
for (const scopeId of this.records.uploadScopeIds()) await this.enqueue(scopeId, async () => {
|
|
1718
|
+
const current = this.require(scopeId);
|
|
1719
|
+
const pending = current.uploadPending ?? [];
|
|
1720
|
+
if (pending.length === 0) return;
|
|
1721
|
+
queue.append(pending);
|
|
1722
|
+
await this.records.putScope(scopeId, {
|
|
1723
|
+
...current,
|
|
1724
|
+
uploadPending: []
|
|
1725
|
+
});
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
async persistMutation(current, next) {
|
|
1729
|
+
const target = this.host.uploadTarget?.(next.scope);
|
|
1730
|
+
const observations = [];
|
|
1731
|
+
const destinations = { ...current.uploadTargets };
|
|
1732
|
+
const identities = { ...current.uploadIdentities };
|
|
1733
|
+
const unknownIdentity = {
|
|
1734
|
+
senderAccountId: null,
|
|
1735
|
+
senderDisplayName: null,
|
|
1736
|
+
senderDomain: null,
|
|
1737
|
+
assignedAccountId: null
|
|
1738
|
+
};
|
|
1739
|
+
let observedAt = new Date(Math.max(Date.now(), Date.parse(current.uploadObservedAt ?? "1970-01-01T00:00:00.000Z") + 1)).toISOString();
|
|
1740
|
+
const record = (message, destination, key) => {
|
|
1741
|
+
destinations[key] = destination;
|
|
1742
|
+
let identity = identities[key];
|
|
1743
|
+
if (message.kind === "inbound") {
|
|
1744
|
+
const value = message.value;
|
|
1745
|
+
const sender = value.sender;
|
|
1746
|
+
const previous = current.messages[value.messageId];
|
|
1747
|
+
const unchangedSender = previous !== void 0 && JSON.stringify(previous.sender) === JSON.stringify(sender);
|
|
1748
|
+
const own = sender.kind === "ai" || sender.kind === "human-dsh" ? identities[`outbound:${sender.outboundRequestId}`] : void 0;
|
|
1749
|
+
identity = previous !== void 0 && identity === void 0 && previous.uploadIdentity === void 0 ? unknownIdentity : own === void 0 ? value.uploadIdentity ?? (unchangedSender ? identity : void 0) ?? {
|
|
1750
|
+
senderAccountId: sender.kind === "external" ? sender.senderId : sender.kind === "human-native" || sender.kind === "human-dsh" ? sender.providerActorId ?? null : null,
|
|
1751
|
+
senderDisplayName: sender.kind === "external" ? sender.senderDisplayName ?? null : null,
|
|
1752
|
+
senderDomain: null,
|
|
1753
|
+
assignedAccountId: next.presentation?.assignedActorId ?? null
|
|
1754
|
+
} : {
|
|
1755
|
+
...own,
|
|
1756
|
+
assignedAccountId: value.uploadIdentity?.assignedAccountId ?? own.assignedAccountId
|
|
1757
|
+
};
|
|
1758
|
+
} else if (message.kind === "outbound" && identity === void 0) {
|
|
1759
|
+
const actor = this.host.uploadIdentity?.(next.scope);
|
|
1760
|
+
identity = current.outbounds[message.value.requestId] !== void 0 ? unknownIdentity : {
|
|
1761
|
+
senderAccountId: message.value.sender?.providerActorId ?? actor?.accountId ?? null,
|
|
1762
|
+
senderDisplayName: actor?.displayName ?? null,
|
|
1763
|
+
senderDomain: actor?.domain ?? null,
|
|
1764
|
+
assignedAccountId: next.presentation?.assignedActorId ?? null
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
if (identity !== void 0) identities[key] = identity;
|
|
1768
|
+
observations.push({
|
|
1769
|
+
id: brandString(randomUUID()),
|
|
1770
|
+
target: destination,
|
|
1771
|
+
scope: next.scope,
|
|
1772
|
+
observedAt,
|
|
1773
|
+
message,
|
|
1774
|
+
...identity === void 0 ? {} : { identity }
|
|
1775
|
+
});
|
|
1776
|
+
observedAt = new Date(Date.parse(observedAt) + 1).toISOString();
|
|
1777
|
+
};
|
|
1778
|
+
for (const [id, value] of Object.entries(next.messages)) {
|
|
1779
|
+
const key = `inbound:${id}`;
|
|
1780
|
+
const previous = current.messages[id];
|
|
1781
|
+
const ownEcho = value.sender.kind === "ai" || value.sender.kind === "human-dsh";
|
|
1782
|
+
const senderRevised = previous !== void 0 && JSON.stringify(previous.sender) !== JSON.stringify(value.sender);
|
|
1783
|
+
const destination = destinations[key] ?? (previous === void 0 ? target : void 0);
|
|
1784
|
+
if (destination !== void 0 && value.origin === "live" && JSON.stringify(value) !== JSON.stringify(previous) && (!ownEcho || senderRevised)) record({
|
|
1785
|
+
kind: "inbound",
|
|
1786
|
+
value
|
|
1787
|
+
}, destination, key);
|
|
1788
|
+
}
|
|
1789
|
+
for (const [id, value] of Object.entries(next.outbounds)) {
|
|
1790
|
+
const key = `outbound:${id}`;
|
|
1791
|
+
const destination = destinations[key] ?? (current.outbounds[id] === void 0 ? target : void 0);
|
|
1792
|
+
if (destination !== void 0 && JSON.stringify(value) !== JSON.stringify(current.outbounds[id])) record({
|
|
1793
|
+
kind: "outbound",
|
|
1794
|
+
value
|
|
1795
|
+
}, destination, key);
|
|
1796
|
+
}
|
|
1797
|
+
await this.records.putScope(next.scopeId, observations.length === 0 ? next : {
|
|
1798
|
+
...next,
|
|
1799
|
+
uploadPending: [...current.uploadPending ?? [], ...observations],
|
|
1800
|
+
uploadObservedAt: observedAt,
|
|
1801
|
+
uploadTargets: destinations,
|
|
1802
|
+
uploadIdentities: identities
|
|
1803
|
+
});
|
|
1804
|
+
if (observations.length > 0) this.host.onUploadReady?.();
|
|
1805
|
+
}
|
|
1806
|
+
enqueue(scopeId, job) {
|
|
1807
|
+
const result = (this.tails.get(scopeId) ?? Promise.resolve()).then(job);
|
|
1808
|
+
const tail = settled(result);
|
|
1809
|
+
this.tails.set(scopeId, tail);
|
|
1810
|
+
tail.finally(() => {
|
|
1811
|
+
if (this.tails.get(scopeId) === tail) this.tails.delete(scopeId);
|
|
1812
|
+
});
|
|
1813
|
+
return result;
|
|
1814
|
+
}
|
|
1815
|
+
enqueueCursor(id, job) {
|
|
1816
|
+
const result = (this.cursorTails.get(id) ?? Promise.resolve()).then(job);
|
|
1817
|
+
const tail = settled(result);
|
|
1818
|
+
this.cursorTails.set(id, tail);
|
|
1819
|
+
tail.finally(() => {
|
|
1820
|
+
if (this.cursorTails.get(id) === tail) this.cursorTails.delete(id);
|
|
1821
|
+
});
|
|
1822
|
+
return result;
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
//#endregion
|
|
1826
|
+
//#region src/read-state.ts
|
|
1827
|
+
/** Strict interpretation of untrusted single-chat buyer receipt evidence. */
|
|
1828
|
+
/** @param buyer - verified direct recipient. @returns its SDK user id when present, otherwise the platform actor id. */
|
|
1829
|
+
function buyerUid(buyer) {
|
|
1830
|
+
return buyer.userId ?? buyer.providerActorId;
|
|
1831
|
+
}
|
|
1832
|
+
/** @param sender - SDK sender string or object; absent receipt senders remain absent. @returns the supplied sender UID, or absence. */
|
|
1833
|
+
function senderUid(sender) {
|
|
1834
|
+
return typeof sender === "string" ? sender : sender?.uid;
|
|
1835
|
+
}
|
|
1836
|
+
/** @param uid - SDK identity. @param buyer - verified OpenAPI buyer UID. @returns whether domain-qualified or unqualified SDK identity names this buyer. */
|
|
1837
|
+
function sameBuyer(uid, buyer) {
|
|
1838
|
+
return uid === buyer || uid === `${buyer}@cntaobao`;
|
|
1839
|
+
}
|
|
1840
|
+
/** @param data - raw 40103 counts. @param singleton - independently verified receiver set consists solely of the target buyer. @returns conservative buyer state. */
|
|
1841
|
+
function buyerReadState(data, singleton) {
|
|
1842
|
+
if (!singleton || data.receiverCount !== 1) return "unknown";
|
|
1843
|
+
return data.unreadCount === 0 ? "read" : data.unreadCount === 1 ? "unread" : "unknown";
|
|
1844
|
+
}
|
|
1845
|
+
const rowsSchema = z.array(z.object({
|
|
1846
|
+
uid: z.string(),
|
|
1847
|
+
status: z.number().int().optional()
|
|
1848
|
+
}));
|
|
1849
|
+
/** @param data - ListMessageReadStatus response body. @param buyer - independently verified buyer UID. @returns matched status and whether the response establishes a singleton receiver set. */
|
|
1850
|
+
function buyerReadQuery(data, buyer) {
|
|
1851
|
+
const parsed = rowsSchema.safeParse(data);
|
|
1852
|
+
if (!parsed.success) return {
|
|
1853
|
+
state: "unknown",
|
|
1854
|
+
singleton: false
|
|
1855
|
+
};
|
|
1856
|
+
const matches = parsed.data.filter((row) => sameBuyer(row.uid, buyer));
|
|
1857
|
+
if (matches.length !== 1) return {
|
|
1858
|
+
state: "unknown",
|
|
1859
|
+
singleton: false
|
|
1860
|
+
};
|
|
1861
|
+
const status = matches[0].status;
|
|
1862
|
+
return {
|
|
1863
|
+
state: status === 2 ? "read" : status === 0 ? "unread" : "unknown",
|
|
1864
|
+
singleton: parsed.data.length === 1
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
//#endregion
|
|
1868
|
+
export { senderUid as a, encodeImScopeId as c, imExecutionAggregateSchema as d, imProviderCursorAggregateSchema as f, sameBuyer as i, imDeliveryAggregateSchema as l, ImRuntimeError as m, buyerReadState as n, ImDeliveryStore as o, imUploadObservationSchema as p, buyerUid as r, encodeImProviderCursorId as s, buyerReadQuery as t, imDeliveryScopeSchema as u };
|