@agentunion/fastaun 0.5.3 → 0.5.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.
Files changed (133) hide show
  1. package/CHANGELOG.md +126 -52
  2. package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/216/245/345/205/245/344/270/216/346/274/224/347/244/272/346/214/207/345/215/227.md +290 -0
  3. package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/234/215/345/212/241/346/236/266/346/236/204/344/270/216/350/257/246/347/273/206/345/256/236/347/216/260/350/256/241/345/210/222-codex.md +994 -0
  4. package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/234/215/345/212/241/350/277/220/347/273/264/344/270/216/345/217/221/345/270/203/346/214/207/345/215/227.md +144 -0
  5. package/_packed_docs/CHANGELOG.md +126 -52
  6. package/_packed_docs/INDEX.md +127 -69
  7. package/_packed_docs/KITE_DOCS_GUIDE.md +63 -29
  8. package/_packed_docs/audit/AUN/346/234/215/345/212/241Go/345/214/226/351/207/215/346/236/204/347/262/276/347/273/206/345/214/226/346/272/220/347/240/201/345/256/241/346/237/245-20260718.md +366 -0
  9. package/_packed_docs/aun/345/205/254/347/275/221/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +227 -0
  10. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +1004 -0
  11. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +999 -0
  12. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +6 -4
  13. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +58 -20
  14. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +219 -247
  15. package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +14 -14
  16. package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +3 -3
  17. package/_packed_docs/protocol/15-/347/246/273/347/272/277/346/216/250/351/200/201/351/200/232/347/237/245/345/215/217/350/256/256.md +165 -421
  18. package/_packed_docs/protocol/README.md +1 -0
  19. package/_packed_docs/protocol/aun-docs-guide.md +7 -4
  20. package/_packed_docs/protocol/index.md +25 -19
  21. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +62 -17
  22. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -0
  23. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -78
  24. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +6 -2
  25. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +87 -41
  26. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +18 -5
  27. package/_packed_docs/sdk/09-group-rpc-manual.md +307 -267
  28. package/_packed_docs/sdk/09-message-rpc-manual.md +142 -103
  29. package/_packed_docs/sdk/09-payload-reference.md +1 -1
  30. package/_packed_docs/sdk/09-stream-rpc-manual.md +8 -8
  31. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +26 -17
  32. package/_packed_docs/sdk/INDEX.md +42 -34
  33. package/_packed_docs/sdk/README.md +7 -6
  34. package/_packed_docs//346/266/210/346/201/257/345/220/214/346/255/245/344/270/216/345/216/206/345/217/262/346/213/211/345/217/226/344/273/243/347/240/201/345/256/241/346/237/245/351/227/256/351/242/230/346/270/205/345/215/225.md +229 -0
  35. package/_packed_docs//346/266/210/346/201/257/345/220/214/346/255/245/344/270/216/345/216/206/345/217/262/346/213/211/345/217/226/346/224/271/351/200/240/346/226/271/346/241/210.md +750 -0
  36. package/dist/agent-md.js +8 -2
  37. package/dist/agent-md.js.map +1 -1
  38. package/dist/aid-store.d.ts +2 -0
  39. package/dist/aid-store.js +43 -7
  40. package/dist/aid-store.js.map +1 -1
  41. package/dist/auth.d.ts +0 -2
  42. package/dist/auth.js +97 -50
  43. package/dist/auth.js.map +1 -1
  44. package/dist/client/delivery.d.ts +70 -7
  45. package/dist/client/delivery.js +1334 -236
  46. package/dist/client/delivery.js.map +1 -1
  47. package/dist/client/group-state.js +25 -22
  48. package/dist/client/group-state.js.map +1 -1
  49. package/dist/client/lifecycle.js +46 -3
  50. package/dist/client/lifecycle.js.map +1 -1
  51. package/dist/client/mention-mode.d.ts +6 -0
  52. package/dist/client/mention-mode.js +184 -0
  53. package/dist/client/mention-mode.js.map +1 -0
  54. package/dist/client/peers.d.ts +1 -1
  55. package/dist/client/peers.js +26 -3
  56. package/dist/client/peers.js.map +1 -1
  57. package/dist/client/rpc-pipeline.d.ts +20 -1
  58. package/dist/client/rpc-pipeline.js +332 -73
  59. package/dist/client/rpc-pipeline.js.map +1 -1
  60. package/dist/client/v2-e2ee.d.ts +20 -1
  61. package/dist/client/v2-e2ee.js +625 -105
  62. package/dist/client/v2-e2ee.js.map +1 -1
  63. package/dist/client.d.ts +17 -5
  64. package/dist/client.js +332 -167
  65. package/dist/client.js.map +1 -1
  66. package/dist/errors.js +4 -1
  67. package/dist/errors.js.map +1 -1
  68. package/dist/facades.d.ts +6 -0
  69. package/dist/facades.js +196 -80
  70. package/dist/facades.js.map +1 -1
  71. package/dist/group-index.d.ts +5 -0
  72. package/dist/group-index.js +65 -30
  73. package/dist/group-index.js.map +1 -1
  74. package/dist/keystore/aid-db.d.ts +12 -0
  75. package/dist/keystore/aid-db.js +68 -4
  76. package/dist/keystore/aid-db.js.map +1 -1
  77. package/dist/keystore/index.d.ts +12 -0
  78. package/dist/keystore/local-identity-store.d.ts +10 -0
  79. package/dist/keystore/local-identity-store.js +8 -2
  80. package/dist/keystore/local-identity-store.js.map +1 -1
  81. package/dist/keystore/local-token-store.d.ts +10 -0
  82. package/dist/keystore/local-token-store.js +8 -2
  83. package/dist/keystore/local-token-store.js.map +1 -1
  84. package/dist/register-flow.d.ts +0 -1
  85. package/dist/register-flow.js +96 -38
  86. package/dist/register-flow.js.map +1 -1
  87. package/dist/secret-store/file-store.d.ts +3 -14
  88. package/dist/secret-store/file-store.js +7 -98
  89. package/dist/secret-store/file-store.js.map +1 -1
  90. package/dist/secret-store/index.d.ts +1 -4
  91. package/dist/secret-store/index.js +2 -2
  92. package/dist/secret-store/index.js.map +1 -1
  93. package/dist/seq-tracker.d.ts +29 -9
  94. package/dist/seq-tracker.js +233 -64
  95. package/dist/seq-tracker.js.map +1 -1
  96. package/dist/storage/vfs.d.ts +1 -0
  97. package/dist/storage/vfs.js +28 -4
  98. package/dist/storage/vfs.js.map +1 -1
  99. package/dist/tools/cross-sdk-agent.js +360 -8
  100. package/dist/tools/cross-sdk-agent.js.map +1 -1
  101. package/dist/transport.d.ts +19 -0
  102. package/dist/transport.js +197 -14
  103. package/dist/transport.js.map +1 -1
  104. package/dist/v2/e2ee/decrypt.js +26 -23
  105. package/dist/v2/e2ee/decrypt.js.map +1 -1
  106. package/dist/v2/session/session.js +3 -4
  107. package/dist/v2/session/session.js.map +1 -1
  108. package/dist/v2/state/commitment.js +1 -2
  109. package/dist/v2/state/commitment.js.map +1 -1
  110. package/dist/version.d.ts +1 -1
  111. package/dist/version.js +1 -1
  112. package/package.json +5 -4
  113. package/dist/e2ee-group.d.ts +0 -249
  114. package/dist/e2ee-group.js +0 -1567
  115. package/dist/e2ee-group.js.map +0 -1
  116. package/dist/group-resources.d.ts +0 -31
  117. package/dist/group-resources.js +0 -84
  118. package/dist/group-resources.js.map +0 -1
  119. package/dist/keystore/file.d.ts +0 -109
  120. package/dist/keystore/file.js +0 -772
  121. package/dist/keystore/file.js.map +0 -1
  122. package/dist/keystore/sqlite-backup.d.ts +0 -44
  123. package/dist/keystore/sqlite-backup.js +0 -382
  124. package/dist/keystore/sqlite-backup.js.map +0 -1
  125. package/dist/namespaces/auth.d.ts +0 -118
  126. package/dist/namespaces/auth.js +0 -968
  127. package/dist/namespaces/auth.js.map +0 -1
  128. package/dist/namespaces/custody.d.ts +0 -48
  129. package/dist/namespaces/custody.js +0 -313
  130. package/dist/namespaces/custody.js.map +0 -1
  131. package/dist/namespaces/meta.d.ts +0 -76
  132. package/dist/namespaces/meta.js +0 -490
  133. package/dist/namespaces/meta.js.map +0 -1
