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.
- package/CHANGELOG.md +43 -1
- package/README.md +60 -9
- package/bin/install-codex-managed-hooks.mjs +61 -0
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +383 -111
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +405 -137
- package/dist/agents/ecagent-runner.js +174 -63
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +66 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +86 -9
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +26 -11
- package/dist/aun/outbox.js +295 -68
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +1004 -273
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +81 -121
- package/dist/cli/index.js +1 -0
- package/dist/cli/init.js +76 -24
- package/dist/cli/restart-monitor.js +3 -3
- package/dist/cli/task-context.js +46 -0
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +16 -5
- package/dist/config/config-manager.js +226 -24
- package/dist/config/config-operation-service.js +1 -2
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +29 -16
- package/dist/config/lifecycle.js +16 -5
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +161 -12
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +31 -1
- package/dist/core/auth/auth-gateway.js +33 -4
- package/dist/core/auth/authorization-audit.js +155 -4
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +2 -2
- package/dist/core/bootstrap-service.js +27 -38
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/channel-loader.js +0 -2
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +290 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +279 -80
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +350 -32
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +9 -4
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +10 -0
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +83 -84
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +124 -10
- package/dist/core/message/message-log.js +14 -7
- package/dist/core/message/message-queue.js +206 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +495 -72
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +465 -56
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +205 -13
- package/dist/core/permission/tool-policy.js +673 -62
- package/dist/core/relation/peer-identity.js +18 -0
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +299 -30
- package/dist/core/session/session-renew.js +19 -12
- package/dist/core/session/session-turn-coordinator.js +11 -4
- package/dist/eck/kit-renderer.js +18 -9
- package/dist/index.js +283 -65
- package/dist/ipc.js +374 -24
- package/dist/paths.js +64 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +56 -5
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +3 -2
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +21 -2
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/stats.js +52 -18
- package/dist/utils/welcome.js +2 -2
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/rules/01-overview.md +1 -1
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/schemas/_meta.json +10 -5
- package/kits/schemas/agent-config.schema.10.json +2 -1
- package/kits/schemas/agent-config.schema.11.json +421 -0
- package/kits/schemas/daemon.schema.5.json +135 -0
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +119 -0
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/package.json +7 -3
- package/skills/eclink/SKILL.md +2 -0
package/dist/channels/aun.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
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.
|
|
1178
|
-
|
|
1179
|
-
|
|
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 =
|
|
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
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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 ||
|
|
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),
|
|
1494
|
-
if (sent)
|
|
1495
|
-
|
|
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
|
-
//
|
|
1747
|
-
// carry a new seq, but the application
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
3175
|
-
|
|
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'}`,
|
|
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
|
-
|
|
3196
|
-
|
|
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
|
-
|
|
3201
|
-
|
|
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
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
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 {
|
|
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
|
-
|
|
3589
|
+
this.removeDeliveredOutboxEntry(entry);
|
|
3276
3590
|
return { messageId: result.messageId };
|
|
3277
3591
|
}
|
|
3278
|
-
|
|
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
|
-
|
|
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
|
|
3480
|
-
if (
|
|
3481
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
3768
|
-
return
|
|
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 (
|
|
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
|
|
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
|
|
4205
|
+
const mid = this.messageIdFromSendResult(result);
|
|
3823
4206
|
if (!mid) {
|
|
3824
|
-
const
|
|
3825
|
-
|
|
3826
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
4240
|
+
return classifyAunSendFailure(result, 'message.send returned no message_id');
|
|
3853
4241
|
}
|
|
3854
4242
|
else {
|
|
3855
|
-
|
|
3856
|
-
this.
|
|
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(
|
|
4248
|
+
registerAunCausation(mid, this.config.aid, targetAid, causation);
|
|
3860
4249
|
recordCausationSpan(causation, 'message.outbound', {
|
|
3861
4250
|
status: 'completed',
|
|
3862
|
-
refs: { messageId:
|
|
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:
|
|
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:
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
3905
|
-
const mid = result
|
|
3906
|
-
|
|
3907
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4344
|
+
return classifyAunSendFailure(e, `send failed to ${channelId}`);
|
|
3940
4345
|
}
|
|
3941
4346
|
}
|
|
3942
4347
|
}
|
|
3943
4348
|
checkpointTextDelivery(entry, messageId, encrypt, result) {
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
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
|
-
|
|
3952
|
-
|
|
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:
|
|
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
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
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
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
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
|
-
/**
|
|
4131
|
-
async
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
4668
|
+
return { ok: true, messageId: mid };
|
|
4181
4669
|
}
|
|
4182
4670
|
}
|
|
4183
4671
|
catch (e) {
|
|
4184
|
-
const
|
|
4185
|
-
logger.warn(`${this.logPrefix()} sendStructured failed (${payload.type}) to ${channelId}:
|
|
4186
|
-
return
|
|
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
|
|
4223
|
-
if (
|
|
4224
|
-
|
|
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
|
|
4267
|
-
if (
|
|
4268
|
-
|
|
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
|
|
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
|
|
4827
|
+
return { status: 'permanent', error: 'durable image data is invalid', code: 'INVALID_IMAGE_DATA' };
|
|
4317
4828
|
}
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
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
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
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
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
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
|
|
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.
|
|
4962
|
+
const result = await this.callClient('group.send', params);
|
|
4421
4963
|
sendResult = result;
|
|
4422
|
-
const fileMid = result
|
|
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
|
|
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.
|
|
4975
|
+
const result = await this.callClient('message.send', params);
|
|
4434
4976
|
sendResult = result;
|
|
4435
|
-
sentMid = result
|
|
4977
|
+
sentMid = this.messageIdFromSendResult(result);
|
|
4436
4978
|
this.trace('OUT', 'message.send.file.ok', { message_id: sentMid });
|
|
4437
|
-
if (!
|
|
4979
|
+
if (!sentMid) {
|
|
4438
4980
|
logger.warn(`${this.logPrefix()} message.send.file returned no message_id: ${JSON.stringify(result)}`);
|
|
4439
|
-
return
|
|
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.
|
|
4996
|
+
const result = await this.callClient('group.send', params);
|
|
4455
4997
|
sendResult = result;
|
|
4456
|
-
const fbMid = result
|
|
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
|
|
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.
|
|
5008
|
+
const result = await this.callClient('message.send', params);
|
|
4467
5009
|
sendResult = result;
|
|
4468
|
-
sentMid = result
|
|
5010
|
+
sentMid = this.messageIdFromSendResult(result);
|
|
4469
5011
|
this.trace('OUT', 'message.send.file.fallback.ok', { message_id: sentMid });
|
|
4470
|
-
if (!
|
|
5012
|
+
if (!sentMid) {
|
|
4471
5013
|
logger.warn(`${this.logPrefix()} message.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
4472
|
-
return
|
|
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
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
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
|
|
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
|
-
|
|
4499
|
-
|
|
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
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
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
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
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
|
|
4551
|
-
|
|
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(
|
|
4554
|
-
postBootstrapWelcomeOperationId(
|
|
5123
|
+
bootstrapInitialMessageOperationId(aid),
|
|
5124
|
+
postBootstrapWelcomeOperationId(aid),
|
|
4555
5125
|
]);
|
|
4556
|
-
const entries =
|
|
4557
|
-
|
|
4558
|
-
|
|
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
|
|
4562
|
-
|
|
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
|
|
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()}
|
|
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.
|
|
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 = {
|
|
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
|
-
|
|
4941
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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);
|