@awiki/dsh-plugin 0.3.4 → 0.3.5

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 +32 -38
  2. package/README.zh.md +28 -31
  3. package/cordis.patch.yml +0 -17
  4. package/lib/client.js +269 -1266
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +316 -922
  7. package/lib/provider.js +16 -52
  8. package/lib/{sdk-adapter-BeJakxLP.mjs → sdk-adapter-CDLTgHJA.mjs} +4 -142
  9. package/lib/typert.host.js +89 -419
  10. package/lib/typert.remote-client.d.ts +3 -19
  11. package/lib/typert.remote-client.d.ts.map +1 -1
  12. package/lib/typert.remote-client.js +89 -419
  13. package/lib/types/client/AwikiIdentityAccess.d.ts +5 -6
  14. package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
  15. package/lib/types/client/AwikiIdentityAccess.js +27 -88
  16. package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
  17. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  18. package/lib/types/client/AwikiOverlay.js +3 -4
  19. package/lib/types/client/AwikiOverlay.js.map +1 -1
  20. package/lib/types/client/controller.d.ts +5 -20
  21. package/lib/types/client/controller.d.ts.map +1 -1
  22. package/lib/types/client/controller.js +7 -38
  23. package/lib/types/client/controller.js.map +1 -1
  24. package/lib/types/client/index.d.ts.map +1 -1
  25. package/lib/types/client/index.js +0 -8
  26. package/lib/types/client/index.js.map +1 -1
  27. package/lib/types/client/slots.d.ts +2 -10
  28. package/lib/types/client/slots.d.ts.map +1 -1
  29. package/lib/types/index.d.ts +11 -54
  30. package/lib/types/index.d.ts.map +1 -1
  31. package/lib/types/index.js +176 -735
  32. package/lib/types/index.js.map +1 -1
  33. package/lib/types/listener-state.d.ts +3 -5
  34. package/lib/types/listener-state.d.ts.map +1 -1
  35. package/lib/types/listener-state.js +13 -28
  36. package/lib/types/listener-state.js.map +1 -1
  37. package/lib/types/listener.d.ts +32 -9
  38. package/lib/types/listener.d.ts.map +1 -1
  39. package/lib/types/listener.js +127 -11
  40. package/lib/types/listener.js.map +1 -1
  41. package/lib/types/profile-state.d.ts +11 -0
  42. package/lib/types/profile-state.d.ts.map +1 -0
  43. package/lib/types/profile-state.js +48 -0
  44. package/lib/types/profile-state.js.map +1 -0
  45. package/lib/types/provider-api.d.ts +4 -96
  46. package/lib/types/provider-api.d.ts.map +1 -1
  47. package/lib/types/provider.d.ts +1 -7
  48. package/lib/types/provider.d.ts.map +1 -1
  49. package/lib/types/provider.js +15 -53
  50. package/lib/types/provider.js.map +1 -1
  51. package/lib/types/sdk-adapter.d.ts +2 -28
  52. package/lib/types/sdk-adapter.d.ts.map +1 -1
  53. package/lib/types/sdk-adapter.js +3 -132
  54. package/lib/types/sdk-adapter.js.map +1 -1
  55. package/lib/types/types.d.ts +6 -76
  56. package/lib/types/types.d.ts.map +1 -1
  57. package/lib/types/types.js.map +1 -1
  58. package/package.json +18 -18
  59. package/lib/types/client/AwikiDevices.d.ts +0 -28
  60. package/lib/types/client/AwikiDevices.d.ts.map +0 -1
  61. package/lib/types/client/AwikiDevices.js +0 -85
  62. package/lib/types/client/AwikiDevices.js.map +0 -1
  63. package/lib/types/realtime-supervisor.d.ts +0 -43
  64. package/lib/types/realtime-supervisor.d.ts.map +0 -1
  65. package/lib/types/realtime-supervisor.js +0 -166
  66. package/lib/types/realtime-supervisor.js.map +0 -1
package/lib/index.js CHANGED
@@ -1,16 +1,18 @@
1
- import { n as downloadedAttachment } from "./sdk-adapter-BeJakxLP.mjs";
1
+ import { n as downloadedAttachment } from "./sdk-adapter-CDLTgHJA.mjs";
2
2
  import "@deepseek-ai/cordis";
