@awiki/dsh-plugin 0.2.4 → 0.2.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 (115) hide show
  1. package/README.md +80 -11
  2. package/README.zh.md +67 -12
  3. package/THIRD_PARTY_NOTICES.md +8 -1
  4. package/cordis.patch.yml +13 -0
  5. package/lib/client.js +8223 -1939
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +1486 -59
  8. package/lib/model-proxy-contract.js +89 -0
  9. package/lib/model-proxy.js +383 -0
  10. package/lib/provider.js +2 -1
  11. package/lib/sdk-adapter-DKgkyApB.mjs +669 -0
  12. package/lib/typert.host.js +470 -482
  13. package/lib/typert.remote-client.d.ts +15 -1
  14. package/lib/typert.remote-client.d.ts.map +1 -1
  15. package/lib/typert.remote-client.js +469 -103
  16. package/lib/types/attachment-cache.d.ts +28 -0
  17. package/lib/types/attachment-cache.d.ts.map +1 -0
  18. package/lib/types/attachment-cache.js +194 -0
  19. package/lib/types/attachment-cache.js.map +1 -0
  20. package/lib/types/client/AwikiMail.d.ts +14 -0
  21. package/lib/types/client/AwikiMail.d.ts.map +1 -0
  22. package/lib/types/client/AwikiMail.js +260 -0
  23. package/lib/types/client/AwikiMail.js.map +1 -0
  24. package/lib/types/client/AwikiOnboarding.d.ts +22 -0
  25. package/lib/types/client/AwikiOnboarding.d.ts.map +1 -0
  26. package/lib/types/client/AwikiOnboarding.js +87 -0
  27. package/lib/types/client/AwikiOnboarding.js.map +1 -0
  28. package/lib/types/client/AwikiOverlay.d.ts +15 -0
  29. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  30. package/lib/types/client/AwikiOverlay.js +251 -20
  31. package/lib/types/client/AwikiOverlay.js.map +1 -1
  32. package/lib/types/client/AwikiSettingsSection.d.ts +20 -4
  33. package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
  34. package/lib/types/client/AwikiSettingsSection.js +279 -23
  35. package/lib/types/client/AwikiSettingsSection.js.map +1 -1
  36. package/lib/types/client/RechargeComingSoonDialog.d.ts +11 -0
  37. package/lib/types/client/RechargeComingSoonDialog.d.ts.map +1 -0
  38. package/lib/types/client/RechargeComingSoonDialog.js +8 -0
  39. package/lib/types/client/RechargeComingSoonDialog.js.map +1 -0
  40. package/lib/types/client/controller.d.ts +82 -4
  41. package/lib/types/client/controller.d.ts.map +1 -1
  42. package/lib/types/client/controller.js +517 -74
  43. package/lib/types/client/controller.js.map +1 -1
  44. package/lib/types/client/image-cache.d.ts +20 -0
  45. package/lib/types/client/image-cache.d.ts.map +1 -0
  46. package/lib/types/client/image-cache.js +177 -0
  47. package/lib/types/client/image-cache.js.map +1 -0
  48. package/lib/types/client/index.d.ts +2 -0
  49. package/lib/types/client/index.d.ts.map +1 -1
  50. package/lib/types/client/index.js +84 -41
  51. package/lib/types/client/index.js.map +1 -1
  52. package/lib/types/client/model-account-settings-availability.d.ts +4 -0
  53. package/lib/types/client/model-account-settings-availability.d.ts.map +1 -0
  54. package/lib/types/client/model-account-settings-availability.js +4 -0
  55. package/lib/types/client/model-account-settings-availability.js.map +1 -0
  56. package/lib/types/client/model-availability-controller.d.ts +27 -0
  57. package/lib/types/client/model-availability-controller.d.ts.map +1 -0
  58. package/lib/types/client/model-availability-controller.js +111 -0
  59. package/lib/types/client/model-availability-controller.js.map +1 -0
  60. package/lib/types/client/model-proxy-controller.d.ts +41 -0
  61. package/lib/types/client/model-proxy-controller.d.ts.map +1 -0
  62. package/lib/types/client/model-proxy-controller.js +241 -0
  63. package/lib/types/client/model-proxy-controller.js.map +1 -0
  64. package/lib/types/client/recharge-availability.d.ts +6 -0
  65. package/lib/types/client/recharge-availability.d.ts.map +1 -0
  66. package/lib/types/client/recharge-availability.js +6 -0
  67. package/lib/types/client/recharge-availability.js.map +1 -0
  68. package/lib/types/client/settings-locales.d.ts +1 -1
  69. package/lib/types/client/settings-locales.d.ts.map +1 -1
  70. package/lib/types/client/settings-locales.js +180 -2
  71. package/lib/types/client/settings-locales.js.map +1 -1
  72. package/lib/types/client/slots.d.ts +15 -2
  73. package/lib/types/client/slots.d.ts.map +1 -1
  74. package/lib/types/index.d.ts +56 -2
  75. package/lib/types/index.d.ts.map +1 -1
  76. package/lib/types/index.js +462 -31
  77. package/lib/types/index.js.map +1 -1
  78. package/lib/types/listener-state.d.ts +23 -0
  79. package/lib/types/listener-state.d.ts.map +1 -0
  80. package/lib/types/listener-state.js +124 -0
  81. package/lib/types/listener-state.js.map +1 -0
  82. package/lib/types/listener.d.ts +96 -0
  83. package/lib/types/listener.d.ts.map +1 -0
  84. package/lib/types/listener.js +575 -0
  85. package/lib/types/listener.js.map +1 -0
  86. package/lib/types/mail.d.ts +8 -0
  87. package/lib/types/mail.d.ts.map +1 -0
  88. package/lib/types/mail.js +75 -0
  89. package/lib/types/mail.js.map +1 -0
  90. package/lib/types/model-proxy-contract.d.ts +61 -0
  91. package/lib/types/model-proxy-contract.d.ts.map +1 -0
  92. package/lib/types/model-proxy-contract.js +129 -0
  93. package/lib/types/model-proxy-contract.js.map +1 -0
  94. package/lib/types/model-proxy.d.ts +14 -0
  95. package/lib/types/model-proxy.d.ts.map +1 -0
  96. package/lib/types/model-proxy.js +371 -0
  97. package/lib/types/model-proxy.js.map +1 -0
  98. package/lib/types/provider-api.d.ts +78 -2
  99. package/lib/types/provider-api.d.ts.map +1 -1
  100. package/lib/types/provider.d.ts.map +1 -1
  101. package/lib/types/provider.js +1 -0
  102. package/lib/types/provider.js.map +1 -1
  103. package/lib/types/sdk-adapter.d.ts +26 -2
  104. package/lib/types/sdk-adapter.d.ts.map +1 -1
  105. package/lib/types/sdk-adapter.js +380 -24
  106. package/lib/types/sdk-adapter.js.map +1 -1
  107. package/lib/types/tools.d.ts +10 -0
  108. package/lib/types/tools.d.ts.map +1 -1
  109. package/lib/types/tools.js +98 -0
  110. package/lib/types/tools.js.map +1 -1
  111. package/lib/types/types.d.ts +103 -1
  112. package/lib/types/types.d.ts.map +1 -1
  113. package/lib/types/types.js.map +1 -1
  114. package/package.json +30 -5
  115. package/lib/sdk-adapter-DK6RqV-c.mjs +0 -355
