@awiki/dsh-plugin 0.3.5 → 0.3.7-dsh-test.20260831.1

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 (73) hide show
  1. package/README.md +45 -32
  2. package/README.zh.md +33 -28
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/cordis.patch.yml +3 -1
  5. package/lib/client.js +1348 -637
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +989 -393
  8. package/lib/provider.js +57 -16
  9. package/lib/{sdk-adapter-CDLTgHJA.mjs → sdk-adapter-FGaOi8It.mjs} +167 -50
  10. package/lib/typert.host.js +411 -128
  11. package/lib/typert.remote-client.d.ts +19 -5
  12. package/lib/typert.remote-client.d.ts.map +1 -1
  13. package/lib/typert.remote-client.js +411 -128
  14. package/lib/types/client/AwikiDevices.d.ts +28 -0
  15. package/lib/types/client/AwikiDevices.d.ts.map +1 -0
  16. package/lib/types/client/AwikiDevices.js +85 -0
  17. package/lib/types/client/AwikiDevices.js.map +1 -0
  18. package/lib/types/client/AwikiIdentityAccess.d.ts +6 -5
  19. package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
  20. package/lib/types/client/AwikiIdentityAccess.js +88 -27
  21. package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
  22. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  23. package/lib/types/client/AwikiOverlay.js +5 -12
  24. package/lib/types/client/AwikiOverlay.js.map +1 -1
  25. package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -1
  26. package/lib/types/client/AwikiRecoveryForm.js +1 -1
  27. package/lib/types/client/AwikiRecoveryForm.js.map +1 -1
  28. package/lib/types/client/controller.d.ts +20 -28
  29. package/lib/types/client/controller.d.ts.map +1 -1
  30. package/lib/types/client/controller.js +41 -162
  31. package/lib/types/client/controller.js.map +1 -1
  32. package/lib/types/client/index.d.ts.map +1 -1
  33. package/lib/types/client/index.js +8 -2
  34. package/lib/types/client/index.js.map +1 -1
  35. package/lib/types/client/slots.d.ts +10 -6
  36. package/lib/types/client/slots.d.ts.map +1 -1
  37. package/lib/types/conversation-preferences.d.ts.map +1 -1
  38. package/lib/types/conversation-preferences.js +0 -17
  39. package/lib/types/conversation-preferences.js.map +1 -1
  40. package/lib/types/index.d.ts +54 -13
  41. package/lib/types/index.d.ts.map +1 -1
  42. package/lib/types/index.js +739 -221
  43. package/lib/types/index.js.map +1 -1
  44. package/lib/types/listener-state.d.ts +5 -3
  45. package/lib/types/listener-state.d.ts.map +1 -1
  46. package/lib/types/listener-state.js +28 -13
  47. package/lib/types/listener-state.js.map +1 -1
  48. package/lib/types/listener.d.ts +9 -32
  49. package/lib/types/listener.d.ts.map +1 -1
  50. package/lib/types/listener.js +11 -127
  51. package/lib/types/listener.js.map +1 -1
  52. package/lib/types/provider-api.d.ts +99 -7
  53. package/lib/types/provider-api.d.ts.map +1 -1
  54. package/lib/types/provider.d.ts +7 -1
  55. package/lib/types/provider.d.ts.map +1 -1
  56. package/lib/types/provider.js +58 -15
  57. package/lib/types/provider.js.map +1 -1
  58. package/lib/types/realtime-supervisor.d.ts +54 -0
  59. package/lib/types/realtime-supervisor.d.ts.map +1 -0
  60. package/lib/types/realtime-supervisor.js +242 -0
  61. package/lib/types/realtime-supervisor.js.map +1 -0
  62. package/lib/types/sdk-adapter.d.ts +31 -5
  63. package/lib/types/sdk-adapter.d.ts.map +1 -1
  64. package/lib/types/sdk-adapter.js +162 -59
  65. package/lib/types/sdk-adapter.js.map +1 -1
  66. package/lib/types/types.d.ts +76 -27
  67. package/lib/types/types.d.ts.map +1 -1
  68. package/lib/types/types.js.map +1 -1
  69. package/package.json +5 -2
  70. package/lib/types/profile-state.d.ts +0 -11
  71. package/lib/types/profile-state.d.ts.map +0 -1
  72. package/lib/types/profile-state.js +0 -48
  73. package/lib/types/profile-state.js.map +0 -1
package/lib/index.js CHANGED
@@ -1,18 +1,16 @@
1
- import { n as downloadedAttachment } from "./sdk-adapter-CDLTgHJA.mjs";
1
+ import { n as downloadedAttachment } from "./sdk-adapter-FGaOi8It.mjs";
2
2
  import "@deepseek-ai/cordis";
3
- import { existsSync } from "node:fs";
3
+ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
4
4
  import { homedir } from "node:os";
5
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { isAbsolute, join } from "node:path";
6
6
  import z from "@deepseek-ai/schemastery";
7
7
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
8
8
  import { SettingsConflictError, settingsNamespace } from "@deepseek-ai/dsh-settings";
9
9
  import { defineTool } from "@deepseek-ai/dsh-tools";
10
10
  import { chmod, lstat, mkdir, readFile, readdir, rename, rm, unlink, utimes, writeFile } from "node:fs/promises";
11
- import { createHash, randomUUID } from "node:crypto";
12
11
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
13
12
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
14
13
  import { SessionId } from "@deepseek-ai/dsh-session";
15
- import { fileURLToPath } from "node:url";
16
14
  //#region lib/types/types.js
17
15
  /** Client-safe AWiki service and Remote data types. */
18
16
  /** Exact browser acknowledgement required before locally signing out. */
@@ -1180,7 +1178,6 @@ const MAX_IDENTIFIER_CHARACTERS = 2048;
1180
1178
  const MAX_TITLE_CHARACTERS = 1024;
1181
1179
  const MAX_PREVIEW_CHARACTERS = 4096;
1182
1180
  const MAX_HANDLE_CHARACTERS = 512;
1183
- const MAX_RECOVERY_SIGNATURE_CHARACTERS = 128;
1184
1181
  function invalidState() {
1185
1182
  throw new TypeError("awiki: conversation preferences are invalid");
1186
1183
  }
@@ -1266,22 +1263,17 @@ function preferences(value, expectedOwnerDid) {
1266
1263
  if (seen.has(hidden.conversation.id)) invalidState();
1267
1264
  seen.add(hidden.conversation.id);
1268
1265
  }
1269
- const dismissedGroupRecoverySignature = optionalString(value.dismissedGroupRecoverySignature, MAX_RECOVERY_SIGNATURE_CHARACTERS);
1270
1266
  return {
1271
1267
  version: STORE_VERSION,
1272
1268
  ownerDid: String(expectedOwnerDid),
1273
- hiddenConversations,
1274
- ...dismissedGroupRecoverySignature === void 0 ? {} : { dismissedGroupRecoverySignature }
1269
+ hiddenConversations
1275
1270
  };
1276
1271
  }
1277
1272
  function publicPreferences(value) {
1278
- return {
1279
- hiddenConversations: value.hiddenConversations.map((hidden) => ({
1280
- conversation: { ...hidden.conversation },
1281
- hiddenAt: hidden.hiddenAt
1282
- })),
1283
- ...value.dismissedGroupRecoverySignature === void 0 ? {} : { dismissedGroupRecoverySignature: value.dismissedGroupRecoverySignature }
1284
- };
1273
+ return { hiddenConversations: value.hiddenConversations.map((hidden) => ({
1274
+ conversation: { ...hidden.conversation },
1275
+ hiddenAt: hidden.hiddenAt
1276
+ })) };
1285
1277
  }
1286
1278
  function emptyPreferences(ownerDid) {
1287
1279
  return {
@@ -1302,10 +1294,6 @@ function normalizeConversationPreferenceMutation(value) {
1302
1294
  action: "restore",
1303
1295
  conversationId: boundedString$1(value.conversationId, MAX_IDENTIFIER_CHARACTERS)
1304
1296
  };
1305
- if (value.action === "dismiss-group-recovery") return {
1306
- action: "dismiss-group-recovery",
1307
- signature: boundedString$1(value.signature, MAX_RECOVERY_SIGNATURE_CHARACTERS)
1308
- };
1309
1297
  } catch {
1310
1298
  return;
1311
1299
  }
@@ -1351,14 +1339,6 @@ var AwikiConversationPreferenceStore = class {
1351
1339
  };
1352
1340
  break;
1353
1341
  }
1354
- case "dismiss-group-recovery": {
1355
- const signature = normalized.signature;
1356
- next = {
1357
- ...current,
1358
- dismissedGroupRecoverySignature: signature
1359
- };
1360
- break;
1361
- }
1362
1342
  default: invalidState();
1363
1343
  }
1364
1344
  await this.write(ownerDid, next);
@@ -1450,12 +1430,13 @@ var AwikiConversationPreferenceStore = class {
1450
1430
  };
1451
1431
  //#endregion
1452
1432
  //#region lib/types/listener-state.js
1453
- const STATE_VERSION = 1;
1433
+ const STATE_VERSION = 2;
1454
1434
  const MAX_STATE_BYTES = 1048576;
1455
1435
  const MAX_CONVERSATIONS = 1e3;