@@ -1,12 +1,114 @@
1
1
  import * as crypto from 'node:crypto';
2
- import { normalizeGroupId } from '../group-id.js';
3
2
  import { E2EEError, StateError, ValidationError } from '../errors.js';
4
3
  import { decryptMessage, encryptGroupMessage, encryptP2PMessage, } from '../v2/e2ee/index.js';
5
4
  import { V2Session } from '../v2/session/index.js';
6
5
  import { isJsonObject } from '../types.js';
6
+ import { normalizeGroupMentionMode } from './mention-mode.js';
7
7
  const V2_BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
8
8
  const V2_RETRYABLE_CODES = new Set([-33011, -33012, -33050, -33052, -33054]);
9
9
  const V2_GROUP_STALE_BOOTSTRAP_CODE = -33054;
10
+ const V2_PULL_DEFAULT_PAGE_LIMIT = 50;
11
+ const V2_PULL_MAX_PAGE_LIMIT = 50;
12
+ const V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS = 3_000;
13
+ function strictWindowSeq(value, field, positive = false) {
14
+ if (!Number.isSafeInteger(value))
15
+ throw new ValidationError(`${field} must be an integer`);
16
+ const minimum = positive ? 1 : 0;
17
+ if (value < minimum)
18
+ throw new ValidationError(`${field} must be >= ${minimum}`);
19
+ return value;
20
+ }
21
+ function strictWindowLimit(value) {
22
+ const limit = strictWindowSeq(value, 'limit', true);
23
+ if (limit > V2_PULL_MAX_PAGE_LIMIT)
24
+ throw new ValidationError(`limit must be <= ${V2_PULL_MAX_PAGE_LIMIT}`);
25
+ return limit;
26
+ }
27
+ function deduplicateWindowMessages(value, lowerSeq, upperSeq, boundary) {
28
+ if (!Array.isArray(value))
29
+ throw new ValidationError('messages must be an array');
30
+ const bySeq = new Map();
31
+ for (const raw of value) {
32
+ if (!isJsonObject(raw))
33
+ throw new ValidationError('messages entries must be objects');
34
+ const row = raw;
35
+ const seq = strictWindowSeq(row.seq, 'messages[].seq', true);
36
+ if (seq < lowerSeq || seq > upperSeq)
37
+ throw new ValidationError(`messages[].seq is outside ${boundary}`);
38
+ const previous = bySeq.get(seq);
39
+ if (previous && JSON.stringify(previous) !== JSON.stringify(row))
40
+ throw new ValidationError('messages contains conflicting duplicate seq');
41
+ bySeq.set(seq, row);
42
+ }
43
+ return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
44
+ }
45
+ function deduplicateForwardPageMessages(value, afterSeq) {
46
+ if (!Array.isArray(value))
47
+ return [];
48
+ const bySeq = new Map();
49
+ for (const raw of value) {
50
+ if (!isJsonObject(raw))
51
+ continue;
52
+ const row = raw;
53
+ const seq = Number(row.seq ?? 0);
54
+ if (!Number.isSafeInteger(seq) || seq <= afterSeq || bySeq.has(seq))
55
+ continue;
56
+ bySeq.set(seq, row);
57
+ }
58
+ return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
59
+ }
60
+ function validateTailPage(result, pageLimit) {
61
+ if (!isJsonObject(result))
62
+ throw new ValidationError('tail response must be an object');
63
+ const row = result;
64
+ if (row.window_mode !== 'tail')
65
+ throw new ValidationError('tail response window_mode must equal tail');
66
+ const windowStart = strictWindowSeq(row.window_start_seq, 'window_start_seq');
67
+ const head = strictWindowSeq(row.head_seq, 'head_seq');
68
+ const covered = strictWindowSeq(row.covered_through_seq, 'covered_through_seq');
69
+ if (windowStart > head)
70
+ throw new ValidationError('window_start_seq must not exceed head_seq');
71
+ if (covered !== head)
72
+ throw new ValidationError('covered_through_seq must equal head_seq');
73
+ if (head - windowStart + 1 > pageLimit)
74
+ throw new ValidationError('tail proven window exceeds limit');
75
+ return { windowStart, head, messages: deduplicateWindowMessages(row.messages, windowStart, head, 'the proven tail window') };
76
+ }
77
+ function validateHistoryPage(result, beforeSeq) {
78
+ if (!isJsonObject(result))
79
+ throw new ValidationError('history response must be an object');
80
+ const row = result;
81
+ const rawStart = row.window_start_seq;
82
+ const rawNext = row.next_before_seq;
83
+ if ((rawStart === undefined) !== (rawNext === undefined))
84
+ throw new ValidationError('history pagination boundaries must both be present or absent');
85
+ let lowerSeq = 1;
86
+ let windowStart;
87
+ if (rawStart !== undefined) {
88
+ windowStart = strictWindowSeq(rawStart, 'window_start_seq', true);
89
+ const nextBefore = strictWindowSeq(rawNext, 'next_before_seq', true);
90
+ if (windowStart !== nextBefore)
91
+ throw new ValidationError('next_before_seq must equal window_start_seq');
92
+ if (windowStart >= beforeSeq)
93
+ throw new ValidationError('history boundary must be less than before_seq');
94
+ lowerSeq = windowStart;
95
+ }
96
+ const messages = deduplicateWindowMessages(row.messages, lowerSeq, beforeSeq - 1, 'the history page');
97
+ if (messages.length > 0) {
98
+ if (windowStart === undefined)
99
+ throw new ValidationError('history messages require pagination boundaries');
100
+ if (Number(messages[0].seq) !== windowStart)
101
+ throw new ValidationError('window_start_seq must equal the first history message seq');
102
+ }
103
+ else if (windowStart !== undefined) {
104
+ throw new ValidationError('empty history page must not include pagination boundaries');
105
+ }
106
+ if (typeof row.has_older !== 'boolean')
107
+ throw new ValidationError('has_older must be a boolean');
108
+ if (messages.length === 0 && row.has_older)
109
+ throw new ValidationError('empty history page cannot report has_older');
110
+ return messages;
111
+ }
10
112
  const MEMBERSHIP_ACTIONS = new Set([
11
113
  'member_added',
12
114
  'member_left',
@@ -121,6 +223,12 @@ function v2WrapCapabilities() {
121
223
  per_device_wrap: true,
122
224
  };
123
225
  }
226
+ function withExplicitGroupAid(params, groupAid) {
227
+ const aid = String(groupAid ?? '').trim();
228
+ if (aid)
229
+ params.group_aid = aid;
230
+ return params;
231
+ }
124
232
  function applyV2WrapPolicyToTargets(targets, policy) {
125
233
  if (!policy.explicit)
126
234
  return targets;
@@ -148,6 +256,7 @@ export class V2E2EECoordinator {
148
256
  runtime;
149
257
  _initSessionPromise = null;
150
258
  targetSetCache = new Map();
259
+ senderIKSyncFetches = new Map();
151
260
  constructor(runtime) {
152
261
  this.runtime = runtime;
153
262
  }
@@ -167,6 +276,18 @@ export class V2E2EECoordinator {
167
276
  setBootstrapCacheEntry(key, entry) {
168
277
  this.bootstrapCache.set(key, entry);
169
278
  }
279
+ cacheBundledSelfDevices(bootstrap, wrapPolicy) {
280
+ const client = this.client;
281
+ if (!client._aid || !Object.prototype.hasOwnProperty.call(bootstrap, 'self_devices'))
282
+ return;
283
+ const selfDevices = (Array.isArray(bootstrap.self_devices) ? bootstrap.self_devices : []);
284
+ this.setBootstrapCacheEntry(client._aid, {
285
+ devices: selfDevices,
286
+ auditRecipients: [],
287
+ cachedAt: Date.now(),
288
+ wrapPolicy,
289
+ });
290
+ }
170
291
  deleteBootstrapCacheEntry(key) {
171
292
  this.bootstrapCache.delete(key);
172
293
  this.targetSetCache.delete(key);
@@ -375,6 +496,84 @@ export class V2E2EECoordinator {
375
496
  pendingSenderIKFetchKey(fromAid, senderDeviceId, groupId) {
376
497
  return `${fromAid}#${senderDeviceId}#${groupId || ''}`;
377
498
  }
499
+ async fetchSenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint = '') {
500
+ const client = this.client;
501
+ const peerAid = String(fromAid ?? '').trim();
502
+ const deviceId = String(senderDeviceId ?? '').trim();
503
+ const gid = String(groupId ?? '').trim();
504
+ const fingerprint = String(certFingerprint ?? '').trim().toLowerCase();
505
+ if (!peerAid)
506
+ return null;
507
+ const fetchKey = [peerAid, deviceId, gid, fingerprint].join('#');
508
+ const existing = this.senderIKSyncFetches.get(fetchKey);
509
+ if (existing)
510
+ return await existing;
511
+ const task = (async () => {
512
+ const started = Date.now();
513
+ let timer;
514
+ const timedOut = Symbol('sender-ik-timeout');
515
+ const fetch = async () => {
516
+ try {
517
+ const bootstrap = await client.call('message.v2.bootstrap', {
518
+ peer_aid: peerAid,
519
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
520
+ });
521
+ const peers = (Array.isArray(bootstrap?.peer_devices) ? bootstrap.peer_devices : []);
522
+ for (const dev of peers)
523
+ client._cacheV2PeerIKFromDevice(dev, peerAid);
524
+ }
525
+ catch (exc) {
526
+ client._clientLog.warn(`V2 sender IK 同步 bootstrap 失败: peer=${peerAid} device=${deviceId || '-'} err=${formatE2EEError(exc)}`);
527
+ }
528
+ let senderPubDer = await this.getV2SenderPubDer(peerAid, deviceId, fingerprint);
529
+ if (senderPubDer || !gid)
530
+ return senderPubDer;
531
+ try {
532
+ const bootstrap = await client.call('group.v2.bootstrap', {
533
+ group_id: gid,
534
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
535
+ });
536
+ const devices = (Array.isArray(bootstrap?.devices) ? bootstrap.devices : []);
537
+ const audit = (Array.isArray(bootstrap?.audit_recipients) ? bootstrap.audit_recipients : []);
538
+ for (const dev of devices)
539
+ client._cacheV2PeerIKFromDevice(dev);
540
+ for (const dev of audit)
541
+ client._cacheV2PeerIKFromDevice(dev);
542
+ }
543
+ catch (exc) {
544
+ client._clientLog.warn(`V2 sender IK 同步 group bootstrap 失败: group=${gid} peer=${peerAid} err=${formatE2EEError(exc)}`);
545
+ }
546
+ senderPubDer = await this.getV2SenderPubDer(peerAid, deviceId, fingerprint);
547
+ return senderPubDer;
548
+ };
549
+ try {
550
+ const result = await Promise.race([
551
+ fetch(),
552
+ new Promise((resolve) => {
553
+ timer = setTimeout(() => resolve(timedOut), V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS);
554
+ }),
555
+ ]);
556
+ if (result === timedOut) {
557
+ client._clientLog.warn(`V2 sender IK 同步获取超时: peer=${peerAid} device=${deviceId || '-'} group=${gid || '<p2p>'} timeout_ms=${V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS}`);
558
+ return null;
559
+ }
560
+ return result;
561
+ }
562
+ finally {
563
+ if (timer !== undefined)
564
+ clearTimeout(timer);
565
+ client._clientLog.debug(`V2 sender IK 同步获取完成: peer=${peerAid} device=${deviceId || '-'} group=${gid || '<p2p>'} elapsed_ms=${Date.now() - started}`);
566
+ }
567
+ })();
568
+ this.senderIKSyncFetches.set(fetchKey, task);
569
+ try {
570
+ return await task;
571
+ }
572
+ finally {
573
+ if (this.senderIKSyncFetches.get(fetchKey) === task)
574
+ this.senderIKSyncFetches.delete(fetchKey);
575
+ }
576
+ }
378
577
  scheduleSenderIKPending(args) {
379
578
  const client = this.client;
380
579
  const fromAid = String(args.fromAid ?? '').trim();
@@ -450,21 +649,23 @@ export class V2E2EECoordinator {
450
649
  }
451
650
  catch (exc) {
452
651
  client._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${formatE2EEError(exc)}`);
652
+ client._v2SenderIKPending.delete(key);
653
+ continue;
453
654
  }
454
- client._v2SenderIKPending.delete(key);
455
655
  if (plaintext === null) {
456
- client._clientLog.debug(`V2 sender IK pending retry failed: key=${key}`);
457
- client.emit('message.undecryptable', {
458
- messageId: entry.msg.message_id ?? entry.msg.id ?? key,
459
- error: 'V2 sender IK pending retry failed after IK resolution',
460
- fromAid,
461
- senderDeviceId,
462
- groupId: entry.groupId ?? null,
463
- });
656
+ client._clientLog.warn(`V2 sender IK pending retry still undecryptable: key=${key}`);
657
+ const isGroup = Boolean(entry.groupId);
658
+ const eventName = isGroup ? 'group.message_undecryptable' : 'message.undecryptable';
659
+ const safeEvent = this.safeUndecryptablePushEvent(entry.msg, isGroup);
660
+ safeEvent._decrypt_error = 'sender IK pending retry failed';
661
+ safeEvent._decrypt_stage = 'sender_ik_pending_retry';
662
+ await client._publishAppEvent(eventName, safeEvent, 'pending_retry');
464
663
  continue;
465
664
  }
665
+ client._v2SenderIKPending.delete(key);
466
666
  const seq = Number(entry.msg.seq ?? 0);
467
667
  if (entry.groupId) {
668
+ plaintext = normalizeGroupMentionMode(plaintext, entry.msg, entry.msg.envelope_json);
468
669
  plaintext.group_id = entry.groupId;
469
670
  await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext);
470
671
  }
@@ -510,6 +711,7 @@ export class V2E2EECoordinator {
510
711
  wrapPolicy = normalizeV2WrapPolicy(bs.e2ee_wrap_policy);
511
712
  peerDevices = (Array.isArray(bs?.peer_devices) ? bs.peer_devices : []);
512
713
  auditRaw = (Array.isArray(bs?.audit_recipients) ? bs.audit_recipients : []);
714
+ this.cacheBundledSelfDevices(bs, wrapPolicy);
513
715
  client._clientLog.debug(`message.v2.bootstrap fetched: to=${to}, devices=${peerDevices.length}, audit=${auditRaw.length}`);
514
716
  if (peerDevices.length > 0) {
515
717
  this.setBootstrapCacheEntry(to, {
@@ -694,26 +896,187 @@ export class V2E2EECoordinator {
694
896
  throw exc;
695
897
  }
696
898
  }
899
+ historyDecryptFailure(msg, reason) {
900
+ return {
901
+ message_id: String(msg.message_id ?? ''),
902
+ seq: Number(msg.seq ?? 0),
903
+ encrypted: true,
904
+ history_error: { code: 'decrypt_failed', message: reason },
905
+ };
906
+ }
907
+ async decodeP2PWindowMessage(msg, publish, history, source) {
908
+ const client = this.client;
909
+ const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
910
+ let plaintext = null;
911
+ if (String(msg.version ?? '') === 'v1') {
912
+ const legacy = isJsonObject(msg.legacy_v1)
913
+ ? msg.legacy_v1 : {};
914
+ const payload = legacy.payload;
915
+ const payloadType = isJsonObject(payload)
916
+ ? String(payload.type ?? '').trim() : '';
917
+ if (payload !== undefined && payload !== null && !['e2ee.encrypted', 'e2ee.group_encrypted'].includes(payloadType)) {
918
+ plaintext = {
919
+ message_id: String(msg.message_id ?? ''),
920
+ from: String(msg.from_aid ?? ''),
921
+ to: String(legacy.to ?? client._aid ?? ''),
922
+ seq,
923
+ type: String(msg.type ?? ''),
924
+ timestamp: msg.t_server,
925
+ payload: payload,
926
+ encrypted: false,
927
+ };
928
+ attachGatewayProximity(plaintext, msg);
929
+ }
930
+ }
931
+ else {
932
+ plaintext = await client._decryptV2Message(msg, false, !history);
933
+ }
934
+ if (!plaintext) {
935
+ if (history)
936
+ return this.historyDecryptFailure(msg, 'message could not be decrypted');
937
+ client._clientLog.warn(`P2P Tail 单条消息解密失败,连续性证明仍保留: seq=${seq}`);
938
+ return null;
939
+ }
940
+ if (publish) {
941
+ const event = client._delivery.p2pAppEventForMessage(plaintext);
942
+ await client._publishAppEvent(event.event, event.payload, source);
943
+ }
944
+ return plaintext;
945
+ }
946
+ async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source) {
947
+ const client = this.client;
948
+ const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
949
+ let plaintext = null;
950
+ if (String(msg.version ?? '') === 'v1') {
951
+ const payload = msg.payload;
952
+ if (isJsonObject(payload)
953
+ && ['e2ee.encrypted', 'e2ee.group_encrypted'].includes(String(payload.type ?? ''))) {
954
+ if (history)
955
+ return this.historyDecryptFailure(msg, 'unsupported legacy encrypted envelope');
956
+ client._clientLog.warn(`Group Tail 不支持的旧加密消息已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
957
+ return null;
958
+ }
959
+ plaintext = {
960
+ message_id: String(msg.message_id ?? ''),
961
+ from: String(msg.from_aid ?? ''),
962
+ group_id: groupId,
963
+ group_aid: groupAid,
964
+ seq,
965
+ type: String(msg.type ?? ''),
966
+ timestamp: msg.t_server,
967
+ payload: payload,
968
+ encrypted: false,
969
+ };
970
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
971
+ attachGatewayProximity(plaintext, msg);
972
+ }
973
+ else {
974
+ plaintext = await client._decryptV2Message(msg, false, !history);
975
+ if (plaintext) {
976
+ plaintext.group_id = groupId;
977
+ plaintext.group_aid = groupAid;
978
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
979
+ }
980
+ }
981
+ if (!plaintext) {
982
+ if (history)
983
+ return this.historyDecryptFailure(msg, 'message could not be decrypted');
984
+ client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
985
+ return null;
986
+ }
987
+ if (publish)
988
+ await client._publishAppEvent('group.message_created', plaintext, source);
989
+ return plaintext;
990
+ }
991
+ async applyTailFloor(ns, floor) {
992
+ const client = this.client;
993
+ if (!ns || floor <= 0 || client._seqTracker.getContiguousSeq(ns) >= floor)
994
+ return;
995
+ client._seqTracker.forceContiguousSeq(ns, floor);
996
+ client._persistSeq(ns);
997
+ client._saveSeqTrackerState?.();
998
+ }
999
+ async pullV2TailInternal(params) {
1000
+ const client = this.client;
1001
+ if (params.window_mode !== 'tail')
1002
+ throw new ValidationError('window_mode must equal tail');
1003
+ if ('ack_up_to_seq' in params || params.force === true)
1004
+ throw new ValidationError('Tail does not accept ack_up_to_seq or force');
1005
+ const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
1006
+ const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1007
+ const ns = client._aid ? `p2p:${client._aid}` : '';
1008
+ const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true });
1009
+ const floor = Number(result.retention_floor_seq ?? 0);
1010
+ const previous = ns ? client._seqTracker.getSyncState(ns) : null;
1011
+ await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
1012
+ const page = validateTailPage(result, limit);
1013
+ if (ns) {
1014
+ client._seqTracker.commitTailWindow(ns, {
1015
+ windowStartSeq: page.windowStart,
1016
+ headSeq: page.head,
1017
+ coveredThroughSeq: page.head,
1018
+ pageLimit: limit,
1019
+ });
1020
+ client._persistSeq(ns);
1021
+ client._saveSeqTrackerState?.();
1022
+ }
1023
+ const decoded = [];
1024
+ for (const msg of page.messages) {
1025
+ const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail');
1026
+ if (plaintext)
1027
+ decoded.push(plaintext);
1028
+ }
1029
+ const ack = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1030
+ if (previous && ack > previous.ack)
1031
+ await this.ackV2(ack);
1032
+ return { ...result, messages: decoded, raw_count: page.messages.length };
1033
+ }
1034
+ async historyV2Internal(params) {
1035
+ const client = this.client;
1036
+ const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
1037
+ const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1038
+ const result = await client._callRawV2Rpc('message.history', { before_seq: beforeSeq, limit, _rpc_foreground: true });
1039
+ const messages = validateHistoryPage(result, beforeSeq);
1040
+ const decoded = [];
1041
+ for (const msg of messages) {
1042
+ const plaintext = await this.decodeP2PWindowMessage(msg, false, true, 'history');
1043
+ if (plaintext)
1044
+ decoded.push(plaintext);
1045
+ }
1046
+ return { ...result, messages: decoded };
1047
+ }
697
1048
  async pullV2(afterSeq = 0, limit = 50, opts) {
698
1049
  const client = this.client;
699
1050
  await client._ensureV2SessionReady('message.pull');
700
1051
  const ns = client._aid ? `p2p:${client._aid}` : '';
1052
+ if (opts?.windowMode === 'tail') {
1053
+ if (!opts.gateLocked) {
1054
+ const key = `${ns}|tail|after=${afterSeq}|limit=${limit}`;
1055
+ return (await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, { ...opts, gateLocked: true }), false));
1056
+ }
1057
+ const result = await this.pullV2TailInternal({ window_mode: 'tail', after_seq: afterSeq, limit });
1058
+ return (Array.isArray(result.messages) ? result.messages : []);
1059
+ }
701
1060
  if (ns && !opts?.gateLocked) {
702
- return await client._runPullSerialized(ns, async () => this.pullV2(afterSeq, limit, {
1061
+ const key = `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
1062
+ return await client._runPullSerialized(key, async () => this.pullV2(afterSeq, limit, {
703
1063
  ...(opts ?? {}),
704
1064
  gateLocked: true,
705
1065
  scheduleFollowup: true,
706
- }));
1066
+ }), true);
707
1067
  }
708
1068
  const decrypted = [];
709
1069
  let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
710
1070
  let pageCount = 0;
711
1071
  let lastAutoAckSeq = 0;
712
1072
  let pendingAckSeq = 0;
713
- const maxPages = 100;
1073
+ const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
714
1074
  while (pageCount < maxPages) {
715
1075
  pageCount += 1;
716
1076
  const ackUpToSeq = opts?.skipAutoAck ? 0 : pendingAckSeq;
1077
+ const inlineAckGeneration = ackUpToSeq > 0
1078
+ ? client._delivery.captureP2PInlineAckGeneration()
1079
+ : null;
717
1080
  client._clientLog.debug(`message.v2.pull page request: page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns || '<none>'}`);
718
1081
  const result = await client._callRawV2Rpc('message.v2.pull', {
719
1082
  after_seq: nextAfterSeq,
@@ -722,14 +1085,14 @@ export class V2E2EECoordinator {
722
1085
  ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
723
1086
  });
724
1087
  if (ackUpToSeq > 0) {
1088
+ if (inlineAckGeneration !== null) {
1089
+ client._delivery.onP2PV2PullAckResult(result, ackUpToSeq, inlineAckGeneration);
1090
+ }
725
1091
  pendingAckSeq = 0;
726
1092
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
727
1093
  }
728
1094
  const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
729
- const messages = pageMessages.filter((msg) => {
730
- const seq = Number(msg.seq ?? 0);
731
- return Number.isFinite(seq) && seq > nextAfterSeq;
732
- }).sort((a, b) => Number(a.seq ?? 0) - Number(b.seq ?? 0));
1095
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
733
1096
  client._clientLog.debug(`message.v2.pull page response: page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_ack_seq=${String(result.server_ack_seq ?? '')}`);
734
1097
  for (const msg of messages) {
735
1098
  client._logMessageDebug('pull-raw', 'message.v2.pull', 'message.received', msg);
@@ -738,13 +1101,12 @@ export class V2E2EECoordinator {
738
1101
  .map((msg) => Number(msg.seq ?? 0))
739
1102
  .filter((seq) => Number.isFinite(seq) && seq > 0);
740
1103
  const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1104
+ const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
1105
+ ? client._seqTracker.snapshotNamespace(ns)
1106
+ : null;
741
1107
  let pageMaxSeq = nextAfterSeq;
742
1108
  if (seqs.length > 0) {
743
1109
  pageMaxSeq = Math.max(...seqs);
744
- if (ns) {
745
- client._seqTracker.forceContiguousSeq(ns, pageMaxSeq);
746
- client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
747
- }
748
1110
  }
749
1111
  for (const msg of messages) {
750
1112
  const seq = Number(msg.seq ?? 0);
@@ -808,19 +1170,36 @@ export class V2E2EECoordinator {
808
1170
  }
809
1171
  const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
810
1172
  const serverAckSeq = Number(result.server_ack_seq ?? 0);
811
- if (ns && Number.isFinite(serverAckSeq) && serverAckSeq > 0) {
812
- const contig = client._seqTracker.getContiguousSeq(ns);
813
- if (contig < serverAckSeq) {
814
- client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${contig} -> server_ack_seq=${serverAckSeq}`);
815
- client._seqTracker.forceContiguousSeq(ns, serverAckSeq);
816
- }
817
- }
1173
+ const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
818
1174
  if (ns) {
819
- const ackSeq = client._seqTracker.getContiguousSeq(ns);
820
- const contigAdvanced = ackSeq !== pageContigBefore;
821
- if (contigAdvanced) {
822
- await client._drainOrderedMessages(ns, undefined, true);
823
- client._persistSeq(ns);
1175
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1176
+ let ackSeq = pageContigBefore;
1177
+ let contigAdvanced = false;
1178
+ try {
1179
+ if (commitTarget > pageContigBefore) {
1180
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
1181
+ if (retentionFloor > pageContigBefore) {
1182
+ client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1183
+ }
1184
+ if (seqs.length > 0) {
1185
+ client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1186
+ }
1187
+ }
1188
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
1189
+ contigAdvanced = ackSeq !== pageContigBefore;
1190
+ if (contigAdvanced) {
1191
+ await client._drainOrderedMessages(ns, undefined, true);
1192
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
1193
+ contigAdvanced = ackSeq !== pageContigBefore;
1194
+ client._commitSeqTrackerState(ns);
1195
+ }
1196
+ }
1197
+ catch (exc) {
1198
+ if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1199
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1200
+ }
1201
+ client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1202
+ throw exc;
824
1203
  }
825
1204
  const ackNeeded = pageMessages.length > 0
826
1205
  && ackSeq > 0
@@ -874,28 +1253,30 @@ export class V2E2EECoordinator {
874
1253
  return { acked: 0 };
875
1254
  }
876
1255
  seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
1256
+ if (seq <= 0)
1257
+ return { acked: 0 };
1258
+ const inlineAckGeneration = client._delivery.captureP2PInlineAckGeneration();
1259
+ const v2SessionAtStart = client._v2Session;
1260
+ const v2RuntimeGenerationAtStart = Number(client._v2RuntimeGeneration ?? 0);
877
1261
  client._clientLog.debug(`message.v2.ack send: ns=${ns || '<none>'}, up_to_seq=${seq}`);
878
1262
  const raw = await client._callRawV2Rpc('message.v2.ack', { up_to_seq: seq });
879
1263
  const result = isJsonObject(raw)
880
1264
  ? { ...raw }
881
1265
  : { result: raw };
882
- let actualAckSeq = seq;
883
- if ('effective_ack_seq' in result)
884
- actualAckSeq = Number(result.effective_ack_seq ?? 0);
885
- else if ('ack_seq' in result)
886
- actualAckSeq = Number(result.ack_seq ?? 0);
887
- else if ('cursor' in result)
888
- actualAckSeq = Number(result.cursor ?? 0);
889
- if (!Number.isFinite(actualAckSeq))
890
- actualAckSeq = seq;
1266
+ const actualAckSeq = client._delivery.resolveActualAckSeq(result, seq);
891
1267
  result.ack_seq = actualAckSeq;
892
1268
  result.success = true;
893
- if (Number(result.acked ?? 0) === 0)
894
- result.acked = actualAckSeq;
895
- if (client._v2Session) {
1269
+ if (inlineAckGeneration !== null) {
1270
+ client._delivery.onP2PV2AckSucceeded(actualAckSeq, inlineAckGeneration);
1271
+ }
1272
+ if (v2SessionAtStart
1273
+ && client._v2Session === v2SessionAtStart
1274
+ && Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart) {
896
1275
  try {
897
- const destroyed = await Promise.resolve(client._v2Session.maybeDestroyOldSPKs(actualAckSeq));
898
- if (destroyed.length > 0) {
1276
+ const destroyed = await Promise.resolve(v2SessionAtStart.maybeDestroyOldSPKs(actualAckSeq));
1277
+ if (client._v2Session === v2SessionAtStart
1278
+ && Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart
1279
+ && destroyed.length > 0) {
899
1280
  client._clientLog.info(`V2 destroyed old SPKs after ack: ${destroyed.slice(0, 3).join(',')} (PFS)`);
900
1281
  }
901
1282
  }
@@ -909,16 +1290,17 @@ export class V2E2EECoordinator {
909
1290
  async sendGroupV2(groupId, payload, opts) {
910
1291
  const client = this.client;
911
1292
  await client._ensureV2SessionReady('group.send', 'V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
912
- const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1293
+ const gid = String(groupId ?? '').trim();
913
1294
  if (!gid)
914
1295
  throw new ValidationError("group.send requires 'group_id'");
1296
+ const groupAid = String(opts?.groupAid ?? '').trim();
915
1297
  if (!isJsonObject(payload))
916
1298
  throw new ValidationError('group.send payload must be a dict for V2 encryption');
917
- client._logMessageDebug('send-plaintext', 'group.send.v2', 'group.send', {
1299
+ client._logMessageDebug('send-plaintext', 'group.send.v2', 'group.send', withExplicitGroupAid({
918
1300
  group_id: gid,
919
1301
  message_id: opts?.messageId ?? '',
920
1302
  payload,
921
- }, { payloadOverride: payload });
1303
+ }, groupAid), { payloadOverride: payload });
922
1304
  const attempt = async (useCache) => {
923
1305
  client._clientLog.debug(`group.v2.send attempt: group=${gid}, use_cache=${useCache}`);
924
1306
  const envelope = await client._buildV2GroupEnvelope({
@@ -930,10 +1312,10 @@ export class V2E2EECoordinator {
930
1312
  context: opts?.context,
931
1313
  useCache,
932
1314
  });
933
- const result = await client.call('group.v2.send', {
1315
+ const result = await client.call('group.v2.send', withExplicitGroupAid({
934
1316
  group_id: gid,
935
1317
  envelope,
936
- });
1318
+ }, groupAid));
937
1319
  client._clientLog.debug(`group.v2.send ok: group=${gid}, use_cache=${useCache}, seq=${String((isJsonObject(result) ? result.seq : '') ?? '')}`);
938
1320
  return result;
939
1321
  };
@@ -953,14 +1335,14 @@ export class V2E2EECoordinator {
953
1335
  try {
954
1336
  const result = await attempt(true);
955
1337
  markSentSeq(result);
956
- return client._delivery.attachSendResultEnvelope('group.send', {
1338
+ return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
957
1339
  group_id: gid,
958
1340
  payload,
959
1341
  message_id: opts?.messageId,
960
1342
  timestamp: opts?.timestamp,
961
1343
  protected_headers: opts?.protectedHeaders,
962
1344
  context: opts?.context,
963
- }, result, true);
1345
+ }, groupAid), result, true);
964
1346
  }
965
1347
  catch (exc) {
966
1348
  const excCode = Number(exc?.code);
@@ -969,14 +1351,14 @@ export class V2E2EECoordinator {
969
1351
  this.deleteBootstrapCacheEntry(`group:${gid}`);
970
1352
  const result = await attempt(false);
971
1353
  markSentSeq(result);
972
- return client._delivery.attachSendResultEnvelope('group.send', {
1354
+ return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
973
1355
  group_id: gid,
974
1356
  payload,
975
1357
  message_id: opts?.messageId,
976
1358
  timestamp: opts?.timestamp,
977
1359
  protected_headers: opts?.protectedHeaders,
978
1360
  context: opts?.context,
979
- }, result, true);
1361
+ }, groupAid), result, true);
980
1362
  }
981
1363
  throw exc;
982
1364
  }
@@ -986,8 +1368,7 @@ export class V2E2EECoordinator {
986
1368
  if (!client._v2Session)
987
1369
  throw new StateError('V2 session not initialized');
988
1370
  const session = client._v2Session;
989
- const rawGroupId = String(opts.groupId ?? '').trim();
990
- const groupId = normalizeGroupId(rawGroupId) || rawGroupId;
1371
+ const groupId = String(opts.groupId ?? '').trim();
991
1372
  if (!groupId)
992
1373
  throw new ValidationError("group.send requires 'group_id'");
993
1374
  const cacheKey = `group:${groupId}`;
@@ -998,15 +1379,7 @@ export class V2E2EECoordinator {
998
1379
  let stateCommitment = { state_version: 0, state_hash: '', state_chain: '' };
999
1380
  let wrapPolicy = normalizeV2WrapPolicy(undefined);
1000
1381
  let usedCachedBootstrap = false;
1001
- let cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : undefined;
1002
- if (!cached && useCache && rawGroupId && rawGroupId !== groupId) {
1003
- const legacyCacheKey = `group:${rawGroupId}`;
1004
- cached = this.getBootstrapCacheEntry(legacyCacheKey);
1005
- if (cached) {
1006
- this.setBootstrapCacheEntry(cacheKey, cached);
1007
- this.deleteBootstrapCacheEntry(legacyCacheKey);
1008
- }
1009
- }
1382
+ const cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : undefined;
1010
1383
  if (cached && Date.now() - Number(cached.cachedAt ?? 0) < V2_BOOTSTRAP_TTL_MS) {
1011
1384
  allDevices = (cached.devices ?? []);
1012
1385
  auditRecipientsRaw = (cached.auditRecipients ?? []);
@@ -1136,53 +1509,157 @@ export class V2E2EECoordinator {
1136
1509
  });
1137
1510
  return envelope;
1138
1511
  }
1512
+ async pullGroupV2TailInternal(params) {
1513
+ const client = this.client;
1514
+ if (params.window_mode !== 'tail')
1515
+ throw new ValidationError('window_mode must equal tail');
1516
+ if ('ack_up_to_seq' in params || params.force === true)
1517
+ throw new ValidationError('Tail does not accept ack_up_to_seq or force');
1518
+ const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
1519
+ if (!groupId)
1520
+ throw new ValidationError('group_id is required');
1521
+ let groupAid = String(params.group_aid ?? '').trim();
1522
+ const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
1523
+ const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1524
+ const ns = `group:${groupId}`;
1525
+ const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
1526
+ group_id: groupId,
1527
+ window_mode: 'tail',
1528
+ after_seq: afterSeq,
1529
+ limit,
1530
+ _rpc_foreground: true,
1531
+ }, groupAid));
1532
+ const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
1533
+ if (resultAid)
1534
+ groupAid = resultAid;
1535
+ const floor = Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0);
1536
+ const previous = client._seqTracker.getSyncState(ns);
1537
+ await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
1538
+ const page = validateTailPage(result, limit);
1539
+ client._seqTracker.commitTailWindow(ns, {
1540
+ windowStartSeq: page.windowStart,
1541
+ headSeq: page.head,
1542
+ coveredThroughSeq: page.head,
1543
+ pageLimit: limit,
1544
+ });
1545
+ client._persistSeq(ns);
1546
+ client._saveSeqTrackerState?.();
1547
+ const decoded = [];
1548
+ for (const msg of page.messages) {
1549
+ const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail');
1550
+ if (plaintext)
1551
+ decoded.push(plaintext);
1552
+ }
1553
+ const ack = client._seqTracker.getContiguousSeq(ns);
1554
+ if (ack > previous.ack)
1555
+ await this.ackGroupV2(groupId, ack, groupAid);
1556
+ return { ...result, messages: decoded, raw_count: page.messages.length };
1557
+ }
1558
+ async historyGroupV2Internal(params) {
1559
+ const client = this.client;
1560
+ const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
1561
+ if (!groupId)
1562
+ throw new ValidationError('group_id is required');
1563
+ let groupAid = String(params.group_aid ?? '').trim();
1564
+ const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
1565
+ const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
1566
+ const result = await client._callRawV2Rpc('group.history', withExplicitGroupAid({
1567
+ group_id: groupId,
1568
+ before_seq: beforeSeq,
1569
+ limit,
1570
+ _rpc_foreground: true,
1571
+ }, groupAid));
1572
+ const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
1573
+ if (resultAid)
1574
+ groupAid = resultAid;
1575
+ const messages = validateHistoryPage(result, beforeSeq);
1576
+ const decoded = [];
1577
+ for (const msg of messages) {
1578
+ const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, false, true, 'history');
1579
+ if (plaintext)
1580
+ decoded.push(plaintext);
1581
+ }
1582
+ return { ...result, messages: decoded };
1583
+ }
1139
1584
  async pullGroupV2Internal(params) {
1140
- await this.pullGroupV2(params.group_id, params.after_seq, params.limit, { gateLocked: true });
1585
+ await this.pullGroupV2(params.group_id, params.after_seq, params.limit, {
1586
+ gateLocked: true,
1587
+ explicitAfterSeq: true,
1588
+ force: params.force === true,
1589
+ });
1141
1590
  }
