@agentunion/fastaun 0.5.0 → 0.5.2

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 (114) hide show
  1. package/CHANGELOG.md +561 -472
  2. package/_packed_docs/CHANGELOG-validators.md +147 -0
  3. package/_packed_docs/CHANGELOG.md +561 -472
  4. package/_packed_docs/INDEX.md +201 -177
  5. package/_packed_docs/KITE_DOCS_GUIDE.md +38 -32
  6. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +169 -116
  7. package/_packed_docs/aun-perf-audit-critical-bugs.md +315 -0
  8. package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +263 -260
  9. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +331 -0
  10. package/_packed_docs/protocol/00-/346/200/273/350/247/210/344/270/216/345/210/206/345/261/202.md +2 -2
  11. package/_packed_docs/protocol/00A-/350/256/276/350/256/241/345/216/237/345/210/231-/344/270/272Agent/350/200/214/347/224/237.md +1 -1
  12. package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +39 -16
  13. package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +8 -5
  14. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +18 -19
  15. package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +1 -1
  16. package/_packed_docs/protocol/08-AUN-E2EE-Group.md +139 -746
  17. package/_packed_docs/protocol/08-AUN-E2EE.md +12 -10
  18. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +117 -171
  19. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +6 -0
  20. 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 +1 -1
  21. package/_packed_docs/protocol/16-/347/263/273/347/273/237/347/233/256/345/275/225/344/277/235/346/212/244/346/226/271/346/241/210.md +177 -0
  22. package/_packed_docs/protocol/README.md +5 -4
  23. package/_packed_docs/protocol/aun-docs-guide.md +2 -2
  24. package/_packed_docs/protocol/index.md +12 -7
  25. package/_packed_docs/protocol//350/215/211/346/241/210-/346/213/222/347/273/235/344/277/241/345/217/267/345/215/217/350/256/256.md +1 -1
  26. package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +13 -13
  27. package/_packed_docs/protocol//351/231/204/345/275/225L-E2EE/345/256/236/347/216/260/346/214/207/345/215/227.md +9 -9
  28. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +15 -13
  29. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +26 -18
  30. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +35 -284
  31. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +473 -442
  32. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +11 -7
  33. package/_packed_docs/sdk/09-collab-rpc-manual.md +581 -550
  34. package/_packed_docs/sdk/09-group-rpc-manual.md +367 -433
  35. package/_packed_docs/sdk/09-message-rpc-manual.md +50 -28
  36. package/_packed_docs/sdk/09-payload-reference.md +3 -3
  37. package/_packed_docs/sdk/09-storage-rpc-manual.md +57 -20
  38. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +18 -17
  39. package/_packed_docs/sdk/E2EE_V2/346/266/210/346/201/257/351/200/232/344/277/241/346/227/266/345/272/217/345/233/276.md +3 -2
  40. package/_packed_docs/sdk/INDEX.md +25 -24
  41. package/_packed_docs/sdk/Notify/351/200/232/347/237/245/346/226/271/346/241/210.md +6 -2
  42. package/dist/agent-md.js +18 -9
  43. package/dist/agent-md.js.map +1 -1
  44. package/dist/client/delivery.d.ts +9 -0
  45. package/dist/client/delivery.js +157 -24
  46. package/dist/client/delivery.js.map +1 -1
  47. package/dist/client/group-state.js +33 -11
  48. package/dist/client/group-state.js.map +1 -1
  49. package/dist/client/rpc-pipeline.d.ts +1 -0
  50. package/dist/client/rpc-pipeline.js +127 -40
  51. package/dist/client/rpc-pipeline.js.map +1 -1
  52. package/dist/client/v2-e2ee.d.ts +6 -1
  53. package/dist/client/v2-e2ee.js +284 -140
  54. package/dist/client/v2-e2ee.js.map +1 -1
  55. package/dist/client.d.ts +8 -1
  56. package/dist/client.js +227 -31
  57. package/dist/client.js.map +1 -1
  58. package/dist/collab/client.d.ts +8 -0
  59. package/dist/collab/client.js +12 -0
  60. package/dist/collab/client.js.map +1 -1
  61. package/dist/errors.d.ts +0 -20
  62. package/dist/errors.js +8 -53
  63. package/dist/errors.js.map +1 -1
  64. package/dist/facades.d.ts +9 -4
  65. package/dist/facades.js +137 -15
  66. package/dist/facades.js.map +1 -1
  67. package/dist/group-fs.d.ts +26 -9
  68. package/dist/group-fs.js +84 -84
  69. package/dist/group-fs.js.map +1 -1
  70. package/dist/group-id.d.ts +9 -12
  71. package/dist/group-id.js +41 -63
  72. package/dist/group-id.js.map +1 -1
  73. package/dist/index.d.ts +4 -2
  74. package/dist/index.js +4 -1
  75. package/dist/index.js.map +1 -1
  76. package/dist/keystore/aid-db.d.ts +1 -0
  77. package/dist/keystore/aid-db.js +37 -37
  78. package/dist/keystore/aid-db.js.map +1 -1
  79. package/dist/keystore/local-identity-store.d.ts +16 -0
  80. package/dist/keystore/local-identity-store.js +190 -18
  81. package/dist/keystore/local-identity-store.js.map +1 -1
  82. package/dist/keystore/local-token-store.d.ts +0 -13
  83. package/dist/keystore/local-token-store.js +0 -105
  84. package/dist/keystore/local-token-store.js.map +1 -1
  85. package/dist/logger.d.ts +2 -0
  86. package/dist/logger.js +4 -0
  87. package/dist/logger.js.map +1 -1
  88. package/dist/storage/lowlevel.d.ts +9 -1
  89. package/dist/storage/lowlevel.js +12 -1
  90. package/dist/storage/lowlevel.js.map +1 -1
  91. package/dist/storage/types.d.ts +0 -1
  92. package/dist/storage/types.js.map +1 -1
  93. package/dist/storage/vfs.d.ts +22 -3
  94. package/dist/storage/vfs.js +89 -54
  95. package/dist/storage/vfs.js.map +1 -1
  96. package/dist/tools/cross-sdk-agent.js +336 -49
  97. package/dist/tools/cross-sdk-agent.js.map +1 -1
  98. package/dist/transport.d.ts +2 -0
  99. package/dist/transport.js +52 -3
  100. package/dist/transport.js.map +1 -1
  101. package/dist/types.d.ts +38 -0
  102. package/dist/v2/session/session.d.ts +2 -0
  103. package/dist/v2/session/session.js +53 -23
  104. package/dist/v2/session/session.js.map +1 -1
  105. package/dist/v2/state/commitment.d.ts +1 -1
  106. package/dist/v2/state/commitment.js +4 -2
  107. package/dist/v2/state/commitment.js.map +1 -1
  108. package/dist/validators.d.ts +32 -0
  109. package/dist/validators.js +128 -0
  110. package/dist/validators.js.map +1 -0
  111. package/dist/version.d.ts +1 -1
  112. package/dist/version.js +1 -1
  113. package/package.json +46 -46
  114. package/_packed_docs/collab-gateway-boundary-test-report.md +0 -164
