@xmanrui/dsh-im 4.22.0 → 4.24.0

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 (148) hide show
  1. package/README.en.md +5 -4
  2. package/README.md +5 -4
  3. package/lib/client.js +2584 -1153
  4. package/lib/index.js +290 -298
  5. package/package.json +14 -2
  6. package/plugin-src/client/channel-card-meta.js +2 -0
  7. package/plugin-src/client/channel-logos.js +11 -0
  8. package/plugin-src/client/channels/dingtalk/api.js +5 -1
  9. package/plugin-src/client/channels/dingtalk/index.js +15 -13
  10. package/plugin-src/client/channels/email/api.js +52 -0
  11. package/plugin-src/client/channels/email/index.js +728 -0
  12. package/plugin-src/client/channels/email/styles.js +51 -0
  13. package/plugin-src/client/channels/feishu/api.js +4 -2
  14. package/plugin-src/client/channels/feishu/index.js +23 -7
  15. package/plugin-src/client/channels/imessage/index.js +2 -1
  16. package/plugin-src/client/channels/office/api.js +2 -0
  17. package/plugin-src/client/channels/office/index.js +6 -5
  18. package/plugin-src/client/channels/qq/api.js +7 -0
  19. package/plugin-src/client/channels/qq/index.js +16 -3
  20. package/plugin-src/client/channels/shared/token-api.js +8 -1
  21. package/plugin-src/client/channels/shared/token-channel.js +28 -3
  22. package/plugin-src/client/channels/slack/index.js +2 -1
  23. package/plugin-src/client/channels/telegram/index.js +3 -0
  24. package/plugin-src/client/channels/telegram/styles.js +12 -0
  25. package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
  26. package/plugin-src/client/channels/wecom/api.js +7 -1
  27. package/plugin-src/client/channels/wecom/index.js +16 -3
  28. package/plugin-src/client/channels/wecom-app/api.js +6 -1
  29. package/plugin-src/client/channels/wecom-app/index.js +16 -3
  30. package/plugin-src/client/channels/weixin/api.js +2 -1
  31. package/plugin-src/client/channels/weixin/connection-error.js +1 -71
  32. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  33. package/plugin-src/client/channels/whatsapp/index.js +16 -3
  34. package/plugin-src/client/connection-error.js +87 -0
  35. package/plugin-src/client/global-settings.js +81 -1
  36. package/plugin-src/client/i18n.js +69 -1
  37. package/plugin-src/client/index.js +40 -4
  38. package/plugin-src/client/last-message-error.js +2 -1
  39. package/plugin-src/client/styles.js +8 -2
  40. package/plugin-src/host/build.mjs +4 -0
  41. package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
  42. package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
  43. package/plugin-src/host/channels/email/availability.mjs +60 -0
  44. package/plugin-src/host/channels/email/index.mjs +36 -0
  45. package/plugin-src/host/channels/email/production.mjs +58 -0
  46. package/plugin-src/host/channels/email/rpc.mjs +163 -0
  47. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  48. package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
  49. package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
  50. package/plugin-src/host/channels/office/rpc.mjs +3 -1
  51. package/plugin-src/host/channels/qq/production.mjs +2 -0
  52. package/plugin-src/host/channels/qq/rpc.mjs +5 -3
  53. package/plugin-src/host/channels/shared/production.mjs +52 -5
  54. package/plugin-src/host/channels/shared/rpc.mjs +14 -3
  55. package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
  56. package/plugin-src/host/channels/shared/startup.mjs +11 -6
  57. package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
  58. package/plugin-src/host/channels/slack/production.mjs +2 -0
  59. package/plugin-src/host/channels/slack/rpc.mjs +5 -3
  60. package/plugin-src/host/channels/wecom/production.mjs +2 -0
  61. package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
  62. package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
  63. package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
  64. package/plugin-src/host/channels/weixin/production.mjs +2 -0
  65. package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
  66. package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
  67. package/plugin-src/host/delivery-adapter.mjs +11 -0
  68. package/plugin-src/host/image-input-rpc.mjs +24 -0
  69. package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
  70. package/plugin-src/host/index.mjs +3 -0
  71. package/plugin-src/host/modern-harness-api.mjs +7 -2
  72. package/plugin-src/management-rpc.mjs +12 -2
  73. package/scripts/verify-package.mjs +13 -5
  74. package/src/channels/dingtalk/connection-error.mjs +5 -12
  75. package/src/channels/dingtalk/device-auth.mjs +4 -1
  76. package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
  77. package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
  78. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
  79. package/src/channels/discord/discord-api.mjs +3 -3
  80. package/src/channels/discord/discord-runtime.mjs +18 -12
  81. package/src/channels/email/config-store.mjs +194 -0
  82. package/src/channels/email/email-api.mjs +20 -0
  83. package/src/channels/email/email-bridge.mjs +17 -0
  84. package/src/channels/email/email-controller.mjs +824 -0
  85. package/src/channels/email/email-runtime.mjs +699 -0
  86. package/src/channels/email/harness-client.mjs +7 -0
  87. package/src/channels/email/mail-format.mjs +91 -0
  88. package/src/channels/email/state-store.mjs +176 -0
  89. package/src/channels/email/transport.mjs +64 -0
  90. package/src/channels/email/transports/agent-mail.mjs +604 -0
  91. package/src/channels/email/transports/agently-cli.mjs +288 -0
  92. package/src/channels/email/transports/imap-smtp.mjs +165 -0
  93. package/src/channels/feishu/bridge.mjs +39 -11
  94. package/src/channels/feishu/feishu-channel.mjs +35 -0
  95. package/src/channels/feishu/feishu-runtime.mjs +15 -8
  96. package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
  97. package/src/channels/feishu/live-cot.mjs +260 -0
  98. package/src/channels/feishu/message-utils.mjs +3 -2
  99. package/src/channels/feishu/multi-bot-controller.mjs +56 -47
  100. package/src/channels/feishu/registration-manager.mjs +8 -4
  101. package/src/channels/feishu/repair-manager.mjs +2 -1
  102. package/src/channels/feishu/slash-command-registry.mjs +17 -0
  103. package/src/channels/feishu/step-push-mode.mjs +10 -4
  104. package/src/channels/imessage/runtime.mjs +9 -5
  105. package/src/channels/office/office-controller.mjs +23 -12
  106. package/src/channels/office/office-runtime.mjs +6 -3
  107. package/src/channels/office/office-transport.mjs +1 -0
  108. package/src/channels/qq/qq-bridge.mjs +7 -5
  109. package/src/channels/qq/qq-controller.mjs +56 -42
  110. package/src/channels/qq/qq-runtime.mjs +16 -11
  111. package/src/channels/shared/bot-workspace-store.mjs +29 -10
  112. package/src/channels/shared/connection-error.mjs +191 -0
  113. package/src/channels/shared/connection-test.mjs +3 -1
  114. package/src/channels/shared/conversation-state-store.mjs +46 -1
  115. package/src/channels/shared/diagnostic-details.mjs +95 -0
  116. package/src/channels/shared/harness-client.mjs +287 -57
  117. package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
  118. package/src/channels/shared/i18n-en/email.mjs +25 -0
  119. package/src/channels/shared/i18n-en/image-input.mjs +17 -0
  120. package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
  121. package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
  122. package/src/channels/shared/i18n-en.mjs +6 -0
  123. package/src/channels/shared/image-input-policy.mjs +35 -0
  124. package/src/channels/shared/image-input-settings-store.mjs +60 -0
  125. package/src/channels/shared/image-input.mjs +124 -0
  126. package/src/channels/shared/image-prompt.mjs +15 -7
  127. package/src/channels/shared/message-failure.mjs +12 -0
  128. package/src/channels/shared/semantic/reply-reference.mjs +3 -3
  129. package/src/channels/shared/text-harness-bridge.mjs +150 -25
  130. package/src/channels/shared/token-bot-controller.mjs +52 -47
  131. package/src/channels/slack/slack-api.mjs +3 -3
  132. package/src/channels/slack/slack-controller.mjs +59 -51
  133. package/src/channels/slack/slack-runtime.mjs +18 -12
  134. package/src/channels/telegram/config-store.mjs +4 -1
  135. package/src/channels/telegram/telegram-api.mjs +2 -2
  136. package/src/channels/telegram/telegram-controller.mjs +13 -1
  137. package/src/channels/telegram/telegram-runtime.mjs +215 -14
  138. package/src/channels/wecom/wecom-bridge.mjs +15 -6
  139. package/src/channels/wecom/wecom-controller.mjs +51 -37
  140. package/src/channels/wecom/wecom-runtime.mjs +10 -6
  141. package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
  142. package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
  143. package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
  144. package/src/channels/weixin/connection-error.mjs +26 -92
  145. package/src/channels/weixin/diagnostic-details.mjs +1 -63
  146. package/src/channels/weixin/weixin-bridge.mjs +7 -5
  147. package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
  148. package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