1142
1591
  async pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
1143
1592
  const client = this.client;
1144
1593
  await client._ensureV2SessionReady('group.pull');
1145
- const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1594
+ const gid = String(groupId ?? '').trim();
1146
1595
  if (!gid)
1147
1596
  throw new ValidationError('group.pull requires group_id');
1597
+ let groupAid = String(opts?.groupAid ?? '').trim();
1148
1598
  const ns = `group:${gid}`;
1599
+ if (opts?.windowMode === 'tail') {
1600
+ if (!opts.gateLocked) {
1601
+ const key = `${ns}|tail|after=${afterSeq}|limit=${limit}`;
1602
+ return (await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
1603
+ ...opts,
1604
+ gateLocked: true,
1605
+ }), false));
1606
+ }
1607
+ const result = await this.pullGroupV2TailInternal({
1608
+ group_id: String(opts.wireGroupId ?? gid),
1609
+ group_aid: groupAid || undefined,
1610
+ window_mode: 'tail',
1611
+ after_seq: afterSeq,
1612
+ limit,
1613
+ });
1614
+ return (Array.isArray(result.messages) ? result.messages : []);
1615
+ }
1149
1616
  if (!opts?.gateLocked) {
1150
- return await client._runPullSerialized(ns, async () => this.pullGroupV2(gid, afterSeq, limit, {
1617
+ const key = `${ns}|forward|after=${afterSeq}|force=${opts?.force === true}|limit=${limit}`;
1618
+ return await client._runPullSerialized(key, async () => this.pullGroupV2(gid, afterSeq, limit, {
1151
1619
  ...(opts ?? {}),
1152
1620
  gateLocked: true,
1153
1621
  scheduleFollowup: true,
1154
- }));
1622
+ }), true);
1155
1623
  }
