@agentunion/fastaun 0.5.20 → 0.5.21

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.
Files changed (107) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/_packed_docs/CHANGELOG.md +47 -0
  3. package/_packed_docs/INDEX.md +453 -440
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +164 -161
  5. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
  6. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
  7. package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
  8. package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
  9. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
  10. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  11. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
  12. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -658
  13. package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
  14. package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
  15. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  16. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  17. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  18. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +4 -1
  19. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +147 -14
  21. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
  22. package/_packed_docs/sdk/09-group-rpc-manual.md +5 -2
  23. package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
  24. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
  25. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +134 -130
  27. package/_packed_docs/sdk/INDEX.md +239 -221
  28. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -922
  29. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +106 -43
  30. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +180 -9
  31. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -708
  32. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -476
  33. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +1 -1
  34. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +2 -2
  35. package/dist/agent-md-schema.js +6 -4
  36. package/dist/agent-md-schema.js.map +1 -1
  37. package/dist/agent-md.d.ts +22 -2
  38. package/dist/agent-md.js +84 -67
  39. package/dist/agent-md.js.map +1 -1
  40. package/dist/aid-store.d.ts +13 -0
  41. package/dist/aid-store.js +50 -15
  42. package/dist/aid-store.js.map +1 -1
  43. package/dist/auth.js +4 -0
  44. package/dist/auth.js.map +1 -1
  45. package/dist/cert-utils.d.ts +1 -0
  46. package/dist/cert-utils.js +13 -0
  47. package/dist/cert-utils.js.map +1 -1
  48. package/dist/client/delivery.d.ts +59 -1
  49. package/dist/client/delivery.js +1018 -125
  50. package/dist/client/delivery.js.map +1 -1
  51. package/dist/client/rpc-pipeline.d.ts +4 -1
  52. package/dist/client/rpc-pipeline.js +123 -37
  53. package/dist/client/rpc-pipeline.js.map +1 -1
  54. package/dist/client/v2-e2ee.d.ts +12 -2
  55. package/dist/client/v2-e2ee.js +751 -294
  56. package/dist/client/v2-e2ee.js.map +1 -1
  57. package/dist/client.d.ts +26 -2
  58. package/dist/client.js +379 -63
  59. package/dist/client.js.map +1 -1
  60. package/dist/config.d.ts +4 -0
  61. package/dist/config.js +1 -0
  62. package/dist/config.js.map +1 -1
  63. package/dist/events.d.ts +18 -1
  64. package/dist/events.js +148 -9
  65. package/dist/events.js.map +1 -1
  66. package/dist/facades.d.ts +43 -1
  67. package/dist/facades.js +363 -7
  68. package/dist/facades.js.map +1 -1
  69. package/dist/index.d.ts +5 -3
  70. package/dist/index.js +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/keystore/aid-db.d.ts +6 -1
  73. package/dist/keystore/aid-db.js +184 -42
  74. package/dist/keystore/aid-db.js.map +1 -1
  75. package/dist/keystore/index.d.ts +14 -0
  76. package/dist/keystore/local-token-store.d.ts +9 -1
  77. package/dist/keystore/local-token-store.js +79 -14
  78. package/dist/keystore/local-token-store.js.map +1 -1
  79. package/dist/org-protocol.d.ts +45 -0
  80. package/dist/org-protocol.js +754 -11
  81. package/dist/org-protocol.js.map +1 -1
  82. package/dist/ownership-v21.d.ts +16 -3
  83. package/dist/ownership-v21.js +35 -17
  84. package/dist/ownership-v21.js.map +1 -1
  85. package/dist/ownership-workflow.d.ts +17 -0
  86. package/dist/ownership-workflow.js +511 -26
  87. package/dist/ownership-workflow.js.map +1 -1
  88. package/dist/secret-store/file-store.js +84 -0
  89. package/dist/secret-store/file-store.js.map +1 -1
  90. package/dist/seq-tracker.d.ts +1 -0
  91. package/dist/seq-tracker.js +2 -2
  92. package/dist/seq-tracker.js.map +1 -1
  93. package/dist/tools/cross-sdk-agent.d.ts +66 -3
  94. package/dist/tools/cross-sdk-agent.js +466 -36
  95. package/dist/tools/cross-sdk-agent.js.map +1 -1
  96. package/dist/transport.d.ts +3 -0
  97. package/dist/transport.js +98 -44
  98. package/dist/transport.js.map +1 -1
  99. package/dist/types.d.ts +1 -0
  100. package/dist/v2/e2ee/encrypt-group.js +1 -0
  101. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  102. package/dist/v2/e2ee/encrypt-p2p.js +1 -0
  103. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  104. package/dist/v2/e2ee/types.d.ts +1 -0
  105. package/dist/version.d.ts +1 -1
  106. package/dist/version.js +1 -1
  107. package/package.json +1 -1
@@ -14,6 +14,7 @@ import { publicKeyFingerprint } from '../cert-utils.js';
14
14
  import { VERSION } from '../version.js';
15
15
  import { canonicalJson, digest, buildRequestBinding, verifyMembershipBinding, ProtocolError } from '../org-protocol.js';
16
16
  import { ownershipTaskResult } from '../ownership-workflow.js';