@@ -1,3 +1,4 @@
1
+ import { extractConnectionEvidence, atConnectionStage, createConnectionDiagnostics } from '../shared/connection-error.mjs';
1
2
  import { connectionTestMessage } from '../shared/connection-test.mjs';
2
3
  import { publicMessageFailure } from '../shared/message-failure.mjs';
3
4
  import { t } from '../shared/i18n.mjs';
@@ -20,6 +21,7 @@ export class SlackController {
20
21
  #createRuntime;
21
22
  #deleteState;
22
23
  #logger;
24
+ #diagnostics;
23
25
  #runtimes = new Map();
24
26
  #errors = new Map();
25
27
  #transitions = new Map();
@@ -51,6 +53,14 @@ export class SlackController {
51
53
  this.#createRuntime = createRuntime;
52
54
  this.#deleteState = deleteState;
53
55
  this.#logger = logger;
56
+ this.#diagnostics = createConnectionDiagnostics({ channel: 'slack', logger });
57
+ }
58
+
59
+ get diagnostics() { return this.#diagnostics; }
60
+
61
+ #failure(error, code, message) {
62
+ const stage = code.startsWith('qr-') ? 'qr.begin' : code === 'activation-failed' ? 'activation' : 'connection.start';
63
+ return this.#diagnostics.report(error, { reuse: true, stage, publicError: { code, message } }).publicError;
54
64
  }
55
65
 
56
66
  async initialize() {
@@ -58,26 +68,23 @@ export class SlackController {
58
68
  for (const config of this.#configStore.list()) {
59
69
  await this.#withBotTransition(config.botId, async () => {
60
70
  if (this.#closed || this.#runtimes.get(config.botId)?.status?.ready) return;
61
- const resolved = await this.#resolveCredentials(config);
62
- if (!resolved) {
63
- this.#errors.set(config.botId, safeError(
64
- 'missing-token',
65
- t('Slack机器人凭据缺失,请移除后重新接入。'),
66
- ));
67
- return;
68
- }
69
71
  try {
72
+ const resolved = await this.#resolveCredentials(config);
73
+ if (!resolved) {
74
+ this.#errors.set(config.botId, safeError(
75
+ 'missing-token',
76
+ t('Slack机器人凭据缺失,请移除后重新接入。'),
77
+ ));
78
+ return;
79
+ }
70
80
  await this.#startRuntime(config, resolved);
71
81
  this.#errors.delete(config.botId);
72
82
  } catch (error) {
73
- this.#errors.set(config.botId, safeError(
83
+ this.#errors.set(config.botId, this.#failure(error,
74
84
  'connection-failed',
75
85
  t('Slack Socket Mode 连接未就绪,插件会自动重试。'),
76
86
  ));