1156
1624
  const decrypted = [];
1157
1625
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
1158
1626
  const cursorParams = opts?.cursorParams ?? {};
1159
1627
  const ownsCursor = opts?.ownsCursor !== false;
1160
- let nextAfterSeq = opts?.explicitAfterSeq ? afterSeq : (afterSeq || client._seqTracker.getContiguousSeq(ns));
1628
+ let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
1629
+ ? afterSeq
1630
+ : (afterSeq || client._seqTracker.getContiguousSeq(ns));
1161
1631
  let pageCount = 0;
1162
1632
  let lastAutoAckSeq = 0;
1163
1633
  let pendingAckSeq = 0;
1164
- const maxPages = 100;
1634
+ const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1165
1635
  while (pageCount < maxPages) {
1166
1636
  pageCount += 1;
1167
1637
  const ackUpToSeq = ownsCursor ? pendingAckSeq : 0;
1638
+ const inlineAckGeneration = ackUpToSeq > 0
1639
+ ? client._delivery.captureGroupInlineAckGeneration()
1640
+ : null;
1168
1641
  client._clientLog.debug(`group.v2.pull page request: group=${gid}, page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns}`);
1169
- const result = await client._callRawV2Rpc('group.v2.pull', {
1642
+ const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
1170
1643
  group_id: wireGroupId,
1171
- group_aid: gid,
1172
1644
  after_seq: nextAfterSeq,
1173
1645
  limit,
1174
1646
  ...cursorParams,
1647
+ ...(opts?.force ? { force: true } : {}),
1175
1648
  ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1176
- });
1649
+ }, groupAid));
1177
1650
  if (ackUpToSeq > 0) {
1651
+ if (inlineAckGeneration !== null) {
1652
+ client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
1653
+ }
1178
1654
  pendingAckSeq = 0;
1179
1655
  lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1180
1656
  }
