@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
@@ -34,7 +34,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
34
34
  done = true;
35
35
  };
36
36
  import { Context } from '@deepseek-ai/cordis';
37
- import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
37
+ import { existsSync } from 'node:fs';
38
38
  import { homedir } from 'node:os';
39
39
  import { isAbsolute, join } from 'node:path';
40
40
  import z from '@deepseek-ai/schemastery';
@@ -54,7 +54,7 @@ import { AwikiImageAttachmentCache, minimumImageAttachmentCacheMaxBytes } from "
54
54
  import { AwikiSentMailStore, isLocalSentMailId } from "./sent-mail-store.js";
55
55
  import { AwikiConversationPreferenceStore, normalizeConversationPreferenceMutation, } from "./conversation-preferences.js";
56
56
  import { AwikiAgentListener, DshAwikiListenerAgentRuntime, } from "./listener.js";
57
- import { IdentityRealtimeSupervisor, } from "./realtime-supervisor.js";
57
+ import { resolveAwikiProfileName, resolveAwikiStateRoot } from "./profile-state.js";
58
58
  export { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "./types.js";
59
59
  export { AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES, AwikiExternalHttpAuthError, } from "./external-http-auth.js";
60
60
  export { AWIKI_DOMAIN_FIELD, AWIKI_SETTINGS_NAMESPACE, AwikiSettingsSchema, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain, validateAwikiSettings, } from "./settings.js";
@@ -87,7 +87,6 @@ export const Config = z.object({
87
87
  attachmentMaxBytes: z.number().default(DEFAULT_ATTACHMENT_MAX_BYTES),
88
88
  imageAttachmentCacheMaxBytes: z.number().default(DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES),
89
89
  pollIntervalMs: z.number().default(DEFAULT_POLL_INTERVAL_MS),
90
- realtimeEnabled: z.boolean().default(true),
91
90
  listenerEnabled: z.boolean().default(false),
92
91
  listenerAllowedPeers: z.array(z.string()).default([]),
93
92
  listenerWorkspacePath: z.string(),
@@ -105,6 +104,7 @@ const FAILURE_CODES = new Set([
105
104
  'forbidden',
106
105
  'identity-recovery-required',
107
106
  'conflict',
107
+ 'state-in-use',
108
108
  'rate-limited',
109
109
  'group-membership-required',
110
110
  'group-identity-stale',
@@ -118,17 +118,10 @@ const FAILURE_CODES = new Set([
118
118
  'network',
119
119
  'remote',
120
120
  ]);
121
- const DEVICE_JOIN_APPROVAL_CONFIRMATION = 'APPROVE';
122
- const DEVICE_REVOKE_CONFIRMATION = 'REVOKE';
123
- const DEVICE_JOIN_CHALLENGE_TTL_SECONDS = 240;
124
- const RESUMABLE_JOIN_PHASES = new Set([
125
- 'pending',
126
- 'challenge_prepared',
127
- 'response_prepared',
128
- 'response_verified',
129
- 'approval_prepared',
130
- 'authorized',
131
- ]);
121
+ const LISTENER_RESTART_BASE_DELAY_MS = 1_000;
122
+ const LISTENER_RESTART_MAX_DELAY_MS = 30_000;
123
+ const LISTENER_RESTART_MAX_ATTEMPT = 6;
124
+ const LISTENER_STABLE_RESET_MS = 60_000;
132
125
  const FAILURE_MESSAGES = {
133
126
  'not-registered': 'No AWiki identity is registered for this deployment.',
134
127
  'signed-out': 'This installation is signed out of AWiki.',
@@ -141,6 +134,7 @@ const FAILURE_MESSAGES = {
141
134
  'forbidden': 'The AWiki operation is not permitted.',
142
135
  'identity-recovery-required': 'The local AWiki identity must be recovered before it can be used again.',
143
136
  'conflict': 'The AWiki operation conflicts with current state.',
137
+ 'state-in-use': 'AWiki data is already open in another client.',
144
138
  'rate-limited': 'The AWiki service rate-limited the request.',
145
139
  'group-membership-required': 'The active AWiki identity is not a member of this group.',
146
140
  'group-identity-stale': 'The AWiki group identity binding is still recovering.',
@@ -158,67 +152,6 @@ class ProviderUnavailableError extends Error {
158
152
  }
159
153
  class SummaryProviderUnavailableError extends Error {
160
154
  }
161
- function candidateJoinPhase(value) {
162
- if (value.remoteState === 'rejected')
163
- return 'rejected';
164
- if (value.remoteState === 'cancelled' || value.localPhase === 'cancelled')
165
- return 'cancelled';
166
- if (value.remoteState === 'expired' || value.localPhase === 'expired')
167
- return 'expired';
168
- if (value.localPhase === 'authorized'
169
- && value.remoteState === 'consumed'
170
- && value.completed
171
- && value.identity !== undefined)
172
- return 'authorized';
173
- if (value.sas !== undefined) {
174
- return value.remoteState === 'response_verified' && /^\d{6}$/u.test(value.sas)
175
- ? 'sas-ready'
176
- : undefined;
177
- }
178
- if (value.remoteState === 'challenge_sent'
179
- || value.remoteState === 'response_verified'
180
- || value.remoteState === 'consumed'
181
- || value.localPhase !== 'pending')
182
- return 'verifying';
183
- return value.remoteState === 'pending' ? 'pending' : undefined;
184
- }
185
- function adminJoinPhase(value) {
186
- if (value.remoteState === 'rejected')
187
- return 'rejected';
188
- if (value.remoteState === 'cancelled' || value.localPhase === 'cancelled')
189
- return 'cancelled';
190
- if (value.remoteState === 'expired' || value.localPhase === 'expired')
191
- return 'expired';
192
- if (value.localPhase === 'authorized' && value.remoteState === 'consumed')
193
- return 'authorized';
194
- if (value.sas !== undefined) {
195
- return value.remoteState === 'response_verified' && /^\d{6}$/u.test(value.sas)
196
- ? 'sas-ready'
197
- : undefined;
198
- }
199
- if (value.remoteState === 'challenge_sent'
200
- || value.remoteState === 'response_verified'
201
- || value.remoteState === 'consumed'
202
- || value.localPhase !== 'pending')
203
- return 'verifying';
204
- return value.remoteState === 'pending' ? 'pending' : undefined;
205
- }
206
- function requestJoinPhase(value) {
207
- switch (value.state) {
208
- case 'pending': return 'pending';
209
- case 'challenge_sent':
210
- case 'response_verified': return 'verifying';
211
- case 'consumed': return 'authorized';
212
- case 'cancelled': return 'cancelled';
213
- case 'rejected': return 'rejected';
214
- case 'expired': return 'expired';
215
- }
216
- }
217
- function constantTimeSasMatches(expected, actual) {
218
- if (!/^\d{6}$/u.test(expected) || !/^\d{6}$/u.test(actual))
219
- return false;
220
- return timingSafeEqual(Buffer.from(expected, 'ascii'), Buffer.from(actual, 'ascii'));
221
- }
222
155
  /** Validate and preserve one SDK service URL without accepting insecure remote HTTP. */
223
156
  function serviceUrl(field, raw, allowInsecureLoopbackForTesting) {
224
157
  let url;
@@ -249,30 +182,6 @@ function recoveryReconciliationEndpoint(target, allowInsecureLoopbackForTesting)
249
182
  }
250
183
  return new URL('/api/identity-recovery', parsed).toString();
251
184
  }
252
- /** Accept only the closed Model Proxy success response; no ledger identifier may cross back. */
253
- async function acceptsRecoveryReconciliation(response) {
254
- if (!response.ok)
255
- return false;
256
- let value;
257
- try {
258
- const text = await readBoundedResponseText(response, RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES);
259
- if (text === undefined)
260
- return false;
261
- value = JSON.parse(text);
262
- }
263
- catch {
264
- return false;
265
- }
266
- if (typeof value !== 'object' || value === null || Array.isArray(value))
267
- return false;
268
- const result = value;
269
- const keys = Object.keys(result).sort();
270
- return keys.length === 2
271
- && keys[0] === 'idempotent'
272
- && keys[1] === 'restored'
273
- && result.restored === true
274
- && typeof result.idempotent === 'boolean';
275
- }
276
185
  /** Validate a provider domain without inferring it from an API endpoint. */
277
186
  function serviceDomain(raw, field = 'userServiceDomain') {
278
187
  return normalizeAwikiDomain(raw, field);
@@ -320,16 +229,21 @@ function listenerAllowedPeers(raw, enabled) {
320
229
  return peers;
321
230
  }
322
231
  /** Resolve and validate every deployment choice before publishing the service. */
323
- function resolveConfig(config) {
232
+ function resolveConfig(ctx, config) {
324
233
  const allowInsecureLoopbackForTesting = config.allowInsecureLoopbackForTesting ?? false;
325
234
  const configuredStateRoot = config.stateRoot?.trim();
326
235
  const configuredDshHome = process.env.DSH_HOME?.trim();
327
236
  const dshHome = configuredDshHome === undefined || configuredDshHome.length === 0
328
237
  ? join(homedir(), '.dsh')
329
238
  : configuredDshHome;
239
+ const profileName = configuredStateRoot === undefined || configuredStateRoot.length === 0
240
+ ? resolveAwikiProfileName(ctx, dshHome)
241
+ : undefined;
330
242
  const stateRoot = configuredStateRoot === undefined || configuredStateRoot.length === 0
331
- ? join(dshHome, 'awiki', 'im-core')
243
+ ? resolveAwikiStateRoot(ctx, dshHome)
332
244
  : configuredStateRoot;
245
+ const legacySharedStateDetected = profileName !== undefined
246
+ && existsSync(join(dshHome, 'awiki', 'im-core'));
333
247
  if (!isAbsolute(stateRoot))
334
248
  throw new TypeError('awiki: stateRoot must be an absolute path');
335
249
  const attachmentMaxBytes = config.attachmentMaxBytes ?? DEFAULT_ATTACHMENT_MAX_BYTES;
@@ -346,10 +260,6 @@ function resolveConfig(config) {
346
260
  throw new TypeError('awiki: pollIntervalMs must be a safe integer from 1000 through 60000');
347
261
  }
348
262
  const listenerEnabled = config.listenerEnabled ?? false;
349
- const realtimeEnabled = config.realtimeEnabled ?? true;
350
- if (listenerEnabled && !realtimeEnabled) {
351
- throw new TypeError('awiki: listenerEnabled requires realtimeEnabled');
352
- }
353
263
  const listenerWorkspacePath = config.listenerWorkspacePath?.trim() || join(dshHome, 'workspaces', 'awiki');
354
264
  if (!isAbsolute(listenerWorkspacePath)) {
355
265
  throw new TypeError('awiki: listenerWorkspacePath must be an absolute path');
@@ -376,7 +286,7 @@ function resolveConfig(config) {
376
286
  attachmentMaxBytes,
377
287
  imageAttachmentCacheMaxBytes,
378
288
  pollIntervalMs,
379
- realtimeEnabled,
289
+ ...profileName === undefined ? {} : { profileName, legacySharedStateDetected },
380
290
  listenerEnabled,
381
291
  listener: {
382
292
  allowedPeers,
@@ -537,8 +447,6 @@ function normalizeRecoveryPrepareRequest(request) {
537
447
  return { ...operation, phone, otp };
538
448
  }
539
449
  const IDENTITY_ACCESS_RESPONSE_MAX_BYTES = 64 * 1024;
540
- const SERVER_INFO_RESPONSE_MAX_BYTES = 64 * 1024;
541
- const RECOVERY_RECONCILIATION_RESPONSE_MAX_BYTES = 4 * 1024;
542
450
  /** Read one untrusted discovery response without buffering beyond the fixed Host limit. */
543
451
  async function readBoundedResponseText(response, maxBytes) {
544
452
  const declaredLength = response.headers.get('content-length');
@@ -767,14 +675,6 @@ let AwikiService = (() => {
767
675
  let _inspectIdentityAccess_decorators;
768
676
  let _sendRegistrationOtp_decorators;
769
677
  let _registerIdentity_decorators;
770
- let _beginDeviceJoin_decorators;
771
- let _getDeviceJoinStatus_decorators;
772
- let _cancelDeviceJoin_decorators;
773
- let _refreshDeviceManagement_decorators;
774
- let _startDeviceJoinVerification_decorators;
775
- let _approveDeviceJoin_decorators;
776
- let _rejectDeviceJoin_decorators;
777
- let _revokeDevice_decorators;
778
678
  let _updateDisplayName_decorators;
779
679
  let _getProfile_decorators;
780
680
  let _updateProfile_decorators;
@@ -820,14 +720,6 @@ let AwikiService = (() => {
820
720
  _inspectIdentityAccess_decorators = [Remote];
821
721
  _sendRegistrationOtp_decorators = [Remote];
822
722
  _registerIdentity_decorators = [Remote];
823
- _beginDeviceJoin_decorators = [Remote];
824
- _getDeviceJoinStatus_decorators = [Remote];
825
- _cancelDeviceJoin_decorators = [Remote];
826
- _refreshDeviceManagement_decorators = [Remote];
827
- _startDeviceJoinVerification_decorators = [Remote];
828
- _approveDeviceJoin_decorators = [Remote];
829
- _rejectDeviceJoin_decorators = [Remote];
830
- _revokeDevice_decorators = [Remote];
831
723
  _updateDisplayName_decorators = [Remote];
832
724
  _getProfile_decorators = [Remote];
833
725
  _updateProfile_decorators = [Remote];
@@ -870,14 +762,6 @@ let AwikiService = (() => {
870
762
  __esDecorate(this, null, _inspectIdentityAccess_decorators, { kind: "method", name: "inspectIdentityAccess", static: false, private: false, access: { has: obj => "inspectIdentityAccess" in obj, get: obj => obj.inspectIdentityAccess }, metadata: _metadata }, null, _instanceExtraInitializers);
871
763
  __esDecorate(this, null, _sendRegistrationOtp_decorators, { kind: "method", name: "sendRegistrationOtp", static: false, private: false, access: { has: obj => "sendRegistrationOtp" in obj, get: obj => obj.sendRegistrationOtp }, metadata: _metadata }, null, _instanceExtraInitializers);
872
764
  __esDecorate(this, null, _registerIdentity_decorators, { kind: "method", name: "registerIdentity", static: false, private: false, access: { has: obj => "registerIdentity" in obj, get: obj => obj.registerIdentity }, metadata: _metadata }, null, _instanceExtraInitializers);
873
- __esDecorate(this, null, _beginDeviceJoin_decorators, { kind: "method", name: "beginDeviceJoin", static: false, private: false, access: { has: obj => "beginDeviceJoin" in obj, get: obj => obj.beginDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
874
- __esDecorate(this, null, _getDeviceJoinStatus_decorators, { kind: "method", name: "getDeviceJoinStatus", static: false, private: false, access: { has: obj => "getDeviceJoinStatus" in obj, get: obj => obj.getDeviceJoinStatus }, metadata: _metadata }, null, _instanceExtraInitializers);
875
- __esDecorate(this, null, _cancelDeviceJoin_decorators, { kind: "method", name: "cancelDeviceJoin", static: false, private: false, access: { has: obj => "cancelDeviceJoin" in obj, get: obj => obj.cancelDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
876
- __esDecorate(this, null, _refreshDeviceManagement_decorators, { kind: "method", name: "refreshDeviceManagement", static: false, private: false, access: { has: obj => "refreshDeviceManagement" in obj, get: obj => obj.refreshDeviceManagement }, metadata: _metadata }, null, _instanceExtraInitializers);
877
- __esDecorate(this, null, _startDeviceJoinVerification_decorators, { kind: "method", name: "startDeviceJoinVerification", static: false, private: false, access: { has: obj => "startDeviceJoinVerification" in obj, get: obj => obj.startDeviceJoinVerification }, metadata: _metadata }, null, _instanceExtraInitializers);
878
- __esDecorate(this, null, _approveDeviceJoin_decorators, { kind: "method", name: "approveDeviceJoin", static: false, private: false, access: { has: obj => "approveDeviceJoin" in obj, get: obj => obj.approveDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
879
- __esDecorate(this, null, _rejectDeviceJoin_decorators, { kind: "method", name: "rejectDeviceJoin", static: false, private: false, access: { has: obj => "rejectDeviceJoin" in obj, get: obj => obj.rejectDeviceJoin }, metadata: _metadata }, null, _instanceExtraInitializers);
880
- __esDecorate(this, null, _revokeDevice_decorators, { kind: "method", name: "revokeDevice", static: false, private: false, access: { has: obj => "revokeDevice" in obj, get: obj => obj.revokeDevice }, metadata: _metadata }, null, _instanceExtraInitializers);
881
765
  __esDecorate(this, null, _updateDisplayName_decorators, { kind: "method", name: "updateDisplayName", static: false, private: false, access: { has: obj => "updateDisplayName" in obj, get: obj => obj.updateDisplayName }, metadata: _metadata }, null, _instanceExtraInitializers);
882
766
  __esDecorate(this, null, _getProfile_decorators, { kind: "method", name: "getProfile", static: false, private: false, access: { has: obj => "getProfile" in obj, get: obj => obj.getProfile }, metadata: _metadata }, null, _instanceExtraInitializers);
883
767
  __esDecorate(this, null, _updateProfile_decorators, { kind: "method", name: "updateProfile", static: false, private: false, access: { has: obj => "updateProfile" in obj, get: obj => obj.updateProfile }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -928,12 +812,6 @@ let AwikiService = (() => {
928
812
  sessionMutation = Promise.resolve();
929
813
  sessionRevision = 0;
930
814
  activeIdentityDid;
931
- pendingDeviceJoin;
932
- activeDeviceJoinSessionId;
933
- requestRefs = new Map();
934
- requestSessions = new Map();
935
- deviceRefs = new Map();
936
- deviceIds = new Map();
937
815
  activeSummaryRequests = new Set();
938
816
  summaryProvider;
939
817
  recoveryReconciliationTarget;
@@ -948,7 +826,7 @@ let AwikiService = (() => {
948
826
  constructor(ctx, config) {
949
827
  super(ctx, 'awiki');
950
828
  this.hostContext = ctx;
951
- this.resolved = resolveConfig(config);
829
+ this.resolved = resolveConfig(ctx, config);
952
830
  this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
953
831
  this.sessionStore = new AwikiSessionStore(this.resolved.stateRoot);
954
832
  this.imageAttachmentCache = new AwikiImageAttachmentCache(this.resolved.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
@@ -978,14 +856,14 @@ let AwikiService = (() => {
978
856
  this.workspaceContext = workspaceCtx;
979
857
  const provider = this.provider;
980
858
  if (provider !== undefined)
981
- this.ensureAgentConsumer(provider);
859
+ void this.startListener(provider);
982
860
  workspaceCtx.effect(() => async () => {
983
861
  if (this.workspaceContext !== workspaceCtx)
984
862
  return;
985
863
  this.workspaceContext = undefined;
986
864
  const current = this.provider;
987
865
  if (current !== undefined)
988
- await this.stopAgentConsumer(current);
866
+ await this.stopListener(current);
989
867
  }, 'awiki: release Workspace listener composition');
990
868
  });
991
869
  registerAwikiTools(ctx, this);
@@ -1008,8 +886,6 @@ let AwikiService = (() => {
1008
886
  registerClientFactory(factory) {
1009
887
  if (this.provider !== undefined)
1010
888
  throw new Error('awiki: a client provider is already registered');
1011
- this.pendingDeviceJoin = undefined;
1012
- this.activeDeviceJoinSessionId = undefined;
1013
889
  const client = factory({
1014
890
  userServiceUrl: this.resolved.userServiceUrl,
1015
891
  userServiceDomain: this.startupUserServiceDomain,
@@ -1024,29 +900,14 @@ let AwikiService = (() => {
1024
900
  });
1025
901
  const provider = {
1026
902
  client,
1027
- realtimeGeneration: 0,
1028
- agentConsumerGeneration: 0,
1029
- localDeviceJoinRequestCountAfterSync: 0,
903
+ listenerRestartAttempt: 0,
904
+ listenerGeneration: 0,
905
+ listenerRecoveryBlocked: false,
1030
906
  };
1031
907
  this.provider = provider;
1032
- this.ensureRealtimeSupervisor(provider);
1033
- this.ensureAgentConsumer(provider);
908
+ void this.startListener(provider);
1034
909
  return () => this.disposeProvider(provider);
1035
910
  }
1036
- /** Safe same-process diagnostics for focused E2E. Never exposed through Typert Remote. */
1037
- getRealtimeDiagnostics() {
1038
- const provider = this.provider;
1039
- const realtime = provider?.realtimeSupervisor?.diagnostics() ?? {
1040
- connected: false,
1041
- activeSessionCount: 0,
1042
- startCount: 0,
1043
- stopCount: 0,
1044
- };
1045
- return {
1046
- ...realtime,
1047
- localDeviceJoinRequestCountAfterSync: provider?.localDeviceJoinRequestCountAfterSync ?? 0,
1048
- };
1049
- }
1050
911
  /** Register the optional Model Proxy recovery target without exposing an arbitrary callback or token. */
1051
912
  registerRecoveryReconciliationTarget(target) {
1052
913
  if (this.recoveryReconciliationTarget !== undefined) {
@@ -1084,43 +945,18 @@ let AwikiService = (() => {
1084
945
  * Read settings needed by the browser presentation.
1085
946
  * @returns Browser-safe polling configuration without SDK endpoints or state paths.
1086
947
  */
1087
- async getConfig() {
1088
- let handleRecoveryPhoneEnabled = false;
1089
- const abort = new AbortController();
1090
- const timeout = setTimeout(() => { abort.abort(); }, 10_000);
1091
- try {
1092
- const response = await fetch(new URL('/user-service/v1/server-info', this.resolved.userServiceUrl), {
1093
- method: 'GET',
1094
- headers: { accept: 'application/json', 'cache-control': 'no-store' },
1095
- cache: 'no-store',
1096
- redirect: 'error',
1097
- signal: abort.signal,
1098
- });
1099
- if (response.ok) {
1100
- const text = await readBoundedResponseText(response, SERVER_INFO_RESPONSE_MAX_BYTES);
1101
- const value = text === undefined ? undefined : JSON.parse(text);
1102
- if (typeof value === 'object' && value !== null && value.schema_version === 1) {
1103
- const methods = value.identity?.handle_recovery?.methods;
1104
- handleRecoveryPhoneEnabled = Array.isArray(methods) && methods.some((method) => (typeof method === 'object' && method !== null
1105
- && method.id === 'phone'
1106
- && method.enabled === true
1107
- && method.verification?.required === true
1108
- && method.verification?.type === 'sms_otp'));
1109
- }
1110
- }
1111
- }
1112
- catch { }
1113
- finally {
1114
- clearTimeout(timeout);
1115
- }
1116
- return {
948
+ getConfig() {
949
+ return Promise.resolve({
1117
950
  ok: true,
1118
951
  value: {
1119
952
  pollIntervalMs: this.resolved.pollIntervalMs,
1120
953
  attachmentMaxBytes: this.resolved.attachmentMaxBytes,
1121
- handleRecoveryPhoneEnabled,
954
+ ...this.resolved.profileName === undefined ? {} : {
955
+ profileName: this.resolved.profileName,
956
+ legacySharedStateDetected: this.resolved.legacySharedStateDetected,
957
+ },
1122
958
  },
1123
- };
959
+ });
1124
960
  }
1125
961
  /**
1126
962
  * Read the deployment's identity status.
@@ -1142,9 +978,6 @@ let AwikiService = (() => {
1142
978
  if (!identity.ok)
1143
979
  return identity;
1144
980
  this.activeIdentityDid = identity.value?.did;
1145
- const provider = this.provider;
1146
- if (identity.value !== null && provider !== undefined)
1147
- this.ensureProviderRuntime(provider);
1148
981
  return identity.value === null
1149
982
  ? { ok: true, value: { status: 'unregistered' } }
1150
983
  : { ok: true, value: { status: 'active', identity: identity.value } };
@@ -1171,7 +1004,7 @@ let AwikiService = (() => {
1171
1004
  this.publishSession(session);
1172
1005
  const provider = this.provider;
1173
1006
  if (provider !== undefined)
1174
- await this.stopProviderRuntime(provider);
1007
+ await this.stopListener(provider);
1175
1008
  return { ok: true, value: session };
1176
1009
  }
1177
1010
  catch {
@@ -1196,7 +1029,7 @@ let AwikiService = (() => {
1196
1029
  this.publishSession(session);
1197
1030
  const provider = this.provider;
1198
1031
  if (provider !== undefined)
1199
- this.ensureProviderRuntime(provider);
1032
+ void this.startListener(provider);
1200
1033
  return { ok: true, value: session };
1201
1034
  }
1202
1035
  catch {
@@ -1254,14 +1087,8 @@ let AwikiService = (() => {
1254
1087
  return { ok: false, error: failure('remote') };
1255
1088
  }
1256
1089
  }
1257
- async sendRegistrationOtp(request) {
1258
- this.pendingDeviceJoin = undefined;
1259
- return this.run(async (client) => {
1260
- if (await this.selectDeviceJoinSession(client) !== null) {
1261
- throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
1262
- }
1263
- return client.sendRegistrationOtp(request);
1264
- });
1090
+ sendRegistrationOtp(request) {
1091
+ return this.run(client => client.sendRegistrationOtp(request));
1265
1092
  }
1266
1093
  /**
1267
1094
  * Register and persist the deployment's only AWiki identity.
@@ -1269,177 +1096,10 @@ let AwikiService = (() => {
1269
1096
  * @returns The new public identity or a closed failure.
1270
1097
  */
1271
1098
  async registerIdentity(request) {
1272
- this.pendingDeviceJoin = undefined;
1273
- const result = await this.run(async (client) => {
1274
- if (await this.selectDeviceJoinSession(client) !== null) {
1275
- throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
1276
- }
1277
- return client.registerIdentity(request);
1278
- });
1279
- if (!result.ok)
1280
- return result;
1281
- if (result.value.status === 'registered') {
1282
- await this.activateRegisteredIdentity(result.value.identity);
1283
- return { ok: true, value: { status: 'registered', identity: result.value.identity } };
1284
- }
1285
- this.pendingDeviceJoin = {
1286
- continuationId: result.value.continuationId,
1287
- fullHandle: result.value.fullHandle,
1288
- mode: result.value.mode,
1289
- requiresUserPresence: result.value.requiresUserPresence,
1290
- };
1291
- return {
1292
- ok: true,
1293
- value: {
1294
- status: 'join-required',
1295
- fullHandle: result.value.fullHandle,
1296
- mode: result.value.mode,
1297
- requiresUserPresence: result.value.requiresUserPresence,
1298
- },
1299
- };
1300
- }
1301
- /** Consume the exact in-memory continuation; ordinary Join never claims rebind user presence. */
1302
- async beginDeviceJoin() {
1303
- const continuation = this.pendingDeviceJoin;
1304
- if (continuation === undefined)
1305
- return { ok: false, error: failure('conflict') };
1306
- if (continuation.mode !== 'ordinary' || continuation.requiresUserPresence) {
1307
- return { ok: false, error: failure('forbidden') };
1308
- }
1309
- this.pendingDeviceJoin = undefined;
1310
- const result = await this.run(client => client.beginDeviceJoin({
1311
- continuationId: continuation.continuationId,
1312
- operationId: `dsh-device-join-${randomUUID()}`,
1313
- userPresenceConfirmed: false,
1314
- }));
1315
- if (!result.ok)
1316
- return result;
1317
- this.activeDeviceJoinSessionId = result.value.joinSessionId;
1318
- return this.applyCandidateJoinProgress(result.value);
1319
- }
1320
- /** Restore from Core local_sessions and advance only the exact resumable Join. */
1321
- async getDeviceJoinStatus() {
1322
- const result = await this.run(async (client) => {
1323
- const joinSessionId = await this.selectDeviceJoinSession(client);
1324
- return joinSessionId === null ? null : client.getDeviceJoinStatus(joinSessionId);
1325
- });
1326
- if (!result.ok)
1327
- return result;
1328
- if (result.value === null)
1329
- return { ok: true, value: null };
1330
- this.activeDeviceJoinSessionId = result.value.joinSessionId;
1331
- return this.applyCandidateJoinProgress(result.value);
1332
- }
1333
- async cancelDeviceJoin() {
1334
- const result = await this.run(async (client) => {
1335
- const joinSessionId = await this.selectDeviceJoinSession(client);
1336
- if (joinSessionId === null)
1337
- return null;
1338
- return client.cancelDeviceJoin(joinSessionId);
1339
- });
1340
- if (!result.ok)
1341
- return result;
1342
- this.pendingDeviceJoin = undefined;
1343
- this.activeDeviceJoinSessionId = undefined;
1344
- return { ok: true, value: { completed: true } };
1345
- }
1346
- /** Reliable-sync and project only Host-opaque device/request references. */
1347
- refreshDeviceManagement() {
1348
- return this.run(client => this.deviceManagementSnapshot(client));
1349
- }
1350
- async startDeviceJoinVerification(request) {
1351
- const joinSessionId = this.requestSessions.get(request?.requestRef);
1352
- if (joinSessionId === undefined)
1353
- return { ok: false, error: failure('invalid-request') };
1354
- const result = await this.run(async (client) => {
1355
- await this.requireDeviceManager(client);
1356
- await client.syncDeviceManagement();
1357
- let notice = (await client.listLocalDeviceJoinRequests())
1358
- .find(value => value.joinSessionId === joinSessionId);
1359
- if (notice === undefined) {
1360
- throw Object.assign(new Error('request not found'), { name: 'AwikiSdkError', code: 'not-found' });
1361
- }
1362
- const localProgress = await this.localAdminJoinProgress(client, notice);
1363
- if (localProgress !== undefined)
1364
- return localProgress;
1365
- if (!notice.canStartVerification) {
1366
- throw Object.assign(new Error('request unavailable'), { name: 'AwikiSdkError', code: 'forbidden' });
1367
- }
1368
- const operationId = `dsh-device-verify-${createHash('sha256').update(joinSessionId).digest('hex').slice(0, 32)}`;
1369
- let started;
1370
- try {
1371
- started = await client.startDeviceJoinVerification({
1372
- joinSessionId,
1373
- operationId,
1374
- challengeTtlSeconds: DEVICE_JOIN_CHALLENGE_TTL_SECONDS,
1375
- });
1376
- }
1377
- catch (error) {
1378
- await client.syncDeviceManagement();
1379
- notice = (await client.listLocalDeviceJoinRequests())
1380
- .find(value => value.joinSessionId === joinSessionId);
1381
- const recovered = notice === undefined ? undefined : await this.localAdminJoinProgress(client, notice);
1382
- if (recovered !== undefined)
1383
- return recovered;
1384
- throw error;
1385
- }
1386
- await client.syncDeviceManagement();
1387
- notice = (await client.listLocalDeviceJoinRequests())
1388
- .find(value => value.joinSessionId === joinSessionId);
1389
- if (notice === undefined)
1390
- return started;
1391
- return (await this.localAdminJoinProgress(client, notice)) ?? started;
1392
- });
1393
- return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
1394
- }
1395
- async approveDeviceJoin(request) {
1396
- if (request?.confirmation !== DEVICE_JOIN_APPROVAL_CONFIRMATION) {
1397
- return { ok: false, error: failure('invalid-request') };
1398
- }
1399
- const joinSessionId = this.requestSessions.get(request.requestRef);
1400
- if (joinSessionId === undefined || !/^\d{6}$/u.test(request.enteredSas)) {
1401
- return { ok: false, error: failure('invalid-request') };
1402
- }
1403
- const result = await this.run(async (client) => {
1404
- await this.requireDeviceManager(client);
1405
- const progress = await client.getLocalDeviceJoinVerificationProgress(joinSessionId);
1406
- if (progress.sas === undefined || !constantTimeSasMatches(progress.sas, request.enteredSas)) {
1407
- throw Object.assign(new Error('sas mismatch'), { name: 'AwikiSdkError', code: 'forbidden' });
1408
- }
1409
- const prompt = await client.prepareDeviceJoinApproval(joinSessionId);
1410
- return client.confirmDeviceJoinApproval(prompt.approvalHandle);
1411
- });
1412
- return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
1413
- }
1414
- async rejectDeviceJoin(request) {
1415
- const joinSessionId = this.requestSessions.get(request?.requestRef);
1416
- if (joinSessionId === undefined
1417
- || (request.reason !== 'user_rejected' && request.reason !== 'sas_mismatch')) {
1418
- return { ok: false, error: failure('invalid-request') };
1419
- }
1420
- const result = await this.run(async (client) => {
1421
- await this.requireDeviceManager(client);
1422
- return client.rejectDeviceJoin(joinSessionId, request.reason);
1423
- });
1424
- return result.ok ? this.publicAdminJoinProgress(request.requestRef, result.value) : result;
1425
- }
1426
- async revokeDevice(request) {
1427
- if (request?.confirmation !== DEVICE_REVOKE_CONFIRMATION) {
1428
- return { ok: false, error: failure('invalid-request') };
1429
- }
1430
- const deviceId = this.deviceIds.get(request.deviceRef);
1431
- if (deviceId === undefined)
1432
- return { ok: false, error: failure('invalid-request') };
1433
- return this.run(async (client) => {
1434
- await this.requireDeviceManager(client);
1435
- const devices = await client.getDeviceRegistry();
1436
- const target = devices.find(device => device.deviceId === deviceId);
1437
- if (target === undefined || target.isCurrent) {
1438
- throw Object.assign(new Error('device unavailable'), { name: 'AwikiSdkError', code: 'forbidden' });
1439
- }
1440
- await client.revokeDevice(deviceId);
1441
- return this.deviceManagementSnapshot(client);
1442
- });
1099
+ const result = await this.run(client => client.registerIdentity(request));
1100
+ if (result.ok)
1101
+ await this.activateRegisteredIdentity(result.value);
1102
+ return result;
1443
1103
  }
1444
1104
  /**
1445
1105
  * Update the deployment identity's public WNS display name.
@@ -1465,16 +1125,10 @@ let AwikiService = (() => {
1465
1125
  }
1466
1126
  /** Start durable phone recovery for one existing full Handle. */
1467
1127
  sendRecoveryOtp(request) {
1468
- this.pendingDeviceJoin = undefined;
1469
1128
  const normalized = normalizeRecoveryOtpRequest(request);
1470
1129
  if (normalized === undefined)
1471
1130
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
1472
- return this.run(async (client) => {
1473
- if (await this.selectDeviceJoinSession(client) !== null) {
1474
- throw Object.assign(new Error('join already exists'), { name: 'AwikiSdkError', code: 'conflict' });
1475
- }
1476
- return client.sendRecoveryOtp(normalized);
1477
- }, { allowSignedOut: true });
1131
+ return this.run(client => client.sendRecoveryOtp(normalized), { allowSignedOut: true });
1478
1132
  }
1479
1133
  /** Verify a recovery OTP and freeze its exact intent before the remote commit. */
1480
1134
  prepareRecovery(request) {
@@ -1921,11 +1575,11 @@ let AwikiService = (() => {
1921
1575
  return this.mutateSession(async () => {
1922
1576
  const provider = this.provider;
1923
1577
  if (provider !== undefined)
1924
- await this.stopProviderRuntime(provider);
1578
+ await this.stopListener(provider);
1925
1579
  const result = await this.run(client => client.clearLocalData(), { allowSignedOut: true });
1926
1580
  if (!result.ok) {
1927
1581
  if (provider !== undefined && this.provider === provider)
1928
- this.ensureProviderRuntime(provider);
1582
+ void this.startListener(provider);
1929
1583
  return result;
1930
1584
  }
1931
1585
  try {
@@ -1935,10 +1589,10 @@ let AwikiService = (() => {
1935
1589
  await this.sessionStore.signIn();
1936
1590
  this.signedOut = false;
1937
1591
  this.activeIdentityDid = undefined;
1938
- this.pendingDeviceJoin = undefined;
1939
- this.activeDeviceJoinSessionId = undefined;
1940
1592
  this.invalidateSummaries();
1941
1593
  this.publishSession({ status: 'unregistered' });
1594
+ if (provider !== undefined && this.provider === provider)
1595
+ void this.startListener(provider);
1942
1596
  return result;
1943
1597
  }
1944
1598
  catch {
@@ -1946,7 +1600,7 @@ let AwikiService = (() => {
1946
1600
  }
1947
1601
  });
1948
1602
  }
1949
- /** Re-enter only after Core confirms that the exact recovered identity is applied locally. */
1603
+ /** Re-enter after Core applies the recovered identity without blocking on optional account restoration. */
1950
1604
  async applyRecoveredSession(progress) {
1951
1605
  if (progress.phase !== 'applied')
1952
1606
  return true;
@@ -1965,211 +1619,34 @@ let AwikiService = (() => {
1965
1619
  this.activeIdentityDid = identity.did;
1966
1620
  this.invalidateSummaries();
1967
1621
  }
1968
- const reconciled = await this.reconcileRecoveredIdentity(provider, progress.operationId);
1969
1622
  if (this.provider !== provider)
1970
1623
  return false;
1971
1624
  if (!alreadyActive) {
1972
1625
  const session = { status: 'active', identity };
1973
1626
  this.publishSession(session);
1974
- this.ensureProviderRuntime(provider);
1627
+ await provider.listenerStartup;
1628
+ await this.startListener(provider);
1975
1629
  }
1976
- return reconciled;
1630
+ return true;
1977
1631
  }
1978
1632
  catch {
1979
1633
  return false;
1980
1634
  }
1981
1635
  });
1982
1636
  }
1983
- /** Rebind Mail first-use ownership and, when installed, the canonical model billing account. */
1984
- async reconcileRecoveredIdentity(provider, operationId) {
1985
- const logger = this.ctx.logger('awiki-recovery');
1986
- let mailboxRestored = false;
1987
- try {
1988
- await provider.client.getMailAccount();
1989
- mailboxRestored = true;
1990
- }
1991
- catch {
1992
- logger.warn('awiki: recovered mailbox reconciliation is pending');
1993
- }
1994
- const target = this.recoveryReconciliationTarget;
1995
- if (target === undefined)
1996
- return mailboxRestored;
1997
- try {
1998
- const authority = await provider.client.issueRecoveryAttestation({ operationId });
1999
- const response = await this.externalHttpAuth.dispatch(new Request(target.endpoint, {
2000
- method: 'POST',
2001
- headers: { 'content-type': 'application/json' },
2002
- body: JSON.stringify({ attestation: authority.attestation }),
2003
- }), request => fetch(request));
2004
- if (!await acceptsRecoveryReconciliation(response)) {
2005
- logger.warn('awiki: recovered model account reconciliation is pending');
2006
- return false;
2007
- }
2008
- return mailboxRestored;
2009
- }
2010
- catch {
2011
- logger.warn('awiki: recovered model account reconciliation is pending');
2012
- return false;
2013
- }
2014
- }
2015
- /** Select the only resumable new-device session; Core local_sessions is the sole restart SoT. */
2016
- async selectDeviceJoinSession(client) {
2017
- const sessions = await client.listLocalDeviceJoinSessions();
2018
- if (this.activeDeviceJoinSessionId !== undefined) {
2019
- const tracked = sessions.find(value => (value.side === 'new_device' && value.joinSessionId === this.activeDeviceJoinSessionId));
2020
- if (tracked !== undefined)
2021
- return tracked.joinSessionId;
2022
- this.activeDeviceJoinSessionId = undefined;
2023
- }
2024
- const resumable = sessions.filter(value => (value.side === 'new_device' && RESUMABLE_JOIN_PHASES.has(value.localPhase)));
2025
- if (resumable.length > 1) {
2026
- throw Object.assign(new Error('multiple local joins'), { name: 'AwikiSdkError', code: 'conflict' });
2027
- }
2028
- const selected = resumable[0];
2029
- this.activeDeviceJoinSessionId = selected?.joinSessionId;
2030
- return selected?.joinSessionId ?? null;
2031
- }
2032
- async applyCandidateJoinProgress(value) {
2033
- const phase = candidateJoinPhase(value);
2034
- if (phase === undefined)
2035
- return { ok: false, error: failure('remote') };
2036
- if (phase === 'cancelled' || phase === 'rejected' || phase === 'expired') {
2037
- this.activeDeviceJoinSessionId = undefined;
2038
- }
2039
- if (phase === 'authorized') {
2040
- if (value.identity === undefined)
2041
- return { ok: false, error: failure('remote') };
2042
- await this.activateRegisteredIdentity(value.identity);
2043
- }
2044
- return {
2045
- ok: true,
2046
- value: {
2047
- phase,
2048
- expiresAt: value.expiresAt,
2049
- ...(phase === 'sas-ready' && value.sas !== undefined ? { sas: value.sas } : {}),
2050
- completed: phase === 'authorized',
2051
- },
2052
- };
2053
- }
2054
- publicAdminJoinProgress(requestRef, value) {
2055
- const phase = adminJoinPhase(value);
2056
- if (phase === undefined)
2057
- return { ok: false, error: failure('remote') };
2058
- return {
2059
- ok: true,
2060
- value: {
2061
- requestRef,
2062
- phase,
2063
- expiresAt: value.expiresAt,
2064
- ...(phase === 'sas-ready' && value.sas !== undefined ? { sas: value.sas } : {}),
2065
- },
2066
- };
2067
- }
2068
- async localAdminJoinProgress(client, notice) {
2069
- const local = (await client.listLocalDeviceJoinSessions())
2070
- .find(session => session.side === 'admin' && session.joinSessionId === notice.joinSessionId);
2071
- if (local === undefined) {
2072
- return notice.claimedByCurrentDevice
2073
- ? client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId)
2074
- : undefined;
2075
- }
2076
- if (local.localPhase === 'challenge_prepared') {
2077
- return {
2078
- joinSessionId: notice.joinSessionId,
2079
- localPhase: local.localPhase,
2080
- remoteState: notice.state,
2081
- expiresAt: local.expiresAt,
2082
- };
2083
- }
2084
- return client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId);
2085
- }
2086
- async requireDeviceManager(client) {
2087
- const current = await client.getCurrentDeviceSummary();
2088
- if (!current.canManage || current.role !== 'admin' || current.readiness !== 'admin_ready') {
2089
- throw Object.assign(new Error('device manager required'), { name: 'AwikiSdkError', code: 'forbidden' });
2090
- }
2091
- }
2092
- requestRef(joinSessionId) {
2093
- const existing = this.requestRefs.get(joinSessionId);
2094
- if (existing !== undefined)
2095
- return existing;
2096
- const reference = `join-${randomUUID()}`;
2097
- this.requestRefs.set(joinSessionId, reference);
2098
- this.requestSessions.set(reference, joinSessionId);
2099
- return reference;
2100
- }
2101
- deviceRef(deviceId) {
2102
- const existing = this.deviceRefs.get(deviceId);
2103
- if (existing !== undefined)
2104
- return existing;
2105
- const reference = `device-${randomUUID()}`;
2106
- this.deviceRefs.set(deviceId, reference);
2107
- this.deviceIds.set(reference, deviceId);
2108
- return reference;
2109
- }
2110
- async deviceManagementSnapshot(client) {
2111
- await client.syncDeviceManagement();
2112
- const current = await client.getCurrentDeviceSummary();
2113
- if (!current.canManage || current.role !== 'admin' || current.readiness !== 'admin_ready') {
2114
- this.requestRefs.clear();
2115
- this.requestSessions.clear();
2116
- this.deviceRefs.clear();
2117
- this.deviceIds.clear();
2118
- return {
2119
- canManage: false,
2120
- ...current.role === undefined ? {} : { role: current.role },
2121
- readiness: current.readiness,
2122
- devices: [],
2123
- requests: [],
2124
- };
2125
- }
2126
- const requests = await client.listLocalDeviceJoinRequests();
2127
- await Promise.all(requests
2128
- .filter(request => request.claimedByCurrentDevice)
2129
- .map(request => client.getLocalDeviceJoinVerificationProgress(request.joinSessionId).catch(() => undefined)));
2130
- const devices = await client.getDeviceRegistry();
2131
- return {
2132
- canManage: true,
2133
- role: 'admin',
2134
- readiness: 'admin_ready',
2135
- devices: devices.map(device => this.publicDevice(device)),
2136
- requests: requests.map(request => ({
2137
- requestRef: this.requestRef(request.joinSessionId),
2138
- candidateKeyFingerprint: request.candidateKeyFingerprint,
2139
- issuedAt: request.issuedAt,
2140
- expiresAt: request.expiresAt,
2141
- state: requestJoinPhase(request),
2142
- claimedByCurrentDevice: request.claimedByCurrentDevice,
2143
- canStartVerification: request.canStartVerification,
2144
- })),
2145
- };
2146
- }
2147
- publicDevice(device) {
2148
- return {
2149
- deviceRef: this.deviceRef(device.deviceId),
2150
- status: device.status,
2151
- role: device.role,
2152
- managementReady: device.managementReady,
2153
- isCurrent: device.isCurrent,
2154
- };
2155
- }
2156
- /** Publish one newly registered identity, then start realtime only in the background. */
1637
+ /** Publish one newly registered identity and start its listener through the existing session path. */
2157
1638
  async activateRegisteredIdentity(identity) {
2158
- this.pendingDeviceJoin = undefined;
2159
- this.activeDeviceJoinSessionId = undefined;
2160
1639
  this.activeIdentityDid = identity.did;
2161
1640
  this.publishSession({ status: 'active', identity });
2162
1641
  const provider = this.provider;
2163
- if (provider !== undefined)
2164
- this.ensureProviderRuntime(provider);
1642
+ if (provider !== undefined) {
1643
+ await provider.listenerStartup;
1644
+ await this.startListener(provider);
1645
+ }
2165
1646
  }
2166
1647
  /** Invalidate cached session work and cancel every model request still owned by the old session. */
2167
1648
  invalidateSummaries() {
2168
1649
  this.sessionRevision += 1;
2169
- this.requestRefs.clear();
2170
- this.requestSessions.clear();
2171
- this.deviceRefs.clear();
2172
- this.deviceIds.clear();
2173
1650
  for (const controller of this.activeSummaryRequests)
2174
1651
  controller.abort();
2175
1652
  this.activeSummaryRequests.clear();
@@ -2271,194 +1748,158 @@ let AwikiService = (() => {
2271
1748
  this.sessionMutation = pending.then(() => undefined, () => undefined);
2272
1749
  return pending;
2273
1750
  }
2274
- /** Start identity realtime and the optional Agent consumer without blocking identity success. */
2275
- ensureProviderRuntime(provider) {
2276
- const identityDid = this.activeIdentityDid;
2277
- if (provider.runtimeReplacement !== undefined)
2278
- return;
2279
- if (identityDid !== undefined
2280
- && provider.realtimeIdentityDid !== undefined
2281
- && provider.realtimeIdentityDid !== identityDid) {
2282
- this.replaceProviderRuntime(provider, identityDid);
2283
- return;
2284
- }
2285
- this.ensureRealtimeSupervisor(provider);
2286
- this.ensureAgentConsumer(provider);
2287
- }
2288
- replaceProviderRuntime(provider, identityDid) {
2289
- if (provider.runtimeReplacement !== undefined || this.provider !== provider)
2290
- return;
2291
- const replacement = (async () => {
2292
- await this.stopProviderRuntime(provider);
2293
- if (this.provider !== provider || this.activeIdentityDid !== identityDid)
2294
- return;
2295
- this.ensureRealtimeSupervisor(provider);
2296
- this.ensureAgentConsumer(provider);
2297
- })();
2298
- const observed = replacement
2299
- .catch((error) => {
2300
- this.ctx.logger('awiki-realtime').warn('AWiki identity realtime replacement failed: %s', error instanceof Error ? error.message : 'unknown failure');
2301
- })
2302
- .finally(() => {
2303
- if (provider.runtimeReplacement === observed)
2304
- delete provider.runtimeReplacement;
2305
- if (this.provider === provider)
2306
- this.ensureProviderRuntime(provider);
2307
- });
2308
- provider.runtimeReplacement = observed;
2309
- }
2310
- ensureRealtimeSupervisor(provider) {
2311
- if (!this.resolved.realtimeEnabled
2312
- || provider.realtimeSupervisor !== undefined
2313
- || provider.realtimeStartup !== undefined
2314
- || this.provider !== provider
2315
- || (provider.client.realtime ?? provider.client.listener) === undefined)
2316
- return;
2317
- const generation = provider.realtimeGeneration;
2318
- const source = provider.client.realtime ?? provider.client.listener;
2319
- if (source === undefined)
2320
- return;
2321
- const logger = this.ctx.logger('awiki-realtime');
1751
+ /** Start one exact identity-bound listener, atomically releasing a failed startup. */
1752
+ startListener(provider) {
1753
+ if (!this.resolved.listenerEnabled
1754
+ || provider.listener !== undefined
1755
+ || provider.listenerRecoveryBlocked
1756
+ || this.provider !== provider) {
1757
+ return Promise.resolve();
1758
+ }
1759
+ if (provider.listenerStartup !== undefined)
1760
+ return provider.listenerStartup;
1761
+ if (provider.listenerCleanup !== undefined || provider.listenerRestartTimer !== undefined)
1762
+ return Promise.resolve();
1763
+ const workspaceContext = this.workspaceContext;
1764
+ const source = provider.client.listener;
1765
+ if (workspaceContext === undefined || source === undefined)
1766
+ return Promise.resolve();
1767
+ const generation = provider.listenerGeneration;
1768
+ const logger = this.ctx.logger('awiki-listener');
2322
1769
  const startup = (async () => {
2323
1770
  if (await this.isSignedOut())
2324
1771
  return;
2325
- const identity = await provider.client.getIdentity();
2326
- if (identity === null || !this.realtimeFenceMatches(provider, generation))
1772
+ if (await provider.client.getIdentity() === null)
2327
1773
  return;
2328
- this.activeIdentityDid = identity.did;
2329
- const supervisor = new IdentityRealtimeSupervisor(source, {
2330
- onSynchronized: cause => this.onRealtimeSynchronized(provider, generation, cause),
2331
- }, logger);
2332
- provider.realtimeSupervisor = supervisor;
2333
- provider.realtimeIdentityDid = identity.did;
2334
- supervisor.start();
2335
- this.ensureAgentConsumer(provider);
2336
- })();
2337
- const observed = startup
2338
- .catch((error) => {
2339
- logger.warn('AWiki realtime startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
2340
- })
2341
- .finally(() => {
2342
- if (provider.realtimeStartup === observed)
2343
- delete provider.realtimeStartup;
2344
- });
2345
- provider.realtimeStartup = observed;
2346
- }
2347
- realtimeFenceMatches(provider, generation) {
2348
- return this.provider === provider && provider.realtimeGeneration === generation;
2349
- }
2350
- async onRealtimeSynchronized(provider, generation, cause) {
2351
- if (!this.realtimeFenceMatches(provider, generation))
2352
- return;
2353
- if (cause === 'system_notification' || cause === 'stream_recovery') {
1774
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation))
1775
+ return;
1776
+ const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
1777
+ const listener = new AwikiAgentListener(source, agents, this.resolved.listener, logger);
1778
+ provider.listener = listener;
2354
1779
  try {
2355
- provider.localDeviceJoinRequestCountAfterSync = (await provider.client.listLocalDeviceJoinRequests()).length;
1780
+ await listener.start();
2356
1781
  }
2357
- catch {
2358
- this.ctx.logger('awiki-realtime').debug('AWiki realtime could not observe local device Join requests');
1782
+ catch (error) {
1783
+ if (provider.listener === listener)
1784
+ delete provider.listener;
1785
+ try {
1786
+ await listener.dispose();
1787
+ }
1788
+ catch {
1789
+ provider.listenerRecoveryBlocked = true;
1790
+ }
1791
+ if (!provider.listenerRecoveryBlocked && provider.listenerRestartAttempt > 0) {
1792
+ this.scheduleListenerRestart(provider, workspaceContext, generation);
1793
+ }
1794
+ throw error;
2359
1795
  }
2360
- }
2361
- if (!['session_start', 'connection_ready', 'reconnected', 'message', 'message_update'].includes(cause))
2362
- return;
2363
- this.ensureAgentConsumer(provider);
2364
- await provider.agentConsumerStartup;
2365
- if (!this.realtimeFenceMatches(provider, generation))
2366
- return;
2367
- await provider.agentConsumer?.reconcileOnce();
2368
- }
2369
- ensureAgentConsumer(provider) {
2370
- const workspaceContext = this.workspaceContext;
2371
- const source = provider.client.agentInbox ?? provider.client.listener;
2372
- const identityDid = this.activeIdentityDid;
2373
- if (!this.resolved.listenerEnabled
2374
- || workspaceContext === undefined
2375
- || source === undefined
2376
- || identityDid === undefined
2377
- || this.provider !== provider)
2378
- return;
2379
- if (provider.agentConsumer !== undefined && provider.agentConsumerIdentityDid === identityDid)
2380
- return;
2381
- if (provider.agentConsumerStartup !== undefined)
2382
- return;
2383
- const generation = ++provider.agentConsumerGeneration;
2384
- const logger = this.ctx.logger('awiki-listener');
2385
- const startup = (async () => {
2386
- await this.detachAgentConsumer(provider);
2387
- if (!this.agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation))
1796
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation)) {
1797
+ if (provider.listener === listener)
1798
+ delete provider.listener;
1799
+ try {
1800
+ await listener.dispose();
1801
+ }
1802
+ catch (error) {
1803
+ provider.listenerRecoveryBlocked = true;
1804
+ throw error;
1805
+ }
2388
1806
  return;
2389
- const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
2390
- const consumer = new AwikiAgentListener(source, agents, {
2391
- ...this.resolved.listener,
2392
- identityScope: identityDid,
2393
- }, logger);
2394
- provider.agentConsumer = consumer;
2395
- provider.agentConsumerIdentityDid = identityDid;
2396
- await consumer.reconcileOnce();
1807
+ }
1808
+ provider.listenerStartedAt = Date.now();
1809
+ void listener.whenTerminated().then((result) => {
1810
+ if (result.kind === 'failed') {
1811
+ this.releaseFailedListener(provider, listener, workspaceContext, generation, result.error);
1812
+ }
1813
+ });
2397
1814
  })();
2398
1815
  const observed = startup
2399
1816
  .catch((error) => {
2400
- logger.warn('AWiki Agent consumer startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
1817
+ logger.warn('AWiki listener startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
2401
1818
  })
2402
1819
  .finally(() => {
2403
- if (provider.agentConsumerStartup === observed)
2404
- delete provider.agentConsumerStartup;
1820
+ if (provider.listenerStartup === observed)
1821
+ delete provider.listenerStartup;
2405
1822
  });
2406
- provider.agentConsumerStartup = observed;
1823
+ provider.listenerStartup = observed;
1824
+ return observed;
1825
+ }
1826
+ async stopListener(provider) {
1827
+ provider.listenerGeneration += 1;
1828
+ provider.listenerRestartAttempt = 0;
1829
+ delete provider.listenerStartedAt;
1830
+ const timer = provider.listenerRestartTimer;
1831
+ if (timer !== undefined) {
1832
+ delete provider.listenerRestartTimer;
1833
+ clearTimeout(timer);
1834
+ }
1835
+ await provider.listenerStartup;
1836
+ await provider.listenerCleanup;
1837
+ const listener = provider.listener;
1838
+ if (listener === undefined)
1839
+ return;
1840
+ delete provider.listener;
1841
+ try {
1842
+ await listener.dispose();
1843
+ }
1844
+ catch (error) {
1845
+ provider.listenerRecoveryBlocked = true;
1846
+ throw error;
1847
+ }
2407
1848
  }
2408
- agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation) {
1849
+ listenerFenceMatches(provider, workspaceContext, generation) {
2409
1850
  return this.provider === provider
2410
1851
  && this.workspaceContext === workspaceContext
2411
- && this.activeIdentityDid === identityDid
2412
- && provider.agentConsumerGeneration === generation;
1852
+ && provider.listenerGeneration === generation;
2413
1853
  }
2414
- async detachAgentConsumer(provider) {
2415
- const consumer = provider.agentConsumer;
2416
- if (consumer === undefined)
1854
+ releaseFailedListener(provider, listener, workspaceContext, generation, error) {
1855
+ if (provider.listener !== listener)
2417
1856
  return;
2418
- delete provider.agentConsumer;
2419
- delete provider.agentConsumerIdentityDid;
2420
- const cleanup = consumer.dispose();
1857
+ delete provider.listener;
1858
+ const startedAt = provider.listenerStartedAt;
1859
+ delete provider.listenerStartedAt;
1860
+ if (startedAt !== undefined && Date.now() - startedAt >= LISTENER_STABLE_RESET_MS) {
1861
+ provider.listenerRestartAttempt = 0;
1862
+ }
1863
+ const logger = this.ctx.logger('awiki-listener');
1864
+ logger.warn('AWiki listener lifecycle failed: %s', error instanceof Error ? error.message : 'unknown failure');
1865
+ let disposed = false;
1866
+ const cleanup = listener.dispose()
1867
+ .then(() => { disposed = true; }, (cleanupError) => {
1868
+ provider.listenerRecoveryBlocked = true;
1869
+ logger.warn('AWiki listener cleanup failed: %s', cleanupError instanceof Error ? cleanupError.message : 'unknown failure');
1870
+ });
2421
1871
  const observed = cleanup.finally(() => {
2422
- if (provider.agentConsumerCleanup === observed)
2423
- delete provider.agentConsumerCleanup;
1872
+ if (provider.listenerCleanup === observed)
1873
+ delete provider.listenerCleanup;
1874
+ if (disposed)
1875
+ this.scheduleListenerRestart(provider, workspaceContext, generation);
2424
1876
  });
2425
- provider.agentConsumerCleanup = observed;
2426
- await observed;
2427
- }
2428
- async stopAgentConsumer(provider) {
2429
- provider.agentConsumerGeneration += 1;
2430
- await provider.agentConsumerStartup;
2431
- await provider.agentConsumerCleanup;
2432
- await this.detachAgentConsumer(provider);
2433
- }
2434
- async stopRealtimeSupervisor(provider) {
2435
- provider.realtimeGeneration += 1;
2436
- await provider.realtimeStartup;
2437
- const supervisor = provider.realtimeSupervisor;
2438
- if (supervisor === undefined)
2439
- return;
2440
- delete provider.realtimeSupervisor;
2441
- delete provider.realtimeIdentityDid;
2442
- await supervisor.dispose();
1877
+ provider.listenerCleanup = observed;
2443
1878
  }
2444
- async stopProviderRuntime(provider) {
2445
- await Promise.all([
2446
- this.stopRealtimeSupervisor(provider),
2447
- this.stopAgentConsumer(provider),
2448
- ]);
1879
+ scheduleListenerRestart(provider, workspaceContext, generation) {
1880
+ if (provider.listenerRestartTimer !== undefined
1881
+ || !this.listenerFenceMatches(provider, workspaceContext, generation))
1882
+ return;
1883
+ provider.listenerRestartAttempt = Math.min(provider.listenerRestartAttempt + 1, LISTENER_RESTART_MAX_ATTEMPT);
1884
+ const exponent = provider.listenerRestartAttempt - 1;
1885
+ const delay = Math.min(LISTENER_RESTART_BASE_DELAY_MS * 2 ** exponent, LISTENER_RESTART_MAX_DELAY_MS);
1886
+ const timer = setTimeout(() => {
1887
+ if (provider.listenerRestartTimer !== timer)
1888
+ return;
1889
+ delete provider.listenerRestartTimer;
1890
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation))
1891
+ return;
1892
+ void this.startListener(provider);
1893
+ }, delay);
1894
+ provider.listenerRestartTimer = timer;
2449
1895
  }
2450
1896
  /** Clear one exact provider slot before joining its one shared disposal. */
2451
1897
  disposeProvider(provider) {
2452
- if (this.provider === provider) {
1898
+ if (this.provider === provider)
2453
1899
  this.provider = undefined;
2454
- this.pendingDeviceJoin = undefined;
2455
- this.activeDeviceJoinSessionId = undefined;
2456
- this.invalidateSummaries();
2457
- }
2458
1900
  provider.disposal ??= (async () => {
2459
1901
  try {
2460
- await provider.runtimeReplacement;
2461
- await this.stopProviderRuntime(provider);
1902
+ await this.stopListener(provider);
2462
1903
  }
2463
1904
  finally {
2464
1905
  await provider.client.dispose();