77
- this.#logger.warn?.(
78
- `[dsh-im:slack] bot ${config.botId} failed to initialize:`,
79
- error,
80
- );
87
+
81
88
  } finally {
82
89
  this.#touch();
83
90
  }
@@ -105,8 +112,8 @@ export class SlackController {
105
112
  await this.#withBotTransition(identity.botId, async () => {
106
113
  if (this.#closed) throw new Error('Slack controller is closed');
107
114
  const previousConfig = this.#configStore.getByPlatformId(platformId);
108
- const previousBotToken = await this.#credentials.resolve(identity.botTokenRef).catch(() => undefined);
109
- const previousAppToken = await this.#credentials.resolve(identity.appTokenRef).catch(() => undefined);
115
+ const previousBotToken = await atConnectionStage('credential.read', () => this.#credentials.resolve(identity.botTokenRef), 'credential-store');
116
+ const previousAppToken = await atConnectionStage('credential.read', () => this.#credentials.resolve(identity.appTokenRef), 'credential-store');
110
117
  const config = {
111
118
  ...identity,
112
119
  platformId,
@@ -118,9 +125,9 @@ export class SlackController {
118
125
  connectedAt: new Date().toISOString(),
119
126
  };
120
127
  try {
121
- await this.#credentials.set(identity.botTokenRef, normalizedBotToken);
122
- await this.#credentials.set(identity.appTokenRef, normalizedAppToken);
123
- await this.#configStore.save(config);
128
+ await atConnectionStage('credential.save', () => this.#credentials.set(identity.botTokenRef, normalizedBotToken), 'credential-store');
129
+ await atConnectionStage('credential.save', () => this.#credentials.set(identity.appTokenRef, normalizedAppToken), 'credential-store');
130
+ await atConnectionStage('account.save', () => this.#configStore.save(config), 'account-config');
124
131
  } catch (error) {
125
132
  await Promise.all([
126
133
  this.#restoreCredential(identity.botTokenRef, previousBotToken),
@@ -135,14 +142,11 @@ export class SlackController {
135
142
  });
136
143
  this.#errors.delete(identity.botId);
137
144
  } catch (error) {
138
- this.#errors.set(identity.botId, safeError(
145
+ this.#errors.set(identity.botId, this.#failure(error,
139
146
  'connection-failed',
140
147
  t('Slack机器人已接入,Socket Mode 连接暂未就绪。'),
141
148
  ));
142
- this.#logger.warn?.(
143
- `[dsh-im:slack] bot ${identity.botId} credential connection failed:`,
144
- error,
145
- );
149
+
146
150
  }
147
151
  this.#touch();
148
152
  });
@@ -159,7 +163,7 @@ export class SlackController {
159
163
  await this.#startRuntime(config, resolved);
160
164
  this.#errors.delete(botId);
161
165
  } catch (error) {
162
- this.#errors.set(botId, safeError(
166
+ this.#errors.set(botId, this.#failure(error,
163
167
  'connection-failed',
164
168
  t('Slack Socket Mode 连接仍未就绪,请检查两个 Token。'),
165
169
  ));
@@ -204,36 +208,40 @@ export class SlackController {
204
208
  }
205
209
 
206
210
  async deleteBot(botId) {
211
+ const warnings = [];
207
212
  const config = this.#configStore.get(botId);
208
213
  if (!config) throw new Error('Unknown Slack bot');
209
214
  await this.#withBotTransition(botId, async () => {
210
- const previousBotToken = await this.#credentials.resolve(config.botTokenRef).catch(() => undefined);
211
- const previousAppToken = await this.#credentials.resolve(config.appTokenRef).catch(() => undefined);
215
+ const previousBotToken = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.botTokenRef), 'credential-store');
216
+ const previousAppToken = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.appTokenRef), 'credential-store');
212
217
  await this.#stopRuntime(botId);
