@awiki/dsh-plugin 0.3.2 → 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 (62) hide show
  1. package/README.md +36 -22
  2. package/README.zh.md +29 -19
  3. package/cordis.patch.yml +17 -0
  4. package/lib/client.js +1273 -216
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +874 -266
  7. package/lib/provider.js +52 -16
  8. package/lib/{sdk-adapter-C0gdpg1u.mjs → sdk-adapter-BeJakxLP.mjs} +141 -3
  9. package/lib/typert.host.js +376 -44
  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 +376 -44
  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 -3
  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 -3
  25. package/lib/types/client/controller.d.ts.map +1 -1
  26. package/lib/types/client/controller.js +38 -3
  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 +50 -9
  34. package/lib/types/index.d.ts.map +1 -1
  35. package/lib/types/index.js +672 -157
  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 +131 -2
  58. package/lib/types/sdk-adapter.js.map +1 -1
  59. package/lib/types/types.d.ts +75 -1
  60. package/lib/types/types.d.ts.map +1 -1
  61. package/lib/types/types.js.map +1 -1
  62. package/package.json +5 -3
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { n as downloadedAttachment } from "./sdk-adapter-C0gdpg1u.mjs";
1
+ import { n as downloadedAttachment } from "./sdk-adapter-BeJakxLP.mjs";
2
2
  import "@deepseek-ai/cordis";
3
+ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
3
4
  import { homedir } from "node:os";
4
5
  import { isAbsolute, join } from "node:path";
5
6
  import z from "@deepseek-ai/schemastery";
@@ -7,7 +8,6 @@ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
7
8
  import { SettingsConflictError, settingsNamespace } from "@deepseek-ai/dsh-settings";
8
9
  import { defineTool } from "@deepseek-ai/dsh-tools";
9
10
  import { chmod, lstat, mkdir, readFile, readdir, rename, rm, unlink, utimes, writeFile } from "node:fs/promises";
10
- import { createHash, randomUUID } from "node:crypto";
11
11
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
12
12
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
13
13
  import { SessionId } from "@deepseek-ai/dsh-session";
@@ -1448,12 +1448,13 @@ var AwikiConversationPreferenceStore = class {
1448
1448
  };
1449
1449
  //#endregion
1450
1450
  //#region lib/types/listener-state.js
1451
- const STATE_VERSION = 1;
1451
+ const STATE_VERSION = 2;
1452
1452
  const MAX_STATE_BYTES = 1048576;
1453
1453
  const MAX_CONVERSATIONS = 1e3;
