evolcore 0.0.18 → 0.0.20
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 +39 -0
- package/README.md +2 -0
- package/bin/install-codex-managed-hooks.mjs +61 -0
- package/dist/agents/claude-runner.js +4 -3
- package/dist/agents/codex-runner.js +25 -20
- package/dist/agents/ecagent-runner.js +3 -2
- package/dist/aun/aid/agentmd.js +7 -0
- package/dist/aun/msg/group.js +14 -3
- package/dist/aun/msg/p2p.js +21 -11
- package/dist/aun/outbox.js +144 -19
- package/dist/channels/aun.js +621 -211
- package/dist/cli/daemon-commands.js +41 -8
- package/dist/cli/index.js +1 -0
- package/dist/cli/init.js +55 -15
- package/dist/cli/restart-monitor.js +3 -3
- package/dist/config/aun-gateway-config.js +2 -0
- package/dist/config/config-manager.js +92 -8
- package/dist/config/config-operation-service.js +1 -2
- package/dist/config/gateway-config.js +9 -7
- package/dist/config/lifecycle.js +16 -5
- package/dist/config-store.js +13 -6
- package/dist/core/auth/authorization-audit.js +5 -2
- package/dist/core/bootstrap-messages.js +2 -2
- package/dist/core/bootstrap-service.js +21 -36
- package/dist/core/channel-loader.js +0 -2
- package/dist/core/data-migration.js +10 -4
- package/dist/core/evolagent.js +5 -4
- package/dist/core/message/message-bridge.js +6 -11
- package/dist/core/message/response-engine.js +62 -6
- package/dist/core/permission/ec-command-parser.js +203 -24
- package/dist/core/permission/sandbox-runtime.js +46 -12
- package/dist/core/permission/tool-policy.js +116 -47
- package/dist/core/relation/peer-identity.js +18 -0
- package/dist/eck/kit-renderer.js +17 -8
- package/dist/index.js +30 -19
- package/dist/ipc.js +6 -1
- package/dist/paths.js +0 -3
- package/dist/utils/stats.js +52 -18
- package/dist/utils/welcome.js +2 -2
- package/kits/docs/path-registry.md +1 -1
- package/kits/rules/01-overview.md +1 -1
- package/kits/rules/02-navigation.md +2 -2
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.10.json +2 -1
- package/kits/schemas/agent-config.schema.11.json +408 -0
- package/kits/schemas/daemon.schema.5.json +136 -0
- package/kits/schemas/defaults.schema.5.json +107 -0
- package/package.json +2 -1
- package/dist/core/message/pause-controller.js +0 -53
package/dist/channels/aun.js
CHANGED
|
@@ -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';
|
|
@@ -195,6 +195,211 @@ export function buildAunFilePayload(params) {
|
|
|
195
195
|
}
|
|
196
196
|
return payload;
|
|
197
197
|
}
|
|
198
|
+
const AUN_PERMANENT_ERROR_CODES = new Set([
|
|
199
|
+
400, 401, 403, 404,
|
|
200
|
+
4000, 4001, 4010, 4030, 4040, 4090,
|
|
201
|
+
-32700, -32600, -32601, -32602,
|
|
202
|
+
-32001, -32002, -32003, -32004, -32005, -32008, -32009, -32011, -32013,
|
|
203
|
+
-32040, -32041, -32042, -32043, -32044, -32050, -32051,
|
|
204
|
+
-32100, -32101, -32102, -32103, -32104, -32105, -32150, -32152, -32153,
|
|
205
|
+
-32160, -32161, -32162, -32164, -32170, -32171, -32172, -32173, -32174, -32175,
|
|
206
|
+
-32176, -32177, -32178, -32179, -32185, -32186,
|
|
207
|
+
-32180, -32181, -32182, -32183, -32184,
|
|
208
|
+
-33001, -33004, -33005, -33006, -33007, -33008, -33009,
|
|
209
|
+
-33401, -33403, -33404, -33405,
|
|
210
|
+
]);
|
|
211
|
+
const AUN_RETRYABLE_ERROR_CODES = new Set([
|
|
212
|
+
429, 4290,
|
|
213
|
+
-32603,
|
|
214
|
+
-32010, -32029, -32429,
|
|
215
|
+
-32151, -32154, -32163,
|
|
216
|
+
-33402, -33406, -33407,
|
|
217
|
+
]);
|
|
218
|
+
const AUN_ACCEPTED_DISPATCH_STATUSES = new Set([
|
|
219
|
+
'debounced', 'dispatched', 'accepted', 'queued', 'queued_batch', 'broadcast', 'sent',
|
|
220
|
+
]);
|
|
221
|
+
const AUN_PERMANENT_STRING_CODES = new Set([
|
|
222
|
+
'invalid_params', 'invalid_param', 'invalid_request', 'permission_denied',
|
|
223
|
+
'unauthorized', 'authentication_failed', 'not_found', 'group_not_found',
|
|
224
|
+
'group_not_member', 'member_not_found', 'target_not_found', 'agent_not_found',
|
|
225
|
+
'recipient_not_found', 'invalid_argument', 'invalid_payload', 'forbidden',
|
|
226
|
+
]);
|
|
227
|
+
const AUN_RETRYABLE_STRING_CODES = new Set([
|
|
228
|
+
'network_error', 'connection_error', 'timeout', 'timed_out', 'rate_limited',
|
|
229
|
+
'too_many_requests', 'temporarily_unavailable', 'server_error', 'aun_not_connected',
|
|
230
|
+
]);
|
|
231
|
+
function errorRecord(value) {
|
|
232
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
233
|
+
? value
|
|
234
|
+
: undefined;
|
|
235
|
+
}
|
|
236
|
+
function firstErrorField(candidates, keys, accept) {
|
|
237
|
+
for (const candidate of candidates) {
|
|
238
|
+
for (const key of keys) {
|
|
239
|
+
const value = candidate[key];
|
|
240
|
+
if (value !== undefined && value !== null && value !== '' && (!accept || accept(value)))
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
function aunErrorDetails(value, fallback = 'AUN send failed') {
|
|
247
|
+
const root = errorRecord(value);
|
|
248
|
+
const nested = root
|
|
249
|
+
? [root.error, root.data, root.details, root.cause]
|
|
250
|
+
.map(errorRecord)
|
|
251
|
+
.filter((candidate) => !!candidate)
|
|
252
|
+
: [];
|
|
253
|
+
const candidates = root ? [root, ...nested] : [];
|
|
254
|
+
const rawMessage = firstErrorField(candidates, [
|
|
255
|
+
'message', 'error_message', 'errorMessage', 'error', 'reason', 'detail',
|
|
256
|
+
], value => typeof value === 'string');
|
|
257
|
+
const message = typeof value === 'string'
|
|
258
|
+
? value
|
|
259
|
+
: typeof rawMessage === 'string'
|
|
260
|
+
? rawMessage
|
|
261
|
+
: value instanceof Error
|
|
262
|
+
? value.message
|
|
263
|
+
: fallback;
|
|
264
|
+
const rawCode = firstErrorField(candidates, [
|
|
265
|
+
'code', 'error_code', 'errorCode', 'stringCode', 'string_code',
|
|
266
|
+
], value => typeof value === 'string' || typeof value === 'number');
|
|
267
|
+
const rawStatus = firstErrorField(candidates, ['status', 'statusCode', 'http_status', 'httpStatus'], value => (typeof value === 'number' && Number.isFinite(value))
|
|
268
|
+
|| (typeof value === 'string' && /^\d+$/.test(value)));
|
|
269
|
+
const status = typeof rawStatus === 'number'
|
|
270
|
+
? rawStatus
|
|
271
|
+
: typeof rawStatus === 'string' && /^\d+$/.test(rawStatus)
|
|
272
|
+
? Number(rawStatus)
|
|
273
|
+
: undefined;
|
|
274
|
+
const retryableCandidate = candidates.find(candidate => Object.prototype.hasOwnProperty.call(candidate, 'retryable'));
|
|
275
|
+
const retryable = retryableCandidate && typeof retryableCandidate.retryable === 'boolean'
|
|
276
|
+
? retryableCandidate.retryable
|
|
277
|
+
: undefined;
|
|
278
|
+
const name = firstErrorField(candidates, ['name', 'type'], value => typeof value === 'string');
|
|
279
|
+
const messageStatus = message.match(/\b(?:HTTP\b[^\d]{0,40}|status(?:\s*code)?\s*[:=]?\s*)(\d{3})\b/i);
|
|
280
|
+
const dispatchStatus = root?.message_dispatch && typeof root.message_dispatch === 'object'
|
|
281
|
+
? root.message_dispatch.status
|
|
282
|
+
: undefined;
|
|
283
|
+
return {
|
|
284
|
+
code: typeof rawCode === 'string' || typeof rawCode === 'number' ? rawCode : undefined,
|
|
285
|
+
message: message || fallback,
|
|
286
|
+
name: typeof name === 'string' ? name : undefined,
|
|
287
|
+
retryable,
|
|
288
|
+
status: status ?? (messageStatus ? Number(messageStatus[1]) : undefined),
|
|
289
|
+
dispatchStatus: typeof dispatchStatus === 'string' ? dispatchStatus.toLowerCase() : undefined,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function numericErrorCode(code) {
|
|
293
|
+
if (typeof code === 'number' && Number.isFinite(code))
|
|
294
|
+
return code;
|
|
295
|
+
if (typeof code === 'string' && /^-?\d+$/.test(code.trim()))
|
|
296
|
+
return Number(code);
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
function normalizedErrorCode(code) {
|
|
300
|
+
return code === undefined ? '' : String(code).trim().toLowerCase().replace(/[\s-]+/g, '_');
|
|
301
|
+
}
|
|
302
|
+
function shortErrorMessage(message) {
|
|
303
|
+
const compact = message.replace(/\s+/g, ' ').trim();
|
|
304
|
+
return compact.length > 500 ? `${compact.slice(0, 497)}...` : compact;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Classify a gateway response before it reaches the durable queue. Protocol
|
|
308
|
+
* and HTTP semantics take precedence; the SDK's `retryable` bit is used when
|
|
309
|
+
* no known code or explicit rejection already determines the outcome.
|
|
310
|
+
*/
|
|
311
|
+
export function classifyAunSendFailure(value, fallback = 'AUN send failed') {
|
|
312
|
+
const details = aunErrorDetails(value, fallback);
|
|
313
|
+
const numericCode = numericErrorCode(details.code);
|
|
314
|
+
const stringCode = normalizedErrorCode(details.code);
|
|
315
|
+
const text = `${details.name ?? ''} ${stringCode} ${details.message}`.toLowerCase();
|
|
316
|
+
const httpStatus = details.status
|
|
317
|
+
?? (numericCode !== undefined && numericCode >= 100 && numericCode <= 599 ? numericCode : undefined);
|
|
318
|
+
const retryByName = /(?:timeout|connection|rate.?limit|temporar(?:y|ily)|unavailable|network|socket)/i.test(details.name ?? '');
|
|
319
|
+
const relayTargetMissing = text.includes('relay_target_not_found') || text.includes('relay target not found');
|
|
320
|
+
const rpcHandlerTimeout = numericCode === -32004 && /rpc handler timeout/i.test(text);
|
|
321
|
+
const groupStateCode = numericCode === -33002 || numericCode === -33003;
|
|
322
|
+
const groupClosed = groupStateCode && /\b(?:closed|dissolved|disbanded)\b|解散|已关闭/i.test(text);
|
|
323
|
+
const groupSuspended = groupStateCode && /\bsuspend(?:ed)?\b|暂停/i.test(text);
|
|
324
|
+
const transientHttpStatus = httpStatus === 408 || httpStatus === 425 || httpStatus === 429
|
|
325
|
+
|| (httpStatus !== undefined && httpStatus >= 500 && httpStatus < 600);
|
|
326
|
+
const permanentHttpStatus = httpStatus !== undefined
|
|
327
|
+
&& httpStatus >= 400 && httpStatus < 500
|
|
328
|
+
&& !transientHttpStatus;
|
|
329
|
+
const permanentByText = !relayTargetMissing && /(?:group|peer|agent|recipient|target|member|object|task|stream)\s*(?:id\s*)?(?:not found|does not 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
|
+
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
|
+
const acceptedDispatch = details.dispatchStatus !== undefined
|
|
332
|
+
&& AUN_ACCEPTED_DISPATCH_STATUSES.has(details.dispatchStatus);
|
|
333
|
+
const rejectedDispatch = details.dispatchStatus === 'failed'
|
|
334
|
+
|| details.dispatchStatus === 'skipped';
|
|
335
|
+
const successfulHttpWithoutReceipt = httpStatus !== undefined
|
|
336
|
+
&& httpStatus >= 200 && httpStatus < 300
|
|
337
|
+
&& numericCode === undefined && !permanentByText && !retryByText;
|
|
338
|
+
const ambiguousAccepted = errorRecord(value)?.ok === true
|
|
339
|
+
&& numericCode === undefined
|
|
340
|
+
&& httpStatus === undefined
|
|
341
|
+
&& !permanentByText
|
|
342
|
+
&& !retryByText;
|
|
343
|
+
const ambiguousNoReceipt = (value === null || value === undefined)
|
|
344
|
+
&& /no message[_ -]?id|no receipt|empty response|returned no/i.test(fallback);
|
|
345
|
+
const missingReceipt = /no message[_ -]?id|missing message[_ -]?id|no receipt|empty response|returned no/i.test(fallback);
|
|
346
|
+
const unconfirmedDelivery = missingReceipt
|
|
347
|
+
&& (acceptedDispatch || ambiguousAccepted || ambiguousNoReceipt || successfulHttpWithoutReceipt);
|
|
348
|
+
let status;
|
|
349
|
+
if ((AUN_PERMANENT_ERROR_CODES.has(numericCode ?? Number.NaN) && !rpcHandlerTimeout)
|
|
350
|
+
|| permanentByText
|
|
351
|
+
|| AUN_PERMANENT_STRING_CODES.has(stringCode)
|
|
352
|
+
|| rejectedDispatch
|
|
353
|
+
|| groupClosed
|
|
354
|
+
|| (groupStateCode && !groupSuspended)
|
|
355
|
+
|| permanentHttpStatus) {
|
|
356
|
+
status = 'permanent';
|
|
357
|
+
}
|
|
358
|
+
else if (unconfirmedDelivery) {
|
|
359
|
+
// The RPC completed with a success-looking result but without the protocol
|
|
360
|
+
// receipt. Reissuing it could duplicate a message because AUN send calls
|
|
361
|
+
// do not carry our local outbox operation ID as an idempotency key.
|
|
362
|
+
status = 'permanent';
|
|
363
|
+
}
|
|
364
|
+
else if (AUN_RETRYABLE_ERROR_CODES.has(numericCode ?? Number.NaN)
|
|
365
|
+
|| transientHttpStatus
|
|
366
|
+
|| rpcHandlerTimeout
|
|
367
|
+
|| AUN_RETRYABLE_STRING_CODES.has(stringCode)
|
|
368
|
+
|| retryByName
|
|
369
|
+
|| relayTargetMissing
|
|
370
|
+
|| groupSuspended
|
|
371
|
+
|| retryByText) {
|
|
372
|
+
// Known transient protocol, HTTP, SDK-class and transport signals take
|
|
373
|
+
// precedence over the SDK's generic `retryable` default. Several SDK
|
|
374
|
+
// error classes default that flag to false even for connection failures.
|
|
375
|
+
status = 'retry';
|
|
376
|
+
}
|
|
377
|
+
else if (details.retryable === false) {
|
|
378
|
+
status = 'permanent';
|
|
379
|
+
}
|
|
380
|
+
else if (details.retryable === true) {
|
|
381
|
+
status = 'retry';
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
// A response with no recognized transient signal is deterministic enough
|
|
385
|
+
// to stop retrying. This is deliberately fail-closed for durable sends.
|
|
386
|
+
status = 'permanent';
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
status,
|
|
390
|
+
error: shortErrorMessage(details.message),
|
|
391
|
+
...(details.code !== undefined
|
|
392
|
+
? { code: details.code }
|
|
393
|
+
: details.status !== undefined
|
|
394
|
+
? { code: details.status }
|
|
395
|
+
: missingReceipt
|
|
396
|
+
? { code: 'MISSING_MESSAGE_ID' }
|
|
397
|
+
: {}),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function sentOutboxResult() {
|
|
401
|
+
return { status: 'sent' };
|
|
402
|
+
}
|
|
198
403
|
function setIfDefined(target, key, value) {
|
|
199
404
|
if (value !== undefined)
|
|
200
405
|
target[key] = value;
|
|
@@ -1017,6 +1222,7 @@ export class AUNChannel {
|
|
|
1017
1222
|
* the event handler boundary, before payload/slash/mention parsing.
|
|
1018
1223
|
*/
|
|
1019
1224
|
inboundSeenMessages = new Map();
|
|
1225
|
+
invalidInboundEnvelopeLastLogAt = 0;
|
|
1020
1226
|
groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
|
|
1021
1227
|
peerInfoCache = new Map();
|
|
1022
1228
|
messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
|
|
@@ -1174,11 +1380,9 @@ export class AUNChannel {
|
|
|
1174
1380
|
const aidName = this.config.aid;
|
|
1175
1381
|
// encryptionSeed 由 getAidStore 内部解析(config / env / 'evol')
|
|
1176
1382
|
// 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}`);
|
|
1383
|
+
// Gateway discovery/cache is owned entirely by the SDK.
|
|
1384
|
+
this.gatewayUrl = '';
|
|
1385
|
+
logger.info(`${this.logPrefix()} Initializing: aid=${aidName}, gateway=<sdk-discovery>, aun_path=${aunPath}`);
|
|
1182
1386
|
// 构造 AIDStore。daemon 使用独立 slot,CLI/netcheck 不能触碰业务入站游标。
|
|
1183
1387
|
// encryptionSeed / rootCaPath 由 getAidStore 内部注入
|
|
1184
1388
|
const store = await getAidStore({
|
|
@@ -1193,13 +1397,6 @@ export class AUNChannel {
|
|
|
1193
1397
|
// A reconnect replaces the SDK client. Late events from the retired
|
|
1194
1398
|
// instance must not mutate the new instance's health or message state.
|
|
1195
1399
|
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
1400
|
// Register event handlers before connecting
|
|
1204
1401
|
client.on('message.received', (data) => {
|
|
1205
1402
|
if (!isCurrentClient())
|
|
@@ -1306,7 +1503,7 @@ export class AUNChannel {
|
|
|
1306
1503
|
const auth = await client.authenticate();
|
|
1307
1504
|
this.trace('OUT', 'auth.authenticate.ok', { aid: client.aid, gateway: auth?.gateway, hasToken: !!auth?.access_token });
|
|
1308
1505
|
this.trace('IN', 'auth.result', { aid: client.aid, gateway: auth?.gateway, hasToken: !!auth?.access_token });
|
|
1309
|
-
const resolvedGateway =
|
|
1506
|
+
const resolvedGateway = typeof auth?.gateway === 'string' ? auth.gateway : '';
|
|
1310
1507
|
this.gatewayUrl = resolvedGateway;
|
|
1311
1508
|
logger.info(`${this.logPrefix()} Authenticated as ${client.aid ?? '?'}, gateway=${resolvedGateway}`);
|
|
1312
1509
|
}
|
|
@@ -1393,22 +1590,11 @@ export class AUNChannel {
|
|
|
1393
1590
|
if (hasMessageLogOperation(chatDir, operationId))
|
|
1394
1591
|
return true;
|
|
1395
1592
|
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' });
|
|
1593
|
+
// Bootstrap welcomes target the configured personal Owner AID. They are
|
|
1594
|
+
// not replies to an inbound group message, so their route is always
|
|
1595
|
+
// private and must not be inferred with group.get_info.
|
|
1596
|
+
const lifecycle = resolveAgentLifecycle(agentConfig);
|
|
1597
|
+
const delivery = { chatType: 'private' };
|
|
1412
1598
|
if (prepared
|
|
1413
1599
|
&& prepared.channelId === owner
|
|
1414
1600
|
&& isDeliveryTarget(prepared.delivery)
|
|
@@ -1452,6 +1638,7 @@ export class AUNChannel {
|
|
|
1452
1638
|
return this.reconcilePostBootstrapWelcome();
|
|
1453
1639
|
}
|
|
1454
1640
|
async hasPendingPostBootstrapWelcome() {
|
|
1641
|
+
this.repairBootstrapRoutes();
|
|
1455
1642
|
const aid = this.config.aid.replace(/^@/, '');
|
|
1456
1643
|
return hasPendingPostBootstrapWelcomeOutbox(aid);
|
|
1457
1644
|
}
|
|
@@ -1467,6 +1654,7 @@ export class AUNChannel {
|
|
|
1467
1654
|
* already-active agent from manufacturing another welcome message.
|
|
1468
1655
|
*/
|
|
1469
1656
|
async reconcilePostBootstrapWelcome() {
|
|
1657
|
+
this.repairBootstrapRoutes();
|
|
1470
1658
|
const configuredAid = this.config.aid;
|
|
1471
1659
|
const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
|
|
1472
1660
|
const operationId = postBootstrapWelcomeOperationId(aid);
|
|
@@ -1484,15 +1672,17 @@ export class AUNChannel {
|
|
|
1484
1672
|
return false;
|
|
1485
1673
|
}
|
|
1486
1674
|
const agentConfig = loadAgent(aid);
|
|
1487
|
-
if (!agentConfig ||
|
|
1675
|
+
if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
|
|
1488
1676
|
logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
|
|
1489
1677
|
return true;
|
|
1490
1678
|
}
|
|
1491
1679
|
if (!this.connected || !this.client)
|
|
1492
1680
|
return true;
|
|
1493
|
-
const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry),
|
|
1494
|
-
if (sent)
|
|
1495
|
-
|
|
1681
|
+
const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
1682
|
+
if (sent.status === 'sent')
|
|
1683
|
+
this.removeDeliveredOutboxEntry(entry);
|
|
1684
|
+
else if (sent.status === 'permanent')
|
|
1685
|
+
this.markPermanentOutboxFailure(entry, sent);
|
|
1496
1686
|
return true;
|
|
1497
1687
|
}
|
|
1498
1688
|
// ── Event handlers ──────────────────────────────────────────
|
|
@@ -1743,17 +1933,30 @@ export class AUNChannel {
|
|
|
1743
1933
|
const msg = data;
|
|
1744
1934
|
const receivedAt = Date.now();
|
|
1745
1935
|
const receivedAtMono = performance.now();
|
|
1746
|
-
//
|
|
1747
|
-
// carry a new seq, but the application
|
|
1936
|
+
// Validate the authenticated envelope before any payload extraction or
|
|
1937
|
+
// command parsing. Retransmits may carry a new seq, but the application
|
|
1938
|
+
// identity remains message_id.
|
|
1748
1939
|
const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
|
|
1749
1940
|
const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
|
|
1750
|
-
if (!this.claimInboundMessage('private', messageId, seq))
|
|
1751
|
-
return;
|
|
1752
1941
|
// SDK 0.5.* 移除了顶层 from/to/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
|
|
1753
1942
|
// message_id / seq / payload / same_* 等仍是顶层独立字段,不在 envelope 内。
|
|
1754
1943
|
const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
|
|
1944
|
+
const fromAid = typeof env.from === 'string' ? env.from.trim() : '';
|
|
1945
|
+
if (!messageId || !fromAid) {
|
|
1946
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
1947
|
+
const now = Date.now();
|
|
1948
|
+
if (now - this.invalidInboundEnvelopeLastLogAt >= 60_000) {
|
|
1949
|
+
this.invalidInboundEnvelopeLastLogAt = now;
|
|
1950
|
+
logger.warn(`${this.logPrefix()} Dropped private inbound: invalid envelope code=INVALID_INBOUND_ENVELOPE mid=${messageId || 'unknown'} from=${fromAid || 'unknown'}`);
|
|
1951
|
+
}
|
|
1952
|
+
else {
|
|
1953
|
+
logger.debug(`${this.logPrefix()} Dropped private inbound: invalid envelope (suppressed duplicate) mid=${messageId || 'unknown'}`);
|
|
1954
|
+
}
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
if (!this.claimInboundMessage('private', messageId, seq))
|
|
1958
|
+
return;
|
|
1755
1959
|
const protectedHeaders = verifiedProtectedHeaders(msg, env);
|
|
1756
|
-
const fromAid = env.from ?? '';
|
|
1757
1960
|
const payload = msg.payload ?? '';
|
|
1758
1961
|
const mentions = this.parsePayloadMentionsOrReject(payload, 'p2p.inbound', messageId, seq);
|
|
1759
1962
|
if (!mentions)
|
|
@@ -2319,7 +2522,7 @@ export class AUNChannel {
|
|
|
2319
2522
|
setObserverConfigResolver(fn) {
|
|
2320
2523
|
this.observerConfigResolver = fn;
|
|
2321
2524
|
}
|
|
2322
|
-
/** 读取 observable 开关 + owners
|
|
2525
|
+
/** 读取 observable 开关 + owners;未接入 daemon 时没有 owning Agent,保持关闭。 */
|
|
2323
2526
|
getObserverConfig() {
|
|
2324
2527
|
return this.observerConfigResolver?.() ?? { observable: false, owners: [] };
|
|
2325
2528
|
}
|
|
@@ -2869,8 +3072,27 @@ export class AUNChannel {
|
|
|
2869
3072
|
this.outboxInFlight.delete(entry.id);
|
|
2870
3073
|
}
|
|
2871
3074
|
}
|
|
3075
|
+
markPermanentOutboxFailure(entry, result) {
|
|
3076
|
+
const error = result.error ?? 'permanent send failure';
|
|
3077
|
+
const terminated = outbox.markTerminal(this.config.aid, entry.id, { error, code: result.code }, entry);
|
|
3078
|
+
if (terminated) {
|
|
3079
|
+
logger.error(`${this.logPrefix()} Permanent AUN delivery failure; outbox entry terminated: id=${entry.id} channel=${entry.channelId} code=${result.code ?? 'unknown'} error=${error}`);
|
|
3080
|
+
}
|
|
3081
|
+
else {
|
|
3082
|
+
logger.warn(`${this.logPrefix()} Ignored stale permanent delivery result after outbox route changed: id=${entry.id} submittedChannel=${entry.channelId}`);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
removeDeliveredOutboxEntry(entry) {
|
|
3086
|
+
if (!outbox.removeIfRouteMatches(this.config.aid, entry)) {
|
|
3087
|
+
logger.warn(`${this.logPrefix()} Preserved outbox entry whose route changed while delivery was in flight: id=${entry.id} submittedChannel=${entry.channelId}`);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
2872
3090
|
messageIdFromSendResult(result) {
|
|
2873
|
-
|
|
3091
|
+
const nested = result?.message?.message_id;
|
|
3092
|
+
if (typeof nested === 'string' && nested.trim())
|
|
3093
|
+
return nested;
|
|
3094
|
+
const direct = result?.message_id;
|
|
3095
|
+
return typeof direct === 'string' && direct.trim() ? direct : null;
|
|
2874
3096
|
}
|
|
2875
3097
|
/**
|
|
2876
3098
|
* An AUN send result is only a gateway transport result. It is not a
|
|
@@ -3140,10 +3362,11 @@ export class AUNChannel {
|
|
|
3140
3362
|
const detail = error instanceof Error ? error.message : String(error);
|
|
3141
3363
|
const code = error?.code ?? 'INVALID_MENTION_SCHEMA';
|
|
3142
3364
|
logger.error(`${this.logPrefix()} Dropped durable AUN payload: invalid payload.mentions (${detail}) code=${code} channel=${channelId}`);
|
|
3143
|
-
return { ok: false,
|
|
3365
|
+
return { ok: false, status: 'permanent', error: detail, code };
|
|
3366
|
+
}
|
|
3367
|
+
if (!this.client || !this.connected) {
|
|
3368
|
+
return { ok: false, status: 'retry', error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
|
|
3144
3369
|
}
|
|
3145
|
-
if (!this.client || !this.connected)
|
|
3146
|
-
return { ok: false };
|
|
3147
3370
|
const isGroup = delivery.chatType === 'group';
|
|
3148
3371
|
const targetAid = channelId;
|
|
3149
3372
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
@@ -3171,10 +3394,11 @@ export class AUNChannel {
|
|
|
3171
3394
|
: await this.callAndTrace(method, sendParams);
|
|
3172
3395
|
const mid = this.messageIdFromSendResult(result);
|
|
3173
3396
|
if (!mid) {
|
|
3174
|
-
|
|
3175
|
-
|
|
3397
|
+
const failure = classifyAunSendFailure(result, `${method} (${label}) returned no message_id`);
|
|
3398
|
+
logger.warn(`${this.logPrefix()} ${method}${fallback ? ' fallback' : ''} (${label}) returned no message_id: ${JSON.stringify(result)}; disposition=${failure.status}`);
|
|
3399
|
+
return { ok: false, status: failure.status, error: failure.error, code: failure.code, result, encrypt: !!sendParams.encrypt };
|
|
3176
3400
|
}
|
|
3177
|
-
return { ok: true, messageId: mid, result, encrypt: !!sendParams.encrypt };
|
|
3401
|
+
return { ok: true, status: 'sent', messageId: mid, result, encrypt: !!sendParams.encrypt };
|
|
3178
3402
|
};
|
|
3179
3403
|
try {
|
|
3180
3404
|
return await callOnce(params, false);
|
|
@@ -3187,18 +3411,22 @@ export class AUNChannel {
|
|
|
3187
3411
|
try {
|
|
3188
3412
|
this.trace('OUT', `${method}.${label}.fallback`, fallbackParams);
|
|
3189
3413
|
const sent = await callOnce(fallbackParams, true);
|
|
3190
|
-
this.trace('OUT', `${method}.${label}.fallback.${sent.ok ? 'ok' : 'missing_id'}`,
|
|
3414
|
+
this.trace('OUT', `${method}.${label}.fallback.${sent.ok ? 'ok' : 'missing_id'}`, sent.ok
|
|
3415
|
+
? { message_id: sent.messageId }
|
|
3416
|
+
: { disposition: sent.status, code: sent.code });
|
|
3191
3417
|
return sent;
|
|
3192
3418
|
}
|
|
3193
3419
|
catch (e2) {
|
|
3194
3420
|
this.trace('OUT', `${method}.${label}.fallback.error`, { channelId, error: String(e2) });
|
|
3195
|
-
|
|
3196
|
-
|
|
3421
|
+
const failure = classifyAunSendFailure(e2, `${method} plaintext fallback failed`);
|
|
3422
|
+
logger.error(`${this.logPrefix()} Plaintext ${label} fallback also failed to ${channelId}: ${failure.error}; disposition=${failure.status}`);
|
|
3423
|
+
return { ok: false, status: failure.status, error: failure.error, code: failure.code };
|
|
3197
3424
|
}
|
|
3198
3425
|
}
|
|
3199
3426
|
this.trace('OUT', `${method}.${label}.error`, { channelId, error: String(e) });
|
|
3200
|
-
|
|
3201
|
-
|
|
3427
|
+
const failure = classifyAunSendFailure(e, `${method} failed`);
|
|
3428
|
+
logger.error(`${this.logPrefix()} ${label} send failed to ${channelId}: ${failure.error}; disposition=${failure.status}${failure.code === undefined ? '' : ` code=${failure.code}`}`);
|
|
3429
|
+
return { ok: false, status: failure.status, error: failure.error, code: failure.code };
|
|
3202
3430
|
}
|
|
3203
3431
|
}
|
|
3204
3432
|
recordDurableOutbound(channelId, payload, messageId, encrypt, context, isGroup, contentKind, logText, result, statsContext) {
|
|
@@ -3270,11 +3498,19 @@ export class AUNChannel {
|
|
|
3270
3498
|
}
|
|
3271
3499
|
return { queued: true };
|
|
3272
3500
|
}
|
|
3273
|
-
const result = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false });
|
|
3501
|
+
const result = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false, status: 'retry', error: 'outbox send is already in flight' });
|
|
3274
3502
|
if (result.ok) {
|
|
3275
|
-
|
|
3503
|
+
this.removeDeliveredOutboxEntry(entry);
|
|
3276
3504
|
return { messageId: result.messageId };
|
|
3277
3505
|
}
|
|
3506
|
+
if (result.status === 'permanent') {
|
|
3507
|
+
this.markPermanentOutboxFailure(entry, result);
|
|
3508
|
+
return {
|
|
3509
|
+
status: 'permanent',
|
|
3510
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
3511
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3278
3514
|
return { queued: true };
|
|
3279
3515
|
}
|
|
3280
3516
|
buildTaskPayloadBase(envelope, context) {
|
|
@@ -3462,6 +3698,10 @@ export class AUNChannel {
|
|
|
3462
3698
|
? routedContext.metadata.outboxTtl
|
|
3463
3699
|
: undefined,
|
|
3464
3700
|
});
|
|
3701
|
+
if (entry.terminal) {
|
|
3702
|
+
logger.warn(`${this.logPrefix()} Skipping previously terminated durable operation: operation=${operationId ?? '<none>'} entry=${entry.id} code=${entry.lastErrorCode ?? 'unknown'}`);
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3465
3705
|
logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
|
|
3466
3706
|
// 积压深度告警:outbox 待发条目累积说明发送速度跟不上,回复将出现明显延迟。
|
|
3467
3707
|
const backlog = outbox.pendingCount(this.config.aid);
|
|
@@ -3476,9 +3716,12 @@ export class AUNChannel {
|
|
|
3476
3716
|
return;
|
|
3477
3717
|
}
|
|
3478
3718
|
// Attempt immediate delivery
|
|
3479
|
-
const
|
|
3480
|
-
if (
|
|
3481
|
-
|
|
3719
|
+
const result = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
3720
|
+
if (result.status === 'sent') {
|
|
3721
|
+
this.removeDeliveredOutboxEntry(entry);
|
|
3722
|
+
}
|
|
3723
|
+
else if (result.status === 'permanent') {
|
|
3724
|
+
this.markPermanentOutboxFailure(entry, result);
|
|
3482
3725
|
}
|
|
3483
3726
|
}
|
|
3484
3727
|
/** Daemon-side transport for `ec msg send` running inside an agent task. */
|
|
@@ -3724,7 +3967,7 @@ export class AUNChannel {
|
|
|
3724
3967
|
ttl: 300_000,
|
|
3725
3968
|
};
|
|
3726
3969
|
const ok = await this.deliverTextEntry(echoEntry);
|
|
3727
|
-
if (
|
|
3970
|
+
if (ok.status !== 'sent' && ok.status !== 'permanent') {
|
|
3728
3971
|
outbox.enqueue(this.config.aid, {
|
|
3729
3972
|
channelId,
|
|
3730
3973
|
delivery,
|
|
@@ -3733,7 +3976,7 @@ export class AUNChannel {
|
|
|
3733
3976
|
context: this.withDelivery(echo.context, delivery),
|
|
3734
3977
|
});
|
|
3735
3978
|
}
|
|
3736
|
-
logger.info(`${this.logPrefix()} [Echo] long echo trace
|
|
3979
|
+
logger.info(`${this.logPrefix()} [Echo] long echo trace status=${ok.status} code=${ok.code ?? 'none'} to=${channelId} agentDurationMs=${agentDuration}`);
|
|
3737
3980
|
}
|
|
3738
3981
|
else {
|
|
3739
3982
|
outbox.enqueue(this.config.aid, {
|
|
@@ -3751,6 +3994,10 @@ export class AUNChannel {
|
|
|
3751
3994
|
}
|
|
3752
3995
|
async deliverTextEntry(entry) {
|
|
3753
3996
|
const channelId = entry.channelId;
|
|
3997
|
+
if (typeof entry.text !== 'string' || !entry.text.trim()) {
|
|
3998
|
+
logger.warn(`${this.logPrefix()} deliverTextEntry: missing or empty text (outbox id=${entry.id})`);
|
|
3999
|
+
return { status: 'permanent', error: 'durable text is missing or empty', code: 'MISSING_TEXT' };
|
|
4000
|
+
}
|
|
3754
4001
|
const finalText = entry.text;
|
|
3755
4002
|
const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
|
|
3756
4003
|
const context = this.withDelivery(entry.context, delivery);
|
|
@@ -3760,12 +4007,13 @@ export class AUNChannel {
|
|
|
3760
4007
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
3761
4008
|
if (hasMessageLogOperation(chatDir, operationId)) {
|
|
3762
4009
|
logger.info(`${this.logPrefix()} Durable operation already logged; skipping duplicate send: ${operationId}`);
|
|
3763
|
-
return
|
|
4010
|
+
return sentOutboxResult();
|
|
3764
4011
|
}
|
|
3765
4012
|
if (operationId === postBootstrapWelcomeOperationId(this.config.aid)) {
|
|
3766
4013
|
const agentConfig = loadAgent(this.config.aid);
|
|
3767
|
-
if (!agentConfig ||
|
|
3768
|
-
return
|
|
4014
|
+
if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
|
|
4015
|
+
return { status: 'retry', error: 'post-bootstrap welcome is waiting for active lifecycle', code: 'BOOTSTRAP_NOT_ACTIVE' };
|
|
4016
|
+
}
|
|
3769
4017
|
}
|
|
3770
4018
|
}
|
|
3771
4019
|
// 从 context.metadata.source 读取 source,默认为 'daemon'
|
|
@@ -3796,7 +4044,7 @@ export class AUNChannel {
|
|
|
3796
4044
|
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
4045
|
const isGroup = delivery.chatType === 'group';
|
|
3798
4046
|
const targetAid = channelId;
|
|
3799
|
-
if (
|
|
4047
|
+
if (entry.deliveryReceipt) {
|
|
3800
4048
|
this.appendOutboundJsonl(channelId, {
|
|
3801
4049
|
...classifyAunPayloadForLog(payload),
|
|
3802
4050
|
msgId: entry.deliveryReceipt.messageId,
|
|
@@ -3806,7 +4054,7 @@ export class AUNChannel {
|
|
|
3806
4054
|
source,
|
|
3807
4055
|
transport: entry.deliveryReceipt.transport,
|
|
3808
4056
|
});
|
|
3809
|
-
return
|
|
4057
|
+
return sentOutboxResult();
|
|
3810
4058
|
}
|
|
3811
4059
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
3812
4060
|
const encrypt = context?.metadata?.encrypted != null
|
|
@@ -3819,16 +4067,19 @@ export class AUNChannel {
|
|
|
3819
4067
|
if (isGroup) {
|
|
3820
4068
|
params.group_id = channelId;
|
|
3821
4069
|
const result = await this.callAndTrace('group.send', params);
|
|
3822
|
-
const mid = result
|
|
4070
|
+
const mid = this.messageIdFromSendResult(result);
|
|
3823
4071
|
if (!mid) {
|
|
3824
|
-
const
|
|
3825
|
-
|
|
3826
|
-
|
|
4072
|
+
const failure = classifyAunSendFailure(result, 'group.send returned no message_id');
|
|
4073
|
+
const dispatchStatus = typeof result?.message_dispatch?.status === 'string'
|
|
4074
|
+
? result.message_dispatch.status.toLowerCase()
|
|
4075
|
+
: undefined;
|
|
4076
|
+
if (dispatchStatus && AUN_ACCEPTED_DISPATCH_STATUSES.has(dispatchStatus)) {
|
|
4077
|
+
logger.warn(`${this.logPrefix()} group.send returned ${dispatchStatus} without message_id: disposition=${failure.status} code=${failure.code ?? 'unknown'} group=${channelId}`);
|
|
3827
4078
|
}
|
|
3828
4079
|
else {
|
|
3829
|
-
logger.warn(`${this.logPrefix()} group.send returned no message_id:
|
|
4080
|
+
logger.warn(`${this.logPrefix()} group.send returned no message_id: disposition=${failure.status} code=${failure.code ?? 'unknown'} group=${channelId} dispatch=${dispatchStatus ?? 'unknown'}`);
|
|
3830
4081
|
}
|
|
3831
|
-
return
|
|
4082
|
+
return failure;
|
|
3832
4083
|
}
|
|
3833
4084
|
else {
|
|
3834
4085
|
this.logAunSendAccepted('group.send', channelId, mid, encrypt, result, finalText);
|
|
@@ -3847,34 +4098,40 @@ export class AUNChannel {
|
|
|
3847
4098
|
else {
|
|
3848
4099
|
params.to = targetAid;
|
|
3849
4100
|
const result = await this.callAndTrace('message.send', params);
|
|
3850
|
-
|
|
4101
|
+
const mid = this.messageIdFromSendResult(result);
|
|
4102
|
+
if (!mid) {
|
|
3851
4103
|
logger.warn(`${this.logPrefix()} message.send returned no message_id: ${JSON.stringify(result)}`);
|
|
3852
|
-
return
|
|
4104
|
+
return classifyAunSendFailure(result, 'message.send returned no message_id');
|
|
3853
4105
|
}
|
|
3854
4106
|
else {
|
|
3855
|
-
this.logAunSendAccepted('message.send', this.peerLabel(targetAid),
|
|
3856
|
-
this.checkpointTextDelivery(entry,
|
|
4107
|
+
this.logAunSendAccepted('message.send', this.peerLabel(targetAid), mid, encrypt, result, finalText);
|
|
4108
|
+
this.checkpointTextDelivery(entry, mid, encrypt, result);
|
|
3857
4109
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
3858
4110
|
if (causation) {
|
|
3859
|
-
registerAunCausation(
|
|
4111
|
+
registerAunCausation(mid, this.config.aid, targetAid, causation);
|
|
3860
4112
|
recordCausationSpan(causation, 'message.outbound', {
|
|
3861
4113
|
status: 'completed',
|
|
3862
|
-
refs: { messageId:
|
|
4114
|
+
refs: { messageId: mid, taskId: context?.metadata?.taskId },
|
|
3863
4115
|
});
|
|
3864
4116
|
}
|
|
3865
|
-
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: targetAid, msgId:
|
|
4117
|
+
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
4118
|
this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode, 'send');
|
|
3867
4119
|
this.appendOutboundJsonl(targetAid, {
|
|
3868
|
-
...classifyAunPayloadForLog(payload), msgId:
|
|
4120
|
+
...classifyAunPayloadForLog(payload), msgId: mid, encrypt, context, isGroup: false, source,
|
|
3869
4121
|
transport: this.sendReceiptFromResult(result),
|
|
3870
4122
|
});
|
|
3871
4123
|
// Observer forward: outbound (private) — 原样转发 SDK SendResult(含 envelope + payload)
|
|
3872
4124
|
this.forwardOutbound(result);
|
|
3873
4125
|
}
|
|
3874
4126
|
}
|
|
3875
|
-
return
|
|
4127
|
+
return sentOutboxResult();
|
|
3876
4128
|
}
|
|
3877
4129
|
catch (e) {
|
|
4130
|
+
if (entry.deliveryReceipt) {
|
|
4131
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
4132
|
+
logger.error(`${this.logPrefix()} AUN accepted the durable text but local post-send processing failed; retaining receipt for recovery: id=${entry.id} error=${error}`);
|
|
4133
|
+
return { status: 'retry', error, code: 'OUTBOX_POST_SEND_FAILED' };
|
|
4134
|
+
}
|
|
3878
4135
|
if (encrypt && e instanceof E2EEError) {
|
|
3879
4136
|
this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
|
|
3880
4137
|
logger.warn(`${this.logPrefix()} E2EE send failed to ${channelId}, retrying plaintext: ${e}`);
|
|
@@ -3884,11 +4141,14 @@ export class AUNChannel {
|
|
|
3884
4141
|
this.trace('OUT', 'group.send.fallback', params);
|
|
3885
4142
|
const result = await this.client.call('group.send', params);
|
|
3886
4143
|
const mid = this.messageIdFromSendResult(result);
|
|
3887
|
-
this.trace('OUT', 'group.send.fallback.ok', { message_id: mid });
|
|
3888
4144
|
if (!mid) {
|
|
4145
|
+
const resultRecord = errorRecord(result);
|
|
4146
|
+
const dispatch = errorRecord(resultRecord?.message_dispatch)?.status;
|
|
4147
|
+
this.trace('OUT', 'group.send.fallback.missing_id', { dispatch });
|
|
3889
4148
|
logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
3890
|
-
return
|
|
4149
|
+
return classifyAunSendFailure(result, 'group.send plaintext fallback returned no message_id');
|
|
3891
4150
|
}
|
|
4151
|
+
this.trace('OUT', 'group.send.fallback.ok', { message_id: mid });
|
|
3892
4152
|
this.checkpointTextDelivery(entry, mid, false, result);
|
|
3893
4153
|
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
4154
|
const statsContext = await this.groupStatsContext(channelId);
|
|
@@ -3902,12 +4162,13 @@ export class AUNChannel {
|
|
|
3902
4162
|
else {
|
|
3903
4163
|
this.trace('OUT', 'message.send.fallback', params);
|
|
3904
4164
|
const result = await this.client.call('message.send', params);
|
|
3905
|
-
const mid = result
|
|
3906
|
-
|
|
3907
|
-
|
|
4165
|
+
const mid = this.messageIdFromSendResult(result);
|
|
4166
|
+
if (!mid) {
|
|
4167
|
+
this.trace('OUT', 'message.send.fallback.missing_id', {});
|
|
3908
4168
|
logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
3909
|
-
return
|
|
4169
|
+
return classifyAunSendFailure(result, 'message.send plaintext fallback returned no message_id');
|
|
3910
4170
|
}
|
|
4171
|
+
this.trace('OUT', 'message.send.fallback.ok', { message_id: mid });
|
|
3911
4172
|
this.checkpointTextDelivery(entry, mid, false, result);
|
|
3912
4173
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
3913
4174
|
if (causation) {
|
|
@@ -3925,31 +4186,48 @@ export class AUNChannel {
|
|
|
3925
4186
|
});
|
|
3926
4187
|
this.forwardOutbound(result);
|
|
3927
4188
|
}
|
|
3928
|
-
return
|
|
4189
|
+
return sentOutboxResult();
|
|
3929
4190
|
}
|
|
3930
4191
|
catch (e2) {
|
|
4192
|
+
if (entry.deliveryReceipt) {
|
|
4193
|
+
const error = e2 instanceof Error ? e2.message : String(e2);
|
|
4194
|
+
logger.error(`${this.logPrefix()} AUN accepted the plaintext fallback but local post-send processing failed; retaining receipt for recovery: id=${entry.id} error=${error}`);
|
|
4195
|
+
return { status: 'retry', error, code: 'OUTBOX_POST_SEND_FAILED' };
|
|
4196
|
+
}
|
|
3931
4197
|
this.trace('OUT', 'send.fallback.error', { channelId, error: String(e2) });
|
|
3932
4198
|
logger.error(`${this.logPrefix()} Plaintext fallback also failed to ${channelId}: ${e2}`);
|
|
3933
|
-
return
|
|
4199
|
+
return classifyAunSendFailure(e2, `plaintext send fallback failed to ${channelId}`);
|
|
3934
4200
|
}
|
|
3935
4201
|
}
|
|
3936
4202
|
else {
|
|
3937
4203
|
this.trace('OUT', 'send.error', { channelId, error: String(e) });
|
|
3938
4204
|
logger.error(`${this.logPrefix()} Send failed to ${channelId} (outbox id=${entry.id}): ${e}`);
|
|
3939
|
-
return
|
|
4205
|
+
return classifyAunSendFailure(e, `send failed to ${channelId}`);
|
|
3940
4206
|
}
|
|
3941
4207
|
}
|
|
3942
4208
|
}
|
|
3943
4209
|
checkpointTextDelivery(entry, messageId, encrypt, result) {
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
4210
|
+
this.checkpointDurableDelivery(entry, messageId, encrypt, result);
|
|
4211
|
+
}
|
|
4212
|
+
checkpointDurableDelivery(entry, messageId, encrypt, result) {
|
|
4213
|
+
const submittedRoute = {
|
|
4214
|
+
id: entry.id,
|
|
4215
|
+
channelId: entry.channelId,
|
|
4216
|
+
delivery: entry.delivery,
|
|
4217
|
+
};
|
|
4218
|
+
const receipt = {
|
|
3947
4219
|
messageId,
|
|
3948
4220
|
encrypt,
|
|
3949
4221
|
transport: this.sendReceiptFromResult(result),
|
|
3950
4222
|
};
|
|
3951
|
-
|
|
3952
|
-
|
|
4223
|
+
const replaced = outbox.updateDeliveryReceiptIfRouteMatches(this.config.aid, submittedRoute, receipt);
|
|
4224
|
+
if (replaced === 'replaced') {
|
|
4225
|
+
entry.deliveryReceipt = receipt;
|
|
4226
|
+
return true;
|
|
4227
|
+
}
|
|
4228
|
+
else {
|
|
4229
|
+
logger.warn(`${this.logPrefix()} Did not checkpoint durable delivery receipt: entry=${entry.id} operation=${entry.dedupeKey ?? 'none'} reason=${replaced}`);
|
|
4230
|
+
return false;
|
|
3953
4231
|
}
|
|
3954
4232
|
}
|
|
3955
4233
|
async deliverPayloadEntry(entry) {
|
|
@@ -3958,32 +4236,71 @@ export class AUNChannel {
|
|
|
3958
4236
|
: undefined;
|
|
3959
4237
|
if (interactionId && this.invalidatedInteractions.has(interactionId)) {
|
|
3960
4238
|
logger.info(`${this.logPrefix()} Discarded invalidated interaction from durable outbox: request=${interactionId} entry=${entry.id}`);
|
|
3961
|
-
return { ok: true };
|
|
4239
|
+
return { ok: true, status: 'sent' };
|
|
3962
4240
|
}
|
|
3963
4241
|
const channelId = entry.channelId;
|
|
3964
4242
|
const payload = entry.payload;
|
|
3965
4243
|
if (!payload) {
|
|
3966
4244
|
logger.warn(`${this.logPrefix()} deliverPayloadEntry: missing payload (outbox id=${entry.id})`);
|
|
3967
|
-
return { ok:
|
|
4245
|
+
return { ok: false, status: 'permanent', error: 'durable payload is missing', code: 'MISSING_PAYLOAD' };
|
|
3968
4246
|
}
|
|
3969
4247
|
const contentKind = entry.contentKind;
|
|
3970
4248
|
const logText = entry.logText ?? this.payloadLogText(payload, contentKind);
|
|
3971
4249
|
const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
|
|
3972
4250
|
const context = this.withDelivery(entry.context, delivery);
|
|
3973
4251
|
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
|
-
|
|
4252
|
+
const isGroup = delivery.chatType === 'group';
|
|
4253
|
+
const source = context?.metadata?.source ?? 'daemon';
|
|
4254
|
+
if (entry.deliveryReceipt) {
|
|
4255
|
+
try {
|
|
4256
|
+
this.appendOutboundJsonl(channelId, {
|
|
4257
|
+
...classifyAunPayloadForLog(payload),
|
|
4258
|
+
msgId: entry.deliveryReceipt.messageId,
|
|
4259
|
+
encrypt: entry.deliveryReceipt.encrypt,
|
|
4260
|
+
context,
|
|
4261
|
+
isGroup,
|
|
4262
|
+
source,
|
|
4263
|
+
transport: entry.deliveryReceipt.transport,
|
|
4264
|
+
});
|
|
4265
|
+
this.runPostSend(entry, entry.deliveryReceipt.messageId);
|
|
4266
|
+
return {
|
|
4267
|
+
ok: true,
|
|
4268
|
+
status: 'sent',
|
|
4269
|
+
messageId: entry.deliveryReceipt.messageId,
|
|
4270
|
+
encrypt: entry.deliveryReceipt.encrypt,
|
|
4271
|
+
};
|
|
4272
|
+
}
|
|
4273
|
+
catch (error) {
|
|
4274
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4275
|
+
logger.error(`${this.logPrefix()} Failed to recover local post-send state from durable payload receipt: id=${entry.id} error=${detail}`);
|
|
4276
|
+
return { ok: false, status: 'retry', error: detail, code: 'OUTBOX_POST_SEND_FAILED' };
|
|
4277
|
+
}
|
|
3978
4278
|
}
|
|
4279
|
+
const sent = await this.sendAunPayload(channelId, payload, context, `${contentKind ?? payload.type ?? 'payload'}`);
|
|
3979
4280
|
if (!sent.ok || !sent.messageId)
|
|
3980
4281
|
return sent;
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
4282
|
+
try {
|
|
4283
|
+
this.checkpointDurableDelivery(entry, sent.messageId, !!sent.encrypt, sent.result);
|
|
4284
|
+
}
|
|
4285
|
+
catch (error) {
|
|
4286
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4287
|
+
logger.error(`${this.logPrefix()} AUN accepted the durable payload but its receipt could not be persisted: id=${entry.id} mid=${sent.messageId} error=${detail}`);
|
|
4288
|
+
return { ok: false, status: 'permanent', error: detail, code: 'OUTBOX_RECEIPT_CHECKPOINT_FAILED' };
|
|
4289
|
+
}
|
|
4290
|
+
try {
|
|
4291
|
+
const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
|
|
4292
|
+
this.logAunSendAccepted(isGroup ? 'group.send' : 'message.send', isGroup ? channelId : this.peerLabel(channelId), sent.messageId, !!sent.encrypt, sent.result, logText);
|
|
4293
|
+
this.recordDurableOutbound(channelId, payload, sent.messageId, !!sent.encrypt, context, isGroup, contentKind, logText, sent.result, statsContext);
|
|
4294
|
+
this.runPostSend(entry, sent.messageId);
|
|
4295
|
+
return sent;
|
|
4296
|
+
}
|
|
4297
|
+
catch (error) {
|
|
4298
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4299
|
+
logger.error(`${this.logPrefix()} AUN accepted the durable payload but local post-send processing failed: id=${entry.id} mid=${sent.messageId} error=${detail}`);
|
|
4300
|
+
return entry.deliveryReceipt
|
|
4301
|
+
? { ok: false, status: 'retry', error: detail, code: 'OUTBOX_POST_SEND_FAILED' }
|
|
4302
|
+
: { ok: false, status: 'permanent', error: detail, code: 'OUTBOX_POST_SEND_UNCHECKPOINTED' };
|
|
4303
|
+
}
|
|
3987
4304
|
}
|
|
3988
4305
|
/** 有效会话正文写入 messages.jsonl(message.send/group.send 成功后调用)。 */
|
|
3989
4306
|
appendOutboundJsonl(channelId, descriptor) {
|
|
@@ -4127,16 +4444,22 @@ export class AUNChannel {
|
|
|
4127
4444
|
logger.debug(`${this.logPrefix()} thought.put failed to ${channelId}: ${err?.name}(${err?.code})=${err?.message}`);
|
|
4128
4445
|
}
|
|
4129
4446
|
}
|
|
4130
|
-
/**
|
|
4131
|
-
async
|
|
4447
|
+
/** Send a transient structured payload and preserve the gateway disposition. */
|
|
4448
|
+
async sendStructuredResult(channelId, payload, context) {
|
|
4132
4449
|
const delivery = this.requireDelivery(channelId, context);
|
|
4133
4450
|
context = this.withDelivery(context, delivery);
|
|
4134
4451
|
// Validate before checking connection state so malformed producer payloads
|
|
4135
4452
|
// are reported deterministically instead of being hidden by a disconnect.
|
|
4136
4453
|
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
4137
4454
|
const finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
4138
|
-
if (!this.connected || !this.client)
|
|
4139
|
-
return
|
|
4455
|
+
if (!this.connected || !this.client) {
|
|
4456
|
+
return {
|
|
4457
|
+
ok: false,
|
|
4458
|
+
status: 'retry',
|
|
4459
|
+
error: 'AUN channel is not connected',
|
|
4460
|
+
code: 'AUN_NOT_CONNECTED',
|
|
4461
|
+
};
|
|
4462
|
+
}
|
|
4140
4463
|
const isGroup = delivery.chatType === 'group';
|
|
4141
4464
|
const targetAid = channelId;
|
|
4142
4465
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
@@ -4165,27 +4488,54 @@ export class AUNChannel {
|
|
|
4165
4488
|
if (isGroup) {
|
|
4166
4489
|
params.group_id = delivery.groupId;
|
|
4167
4490
|
const result = await this.callAndTrace('group.send', params);
|
|
4168
|
-
const mid = result
|
|
4491
|
+
const mid = this.messageIdFromSendResult(result);
|
|
4492
|
+
if (!mid) {
|
|
4493
|
+
const failure = classifyAunSendFailure(result, 'group.send returned no message_id');
|
|
4494
|
+
logger.warn(`${this.logPrefix()} group.send (${payload.type}) returned no message_id; disposition=${failure.status}`);
|
|
4495
|
+
return { ok: false, ...failure };
|
|
4496
|
+
}
|
|
4169
4497
|
logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
|
|
4170
4498
|
if (!isMenuPayload)
|
|
4171
4499
|
this.forwardOutbound(result);
|
|
4172
|
-
return mid;
|
|
4500
|
+
return { ok: true, messageId: mid };
|
|
4173
4501
|
}
|
|
4174
4502
|
else {
|
|
4175
4503
|
params.to = targetAid;
|
|
4176
4504
|
const result = await this.callAndTrace('message.send', params);
|
|
4177
|
-
|
|
4505
|
+
const mid = this.messageIdFromSendResult(result);
|
|
4506
|
+
if (!mid) {
|
|
4507
|
+
const failure = classifyAunSendFailure(result, 'message.send returned no message_id');
|
|
4508
|
+
logger.warn(`${this.logPrefix()} message.send (${payload.type}) returned no message_id; disposition=${failure.status}`);
|
|
4509
|
+
return { ok: false, ...failure };
|
|
4510
|
+
}
|
|
4511
|
+
logger.info(`${this.logPrefix()} message.send (${payload.type}) ok: to=${this.peerLabel(targetAid)} mid=${mid} encrypt=${encrypt}`);
|
|
4178
4512
|
if (!isMenuPayload)
|
|
4179
4513
|
this.forwardOutbound(result);
|
|
4180
|
-
return
|
|
4514
|
+
return { ok: true, messageId: mid };
|
|
4181
4515
|
}
|
|
4182
4516
|
}
|
|
4183
4517
|
catch (e) {
|
|
4184
|
-
const
|
|
4185
|
-
logger.warn(`${this.logPrefix()} sendStructured failed (${payload.type}) to ${channelId}:
|
|
4186
|
-
return
|
|
4518
|
+
const failure = classifyAunSendFailure(e, `structured send failed to ${channelId}`);
|
|
4519
|
+
logger.warn(`${this.logPrefix()} sendStructured failed (${payload.type}) to ${channelId}: disposition=${failure.status} code=${failure.code ?? 'unknown'} error=${failure.error}`);
|
|
4520
|
+
return { ok: false, ...failure };
|
|
4187
4521
|
}
|
|
4188
4522
|
}
|
|
4523
|
+
/** Compatibility API for optional transient payloads. */
|
|
4524
|
+
async sendStructured(channelId, payload, context) {
|
|
4525
|
+
const result = await this.sendStructuredResult(channelId, payload, context);
|
|
4526
|
+
return result.ok ? result.messageId : null;
|
|
4527
|
+
}
|
|
4528
|
+
/** Strict API for callers that must not report a failed transport as sent. */
|
|
4529
|
+
async sendStructuredOrThrow(channelId, payload, context) {
|
|
4530
|
+
const result = await this.sendStructuredResult(channelId, payload, context);
|
|
4531
|
+
if (result.ok)
|
|
4532
|
+
return result.messageId;
|
|
4533
|
+
throw Object.assign(new Error(result.error), {
|
|
4534
|
+
name: 'AUNSendError',
|
|
4535
|
+
code: result.code ?? 'AUN_SEND_FAILED',
|
|
4536
|
+
retryable: result.status === 'retry',
|
|
4537
|
+
});
|
|
4538
|
+
}
|
|
4189
4539
|
async sendFile(channelId, filePath, context) {
|
|
4190
4540
|
const delivery = this.requireDelivery(channelId, context);
|
|
4191
4541
|
context = this.withDelivery(context, delivery);
|
|
@@ -4208,7 +4558,9 @@ export class AUNChannel {
|
|
|
4208
4558
|
channelId,
|
|
4209
4559
|
delivery,
|
|
4210
4560
|
type: 'file',
|
|
4561
|
+
contentKind: 'file',
|
|
4211
4562
|
filePath: absPath,
|
|
4563
|
+
logText: `📎 ${path.basename(absPath)} (${formatSize(stat.size)})`,
|
|
4212
4564
|
context,
|
|
4213
4565
|
});
|
|
4214
4566
|
logger.debug(`${this.logPrefix()} Outbox enqueued file: id=${entry.id} channel=${channelId} file=${absPath}`);
|
|
@@ -4219,9 +4571,12 @@ export class AUNChannel {
|
|
|
4219
4571
|
}
|
|
4220
4572
|
return;
|
|
4221
4573
|
}
|
|
4222
|
-
const
|
|
4223
|
-
if (
|
|
4224
|
-
|
|
4574
|
+
const result = await this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
4575
|
+
if (result.status === 'sent') {
|
|
4576
|
+
this.removeDeliveredOutboxEntry(entry);
|
|
4577
|
+
}
|
|
4578
|
+
else if (result.status === 'permanent') {
|
|
4579
|
+
this.markPermanentOutboxFailure(entry, result);
|
|
4225
4580
|
}
|
|
4226
4581
|
}
|
|
4227
4582
|
async sendImage(channelId, data, mimeType, alt, context) {
|
|
@@ -4263,9 +4618,11 @@ export class AUNChannel {
|
|
|
4263
4618
|
}
|
|
4264
4619
|
return;
|
|
4265
4620
|
}
|
|
4266
|
-
const
|
|
4267
|
-
if (
|
|
4268
|
-
|
|
4621
|
+
const result = await this.withOutboxInFlight(entry, () => this.deliverImageEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
4622
|
+
if (result.status === 'sent')
|
|
4623
|
+
this.removeDeliveredOutboxEntry(entry);
|
|
4624
|
+
else if (result.status === 'permanent')
|
|
4625
|
+
this.markPermanentOutboxFailure(entry, result);
|
|
4269
4626
|
}
|
|
4270
4627
|
async buildUploadedImagePayload(data, mimeType, alt) {
|
|
4271
4628
|
if (!this.connected || !this.client) {
|
|
@@ -4305,7 +4662,7 @@ export class AUNChannel {
|
|
|
4305
4662
|
const image = entry.image;
|
|
4306
4663
|
if (!image || typeof image.dataBase64 !== 'string') {
|
|
4307
4664
|
logger.warn(`${this.logPrefix()} deliverImageEntry: missing image data (outbox id=${entry.id})`);
|
|
4308
|
-
return
|
|
4665
|
+
return { status: 'permanent', error: 'durable image data is missing', code: 'MISSING_IMAGE_DATA' };
|
|
4309
4666
|
}
|
|
4310
4667
|
const delivery = this.requirePersistedDelivery(entry.channelId, entry.delivery);
|
|
4311
4668
|
entry.delivery = delivery;
|
|
@@ -4313,43 +4670,75 @@ export class AUNChannel {
|
|
|
4313
4670
|
const data = Buffer.from(image.dataBase64, 'base64');
|
|
4314
4671
|
if (data.length === 0) {
|
|
4315
4672
|
logger.warn(`${this.logPrefix()} deliverImageEntry: invalid image data (outbox id=${entry.id})`);
|
|
4316
|
-
return
|
|
4673
|
+
return { status: 'permanent', error: 'durable image data is invalid', code: 'INVALID_IMAGE_DATA' };
|
|
4317
4674
|
}
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4675
|
+
try {
|
|
4676
|
+
const imagePayload = await this.buildUploadedImagePayload(data, image.mimeType, image.alt);
|
|
4677
|
+
entry.type = 'payload';
|
|
4678
|
+
entry.contentKind = 'image';
|
|
4679
|
+
entry.payload = this.applyReplyContextToPayload(imagePayload, entry.context);
|
|
4680
|
+
entry.logText ??= image.alt ? `[image] ${image.alt}` : '[image]';
|
|
4681
|
+
delete entry.image;
|
|
4682
|
+
// Persist the attachment reference before message.send. A failed send then
|
|
4683
|
+
// retries the compact wire payload without re-uploading the same image.
|
|
4684
|
+
const submittedRoute = {
|
|
4685
|
+
id: entry.id,
|
|
4329
4686
|
channelId: entry.channelId,
|
|
4330
4687
|
delivery,
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4688
|
+
};
|
|
4689
|
+
const replaced = outbox.replaceIfRouteMatches(this.config.aid, submittedRoute, entry);
|
|
4690
|
+
if (replaced === 'route-changed') {
|
|
4691
|
+
logger.warn(`${this.logPrefix()} Image route changed during upload; preserving original outbox entry for the corrected route: id=${entry.id}`);
|
|
4692
|
+
return { status: 'retry', error: 'image delivery route changed during upload', code: 'OUTBOX_ROUTE_CHANGED' };
|
|
4693
|
+
}
|
|
4694
|
+
if (replaced === 'missing') {
|
|
4695
|
+
logger.warn(`${this.logPrefix()} Image outbox entry disappeared during upload; skipping send: id=${entry.id}`);
|
|
4696
|
+
return { status: 'permanent', error: 'image outbox entry disappeared during upload', code: 'OUTBOX_ENTRY_MISSING' };
|
|
4697
|
+
}
|
|
4698
|
+
const sent = await this.deliverPayloadEntry(entry);
|
|
4699
|
+
return sent.ok ? sentOutboxResult() : sent;
|
|
4338
4700
|
}
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4701
|
+
catch (error) {
|
|
4702
|
+
const failure = classifyAunSendFailure(error, 'image upload or send failed');
|
|
4703
|
+
logger.error(`${this.logPrefix()} Image delivery failed (outbox id=${entry.id}): ${failure.error}; disposition=${failure.status}`);
|
|
4704
|
+
return failure;
|
|
4342
4705
|
}
|
|
4343
|
-
return sent.ok;
|
|
4344
4706
|
}
|
|
4345
4707
|
async deliverFileEntry(entry) {
|
|
4346
4708
|
const channelId = entry.channelId;
|
|
4709
|
+
if (typeof entry.filePath !== 'string' || !entry.filePath.trim()) {
|
|
4710
|
+
logger.warn(`${this.logPrefix()} deliverFileEntry: missing file path (outbox id=${entry.id})`);
|
|
4711
|
+
return { status: 'permanent', error: 'durable file path is missing', code: 'MISSING_FILE_PATH' };
|
|
4712
|
+
}
|
|
4347
4713
|
const absPath = entry.filePath;
|
|
4348
4714
|
const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
|
|
4349
4715
|
const context = this.withDelivery(entry.context, delivery);
|
|
4716
|
+
const isGroup = delivery.chatType === 'group';
|
|
4717
|
+
const source = context?.metadata?.source ?? 'daemon';
|
|
4718
|
+
if (entry.deliveryReceipt) {
|
|
4719
|
+
const filename = path.basename(absPath);
|
|
4720
|
+
const logText = entry.logText ?? `[file] ${filename}`;
|
|
4721
|
+
try {
|
|
4722
|
+
this.appendOutboundJsonl(channelId, {
|
|
4723
|
+
...classifyAunPayloadForLog({ type: 'file', text: logText }),
|
|
4724
|
+
msgId: entry.deliveryReceipt.messageId,
|
|
4725
|
+
encrypt: entry.deliveryReceipt.encrypt,
|
|
4726
|
+
context,
|
|
4727
|
+
isGroup,
|
|
4728
|
+
source,
|
|
4729
|
+
transport: entry.deliveryReceipt.transport,
|
|
4730
|
+
});
|
|
4731
|
+
return sentOutboxResult();
|
|
4732
|
+
}
|
|
4733
|
+
catch (error) {
|
|
4734
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4735
|
+
logger.error(`${this.logPrefix()} Failed to recover local file post-send state from durable receipt: id=${entry.id} error=${detail}`);
|
|
4736
|
+
return { status: 'retry', error: detail, code: 'OUTBOX_POST_SEND_FAILED' };
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4350
4739
|
if (!fs.existsSync(absPath)) {
|
|
4351
4740
|
logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
|
|
4352
|
-
return
|
|
4741
|
+
return { status: 'permanent', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
|
|
4353
4742
|
}
|
|
4354
4743
|
const filename = path.basename(absPath);
|
|
4355
4744
|
const fileData = fs.readFileSync(absPath);
|
|
@@ -4404,7 +4793,6 @@ export class AUNChannel {
|
|
|
4404
4793
|
attachment,
|
|
4405
4794
|
context,
|
|
4406
4795
|
});
|
|
4407
|
-
const isGroup = delivery.chatType === 'group';
|
|
4408
4796
|
const fileTargetAid = channelId;
|
|
4409
4797
|
const encryptTarget = isGroup ? channelId : fileTargetAid;
|
|
4410
4798
|
const encrypt = context?.metadata?.encrypted != null
|
|
@@ -4419,12 +4807,12 @@ export class AUNChannel {
|
|
|
4419
4807
|
this.trace('OUT', 'group.send.file', params);
|
|
4420
4808
|
const result = await this.client.call('group.send', params);
|
|
4421
4809
|
sendResult = result;
|
|
4422
|
-
const fileMid = result
|
|
4810
|
+
const fileMid = this.messageIdFromSendResult(result);
|
|
4423
4811
|
sentMid = fileMid ?? null;
|
|
4424
4812
|
this.trace('OUT', 'group.send.file.ok', { message_id: fileMid });
|
|
4425
4813
|
if (!fileMid) {
|
|
4426
4814
|
logger.warn(`${this.logPrefix()} group.send.file returned no message_id: ${JSON.stringify(result)}`);
|
|
4427
|
-
return
|
|
4815
|
+
return classifyAunSendFailure(result, 'group.send.file returned no message_id');
|
|
4428
4816
|
}
|
|
4429
4817
|
}
|
|
4430
4818
|
else {
|
|
@@ -4432,11 +4820,11 @@ export class AUNChannel {
|
|
|
4432
4820
|
this.trace('OUT', 'message.send.file', params);
|
|
4433
4821
|
const result = await this.client.call('message.send', params);
|
|
4434
4822
|
sendResult = result;
|
|
4435
|
-
sentMid = result
|
|
4823
|
+
sentMid = this.messageIdFromSendResult(result);
|
|
4436
4824
|
this.trace('OUT', 'message.send.file.ok', { message_id: sentMid });
|
|
4437
|
-
if (!
|
|
4825
|
+
if (!sentMid) {
|
|
4438
4826
|
logger.warn(`${this.logPrefix()} message.send.file returned no message_id: ${JSON.stringify(result)}`);
|
|
4439
|
-
return
|
|
4827
|
+
return classifyAunSendFailure(result, 'message.send.file returned no message_id');
|
|
4440
4828
|
}
|
|
4441
4829
|
}
|
|
4442
4830
|
}
|
|
@@ -4453,23 +4841,23 @@ export class AUNChannel {
|
|
|
4453
4841
|
this.trace('OUT', 'group.send.file.fallback', params);
|
|
4454
4842
|
const result = await this.client.call('group.send', params);
|
|
4455
4843
|
sendResult = result;
|
|
4456
|
-
const fbMid = result
|
|
4844
|
+
const fbMid = this.messageIdFromSendResult(result);
|
|
4457
4845
|
sentMid = fbMid ?? null;
|
|
4458
4846
|
this.trace('OUT', 'group.send.file.fallback.ok', { message_id: fbMid });
|
|
4459
4847
|
if (!fbMid) {
|
|
4460
4848
|
logger.warn(`${this.logPrefix()} group.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
4461
|
-
return
|
|
4849
|
+
return classifyAunSendFailure(result, 'group.send.file plaintext fallback returned no message_id');
|
|
4462
4850
|
}
|
|
4463
4851
|
}
|
|
4464
4852
|
else {
|
|
4465
4853
|
this.trace('OUT', 'message.send.file.fallback', params);
|
|
4466
4854
|
const result = await this.client.call('message.send', params);
|
|
4467
4855
|
sendResult = result;
|
|
4468
|
-
sentMid = result
|
|
4856
|
+
sentMid = this.messageIdFromSendResult(result);
|
|
4469
4857
|
this.trace('OUT', 'message.send.file.fallback.ok', { message_id: sentMid });
|
|
4470
|
-
if (!
|
|
4858
|
+
if (!sentMid) {
|
|
4471
4859
|
logger.warn(`${this.logPrefix()} message.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
4472
|
-
return
|
|
4860
|
+
return classifyAunSendFailure(result, 'message.send.file plaintext fallback returned no message_id');
|
|
4473
4861
|
}
|
|
4474
4862
|
}
|
|
4475
4863
|
}
|
|
@@ -4477,26 +4865,40 @@ export class AUNChannel {
|
|
|
4477
4865
|
throw sendErr;
|
|
4478
4866
|
}
|
|
4479
4867
|
}
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
});
|
|
4868
|
+
if (!sentMid) {
|
|
4869
|
+
return { status: 'permanent', error: 'file send returned no message_id', code: 'MISSING_MESSAGE_ID' };
|
|
4870
|
+
}
|
|
4871
|
+
try {
|
|
4872
|
+
this.checkpointDurableDelivery(entry, sentMid, !!params.encrypt, sendResult);
|
|
4873
|
+
}
|
|
4874
|
+
catch (error) {
|
|
4875
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4876
|
+
logger.error(`${this.logPrefix()} AUN accepted the durable file but its receipt could not be persisted: id=${entry.id} mid=${sentMid} error=${detail}`);
|
|
4877
|
+
return { status: 'permanent', error: detail, code: 'OUTBOX_RECEIPT_CHECKPOINT_FAILED' };
|
|
4491
4878
|
}
|
|
4879
|
+
logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
|
|
4880
|
+
const fileText = filePayload.text;
|
|
4881
|
+
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 }) });
|
|
4882
|
+
const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
|
|
4883
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(fileText, 'utf-8'), fileText, false, !!params.encrypt, context?.metadata?.chatmode, 'send', statsContext);
|
|
4884
|
+
this.appendOutboundJsonl(channelId, {
|
|
4885
|
+
...classifyAunPayloadForLog(filePayload), msgId: sentMid, encrypt: !!params.encrypt,
|
|
4886
|
+
context, isGroup, source, transport: this.sendReceiptFromResult(sendResult),
|
|
4887
|
+
});
|
|
4492
4888
|
if (sendResult)
|
|
4493
4889
|
this.forwardOutbound(sendResult);
|
|
4494
|
-
return
|
|
4890
|
+
return sentOutboxResult();
|
|
4495
4891
|
}
|
|
4496
4892
|
catch (e) {
|
|
4893
|
+
if (entry.deliveryReceipt) {
|
|
4894
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
4895
|
+
logger.error(`${this.logPrefix()} AUN accepted the durable file but local post-send processing failed; retaining receipt for recovery: id=${entry.id} error=${detail}`);
|
|
4896
|
+
return { status: 'retry', error: detail, code: 'OUTBOX_POST_SEND_FAILED' };
|
|
4897
|
+
}
|
|
4497
4898
|
this.trace('OUT', 'sendFile.error', { channelId, filePath: absPath, error: String(e) });
|
|
4498
|
-
|
|
4499
|
-
|
|
4899
|
+
const failure = classifyAunSendFailure(e, `sendFile failed for ${channelId}`);
|
|
4900
|
+
logger.error(`${this.logPrefix()} sendFile failed for ${channelId} (outbox id=${entry.id}): ${failure.error}; disposition=${failure.status}`);
|
|
4901
|
+
return failure;
|
|
4500
4902
|
}
|
|
4501
4903
|
}
|
|
4502
4904
|
// ── Outbox drain ───────────────────────────────────────────
|
|
@@ -4519,58 +4921,80 @@ export class AUNChannel {
|
|
|
4519
4921
|
async drainOutbox() {
|
|
4520
4922
|
if (!this.connected || !this.client)
|
|
4521
4923
|
return;
|
|
4522
|
-
|
|
4924
|
+
this.repairBootstrapRoutes();
|
|
4523
4925
|
if (!outbox.hasPending(this.config.aid))
|
|
4524
4926
|
return;
|
|
4525
4927
|
logger.info(`${this.logPrefix()} Draining outbox...`);
|
|
4526
4928
|
const result = await outbox.drain(this.config.aid, async (entry) => {
|
|
4527
4929
|
if (!isDeliveryTarget(entry.delivery)) {
|
|
4528
4930
|
logger.warn(`${this.logPrefix()} Discarding legacy/malformed outbox entry without a trusted delivery route: id=${entry.id} channel=${entry.channelId}`);
|
|
4529
|
-
return
|
|
4931
|
+
return {
|
|
4932
|
+
status: 'permanent',
|
|
4933
|
+
error: 'outbox entry has no valid delivery route',
|
|
4934
|
+
code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
4935
|
+
};
|
|
4530
4936
|
}
|
|
4531
4937
|
if (entry.type === 'text') {
|
|
4532
|
-
return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry),
|
|
4938
|
+
return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
4533
4939
|
}
|
|
4534
4940
|
else if (entry.type === 'file') {
|
|
4535
|
-
return this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry),
|
|
4941
|
+
return this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
4536
4942
|
}
|
|
4537
4943
|
else if (entry.type === 'image') {
|
|
4538
|
-
return this.withOutboxInFlight(entry, () => this.deliverImageEntry(entry),
|
|
4944
|
+
return this.withOutboxInFlight(entry, () => this.deliverImageEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
4539
4945
|
}
|
|
4540
4946
|
else if (entry.type === 'payload') {
|
|
4541
|
-
const sent = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false });
|
|
4542
|
-
return sent.ok;
|
|
4947
|
+
const sent = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false, status: 'retry', error: 'outbox send is already in flight' });
|
|
4948
|
+
return sent.ok ? { status: 'sent' } : sent;
|
|
4543
4949
|
}
|
|
4544
|
-
return
|
|
4950
|
+
return { status: 'permanent', error: `unsupported outbox entry type: ${entry.type}`, code: 'UNSUPPORTED_OUTBOX_TYPE' };
|
|
4545
4951
|
});
|
|
4546
|
-
if (result.sent > 0 || result.expired > 0) {
|
|
4547
|
-
logger.info(`${this.logPrefix()} Outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed}`);
|
|
4952
|
+
if (result.sent > 0 || result.expired > 0 || result.permanent) {
|
|
4953
|
+
logger.info(`${this.logPrefix()} Outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed} permanent=${result.permanent ?? 0}`);
|
|
4548
4954
|
}
|
|
4549
4955
|
}
|
|
4550
|
-
/** Repair
|
|
4551
|
-
|
|
4956
|
+
/** Repair bootstrap outbox entries to the configured personal Owner route. */
|
|
4957
|
+
repairBootstrapRoutes() {
|
|
4958
|
+
const aid = this.config.aid.replace(/^@/, '');
|
|
4959
|
+
const owner = getFirstStaticAgentOwner(aid);
|
|
4960
|
+
if (!owner)
|
|
4961
|
+
return;
|
|
4552
4962
|
const operationIds = new Set([
|
|
4553
|
-
bootstrapInitialMessageOperationId(
|
|
4554
|
-
postBootstrapWelcomeOperationId(
|
|
4963
|
+
bootstrapInitialMessageOperationId(aid),
|
|
4964
|
+
postBootstrapWelcomeOperationId(aid),
|
|
4555
4965
|
]);
|
|
4556
|
-
const entries =
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
&& entry.delivery?.chatType === 'private');
|
|
4966
|
+
const entries = [...operationIds]
|
|
4967
|
+
.map(operationId => outbox.findByDedupeKey(aid, operationId, { includeTerminal: true }))
|
|
4968
|
+
.filter((entry) => !!entry?.channelId);
|
|
4560
4969
|
for (const entry of entries) {
|
|
4561
|
-
const
|
|
4562
|
-
|
|
4970
|
+
const delivery = { chatType: 'private' };
|
|
4971
|
+
const nestedDelivery = entry.context?.delivery;
|
|
4972
|
+
const routeAlreadyPrivate = entry.channelId === owner
|
|
4973
|
+
&& entry.delivery?.chatType === 'private'
|
|
4974
|
+
&& (nestedDelivery === undefined || nestedDelivery?.chatType === 'private');
|
|
4975
|
+
if (routeAlreadyPrivate)
|
|
4563
4976
|
continue;
|
|
4564
|
-
const
|
|
4565
|
-
if (!outbox.replace(this.config.aid, {
|
|
4977
|
+
const repaired = {
|
|
4566
4978
|
...entry,
|
|
4979
|
+
channelId: owner,
|
|
4567
4980
|
delivery,
|
|
4568
4981
|
context: entry.context
|
|
4569
4982
|
? { ...entry.context, delivery }
|
|
4570
4983
|
: entry.context,
|
|
4571
|
-
}
|
|
4984
|
+
};
|
|
4985
|
+
// A receipt is scoped to the original transport target. Never treat a
|
|
4986
|
+
// send accepted for a stale group/private address as proof that the
|
|
4987
|
+
// corrected Owner route was delivered.
|
|
4988
|
+
if (entry.channelId !== owner || entry.delivery?.chatType !== 'private') {
|
|
4989
|
+
delete repaired.deliveryReceipt;
|
|
4990
|
+
}
|
|
4991
|
+
delete repaired.terminal;
|
|
4992
|
+
delete repaired.lastError;
|
|
4993
|
+
delete repaired.lastErrorCode;
|
|
4994
|
+
delete repaired.attempts;
|
|
4995
|
+
if (outbox.replaceIfRouteMatches(aid, entry, repaired) !== 'replaced')
|
|
4572
4996
|
continue;
|
|
4573
|
-
logger.warn(`${this.logPrefix()}
|
|
4997
|
+
logger.warn(`${this.logPrefix()} Repaired bootstrap outbox route to private Owner delivery: entry=${entry.id} owner=${owner}`);
|
|
4574
4998
|
}
|
|
4575
4999
|
}
|
|
4576
5000
|
acknowledge(messageId) {
|
|
@@ -4673,7 +5097,7 @@ export class AUNChannel {
|
|
|
4673
5097
|
catch {
|
|
4674
5098
|
payloadObj = { text: payload };
|
|
4675
5099
|
}
|
|
4676
|
-
await this.
|
|
5100
|
+
await this.sendStructuredOrThrow(channelId, payloadObj, context);
|
|
4677
5101
|
}
|
|
4678
5102
|
async disconnect() {
|
|
4679
5103
|
this.intentionalDisconnect = true;
|
|
@@ -4839,18 +5263,6 @@ export class AUNChannel {
|
|
|
4839
5263
|
return undefined; // 不写缓存,下次仍可重试
|
|
4840
5264
|
}
|
|
4841
5265
|
}
|
|
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
5266
|
/** 统计关系键继续使用群 ID,仅为智能体预览补充可读群名。 */
|
|
4855
5267
|
async groupStatsContext(groupId) {
|
|
4856
5268
|
return {
|
|
@@ -4895,7 +5307,6 @@ export class AUNChannelPlugin {
|
|
|
4895
5307
|
const channel = new AUNChannel({
|
|
4896
5308
|
aid,
|
|
4897
5309
|
keystorePath: inst.keystorePath,
|
|
4898
|
-
gatewayUrl: inst.gatewayUrl,
|
|
4899
5310
|
defaultEncrypt: ctx.aunDefaultEncrypt,
|
|
4900
5311
|
accessToken: inst.accessToken,
|
|
4901
5312
|
flushDelay: inst.flushDelay,
|
|
@@ -5133,7 +5544,6 @@ export class AUNChannelPlugin {
|
|
|
5133
5544
|
uploadAgentMd: (content) => channel.uploadAgentMd(content),
|
|
5134
5545
|
downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
|
|
5135
5546
|
getGroupName: (groupId) => channel.getGroupName(groupId),
|
|
5136
|
-
isGroup: (groupId) => channel.isGroup(groupId),
|
|
5137
5547
|
getGroupMemberRole: (groupId, aid) => channel.getGroupMemberRole(groupId, aid),
|
|
5138
5548
|
_selfAid: () => channel.getStatus().aid,
|
|
5139
5549
|
_selfName: () => channel.getSelfName(),
|