213
218
  try {
214
- await this.#credentials.unset(config.botTokenRef);
215
- await this.#credentials.unset(config.appTokenRef);
216
- await this.#configStore.remove(botId);
219
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(config.botTokenRef), 'credential-store');
220
+ await atConnectionStage('credential.remove', () => this.#credentials.unset(config.appTokenRef), 'credential-store');
221
+ await atConnectionStage('account.remove', () => this.#configStore.remove(botId), 'account-config');
217
222
  } catch (error) {
218
- await Promise.all([
219
- this.#restoreCredential(config.botTokenRef, previousBotToken),
220
- this.#restoreCredential(config.appTokenRef, previousAppToken),
221
- ]);
222
- if (previousBotToken?.value && previousAppToken?.value) {
223
- await this.#startRuntime(config, {
224
- botToken: previousBotToken.value,
225
- appToken: previousAppToken.value,
226
- }).catch(() => undefined);
223
+ if (!this.#configStore.get(botId)) {
224
+ warnings.push(this.#diagnostics.report(error, { operation: 'bot.delete', stage: 'workspace.cleanup', warning: true,
225
+ publicError: { code: 'workspace-cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError);
226
+ } else {
227
+ await Promise.all([
228
+ this.#restoreCredential(config.botTokenRef, previousBotToken),
229
+ this.#restoreCredential(config.appTokenRef, previousAppToken),
230
+ ]);
231
+ if (previousBotToken?.value && previousAppToken?.value) {
232
+ await this.#startRuntime(config, {
233
+ botToken: previousBotToken.value,
234
+ appToken: previousAppToken.value,
235
+ }).catch(() => undefined);
236
+ }
237
+ throw new Error('Unable to remove the Slack bot safely.', { cause: error });
227
238
  }
228
- throw new Error('Unable to remove the Slack bot safely.', { cause: error });
229
239
  }