17
+ import { TimeoutError } from '../errors.js';
17
18
  function envBool(name, fallback = false) {
18
19
  const raw = process.env[name];
19
20
  if (raw == null)
@@ -128,10 +129,25 @@ export class CrossSdkTsAgent {
128
129
  rawPushObservations = [];
129
130
  rawPushObservationId = '';
130
131
  rawPushRpcMethods = [];
132
+ eventLedger = [];
133
+ batchEventMode = false;
134
+ batchEvents = [];
135
+ singleEventCounts = { 'message.received': 0, 'group.message_created': 0, 'group.changed': 0 };
136
+ eventBoundClients = new WeakSet();
137
+ batchEventClient;
138
+ batchEventHandlers = [];
139
+ rpcSpans = [];
140
+ observationGeneration = 0;
141
+ pullBarrier = null;
142
+ rpcFailures = [];
143
+ messageOperations = new Map();
131
144
  rpcObservedTransports = new WeakSet();
145
+ rpcObservedPipelines = new WeakSet();
146
+ rpcObservedPulls = new WeakMap();
132
147
  groupChangedEvents = [];
133
148
  groupInviteEvents = [];
134
149
  readStateEvents = [];
150
+ peersSnapshots = [];
135
151
  ownershipEvents = [];
136
152
  traces = new Map();
137
153
  sendResults = new Map();
@@ -154,6 +170,12 @@ export class CrossSdkTsAgent {
154
170
  return client;
155
171
  }
156
172
  bindClientEvents() {
173
+ this.eventBoundClients ??= new WeakSet();
174
+ if (this.eventBoundClients.has(this.client)) {
175
+ this.setBatchEventMode(this.batchEventMode ?? false);
176
+ return;
177
+ }
178
+ this.eventBoundClients.add(this.client);
157
179
  this.installTransportRpcObserver();
158
180
  this.client.on('_raw.message.received', (data) => this.storeRawPushObservation('_raw.message.received', 'p2p', 'legacy', data));
159
181
  this.client.on('_raw.peer.v2.message_received', (data) => this.storeRawPushObservation('_raw.peer.v2.message_received', 'p2p', 'v2', data));
@@ -161,22 +183,30 @@ export class CrossSdkTsAgent {
161
183
  this.client.on('_raw.group.v2.message_created', (data) => this.storeRawPushObservation('_raw.group.v2.message_created', 'group', 'v2', data));
162
184
  this.client.on('_raw.group.changed', (data) => this.storeRawGroupChangedObservation(data));
163
185
  this.client.on('message.received', (msg) => {
164
- void this.storeInboxItem(this.normalizeMessage(msg, true));
186
+ this.countSingleEvent('message.received');
187
+ void this.storeApplicationEvent('message.received', msg);
165
188
  });
166
189
  this.client.on('message.undecryptable', (msg) => {
167
190
  void this.storeInboxItem(this.normalizeMessage(msg, false, 'undecryptable'));
168
191
  });
169
192
  this.client.on('group.message_created', (msg) => {
170
- void this.storeGroupInboxItem(this.normalizeGroupMessage(msg, true));
193
+ this.countSingleEvent('group.message_created');
194
+ void this.storeApplicationEvent('group.message_created', msg);
171
195
  });
172
196
  this.client.on('group.message_undecryptable', (msg) => {
173
197
  void this.storeGroupInboxItem(this.normalizeGroupMessage(msg, false, 'undecryptable'));
174
198
  });
175
- this.client.on('group.changed', (data) => this.storeGroupChangedPublicEvent(data));
199
+ this.client.on('group.changed', (data) => {
200
+ this.countSingleEvent('group.changed');
201
+ void this.storeApplicationEvent('group.changed', data);
202
+ });
176
203
  this.client.on('group.invite_received', (data) => this.storeGroupInviteEvent(data));
177
204
  for (const event of ['message.read_changed', 'group.read_changed', 'read_state_sync']) {
178
205
  this.client.on(event, (payload) => { this.readStateEvents.push({ event, payload: jsonSafe(payload) }); });
179
206
  }
207
+ this.client.on('message.peers_snapshot', (payload) => {
208
+ this.peersSnapshots.push({ event: 'message.peers_snapshot', payload: jsonSafe(payload) });
209
+ });
180
210
  for (const event of ['ownership.request', 'ownership.pending', 'ownership.progress', 'ownership.completed', 'ownership.rejected', 'ownership.stopped', 'ownership.error', 'ownership.action_required']) {
181
211
  this.client.on(event, (payload) => {
182
212
  this.ownershipEvents.push({ event, payload: jsonSafe(payload) });
@@ -189,15 +219,91 @@ export class CrossSdkTsAgent {
189
219
  this.recordTrace(textOf(payload.org_id), { stage: event, payload });
190
220
  });
191
221
  }
222
+ this.setBatchEventMode(this.batchEventMode ?? false);
223
+ }
224
+ countSingleEvent(event) {
225
+ this.singleEventCounts ??= { 'message.received': 0, 'group.message_created': 0, 'group.changed': 0 };
226
+ this.singleEventCounts[event] = Number(this.singleEventCounts[event] ?? 0) + 1;
227
+ }
228
+ async storeApplicationEvent(event, value) {
229
+ if (event === 'message.received') {
230
+ this.storeEventLedger(event, value);
231
+ await this.storeInboxItem(this.normalizeMessage(value, true));
232
+ }
233
+ else if (event === 'group.message_created') {
234
+ this.storeEventLedger(event, value);
235
+ await this.storeGroupInboxItem(this.normalizeGroupMessage(value, true));
236
+ }
237
+ else if (event === 'group.changed')
238
+ this.storeGroupChangedPublicEvent(value);
239
+ }
240
+ setBatchEventMode(enabled) {
241
+ this.batchEventMode = enabled;
242
+ if (enabled && this.batchEventClient === this.client && this.batchEventHandlers?.length)
243
+ return;
244
+ for (const { event, handler } of this.batchEventHandlers ?? [])
245
+ this.batchEventClient?.off(event, handler);
246
+ this.batchEventClient = this.client;
247
+ this.batchEventHandlers = [];
248
+ if (!enabled)
249
+ return;
250
+ for (const event of ['message.batch', 'group.message_batch', 'group.event_batch']) {
251
+ const handler = async (value) => {
252
+ const data = jsonObjectOf(value) ?? {};
253
+ const items = JSON.parse(JSON.stringify(jsonSafe(Array.isArray(data.items) ? data.items : [])));
254
+ this.batchEvents ??= [];
255
+ this.batchEvents.push({ observation_id: this.rawPushObservationId, time_ms: Date.now(), event,
256
+ namespace: textOf(data.namespace), source: textOf(data.source), items });
257
+ for (const item of items)
258
+ await this.storeApplicationEvent(textOf(item.event), item.payload);
259
+ };
260
+ this.batchEventHandlers.push({ event, handler });
261
+ this.client.on(event, handler);
262
+ }
192
263
  }
193
264
  installTransportRpcObserver() {
265
+ const client = this.client;
266
+ const pipeline = client._rpcPipeline;
267
+ this.rpcObservedPipelines ??= new WeakSet();
268
+ this.rpcObservedPulls ??= new WeakMap();
269
+ if (pipeline?.bindActivePullCancellation && !this.rpcObservedPipelines.has(pipeline)) {
270
+ const originalBind = pipeline.bindActivePullCancellation;
271
+ this.rpcObservedPipelines.add(pipeline);
272
+ pipeline.bindActivePullCancellation = (method, params, request, gateKey = '') => {
273
+ const result = originalBind.call(pipeline, method, params, request, gateKey);
274
+ const observed = this.rpcObservedPulls.get(request);
275
+ if (!this.pullBarrier?.preserve || !observed || !['message.pull', 'message.v2.pull'].includes(method)
276
+ || !textOf(params.sender_aid) || typeof client._delivery?.peerNamespace !== 'function')
277
+ return result;
278
+ const key = gateKey || pipeline.pullGateKeyForCall(method, params);
279
+ const namespace = client._delivery?.peerNamespace(textOf(params.sender_aid));
280
+ const owners = [...pipeline.pullGateStates.values()].flatMap((state) => [...state.activeByNamespace.values()])
281
+ .filter((job) => job.key === key && job.namespace === namespace && job.running && !job.timedOut && !job.invalidated && job.cancel);
282
+ if (owners.length === 1)
283
+ observed.owner = { job: owners[0], cancel: owners[0].cancel, key, namespace: namespace };
284
+ return result;
285
+ };
286
+ }
194
287
  const transport = this.client._transport;
195
288
  transport?.setTraceMode('diag');
196
289
  if (!transport?.call || this.rpcObservedTransports.has(transport))
197
290
  return;
198
291
  const originalCall = transport.call;
292
+ const originalClose = transport.close;
199
293
  this.rpcObservedTransports.add(transport);
200
- transport.call = async (method, params, timeout, trace, background) => {
294
+ if (originalClose)
295
+ transport.close = async () => {
296
+ if (!this.pullBarrier?.preserve)
297
+ this.releasePullBarrier();
298
+ try {
299
+ await originalClose.call(transport);
300
+ }
301
+ finally {
302
+ if (!this.pullBarrier?.preserve)
303
+ this.releasePullBarrier();
304
+ }
305
+ };
306
+ transport.call = (method, params, timeout, trace, background) => {
201
307
  this.storeRawPushRpcMethod({ type: 'rpc', method, request_window: this.rawPushRequestWindow(params) });
202
308
  let requestParams = params;
203
309
  if (method === 'auth.connect' && !this.inlineRealtimePayloadV1 && jsonObjectOf(params)) {
@@ -209,8 +315,219 @@ export class CrossSdkTsAgent {
209
315
  }
210
316
  requestParams = request;
211
317
  }
212
- return await originalCall.call(transport, method, requestParams, timeout, trace, background);
318
+ const generation = this.observationGeneration;
319
+ const span = this.startRpcSpan(method, requestParams);
320
+ const controller = new AbortController();
321
+ let request;
322
+ const observed = {};
323
+ const pending = (async () => {
324
+ try {
325
+ request = originalCall.call(transport, method, requestParams, timeout, trace, background);
326
+ const result = await request;
327
+ if (span) {
328
+ this.finishRpcResponse(span, result);
329
+ if (generation === this.observationGeneration && (this.isObservedPull(method) || this.pullBarrier?.method === method)) {
330
+ await this.waitForPullBarrier(method, controller.signal, timeout ?? transport._timeout ?? 35_000, span, observed.owner);
331
+ }
332
+ if (generation === this.observationGeneration)
333
+ this.injectRPCFailure(method, span);
334
+ span.success = true;
335
+ }
336
+ return result;
337
+ }
338
+ catch (error) {
339
+ if (span) {
340
+ span.success = false;
341
+ span.error = error instanceof Error ? error.name : String(error);
342
+ }
343
+ throw error;
344
+ }
345
+ finally {
346
+ if (span)
347
+ span.end_ms = Date.now();
348
+ }
349
+ })();
350
+ pending.cancel = () => { controller.abort(); request?.cancel?.(); };
351
+ this.rpcObservedPulls.set(pending, observed);
352
+ return pending;
353
+ };
354
+ }
355
+ storeEventLedger(event, value) {
356
+ const data = jsonObjectOf(value) ?? {};
357
+ const internal = this.client;
358
+ const payload = JSON.parse(stableStringify(jsonSafe(data.payload ?? null)));
359
+ this.eventLedger.push({
360
+ observation_id: this.rawPushObservationId, event, event_index: this.eventLedger.length + 1, time_ms: Date.now(),
361
+ scope: event === 'message.received' ? 'p2p' : 'group', receiver_aid: this.aid,
362
+ device_id: textOf(internal._deviceId), slot_id: textOf(internal._slotId ?? this.slotId),
363
+ sender_aid: textOf(data.sender_aid ?? data.from_aid ?? data.from), group_id: textOf(data.group_id),
364
+ message_id: textOf(data.message_id ?? data.id), seq: Number(data.seq ?? data.message_seq ?? data.msg_seq ?? 0) || 0,
365
+ storage: textOf(data.storage), volatile_cursor: textOf(data.volatile_cursor),
366
+ payload, payload_sha256: sha256Json(payload), need_render: data.need_render !== false,
367
+ ...(Object.prototype.hasOwnProperty.call(data, 'mention_mode') ? { mention_mode: data.mention_mode } : {}),
368
+ });
369
+ }
370
+ isObservedPull(method) {
371
+ return ['message.pull', 'message.v2.pull', 'group.pull', 'group.v2.pull', 'group.pull_messages', 'group.pull_events', 'group.events'].includes(method);
372
+ }
373
+ startRpcSpan(method, value) {
374
+ if (!this.isObservedPull(method) && !['message.peers', 'message.ack', 'message.v2.ack', 'group.ack', 'group.v2.ack', 'group.ack_messages', 'group.ack_events'].includes(method))
375
+ return null;
376
+ const params = jsonObjectOf(value) ?? {};
377
+ const senderAid = textOf(params.sender_aid), groupId = textOf(params.group_id ?? params.group_aid);
378
+ const namespace = method === 'message.peers' ? '' : method.startsWith('group.')
379
+ ? groupId ? `${method === 'group.pull_events' || method === 'group.events' || method === 'group.ack_events' ? 'group_event' : 'group'}:${groupId}` : ''
380
+ : senderAid ? `p2p.peer.v1/${Buffer.from(senderAid, 'utf8').toString('base64url')}` : `p2p:${this.aid}`;
381
+ const span = {
382
+ observation_id: this.rawPushObservationId, span_id: crypto.randomUUID(), boundary: 'transport_call', method,
383
+ start_ms: Date.now(), response_ms: null, end_ms: null, success: null,
384
+ cursor_scope: textOf(params.cursor_scope) || (method === 'message.peers' || senderAid ? 'peer' : method.startsWith('group.') ? namespace.startsWith('group_event:') ? 'group_event' : 'group' : 'global'), sender_aid: senderAid,
385
+ group_id: groupId, namespace, request_window: this.observedRequestWindow(params),
386
+ response_message_count: 0, response_volatile_count: 0, response_event_count: 0,
387
+ response_item_keys: [], response_event_keys: [],
388
+ has_more: null, persistent_has_more: null, volatile_has_more: null, has_more_persistent: null, has_more_volatile: null, effective_ack_seq: null,
213
389
  };
390
+ this.rpcSpans.push(span);
391
+ return span;
392
+ }
393
+ observedRequestWindow(params) {
394
+ const window = {};
395
+ for (const key of ['after_seq', 'before_seq', 'until_seq', 'after_message_seq', 'after_event_seq', 'up_to_seq', 'seq', 'ack_seq', 'msg_seq', 'event_seq', 'limit', 'max_pages']) {
396
+ if (typeof params[key] === 'number' && Number.isSafeInteger(params[key]))
397
+ window[key] = params[key];
398
+ }
399
+ for (const key of ['window_mode', 'cursor_scope', 'sender_aid', 'group_id', 'group_aid', 'device_id', 'slot_id', 'after_volatile_cursor', 'mode', 'after_sender_aid']) {
400
+ if (typeof params[key] === 'string')
401
+ window[key] = params[key];
402
+ }
403
+ for (const key of ['force', 'pending_only'])
404
+ if (typeof params[key] === 'boolean')
405
+ window[key] = params[key];
406
+ for (const key of ['volatile_cursors', 'volatile_ids']) {
407
+ if (Array.isArray(params[key]))
408
+ window[key] = params[key].filter((item) => typeof item === 'string');
409
+ }
410
+ return window;
411
+ }
412
+ finishRpcResponse(span, value) {
413
+ const response = jsonObjectOf(value) ?? {};
414
+ const messages = Array.isArray(response.messages) ? response.messages : [];
415
+ span.response_ms = Date.now();
416
+ span.response_message_count = messages.length;
417
+ span.response_volatile_count = messages.filter((item) => jsonObjectOf(item)?.storage === 'volatile').length;
418
+ span.response_event_count = Array.isArray(response.events) ? response.events.length : 0;
419
+ const sequenceOf = (raw, ...keys) => {
420
+ for (const key of keys) {
421
+ const value = raw[key];
422
+ if (typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value))
423
+ return value;
424
+ }
425
+ return 0;
426
+ };
427
+ const itemKeys = new Map();
428
+ for (const key of ['messages', 'volatile_messages', 'volatile']) {
429
+ for (const item of Array.isArray(response[key]) ? response[key] : []) {
430
+ const raw = jsonObjectOf(item) ?? {}, cursor = textOf(raw.volatile_cursor);
431
+ const identity = { message_id: textOf(raw.message_id ?? raw.id),
432
+ seq: sequenceOf(raw, 'seq', 'message_seq', 'msg_seq'),
433
+ storage: typeof raw.storage === 'string' ? raw.storage : cursor ? 'volatile' : 'persistent', volatile_cursor: cursor };
434
+ itemKeys.set(JSON.stringify(identity), identity);
435
+ }
436
+ }
437
+ span.response_item_keys = [...itemKeys.values()];
438
+ span.response_event_keys = (Array.isArray(response.events) ? response.events : []).map((item) => {
439
+ const raw = jsonObjectOf(item) ?? {};
440
+ return { event_id: textOf(raw.event_id), event_seq: sequenceOf(raw, 'event_seq', 'seq') };
441
+ });
442
+ span.has_more = typeof response.has_more === 'boolean' ? response.has_more : null;
443
+ span.has_more_persistent = typeof response.persistent_has_more === 'boolean' ? response.persistent_has_more : null;
444
+ span.has_more_volatile = typeof response.volatile_has_more === 'boolean' ? response.volatile_has_more : null;
445
+ span.persistent_has_more = span.has_more_persistent;
446
+ span.volatile_has_more = span.has_more_volatile;
447
+ for (const key of ['peer_ack_seq', 'effective_ack_seq', 'server_ack_seq', 'ack_seq', 'up_to_seq', 'acked_seq']) {
448
+ if (typeof response[key] === 'number' && Number.isSafeInteger(response[key]))
449
+ span[key] = response[key];
450
+ }
451
+ const effective = response.peer_ack_seq ?? response.effective_ack_seq ?? response.ack_seq ?? response.up_to_seq ?? response.acked_seq;
452
+ if (typeof effective === 'number' && Number.isSafeInteger(effective))
453
+ span.effective_ack_seq = effective;
454
+ const volatileAck = jsonObjectOf(response.volatile_ack);
455
+ if (volatileAck) {
456
+ const ack = {};
457
+ for (const key of ['accepted', 'expired'])
458
+ if (typeof volatileAck[key] === 'number' && Number.isSafeInteger(volatileAck[key]))
459
+ ack[key] = volatileAck[key];
460
+ if (typeof volatileAck.storage === 'string')
461
+ ack.storage = volatileAck.storage;
462
+ span.volatile_ack = ack;
463
+ }
464
+ }
465
+ releasePullBarrier() {
466
+ const barrier = this.pullBarrier;
467
+ if (!barrier || barrier.released)
468
+ return;
469
+ barrier.released = true;
470
+ barrier.release();
471
+ }
472
+ injectRPCFailure(method, span) {
473
+ const failure = this.rpcFailures?.find((item) => !item.hit && item.method === method && item.sender_aid === span.sender_aid);
474
+ if (!failure)
475
+ return;
476
+ failure.hit = true;
477
+ span.injected_failure = failure.kind ?? 'error';
478
+ if (failure.kind === 'timeout')
479
+ throw new TimeoutError('cross-sdk 响应后一次超时');
480
+ throw new Error('cross-sdk 响应后一次失败');
481
+ }
482
+ resetObservationLedgers(pullBarrierCount = 0, scope = 'all', controls = {}) {
483
+ this.releasePullBarrier();
484
+ this.observationGeneration = (this.observationGeneration ?? 0) + 1;
485
+ this.eventLedger = [];
486
+ this.batchEvents = [];
487
+ this.singleEventCounts = { 'message.received': 0, 'group.message_created': 0, 'group.changed': 0 };
488
+ this.rpcSpans = [];
489
+ this.rpcFailures = Array.isArray(controls.rpc_failures) ? controls.rpc_failures.map((item) => ({ ...jsonObjectOf(item) })) : [];
490
+ this.pullBarrier = null;
491
+ if (Number.isSafeInteger(pullBarrierCount) && pullBarrierCount > 0) {
492
+ let release;
493
+ const promise = new Promise((resolve) => { release = resolve; });
494
+ this.pullBarrier = { target: pullBarrierCount, scope: scope === 'p2p' ? 'p2p' : 'all', admitted: 0, waiting: 0, released: false, promise, release,
495
+ sender: textOf(controls.pull_barrier_sender_aid), method: textOf(controls.pull_barrier_method), preserve: controls.preserve_pull_barrier === true };
496
+ }
497
+ }
498
+ async waitForPullBarrier(method, signal, timeout, span, owner) {
499
+ const barrier = this.pullBarrier;
500
+ if (!barrier || barrier.released || barrier.admitted >= barrier.target)
501
+ return;
502
+ if (barrier.scope === 'p2p' && method !== 'message.pull' && method !== 'message.v2.pull' && barrier.method !== method)
503
+ return;
504
+ if ((barrier.sender && barrier.sender !== span.sender_aid) || (barrier.method && barrier.method !== method))
505
+ return;
506
+ barrier.admitted += 1;
507
+ barrier.waiting += 1;
508
+ if (barrier.preserve) {
509
+ barrier.heldMethod = method;
510
+ barrier.owner = owner;
511
+ barrier.span = span;
512
+ }
513
+ let timer;
514
+ let abort;
515
+ try {
516
+ await Promise.race([barrier.promise, new Promise((_resolve, reject) => {
517
+ abort = () => reject(new Error('pull observation barrier cancelled'));
518
+ if (!barrier.preserve)
519
+ signal.addEventListener('abort', abort, { once: true });
520
+ if (signal.aborted && !barrier.preserve)
521
+ abort();
522
+ timer = setTimeout(() => reject(new Error('pull observation barrier timeout')), Math.max(1, timeout));
523
+ })]);
524
+ }
525
+ finally {
526
+ if (timer)
527
+ clearTimeout(timer);
528
+ signal.removeEventListener('abort', abort);
529
+ barrier.waiting -= 1;
530
+ }
214
531
  }
215
532
  storeRawPushObservation(rawEvent, scope, defaultKind, value) {
216
533
  const data = jsonObjectOf(value) ?? {};
@@ -299,6 +616,7 @@ export class CrossSdkTsAgent {
299
616
  };
300
617
  }
301
618
  async close() {
619
+ this.resetObservationLedgers();
302
620
  try {
303
621
  await this.client.close();
304
622
  }
@@ -489,6 +807,7 @@ export class CrossSdkTsAgent {
489
807
  decrypted,
490
808
  encrypted: Boolean(data.e2ee ?? data.encrypted),
491
809
  seq: Number(data.seq ?? data.message_seq ?? 0) || 0,
810
+ need_render: data.need_render !== false,
492
811
  ack_seq: Number(data.ack_seq ?? 0) || 0,
493
812
  storage: textOf(data.storage),
494
813
  volatile_cursor: textOf(data.volatile_cursor),
@@ -514,6 +833,7 @@ export class CrossSdkTsAgent {
514
833
  decrypted,
515
834
  encrypted: Boolean(data.e2ee ?? data.encrypted),
516
835
  seq: Number(data.seq ?? data.message_seq ?? data.msg_seq ?? 0) || 0,
836
+ need_render: data.need_render !== false,
517
837
  error_code: errorCode,
518
838
  raw_sha256: sha256Json(data),
519
839
  ...metadata,
@@ -543,6 +863,7 @@ export class CrossSdkTsAgent {
543
863
  }
544
864
  if (req.method === 'POST' && url.pathname === '/reset') {
545
865
  const body = await readJson(req);
866
+ this.resetObservationLedgers();
546
867
  const traceId = textOf(body.trace_id ?? '');
547
868
  if (traceId) {
548
869
  this.inbox = this.inbox.filter((item) => item.trace_id !== traceId);
@@ -553,6 +874,7 @@ export class CrossSdkTsAgent {
553
874
  else {
554
875
  this.inbox = [];
555
876
  this.groupInbox = [];
877
+ this.peersSnapshots = [];
556
878
  this.traces.clear();
557
879
  this.sendResults.clear();
558
880
  }
@@ -561,11 +883,25 @@ export class CrossSdkTsAgent {
561
883
  }
562
884
  if (req.method === 'POST' && url.pathname === '/test/raw-push/reset') {
563
885
  const body = await readJson(req);
886
+ if ((body.pull_barrier_count !== undefined && (typeof body.pull_barrier_count !== 'number' || !Number.isSafeInteger(body.pull_barrier_count) || body.pull_barrier_count < 0)) ||
887
+ (body.pull_barrier_scope !== undefined && !['all', 'p2p'].includes(body.pull_barrier_scope)) ||
888
+ (body.observation_id !== undefined && typeof body.observation_id !== 'string') ||
889
+ (body.release_pull_barrier !== undefined && typeof body.release_pull_barrier !== 'boolean')) {
890
+ sendJson(res, 400, { ok: false, error_code: 'bad_request', error_message: 'invalid observation parameters' });
891
+ return;
892
+ }
893
+ if (body.release_pull_barrier === true) {
894
+ this.releasePullBarrier();
895
+ sendJson(res, 200, { ok: true, observation_id: this.rawPushObservationId });
896
+ return;
897
+ }
898
+ this.resetObservationLedgers(body.pull_barrier_count, textOf(body.pull_barrier_scope), body);
564
899
  this.rawPushObservations = [];
565
900
  this.rawPushRpcMethods = [];
566
901
  this.groupChangedEvents = [];
567
902
  this.groupInviteEvents = [];
568
903
  this.readStateEvents = [];
904
+ this.peersSnapshots = [];
569
905
  this.rawPushObservationId = textOf(body.observation_id ?? '');
570
906
  sendJson(res, 200, { ok: true, observation_id: this.rawPushObservationId });
571
907
  return;
@@ -584,6 +920,23 @@ export class CrossSdkTsAgent {
584
920
  let rpcEntries = [...this.rawPushRpcMethods];
585
921
  let groupChangedEvents = [...this.groupChangedEvents];
586
922
  let groupInviteEvents = [...this.groupInviteEvents];
923
+ let eventLedger = [...this.eventLedger ?? []];
924
+ let rpcSpans = [...this.rpcSpans ?? []];
925
+ let batchEvents = [...this.batchEvents ?? []];
926
+ if (observationId)
927
+ batchEvents = batchEvents.filter((item) => textOf(item.observation_id) === observationId);
928
+ if (observationId)
929
+ eventLedger = eventLedger.filter((item) => textOf(item.observation_id) === observationId);
930
+ if (observationId)
931
+ rpcSpans = rpcSpans.filter((item) => textOf(item.observation_id) === observationId);
932
+ if (filters.scope)
933
+ eventLedger = eventLedger.filter((item) => textOf(item.scope) === filters.scope);
934
+ if (filters.scope)
935
+ rpcSpans = rpcSpans.filter((item) => textOf(item.method).startsWith(filters.scope === 'p2p' ? 'message.' : 'group.'));
936
+ if (filters.group_id)
937
+ eventLedger = eventLedger.filter((item) => textOf(item.group_id) === filters.group_id);
938
+ if (filters.group_id)
939
+ rpcSpans = rpcSpans.filter((item) => textOf(item.group_id) === filters.group_id);
587
940
  if (observationId)
588
941
  rpcEntries = rpcEntries.filter((item) => textOf(item.observation_id) === observationId);
589
942
  if (observationId)
@@ -600,11 +953,18 @@ export class CrossSdkTsAgent {
600
953
  for (const method of rpcMethods)
601
954
  rpcMethodCounts[method] = Number(rpcMethodCounts[method] ?? 0) + 1;
602
955
  sendJson(res, 200, {
603
- ok: true, items, count: items.length, rpc_methods: rpcMethods, rpc_method_counts: rpcMethodCounts,
956
+ ok: true, time_ms: Date.now(), items, count: items.length, rpc_methods: rpcMethods, rpc_method_counts: rpcMethodCounts,
604
957
  rpc_calls: rpcEntries,
605
958
  group_changed_events: groupChangedEvents.slice(-limit),
606
959
  group_invite_events: groupInviteEvents.slice(-limit),
607
960
  read_state_events: this.readStateEvents.slice(-limit),
961
+ peers_snapshots: this.peersSnapshots.slice(-limit),
962
+ event_ledger: eventLedger,
963
+ rpc_spans: rpcSpans,
964
+ batch_event_mode: this.batchEventMode ?? false,
965
+ batch_events: batchEvents,
966
+ single_event_counts: this.singleEventCounts ?? { 'message.received': 0, 'group.message_created': 0, 'group.changed': 0 },
967
+ pull_barrier: { target: this.pullBarrier?.target ?? 0, waiting: this.pullBarrier?.waiting ?? 0, released: this.pullBarrier?.released ?? true, scope: this.pullBarrier?.scope ?? 'all' },
608
968
  });
609
969
  return;
610
970
  }
@@ -896,6 +1256,8 @@ export class CrossSdkTsAgent {
896
1256
  };
897
1257
  if (body.ttl !== undefined)
898
1258
  params.ttl = Number(body.ttl);
1259
+ if (body.need_render !== undefined)
1260
+ params.need_render = body.need_render;
899
1261
  try {
900
1262
  const result = await this.client.call('message.send', params);
901
1263
  const resultObj = result && typeof result === 'object' ? result : {};
@@ -931,6 +1293,10 @@ export class CrossSdkTsAgent {
931
1293
  const params = {};
932
1294
  if (seq > 0)
933
1295
  params.seq = seq;
1296
+ if (textOf(body.sender_aid).trim())
1297
+ params.sender_aid = textOf(body.sender_aid).trim();
1298
+ if (body.cursor_scope !== undefined)
1299
+ params.cursor_scope = body.cursor_scope;
934
1300
  if (Array.isArray(body.volatile_cursors))
935
1301
  params.volatile_cursors = body.volatile_cursors;
936
1302
  else if (textOf(body.volatile_cursor))
@@ -952,9 +1318,26 @@ export class CrossSdkTsAgent {
952
1318
  }
953
1319
  async pull(req, res) {
954
1320
  const body = await readJson(req);
955
- const afterSeq = Number(body.after_seq ?? 0) || 0;
956
- const limit = Number(body.limit ?? 50) || 50;
957
- const params = { after_seq: afterSeq, limit };
1321
+ if ((body.limit !== undefined && (typeof body.limit !== 'number' || !Number.isSafeInteger(body.limit) || body.limit < 1 || body.limit > 200)) ||
1322
+ (body.max_pages !== undefined && (typeof body.max_pages !== 'number' || !Number.isSafeInteger(body.max_pages) || body.max_pages < 1)) ||
1323
+ (body.after_seq !== undefined && (typeof body.after_seq !== 'number' || !Number.isSafeInteger(body.after_seq) || body.after_seq < 0)) ||
1324
+ ['sender_aid', 'cursor_scope', 'after_volatile_cursor'].some((key) => body[key] !== undefined && typeof body[key] !== 'string') ||
1325
+ (body.force !== undefined && typeof body.force !== 'boolean') ||
1326
+ (body.window_mode !== undefined && !['forward', 'tail'].includes(body.window_mode))) {
1327
+ sendJson(res, 400, { ok: false, error_code: 'bad_request', error_message: 'invalid pull parameters' });
1328
+ return;
1329
+ }
1330
+ const senderAid = textOf(body.sender_aid).trim();
1331
+ const limit = body.limit ?? 50;
1332
+ const params = { limit };
1333
+ if (senderAid)
1334
+ params.sender_aid = senderAid;
1335
+ if (body.after_seq !== undefined)
1336
+ params.after_seq = body.after_seq;
1337
+ for (const key of ['cursor_scope', 'force', 'window_mode', 'max_pages']) {
1338
+ if (body[key] !== undefined)
1339
+ params[key] = body[key];
1340
+ }
958
1341
  if (textOf(body.after_volatile_cursor))
959
1342
  params.after_volatile_cursor = textOf(body.after_volatile_cursor);
960
1343
  try {
@@ -976,6 +1359,20 @@ export class CrossSdkTsAgent {
976
1359
  }
977
1360
  async messageCall(req, res) {
978
1361
  const body = await readJson(req);
1362
+ if (['start', 'status', 'detach'].includes(textOf(body.action))) {
1363
+ this.messageOperations ??= new Map();
1364
+ const id = textOf(body.operation_id);
1365
+ if (body.action === 'start') {
1366
+ const operation = { ok: true, operation_id: id, status: 'running', started_ms: Date.now() };
1367
+ this.messageOperations.set(id, operation);
1368
+ Promise.resolve().then(() => { operation.entered_ms = Date.now(); return this.client.call(textOf(body.method), (jsonObjectOf(body.params) ?? {})); }).then((result) => Object.assign(operation, { status: 'completed', end_ms: Date.now(), result: jsonSafe(result) }), (error) => Object.assign(operation, { status: 'failed', end_ms: Date.now(), error_code: error instanceof Error ? error.name : 'Error', error_message: error instanceof Error ? error.message : String(error) }));
1369
+ }
1370
+ const operation = this.messageOperations.get(id);
1371
+ if (body.action === 'detach')
1372
+ Object.assign(operation, { detached: true, cancellation: 'application_wait_only', detached_ms: Date.now() });
1373
+ sendJson(res, 200, operation);
1374
+ return;
1375
+ }
979
1376
  const traceId = textOf(body.trace_id || crypto.randomUUID().replace(/-/g, ''));
980
1377
  const method = textOf(body.method).trim();
981
1378
  const params = jsonObjectOf(body.params) ?? {};
@@ -1007,15 +1404,15 @@ export class CrossSdkTsAgent {
1007
1404
  sendJson(res, 500, error);
1008
1405
  }
1009
1406
  }
1010
- syncNamespace(scope, groupId = '') {
1407
+ syncNamespace(scope, groupId = '', senderAid = '') {
1011
1408
  if (scope === 'p2p')
1012
- return `p2p:${this.aid}`;
1409
+ return senderAid ? `p2p.peer.v1/${Buffer.from(senderAid, 'utf8').toString('base64url')}` : `p2p:${this.aid}`;
1013
1410
  if (scope === 'group' && groupId)
1014
1411
  return `group:${groupId}`;
1015
1412
  throw new Error('scope must be p2p, or group with group_id');
1016
1413
  }
1017
- syncStateValue(scope, groupId = '') {
1018
- const namespace = this.syncNamespace(scope, groupId);
1414
+ syncStateValue(scope, groupId = '', senderAid = '') {
1415
+ const namespace = this.syncNamespace(scope, groupId, senderAid);
1019
1416
  const tracker = this.client._seqTracker;
1020
1417
  if (!tracker)
1021
1418
  throw new Error('SeqTracker is unavailable');
@@ -1024,9 +1421,10 @@ export class CrossSdkTsAgent {
1024
1421
  syncState(url, res) {
1025
1422
  const scope = textOf(url.searchParams.get('scope') || 'p2p').trim().toLowerCase();
1026
1423
  const groupId = textOf(url.searchParams.get('group_id') || '').trim();
1424
+ const senderAid = textOf(url.searchParams.get('sender_aid') || '').trim();
1027
1425
  try {
1028
- const snapshot = this.syncStateValue(scope, groupId);
1029
- sendJson(res, 200, { ok: true, scope, ...snapshot });
1426
+ const snapshot = this.syncStateValue(scope, groupId, senderAid);
1427
+ sendJson(res, 200, { ok: true, scope, ...snapshot, peers_snapshots: this.peersSnapshots });
1030
1428
  }
1031
1429
  catch (err) {
1032
1430
  sendJson(res, 400, {
@@ -1046,27 +1444,24 @@ export class CrossSdkTsAgent {
1046
1444
  const body = await readJson(req);
1047
1445
  const scope = textOf(body.scope || 'p2p').trim().toLowerCase();
1048
1446
  const groupId = textOf(body.group_id || '').trim();
1447
+ const senderAid = textOf(body.sender_aid || '').trim();
1049
1448
  const afterSeq = Number(body.after_seq ?? 0) || 0;
1050
1449
  const limit = Number(body.limit ?? 50) || 50;
1051
1450
  try {
1052
- const params = { after_seq: afterSeq, limit };
1053
- if (tail)
1054
- params.window_mode = 'tail';
1451
+ const params = { limit, window_mode: tail ? 'tail' : 'forward' };
1452
+ if (scope === 'group' || body.after_seq !== undefined)
1453
+ params.after_seq = afterSeq;
1454
+ if (scope === 'p2p' && senderAid)
1455
+ params.sender_aid = senderAid;
1456
+ for (const key of ['cursor_scope', 'force', 'max_pages', 'after_volatile_cursor']) {
1457
+ if (body[key] !== undefined)
1458
+ params[key] = body[key];
1459
+ }
1055
1460
  const method = scope === 'group' ? 'group.pull' : 'message.pull';
1056
1461
  if (scope === 'group')
1057
1462
  params.group_id = groupId;
1058
- this.syncNamespace(scope, groupId);
1059
- let result;
1060
- if (tail) {
1061
- result = await this.client.call(method, params);
1062
- }
1063
- else {
1064
- const transport = this.client._transport;
1065
- if (!transport?.call)
1066
- throw new Error('raw transport is unavailable');
1067
- const wireMethod = scope === 'group' ? 'group.v2.pull' : 'message.v2.pull';
1068
- result = await transport.call(wireMethod, params);
1069
- }
1463
+ this.syncNamespace(scope, groupId, senderAid);
1464
+ const result = await this.client.call(method, params);
1070
1465
  const resultObj = jsonObjectOf(result) ?? {};
1071
1466
  const messages = Array.isArray(resultObj.messages) ? resultObj.messages : [];
1072
1467
  if (tail) {
@@ -1077,7 +1472,7 @@ export class CrossSdkTsAgent {
1077
1472
  await this.storeInboxItem(this.normalizeMessage(msg, true));
1078
1473
  }
1079
1474
  }
1080
- const snapshot = this.syncStateValue(scope, groupId);
1475
+ const snapshot = this.syncStateValue(scope, groupId, senderAid);
1081
1476
  sendJson(res, 200, { ok: true, scope, ...snapshot, result: jsonSafe(result) });
1082
1477
  }
1083
1478
  catch (err) {
@@ -1093,6 +1488,7 @@ export class CrossSdkTsAgent {
1093
1488
  const body = await readJson(req);
1094
1489
  const scope = textOf(body.scope || 'p2p').trim().toLowerCase();
1095
1490
  const groupId = textOf(body.group_id || '').trim();
1491
+ const senderAid = textOf(body.sender_aid || '').trim();
1096
1492
  const beforeSeq = Number(body.before_seq ?? 0) || 0;
1097
1493
  const limit = Number(body.limit ?? 50) || 50;
1098
1494
  try {
@@ -1100,9 +1496,13 @@ export class CrossSdkTsAgent {
1100
1496
  const method = scope === 'group' ? 'group.history' : 'message.history';
1101
1497
  if (scope === 'group')
1102
1498
  params.group_id = groupId;
1103
- this.syncNamespace(scope, groupId);
1499
+ if (scope === 'p2p' && senderAid)
1500
+ params.sender_aid = senderAid;
1501
+ if (body.cursor_scope !== undefined)
1502
+ params.cursor_scope = body.cursor_scope;
1503
+ this.syncNamespace(scope, groupId, senderAid);
1104
1504
  const result = await this.client.call(method, params);
1105
- const snapshot = this.syncStateValue(scope, groupId);
1505
+ const snapshot = this.syncStateValue(scope, groupId, senderAid);
1106
1506
  sendJson(res, 200, { ok: true, scope, ...snapshot, result: jsonSafe(result) });
1107
1507
  }
1108
1508
  catch (err) {
@@ -1117,7 +1517,25 @@ export class CrossSdkTsAgent {
1117
1517
  async sessionDisconnect(res) {
1118
1518
  try {
1119
1519
  this.ready = false;
1120
- await this.client.disconnect();
1520
+ const barrier = this.pullBarrier;
1521
+ let restore;
1522
+ if (barrier?.preserve && barrier.waiting === 1 && !barrier.released && ['message.pull', 'message.v2.pull'].includes(barrier.heldMethod ?? '')) {
1523
+ const binding = barrier.owner, pipeline = this.client._rpcPipeline;
1524
+ const owners = [...(pipeline?.pullGateStates.values() ?? [])].flatMap((state) => [...state.activeByNamespace.values()])
1525
+ .filter((job) => job === binding?.job && job.key === binding.key && job.namespace === binding.namespace && job.running && !job.timedOut && !job.invalidated && job.cancel === binding.cancel);
1526
+ if (owners.length !== 1)
1527
+ throw new Error('保留响应无法定位唯一受跟踪的 pull owner');
1528
+ const owner = owners[0], settled = owner.settled;
1529
+ owner.settled = Promise.resolve();
1530
+ restore = () => { owner.settled = settled; };
1531
+ barrier.span.barrier_owner_isolated = true;
1532
+ }
1533
+ try {
1534
+ await this.client.disconnect();
1535
+ }
1536
+ finally {
1537
+ restore?.();
1538
+ }
1121
1539
  sendJson(res, 200, { ok: true, state: textOf(this.client.state) });
1122
1540
  }
1123
1541
  catch (err) {
@@ -1130,14 +1548,19 @@ export class CrossSdkTsAgent {
1130
1548
  }
1131
1549
  async sessionConnect(req, res) {
1132
1550
  const body = await readJson(req);
1551
+ if (['background_sync', 'inline_realtime_payload_v1', 'batch_events'].some((key) => body[key] !== undefined && typeof body[key] !== 'boolean')) {
1552
+ sendJson(res, 400, { ok: false, error_code: 'bad_request', error_message: 'invalid session parameters' });
1553
+ return;
1554
+ }
1133
1555
  const backgroundSync = body.background_sync === true;
1134
1556
  if (Object.prototype.hasOwnProperty.call(body, 'inline_realtime_payload_v1')) {
1135
1557
  this.inlineRealtimePayloadV1 = body.inline_realtime_payload_v1 !== false;
1136
1558
  }
1137
1559
  try {
1560
+ this.setBatchEventMode(typeof body.batch_events === 'boolean' ? body.batch_events : this.batchEventMode ?? false);
1138
1561
  await this.client.connect({ auto_reconnect: false, background_sync: backgroundSync });
1139
1562
  this.ready = true;
1140
- sendJson(res, 200, { ok: true, background_sync: backgroundSync, inline_realtime_payload_v1: this.inlineRealtimePayloadV1 });
1563
+ sendJson(res, 200, { ok: true, background_sync: backgroundSync, inline_realtime_payload_v1: this.inlineRealtimePayloadV1, batch_events: this.batchEventMode });
1141
1564
  }
1142
1565
  catch (err) {
1143
1566
  sendJson(res, 500, {
@@ -1149,19 +1572,25 @@ export class CrossSdkTsAgent {
1149
1572
  }
1150
1573
  async sessionRestart(req, res) {
1151
1574
  const body = await readJson(req);
1575
+ if (['background_sync', 'inline_realtime_payload_v1', 'batch_events'].some((key) => body[key] !== undefined && typeof body[key] !== 'boolean')) {
1576
+ sendJson(res, 400, { ok: false, error_code: 'bad_request', error_message: 'invalid session parameters' });
1577
+ return;
1578
+ }
1152
1579
  const backgroundSync = body.background_sync === true;
1153
1580
  if (Object.prototype.hasOwnProperty.call(body, 'inline_realtime_payload_v1')) {
1154
1581
  this.inlineRealtimePayloadV1 = body.inline_realtime_payload_v1 !== false;
1155
1582
  }
1156
1583
  this.ready = false;
1157
1584
  try {
1585
+ if (typeof body.batch_events === 'boolean')
1586
+ this.batchEventMode = body.batch_events;
1158
1587
  await this.client.close();
1159
1588
  this.client = this.newClient();
1160
1589
  this.bindClientEvents();
1161
1590
  await this.ensureConnected(backgroundSync);
1162
1591
  this.startupError = '';
1163
1592
  this.ready = true;
1164
- sendJson(res, 200, { ok: true, background_sync: backgroundSync, inline_realtime_payload_v1: this.inlineRealtimePayloadV1 });
1593
+ sendJson(res, 200, { ok: true, background_sync: backgroundSync, inline_realtime_payload_v1: this.inlineRealtimePayloadV1, batch_events: this.batchEventMode });
1165
1594
  }
1166
1595
  catch (err) {
1167
1596
  this.startupError = `${err instanceof Error ? err.name : 'Error'}: ${err instanceof Error ? err.message : String(err)}`;
@@ -2053,6 +2482,7 @@ export class CrossSdkTsAgent {
2053
2482
  payload,
2054
2483
  encrypt: e2ee,
2055
2484
  message_id: messageId,
2485
+ ...(body.need_render !== undefined ? { need_render: body.need_render } : {}),
2056
2486
  });
2057
2487
  const resultObj = result && typeof result === 'object' && !Array.isArray(result) ? result : {};
2058
2488
  const response = {