1454
- function emptyState() {
1454
+ function emptyState(identityScopeHash) {
1455
1455
  return {
1456
1456
  version: STATE_VERSION,
1457
+ identityScopeHash,
1457
1458
  conversations: {}
1458
1459
  };
1459
1460
  }
@@ -1465,8 +1466,10 @@ function boundedString(value, field) {
1465
1466
  return value;
1466
1467
  }
1467
1468
  /** Validate and detach the complete state file before the listener trusts any route. */
1468
- function parseState(value) {
1469
- 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");
1470
1473
  const entries = Object.entries(value.conversations);
1471
1474
  if (entries.length > MAX_CONVERSATIONS) throw new TypeError("awiki: listener state is too large");
1472
1475
  const conversations = {};
@@ -1489,6 +1492,7 @@ function parseState(value) {
1489
1492
  }
1490
1493
  return {
1491
1494
  version: STATE_VERSION,
1495
+ identityScopeHash,
1492
1496
  conversations
1493
1497
  };
1494
1498
  }
@@ -1499,28 +1503,31 @@ function isMissing(error) {
1499
1503
  var AwikiListenerStateStore = class {
1500
1504
  hostDirectory;
1501
1505
  statePath;
1502
- 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");
1503
1510
  this.hostDirectory = join(stateRoot, ".host");
1504
1511
  this.statePath = join(this.hostDirectory, "listener-state.json");
1505
1512
  }
1506
- /** 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. */
1507
1514
  async load() {
1508
- if (!await this.hasPrivateHostDirectory()) return emptyState();
1515
+ if (!await this.hasPrivateHostDirectory()) return emptyState(this.identityScopeHash);
1509
1516
  try {
1510
1517
  const metadata = await lstat(this.statePath);
1511
1518
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is invalid");
1512
1519
  const text = await readFile(this.statePath, "utf8");
1513
1520
  if (Buffer.byteLength(text, "utf8") > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is invalid");
1514
- return parseState(JSON.parse(text));
1521
+ return parseState(JSON.parse(text), this.identityScopeHash);
1515
1522
  } catch (error) {
1516
- if (isMissing(error)) return emptyState();
1523
+ if (isMissing(error)) return emptyState(this.identityScopeHash);
1517
1524
  if (error instanceof SyntaxError) throw new TypeError("awiki: listener state file is invalid");
1518
1525
  throw error;
1519
1526
  }
1520
1527
  }
1521
1528
  /** Replace the state atomically without ever writing message or Agent text. */
1522
1529
  async save(state) {
1523
- const snapshot = parseState(state);
1530
+ const snapshot = parseState(state, this.identityScopeHash);
1524
1531
  const text = `${JSON.stringify(snapshot)}\n`;
1525
1532
  if (Buffer.byteLength(text, "utf8") > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is too large");
1526
1533
  await mkdir(this.hostDirectory, {
@@ -1742,11 +1749,12 @@ function incomingFromPeer(message, conversation) {
1742
1749
  }
1743
1750
  function copyState(state) {
1744
1751
  return {
1745
- version: 1,
1752
+ version: 2,
1753
+ identityScopeHash: state.identityScopeHash,
1746
1754
  conversations: Object.fromEntries(Object.entries(state.conversations).map(([id, route]) => [id, { ...route }]))
1747
1755
  };
1748
1756
  }
1749
- /** Reconcile authorized Direct text from Core history whenever realtime schedules synchronization. */
1757
+ /** Consume authorized Direct text only after the identity supervisor commits synchronization. */
1750
1758
  var AwikiAgentListener = class {
1751
1759
  awiki;
1752
1760
  agents;
@@ -1755,34 +1763,23 @@ var AwikiAgentListener = class {
1755
1763
  store;
1756
1764
  logger;
1757
1765
  stateReady;
1758
- state = {
1759
- version: 1,
1760
- conversations: {}
1761
- };
1766
+ state;
1762
1767
  stateMutation = Promise.resolve();
1763
1768
  syncMutation = Promise.resolve();
1764
1769
  scheduledMessageIds = /* @__PURE__ */ new Set();
1765
1770
  conversationQueues = /* @__PURE__ */ new Map();
1766
- lifecycle;
1767
- started;
1768
- resolveStarted;
1769
- rejectStarted;
1770
- lifecycleGeneration = 0;
1771
- streamGeneration = 0;
1772
- activeRealtime;
1773
1771
  stopped = false;
1774
- termination;
1775
- resolveTermination;
1776
- terminationSettled = false;
1777
1772
  constructor(awiki, agents, config, logger, store) {
1778
1773
  this.awiki = awiki;
1779
1774
  this.agents = agents;
1780
1775
  this.config = config;
1781
- this.termination = new Promise((resolve) => {
1782
- this.resolveTermination = resolve;
1783
- });
1784
1776
  this.allowedPeers = new Set(config.allowedPeers.map((peer) => peer.startsWith("did:") ? peer : peer.toLowerCase()));
1785
- 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
+ };
1786
1783
  this.logger = logger ?? {
1787
1784
  debug() {},
1788
1785
  info() {},
@@ -1794,38 +1791,11 @@ var AwikiAgentListener = class {
1794
1791
  this.state = state;
1795
1792
  });
1796
1793
  }
1797
- /** Start canonical startup sync followed by the single Core-owned realtime stream. */
1798
- start() {
1799
- if (this.started !== void 0) return this.started;
1800
- this.started = new Promise((resolve, reject) => {
1801
- this.resolveStarted = resolve;
1802
- this.rejectStarted = reject;
1803
- });
1804
- const generation = ++this.lifecycleGeneration;
1805
- this.lifecycle = this.run(generation).then(() => {
1806
- this.finishTermination({ kind: "stopped" });
1807
- }, (error) => {
1808
- this.rejectStarted?.(error);
1809
- this.rejectStarted = void 0;
1810
- this.finishTermination({
1811
- kind: "failed",
1812
- error
1813
- });
1814
- if (!this.stopped) this.logger.warn("AWiki realtime listener stopped: %s", error instanceof Error ? error.message : "unknown failure");
1815
- });
1816
- return this.started;
1817
- }
1818
- /** Resolve once with either orderly shutdown or the exact terminal lifecycle failure. */
1819
- whenTerminated() {
1820
- return this.termination;
1821
- }
1822
- /** Deterministic canonical sync plus committed-history reconciliation for tests and recovery. */
1823
- async synchronizeOnce(reason) {
1794
+ /** Reconcile only committed history; this consumer cannot start WSS or advance sync. */
1795
+ async reconcileOnce() {
1824
1796
  await this.enqueueSync(async () => {
1825
1797
  await this.stateReady;
1826
1798
  if (this.stopped) return;
1827
- await this.awiki.syncNow(reason);
1828
- if (this.stopped) return;
1829
1799
  await this.reconcileCommittedHistory();
1830
1800
  });
1831
1801
  }
@@ -1835,84 +1805,12 @@ var AwikiAgentListener = class {
1835
1805
  while (this.conversationQueues.size > 0) await Promise.allSettled([...this.conversationQueues.values()]);
1836
1806
  await this.stateMutation;
1837
1807
  }
1838
- /** 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. */
1839
1809
  async dispose() {
1840
1810
  if (this.stopped) return;
1841
1811
  this.stopped = true;
1842
- this.lifecycleGeneration += 1;
1843
- this.streamGeneration += 1;
1844
- const active = this.activeRealtime;
1845
- this.activeRealtime = void 0;
1846
- await active?.session.stop().catch(() => void 0);
1847
- await this.lifecycle;
1848
- this.resolveStarted?.();
1849
- this.resolveStarted = void 0;
1850
- this.rejectStarted = void 0;
1851
1812
  await this.whenIdle();
1852
1813
  await this.agents.dispose();
1853
- this.finishTermination({ kind: "stopped" });
1854
- }
1855
- finishTermination(result) {
1856
- if (this.terminationSettled) return;
1857
- this.terminationSettled = true;
1858
- this.resolveTermination(result);
1859
- }
1860
- currentLifecycle(generation) {
1861
- return !this.stopped && this.lifecycleGeneration === generation;
1862
- }
1863
- async run(lifecycleGeneration) {
1864
- await this.synchronize("session_start", lifecycleGeneration);
1865
- if (!this.currentLifecycle(lifecycleGeneration)) return;
1866
- let active = await this.openRealtime(lifecycleGeneration);
1867
- if (active === void 0) return;
1868
- this.resolveStarted?.();
1869
- this.resolveStarted = void 0;
1870
- this.rejectStarted = void 0;
1871
- try {
1872
- while (this.currentLifecycle(lifecycleGeneration)) {
1873
- const event = await active.session.nextEvent();
1874
- if (!this.currentLifecycle(lifecycleGeneration) || this.activeRealtime?.generation !== active.generation) return;
1875
- if (event === null) {
1876
- this.activeRealtime = void 0;
1877
- this.streamGeneration += 1;
1878
- await active.session.stop();
1879
- if (!this.currentLifecycle(lifecycleGeneration)) return;
1880
- await this.synchronize("websocket_reconnect", lifecycleGeneration);
1881
- if (!this.currentLifecycle(lifecycleGeneration)) return;
1882
- const replacement = await this.openRealtime(lifecycleGeneration);
1883
- if (replacement === void 0) return;
1884
- active = replacement;
1885
- continue;
1886
- }
1887
- if (event.kind !== "sync_required") continue;
1888
- await this.synchronize(event.cause === "reconnected" ? "websocket_reconnect" : "websocket_hint", lifecycleGeneration);
1889
- }
1890
- } finally {
1891
- if (this.activeRealtime?.generation === active.generation) this.activeRealtime = void 0;
1892
- await active.session.stop().catch(() => void 0);
1893
- }
1894
- }
1895
- async openRealtime(lifecycleGeneration) {
1896
- const session = await this.awiki.startRealtime();
1897
- if (!this.currentLifecycle(lifecycleGeneration)) {
1898
- await session.stop().catch(() => void 0);
1899
- return;
1900
- }
1901
- const active = {
1902
- generation: ++this.streamGeneration,
1903
- session
1904
- };
1905
- this.activeRealtime = active;
1906
- return active;
1907
- }
1908
- async synchronize(reason, lifecycleGeneration) {
1909
- await this.enqueueSync(async () => {
1910
- await this.stateReady;
1911
- if (!this.currentLifecycle(lifecycleGeneration)) return;
1912
- await this.awiki.syncNow(reason);
1913
- if (!this.currentLifecycle(lifecycleGeneration)) return;
1914
- await this.reconcileCommittedHistory();
1915
- });
1916
1814
  }
1917
1815
  enqueueSync(operation) {
1918
1816
  const pending = this.syncMutation.then(operation, operation);
@@ -2079,7 +1977,8 @@ var AwikiAgentListener = class {
2079
1977
  ...update.lastProcessedMessageId === void 0 ? current?.lastProcessedMessageId === void 0 ? {} : { lastProcessedMessageId: current.lastProcessedMessageId } : { lastProcessedMessageId: update.lastProcessedMessageId }
2080
1978
  };
2081
1979
  this.state = {
2082
- version: 1,
1980
+ version: 2,
1981
+ identityScopeHash: this.state.identityScopeHash,
2083
1982
  conversations: {
2084
1983
  ...this.state.conversations,
2085
1984
  [conversation.id]: route
@@ -2092,6 +1991,154 @@ var AwikiAgentListener = class {
2092
1991
  }
2093
1992
  };
2094
1993
  //#endregion
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";
2000
+ }
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;
2027
+ }
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
+ };
2141
+ //#endregion
2095
2142
  //#region lib/types/index.js
2096
2143
  /** Unified AWiki identity, messaging, attachment, Remote, and model-tool service. */
2097
2144
  var __runInitializers = function(thisArg, initializers, value) {
@@ -2162,6 +2209,7 @@ const Config = z.object({
2162
2209
  attachmentMaxBytes: z.number().default(DEFAULT_ATTACHMENT_MAX_BYTES),
2163
2210
  imageAttachmentCacheMaxBytes: z.number().default(DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES),
2164
2211
  pollIntervalMs: z.number().default(DEFAULT_POLL_INTERVAL_MS),
2212
+ realtimeEnabled: z.boolean().default(true),
2165
2213
  listenerEnabled: z.boolean().default(false),
2166
2214
  listenerAllowedPeers: z.array(z.string()).default([]),
2167
2215
  listenerWorkspacePath: z.string(),
@@ -2192,10 +2240,17 @@ const FAILURE_CODES = /* @__PURE__ */ new Set([
2192
2240
  "network",
2193
2241
  "remote"
2194
2242
  ]);
2195
- const LISTENER_RESTART_BASE_DELAY_MS = 1e3;
2196
- const LISTENER_RESTART_MAX_DELAY_MS = 3e4;
2197
- const LISTENER_RESTART_MAX_ATTEMPT = 6;
2198
- 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
+ ]);
2199
2254
  const FAILURE_MESSAGES = {
2200
2255
  "not-registered": "No AWiki identity is registered for this deployment.",
2201
2256
  "signed-out": "This installation is signed out of AWiki.",
@@ -2223,6 +2278,39 @@ const FAILURE_MESSAGES = {
2223
2278
  };
2224
2279
  var ProviderUnavailableError = class extends Error {};
2225
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
+ }
2226
2314
  /** Validate and preserve one SDK service URL without accepting insecure remote HTTP. */
2227
2315
  function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
2228
2316
  let url;
@@ -2307,6 +2395,8 @@ function resolveConfig(config) {
2307
2395
  const pollIntervalMs = config.pollIntervalMs ?? 3e3;
2308
2396
  if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 1e3 || pollIntervalMs > 6e4) throw new TypeError("awiki: pollIntervalMs must be a safe integer from 1000 through 60000");
2309
2397
  const listenerEnabled = config.listenerEnabled ?? false;
2398
+ const realtimeEnabled = config.realtimeEnabled ?? true;
2399
+ if (listenerEnabled && !realtimeEnabled) throw new TypeError("awiki: listenerEnabled requires realtimeEnabled");
2310
2400
  const listenerWorkspacePath = config.listenerWorkspacePath?.trim() || join(dshHome, "workspaces", "awiki");
2311
2401
  if (!isAbsolute(listenerWorkspacePath)) throw new TypeError("awiki: listenerWorkspacePath must be an absolute path");
2312
2402
  const allowedPeers = listenerAllowedPeers(config.listenerAllowedPeers, listenerEnabled);
@@ -2329,6 +2419,7 @@ function resolveConfig(config) {
2329
2419
  attachmentMaxBytes,
2330
2420
  imageAttachmentCacheMaxBytes,
2331
2421
  pollIntervalMs,
2422
+ realtimeEnabled,
2332
2423
  listenerEnabled,
2333
2424
  listener: {
2334
2425
  allowedPeers,
@@ -2479,6 +2570,7 @@ function normalizeRecoveryPrepareRequest(request) {
2479
2570
  };
2480
2571
  }
2481
2572
  const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 65536;
2573
+ const SERVER_INFO_RESPONSE_MAX_BYTES = 65536;
2482
2574
  const RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES = 4096;
2483
2575
  /** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
2484
2576
  async function readBoundedResponseText(response, maxBytes) {
@@ -2697,6 +2789,14 @@ let AwikiService = (() => {
2697
2789
  let _inspectIdentityAccess_decorators;
2698
2790
  let _sendRegistrationOtp_decorators;
2699
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;
2700
2800
  let _updateDisplayName_decorators;
2701
2801
  let _getProfile_decorators;
2702
2802
  let _updateProfile_decorators;
@@ -2742,6 +2842,14 @@ let AwikiService = (() => {
2742
2842
  _inspectIdentityAccess_decorators = [Remote];
2743
2843
  _sendRegistrationOtp_decorators = [Remote];
2744
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];
2745
2853
  _updateDisplayName_decorators = [Remote];
2746
2854
  _getProfile_decorators = [Remote];
2747
2855
  _updateProfile_decorators = [Remote];
@@ -2864,6 +2972,94 @@ let AwikiService = (() => {
2864
2972
  },
2865
2973
  metadata: _metadata
2866
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);
2867
3063
  __esDecorate(this, null, _updateDisplayName_decorators, {
2868
3064
  kind: "method",
2869
3065
  name: "updateDisplayName",
@@ -3259,6 +3455,12 @@ let AwikiService = (() => {
3259
3455
  sessionMutation = Promise.resolve();
3260
3456
  sessionRevision = 0;
3261
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();
3262
3464
  activeSummaryRequests = /* @__PURE__ */ new Set();
3263
3465
  summaryProvider;
3264
3466
  recoveryReconciliationTarget;
@@ -3302,12 +3504,12 @@ let AwikiService = (() => {
3302
3504
  ctx.inject(["workspaceRegistry"], (workspaceCtx) => {
3303
3505
  this.workspaceContext = workspaceCtx;
3304
3506
  const provider = this.provider;
3305
- if (provider !== void 0) this.startListener(provider);
3507
+ if (provider !== void 0) this.ensureAgentConsumer(provider);
3306
3508
  workspaceCtx.effect(() => async () => {
3307
3509
  if (this.workspaceContext !== workspaceCtx) return;
3308
3510
  this.workspaceContext = void 0;
3309
3511
  const current = this.provider;
3310
- if (current !== void 0) await this.stopListener(current);
3512
+ if (current !== void 0) await this.stopAgentConsumer(current);
3311
3513
  }, "awiki: release Workspace listener composition");
3312
3514
  });
3313
3515
  registerAwikiTools(ctx, this);
@@ -3328,6 +3530,8 @@ let AwikiService = (() => {
3328
3530
  */
3329
3531
  registerClientFactory(factory) {
3330
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;
3331
3535
  const provider = {
3332
3536
  client: factory({
3333
3537
  userServiceUrl: this.resolved.userServiceUrl,
@@ -3341,14 +3545,28 @@ let AwikiService = (() => {
3341
3545
  allowInsecureLoopbackForTesting: this.resolved.allowInsecureLoopbackForTesting,
3342
3546
  stateRoot: this.resolved.stateRoot
3343
3547
  }),
3344
- listenerRestartAttempt: 0,
3345
- listenerGeneration: 0,
3346
- listenerRecoveryBlocked: false
3548
+ realtimeGeneration: 0,
3549
+ agentConsumerGeneration: 0,
3550
+ localDeviceJoinRequestCountAfterSync: 0
3347
3551
  };
3348
3552
  this.provider = provider;
3349
- this.startListener(provider);
3553
+ this.ensureRealtimeSupervisor(provider);
3554
+ this.ensureAgentConsumer(provider);
3350
3555
  return () => this.disposeProvider(provider);
3351
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
+ }
3352
3570
  /** Register the optional Model Proxy recovery target without exposing an arbitrary callback or token. */
3353
3571
  registerRecoveryReconciliationTarget(target) {
3354
3572
  if (this.recoveryReconciliationTarget !== void 0) throw new Error("awiki: a recovery reconciliation target is already registered");
@@ -3376,14 +3594,42 @@ let AwikiService = (() => {
3376
3594
  * Read settings needed by the browser presentation.
3377
3595
  * @returns Browser-safe polling configuration without SDK endpoints or state paths.
3378
3596
  */
3379
- getConfig() {
3380
- 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 {
3381
3626
  ok: true,
3382
3627
  value: {
3383
3628
  pollIntervalMs: this.resolved.pollIntervalMs,
3384
- attachmentMaxBytes: this.resolved.attachmentMaxBytes
3629
+ attachmentMaxBytes: this.resolved.attachmentMaxBytes,
3630
+ handleRecoveryPhoneEnabled
3385
3631
  }
3386
- });
3632
+ };
3387
3633
  }
3388
3634
  /**
3389
3635
  * Read the deployment's identity status.
@@ -3406,6 +3652,8 @@ let AwikiService = (() => {
3406
3652
  const identity = await this.run((client) => client.getIdentity(), { allowSignedOut: true });
3407
3653
  if (!identity.ok) return identity;
3408
3654
  this.activeIdentityDid = identity.value?.did;
3655
+ const provider = this.provider;
3656
+ if (identity.value !== null && provider !== void 0) this.ensureProviderRuntime(provider);
3409
3657
  return identity.value === null ? {
3410
3658
  ok: true,
3411
3659
  value: { status: "unregistered" }
@@ -3442,7 +3690,7 @@ let AwikiService = (() => {
3442
3690
  const session = { status: "signed-out" };
3443
3691
  this.publishSession(session);
3444
3692
  const provider = this.provider;
3445
- if (provider !== void 0) await this.stopListener(provider);
3693
+ if (provider !== void 0) await this.stopProviderRuntime(provider);
3446
3694
  return {
3447
3695
  ok: true,
3448
3696
  value: session
@@ -3475,7 +3723,7 @@ let AwikiService = (() => {
3475
3723
  };
3476
3724
  this.publishSession(session);
3477
3725
  const provider = this.provider;
3478
- if (provider !== void 0) this.startListener(provider);
3726
+ if (provider !== void 0) this.ensureProviderRuntime(provider);
3479
3727
  return {
3480
3728
  ok: true,
3481
3729
  value: session
@@ -3561,8 +3809,15 @@ let AwikiService = (() => {
3561
3809
  };
3562
3810
  }
3563
3811
  }
3564
- sendRegistrationOtp(request) {
3565
- 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
+ });
3566
3821
  }
3567
3822
  /**
3568
3823
  * Register and persist the deployment's only AWiki identity.
@@ -3570,9 +3825,190 @@ let AwikiService = (() => {
3570
3825
  * @returns The new public identity or a closed failure.
3571
3826
  */
3572
3827
  async registerIdentity(request) {
3573
- const result = await this.run((client) => client.registerIdentity(request));
3574
- if (result.ok) await this.activateRegisteredIdentity(result.value);
3575
- 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
+ });
3576
4012
  }
3577
4013
  /**
3578
4014
  * Update the deployment identity's public WNS display name.
@@ -3599,12 +4035,19 @@ let AwikiService = (() => {
3599
4035
  }
3600
4036
  /** Start durable phone recovery for one existing full Handle. */
3601
4037
  sendRecoveryOtp(request) {
4038
+ this.pendingDeviceJoin = void 0;
3602
4039
  const normalized = normalizeRecoveryOtpRequest(request);
3603
4040
  if (normalized === void 0) return Promise.resolve({
3604
4041
  ok: false,
3605
4042
  error: failure("invalid-request")
3606
4043
  });
3607
- 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 });
3608
4051
  }
3609
4052
  /** Verify a recovery OTP and freeze its exact intent before the remote commit. */
3610
4053
  prepareRecovery(request) {
@@ -4116,10 +4559,10 @@ let AwikiService = (() => {
4116
4559
  });
4117
4560
  return this.mutateSession(async () => {
4118
4561
  const provider = this.provider;
4119
- if (provider !== void 0) await this.stopListener(provider);
4562
+ if (provider !== void 0) await this.stopProviderRuntime(provider);
4120
4563
  const result = await this.run((client) => client.clearLocalData(), { allowSignedOut: true });
4121
4564
  if (!result.ok) {
4122
- if (provider !== void 0 && this.provider === provider) this.startListener(provider);
4565
+ if (provider !== void 0 && this.provider === provider) this.ensureProviderRuntime(provider);
4123
4566
  return result;
4124
4567
  }
4125
4568
  try {
@@ -4129,9 +4572,10 @@ let AwikiService = (() => {
4129
4572
  await this.sessionStore.signIn();
4130
4573
  this.signedOut = false;
4131
4574
  this.activeIdentityDid = void 0;
4575
+ this.pendingDeviceJoin = void 0;
4576
+ this.activeDeviceJoinSessionId = void 0;
4132
4577
  this.invalidateSummaries();
4133
4578
  this.publishSession({ status: "unregistered" });
4134
- if (provider !== void 0 && this.provider === provider) this.startListener(provider);
4135
4579
  return result;
4136
4580
  } catch {
4137
4581
  return {
@@ -4165,8 +4609,7 @@ let AwikiService = (() => {
4165
4609
  identity
4166
4610
  };
4167
4611
  this.publishSession(session);
4168
- await provider.listenerStartup;
4169
- await this.startListener(provider);
4612
+ this.ensureProviderRuntime(provider);
4170
4613
  }
4171
4614
  return reconciled;
4172
4615
  } catch {
@@ -4202,22 +4645,159 @@ let AwikiService = (() => {
4202
4645
  return false;
4203
4646
  }
4204
4647
  }
4205
- /** 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. */
4206
4783
  async activateRegisteredIdentity(identity) {
4784
+ this.pendingDeviceJoin = void 0;
4785
+ this.activeDeviceJoinSessionId = void 0;
4207
4786
  this.activeIdentityDid = identity.did;
4208
4787
  this.publishSession({
4209
4788
  status: "active",
4210
4789
  identity
4211
4790
  });
4212
4791
  const provider = this.provider;
4213
- if (provider !== void 0) {
4214
- await provider.listenerStartup;
4215
- await this.startListener(provider);
4216
- }
4792
+ if (provider !== void 0) this.ensureProviderRuntime(provider);
4217
4793
  }
4218
4794
  /** Invalidate cached session work and cancel every model request still owned by the old session. */
4219
4795
  invalidateSummaries() {
4220
4796
  this.sessionRevision += 1;
4797
+ this.requestRefs.clear();
4798
+ this.requestSessions.clear();
4799
+ this.deviceRefs.clear();
4800
+ this.deviceIds.clear();
4221
4801
  for (const controller of this.activeSummaryRequests) controller.abort();
4222
4802
  this.activeSummaryRequests.clear();
4223
4803
  }
@@ -4317,120 +4897,148 @@ let AwikiService = (() => {
4317
4897
  this.sessionMutation = pending.then(() => void 0, () => void 0);
4318
4898
  return pending;
4319
4899
  }
4320
- /** Start one exact identity-bound listener, atomically releasing a failed startup. */
4321
- startListener(provider) {
4322
- if (!this.resolved.listenerEnabled || provider.listener !== void 0 || provider.listenerRecoveryBlocked || this.provider !== provider) return Promise.resolve();
4323
- if (provider.listenerStartup !== void 0) return provider.listenerStartup;
4324
- if (provider.listenerCleanup !== void 0 || provider.listenerRestartTimer !== void 0) return Promise.resolve();
4325
- const workspaceContext = this.workspaceContext;
4326
- const source = provider.client.listener;
4327
- if (workspaceContext === void 0 || source === void 0) return Promise.resolve();
4328
- const generation = provider.listenerGeneration;
4329
- 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");
4330
4932
  const observed = (async () => {
4331
4933
  if (await this.isSignedOut()) return;
4332
- if (await provider.client.getIdentity() === null) return;
4333
- if (!this.listenerFenceMatches(provider, workspaceContext, generation)) return;
4334
- const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
4335
- const listener = new AwikiAgentListener(source, agents, this.resolved.listener, logger);
4336
- provider.listener = listener;
4337
- try {
4338
- await listener.start();
4339
- } catch (error) {
4340
- if (provider.listener === listener) delete provider.listener;
4341
- try {
4342
- await listener.dispose();
4343
- } catch {
4344
- provider.listenerRecoveryBlocked = true;
4345
- }
4346
- if (!provider.listenerRecoveryBlocked && provider.listenerRestartAttempt > 0) this.scheduleListenerRestart(provider, workspaceContext, generation);
4347
- throw error;
4348
- }
4349
- if (!this.listenerFenceMatches(provider, workspaceContext, generation)) {
4350
- if (provider.listener === listener) delete provider.listener;
4351
- try {
4352
- await listener.dispose();
4353
- } catch (error) {
4354
- provider.listenerRecoveryBlocked = true;
4355
- throw error;
4356
- }
4357
- return;
4358
- }
4359
- provider.listenerStartedAt = Date.now();
4360
- listener.whenTerminated().then((result) => {
4361
- if (result.kind === "failed") this.releaseFailedListener(provider, listener, workspaceContext, generation, result.error);
4362
- });
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);
4363
4942
  })().catch((error) => {
4364
- 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");
4365
4944
  }).finally(() => {
4366
- if (provider.listenerStartup === observed) delete provider.listenerStartup;
4945
+ if (provider.realtimeStartup === observed) delete provider.realtimeStartup;
4367
4946
  });
4368
- provider.listenerStartup = observed;
4369
- return observed;
4370
- }
4371
- async stopListener(provider) {
4372
- provider.listenerGeneration += 1;
4373
- provider.listenerRestartAttempt = 0;
4374
- delete provider.listenerStartedAt;
4375
- const timer = provider.listenerRestartTimer;
4376
- if (timer !== void 0) {
4377
- delete provider.listenerRestartTimer;
4378
- clearTimeout(timer);
4379
- }
4380
- await provider.listenerStartup;
4381
- await provider.listenerCleanup;
4382
- const listener = provider.listener;
4383
- if (listener === void 0) return;
4384
- delete provider.listener;
4385
- try {
4386
- await listener.dispose();
4387
- } catch (error) {
4388
- provider.listenerRecoveryBlocked = true;
4389
- throw error;
4390
- }
4947
+ provider.realtimeStartup = observed;
4391
4948
  }
4392
- listenerFenceMatches(provider, workspaceContext, generation) {
4393
- return this.provider === provider && this.workspaceContext === workspaceContext && provider.listenerGeneration === generation;
4949
+ realtimeFenceMatches(provider, generation) {
4950
+ return this.provider === provider && provider.realtimeGeneration === generation;
4394
4951
  }
4395
- releaseFailedListener(provider, listener, workspaceContext, generation, error) {
4396
- if (provider.listener !== listener) return;
4397
- delete provider.listener;
4398
- const startedAt = provider.listenerStartedAt;
4399
- delete provider.listenerStartedAt;
4400
- 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;
4401
4979
  const logger = this.ctx.logger("awiki-listener");
4402
- logger.warn("AWiki listener lifecycle failed: %s", error instanceof Error ? error.message : "unknown failure");
4403
- let disposed = false;
4404
- const observed = listener.dispose().then(() => {
4405
- disposed = true;
4406
- }, (cleanupError) => {
4407
- provider.listenerRecoveryBlocked = true;
4408
- 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");
4409
4993
  }).finally(() => {
4410
- if (provider.listenerCleanup === observed) delete provider.listenerCleanup;
4411
- 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;
4412
5008
  });
4413
- provider.listenerCleanup = observed;
4414
- }
4415
- scheduleListenerRestart(provider, workspaceContext, generation) {
4416
- if (provider.listenerRestartTimer !== void 0 || !this.listenerFenceMatches(provider, workspaceContext, generation)) return;
4417
- provider.listenerRestartAttempt = Math.min(provider.listenerRestartAttempt + 1, LISTENER_RESTART_MAX_ATTEMPT);
4418
- const exponent = provider.listenerRestartAttempt - 1;
4419
- const delay = Math.min(LISTENER_RESTART_BASE_DELAY_MS * 2 ** exponent, LISTENER_RESTART_MAX_DELAY_MS);
4420
- const timer = setTimeout(() => {
4421
- if (provider.listenerRestartTimer !== timer) return;
4422
- delete provider.listenerRestartTimer;
4423
- if (!this.listenerFenceMatches(provider, workspaceContext, generation)) return;
4424
- this.startListener(provider);
4425
- }, delay);
4426
- 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)]);
4427
5029
  }
4428
5030
  /** Clear one exact provider slot before joining its one shared disposal. */
4429
5031
  disposeProvider(provider) {
4430
- 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
+ }
4431
5038
  provider.disposal ??= (async () => {
4432
5039
  try {
4433
- await this.stopListener(provider);
5040
+ await provider.runtimeReplacement;
5041
+ await this.stopProviderRuntime(provider);
4434
5042
  } finally {
4435
5043
  await provider.client.dispose();
4436
5044
  }