230
- await this.#deleteState({ botId, config }).catch((error) => {
231
- this.#logger.warn?.(`[dsh-im:slack] bot ${botId} state cleanup failed:`, error);
232
- });
240
+ await this.#deleteState({ botId, config }).catch(error => { warnings.push(this.#diagnostics.report(error, { reuse: true, operation: 'bot.delete', stage: 'state.cleanup', resource: 'account-state', warning: true, publicError: { code: 'cleanup-failed', message: '账号已移除,但本地状态清理失败。' } }).publicError); });
233
241
  this.#errors.delete(botId);
234
242
  this.#touch();
235
243
  });
236
- return this.status();
244
+ return { ...this.status(), ...(warnings.length ? { warnings } : {}) };
237
245
  }
238
246
 
239
247
  status() {
@@ -270,7 +278,7 @@ export class SlackController {
270
278
  messagesReplied: runtimeStatus?.messagesReplied ?? 0,
271
279
  },
272
280
  lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
273
- error: structuredClone(this.#errors.get(config.botId) ?? null),
281
+ error: structuredClone(runtimeStatus?.error ?? this.#errors.get(config.botId) ?? null),
274
282
  };
275
283
  });
276
284
  const connected = bots.filter((bot) => bot.connected).length;
@@ -296,12 +304,12 @@ export class SlackController {
296
304
  if (this.#closed) throw new Error('Slack controller is closed');
297
305
  await this.#stopRuntime(config.botId);
298
306
  if (this.#closed) throw new Error('Slack controller is closed');
299
- const runtime = await this.#createRuntime({
307
+ const runtime = await atConnectionStage('runtime.prepare', () => this.#createRuntime({
300
308
  botId: config.botId,
301
309
  config,
302
310
  botToken,
303
311
  appToken,
304
- });
312
+ }));
305
313
  if (!runtime || typeof runtime.start !== 'function' || typeof runtime.stop !== 'function') {
306
314
  throw new TypeError('createRuntime returned an invalid Slack runtime');
307
315
  }
@@ -319,14 +327,14 @@ export class SlackController {
319
327
  const runtime = this.#runtimes.get(botId);
320
328
  this.#runtimes.delete(botId);
321
329
  await runtime?.stop().catch((error) => {
322
- this.#logger.warn?.(`[dsh-im:slack] bot ${botId} failed to stop cleanly:`, error);
330
+ this.#logger.warn?.(`[dsh-im:slack] bot ${botId} failed to stop cleanly:`, extractConnectionEvidence(error).details);
323
331
  });
324
332
  }
325
333
 
326
334
  async #resolveCredentials(config) {
