evolcore 0.0.20 → 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 +22 -0
- package/README.md +58 -9
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +379 -108
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +402 -135
- package/dist/agents/ecagent-runner.js +171 -61
- 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 +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +182 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +409 -88
- 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 +69 -115
- package/dist/cli/init.js +27 -15
- 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 +135 -17
- 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 +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- 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 +150 -2
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-service.js +6 -2
- package/dist/core/causation/aun-association.js +7 -4
- 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 +4 -0
- 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 +486 -68
- 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 +410 -54
- 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 +159 -1
- package/dist/core/permission/tool-policy.js +575 -21
- 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 +1 -1
- package/dist/index.js +253 -46
- 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/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/schemas/_meta.json +4 -2
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- 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 +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
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';
|
|
@@ -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>';
|
|
@@ -326,7 +331,7 @@ export function classifyAunSendFailure(value, fallback = 'AUN send failed') {
|
|
|
326
331
|
const permanentHttpStatus = httpStatus !== undefined
|
|
327
332
|
&& httpStatus >= 400 && httpStatus < 500
|
|
328
333
|
&& !transientHttpStatus;
|
|
329
|
-
const permanentByText = !relayTargetMissing &&
|
|
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);
|
|
330
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);
|
|
331
336
|
const acceptedDispatch = details.dispatchStatus !== undefined
|
|
332
337
|
&& AUN_ACCEPTED_DISPATCH_STATUSES.has(details.dispatchStatus);
|
|
@@ -397,8 +402,10 @@ export function classifyAunSendFailure(value, fallback = 'AUN send failed') {
|
|
|
397
402
|
: {}),
|
|
398
403
|
};
|
|
399
404
|
}
|
|
400
|
-
function sentOutboxResult() {
|
|
401
|
-
return
|
|
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' };
|
|
402
409
|
}
|
|
403
410
|
function setIfDefined(target, key, value) {
|
|
404
411
|
if (value !== undefined)
|
|
@@ -551,6 +558,35 @@ export class AUNChannel {
|
|
|
551
558
|
* 统一的 RPC 调用包装:自动记录 OUT 发送、.ok 结果、.error 错误(含 trace + daemon.log 失败日志)。
|
|
552
559
|
* 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
|
|
553
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
|
+
}
|
|
554
590
|
async callAndTrace(method, params, opts) {
|
|
555
591
|
this.trace('OUT', method, params);
|
|
556
592
|
// RPC 往返计时:区分「网关慢」与「本地队列堵塞」。message.send/group.send 的
|
|
@@ -560,7 +596,7 @@ export class AUNChannel {
|
|
|
560
596
|
// SLOW_RPC_WARN_MS 需明显低于 SDK 默认 10s 超时,以便在真正超时前提前告警。
|
|
561
597
|
const SLOW_RPC_WARN_MS = 3000;
|
|
562
598
|
try {
|
|
563
|
-
const result = await this.
|
|
599
|
+
const result = await this.callClient(method, params);
|
|
564
600
|
const durationMs = Date.now() - rpcStart;
|
|
565
601
|
if (!opts?.silentOk) {
|
|
566
602
|
const r = result;
|
|
@@ -1283,6 +1319,16 @@ export class AUNChannel {
|
|
|
1283
1319
|
aidState;
|
|
1284
1320
|
aidStatsCollector;
|
|
1285
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 = [];
|
|
1286
1332
|
constructor(config) {
|
|
1287
1333
|
this.config = config;
|
|
1288
1334
|
this.agentDir = agentDirPath(config.aid);
|
|
@@ -3083,7 +3129,11 @@ export class AUNChannel {
|
|
|
3083
3129
|
}
|
|
3084
3130
|
}
|
|
3085
3131
|
removeDeliveredOutboxEntry(entry) {
|
|
3086
|
-
|
|
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) {
|
|
3087
3137
|
logger.warn(`${this.logPrefix()} Preserved outbox entry whose route changed while delivery was in flight: id=${entry.id} submittedChannel=${entry.channelId}`);
|
|
3088
3138
|
}
|
|
3089
3139
|
}
|
|
@@ -3115,7 +3165,7 @@ export class AUNChannel {
|
|
|
3115
3165
|
return `aun_status=${receipt.status ?? 'unknown'} seq=${receipt.seq ?? 'unknown'} delivery_mode=${receipt.deliveryMode ?? 'unknown'} gateway_timestamp=${receipt.timestamp ?? 'unknown'}`;
|
|
3116
3166
|
}
|
|
3117
3167
|
logAunSendAccepted(method, target, messageId, encrypt, result, text) {
|
|
3118
|
-
const preview = text === undefined ? '' : ` text=${text
|
|
3168
|
+
const preview = text === undefined ? '' : ` text=${formatAunSendLogText(text)}`;
|
|
3119
3169
|
logger.info(`${this.logPrefix()} ${method} accepted by AUN: target=${target} mid=${messageId} encrypt=${encrypt} ${this.receiptLogFields(result)}${preview}`);
|
|
3120
3170
|
}
|
|
3121
3171
|
stripUndefinedDeep(value) {
|
|
@@ -3326,8 +3376,8 @@ export class AUNChannel {
|
|
|
3326
3376
|
}
|
|
3327
3377
|
messageIds.add(messageId);
|
|
3328
3378
|
const now = Date.now();
|
|
3329
|
-
const mapTtl = action.expiresAt && action.expiresAt
|
|
3330
|
-
? action.expiresAt - now
|
|
3379
|
+
const mapTtl = typeof action.expiresAt === 'number' && Number.isFinite(action.expiresAt)
|
|
3380
|
+
? Math.max(0, action.expiresAt - now)
|
|
3331
3381
|
: AUN_INTERACTION_CARD_TTL_MS;
|
|
3332
3382
|
const mapTimer = setTimeout(() => {
|
|
3333
3383
|
this.cardMessageIdMap.delete(messageId);
|
|
@@ -3390,7 +3440,7 @@ export class AUNChannel {
|
|
|
3390
3440
|
params.to = targetAid;
|
|
3391
3441
|
const callOnce = async (sendParams, fallback) => {
|
|
3392
3442
|
const result = fallback
|
|
3393
|
-
? await this.
|
|
3443
|
+
? await this.callClient(method, sendParams)
|
|
3394
3444
|
: await this.callAndTrace(method, sendParams);
|
|
3395
3445
|
const mid = this.messageIdFromSendResult(result);
|
|
3396
3446
|
if (!mid) {
|
|
@@ -3479,39 +3529,81 @@ export class AUNChannel {
|
|
|
3479
3529
|
// turn malformed `mentions: [undefined]` into an apparently valid `[]`.
|
|
3480
3530
|
const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, context));
|
|
3481
3531
|
const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
|
|
3482
|
-
const
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
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)}`);
|
|
3494
3575
|
if (!this.connected || !this.client) {
|
|
3495
3576
|
logger.warn(`${this.logPrefix()} Not connected, payload queued in outbox (id=${entry.id}, kind=${opts.contentKind}). Triggering reconnect.`);
|
|
3496
3577
|
if (!this.reconnectTimer && !this.client) {
|
|
3497
3578
|
this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendContentPayload failed: ${e}`));
|
|
3498
3579
|
}
|
|
3499
|
-
return {
|
|
3580
|
+
return {
|
|
3581
|
+
queued: true,
|
|
3582
|
+
outboxId: entry.id,
|
|
3583
|
+
error: 'AUN channel is not connected',
|
|
3584
|
+
code: 'AUN_NOT_CONNECTED',
|
|
3585
|
+
};
|
|
3500
3586
|
}
|
|
3501
3587
|
const result = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false, status: 'retry', error: 'outbox send is already in flight' });
|
|
3502
3588
|
if (result.ok) {
|
|
3503
3589
|
this.removeDeliveredOutboxEntry(entry);
|
|
3504
3590
|
return { messageId: result.messageId };
|
|
3505
3591
|
}
|
|
3506
|
-
if (result.status === 'permanent') {
|
|
3592
|
+
if (result.status === 'permanent' || result.status === 'failed') {
|
|
3507
3593
|
this.markPermanentOutboxFailure(entry, result);
|
|
3508
3594
|
return {
|
|
3509
3595
|
status: 'permanent',
|
|
3596
|
+
outboxId: entry.id,
|
|
3510
3597
|
...(result.error !== undefined ? { error: result.error } : {}),
|
|
3511
3598
|
...(result.code !== undefined ? { code: result.code } : {}),
|
|
3512
3599
|
};
|
|
3513
3600
|
}
|
|
3514
|
-
return {
|
|
3601
|
+
return {
|
|
3602
|
+
queued: true,
|
|
3603
|
+
outboxId: entry.id,
|
|
3604
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
3605
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
3606
|
+
};
|
|
3515
3607
|
}
|
|
3516
3608
|
buildTaskPayloadBase(envelope, context) {
|
|
3517
3609
|
const base = {};
|
|
@@ -3629,16 +3721,22 @@ export class AUNChannel {
|
|
|
3629
3721
|
};
|
|
3630
3722
|
}
|
|
3631
3723
|
async sendReliableStructured(channelId, payload, context, logText) {
|
|
3632
|
-
await this.sendContentPayload(channelId, payload, {
|
|
3724
|
+
const result = await this.sendContentPayload(channelId, payload, {
|
|
3633
3725
|
contentKind: 'custom',
|
|
3634
3726
|
context,
|
|
3635
3727
|
logText: logText ?? this.payloadLogText(payload, 'custom'),
|
|
3636
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
|
+
}
|
|
3637
3735
|
}
|
|
3638
3736
|
async sendMessage(channelId, text, context) {
|
|
3639
3737
|
if (!text?.trim()) {
|
|
3640
3738
|
logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
|
|
3641
|
-
return;
|
|
3739
|
+
return { status: 'failed', error: 'message text is empty', code: 'EMPTY_MESSAGE' };
|
|
3642
3740
|
}
|
|
3643
3741
|
const delivery = this.requireDelivery(channelId, context);
|
|
3644
3742
|
const routedContext = this.withDelivery(context, delivery);
|
|
@@ -3676,13 +3774,14 @@ export class AUNChannel {
|
|
|
3676
3774
|
: undefined;
|
|
3677
3775
|
if (operationId) {
|
|
3678
3776
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
3679
|
-
|
|
3777
|
+
const completedMessageId = findMessageLogOperationMessageId(chatDir, operationId);
|
|
3778
|
+
if (completedMessageId) {
|
|
3680
3779
|
const duplicate = outbox.findByDedupeKey(this.config.aid, operationId);
|
|
3681
3780
|
if (duplicate) {
|
|
3682
3781
|
outbox.remove(this.config.aid, duplicate.id);
|
|
3683
3782
|
logger.info(`${this.logPrefix()} Removed stale outbox entry for completed operation: ${operationId}`);
|
|
3684
3783
|
}
|
|
3685
|
-
return;
|
|
3784
|
+
return { status: 'sent', messageId: completedMessageId };
|
|
3686
3785
|
}
|
|
3687
3786
|
}
|
|
3688
3787
|
// Write-ahead: persist to outbox before attempting send
|
|
@@ -3698,9 +3797,19 @@ export class AUNChannel {
|
|
|
3698
3797
|
? routedContext.metadata.outboxTtl
|
|
3699
3798
|
: undefined,
|
|
3700
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
|
+
}
|
|
3701
3805
|
if (entry.terminal) {
|
|
3702
3806
|
logger.warn(`${this.logPrefix()} Skipping previously terminated durable operation: operation=${operationId ?? '<none>'} entry=${entry.id} code=${entry.lastErrorCode ?? 'unknown'}`);
|
|
3703
|
-
return
|
|
3807
|
+
return {
|
|
3808
|
+
status: 'failed',
|
|
3809
|
+
outboxId: entry.id,
|
|
3810
|
+
error: entry.lastError ?? entry.terminal.error,
|
|
3811
|
+
...(entry.lastErrorCode !== undefined ? { code: entry.lastErrorCode } : {}),
|
|
3812
|
+
};
|
|
3704
3813
|
}
|
|
3705
3814
|
logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
|
|
3706
3815
|
// 积压深度告警:outbox 待发条目累积说明发送速度跟不上,回复将出现明显延迟。
|
|
@@ -3713,16 +3822,40 @@ export class AUNChannel {
|
|
|
3713
3822
|
if (!this.reconnectTimer && !this.client) {
|
|
3714
3823
|
this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendMessage failed: ${e}`));
|
|
3715
3824
|
}
|
|
3716
|
-
return;
|
|
3825
|
+
return { status: 'queued', outboxId: entry.id, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
|
|
3717
3826
|
}
|
|
3718
3827
|
// Attempt immediate delivery
|
|
3719
3828
|
const result = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
3720
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
|
+
}
|
|
3721
3836
|
this.removeDeliveredOutboxEntry(entry);
|
|
3837
|
+
return { status: 'sent', messageId };
|
|
3722
3838
|
}
|
|
3723
3839
|
else if (result.status === 'permanent') {
|
|
3724
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
|
+
};
|
|
3725
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 };
|
|
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
|
+
};
|
|
3726
3859
|
}
|
|
3727
3860
|
/** Daemon-side transport for `ec msg send` running inside an agent task. */
|
|
3728
3861
|
async sendDaemonMsg(args) {
|
|
@@ -4005,9 +4138,10 @@ export class AUNChannel {
|
|
|
4005
4138
|
|| (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
|
|
4006
4139
|
if (operationId) {
|
|
4007
4140
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
4008
|
-
|
|
4141
|
+
const completedMessageId = findMessageLogOperationMessageId(chatDir, operationId);
|
|
4142
|
+
if (completedMessageId) {
|
|
4009
4143
|
logger.info(`${this.logPrefix()} Durable operation already logged; skipping duplicate send: ${operationId}`);
|
|
4010
|
-
return sentOutboxResult();
|
|
4144
|
+
return sentOutboxResult(completedMessageId);
|
|
4011
4145
|
}
|
|
4012
4146
|
if (operationId === postBootstrapWelcomeOperationId(this.config.aid)) {
|
|
4013
4147
|
const agentConfig = loadAgent(this.config.aid);
|
|
@@ -4054,7 +4188,7 @@ export class AUNChannel {
|
|
|
4054
4188
|
source,
|
|
4055
4189
|
transport: entry.deliveryReceipt.transport,
|
|
4056
4190
|
});
|
|
4057
|
-
return sentOutboxResult();
|
|
4191
|
+
return sentOutboxResult(entry.deliveryReceipt.messageId);
|
|
4058
4192
|
}
|
|
4059
4193
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
4060
4194
|
const encrypt = context?.metadata?.encrypted != null
|
|
@@ -4063,6 +4197,7 @@ export class AUNChannel {
|
|
|
4063
4197
|
const params = { payload, encrypt };
|
|
4064
4198
|
if (context?.metadata?.persistRequired === true)
|
|
4065
4199
|
params.persist_required = true;
|
|
4200
|
+
let acceptedMessageId;
|
|
4066
4201
|
try {
|
|
4067
4202
|
if (isGroup) {
|
|
4068
4203
|
params.group_id = channelId;
|
|
@@ -4082,6 +4217,7 @@ export class AUNChannel {
|
|
|
4082
4217
|
return failure;
|
|
4083
4218
|
}
|
|
4084
4219
|
else {
|
|
4220
|
+
acceptedMessageId = mid;
|
|
4085
4221
|
this.logAunSendAccepted('group.send', channelId, mid, encrypt, result, finalText);
|
|
4086
4222
|
this.checkpointTextDelivery(entry, mid, encrypt, result);
|
|
4087
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 });
|
|
@@ -4104,6 +4240,7 @@ export class AUNChannel {
|
|
|
4104
4240
|
return classifyAunSendFailure(result, 'message.send returned no message_id');
|
|
4105
4241
|
}
|
|
4106
4242
|
else {
|
|
4243
|
+
acceptedMessageId = mid;
|
|
4107
4244
|
this.logAunSendAccepted('message.send', this.peerLabel(targetAid), mid, encrypt, result, finalText);
|
|
4108
4245
|
this.checkpointTextDelivery(entry, mid, encrypt, result);
|
|
4109
4246
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
@@ -4124,7 +4261,7 @@ export class AUNChannel {
|
|
|
4124
4261
|
this.forwardOutbound(result);
|
|
4125
4262
|
}
|
|
4126
4263
|
}
|
|
4127
|
-
return sentOutboxResult();
|
|
4264
|
+
return sentOutboxResult(acceptedMessageId);
|
|
4128
4265
|
}
|
|
4129
4266
|
catch (e) {
|
|
4130
4267
|
if (entry.deliveryReceipt) {
|
|
@@ -4139,7 +4276,7 @@ export class AUNChannel {
|
|
|
4139
4276
|
try {
|
|
4140
4277
|
if (isGroup) {
|
|
4141
4278
|
this.trace('OUT', 'group.send.fallback', params);
|
|
4142
|
-
const result = await this.
|
|
4279
|
+
const result = await this.callClient('group.send', params);
|
|
4143
4280
|
const mid = this.messageIdFromSendResult(result);
|
|
4144
4281
|
if (!mid) {
|
|
4145
4282
|
const resultRecord = errorRecord(result);
|
|
@@ -4148,6 +4285,7 @@ export class AUNChannel {
|
|
|
4148
4285
|
logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
4149
4286
|
return classifyAunSendFailure(result, 'group.send plaintext fallback returned no message_id');
|
|
4150
4287
|
}
|
|
4288
|
+
acceptedMessageId = mid;
|
|
4151
4289
|
this.trace('OUT', 'group.send.fallback.ok', { message_id: mid });
|
|
4152
4290
|
this.checkpointTextDelivery(entry, mid, false, result);
|
|
4153
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 });
|
|
@@ -4161,13 +4299,14 @@ export class AUNChannel {
|
|
|
4161
4299
|
}
|
|
4162
4300
|
else {
|
|
4163
4301
|
this.trace('OUT', 'message.send.fallback', params);
|
|
4164
|
-
const result = await this.
|
|
4302
|
+
const result = await this.callClient('message.send', params);
|
|
4165
4303
|
const mid = this.messageIdFromSendResult(result);
|
|
4166
4304
|
if (!mid) {
|
|
4167
4305
|
this.trace('OUT', 'message.send.fallback.missing_id', {});
|
|
4168
4306
|
logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
4169
4307
|
return classifyAunSendFailure(result, 'message.send plaintext fallback returned no message_id');
|
|
4170
4308
|
}
|
|
4309
|
+
acceptedMessageId = mid;
|
|
4171
4310
|
this.trace('OUT', 'message.send.fallback.ok', { message_id: mid });
|
|
4172
4311
|
this.checkpointTextDelivery(entry, mid, false, result);
|
|
4173
4312
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
@@ -4186,7 +4325,7 @@ export class AUNChannel {
|
|
|
4186
4325
|
});
|
|
4187
4326
|
this.forwardOutbound(result);
|
|
4188
4327
|
}
|
|
4189
|
-
return sentOutboxResult();
|
|
4328
|
+
return sentOutboxResult(acceptedMessageId);
|
|
4190
4329
|
}
|
|
4191
4330
|
catch (e2) {
|
|
4192
4331
|
if (entry.deliveryReceipt) {
|
|
@@ -4214,6 +4353,7 @@ export class AUNChannel {
|
|
|
4214
4353
|
id: entry.id,
|
|
4215
4354
|
channelId: entry.channelId,
|
|
4216
4355
|
delivery: entry.delivery,
|
|
4356
|
+
queue: entry.queue,
|
|
4217
4357
|
};
|
|
4218
4358
|
const receipt = {
|
|
4219
4359
|
messageId,
|
|
@@ -4238,6 +4378,20 @@ export class AUNChannel {
|
|
|
4238
4378
|
logger.info(`${this.logPrefix()} Discarded invalidated interaction from durable outbox: request=${interactionId} entry=${entry.id}`);
|
|
4239
4379
|
return { ok: true, status: 'sent' };
|
|
4240
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
|
+
};
|
|
4394
|
+
}
|
|
4241
4395
|
const channelId = entry.channelId;
|
|
4242
4396
|
const payload = entry.payload;
|
|
4243
4397
|
if (!payload) {
|
|
@@ -4696,7 +4850,7 @@ export class AUNChannel {
|
|
|
4696
4850
|
return { status: 'permanent', error: 'image outbox entry disappeared during upload', code: 'OUTBOX_ENTRY_MISSING' };
|
|
4697
4851
|
}
|
|
4698
4852
|
const sent = await this.deliverPayloadEntry(entry);
|
|
4699
|
-
return sent.ok ? sentOutboxResult() : sent;
|
|
4853
|
+
return sent.ok ? sentOutboxResult(sent.messageId) : sent;
|
|
4700
4854
|
}
|
|
4701
4855
|
catch (error) {
|
|
4702
4856
|
const failure = classifyAunSendFailure(error, 'image upload or send failed');
|
|
@@ -4728,7 +4882,7 @@ export class AUNChannel {
|
|
|
4728
4882
|
source,
|
|
4729
4883
|
transport: entry.deliveryReceipt.transport,
|
|
4730
4884
|
});
|
|
4731
|
-
return sentOutboxResult();
|
|
4885
|
+
return sentOutboxResult(entry.deliveryReceipt.messageId);
|
|
4732
4886
|
}
|
|
4733
4887
|
catch (error) {
|
|
4734
4888
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -4805,7 +4959,7 @@ export class AUNChannel {
|
|
|
4805
4959
|
if (isGroup) {
|
|
4806
4960
|
params.group_id = delivery.groupId;
|
|
4807
4961
|
this.trace('OUT', 'group.send.file', params);
|
|
4808
|
-
const result = await this.
|
|
4962
|
+
const result = await this.callClient('group.send', params);
|
|
4809
4963
|
sendResult = result;
|
|
4810
4964
|
const fileMid = this.messageIdFromSendResult(result);
|
|
4811
4965
|
sentMid = fileMid ?? null;
|
|
@@ -4818,7 +4972,7 @@ export class AUNChannel {
|
|
|
4818
4972
|
else {
|
|
4819
4973
|
params.to = fileTargetAid;
|
|
4820
4974
|
this.trace('OUT', 'message.send.file', params);
|
|
4821
|
-
const result = await this.
|
|
4975
|
+
const result = await this.callClient('message.send', params);
|
|
4822
4976
|
sendResult = result;
|
|
4823
4977
|
sentMid = this.messageIdFromSendResult(result);
|
|
4824
4978
|
this.trace('OUT', 'message.send.file.ok', { message_id: sentMid });
|
|
@@ -4839,7 +4993,7 @@ export class AUNChannel {
|
|
|
4839
4993
|
params.encrypt = false;
|
|
4840
4994
|
if (isGroup) {
|
|
4841
4995
|
this.trace('OUT', 'group.send.file.fallback', params);
|
|
4842
|
-
const result = await this.
|
|
4996
|
+
const result = await this.callClient('group.send', params);
|
|
4843
4997
|
sendResult = result;
|
|
4844
4998
|
const fbMid = this.messageIdFromSendResult(result);
|
|
4845
4999
|
sentMid = fbMid ?? null;
|
|
@@ -4851,7 +5005,7 @@ export class AUNChannel {
|
|
|
4851
5005
|
}
|
|
4852
5006
|
else {
|
|
4853
5007
|
this.trace('OUT', 'message.send.file.fallback', params);
|
|
4854
|
-
const result = await this.
|
|
5008
|
+
const result = await this.callClient('message.send', params);
|
|
4855
5009
|
sendResult = result;
|
|
4856
5010
|
sentMid = this.messageIdFromSendResult(result);
|
|
4857
5011
|
this.trace('OUT', 'message.send.file.fallback.ok', { message_id: sentMid });
|
|
@@ -4887,7 +5041,7 @@ export class AUNChannel {
|
|
|
4887
5041
|
});
|
|
4888
5042
|
if (sendResult)
|
|
4889
5043
|
this.forwardOutbound(sendResult);
|
|
4890
|
-
return sentOutboxResult();
|
|
5044
|
+
return sentOutboxResult(sentMid);
|
|
4891
5045
|
}
|
|
4892
5046
|
catch (e) {
|
|
4893
5047
|
if (entry.deliveryReceipt) {
|
|
@@ -4907,7 +5061,7 @@ export class AUNChannel {
|
|
|
4907
5061
|
if (this.outboxTimer)
|
|
4908
5062
|
return;
|
|
4909
5063
|
this.outboxTimer = setInterval(() => {
|
|
4910
|
-
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'))) {
|
|
4911
5065
|
this.drainOutbox();
|
|
4912
5066
|
}
|
|
4913
5067
|
}, 30_000);
|
|
@@ -4922,36 +5076,42 @@ export class AUNChannel {
|
|
|
4922
5076
|
if (!this.connected || !this.client)
|
|
4923
5077
|
return;
|
|
4924
5078
|
this.repairBootstrapRoutes();
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
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}`);
|
|
4949
5111
|
}
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
logger.info(`${this.logPrefix()} Outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed} permanent=${result.permanent ?? 0}`);
|
|
4954
|
-
}
|
|
5112
|
+
};
|
|
5113
|
+
await drainQueue('default');
|
|
5114
|
+
await drainQueue('activity');
|
|
4955
5115
|
}
|
|
4956
5116
|
/** Repair bootstrap outbox entries to the configured personal Owner route. */
|
|
4957
5117
|
repairBootstrapRoutes() {
|
|
@@ -5325,6 +5485,13 @@ export class AUNChannelPlugin {
|
|
|
5325
5485
|
const delivery = requireEnvelopeDelivery(channelId, envelope?.delivery, envelope?.replyContext?.delivery);
|
|
5326
5486
|
const parentCausation = normalizeCausation(envelope.causation ?? envelope.replyContext?.metadata?.causation);
|
|
5327
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;
|
|
5328
5495
|
const replyCtx = outboundCausation
|
|
5329
5496
|
? {
|
|
5330
5497
|
...(envelope.replyContext ?? {}),
|
|
@@ -5345,11 +5512,61 @@ export class AUNChannelPlugin {
|
|
|
5345
5512
|
case 'result.text':
|
|
5346
5513
|
case 'command.result':
|
|
5347
5514
|
case 'command.error': {
|
|
5348
|
-
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
|
+
};
|
|
5349
5523
|
if (payload.kind === 'result.text' && payload.isFinal)
|
|
5350
5524
|
sendCtx.title = '✅ 最终回复:';
|
|
5351
|
-
|
|
5352
|
-
|
|
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
|
+
};
|
|
5353
5570
|
}
|
|
5354
5571
|
case 'system.notice': {
|
|
5355
5572
|
const noticePayload = {
|
|
@@ -5402,6 +5619,9 @@ export class AUNChannelPlugin {
|
|
|
5402
5619
|
}
|
|
5403
5620
|
case 'activity.batch': {
|
|
5404
5621
|
const items = Array.isArray(payload.items) ? payload.items : [];
|
|
5622
|
+
const messageIds = [];
|
|
5623
|
+
let queuedResult;
|
|
5624
|
+
let failedResult;
|
|
5405
5625
|
for (const item of items) {
|
|
5406
5626
|
if (item?.kind === 'progress') {
|
|
5407
5627
|
const metadata = { activityType: 'progress' };
|
|
@@ -5417,10 +5637,51 @@ export class AUNChannelPlugin {
|
|
|
5417
5637
|
await channel.sendThought(channelId, envelope.taskId, aunPayload, replyCtx);
|
|
5418
5638
|
}
|
|
5419
5639
|
else {
|
|
5420
|
-
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);
|
|
5421
5652
|
}
|
|
5422
5653
|
}
|
|
5423
|
-
|
|
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');
|
|
5424
5685
|
}
|
|
5425
5686
|
case 'status.progress':
|
|
5426
5687
|
channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
|
|
@@ -5449,6 +5710,44 @@ export class AUNChannelPlugin {
|
|
|
5449
5710
|
case 'interaction': {
|
|
5450
5711
|
const req = payload.interaction;
|
|
5451
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
|
+
};
|
|
5452
5751
|
if (req.kind.kind === 'action') {
|
|
5453
5752
|
const action = req.kind;
|
|
5454
5753
|
const aunCard = {
|
|
@@ -5472,7 +5771,7 @@ export class AUNChannelPlugin {
|
|
|
5472
5771
|
aunCard.initiator = req.initiatorId;
|
|
5473
5772
|
if (replyCtx?.threadId)
|
|
5474
5773
|
aunCard.thread_id = replyCtx.threadId;
|
|
5475
|
-
await channel.sendContentPayload(channelId, aunCard, {
|
|
5774
|
+
const result = await channel.sendContentPayload(channelId, aunCard, {
|
|
5476
5775
|
contentKind: 'card',
|
|
5477
5776
|
context: replyCtx,
|
|
5478
5777
|
logText: action.title ? `[card] ${action.title}` : '[card]',
|
|
@@ -5482,9 +5781,10 @@ export class AUNChannelPlugin {
|
|
|
5482
5781
|
isCommandCard: false,
|
|
5483
5782
|
initiatorAid: req.initiatorId,
|
|
5484
5783
|
delivery: replyCtx?.delivery,
|
|
5485
|
-
expiresAt:
|
|
5784
|
+
expiresAt: cardExpiresAt,
|
|
5486
5785
|
},
|
|
5487
5786
|
});
|
|
5787
|
+
return toInteractionReceipt(result);
|
|
5488
5788
|
}
|
|
5489
5789
|
else if (req.kind.kind === 'command-card') {
|
|
5490
5790
|
const card = req.kind;
|
|
@@ -5510,7 +5810,7 @@ export class AUNChannelPlugin {
|
|
|
5510
5810
|
aunCard.initiator = req.initiatorId;
|
|
5511
5811
|
if (replyCtx?.threadId)
|
|
5512
5812
|
aunCard.thread_id = replyCtx.threadId;
|
|
5513
|
-
await channel.sendContentPayload(channelId, aunCard, {
|
|
5813
|
+
const result = await channel.sendContentPayload(channelId, aunCard, {
|
|
5514
5814
|
contentKind: 'card',
|
|
5515
5815
|
context: replyCtx,
|
|
5516
5816
|
logText: card.title ? `[card] ${card.title}` : '[card]',
|
|
@@ -5520,14 +5820,35 @@ export class AUNChannelPlugin {
|
|
|
5520
5820
|
isCommandCard: true,
|
|
5521
5821
|
initiatorAid: req.initiatorId,
|
|
5522
5822
|
delivery: replyCtx?.delivery,
|
|
5523
|
-
expiresAt:
|
|
5823
|
+
expiresAt: cardExpiresAt,
|
|
5524
5824
|
},
|
|
5525
5825
|
});
|
|
5826
|
+
return toInteractionReceipt(result);
|
|
5526
5827
|
}
|
|
5527
5828
|
else if (payload.fallbackText) {
|
|
5528
|
-
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
|
+
};
|
|
5529
5850
|
}
|
|
5530
|
-
return;
|
|
5851
|
+
return suppressedReceipt(envelope, 'empty_interaction');
|
|
5531
5852
|
}
|
|
5532
5853
|
case 'custom': {
|
|
5533
5854
|
const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
|
|
@@ -5570,7 +5891,7 @@ export class AUNChannelPlugin {
|
|
|
5570
5891
|
registerBridge(bridge, channelType) {
|
|
5571
5892
|
bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
|
|
5572
5893
|
handler(aunOptsToInbound(opts, adapter.channelName, channelType));
|
|
5573
|
-
}), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
|
|
5894
|
+
}), async (channelId, text, replyContext) => { await channel.sendMessage(channelId, text, replyContext); }, adapter, channelType);
|
|
5574
5895
|
},
|
|
5575
5896
|
registerHooks(hookCtx) {
|
|
5576
5897
|
channel.setEventBus(hookCtx.eventBus);
|