@awiki/dsh-plugin 0.3.3 → 0.3.4

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 (66) hide show
  1. package/README.md +37 -31
  2. package/README.zh.md +30 -27
  3. package/cordis.patch.yml +17 -0
  4. package/lib/client.js +1269 -272
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +874 -314
  7. package/lib/provider.js +52 -16
  8. package/lib/{sdk-adapter-CDLTgHJA.mjs → sdk-adapter-BeJakxLP.mjs} +142 -4
  9. package/lib/typert.host.js +419 -89
  10. package/lib/typert.remote-client.d.ts +19 -3
  11. package/lib/typert.remote-client.d.ts.map +1 -1
  12. package/lib/typert.remote-client.js +419 -89
  13. package/lib/types/client/AwikiDevices.d.ts +28 -0
  14. package/lib/types/client/AwikiDevices.d.ts.map +1 -0
  15. package/lib/types/client/AwikiDevices.js +85 -0
  16. package/lib/types/client/AwikiDevices.js.map +1 -0
  17. package/lib/types/client/AwikiIdentityAccess.d.ts +6 -5
  18. package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
  19. package/lib/types/client/AwikiIdentityAccess.js +88 -27
  20. package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
  21. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  22. package/lib/types/client/AwikiOverlay.js +4 -3
  23. package/lib/types/client/AwikiOverlay.js.map +1 -1
  24. package/lib/types/client/controller.d.ts +20 -5
  25. package/lib/types/client/controller.d.ts.map +1 -1
  26. package/lib/types/client/controller.js +38 -7
  27. package/lib/types/client/controller.js.map +1 -1
  28. package/lib/types/client/index.d.ts.map +1 -1
  29. package/lib/types/client/index.js +8 -0
  30. package/lib/types/client/index.js.map +1 -1
  31. package/lib/types/client/slots.d.ts +10 -2
  32. package/lib/types/client/slots.d.ts.map +1 -1
  33. package/lib/types/index.d.ts +51 -10
  34. package/lib/types/index.d.ts.map +1 -1
  35. package/lib/types/index.js +675 -174
  36. package/lib/types/index.js.map +1 -1
  37. package/lib/types/listener-state.d.ts +5 -3
  38. package/lib/types/listener-state.d.ts.map +1 -1
  39. package/lib/types/listener-state.js +28 -13
  40. package/lib/types/listener-state.js.map +1 -1
  41. package/lib/types/listener.d.ts +9 -32
  42. package/lib/types/listener.d.ts.map +1 -1
  43. package/lib/types/listener.js +11 -127
  44. package/lib/types/listener.js.map +1 -1
  45. package/lib/types/provider-api.d.ts +96 -4
  46. package/lib/types/provider-api.d.ts.map +1 -1
  47. package/lib/types/provider.d.ts +7 -1
  48. package/lib/types/provider.d.ts.map +1 -1
  49. package/lib/types/provider.js +53 -15
  50. package/lib/types/provider.js.map +1 -1
  51. package/lib/types/realtime-supervisor.d.ts +43 -0
  52. package/lib/types/realtime-supervisor.d.ts.map +1 -0
  53. package/lib/types/realtime-supervisor.js +166 -0
  54. package/lib/types/realtime-supervisor.js.map +1 -0
  55. package/lib/types/sdk-adapter.d.ts +28 -2
  56. package/lib/types/sdk-adapter.d.ts.map +1 -1
  57. package/lib/types/sdk-adapter.js +132 -3
  58. package/lib/types/sdk-adapter.js.map +1 -1
  59. package/lib/types/types.d.ts +76 -6
  60. package/lib/types/types.d.ts.map +1 -1
  61. package/lib/types/types.js.map +1 -1
  62. package/package.json +18 -18
  63. package/lib/types/profile-state.d.ts +0 -11
  64. package/lib/types/profile-state.d.ts.map +0 -1
  65. package/lib/types/profile-state.js +0 -48
  66. 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-BeJakxLP.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. */
@@ -1450,12 +1448,13 @@ var AwikiConversationPreferenceStore = class {
1450
1448
  };
1451
1449
  //#endregion
1452
1450
  //#region lib/types/listener-state.js
1453
- const STATE_VERSION = 1;
1451
+ const STATE_VERSION = 2;
1454
1452
  const MAX_STATE_BYTES = 1048576;
1455
1453
  const MAX_CONVERSATIONS = 1e3;