3
- import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
3
+ import { existsSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
- import { isAbsolute, join } from "node:path";
5
+ import { basename, dirname, isAbsolute, join, resolve } 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";
11
12
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
12
13
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
13
14
  import { SessionId } from "@deepseek-ai/dsh-session";
15
+ import { fileURLToPath } from "node:url";
14
16
  //#region lib/types/types.js
15
17
  /** Client-safe AWiki service and Remote data types. */
16
18
  /** Exact browser acknowledgement required before locally signing out. */
@@ -1448,13 +1450,12 @@ var AwikiConversationPreferenceStore = class {
1448
1450
  };
1449
1451
  //#endregion
1450
1452
  //#region lib/types/listener-state.js
1451
- const STATE_VERSION = 2;
1453
+ const STATE_VERSION = 1;
1452
1454
  const MAX_STATE_BYTES = 1048576;
1453
1455
  const MAX_CONVERSATIONS = 1e3;
1454
- function emptyState(identityScopeHash) {
1456
+ function emptyState() {
1455
1457
  return {
1456
1458
  version: STATE_VERSION,
1457
- identityScopeHash,
1458
1459
  conversations: {}
1459
1460
  };
1460
1461
  }
@@ -1466,10 +1467,8 @@ function boundedString(value, field) {
1466
1467
  return value;
1467
1468
  }
1468
1469
  /** Validate and detach the complete state file before the listener trusts any route. */
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
+ function parseState(value) {
1471
+ if (!isRecord(value) || value.version !== STATE_VERSION || !isRecord(value.conversations)) throw new TypeError("awiki: listener state is invalid");
1473
1472
  const entries = Object.entries(value.conversations);
1474
1473
  if (entries.length > MAX_CONVERSATIONS) throw new TypeError("awiki: listener state is too large");
1475
1474
  const conversations = {};
@@ -1492,7 +1491,6 @@ function parseState(value, identityScopeHash) {
1492
1491
  }
1493
1492
  return {
1494
1493
  version: STATE_VERSION,
1495
- identityScopeHash,
1496
1494
  conversations
1497
1495
  };
1498
1496
  }
@@ -1503,31 +1501,28 @@ function isMissing(error) {
1503
1501
  var AwikiListenerStateStore = class {
1504
1502
  hostDirectory;
1505
1503
  statePath;
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");
1504
+ constructor(stateRoot) {
1510
1505
  this.hostDirectory = join(stateRoot, ".host");
1511
1506
  this.statePath = join(this.hostDirectory, "listener-state.json");
1512
1507
  }
1513
- /** Load the current identity scope or reset unscoped v1 state on first use. */
1508
+ /** Load the current state or return an empty v1 document on first use. */
1514
1509
  async load() {
1515
- if (!await this.hasPrivateHostDirectory()) return emptyState(this.identityScopeHash);
1510
+ if (!await this.hasPrivateHostDirectory()) return emptyState();
1516
1511
  try {
1517
1512
  const metadata = await lstat(this.statePath);
1518
1513
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is invalid");
1519
1514
  const text = await readFile(this.statePath, "utf8");
1520
1515
  if (Buffer.byteLength(text, "utf8") > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is invalid");
1521
- return parseState(JSON.parse(text), this.identityScopeHash);
1516
+ return parseState(JSON.parse(text));
1522
1517
  } catch (error) {
1523
- if (isMissing(error)) return emptyState(this.identityScopeHash);
1518
+ if (isMissing(error)) return emptyState();
1524
1519
  if (error instanceof SyntaxError) throw new TypeError("awiki: listener state file is invalid");
1525
1520
  throw error;
1526
1521
  }
1527
1522
  }
1528
1523
  /** Replace the state atomically without ever writing message or Agent text. */
1529
1524
  async save(state) {
1530
- const snapshot = parseState(state, this.identityScopeHash);
1525
+ const snapshot = parseState(state);
1531
1526
  const text = `${JSON.stringify(snapshot)}\n`;
1532
1527
  if (Buffer.byteLength(text, "utf8") > MAX_STATE_BYTES) throw new TypeError("awiki: listener state file is too large");
1533
1528
  await mkdir(this.hostDirectory, {
@@ -1749,12 +1744,11 @@ function incomingFromPeer(message, conversation) {
1749
1744
  }
1750
1745
  function copyState(state) {
1751
1746
  return {
1752
- version: 2,
1753
- identityScopeHash: state.identityScopeHash,
1747
+ version: 1,
1754
1748
  conversations: Object.fromEntries(Object.entries(state.conversations).map(([id, route]) => [id, { ...route }]))
1755
1749
  };
1756
1750
  }
1757
- /** Consume authorized Direct text only after the identity supervisor commits synchronization. */
1751
+ /** Reconcile authorized Direct text from Core history whenever realtime schedules synchronization. */
1758
1752
  var AwikiAgentListener = class {
1759
1753
  awiki;
1760
1754
  agents;
@@ -1763,23 +1757,34 @@ var AwikiAgentListener = class {
1763
1757
  store;
1764
1758
  logger;
1765
1759
  stateReady;
1766
- state;
1760
+ state = {
1761
+ version: 1,
1762
+ conversations: {}
1763
+ };
1767
1764
  stateMutation = Promise.resolve();
1768
1765
  syncMutation = Promise.resolve();
1769
1766
  scheduledMessageIds = /* @__PURE__ */ new Set();
1770
1767
  conversationQueues = /* @__PURE__ */ new Map();
1768
+ lifecycle;
1769
+ started;
1770
+ resolveStarted;
1771
+ rejectStarted;
1772
+ lifecycleGeneration = 0;
1773
+ streamGeneration = 0;
1774
+ activeRealtime;
1771
1775
  stopped = false;
1776
+ termination;
1777
+ resolveTermination;
1778
+ terminationSettled = false;
1772
1779
  constructor(awiki, agents, config, logger, store) {
1773
1780
  this.awiki = awiki;
1774
1781
  this.agents = agents;
1775
1782
  this.config = config;
1783
+ this.termination = new Promise((resolve) => {
1784
+ this.resolveTermination = resolve;
1785
+ });
1776
1786
  this.allowedPeers = new Set(config.allowedPeers.map((peer) => peer.startsWith("did:") ? peer : peer.toLowerCase()));
1777
- this.store = store ?? new AwikiListenerStateStore(config.stateRoot, config.identityScope);
1778
- this.state = {
1779
- version: 2,
1780
- identityScopeHash: this.store.identityScopeHash,
1781
- conversations: {}
1782
- };
1787
+ this.store = store ?? new AwikiListenerStateStore(config.stateRoot);
1783
1788
  this.logger = logger ?? {
1784
1789
  debug() {},
1785
1790
  info() {},
@@ -1791,11 +1796,38 @@ var AwikiAgentListener = class {
1791
1796
  this.state = state;
1792
1797
  });
1793
1798
  }
1794
- /** Reconcile only committed history; this consumer cannot start WSS or advance sync. */
1795
- async reconcileOnce() {
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) {
1796
1826
  await this.enqueueSync(async () => {
1797
1827
  await this.stateReady;
1798
1828
  if (this.stopped) return;
1829
+ await this.awiki.syncNow(reason);
1830
+ if (this.stopped) return;
1799
1831
  await this.reconcileCommittedHistory();
1800
1832
  });
1801
1833
  }
@@ -1805,12 +1837,84 @@ var AwikiAgentListener = class {
1805
1837
  while (this.conversationQueues.size > 0) await Promise.allSettled([...this.conversationQueues.values()]);
1806
1838
  await this.stateMutation;
1807
1839
  }
1808
- /** Fence late work, drain committed messages, then release listener-owned Agents. */
1840
+ /** Stop realtime first, fence late events, drain messages, then release listener-owned Agents. */
1809
1841
  async dispose() {
1810
1842
  if (this.stopped) return;
1811
1843
  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;
1812
1853
  await this.whenIdle();
1813
1854
  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
+ });
1814
1918
  }
1815
1919
  enqueueSync(operation) {
1816
1920
  const pending = this.syncMutation.then(operation, operation);
@@ -1977,8 +2081,7 @@ var AwikiAgentListener = class {
1977
2081
  ...update.lastProcessedMessageId === void 0 ? current?.lastProcessedMessageId === void 0 ? {} : { lastProcessedMessageId: current.lastProcessedMessageId } : { lastProcessedMessageId: update.lastProcessedMessageId }
1978
2082
  };
1979
2083
  this.state = {
1980
- version: 2,
1981
- identityScopeHash: this.state.identityScopeHash,
2084
+ version: 1,
1982
2085
  conversations: {
1983
2086
  ...this.state.conversations,
1984
2087
  [conversation.id]: route
@@ -1991,153 +2094,39 @@ var AwikiAgentListener = class {
1991
2094
  }
1992
2095
  };
1993
2096
  //#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";
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;
2000
2102
  }
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?.();
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;
2139
2120
  }
2140
- };
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
+ }
2141
2130
  //#endregion
2142
2131
  //#region lib/types/index.js
2143
2132
  /** Unified AWiki identity, messaging, attachment, Remote, and model-tool service. */
@@ -2209,7 +2198,6 @@ const Config = z.object({
2209
2198
  attachmentMaxBytes: z.number().default(DEFAULT_ATTACHMENT_MAX_BYTES),
2210
2199
  imageAttachmentCacheMaxBytes: z.number().default(DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES),
2211
2200
  pollIntervalMs: z.number().default(DEFAULT_POLL_INTERVAL_MS),
2212
- realtimeEnabled: z.boolean().default(true),
2213
2201
  listenerEnabled: z.boolean().default(false),
2214
2202
  listenerAllowedPeers: z.array(z.string()).default([]),
2215
2203
  listenerWorkspacePath: z.string(),
@@ -2227,6 +2215,7 @@ const FAILURE_CODES = /* @__PURE__ */ new Set([
2227
2215
  "forbidden",
2228
2216
  "identity-recovery-required",
2229
2217
  "conflict",
2218
+ "state-in-use",
2230
2219
  "rate-limited",
2231
2220
  "group-membership-required",
2232
2221
  "group-identity-stale",
@@ -2240,17 +2229,10 @@ const FAILURE_CODES = /* @__PURE__ */ new Set([
2240
2229
  "network",
2241
2230
  "remote"
2242
2231
  ]);
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
- ]);
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;
2254
2236
  const FAILURE_MESSAGES = {
2255
2237
  "not-registered": "No AWiki identity is registered for this deployment.",
2256
2238
  "signed-out": "This installation is signed out of AWiki.",
@@ -2263,6 +2245,7 @@ const FAILURE_MESSAGES = {
2263
2245
  "forbidden": "The AWiki operation is not permitted.",
2264
2246
  "identity-recovery-required": "The local AWiki identity must be recovered before it can be used again.",
2265
2247
  "conflict": "The AWiki operation conflicts with current state.",
2248
+ "state-in-use": "AWiki data is already open in another client.",
2266
2249
  "rate-limited": "The AWiki service rate-limited the request.",
2267
2250
  "group-membership-required": "The active AWiki identity is not a member of this group.",
2268
2251
  "group-identity-stale": "The AWiki group identity binding is still recovering.",
@@ -2278,39 +2261,6 @@ const FAILURE_MESSAGES = {
2278
2261
  };
2279
2262
  var ProviderUnavailableError = class extends Error {};
2280
2263
  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
- }
2314
2264
  /** Validate and preserve one SDK service URL without accepting insecure remote HTTP. */
2315
2265
  function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
2316
2266
  let url;
@@ -2332,22 +2282,6 @@ function recoveryReconciliationEndpoint(target, allowInsecureLoopbackForTesting)
2332
2282
  if (parsed.search !== "") throw new TypeError("awiki: recovery reconciliation target must not contain a query");
2333
2283
  return new URL("/api/identity-recovery", parsed).toString();
2334
2284
  }
2335
- /** Accept only the closed Model Proxy success response; no ledger identifier may cross back. */
2336
- async function acceptsRecoveryReconciliation(response) {
2337
- if (!response.ok) return false;
2338
- let value;
2339
- try {
2340
- const text = await readBoundedResponseText(response, RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES);
2341
- if (text === void 0) return false;
2342
- value = JSON.parse(text);
2343
- } catch {
2344
- return false;
2345
- }
2346
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2347
- const result = value;
2348
- const keys = Object.keys(result).sort();
2349
- return keys.length === 2 && keys[0] === "idempotent" && keys[1] === "restored" && result.restored === true && typeof result.idempotent === "boolean";
2350
- }
2351
2285
  /** Validate a provider domain without inferring it from an API endpoint. */
2352
2286
  function serviceDomain(raw, field = "userServiceDomain") {
2353
2287
  return normalizeAwikiDomain(raw, field);
@@ -2381,12 +2315,14 @@ function listenerAllowedPeers(raw, enabled) {
2381
2315
  return peers;
2382
2316
  }
2383
2317
  /** Resolve and validate every deployment choice before publishing the service. */
2384
- function resolveConfig(config) {
2318
+ function resolveConfig(ctx, config) {
2385
2319
  const allowInsecureLoopbackForTesting = config.allowInsecureLoopbackForTesting ?? false;
2386
2320
  const configuredStateRoot = config.stateRoot?.trim();
2387
2321
  const configuredDshHome = process.env.DSH_HOME?.trim();
2388
2322
  const dshHome = configuredDshHome === void 0 || configuredDshHome.length === 0 ? join(homedir(), ".dsh") : configuredDshHome;
2389
- const stateRoot = configuredStateRoot === void 0 || configuredStateRoot.length === 0 ? join(dshHome, "awiki", "im-core") : configuredStateRoot;
2323
+ const profileName = configuredStateRoot === void 0 || configuredStateRoot.length === 0 ? resolveAwikiProfileName(ctx, dshHome) : void 0;
2324
+ const stateRoot = configuredStateRoot === void 0 || configuredStateRoot.length === 0 ? resolveAwikiStateRoot(ctx, dshHome) : configuredStateRoot;
2325
+ const legacySharedStateDetected = profileName !== void 0 && existsSync(join(dshHome, "awiki", "im-core"));
2390
2326
  if (!isAbsolute(stateRoot)) throw new TypeError("awiki: stateRoot must be an absolute path");
2391
2327
  const attachmentMaxBytes = config.attachmentMaxBytes ?? 10485760;
2392
2328
  if (!Number.isSafeInteger(attachmentMaxBytes) || attachmentMaxBytes < 1) throw new TypeError("awiki: attachmentMaxBytes must be a positive safe integer");
@@ -2395,8 +2331,6 @@ function resolveConfig(config) {
2395
2331
  const pollIntervalMs = config.pollIntervalMs ?? 3e3;
2396
2332
  if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 1e3 || pollIntervalMs > 6e4) throw new TypeError("awiki: pollIntervalMs must be a safe integer from 1000 through 60000");
2397
2333
  const listenerEnabled = config.listenerEnabled ?? false;
2398
- const realtimeEnabled = config.realtimeEnabled ?? true;
2399
- if (listenerEnabled && !realtimeEnabled) throw new TypeError("awiki: listenerEnabled requires realtimeEnabled");
2400
2334
  const listenerWorkspacePath = config.listenerWorkspacePath?.trim() || join(dshHome, "workspaces", "awiki");
2401
2335
  if (!isAbsolute(listenerWorkspacePath)) throw new TypeError("awiki: listenerWorkspacePath must be an absolute path");
2402
2336
  const allowedPeers = listenerAllowedPeers(config.listenerAllowedPeers, listenerEnabled);
@@ -2419,7 +2353,10 @@ function resolveConfig(config) {
2419
2353
  attachmentMaxBytes,
2420
2354
  imageAttachmentCacheMaxBytes,
2421
2355
  pollIntervalMs,
2422
- realtimeEnabled,
2356
+ ...profileName === void 0 ? {} : {
2357
+ profileName,
2358
+ legacySharedStateDetected
2359
+ },
2423
2360
  listenerEnabled,
2424
2361
  listener: {
2425
2362
  allowedPeers,
@@ -2570,8 +2507,6 @@ function normalizeRecoveryPrepareRequest(request) {
2570
2507
  };
2571
2508
  }
2572
2509
  const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 65536;
2573
- const SERVER_INFO_RESPONSE_MAX_BYTES = 65536;
2574
- const RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES = 4096;
2575
2510
  /** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
2576
2511
  async function readBoundedResponseText(response, maxBytes) {
2577
2512
  const declaredLength = response.headers.get("content-length");
@@ -2789,14 +2724,6 @@ let AwikiService = (() => {
2789
2724
  let _inspectIdentityAccess_decorators;
2790
2725
  let _sendRegistrationOtp_decorators;
2791
2726
  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;
2800
2727
  let _updateDisplayName_decorators;
2801
2728
  let _getProfile_decorators;
2802
2729
  let _updateProfile_decorators;
@@ -2842,14 +2769,6 @@ let AwikiService = (() => {
2842
2769
  _inspectIdentityAccess_decorators = [Remote];
2843
2770
  _sendRegistrationOtp_decorators = [Remote];
2844
2771
  _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];
2853
2772
  _updateDisplayName_decorators = [Remote];
2854
2773
  _getProfile_decorators = [Remote];
2855
2774
  _updateProfile_decorators = [Remote];
@@ -2972,94 +2891,6 @@ let AwikiService = (() => {
2972
2891
  },
2973
2892
  metadata: _metadata
2974
2893
  }, 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);
3063
2894
  __esDecorate(this, null, _updateDisplayName_decorators, {
3064
2895
  kind: "method",
3065
2896
  name: "updateDisplayName",
@@ -3455,12 +3286,6 @@ let AwikiService = (() => {
3455
3286
  sessionMutation = Promise.resolve();
3456
3287
  sessionRevision = 0;
3457
3288
  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();
3464
3289
  activeSummaryRequests = /* @__PURE__ */ new Set();
3465
3290
  summaryProvider;
3466
3291
  recoveryReconciliationTarget;
@@ -3475,7 +3300,7 @@ let AwikiService = (() => {
3475
3300
  constructor(ctx, config) {
3476
3301
  super(ctx, "awiki");
3477
3302
  this.hostContext = ctx;
3478
- this.resolved = resolveConfig(config);
3303
+ this.resolved = resolveConfig(ctx, config);
3479
3304
  this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
3480
3305
  this.sessionStore = new AwikiSessionStore(this.resolved.stateRoot);
3481
3306
  this.imageAttachmentCache = new AwikiImageAttachmentCache(this.resolved.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
@@ -3504,12 +3329,12 @@ let AwikiService = (() => {
3504
3329
  ctx.inject(["workspaceRegistry"], (workspaceCtx) => {
3505
3330
  this.workspaceContext = workspaceCtx;
3506
3331
  const provider = this.provider;
3507
- if (provider !== void 0) this.ensureAgentConsumer(provider);
3332
+ if (provider !== void 0) this.startListener(provider);
3508
3333
  workspaceCtx.effect(() => async () => {
3509
3334
  if (this.workspaceContext !== workspaceCtx) return;
3510
3335
  this.workspaceContext = void 0;
3511
3336
  const current = this.provider;
3512
- if (current !== void 0) await this.stopAgentConsumer(current);
3337
+ if (current !== void 0) await this.stopListener(current);
3513
3338
  }, "awiki: release Workspace listener composition");
3514
3339
  });
3515
3340
  registerAwikiTools(ctx, this);
@@ -3530,8 +3355,6 @@ let AwikiService = (() => {
3530
3355
  */
3531
3356
  registerClientFactory(factory) {
3532
3357
  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;
3535
3358
  const provider = {
3536
3359
  client: factory({
3537
3360
  userServiceUrl: this.resolved.userServiceUrl,
@@ -3545,28 +3368,14 @@ let AwikiService = (() => {
3545
3368
  allowInsecureLoopbackForTesting: this.resolved.allowInsecureLoopbackForTesting,
3546
3369
  stateRoot: this.resolved.stateRoot
3547
3370
  }),
3548
- realtimeGeneration: 0,
3549
- agentConsumerGeneration: 0,
3550
- localDeviceJoinRequestCountAfterSync: 0
3371
+ listenerRestartAttempt: 0,
3372
+ listenerGeneration: 0,
3373
+ listenerRecoveryBlocked: false
3551
3374
  };
3552
3375
  this.provider = provider;
3553
- this.ensureRealtimeSupervisor(provider);
3554
- this.ensureAgentConsumer(provider);
3376
+ this.startListener(provider);
3555
3377
  return () => this.disposeProvider(provider);
3556
3378
  }
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
- }
3570
3379
  /** Register the optional Model Proxy recovery target without exposing an arbitrary callback or token. */
3571
3380
  registerRecoveryReconciliationTarget(target) {
3572
3381
  if (this.recoveryReconciliationTarget !== void 0) throw new Error("awiki: a recovery reconciliation target is already registered");
@@ -3594,42 +3403,18 @@ let AwikiService = (() => {
3594
3403
  * Read settings needed by the browser presentation.
3595
3404
  * @returns Browser-safe polling configuration without SDK endpoints or state paths.
3596
3405
  */
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 {
3406
+ getConfig() {
3407
+ return Promise.resolve({
3626
3408
  ok: true,
3627
3409
  value: {
3628
3410
  pollIntervalMs: this.resolved.pollIntervalMs,
3629
3411
  attachmentMaxBytes: this.resolved.attachmentMaxBytes,
3630
- handleRecoveryPhoneEnabled
3412
+ ...this.resolved.profileName === void 0 ? {} : {
3413
+ profileName: this.resolved.profileName,
3414
+ legacySharedStateDetected: this.resolved.legacySharedStateDetected
3415
+ }
3631
3416
  }
3632
- };
3417
+ });
3633
3418
  }
3634
3419
  /**
3635
3420
  * Read the deployment's identity status.
@@ -3652,8 +3437,6 @@ let AwikiService = (() => {
3652
3437
  const identity = await this.run((client) => client.getIdentity(), { allowSignedOut: true });
3653
3438
  if (!identity.ok) return identity;
3654
3439
  this.activeIdentityDid = identity.value?.did;
3655
- const provider = this.provider;
3656
- if (identity.value !== null && provider !== void 0) this.ensureProviderRuntime(provider);
3657
3440
  return identity.value === null ? {
3658
3441
  ok: true,
3659
3442
  value: { status: "unregistered" }
@@ -3690,7 +3473,7 @@ let AwikiService = (() => {
3690
3473
  const session = { status: "signed-out" };
3691
3474
  this.publishSession(session);
3692
3475
  const provider = this.provider;
3693
- if (provider !== void 0) await this.stopProviderRuntime(provider);
3476
+ if (provider !== void 0) await this.stopListener(provider);
3694
3477
  return {
3695
3478
  ok: true,
3696
3479
  value: session
@@ -3723,7 +3506,7 @@ let AwikiService = (() => {
3723
3506
  };
3724
3507
  this.publishSession(session);
3725
3508
  const provider = this.provider;
3726
- if (provider !== void 0) this.ensureProviderRuntime(provider);
3509
+ if (provider !== void 0) this.startListener(provider);
3727
3510
  return {
3728
3511
  ok: true,
3729
3512
  value: session
@@ -3809,15 +3592,8 @@ let AwikiService = (() => {
3809
3592
  };
3810
3593
  }
3811
3594
  }
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
- });
3595
+ sendRegistrationOtp(request) {
3596
+ return this.run((client) => client.sendRegistrationOtp(request));
3821
3597
  }
3822
3598
  /**
3823
3599
  * Register and persist the deployment's only AWiki identity.
@@ -3825,190 +3601,9 @@ let AwikiService = (() => {
3825
3601
  * @returns The new public identity or a closed failure.
3826
3602
  */
3827
3603
  async registerIdentity(request) {
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
- });
3604
+ const result = await this.run((client) => client.registerIdentity(request));
3605
+ if (result.ok) await this.activateRegisteredIdentity(result.value);
3606
+ return result;
4012
3607
  }
4013
3608
  /**
4014
3609
  * Update the deployment identity's public WNS display name.
@@ -4035,19 +3630,12 @@ let AwikiService = (() => {
4035
3630
  }
4036
3631
  /** Start durable phone recovery for one existing full Handle. */
4037
3632
  sendRecoveryOtp(request) {
4038
- this.pendingDeviceJoin = void 0;
4039
3633
  const normalized = normalizeRecoveryOtpRequest(request);
4040
3634
  if (normalized === void 0) return Promise.resolve({
4041
3635
  ok: false,
4042
3636
  error: failure("invalid-request")
4043
3637
  });
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 });
3638
+ return this.run((client) => client.sendRecoveryOtp(normalized), { allowSignedOut: true });
4051
3639
  }
4052
3640
  /** Verify a recovery OTP and freeze its exact intent before the remote commit. */
4053
3641
  prepareRecovery(request) {
@@ -4559,10 +4147,10 @@ let AwikiService = (() => {
4559
4147
  });
4560
4148
  return this.mutateSession(async () => {
4561
4149
  const provider = this.provider;
4562
- if (provider !== void 0) await this.stopProviderRuntime(provider);
4150
+ if (provider !== void 0) await this.stopListener(provider);
4563
4151
  const result = await this.run((client) => client.clearLocalData(), { allowSignedOut: true });
4564
4152
  if (!result.ok) {
4565
- if (provider !== void 0 && this.provider === provider) this.ensureProviderRuntime(provider);
4153
+ if (provider !== void 0 && this.provider === provider) this.startListener(provider);
4566
4154
  return result;
4567
4155
  }
4568
4156
  try {
@@ -4572,10 +4160,9 @@ let AwikiService = (() => {
4572
4160
  await this.sessionStore.signIn();
4573
4161
  this.signedOut = false;
4574
4162
  this.activeIdentityDid = void 0;
4575
- this.pendingDeviceJoin = void 0;
4576
- this.activeDeviceJoinSessionId = void 0;
4577
4163
  this.invalidateSummaries();
4578
4164
  this.publishSession({ status: "unregistered" });
4165
+ if (provider !== void 0 && this.provider === provider) this.startListener(provider);
4579
4166
  return result;
4580
4167
  } catch {
4581
4168
  return {
@@ -4585,7 +4172,7 @@ let AwikiService = (() => {
4585
4172
  }
4586
4173
  });
4587
4174
  }
4588
- /** Re-enter only after Core confirms that the exact recovered identity is applied locally. */
4175
+ /** Re-enter after Core applies the recovered identity without blocking on optional account restoration. */
4589
4176
  async applyRecoveredSession(progress) {
4590
4177
  if (progress.phase !== "applied") return true;
4591
4178
  return this.mutateSession(async () => {
@@ -4601,7 +4188,6 @@ let AwikiService = (() => {
4601
4188
  this.activeIdentityDid = identity.did;
4602
4189
  this.invalidateSummaries();
4603
4190
  }
4604
- const reconciled = await this.reconcileRecoveredIdentity(provider, progress.operationId);
4605
4191
  if (this.provider !== provider) return false;
4606
4192
  if (!alreadyActive) {
4607
4193
  const session = {
@@ -4609,195 +4195,31 @@ let AwikiService = (() => {
4609
4195
  identity
4610
4196
  };
4611
4197
  this.publishSession(session);
4612
- this.ensureProviderRuntime(provider);
4198
+ await provider.listenerStartup;
4199
+ await this.startListener(provider);
4613
4200
  }
4614
- return reconciled;
4201
+ return true;
4615
4202
  } catch {
4616
4203
  return false;
4617
4204
  }
4618
4205
  });
4619
4206
  }
4620
- /** Rebind Mail first-use ownership and, when installed, the canonical model billing account. */
4621
- async reconcileRecoveredIdentity(provider, operationId) {
4622
- const logger = this.ctx.logger("awiki-recovery");
4623
- let mailboxRestored = false;
4624
- try {
4625
- await provider.client.getMailAccount();
4626
- mailboxRestored = true;
4627
- } catch {
4628
- logger.warn("awiki: recovered mailbox reconciliation is pending");
4629
- }
4630
- const target = this.recoveryReconciliationTarget;
4631
- if (target === void 0) return mailboxRestored;
4632
- try {
4633
- const authority = await provider.client.issueRecoveryAttestation({ operationId });
4634
- if (!await acceptsRecoveryReconciliation(await this.externalHttpAuth.dispatch(new Request(target.endpoint, {
4635
- method: "POST",
4636
- headers: { "content-type": "application/json" },
4637
- body: JSON.stringify({ attestation: authority.attestation })
4638
- }), (request) => fetch(request)))) {
4639
- logger.warn("awiki: recovered model account reconciliation is pending");
4640
- return false;
4641
- }
4642
- return mailboxRestored;
4643
- } catch {
4644
- logger.warn("awiki: recovered model account reconciliation is pending");
4645
- return false;
4646
- }
4647
- }
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. */
4207
+ /** Publish one newly registered identity and start its listener through the existing session path. */
4783
4208
  async activateRegisteredIdentity(identity) {
4784
- this.pendingDeviceJoin = void 0;
4785
- this.activeDeviceJoinSessionId = void 0;
4786
4209
  this.activeIdentityDid = identity.did;
4787
4210
  this.publishSession({
4788
4211
  status: "active",
4789
4212
  identity
4790
4213
  });
4791
4214
  const provider = this.provider;
4792
- if (provider !== void 0) this.ensureProviderRuntime(provider);
4215
+ if (provider !== void 0) {
4216
+ await provider.listenerStartup;
4217
+ await this.startListener(provider);
4218
+ }
4793
4219
  }
4794
4220
  /** Invalidate cached session work and cancel every model request still owned by the old session. */
4795
4221
  invalidateSummaries() {
4796
4222
  this.sessionRevision += 1;
4797
- this.requestRefs.clear();
4798
- this.requestSessions.clear();
4799
- this.deviceRefs.clear();
4800
- this.deviceIds.clear();
4801
4223
  for (const controller of this.activeSummaryRequests) controller.abort();
4802
4224
  this.activeSummaryRequests.clear();
4803
4225
  }
@@ -4897,148 +4319,120 @@ let AwikiService = (() => {
4897
4319
  this.sessionMutation = pending.then(() => void 0, () => void 0);
4898
4320
  return pending;
4899
4321
  }
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");
4932
- const observed = (async () => {
4933
- if (await this.isSignedOut()) return;
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);
4942
- })().catch((error) => {
4943
- logger.warn("AWiki realtime startup failed: %s", error instanceof Error ? error.message : "unknown failure");
4944
- }).finally(() => {
4945
- if (provider.realtimeStartup === observed) delete provider.realtimeStartup;
4946
- });
4947
- provider.realtimeStartup = observed;
4948
- }
4949
- realtimeFenceMatches(provider, generation) {
4950
- return this.provider === provider && provider.realtimeGeneration === generation;
4951
- }
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) {
4322
+ /** Start one exact identity-bound listener, atomically releasing a failed startup. */
4323
+ startListener(provider) {
4324
+ if (!this.resolved.listenerEnabled || provider.listener !== void 0 || provider.listenerRecoveryBlocked || this.provider !== provider) return Promise.resolve();
4325
+ if (provider.listenerStartup !== void 0) return provider.listenerStartup;
4326
+ if (provider.listenerCleanup !== void 0 || provider.listenerRestartTimer !== void 0) return Promise.resolve();
4972
4327
  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;
4328
+ const source = provider.client.listener;
4329
+ if (workspaceContext === void 0 || source === void 0) return Promise.resolve();
4330
+ const generation = provider.listenerGeneration;
4979
4331
  const logger = this.ctx.logger("awiki-listener");
4980
4332
  const observed = (async () => {
4981
- await this.detachAgentConsumer(provider);
4982
- if (!this.agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation)) return;
4333
+ if (await this.isSignedOut()) return;
4334
+ if (await provider.client.getIdentity() === null) return;
4335
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation)) return;
4983
4336
  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();
4337
+ const listener = new AwikiAgentListener(source, agents, this.resolved.listener, logger);
4338
+ provider.listener = listener;
4339
+ try {
4340
+ await listener.start();
4341
+ } catch (error) {
4342
+ if (provider.listener === listener) delete provider.listener;
4343
+ try {
4344
+ await listener.dispose();
4345
+ } catch {
4346
+ provider.listenerRecoveryBlocked = true;
4347
+ }
4348
+ if (!provider.listenerRecoveryBlocked && provider.listenerRestartAttempt > 0) this.scheduleListenerRestart(provider, workspaceContext, generation);
4349
+ throw error;
4350
+ }
4351
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation)) {
4352
+ if (provider.listener === listener) delete provider.listener;
4353
+ try {
4354
+ await listener.dispose();
4355
+ } catch (error) {
4356
+ provider.listenerRecoveryBlocked = true;
4357
+ throw error;
4358
+ }
4359
+ return;
4360
+ }
4361
+ provider.listenerStartedAt = Date.now();
4362
+ listener.whenTerminated().then((result) => {
4363
+ if (result.kind === "failed") this.releaseFailedListener(provider, listener, workspaceContext, generation, result.error);
4364
+ });
4991
4365
  })().catch((error) => {
4992
- logger.warn("AWiki Agent consumer startup failed: %s", error instanceof Error ? error.message : "unknown failure");
4366
+ logger.warn("AWiki listener startup failed: %s", error instanceof Error ? error.message : "unknown failure");
4993
4367
  }).finally(() => {
4994
- if (provider.agentConsumerStartup === observed) delete provider.agentConsumerStartup;
4368
+ if (provider.listenerStartup === observed) delete provider.listenerStartup;
4995
4369
  });
4996
- provider.agentConsumerStartup = observed;
4370
+ provider.listenerStartup = observed;
4371
+ return observed;
4372
+ }
4373
+ async stopListener(provider) {
4374
+ provider.listenerGeneration += 1;
4375
+ provider.listenerRestartAttempt = 0;
4376
+ delete provider.listenerStartedAt;
4377
+ const timer = provider.listenerRestartTimer;
4378
+ if (timer !== void 0) {
4379
+ delete provider.listenerRestartTimer;
4380
+ clearTimeout(timer);
4381
+ }
4382
+ await provider.listenerStartup;
4383
+ await provider.listenerCleanup;
4384
+ const listener = provider.listener;
4385
+ if (listener === void 0) return;
4386
+ delete provider.listener;
4387
+ try {
4388
+ await listener.dispose();
4389
+ } catch (error) {
4390
+ provider.listenerRecoveryBlocked = true;
4391
+ throw error;
4392
+ }
4997
4393
  }
4998
- agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation) {
4999
- return this.provider === provider && this.workspaceContext === workspaceContext && this.activeIdentityDid === identityDid && provider.agentConsumerGeneration === generation;
4394
+ listenerFenceMatches(provider, workspaceContext, generation) {
4395
+ return this.provider === provider && this.workspaceContext === workspaceContext && provider.listenerGeneration === generation;
5000
4396
  }
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;
4397
+ releaseFailedListener(provider, listener, workspaceContext, generation, error) {
4398
+ if (provider.listener !== listener) return;
4399
+ delete provider.listener;
4400
+ const startedAt = provider.listenerStartedAt;
4401
+ delete provider.listenerStartedAt;
4402
+ if (startedAt !== void 0 && Date.now() - startedAt >= LISTENER_STABLE_RESET_MS) provider.listenerRestartAttempt = 0;
4403
+ const logger = this.ctx.logger("awiki-listener");
4404
+ logger.warn("AWiki listener lifecycle failed: %s", error instanceof Error ? error.message : "unknown failure");
4405
+ let disposed = false;
4406
+ const observed = listener.dispose().then(() => {
4407
+ disposed = true;
4408
+ }, (cleanupError) => {
4409
+ provider.listenerRecoveryBlocked = true;
4410
+ logger.warn("AWiki listener cleanup failed: %s", cleanupError instanceof Error ? cleanupError.message : "unknown failure");
4411
+ }).finally(() => {
4412
+ if (provider.listenerCleanup === observed) delete provider.listenerCleanup;
4413
+ if (disposed) this.scheduleListenerRestart(provider, workspaceContext, generation);
5008
4414
  });
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)]);
4415
+ provider.listenerCleanup = observed;
4416
+ }
4417
+ scheduleListenerRestart(provider, workspaceContext, generation) {
4418
+ if (provider.listenerRestartTimer !== void 0 || !this.listenerFenceMatches(provider, workspaceContext, generation)) return;
4419
+ provider.listenerRestartAttempt = Math.min(provider.listenerRestartAttempt + 1, LISTENER_RESTART_MAX_ATTEMPT);
4420
+ const exponent = provider.listenerRestartAttempt - 1;
4421
+ const delay = Math.min(LISTENER_RESTART_BASE_DELAY_MS * 2 ** exponent, LISTENER_RESTART_MAX_DELAY_MS);
4422
+ const timer = setTimeout(() => {
4423
+ if (provider.listenerRestartTimer !== timer) return;
4424
+ delete provider.listenerRestartTimer;
4425
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation)) return;
4426
+ this.startListener(provider);
4427
+ }, delay);
4428
+ provider.listenerRestartTimer = timer;
5029
4429
  }
5030
4430
  /** Clear one exact provider slot before joining its one shared disposal. */
5031
4431
  disposeProvider(provider) {
5032
- if (this.provider === provider) {
5033
- this.provider = void 0;
5034
- this.pendingDeviceJoin = void 0;
5035
- this.activeDeviceJoinSessionId = void 0;
5036
- this.invalidateSummaries();
5037
- }
4432
+ if (this.provider === provider) this.provider = void 0;
5038
4433
  provider.disposal ??= (async () => {
5039
4434
  try {
5040
- await provider.runtimeReplacement;
5041
- await this.stopProviderRuntime(provider);
4435
+ await this.stopListener(provider);
5042
4436
  } finally {
5043
4437
  await provider.client.dispose();
5044
4438
  }