evolcore 0.0.19 → 0.0.21

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 (136) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +60 -9
  3. package/bin/install-codex-managed-hooks.mjs +61 -0
  4. package/dist/agents/baseagent.js +10 -6
  5. package/dist/agents/claude-runner.js +383 -111
  6. package/dist/agents/codex-app-server-client.js +10 -2
  7. package/dist/agents/codex-runner.js +405 -137
  8. package/dist/agents/ecagent-runner.js +174 -63
  9. package/dist/agents/gemini-runner.js +130 -30
  10. package/dist/agents/request-identity.js +25 -0
  11. package/dist/agents/runner-types.js +19 -0
  12. package/dist/aun/aid/agentmd.js +66 -2
  13. package/dist/aun/aid/identity.js +4 -1
  14. package/dist/aun/aid/index.js +1 -1
  15. package/dist/aun/msg/group.js +86 -9
  16. package/dist/aun/msg/history.js +213 -36
  17. package/dist/aun/msg/managed-operation.js +58 -9
  18. package/dist/aun/msg/p2p.js +26 -11
  19. package/dist/aun/outbox.js +295 -68
  20. package/dist/aun/service-proxy.js +43 -25
  21. package/dist/channels/aun.js +1004 -273
  22. package/dist/channels/daemon.js +6 -1
  23. package/dist/cli/agent-command.js +4 -3
  24. package/dist/cli/agent.js +66 -56
  25. package/dist/cli/aun-commands.js +177 -42
  26. package/dist/cli/command-log.js +10 -11
  27. package/dist/cli/contact.js +1 -0
  28. package/dist/cli/daemon-commands.js +81 -121
  29. package/dist/cli/index.js +1 -0
  30. package/dist/cli/init.js +76 -24
  31. package/dist/cli/restart-monitor.js +3 -3
  32. package/dist/cli/task-context.js +46 -0
  33. package/dist/cli/trigger-command.js +1 -1
  34. package/dist/cli/watch-logs.js +10 -3
  35. package/dist/config/builtin-roles.js +1 -0
  36. package/dist/config/config-field-policy.js +16 -5
  37. package/dist/config/config-manager.js +226 -24
  38. package/dist/config/config-operation-service.js +1 -2
  39. package/dist/config/contact-operation-service.js +32 -1
  40. package/dist/config/contact-request-service.js +44 -0
  41. package/dist/config/daemon-services.js +186 -0
  42. package/dist/config/gateway-config.js +29 -16
  43. package/dist/config/lifecycle.js +16 -5
  44. package/dist/config/role-service.js +54 -3
  45. package/dist/config/schema-migration.js +550 -0
  46. package/dist/config-store.js +161 -12
  47. package/dist/core/agent-application-service.js +279 -0
  48. package/dist/core/audit/log-integrity.js +102 -0
  49. package/dist/core/auth/agent-delegation.js +31 -1
  50. package/dist/core/auth/auth-gateway.js +33 -4
  51. package/dist/core/auth/authorization-audit.js +155 -4
  52. package/dist/core/auth/operation-authorizer.js +41 -1
  53. package/dist/core/auth/operation-catalog.js +9 -1
  54. package/dist/core/bootstrap-messages.js +2 -2
  55. package/dist/core/bootstrap-service.js +27 -38
  56. package/dist/core/causation/aun-association.js +7 -4
  57. package/dist/core/channel-loader.js +0 -2
  58. package/dist/core/command/agent-control.js +56 -16
  59. package/dist/core/command/command-handler.js +290 -44
  60. package/dist/core/command/connect-menu.js +3 -4
  61. package/dist/core/command/group-menu.js +5 -7
  62. package/dist/core/command/menu-handler.js +279 -80
  63. package/dist/core/command/role-menu.js +21 -11
  64. package/dist/core/command/slash-gate.js +85 -18
  65. package/dist/core/command/slash-handler.js +350 -32
  66. package/dist/core/event-catalog.js +5 -0
  67. package/dist/core/evolagent.js +9 -4
  68. package/dist/core/handoff/dispatcher.js +4 -0
  69. package/dist/core/handoff/runtime.js +10 -0
  70. package/dist/core/handoff/store.js +32 -9
  71. package/dist/core/inference/text-inference.js +7 -15
  72. package/dist/core/message/im-renderer.js +83 -84
  73. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  74. package/dist/core/message/message-bridge.js +124 -10
  75. package/dist/core/message/message-log.js +14 -7
  76. package/dist/core/message/message-queue.js +206 -16
  77. package/dist/core/message/message-utils.js +12 -5
  78. package/dist/core/message/response-engine.js +495 -72
  79. package/dist/core/message/send-receipt.js +1 -0
  80. package/dist/core/message/stream-debouncer.js +9 -2
  81. package/dist/core/model/model-catalog.js +23 -15
  82. package/dist/core/model/model-diagnostics.js +28 -10
  83. package/dist/core/permission/approval-gateway.js +180 -6
  84. package/dist/core/permission/ec-command-parser.js +465 -56
  85. package/dist/core/permission/mode.js +18 -3
  86. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  87. package/dist/core/permission/readonly-shell-query.js +263 -9
  88. package/dist/core/permission/sandbox-runtime.js +205 -13
  89. package/dist/core/permission/tool-policy.js +673 -62
  90. package/dist/core/relation/peer-identity.js +18 -0
  91. package/dist/core/session/session-fs-store.js +154 -5
  92. package/dist/core/session/session-manager.js +299 -30
  93. package/dist/core/session/session-renew.js +19 -12
  94. package/dist/core/session/session-turn-coordinator.js +11 -4
  95. package/dist/eck/kit-renderer.js +18 -9
  96. package/dist/index.js +283 -65
  97. package/dist/ipc.js +374 -24
  98. package/dist/paths.js +64 -7
  99. package/dist/response-system/context-builder.js +1 -7
  100. package/dist/trigger/anomaly-store.js +1 -0
  101. package/dist/trigger/feedback.js +56 -5
  102. package/dist/trigger/history.js +79 -4
  103. package/dist/trigger/legacy-session-history.js +2 -2
  104. package/dist/trigger/parser.js +3 -2
  105. package/dist/trigger/validation.js +6 -1
  106. package/dist/utils/atomic-write.js +27 -0
  107. package/dist/utils/ecweb-utils.js +16 -2
  108. package/dist/utils/error-utils.js +4 -1
  109. package/dist/utils/logger.js +21 -2
  110. package/dist/utils/process-tree-stats.js +24 -4
  111. package/dist/utils/process-tree-worker.js +31 -0
  112. package/dist/utils/project-path.js +1 -2
  113. package/dist/utils/stats.js +52 -18
  114. package/dist/utils/welcome.js +2 -2
  115. package/kits/docs/INDEX.md +1 -1
  116. package/kits/docs/evolcore/INDEX.md +1 -1
  117. package/kits/docs/evolcore/contact.md +7 -1
  118. package/kits/docs/evolcore/msg.md +16 -0
  119. package/kits/rules/01-overview.md +1 -1
  120. package/kits/rules/03-identity.md +1 -1
  121. package/kits/rules/05-venue.md +1 -1
  122. package/kits/schemas/_meta.json +10 -5
  123. package/kits/schemas/agent-config.schema.10.json +2 -1
  124. package/kits/schemas/agent-config.schema.11.json +421 -0
  125. package/kits/schemas/daemon.schema.5.json +135 -0
  126. package/kits/schemas/daemon.schema.6.json +131 -0
  127. package/kits/schemas/defaults.schema.5.json +119 -0
  128. package/kits/schemas/migrations/README.md +3 -1
  129. package/kits/schemas/relation-config.schema.8.json +13 -0
  130. package/kits/schemas/role-config.schema.1.json +1 -2
  131. package/kits/schemas/single-session.schema.3.json +32 -0
  132. package/kits/templates/roles/admin.json +1 -0
  133. package/kits/templates/roles/member.json +1 -0
  134. package/kits/templates/roles/visitor.json +1 -0
  135. package/package.json +7 -3
  136. package/skills/eclink/SKILL.md +2 -0
@@ -11,7 +11,7 @@ import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
11
11
  import { resolvePaths, agentDir as agentDirPath, resolveRoot, channelStatePath } from '../paths.js';
12
12
  import { saveToUploads, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
13
13
  import { appendAidEvent } from '../utils/instance-registry.js';
14
- import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, hasMessageLogOperation } from '../core/message/message-log.js';
14
+ import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, findMessageLogOperationMessageId, hasMessageLogOperation } from '../core/message/message-log.js';
15
15
  import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
16
16
  import { chatDirPath } from '../core/session/session-fs-store.js';
17
17
  import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