1456
- function emptyState() {
1454
+ function emptyState(identityScopeHash) {
1457
1455
  return {
1458
1456
  version: STATE_VERSION,
1457
+ identityScopeHash,
1459
1458
  conversations: {}
1460
1459
  };
1461
1460
  }
@@ -1467,8 +1466,10 @@ function boundedString(value, field) {
1467
1466
  return value;
1468
1467
  }
1469
1468
  /** 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");
1469
+ function parseState(value, identityScopeHash) {
1470
+ if (isRecord(value) && value.version === 1) return emptyState(identityScopeHash);
1471
+ if (isRecord(value) && value.version === STATE_VERSION && typeof value.identityScopeHash === "string" && value.identityScopeHash !== identityScopeHash) return emptyState(identityScopeHash);
1472
+ if (!isRecord(value) || value.version !== STATE_VERSION || value.identityScopeHash !== identityScopeHash || !isRecord(value.conversations)) throw new TypeError("awiki: listener state is invalid");
1472
1473
  const entries = Object.entries(value.conversations);
1473
1474
  if (entries.length > MAX_CONVERSATIONS) throw new TypeError("awiki: listener state is too large");
1474
1475
  const conversations = {};
@@ -1491,6 +1492,7 @@ function parseState(value) {
1491
1492
  }
1492
1493
  return {
1493
1494
  version: STATE_VERSION,
1495
+ identityScopeHash,
1494
1496
  conversations
1495
1497
  };
1496
1498
  }
@@ -1501,28 +1503,31 @@ function isMissing(error) {
1501
1503
  var AwikiListenerStateStore = class {
1502
1504
  hostDirectory;
1503
1505
  statePath;
1504
- constructor(stateRoot) {
1506
+ identityScopeHash;
1507
+ constructor(stateRoot, identityScope) {
1508
+ if (identityScope.length === 0 || identityScope.length > 2048) throw new TypeError("awiki: listener identity scope is invalid");
1509
+ this.identityScopeHash = createHash("sha256").update(identityScope).digest("hex");
1505
1510
  this.hostDirectory = join(stateRoot, ".host");
1506
1511
  this.statePath = join(this.hostDirectory, "listener-state.json");
1507
1512
  }
1508
- /** Load the current state or return an empty v1 document on first use. */
1513
+ /** Load the current identity scope or reset unscoped v1 state on first use. */
1509
1514
  async load() {
1510
- if (!await this.hasPrivateHostDirectory()) return emptyState();
1515
+ if (!await this.hasPrivateHostDirectory()) return emptyState(this.identityScopeHash);
1511
1516
  try {
1512
1517
  const metadata = await lstat(this.statePath);
1513
1518
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is invalid");
1514
1519
  const text = await readFile(this.statePath, "utf8");
1515
1520
  if (Buffer.byteLength(text, "utf8") > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is invalid");
1516
- return parseState(JSON.parse(text));
1521
+ return parseState(JSON.parse(text), this.identityScopeHash);
1517
1522
  } catch (error) {
1518
- if (isMissing(error)) return emptyState();
1523
+ if (isMissing(error)) return emptyState(this.identityScopeHash);
1519
1524
  if (error instanceof SyntaxError) throw new TypeError("awiki: listener state file is invalid");
1520
1525
  throw error;
1521
1526
  }
1522
1527
  }
1523
1528
  /** Replace the state atomically without ever writing message or Agent text. */
1524
1529
  async save(state) {
1525
- const snapshot = parseState(state);
1530
+ const snapshot = parseState(state, this.identityScopeHash);
1526
1531
  const text = `${JSON.stringify(snapshot)}\n`;
1527
1532
  if (Buffer.byteLength(text, "utf8") > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is too large");
1528
1533
  await mkdir(this.hostDirectory, {
@@ -1744,11 +1749,12 @@ function incomingFromPeer(message, conversation) {
1744
1749
  }
1745
1750
  function copyState(state) {
1746
1751
  return {
1747
- version: 1,
1752
+ version: 2,
1753
+ identityScopeHash: state.identityScopeHash,
1748
1754
  conversations: Object.fromEntries(Object.entries(state.conversations).map(([id, route]) => [id, { ...route }]))
1749
1755
  };
1750
1756
  }
1751
- /** Reconcile authorized Direct text from Core history whenever realtime schedules synchronization. */
1757
+ /** Consume authorized Direct text only after the identity supervisor commits synchronization. */
1752
1758
  var AwikiAgentListener = class {
1753
1759
  awiki;
1754
1760
  agents;
@@ -1757,34 +1763,23 @@ var AwikiAgentListener = class {
1757
1763
  store;
1758
1764
  logger;
1759
1765
  stateReady;
1760
- state = {
1761
- version: 1,
1762
- conversations: {}
1763
- };
1766
+ state;
1764
1767
  stateMutation = Promise.resolve();
1765
1768
  syncMutation = Promise.resolve();
1766
1769
  scheduledMessageIds = /* @__PURE__ */ new Set();
1767
1770
  conversationQueues = /* @__PURE__ */ new Map();
1768
- lifecycle;
1769
- started;
1770
- resolveStarted;
1771
- rejectStarted;
1772
- lifecycleGeneration = 0;
1773
- streamGeneration = 0;
1774
- activeRealtime;
1775
1771
  stopped = false;
1776
- termination;
1777
- resolveTermination;
1778
- terminationSettled = false;
1779
1772
  constructor(awiki, agents, config, logger, store) {
1780
1773
  this.awiki = awiki;
1781
1774
  this.agents = agents;
1782
1775
  this.config = config;
1783
- this.termination = new Promise((resolve) => {
1784
- this.resolveTermination = resolve;
1785
- });
1786
1776
  this.allowedPeers = new Set(config.allowedPeers.map((peer) => peer.startsWith("did:") ? peer : peer.toLowerCase()));
1787
- this.store = store ?? new AwikiListenerStateStore(config.stateRoot);
1777
+ this.store = store ?? new AwikiListenerStateStore(config.stateRoot, config.identityScope);
1778
+ this.state = {
1779
+ version: 2,
1780
+ identityScopeHash: this.store.identityScopeHash,
1781
+ conversations: {}
1782
+ };
1788
1783
  this.logger = logger ?? {
1789
1784
  debug() {},
1790
1785
  info() {},
@@ -1796,38 +1791,11 @@ var AwikiAgentListener = class {
1796
1791
  this.state = state;
1797
1792
  });
1798
1793
  }
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) {
1794
+ /** Reconcile only committed history; this consumer cannot start WSS or advance sync. */
1795
+ async reconcileOnce() {
1826
1796
  await this.enqueueSync(async () => {
1827
1797
  await this.stateReady;
1828
1798
  if (this.stopped) return;
1829
- await this.awiki.syncNow(reason);
1830
- if (this.stopped) return;
1831
1799
  await this.reconcileCommittedHistory();
1832
1800
  });
1833
1801
  }
@@ -1837,84 +1805,12 @@ var AwikiAgentListener = class {
1837
1805
  while (this.conversationQueues.size > 0) await Promise.allSettled([...this.conversationQueues.values()]);
1838
1806
  await this.stateMutation;
1839
1807
  }
1840
- /** Stop realtime first, fence late events, drain messages, then release listener-owned Agents. */
1808
+ /** Fence late work, drain committed messages, then release listener-owned Agents. */
1841
1809
  async dispose() {
1842
1810
  if (this.stopped) return;
1843
1811
  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
1812
  await this.whenIdle();
1854
1813
  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
1814
  }
1919
1815
  enqueueSync(operation) {
1920
1816
  const pending = this.syncMutation.then(operation, operation);
@@ -2081,7 +1977,8 @@ var AwikiAgentListener = class {
2081
1977
  ...update.lastProcessedMessageId === void 0 ? current?.lastProcessedMessageId === void 0 ? {} : { lastProcessedMessageId: current.lastProcessedMessageId } : { lastProcessedMessageId: update.lastProcessedMessageId }
2082
1978
  };
2083
1979
  this.state = {
2084
- version: 1,
1980
+ version: 2,
1981
+ identityScopeHash: this.state.identityScopeHash,
2085
1982
  conversations: {
2086
1983
  ...this.state.conversations,
2087
1984
  [conversation.id]: route
@@ -2094,39 +1991,153 @@ var AwikiAgentListener = class {
2094
1991
  }
2095
1992
  };
2096
1993
  //#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;
1994
+ //#region lib/types/realtime-supervisor.js
1995
+ const RETRY_BASE_DELAY_MS = 1e3;
1996
+ const RETRY_MAX_DELAY_MS = 3e4;
1997
+ function syncReason(cause) {
1998
+ if (cause === "session_start") return "session_start";
1999
+ return cause === "reconnected" ? "websocket_reconnect" : "websocket_hint";
2102
2000
  }
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;
2113
- 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));
2118
- } catch {
2119
- return;
2001
+ /** Own the deployment identity's only WSS without knowing Workspace or Agent policy. */
2002
+ var IdentityRealtimeSupervisor = class {
2003
+ realtime;
2004
+ config;
2005
+ logger;
2006
+ lifecycle;
2007
+ generation = 0;
2008
+ activeSession;
2009
+ stoppedSessions = /* @__PURE__ */ new WeakSet();
2010
+ stopped = false;
2011
+ connected = false;
2012
+ startCount = 0;
2013
+ stopCount = 0;
2014
+ lastCommittedSyncCause;
2015
+ retryTimer;
2016
+ resolveRetry;
2017
+ constructor(realtime, config = {}, logger = {
2018
+ debug() {},
2019
+ info() {},
2020
+ warn() {},
2021
+ error() {},
2022
+ name: "awiki-realtime"
2023
+ }) {
2024
+ this.realtime = realtime;
2025
+ this.config = config;
2026
+ this.logger = logger;
2120
2027
  }
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
- }
2028
+ /** Start in the background; identity activation must never await connectivity. */
2029
+ start() {
2030
+ if (this.lifecycle !== void 0 || this.stopped) return;
2031
+ const generation = ++this.generation;
2032
+ this.lifecycle = this.run(generation).catch((error) => {
2033
+ if (!this.stopped) this.logger.warn("AWiki realtime supervisor stopped unexpectedly: %s", error instanceof Error ? error.message : "unknown failure");
2034
+ });
2035
+ }
2036
+ diagnostics() {
2037
+ return {
2038
+ connected: this.connected,
2039
+ activeSessionCount: this.activeSession === void 0 ? 0 : 1,
2040
+ startCount: this.startCount,
2041
+ stopCount: this.stopCount,
2042
+ ...this.lastCommittedSyncCause === void 0 ? {} : { lastCommittedSyncCause: this.lastCommittedSyncCause }
2043
+ };
2044
+ }
2045
+ /** Fence late events, wake retry sleep, stop the exact session, and join the lifecycle. */
2046
+ async dispose() {
2047
+ if (this.stopped) return;
2048
+ this.stopped = true;
2049
+ this.generation += 1;
2050
+ this.connected = false;
2051
+ this.wakeRetry();
2052
+ const session = this.activeSession;
2053
+ if (session !== void 0) await this.stopSession(session).catch(() => void 0);
2054
+ await this.lifecycle;
2055
+ }
2056
+ current(generation) {
2057
+ return !this.stopped && this.generation === generation;
2058
+ }
2059
+ async run(generation) {
2060
+ let cause = "session_start";
2061
+ let failures = 0;
2062
+ while (this.current(generation)) {
2063
+ let session;
2064
+ try {
2065
+ await this.synchronize(cause, generation);
2066
+ if (!this.current(generation)) return;
2067
+ session = await this.realtime.startRealtime();
2068
+ if (!this.current(generation)) {
2069
+ await this.stopSession(session).catch(() => void 0);
2070
+ return;
2071
+ }
2072
+ this.activeSession = session;
2073
+ this.startCount += 1;
2074
+ this.connected = (await session.getStatus().catch(() => ({ connected: false }))).connected;
2075
+ failures = 0;
2076
+ while (this.current(generation) && this.activeSession === session) {
2077
+ const event = await session.nextEvent();
2078
+ if (!this.current(generation) || this.activeSession !== session) return;
2079
+ if (event === null) {
2080
+ await this.stopSession(session);
2081
+ cause = "reconnected";
2082
+ break;
2083
+ }
2084
+ this.observeConnection(event);
2085
+ if (event.kind !== "sync_required") continue;
2086
+ await this.synchronize(event.cause, generation);
2087
+ }
2088
+ } catch (error) {
2089
+ this.connected = false;
2090
+ if (session !== void 0) await this.stopSession(session).catch(() => void 0);
2091
+ if (!this.current(generation)) return;
2092
+ failures += 1;
2093
+ this.logger.warn("AWiki realtime lifecycle failed; retrying: %s", error instanceof Error ? error.message : "unknown failure");
2094
+ await this.waitForRetry(failures, generation);
2095
+ cause = cause === "session_start" ? "session_start" : "reconnected";
2096
+ }
2097
+ }
2098
+ }
2099
+ async synchronize(cause, generation) {
2100
+ await this.realtime.syncNow(syncReason(cause));
2101
+ if (!this.current(generation)) return;
2102
+ this.lastCommittedSyncCause = cause;
2103
+ try {
2104
+ await this.config.onSynchronized?.(cause);
2105
+ } catch (error) {
2106
+ this.logger.warn("AWiki realtime post-sync consumer failed: %s", error instanceof Error ? error.message : "unknown failure");
2107
+ }
2108
+ }
2109
+ observeConnection(event) {
2110
+ if (event.kind !== "connection_state_changed") return;
2111
+ this.connected = event.state === "connected";
2112
+ }
2113
+ async stopSession(session) {
2114
+ if (this.stoppedSessions.has(session)) return;
2115
+ this.stoppedSessions.add(session);
2116
+ if (this.activeSession === session) this.activeSession = void 0;
2117
+ this.connected = false;
2118
+ this.stopCount += 1;
2119
+ await session.stop();
2120
+ }
2121
+ waitForRetry(failures, generation) {
2122
+ if (!this.current(generation)) return Promise.resolve();
2123
+ const delay = Math.min(RETRY_BASE_DELAY_MS * 2 ** Math.min(failures - 1, 10), RETRY_MAX_DELAY_MS);
2124
+ return new Promise((resolve) => {
2125
+ const finish = () => {
2126
+ if (this.retryTimer === timer) this.retryTimer = void 0;
2127
+ if (this.resolveRetry === finish) this.resolveRetry = void 0;
2128
+ resolve();
2129
+ };
2130
+ const timer = setTimeout(finish, delay);
2131
+ this.retryTimer = timer;
2132
+ this.resolveRetry = finish;
2133
+ });
2134
+ }
2135
+ wakeRetry() {
2136
+ const timer = this.retryTimer;
2137
+ if (timer !== void 0) clearTimeout(timer);
2138
+ this.resolveRetry?.();
2139
+ }
2140
+ };
2130
2141
  //#endregion
2131
2142
  //#region lib/types/index.js
2132
2143
  /** Unified AWiki identity, messaging, attachment, Remote, and model-tool service. */
@@ -2198,6 +2209,7 @@ const Config = z.object({
2198
2209
  attachmentMaxBytes: z.number().default(DEFAULT_ATTACHMENT_MAX_BYTES),
2199
2210
  imageAttachmentCacheMaxBytes: z.number().default(DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES),
2200
2211
  pollIntervalMs: z.number().default(DEFAULT_POLL_INTERVAL_MS),
2212
+ realtimeEnabled: z.boolean().default(true),
2201
2213
  listenerEnabled: z.boolean().default(false),
2202
2214
  listenerAllowedPeers: z.array(z.string()).default([]),
2203
2215
  listenerWorkspacePath: z.string(),
@@ -2215,7 +2227,6 @@ const FAILURE_CODES = /* @__PURE__ */ new Set([
2215
2227
  "forbidden",
2216
2228
  "identity-recovery-required",
2217
2229
  "conflict",
2218
- "state-in-use",
2219
2230
  "rate-limited",
2220
2231
  "group-membership-required",
2221
2232
  "group-identity-stale",
@@ -2229,10 +2240,17 @@ const FAILURE_CODES = /* @__PURE__ */ new Set([
2229
2240
  "network",
2230
2241
  "remote"
2231
2242
  ]);
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;
2243
+ const DEVICE_JOIN_APPROVAL_CONFIRMATION = "APPROVE";
2244
+ const DEVICE_REVOKE_CONFIRMATION = "REVOKE";
2245
+ const DEVICE_JOIN_CHALLENGE_TTL_SECONDS = 240;
2246
+ const RESUMABLE_JOIN_PHASES = /* @__PURE__ */ new Set([
2247
+ "pending",
2248
+ "challenge_prepared",
2249
+ "response_prepared",
2250
+ "response_verified",
2251
+ "approval_prepared",
2252
+ "authorized"
2253
+ ]);
2236
2254
  const FAILURE_MESSAGES = {
2237
2255
  "not-registered": "No AWiki identity is registered for this deployment.",
2238
2256
  "signed-out": "This installation is signed out of AWiki.",
@@ -2245,7 +2263,6 @@ const FAILURE_MESSAGES = {
2245
2263
  "forbidden": "The AWiki operation is not permitted.",
2246
2264
  "identity-recovery-required": "The local AWiki identity must be recovered before it can be used again.",
2247
2265
  "conflict": "The AWiki operation conflicts with current state.",
2248
- "state-in-use": "AWiki data is already open in another client.",
2249
2266
  "rate-limited": "The AWiki service rate-limited the request.",
2250
2267
  "group-membership-required": "The active AWiki identity is not a member of this group.",
2251
2268
  "group-identity-stale": "The AWiki group identity binding is still recovering.",
@@ -2261,6 +2278,39 @@ const FAILURE_MESSAGES = {
2261
2278
  };
2262
2279
  var ProviderUnavailableError = class extends Error {};
2263
2280
  var SummaryProviderUnavailableError = class extends Error {};
2281
+ function candidateJoinPhase(value) {
2282
+ if (value.remoteState === "rejected") return "rejected";
2283
+ if (value.remoteState === "cancelled" || value.localPhase === "cancelled") return "cancelled";
2284
+ if (value.remoteState === "expired" || value.localPhase === "expired") return "expired";
2285
+ if (value.localPhase === "authorized" && value.remoteState === "consumed" && value.completed && value.identity !== void 0) return "authorized";
2286
+ if (value.sas !== void 0) return value.remoteState === "response_verified" && /^\d{6}$/u.test(value.sas) ? "sas-ready" : void 0;
2287
+ if (value.remoteState === "challenge_sent" || value.remoteState === "response_verified" || value.remoteState === "consumed" || value.localPhase !== "pending") return "verifying";
2288
+ return value.remoteState === "pending" ? "pending" : void 0;
2289
+ }
2290
+ function adminJoinPhase(value) {
2291
+ if (value.remoteState === "rejected") return "rejected";
2292
+ if (value.remoteState === "cancelled" || value.localPhase === "cancelled") return "cancelled";
2293
+ if (value.remoteState === "expired" || value.localPhase === "expired") return "expired";
2294
+ if (value.localPhase === "authorized" && value.remoteState === "consumed") return "authorized";
2295
+ if (value.sas !== void 0) return value.remoteState === "response_verified" && /^\d{6}$/u.test(value.sas) ? "sas-ready" : void 0;
2296
+ if (value.remoteState === "challenge_sent" || value.remoteState === "response_verified" || value.remoteState === "consumed" || value.localPhase !== "pending") return "verifying";
2297
+ return value.remoteState === "pending" ? "pending" : void 0;
2298
+ }
2299
+ function requestJoinPhase(value) {
2300
+ switch (value.state) {
2301
+ case "pending": return "pending";
2302
+ case "challenge_sent":
2303
+ case "response_verified": return "verifying";
2304
+ case "consumed": return "authorized";
2305
+ case "cancelled": return "cancelled";
2306
+ case "rejected": return "rejected";
2307
+ case "expired": return "expired";
2308
+ }
2309
+ }
2310
+ function constantTimeSasMatches(expected, actual) {
2311
+ if (!/^\d{6}$/u.test(expected) || !/^\d{6}$/u.test(actual)) return false;
2312
+ return timingSafeEqual(Buffer.from(expected, "ascii"), Buffer.from(actual, "ascii"));
2313
+ }
2264
2314
  /** Validate and preserve one SDK service URL without accepting insecure remote HTTP. */
2265
2315
  function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
2266
2316
  let url;
@@ -2331,14 +2381,12 @@ function listenerAllowedPeers(raw, enabled) {
2331
2381
  return peers;
2332
2382
  }
2333
2383
  /** Resolve and validate every deployment choice before publishing the service. */
2334
- function resolveConfig(ctx, config) {
2384
+ function resolveConfig(config) {
2335
2385
  const allowInsecureLoopbackForTesting = config.allowInsecureLoopbackForTesting ?? false;
2336
2386
  const configuredStateRoot = config.stateRoot?.trim();
2337
2387
  const configuredDshHome = process.env.DSH_HOME?.trim();
2338
2388
  const dshHome = configuredDshHome === void 0 || configuredDshHome.length === 0 ? join(homedir(), ".dsh") : configuredDshHome;
2339
- const profileName = configuredStateRoot === void 0 || configuredStateRoot.length === 0 ? resolveAwikiProfileName(ctx, dshHome) : void 0;
2340
- const stateRoot = configuredStateRoot === void 0 || configuredStateRoot.length === 0 ? resolveAwikiStateRoot(ctx, dshHome) : configuredStateRoot;
2341
- const legacySharedStateDetected = profileName !== void 0 && existsSync(join(dshHome, "awiki", "im-core"));
2389
+ const stateRoot = configuredStateRoot === void 0 || configuredStateRoot.length === 0 ? join(dshHome, "awiki", "im-core") : configuredStateRoot;
2342
2390
  if (!isAbsolute(stateRoot)) throw new TypeError("awiki: stateRoot must be an absolute path");
2343
2391
  const attachmentMaxBytes = config.attachmentMaxBytes ?? 10485760;
2344
2392
  if (!Number.isSafeInteger(attachmentMaxBytes) || attachmentMaxBytes < 1) throw new TypeError("awiki: attachmentMaxBytes must be a positive safe integer");
@@ -2347,6 +2395,8 @@ function resolveConfig(ctx, config) {
2347
2395
  const pollIntervalMs = config.pollIntervalMs ?? 3e3;
2348
2396
  if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 1e3 || pollIntervalMs > 6e4) throw new TypeError("awiki: pollIntervalMs must be a safe integer from 1000 through 60000");
2349
2397
  const listenerEnabled = config.listenerEnabled ?? false;
2398
+ const realtimeEnabled = config.realtimeEnabled ?? true;
2399
+ if (listenerEnabled && !realtimeEnabled) throw new TypeError("awiki: listenerEnabled requires realtimeEnabled");
2350
2400
  const listenerWorkspacePath = config.listenerWorkspacePath?.trim() || join(dshHome, "workspaces", "awiki");
2351
2401
  if (!isAbsolute(listenerWorkspacePath)) throw new TypeError("awiki: listenerWorkspacePath must be an absolute path");
2352
2402
  const allowedPeers = listenerAllowedPeers(config.listenerAllowedPeers, listenerEnabled);
@@ -2369,10 +2419,7 @@ function resolveConfig(ctx, config) {
2369
2419
  attachmentMaxBytes,
2370
2420
  imageAttachmentCacheMaxBytes,
2371
2421
  pollIntervalMs,
2372
- ...profileName === void 0 ? {} : {
2373
- profileName,
2374
- legacySharedStateDetected
2375
- },
2422
+ realtimeEnabled,
2376
2423
  listenerEnabled,
2377
2424
  listener: {
2378
2425
  allowedPeers,
@@ -2523,6 +2570,7 @@ function normalizeRecoveryPrepareRequest(request) {
2523
2570
  };
2524
2571
  }
2525
2572
  const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 65536;
2573
+ const SERVER_INFO_RESPONSE_MAX_BYTES = 65536;
2526
2574
  const RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES = 4096;
2527
2575
  /** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
2528
2576
  async function readBoundedResponseText(response, maxBytes) {
@@ -2741,6 +2789,14 @@ let AwikiService = (() => {
2741
2789
  let _inspectIdentityAccess_decorators;
2742
2790
  let _sendRegistrationOtp_decorators;
2743
2791
  let _registerIdentity_decorators;
2792
+ let _beginDeviceJoin_decorators;
2793
+ let _getDeviceJoinStatus_decorators;
2794
+ let _cancelDeviceJoin_decorators;
2795
+ let _refreshDeviceManagement_decorators;
2796
+ let _startDeviceJoinVerification_decorators;
2797
+ let _approveDeviceJoin_decorators;
2798
+ let _rejectDeviceJoin_decorators;
2799
+ let _revokeDevice_decorators;
2744
2800
  let _updateDisplayName_decorators;
2745
2801
  let _getProfile_decorators;
2746
2802
  let _updateProfile_decorators;
@@ -2786,6 +2842,14 @@ let AwikiService = (() => {
2786
2842
  _inspectIdentityAccess_decorators = [Remote];
2787
2843
  _sendRegistrationOtp_decorators = [Remote];
2788
2844
  _registerIdentity_decorators = [Remote];
2845
+ _beginDeviceJoin_decorators = [Remote];
2846
+ _getDeviceJoinStatus_decorators = [Remote];
2847
+ _cancelDeviceJoin_decorators = [Remote];
2848
+ _refreshDeviceManagement_decorators = [Remote];
2849
+ _startDeviceJoinVerification_decorators = [Remote];
2850
+ _approveDeviceJoin_decorators = [Remote];
2851
+ _rejectDeviceJoin_decorators = [Remote];
2852
+ _revokeDevice_decorators = [Remote];
2789
2853
  _updateDisplayName_decorators = [Remote];
2790
2854
  _getProfile_decorators = [Remote];
2791
2855
  _updateProfile_decorators = [Remote];
@@ -2908,6 +2972,94 @@ let AwikiService = (() => {
2908
2972
  },
2909
2973
  metadata: _metadata
2910
2974
  }, null, _instanceExtraInitializers);
2975
+ __esDecorate(this, null, _beginDeviceJoin_decorators, {
2976
+ kind: "method",
2977
+ name: "beginDeviceJoin",
2978
+ static: false,
2979
+ private: false,
2980
+ access: {
2981
+ has: (obj) => "beginDeviceJoin" in obj,
2982
+ get: (obj) => obj.beginDeviceJoin
2983
+ },
2984
+ metadata: _metadata
2985
+ }, null, _instanceExtraInitializers);
2986
+ __esDecorate(this, null, _getDeviceJoinStatus_decorators, {
2987
+ kind: "method",
2988
+ name: "getDeviceJoinStatus",
2989
+ static: false,
2990
+ private: false,
2991
+ access: {
2992
+ has: (obj) => "getDeviceJoinStatus" in obj,
2993
+ get: (obj) => obj.getDeviceJoinStatus
2994
+ },
2995
+ metadata: _metadata
2996
+ }, null, _instanceExtraInitializers);
2997
+ __esDecorate(this, null, _cancelDeviceJoin_decorators, {
2998
+ kind: "method",
2999
+ name: "cancelDeviceJoin",
3000
+ static: false,
3001
+ private: false,
3002
+ access: {
3003
+ has: (obj) => "cancelDeviceJoin" in obj,
3004
+ get: (obj) => obj.cancelDeviceJoin
3005
+ },
3006
+ metadata: _metadata
3007
+ }, null, _instanceExtraInitializers);
3008
+ __esDecorate(this, null, _refreshDeviceManagement_decorators, {
3009
+ kind: "method",
3010
+ name: "refreshDeviceManagement",
3011
+ static: false,
3012
+ private: false,
3013
+ access: {
3014
+ has: (obj) => "refreshDeviceManagement" in obj,
3015
+ get: (obj) => obj.refreshDeviceManagement
3016
+ },
3017
+ metadata: _metadata
3018
+ }, null, _instanceExtraInitializers);
3019
+ __esDecorate(this, null, _startDeviceJoinVerification_decorators, {
3020
+ kind: "method",
3021
+ name: "startDeviceJoinVerification",
3022
+ static: false,
3023
+ private: false,
3024
+ access: {
3025
+ has: (obj) => "startDeviceJoinVerification" in obj,
3026
+ get: (obj) => obj.startDeviceJoinVerification
3027
+ },
3028
+ metadata: _metadata
3029
+ }, null, _instanceExtraInitializers);
3030
+ __esDecorate(this, null, _approveDeviceJoin_decorators, {
3031
+ kind: "method",
3032
+ name: "approveDeviceJoin",
3033
+ static: false,
3034
+ private: false,
3035
+ access: {
3036
+ has: (obj) => "approveDeviceJoin" in obj,
3037
+ get: (obj) => obj.approveDeviceJoin
3038
+ },
3039
+ metadata: _metadata
3040
+ }, null, _instanceExtraInitializers);
3041
+ __esDecorate(this, null, _rejectDeviceJoin_decorators, {
3042
+ kind: "method",
3043
+ name: "rejectDeviceJoin",
3044
+ static: false,
3045
+ private: false,
3046
+ access: {
3047
+ has: (obj) => "rejectDeviceJoin" in obj,
3048
+ get: (obj) => obj.rejectDeviceJoin
3049
+ },
3050
+ metadata: _metadata
3051
+ }, null, _instanceExtraInitializers);
3052
+ __esDecorate(this, null, _revokeDevice_decorators, {
3053
+ kind: "method",
3054
+ name: "revokeDevice",
3055
+ static: false,
3056
+ private: false,
3057
+ access: {
3058
+ has: (obj) => "revokeDevice" in obj,
3059
+ get: (obj) => obj.revokeDevice
3060
+ },
3061
+ metadata: _metadata
3062
+ }, null, _instanceExtraInitializers);
2911
3063
  __esDecorate(this, null, _updateDisplayName_decorators, {
2912
3064
  kind: "method",
2913
3065
  name: "updateDisplayName",
@@ -3303,6 +3455,12 @@ let AwikiService = (() => {
3303
3455
  sessionMutation = Promise.resolve();
3304
3456
  sessionRevision = 0;
3305
3457
  activeIdentityDid;
3458
+ pendingDeviceJoin;
3459
+ activeDeviceJoinSessionId;
3460
+ requestRefs = /* @__PURE__ */ new Map();
3461
+ requestSessions = /* @__PURE__ */ new Map();
3462
+ deviceRefs = /* @__PURE__ */ new Map();
3463
+ deviceIds = /* @__PURE__ */ new Map();
3306
3464
  activeSummaryRequests = /* @__PURE__ */ new Set();
3307
3465
  summaryProvider;
3308
3466
  recoveryReconciliationTarget;
@@ -3317,7 +3475,7 @@ let AwikiService = (() => {
3317
3475
  constructor(ctx, config) {
3318
3476
  super(ctx, "awiki");
3319
3477
  this.hostContext = ctx;
3320
- this.resolved = resolveConfig(ctx, config);
3478
+ this.resolved = resolveConfig(config);
3321
3479
  this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
3322
3480
  this.sessionStore = new AwikiSessionStore(this.resolved.stateRoot);
3323
3481
  this.imageAttachmentCache = new AwikiImageAttachmentCache(this.resolved.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
@@ -3346,12 +3504,12 @@ let AwikiService = (() => {
3346
3504
  ctx.inject(["workspaceRegistry"], (workspaceCtx) => {
3347
3505
  this.workspaceContext = workspaceCtx;
3348
3506
  const provider = this.provider;
3349
- if (provider !== void 0) this.startListener(provider);
3507
+ if (provider !== void 0) this.ensureAgentConsumer(provider);
3350
3508
  workspaceCtx.effect(() => async () => {
3351
3509
  if (this.workspaceContext !== workspaceCtx) return;
3352
3510
  this.workspaceContext = void 0;
3353
3511
  const current = this.provider;
3354
- if (current !== void 0) await this.stopListener(current);
3512
+ if (current !== void 0) await this.stopAgentConsumer(current);
3355
3513
  }, "awiki: release Workspace listener composition");
3356
3514
  });
3357
3515
  registerAwikiTools(ctx, this);
@@ -3372,6 +3530,8 @@ let AwikiService = (() => {
3372
3530
  */
3373
3531
  registerClientFactory(factory) {
3374
3532
  if (this.provider !== void 0) throw new Error("awiki: a client provider is already registered");
3533
+ this.pendingDeviceJoin = void 0;
3534
+ this.activeDeviceJoinSessionId = void 0;
3375
3535
  const provider = {
3376
3536
  client: factory({
3377
3537
  userServiceUrl: this.resolved.userServiceUrl,
@@ -3385,14 +3545,28 @@ let AwikiService = (() => {
3385
3545
  allowInsecureLoopbackForTesting: this.resolved.allowInsecureLoopbackForTesting,
3386
3546
  stateRoot: this.resolved.stateRoot
3387
3547
  }),
3388
- listenerRestartAttempt: 0,
3389
- listenerGeneration: 0,
3390
- listenerRecoveryBlocked: false
3548
+ realtimeGeneration: 0,
3549
+ agentConsumerGeneration: 0,
3550
+ localDeviceJoinRequestCountAfterSync: 0
3391
3551
  };
3392
3552
  this.provider = provider;
3393
- this.startListener(provider);
3553
+ this.ensureRealtimeSupervisor(provider);
3554
+ this.ensureAgentConsumer(provider);
3394
3555
  return () => this.disposeProvider(provider);
3395
3556
  }
3557
+ /** Safe same-process diagnostics for focused E2E. Never exposed through Typert Remote. */
3558
+ getRealtimeDiagnostics() {
3559
+ const provider = this.provider;
3560
+ return {
3561
+ ...provider?.realtimeSupervisor?.diagnostics() ?? {
3562
+ connected: false,
3563
+ activeSessionCount: 0,
3564
+ startCount: 0,
3565
+ stopCount: 0
3566
+ },
3567
+ localDeviceJoinRequestCountAfterSync: provider?.localDeviceJoinRequestCountAfterSync ?? 0
3568
+ };
3569
+ }
3396
3570
  /** Register the optional Model Proxy recovery target without exposing an arbitrary callback or token. */
3397
3571
  registerRecoveryReconciliationTarget(target) {
3398
3572
  if (this.recoveryReconciliationTarget !== void 0) throw new Error("awiki: a recovery reconciliation target is already registered");
@@ -3420,18 +3594,42 @@ let AwikiService = (() => {
3420
3594
  * Read settings needed by the browser presentation.
3421
3595
  * @returns Browser-safe polling configuration without SDK endpoints or state paths.
3422
3596
  */
3423
- getConfig() {
3424
- return Promise.resolve({
3597
+ async getConfig() {
3598
+ let handleRecoveryPhoneEnabled = false;
3599
+ const abort = new AbortController();
3600
+ const timeout = setTimeout(() => {
3601
+ abort.abort();
3602
+ }, 1e4);
3603
+ try {
3604
+ const response = await fetch(new URL("/user-service/v1/server-info", this.resolved.userServiceUrl), {
3605
+ method: "GET",
3606
+ headers: {
3607
+ accept: "application/json",
3608
+ "cache-control": "no-store"
3609
+ },
3610
+ cache: "no-store",
3611
+ redirect: "error",
3612
+ signal: abort.signal
3613
+ });
3614
+ if (response.ok) {
3615
+ const text = await readBoundedResponseText(response, SERVER_INFO_RESPONSE_MAX_BYTES);
3616
+ const value = text === void 0 ? void 0 : JSON.parse(text);
3617
+ if (typeof value === "object" && value !== null && value.schema_version === 1) {
3618
+ const methods = value.identity?.handle_recovery?.methods;
3619
+ 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");
3620
+ }
3621
+ }
3622
+ } catch {} finally {
3623
+ clearTimeout(timeout);
3624
+ }
3625
+ return {
3425
3626
  ok: true,
3426
3627
  value: {
3427
3628
  pollIntervalMs: this.resolved.pollIntervalMs,
3428
3629
  attachmentMaxBytes: this.resolved.attachmentMaxBytes,
3429
- ...this.resolved.profileName === void 0 ? {} : {
3430
- profileName: this.resolved.profileName,
3431
- legacySharedStateDetected: this.resolved.legacySharedStateDetected
3432
- }
3630
+ handleRecoveryPhoneEnabled
3433
3631
  }
3434
- });
3632
+ };
3435
3633
  }
3436
3634
  /**
3437
3635
  * Read the deployment's identity status.
@@ -3454,6 +3652,8 @@ let AwikiService = (() => {
3454
3652
  const identity = await this.run((client) => client.getIdentity(), { allowSignedOut: true });
3455
3653
  if (!identity.ok) return identity;
3456
3654
  this.activeIdentityDid = identity.value?.did;
3655
+ const provider = this.provider;
3656
+ if (identity.value !== null && provider !== void 0) this.ensureProviderRuntime(provider);
3457
3657
  return identity.value === null ? {
3458
3658
  ok: true,
3459
3659
  value: { status: "unregistered" }
@@ -3490,7 +3690,7 @@ let AwikiService = (() => {
3490
3690
  const session = { status: "signed-out" };
3491
3691
  this.publishSession(session);
3492
3692
  const provider = this.provider;
3493
- if (provider !== void 0) await this.stopListener(provider);
3693
+ if (provider !== void 0) await this.stopProviderRuntime(provider);
3494
3694
  return {
3495
3695
  ok: true,
3496
3696
  value: session
@@ -3523,7 +3723,7 @@ let AwikiService = (() => {
3523
3723
  };
3524
3724
  this.publishSession(session);
3525
3725
  const provider = this.provider;
3526
- if (provider !== void 0) this.startListener(provider);
3726
+ if (provider !== void 0) this.ensureProviderRuntime(provider);
3527
3727
  return {
3528
3728
  ok: true,
3529
3729
  value: session
@@ -3609,8 +3809,15 @@ let AwikiService = (() => {
3609
3809
  };
3610
3810
  }
3611
3811
  }
3612
- sendRegistrationOtp(request) {
3613
- return this.run((client) => client.sendRegistrationOtp(request));
3812
+ async sendRegistrationOtp(request) {
3813
+ this.pendingDeviceJoin = void 0;
3814
+ return this.run(async (client) => {
3815
+ if (await this.selectDeviceJoinSession(client) !== null) throw Object.assign(/* @__PURE__ */ new Error("join already exists"), {
3816
+ name: "AwikiSdkError",
3817
+ code: "conflict"
3818
+ });
3819
+ return client.sendRegistrationOtp(request);
3820
+ });
3614
3821
  }
3615
3822
  /**
3616
3823
  * Register and persist the deployment's only AWiki identity.
@@ -3618,9 +3825,190 @@ let AwikiService = (() => {
3618
3825
  * @returns The new public identity or a closed failure.
3619
3826
  */
3620
3827
  async registerIdentity(request) {
3621
- const result = await this.run((client) => client.registerIdentity(request));
3622
- if (result.ok) await this.activateRegisteredIdentity(result.value);
3623
- return result;
3828
+ this.pendingDeviceJoin = void 0;
3829
+ const result = await this.run(async (client) => {
3830
+ if (await this.selectDeviceJoinSession(client) !== null) throw Object.assign(/* @__PURE__ */ new Error("join already exists"), {
3831
+ name: "AwikiSdkError",
3832
+ code: "conflict"
3833
+ });
3834
+ return client.registerIdentity(request);
3835
+ });
3836
+ if (!result.ok) return result;
3837
+ if (result.value.status === "registered") {
3838
+ await this.activateRegisteredIdentity(result.value.identity);
3839
+ return {
3840
+ ok: true,
3841
+ value: {
3842
+ status: "registered",
3843
+ identity: result.value.identity
3844
+ }
3845
+ };
3846
+ }
3847
+ this.pendingDeviceJoin = {
3848
+ continuationId: result.value.continuationId,
3849
+ fullHandle: result.value.fullHandle,
3850
+ mode: result.value.mode,
3851
+ requiresUserPresence: result.value.requiresUserPresence
3852
+ };
3853
+ return {
3854
+ ok: true,
3855
+ value: {
3856
+ status: "join-required",
3857
+ fullHandle: result.value.fullHandle,
3858
+ mode: result.value.mode,
3859
+ requiresUserPresence: result.value.requiresUserPresence
3860
+ }
3861
+ };
3862
+ }
3863
+ /** Consume the exact in-memory continuation; ordinary Join never claims rebind user presence. */
3864
+ async beginDeviceJoin() {
3865
+ const continuation = this.pendingDeviceJoin;
3866
+ if (continuation === void 0) return {
3867
+ ok: false,
3868
+ error: failure("conflict")
3869
+ };
3870
+ if (continuation.mode !== "ordinary" || continuation.requiresUserPresence) return {
3871
+ ok: false,
3872
+ error: failure("forbidden")
3873
+ };
3874
+ this.pendingDeviceJoin = void 0;
3875
+ const result = await this.run((client) => client.beginDeviceJoin({
3876
+ continuationId: continuation.continuationId,
3877
+ operationId: `dsh-device-join-${randomUUID()}`,
3878
+ userPresenceConfirmed: false
3879
+ }));
3880
+ if (!result.ok) return result;
3881
+ this.activeDeviceJoinSessionId = result.value.joinSessionId;
3882
+ return this.applyCandidateJoinProgress(result.value);
3883
+ }
3884
+ /** Restore from Core local_sessions and advance only the exact resumable Join. */
3885
+ async getDeviceJoinStatus() {
3886
+ const result = await this.run(async (client) => {
3887
+ const joinSessionId = await this.selectDeviceJoinSession(client);
3888
+ return joinSessionId === null ? null : client.getDeviceJoinStatus(joinSessionId);
3889
+ });
3890
+ if (!result.ok) return result;
3891
+ if (result.value === null) return {
3892
+ ok: true,
3893
+ value: null
3894
+ };
3895
+ this.activeDeviceJoinSessionId = result.value.joinSessionId;
3896
+ return this.applyCandidateJoinProgress(result.value);
3897
+ }
3898
+ async cancelDeviceJoin() {
3899
+ const result = await this.run(async (client) => {
3900
+ const joinSessionId = await this.selectDeviceJoinSession(client);
3901
+ if (joinSessionId === null) return null;
3902
+ return client.cancelDeviceJoin(joinSessionId);
3903
+ });
3904
+ if (!result.ok) return result;
3905
+ this.pendingDeviceJoin = void 0;
3906
+ this.activeDeviceJoinSessionId = void 0;
3907
+ return {
3908
+ ok: true,
3909
+ value: { completed: true }
3910
+ };
3911
+ }
3912
+ /** Reliable-sync and project only Host-opaque device/request references. */
3913
+ refreshDeviceManagement() {
3914
+ return this.run((client) => this.deviceManagementSnapshot(client));
3915
+ }
3916
+ async startDeviceJoinVerification(request) {
3917
+ const joinSessionId = this.requestSessions.get(request?.requestRef);
3918
+ if (joinSessionId === void 0) return {
3919
+ ok: false,
3920
+ error: failure("invalid-request")
3921
+ };
3922
+ const result = await this.run(async (client) => {
3923
+ await this.requireDeviceManager(client);
3924
+ await client.syncDeviceManagement();
3925
+ let notice = (await client.listLocalDeviceJoinRequests()).find((value) => value.joinSessionId === joinSessionId);
3926
+ if (notice === void 0) throw Object.assign(/* @__PURE__ */ new Error("request not found"), {
3927
+ name: "AwikiSdkError",
3928
+ code: "not-found"
3929
+ });
3930
+ const localProgress = await this.localAdminJoinProgress(client, notice);
3931
+ if (localProgress !== void 0) return localProgress;
3932
+ if (!notice.canStartVerification) throw Object.assign(/* @__PURE__ */ new Error("request unavailable"), {
3933
+ name: "AwikiSdkError",
3934
+ code: "forbidden"
3935
+ });
3936
+ const operationId = `dsh-device-verify-${createHash("sha256").update(joinSessionId).digest("hex").slice(0, 32)}`;
3937
+ let started;
3938
+ try {
3939
+ started = await client.startDeviceJoinVerification({
3940
+ joinSessionId,
3941
+ operationId,
3942
+ challengeTtlSeconds: DEVICE_JOIN_CHALLENGE_TTL_SECONDS
3943
+ });
3944
+ } catch (error) {
3945
+ await client.syncDeviceManagement();
3946
+ notice = (await client.listLocalDeviceJoinRequests()).find((value) => value.joinSessionId === joinSessionId);
3947
+ const recovered = notice === void 0 ? void 0 : await this.localAdminJoinProgress(client, notice);
3948
+ if (recovered !== void 0) return recovered;
3949
+ throw error;
3950
+ }
3951
+ await client.syncDeviceManagement();
3952
+ notice = (await client.listLocalDeviceJoinRequests()).find((value) => value.joinSessionId === joinSessionId);
3953
+ if (notice === void 0) return started;
3954
+ return await this.localAdminJoinProgress(client, notice) ?? started;
3955
+ });
3956
+ return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
3957
+ }
3958
+ async approveDeviceJoin(request) {
3959
+ if (request?.confirmation !== DEVICE_JOIN_APPROVAL_CONFIRMATION) return {
3960
+ ok: false,
3961
+ error: failure("invalid-request")
3962
+ };
3963
+ const joinSessionId = this.requestSessions.get(request.requestRef);
3964
+ if (joinSessionId === void 0 || !/^\d{6}$/u.test(request.enteredSas)) return {
3965
+ ok: false,
3966
+ error: failure("invalid-request")
3967
+ };
3968
+ const result = await this.run(async (client) => {
3969
+ await this.requireDeviceManager(client);
3970
+ const progress = await client.getLocalDeviceJoinVerificationProgress(joinSessionId);
3971
+ if (progress.sas === void 0 || !constantTimeSasMatches(progress.sas, request.enteredSas)) throw Object.assign(/* @__PURE__ */ new Error("sas mismatch"), {
3972
+ name: "AwikiSdkError",
3973
+ code: "forbidden"
3974
+ });
3975
+ const prompt = await client.prepareDeviceJoinApproval(joinSessionId);
3976
+ return client.confirmDeviceJoinApproval(prompt.approvalHandle);
3977
+ });
3978
+ return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
3979
+ }
3980
+ async rejectDeviceJoin(request) {
3981
+ const joinSessionId = this.requestSessions.get(request?.requestRef);
3982
+ if (joinSessionId === void 0 || request.reason !== "user_rejected" && request.reason !== "sas_mismatch") return {
3983
+ ok: false,
3984
+ error: failure("invalid-request")
3985
+ };
3986
+ const result = await this.run(async (client) => {
3987
+ await this.requireDeviceManager(client);
3988
+ return client.rejectDeviceJoin(joinSessionId, request.reason);
3989
+ });
3990
+ return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
3991
+ }
3992
+ async revokeDevice(request) {
3993
+ if (request?.confirmation !== DEVICE_REVOKE_CONFIRMATION) return {
3994
+ ok: false,
3995
+ error: failure("invalid-request")
3996
+ };
3997
+ const deviceId = this.deviceIds.get(request.deviceRef);
3998
+ if (deviceId === void 0) return {
3999
+ ok: false,
4000
+ error: failure("invalid-request")
4001
+ };
4002
+ return this.run(async (client) => {
4003
+ await this.requireDeviceManager(client);
4004
+ const target = (await client.getDeviceRegistry()).find((device) => device.deviceId === deviceId);
4005
+ if (target === void 0 || target.isCurrent) throw Object.assign(/* @__PURE__ */ new Error("device unavailable"), {
4006
+ name: "AwikiSdkError",
4007
+ code: "forbidden"
4008
+ });
4009
+ await client.revokeDevice(deviceId);
4010
+ return this.deviceManagementSnapshot(client);
4011
+ });
3624
4012
  }
3625
4013
  /**
3626
4014
  * Update the deployment identity's public WNS display name.
@@ -3647,12 +4035,19 @@ let AwikiService = (() => {
3647
4035
  }
3648
4036
  /** Start durable phone recovery for one existing full Handle. */
3649
4037
  sendRecoveryOtp(request) {
4038
+ this.pendingDeviceJoin = void 0;
3650
4039
  const normalized = normalizeRecoveryOtpRequest(request);
3651
4040
  if (normalized === void 0) return Promise.resolve({
3652
4041
  ok: false,
3653
4042
  error: failure("invalid-request")
3654
4043
  });
3655
- return this.run((client) => client.sendRecoveryOtp(normalized), { allowSignedOut: true });
4044
+ return this.run(async (client) => {
4045
+ if (await this.selectDeviceJoinSession(client) !== null) throw Object.assign(/* @__PURE__ */ new Error("join already exists"), {
4046
+ name: "AwikiSdkError",
4047
+ code: "conflict"
4048
+ });
4049
+ return client.sendRecoveryOtp(normalized);
4050
+ }, { allowSignedOut: true });
3656
4051
  }
3657
4052
  /** Verify a recovery OTP and freeze its exact intent before the remote commit. */
3658
4053
  prepareRecovery(request) {
@@ -4164,10 +4559,10 @@ let AwikiService = (() => {
4164
4559
  });
4165
4560
  return this.mutateSession(async () => {
4166
4561
  const provider = this.provider;
4167
- if (provider !== void 0) await this.stopListener(provider);
4562
+ if (provider !== void 0) await this.stopProviderRuntime(provider);
4168
4563
  const result = await this.run((client) => client.clearLocalData(), { allowSignedOut: true });
4169
4564
  if (!result.ok) {
4170
- if (provider !== void 0 && this.provider === provider) this.startListener(provider);
4565
+ if (provider !== void 0 && this.provider === provider) this.ensureProviderRuntime(provider);
4171
4566
  return result;
4172
4567
  }
4173
4568
  try {
@@ -4177,9 +4572,10 @@ let AwikiService = (() => {
4177
4572
  await this.sessionStore.signIn();
4178
4573
  this.signedOut = false;
4179
4574
  this.activeIdentityDid = void 0;
4575
+ this.pendingDeviceJoin = void 0;
4576
+ this.activeDeviceJoinSessionId = void 0;
4180
4577
  this.invalidateSummaries();
4181
4578
  this.publishSession({ status: "unregistered" });
4182
- if (provider !== void 0 && this.provider === provider) this.startListener(provider);
4183
4579
  return result;
4184
4580
  } catch {
4185
4581
  return {
@@ -4213,8 +4609,7 @@ let AwikiService = (() => {
4213
4609
  identity
4214
4610
  };
4215
4611
  this.publishSession(session);
4216
- await provider.listenerStartup;
4217
- await this.startListener(provider);
4612
+ this.ensureProviderRuntime(provider);
4218
4613
  }
4219
4614
  return reconciled;
4220
4615
  } catch {
@@ -4250,22 +4645,159 @@ let AwikiService = (() => {
4250
4645
  return false;
4251
4646
  }
4252
4647
  }
4253
- /** Publish one newly registered identity and start its listener through the existing session path. */
4648
+ /** Select the only resumable new-device session; Core local_sessions is the sole restart SoT. */
4649
+ async selectDeviceJoinSession(client) {
4650
+ const sessions = await client.listLocalDeviceJoinSessions();
4651
+ if (this.activeDeviceJoinSessionId !== void 0) {
4652
+ const tracked = sessions.find((value) => value.side === "new_device" && value.joinSessionId === this.activeDeviceJoinSessionId);
4653
+ if (tracked !== void 0) return tracked.joinSessionId;
4654
+ this.activeDeviceJoinSessionId = void 0;
4655
+ }
4656
+ const resumable = sessions.filter((value) => value.side === "new_device" && RESUMABLE_JOIN_PHASES.has(value.localPhase));
4657
+ if (resumable.length > 1) throw Object.assign(/* @__PURE__ */ new Error("multiple local joins"), {
4658
+ name: "AwikiSdkError",
4659
+ code: "conflict"
4660
+ });
4661
+ const selected = resumable[0];
4662
+ this.activeDeviceJoinSessionId = selected?.joinSessionId;
4663
+ return selected?.joinSessionId ?? null;
4664
+ }
4665
+ async applyCandidateJoinProgress(value) {
4666
+ const phase = candidateJoinPhase(value);
4667
+ if (phase === void 0) return {
4668
+ ok: false,
4669
+ error: failure("remote")
4670
+ };
4671
+ if (phase === "cancelled" || phase === "rejected" || phase === "expired") this.activeDeviceJoinSessionId = void 0;
4672
+ if (phase === "authorized") {
4673
+ if (value.identity === void 0) return {
4674
+ ok: false,
4675
+ error: failure("remote")
4676
+ };
4677
+ await this.activateRegisteredIdentity(value.identity);
4678
+ }
4679
+ return {
4680
+ ok: true,
4681
+ value: {
4682
+ phase,
4683
+ expiresAt: value.expiresAt,
4684
+ ...phase === "sas-ready" && value.sas !== void 0 ? { sas: value.sas } : {},
4685
+ completed: phase === "authorized"
4686
+ }
4687
+ };
4688
+ }
4689
+ publicAdminJoinProgress(requestRef, value) {
4690
+ const phase = adminJoinPhase(value);
4691
+ if (phase === void 0) return {
4692
+ ok: false,
4693
+ error: failure("remote")
4694
+ };
4695
+ return {
4696
+ ok: true,
4697
+ value: {
4698
+ requestRef,
4699
+ phase,
4700
+ expiresAt: value.expiresAt,
4701
+ ...phase === "sas-ready" && value.sas !== void 0 ? { sas: value.sas } : {}
4702
+ }
4703
+ };
4704
+ }
4705
+ async localAdminJoinProgress(client, notice) {
4706
+ const local = (await client.listLocalDeviceJoinSessions()).find((session) => session.side === "admin" && session.joinSessionId === notice.joinSessionId);
4707
+ if (local === void 0) return notice.claimedByCurrentDevice ? client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId) : void 0;
4708
+ if (local.localPhase === "challenge_prepared") return {
4709
+ joinSessionId: notice.joinSessionId,
4710
+ localPhase: local.localPhase,
4711
+ remoteState: notice.state,
4712
+ expiresAt: local.expiresAt
4713
+ };
4714
+ return client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId);
4715
+ }
4716
+ async requireDeviceManager(client) {
4717
+ const current = await client.getCurrentDeviceSummary();
4718
+ if (!current.canManage || current.role !== "admin" || current.readiness !== "admin_ready") throw Object.assign(/* @__PURE__ */ new Error("device manager required"), {
4719
+ name: "AwikiSdkError",
4720
+ code: "forbidden"
4721
+ });
4722
+ }
4723
+ requestRef(joinSessionId) {
4724
+ const existing = this.requestRefs.get(joinSessionId);
4725
+ if (existing !== void 0) return existing;
4726
+ const reference = `join-${randomUUID()}`;
4727
+ this.requestRefs.set(joinSessionId, reference);
4728
+ this.requestSessions.set(reference, joinSessionId);
4729
+ return reference;
4730
+ }
4731
+ deviceRef(deviceId) {
4732
+ const existing = this.deviceRefs.get(deviceId);
4733
+ if (existing !== void 0) return existing;
4734
+ const reference = `device-${randomUUID()}`;
4735
+ this.deviceRefs.set(deviceId, reference);
4736
+ this.deviceIds.set(reference, deviceId);
4737
+ return reference;
4738
+ }
4739
+ async deviceManagementSnapshot(client) {
4740
+ await client.syncDeviceManagement();
4741
+ const current = await client.getCurrentDeviceSummary();
4742
+ if (!current.canManage || current.role !== "admin" || current.readiness !== "admin_ready") {
4743
+ this.requestRefs.clear();
4744
+ this.requestSessions.clear();
4745
+ this.deviceRefs.clear();
4746
+ this.deviceIds.clear();
4747
+ return {
4748
+ canManage: false,
4749
+ ...current.role === void 0 ? {} : { role: current.role },
4750
+ readiness: current.readiness,
4751
+ devices: [],
4752
+ requests: []
4753
+ };
4754
+ }
4755
+ const requests = await client.listLocalDeviceJoinRequests();
4756
+ await Promise.all(requests.filter((request) => request.claimedByCurrentDevice).map((request) => client.getLocalDeviceJoinVerificationProgress(request.joinSessionId).catch(() => void 0)));
4757
+ return {
4758
+ canManage: true,
4759
+ role: "admin",
4760
+ readiness: "admin_ready",
4761
+ devices: (await client.getDeviceRegistry()).map((device) => this.publicDevice(device)),
4762
+ requests: requests.map((request) => ({
4763
+ requestRef: this.requestRef(request.joinSessionId),
4764
+ candidateKeyFingerprint: request.candidateKeyFingerprint,
4765
+ issuedAt: request.issuedAt,
4766
+ expiresAt: request.expiresAt,
4767
+ state: requestJoinPhase(request),
4768
+ claimedByCurrentDevice: request.claimedByCurrentDevice,
4769
+ canStartVerification: request.canStartVerification
4770
+ }))
4771
+ };
4772
+ }
4773
+ publicDevice(device) {
4774
+ return {
4775
+ deviceRef: this.deviceRef(device.deviceId),
4776
+ status: device.status,
4777
+ role: device.role,
4778
+ managementReady: device.managementReady,
4779
+ isCurrent: device.isCurrent
4780
+ };
4781
+ }
4782
+ /** Publish one newly registered identity, then start realtime only in the background. */
4254
4783
  async activateRegisteredIdentity(identity) {
4784
+ this.pendingDeviceJoin = void 0;
4785
+ this.activeDeviceJoinSessionId = void 0;
4255
4786
  this.activeIdentityDid = identity.did;
4256
4787
  this.publishSession({
4257
4788
  status: "active",
4258
4789
  identity
4259
4790
  });
4260
4791
  const provider = this.provider;
4261
- if (provider !== void 0) {
4262
- await provider.listenerStartup;
4263
- await this.startListener(provider);
4264
- }
4792
+ if (provider !== void 0) this.ensureProviderRuntime(provider);
4265
4793
  }
4266
4794
  /** Invalidate cached session work and cancel every model request still owned by the old session. */
4267
4795
  invalidateSummaries() {
4268
4796
  this.sessionRevision += 1;
4797
+ this.requestRefs.clear();
4798
+ this.requestSessions.clear();
4799
+ this.deviceRefs.clear();
4800
+ this.deviceIds.clear();
4269
4801
  for (const controller of this.activeSummaryRequests) controller.abort();
4270
4802
  this.activeSummaryRequests.clear();
4271
4803
  }
@@ -4365,120 +4897,148 @@ let AwikiService = (() => {
4365
4897
  this.sessionMutation = pending.then(() => void 0, () => void 0);
4366
4898
  return pending;
4367
4899
  }
4368
- /** Start one exact identity-bound listener, atomically releasing a failed startup. */
4369
- startListener(provider) {
4370
- if (!this.resolved.listenerEnabled || provider.listener !== void 0 || provider.listenerRecoveryBlocked || this.provider !== provider) return Promise.resolve();
4371
- if (provider.listenerStartup !== void 0) return provider.listenerStartup;
4372
- if (provider.listenerCleanup !== void 0 || provider.listenerRestartTimer !== void 0) return Promise.resolve();
4373
- const workspaceContext = this.workspaceContext;
4374
- const source = provider.client.listener;
4375
- if (workspaceContext === void 0 || source === void 0) return Promise.resolve();
4376
- const generation = provider.listenerGeneration;
4377
- const logger = this.ctx.logger("awiki-listener");
4900
+ /** Start identity realtime and the optional Agent consumer without blocking identity success. */
4901
+ ensureProviderRuntime(provider) {
4902
+ const identityDid = this.activeIdentityDid;
4903
+ if (provider.runtimeReplacement !== void 0) return;
4904
+ if (identityDid !== void 0 && provider.realtimeIdentityDid !== void 0 && provider.realtimeIdentityDid !== identityDid) {
4905
+ this.replaceProviderRuntime(provider, identityDid);
4906
+ return;
4907
+ }
4908
+ this.ensureRealtimeSupervisor(provider);
4909
+ this.ensureAgentConsumer(provider);
4910
+ }
4911
+ replaceProviderRuntime(provider, identityDid) {
4912
+ if (provider.runtimeReplacement !== void 0 || this.provider !== provider) return;
4913
+ const observed = (async () => {
4914
+ await this.stopProviderRuntime(provider);
4915
+ if (this.provider !== provider || this.activeIdentityDid !== identityDid) return;
4916
+ this.ensureRealtimeSupervisor(provider);
4917
+ this.ensureAgentConsumer(provider);
4918
+ })().catch((error) => {
4919
+ this.ctx.logger("awiki-realtime").warn("AWiki identity realtime replacement failed: %s", error instanceof Error ? error.message : "unknown failure");
4920
+ }).finally(() => {
4921
+ if (provider.runtimeReplacement === observed) delete provider.runtimeReplacement;
4922
+ if (this.provider === provider) this.ensureProviderRuntime(provider);
4923
+ });
4924
+ provider.runtimeReplacement = observed;
4925
+ }
4926
+ ensureRealtimeSupervisor(provider) {
4927
+ if (!this.resolved.realtimeEnabled || provider.realtimeSupervisor !== void 0 || provider.realtimeStartup !== void 0 || this.provider !== provider || (provider.client.realtime ?? provider.client.listener) === void 0) return;
4928
+ const generation = provider.realtimeGeneration;
4929
+ const source = provider.client.realtime ?? provider.client.listener;
4930
+ if (source === void 0) return;
4931
+ const logger = this.ctx.logger("awiki-realtime");
4378
4932
  const observed = (async () => {
4379
4933
  if (await this.isSignedOut()) return;
4380
- if (await provider.client.getIdentity() === null) return;
4381
- if (!this.listenerFenceMatches(provider, workspaceContext, generation)) return;
4382
- const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
4383
- const listener = new AwikiAgentListener(source, agents, this.resolved.listener, logger);
4384
- provider.listener = listener;
4385
- try {
4386
- await listener.start();
4387
- } catch (error) {
4388
- if (provider.listener === listener) delete provider.listener;
4389
- try {
4390
- await listener.dispose();
4391
- } catch {
4392
- provider.listenerRecoveryBlocked = true;
4393
- }
4394
- if (!provider.listenerRecoveryBlocked && provider.listenerRestartAttempt > 0) this.scheduleListenerRestart(provider, workspaceContext, generation);
4395
- throw error;
4396
- }
4397
- if (!this.listenerFenceMatches(provider, workspaceContext, generation)) {
4398
- if (provider.listener === listener) delete provider.listener;
4399
- try {
4400
- await listener.dispose();
4401
- } catch (error) {
4402
- provider.listenerRecoveryBlocked = true;
4403
- throw error;
4404
- }
4405
- return;
4406
- }
4407
- provider.listenerStartedAt = Date.now();
4408
- listener.whenTerminated().then((result) => {
4409
- if (result.kind === "failed") this.releaseFailedListener(provider, listener, workspaceContext, generation, result.error);
4410
- });
4934
+ const identity = await provider.client.getIdentity();
4935
+ if (identity === null || !this.realtimeFenceMatches(provider, generation)) return;
4936
+ this.activeIdentityDid = identity.did;
4937
+ const supervisor = new IdentityRealtimeSupervisor(source, { onSynchronized: (cause) => this.onRealtimeSynchronized(provider, generation, cause) }, logger);
4938
+ provider.realtimeSupervisor = supervisor;
4939
+ provider.realtimeIdentityDid = identity.did;
4940
+ supervisor.start();
4941
+ this.ensureAgentConsumer(provider);
4411
4942
  })().catch((error) => {
4412
- logger.warn("AWiki listener startup failed: %s", error instanceof Error ? error.message : "unknown failure");
4943
+ logger.warn("AWiki realtime startup failed: %s", error instanceof Error ? error.message : "unknown failure");
4413
4944
  }).finally(() => {
4414
- if (provider.listenerStartup === observed) delete provider.listenerStartup;
4945
+ if (provider.realtimeStartup === observed) delete provider.realtimeStartup;
4415
4946
  });
4416
- provider.listenerStartup = observed;
4417
- return observed;
4418
- }
4419
- async stopListener(provider) {
4420
- provider.listenerGeneration += 1;
4421
- provider.listenerRestartAttempt = 0;
4422
- delete provider.listenerStartedAt;
4423
- const timer = provider.listenerRestartTimer;
4424
- if (timer !== void 0) {
4425
- delete provider.listenerRestartTimer;
4426
- clearTimeout(timer);
4427
- }
4428
- await provider.listenerStartup;
4429
- await provider.listenerCleanup;
4430
- const listener = provider.listener;
4431
- if (listener === void 0) return;
4432
- delete provider.listener;
4433
- try {
4434
- await listener.dispose();
4435
- } catch (error) {
4436
- provider.listenerRecoveryBlocked = true;
4437
- throw error;
4438
- }
4947
+ provider.realtimeStartup = observed;
4439
4948
  }
4440
- listenerFenceMatches(provider, workspaceContext, generation) {
4441
- return this.provider === provider && this.workspaceContext === workspaceContext && provider.listenerGeneration === generation;
4949
+ realtimeFenceMatches(provider, generation) {
4950
+ return this.provider === provider && provider.realtimeGeneration === generation;
4442
4951
  }
4443
- releaseFailedListener(provider, listener, workspaceContext, generation, error) {
4444
- if (provider.listener !== listener) return;
4445
- delete provider.listener;
4446
- const startedAt = provider.listenerStartedAt;
4447
- delete provider.listenerStartedAt;
4448
- if (startedAt !== void 0 && Date.now() - startedAt >= LISTENER_STABLE_RESET_MS) provider.listenerRestartAttempt = 0;
4952
+ async onRealtimeSynchronized(provider, generation, cause) {
4953
+ if (!this.realtimeFenceMatches(provider, generation)) return;
4954
+ if (cause === "system_notification" || cause === "stream_recovery") try {
4955
+ provider.localDeviceJoinRequestCountAfterSync = (await provider.client.listLocalDeviceJoinRequests()).length;
4956
+ } catch {
4957
+ this.ctx.logger("awiki-realtime").debug("AWiki realtime could not observe local device Join requests");
4958
+ }
4959
+ if (![
4960
+ "session_start",
4961
+ "connection_ready",
4962
+ "reconnected",
4963
+ "message",
4964
+ "message_update"
4965
+ ].includes(cause)) return;
4966
+ this.ensureAgentConsumer(provider);
4967
+ await provider.agentConsumerStartup;
4968
+ if (!this.realtimeFenceMatches(provider, generation)) return;
4969
+ await provider.agentConsumer?.reconcileOnce();
4970
+ }
4971
+ ensureAgentConsumer(provider) {
4972
+ const workspaceContext = this.workspaceContext;
4973
+ const source = provider.client.agentInbox ?? provider.client.listener;
4974
+ const identityDid = this.activeIdentityDid;
4975
+ if (!this.resolved.listenerEnabled || workspaceContext === void 0 || source === void 0 || identityDid === void 0 || this.provider !== provider) return;
4976
+ if (provider.agentConsumer !== void 0 && provider.agentConsumerIdentityDid === identityDid) return;
4977
+ if (provider.agentConsumerStartup !== void 0) return;
4978
+ const generation = ++provider.agentConsumerGeneration;
4449
4979
  const logger = this.ctx.logger("awiki-listener");
4450
- logger.warn("AWiki listener lifecycle failed: %s", error instanceof Error ? error.message : "unknown failure");
4451
- let disposed = false;
4452
- const observed = listener.dispose().then(() => {
4453
- disposed = true;
4454
- }, (cleanupError) => {
4455
- provider.listenerRecoveryBlocked = true;
4456
- logger.warn("AWiki listener cleanup failed: %s", cleanupError instanceof Error ? cleanupError.message : "unknown failure");
4980
+ const observed = (async () => {
4981
+ await this.detachAgentConsumer(provider);
4982
+ if (!this.agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation)) return;
4983
+ const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
4984
+ const consumer = new AwikiAgentListener(source, agents, {
4985
+ ...this.resolved.listener,
4986
+ identityScope: identityDid
4987
+ }, logger);
4988
+ provider.agentConsumer = consumer;
4989
+ provider.agentConsumerIdentityDid = identityDid;
4990
+ await consumer.reconcileOnce();
4991
+ })().catch((error) => {
4992
+ logger.warn("AWiki Agent consumer startup failed: %s", error instanceof Error ? error.message : "unknown failure");
4457
4993
  }).finally(() => {
4458
- if (provider.listenerCleanup === observed) delete provider.listenerCleanup;
4459
- if (disposed) this.scheduleListenerRestart(provider, workspaceContext, generation);
4994
+ if (provider.agentConsumerStartup === observed) delete provider.agentConsumerStartup;
4995
+ });
4996
+ provider.agentConsumerStartup = observed;
4997
+ }
4998
+ agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation) {
4999
+ return this.provider === provider && this.workspaceContext === workspaceContext && this.activeIdentityDid === identityDid && provider.agentConsumerGeneration === generation;
5000
+ }
5001
+ async detachAgentConsumer(provider) {
5002
+ const consumer = provider.agentConsumer;
5003
+ if (consumer === void 0) return;
5004
+ delete provider.agentConsumer;
5005
+ delete provider.agentConsumerIdentityDid;
5006
+ const observed = consumer.dispose().finally(() => {
5007
+ if (provider.agentConsumerCleanup === observed) delete provider.agentConsumerCleanup;
4460
5008
  });
4461
- provider.listenerCleanup = observed;
4462
- }
4463
- scheduleListenerRestart(provider, workspaceContext, generation) {
4464
- if (provider.listenerRestartTimer !== void 0 || !this.listenerFenceMatches(provider, workspaceContext, generation)) return;
4465
- provider.listenerRestartAttempt = Math.min(provider.listenerRestartAttempt + 1, LISTENER_RESTART_MAX_ATTEMPT);
4466
- const exponent = provider.listenerRestartAttempt - 1;
4467
- const delay = Math.min(LISTENER_RESTART_BASE_DELAY_MS * 2 ** exponent, LISTENER_RESTART_MAX_DELAY_MS);
4468
- const timer = setTimeout(() => {
4469
- if (provider.listenerRestartTimer !== timer) return;
4470
- delete provider.listenerRestartTimer;
4471
- if (!this.listenerFenceMatches(provider, workspaceContext, generation)) return;
4472
- this.startListener(provider);
4473
- }, delay);
4474
- provider.listenerRestartTimer = timer;
5009
+ provider.agentConsumerCleanup = observed;
5010
+ await observed;
5011
+ }
5012
+ async stopAgentConsumer(provider) {
5013
+ provider.agentConsumerGeneration += 1;
5014
+ await provider.agentConsumerStartup;
5015
+ await provider.agentConsumerCleanup;
5016
+ await this.detachAgentConsumer(provider);
5017
+ }
5018
+ async stopRealtimeSupervisor(provider) {
5019
+ provider.realtimeGeneration += 1;
5020
+ await provider.realtimeStartup;
5021
+ const supervisor = provider.realtimeSupervisor;
5022
+ if (supervisor === void 0) return;
5023
+ delete provider.realtimeSupervisor;
5024
+ delete provider.realtimeIdentityDid;
5025
+ await supervisor.dispose();
5026
+ }
5027
+ async stopProviderRuntime(provider) {
5028
+ await Promise.all([this.stopRealtimeSupervisor(provider), this.stopAgentConsumer(provider)]);
4475
5029
  }
4476
5030
  /** Clear one exact provider slot before joining its one shared disposal. */
4477
5031
  disposeProvider(provider) {
4478
- if (this.provider === provider) this.provider = void 0;
5032
+ if (this.provider === provider) {
5033
+ this.provider = void 0;
5034
+ this.pendingDeviceJoin = void 0;
5035
+ this.activeDeviceJoinSessionId = void 0;
5036
+ this.invalidateSummaries();
5037
+ }
4479
5038
  provider.disposal ??= (async () => {
4480
5039
  try {
4481
- await this.stopListener(provider);
5040
+ await provider.runtimeReplacement;
5041
+ await this.stopProviderRuntime(provider);
4482
5042
  } finally {
4483
5043
  await provider.client.dispose();
4484
5044
  }