@@ -35,7 +35,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
35
35
  };
36
36
  import { Context } from '@deepseek-ai/cordis';
37
37
  import { homedir } from 'node:os';
38
- import { join } from 'node:path';
38
+ import { isAbsolute, join } from 'node:path';
39
39
  import z from '@deepseek-ai/schemastery';
40
40
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
41
41
  import { settingsNamespace } from '@deepseek-ai/dsh-settings';
@@ -43,17 +43,22 @@ import { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "
43
43
  import { AwikiExternalHttpAuthError, createAwikiExternalHttpAuth, externalHttpAuthError, mapProviderError as mapExternalHttpProviderError, } from "./external-http-auth.js";
44
44
  import { downloadedAttachment } from "./sdk-adapter.js";
45
45
  import { registerAwikiTools } from "./tools.js";
46
+ import { mailInboxRequest, mailMarkReadRequest, mailReadRequest, mailSendRequest, } from "./mail.js";
46
47
  import { AwikiSettingsSchema, validateAwikiSettings, } from "./settings.js";
47
48
  import { AWIKI_SETTINGS_NAMESPACE, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain } from "./domain.js";
48
49
  import { AWIKI_SETTINGS_RPC_CHANNEL } from "./settings-rpc-contract.js";
49
50
  import { createAwikiSettingsRpcHandler } from "./settings-rpc.js";
50
51
  import { AwikiSessionStore } from "./session.js";
52
+ import { AwikiImageAttachmentCache, minimumImageAttachmentCacheMaxBytes } from "./attachment-cache.js";
53
+ import { AwikiAgentListener, DshAwikiListenerAgentRuntime, } from "./listener.js";
51
54
  export { AWIKI_CLEAR_LOCAL_DATA_CONFIRMATION, AWIKI_LOGOUT_CONFIRMATION } from "./types.js";
52
55
  export { AWIKI_EXTERNAL_HTTP_MAX_BODY_BYTES, AwikiExternalHttpAuthError, } from "./external-http-auth.js";
53
56
  export { AWIKI_DOMAIN_FIELD, AWIKI_SETTINGS_NAMESPACE, AwikiSettingsSchema, DEFAULT_AWIKI_DOMAIN, normalizeAwikiDomain, validateAwikiSettings, } from "./settings.js";
54
- export { AWIKI_HISTORY_TOOL, AWIKI_IDENTITY_STATUS_TOOL, AWIKI_LIST_CONVERSATIONS_TOOL, AWIKI_SEND_ATTACHMENT_TOOL, AWIKI_SEND_MESSAGE_TOOL, } from "./tools.js";
57
+ export { AWIKI_HISTORY_TOOL, AWIKI_IDENTITY_STATUS_TOOL, AWIKI_LIST_CONVERSATIONS_TOOL, AWIKI_MAIL_ACCOUNT_TOOL, AWIKI_MAIL_INBOX_TOOL, AWIKI_MAIL_MARK_READ_TOOL, AWIKI_MAIL_READ_TOOL, AWIKI_MAIL_SEND_TOOL, AWIKI_SEND_ATTACHMENT_TOOL, AWIKI_SEND_MESSAGE_TOOL, } from "./tools.js";
55
58
  /** Default maximum attachment size: 10 MiB. */
56
59
  export const DEFAULT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
60
+ /** Default private on-disk budget for verified image previews: 64 MiB. */
61
+ export const DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES = 64 * 1024 * 1024;
57
62
  /** Default browser polling interval while the AWiki drawer is open. */
58
63
  export const DEFAULT_POLL_INTERVAL_MS = 3_000;
59
64
  /** Default AWiki production service origin. */
@@ -69,13 +74,18 @@ export const Config = z.object({
69
74
  userServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
70
75
  userServiceDomain: z.string().default(DEFAULT_AWIKI_DOMAIN),
71
76
  messageServiceUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
77
+ mailServiceUrl: z.string(),
72
78
  messageServicePublicUrl: z.string().default(DEFAULT_AWIKI_SERVICE_URL),
73
79
  messageServiceDid: z.string().default(DEFAULT_AWIKI_MESSAGE_SERVICE_DID),
74
80
  allowedAttachmentOrigins: z.array(z.string()).default([]),
75
81
  allowInsecureLoopbackForTesting: z.boolean().default(false),
76
82
  stateRoot: z.string(),
77
83
  attachmentMaxBytes: z.number().default(DEFAULT_ATTACHMENT_MAX_BYTES),
84
+ imageAttachmentCacheMaxBytes: z.number().default(DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES),
78
85
  pollIntervalMs: z.number().default(DEFAULT_POLL_INTERVAL_MS),
86
+ listenerEnabled: z.boolean().default(false),
87
+ listenerAllowedPeers: z.array(z.string()).default([]),
88
+ listenerWorkspacePath: z.string(),
79
89
  summaryMaxInputBytes: z.number().default(DEFAULT_SUMMARY_MAX_INPUT_BYTES),
80
90
  });
81
91
  const FAILURE_CODES = new Set([
@@ -96,9 +106,14 @@ const FAILURE_CODES = new Set([
96
106
  'summary-cancelled',
97
107
  'summary-invalid-output',
98
108
  'summary-failed',
109
+ 'delivery-unknown',
99
110
  'network',
100
111
  'remote',
101
112
  ]);
113
+ const LISTENER_RESTART_BASE_DELAY_MS = 1_000;
114
+ const LISTENER_RESTART_MAX_DELAY_MS = 30_000;
115
+ const LISTENER_RESTART_MAX_ATTEMPT = 6;
116
+ const LISTENER_STABLE_RESET_MS = 60_000;
102
117
  const FAILURE_MESSAGES = {
103
118
  'not-registered': 'No AWiki identity is registered for this deployment.',
104
119
  'signed-out': 'This installation is signed out of AWiki.',
@@ -117,6 +132,7 @@ const FAILURE_MESSAGES = {
117
132
  'summary-cancelled': 'AI summary was cancelled. Try again.',
118
133
  'summary-invalid-output': 'The model returned an invalid summary. Try again.',
119
134
  'summary-failed': 'AI summary could not be generated. Try again.',
135
+ 'delivery-unknown': 'Mail delivery could not be confirmed. Inspect the mailbox before retrying.',
120
136
  'network': 'The AWiki service could not be reached.',
121
137
  'remote': 'The AWiki service rejected the operation.',
122
138
  };
@@ -170,42 +186,83 @@ function attachmentOrigins(raw, messageServicePublicUrl, allowInsecureLoopbackFo
170
186
  }
171
187
  return origins;
172
188
  }
189
+ /** Resolve the explicit listener allowlist without accepting wildcards or ambiguous whitespace. */
190
+ function listenerAllowedPeers(raw, enabled) {
191
+ const peers = (raw ?? []).map((peer) => {
192
+ if (peer !== peer.trim() || peer.length === 0 || peer.length > 2_048 || /[\u0000-\u001f\u007f]/u.test(peer)) {
193
+ throw new TypeError('awiki: listenerAllowedPeers entries must be non-empty exact Handles or DIDs');
194
+ }
195
+ if (peer === '*')
196
+ throw new TypeError('awiki: listenerAllowedPeers does not accept wildcards');
197
+ return peer.startsWith('did:') ? peer : peer.toLowerCase();
198
+ });
199
+ if (peers.length > 100 || new Set(peers).size !== peers.length) {
200
+ throw new TypeError('awiki: listenerAllowedPeers must contain at most 100 unique entries');
201
+ }
202
+ if (enabled && peers.length === 0) {
203
+ throw new TypeError('awiki: listenerAllowedPeers must contain at least one Handle or DID when listenerEnabled is true');
204
+ }
205
+ return peers;
206
+ }
173
207
  /** Resolve and validate every deployment choice before publishing the service. */
174
208
  function resolveConfig(config) {
175
209
  const allowInsecureLoopbackForTesting = config.allowInsecureLoopbackForTesting ?? false;
176
210
  const configuredStateRoot = config.stateRoot?.trim();
177
211
  const configuredDshHome = process.env.DSH_HOME?.trim();
212
+ const dshHome = configuredDshHome === undefined || configuredDshHome.length === 0
213
+ ? join(homedir(), '.dsh')
214
+ : configuredDshHome;
178
215
  const stateRoot = configuredStateRoot === undefined || configuredStateRoot.length === 0
179
- ? join(configuredDshHome === undefined || configuredDshHome.length === 0 ? join(homedir(), '.dsh') : configuredDshHome, 'awiki', 'im-core')
216
+ ? join(dshHome, 'awiki', 'im-core')
180
217
  : configuredStateRoot;
181
- if (stateRoot.length === 0)
182
- throw new TypeError('awiki: stateRoot must be non-empty');
218
+ if (!isAbsolute(stateRoot))
219
+ throw new TypeError('awiki: stateRoot must be an absolute path');
183
220
  const attachmentMaxBytes = config.attachmentMaxBytes ?? DEFAULT_ATTACHMENT_MAX_BYTES;
184
221
  if (!Number.isSafeInteger(attachmentMaxBytes) || attachmentMaxBytes < 1) {
185
222
  throw new TypeError('awiki: attachmentMaxBytes must be a positive safe integer');
186
223
  }
224
+ const imageAttachmentCacheMaxBytes = config.imageAttachmentCacheMaxBytes ?? DEFAULT_IMAGE_ATTACHMENT_CACHE_MAX_BYTES;
225
+ if (!Number.isSafeInteger(imageAttachmentCacheMaxBytes)
226
+ || imageAttachmentCacheMaxBytes < minimumImageAttachmentCacheMaxBytes(attachmentMaxBytes)) {
227
+ throw new TypeError('awiki: imageAttachmentCacheMaxBytes cannot retain one maximum-sized attachment');
228
+ }
187
229
  const pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
188
230
  if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 1_000 || pollIntervalMs > 60_000) {
189
231
  throw new TypeError('awiki: pollIntervalMs must be a safe integer from 1000 through 60000');
190
232
  }
233
+ const listenerEnabled = config.listenerEnabled ?? false;
234
+ const listenerWorkspacePath = config.listenerWorkspacePath?.trim() || join(dshHome, 'workspaces', 'awiki');
235
+ if (!isAbsolute(listenerWorkspacePath)) {
236
+ throw new TypeError('awiki: listenerWorkspacePath must be an absolute path');
237
+ }
238
+ const allowedPeers = listenerAllowedPeers(config.listenerAllowedPeers, listenerEnabled);
191
239
  const summaryMaxInputBytes = config.summaryMaxInputBytes ?? DEFAULT_SUMMARY_MAX_INPUT_BYTES;
192
240
  if (!Number.isSafeInteger(summaryMaxInputBytes) || summaryMaxInputBytes < 1_024) {
193
241
  throw new TypeError('awiki: summaryMaxInputBytes must be a safe integer of at least 1024');
194
242
  }
195
243
  const userServiceUrl = serviceUrl('userServiceUrl', config.userServiceUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
196
244
  const messageServiceUrl = serviceUrl('messageServiceUrl', config.messageServiceUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
245
+ const mailServiceUrl = serviceUrl('mailServiceUrl', config.mailServiceUrl ?? userServiceUrl, allowInsecureLoopbackForTesting);
197
246
  const messageServicePublicUrl = serviceUrl('messageServicePublicUrl', config.messageServicePublicUrl ?? DEFAULT_AWIKI_SERVICE_URL, allowInsecureLoopbackForTesting);
198
247
  return {
199
248
  userServiceUrl,
200
249
  userServiceDomain: serviceDomain(config.userServiceDomain ?? DEFAULT_AWIKI_DOMAIN),
201
250
  messageServiceUrl,
251
+ mailServiceUrl,
202
252
  messageServicePublicUrl,
203
253
  messageServiceDid: serviceDid(config.messageServiceDid ?? DEFAULT_AWIKI_MESSAGE_SERVICE_DID),
204
254
  allowedAttachmentOrigins: attachmentOrigins(config.allowedAttachmentOrigins, messageServicePublicUrl, allowInsecureLoopbackForTesting),
205
255
  allowInsecureLoopbackForTesting,
206
256
  stateRoot,
207
257
  attachmentMaxBytes,
258
+ imageAttachmentCacheMaxBytes,
208
259
  pollIntervalMs,
260
+ listenerEnabled,
261
+ listener: {
262
+ allowedPeers,
263
+ workspacePath: listenerWorkspacePath,
264
+ stateRoot,
265
+ },
209
266
  summaryMaxInputBytes,
210
267
  };
211
268
  }
@@ -234,6 +291,32 @@ function normalizeFailure(error) {
234
291
  }
235
292
  return failure('remote');
236
293
  }
294
+ const MAX_GROUP_NAME_CHARACTERS = 100;
295
+ const MAX_GROUP_INITIAL_MEMBERS = 50;
296
+ const MAX_GROUP_MEMBER_CHARACTERS = 512;
297
+ function normalizeCreateGroupRequest(request) {
298
+ if (typeof request?.name !== 'string' || !Array.isArray(request.members))
299
+ return undefined;
300
+ const name = request.name.trim();
301
+ if (name.length === 0 || Array.from(name).length > MAX_GROUP_NAME_CHARACTERS)
302
+ return undefined;
303
+ if (request.members.length === 0 || request.members.length > MAX_GROUP_INITIAL_MEMBERS)
304
+ return undefined;
305
+ const members = [];
306
+ const seen = new Set();
307
+ for (const raw of request.members) {
308
+ if (typeof raw !== 'string')
309
+ return undefined;
310
+ const member = raw.trim().replace(/^@+/u, '');
311
+ if (member.length === 0 || Array.from(member).length > MAX_GROUP_MEMBER_CHARACTERS)
312
+ return undefined;
313
+ if (seen.has(member))
314
+ continue;
315
+ seen.add(member);
316
+ members.push(member);
317
+ }
318
+ return members.length === 0 ? undefined : { name, members };
319
+ }
237
320
  /** Normalize summary-provider failures without returning prompts, model output, routes, or causes. */
238
321
  function normalizeSummaryFailure(error) {
239
322
  if (error instanceof SummaryProviderUnavailableError)
@@ -371,13 +454,20 @@ let AwikiService = (() => {
371
454
  let _registerIdentity_decorators;
372
455
  let _updateDisplayName_decorators;
373
456
  let _resolvePeer_decorators;
457
+ let _createGroup_decorators;
374
458
  let _listConversations_decorators;
375
459
  let _getHistory_decorators;
460
+ let _getLocalHistory_decorators;
376
461
  let _summarizeConversation_decorators;
377
462
  let _markConversationRead_decorators;
378
463
  let _sendText_decorators;
379
464
  let _sendAttachment_decorators;
380
465
  let _downloadAttachment_decorators;
466
+ let _getMailAccount_decorators;
467
+ let _listMailInbox_decorators;
468
+ let _readMail_decorators;
469
+ let _markMailRead_decorators;
470
+ let _sendMail_decorators;
381
471
  let _clearLocalData_decorators;
382
472
  return class AwikiService extends _classSuper {
383
473
  static {
@@ -391,13 +481,20 @@ let AwikiService = (() => {
391
481
  _registerIdentity_decorators = [Remote];
392
482
  _updateDisplayName_decorators = [Remote];
393
483
  _resolvePeer_decorators = [Remote];
484
+ _createGroup_decorators = [Remote];
394
485
  _listConversations_decorators = [Remote];
395
486
  _getHistory_decorators = [Remote];
487
+ _getLocalHistory_decorators = [Remote];
396
488
  _summarizeConversation_decorators = [Remote];
397
489
  _markConversationRead_decorators = [Remote];
398
490
  _sendText_decorators = [Remote];
399
491
  _sendAttachment_decorators = [Remote];
400
492
  _downloadAttachment_decorators = [Remote];
493
+ _getMailAccount_decorators = [Remote];
494
+ _listMailInbox_decorators = [Remote];
495
+ _readMail_decorators = [Remote];
496
+ _markMailRead_decorators = [Remote];
497
+ _sendMail_decorators = [Remote];
401
498
  _clearLocalData_decorators = [Remote];
402
499
  __esDecorate(this, null, _getConfig_decorators, { kind: "method", name: "getConfig", static: false, private: false, access: { has: obj => "getConfig" in obj, get: obj => obj.getConfig }, metadata: _metadata }, null, _instanceExtraInitializers);
403
500
  __esDecorate(this, null, _getIdentity_decorators, { kind: "method", name: "getIdentity", static: false, private: false, access: { has: obj => "getIdentity" in obj, get: obj => obj.getIdentity }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -408,13 +505,20 @@ let AwikiService = (() => {
408
505
  __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);
409
506
  __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);
410
507
  __esDecorate(this, null, _resolvePeer_decorators, { kind: "method", name: "resolvePeer", static: false, private: false, access: { has: obj => "resolvePeer" in obj, get: obj => obj.resolvePeer }, metadata: _metadata }, null, _instanceExtraInitializers);
508
+ __esDecorate(this, null, _createGroup_decorators, { kind: "method", name: "createGroup", static: false, private: false, access: { has: obj => "createGroup" in obj, get: obj => obj.createGroup }, metadata: _metadata }, null, _instanceExtraInitializers);
411
509
  __esDecorate(this, null, _listConversations_decorators, { kind: "method", name: "listConversations", static: false, private: false, access: { has: obj => "listConversations" in obj, get: obj => obj.listConversations }, metadata: _metadata }, null, _instanceExtraInitializers);
412
510
  __esDecorate(this, null, _getHistory_decorators, { kind: "method", name: "getHistory", static: false, private: false, access: { has: obj => "getHistory" in obj, get: obj => obj.getHistory }, metadata: _metadata }, null, _instanceExtraInitializers);
511
+ __esDecorate(this, null, _getLocalHistory_decorators, { kind: "method", name: "getLocalHistory", static: false, private: false, access: { has: obj => "getLocalHistory" in obj, get: obj => obj.getLocalHistory }, metadata: _metadata }, null, _instanceExtraInitializers);
413
512
  __esDecorate(this, null, _summarizeConversation_decorators, { kind: "method", name: "summarizeConversation", static: false, private: false, access: { has: obj => "summarizeConversation" in obj, get: obj => obj.summarizeConversation }, metadata: _metadata }, null, _instanceExtraInitializers);
414
513
  __esDecorate(this, null, _markConversationRead_decorators, { kind: "method", name: "markConversationRead", static: false, private: false, access: { has: obj => "markConversationRead" in obj, get: obj => obj.markConversationRead }, metadata: _metadata }, null, _instanceExtraInitializers);
415
514
  __esDecorate(this, null, _sendText_decorators, { kind: "method", name: "sendText", static: false, private: false, access: { has: obj => "sendText" in obj, get: obj => obj.sendText }, metadata: _metadata }, null, _instanceExtraInitializers);
416
515
  __esDecorate(this, null, _sendAttachment_decorators, { kind: "method", name: "sendAttachment", static: false, private: false, access: { has: obj => "sendAttachment" in obj, get: obj => obj.sendAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
417
516
  __esDecorate(this, null, _downloadAttachment_decorators, { kind: "method", name: "downloadAttachment", static: false, private: false, access: { has: obj => "downloadAttachment" in obj, get: obj => obj.downloadAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
517
+ __esDecorate(this, null, _getMailAccount_decorators, { kind: "method", name: "getMailAccount", static: false, private: false, access: { has: obj => "getMailAccount" in obj, get: obj => obj.getMailAccount }, metadata: _metadata }, null, _instanceExtraInitializers);
518
+ __esDecorate(this, null, _listMailInbox_decorators, { kind: "method", name: "listMailInbox", static: false, private: false, access: { has: obj => "listMailInbox" in obj, get: obj => obj.listMailInbox }, metadata: _metadata }, null, _instanceExtraInitializers);
519
+ __esDecorate(this, null, _readMail_decorators, { kind: "method", name: "readMail", static: false, private: false, access: { has: obj => "readMail" in obj, get: obj => obj.readMail }, metadata: _metadata }, null, _instanceExtraInitializers);
520
+ __esDecorate(this, null, _markMailRead_decorators, { kind: "method", name: "markMailRead", static: false, private: false, access: { has: obj => "markMailRead" in obj, get: obj => obj.markMailRead }, metadata: _metadata }, null, _instanceExtraInitializers);
521
+ __esDecorate(this, null, _sendMail_decorators, { kind: "method", name: "sendMail", static: false, private: false, access: { has: obj => "sendMail" in obj, get: obj => obj.sendMail }, metadata: _metadata }, null, _instanceExtraInitializers);
418
522
  __esDecorate(this, null, _clearLocalData_decorators, { kind: "method", name: "clearLocalData", static: false, private: false, access: { has: obj => "clearLocalData" in obj, get: obj => obj.clearLocalData }, metadata: _metadata }, null, _instanceExtraInitializers);
419
523
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
420
524
  }
@@ -422,25 +526,31 @@ let AwikiService = (() => {
422
526
  static Config = Config;
423
527
  resolved = __runInitializers(this, _instanceExtraInitializers);
424
528
  sessionStore;
529
+ imageAttachmentCache;
425
530
  startupUserServiceDomain;
426
531
  settingsProvider;
427
532
  provider;
428
533
  signedOut;
429
534
  sessionMutation = Promise.resolve();
430
535
  sessionRevision = 0;
536
+ activeIdentityDid;
431
537
  activeSummaryRequests = new Set();
432
538
  summaryProvider;
539
+ hostContext;
433
540
  /** Trusted same-process external HTTP authentication dispatcher. Never Remote. */
434
541
  externalHttpAuth;
542
+ workspaceContext;
435
543
  /**
436
544
  * @param ctx - owning Host context.
437
545
  * @param config - service endpoints, SDK state path, and public limits.
438
546
  */
439
547
  constructor(ctx, config) {
440
548
  super(ctx, 'awiki');
549
+ this.hostContext = ctx;
441
550
  this.resolved = resolveConfig(config);
442
551
  this.externalHttpAuth = createAwikiExternalHttpAuth(() => this.acquireExternalHttpAuthSession());
443
552
  this.sessionStore = new AwikiSessionStore(this.resolved.stateRoot);
553
+ this.imageAttachmentCache = new AwikiImageAttachmentCache(this.resolved.stateRoot, this.resolved.attachmentMaxBytes, this.resolved.imageAttachmentCacheMaxBytes);
444
554
  this.startupUserServiceDomain = this.resolved.userServiceDomain;
445
555
  ctx.inject(['settings'], (settingsCtx) => {
446
556
  const provider = settingsCtx.settings;
@@ -461,6 +571,20 @@ let AwikiService = (() => {
461
571
  ctx.inject(['connection'], (connectionCtx) => {
462
572
  connectionCtx.connection.rpc.handle(AWIKI_SETTINGS_RPC_CHANNEL, createAwikiSettingsRpcHandler(() => this.settingsProvider), { authority: 'loopback' });
463
573
  });
574
+ ctx.inject(['workspaceRegistry'], (workspaceCtx) => {
575
+ this.workspaceContext = workspaceCtx;
576
+ const provider = this.provider;
577
+ if (provider !== undefined)
578
+ void this.startListener(provider);
579
+ workspaceCtx.effect(() => async () => {
580
+ if (this.workspaceContext !== workspaceCtx)
581
+ return;
582
+ this.workspaceContext = undefined;
583
+ const current = this.provider;
584
+ if (current !== undefined)
585
+ await this.stopListener(current);
586
+ }, 'awiki: release Workspace listener composition');
587
+ });
464
588
  registerAwikiTools(ctx, this);
465
589
  ctx.effect(() => async () => {
466
590
  const provider = this.provider;
@@ -485,6 +609,7 @@ let AwikiService = (() => {
485
609
  userServiceUrl: this.resolved.userServiceUrl,
486
610
  userServiceDomain: this.startupUserServiceDomain,
487
611
  messageServiceUrl: this.resolved.messageServiceUrl,
612
+ mailServiceUrl: this.resolved.mailServiceUrl,
488
613
  messageServicePublicUrl: this.resolved.messageServicePublicUrl,
489
614
  messageServiceDid: this.resolved.messageServiceDid,
490
615
  allowedAttachmentOrigins: this.resolved.allowedAttachmentOrigins,
@@ -492,8 +617,14 @@ let AwikiService = (() => {
492
617
  allowInsecureLoopbackForTesting: this.resolved.allowInsecureLoopbackForTesting,
493
618
  stateRoot: this.resolved.stateRoot,
494
619
  });
495
- const provider = { client };
620
+ const provider = {
621
+ client,
622
+ listenerRestartAttempt: 0,
623
+ listenerGeneration: 0,
624
+ listenerRecoveryBlocked: false,
625
+ };
496
626
  this.provider = provider;
627
+ void this.startListener(provider);
497
628
  return () => this.disposeProvider(provider);
498
629
  }
499
630
  /** Register one replaceable conversation-summary provider for this deployment. */
@@ -527,16 +658,22 @@ let AwikiService = (() => {
527
658
  * Read the deployment's identity status.
528
659
  * @returns The public deployment identity or `null`.
529
660
  */
530
- getIdentity() {
531
- return this.run(client => client.getIdentity());
661
+ async getIdentity() {
662
+ const result = await this.run(client => client.getIdentity());
663
+ if (result.ok)
664
+ this.activeIdentityDid = result.value?.did;
665
+ return result;
532
666
  }
533
667
  /** Return the local registration and sign-in state without exposing secrets. */
534
668
  async getSession() {
535
- if (await this.isSignedOut())
669
+ if (await this.isSignedOut()) {
670
+ this.activeIdentityDid = undefined;
536
671
  return { ok: true, value: { status: 'signed-out' } };
672
+ }
537
673
  const identity = await this.run(client => client.getIdentity(), { allowSignedOut: true });
538
674
  if (!identity.ok)
539
675
  return identity;
676
+ this.activeIdentityDid = identity.value?.did;
540
677
  return identity.value === null
541
678
  ? { ok: true, value: { status: 'unregistered' } }
542
679
  : { ok: true, value: { status: 'active', identity: identity.value } };
@@ -557,8 +694,14 @@ let AwikiService = (() => {
557
694
  try {
558
695
  await this.sessionStore.signOut();
559
696
  this.signedOut = true;
697
+ this.activeIdentityDid = undefined;
560
698
  this.invalidateSummaries();
561
- return { ok: true, value: { status: 'signed-out' } };
699
+ const session = { status: 'signed-out' };
700
+ this.publishSession(session);
701
+ const provider = this.provider;
702
+ if (provider !== undefined)
703
+ await this.stopListener(provider);
704
+ return { ok: true, value: session };
562
705
  }
563
706
  catch {
564
707
  return { ok: false, error: failure('remote') };
@@ -576,8 +719,14 @@ let AwikiService = (() => {
576
719
  try {
577
720
  await this.sessionStore.signIn();
578
721
  this.signedOut = false;
722
+ this.activeIdentityDid = identity.value.did;
579
723
  this.invalidateSummaries();
580
- return { ok: true, value: { status: 'active', identity: identity.value } };
724
+ const session = { status: 'active', identity: identity.value };
725
+ this.publishSession(session);
726
+ const provider = this.provider;
727
+ if (provider !== undefined)
728
+ void this.startListener(provider);
729
+ return { ok: true, value: session };
581
730
  }
582
731
  catch {
583
732
  return { ok: false, error: failure('remote') };
@@ -597,16 +746,29 @@ let AwikiService = (() => {
597
746
  * @param request - Handle, phone, and verification code for registration.
598
747
  * @returns The new public identity or a closed failure.
599
748
  */
600
- registerIdentity(request) {
601
- return this.run(client => client.registerIdentity(request));
749
+ async registerIdentity(request) {
750
+ const result = await this.run(client => client.registerIdentity(request));
751
+ if (result.ok) {
752
+ this.activeIdentityDid = result.value.did;
753
+ this.publishSession({ status: 'active', identity: result.value });
754
+ const provider = this.provider;
755
+ if (provider !== undefined) {
756
+ await provider.listenerStartup;
757
+ await this.startListener(provider);
758
+ }
759
+ }
760
+ return result;
602
761
  }
603
762
  /**
604
763
  * Update the deployment identity's public WNS display name.
605
764
  * @param request - replacement display name selected by the user.
606
765
  * @returns The updated public identity or a closed failure.
607
766
  */
608
- updateDisplayName(request) {
609
- return this.run(client => client.updateDisplayName(request));
767
+ async updateDisplayName(request) {
768
+ const result = await this.run(client => client.updateDisplayName(request));
769
+ if (result.ok)
770
+ this.activeIdentityDid = result.value.did;
771
+ return result;
610
772
  }
611
773
  /**
612
774
  * Resolve one Handle or DID before the browser opens a direct chat.
@@ -616,6 +778,32 @@ let AwikiService = (() => {
616
778
  resolvePeer(request) {
617
779
  return this.run(client => client.resolvePeer(request.peer));
618
780
  }
781
+ /**
782
+ * Create one group, then settle every initial-member invitation without hiding a created group.
783
+ * @param request - bounded group name and initial Handle/DID values.
784
+ * @returns The created conversation and per-member outcomes.
785
+ */
786
+ async createGroup(request) {
787
+ const normalized = normalizeCreateGroupRequest(request);
788
+ if (normalized === undefined)
789
+ return { ok: false, error: failure('invalid-request') };
790
+ const created = await this.run(client => client.createGroup(normalized.name));
791
+ if (!created.ok)
792
+ return created;
793
+ const addedMembers = [];
794
+ const failedMembers = [];
795
+ for (const member of normalized.members) {
796
+ const result = await this.run(client => client.addGroupMember(created.value.groupDid, member));
797
+ if (result.ok)
798
+ addedMembers.push(result.value);
799
+ else
800
+ failedMembers.push({ member, error: result.error });
801
+ }
802
+ return {
803
+ ok: true,
804
+ value: { conversation: created.value, addedMembers, failedMembers },
805
+ };
806
+ }
619
807
  /**
620
808
  * List direct and existing group conversations.
621
809
  * @param request - Optional opaque cursor and page limit.
@@ -632,6 +820,10 @@ let AwikiService = (() => {
632
820
  getHistory(request) {
633
821
  return this.run(client => client.getHistory(request));
634
822
  }
823
+ /** Read one committed local conversation page without sync, history, or Directory RPC. */
824
+ getLocalHistory(request) {
825
+ return this.run(client => client.getLocalHistory(request));
826
+ }
635
827
  /**
636
828
  * Read real AWiki history, enforce range and byte caps, then invoke the configured model once.
637
829
  * @param request - selected conversation and its unread snapshot at open time.
@@ -747,16 +939,29 @@ let AwikiService = (() => {
747
939
  const decoded = decodeAttachment(request.bytesBase64, this.resolved.attachmentMaxBytes);
748
940
  if (!decoded.ok)
749
941
  return decoded;
750
- return this.run(client => client.sendAttachment({
751
- target: request.target,
752
- attachment: {
753
- fileName: request.fileName,
754
- mimeType: request.mimeType,
755
- bytes: decoded.value,
756
- },
757
- ...request.caption === undefined ? {} : { caption: request.caption },
758
- idempotencyKey: request.idempotencyKey,
759
- }), { skipAttachmentByteValidation: true });
942
+ return this.run(async (client) => {
943
+ const message = await client.sendAttachment({
944
+ target: request.target,
945
+ attachment: {
946
+ fileName: request.fileName,
947
+ mimeType: request.mimeType,
948
+ bytes: decoded.value,
949
+ },
950
+ ...request.caption === undefined ? {} : { caption: request.caption },
951
+ idempotencyKey: request.idempotencyKey,
952
+ });
953
+ if (message.content.kind === 'attachment') {
954
+ const ownerDid = this.activeIdentityDid ?? (await client.getIdentity().catch(() => null))?.did;
955
+ if (ownerDid !== undefined) {
956
+ this.activeIdentityDid = ownerDid;
957
+ await this.imageAttachmentCache.write(ownerDid, message.id, {
958
+ attachment: message.content.attachment,
959
+ bytes: decoded.value,
960
+ }).catch(() => undefined);
961
+ }
962
+ }
963
+ return message;
964
+ }, { skipAttachmentByteValidation: true });
760
965
  }
761
966
  /**
762
967
  * Download and encode one provider-verified attachment.
@@ -764,16 +969,64 @@ let AwikiService = (() => {
764
969
  * @returns Verified public metadata and canonical Base64 bytes, or a closed failure.
765
970
  */
766
971
  async downloadAttachment(request) {
767
- const result = await this.run(client => client.downloadAttachment(request), { skipAttachmentByteValidation: true });
972
+ try {
973
+ if (await this.isSignedOut())
974
+ return { ok: false, error: failure('signed-out') };
975
+ }
976
+ catch {
977
+ return { ok: false, error: failure('remote') };
978
+ }
979
+ if (this.provider !== undefined && this.activeIdentityDid !== undefined) {
980
+ const cached = await this.imageAttachmentCache.read(this.activeIdentityDid, request);
981
+ if (cached !== undefined)
982
+ return this.publicDownloadedAttachment(cached);
983
+ }
984
+ const result = await this.run(async (client) => {
985
+ const identity = this.activeIdentityDid === undefined ? await client.getIdentity() : undefined;
986
+ const ownerDid = this.activeIdentityDid ?? identity?.did;
987
+ if (ownerDid === undefined)
988
+ throw Object.assign(new Error('not registered'), { name: 'AwikiSdkError', code: 'not-registered' });
989
+ this.activeIdentityDid = ownerDid;
990
+ const cached = await this.imageAttachmentCache.read(ownerDid, request);
991
+ if (cached !== undefined)
992
+ return cached;
993
+ const downloaded = await client.downloadAttachment(request);
994
+ await this.imageAttachmentCache.write(ownerDid, request.messageId, downloaded).catch(() => undefined);
995
+ return downloaded;
996
+ }, { skipAttachmentByteValidation: true });
768
997
  if (!result.ok)
769
998
  return result;
770
- if (result.value.bytes.byteLength > this.resolved.attachmentMaxBytes) {
999
+ return this.publicDownloadedAttachment(result.value);
1000
+ }
1001
+ /** Revalidate cached/provider bytes before crossing the browser Remote boundary. */
1002
+ publicDownloadedAttachment(value) {
1003
+ if (value.bytes.byteLength > this.resolved.attachmentMaxBytes) {
771
1004
  return { ok: false, error: failure('attachment-too-large') };
772
1005
  }
773
- if (result.value.bytes.byteLength !== result.value.attachment.size) {
1006
+ if (value.bytes.byteLength !== value.attachment.size) {
774
1007
  return { ok: false, error: failure('remote') };
775
1008
  }
776
- return { ok: true, value: downloadedAttachment(result.value) };
1009
+ return { ok: true, value: downloadedAttachment(value) };
1010
+ }
1011
+ /** Return the deployment identity's public mailbox state. */
1012
+ getMailAccount() {
1013
+ return this.run(client => client.getMailAccount());
1014
+ }
1015
+ /** List one bounded mailbox page on explicit browser/tool demand. */
1016
+ listMailInbox(request) {
1017
+ return this.runValidatedMail(() => mailInboxRequest(request ?? {}), (client, normalized) => client.listMailInbox(normalized));
1018
+ }
1019
+ /** Read one bounded plain-text mail message. */
1020
+ readMail(request) {
1021
+ return this.runValidatedMail(() => mailReadRequest(request), (client, normalized) => client.readMail(normalized));
1022
+ }
1023
+ /** Mark explicitly selected mail messages read. Browser callers require an explicit click. */
1024
+ markMailRead(request) {
1025
+ return this.runValidatedMail(() => mailMarkReadRequest(request), (client, normalized) => client.markMailRead(normalized));
1026
+ }
1027
+ /** Send one plain-text mail once. Browser callers require an explicit confirmation. */
1028
+ sendMail(request) {
1029
+ return this.runValidatedMail(() => mailSendRequest(request), (client, normalized) => client.sendMail(normalized));
777
1030
  }
778
1031
  /**
779
1032
  * Permanently remove the exact SDK-owned local state after an explicit browser acknowledgement.
@@ -786,13 +1039,24 @@ let AwikiService = (() => {
786
1039
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
787
1040
  }
788
1041
  return this.mutateSession(async () => {
1042
+ const provider = this.provider;
1043
+ if (provider !== undefined)
1044
+ await this.stopListener(provider);
789
1045
  const result = await this.run(client => client.clearLocalData(), { allowSignedOut: true });
790
- if (!result.ok)
1046
+ if (!result.ok) {
1047
+ if (provider !== undefined && this.provider === provider)
1048
+ void this.startListener(provider);
791
1049
  return result;
1050
+ }
792
1051
  try {
1052
+ await this.imageAttachmentCache.clear();
793
1053
  await this.sessionStore.signIn();
794
1054
  this.signedOut = false;
1055
+ this.activeIdentityDid = undefined;
795
1056
  this.invalidateSummaries();
1057
+ this.publishSession({ status: 'unregistered' });
1058
+ if (provider !== undefined && this.provider === provider)
1059
+ void this.startListener(provider);
796
1060
  return result;
797
1061
  }
798
1062
  catch {
@@ -807,6 +1071,10 @@ let AwikiService = (() => {
807
1071
  controller.abort();
808
1072
  this.activeSummaryRequests.clear();
809
1073
  }
1074
+ /** Publish a committed session transition to same-process Host consumers. */
1075
+ publishSession(session) {
1076
+ this.hostContext.emit('awiki/session', session);
1077
+ }
810
1078
  /** Invoke the current client and normalize every rejection to a public result. */
811
1079
  async run(operation, options = {}) {
812
1080
  try {
@@ -826,6 +1094,17 @@ let AwikiService = (() => {
826
1094
  return { ok: false, error: normalizeFailure(error) };
827
1095
  }
828
1096
  }
1097
+ /** Validate mail input before entering the provider and preserve fixed public failures. */
1098
+ runValidatedMail(validate, operation) {
1099
+ let request;
1100
+ try {
1101
+ request = validate();
1102
+ }
1103
+ catch {
1104
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
1105
+ }
1106
+ return this.run(client => operation(client, request));
1107
+ }
829
1108
  /** Read and cache the private Host-owned session marker. */
830
1109
  async isSignedOut() {
831
1110
  this.signedOut ??= await this.sessionStore.isSignedOut();
@@ -879,11 +1158,163 @@ let AwikiService = (() => {
879
1158
  this.sessionMutation = pending.then(() => undefined, () => undefined);
880
1159
  return pending;
881
1160
  }
1161
+ /** Start one exact identity-bound listener, atomically releasing a failed startup. */
1162
+ startListener(provider) {
1163
+ if (!this.resolved.listenerEnabled
1164
+ || provider.listener !== undefined
1165
+ || provider.listenerRecoveryBlocked
1166
+ || this.provider !== provider) {
1167
+ return Promise.resolve();
1168
+ }
1169
+ if (provider.listenerStartup !== undefined)
1170
+ return provider.listenerStartup;
1171
+ if (provider.listenerCleanup !== undefined || provider.listenerRestartTimer !== undefined)
1172
+ return Promise.resolve();
1173
+ const workspaceContext = this.workspaceContext;
1174
+ const source = provider.client.listener;
1175
+ if (workspaceContext === undefined || source === undefined)
1176
+ return Promise.resolve();
1177
+ const generation = provider.listenerGeneration;
1178
+ const logger = this.ctx.logger('awiki-listener');
1179
+ const startup = (async () => {
1180
+ if (await this.isSignedOut())
1181
+ return;
1182
+ if (await provider.client.getIdentity() === null)
1183
+ return;
1184
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation))
1185
+ return;
1186
+ const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
1187
+ const listener = new AwikiAgentListener(source, agents, this.resolved.listener, logger);
1188
+ provider.listener = listener;
1189
+ try {
1190
+ await listener.start();
1191
+ }
1192
+ catch (error) {
1193
+ if (provider.listener === listener)
1194
+ delete provider.listener;
1195
+ try {
1196
+ await listener.dispose();
1197
+ }
1198
+ catch {
1199
+ provider.listenerRecoveryBlocked = true;
1200
+ }
1201
+ if (!provider.listenerRecoveryBlocked && provider.listenerRestartAttempt > 0) {
1202
+ this.scheduleListenerRestart(provider, workspaceContext, generation);
1203
+ }
1204
+ throw error;
1205
+ }
1206
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation)) {
1207
+ if (provider.listener === listener)
1208
+ delete provider.listener;
1209
+ try {
1210
+ await listener.dispose();
1211
+ }
1212
+ catch (error) {
1213
+ provider.listenerRecoveryBlocked = true;
1214
+ throw error;
1215
+ }
1216
+ return;
1217
+ }
1218
+ provider.listenerStartedAt = Date.now();
1219
+ void listener.whenTerminated().then((result) => {
1220
+ if (result.kind === 'failed') {
1221
+ this.releaseFailedListener(provider, listener, workspaceContext, generation, result.error);
1222
+ }
1223
+ });
1224
+ })();
1225
+ const observed = startup
1226
+ .catch((error) => {
1227
+ logger.warn('AWiki listener startup failed: %s', error instanceof Error ? error.message : 'unknown failure');
1228
+ })
1229
+ .finally(() => {
1230
+ if (provider.listenerStartup === observed)
1231
+ delete provider.listenerStartup;
1232
+ });
1233
+ provider.listenerStartup = observed;
1234
+ return observed;
1235
+ }
1236
+ async stopListener(provider) {
1237
+ provider.listenerGeneration += 1;
1238
+ provider.listenerRestartAttempt = 0;
1239
+ delete provider.listenerStartedAt;
1240
+ const timer = provider.listenerRestartTimer;
1241
+ if (timer !== undefined) {
1242
+ delete provider.listenerRestartTimer;
1243
+ clearTimeout(timer);
1244
+ }
1245
+ await provider.listenerStartup;
1246
+ await provider.listenerCleanup;
1247
+ const listener = provider.listener;
1248
+ if (listener === undefined)
1249
+ return;
1250
+ delete provider.listener;
1251
+ try {
1252
+ await listener.dispose();
1253
+ }
1254
+ catch (error) {
1255
+ provider.listenerRecoveryBlocked = true;
1256
+ throw error;
1257
+ }
1258
+ }
1259
+ listenerFenceMatches(provider, workspaceContext, generation) {
1260
+ return this.provider === provider
1261
+ && this.workspaceContext === workspaceContext
1262
+ && provider.listenerGeneration === generation;
1263
+ }
1264
+ releaseFailedListener(provider, listener, workspaceContext, generation, error) {
1265
+ if (provider.listener !== listener)
1266
+ return;
1267
+ delete provider.listener;
1268
+ const startedAt = provider.listenerStartedAt;
1269
+ delete provider.listenerStartedAt;
1270
+ if (startedAt !== undefined && Date.now() - startedAt >= LISTENER_STABLE_RESET_MS) {
1271
+ provider.listenerRestartAttempt = 0;
1272
+ }
1273
+ const logger = this.ctx.logger('awiki-listener');
1274
+ logger.warn('AWiki listener lifecycle failed: %s', error instanceof Error ? error.message : 'unknown failure');
1275
+ let disposed = false;
1276
+ const cleanup = listener.dispose()
1277
+ .then(() => { disposed = true; }, (cleanupError) => {
1278
+ provider.listenerRecoveryBlocked = true;
1279
+ logger.warn('AWiki listener cleanup failed: %s', cleanupError instanceof Error ? cleanupError.message : 'unknown failure');
1280
+ });
1281
+ const observed = cleanup.finally(() => {
1282
+ if (provider.listenerCleanup === observed)
1283
+ delete provider.listenerCleanup;
1284
+ if (disposed)
1285
+ this.scheduleListenerRestart(provider, workspaceContext, generation);
1286
+ });
1287
+ provider.listenerCleanup = observed;
1288
+ }
1289
+ scheduleListenerRestart(provider, workspaceContext, generation) {
1290
+ if (provider.listenerRestartTimer !== undefined
1291
+ || !this.listenerFenceMatches(provider, workspaceContext, generation))
1292
+ return;
1293
+ provider.listenerRestartAttempt = Math.min(provider.listenerRestartAttempt + 1, LISTENER_RESTART_MAX_ATTEMPT);
1294
+ const exponent = provider.listenerRestartAttempt - 1;
1295
+ const delay = Math.min(LISTENER_RESTART_BASE_DELAY_MS * 2 ** exponent, LISTENER_RESTART_MAX_DELAY_MS);
1296
+ const timer = setTimeout(() => {
1297
+ if (provider.listenerRestartTimer !== timer)
1298
+ return;
1299
+ delete provider.listenerRestartTimer;
1300
+ if (!this.listenerFenceMatches(provider, workspaceContext, generation))
1301
+ return;
1302
+ void this.startListener(provider);
1303
+ }, delay);
1304
+ provider.listenerRestartTimer = timer;
1305
+ }
882
1306
  /** Clear one exact provider slot before joining its one shared disposal. */
883
1307
  disposeProvider(provider) {
884
1308
  if (this.provider === provider)
885
1309
  this.provider = undefined;
886
- provider.disposal ??= provider.client.dispose();
1310
+ provider.disposal ??= (async () => {
1311
+ try {
1312
+ await this.stopListener(provider);
1313
+ }
1314
+ finally {
1315
+ await provider.client.dispose();
1316
+ }
1317
+ })();
887
1318
  return provider.disposal;
888
1319
  }
889
1320
  };