@@ -6,6 +6,7 @@ import { V2Session } from '../v2/session/index.js';
6
6
  import { isJsonObject } from '../types.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
+ const V2_GROUP_STALE_BOOTSTRAP_CODE = -33054;
9
10
  const MEMBERSHIP_ACTIONS = new Set([
10
11
  'member_added',
11
12
  'member_left',
@@ -17,6 +18,18 @@ const MEMBERSHIP_ACTIONS = new Set([
17
18
  'invite_code_used',
18
19
  ]);
19
20
  const JOIN_ACTIONS = new Set(['member_added', 'joined', 'join_approved', 'invite_code_used']);
21
+ function v2PullTailDelayMs(msgCount, pageLimit) {
22
+ const count = Math.trunc(Number(msgCount || 0));
23
+ const limit = Math.trunc(Number(pageLimit || 0));
24
+ if (count <= 0 || (limit > 0 && count >= limit))
25
+ return 0;
26
+ return Math.min(500, Math.max(5, 1000 / (count + 1)));
27
+ }
28
+ function v2Sleep(ms) {
29
+ return new Promise((resolve) => {
30
+ globalThis.setTimeout(resolve, ms);
31
+ });
32
+ }
20
33
  function pubDerMatchesFingerprint(pubDer, certFingerprint) {
21
34
  const expected = String(certFingerprint ?? '').trim().toLowerCase();
22
35
  if (!expected)
@@ -134,6 +147,7 @@ function applyV2WrapPolicyToTargets(targets, policy) {
134
147
  export class V2E2EECoordinator {
135
148
  runtime;
136
149
  _initSessionPromise = null;
150
+ targetSetCache = new Map();
137
151
  constructor(runtime) {
138
152
  this.runtime = runtime;
139
153
  }
@@ -155,17 +169,49 @@ export class V2E2EECoordinator {
155
169
  }
156
170
  deleteBootstrapCacheEntry(key) {
157
171
  this.bootstrapCache.delete(key);
172
+ this.targetSetCache.delete(key);
158
173
  }
159
174
  clearBootstrapCache() {
160
175
  this.bootstrapCache.clear();
176
+ this.targetSetCache.clear();
161
177
  }
162
178
  pruneExpiredBootstrapCache(ttlMs, now = Date.now()) {
163
179
  for (const [key, entry] of this.bootstrapCache) {
164
180
  if (now - Number(entry.cachedAt ?? 0) >= ttlMs) {
165
181
  this.bootstrapCache.delete(key);
182
+ this.targetSetCache.delete(key);
183
+ }
184
+ }
185
+ for (const [key, entry] of this.targetSetCache) {
186
+ if (now - Number(entry.cachedAt ?? 0) >= ttlMs) {
187
+ this.targetSetCache.delete(key);
166
188
  }
167
189
  }
168
190
  }
191
+ getTargetSetCache(key) {
192
+ const cached = this.targetSetCache.get(key);
193
+ if (!cached || Date.now() - cached.cachedAt >= V2_BOOTSTRAP_TTL_MS) {
194
+ if (cached)
195
+ this.targetSetCache.delete(key);
196
+ return undefined;
197
+ }
198
+ return cached.value;
199
+ }
200
+ setTargetSetCache(key, value) {
201
+ this.targetSetCache.set(key, { cachedAt: Date.now(), value });
202
+ return value;
203
+ }
204
+ cloneP2PTargetSet(value) {
205
+ return {
206
+ targets: value.targets.map((target) => ({ ...target })),
207
+ auditRecipients: value.auditRecipients.map((target) => ({ ...target })),
208
+ };
209
+ }
210
+ cloneGroupTargetSet(value) {
211
+ return {
212
+ targets: value.targets.map((target) => ({ ...target })),
213
+ };
214
+ }
169
215
  async onConnected(opts) {
170
216
  const client = this.client;
171
217
  try {
@@ -453,6 +499,7 @@ export class V2E2EECoordinator {
453
499
  client._clientLog.debug(`message.v2.bootstrap cache hit: to=${to}, devices=${peerDevices.length}, audit=${auditRaw.length}`);
454
500
  }
455
501
  else {
502
+ this.targetSetCache.delete(to);
456
503
  const bs = await client.call('message.v2.bootstrap', {
457
504
  peer_aid: to,
458
505
  e2ee_wrap_capabilities: v2WrapCapabilities(),
@@ -464,7 +511,7 @@ export class V2E2EECoordinator {
464
511
  peerDevices = (Array.isArray(bs?.peer_devices) ? bs.peer_devices : []);
465
512
  auditRaw = (Array.isArray(bs?.audit_recipients) ? bs.audit_recipients : []);
466
513
  client._clientLog.debug(`message.v2.bootstrap fetched: to=${to}, devices=${peerDevices.length}, audit=${auditRaw.length}`);
467
- if (peerDevices.length > 0 && !this.getBootstrapCacheEntry(to)) {
514
+ if (peerDevices.length > 0) {
468
515
  this.setBootstrapCacheEntry(to, {
469
516
  devices: peerDevices,
470
517
  auditRecipients: auditRaw,
@@ -476,83 +523,92 @@ export class V2E2EECoordinator {
476
523
  if (peerDevices.length === 0) {
477
524
  throw new E2EEError(`V2 bootstrap: no devices found for ${to}`);
478
525
  }
479
- const targets = [];
480
- for (const dev of peerDevices) {
481
- const devId = getV2DeviceId(dev);
482
- const target = await client._v2BuildTargetFromDevice({
483
- dev,
484
- aid: to,
485
- deviceId: devId.value,
486
- role: 'peer',
487
- defaultKeySource: 'peer_device_prekey',
488
- });
489
- if (target)
490
- targets.push(target);
491
- }
492
- const auditTargets = [];
493
- for (const dev of auditRaw) {
494
- const target = await client._v2BuildTargetFromDevice({
495
- dev,
496
- aid: String(dev.aid ?? ''),
497
- deviceId: String(dev.device_id ?? ''),
498
- role: 'audit',
499
- defaultKeySource: 'peer_device_prekey',
500
- });
501
- if (target)
502
- auditTargets.push(target);
526
+ let targetSet = useCache ? this.getTargetSetCache(to) : undefined;
527
+ if (targetSet) {
528
+ targetSet = this.cloneP2PTargetSet(targetSet);
503
529
  }
504
- if (client._aid && client._aid !== to) {
505
- try {
506
- const selfCached = this.getBootstrapCacheEntry(client._aid);
507
- let selfDevices = [];
508
- if (selfCached && Date.now() - Number(selfCached.cachedAt ?? 0) < V2_BOOTSTRAP_TTL_MS) {
509
- selfDevices = (selfCached.devices ?? []);
510
- }
511
- else {
512
- const selfBs = await client.call('message.v2.bootstrap', {
513
- peer_aid: client._aid,
514
- e2ee_wrap_capabilities: v2WrapCapabilities(),
515
- });
516
- if (typeof client._primeBootstrapPeerCerts === 'function') {
517
- await client._primeBootstrapPeerCerts(selfBs, client._aid);
530
+ else {
531
+ const targets = [];
532
+ for (const dev of peerDevices) {
533
+ const devId = getV2DeviceId(dev);
534
+ const target = await client._v2BuildTargetFromDevice({
535
+ dev,
536
+ aid: to,
537
+ deviceId: devId.value,
538
+ role: 'peer',
539
+ defaultKeySource: 'peer_device_prekey',
540
+ });
541
+ if (target)
542
+ targets.push(target);
543
+ }
544
+ const auditTargets = [];
545
+ for (const dev of auditRaw) {
546
+ const target = await client._v2BuildTargetFromDevice({
547
+ dev,
548
+ aid: String(dev.aid ?? ''),
549
+ deviceId: String(dev.device_id ?? ''),
550
+ role: 'audit',
551
+ defaultKeySource: 'peer_device_prekey',
552
+ });
553
+ if (target)
554
+ auditTargets.push(target);
555
+ }
556
+ if (client._aid && client._aid !== to) {
557
+ try {
558
+ const selfCached = this.getBootstrapCacheEntry(client._aid);
559
+ let selfDevices = [];
560
+ if (selfCached && Date.now() - Number(selfCached.cachedAt ?? 0) < V2_BOOTSTRAP_TTL_MS) {
561
+ selfDevices = (selfCached.devices ?? []);
562
+ }
563
+ else {
564
+ this.targetSetCache.delete(client._aid);
565
+ const selfBs = await client.call('message.v2.bootstrap', {
566
+ peer_aid: client._aid,
567
+ e2ee_wrap_capabilities: v2WrapCapabilities(),
568
+ });
569
+ if (typeof client._primeBootstrapPeerCerts === 'function') {
570
+ await client._primeBootstrapPeerCerts(selfBs, client._aid);
571
+ }
572
+ selfDevices = (Array.isArray(selfBs?.peer_devices) ? selfBs.peer_devices : []);
573
+ const selfWrapPolicy = normalizeV2WrapPolicy(selfBs.e2ee_wrap_policy);
574
+ if (selfDevices.length > 0) {
575
+ this.setBootstrapCacheEntry(client._aid, {
576
+ devices: selfDevices,
577
+ auditRecipients: [],
578
+ cachedAt: Date.now(),
579
+ wrapPolicy: selfWrapPolicy,
580
+ });
581
+ }
518
582
  }
519
- selfDevices = (Array.isArray(selfBs?.peer_devices) ? selfBs.peer_devices : []);
520
- const selfWrapPolicy = normalizeV2WrapPolicy(selfBs.e2ee_wrap_policy);
521
- if (selfDevices.length > 0) {
522
- this.setBootstrapCacheEntry(client._aid, {
523
- devices: selfDevices,
524
- auditRecipients: [],
525
- cachedAt: Date.now(),
526
- wrapPolicy: selfWrapPolicy,
583
+ for (const dev of selfDevices) {
584
+ const devId = getV2DeviceId(dev);
585
+ if (!devId.present || devId.value === client._deviceId)
586
+ continue;
587
+ const target = await client._v2BuildTargetFromDevice({
588
+ dev,
589
+ aid: client._aid,
590
+ deviceId: devId.value,
591
+ role: 'self_sync',
592
+ defaultKeySource: 'peer_device_prekey',
527
593
  });
594
+ if (target)
595
+ targets.push(target);
528
596
  }
529
597
  }
530
- for (const dev of selfDevices) {
531
- const devId = getV2DeviceId(dev);
532
- if (!devId.present || devId.value === client._deviceId)
533
- continue;
534
- const target = await client._v2BuildTargetFromDevice({
535
- dev,
536
- aid: client._aid,
537
- deviceId: devId.value,
538
- role: 'self_sync',
539
- defaultKeySource: 'peer_device_prekey',
540
- });
541
- if (target)
542
- targets.push(target);
598
+ catch (exc) {
599
+ client._clientLog.debug(`V2 self-sync bootstrap failed (non-fatal): ${formatE2EEError(exc)}`);
543
600
  }
544
601
  }
545
- catch (exc) {
546
- client._clientLog.debug(`V2 self-sync bootstrap failed (non-fatal): ${formatE2EEError(exc)}`);
547
- }
602
+ targetSet = this.setTargetSetCache(to, {
603
+ targets: applyV2WrapPolicyToTargets(targets, wrapPolicy),
604
+ auditRecipients: applyV2WrapPolicyToTargets(auditTargets, wrapPolicy),
605
+ });
606
+ targetSet = this.cloneP2PTargetSet(targetSet);
548
607
  }
549
- if (targets.length === 0) {
608
+ if (targetSet.targets.length === 0) {
550
609
  throw new E2EEError(`V2 bootstrap: no usable devices found for ${to}`);
551
610
  }
552
- const envelope = encryptP2PMessage(session.getSenderIdentity(), {
553
- targets: applyV2WrapPolicyToTargets(targets, wrapPolicy),
554
- auditRecipients: applyV2WrapPolicyToTargets(auditTargets, wrapPolicy),
555
- }, opts.payload, {
611
+ const envelope = encryptP2PMessage(session.getSenderIdentity(), targetSet, opts.payload, {
556
612
  messageId: opts.messageId,
557
613
  timestamp: opts.timestamp,
558
614
  protectedHeaders: opts.protectedHeaders,
@@ -569,8 +625,8 @@ export class V2E2EECoordinator {
569
625
  payloadOverride: envelope,
570
626
  extra: {
571
627
  plaintext_payload: opts.payload,
572
- target_count: targets.length,
573
- audit_count: auditTargets.length,
628
+ target_count: targetSet.targets.length,
629
+ audit_count: targetSet.auditRecipients.length,
574
630
  use_cache: useCache,
575
631
  },
576
632
  });
@@ -652,17 +708,29 @@ export class V2E2EECoordinator {
652
708
  const decrypted = [];
653
709
  let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
654
710
  let pageCount = 0;
711
+ let lastAutoAckSeq = 0;
712
+ let pendingAckSeq = 0;
655
713
  const maxPages = 100;
656
714
  while (pageCount < maxPages) {
657
715
  pageCount += 1;
658
- client._clientLog.debug(`message.v2.pull page request: page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ns=${ns || '<none>'}`);
716
+ const ackUpToSeq = opts?.skipAutoAck ? 0 : pendingAckSeq;
717
+ client._clientLog.debug(`message.v2.pull page request: page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns || '<none>'}`);
659
718
  const result = await client._callRawV2Rpc('message.v2.pull', {
660
719
  after_seq: nextAfterSeq,
661
720
  limit,
662
721
  ...(opts?.force ? { force: true } : {}),
722
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
723
+ });
724
+ if (ackUpToSeq > 0) {
725
+ pendingAckSeq = 0;
726
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
727
+ }
728
+ 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;
663
732
  });
664
- const messages = (Array.isArray(result?.messages) ? result.messages : []);
665
- client._clientLog.debug(`message.v2.pull page response: page=${pageCount}, raw_count=${messages.length}, has_more=${String(result.has_more ?? '')}, server_ack_seq=${String(result.server_ack_seq ?? '')}`);
733
+ 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 ?? '')}`);
666
734
  for (const msg of messages) {
667
735
  client._logMessageDebug('pull-raw', 'message.v2.pull', 'message.received', msg);
668
736
  }
@@ -752,24 +820,47 @@ export class V2E2EECoordinator {
752
820
  const contigAdvanced = ackSeq !== pageContigBefore;
753
821
  if (contigAdvanced) {
754
822
  await client._drainOrderedMessages(ns, undefined, true);
755
- client._saveSeqTrackerState();
823
+ client._persistSeq(ns);
756
824
  }
757
- const ackNeeded = messages.length > 0
825
+ const ackNeeded = pageMessages.length > 0
758
826
  && ackSeq > 0
827
+ && ackSeq > lastAutoAckSeq
759
828
  && !opts?.skipAutoAck
760
829
  && (contigAdvanced || (hasServerAckSeq && ackSeq > serverAckSeq));
761
830
  if (ackNeeded) {
762
- client._clientLog.debug(`message.v2.pull sending auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
763
- await this.ackV2(ackSeq);
831
+ const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
832
+ const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq;
833
+ if (canContinuePage) {
834
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
835
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
836
+ client._clientLog.debug(`message.v2.pull queued piggyback auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
837
+ }
838
+ else {
839
+ client._clientLog.debug(`message.v2.pull sending terminal auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
840
+ await this.ackV2(ackSeq);
841
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
842
+ }
764
843
  }
765
844
  }
766
845
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
767
- if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
846
+ const rawCount = messages.length;
847
+ const canContinuePage = rawCount > 0 && nextAfter > nextAfterSeq;
848
+ const fullPage = canContinuePage && limit > 0 && rawCount >= limit;
849
+ const shouldContinue = canContinuePage && (fullPage || pendingAckSeq > 0);
850
+ if (!shouldContinue)
768
851
  break;
852
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
853
+ if (tailDelayMs > 0)
854
+ await v2Sleep(tailDelayMs);
769
855
  nextAfterSeq = nextAfter;
770
856
  }
771
857
  if (pageCount >= maxPages) {
772
858
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
859
+ if (pendingAckSeq > 0) {
860
+ client._clientLog.debug(`message.v2.pull flushing pending auto-ack after max_pages: ns=${ns}, ack_seq=${pendingAckSeq}`);
861
+ await this.ackV2(pendingAckSeq);
862
+ pendingAckSeq = 0;
863
+ }
773
864
  }
774
865
  client._clientLog.debug(`message.v2.pull done: requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns || '<none>'}`);
775
866
  return decrypted;
@@ -895,7 +986,8 @@ export class V2E2EECoordinator {
895
986
  if (!client._v2Session)
896
987
  throw new StateError('V2 session not initialized');
897
988
  const session = client._v2Session;
898
- const groupId = normalizeGroupId(opts.groupId) || String(opts.groupId ?? '').trim();
989
+ const rawGroupId = String(opts.groupId ?? '').trim();
990
+ const groupId = normalizeGroupId(rawGroupId) || rawGroupId;
899
991
  if (!groupId)
900
992
  throw new ValidationError("group.send requires 'group_id'");
901
993
  const cacheKey = `group:${groupId}`;
@@ -905,16 +997,27 @@ export class V2E2EECoordinator {
905
997
  let epoch = 0;
906
998
  let stateCommitment = { state_version: 0, state_hash: '', state_chain: '' };
907
999
  let wrapPolicy = normalizeV2WrapPolicy(undefined);
908
- const cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : undefined;
1000
+ 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
+ }
909
1010
  if (cached && Date.now() - Number(cached.cachedAt ?? 0) < V2_BOOTSTRAP_TTL_MS) {
910
1011
  allDevices = (cached.devices ?? []);
911
1012
  auditRecipientsRaw = (cached.auditRecipients ?? []);
912
1013
  epoch = Number(cached.epoch ?? 0) || 0;
913
1014
  stateCommitment = cached.stateCommitment ?? stateCommitment;
914
1015
  wrapPolicy = cached.wrapPolicy ?? wrapPolicy;
1016
+ usedCachedBootstrap = true;
915
1017
  client._clientLog.debug(`group.v2.bootstrap cache hit: group=${groupId}, devices=${allDevices.length}, audit=${auditRecipientsRaw.length}, epoch=${epoch}, state_version=${stateCommitment.state_version}`);
916
1018
  }
917
1019
  else {
1020
+ this.targetSetCache.delete(cacheKey);
918
1021
  const bs = await client.call('group.v2.bootstrap', {
919
1022
  group_id: groupId,
920
1023
  e2ee_wrap_capabilities: v2WrapCapabilities(),
@@ -951,38 +1054,63 @@ export class V2E2EECoordinator {
951
1054
  if (allDevices.length === 0) {
952
1055
  throw new E2EEError(`V2 group bootstrap: no devices found for group ${groupId}`);
953
1056
  }
954
- const targets = [];
955
- for (const dev of allDevices) {
956
- const devAid = String(dev.aid ?? '').trim();
957
- const devId = getV2DeviceId(dev);
958
- if (devAid === client._aid && devId.present && devId.value === client._deviceId)
959
- continue;
960
- const role = devAid === client._aid ? 'self_sync' : 'member';
961
- const target = await client._v2BuildTargetFromDevice({
962
- dev,
963
- aid: devAid,
964
- deviceId: devId.value,
965
- role,
966
- defaultKeySource: 'peer_device_prekey',
1057
+ let targetSet = useCache ? this.getTargetSetCache(cacheKey) : undefined;
1058
+ if (targetSet) {
1059
+ targetSet = this.cloneGroupTargetSet(targetSet);
1060
+ }
1061
+ else {
1062
+ const targets = [];
1063
+ for (const dev of allDevices) {
1064
+ const devAid = String(dev.aid ?? '').trim();
1065
+ const devId = getV2DeviceId(dev);
1066
+ if (devAid === client._aid && devId.present && devId.value === client._deviceId)
1067
+ continue;
1068
+ const role = devAid === client._aid ? 'self_sync' : 'member';
1069
+ const target = await client._v2BuildTargetFromDevice({
1070
+ dev,
1071
+ aid: devAid,
1072
+ deviceId: devId.value,
1073
+ role,
1074
+ defaultKeySource: 'peer_device_prekey',
1075
+ });
1076
+ if (target)
1077
+ targets.push(target);
1078
+ }
1079
+ if (targets.length === 0) {
1080
+ if (usedCachedBootstrap) {
1081
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
1082
+ code: V2_GROUP_STALE_BOOTSTRAP_CODE,
1083
+ data: { expected_peer_aids: [] },
1084
+ });
1085
+ }
1086
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}`);
1087
+ }
1088
+ for (const dev of auditRecipientsRaw) {
1089
+ const target = await client._v2BuildTargetFromDevice({
1090
+ dev,
1091
+ aid: String(dev.aid ?? ''),
1092
+ deviceId: String(dev.device_id ?? ''),
1093
+ role: 'audit',
1094
+ defaultKeySource: 'peer_device_prekey',
1095
+ });
1096
+ if (target)
1097
+ targets.push(target);
1098
+ }
1099
+ targetSet = this.setTargetSetCache(cacheKey, {
1100
+ targets: applyV2WrapPolicyToTargets(targets, wrapPolicy),
967
1101
  });
968
- if (target)
969
- targets.push(target);
1102
+ targetSet = this.cloneGroupTargetSet(targetSet);
970
1103
  }
971
- if (targets.length === 0) {
1104
+ if (targetSet.targets.length === 0) {
1105
+ if (usedCachedBootstrap) {
1106
+ throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
1107
+ code: V2_GROUP_STALE_BOOTSTRAP_CODE,
1108
+ data: { expected_peer_aids: [] },
1109
+ });
1110
+ }
972
1111
  throw new E2EEError(`V2 group: no target devices for group ${groupId}`);
973
1112
  }
974
- for (const dev of auditRecipientsRaw) {
975
- const target = await client._v2BuildTargetFromDevice({
976
- dev,
977
- aid: String(dev.aid ?? ''),
978
- deviceId: String(dev.device_id ?? ''),
979
- role: 'audit',
980
- defaultKeySource: 'peer_device_prekey',
981
- });
982
- if (target)
983
- targets.push(target);
984
- }
985
- const envelope = encryptGroupMessage(session.getSenderIdentity(), groupId, epoch, applyV2WrapPolicyToTargets(targets, wrapPolicy), opts.payload, {
1113
+ const envelope = encryptGroupMessage(session.getSenderIdentity(), groupId, epoch, targetSet.targets, opts.payload, {
986
1114
  messageId: opts.messageId,
987
1115
  timestamp: opts.timestamp,
988
1116
  protectedHeaders: opts.protectedHeaders,
@@ -1000,7 +1128,7 @@ export class V2E2EECoordinator {
1000
1128
  extra: {
1001
1129
  plaintext_payload: opts.payload,
1002
1130
  epoch,
1003
- target_count: targets.length,
1131
+ target_count: targetSet.targets.length,
1004
1132
  audit_count: auditRecipientsRaw.length,
1005
1133
  state_version: stateCommitment.state_version,
1006
1134
  use_cache: useCache,
@@ -1026,23 +1154,37 @@ export class V2E2EECoordinator {
1026
1154
  }));
1027
1155
  }
1028
1156
  const decrypted = [];
1157
+ const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
1029
1158
  const cursorParams = opts?.cursorParams ?? {};
1030
1159
  const ownsCursor = opts?.ownsCursor !== false;
1031
1160
  let nextAfterSeq = opts?.explicitAfterSeq ? afterSeq : (afterSeq || client._seqTracker.getContiguousSeq(ns));
1032
1161
  let pageCount = 0;
1162
+ let lastAutoAckSeq = 0;
1163
+ let pendingAckSeq = 0;
1033
1164
  const maxPages = 100;
1034
1165
  while (pageCount < maxPages) {
1035
1166
  pageCount += 1;
1036
- client._clientLog.debug(`group.v2.pull page request: group=${gid}, page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ns=${ns}`);
1167
+ const ackUpToSeq = ownsCursor ? pendingAckSeq : 0;
1168
+ client._clientLog.debug(`group.v2.pull page request: group=${gid}, page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns}`);
1037
1169
  const result = await client._callRawV2Rpc('group.v2.pull', {
1038
- group_id: gid,
1170
+ group_id: wireGroupId,
1171
+ group_aid: gid,
1039
1172
  after_seq: nextAfterSeq,
1040
1173
  limit,
1041
1174
  ...cursorParams,
1175
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1176
+ });
1177
+ if (ackUpToSeq > 0) {
1178
+ pendingAckSeq = 0;
1179
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1180
+ }
1181
+ 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;
1042
1185
  });
1043
- const messages = (Array.isArray(result.messages) ? result.messages : []);
1044
1186
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1045
- client._clientLog.debug(`group.v2.pull page response: group=${gid}, page=${pageCount}, raw_count=${messages.length}, has_more=${String(result.has_more ?? '')}, cursor_current=${String(cursor?.current_seq ?? '')}`);
1187
+ 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 ?? '')}`);
1046
1188
  for (const msg of messages) {
1047
1189
  client._logMessageDebug('pull-raw', 'group.v2.pull', 'group.message_created', msg);
1048
1190
  }
@@ -1139,25 +1281,48 @@ export class V2E2EECoordinator {
1139
1281
  const contigAdvanced = ackSeq !== pageContigBefore;
1140
1282
  if (contigAdvanced) {
1141
1283
  await client._drainOrderedMessages(ns, undefined, true);
1142
- client._saveSeqTrackerState();
1284
+ client._persistSeq(ns);
1143
1285
  }
1144
- const ackNeeded = messages.length > 0
1286
+ const ackNeeded = pageMessages.length > 0
1145
1287
  && ackSeq > 0
1288
+ && ackSeq > lastAutoAckSeq
1146
1289
  && ownsCursor
1147
1290
  && (contigAdvanced || (hasServerCursor && ackSeq > cursorCurrentSeq));
1148
1291
  if (ackNeeded) {
1149
- client._clientLog.debug(`group.v2.pull sending auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
1150
- await this.ackGroupV2(gid, ackSeq);
1292
+ const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1293
+ const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
1294
+ if (canContinuePage) {
1295
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1296
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1297
+ client._clientLog.debug(`group.v2.pull queued piggyback auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
1298
+ }
1299
+ else {
1300
+ client._clientLog.debug(`group.v2.pull sending terminal auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
1301
+ await this.ackGroupV2(gid, ackSeq);
1302
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1303
+ }
1151
1304
  }
1152
1305
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
1153
1306
  if (!ownsCursor)
1154
1307
  break;
1155
- if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
1308
+ const rawCount = messages.length;
1309
+ const canContinuePage = rawCount > 0 && nextAfter > nextAfterSeq;
1310
+ const fullPage = canContinuePage && limit > 0 && rawCount >= limit;
1311
+ const shouldContinue = canContinuePage && (fullPage || pendingAckSeq > 0);
1312
+ if (!shouldContinue)
1156
1313
  break;
1314
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1315
+ if (tailDelayMs > 0)
1316
+ await v2Sleep(tailDelayMs);
1157
1317
  nextAfterSeq = nextAfter;
1158
1318
  }
1159
1319
  if (pageCount >= maxPages) {
1160
1320
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
1321
+ if (pendingAckSeq > 0) {
1322
+ client._clientLog.debug(`group.v2.pull flushing pending auto-ack after max_pages: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}`);
1323
+ await this.ackGroupV2(gid, pendingAckSeq);
1324
+ pendingAckSeq = 0;
1325
+ }
1161
1326
  }
1162
1327
  client._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
1163
1328
  return decrypted;
@@ -1175,7 +1340,7 @@ export class V2E2EECoordinator {
1175
1340
  }
1176
1341
  seq = client._clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
1177
1342
  client._clientLog.debug(`group.v2.ack send: group=${gid}, ns=${ns}, up_to_seq=${seq}`);
1178
- const result = await client._callRawV2Rpc('group.v2.ack', { group_id: gid, up_to_seq: seq });
1343
+ const result = await client._callRawV2Rpc('group.v2.ack', { group_id: gid, group_aid: gid, up_to_seq: seq });
1179
1344
  client._clientLog.debug(`group.v2.ack ok: group=${gid}, ns=${ns}, requested=${seq}, result=${client._debugJson(result)}`);
1180
1345
  return result;
1181
1346
  }
@@ -1698,26 +1863,5 @@ export class V2E2EECoordinator {
1698
1863
  this.scheduleGroupSpkRotation(groupId, { reason: `group_changed:${action}` });
1699
1864
  }
1700
1865
  }
1701
- async handleV2EpochRotated(data) {
1702
- const client = this.client;
1703
- if (!data || typeof data !== 'object' || Array.isArray(data))
1704
- return;
1705
- const d = data;
1706
- const groupId = String(d.group_id ?? '').trim();
1707
- if (!groupId)
1708
- return;
1709
- const epoch = d.epoch ?? 0;
1710
- client._clientLog.debug(`_onV2EpochRotated: group=${groupId} epoch=${String(epoch)}`);
1711
- this.deleteBootstrapCacheEntry(`group:${groupId}`);
1712
- if (!client._v2Session)
1713
- return;
1714
- try {
1715
- await client._v2Session.rotateSPK(client._v2CallFn());
1716
- client._clientLog.info(`SPK rotated after V2 epoch change: group=${groupId} epoch=${String(epoch)}`);
1717
- }
1718
- catch (exc) {
1719
- client._clientLog.debug(`SPK rotation after V2 epoch change failed (non-fatal): ${formatE2EEError(exc)}`);
1720
- }
1721
- }
1722
1866
  }
1723
1867
  //# sourceMappingURL=v2-e2ee.js.map