1657
+ const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
1658
+ if (resultGroupAid)
1659
+ groupAid = resultGroupAid;
1660
+ const eventGroupAid = resultGroupAid || groupAid || gid;
1181
1661
  const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
1182
- const messages = pageMessages.filter((msg) => {
1183
- const seq = Number(msg.seq ?? 0);
1184
- return Number.isFinite(seq) && seq > nextAfterSeq;
1185
- }).sort((a, b) => Number(a.seq ?? 0) - Number(b.seq ?? 0));
1662
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1186
1663
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1187
1664
  client._clientLog.debug(`group.v2.pull page response: group=${gid}, page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, cursor_current=${String(cursor?.current_seq ?? '')}`);
1188
1665
  for (const msg of messages) {
@@ -1192,11 +1669,12 @@ export class V2E2EECoordinator {
1192
1669
  .map((msg) => Number(msg.seq ?? 0))
1193
1670
  .filter((seq) => Number.isFinite(seq) && seq > 0);
1194
1671
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
1672
+ const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
1673
+ ? client._seqTracker.snapshotNamespace(ns)
1674
+ : null;
1195
1675
  let pageMaxSeq = nextAfterSeq;
1196
1676
  if (seqs.length > 0) {
1197
1677
  pageMaxSeq = Math.max(...seqs);
1198
- client._seqTracker.forceContiguousSeq(ns, pageMaxSeq);
1199
- client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1200
1678
  }
1201
1679
  for (const msg of messages) {
1202
1680
  const seq = Number(msg.seq ?? 0);
@@ -1208,7 +1686,11 @@ export class V2E2EECoordinator {
1208
1686
  const payloadObj = isJsonObject(payload) ? payload : null;
1209
1687
  // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
1210
1688
  if (client._delivery.recallEventFromGroupMessage(msg)) {
1211
- await client._delivery.publishGroupRecallTombstone(gid, seq, msg);
1689
+ await client._delivery.publishGroupRecallTombstone(gid, seq, {
1690
+ ...msg,
1691
+ group_id: gid,
1692
+ group_aid: eventGroupAid,
1693
+ });
1212
1694
  client._markPublishedSeq(ns, seq);
1213
1695
  client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
1214
1696
  continue;
@@ -1216,16 +1698,18 @@ export class V2E2EECoordinator {
1216
1698
  if (payloadObj) {
1217
1699
  const payloadType = String(payloadObj.type ?? '').trim();
1218
1700
  if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted') {
1219
- const v1Msg = {
1701
+ let v1Msg = {
1220
1702
  message_id: String(msg.message_id ?? ''),
1221
1703
  from: String(msg.from_aid ?? ''),
1222
1704
  group_id: gid,
1705
+ group_aid: eventGroupAid,
1223
1706
  seq: msg.seq,
1224
1707
  type: String(msg.type ?? ''),
1225
1708
  timestamp: msg.t_server,
1226
1709
  payload,
1227
1710
  encrypted: false,
1228
1711
  };
1712
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
1229
1713
  attachGatewayProximity(v1Msg, msg);
1230
1714
  await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
1231
1715
  decrypted.push(v1Msg);
@@ -1234,16 +1718,18 @@ export class V2E2EECoordinator {
1234
1718
  }
1235
1719
  }
1236
1720
  else if (payload !== undefined && payload !== null) {
1237
- const v1Msg = {
1721
+ let v1Msg = {
1238
1722
  message_id: String(msg.message_id ?? ''),
1239
1723
  from: String(msg.from_aid ?? ''),
1240
1724
  group_id: gid,
1725
+ group_aid: eventGroupAid,
1241
1726
  seq: msg.seq,
1242
1727
  type: String(msg.type ?? ''),
1243
1728
  timestamp: msg.t_server,
1244
1729
  payload,
1245
1730
  encrypted: false,
1246
1731
  };
1732
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
1247
1733
  attachGatewayProximity(v1Msg, msg);
1248
1734
  await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
1249
1735
  decrypted.push(v1Msg);
@@ -1257,31 +1743,51 @@ export class V2E2EECoordinator {
1257
1743
  client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
1258
1744
  continue;
1259
1745
  }
1260
- const plaintext = await client._decryptV2Message(msg);
1746
+ let plaintext = await client._decryptV2Message(msg);
1261
1747
  if (plaintext === null) {
1262
1748
  client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
1263
1749
  continue;
1264
1750
  }
1751
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1265
1752
  plaintext.group_id = gid;
1753
+ plaintext.group_aid = eventGroupAid;
1266
1754
  await client._publishPulledMessage('group.message_created', ns, seq, plaintext);
1267
1755
  decrypted.push(plaintext);
1268
1756
  client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
1269
1757
  }
1270
1758
  const cursorCurrentSeq = Number(cursor?.current_seq ?? 0);
1271
1759
  const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
1272
- const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'), Number.isFinite(cursorCurrentSeq) ? cursorCurrentSeq : 0);
1273
- if (retentionFloor > 0) {
1274
- const contig = client._seqTracker.getContiguousSeq(ns);
1275
- if (contig < retentionFloor) {
1276
- client._clientLog.info(`group.v2.pull retention-floor advance: ns=${ns} contiguous=${contig} -> retention_floor=${retentionFloor}`);
1277
- client._seqTracker.forceContiguousSeq(ns, retentionFloor);
1760
+ const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
1761
+ const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
1762
+ const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
1763
+ const commitTarget = Math.max(pageContigBefore, effectiveFloor, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
1764
+ let ackSeq = pageContigBefore;
1765
+ let contigAdvanced = false;
1766
+ try {
1767
+ if (commitTarget > pageContigBefore) {
1768
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
1769
+ if (effectiveFloor > pageContigBefore) {
1770
+ client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
1771
+ }
1772
+ if (seqs.length > 0) {
1773
+ client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1774
+ }
1775
+ }
1776
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
1777
+ contigAdvanced = ackSeq !== pageContigBefore;
1778
+ if (contigAdvanced) {
1779
+ await client._drainOrderedMessages(ns, undefined, true);
1780
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
1781
+ contigAdvanced = ackSeq !== pageContigBefore;
1782
+ client._commitSeqTrackerState(ns);
1278
1783
  }
1279
1784
  }
1280
- const ackSeq = client._seqTracker.getContiguousSeq(ns);
1281
- const contigAdvanced = ackSeq !== pageContigBefore;
1282
- if (contigAdvanced) {
1283
- await client._drainOrderedMessages(ns, undefined, true);
1284
- client._persistSeq(ns);
1785
+ catch (exc) {
1786
+ if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1787
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1788
+ }
1789
+ client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1790
+ throw exc;
1285
1791
  }
1286
1792
  const ackNeeded = pageMessages.length > 0
1287
1793
  && ackSeq > 0
@@ -1327,21 +1833,30 @@ export class V2E2EECoordinator {
1327
1833
  client._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
1328
1834
  return decrypted;
1329
1835
  }
1330
- async ackGroupV2(groupId, upToSeq) {
1836
+ async ackGroupV2(groupId, upToSeq, groupAid) {
1331
1837
  const client = this.client;
1332
- const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
1838
+ const gid = String(groupId ?? '').trim();
1333
1839
  if (!gid)
1334
1840
  throw new ValidationError('group.ack_messages requires group_id');
1335
- const ns = `group:${gid}`;
1841
+ const explicitGroupAid = String(groupAid ?? '').trim();
1842
+ const wireNs = `group:${gid}`;
1843
+ const ns = client._delivery.resolvePendingGroupInlineAckNamespace?.(wireNs) ?? wireNs;
1336
1844
  let seq = Number(upToSeq ?? client._seqTracker.getContiguousSeq(ns));
1337
1845
  if (!Number.isFinite(seq) || seq <= 0) {
1338
1846
  client._clientLog.debug(`group.v2.ack skipped: group=${gid}, ns=${ns}, up_to_seq=${String(upToSeq ?? '')}`);
1339
1847
  return { acked: 0 };
1340
1848
  }
1341
1849
  seq = client._clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
1850
+ if (seq <= 0)
1851
+ return { acked: 0 };
1852
+ const inlineAckGeneration = client._delivery.captureGroupInlineAckGeneration();
1342
1853
  client._clientLog.debug(`group.v2.ack send: group=${gid}, ns=${ns}, up_to_seq=${seq}`);
1343
- const result = await client._callRawV2Rpc('group.v2.ack', { group_id: gid, group_aid: gid, up_to_seq: seq });
1344
- client._clientLog.debug(`group.v2.ack ok: group=${gid}, ns=${ns}, requested=${seq}, result=${client._debugJson(result)}`);
1854
+ const result = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq }, explicitGroupAid));
1855
+ const actualAckSeq = client._delivery.resolveActualAckSeq(result, seq);
1856
+ if (inlineAckGeneration !== null) {
1857
+ client._delivery.onGroupV2AckSucceeded(gid, actualAckSeq, inlineAckGeneration);
1858
+ }
1859
+ client._clientLog.debug(`group.v2.ack ok: group=${gid}, ns=${ns}, requested=${seq}, effective=${actualAckSeq}, result=${client._debugJson(result)}`);
1345
1860
  return result;
1346
1861
  }
1347
1862
  async putMessageThoughtEncryptedV2(params) {
@@ -1754,7 +2269,7 @@ export class V2E2EECoordinator {
1754
2269
  }
1755
2270
  safeUndecryptablePushEvent(msg, group) {
1756
2271
  const client = this.client;
1757
- const event = {
2272
+ let event = {
1758
2273
  message_id: msg.message_id,
1759
2274
  from: msg.from,
1760
2275
  seq: msg.seq,
@@ -1778,6 +2293,10 @@ export class V2E2EECoordinator {
1778
2293
  client._attachV2EnvelopeMetadata(event, client._v2E2eeMeta(envelope));
1779
2294
  }
1780
2295
  }
2296
+ if (group) {
2297
+ const modeSource = envelope ? { ...msg, envelope } : msg;
2298
+ event = normalizeGroupMentionMode(event, modeSource);
2299
+ }
1781
2300
  return event;
1782
2301
  }
1783
2302
  async decryptEncryptedPushPayload(msg, group) {
@@ -1791,7 +2310,7 @@ export class V2E2EECoordinator {
1791
2310
  if (!plaintext)
1792
2311
  return null;
1793
2312
  const e2ee = client._v2E2eeMeta(envelope);
1794
- const result = {
2313
+ let result = {
1795
2314
  message_id: String(msg.message_id ?? ''),
1796
2315
  from: fromAid,
1797
2316
  seq: msg.seq,
@@ -1810,6 +2329,7 @@ export class V2E2EECoordinator {
1810
2329
  attachGatewayProximity(result, msg);
1811
2330
  if (group) {
1812
2331
  result.group_id = (msg.group_id ?? aad.group_id ?? envelope.group_id);
2332
+ result = normalizeGroupMentionMode(result, msg, envelope);
1813
2333
  }
1814
2334
  else {
1815
2335
  result.to = (msg.to ?? client._aid ?? '');
@@ -1846,7 +2366,7 @@ export class V2E2EECoordinator {
1846
2366
  async decryptV2PushMessage(data) {
1847
2367
  if (!isJsonObject(data))
1848
2368
  return null;
1849
- return await this.client._decryptV2Message(data);
2369
+ return await this.client._decryptV2Message(data, false, false, false, false);
1850
2370
  }
1851
2371
  handleGroupChangedSpk(data, groupId, action) {
1852
2372
  if (!this.client._v2Session || !groupId || !MEMBERSHIP_ACTIONS.has(action))