1456
- function emptyState() {
1436
+ function emptyState(identityScopeHash) {
1457
1437
  return {
1458
1438
  version: STATE_VERSION,
1439
+ identityScopeHash,
1459
1440
  conversations: {}
1460
1441
  };
1461
1442
  }
@@ -1467,8 +1448,10 @@ function boundedString(value, field) {
1467
1448
  return value;
1468
1449
  }
1469
1450
  /** Validate and detach the complete state file before the listener trusts any route. */
1470
- function parseState(value) {
1471
- if (!isRecord(value) || value.version !== STATE_VERSION || !isRecord(value.conversations)) throw new TypeError("awiki: listener state is invalid");
1451
+ function parseState(value, identityScopeHash) {
1452
+ if (isRecord(value) && value.version === 1) return emptyState(identityScopeHash);
1453
+ if (isRecord(value) && value.version === STATE_VERSION && typeof value.identityScopeHash === "string" && value.identityScopeHash !== identityScopeHash) return emptyState(identityScopeHash);
1454
+ if (!isRecord(value) || value.version !== STATE_VERSION || value.identityScopeHash !== identityScopeHash || !isRecord(value.conversations)) throw new TypeError("awiki: listener state is invalid");
1472
1455
  const entries = Object.entries(value.conversations);
1473
1456
  if (entries.length > MAX_CONVERSATIONS) throw new TypeError("awiki: listener state is too large");
1474
1457
  const conversations = {};
@@ -1491,6 +1474,7 @@ function parseState(value) {
1491
1474
  }
1492
1475
  return {
1493
1476
  version: STATE_VERSION,
1477
+ identityScopeHash,
1494
1478
  conversations
1495
1479
  };
1496
1480
  }
@@ -1501,28 +1485,31 @@ function isMissing(error) {
1501
1485
  var AwikiListenerStateStore = class {
1502
1486
  hostDirectory;
1503
1487
  statePath;
1504
- constructor(stateRoot) {
1488
+ identityScopeHash;
1489
+ constructor(stateRoot, identityScope) {
1490
+ if (identityScope.length === 0 || identityScope.length > 2048) throw new TypeError("awiki: listener identity scope is invalid");
1491
+ this.identityScopeHash = createHash("sha256").update(identityScope).digest("hex");
1505
1492
  this.hostDirectory = join(stateRoot, ".host");
1506
1493
  this.statePath = join(this.hostDirectory, "listener-state.json");
1507
1494
  }
1508
- /** Load the current state or return an empty v1 document on first use. */
1495
+ /** Load the current identity scope or reset unscoped v1 state on first use. */
1509
1496
  async load() {
1510
- if (!await this.hasPrivateHostDirectory()) return emptyState();
1497
+ if (!await this.hasPrivateHostDirectory()) return emptyState(this.identityScopeHash);
1511
1498
  try {
1512
1499
  const metadata = await lstat(this.statePath);
1513
1500
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is invalid");
1514
1501
  const text = await readFile(this.statePath, "utf8");
1515
1502
  if (Buffer.byteLength(text, "utf8") > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is invalid");
1516
- return parseState(JSON.parse(text));
1503
+ return parseState(JSON.parse(text), this.identityScopeHash);
1517
1504
  } catch (error) {
1518
- if (isMissing(error)) return emptyState();
1505
+ if (isMissing(error)) return emptyState(this.identityScopeHash);
1519
1506
  if (error instanceof SyntaxError) throw new TypeError("awiki: listener state file is invalid");
1520
1507
  throw error;
1521
1508
  }
1522
1509
  }
1523
1510
  /** Replace the state atomically without ever writing message or Agent text. */
1524
1511
  async save(state) {
1525
- const snapshot = parseState(state);
1512
+ const snapshot = parseState(state, this.identityScopeHash);
1526
1513
  const text = `${JSON.stringify(snapshot)}\n`;
1527
1514
  if (Buffer.byteLength(text, "utf8") > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is too large");
1528
1515
  await mkdir(this.hostDirectory, {
@@ -1744,11 +1731,12 @@ function incomingFromPeer(message, conversation) {
1744
1731
  }
1745
1732
  function copyState(state) {
1746
1733
  return {
1747
- version: 1,
1734
+ version: 2,
1735
+ identityScopeHash: state.identityScopeHash,
1748
1736
  conversations: Object.fromEntries(Object.entries(state.conversations).map(([id, route]) => [id, { ...route }]))
1749
1737
  };
1750
1738
  }
1751
- /** Reconcile authorized Direct text from Core history whenever realtime schedules synchronization. */
1739
+ /** Consume authorized Direct text only after the identity supervisor commits synchronization. */
1752
1740
  var AwikiAgentListener = class {
1753
1741
  awiki;
1754
1742
  agents;
@@ -1757,34 +1745,23 @@ var AwikiAgentListener = class {
1757
1745
  store;
1758
1746
  logger;
1759
1747
  stateReady;
1760
- state = {
1761
- version: 1,
1762
- conversations: {}
1763
- };
1748
+ state;
1764
1749
  stateMutation = Promise.resolve();
1765
1750
  syncMutation = Promise.resolve();
1766
1751
  scheduledMessageIds = /* @__PURE__ */ new Set();
1767
1752
  conversationQueues = /* @__PURE__ */ new Map();
1768
- lifecycle;
1769
- started;
1770
- resolveStarted;
1771
- rejectStarted;
1772
- lifecycleGeneration = 0;
1773
- streamGeneration = 0;
1774
- activeRealtime;
1775
1753
  stopped = false;
1776
- termination;
1777
- resolveTermination;
1778
- terminationSettled = false;
1779
1754
  constructor(awiki, agents, config, logger, store) {
1780
1755
  this.awiki = awiki;
1781
1756
  this.agents = agents;
1782
1757
  this.config = config;
1783
- this.termination = new Promise((resolve) => {
1784
- this.resolveTermination = resolve;
1785
- });
1786
1758
  this.allowedPeers = new Set(config.allowedPeers.map((peer) => peer.startsWith("did:") ? peer : peer.toLowerCase()));
1787
- this.store = store ?? new AwikiListenerStateStore(config.stateRoot);
1759
+ this.store = store ?? new AwikiListenerStateStore(config.stateRoot, config.identityScope);
1760
+ this.state = {
1761
+ version: 2,
1762
+ identityScopeHash: this.store.identityScopeHash,
1763
+ conversations: {}
1764
+ };
1788
1765
  this.logger = logger ?? {
1789
1766
  debug() {},
1790
1767
  info() {},
@@ -1796,38 +1773,11 @@ var AwikiAgentListener = class {
1796
1773
  this.state = state;
1797
1774
  });
1798
1775
  }
1799
- /** Start canonical startup sync followed by the single Core-owned realtime stream. */
1800
- start() {
1801
- if (this.started !== void 0) return this.started;
1802
- this.started = new Promise((resolve, reject) => {
1803
- this.resolveStarted = resolve;
1804
- this.rejectStarted = reject;
1805
- });
1806
- const generation = ++this.lifecycleGeneration;
1807
- this.lifecycle = this.run(generation).then(() => {
1808
- this.finishTermination({ kind: "stopped" });
1809
- }, (error) => {
1810
- this.rejectStarted?.(error);
1811
- this.rejectStarted = void 0;
1812
- this.finishTermination({
1813
- kind: "failed",
1814
- error
1815
- });
1816
- if (!this.stopped) this.logger.warn("AWiki realtime listener stopped: %s", error instanceof Error ? error.message : "unknown failure");
1817
- });
1818
- return this.started;
1819
- }
1820
- /** Resolve once with either orderly shutdown or the exact terminal lifecycle failure. */
1821
- whenTerminated() {
1822
- return this.termination;
1823
- }
1824
- /** Deterministic canonical sync plus committed-history reconciliation for tests and recovery. */
1825
- async synchronizeOnce(reason) {
1776
+ /** Reconcile only committed history; this consumer cannot start WSS or advance sync. */
1777
+ async reconcileOnce() {
1826
1778
  await this.enqueueSync(async () => {
1827
1779
  await this.stateReady;
1828
1780
  if (this.stopped) return;
1829
- await this.awiki.syncNow(reason);
1830
- if (this.stopped) return;
1831
1781
  await this.reconcileCommittedHistory();
1832
1782
  });
1833
1783
  }
@@ -1837,84 +1787,12 @@ var AwikiAgentListener = class {
1837
1787
  while (this.conversationQueues.size > 0) await Promise.allSettled([...this.conversationQueues.values()]);
1838
1788
  await this.stateMutation;
1839
1789
  }
1840
- /** Stop realtime first, fence late events, drain messages, then release listener-owned Agents. */
1790
+ /** Fence late work, drain committed messages, then release listener-owned Agents. */
1841
1791
  async dispose() {
1842
1792
  if (this.stopped) return;
1843
1793
  this.stopped = true;
1844
- this.lifecycleGeneration += 1;
1845
- this.streamGeneration += 1;
1846
- const active = this.activeRealtime;
1847
- this.activeRealtime = void 0;
1848
- await active?.session.stop().catch(() => void 0);
1849
- await this.lifecycle;
1850
- this.resolveStarted?.();
1851
- this.resolveStarted = void 0;
1852
- this.rejectStarted = void 0;
1853
1794
  await this.whenIdle();
1854
1795
  await this.agents.dispose();
1855
- this.finishTermination({ kind: "stopped" });
1856
- }
1857
- finishTermination(result) {
1858
- if (this.terminationSettled) return;
1859
- this.terminationSettled = true;
1860
- this.resolveTermination(result);
1861
- }
1862
- currentLifecycle(generation) {
1863
- return !this.stopped && this.lifecycleGeneration === generation;
1864
- }
1865
- async run(lifecycleGeneration) {
1866
- await this.synchronize("session_start", lifecycleGeneration);
1867
- if (!this.currentLifecycle(lifecycleGeneration)) return;
1868
- let active = await this.openRealtime(lifecycleGeneration);
1869
- if (active === void 0) return;
1870
- this.resolveStarted?.();
1871
- this.resolveStarted = void 0;
1872
- this.rejectStarted = void 0;
1873
- try {
1874
- while (this.currentLifecycle(lifecycleGeneration)) {
1875
- const event = await active.session.nextEvent();
1876
- if (!this.currentLifecycle(lifecycleGeneration) || this.activeRealtime?.generation !== active.generation) return;
1877
- if (event === null) {
1878
- this.activeRealtime = void 0;
1879
- this.streamGeneration += 1;
1880
- await active.session.stop();
1881
- if (!this.currentLifecycle(lifecycleGeneration)) return;
1882
- await this.synchronize("websocket_reconnect", lifecycleGeneration);
1883
- if (!this.currentLifecycle(lifecycleGeneration)) return;
1884
- const replacement = await this.openRealtime(lifecycleGeneration);
1885
- if (replacement === void 0) return;
1886
- active = replacement;
1887
- continue;
1888
- }
1889
- if (event.kind !== "sync_required") continue;
1890
- await this.synchronize(event.cause === "reconnected" ? "websocket_reconnect" : "websocket_hint", lifecycleGeneration);
1891
- }
1892
- } finally {
1893
- if (this.activeRealtime?.generation === active.generation) this.activeRealtime = void 0;
1894
- await active.session.stop().catch(() => void 0);
1895
- }
1896
- }
1897
- async openRealtime(lifecycleGeneration) {
1898
- const session = await this.awiki.startRealtime();
1899
- if (!this.currentLifecycle(lifecycleGeneration)) {
1900
- await session.stop().catch(() => void 0);
1901
- return;
1902
- }
1903
- const active = {
1904
- generation: ++this.streamGeneration,
1905
- session
1906
- };
1907
- this.activeRealtime = active;
1908
- return active;
1909
- }
1910
- async synchronize(reason, lifecycleGeneration) {
1911
- await this.enqueueSync(async () => {
1912
- await this.stateReady;
1913
- if (!this.currentLifecycle(lifecycleGeneration)) return;
1914
- await this.awiki.syncNow(reason);
1915
- if (!this.currentLifecycle(lifecycleGeneration)) return;
1916
- await this.reconcileCommittedHistory();
1917
- });
1918
1796
  }
1919
1797
  enqueueSync(operation) {
1920
1798
  const pending = this.syncMutation.then(operation, operation);
@@ -2081,7 +1959,8 @@ var AwikiAgentListener = class {
2081
1959
  ...update.lastProcessedMessageId === void 0 ? current?.lastProcessedMessageId === void 0 ? {} : { lastProcessedMessageId: current.lastProcessedMessageId } : { lastProcessedMessageId: update.lastProcessedMessageId }
2082
1960
  };
2083
1961
  this.state = {
2084
- version: 1,
1962
+ version: 2,
1963
+ identityScopeHash: this.state.identityScopeHash,
2085
1964
  conversations: {
2086
1965
  ...this.state.conversations,
2087
1966
  [conversation.id]: route
@@ -2094,39 +1973,215 @@ var AwikiAgentListener = class {
2094
1973
  }
2095
1974
  };
2096
1975
  //#endregion
2097
- //#region lib/types/profile-state.js
2098
- /** Match the profile-name boundary enforced by the DSH launcher. */
2099
- function assertProfileName(value, source) {
2100
- if (typeof value !== "string" || value.length === 0 || value.includes("/") || value.includes("\\") || value === "." || value === ".." || value === "node_modules") throw new TypeError(`awiki: ${source} supplied an invalid profile name`);
2101
- return value;
1976
+ //#region lib/types/realtime-supervisor.js
1977
+ const RETRY_BASE_DELAY_MS = 1e3;
1978
+ const RETRY_MAX_DELAY_MS = 3e4;
1979
+ const SYNC_FAILURE_CODES = /* @__PURE__ */ new Set([
1980
+ "sync.retry.transport_unavailable",
1981
+ "sync.retry.service_unavailable",
1982
+ "sync.retry.local_state_unavailable",
1983
+ "sync.retry.local_state.actor_closed",
1984
+ "sync.retry.local_state.database_busy",
1985
+ "sync.retry.local_state.constraint_failed",
1986
+ "sync.retry.local_state.schema_unavailable",
1987
+ "sync.retry.local_state.storage_unavailable",
1988
+ "sync.retry.local_state.codec_unavailable",
1989
+ "sync.retry.local_state.other",
1990
+ "sync.retryable_failure",
1991
+ "sync.recovery_required",
1992
+ "sync.auth_revoked",
1993
+ "sync.blocked",
1994
+ "sync.unexpected_status"
1995
+ ]);
1996
+ function syncReason(cause) {
1997
+ if (cause === "session_start") return "session_start";
1998
+ return cause === "reconnected" ? "websocket_reconnect" : "websocket_hint";
2102
1999
  }
2103
- /**
2104
- * Resolve the active DSH profile without guessing it from argv or process type.
2105
- * Desktop's generation-scoped service is authoritative. Ordinary DSH has no
2106
- * corresponding service, so its Loader root is accepted only when it is the
2107
- * exact `$DSH_HOME/profiles/<name>` directory.
2108
- */
2109
- function resolveAwikiProfileName(ctx, dshHome) {
2110
- const desktopProfiles = ctx.get("desktopProfiles");
2111
- if (desktopProfiles !== void 0) return assertProfileName(desktopProfiles.current?.name, "desktopProfiles.current");
2112
- let profileDir;
2000
+ function syncFailureCode(error) {
2113
2001
  try {
2114
- if (ctx.baseUrl === void 0) return void 0;
2115
- const baseUrl = new URL(ctx.baseUrl);
2116
- if (baseUrl.protocol !== "file:") return void 0;
2117
- profileDir = resolve(fileURLToPath(baseUrl));
2002
+ if (typeof error !== "object" || error === null) return void 0;
2003
+ const value = error.realtimeFailureCode;
2004
+ if (typeof value !== "string") return void 0;
2005
+ return SYNC_FAILURE_CODES.has(value) ? value : void 0;
2118
2006
  } catch {
2119
2007
  return;
2120
2008
  }
2121
- const profilesDir = resolve(dshHome, "profiles");
2122
- if (dirname(profileDir) !== profilesDir) return void 0;
2123
- return assertProfileName(basename(profileDir), "Loader profile directory");
2124
- }
2125
- /** Resolve the profile-local default while preserving the legacy fallback. */
2126
- function resolveAwikiStateRoot(ctx, dshHome) {
2127
- const profileName = resolveAwikiProfileName(ctx, dshHome);
2128
- return profileName === void 0 ? join(dshHome, "awiki", "im-core") : join(dshHome, "awiki", profileName, "im-core");
2129
2009
  }
2010
+ /** Own the deployment identity's only WSS without knowing Workspace or Agent policy. */
2011
+ var IdentityRealtimeSupervisor = class {
2012
+ realtime;
2013
+ config;
2014
+ logger;
2015
+ lifecycle;
2016
+ generation = 0;
2017
+ activeSession;
2018
+ stoppedSessions = /* @__PURE__ */ new WeakSet();
2019
+ stopped = false;
2020
+ connected = false;
2021
+ startCount = 0;
2022
+ stopCount = 0;
2023
+ maxActiveSessionCount = 0;
2024
+ retryCount = 0;
2025
+ lifecyclePhase = "idle";
2026
+ lastFailureCode;
2027
+ lastConnectedAtMs;
2028
+ lastCommittedSyncCause;
2029
+ retryTimer;
2030
+ resolveRetry;
2031
+ constructor(realtime, config = {}, logger = {
2032
+ debug() {},
2033
+ info() {},
2034
+ warn() {},
2035
+ error() {},
2036
+ name: "awiki-realtime"
2037
+ }) {
2038
+ this.realtime = realtime;
2039
+ this.config = config;
2040
+ this.logger = logger;
2041
+ }
2042
+ /** Start in the background; identity activation must never await connectivity. */
2043
+ start() {
2044
+ if (this.lifecycle !== void 0 || this.stopped) return;
2045
+ const generation = ++this.generation;
2046
+ this.lifecycle = this.run(generation).catch((error) => {
2047
+ if (!this.stopped) this.logger.warn("AWiki realtime supervisor stopped unexpectedly: %s", error instanceof Error ? error.message : "unknown failure");
2048
+ });
2049
+ }
2050
+ diagnostics() {
2051
+ return {
2052
+ connected: this.connected,
2053
+ activeSessionCount: this.activeSession === void 0 ? 0 : 1,
2054
+ startCount: this.startCount,
2055
+ stopCount: this.stopCount,
2056
+ maxActiveSessionCount: this.maxActiveSessionCount,
2057
+ generation: this.generation,
2058
+ retryCount: this.retryCount,
2059
+ lifecyclePhase: this.lifecyclePhase,
2060
+ ...this.lastFailureCode === void 0 ? {} : { lastFailureCode: this.lastFailureCode },
2061
+ ...this.lastConnectedAtMs === void 0 ? {} : { lastConnectedAtMs: this.lastConnectedAtMs },
2062
+ ...this.lastCommittedSyncCause === void 0 ? {} : { lastCommittedSyncCause: this.lastCommittedSyncCause }
2063
+ };
2064
+ }
2065
+ /** Fence late events, wake retry sleep, stop the exact session, and join the lifecycle. */
2066
+ async dispose() {
2067
+ if (this.stopped) return;
2068
+ this.stopped = true;
2069
+ this.generation += 1;
2070
+ this.connected = false;
2071
+ this.lifecyclePhase = "stopping";
2072
+ this.wakeRetry();
2073
+ const session = this.activeSession;
2074
+ if (session !== void 0) await this.stopSession(session).catch(() => void 0);
2075
+ await this.lifecycle;
2076
+ this.lifecyclePhase = "stopped";
2077
+ }
2078
+ current(generation) {
2079
+ return !this.stopped && this.generation === generation;
2080
+ }
2081
+ async run(generation) {
2082
+ let cause = "session_start";
2083
+ let failures = 0;
2084
+ while (this.current(generation)) {
2085
+ let session;
2086
+ try {
2087
+ this.lifecyclePhase = cause === "session_start" ? "initial_sync" : "reconnect_sync";
2088
+ await this.synchronize(cause, generation);
2089
+ if (!this.current(generation)) return;
2090
+ this.lifecyclePhase = "starting";
2091
+ session = await this.realtime.startRealtime();
2092
+ if (!this.current(generation)) {
2093
+ await this.stopSession(session).catch(() => void 0);
2094
+ return;
2095
+ }
2096
+ this.activeSession = session;
2097
+ this.maxActiveSessionCount = 1;
2098
+ this.startCount += 1;
2099
+ this.connected = (await session.getStatus().catch(() => ({ connected: false }))).connected;
2100
+ this.lifecyclePhase = "connected";
2101
+ if (this.connected) this.lastConnectedAtMs = Date.now();
2102
+ this.lastFailureCode = void 0;
2103
+ failures = 0;
2104
+ this.retryCount = 0;
2105
+ while (this.current(generation) && this.activeSession === session) {
2106
+ const event = await session.nextEvent();
2107
+ if (!this.current(generation) || this.activeSession !== session) return;
2108
+ if (event === null) {
2109
+ await this.stopSession(session);
2110
+ cause = "reconnected";
2111
+ break;
2112
+ }
2113
+ this.observeConnection(event);
2114
+ if (event.kind !== "sync_required") continue;
2115
+ await this.synchronize(event.cause, generation);
2116
+ }
2117
+ } catch (error) {
2118
+ const failedPhase = this.lifecyclePhase;
2119
+ const safeSyncFailureCode = syncFailureCode(error);
2120
+ this.connected = false;
2121
+ if (session !== void 0) try {
2122
+ await this.stopSession(session);
2123
+ } catch {
2124
+ this.lastFailureCode = "stop_failed";
2125
+ }
2126
+ if (!this.current(generation)) return;
2127
+ failures += 1;
2128
+ this.retryCount = failures;
2129
+ if (this.lastFailureCode !== "stop_failed") this.lastFailureCode = failedPhase === "initial_sync" || failedPhase === "reconnect_sync" ? safeSyncFailureCode ?? "sync_failed" : failedPhase === "starting" ? "start_failed" : "session_failed";
2130
+ this.lifecyclePhase = "backoff";
2131
+ this.logger.warn("AWiki realtime lifecycle failed; retrying: %s", error instanceof Error ? error.message : "unknown failure");
2132
+ await this.waitForRetry(failures, generation);
2133
+ cause = cause === "session_start" ? "session_start" : "reconnected";
2134
+ }
2135
+ }
2136
+ }
2137
+ async synchronize(cause, generation) {
2138
+ await this.realtime.syncNow(syncReason(cause));
2139
+ if (!this.current(generation)) return;
2140
+ this.lastCommittedSyncCause = cause;
2141
+ try {
2142
+ await this.config.onSynchronized?.(cause);
2143
+ } catch (error) {
2144
+ this.logger.warn("AWiki realtime post-sync consumer failed: %s", error instanceof Error ? error.message : "unknown failure");
2145
+ }
2146
+ }
2147
+ observeConnection(event) {
2148
+ if (event.kind !== "connection_state_changed") return;
2149
+ this.connected = event.state === "connected";
2150
+ if (this.connected) {
2151
+ this.lifecyclePhase = "connected";
2152
+ this.lastConnectedAtMs = Date.now();
2153
+ this.lastFailureCode = void 0;
2154
+ }
2155
+ }
2156
+ async stopSession(session) {
2157
+ if (this.stoppedSessions.has(session)) return;
2158
+ this.stoppedSessions.add(session);
2159
+ if (this.activeSession === session) this.activeSession = void 0;
2160
+ this.connected = false;
2161
+ this.lifecyclePhase = "stopping";
2162
+ this.stopCount += 1;
2163
+ await session.stop();
2164
+ }
2165
+ waitForRetry(failures, generation) {
2166
+ if (!this.current(generation)) return Promise.resolve();
2167
+ const delay = Math.min(RETRY_BASE_DELAY_MS * 2 ** Math.min(failures - 1, 10), RETRY_MAX_DELAY_MS);
2168
+ return new Promise((resolve) => {
2169
+ const finish = () => {
2170
+ if (this.retryTimer === timer) this.retryTimer = void 0;
2171
+ if (this.resolveRetry === finish) this.resolveRetry = void 0;
2172
+ resolve();
2173
+ };
2174
+ const timer = setTimeout(finish, delay);
2175
+ this.retryTimer = timer;
2176
+ this.resolveRetry = finish;
2177
+ });
2178
+ }
2179
+ wakeRetry() {
2180
+ const timer = this.retryTimer;
2181
+ if (timer !== void 0) clearTimeout(timer);
2182
+ this.resolveRetry?.();
2183
+ }
2184
+ };
2130
2185
  //#endregion
2131
2186
  //#region lib/types/index.js
2132
2187
  /** Unified AWiki identity, messaging, attachment, Remote, and model-tool service. */
@@ -2198,6 +2253,7 @@ const Config = z.object({
2198
2253
  attachmentMaxBytes: z.number().default(DEFAULT_ATTACHMENT_MAX_BYTES),
2199
2254
  imageAttachmentCacheMaxBytes: z.number().default(DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES),
2200
2255
  pollIntervalMs: z.number().default(DEFAULT_POLL_INTERVAL_MS),
2256
+ realtimeEnabled: z.boolean().default(true),
2201
2257
  listenerEnabled: z.boolean().default(false),
2202
2258
  listenerAllowedPeers: z.array(z.string()).default([]),
2203
2259
  listenerWorkspacePath: z.string(),
@@ -2215,7 +2271,6 @@ const FAILURE_CODES = /* @__PURE__ */ new Set([
2215
2271
  "forbidden",
2216
2272
  "identity-recovery-required",
2217
2273
  "conflict",
2218
- "state-in-use",
2219
2274
  "rate-limited",
2220
2275
  "group-membership-required",
2221
2276
  "group-identity-stale",
@@ -2229,10 +2284,17 @@ const FAILURE_CODES = /* @__PURE__ */ new Set([
2229
2284
  "network",
2230
2285
  "remote"
2231
2286
  ]);
2232
- const LISTENER_RESTART_BASE_DELAY_MS = 1e3;
2233
- const LISTENER_RESTART_MAX_DELAY_MS = 3e4;
2234
- const LISTENER_RESTART_MAX_ATTEMPT = 6;
2235
- const LISTENER_STABLE_RESET_MS = 6e4;
2287
+ const DEVICE_JOIN_APPROVAL_CONFIRMATION = "APPROVE";
2288
+ const DEVICE_REVOKE_CONFIRMATION = "REVOKE";
2289
+ const DEVICE_JOIN_CHALLENGE_TTL_SECONDS = 240;
2290
+ const RESUMABLE_JOIN_PHASES = /* @__PURE__ */ new Set([
2291
+ "pending",
2292
+ "challenge_prepared",
2293
+ "response_prepared",
2294
+ "response_verified",
2295
+ "approval_prepared",
2296
+ "authorized"
2297
+ ]);
2236
2298
  const FAILURE_MESSAGES = {
2237
2299
  "not-registered": "No AWiki identity is registered for this deployment.",
2238
2300
  "signed-out": "This installation is signed out of AWiki.",
@@ -2245,7 +2307,6 @@ const FAILURE_MESSAGES = {
2245
2307
  "forbidden": "The AWiki operation is not permitted.",
2246
2308
  "identity-recovery-required": "The local AWiki identity must be recovered before it can be used again.",
2247
2309
  "conflict": "The AWiki operation conflicts with current state.",
2248
- "state-in-use": "AWiki data is already open in another client.",
2249
2310
  "rate-limited": "The AWiki service rate-limited the request.",
2250
2311
  "group-membership-required": "The active AWiki identity is not a member of this group.",
2251
2312
  "group-identity-stale": "The AWiki group identity binding is still recovering.",
@@ -2261,6 +2322,39 @@ const FAILURE_MESSAGES = {
2261
2322
  };
2262
2323
  var ProviderUnavailableError = class extends Error {};
2263
2324
  var SummaryProviderUnavailableError = class extends Error {};
2325
+ function candidateJoinPhase(value) {
2326
+ if (value.remoteState === "rejected") return "rejected";
2327
+ if (value.remoteState === "cancelled" || value.localPhase === "cancelled") return "cancelled";
2328
+ if (value.remoteState === "expired" || value.localPhase === "expired") return "expired";
2329
+ if (value.localPhase === "authorized" && value.remoteState === "consumed" && value.completed && value.identity !== void 0) return "authorized";
2330
+ if (value.sas !== void 0) return value.remoteState === "response_verified" && /^\d{6}$/u.test(value.sas) ? "sas-ready" : void 0;
2331
+ if (value.remoteState === "challenge_sent" || value.remoteState === "response_verified" || value.remoteState === "consumed" || value.localPhase !== "pending") return "verifying";
2332
+ return value.remoteState === "pending" ? "pending" : void 0;
2333
+ }
2334
+ function adminJoinPhase(value) {
2335
+ if (value.remoteState === "rejected") return "rejected";
2336
+ if (value.remoteState === "cancelled" || value.localPhase === "cancelled") return "cancelled";
2337
+ if (value.remoteState === "expired" || value.localPhase === "expired") return "expired";
2338
+ if (value.localPhase === "authorized" && value.remoteState === "consumed") return "authorized";
2339
+ if (value.sas !== void 0) return value.remoteState === "response_verified" && /^\d{6}$/u.test(value.sas) ? "sas-ready" : void 0;
2340
+ if (value.remoteState === "challenge_sent" || value.remoteState === "response_verified" || value.remoteState === "consumed" || value.localPhase !== "pending") return "verifying";
2341
+ return value.remoteState === "pending" ? "pending" : void 0;
2342
+ }
2343
+ function requestJoinPhase(value) {
2344
+ switch (value.state) {
2345
+ case "pending": return "pending";
2346
+ case "challenge_sent":
2347
+ case "response_verified": return "verifying";
2348
+ case "consumed": return "authorized";
2349
+ case "cancelled": return "cancelled";
2350
+ case "rejected": return "rejected";
2351
+ case "expired": return "expired";
2352
+ }
2353
+ }
2354
+ function constantTimeSasMatches(expected, actual) {
2355
+ if (!/^\d{6}$/u.test(expected) || !/^\d{6}$/u.test(actual)) return false;
2356
+ return timingSafeEqual(Buffer.from(expected, "ascii"), Buffer.from(actual, "ascii"));
2357
+ }
2264
2358
  /** Validate and preserve one SDK service URL without accepting insecure remote HTTP. */
2265
2359
  function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
2266
2360
  let url;
@@ -2282,6 +2376,22 @@ function recoveryReconciliationEndpoint(target, allowInsecureLoopbackForTesting)
2282
2376
  if (parsed.search !== "") throw new TypeError("awiki: recovery reconciliation target must not contain a query");
2283
2377
  return new URL("/api/identity-recovery", parsed).toString();
2284
2378
  }
2379
+ /** Accept only the closed Model Proxy success response; no ledger identifier may cross back. */
2380
+ async function acceptsRecoveryReconciliation(response) {
2381
+ if (!response.ok) return false;
2382
+ let value;
2383
+ try {
2384
+ const text = await readBoundedResponseText(response, RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES);
2385
+ if (text === void 0) return false;
2386
+ value = JSON.parse(text);
2387
+ } catch {
2388
+ return false;
2389
+ }
2390
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2391
+ const result = value;
2392
+ const keys = Object.keys(result).sort();
2393
+ return keys.length === 2 && keys[0] === "idempotent" && keys[1] === "restored" && result.restored === true && typeof result.idempotent === "boolean";
2394
+ }
2285
2395
  /** Validate a provider domain without inferring it from an API endpoint. */
2286
2396
  function serviceDomain(raw, field = "userServiceDomain") {
2287
2397
  return normalizeAwikiDomain(raw, field);
@@ -2315,14 +2425,12 @@ function listenerAllowedPeers(raw, enabled) {
2315
2425
  return peers;
2316
2426
  }
2317
2427
  /** Resolve and validate every deployment choice before publishing the service. */
2318
- function resolveConfig(ctx, config) {
2428
+ function resolveConfig(config) {
2319
2429
  const allowInsecureLoopbackForTesting = config.allowInsecureLoopbackForTesting ?? false;
2320
2430
  const configuredStateRoot = config.stateRoot?.trim();
2321
2431
  const configuredDshHome = process.env.DSH_HOME?.trim();
2322
2432
  const dshHome = configuredDshHome === void 0 || configuredDshHome.length === 0 ? join(homedir(), ".dsh") : configuredDshHome;
2323
- const profileName = configuredStateRoot === void 0 || configuredStateRoot.length === 0 ? resolveAwikiProfileName(ctx, dshHome) : void 0;
2324
- const stateRoot = configuredStateRoot === void 0 || configuredStateRoot.length === 0 ? resolveAwikiStateRoot(ctx, dshHome) : configuredStateRoot;
2325
- const legacySharedStateDetected = profileName !== void 0 && existsSync(join(dshHome, "awiki", "im-core"));
2433
+ const stateRoot = configuredStateRoot === void 0 || configuredStateRoot.length === 0 ? join(dshHome, "awiki", "im-core") : configuredStateRoot;
2326
2434
  if (!isAbsolute(stateRoot)) throw new TypeError("awiki: stateRoot must be an absolute path");
2327
2435
  const attachmentMaxBytes = config.attachmentMaxBytes ?? 10485760;
2328
2436
  if (!Number.isSafeInteger(attachmentMaxBytes) || attachmentMaxBytes < 1) throw new TypeError("awiki: attachmentMaxBytes must be a positive safe integer");
@@ -2331,6 +2439,8 @@ function resolveConfig(ctx, config) {
2331
2439
  const pollIntervalMs = config.pollIntervalMs ?? 3e3;
2332
2440
  if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 1e3 || pollIntervalMs > 6e4) throw new TypeError("awiki: pollIntervalMs must be a safe integer from 1000 through 60000");
2333
2441
  const listenerEnabled = config.listenerEnabled ?? false;
2442
+ const realtimeEnabled = config.realtimeEnabled ?? true;
2443
+ if (listenerEnabled && !realtimeEnabled) throw new TypeError("awiki: listenerEnabled requires realtimeEnabled");
2334
2444
  const listenerWorkspacePath = config.listenerWorkspacePath?.trim() || join(dshHome, "workspaces", "awiki");
2335
2445
  if (!isAbsolute(listenerWorkspacePath)) throw new TypeError("awiki: listenerWorkspacePath must be an absolute path");
2336
2446
  const allowedPeers = listenerAllowedPeers(config.listenerAllowedPeers, listenerEnabled);
@@ -2353,10 +2463,7 @@ function resolveConfig(ctx, config) {
2353
2463
  attachmentMaxBytes,
2354
2464
  imageAttachmentCacheMaxBytes,
2355
2465
  pollIntervalMs,
2356
- ...profileName === void 0 ? {} : {
2357
- profileName,
2358
- legacySharedStateDetected
2359
- },
2466
+ realtimeEnabled,
2360
2467
  listenerEnabled,
2361
2468
  listener: {
2362
2469
  allowedPeers,
@@ -2426,38 +2533,6 @@ function normalizeGroupDid(value) {
2426
2533
  if (typeof value !== "string" || !value.startsWith("did:") || value.length > 2048) return void 0;
2427
2534
  return value;
2428
2535
  }
2429
- /** Rebuild the browser-safe recovery DTO instead of trusting provider object shape. */
2430
- function publicGroupRebindRecoverySummary(value) {
2431
- if (typeof value !== "object" || value === null) return void 0;
2432
- const candidate = value;
2433
- if ([
2434
- candidate.processed,
2435
- candidate.completed,
2436
- candidate.pending,
2437
- candidate.blocked
2438
- ].some((count) => !Number.isSafeInteger(count) || count < 0 || count > 4294967295)) return;
2439
- if (!Array.isArray(candidate.items) || candidate.items.length > 500) return void 0;
2440
- const seen = /* @__PURE__ */ new Set();
2441
- const items = [];
2442
- for (const value of candidate.items) {
2443
- if (typeof value !== "object" || value === null) return void 0;
2444
- const item = value;
2445
- const groupDid = normalizeGroupDid(item.groupDid);
2446
- if (groupDid === void 0 || seen.has(groupDid) || item.status !== "pending" && item.status !== "blocked") return;
2447
- seen.add(groupDid);
2448
- items.push({
2449
- groupDid,
2450
- status: item.status
2451
- });
2452
- }
2453
- return {
2454
- processed: candidate.processed,
2455
- completed: candidate.completed,
2456
- pending: candidate.pending,
2457
- blocked: candidate.blocked,
2458
- items
2459
- };
2460
- }
2461
2536
  function normalizeProfileRequest(request) {
2462
2537
  if (typeof request?.displayName !== "string" || typeof request.bio !== "string" || !Array.isArray(request.tags)) return void 0;
2463
2538
  const displayName = request.displayName.trim();
@@ -2507,6 +2582,8 @@ function normalizeRecoveryPrepareRequest(request) {
2507
2582
  };
2508
2583
  }
2509
2584
  const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 65536;
2585
+ const SERVER_INFO_RESPONSE_MAX_BYTES = 65536;
2586
+ const RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES = 4096;
2510
2587
  /** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
2511
2588
  async function readBoundedResponseText(response, maxBytes) {
2512
2589
  const declaredLength = response.headers.get("content-length");
@@ -2724,6 +2801,14 @@ let AwikiService = (() => {
2724
2801
  let _inspectIdentityAccess_decorators;
2725
2802
  let _sendRegistrationOtp_decorators;
2726
2803
  let _registerIdentity_decorators;
2804
+ let _beginDeviceJoin_decorators;
2805
+ let _getDeviceJoinStatus_decorators;
2806
+ let _cancelDeviceJoin_decorators;
2807
+ let _refreshDeviceManagement_decorators;
2808
+ let _startDeviceJoinVerification_decorators;
2809
+ let _approveDeviceJoin_decorators;
2810
+ let _rejectDeviceJoin_decorators;
2811
+ let _revokeDevice_decorators;
2727
2812
  let _updateDisplayName_decorators;
2728
2813
  let _getProfile_decorators;
2729
2814
  let _updateProfile_decorators;
@@ -2741,7 +2826,6 @@ let AwikiService = (() => {
2741
2826
  let _listGroupMembers_decorators;
2742
2827
  let _addGroupMember_decorators;
2743
2828
  let _removeGroupMember_decorators;
2744
- let _resumeGroupRebindRecovery_decorators;
2745
2829
  let _getConversationPreferences_decorators;
2746
2830
  let _updateConversationPreference_decorators;
2747
2831
  let _listConversations_decorators;
@@ -2769,6 +2853,14 @@ let AwikiService = (() => {
2769
2853
  _inspectIdentityAccess_decorators = [Remote];
2770
2854
  _sendRegistrationOtp_decorators = [Remote];
2771
2855
  _registerIdentity_decorators = [Remote];
2856
+ _beginDeviceJoin_decorators = [Remote];
2857
+ _getDeviceJoinStatus_decorators = [Remote];
2858
+ _cancelDeviceJoin_decorators = [Remote];
2859
+ _refreshDeviceManagement_decorators = [Remote];
2860
+ _startDeviceJoinVerification_decorators = [Remote];
2861
+ _approveDeviceJoin_decorators = [Remote];
2862
+ _rejectDeviceJoin_decorators = [Remote];
2863
+ _revokeDevice_decorators = [Remote];
2772
2864
  _updateDisplayName_decorators = [Remote];
2773
2865
  _getProfile_decorators = [Remote];
2774
2866
  _updateProfile_decorators = [Remote];
@@ -2786,7 +2878,6 @@ let AwikiService = (() => {
2786
2878
  _listGroupMembers_decorators = [Remote];
2787
2879
  _addGroupMember_decorators = [Remote];
2788
2880
  _removeGroupMember_decorators = [Remote];
2789
- _resumeGroupRebindRecovery_decorators = [Remote];
2790
2881
  _getConversationPreferences_decorators = [Remote];
2791
2882
  _updateConversationPreference_decorators = [Remote];
2792
2883
  _listConversations_decorators = [Remote];
@@ -2891,6 +2982,94 @@ let AwikiService = (() => {
2891
2982
  },
2892
2983
  metadata: _metadata
2893
2984
  }, null, _instanceExtraInitializers);
2985
+ __esDecorate(this, null, _beginDeviceJoin_decorators, {
2986
+ kind: "method",
2987
+ name: "beginDeviceJoin",
2988
+ static: false,
2989
+ private: false,
2990
+ access: {
2991
+ has: (obj) => "beginDeviceJoin" in obj,
2992
+ get: (obj) => obj.beginDeviceJoin
2993
+ },
2994
+ metadata: _metadata
2995
+ }, null, _instanceExtraInitializers);
2996
+ __esDecorate(this, null, _getDeviceJoinStatus_decorators, {
2997
+ kind: "method",
2998
+ name: "getDeviceJoinStatus",
2999
+ static: false,
3000
+ private: false,
3001
+ access: {
3002
+ has: (obj) => "getDeviceJoinStatus" in obj,
3003
+ get: (obj) => obj.getDeviceJoinStatus
3004
+ },
3005
+ metadata: _metadata
3006
+ }, null, _instanceExtraInitializers);
3007
+ __esDecorate(this, null, _cancelDeviceJoin_decorators, {
3008
+ kind: "method",
3009
+ name: "cancelDeviceJoin",
3010
+ static: false,
3011
+ private: false,
3012
+ access: {
3013
+ has: (obj) => "cancelDeviceJoin" in obj,
3014
+ get: (obj) => obj.cancelDeviceJoin
3015
+ },
3016
+ metadata: _metadata
3017
+ }, null, _instanceExtraInitializers);
3018
+ __esDecorate(this, null, _refreshDeviceManagement_decorators, {
3019
+ kind: "method",
3020
+ name: "refreshDeviceManagement",
3021
+ static: false,
3022
+ private: false,
3023
+ access: {
3024
+ has: (obj) => "refreshDeviceManagement" in obj,
3025
+ get: (obj) => obj.refreshDeviceManagement
3026
+ },
3027
+ metadata: _metadata
3028
+ }, null, _instanceExtraInitializers);
3029
+ __esDecorate(this, null, _startDeviceJoinVerification_decorators, {
3030
+ kind: "method",
3031
+ name: "startDeviceJoinVerification",
3032
+ static: false,
3033
+ private: false,
3034
+ access: {
3035
+ has: (obj) => "startDeviceJoinVerification" in obj,
3036
+ get: (obj) => obj.startDeviceJoinVerification
3037
+ },
3038
+ metadata: _metadata
3039
+ }, null, _instanceExtraInitializers);
3040
+ __esDecorate(this, null, _approveDeviceJoin_decorators, {
3041
+ kind: "method",
3042
+ name: "approveDeviceJoin",
3043
+ static: false,
3044
+ private: false,
3045
+ access: {
3046
+ has: (obj) => "approveDeviceJoin" in obj,
3047
+ get: (obj) => obj.approveDeviceJoin
3048
+ },
3049
+ metadata: _metadata
3050
+ }, null, _instanceExtraInitializers);
3051
+ __esDecorate(this, null, _rejectDeviceJoin_decorators, {
3052
+ kind: "method",
3053
+ name: "rejectDeviceJoin",
3054
+ static: false,
3055
+ private: false,
3056
+ access: {
3057
+ has: (obj) => "rejectDeviceJoin" in obj,
3058
+ get: (obj) => obj.rejectDeviceJoin
3059
+ },
3060
+ metadata: _metadata
3061
+ }, null, _instanceExtraInitializers);
3062
+ __esDecorate(this, null, _revokeDevice_decorators, {
3063
+ kind: "method",
3064
+ name: "revokeDevice",
3065
+ static: false,
3066
+ private: false,
3067
+ access: {
3068
+ has: (obj) => "revokeDevice" in obj,
3069
+ get: (obj) => obj.revokeDevice
3070
+ },
3071
+ metadata: _metadata
3072
+ }, null, _instanceExtraInitializers);
2894
3073
  __esDecorate(this, null, _updateDisplayName_decorators, {
2895
3074
  kind: "method",
2896
3075
  name: "updateDisplayName",
@@ -3078,17 +3257,6 @@ let AwikiService = (() => {
3078
3257
  },
3079
3258
  metadata: _metadata
3080
3259
  }, null, _instanceExtraInitializers);
3081
- __esDecorate(this, null, _resumeGroupRebindRecovery_decorators, {
3082
- kind: "method",
3083
- name: "resumeGroupRebindRecovery",
3084
- static: false,
3085
- private: false,
3086
- access: {
3087
- has: (obj) => "resumeGroupRebindRecovery" in obj,
3088
- get: (obj) => obj.resumeGroupRebindRecovery
3089
- },
3090
- metadata: _metadata
3091
- }, null, _instanceExtraInitializers);
3092
3260
  __esDecorate(this, null, _getConversationPreferences_decorators, {
3093
3261
  kind: "method",
3094
3262
  name: "getConversationPreferences",
@@ -3286,6 +3454,12 @@ let AwikiService = (() => {
3286
3454
  sessionMutation = Promise.resolve();
3287
3455
  sessionRevision = 0;
3288
3456
  activeIdentityDid;
3457
+ pendingDeviceJoin;
3458
+ activeDeviceJoinSessionId;
3459
+ requestRefs = /* @__PURE__ */ new Map();
3460
+ requestSessions = /* @__PURE__ */ new Map();
3461
+ deviceRefs = /* @__PURE__ */ new Map();
3462
+ deviceIds = /* @__PURE__ */ new Map();
3289
3463
  activeSummaryRequests = /* @__PURE__ */ new Set();
3290
3464
  summaryProvider;
3291
3465
  recoveryReconciliationTarget;
@@ -3300,7 +3474,7 @@ let AwikiService = (() => {
3300
3474
  constructor(ctx, config) {
3301
3475
  super(ctx, "awiki");
3302
3476
  this.hostContext = ctx;
3303
- this.resolved = resolveConfig(ctx, config);
3477
+ this.resolved = resolveConfig(config);
3304
3478
  this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
3305
3479
  this.sessionStore = new AwikiSessionStore(this.resolved.stateRoot);
3306
3480
  this.imageAttachmentCache = new AwikiImageAttachmentCache(this.resolved.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
@@ -3329,12 +3503,12 @@ let AwikiService = (() => {
3329
3503
  ctx.inject(["workspaceRegistry"], (workspaceCtx) => {
3330
3504
  this.workspaceContext = workspaceCtx;
3331
3505
  const provider = this.provider;
3332
- if (provider !== void 0) this.startListener(provider);
3506
+ if (provider !== void 0) this.ensureAgentConsumer(provider);
3333
3507
  workspaceCtx.effect(() => async () => {
3334
3508
  if (this.workspaceContext !== workspaceCtx) return;
3335
3509
  this.workspaceContext = void 0;
3336
3510
  const current = this.provider;
3337
- if (current !== void 0) await this.stopListener(current);
3511
+ if (current !== void 0) await this.stopAgentConsumer(current);
3338
3512
  }, "awiki: release Workspace listener composition");
3339
3513
  });
3340
3514
  registerAwikiTools(ctx, this);
@@ -3355,6 +3529,8 @@ let AwikiService = (() => {
3355
3529
  */
3356
3530
  registerClientFactory(factory) {
3357
3531
  if (this.provider !== void 0) throw new Error("awiki: a client provider is already registered");
3532
+ this.pendingDeviceJoin = void 0;
3533
+ this.activeDeviceJoinSessionId = void 0;
3358
3534
  const provider = {
3359
3535
  client: factory({
3360
3536
  userServiceUrl: this.resolved.userServiceUrl,
@@ -3368,14 +3544,32 @@ let AwikiService = (() => {
3368
3544
  allowInsecureLoopbackForTesting: this.resolved.allowInsecureLoopbackForTesting,
3369
3545
  stateRoot: this.resolved.stateRoot
3370
3546
  }),
3371
- listenerRestartAttempt: 0,
3372
- listenerGeneration: 0,
3373
- listenerRecoveryBlocked: false
3547
+ realtimeGeneration: 0,
3548
+ agentConsumerGeneration: 0,
3549
+ localDeviceJoinRequestCountAfterSync: 0
3374
3550
  };
3375
3551
  this.provider = provider;
3376
- this.startListener(provider);
3552
+ this.ensureRealtimeSupervisor(provider);
3553
+ this.ensureAgentConsumer(provider);
3377
3554
  return () => this.disposeProvider(provider);
3378
3555
  }
3556
+ /** Safe same-process diagnostics for focused E2E. Never exposed through Typert Remote. */
3557
+ getRealtimeDiagnostics() {
3558
+ const provider = this.provider;
3559
+ return {
3560
+ ...provider?.realtimeSupervisor?.diagnostics() ?? {
3561
+ connected: false,
3562
+ activeSessionCount: 0,
3563
+ startCount: 0,
3564
+ stopCount: 0,
3565
+ maxActiveSessionCount: 0,
3566
+ generation: 0,
3567
+ retryCount: 0,
3568
+ lifecyclePhase: "idle"
3569
+ },
3570
+ localDeviceJoinRequestCountAfterSync: provider?.localDeviceJoinRequestCountAfterSync ?? 0
3571
+ };
3572
+ }
3379
3573
  /** Register the optional Model Proxy recovery target without exposing an arbitrary callback or token. */
3380
3574
  registerRecoveryReconciliationTarget(target) {
3381
3575
  if (this.recoveryReconciliationTarget !== void 0) throw new Error("awiki: a recovery reconciliation target is already registered");
@@ -3403,18 +3597,42 @@ let AwikiService = (() => {
3403
3597
  * Read settings needed by the browser presentation.
3404
3598
  * @returns Browser-safe polling configuration without SDK endpoints or state paths.
3405
3599
  */
3406
- getConfig() {
3407
- return Promise.resolve({
3600
+ async getConfig() {
3601
+ let handleRecoveryPhoneEnabled = false;
3602
+ const abort = new AbortController();
3603
+ const timeout = setTimeout(() => {
3604
+ abort.abort();
3605
+ }, 1e4);
3606
+ try {
3607
+ const response = await fetch(new URL("/user-service/v1/server-info", this.resolved.userServiceUrl), {
3608
+ method: "GET",
3609
+ headers: {
3610
+ accept: "application/json",
3611
+ "cache-control": "no-store"
3612
+ },
3613
+ cache: "no-store",
3614
+ redirect: "error",
3615
+ signal: abort.signal
3616
+ });
3617
+ if (response.ok) {
3618
+ const text = await readBoundedResponseText(response, SERVER_INFO_RESPONSE_MAX_BYTES);
3619
+ const value = text === void 0 ? void 0 : JSON.parse(text);
3620
+ if (typeof value === "object" && value !== null && value.schema_version === 1) {
3621
+ const methods = value.identity?.handle_recovery?.methods;
3622
+ handleRecoveryPhoneEnabled = Array.isArray(methods) && methods.some((method) => typeof method === "object" && method !== null && method.id === "phone" && method.enabled === true && method.verification?.required === true && method.verification?.type === "sms_otp");
3623
+ }
3624
+ }
3625
+ } catch {} finally {
3626
+ clearTimeout(timeout);
3627
+ }
3628
+ return {
3408
3629
  ok: true,
3409
3630
  value: {
3410
3631
  pollIntervalMs: this.resolved.pollIntervalMs,
3411
3632
  attachmentMaxBytes: this.resolved.attachmentMaxBytes,
3412
- ...this.resolved.profileName === void 0 ? {} : {
3413
- profileName: this.resolved.profileName,
3414
- legacySharedStateDetected: this.resolved.legacySharedStateDetected
3415
- }
3633
+ handleRecoveryPhoneEnabled
3416
3634
  }
3417
- });
3635
+ };
3418
3636
  }
3419
3637
  /**
3420
3638
  * Read the deployment's identity status.
@@ -3437,6 +3655,8 @@ let AwikiService = (() => {
3437
3655
  const identity = await this.run((client) => client.getIdentity(), { allowSignedOut: true });
3438
3656
  if (!identity.ok) return identity;
3439
3657
  this.activeIdentityDid = identity.value?.did;
3658
+ const provider = this.provider;
3659
+ if (identity.value !== null && provider !== void 0) this.ensureProviderRuntime(provider);
3440
3660
  return identity.value === null ? {
3441
3661
  ok: true,
3442
3662
  value: { status: "unregistered" }
@@ -3473,7 +3693,7 @@ let AwikiService = (() => {
3473
3693
  const session = { status: "signed-out" };
3474
3694
  this.publishSession(session);
3475
3695
  const provider = this.provider;
3476
- if (provider !== void 0) await this.stopListener(provider);
3696
+ if (provider !== void 0) await this.stopProviderRuntime(provider);
3477
3697
  return {
3478
3698
  ok: true,
3479
3699
  value: session
@@ -3506,7 +3726,7 @@ let AwikiService = (() => {
3506
3726
  };
3507
3727
  this.publishSession(session);
3508
3728
  const provider = this.provider;
3509
- if (provider !== void 0) this.startListener(provider);
3729
+ if (provider !== void 0) this.ensureProviderRuntime(provider);
3510
3730
  return {
3511
3731
  ok: true,
3512
3732
  value: session
@@ -3592,8 +3812,15 @@ let AwikiService = (() => {
3592
3812
  };
3593
3813
  }
3594
3814
  }
3595
- sendRegistrationOtp(request) {
3596
- return this.run((client) => client.sendRegistrationOtp(request));
3815
+ async sendRegistrationOtp(request) {
3816
+ this.pendingDeviceJoin = void 0;
3817
+ return this.run(async (client) => {
3818
+ if (await this.selectDeviceJoinSession(client) !== null) throw Object.assign(/* @__PURE__ */ new Error("join already exists"), {
3819
+ name: "AwikiSdkError",
3820
+ code: "conflict"
3821
+ });
3822
+ return client.sendRegistrationOtp(request);
3823
+ });
3597
3824
  }
3598
3825
  /**
3599
3826
  * Register and persist the deployment's only AWiki identity.
@@ -3601,9 +3828,190 @@ let AwikiService = (() => {
3601
3828
  * @returns The new public identity or a closed failure.
3602
3829
  */
3603
3830
  async registerIdentity(request) {
3604
- const result = await this.run((client) => client.registerIdentity(request));
3605
- if (result.ok) await this.activateRegisteredIdentity(result.value);
3606
- return result;
3831
+ this.pendingDeviceJoin = void 0;
3832
+ const result = await this.run(async (client) => {
3833
+ if (await this.selectDeviceJoinSession(client) !== null) throw Object.assign(/* @__PURE__ */ new Error("join already exists"), {
3834
+ name: "AwikiSdkError",
3835
+ code: "conflict"
3836
+ });
3837
+ return client.registerIdentity(request);
3838
+ });
3839
+ if (!result.ok) return result;
3840
+ if (result.value.status === "registered") {
3841
+ await this.activateRegisteredIdentity(result.value.identity);
3842
+ return {
3843
+ ok: true,
3844
+ value: {
3845
+ status: "registered",
3846
+ identity: result.value.identity
3847
+ }
3848
+ };
3849
+ }
3850
+ this.pendingDeviceJoin = {
3851
+ continuationId: result.value.continuationId,
3852
+ fullHandle: result.value.fullHandle,
3853
+ mode: result.value.mode,
3854
+ requiresUserPresence: result.value.requiresUserPresence
3855
+ };
3856
+ return {
3857
+ ok: true,
3858
+ value: {
3859
+ status: "join-required",
3860
+ fullHandle: result.value.fullHandle,
3861
+ mode: result.value.mode,
3862
+ requiresUserPresence: result.value.requiresUserPresence
3863
+ }
3864
+ };
3865
+ }
3866
+ /** Consume the exact in-memory continuation; ordinary Join never claims rebind user presence. */
3867
+ async beginDeviceJoin() {
3868
+ const continuation = this.pendingDeviceJoin;
3869
+ if (continuation === void 0) return {
3870
+ ok: false,
3871
+ error: failure("conflict")
3872
+ };
3873
+ if (continuation.mode !== "ordinary" || continuation.requiresUserPresence) return {
3874
+ ok: false,
3875
+ error: failure("forbidden")
3876
+ };
3877
+ this.pendingDeviceJoin = void 0;
3878
+ const result = await this.run((client) => client.beginDeviceJoin({
3879
+ continuationId: continuation.continuationId,
3880
+ operationId: `dsh-device-join-${randomUUID()}`,
3881
+ userPresenceConfirmed: false
3882
+ }));
3883
+ if (!result.ok) return result;
3884
+ this.activeDeviceJoinSessionId = result.value.joinSessionId;
3885
+ return this.applyCandidateJoinProgress(result.value);
3886
+ }
3887
+ /** Restore from Core local_sessions and advance only the exact resumable Join. */
3888
+ async getDeviceJoinStatus() {
3889
+ const result = await this.run(async (client) => {
3890
+ const joinSessionId = await this.selectDeviceJoinSession(client);
3891
+ return joinSessionId === null ? null : client.getDeviceJoinStatus(joinSessionId);
3892
+ });
3893
+ if (!result.ok) return result;
3894
+ if (result.value === null) return {
3895
+ ok: true,
3896
+ value: null
3897
+ };
3898
+ this.activeDeviceJoinSessionId = result.value.joinSessionId;
3899
+ return this.applyCandidateJoinProgress(result.value);
3900
+ }
3901
+ async cancelDeviceJoin() {
3902
+ const result = await this.run(async (client) => {
3903
+ const joinSessionId = await this.selectDeviceJoinSession(client);
3904
+ if (joinSessionId === null) return null;
3905
+ return client.cancelDeviceJoin(joinSessionId);
3906
+ });
3907
+ if (!result.ok) return result;
3908
+ this.pendingDeviceJoin = void 0;
3909
+ this.activeDeviceJoinSessionId = void 0;
3910
+ return {
3911
+ ok: true,
3912
+ value: { completed: true }
3913
+ };
3914
+ }
3915
+ /** Reliable-sync and project only Host-opaque device/request references. */
3916
+ refreshDeviceManagement() {
3917
+ return this.run((client) => this.deviceManagementSnapshot(client));
3918
+ }
3919
+ async startDeviceJoinVerification(request) {
3920
+ const joinSessionId = this.requestSessions.get(request?.requestRef);
3921
+ if (joinSessionId === void 0) return {
3922
+ ok: false,
3923
+ error: failure("invalid-request")
3924
+ };
3925
+ const result = await this.run(async (client) => {
3926
+ await this.requireDeviceManager(client);
3927
+ await client.syncDeviceManagement();
3928
+ let notice = (await client.listLocalDeviceJoinRequests()).find((value) => value.joinSessionId === joinSessionId);
3929
+ if (notice === void 0) throw Object.assign(/* @__PURE__ */ new Error("request not found"), {
3930
+ name: "AwikiSdkError",
3931
+ code: "not-found"
3932
+ });
3933
+ const localProgress = await this.localAdminJoinProgress(client, notice);
3934
+ if (localProgress !== void 0) return localProgress;
3935
+ if (!notice.canStartVerification) throw Object.assign(/* @__PURE__ */ new Error("request unavailable"), {
3936
+ name: "AwikiSdkError",
3937
+ code: "forbidden"
3938
+ });
3939
+ const operationId = `dsh-device-verify-${createHash("sha256").update(joinSessionId).digest("hex").slice(0, 32)}`;
3940
+ let started;
3941
+ try {
3942
+ started = await client.startDeviceJoinVerification({
3943
+ joinSessionId,
3944
+ operationId,
3945
+ challengeTtlSeconds: DEVICE_JOIN_CHALLENGE_TTL_SECONDS
3946
+ });
3947
+ } catch (error) {
3948
+ await client.syncDeviceManagement();
3949
+ notice = (await client.listLocalDeviceJoinRequests()).find((value) => value.joinSessionId === joinSessionId);
3950
+ const recovered = notice === void 0 ? void 0 : await this.localAdminJoinProgress(client, notice);
3951
+ if (recovered !== void 0) return recovered;
3952
+ throw error;
3953
+ }
3954
+ await client.syncDeviceManagement();
3955
+ notice = (await client.listLocalDeviceJoinRequests()).find((value) => value.joinSessionId === joinSessionId);
3956
+ if (notice === void 0) return started;
3957
+ return await this.localAdminJoinProgress(client, notice) ?? started;
3958
+ });
3959
+ return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
3960
+ }
3961
+ async approveDeviceJoin(request) {
3962
+ if (request?.confirmation !== DEVICE_JOIN_APPROVAL_CONFIRMATION) return {
3963
+ ok: false,
3964
+ error: failure("invalid-request")
3965
+ };
3966
+ const joinSessionId = this.requestSessions.get(request.requestRef);
3967
+ if (joinSessionId === void 0 || !/^\d{6}$/u.test(request.enteredSas)) return {
3968
+ ok: false,
3969
+ error: failure("invalid-request")
3970
+ };
3971
+ const result = await this.run(async (client) => {
3972
+ await this.requireDeviceManager(client);
3973
+ const progress = await client.getLocalDeviceJoinVerificationProgress(joinSessionId);
3974
+ if (progress.sas === void 0 || !constantTimeSasMatches(progress.sas, request.enteredSas)) throw Object.assign(/* @__PURE__ */ new Error("sas mismatch"), {
3975
+ name: "AwikiSdkError",
3976
+ code: "forbidden"
3977
+ });
3978
+ const prompt = await client.prepareDeviceJoinApproval(joinSessionId);
3979
+ return client.confirmDeviceJoinApproval(prompt.approvalHandle);
3980
+ });
3981
+ return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
3982
+ }
3983
+ async rejectDeviceJoin(request) {
3984
+ const joinSessionId = this.requestSessions.get(request?.requestRef);
3985
+ if (joinSessionId === void 0 || request.reason !== "user_rejected" && request.reason !== "sas_mismatch") return {
3986
+ ok: false,
3987
+ error: failure("invalid-request")
3988
+ };
3989
+ const result = await this.run(async (client) => {
3990
+ await this.requireDeviceManager(client);
3991
+ return client.rejectDeviceJoin(joinSessionId, request.reason);
3992
+ });
3993
+ return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
3994
+ }
3995
+ async revokeDevice(request) {
3996
+ if (request?.confirmation !== DEVICE_REVOKE_CONFIRMATION) return {
3997
+ ok: false,
3998
+ error: failure("invalid-request")
3999
+ };
4000
+ const deviceId = this.deviceIds.get(request.deviceRef);
4001
+ if (deviceId === void 0) return {
4002
+ ok: false,
4003
+ error: failure("invalid-request")
4004
+ };
4005
+ return this.run(async (client) => {
4006
+ await this.requireDeviceManager(client);
4007
+ const target = (await client.getDeviceRegistry()).find((device) => device.deviceId === deviceId);
4008
+ if (target === void 0 || target.isCurrent) throw Object.assign(/* @__PURE__ */ new Error("device unavailable"), {
4009
+ name: "AwikiSdkError",
4010
+ code: "forbidden"
4011
+ });
4012
+ await client.revokeDevice(deviceId);
4013
+ return this.deviceManagementSnapshot(client);
4014
+ });
3607
4015
  }
3608
4016
  /**
3609
4017
  * Update the deployment identity's public WNS display name.
@@ -3630,12 +4038,19 @@ let AwikiService = (() => {
3630
4038
  }
3631
4039
  /** Start durable phone recovery for one existing full Handle. */
3632
4040
  sendRecoveryOtp(request) {
4041
+ this.pendingDeviceJoin = void 0;
3633
4042
  const normalized = normalizeRecoveryOtpRequest(request);
3634
4043
  if (normalized === void 0) return Promise.resolve({
3635
4044
  ok: false,
3636
4045
  error: failure("invalid-request")
3637
4046
  });
3638
- return this.run((client) => client.sendRecoveryOtp(normalized), { allowSignedOut: true });
4047
+ return this.run(async (client) => {
4048
+ if (await this.selectDeviceJoinSession(client) !== null) throw Object.assign(/* @__PURE__ */ new Error("join already exists"), {
4049
+ name: "AwikiSdkError",
4050
+ code: "conflict"
4051
+ });
4052
+ return client.sendRecoveryOtp(normalized);
4053
+ }, { allowSignedOut: true });
3639
4054
  }
3640
4055
  /** Verify a recovery OTP and freeze its exact intent before the remote commit. */
3641
4056
  prepareRecovery(request) {
@@ -3805,19 +4220,6 @@ let AwikiService = (() => {
3805
4220
  });
3806
4221
  return this.run((client) => client.removeGroupMember(groupDid, member));
3807
4222
  }
3808
- /** Resume durable group membership convergence after account sync restores old groups. */
3809
- async resumeGroupRebindRecovery() {
3810
- const result = await this.run((client) => client.resumeGroupRebindRecovery());
3811
- if (!result.ok) return result;
3812
- const summary = publicGroupRebindRecoverySummary(result.value);
3813
- return summary === void 0 ? {
3814
- ok: false,
3815
- error: failure("remote")
3816
- } : {
3817
- ok: true,
3818
- value: summary
3819
- };
3820
- }
3821
4223
  /** Read presentation-only roster preferences for the active identity. */
3822
4224
  getConversationPreferences() {
3823
4225
  return this.run(async (client) => this.conversationPreferenceStore.get(await this.ownerDid(client)));
@@ -4147,10 +4549,10 @@ let AwikiService = (() => {
4147
4549
  });
4148
4550
  return this.mutateSession(async () => {
4149
4551
  const provider = this.provider;
4150
- if (provider !== void 0) await this.stopListener(provider);
4552
+ if (provider !== void 0) await this.stopProviderRuntime(provider);
4151
4553
  const result = await this.run((client) => client.clearLocalData(), { allowSignedOut: true });
4152
4554
  if (!result.ok) {
4153
- if (provider !== void 0 && this.provider === provider) this.startListener(provider);
4555
+ if (provider !== void 0 && this.provider === provider) this.ensureProviderRuntime(provider);
4154
4556
  return result;
4155
4557
  }
4156
4558
  try {
@@ -4160,9 +4562,10 @@ let AwikiService = (() => {
4160
4562
  await this.sessionStore.signIn();
4161
4563
  this.signedOut = false;
4162
4564
  this.activeIdentityDid = void 0;
4565
+ this.pendingDeviceJoin = void 0;
4566
+ this.activeDeviceJoinSessionId = void 0;
4163
4567
  this.invalidateSummaries();
4164
4568
  this.publishSession({ status: "unregistered" });
4165
- if (provider !== void 0 && this.provider === provider) this.startListener(provider);
4166
4569
  return result;
4167
4570
  } catch {
4168
4571
  return {
@@ -4172,7 +4575,7 @@ let AwikiService = (() => {
4172
4575
  }
4173
4576
  });
4174
4577
  }
4175
- /** Re-enter after Core applies the recovered identity without blocking on optional account restoration. */
4578
+ /** Re-enter only after Core confirms that the exact recovered identity is applied locally. */
4176
4579
  async applyRecoveredSession(progress) {
4177
4580
  if (progress.phase !== "applied") return true;
4178
4581
  return this.mutateSession(async () => {
@@ -4188,6 +4591,7 @@ let AwikiService = (() => {
4188
4591
  this.activeIdentityDid = identity.did;
4189
4592
  this.invalidateSummaries();
4190
4593
  }
4594
+ const reconciled = await this.reconcileRecoveredIdentity(provider, progress.operationId);
4191
4595
  if (this.provider !== provider) return false;
4192
4596
  if (!alreadyActive) {
4193
4597
  const session = {
@@ -4195,31 +4599,195 @@ let AwikiService = (() => {
4195
4599
  identity
4196
4600
  };
4197
4601
  this.publishSession(session);
4198
- await provider.listenerStartup;
4199
- await this.startListener(provider);
4602
+ this.ensureProviderRuntime(provider);
4200
4603
  }
4201
- return true;
4604
+ return reconciled;
4202
4605
  } catch {
4203
4606
  return false;
4204
4607
  }
4205
4608
  });
4206
4609
  }
4207
- /** Publish one newly registered identity and start its listener through the existing session path. */
4610
+ /** Rebind Mail first-use ownership and, when installed, the canonical model billing account. */
4611
+ async reconcileRecoveredIdentity(provider, operationId) {
4612
+ const logger = this.ctx.logger("awiki-recovery");
4613
+ let mailboxRestored = false;
4614
+ try {
4615
+ await provider.client.getMailAccount();
4616
+ mailboxRestored = true;
4617
+ } catch {
4618
+ logger.warn("awiki: recovered mailbox reconciliation is pending");
4619
+ }
4620
+ const target = this.recoveryReconciliationTarget;
4621
+ if (target === void 0) return mailboxRestored;
4622
+ try {
4623
+ const authority = await provider.client.issueRecoveryAttestation({ operationId });
4624
+ if (!await acceptsRecoveryReconciliation(await this.externalHttpAuth.dispatch(new Request(target.endpoint, {
4625
+ method: "POST",
4626
+ headers: { "content-type": "application/json" },
4627
+ body: JSON.stringify({ attestation: authority.attestation })
4628
+ }), (request) => fetch(request)))) {
4629
+ logger.warn("awiki: recovered model account reconciliation is pending");
4630
+ return false;
4631
+ }
4632
+ return mailboxRestored;
4633
+ } catch {
4634
+ logger.warn("awiki: recovered model account reconciliation is pending");
4635
+ return false;
4636
+ }
4637
+ }
4638
+ /** Select the only resumable new-device session; Core local_sessions is the sole restart SoT. */
4639
+ async selectDeviceJoinSession(client) {
4640
+ const sessions = await client.listLocalDeviceJoinSessions();
4641
+ if (this.activeDeviceJoinSessionId !== void 0) {
4642
+ const tracked = sessions.find((value) => value.side === "new_device" && value.joinSessionId === this.activeDeviceJoinSessionId);
4643
+ if (tracked !== void 0) return tracked.joinSessionId;
4644
+ this.activeDeviceJoinSessionId = void 0;
4645
+ }
4646
+ const resumable = sessions.filter((value) => value.side === "new_device" && RESUMABLE_JOIN_PHASES.has(value.localPhase));
4647
+ if (resumable.length > 1) throw Object.assign(/* @__PURE__ */ new Error("multiple local joins"), {
4648
+ name: "AwikiSdkError",
4649
+ code: "conflict"
4650
+ });
4651
+ const selected = resumable[0];
4652
+ this.activeDeviceJoinSessionId = selected?.joinSessionId;
4653
+ return selected?.joinSessionId ?? null;
4654
+ }
4655
+ async applyCandidateJoinProgress(value) {
4656
+ const phase = candidateJoinPhase(value);
4657
+ if (phase === void 0) return {
4658
+ ok: false,
4659
+ error: failure("remote")
4660
+ };
4661
+ if (phase === "cancelled" || phase === "rejected" || phase === "expired") this.activeDeviceJoinSessionId = void 0;
4662
+ if (phase === "authorized") {
4663
+ if (value.identity === void 0) return {
4664
+ ok: false,
4665
+ error: failure("remote")
4666
+ };
4667
+ await this.activateRegisteredIdentity(value.identity);
4668
+ }
4669
+ return {
4670
+ ok: true,
4671
+ value: {
4672
+ phase,
4673
+ expiresAt: value.expiresAt,
4674
+ ...phase === "sas-ready" && value.sas !== void 0 ? { sas: value.sas } : {},
4675
+ completed: phase === "authorized"
4676
+ }
4677
+ };
4678
+ }
4679
+ publicAdminJoinProgress(requestRef, value) {
4680
+ const phase = adminJoinPhase(value);
4681
+ if (phase === void 0) return {
4682
+ ok: false,
4683
+ error: failure("remote")
4684
+ };
4685
+ return {
4686
+ ok: true,
4687
+ value: {
4688
+ requestRef,
4689
+ phase,
4690
+ expiresAt: value.expiresAt,
4691
+ ...phase === "sas-ready" && value.sas !== void 0 ? { sas: value.sas } : {}
4692
+ }
4693
+ };
4694
+ }
4695
+ async localAdminJoinProgress(client, notice) {
4696
+ const local = (await client.listLocalDeviceJoinSessions()).find((session) => session.side === "admin" && session.joinSessionId === notice.joinSessionId);
4697
+ if (local === void 0) return notice.claimedByCurrentDevice ? client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId) : void 0;
4698
+ if (local.localPhase === "challenge_prepared") return {
4699
+ joinSessionId: notice.joinSessionId,
4700
+ localPhase: local.localPhase,
4701
+ remoteState: notice.state,
4702
+ expiresAt: local.expiresAt
4703
+ };
4704
+ return client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId);
4705
+ }
4706
+ async requireDeviceManager(client) {
4707
+ const current = await client.getCurrentDeviceSummary();
4708
+ if (!current.canManage || current.role !== "admin" || current.readiness !== "admin_ready") throw Object.assign(/* @__PURE__ */ new Error("device manager required"), {
4709
+ name: "AwikiSdkError",
4710
+ code: "forbidden"
4711
+ });
4712
+ }
4713
+ requestRef(joinSessionId) {
4714
+ const existing = this.requestRefs.get(joinSessionId);
4715
+ if (existing !== void 0) return existing;
4716
+ const reference = `join-${randomUUID()}`;
4717
+ this.requestRefs.set(joinSessionId, reference);
4718
+ this.requestSessions.set(reference, joinSessionId);
4719
+ return reference;
4720
+ }
4721
+ deviceRef(deviceId) {
4722
+ const existing = this.deviceRefs.get(deviceId);
4723
+ if (existing !== void 0) return existing;
4724
+ const reference = `device-${randomUUID()}`;
4725
+ this.deviceRefs.set(deviceId, reference);
4726
+ this.deviceIds.set(reference, deviceId);
4727
+ return reference;
4728
+ }
4729
+ async deviceManagementSnapshot(client) {
4730
+ await client.syncDeviceManagement();
4731
+ const current = await client.getCurrentDeviceSummary();
4732
+ if (!current.canManage || current.role !== "admin" || current.readiness !== "admin_ready") {
4733
+ this.requestRefs.clear();
4734
+ this.requestSessions.clear();
4735
+ this.deviceRefs.clear();
4736
+ this.deviceIds.clear();
4737
+ return {
4738
+ canManage: false,
4739
+ ...current.role === void 0 ? {} : { role: current.role },
4740
+ readiness: current.readiness,
4741
+ devices: [],
4742
+ requests: []
4743
+ };
4744
+ }
4745
+ const requests = await client.listLocalDeviceJoinRequests();
4746
+ await Promise.all(requests.filter((request) => request.claimedByCurrentDevice).map((request) => client.getLocalDeviceJoinVerificationProgress(request.joinSessionId).catch(() => void 0)));
4747
+ return {
4748
+ canManage: true,
4749
+ role: "admin",
4750
+ readiness: "admin_ready",
4751
+ devices: (await client.getDeviceRegistry()).map((device) => this.publicDevice(device)),
4752
+ requests: requests.map((request) => ({
4753
+ requestRef: this.requestRef(request.joinSessionId),
4754
+ candidateKeyFingerprint: request.candidateKeyFingerprint,
4755
+ issuedAt: request.issuedAt,
4756
+ expiresAt: request.expiresAt,
4757
+ state: requestJoinPhase(request),
4758
+ claimedByCurrentDevice: request.claimedByCurrentDevice,
4759
+ canStartVerification: request.canStartVerification
4760
+ }))
4761
+ };
4762
+ }
4763
+ publicDevice(device) {
4764
+ return {
4765
+ deviceRef: this.deviceRef(device.deviceId),
4766
+ status: device.status,
4767
+ role: device.role,
4768
+ managementReady: device.managementReady,
4769
+ isCurrent: device.isCurrent
4770
+ };
4771
+ }
4772
+ /** Publish one newly registered identity, then start realtime only in the background. */
4208
4773
  async activateRegisteredIdentity(identity) {
4774
+ this.pendingDeviceJoin = void 0;
4775
+ this.activeDeviceJoinSessionId = void 0;
4209
4776
  this.activeIdentityDid = identity.did;
4210
4777
  this.publishSession({
4211
4778
  status: "active",
4212
4779
  identity
4213
4780
  });
4214
4781
  const provider = this.provider;
4215
- if (provider !== void 0) {
4216
- await provider.listenerStartup;
4217
- await this.startListener(provider);
4218
- }
4782
+ if (provider !== void 0) this.ensureProviderRuntime(provider);
4219
4783
  }
4220
4784
  /** Invalidate cached session work and cancel every model request still owned by the old session. */
4221
4785
  invalidateSummaries() {
4222
4786
  this.sessionRevision += 1;
4787
+ this.requestRefs.clear();
4788
+ this.requestSessions.clear();
4789
+ this.deviceRefs.clear();
4790
+ this.deviceIds.clear();
4223
4791
  for (const controller of this.activeSummaryRequests) controller.abort();
4224
4792
  this.activeSummaryRequests.clear();
4225
4793
  }
@@ -4319,120 +4887,148 @@ let AwikiService = (() => {
4319
4887
  this.sessionMutation = pending.then(() => void 0, () => void 0);
4320
4888
  return pending;
4321
4889
  }
4322
- /** Start one exact identity-bound listener, atomically releasing a failed startup. */
4323
- startListener(provider) {
4324
- if (!this.resolved.listenerEnabled || provider.listener !== void 0 || provider.listenerRecoveryBlocked || this.provider !== provider) return Promise.resolve();
4325
- if (provider.listenerStartup !== void 0) return provider.listenerStartup;
4326
- if (provider.listenerCleanup !== void 0 || provider.listenerRestartTimer !== void 0) return Promise.resolve();
4327
- const workspaceContext = this.workspaceContext;
4328
- const source = provider.client.listener;
4329
- if (workspaceContext === void 0 || source === void 0) return Promise.resolve();
4330
- const generation = provider.listenerGeneration;
4331
- const logger = this.ctx.logger("awiki-listener");
4890
+ /** Start identity realtime and the optional Agent consumer without blocking identity success. */
4891
+ ensureProviderRuntime(provider) {
4892
+ const identityDid = this.activeIdentityDid;
4893
+ if (provider.runtimeReplacement !== void 0) return;
4894
+ if (identityDid !== void 0 && provider.realtimeIdentityDid !== void 0 && provider.realtimeIdentityDid !== identityDid) {
4895
+ this.replaceProviderRuntime(provider, identityDid);
4896
+ return;
4897
+ }
4898
+ this.ensureRealtimeSupervisor(provider);
4899
+ this.ensureAgentConsumer(provider);
4900
+ }
4901
+ replaceProviderRuntime(provider, identityDid) {
4902
+ if (provider.runtimeReplacement !== void 0 || this.provider !== provider) return;
4903
+ const observed = (async () => {
4904
+ await this.stopProviderRuntime(provider);
4905
+ if (this.provider !== provider || this.activeIdentityDid !== identityDid) return;
4906
+ this.ensureRealtimeSupervisor(provider);
4907
+ this.ensureAgentConsumer(provider);
4908
+ })().catch((error) => {
4909
+ this.ctx.logger("awiki-realtime").warn("AWiki identity realtime replacement failed: %s", error instanceof Error ? error.message : "unknown failure");
4910
+ }).finally(() => {
4911
+ if (provider.runtimeReplacement === observed) delete provider.runtimeReplacement;
4912
+ if (this.provider === provider) this.ensureProviderRuntime(provider);
4913
+ });
4914
+ provider.runtimeReplacement = observed;
4915
+ }
4916
+ ensureRealtimeSupervisor(provider) {
4917
+ if (!this.resolved.realtimeEnabled || provider.realtimeSupervisor !== void 0 || provider.realtimeStartup !== void 0 || this.provider !== provider || (provider.client.realtime ?? provider.client.listener) === void 0) return;
4918
+ const generation = provider.realtimeGeneration;
4919
+ const source = provider.client.realtime ?? provider.client.listener;
4920
+ if (source === void 0) return;
4921
+ const logger = this.ctx.logger("awiki-realtime");
4332
4922
  const observed = (async () => {
4333
4923
  if (await this.isSignedOut()) return;
4334
- if (await provider.client.getIdentity() === null) return;
4335
- if (!this.listenerFenceMatches(provider, workspaceContext, generation)) return;
4336
- const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
4337
- const listener = new AwikiAgentListener(source, agents, this.resolved.listener, logger);
4338
- provider.listener = listener;
4339
- try {
4340
- await listener.start();
4341
- } catch (error) {
4342
- if (provider.listener === listener) delete provider.listener;
4343
- try {
4344
- await listener.dispose();
4345
- } catch {
4346
- provider.listenerRecoveryBlocked = true;
4347
- }
4348
- if (!provider.listenerRecoveryBlocked && provider.listenerRestartAttempt > 0) this.scheduleListenerRestart(provider, workspaceContext, generation);
4349
- throw error;
4350
- }
4351
- if (!this.listenerFenceMatches(provider, workspaceContext, generation)) {
4352
- if (provider.listener === listener) delete provider.listener;
4353
- try {
4354
- await listener.dispose();
4355
- } catch (error) {
4356
- provider.listenerRecoveryBlocked = true;
4357
- throw error;
4358
- }
4359
- return;
4360
- }
4361
- provider.listenerStartedAt = Date.now();
4362
- listener.whenTerminated().then((result) => {
4363
- if (result.kind === "failed") this.releaseFailedListener(provider, listener, workspaceContext, generation, result.error);
4364
- });
4924
+ const identity = await provider.client.getIdentity();
4925
+ if (identity === null || !this.realtimeFenceMatches(provider, generation)) return;
4926
+ this.activeIdentityDid = identity.did;
4927
+ const supervisor = new IdentityRealtimeSupervisor(source, { onSynchronized: (cause) => this.onRealtimeSynchronized(provider, generation, cause) }, logger);
4928
+ provider.realtimeSupervisor = supervisor;
4929
+ provider.realtimeIdentityDid = identity.did;
4930
+ supervisor.start();
4931
+ this.ensureAgentConsumer(provider);
4365
4932
  })().catch((error) => {
4366
- logger.warn("AWiki listener startup failed: %s", error instanceof Error ? error.message : "unknown failure");
4933
+ logger.warn("AWiki realtime startup failed: %s", error instanceof Error ? error.message : "unknown failure");
4367
4934
  }).finally(() => {
4368
- if (provider.listenerStartup === observed) delete provider.listenerStartup;
4935
+ if (provider.realtimeStartup === observed) delete provider.realtimeStartup;
4369
4936
  });
4370
- provider.listenerStartup = observed;
4371
- return observed;
4372
- }
4373
- async stopListener(provider) {
4374
- provider.listenerGeneration += 1;
4375
- provider.listenerRestartAttempt = 0;
4376
- delete provider.listenerStartedAt;
4377
- const timer = provider.listenerRestartTimer;
4378
- if (timer !== void 0) {
4379
- delete provider.listenerRestartTimer;
4380
- clearTimeout(timer);
4381
- }
4382
- await provider.listenerStartup;
4383
- await provider.listenerCleanup;
4384
- const listener = provider.listener;
4385
- if (listener === void 0) return;
4386
- delete provider.listener;
4387
- try {
4388
- await listener.dispose();
4389
- } catch (error) {
4390
- provider.listenerRecoveryBlocked = true;
4391
- throw error;
4392
- }
4937
+ provider.realtimeStartup = observed;
4938
+ }
4939
+ realtimeFenceMatches(provider, generation) {
4940
+ return this.provider === provider && provider.realtimeGeneration === generation;
4393
4941
  }
4394
- listenerFenceMatches(provider, workspaceContext, generation) {
4395
- return this.provider === provider && this.workspaceContext === workspaceContext && provider.listenerGeneration === generation;
4942
+ async onRealtimeSynchronized(provider, generation, cause) {
4943
+ if (!this.realtimeFenceMatches(provider, generation)) return;
4944
+ if (cause === "system_notification" || cause === "stream_recovery") try {
4945
+ provider.localDeviceJoinRequestCountAfterSync = (await provider.client.listLocalDeviceJoinRequests()).length;
4946
+ } catch {
4947
+ this.ctx.logger("awiki-realtime").debug("AWiki realtime could not observe local device Join requests");
4948
+ }
4949
+ if (![
4950
+ "session_start",
4951
+ "connection_ready",
4952
+ "reconnected",
4953
+ "message",
4954
+ "message_update"
4955
+ ].includes(cause)) return;
4956
+ this.ensureAgentConsumer(provider);
4957
+ await provider.agentConsumerStartup;
4958
+ if (!this.realtimeFenceMatches(provider, generation)) return;
4959
+ await provider.agentConsumer?.reconcileOnce();
4396
4960
  }
4397
- releaseFailedListener(provider, listener, workspaceContext, generation, error) {
4398
- if (provider.listener !== listener) return;
4399
- delete provider.listener;
4400
- const startedAt = provider.listenerStartedAt;
4401
- delete provider.listenerStartedAt;
4402
- if (startedAt !== void 0 && Date.now() - startedAt >= LISTENER_STABLE_RESET_MS) provider.listenerRestartAttempt = 0;
4961
+ ensureAgentConsumer(provider) {
4962
+ const workspaceContext = this.workspaceContext;
4963
+ const source = provider.client.agentInbox ?? provider.client.listener;
4964
+ const identityDid = this.activeIdentityDid;
4965
+ if (!this.resolved.listenerEnabled || workspaceContext === void 0 || source === void 0 || identityDid === void 0 || this.provider !== provider) return;
4966
+ if (provider.agentConsumer !== void 0 && provider.agentConsumerIdentityDid === identityDid) return;
4967
+ if (provider.agentConsumerStartup !== void 0) return;
4968
+ const generation = ++provider.agentConsumerGeneration;
4403
4969
  const logger = this.ctx.logger("awiki-listener");
4404
- logger.warn("AWiki listener lifecycle failed: %s", error instanceof Error ? error.message : "unknown failure");
4405
- let disposed = false;
4406
- const observed = listener.dispose().then(() => {
4407
- disposed = true;
4408
- }, (cleanupError) => {
4409
- provider.listenerRecoveryBlocked = true;
4410
- logger.warn("AWiki listener cleanup failed: %s", cleanupError instanceof Error ? cleanupError.message : "unknown failure");
4970
+ const observed = (async () => {
4971
+ await this.detachAgentConsumer(provider);
4972
+ if (!this.agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation)) return;
4973
+ const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
4974
+ const consumer = new AwikiAgentListener(source, agents, {
4975
+ ...this.resolved.listener,
4976
+ identityScope: identityDid
4977
+ }, logger);
4978
+ provider.agentConsumer = consumer;
4979
+ provider.agentConsumerIdentityDid = identityDid;
4980
+ await consumer.reconcileOnce();
4981
+ })().catch((error) => {
4982
+ logger.warn("AWiki Agent consumer startup failed: %s", error instanceof Error ? error.message : "unknown failure");
4411
4983
  }).finally(() => {
4412
- if (provider.listenerCleanup === observed) delete provider.listenerCleanup;
4413
- if (disposed) this.scheduleListenerRestart(provider, workspaceContext, generation);
4984
+ if (provider.agentConsumerStartup === observed) delete provider.agentConsumerStartup;
4985
+ });
4986
+ provider.agentConsumerStartup = observed;
4987
+ }
4988
+ agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation) {
4989
+ return this.provider === provider && this.workspaceContext === workspaceContext && this.activeIdentityDid === identityDid && provider.agentConsumerGeneration === generation;
4990
+ }
4991
+ async detachAgentConsumer(provider) {
4992
+ const consumer = provider.agentConsumer;
4993
+ if (consumer === void 0) return;
4994
+ delete provider.agentConsumer;
4995
+ delete provider.agentConsumerIdentityDid;
4996
+ const observed = consumer.dispose().finally(() => {
4997
+ if (provider.agentConsumerCleanup === observed) delete provider.agentConsumerCleanup;
4414
4998
  });
4415
- provider.listenerCleanup = observed;
4416
- }
4417
- scheduleListenerRestart(provider, workspaceContext, generation) {
4418
- if (provider.listenerRestartTimer !== void 0 || !this.listenerFenceMatches(provider, workspaceContext, generation)) return;
4419
- provider.listenerRestartAttempt = Math.min(provider.listenerRestartAttempt + 1, LISTENER_RESTART_MAX_ATTEMPT);
4420
- const exponent = provider.listenerRestartAttempt - 1;
4421
- const delay = Math.min(LISTENER_RESTART_BASE_DELAY_MS * 2 ** exponent, LISTENER_RESTART_MAX_DELAY_MS);
4422
- const timer = setTimeout(() => {
4423
- if (provider.listenerRestartTimer !== timer) return;
4424
- delete provider.listenerRestartTimer;
4425
- if (!this.listenerFenceMatches(provider, workspaceContext, generation)) return;
4426
- this.startListener(provider);
4427
- }, delay);
4428
- provider.listenerRestartTimer = timer;
4999
+ provider.agentConsumerCleanup = observed;
5000
+ await observed;
5001
+ }
5002
+ async stopAgentConsumer(provider) {
5003
+ provider.agentConsumerGeneration += 1;
5004
+ await provider.agentConsumerStartup;
5005
+ await provider.agentConsumerCleanup;
5006
+ await this.detachAgentConsumer(provider);
5007
+ }
5008
+ async stopRealtimeSupervisor(provider) {
5009
+ provider.realtimeGeneration += 1;
5010
+ await provider.realtimeStartup;
5011
+ const supervisor = provider.realtimeSupervisor;
5012
+ if (supervisor === void 0) return;
5013
+ delete provider.realtimeSupervisor;
5014
+ delete provider.realtimeIdentityDid;
5015
+ await supervisor.dispose();
5016
+ }
5017
+ async stopProviderRuntime(provider) {
5018
+ await Promise.all([this.stopRealtimeSupervisor(provider), this.stopAgentConsumer(provider)]);
4429
5019
  }
4430
5020
  /** Clear one exact provider slot before joining its one shared disposal. */
4431
5021
  disposeProvider(provider) {
4432
- if (this.provider === provider) this.provider = void 0;
5022
+ if (this.provider === provider) {
5023
+ this.provider = void 0;
5024
+ this.pendingDeviceJoin = void 0;
5025
+ this.activeDeviceJoinSessionId = void 0;
5026
+ this.invalidateSummaries();
5027
+ }
4433
5028
  provider.disposal ??= (async () => {
4434
5029
  try {
4435
- await this.stopListener(provider);
5030
+ await provider.runtimeReplacement;
5031
+ await this.stopProviderRuntime(provider);
4436
5032
  } finally {
4437
5033
  await provider.client.dispose();
4438
5034
  }