@@ -19,7 +19,7 @@ import { appendAidLifecycle } from '../aun/aid/identity.js';
19
19
  import { enableFullGroupPullPagination, getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
20
20
  import { MAX_AUN_ATTACHMENT_SIZE, uploadBufferAndBuildPayload, uploadFileAndBuildPayload } from '../aun/msg/upload.js';
21
21
  import { loadAgent } from '../config-store.js';
22
- import { normalizeAgentLifecycle } from '../config/lifecycle.js';
22
+ import { resolveAgentLifecycle } from '../config/lifecycle.js';
23
23
  import { resolveEffective } from '../config/config-manager.js';
24
24
  import { isManagementRole } from '../config/builtin-roles.js';
25
25
  import { normalizeMentionMode } from '../config/mention-mode.js';
@@ -28,7 +28,7 @@ import * as outbox from '../aun/outbox.js';
28
28
  import { guessMime, formatSize } from '../utils/media-cache.js';
29
29
  import { formatPeerKey, PeerIdentityCache } from '../core/relation/peer-identity.js';
30
30
  import { getFirstStaticAgentOwner } from '../config/peer-role-resolver.js';
31
- import { isHClassPath } from '../core/protected-paths.js';
31
+ import { isHClassPath } from '../core/permission/protected-paths.js';
32
32
  import { consumeAunCausation, registerAunCausation } from '../core/causation/aun-association.js';
33
33
  import { deriveCausation, normalizeCausation } from '../core/causation/context.js';
34
34
  import { recordCausationSpan } from '../core/causation/audit.js';
@@ -38,12 +38,17 @@ import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOu
38
38
  import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
39
39
  import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
40
40
  import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
41
+ import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
41
42
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
42
43
  const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
43
44
  const AUN_INBOUND_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
44
45
  const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
45
46
  const AUN_ATTACHMENT_DOWNLOAD_ATTEMPTS = 3;
46
47
  const AUN_ATTACHMENT_RETRY_DELAYS_MS = [250, 750];
48
+ const AUN_SEND_LOG_TEXT_MAX_LENGTH = 60;
49
+ export function formatAunSendLogText(text) {
50
+ return text.replace(/\s+/gu, ' ').trim().slice(0, AUN_SEND_LOG_TEXT_MAX_LENGTH);
51
+ }
47
52
  function attachmentDownloadHost(url) {
48
53
  try {
49
54
  return new URL(url).host || '<unknown>';
@@ -195,6 +200,213 @@ export function buildAunFilePayload(params) {
195
200
  }
196
201
  return payload;
197
202
  }
203
+ const AUN_PERMANENT_ERROR_CODES = new Set([
204
+ 400, 401, 403, 404,
205
+ 4000, 4001, 4010, 4030, 4040, 4090,
206
+ -32700, -32600, -32601, -32602,
207
+ -32001, -32002, -32003, -32004, -32005, -32008, -32009, -32011, -32013,
208
+ -32040, -32041, -32042, -32043, -32044, -32050, -32051,
209
+ -32100, -32101, -32102, -32103, -32104, -32105, -32150, -32152, -32153,
210
+ -32160, -32161, -32162, -32164, -32170, -32171, -32172, -32173, -32174, -32175,
211
+ -32176, -32177, -32178, -32179, -32185, -32186,
212
+ -32180, -32181, -32182, -32183, -32184,
213
+ -33001, -33004, -33005, -33006, -33007, -33008, -33009,
214
+ -33401, -33403, -33404, -33405,
215
+ ]);
216
+ const AUN_RETRYABLE_ERROR_CODES = new Set([
217
+ 429, 4290,
218
+ -32603,
219
+ -32010, -32029, -32429,
220
+ -32151, -32154, -32163,
221
+ -33402, -33406, -33407,
222
+ ]);
223
+ const AUN_ACCEPTED_DISPATCH_STATUSES = new Set([
224
+ 'debounced', 'dispatched', 'accepted', 'queued', 'queued_batch', 'broadcast', 'sent',
225
+ ]);
226
+ const AUN_PERMANENT_STRING_CODES = new Set([
227
+ 'invalid_params', 'invalid_param', 'invalid_request', 'permission_denied',
228
+ 'unauthorized', 'authentication_failed', 'not_found', 'group_not_found',
229
+ 'group_not_member', 'member_not_found', 'target_not_found', 'agent_not_found',
230
+ 'recipient_not_found', 'invalid_argument', 'invalid_payload', 'forbidden',
231
+ ]);
232
+ const AUN_RETRYABLE_STRING_CODES = new Set([
233
+ 'network_error', 'connection_error', 'timeout', 'timed_out', 'rate_limited',
234
+ 'too_many_requests', 'temporarily_unavailable', 'server_error', 'aun_not_connected',
235
+ ]);
236
+ function errorRecord(value) {
237
+ return value && typeof value === 'object' && !Array.isArray(value)
238
+ ? value
239
+ : undefined;
240
+ }
241
+ function firstErrorField(candidates, keys, accept) {
242
+ for (const candidate of candidates) {
243
+ for (const key of keys) {
244
+ const value = candidate[key];
245
+ if (value !== undefined && value !== null && value !== '' && (!accept || accept(value)))
246
+ return value;
247
+ }
248
+ }
249
+ return undefined;
250
+ }
251
+ function aunErrorDetails(value, fallback = 'AUN send failed') {
252
+ const root = errorRecord(value);
253
+ const nested = root
254
+ ? [root.error, root.data, root.details, root.cause]
255
+ .map(errorRecord)
256
+ .filter((candidate) => !!candidate)
257
+ : [];
258
+ const candidates = root ? [root, ...nested] : [];
259
+ const rawMessage = firstErrorField(candidates, [
260
+ 'message', 'error_message', 'errorMessage', 'error', 'reason', 'detail',
261
+ ], value => typeof value === 'string');
262
+ const message = typeof value === 'string'
263
+ ? value
264
+ : typeof rawMessage === 'string'
265
+ ? rawMessage
266
+ : value instanceof Error
267
+ ? value.message
268
+ : fallback;
269
+ const rawCode = firstErrorField(candidates, [
270
+ 'code', 'error_code', 'errorCode', 'stringCode', 'string_code',
271
+ ], value => typeof value === 'string' || typeof value === 'number');
272
+ const rawStatus = firstErrorField(candidates, ['status', 'statusCode', 'http_status', 'httpStatus'], value => (typeof value === 'number' && Number.isFinite(value))
273
+ || (typeof value === 'string' && /^\d+$/.test(value)));
274
+ const status = typeof rawStatus === 'number'
275
+ ? rawStatus
276
+ : typeof rawStatus === 'string' && /^\d+$/.test(rawStatus)
277
+ ? Number(rawStatus)
278
+ : undefined;
279
+ const retryableCandidate = candidates.find(candidate => Object.prototype.hasOwnProperty.call(candidate, 'retryable'));
280
+ const retryable = retryableCandidate && typeof retryableCandidate.retryable === 'boolean'
281
+ ? retryableCandidate.retryable
282
+ : undefined;
283
+ const name = firstErrorField(candidates, ['name', 'type'], value => typeof value === 'string');
284
+ const messageStatus = message.match(/\b(?:HTTP\b[^\d]{0,40}|status(?:\s*code)?\s*[:=]?\s*)(\d{3})\b/i);
285
+ const dispatchStatus = root?.message_dispatch && typeof root.message_dispatch === 'object'
286
+ ? root.message_dispatch.status
287
+ : undefined;
288
+ return {
289
+ code: typeof rawCode === 'string' || typeof rawCode === 'number' ? rawCode : undefined,
290
+ message: message || fallback,
291
+ name: typeof name === 'string' ? name : undefined,
292
+ retryable,
293
+ status: status ?? (messageStatus ? Number(messageStatus[1]) : undefined),
294
+ dispatchStatus: typeof dispatchStatus === 'string' ? dispatchStatus.toLowerCase() : undefined,
295
+ };
296
+ }
297
+ function numericErrorCode(code) {
298
+ if (typeof code === 'number' && Number.isFinite(code))
299
+ return code;
300
+ if (typeof code === 'string' && /^-?\d+$/.test(code.trim()))
301
+ return Number(code);
302
+ return undefined;
303
+ }
304
+ function normalizedErrorCode(code) {
305
+ return code === undefined ? '' : String(code).trim().toLowerCase().replace(/[\s-]+/g, '_');
306
+ }
307
+ function shortErrorMessage(message) {
308
+ const compact = message.replace(/\s+/g, ' ').trim();
309
+ return compact.length > 500 ? `${compact.slice(0, 497)}...` : compact;
310
+ }
311
+ /**
312
+ * Classify a gateway response before it reaches the durable queue. Protocol
313
+ * and HTTP semantics take precedence; the SDK's `retryable` bit is used when
314
+ * no known code or explicit rejection already determines the outcome.
315
+ */
316
+ export function classifyAunSendFailure(value, fallback = 'AUN send failed') {
317
+ const details = aunErrorDetails(value, fallback);
318
+ const numericCode = numericErrorCode(details.code);
319
+ const stringCode = normalizedErrorCode(details.code);
320
+ const text = `${details.name ?? ''} ${stringCode} ${details.message}`.toLowerCase();
321
+ const httpStatus = details.status
322
+ ?? (numericCode !== undefined && numericCode >= 100 && numericCode <= 599 ? numericCode : undefined);
323
+ const retryByName = /(?:timeout|connection|rate.?limit|temporar(?:y|ily)|unavailable|network|socket)/i.test(details.name ?? '');
324
+ const relayTargetMissing = text.includes('relay_target_not_found') || text.includes('relay target not found');
325
+ const rpcHandlerTimeout = numericCode === -32004 && /rpc handler timeout/i.test(text);
326
+ const groupStateCode = numericCode === -33002 || numericCode === -33003;
327
+ const groupClosed = groupStateCode && /\b(?:closed|dissolved|disbanded)\b|解散|已关闭/i.test(text);
328
+ const groupSuspended = groupStateCode && /\bsuspend(?:ed)?\b|暂停/i.test(text);
329
+ const transientHttpStatus = httpStatus === 408 || httpStatus === 425 || httpStatus === 429
330
+ || (httpStatus !== undefined && httpStatus >= 500 && httpStatus < 600);
331
+ const permanentHttpStatus = httpStatus !== undefined
332
+ && httpStatus >= 400 && httpStatus < 500
333
+ && !transientHttpStatus;
334
+ const permanentByText = !relayTargetMissing && /\b(?:group|peer|agent|recipient|target|member|object|task|stream)(?:[\s_-]+(?:id|aid))?[\s_-]*(?:not[\s_-]*found|does[\s_-]*not[\s_-]*exist)|不存在|group[_ -]?not[_ -]?found|not[_ -]?a[_ -]?member|(?:permission|access|role).*(?:denied|forbidden|拒绝|无权限)|(?:invalid|malformed|bad)\s*(?:argument|param|request)|unauthori[sz]ed|authentication failed|signature invalid/i.test(text);
335
+ const retryByText = /timeout|timed out|temporar|unavailable|overload|rate.?limit|too many requests|try again|not connected|connection|network|socket|econn|eai_again|etimedout|epipe|broken pipe|connection refused|connect timeout|fetch failed|dns|reset by peer|gateway service degraded|upstream/i.test(text);
336
+ const acceptedDispatch = details.dispatchStatus !== undefined
337
+ && AUN_ACCEPTED_DISPATCH_STATUSES.has(details.dispatchStatus);
338
+ const rejectedDispatch = details.dispatchStatus === 'failed'
339
+ || details.dispatchStatus === 'skipped';
340
+ const successfulHttpWithoutReceipt = httpStatus !== undefined
341
+ && httpStatus >= 200 && httpStatus < 300
342
+ && numericCode === undefined && !permanentByText && !retryByText;
343
+ const ambiguousAccepted = errorRecord(value)?.ok === true
344
+ && numericCode === undefined
345
+ && httpStatus === undefined
346
+ && !permanentByText
347
+ && !retryByText;
348
+ const ambiguousNoReceipt = (value === null || value === undefined)
349
+ && /no message[_ -]?id|no receipt|empty response|returned no/i.test(fallback);
350
+ const missingReceipt = /no message[_ -]?id|missing message[_ -]?id|no receipt|empty response|returned no/i.test(fallback);
351
+ const unconfirmedDelivery = missingReceipt
352
+ && (acceptedDispatch || ambiguousAccepted || ambiguousNoReceipt || successfulHttpWithoutReceipt);
353
+ let status;
354
+ if ((AUN_PERMANENT_ERROR_CODES.has(numericCode ?? Number.NaN) && !rpcHandlerTimeout)
355
+ || permanentByText
356
+ || AUN_PERMANENT_STRING_CODES.has(stringCode)
357
+ || rejectedDispatch
358
+ || groupClosed
359
+ || (groupStateCode && !groupSuspended)
360
+ || permanentHttpStatus) {
361
+ status = 'permanent';
362
+ }
363
+ else if (unconfirmedDelivery) {
364
+ // The RPC completed with a success-looking result but without the protocol
365
+ // receipt. Reissuing it could duplicate a message because AUN send calls
366
+ // do not carry our local outbox operation ID as an idempotency key.
367
+ status = 'permanent';
368
+ }
369
+ else if (AUN_RETRYABLE_ERROR_CODES.has(numericCode ?? Number.NaN)
370
+ || transientHttpStatus
371
+ || rpcHandlerTimeout
372
+ || AUN_RETRYABLE_STRING_CODES.has(stringCode)
373
+ || retryByName
374
+ || relayTargetMissing
375
+ || groupSuspended
376
+ || retryByText) {
377
+ // Known transient protocol, HTTP, SDK-class and transport signals take
378
+ // precedence over the SDK's generic `retryable` default. Several SDK
379
+ // error classes default that flag to false even for connection failures.
380
+ status = 'retry';
381
+ }
382
+ else if (details.retryable === false) {
383
+ status = 'permanent';
384
+ }
385
+ else if (details.retryable === true) {
386
+ status = 'retry';
387
+ }
388
+ else {
389
+ // A response with no recognized transient signal is deterministic enough
390
+ // to stop retrying. This is deliberately fail-closed for durable sends.
391
+ status = 'permanent';
392
+ }
393
+ return {
394
+ status,
395
+ error: shortErrorMessage(details.message),
396
+ ...(details.code !== undefined
397
+ ? { code: details.code }
398
+ : details.status !== undefined
399
+ ? { code: details.status }
400
+ : missingReceipt
401
+ ? { code: 'MISSING_MESSAGE_ID' }
402
+ : {}),
403
+ };
404
+ }
405
+ function sentOutboxResult(messageId) {
406
+ return messageId
407
+ ? { status: 'sent', messageId }
408
+ : { status: 'permanent', error: 'AUN send completed without a remote message_id', code: 'MISSING_MESSAGE_ID' };
409
+ }
198
410
  function setIfDefined(target, key, value) {
199
411
  if (value !== undefined)
200
412
  target[key] = value;
@@ -346,6 +558,35 @@ export class AUNChannel {
346
558
  * 统一的 RPC 调用包装:自动记录 OUT 发送、.ok 结果、.error 错误(含 trace + daemon.log 失败日志)。
347
559
  * 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
348
560
  */
561
+ callClient(method, params) {
562
+ return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)));
563
+ }
564
+ withOutboundSendGate(method, run) {
565
+ if (!AUNChannel.OUTBOUND_SEND_METHODS.has(method))
566
+ return run();
567
+ return new Promise((resolve, reject) => {
568
+ this.outboundSendQueue.push({
569
+ run: run,
570
+ resolve: resolve,
571
+ reject,
572
+ });
573
+ this.pumpOutboundSendGate();
574
+ });
575
+ }
576
+ pumpOutboundSendGate() {
577
+ while (this.outboundSendActive < AUNChannel.OUTBOUND_SEND_CONCURRENCY
578
+ && this.outboundSendQueue.length > 0) {
579
+ const job = this.outboundSendQueue.shift();
580
+ this.outboundSendActive++;
581
+ Promise.resolve()
582
+ .then(job.run)
583
+ .then(job.resolve, job.reject)
584
+ .finally(() => {
585
+ this.outboundSendActive--;
586
+ this.pumpOutboundSendGate();
587
+ });
588
+ }
589
+ }
349
590
  async callAndTrace(method, params, opts) {
350
591
  this.trace('OUT', method, params);
351
592
  // RPC 往返计时:区分「网关慢」与「本地队列堵塞」。message.send/group.send 的
@@ -355,7 +596,7 @@ export class AUNChannel {
355
596
  // SLOW_RPC_WARN_MS 需明显低于 SDK 默认 10s 超时,以便在真正超时前提前告警。
356
597
  const SLOW_RPC_WARN_MS = 3000;
357
598
  try {
358
- const result = await this.client.call(method, params);
599
+ const result = await this.callClient(method, params);
359
600
  const durationMs = Date.now() - rpcStart;
360
601
  if (!opts?.silentOk) {
361
602
  const r = result;
@@ -1017,6 +1258,7 @@ export class AUNChannel {
1017
1258
  * the event handler boundary, before payload/slash/mention parsing.
1018
1259
  */
1019
1260
  inboundSeenMessages = new Map();
1261
+ invalidInboundEnvelopeLastLogAt = 0;
1020
1262
  groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
1021
1263
  peerInfoCache = new Map();
1022
1264
  messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
@@ -1077,6 +1319,16 @@ export class AUNChannel {
1077
1319
  aidState;
1078
1320
  aidStatsCollector;
1079
1321
  outboxInFlight = new Set();
1322
+ /** Shared gateway-facing send limit for replies, activity, observer and thought messages. */
1323
+ static OUTBOUND_SEND_METHODS = new Set([
1324
+ 'message.send',
1325
+ 'group.send',
1326
+ 'message.thought.put',
1327
+ 'group.thought.put',
1328
+ ]);
1329
+ static OUTBOUND_SEND_CONCURRENCY = 4;
1330
+ outboundSendActive = 0;
1331
+ outboundSendQueue = [];
1080
1332
  constructor(config) {
1081
1333
  this.config = config;
1082
1334
  this.agentDir = agentDirPath(config.aid);
@@ -1174,11 +1426,9 @@ export class AUNChannel {
1174
1426
  const aidName = this.config.aid;
1175
1427
  // encryptionSeed 由 getAidStore 内部解析(config / env / 'evol')
1176
1428
  // Migration from ~/.aun is handled by ensureDataDirs() at startup with a marker file.
1177
- // Gateway discovery/cache is owned by the SDK. Do not do a blocking
1178
- // well-known probe here: it prevents the SDK from using its token-store
1179
- // gateway metadata cache when well-known is temporarily unavailable.
1180
- const configuredGateway = this.config.gatewayUrl || '';
1181
- logger.info(`${this.logPrefix()} Initializing: aid=${aidName}, gateway=${configuredGateway || '<sdk-discovery>'}, aun_path=${aunPath}`);
1429
+ // Gateway discovery/cache is owned entirely by the SDK.
1430
+ this.gatewayUrl = '';
1431
+ logger.info(`${this.logPrefix()} Initializing: aid=${aidName}, gateway=<sdk-discovery>, aun_path=${aunPath}`);
1182
1432
  // 构造 AIDStore。daemon 使用独立 slot,CLI/netcheck 不能触碰业务入站游标。
1183
1433
  // encryptionSeed / rootCaPath 由 getAidStore 内部注入
1184
1434
  const store = await getAidStore({
@@ -1193,13 +1443,6 @@ export class AUNChannel {
1193
1443
  // A reconnect replaces the SDK client. Late events from the retired
1194
1444
  // instance must not mutate the new instance's health or message state.
1195
1445
  const isCurrentClient = () => this.client === client;
1196
- // fastaun gives a preset in-memory gateway precedence over its metadata
1197
- // cache and AID discovery. authenticate({ gateway }) is deliberately
1198
- // rejected by its public API, so set the documented client preset before
1199
- // authentication instead.
1200
- if (configuredGateway)
1201
- client._gatewayUrl = configuredGateway;
1202
- this.gatewayUrl = configuredGateway;
1203
1446
  // Register event handlers before connecting
1204
1447
  client.on('message.received', (data) => {
1205
1448
  if (!isCurrentClient())
@@ -1306,7 +1549,7 @@ export class AUNChannel {
1306
1549
  const auth = await client.authenticate();
1307
1550
  this.trace('OUT', 'auth.authenticate.ok', { aid: client.aid, gateway: auth?.gateway, hasToken: !!auth?.access_token });
1308
1551
  this.trace('IN', 'auth.result', { aid: client.aid, gateway: auth?.gateway, hasToken: !!auth?.access_token });
1309
- const resolvedGateway = String(auth?.gateway ?? this.gatewayUrl);
1552
+ const resolvedGateway = typeof auth?.gateway === 'string' ? auth.gateway : '';
1310
1553
  this.gatewayUrl = resolvedGateway;
1311
1554
  logger.info(`${this.logPrefix()} Authenticated as ${client.aid ?? '?'}, gateway=${resolvedGateway}`);
1312
1555
  }
@@ -1393,22 +1636,11 @@ export class AUNChannel {
1393
1636
  if (hasMessageLogOperation(chatDir, operationId))
1394
1637
  return true;
1395
1638
  const prepared = outbox.findByDedupeKey(aidName, operationId);
1396
- const initial = outbox.findByDedupeKey(aidName, bootstrapInitialMessageOperationId(aidName));
1397
- const initialDelivery = initial && outbox.isDeliveryForChannel(initial.delivery, owner)
1398
- ? initial.delivery
1399
- : undefined;
1400
- // During bootstrap completion the lifecycle is intentionally still
1401
- // non-active. Persisting the welcome must stay local and must not block
1402
- // on a best-effort group RPC before the lifecycle commit. If the initial
1403
- // bootstrap route is already known, preserve it; otherwise migration
1404
- // will resolve a group route after the channel is active.
1405
- const lifecycle = normalizeAgentLifecycle(agentConfig).lifecycle;
1406
- const ownerIsGroup = initialDelivery
1407
- ? initialDelivery.chatType === 'group'
1408
- : lifecycle === 'active' ? await this.isGroup(owner) : undefined;
1409
- const delivery = initialDelivery ?? (ownerIsGroup === true
1410
- ? { chatType: 'group', groupId: owner }
1411
- : { chatType: 'private' });
1639
+ // Bootstrap welcomes target the configured personal Owner AID. They are
1640
+ // not replies to an inbound group message, so their route is always
1641
+ // private and must not be inferred with group.get_info.
1642
+ const lifecycle = resolveAgentLifecycle(agentConfig);
1643
+ const delivery = { chatType: 'private' };
1412
1644
  if (prepared
1413
1645
  && prepared.channelId === owner
1414
1646
  && isDeliveryTarget(prepared.delivery)
@@ -1452,6 +1684,7 @@ export class AUNChannel {
1452
1684
  return this.reconcilePostBootstrapWelcome();
1453
1685
  }
1454
1686
  async hasPendingPostBootstrapWelcome() {
1687
+ this.repairBootstrapRoutes();
1455
1688
  const aid = this.config.aid.replace(/^@/, '');
1456
1689
  return hasPendingPostBootstrapWelcomeOutbox(aid);
1457
1690
  }
@@ -1467,6 +1700,7 @@ export class AUNChannel {
1467
1700
  * already-active agent from manufacturing another welcome message.
1468
1701
  */
1469
1702
  async reconcilePostBootstrapWelcome() {
1703
+ this.repairBootstrapRoutes();
1470
1704
  const configuredAid = this.config.aid;
1471
1705
  const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
1472
1706
  const operationId = postBootstrapWelcomeOperationId(aid);
@@ -1484,15 +1718,17 @@ export class AUNChannel {
1484
1718
  return false;
1485
1719
  }
1486
1720
  const agentConfig = loadAgent(aid);
1487
- if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active') {
1721
+ if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
1488
1722
  logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
1489
1723
  return true;
1490
1724
  }
1491
1725
  if (!this.connected || !this.client)
1492
1726
  return true;
1493
- const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
1494
- if (sent)
1495
- outbox.remove(aid, entry.id);
1727
+ const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
1728
+ if (sent.status === 'sent')
1729
+ this.removeDeliveredOutboxEntry(entry);
1730
+ else if (sent.status === 'permanent')
1731
+ this.markPermanentOutboxFailure(entry, sent);
1496
1732
  return true;
1497
1733
  }
1498
1734
  // ── Event handlers ──────────────────────────────────────────
@@ -1743,17 +1979,30 @@ export class AUNChannel {
1743
1979
  const msg = data;
1744
1980
  const receivedAt = Date.now();
1745
1981
  const receivedAtMono = performance.now();
1746
- // Claim before any payload extraction or command parsing. Retransmits may
1747
- // carry a new seq, but the application identity remains message_id.
1982
+ // Validate the authenticated envelope before any payload extraction or
1983
+ // command parsing. Retransmits may carry a new seq, but the application
1984
+ // identity remains message_id.
1748
1985
  const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
1749
1986
  const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
1750
- if (!this.claimInboundMessage('private', messageId, seq))
1751
- return;
1752
1987
  // SDK 0.5.* 移除了顶层 from/to/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
1753
1988
  // message_id / seq / payload / same_* 等仍是顶层独立字段,不在 envelope 内。
1754
1989
  const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
1990
+ const fromAid = typeof env.from === 'string' ? env.from.trim() : '';
1991
+ if (!messageId || !fromAid) {
1992
+ this.acknowledgeImmediately(messageId, seq);
1993
+ const now = Date.now();
1994
+ if (now - this.invalidInboundEnvelopeLastLogAt >= 60_000) {
1995
+ this.invalidInboundEnvelopeLastLogAt = now;
1996
+ logger.warn(`${this.logPrefix()} Dropped private inbound: invalid envelope code=INVALID_INBOUND_ENVELOPE mid=${messageId || 'unknown'} from=${fromAid || 'unknown'}`);
1997
+ }
1998
+ else {
1999
+ logger.debug(`${this.logPrefix()} Dropped private inbound: invalid envelope (suppressed duplicate) mid=${messageId || 'unknown'}`);
2000
+ }
2001
+ return;
2002
+ }
2003
+ if (!this.claimInboundMessage('private', messageId, seq))
2004
+ return;
1755
2005
  const protectedHeaders = verifiedProtectedHeaders(msg, env);
1756
- const fromAid = env.from ?? '';
1757
2006
  const payload = msg.payload ?? '';
1758
2007
  const mentions = this.parsePayloadMentionsOrReject(payload, 'p2p.inbound', messageId, seq);
1759
2008
  if (!mentions)
@@ -2319,7 +2568,7 @@ export class AUNChannel {
2319
2568
  setObserverConfigResolver(fn) {
2320
2569
  this.observerConfigResolver = fn;
2321
2570
  }
2322
- /** 读取 observable 开关 + owners;无 resolver(如未接入 daemon)时视为关闭。 */
2571
+ /** 读取 observable 开关 + owners;未接入 daemon 时没有 owning Agent,保持关闭。 */
2323
2572
  getObserverConfig() {
2324
2573
  return this.observerConfigResolver?.() ?? { observable: false, owners: [] };
2325
2574
  }
@@ -2869,8 +3118,31 @@ export class AUNChannel {
2869
3118
  this.outboxInFlight.delete(entry.id);
2870
3119
  }
2871
3120
  }
3121
+ markPermanentOutboxFailure(entry, result) {
3122
+ const error = result.error ?? 'permanent send failure';
3123
+ const terminated = outbox.markTerminal(this.config.aid, entry.id, { error, code: result.code }, entry);
3124
+ if (terminated) {
3125
+ logger.error(`${this.logPrefix()} Permanent AUN delivery failure; outbox entry terminated: id=${entry.id} channel=${entry.channelId} code=${result.code ?? 'unknown'} error=${error}`);
3126
+ }
3127
+ else {
3128
+ logger.warn(`${this.logPrefix()} Ignored stale permanent delivery result after outbox route changed: id=${entry.id} submittedChannel=${entry.channelId}`);
3129
+ }
3130
+ }
3131
+ removeDeliveredOutboxEntry(entry) {
3132
+ const messageId = entry.deliveryReceipt?.messageId;
3133
+ const finalized = messageId
3134
+ ? outbox.markDelivered(this.config.aid, entry.id, messageId, entry)
3135
+ : outbox.removeIfRouteMatches(this.config.aid, entry);
3136
+ if (!finalized) {
3137
+ logger.warn(`${this.logPrefix()} Preserved outbox entry whose route changed while delivery was in flight: id=${entry.id} submittedChannel=${entry.channelId}`);
3138
+ }
3139
+ }
2872
3140
  messageIdFromSendResult(result) {
2873
- return result?.message?.message_id ?? result?.message_id ?? null;
3141
+ const nested = result?.message?.message_id;
3142
+ if (typeof nested === 'string' && nested.trim())
3143
+ return nested;
3144
+ const direct = result?.message_id;
3145
+ return typeof direct === 'string' && direct.trim() ? direct : null;
2874
3146
  }
2875
3147
  /**
2876
3148
  * An AUN send result is only a gateway transport result. It is not a
@@ -2893,7 +3165,7 @@ export class AUNChannel {
2893
3165
  return `aun_status=${receipt.status ?? 'unknown'} seq=${receipt.seq ?? 'unknown'} delivery_mode=${receipt.deliveryMode ?? 'unknown'} gateway_timestamp=${receipt.timestamp ?? 'unknown'}`;
2894
3166
  }
2895
3167
  logAunSendAccepted(method, target, messageId, encrypt, result, text) {
2896
- const preview = text === undefined ? '' : ` text=${text.slice(0, 60)}`;
3168
+ const preview = text === undefined ? '' : ` text=${formatAunSendLogText(text)}`;
2897
3169
  logger.info(`${this.logPrefix()} ${method} accepted by AUN: target=${target} mid=${messageId} encrypt=${encrypt} ${this.receiptLogFields(result)}${preview}`);
2898
3170
  }
2899
3171
  stripUndefinedDeep(value) {
@@ -3104,8 +3376,8 @@ export class AUNChannel {
3104
3376
  }
3105
3377
  messageIds.add(messageId);
3106
3378
  const now = Date.now();
3107
- const mapTtl = action.expiresAt && action.expiresAt > now
3108
- ? action.expiresAt - now
3379
+ const mapTtl = typeof action.expiresAt === 'number' && Number.isFinite(action.expiresAt)
3380
+ ? Math.max(0, action.expiresAt - now)
3109
3381
  : AUN_INTERACTION_CARD_TTL_MS;
3110
3382
  const mapTimer = setTimeout(() => {
3111
3383
  this.cardMessageIdMap.delete(messageId);
@@ -3140,10 +3412,11 @@ export class AUNChannel {
3140
3412
  const detail = error instanceof Error ? error.message : String(error);
3141
3413
  const code = error?.code ?? 'INVALID_MENTION_SCHEMA';
3142
3414
  logger.error(`${this.logPrefix()} Dropped durable AUN payload: invalid payload.mentions (${detail}) code=${code} channel=${channelId}`);
3143
- return { ok: false, discarded: true };
3415
+ return { ok: false, status: 'permanent', error: detail, code };
3416
+ }
3417
+ if (!this.client || !this.connected) {
3418
+ return { ok: false, status: 'retry', error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
3144
3419
  }
3145
- if (!this.client || !this.connected)
3146
- return { ok: false };
3147
3420
  const isGroup = delivery.chatType === 'group';
3148
3421
  const targetAid = channelId;
3149
3422
  const encryptTarget = isGroup ? channelId : targetAid;
@@ -3167,14 +3440,15 @@ export class AUNChannel {
3167
3440
  params.to = targetAid;
3168
3441
  const callOnce = async (sendParams, fallback) => {
3169
3442
  const result = fallback
3170
- ? await this.client.call(method, sendParams)
3443
+ ? await this.callClient(method, sendParams)
3171
3444
  : await this.callAndTrace(method, sendParams);
3172
3445
  const mid = this.messageIdFromSendResult(result);
3173
3446
  if (!mid) {
3174
- logger.warn(`${this.logPrefix()} ${method}${fallback ? ' fallback' : ''} (${label}) returned no message_id: ${JSON.stringify(result)}`);
3175
- return { ok: false, result, encrypt: !!sendParams.encrypt };
3447
+ const failure = classifyAunSendFailure(result, `${method} (${label}) returned no message_id`);
3448
+ logger.warn(`${this.logPrefix()} ${method}${fallback ? ' fallback' : ''} (${label}) returned no message_id: ${JSON.stringify(result)}; disposition=${failure.status}`);
3449
+ return { ok: false, status: failure.status, error: failure.error, code: failure.code, result, encrypt: !!sendParams.encrypt };
3176
3450
  }
3177
- return { ok: true, messageId: mid, result, encrypt: !!sendParams.encrypt };
3451
+ return { ok: true, status: 'sent', messageId: mid, result, encrypt: !!sendParams.encrypt };
3178
3452
  };
3179
3453
  try {
3180
3454
  return await callOnce(params, false);
@@ -3187,18 +3461,22 @@ export class AUNChannel {
3187
3461
  try {
3188
3462
  this.trace('OUT', `${method}.${label}.fallback`, fallbackParams);
3189
3463
  const sent = await callOnce(fallbackParams, true);
3190
- this.trace('OUT', `${method}.${label}.fallback.${sent.ok ? 'ok' : 'missing_id'}`, { message_id: sent.messageId });
3464
+ this.trace('OUT', `${method}.${label}.fallback.${sent.ok ? 'ok' : 'missing_id'}`, sent.ok
3465
+ ? { message_id: sent.messageId }
3466
+ : { disposition: sent.status, code: sent.code });
3191
3467
  return sent;
3192
3468
  }
3193
3469
  catch (e2) {
3194
3470
  this.trace('OUT', `${method}.${label}.fallback.error`, { channelId, error: String(e2) });
3195
- logger.error(`${this.logPrefix()} Plaintext ${label} fallback also failed to ${channelId}: ${e2}`);
3196
- return { ok: false };
3471
+ const failure = classifyAunSendFailure(e2, `${method} plaintext fallback failed`);
3472
+ logger.error(`${this.logPrefix()} Plaintext ${label} fallback also failed to ${channelId}: ${failure.error}; disposition=${failure.status}`);
3473
+ return { ok: false, status: failure.status, error: failure.error, code: failure.code };
3197
3474
  }
3198
3475
  }
3199
3476
  this.trace('OUT', `${method}.${label}.error`, { channelId, error: String(e) });
3200
- logger.error(`${this.logPrefix()} ${label} send failed to ${channelId}: ${e}`);
3201
- return { ok: false };
3477
+ const failure = classifyAunSendFailure(e, `${method} failed`);
3478
+ logger.error(`${this.logPrefix()} ${label} send failed to ${channelId}: ${failure.error}; disposition=${failure.status}${failure.code === undefined ? '' : ` code=${failure.code}`}`);
3479
+ return { ok: false, status: failure.status, error: failure.error, code: failure.code };
3202
3480
  }
3203
3481
  }
3204
3482
  recordDurableOutbound(channelId, payload, messageId, encrypt, context, isGroup, contentKind, logText, result, statsContext) {
@@ -3251,31 +3529,81 @@ export class AUNChannel {
3251
3529
  // turn malformed `mentions: [undefined]` into an apparently valid `[]`.
3252
3530
  const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, context));
3253
3531
  const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
3254
- const entry = outbox.enqueue(this.config.aid, {
3255
- channelId,
3256
- delivery,
3257
- type: 'payload',
3258
- contentKind: opts.contentKind,
3259
- payload: finalPayload,
3260
- context,
3261
- logText,
3262
- ttl: opts.ttl,
3263
- postSend: opts.postSend,
3264
- });
3265
- logger.debug(`${this.logPrefix()} Outbox enqueued payload: id=${entry.id} kind=${opts.contentKind} channel=${channelId} text=${logText.slice(0, 40)}`);
3532
+ const expiresAt = opts.postSend?.type === 'register_interaction_card'
3533
+ ? opts.postSend.expiresAt
3534
+ : undefined;
3535
+ if (typeof expiresAt === 'number' && Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
3536
+ return {
3537
+ status: 'permanent',
3538
+ error: 'interaction card has expired',
3539
+ code: 'INTERACTION_EXPIRED',
3540
+ };
3541
+ }
3542
+ const remainingTtl = typeof expiresAt === 'number' && Number.isFinite(expiresAt)
3543
+ ? Math.max(0, expiresAt - Date.now())
3544
+ : undefined;
3545
+ const requestedTtl = opts.ttl ?? outbox.defaultTtl(opts.queue);
3546
+ const ttl = remainingTtl === undefined ? opts.ttl : Math.min(requestedTtl, remainingTtl);
3547
+ let entry;
3548
+ try {
3549
+ entry = outbox.enqueue(this.config.aid, {
3550
+ queue: opts.queue,
3551
+ channelId,
3552
+ delivery,
3553
+ type: 'payload',
3554
+ contentKind: opts.contentKind,
3555
+ payload: finalPayload,
3556
+ context,
3557
+ logText,
3558
+ ttl,
3559
+ postSend: opts.postSend,
3560
+ });
3561
+ }
3562
+ catch (error) {
3563
+ // Queue saturation is a caller-visible failure. Keep route/schema
3564
+ // validation errors as exceptions so their existing fail-closed path is
3565
+ // preserved, but expose OUTBOX_FULL as a structured send result.
3566
+ if (error?.code !== 'OUTBOX_FULL')
3567
+ throw error;
3568
+ return {
3569
+ status: 'permanent',
3570
+ error: error instanceof Error ? error.message : String(error),
3571
+ code: 'OUTBOX_FULL',
3572
+ };
3573
+ }
3574
+ logger.debug(`${this.logPrefix()} Outbox enqueued payload: id=${entry.id} queue=${opts.queue ?? 'default'} kind=${opts.contentKind} channel=${channelId} text=${logText.slice(0, 40)}`);
3266
3575
  if (!this.connected || !this.client) {
3267
3576
  logger.warn(`${this.logPrefix()} Not connected, payload queued in outbox (id=${entry.id}, kind=${opts.contentKind}). Triggering reconnect.`);
3268
3577
  if (!this.reconnectTimer && !this.client) {
3269
3578
  this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendContentPayload failed: ${e}`));
3270
3579
  }
3271
- return { queued: true };
3580
+ return {
3581
+ queued: true,
3582
+ outboxId: entry.id,
3583
+ error: 'AUN channel is not connected',
3584
+ code: 'AUN_NOT_CONNECTED',
3585
+ };
3272
3586
  }
3273
- const result = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false });
3587
+ const result = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false, status: 'retry', error: 'outbox send is already in flight' });
3274
3588
  if (result.ok) {
3275
- outbox.remove(this.config.aid, entry.id);
3589
+ this.removeDeliveredOutboxEntry(entry);
3276
3590
  return { messageId: result.messageId };
3277
3591
  }
3278
- return { queued: true };
3592
+ if (result.status === 'permanent' || result.status === 'failed') {
3593
+ this.markPermanentOutboxFailure(entry, result);
3594
+ return {
3595
+ status: 'permanent',
3596
+ outboxId: entry.id,
3597
+ ...(result.error !== undefined ? { error: result.error } : {}),
3598
+ ...(result.code !== undefined ? { code: result.code } : {}),
3599
+ };
3600
+ }
3601
+ return {
3602
+ queued: true,
3603
+ outboxId: entry.id,
3604
+ ...(result.error !== undefined ? { error: result.error } : {}),
3605
+ ...(result.code !== undefined ? { code: result.code } : {}),
3606
+ };
3279
3607
  }
3280
3608
  buildTaskPayloadBase(envelope, context) {
3281
3609
  const base = {};
@@ -3393,16 +3721,22 @@ export class AUNChannel {
3393
3721
  };
3394
3722
  }
3395
3723
  async sendReliableStructured(channelId, payload, context, logText) {
3396
- await this.sendContentPayload(channelId, payload, {
3724
+ const result = await this.sendContentPayload(channelId, payload, {
3397
3725
  contentKind: 'custom',
3398
3726
  context,
3399
3727
  logText: logText ?? this.payloadLogText(payload, 'custom'),
3400
3728
  });
3729
+ if (result.status === 'permanent' || result.status === 'failed') {
3730
+ throw Object.assign(new Error(result.error ?? 'AUN structured send failed'), {
3731
+ code: result.code ?? 'AUN_SEND_FAILED',
3732
+ outboxId: result.outboxId,
3733
+ });
3734
+ }
3401
3735
  }
3402
3736
  async sendMessage(channelId, text, context) {
3403
3737
  if (!text?.trim()) {
3404
3738
  logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
3405
- return;
3739
+ return { status: 'failed', error: 'message text is empty', code: 'EMPTY_MESSAGE' };
3406
3740
  }
3407
3741
  const delivery = this.requireDelivery(channelId, context);
3408
3742
  const routedContext = this.withDelivery(context, delivery);
@@ -3440,13 +3774,14 @@ export class AUNChannel {
3440
3774
  : undefined;
3441
3775
  if (operationId) {
3442
3776
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
3443
- if (hasMessageLogOperation(chatDir, operationId)) {
3777
+ const completedMessageId = findMessageLogOperationMessageId(chatDir, operationId);
3778
+ if (completedMessageId) {
3444
3779
  const duplicate = outbox.findByDedupeKey(this.config.aid, operationId);
3445
3780
  if (duplicate) {
3446
3781
  outbox.remove(this.config.aid, duplicate.id);
3447
3782
  logger.info(`${this.logPrefix()} Removed stale outbox entry for completed operation: ${operationId}`);
3448
3783
  }
3449
- return;
3784
+ return { status: 'sent', messageId: completedMessageId };
3450
3785
  }
3451
3786
  }
3452
3787
  // Write-ahead: persist to outbox before attempting send
@@ -3462,6 +3797,20 @@ export class AUNChannel {
3462
3797
  ? routedContext.metadata.outboxTtl
3463
3798
  : undefined,
3464
3799
  });
3800
+ if (entry.deliveryResult?.messageId || entry.deliveryReceipt?.messageId) {
3801
+ const messageId = entry.deliveryResult?.messageId ?? entry.deliveryReceipt.messageId;
3802
+ logger.info(`${this.logPrefix()} Reusing completed durable operation: operation=${operationId ?? '<none>'} entry=${entry.id} mid=${messageId}`);
3803
+ return { status: 'sent', messageId };
3804
+ }
3805
+ if (entry.terminal) {
3806
+ logger.warn(`${this.logPrefix()} Skipping previously terminated durable operation: operation=${operationId ?? '<none>'} entry=${entry.id} code=${entry.lastErrorCode ?? 'unknown'}`);
3807
+ return {
3808
+ status: 'failed',
3809
+ outboxId: entry.id,
3810
+ error: entry.lastError ?? entry.terminal.error,
3811
+ ...(entry.lastErrorCode !== undefined ? { code: entry.lastErrorCode } : {}),
3812
+ };
3813
+ }
3465
3814
  logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
3466
3815
  // 积压深度告警:outbox 待发条目累积说明发送速度跟不上,回复将出现明显延迟。
3467
3816
  const backlog = outbox.pendingCount(this.config.aid);
@@ -3473,13 +3822,40 @@ export class AUNChannel {
3473
3822
  if (!this.reconnectTimer && !this.client) {
3474
3823
  this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendMessage failed: ${e}`));
3475
3824
  }
3476
- return;
3825
+ return { status: 'queued', outboxId: entry.id, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
3477
3826
  }
3478
3827
  // Attempt immediate delivery
3479
- const ok = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
3480
- if (ok) {
3481
- outbox.remove(this.config.aid, entry.id);
3828
+ const result = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
3829
+ if (result.status === 'sent') {
3830
+ const messageId = result.messageId ?? entry.deliveryReceipt?.messageId;
3831
+ if (!messageId) {
3832
+ const error = 'AUN send completed without a remote message_id';
3833
+ this.markPermanentOutboxFailure(entry, { error, code: 'MISSING_MESSAGE_ID' });
3834
+ return { status: 'failed', outboxId: entry.id, error, code: 'MISSING_MESSAGE_ID' };
3835
+ }
3836
+ this.removeDeliveredOutboxEntry(entry);
3837
+ return { status: 'sent', messageId };
3838
+ }
3839
+ else if (result.status === 'permanent') {
3840
+ this.markPermanentOutboxFailure(entry, result);
3841
+ return {
3842
+ status: 'failed',
3843
+ outboxId: entry.id,
3844
+ error: result.error ?? 'permanent AUN send failure',
3845
+ ...(result.code !== undefined ? { code: result.code } : {}),
3846
+ };
3847
+ }
3848
+ // A gateway receipt may already have been checkpointed even if local
3849
+ // post-send bookkeeping needs a retry. Remote acceptance still wins.
3850
+ if (entry.deliveryReceipt?.messageId) {
3851
+ return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
3482
3852
  }
3853
+ return {
3854
+ status: 'queued',
3855
+ outboxId: entry.id,
3856
+ ...(result.error !== undefined ? { error: result.error } : {}),
3857
+ ...(result.code !== undefined ? { code: result.code } : {}),
3858
+ };
3483
3859
  }
3484
3860
  /** Daemon-side transport for `ec msg send` running inside an agent task. */
3485
3861
  async sendDaemonMsg(args) {
@@ -3724,7 +4100,7 @@ export class AUNChannel {
3724
4100
  ttl: 300_000,
3725
4101
  };
3726
4102
  const ok = await this.deliverTextEntry(echoEntry);
3727
- if (!ok) {
4103
+ if (ok.status !== 'sent' && ok.status !== 'permanent') {
3728
4104
  outbox.enqueue(this.config.aid, {
3729
4105
  channelId,
3730
4106
  delivery,
@@ -3733,7 +4109,7 @@ export class AUNChannel {
3733
4109
  context: this.withDelivery(echo.context, delivery),
3734
4110
  });
3735
4111
  }
3736
- logger.info(`${this.logPrefix()} [Echo] long echo trace delivered=${ok} to ${channelId} (agent ${agentDuration}ms)`);
4112
+ logger.info(`${this.logPrefix()} [Echo] long echo trace status=${ok.status} code=${ok.code ?? 'none'} to=${channelId} agentDurationMs=${agentDuration}`);
3737
4113
  }
3738
4114
  else {
3739
4115
  outbox.enqueue(this.config.aid, {
@@ -3751,6 +4127,10 @@ export class AUNChannel {
3751
4127
  }
3752
4128
  async deliverTextEntry(entry) {
3753
4129
  const channelId = entry.channelId;
4130
+ if (typeof entry.text !== 'string' || !entry.text.trim()) {
4131
+ logger.warn(`${this.logPrefix()} deliverTextEntry: missing or empty text (outbox id=${entry.id})`);
4132
+ return { status: 'permanent', error: 'durable text is missing or empty', code: 'MISSING_TEXT' };
4133
+ }
3754
4134
  const finalText = entry.text;
3755
4135
  const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
3756
4136
  const context = this.withDelivery(entry.context, delivery);
@@ -3758,14 +4138,16 @@ export class AUNChannel {
3758
4138
  || (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
3759
4139
  if (operationId) {
3760
4140
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
3761
- if (hasMessageLogOperation(chatDir, operationId)) {
4141
+ const completedMessageId = findMessageLogOperationMessageId(chatDir, operationId);
4142
+ if (completedMessageId) {
3762
4143
  logger.info(`${this.logPrefix()} Durable operation already logged; skipping duplicate send: ${operationId}`);
3763
- return true;
4144
+ return sentOutboxResult(completedMessageId);
3764
4145
  }
3765
4146
  if (operationId === postBootstrapWelcomeOperationId(this.config.aid)) {
3766
4147
  const agentConfig = loadAgent(this.config.aid);
3767
- if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active')
3768
- return false;
4148
+ if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
4149
+ return { status: 'retry', error: 'post-bootstrap welcome is waiting for active lifecycle', code: 'BOOTSTRAP_NOT_ACTIVE' };
4150
+ }
3769
4151
  }
3770
4152
  }
3771
4153
  // 从 context.metadata.source 读取 source,默认为 'daemon'
@@ -3796,7 +4178,7 @@ export class AUNChannel {
3796
4178
  logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${finalText.length}`);
3797
4179
  const isGroup = delivery.chatType === 'group';
3798
4180
  const targetAid = channelId;
3799
- if (operationId && entry.deliveryReceipt) {
4181
+ if (entry.deliveryReceipt) {
3800
4182
  this.appendOutboundJsonl(channelId, {
3801
4183
  ...classifyAunPayloadForLog(payload),
3802
4184
  msgId: entry.deliveryReceipt.messageId,
@@ -3806,7 +4188,7 @@ export class AUNChannel {
3806
4188
  source,
3807
4189
  transport: entry.deliveryReceipt.transport,
3808
4190
  });
3809
- return true;
4191
+ return sentOutboxResult(entry.deliveryReceipt.messageId);
3810
4192
  }
3811
4193
  const encryptTarget = isGroup ? channelId : targetAid;
3812
4194
  const encrypt = context?.metadata?.encrypted != null
@@ -3815,22 +4197,27 @@ export class AUNChannel {
3815
4197
  const params = { payload, encrypt };
3816
4198
  if (context?.metadata?.persistRequired === true)
3817
4199
  params.persist_required = true;
4200
+ let acceptedMessageId;
3818
4201
  try {
3819
4202
  if (isGroup) {
3820
4203
  params.group_id = channelId;
3821
4204
  const result = await this.callAndTrace('group.send', params);
3822
- const mid = result?.message?.message_id ?? result?.message_id ?? null;
4205
+ const mid = this.messageIdFromSendResult(result);
3823
4206
  if (!mid) {
3824
- const dispatchStatus = result?.message_dispatch?.status;
3825
- if (dispatchStatus === 'debounced' || dispatchStatus === 'dispatched') {
3826
- logger.warn(`${this.logPrefix()} group.send returned ${dispatchStatus} without message_id; keeping outbox entry: group=${channelId} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
4207
+ const failure = classifyAunSendFailure(result, 'group.send returned no message_id');
4208
+ const dispatchStatus = typeof result?.message_dispatch?.status === 'string'
4209
+ ? result.message_dispatch.status.toLowerCase()
4210
+ : undefined;
4211
+ if (dispatchStatus && AUN_ACCEPTED_DISPATCH_STATUSES.has(dispatchStatus)) {
4212
+ logger.warn(`${this.logPrefix()} group.send returned ${dispatchStatus} without message_id: disposition=${failure.status} code=${failure.code ?? 'unknown'} group=${channelId}`);
3827
4213
  }
3828
4214
  else {
3829
- logger.warn(`${this.logPrefix()} group.send returned no message_id: ${JSON.stringify(result)}`);
4215
+ logger.warn(`${this.logPrefix()} group.send returned no message_id: disposition=${failure.status} code=${failure.code ?? 'unknown'} group=${channelId} dispatch=${dispatchStatus ?? 'unknown'}`);
3830
4216
  }
3831
- return false;
4217
+ return failure;
3832
4218
  }
3833
4219
  else {
4220
+ acceptedMessageId = mid;
3834
4221
  this.logAunSendAccepted('group.send', channelId, mid, encrypt, result, finalText);
3835
4222
  this.checkpointTextDelivery(entry, mid, encrypt, result);
3836
4223
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
@@ -3847,34 +4234,41 @@ export class AUNChannel {
3847
4234
  else {
3848
4235
  params.to = targetAid;
3849
4236
  const result = await this.callAndTrace('message.send', params);
3850
- if (!result || !result.message_id) {
4237
+ const mid = this.messageIdFromSendResult(result);
4238
+ if (!mid) {
3851
4239
  logger.warn(`${this.logPrefix()} message.send returned no message_id: ${JSON.stringify(result)}`);
3852
- return false;
4240
+ return classifyAunSendFailure(result, 'message.send returned no message_id');
3853
4241
  }
3854
4242
  else {
3855
- this.logAunSendAccepted('message.send', this.peerLabel(targetAid), result.message_id, encrypt, result, finalText);
3856
- this.checkpointTextDelivery(entry, result.message_id, encrypt, result);
4243
+ acceptedMessageId = mid;
4244
+ this.logAunSendAccepted('message.send', this.peerLabel(targetAid), mid, encrypt, result, finalText);
4245
+ this.checkpointTextDelivery(entry, mid, encrypt, result);
3857
4246
  const causation = normalizeCausation(context?.metadata?.causation);
3858
4247
  if (causation) {
3859
- registerAunCausation(result.message_id, this.config.aid, targetAid, causation);
4248
+ registerAunCausation(mid, this.config.aid, targetAid, causation);
3860
4249
  recordCausationSpan(causation, 'message.outbound', {
3861
4250
  status: 'completed',
3862
- refs: { messageId: result.message_id, taskId: context?.metadata?.taskId },
4251
+ refs: { messageId: mid, taskId: context?.metadata?.taskId },
3863
4252
  });
3864
4253
  }
3865
- appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: targetAid, msgId: result.message_id, kind: 'text', len: finalText.length });
4254
+ appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: targetAid, msgId: mid, kind: 'text', len: finalText.length });
3866
4255
  this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode, 'send');
3867
4256
  this.appendOutboundJsonl(targetAid, {
3868
- ...classifyAunPayloadForLog(payload), msgId: result.message_id, encrypt, context, isGroup: false, source,
4257
+ ...classifyAunPayloadForLog(payload), msgId: mid, encrypt, context, isGroup: false, source,
3869
4258
  transport: this.sendReceiptFromResult(result),
3870
4259
  });
3871
4260
  // Observer forward: outbound (private) — 原样转发 SDK SendResult(含 envelope + payload)
3872
4261
  this.forwardOutbound(result);
3873
4262
  }
3874
4263
  }
3875
- return true;
4264
+ return sentOutboxResult(acceptedMessageId);
3876
4265
  }
3877
4266
  catch (e) {
4267
+ if (entry.deliveryReceipt) {
4268
+ const error = e instanceof Error ? e.message : String(e);
4269
+ logger.error(`${this.logPrefix()} AUN accepted the durable text but local post-send processing failed; retaining receipt for recovery: id=${entry.id} error=${error}`);
4270
+ return { status: 'retry', error, code: 'OUTBOX_POST_SEND_FAILED' };
4271
+ }
3878
4272
  if (encrypt && e instanceof E2EEError) {
3879
4273
  this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
3880
4274
  logger.warn(`${this.logPrefix()} E2EE send failed to ${channelId}, retrying plaintext: ${e}`);
@@ -3882,13 +4276,17 @@ export class AUNChannel {
3882
4276
  try {
3883
4277
  if (isGroup) {
3884
4278
  this.trace('OUT', 'group.send.fallback', params);
3885
- const result = await this.client.call('group.send', params);
4279
+ const result = await this.callClient('group.send', params);
3886
4280
  const mid = this.messageIdFromSendResult(result);
3887
- this.trace('OUT', 'group.send.fallback.ok', { message_id: mid });
3888
4281
  if (!mid) {
4282
+ const resultRecord = errorRecord(result);
4283
+ const dispatch = errorRecord(resultRecord?.message_dispatch)?.status;
4284
+ this.trace('OUT', 'group.send.fallback.missing_id', { dispatch });
3889
4285
  logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
3890
- return false;
4286
+ return classifyAunSendFailure(result, 'group.send plaintext fallback returned no message_id');
3891
4287
  }
4288
+ acceptedMessageId = mid;
4289
+ this.trace('OUT', 'group.send.fallback.ok', { message_id: mid });
3892
4290
  this.checkpointTextDelivery(entry, mid, false, result);
3893
4291
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
3894
4292
  const statsContext = await this.groupStatsContext(channelId);
@@ -3901,13 +4299,15 @@ export class AUNChannel {
3901
4299
  }
3902
4300
  else {
3903
4301
  this.trace('OUT', 'message.send.fallback', params);
3904
- const result = await this.client.call('message.send', params);
3905
- const mid = result?.message_id;
3906
- this.trace('OUT', 'message.send.fallback.ok', { message_id: mid });
3907
- if (!result || !mid) {
4302
+ const result = await this.callClient('message.send', params);
4303
+ const mid = this.messageIdFromSendResult(result);
4304
+ if (!mid) {
4305
+ this.trace('OUT', 'message.send.fallback.missing_id', {});
3908
4306
  logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
3909
- return false;
4307
+ return classifyAunSendFailure(result, 'message.send plaintext fallback returned no message_id');
3910
4308
  }
4309
+ acceptedMessageId = mid;
4310
+ this.trace('OUT', 'message.send.fallback.ok', { message_id: mid });
3911
4311
  this.checkpointTextDelivery(entry, mid, false, result);
3912
4312
  const causation = normalizeCausation(context?.metadata?.causation);
3913
4313
  if (causation) {
@@ -3925,31 +4325,49 @@ export class AUNChannel {
3925
4325
  });
3926
4326
  this.forwardOutbound(result);
3927
4327
  }
3928
- return true;
4328
+ return sentOutboxResult(acceptedMessageId);
3929
4329
  }
3930
4330
  catch (e2) {
4331
+ if (entry.deliveryReceipt) {
4332
+ const error = e2 instanceof Error ? e2.message : String(e2);
4333
+ logger.error(`${this.logPrefix()} AUN accepted the plaintext fallback but local post-send processing failed; retaining receipt for recovery: id=${entry.id} error=${error}`);
4334
+ return { status: 'retry', error, code: 'OUTBOX_POST_SEND_FAILED' };
4335
+ }
3931
4336
  this.trace('OUT', 'send.fallback.error', { channelId, error: String(e2) });
3932
4337
  logger.error(`${this.logPrefix()} Plaintext fallback also failed to ${channelId}: ${e2}`);
3933
- return false;
4338
+ return classifyAunSendFailure(e2, `plaintext send fallback failed to ${channelId}`);
3934
4339
  }
3935
4340
  }
3936
4341
  else {
3937
4342
  this.trace('OUT', 'send.error', { channelId, error: String(e) });
3938
4343
  logger.error(`${this.logPrefix()} Send failed to ${channelId} (outbox id=${entry.id}): ${e}`);
3939
- return false;
4344
+ return classifyAunSendFailure(e, `send failed to ${channelId}`);
3940
4345
  }
3941
4346
  }
3942
4347
  }
3943
4348
  checkpointTextDelivery(entry, messageId, encrypt, result) {
3944
- if (!entry.dedupeKey)
3945
- return;
3946
- entry.deliveryReceipt = {
4349
+ this.checkpointDurableDelivery(entry, messageId, encrypt, result);
4350
+ }
4351
+ checkpointDurableDelivery(entry, messageId, encrypt, result) {
4352
+ const submittedRoute = {
4353
+ id: entry.id,
4354
+ channelId: entry.channelId,
4355
+ delivery: entry.delivery,
4356
+ queue: entry.queue,
4357
+ };
4358
+ const receipt = {
3947
4359
  messageId,
3948
4360
  encrypt,
3949
4361
  transport: this.sendReceiptFromResult(result),
3950
4362
  };
3951
- if (!outbox.replace(this.config.aid, entry)) {
3952
- logger.warn(`${this.logPrefix()} Failed to checkpoint durable operation receipt: ${entry.dedupeKey}`);
4363
+ const replaced = outbox.updateDeliveryReceiptIfRouteMatches(this.config.aid, submittedRoute, receipt);
4364
+ if (replaced === 'replaced') {
4365
+ entry.deliveryReceipt = receipt;
4366
+ return true;
4367
+ }
4368
+ else {
4369
+ logger.warn(`${this.logPrefix()} Did not checkpoint durable delivery receipt: entry=${entry.id} operation=${entry.dedupeKey ?? 'none'} reason=${replaced}`);
4370
+ return false;
3953
4371
  }
3954
4372
  }
3955
4373
  async deliverPayloadEntry(entry) {
@@ -3958,32 +4376,85 @@ export class AUNChannel {
3958
4376
  : undefined;
3959
4377
  if (interactionId && this.invalidatedInteractions.has(interactionId)) {
3960
4378
  logger.info(`${this.logPrefix()} Discarded invalidated interaction from durable outbox: request=${interactionId} entry=${entry.id}`);
3961
- return { ok: true };
4379
+ return { ok: true, status: 'sent' };
4380
+ }
4381
+ const interactionExpiresAt = entry.postSend?.type === 'register_interaction_card'
4382
+ ? entry.postSend.expiresAt
4383
+ : undefined;
4384
+ if (typeof interactionExpiresAt === 'number'
4385
+ && Number.isFinite(interactionExpiresAt)
4386
+ && interactionExpiresAt <= Date.now()) {
4387
+ logger.info(`${this.logPrefix()} Discarded expired interaction from durable outbox: request=${interactionId ?? '<unknown>'} entry=${entry.id}`);
4388
+ return {
4389
+ ok: false,
4390
+ status: 'permanent',
4391
+ error: 'interaction card has expired',
4392
+ code: 'INTERACTION_EXPIRED',
4393
+ };
3962
4394
  }
3963
4395
  const channelId = entry.channelId;
3964
4396
  const payload = entry.payload;
3965
4397
  if (!payload) {
3966
4398
  logger.warn(`${this.logPrefix()} deliverPayloadEntry: missing payload (outbox id=${entry.id})`);
3967
- return { ok: true };
4399
+ return { ok: false, status: 'permanent', error: 'durable payload is missing', code: 'MISSING_PAYLOAD' };
3968
4400
  }
3969
4401
  const contentKind = entry.contentKind;
3970
4402
  const logText = entry.logText ?? this.payloadLogText(payload, contentKind);
3971
4403
  const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
3972
4404
  const context = this.withDelivery(entry.context, delivery);
3973
4405
  logger.info(`${this.logPrefix()} deliverPayloadEntry: id=${entry.id} kind=${contentKind ?? payload.type ?? 'payload'} channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} textLen=${logText.length}`);
3974
- const sent = await this.sendAunPayload(channelId, payload, context, `${contentKind ?? payload.type ?? 'payload'}`);
3975
- if (sent.discarded) {
3976
- logger.warn(`${this.logPrefix()} Removed malformed durable payload from outbox: id=${entry.id} channel=${channelId}`);
3977
- return { ok: true, discarded: true };
4406
+ const isGroup = delivery.chatType === 'group';
4407
+ const source = context?.metadata?.source ?? 'daemon';
4408
+ if (entry.deliveryReceipt) {
4409
+ try {
4410
+ this.appendOutboundJsonl(channelId, {
4411
+ ...classifyAunPayloadForLog(payload),
4412
+ msgId: entry.deliveryReceipt.messageId,
4413
+ encrypt: entry.deliveryReceipt.encrypt,
4414
+ context,
4415
+ isGroup,
4416
+ source,
4417
+ transport: entry.deliveryReceipt.transport,
4418
+ });
4419
+ this.runPostSend(entry, entry.deliveryReceipt.messageId);
4420
+ return {
4421
+ ok: true,
4422
+ status: 'sent',
4423
+ messageId: entry.deliveryReceipt.messageId,
4424
+ encrypt: entry.deliveryReceipt.encrypt,
4425
+ };
4426
+ }
4427
+ catch (error) {
4428
+ const detail = error instanceof Error ? error.message : String(error);
4429
+ logger.error(`${this.logPrefix()} Failed to recover local post-send state from durable payload receipt: id=${entry.id} error=${detail}`);
4430
+ return { ok: false, status: 'retry', error: detail, code: 'OUTBOX_POST_SEND_FAILED' };
4431
+ }
3978
4432
  }
4433
+ const sent = await this.sendAunPayload(channelId, payload, context, `${contentKind ?? payload.type ?? 'payload'}`);
3979
4434
  if (!sent.ok || !sent.messageId)
3980
4435
  return sent;
3981
- const isGroup = delivery.chatType === 'group';
3982
- const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
3983
- this.logAunSendAccepted(isGroup ? 'group.send' : 'message.send', isGroup ? channelId : this.peerLabel(channelId), sent.messageId, !!sent.encrypt, sent.result, logText);
3984
- this.recordDurableOutbound(channelId, payload, sent.messageId, !!sent.encrypt, context, isGroup, contentKind, logText, sent.result, statsContext);
3985
- this.runPostSend(entry, sent.messageId);
3986
- return sent;
4436
+ try {
4437
+ this.checkpointDurableDelivery(entry, sent.messageId, !!sent.encrypt, sent.result);
4438
+ }
4439
+ catch (error) {
4440
+ const detail = error instanceof Error ? error.message : String(error);
4441
+ logger.error(`${this.logPrefix()} AUN accepted the durable payload but its receipt could not be persisted: id=${entry.id} mid=${sent.messageId} error=${detail}`);
4442
+ return { ok: false, status: 'permanent', error: detail, code: 'OUTBOX_RECEIPT_CHECKPOINT_FAILED' };
4443
+ }
4444
+ try {
4445
+ const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
4446
+ this.logAunSendAccepted(isGroup ? 'group.send' : 'message.send', isGroup ? channelId : this.peerLabel(channelId), sent.messageId, !!sent.encrypt, sent.result, logText);
4447
+ this.recordDurableOutbound(channelId, payload, sent.messageId, !!sent.encrypt, context, isGroup, contentKind, logText, sent.result, statsContext);
4448
+ this.runPostSend(entry, sent.messageId);
4449
+ return sent;
4450
+ }
4451
+ catch (error) {
4452
+ const detail = error instanceof Error ? error.message : String(error);
4453
+ logger.error(`${this.logPrefix()} AUN accepted the durable payload but local post-send processing failed: id=${entry.id} mid=${sent.messageId} error=${detail}`);
4454
+ return entry.deliveryReceipt
4455
+ ? { ok: false, status: 'retry', error: detail, code: 'OUTBOX_POST_SEND_FAILED' }
4456
+ : { ok: false, status: 'permanent', error: detail, code: 'OUTBOX_POST_SEND_UNCHECKPOINTED' };
4457
+ }
3987
4458
  }
3988
4459
  /** 有效会话正文写入 messages.jsonl(message.send/group.send 成功后调用)。 */
3989
4460
  appendOutboundJsonl(channelId, descriptor) {
@@ -4127,16 +4598,22 @@ export class AUNChannel {
4127
4598
  logger.debug(`${this.logPrefix()} thought.put failed to ${channelId}: ${err?.name}(${err?.code})=${err?.message}`);
4128
4599
  }
4129
4600
  }
4130
- /** 发送结构化 AUN payload;transient 协议载荷不写本地 messages.jsonl。 */
4131
- async sendStructured(channelId, payload, context) {
4601
+ /** Send a transient structured payload and preserve the gateway disposition. */
4602
+ async sendStructuredResult(channelId, payload, context) {
4132
4603
  const delivery = this.requireDelivery(channelId, context);
4133
4604
  context = this.withDelivery(context, delivery);
4134
4605
  // Validate before checking connection state so malformed producer payloads
4135
4606
  // are reported deterministically instead of being hidden by a disconnect.
4136
4607
  const validatedPayload = this.normalizeAunPayloadMentions(payload);
4137
4608
  const finalPayload = this.stripUndefinedDeep(validatedPayload);
4138
- if (!this.connected || !this.client)
4139
- return null;
4609
+ if (!this.connected || !this.client) {
4610
+ return {
4611
+ ok: false,
4612
+ status: 'retry',
4613
+ error: 'AUN channel is not connected',
4614
+ code: 'AUN_NOT_CONNECTED',
4615
+ };
4616
+ }
4140
4617
  const isGroup = delivery.chatType === 'group';
4141
4618
  const targetAid = channelId;
4142
4619
  const encryptTarget = isGroup ? channelId : targetAid;
@@ -4165,27 +4642,54 @@ export class AUNChannel {
4165
4642
  if (isGroup) {
4166
4643
  params.group_id = delivery.groupId;
4167
4644
  const result = await this.callAndTrace('group.send', params);
4168
- const mid = result?.message?.message_id ?? result?.message_id ?? null;
4645
+ const mid = this.messageIdFromSendResult(result);
4646
+ if (!mid) {
4647
+ const failure = classifyAunSendFailure(result, 'group.send returned no message_id');
4648
+ logger.warn(`${this.logPrefix()} group.send (${payload.type}) returned no message_id; disposition=${failure.status}`);
4649
+ return { ok: false, ...failure };
4650
+ }
4169
4651
  logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
4170
4652
  if (!isMenuPayload)
4171
4653
  this.forwardOutbound(result);
4172
- return mid;
4654
+ return { ok: true, messageId: mid };
4173
4655
  }
4174
4656
  else {
4175
4657
  params.to = targetAid;
4176
4658
  const result = await this.callAndTrace('message.send', params);
4177
- logger.info(`${this.logPrefix()} message.send (${payload.type}) ok: to=${this.peerLabel(targetAid)} mid=${result?.message_id} encrypt=${encrypt}`);
4659
+ const mid = this.messageIdFromSendResult(result);
4660
+ if (!mid) {
4661
+ const failure = classifyAunSendFailure(result, 'message.send returned no message_id');
4662
+ logger.warn(`${this.logPrefix()} message.send (${payload.type}) returned no message_id; disposition=${failure.status}`);
4663
+ return { ok: false, ...failure };
4664
+ }
4665
+ logger.info(`${this.logPrefix()} message.send (${payload.type}) ok: to=${this.peerLabel(targetAid)} mid=${mid} encrypt=${encrypt}`);
4178
4666
  if (!isMenuPayload)
4179
4667
  this.forwardOutbound(result);
4180
- return result?.message_id ?? null;
4668
+ return { ok: true, messageId: mid };
4181
4669
  }
4182
4670
  }
4183
4671
  catch (e) {
4184
- const err = e;
4185
- logger.warn(`${this.logPrefix()} sendStructured failed (${payload.type}) to ${channelId}: ${err?.name}(${err?.code})=${err?.message}`);
4186
- return null;
4672
+ const failure = classifyAunSendFailure(e, `structured send failed to ${channelId}`);
4673
+ logger.warn(`${this.logPrefix()} sendStructured failed (${payload.type}) to ${channelId}: disposition=${failure.status} code=${failure.code ?? 'unknown'} error=${failure.error}`);
4674
+ return { ok: false, ...failure };
4187
4675
  }
4188
4676
  }
4677
+ /** Compatibility API for optional transient payloads. */
4678
+ async sendStructured(channelId, payload, context) {
4679
+ const result = await this.sendStructuredResult(channelId, payload, context);
4680
+ return result.ok ? result.messageId : null;
4681
+ }
4682
+ /** Strict API for callers that must not report a failed transport as sent. */
4683
+ async sendStructuredOrThrow(channelId, payload, context) {
4684
+ const result = await this.sendStructuredResult(channelId, payload, context);
4685
+ if (result.ok)
4686
+ return result.messageId;
4687
+ throw Object.assign(new Error(result.error), {
4688
+ name: 'AUNSendError',
4689
+ code: result.code ?? 'AUN_SEND_FAILED',
4690
+ retryable: result.status === 'retry',
4691
+ });
4692
+ }
4189
4693
  async sendFile(channelId, filePath, context) {
4190
4694
  const delivery = this.requireDelivery(channelId, context);
4191
4695
  context = this.withDelivery(context, delivery);
@@ -4208,7 +4712,9 @@ export class AUNChannel {
4208
4712
  channelId,
4209
4713
  delivery,
4210
4714
  type: 'file',
4715
+ contentKind: 'file',
4211
4716
  filePath: absPath,
4717
+ logText: `📎 ${path.basename(absPath)} (${formatSize(stat.size)})`,
4212
4718
  context,
4213
4719
  });
4214
4720
  logger.debug(`${this.logPrefix()} Outbox enqueued file: id=${entry.id} channel=${channelId} file=${absPath}`);
@@ -4219,9 +4725,12 @@ export class AUNChannel {
4219
4725
  }
4220
4726
  return;
4221
4727
  }
4222
- const ok = await this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), false);
4223
- if (ok) {
4224
- outbox.remove(this.config.aid, entry.id);
4728
+ const result = await this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
4729
+ if (result.status === 'sent') {
4730
+ this.removeDeliveredOutboxEntry(entry);
4731
+ }
4732
+ else if (result.status === 'permanent') {
4733
+ this.markPermanentOutboxFailure(entry, result);
4225
4734
  }
4226
4735
  }
4227
4736
  async sendImage(channelId, data, mimeType, alt, context) {
@@ -4263,9 +4772,11 @@ export class AUNChannel {
4263
4772
  }
4264
4773
  return;
4265
4774
  }
4266
- const ok = await this.withOutboxInFlight(entry, () => this.deliverImageEntry(entry), false);
4267
- if (ok)
4268
- outbox.remove(this.config.aid, entry.id);
4775
+ const result = await this.withOutboxInFlight(entry, () => this.deliverImageEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
4776
+ if (result.status === 'sent')
4777
+ this.removeDeliveredOutboxEntry(entry);
4778
+ else if (result.status === 'permanent')
4779
+ this.markPermanentOutboxFailure(entry, result);
4269
4780
  }
4270
4781
  async buildUploadedImagePayload(data, mimeType, alt) {
4271
4782
  if (!this.connected || !this.client) {
@@ -4305,7 +4816,7 @@ export class AUNChannel {
4305
4816
  const image = entry.image;
4306
4817
  if (!image || typeof image.dataBase64 !== 'string') {
4307
4818
  logger.warn(`${this.logPrefix()} deliverImageEntry: missing image data (outbox id=${entry.id})`);
4308
- return true;
4819
+ return { status: 'permanent', error: 'durable image data is missing', code: 'MISSING_IMAGE_DATA' };
4309
4820
  }
4310
4821
  const delivery = this.requirePersistedDelivery(entry.channelId, entry.delivery);
4311
4822
  entry.delivery = delivery;
@@ -4313,43 +4824,75 @@ export class AUNChannel {
4313
4824
  const data = Buffer.from(image.dataBase64, 'base64');
4314
4825
  if (data.length === 0) {
4315
4826
  logger.warn(`${this.logPrefix()} deliverImageEntry: invalid image data (outbox id=${entry.id})`);
4316
- return true;
4827
+ return { status: 'permanent', error: 'durable image data is invalid', code: 'INVALID_IMAGE_DATA' };
4317
4828
  }
4318
- const imagePayload = await this.buildUploadedImagePayload(data, image.mimeType, image.alt);
4319
- entry.type = 'payload';
4320
- entry.contentKind = 'image';
4321
- entry.payload = this.applyReplyContextToPayload(imagePayload, entry.context);
4322
- entry.logText ??= image.alt ? `[image] ${image.alt}` : '[image]';
4323
- delete entry.image;
4324
- // Persist the attachment reference before message.send. A failed send then
4325
- // retries the compact wire payload without re-uploading the same image.
4326
- let payloadEntry = entry;
4327
- if (!outbox.replace(this.config.aid, entry)) {
4328
- payloadEntry = outbox.enqueue(this.config.aid, {
4829
+ try {
4830
+ const imagePayload = await this.buildUploadedImagePayload(data, image.mimeType, image.alt);
4831
+ entry.type = 'payload';
4832
+ entry.contentKind = 'image';
4833
+ entry.payload = this.applyReplyContextToPayload(imagePayload, entry.context);
4834
+ entry.logText ??= image.alt ? `[image] ${image.alt}` : '[image]';
4835
+ delete entry.image;
4836
+ // Persist the attachment reference before message.send. A failed send then
4837
+ // retries the compact wire payload without re-uploading the same image.
4838
+ const submittedRoute = {
4839
+ id: entry.id,
4329
4840
  channelId: entry.channelId,
4330
4841
  delivery,
4331
- type: 'payload',
4332
- contentKind: 'image',
4333
- payload: entry.payload,
4334
- context: entry.context,
4335
- logText: entry.logText,
4336
- ttl: entry.ttl,
4337
- });
4842
+ };
4843
+ const replaced = outbox.replaceIfRouteMatches(this.config.aid, submittedRoute, entry);
4844
+ if (replaced === 'route-changed') {
4845
+ logger.warn(`${this.logPrefix()} Image route changed during upload; preserving original outbox entry for the corrected route: id=${entry.id}`);
4846
+ return { status: 'retry', error: 'image delivery route changed during upload', code: 'OUTBOX_ROUTE_CHANGED' };
4847
+ }
4848
+ if (replaced === 'missing') {
4849
+ logger.warn(`${this.logPrefix()} Image outbox entry disappeared during upload; skipping send: id=${entry.id}`);
4850
+ return { status: 'permanent', error: 'image outbox entry disappeared during upload', code: 'OUTBOX_ENTRY_MISSING' };
4851
+ }
4852
+ const sent = await this.deliverPayloadEntry(entry);
4853
+ return sent.ok ? sentOutboxResult(sent.messageId) : sent;
4338
4854
  }
4339
- const sent = await this.deliverPayloadEntry(payloadEntry);
4340
- if (sent.ok && payloadEntry.id !== entry.id) {
4341
- outbox.remove(this.config.aid, payloadEntry.id);
4855
+ catch (error) {
4856
+ const failure = classifyAunSendFailure(error, 'image upload or send failed');
4857
+ logger.error(`${this.logPrefix()} Image delivery failed (outbox id=${entry.id}): ${failure.error}; disposition=${failure.status}`);
4858
+ return failure;
4342
4859
  }
4343
- return sent.ok;
4344
4860
  }
4345
4861
  async deliverFileEntry(entry) {
4346
4862
  const channelId = entry.channelId;
4863
+ if (typeof entry.filePath !== 'string' || !entry.filePath.trim()) {
4864
+ logger.warn(`${this.logPrefix()} deliverFileEntry: missing file path (outbox id=${entry.id})`);
4865
+ return { status: 'permanent', error: 'durable file path is missing', code: 'MISSING_FILE_PATH' };
4866
+ }
4347
4867
  const absPath = entry.filePath;
4348
4868
  const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
4349
4869
  const context = this.withDelivery(entry.context, delivery);
4870
+ const isGroup = delivery.chatType === 'group';
4871
+ const source = context?.metadata?.source ?? 'daemon';
4872
+ if (entry.deliveryReceipt) {
4873
+ const filename = path.basename(absPath);
4874
+ const logText = entry.logText ?? `[file] ${filename}`;
4875
+ try {
4876
+ this.appendOutboundJsonl(channelId, {
4877
+ ...classifyAunPayloadForLog({ type: 'file', text: logText }),
4878
+ msgId: entry.deliveryReceipt.messageId,
4879
+ encrypt: entry.deliveryReceipt.encrypt,
4880
+ context,
4881
+ isGroup,
4882
+ source,
4883
+ transport: entry.deliveryReceipt.transport,
4884
+ });
4885
+ return sentOutboxResult(entry.deliveryReceipt.messageId);
4886
+ }
4887
+ catch (error) {
4888
+ const detail = error instanceof Error ? error.message : String(error);
4889
+ logger.error(`${this.logPrefix()} Failed to recover local file post-send state from durable receipt: id=${entry.id} error=${detail}`);
4890
+ return { status: 'retry', error: detail, code: 'OUTBOX_POST_SEND_FAILED' };
4891
+ }
4892
+ }
4350
4893
  if (!fs.existsSync(absPath)) {
4351
4894
  logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
4352
- return true; // remove from outbox, file no longer exists
4895
+ return { status: 'permanent', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
4353
4896
  }
4354
4897
  const filename = path.basename(absPath);
4355
4898
  const fileData = fs.readFileSync(absPath);
@@ -4404,7 +4947,6 @@ export class AUNChannel {
4404
4947
  attachment,
4405
4948
  context,
4406
4949
  });
4407
- const isGroup = delivery.chatType === 'group';
4408
4950
  const fileTargetAid = channelId;
4409
4951
  const encryptTarget = isGroup ? channelId : fileTargetAid;
4410
4952
  const encrypt = context?.metadata?.encrypted != null
@@ -4417,26 +4959,26 @@ export class AUNChannel {
4417
4959
  if (isGroup) {
4418
4960
  params.group_id = delivery.groupId;
4419
4961
  this.trace('OUT', 'group.send.file', params);
4420
- const result = await this.client.call('group.send', params);
4962
+ const result = await this.callClient('group.send', params);
4421
4963
  sendResult = result;
4422
- const fileMid = result?.message?.message_id ?? result?.message_id;
4964
+ const fileMid = this.messageIdFromSendResult(result);
4423
4965
  sentMid = fileMid ?? null;
4424
4966
  this.trace('OUT', 'group.send.file.ok', { message_id: fileMid });
4425
4967
  if (!fileMid) {
4426
4968
  logger.warn(`${this.logPrefix()} group.send.file returned no message_id: ${JSON.stringify(result)}`);
4427
- return false;
4969
+ return classifyAunSendFailure(result, 'group.send.file returned no message_id');
4428
4970
  }
4429
4971
  }
4430
4972
  else {
4431
4973
  params.to = fileTargetAid;
4432
4974
  this.trace('OUT', 'message.send.file', params);
4433
- const result = await this.client.call('message.send', params);
4975
+ const result = await this.callClient('message.send', params);
4434
4976
  sendResult = result;
4435
- sentMid = result?.message_id ?? null;
4977
+ sentMid = this.messageIdFromSendResult(result);
4436
4978
  this.trace('OUT', 'message.send.file.ok', { message_id: sentMid });
4437
- if (!result || !sentMid) {
4979
+ if (!sentMid) {
4438
4980
  logger.warn(`${this.logPrefix()} message.send.file returned no message_id: ${JSON.stringify(result)}`);
4439
- return false;
4981
+ return classifyAunSendFailure(result, 'message.send.file returned no message_id');
4440
4982
  }
4441
4983
  }
4442
4984
  }
@@ -4451,25 +4993,25 @@ export class AUNChannel {
4451
4993
  params.encrypt = false;
4452
4994
  if (isGroup) {
4453
4995
  this.trace('OUT', 'group.send.file.fallback', params);
4454
- const result = await this.client.call('group.send', params);
4996
+ const result = await this.callClient('group.send', params);
4455
4997
  sendResult = result;
4456
- const fbMid = result?.message?.message_id ?? result?.message_id;
4998
+ const fbMid = this.messageIdFromSendResult(result);
4457
4999
  sentMid = fbMid ?? null;
4458
5000
  this.trace('OUT', 'group.send.file.fallback.ok', { message_id: fbMid });
4459
5001
  if (!fbMid) {
4460
5002
  logger.warn(`${this.logPrefix()} group.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
4461
- return false;
5003
+ return classifyAunSendFailure(result, 'group.send.file plaintext fallback returned no message_id');
4462
5004
  }
4463
5005
  }
4464
5006
  else {
4465
5007
  this.trace('OUT', 'message.send.file.fallback', params);
4466
- const result = await this.client.call('message.send', params);
5008
+ const result = await this.callClient('message.send', params);
4467
5009
  sendResult = result;
4468
- sentMid = result?.message_id ?? null;
5010
+ sentMid = this.messageIdFromSendResult(result);
4469
5011
  this.trace('OUT', 'message.send.file.fallback.ok', { message_id: sentMid });
4470
- if (!result || !sentMid) {
5012
+ if (!sentMid) {
4471
5013
  logger.warn(`${this.logPrefix()} message.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
4472
- return false;
5014
+ return classifyAunSendFailure(result, 'message.send.file plaintext fallback returned no message_id');
4473
5015
  }
4474
5016
  }
4475
5017
  }
@@ -4477,26 +5019,40 @@ export class AUNChannel {
4477
5019
  throw sendErr;
4478
5020
  }
4479
5021
  }
4480
- logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
4481
- if (sentMid) {
4482
- const fileText = filePayload.text;
4483
- appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: sentMid, kind: 'file', len: fileText.length, ...(isGroup && { groupId: channelId }) });
4484
- const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
4485
- this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(fileText, 'utf-8'), fileText, false, !!params.encrypt, context?.metadata?.chatmode, 'send', statsContext);
4486
- const source = context?.metadata?.source ?? 'daemon';
4487
- this.appendOutboundJsonl(channelId, {
4488
- ...classifyAunPayloadForLog(filePayload), msgId: sentMid, encrypt: !!params.encrypt,
4489
- context, isGroup, source,
4490
- });
5022
+ if (!sentMid) {
5023
+ return { status: 'permanent', error: 'file send returned no message_id', code: 'MISSING_MESSAGE_ID' };
5024
+ }
5025
+ try {
5026
+ this.checkpointDurableDelivery(entry, sentMid, !!params.encrypt, sendResult);
5027
+ }
5028
+ catch (error) {
5029
+ const detail = error instanceof Error ? error.message : String(error);
5030
+ logger.error(`${this.logPrefix()} AUN accepted the durable file but its receipt could not be persisted: id=${entry.id} mid=${sentMid} error=${detail}`);
5031
+ return { status: 'permanent', error: detail, code: 'OUTBOX_RECEIPT_CHECKPOINT_FAILED' };
4491
5032
  }
5033
+ logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
5034
+ const fileText = filePayload.text;
5035
+ appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: sentMid, kind: 'file', len: fileText.length, ...(isGroup && { groupId: channelId }) });
5036
+ const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
5037
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(fileText, 'utf-8'), fileText, false, !!params.encrypt, context?.metadata?.chatmode, 'send', statsContext);
5038
+ this.appendOutboundJsonl(channelId, {
5039
+ ...classifyAunPayloadForLog(filePayload), msgId: sentMid, encrypt: !!params.encrypt,
5040
+ context, isGroup, source, transport: this.sendReceiptFromResult(sendResult),
5041
+ });
4492
5042
  if (sendResult)
4493
5043
  this.forwardOutbound(sendResult);
4494
- return true;
5044
+ return sentOutboxResult(sentMid);
4495
5045
  }
4496
5046
  catch (e) {
5047
+ if (entry.deliveryReceipt) {
5048
+ const detail = e instanceof Error ? e.message : String(e);
5049
+ logger.error(`${this.logPrefix()} AUN accepted the durable file but local post-send processing failed; retaining receipt for recovery: id=${entry.id} error=${detail}`);
5050
+ return { status: 'retry', error: detail, code: 'OUTBOX_POST_SEND_FAILED' };
5051
+ }
4497
5052
  this.trace('OUT', 'sendFile.error', { channelId, filePath: absPath, error: String(e) });
4498
- logger.error(`${this.logPrefix()} sendFile failed for ${channelId} (outbox id=${entry.id}): ${e}`);
4499
- return false;
5053
+ const failure = classifyAunSendFailure(e, `sendFile failed for ${channelId}`);
5054
+ logger.error(`${this.logPrefix()} sendFile failed for ${channelId} (outbox id=${entry.id}): ${failure.error}; disposition=${failure.status}`);
5055
+ return failure;
4500
5056
  }
4501
5057
  }
4502
5058
  // ── Outbox drain ───────────────────────────────────────────
@@ -4505,7 +5061,7 @@ export class AUNChannel {
4505
5061
  if (this.outboxTimer)
4506
5062
  return;
4507
5063
  this.outboxTimer = setInterval(() => {
4508
- if (this.connected && this.client && outbox.hasPending(this.config.aid)) {
5064
+ if (this.connected && this.client && (outbox.hasPending(this.config.aid) || outbox.hasPending(this.config.aid, 'activity'))) {
4509
5065
  this.drainOutbox();
4510
5066
  }
4511
5067
  }, 30_000);
@@ -4519,58 +5075,86 @@ export class AUNChannel {
4519
5075
  async drainOutbox() {
4520
5076
  if (!this.connected || !this.client)
4521
5077
  return;
4522
- await this.migrateLegacyBootstrapRoutes();
4523
- if (!outbox.hasPending(this.config.aid))
4524
- return;
4525
- logger.info(`${this.logPrefix()} Draining outbox...`);
4526
- const result = await outbox.drain(this.config.aid, async (entry) => {
4527
- if (!isDeliveryTarget(entry.delivery)) {
4528
- logger.warn(`${this.logPrefix()} Discarding legacy/malformed outbox entry without a trusted delivery route: id=${entry.id} channel=${entry.channelId}`);
4529
- return true;
4530
- }
4531
- if (entry.type === 'text') {
4532
- return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
4533
- }
4534
- else if (entry.type === 'file') {
4535
- return this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), false);
4536
- }
4537
- else if (entry.type === 'image') {
4538
- return this.withOutboxInFlight(entry, () => this.deliverImageEntry(entry), false);
4539
- }
4540
- else if (entry.type === 'payload') {
4541
- const sent = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false });
4542
- return sent.ok;
5078
+ this.repairBootstrapRoutes();
5079
+ const drainQueue = async (queue) => {
5080
+ if (!outbox.hasPending(this.config.aid, queue))
5081
+ return;
5082
+ logger.info(`${this.logPrefix()} Draining ${queue} outbox...`);
5083
+ const result = await outbox.drain(this.config.aid, async (entry) => {
5084
+ if (!isDeliveryTarget(entry.delivery)) {
5085
+ logger.warn(`${this.logPrefix()} Discarding legacy/malformed outbox entry without a trusted delivery route: id=${entry.id} channel=${entry.channelId}`);
5086
+ return {
5087
+ status: 'permanent',
5088
+ error: 'outbox entry has no valid delivery route',
5089
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
5090
+ };
5091
+ }
5092
+ if (entry.type === 'text') {
5093
+ return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
5094
+ }
5095
+ else if (entry.type === 'file') {
5096
+ return this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
5097
+ }
5098
+ else if (entry.type === 'image') {
5099
+ return this.withOutboxInFlight(entry, () => this.deliverImageEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
5100
+ }
5101
+ else if (entry.type === 'payload') {
5102
+ const sent = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false, status: 'retry', error: 'outbox send is already in flight' });
5103
+ return sent.ok
5104
+ ? { status: 'sent', ...(sent.messageId ? { messageId: sent.messageId } : {}) }
5105
+ : sent;
5106
+ }
5107
+ return { status: 'permanent', error: `unsupported outbox entry type: ${entry.type}`, code: 'UNSUPPORTED_OUTBOX_TYPE' };
5108
+ }, queue);
5109
+ if (result.sent > 0 || result.expired > 0 || result.permanent) {
5110
+ logger.info(`${this.logPrefix()} ${queue} outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed} permanent=${result.permanent ?? 0}`);
4543
5111
  }
4544
- return true; // unknown type, discard
4545
- });
4546
- if (result.sent > 0 || result.expired > 0) {
4547
- logger.info(`${this.logPrefix()} Outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed}`);
4548
- }
5112
+ };
5113
+ await drainQueue('default');
5114
+ await drainQueue('activity');
4549
5115
  }
4550
- /** Repair pre-route bootstrap entries once the AUN group endpoint is available. */
4551
- async migrateLegacyBootstrapRoutes() {
5116
+ /** Repair bootstrap outbox entries to the configured personal Owner route. */
5117
+ repairBootstrapRoutes() {
5118
+ const aid = this.config.aid.replace(/^@/, '');
5119
+ const owner = getFirstStaticAgentOwner(aid);
5120
+ if (!owner)
5121
+ return;
4552
5122
  const operationIds = new Set([
4553
- bootstrapInitialMessageOperationId(this.config.aid),
4554
- postBootstrapWelcomeOperationId(this.config.aid),
5123
+ bootstrapInitialMessageOperationId(aid),
5124
+ postBootstrapWelcomeOperationId(aid),
4555
5125
  ]);
4556
- const entries = outbox.load(this.config.aid).filter(entry => typeof entry.dedupeKey === 'string'
4557
- && operationIds.has(entry.dedupeKey)
4558
- && entry.channelId
4559
- && entry.delivery?.chatType === 'private');
5126
+ const entries = [...operationIds]
5127
+ .map(operationId => outbox.findByDedupeKey(aid, operationId, { includeTerminal: true }))
5128
+ .filter((entry) => !!entry?.channelId);
4560
5129
  for (const entry of entries) {
4561
- const group = await this.isGroup(entry.channelId);
4562
- if (group !== true)
5130
+ const delivery = { chatType: 'private' };
5131
+ const nestedDelivery = entry.context?.delivery;
5132
+ const routeAlreadyPrivate = entry.channelId === owner
5133
+ && entry.delivery?.chatType === 'private'
5134
+ && (nestedDelivery === undefined || nestedDelivery?.chatType === 'private');
5135
+ if (routeAlreadyPrivate)
4563
5136
  continue;
4564
- const delivery = { chatType: 'group', groupId: entry.channelId };
4565
- if (!outbox.replace(this.config.aid, {
5137
+ const repaired = {
4566
5138
  ...entry,
5139
+ channelId: owner,
4567
5140
  delivery,
4568
5141
  context: entry.context
4569
5142
  ? { ...entry.context, delivery }
4570
5143
  : entry.context,
4571
- }))
5144
+ };
5145
+ // A receipt is scoped to the original transport target. Never treat a
5146
+ // send accepted for a stale group/private address as proof that the
5147
+ // corrected Owner route was delivered.
5148
+ if (entry.channelId !== owner || entry.delivery?.chatType !== 'private') {
5149
+ delete repaired.deliveryReceipt;
5150
+ }
5151
+ delete repaired.terminal;
5152
+ delete repaired.lastError;
5153
+ delete repaired.lastErrorCode;
5154
+ delete repaired.attempts;
5155
+ if (outbox.replaceIfRouteMatches(aid, entry, repaired) !== 'replaced')
4572
5156
  continue;
4573
- logger.warn(`${this.logPrefix()} Migrated legacy bootstrap outbox route to group.send: entry=${entry.id} group=${entry.channelId}`);
5157
+ logger.warn(`${this.logPrefix()} Repaired bootstrap outbox route to private Owner delivery: entry=${entry.id} owner=${owner}`);
4574
5158
  }
4575
5159
  }
4576
5160
  acknowledge(messageId) {
@@ -4673,7 +5257,7 @@ export class AUNChannel {
4673
5257
  catch {
4674
5258
  payloadObj = { text: payload };
4675
5259
  }
4676
- await this.sendStructured(channelId, payloadObj, context);
5260
+ await this.sendStructuredOrThrow(channelId, payloadObj, context);
4677
5261
  }
4678
5262
  async disconnect() {
4679
5263
  this.intentionalDisconnect = true;
@@ -4839,18 +5423,6 @@ export class AUNChannel {
4839
5423
  return undefined; // 不写缓存,下次仍可重试
4840
5424
  }
4841
5425
  }
4842
- /** Query the authoritative group endpoint without guessing from an AID string. */
4843
- async isGroup(groupId) {
4844
- if (!groupId || !this.client)
4845
- return undefined;
4846
- try {
4847
- const result = await this.callAndTrace('group.get_info', { group_id: groupId });
4848
- return !!(result?.group || result?.group_id || result?.groupId);
4849
- }
4850
- catch {
4851
- return undefined;
4852
- }
4853
- }
4854
5426
  /** 统计关系键继续使用群 ID,仅为智能体预览补充可读群名。 */
4855
5427
  async groupStatsContext(groupId) {
4856
5428
  return {
@@ -4895,7 +5467,6 @@ export class AUNChannelPlugin {
4895
5467
  const channel = new AUNChannel({
4896
5468
  aid,
4897
5469
  keystorePath: inst.keystorePath,
4898
- gatewayUrl: inst.gatewayUrl,
4899
5470
  defaultEncrypt: ctx.aunDefaultEncrypt,
4900
5471
  accessToken: inst.accessToken,
4901
5472
  flushDelay: inst.flushDelay,
@@ -4914,6 +5485,13 @@ export class AUNChannelPlugin {
4914
5485
  const delivery = requireEnvelopeDelivery(channelId, envelope?.delivery, envelope?.replyContext?.delivery);
4915
5486
  const parentCausation = normalizeCausation(envelope.causation ?? envelope.replyContext?.metadata?.causation);
4916
5487
  const outboundCausation = parentCausation ? deriveCausation(parentCausation) : undefined;
5488
+ const operationId = envelope.operationId ?? envelope.taskId;
5489
+ // A task can emit multiple independent intermediate text chunks. Keep
5490
+ // the task operation ID in the receipt, but do not use it as the
5491
+ // durable-send dedupe key for each non-final chunk.
5492
+ const dedupeOperationId = payload.kind === 'result.text' && payload.isFinal === false
5493
+ ? undefined
5494
+ : operationId;
4917
5495
  const replyCtx = outboundCausation
4918
5496
  ? {
4919
5497
  ...(envelope.replyContext ?? {}),
@@ -4934,11 +5512,61 @@ export class AUNChannelPlugin {
4934
5512
  case 'result.text':
4935
5513
  case 'command.result':
4936
5514
  case 'command.error': {
4937
- const sendCtx = { ...(replyCtx ?? {}) };
5515
+ const sendCtx = {
5516
+ ...(replyCtx ?? {}),
5517
+ metadata: {
5518
+ ...Object.fromEntries(Object.entries(replyCtx?.metadata ?? {})
5519
+ .filter(([key]) => key !== 'operationId')),
5520
+ ...(dedupeOperationId ? { operationId: dedupeOperationId } : {}),
5521
+ },
5522
+ };
4938
5523
  if (payload.kind === 'result.text' && payload.isFinal)
4939
5524
  sendCtx.title = '✅ 最终回复:';
4940
- await channel.sendMessage(channelId, payload.text, sendCtx);
4941
- return;
5525
+ let result;
5526
+ try {
5527
+ result = await channel.sendMessage(channelId, payload.text, sendCtx);
5528
+ }
5529
+ catch (error) {
5530
+ if (error?.code !== 'OUTBOX_FULL')
5531
+ throw error;
5532
+ return {
5533
+ status: 'failed',
5534
+ operationId,
5535
+ messages: [],
5536
+ error: error instanceof Error ? error.message : String(error),
5537
+ code: 'OUTBOX_FULL',
5538
+ };
5539
+ }
5540
+ if (result.status === 'sent') {
5541
+ return {
5542
+ status: 'sent',
5543
+ operationId,
5544
+ messageId: result.messageId,
5545
+ messages: [{
5546
+ messageId: result.messageId,
5547
+ partIndex: 0,
5548
+ ...(sendCtx.threadId ? { threadId: sendCtx.threadId } : {}),
5549
+ }],
5550
+ };
5551
+ }
5552
+ if (result.status === 'queued') {
5553
+ return {
5554
+ status: 'queued',
5555
+ operationId,
5556
+ messages: [],
5557
+ outboxId: result.outboxId,
5558
+ ...(result.error !== undefined ? { error: result.error } : {}),
5559
+ ...(result.code !== undefined ? { code: result.code } : {}),
5560
+ };
5561
+ }
5562
+ return {
5563
+ status: 'failed',
5564
+ operationId,
5565
+ messages: [],
5566
+ ...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
5567
+ error: result.error,
5568
+ ...(result.code !== undefined ? { code: result.code } : {}),
5569
+ };
4942
5570
  }
4943
5571
  case 'system.notice': {
4944
5572
  const noticePayload = {
@@ -4991,6 +5619,9 @@ export class AUNChannelPlugin {
4991
5619
  }
4992
5620
  case 'activity.batch': {
4993
5621
  const items = Array.isArray(payload.items) ? payload.items : [];
5622
+ const messageIds = [];
5623
+ let queuedResult;
5624
+ let failedResult;
4994
5625
  for (const item of items) {
4995
5626
  if (item?.kind === 'progress') {
4996
5627
  const metadata = { activityType: 'progress' };
@@ -5006,10 +5637,51 @@ export class AUNChannelPlugin {
5006
5637
  await channel.sendThought(channelId, envelope.taskId, aunPayload, replyCtx);
5007
5638
  }
5008
5639
  else {
5009
- await channel.sendReliableStructured(channelId, aunPayload, replyCtx, channel.activityLogText(item));
5640
+ const result = await channel.sendContentPayload(channelId, aunPayload, {
5641
+ queue: 'activity',
5642
+ contentKind: 'custom',
5643
+ context: replyCtx,
5644
+ logText: channel.activityLogText(item),
5645
+ });
5646
+ if (result.status === 'permanent' || result.status === 'failed')
5647
+ failedResult ??= result;
5648
+ else if (result.status === 'retry' || result.status === 'queued' || result.queued)
5649
+ queuedResult ??= result;
5650
+ else if (result.messageId)
5651
+ messageIds.push(result.messageId);
5010
5652
  }
5011
5653
  }
5012
- return;
5654
+ if (failedResult) {
5655
+ return {
5656
+ status: 'failed',
5657
+ operationId,
5658
+ messages: [],
5659
+ ...(failedResult.outboxId !== undefined ? { outboxId: failedResult.outboxId } : {}),
5660
+ error: failedResult.error ?? 'AUN activity delivery failed',
5661
+ ...(failedResult.code !== undefined ? { code: failedResult.code } : {}),
5662
+ };
5663
+ }
5664
+ if (queuedResult) {
5665
+ return queuedResult.outboxId
5666
+ ? {
5667
+ status: 'queued',
5668
+ operationId,
5669
+ messages: [],
5670
+ outboxId: queuedResult.outboxId,
5671
+ ...(queuedResult.error !== undefined ? { error: queuedResult.error } : {}),
5672
+ ...(queuedResult.code !== undefined ? { code: queuedResult.code } : {}),
5673
+ }
5674
+ : {
5675
+ status: 'failed',
5676
+ operationId,
5677
+ messages: [],
5678
+ error: 'queued activity is missing its outbox id',
5679
+ code: 'MISSING_OUTBOX_ID',
5680
+ };
5681
+ }
5682
+ if (messageIds.length > 0)
5683
+ return sentReceipt(envelope, messageIds, replyCtx?.threadId);
5684
+ return suppressedReceipt(envelope, 'empty_activity');
5013
5685
  }
5014
5686
  case 'status.progress':
5015
5687
  channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
@@ -5038,6 +5710,44 @@ export class AUNChannelPlugin {
5038
5710
  case 'interaction': {
5039
5711
  const req = payload.interaction;
5040
5712
  const cardTtlMs = AUN_INTERACTION_CARD_TTL_MS;
5713
+ const cardExpiresAt = typeof req.expiresAt === 'number' && Number.isFinite(req.expiresAt)
5714
+ ? req.expiresAt
5715
+ : Date.now() + cardTtlMs;
5716
+ const toInteractionReceipt = (result) => {
5717
+ if ((result.status === undefined || result.status === 'sent') && result.messageId) {
5718
+ return sentReceipt(envelope, [result.messageId], replyCtx?.threadId);
5719
+ }
5720
+ if (result.status === 'retry' || result.status === 'queued' || result.queued) {
5721
+ if (!result.outboxId) {
5722
+ return {
5723
+ status: 'failed',
5724
+ operationId,
5725
+ messages: [],
5726
+ error: 'queued interaction is missing its outbox id',
5727
+ code: 'MISSING_OUTBOX_ID',
5728
+ };
5729
+ }
5730
+ return {
5731
+ status: 'queued',
5732
+ operationId,
5733
+ messages: [],
5734
+ outboxId: result.outboxId,
5735
+ ...(result.error !== undefined ? { error: result.error } : {}),
5736
+ ...(result.code !== undefined ? { code: result.code } : {}),
5737
+ };
5738
+ }
5739
+ if (result.status === 'permanent' || result.status === 'failed') {
5740
+ return {
5741
+ status: 'failed',
5742
+ operationId,
5743
+ messages: [],
5744
+ ...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
5745
+ error: result.error ?? 'AUN interaction delivery failed',
5746
+ ...(result.code !== undefined ? { code: result.code } : {}),
5747
+ };
5748
+ }
5749
+ return suppressedReceipt(envelope, 'interaction_not_sent');
5750
+ };
5041
5751
  if (req.kind.kind === 'action') {
5042
5752
  const action = req.kind;
5043
5753
  const aunCard = {
@@ -5061,7 +5771,7 @@ export class AUNChannelPlugin {
5061
5771
  aunCard.initiator = req.initiatorId;
5062
5772
  if (replyCtx?.threadId)
5063
5773
  aunCard.thread_id = replyCtx.threadId;
5064
- await channel.sendContentPayload(channelId, aunCard, {
5774
+ const result = await channel.sendContentPayload(channelId, aunCard, {
5065
5775
  contentKind: 'card',
5066
5776
  context: replyCtx,
5067
5777
  logText: action.title ? `[card] ${action.title}` : '[card]',
@@ -5071,9 +5781,10 @@ export class AUNChannelPlugin {
5071
5781
  isCommandCard: false,
5072
5782
  initiatorAid: req.initiatorId,
5073
5783
  delivery: replyCtx?.delivery,
5074
- expiresAt: Date.now() + cardTtlMs,
5784
+ expiresAt: cardExpiresAt,
5075
5785
  },
5076
5786
  });
5787
+ return toInteractionReceipt(result);
5077
5788
  }
5078
5789
  else if (req.kind.kind === 'command-card') {
5079
5790
  const card = req.kind;
@@ -5099,7 +5810,7 @@ export class AUNChannelPlugin {
5099
5810
  aunCard.initiator = req.initiatorId;
5100
5811
  if (replyCtx?.threadId)
5101
5812
  aunCard.thread_id = replyCtx.threadId;
5102
- await channel.sendContentPayload(channelId, aunCard, {
5813
+ const result = await channel.sendContentPayload(channelId, aunCard, {
5103
5814
  contentKind: 'card',
5104
5815
  context: replyCtx,
5105
5816
  logText: card.title ? `[card] ${card.title}` : '[card]',
@@ -5109,14 +5820,35 @@ export class AUNChannelPlugin {
5109
5820
  isCommandCard: true,
5110
5821
  initiatorAid: req.initiatorId,
5111
5822
  delivery: replyCtx?.delivery,
5112
- expiresAt: Date.now() + cardTtlMs,
5823
+ expiresAt: cardExpiresAt,
5113
5824
  },
5114
5825
  });
5826
+ return toInteractionReceipt(result);
5115
5827
  }
5116
5828
  else if (payload.fallbackText) {
5117
- await channel.sendMessage(channelId, payload.fallbackText, replyCtx);
5829
+ const result = await channel.sendMessage(channelId, payload.fallbackText, replyCtx);
5830
+ if (result.status === 'sent')
5831
+ return sentReceipt(envelope, [result.messageId], replyCtx?.threadId);
5832
+ if (result.status === 'queued') {
5833
+ return {
5834
+ status: 'queued',
5835
+ operationId,
5836
+ messages: [],
5837
+ outboxId: result.outboxId,
5838
+ ...(result.error !== undefined ? { error: result.error } : {}),
5839
+ ...(result.code !== undefined ? { code: result.code } : {}),
5840
+ };
5841
+ }
5842
+ return {
5843
+ status: 'failed',
5844
+ operationId,
5845
+ messages: [],
5846
+ ...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
5847
+ error: result.error,
5848
+ ...(result.code !== undefined ? { code: result.code } : {}),
5849
+ };
5118
5850
  }
5119
- return;
5851
+ return suppressedReceipt(envelope, 'empty_interaction');
5120
5852
  }
5121
5853
  case 'custom': {
5122
5854
  const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
@@ -5133,7 +5865,6 @@ export class AUNChannelPlugin {
5133
5865
  uploadAgentMd: (content) => channel.uploadAgentMd(content),
5134
5866
  downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
5135
5867
  getGroupName: (groupId) => channel.getGroupName(groupId),
5136
- isGroup: (groupId) => channel.isGroup(groupId),
5137
5868
  getGroupMemberRole: (groupId, aid) => channel.getGroupMemberRole(groupId, aid),
5138
5869
  _selfAid: () => channel.getStatus().aid,
5139
5870
  _selfName: () => channel.getSelfName(),
@@ -5160,7 +5891,7 @@ export class AUNChannelPlugin {
5160
5891
  registerBridge(bridge, channelType) {
5161
5892
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
5162
5893
  handler(aunOptsToInbound(opts, adapter.channelName, channelType));
5163
- }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
5894
+ }), async (channelId, text, replyContext) => { await channel.sendMessage(channelId, text, replyContext); }, adapter, channelType);
5164
5895
  },
5165
5896
  registerHooks(hookCtx) {
5166
5897
  channel.setEventBus(hookCtx.eventBus);