327
335
  const [bot, app] = await Promise.all([
328
- this.#credentials.resolve(config.botTokenRef).catch(() => undefined),
329
- this.#credentials.resolve(config.appTokenRef).catch(() => undefined),
336
+ atConnectionStage('credential.read', () => this.#credentials.resolve(config.botTokenRef), 'credential-store'),
337
+ atConnectionStage('credential.read', () => this.#credentials.resolve(config.appTokenRef), 'credential-store'),
330
338
  ]);
331
339
  const botToken = cleanString(bot?.value);
332
340
  const appToken = cleanString(app?.value);
@@ -334,8 +342,8 @@ export class SlackController {
334
342
  }
335
343
 
336
344
  async #restoreCredential(ref, previous) {
337
- if (previous?.value) await this.#credentials.set(ref, previous.value).catch(() => undefined);
338
- else await this.#credentials.unset(ref).catch(() => undefined);
345
+ if (previous?.value) await atConnectionStage('credential.save', () => this.#credentials.set(ref, previous.value), 'credential-store').catch(() => undefined);
346
+ else await atConnectionStage('credential.remove', () => this.#credentials.unset(ref), 'credential-store').catch(() => undefined);
339
347
  }
340
348
 
341
349
  #withBotTransition(botId, operation) {
@@ -1,3 +1,4 @@
1
+ import { extractConnectionEvidence, createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
1
2
  import { splitMessageText } from '../shared/editable-message-stream.mjs';
2
3
  import { t } from '../shared/i18n.mjs';
3
4
  import { SlackApi } from './slack-api.mjs';
@@ -410,6 +411,7 @@ export class SlackRuntime {
410
411
  #contextEnhancement;
411
412
  #accessPolicy;
412
413
  #logger;
414
+ #diagnostics;
413
415
  #replyTimeoutMs;
414
416
  #connectTimeoutMs;
415
417
  #createApi;
@@ -451,7 +453,7 @@ export class SlackRuntime {
451
453
  this.#state = state;
452
454
  this.#contextEnhancement = contextEnhancement;
453
455
  this.#accessPolicy = accessPolicy;
454
- this.#logger = logger;
456
+ this.#logger = logger; this.#diagnostics = createConnectionDiagnostics({ channel: 'slack', logger });
455
457
  this.#replyTimeoutMs = replyTimeoutMs;
456
458
  this.#connectTimeoutMs = connectTimeoutMs;
457
459
  this.#createApi = createApi;
@@ -510,8 +512,8 @@ export class SlackRuntime {
510
512
  this.#reconnectAttempt = 0;
511
513
  this.#status.startedAt = new Date().toISOString();
512
514
  this.#status.connectionState = 'connecting';
513
- this.#status.lastError = null;
514
- await this.#harness.ensureRunning();
515
+ this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
516
+ await atConnectionStage('harness.check', () => this.#harness.ensureRunning());
515
517
  this.#status.harnessReachable = true;
516
518
  const controller = new AbortController();
517
519
  this.#abortController = controller;
@@ -540,7 +542,7 @@ export class SlackRuntime {
540
542
  this.#connect(),
541
543
  new Promise((_, reject) => {
542
544
  timer = setTimeout(
543
- () => reject(new Error('Slack Socket Mode did not become ready in time')),
545
+ () => reject(Object.assign(new Error('Slack Socket Mode did not become ready in time'), { code: 'ETIMEDOUT', timeoutMs: this.#connectTimeoutMs })),
544
546
  this.#connectTimeoutMs,
545
547
  );
546
548
  timer?.unref?.();
@@ -553,7 +555,8 @@ export class SlackRuntime {
553
555
  } catch (error) {
554
556
  this.#status.ready = false;
555
557
  this.#status.connectionState = 'failed';
556
- this.#status.lastError = error?.message ?? String(error);
558
+ this.#status.error = this.#diagnostics.report(error, { operation: 'connection.restore', reuse: true, botId: this.#config?.botId, automatic: true }).publicError;
559
+ this.#status.lastError = this.#status.error.message;
557
560
  await this.stop();
558
561
  throw error;
559
562
  }
@@ -582,7 +585,7 @@ export class SlackRuntime {
582
585
  this.#status.connectionState = 'connected';
583
586
  this.#status.lastCheckedAt = now;
584
587
  this.#status.lastConnectedAt = now;
585
- this.#status.lastError = null;
588
+ this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
586
589
  resolve();
587
590
  };
588
591
 
@@ -624,7 +627,7 @@ export class SlackRuntime {
624
627
  if (generation !== this.#generation || this.#stopped) return;
625
628
  this.#logger.error?.(
626
629
  `[dsh-im:slack] bot ${this.#config.botId} message handling failed:`,
627
- error,
630
+ extractConnectionEvidence(error).details,
628
631
  );
629
632
  });
630
633
  }
@@ -641,7 +644,8 @@ export class SlackRuntime {
641
644
  const error = new Error(`Slack Socket Mode closed (${code || 'unknown'})`);
642
645
  this.#status.ready = false;
643
646
  this.#status.connectionState = 'connecting';
644
- this.#status.lastError = error.message;
647
+ this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
648
+ this.#status.lastError = this.#status.error.message;
645
649
  if (!settled) {
646
650
  settled = true;
647
651
  reject(error);
@@ -649,9 +653,11 @@ export class SlackRuntime {
649
653
  this.#scheduleReconnect();
650
654
  });
651
655
 
652
- addSocketListener(socket, 'error', () => {
656
+ addSocketListener(socket, 'error', (event) => {
657
+ const error = event?.error ?? new Error('WebSocket error');
653
658
  if (generation !== this.#generation || this.#stopped) return;
654
- this.#status.lastError = 'Slack Socket Mode WebSocket error';
659
+ this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
660
+ this.#status.lastError = this.#status.error.message;
655
661
  });
656
662
  });
657
663
  }
@@ -664,7 +670,7 @@ export class SlackRuntime {
664
670
  this.#reconnectTimer = null;
665
671
  void this.#connect().catch((error) => {
666
672
  if (this.#stopped) return;
667
- this.#logger.warn?.('[dsh-im:slack] Socket Mode reconnect failed:', error);
673
+ this.#logger.warn?.('[dsh-im:slack] Socket Mode reconnect failed:', extractConnectionEvidence(error).details);
668
674
  this.#scheduleReconnect();
669
675
  });
670
676
  }, delay);
@@ -687,7 +693,7 @@ export class SlackRuntime {
687
693
  try {
688
694
  if (socket && socket.readyState < 2) socket.close(1000, 'Plugin stopped');
689
695
  } catch (error) {
690
- this.#logger.warn?.(`[dsh-im:slack] bot ${this.#config.botId} failed to close Socket Mode:`, error);
696
+ this.#logger.warn?.(`[dsh-im:slack] bot ${this.#config.botId} failed to close Socket Mode:`, extractConnectionEvidence(error).details);
691
697
  }
692
698
  await Promise.race([
693
699
  bridge?.waitForIdle() ?? Promise.resolve(),
@@ -50,12 +50,15 @@ export function normalizeTelegramAccessPolicy(value = {}) {
50
50
  function normalizeTelegramBotExtension(value) {
51
51
  const hasAccessMode = Object.hasOwn(value, 'accessMode');
52
52
  const hasAllowedUsers = Object.hasOwn(value, 'allowedUsers');
53
- if (!hasAccessMode && !hasAllowedUsers) return {};
53
+ const hasThinkingTraces = Object.hasOwn(value, 'thinkingTraces');
54
+ if (!hasAccessMode && !hasAllowedUsers && !hasThinkingTraces) return {};
54
55
  try {
55
56
  const policy = normalizeTelegramAccessPolicy(value);
56
57
  return {
57
58
  ...(hasAccessMode ? { accessMode: policy.accessMode } : {}),
58
59
  ...(hasAllowedUsers || hasAccessMode ? { allowedUsers: policy.allowedUsers } : {}),
60
+ // Explicit field is kept; an absent field stays absent.
61
+ ...(hasThinkingTraces ? { thinkingTraces: value.thinkingTraces === true } : {}),
59
62
  };
60
63
  } catch {
61
64
  return null;
@@ -450,8 +450,8 @@ export class TelegramApi {
450
450
  let body;
451
451
  try {
452
452
  body = await response.json();
453
- } catch {
454
- const error = new Error(`Telegram ${method} returned invalid JSON`);
453
+ } catch (cause) {
454
+ const error = new Error(`Telegram ${method} returned invalid JSON`, { cause });
455
455
  error.status = response?.status;
456
456
  error.code = 'telegram-response-invalid';
457
457
  error.deliveryOutcome = 'unknown';
@@ -28,7 +28,12 @@ export class TelegramController extends TokenBotController {
28
28
  bots: snapshot.bots.map((bot) => {
29
29
  const config = this.#configStore.get(bot.botId);
30
30
  const accessPolicy = normalizeTelegramAccessPolicy(config ?? {});
31
- return { ...bot, accessPolicy };
31
+ return {
32
+ ...bot,
33
+ accessPolicy,
34
+ // Default ON; only an explicit false in the config opts out.
35
+ thinkingTraces: config?.thinkingTraces !== false,
36
+ };
32
37
  }),
33
38
  };
34
39
  }
@@ -37,4 +42,11 @@ export class TelegramController extends TokenBotController {
37
42
  const accessPolicy = normalizeTelegramAccessPolicy(value);
38
43
  return this.updateBotConfig(botId, (config) => ({ ...config, ...accessPolicy }));
39
44
  }
45
+
46
+ async setThinkingTraces(botId, value) {
47
+ return this.updateBotConfig(botId, (config) => ({
48
+ ...config,
49
+ thinkingTraces: value === true,
50
+ }));
51
+ }
40
52
  }