evolcore 0.0.39 → 0.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +31 -20
  3. package/bin/ec-safe-output.js +14 -2
  4. package/dist/agents/baseagent-availability.js +1 -0
  5. package/dist/agents/claude-runner.js +32 -46
  6. package/dist/agents/codex-runner.js +51 -27
  7. package/dist/agents/ecagent-runner.js +18 -7
  8. package/dist/agents/gemini-runner.js +1 -2
  9. package/dist/agents/runner-types.js +28 -6
  10. package/dist/aun/aid/client.js +1 -1
  11. package/dist/aun/aid/control-aid.js +23 -98
  12. package/dist/aun/aid/ownership-operation.js +1 -3
  13. package/dist/aun/group/invite-service.js +6 -0
  14. package/dist/aun/msg/p2p.js +8 -12
  15. package/dist/channels/aun.js +66 -48
  16. package/dist/channels/dingtalk.js +8 -2
  17. package/dist/channels/feishu.js +6 -1
  18. package/dist/channels/wecom.js +7 -3
  19. package/dist/cli/agent-create.js +23 -4
  20. package/dist/cli/aid-bind-command.js +4 -1
  21. package/dist/cli/daemon-commands.js +242 -17
  22. package/dist/cli/index.js +25 -13
  23. package/dist/cli/memory-command.js +33 -14
  24. package/dist/cli/restart-monitor.js +35 -13
  25. package/dist/cli/task-context.js +1 -0
  26. package/dist/config/config-field-policy.js +1 -0
  27. package/dist/contracts/ecweb-service-protocol.js +44 -0
  28. package/dist/core/agent-runtime.js +6 -0
  29. package/dist/core/auth/agent-delegation.js +7 -4
  30. package/dist/core/auth/auth-gateway.js +3 -1
  31. package/dist/core/auth/authorization-audit.js +5 -1
  32. package/dist/core/auth/debug-context.js +5 -1
  33. package/dist/core/auth/operation-catalog.js +16 -0
  34. package/dist/core/command/command-handler.js +7 -1
  35. package/dist/core/command/menu-catalog.js +48 -12
  36. package/dist/core/command/menu-handler.js +72 -4
  37. package/dist/core/command/menu-protocol.js +20 -1
  38. package/dist/core/command/slash-gate.js +17 -3
  39. package/dist/core/command/slash-handler.js +90 -7
  40. package/dist/core/event-catalog.js +2 -0
  41. package/dist/core/interaction-router.js +2 -1
  42. package/dist/core/ipc-registration.js +26 -7
  43. package/dist/core/memory/batch-meta.js +2 -0
  44. package/dist/core/memory/consolidation-context.js +42 -51
  45. package/dist/core/memory/consolidation-ops-records.js +39 -7
  46. package/dist/core/memory/consolidation-trigger.js +71 -64
  47. package/dist/core/memory/consolidation.js +226 -20
  48. package/dist/core/memory/memory-writer.js +160 -34
  49. package/dist/core/memory/relation-idle-monitor.js +287 -0
  50. package/dist/core/memory/relation-sweep.js +206 -0
  51. package/dist/core/message/aun-ipc-sender.js +5 -0
  52. package/dist/core/message/message-bridge.js +64 -55
  53. package/dist/core/message/message-log.js +17 -0
  54. package/dist/core/message/message-queue.js +6 -0
  55. package/dist/core/message/outbound.js +70 -0
  56. package/dist/core/message/peer-mode.js +7 -2
  57. package/dist/core/message/response-engine.js +341 -73
  58. package/dist/core/ownership/owner-resolver.js +384 -178
  59. package/dist/core/permission/ec-command-parser.js +19 -3
  60. package/dist/core/permission/tool-policy.js +7 -7
  61. package/dist/core/role/runtime-policy.js +64 -0
  62. package/dist/core/session/session-manager.js +25 -5
  63. package/dist/core/system-session/memory.js +3 -18
  64. package/dist/ecweb/basic-settings-service.js +709 -0
  65. package/dist/ecweb/providers-service.js +172 -0
  66. package/dist/index.js +92 -4
  67. package/dist/ipc.js +78 -11
  68. package/dist/stats/db.js +33 -0
  69. package/dist/stats/index.js +2 -2
  70. package/dist/stats/query.js +246 -0
  71. package/dist/stats/writer.js +41 -1
  72. package/dist/trigger/application-service.js +12 -0
  73. package/dist/trigger/authorization.js +2 -2
  74. package/dist/trigger/history.js +36 -0
  75. package/dist/trigger/scheduler.js +3 -0
  76. package/dist/utils/ecweb-supervisor.js +31 -10
  77. package/dist/utils/error-utils.js +8 -4
  78. package/dist/utils/evolcore-version.js +3 -2
  79. package/dist/utils/host-info.js +52 -0
  80. package/ecagent/dist/harness/agent-harness.js +4 -1
  81. package/kits/docs/evolcore/INDEX.md +1 -1
  82. package/kits/docs/evolcore/agent.md +1 -1
  83. package/kits/docs/evolcore/aid.md +29 -12
  84. package/kits/docs/evolcore/config.md +9 -7
  85. package/kits/docs/evolcore/memory-architecture.md +3 -4
  86. package/kits/docs/evolcore/memory-consolidation.md +28 -7
  87. package/kits/docs/evolcore/memory-tags.md +1 -1
  88. package/kits/docs/evolcore/memory.md +1 -1
  89. package/kits/docs/evolcore/menu-client.md +1 -1
  90. package/kits/docs/evolcore/menu-protocol.md +6 -0
  91. package/kits/docs/evolcore/msg.md +1 -1
  92. package/kits/eck_manifest.json +3 -3
  93. package/kits/rules/04-relation.md +3 -2
  94. package/kits/schemas/agent-config.schema.13.json +5 -1
  95. package/kits/templates/memory-consolidation/generalize.md +32 -4
  96. package/kits/templates/memory-consolidation/notes.md +2 -2
  97. package/kits/templates/memory-consolidation/promote.md +10 -2
  98. package/kits/templates/system-fragments/baseagent.md +5 -0
  99. package/kits/templates/system-fragments/memory.md +12 -3
  100. package/kits/templates/system-fragments/session.md +1 -1
  101. package/kits/{rules/07-memory-and-thread.md → templates/system-fragments/thread.md} +17 -80
  102. package/package.json +3 -3
@@ -760,8 +760,7 @@ export class GeminiRunner {
760
760
  if (context?.approvalInteractionPolicy !== 'deny')
761
761
  return;
762
762
  if (isGeminiUnattendedInteraction(toolName)) {
763
- logger.info(`[GeminiRunner] interaction.auto_resolved kind=${normalizeGeminiToolName(toolName)} session=${sessionId}`);
764
- return;
763
+ throw new Error('无人值守任务不能自动回答用户问题');
765
764
  }
766
765
  const lifecycleRequestId = requestId ?? `gemini-approval-${crypto.randomUUID()}`;
767
766
  const auditSummary = `${toolName} runtime authorization denied by Gemini headless policy`;
@@ -34,7 +34,23 @@ export function parseAsyncUserQuestions(value) {
34
34
  || question.options.length > 20 || question.options.some((option) => typeof option !== 'string' || !option.trim() || option.length > 4_096))) {
35
35
  throw new Error('Invalid asynchronous question options');
36
36
  }
37
- return { title: question.title, ...(question.options ? { options: [...question.options] } : {}) };
37
+ if (question.blocking != null && typeof question.blocking !== 'boolean') {
38
+ throw new Error('Invalid asynchronous question blocking flag');
39
+ }
40
+ if (question.recommendedOption != null
41
+ && (typeof question.recommendedOption !== 'string' || !question.recommendedOption.trim())) {
42
+ throw new Error('Invalid asynchronous question recommended option');
43
+ }
44
+ if (question.recommendedOption != null && question.options
45
+ && !question.options.includes(question.recommendedOption)) {
46
+ throw new Error('Asynchronous question recommended option is not in options');
47
+ }
48
+ return {
49
+ title: question.title,
50
+ ...(question.options ? { options: [...question.options] } : {}),
51
+ blocking: question.blocking !== false,
52
+ ...(question.recommendedOption ? { recommendedOption: question.recommendedOption } : {}),
53
+ };
38
54
  });
39
55
  }
40
56
  export function hasTrustedFullAccessContext(context) {
@@ -44,10 +60,13 @@ export function hasTrustedFullAccessContext(context) {
44
60
  || authorization.processRole !== 'fullaccess-run'
45
61
  || authorization.dataScope !== 'daemon')
46
62
  return false;
47
- if (authorization.source === 'fullaccess-command' || authorization.source === 'agent-policy') {
63
+ if (authorization.source === 'fullaccess-command' || authorization.source === 'agent-policy' || authorization.source === 'session-policy') {
48
64
  return typeof authorization.authorizedBy === 'string' && authorization.authorizedBy.length > 0
49
- && (authorization.source !== 'agent-policy'
50
- || (typeof authorization.agentAid === 'string' && authorization.agentAid.length > 0));
65
+ && (authorization.source === 'fullaccess-command'
66
+ || (typeof authorization.agentAid === 'string'
67
+ && authorization.agentAid.length > 0
68
+ && (authorization.source !== 'session-policy'
69
+ || (typeof authorization.sessionId === 'string' && authorization.sessionId.length > 0))));
51
70
  }
52
71
  return authorization.source === 'trigger'
53
72
  && typeof authorization.authorizedBy === 'string' && authorization.authorizedBy.length > 0
@@ -75,10 +94,13 @@ export async function withInterruptDeadline(operation, label, timeoutMs = 5_000)
75
94
  clearTimeout(timer);
76
95
  }
77
96
  }
78
- export function unsettledInterruptError(runner) {
79
- return Object.assign(new Error(`${runner} 的上一次执行尚未确认停止,本次任务未启动;旧执行结束后可在原会话继续。`), {
97
+ export function unsettledInterruptError(runner, cause) {
98
+ const error = Object.assign(new Error(`${runner} 的上一次执行尚未确认停止,本次任务未启动;旧执行结束后可在原会话继续。`), {
80
99
  code: 'RUNNER_INTERRUPT_UNCONFIRMED',
81
100
  });
101
+ if (cause !== undefined)
102
+ error.cause = cause;
103
+ return error;
82
104
  }
83
105
  // ── 类型守卫 ──
84
106
  export function hasModelSwitcher(agent) {
@@ -39,7 +39,7 @@ export function suppressSdkLogs() {
39
39
  };
40
40
  }
41
41
  // ==================== SDK package ====================
42
- export const MIN_AUN_CORE_SDK = [0, 5, 23];
42
+ export const MIN_AUN_CORE_SDK = [0, 5, 25];
43
43
  export const AUN_CORE_SDK_PKG = '@agentunion/fastaun';
44
44
  function compareVersion(a, min) {
45
45
  const parts = a.split('.').map(n => parseInt(n, 10));
@@ -8,7 +8,6 @@ import { getAidStore, loadClient, SLOT } from './store.js';
8
8
  import { LocalTokenStore, validateAgentMdDocument, verifyAgentMdUpload, } from '@agentunion/fastaun';
9
9
  import { AUN_KEYSTORE_ENCRYPTION_SEED } from './encryption-seed-policy.js';
10
10
  import { isValidAid } from './validation.js';
11
- import { OwnerResolver } from '../../core/ownership/owner-resolver.js';
12
11
  import { agentMdPath, resolvePaths } from '../../paths.js';
13
12
  import { PeerIdentityCache } from '../../core/relation/peer-identity.js';
14
13
  import { agentmdPut, buildInitialAgentMd, invalidateAgentDisplayName, stripAgentMdSignature } from './agentmd.js';
@@ -331,7 +330,6 @@ export async function submitControlAidRegistration(aid, owner, options = {}) {
331
330
  let connecting = false;
332
331
  let sentOperationIds = new Set();
333
332
  let invitationObserver;
334
- const resolver = new OwnerResolver();
335
333
  async function bounded(operation, timeoutMs = deadline - Date.now()) {
336
334
  let timer;
337
335
  try {
@@ -346,27 +344,6 @@ export async function submitControlAidRegistration(aid, owner, options = {}) {
346
344
  clearTimeout(timer);
347
345
  }
348
346
  }
349
- function readTask() {
350
- const raw = records.loadOwnershipTask(aid, operationId)?.task;
351
- if (!raw)
352
- return;
353
- if (raw.operation_id !== operationId || raw.operation !== 'bind' || raw.sub_aid !== aid
354
- || raw.owner_aid !== ownerAid || raw.initiator_aid !== aid || raw.new_owner_aid !== undefined
355
- || !['pending', 'completed', 'rejected', 'stopped', 'error'].includes(String(raw.status))
356
- || typeof raw.step !== 'string' || !Number.isSafeInteger(raw.base_relation_version)
357
- || raw.target_relation_version !== Number(raw.base_relation_version) + 1) {
358
- throw Object.assign(new Error('SDK registration task does not match its original intent'), { code: 'TASK_MISMATCH' });
359
- }
360
- task = {
361
- operation_id: operationId, operation: 'bind', sub_aid: aid, owner_aid: ownerAid,
362
- base_relation_version: raw.base_relation_version,
363
- target_relation_version: raw.target_relation_version,
364
- status: raw.status, step: raw.step,
365
- ...(typeof raw.code === 'string' ? { code: raw.code } : {}),
366
- ...(typeof raw.reason === 'string' ? { reason: raw.reason } : {}),
367
- ...(typeof raw.retryable === 'boolean' ? { retryable: raw.retryable } : {}),
368
- };
369
- }
370
347
  function result(status, error) {
371
348
  return { status, ...(operationId ? { operationId } : {}), ...(task ? { task } : {}), ...(error ? { error } : {}) };
372
349
  }
@@ -374,10 +351,8 @@ export async function submitControlAidRegistration(aid, owner, options = {}) {
374
351
  records = new LocalTokenStore(resolvePaths().root, { encryptionSeed: AUN_KEYSTORE_ENCRYPTION_SEED });
375
352
  const intent = records.loadMetadata(aid)?.registration_intent;
376
353
  const registrationIntentOwner = typeof intent?.owner === 'string' ? intent.owner.toLowerCase() : undefined;
377
- // Preserve an existing SDK registration task and let the same SDK client
378
- // resume it during connect before OwnerResolver checks/binds the subject.
379
354
  if (intent && Object.prototype.hasOwnProperty.call(intent, 'owner')) {
380
- if (!registrationIntentOwner) {
355
+ if (!registrationIntentOwner || !isValidAid(registrationIntentOwner)) {
381
356
  return result('unknown', {
382
357
  code: 'REGISTRATION_INTENT_PRESENT',
383
358
  message: 'Control AID has an invalid SDK registration Owner intent',
@@ -386,93 +361,44 @@ export async function submitControlAidRegistration(aid, owner, options = {}) {
386
361
  if (registrationIntentOwner !== ownerAid) {
387
362
  return result('failed', { code: 'OWNER_INTENT_CONFLICT', message: 'Control AID was registered for a different Owner' });
388
363
  }
389
- if (typeof intent.operation_id === 'string' && REGISTRATION_OPERATION_ID.test(intent.operation_id)) {
390
- operationId = intent.operation_id;
391
- readTask();
392
- }
393
- if (!operationId) {
394
- return result('unknown', {
395
- code: 'REGISTRATION_INTENT_PRESENT',
396
- message: 'Control AID has an SDK registration intent without a valid operation ID',
397
- });
398
- }
399
364
  }
400
365
  if (options.agentMdError) {
401
366
  return result('failed', { code: 'CONTROL_AGENTMD_NOT_READY', message: options.agentMdError });
402
367
  }
403
- store = await bounded(getAidStore({ slotId: SLOT.cli }));
404
- const ownershipTasks = records.listOwnershipTasks(aid).map(snapshot => snapshot.task);
405
- const conflictingTask = ownershipTasks.find(candidate => candidate.sub_aid === aid
406
- && (candidate.status === 'pending' || candidate.status === 'error' && candidate.retryable === true)
407
- && !(candidate.operation === 'bind' && candidate.owner_aid === ownerAid
408
- && candidate.initiator_aid === aid && candidate.new_owner_aid === undefined));
409
- if (conflictingTask) {
410
- return result('failed', { code: 'BIND_CONFLICT', message: 'A different active ownership task must be resolved first' });
411
- }
368
+ store = await bounded(getAidStore({ slotId: `ownership-${crypto.randomUUID()}` }));
412
369
  client = await bounded(loadClient(store, aid));
413
370
  connecting = true;
414
371
  sentOperationIds = new Set();
415
372
  invitationObserver = observeInvitationSend(client, ownerAid, sentOperationIds);
416
373
  await bounded(client.connect({
417
- connection_kind: 'long', auto_reconnect: false, background_sync: false,
374
+ connection_kind: 'short', short_ttl_ms: 30_000, auto_reconnect: false, background_sync: false,
418
375
  connect_timeout: 10, call_timeout: 10,
419
376
  }));
420
- if (registrationIntentOwner === ownerAid && operationId) {
421
- readTask();
422
- if ((!task || task.status === 'pending') && !sentOperationIds.has(operationId)) {
423
- await bounded(invitationObserver.waitFor(operationId));
424
- readTask();
377
+ task = await bounded(client.bindOwneraid(ownerAid));
378
+ if (task.operation !== 'bind' || task.sub_aid !== aid || task.owner_aid !== ownerAid
379
+ || task.new_owner_aid !== undefined || !REGISTRATION_OPERATION_ID.test(task.operation_id)
380
+ || !Number.isSafeInteger(task.base_relation_version)
381
+ || task.target_relation_version !== task.base_relation_version + 1) {
382
+ throw Object.assign(new Error('SDK returned a binding result for a different subject or Owner'), { code: 'TASK_MISMATCH' });
383
+ }
384
+ operationId = task.operation_id;
385
+ if (task.status === 'completed') {
386
+ let consensus;
387
+ try {
388
+ consensus = await bounded(client.checkOwnershipConsensus(aid));
425
389
  }
426
- if (!task) {
390
+ catch (error) {
427
391
  return result('unknown', {
428
- code: 'BIND_TASK_NOT_PERSISTED',
429
- message: 'SDK did not expose the recovered registration task after connect',
430
- });
431
- }
432
- if (task?.status === 'completed') {
433
- const consensus = await bounded(client.checkOwnershipConsensus(aid));
434
- if (consensus.status === 'consensus' && consensus.sub_aid === aid && consensus.owner_aid === ownerAid) {
435
- return result('already_bound');
436
- }
437
- return result('failed', {
438
- code: 'CONSENSUS_REQUIRED',
439
- message: 'A completed binding task has no current verified Owner consensus',
392
+ code: 'BIND_SUBMIT_UNKNOWN',
393
+ message: error instanceof Error ? error.message : String(error),
440
394
  });
441
395
  }
442
- if (task && (['rejected', 'stopped', 'error'].includes(task.status) || task.code)) {
443
- return result('failed', { code: task.code ?? 'BIND_SUBMIT_FAILED', message: task.reason ?? `Binding task ${task.status}` });
444
- }
445
- if (sentOperationIds.has(operationId))
446
- return result('submitted');
447
- return result('unknown', {
448
- code: 'BIND_SUBMIT_UNKNOWN',
449
- message: 'SDK is recovering the original registration task; no matching message.send acknowledgement was observed',
450
- });
451
- }
452
- // Reuse the same service as agent-create, including its fresh consensus
453
- // check, explicit SDK bind call, and persistent task projection.
454
- resolver.attachClient(aid, client, true);
455
- const view = await bounded(resolver.bind(aid, ownerAid));
456
- if (view.consensusStatus === 'consensus' && view.ownerAid === ownerAid) {
457
- return result('already_bound');
458
- }
459
- const returnedTask = view.task;
460
- if (!returnedTask || returnedTask.operation !== 'bind' || returnedTask.sub_aid !== aid
461
- || returnedTask.owner_aid !== ownerAid || returnedTask.new_owner_aid !== undefined
462
- || typeof returnedTask.operation_id !== 'string' || !REGISTRATION_OPERATION_ID.test(returnedTask.operation_id)) {
463
- throw Object.assign(new Error('SDK returned a binding task for a different subject or Owner'), { code: 'TASK_MISMATCH' });
464
- }
465
- operationId = returnedTask.operation_id;
466
- readTask();
467
- if (!task) {
468
- throw Object.assign(new Error('SDK binding task was not persisted'), { code: 'BIND_TASK_NOT_PERSISTED' });
469
- }
470
- if (task.status === 'completed') {
471
- const consensus = await bounded(client.checkOwnershipConsensus(aid));
472
396
  if (consensus.status === 'consensus' && consensus.sub_aid === aid && consensus.owner_aid === ownerAid) {
473
397
  return result('already_bound');
474
398
  }
475
- return result('failed', { code: 'CONSENSUS_REQUIRED', message: 'Completed binding has no current verified Owner consensus' });
399
+ if (sentOperationIds.has(operationId))
400
+ return result('submitted');
401
+ return result('unknown', { code: 'BIND_SUBMIT_UNKNOWN', message: 'SDK completed the local bind without a message.send acknowledgement' });
476
402
  }
477
403
  if (['rejected', 'stopped', 'error'].includes(task.status)) {
478
404
  return result('failed', { code: task.code ?? 'BIND_SUBMIT_FAILED', message: task.reason ?? `Binding task ${task.status}` });
@@ -489,15 +415,14 @@ export async function submitControlAidRegistration(aid, owner, options = {}) {
489
415
  const details = error;
490
416
  const code = typeof details.stringCode === 'string' ? details.stringCode
491
417
  : typeof details.code === 'string' ? details.code : undefined;
492
- const uncertain = connecting && (code === 'BIND_SUBMIT_TIMEOUT' || !code
493
- || resolver.snapshot(aid).requestStatus === 'interrupted');
418
+ operationId ??= sentOperationIds.values().next().value;
419
+ const uncertain = connecting && (code === 'BIND_SUBMIT_TIMEOUT' || !code || !!operationId);
494
420
  return result(uncertain ? 'unknown' : 'failed', {
495
421
  code: uncertain ? 'BIND_SUBMIT_UNKNOWN' : code ?? 'BIND_SUBMIT_FAILED',
496
422
  message: details.message ?? String(error),
497
423
  });
498
424
  }
499
425
  finally {
500
- resolver.stop();
501
426
  invitationObserver?.restore();
502
427
  if (client) {
503
428
  try {
@@ -58,15 +58,13 @@ export async function executeOwnershipCommand(command, defaultSubjectAid) {
58
58
  if (!subjectAid)
59
59
  throw new OwnershipError('SUBJECT_REQUIRED', 'No local subject AID is available');
60
60
  if (command.action === 'status') {
61
- ownerResolver.assertLocalSubject(subjectAid);
62
- return ownerResolver.refresh(subjectAid);
61
+ return ownerResolver.status(subjectAid);
63
62
  }
64
63
  if (command.action === 'accept' || command.action === 'reject') {
65
64
  return ownerResolver.decide(subjectAid, command.operationId, command.action, command.action === 'reject' ? command.reason : undefined);
66
65
  }
67
66
  logger.info(`[ownership] bind requested subject=${subjectAid} owner=${command.ownerAid}`);
68
67
  try {
69
- ownerResolver.assertLocalSubject(subjectAid);
70
68
  const result = await ownerResolver.bind(subjectAid, command.ownerAid);
71
69
  const task = result.task;
72
70
  const log = task && ['rejected', 'stopped', 'error'].includes(task.status) ? logger.warn : logger.info;
@@ -372,6 +372,12 @@ export class GroupInviteReviewService {
372
372
  if (!runtime)
373
373
  return Promise.reject(new Error('邀请审核服务未就绪'));
374
374
  const task = this.serial(selfAid, async () => {
375
+ if (!this.active(runtime)) {
376
+ runtime.ready = false;
377
+ runtime.failures = 0;
378
+ runtime.nextSync = Date.now() + 15 * 60_000;
379
+ return;
380
+ }
375
381
  try {
376
382
  await this.sync(runtime);
377
383
  runtime.failures = 0;
@@ -10,6 +10,7 @@ import { resolvePaths } from '../../paths.js';
10
10
  import { ipcQuery } from '../../ipc.js';
11
11
  import { readDelegationCredential } from '../../core/auth/agent-delegation.js';
12
12
  import { loadDaemonConfig } from '../../config-store.js';
13
+ import { formatPeerKey } from '../../core/relation/peer-identity.js';
13
14
  import { normalizeAunMentionEntries, withAunAttention } from './mention-schema.js';
14
15
  import { isTopicThreadId } from '../../core/thread/topic-id.js';
15
16
  const DAEMON_SEND_TIMEOUT_GRACE_MS = 10_000;
@@ -60,9 +61,6 @@ function buildSimplePayload(body) {
60
61
  }
61
62
  }
62
63
  }
63
- function isRawPayloadBody(body) {
64
- return body.mode === 'payload';
65
- }
66
64
  function messageLogContent(body) {
67
65
  return body.mode === 'text' ? body.text
68
66
  : body.mode === 'link' ? `[link] ${body.url}`
@@ -134,7 +132,7 @@ async function tryDaemonMsgSend(args, payload, runtimeContext, file) {
134
132
  if (args.slotId || args.aunPath) {
135
133
  return { ok: false, code: 'INVALID_DELEGATION', error: 'Task sends cannot override the daemon AUN connection' };
136
134
  }
137
- const thread = args.body.mode === 'payload' ? undefined : msgSendThreadId(args, payload, runtimeContext);
135
+ const thread = msgSendThreadId(args, payload, runtimeContext);
138
136
  const response = await ipcQuery(resolvePaths().socket, {
139
137
  type: 'aun-msg-send',
140
138
  aid: args.from,
@@ -254,8 +252,7 @@ export async function msgSend(args) {
254
252
  if (args.body.mode !== 'payload' && Object.prototype.hasOwnProperty.call(payload, 'mentions')) {
255
253
  payload.mentions = normalizeAunMentionEntries(payload.mentions);
256
254
  }
257
- if (!isRawPayloadBody(args.body))
258
- applyRoutingPayloadFields(payload, args, runtimeContext, false);
255
+ applyRoutingPayloadFields(payload, args, runtimeContext, false);
259
256
  const daemonResult = await tryDaemonMsgSend(args, payload, runtimeContext);
260
257
  if (daemonResult)
261
258
  return daemonMsgSendResult(args, payload, runtimeContext, daemonResult);
@@ -327,7 +324,10 @@ export async function msgSend(args) {
327
324
  }).effectiveRole;
328
325
  }
329
326
  catch { /* 角色解析失败时按未知处理(proactive) */ }
330
- const chatmode = chatmodeForPeerIdentity(peerIdentity, peerRole);
327
+ const chatmode = chatmodeForPeerIdentity(peerIdentity, peerRole, {
328
+ self: args.from,
329
+ peerKey: formatPeerKey('aun', args.to),
330
+ });
331
331
  // 3. 构建 payload
332
332
  if (!payload) {
333
333
  payload = buildSimplePayload(args.body);
@@ -336,11 +336,7 @@ export async function msgSend(args) {
336
336
  if (args.body.mode !== 'payload')
337
337
  payload.chatmode = chatmode;
338
338
  // 5. 写入 payload.thread_id/ref_message_id(如果指定)
339
- if (args.body.mode !== 'payload') {
340
- if (!isRawPayloadBody(args.body))
341
- applyRoutingPayloadFields(payload, args, runtimeContext, false);
342
- applyRoutingPayloadFields(payload, args, runtimeContext);
343
- }
339
+ applyRoutingPayloadFields(payload, args, runtimeContext, args.body.mode !== 'payload');
344
340
  if (args.body.mode !== 'payload' && Object.prototype.hasOwnProperty.call(payload, 'mentions')) {
345
341
  payload.mentions = normalizeAunMentionEntries(payload.mentions);
346
342
  }
@@ -568,23 +568,18 @@ function setIfDefined(target, key, value) {
568
568
  if (value !== undefined)
569
569
  target[key] = value;
570
570
  }
571
- function truncateRawErrorForDisplay(raw) {
571
+ function rawErrorText(raw) {
572
572
  if (typeof raw !== 'string' || !raw.trim())
573
573
  return undefined;
574
574
  const text = raw.trim();
575
575
  const htmlStart = text.toLowerCase().indexOf('<html');
576
576
  return htmlStart >= 0 ? text.slice(0, htmlStart).trimEnd() : text;
577
577
  }
578
- function projectedRawError(raw, displayRaw) {
579
- if (typeof raw !== 'string')
580
- return raw;
581
- return raw.trim() ? displayRaw : raw;
582
- }
583
578
  function appendRawErrorText(text, raw) {
584
- const displayRaw = truncateRawErrorForDisplay(raw);
585
- if (!displayRaw || text.includes(displayRaw))
579
+ const rawText = rawErrorText(raw);
580
+ if (!rawText || text.includes(rawText))
586
581
  return text;
587
- return `${text}\n原因:${displayRaw}`;
582
+ return `${text}\n原因:${rawText}`;
588
583
  }
589
584
  /**
590
585
  * 把 AUNChannel 投递的 opts 映射成渠道无关的 InboundMessage。
@@ -4045,6 +4040,9 @@ export class AUNChannel {
4045
4040
  finalPayload.task_id = context.metadata.taskId;
4046
4041
  if (context?.metadata?.chatmode && !finalPayload.chatmode)
4047
4042
  finalPayload.chatmode = context.metadata.chatmode;
4043
+ // 结构化/通知类 payload 与文本路径共用同一份诊断字典;缺省不带,sentAt 由发送时刻盖戳。
4044
+ if (context?.debug)
4045
+ finalPayload.debug = { ...context.debug, sentAt: localTimestamp() };
4048
4046
  return finalPayload;
4049
4047
  }
4050
4048
  /**
@@ -4299,15 +4297,17 @@ export class AUNChannel {
4299
4297
  const now = Date.now();
4300
4298
  const mapTtl = typeof action.expiresAt === 'number' && Number.isFinite(action.expiresAt)
4301
4299
  ? Math.max(0, action.expiresAt - now)
4302
- : AUN_INTERACTION_CARD_TTL_MS;
4303
- const mapTimer = setTimeout(() => {
4304
- this.cardMessageIdMap.delete(messageId);
4305
- const ids = this.interactionCardMessageIds.get(action.requestId);
4306
- ids?.delete(messageId);
4307
- if (ids && ids.size === 0)
4308
- this.interactionCardMessageIds.delete(action.requestId);
4309
- }, mapTtl);
4310
- mapTimer.unref?.();
4300
+ : undefined;
4301
+ if (mapTtl !== undefined) {
4302
+ const mapTimer = setTimeout(() => {
4303
+ this.cardMessageIdMap.delete(messageId);
4304
+ const ids = this.interactionCardMessageIds.get(action.requestId);
4305
+ ids?.delete(messageId);
4306
+ if (ids && ids.size === 0)
4307
+ this.interactionCardMessageIds.delete(action.requestId);
4308
+ }, mapTtl);
4309
+ mapTimer.unref?.();
4310
+ }
4311
4311
  const ownedTimer = setTimeout(() => {
4312
4312
  this.ownedCardMsgIds.delete(messageId);
4313
4313
  this.ownedCardTopics.delete(messageId);
@@ -4492,6 +4492,7 @@ export class AUNChannel {
4492
4492
  channelId,
4493
4493
  delivery,
4494
4494
  dedupeKey: opts.dedupeKey,
4495
+ critical: opts.critical,
4495
4496
  type: 'payload',
4496
4497
  contentKind: opts.contentKind,
4497
4498
  payload: finalPayload,
@@ -4865,7 +4866,10 @@ export class AUNChannel {
4865
4866
  }).effectiveRole;
4866
4867
  }
4867
4868
  catch { /* 角色解析失败按未知处理 */ }
4868
- chatmode = chatmodeForPeerIdentity({ isMachine: info.type !== 'human', type: info.type ?? undefined }, peerRole);
4869
+ chatmode = chatmodeForPeerIdentity({ isMachine: info.type !== 'human', type: info.type ?? undefined }, peerRole, {
4870
+ self: this.config.aid,
4871
+ peerKey: formatPeerKey('aun', args.to),
4872
+ });
4869
4873
  }
4870
4874
  if (chatmode && !finalPayload.chatmode)
4871
4875
  finalPayload.chatmode = chatmode;
@@ -6604,26 +6608,36 @@ export class AUNChannelPlugin {
6604
6608
  }
6605
6609
  const taskBase = () => channel.buildTaskPayloadBase(envelope, replyCtx);
6606
6610
  switch (payload.kind) {
6607
- case 'result.text':
6608
- case 'command.result':
6609
6611
  case 'command.error': {
6612
+ const raw = rawErrorText(payload.raw) ?? payload.text;
6613
+ const errorPayload = {
6614
+ type: 'error',
6615
+ ...taskBase(),
6616
+ subtype: payload.kind,
6617
+ text: appendRawErrorText(payload.text, raw),
6618
+ message: payload.text,
6619
+ user_message: payload.text,
6620
+ raw,
6621
+ recoverable: true,
6622
+ };
6623
+ for (const key of ['format', 'reason', 'reasonCode', 'nextStep']) {
6624
+ setIfDefined(errorPayload, key, payload[key]);
6625
+ }
6626
+ await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6627
+ return;
6628
+ }
6629
+ case 'result.text':
6630
+ case 'command.result': {
6610
6631
  // Group command feedback is control traffic, not model input for
6611
6632
  // the other agents in the conversation.
6612
- if (delivery.chatType === 'group'
6613
- && (payload.kind === 'command.result' || payload.kind === 'command.error')) {
6614
- const isError = payload.kind === 'command.error';
6633
+ if (delivery.chatType === 'group' && payload.kind === 'command.result') {
6615
6634
  const feedback = {
6616
- type: isError ? 'error' : 'notice',
6635
+ type: 'notice',
6617
6636
  ...taskBase(),
6618
6637
  subtype: payload.kind,
6619
6638
  text: payload.text,
6620
- ...(isError
6621
- ? { message: payload.text, user_message: payload.text, recoverable: true }
6622
- : { severity: 'info' }),
6639
+ severity: 'info',
6623
6640
  };
6624
- for (const key of ['format', 'reason', 'reasonCode', 'nextStep']) {
6625
- setIfDefined(feedback, key, payload[key]);
6626
- }
6627
6641
  await channel.sendReliableStructured(channelId, feedback, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6628
6642
  return;
6629
6643
  }
@@ -6699,35 +6713,29 @@ export class AUNChannelPlugin {
6699
6713
  return;
6700
6714
  }
6701
6715
  case 'system.error': {
6702
- const raw = truncateRawErrorForDisplay(payload.raw);
6703
- const projectedRaw = projectedRawError(payload.raw, raw);
6716
+ const raw = rawErrorText(payload.raw) ?? payload.text;
6704
6717
  const errorPayload = {
6705
- type: payload.recoverable === true ? 'notice' : 'error',
6718
+ type: 'error',
6706
6719
  ...taskBase(),
6707
6720
  subtype: payload.subtype,
6708
- text: payload.recoverable === true
6709
- ? payload.text
6710
- : appendRawErrorText(payload.text, raw),
6711
- ...(projectedRaw !== undefined ? { raw: projectedRaw } : {}),
6721
+ text: appendRawErrorText(payload.text, raw),
6722
+ raw,
6712
6723
  ...(payload.reason !== undefined ? { reason: payload.reason } : {}),
6713
- ...(payload.recoverable === true
6714
- ? { severity: 'warn' }
6715
- : { recoverable: false }),
6724
+ ...(payload.recoverable !== undefined ? { recoverable: payload.recoverable } : {}),
6716
6725
  };
6717
6726
  await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6718
6727
  return;
6719
6728
  }
6720
6729
  case 'result.error': {
6721
- const raw = truncateRawErrorForDisplay(payload.raw);
6722
- const projectedRaw = projectedRawError(payload.raw, raw);
6730
+ const raw = rawErrorText(payload.raw) ?? payload.text;
6723
6731
  const errorPayload = {
6724
6732
  type: 'error',
6725
6733
  ...taskBase(),
6726
6734
  text: appendRawErrorText(payload.text, raw),
6735
+ raw,
6727
6736
  };
6728
6737
  setIfDefined(errorPayload, 'subtype', payload.subtype);
6729
6738
  setIfDefined(errorPayload, 'reason', payload.reason);
6730
- setIfDefined(errorPayload, 'raw', projectedRaw);
6731
6739
  await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6732
6740
  return;
6733
6741
  }
@@ -6859,9 +6867,10 @@ export class AUNChannelPlugin {
6859
6867
  case 'interaction': {
6860
6868
  const req = payload.interaction;
6861
6869
  const cardTtlMs = AUN_INTERACTION_CARD_TTL_MS;
6870
+ const indefiniteBlockingInteraction = req.blocking === true && req.expiresAt === undefined;
6862
6871
  const cardExpiresAt = typeof req.expiresAt === 'number' && Number.isFinite(req.expiresAt)
6863
6872
  ? req.expiresAt
6864
- : Date.now() + cardTtlMs;
6873
+ : indefiniteBlockingInteraction ? undefined : Date.now() + cardTtlMs;
6865
6874
  const toInteractionReceipt = (result) => {
6866
6875
  if ((result.status === undefined || result.status === 'sent') && result.messageId) {
6867
6876
  return sentReceipt(envelope, [result.messageId], replyCtx?.threadId);
@@ -6903,15 +6912,21 @@ export class AUNChannelPlugin {
6903
6912
  const aunCard = withAunAttention({
6904
6913
  type: 'action_card',
6905
6914
  title: action.title,
6915
+ ...(req.blocking === true ? { blocking: true } : {}),
6916
+ ...(action.recommendedOption ? { recommended_option: action.recommendedOption } : {}),
6906
6917
  actions: action.buttons.map(btn => ({
6907
- label: btn.label, value: btn.key, style: btn.style ?? 'default', behavior: 'reply',
6918
+ label: action.recommendedOption === btn.key ? `💡 ${btn.label}` : btn.label,
6919
+ value: btn.key, style: btn.style ?? 'default', behavior: 'reply',
6908
6920
  })),
6909
6921
  });
6910
6922
  // AUN action_card 以 compose behavior 声明自由文本动作。新版客户端
6911
6923
  // 在卡片内采集并确认;旧版客户端可继续从主编辑器采集。两者都结构化
6912
6924
  // 回传 action_card_reply{behavior:"compose"},Core 不拦截普通消息队列。
6913
6925
  if (action.allowCustomInput) {
6914
- aunCard.actions.push({ label: '✏️ 手动输入', style: 'default', behavior: 'compose', compose_timeout_ms: cardTtlMs });
6926
+ aunCard.actions.push({
6927
+ label: '✏️ 手动输入', style: 'default', behavior: 'compose',
6928
+ ...(cardExpiresAt !== undefined ? { compose_timeout_ms: Math.max(0, cardExpiresAt - Date.now()) } : {}),
6929
+ });
6915
6930
  }
6916
6931
  if (action.body) {
6917
6932
  aunCard.text = action.body;
@@ -6928,7 +6943,10 @@ export class AUNChannelPlugin {
6928
6943
  contentKind: 'card',
6929
6944
  context: cardReplyContext,
6930
6945
  dedupeKey: req.id.startsWith('gir-') || isAsyncQuestion ? operationId : undefined,
6931
- ttl: isAsyncQuestion ? Math.max(0, cardExpiresAt - Date.now()) : undefined,
6946
+ ...(isAsyncQuestion && cardExpiresAt !== undefined
6947
+ ? { ttl: Math.max(0, cardExpiresAt - Date.now()) }
6948
+ : {}),
6949
+ ...(indefiniteBlockingInteraction ? { critical: true } : {}),
6932
6950
  logText: action.title ? `[card] ${action.title}` : '[card]',
6933
6951
  postSend: {
6934
6952
  type: 'register_interaction_card',
@@ -88,7 +88,13 @@ export function buildDingtalkCardParamMap(interaction, state, status = '') {
88
88
  };
89
89
  for (let i = 0; i < DINGTALK_CARD_MAX_BUTTONS; i++) {
90
90
  const button = interaction.kind.buttons[i];
91
- map[`button_${i}_text`] = button?.label || '';
91
+ map[`button_${i}_text`] = button
92
+ ? (interaction.kind.kind === 'action'
93
+ && 'key' in button
94
+ && interaction.kind.recommendedOption === button.key
95
+ ? `💡 ${button.label}`
96
+ : button.label)
97
+ : '';
92
98
  map[`button_${i}_style`] = button?.style || 'default';
93
99
  map[`button_${i}_visible`] = button ? 'true' : 'false';
94
100
  map[`button_${i}_disabled`] = state !== 'pending' || !!(button && 'disabled' in button && button.disabled)
@@ -1005,7 +1011,7 @@ export class DingtalkChannel {
1005
1011
  continue;
1006
1012
  }
1007
1013
  const expired = (entry.interaction.expiresAt != null && entry.interaction.expiresAt <= now)
1008
- || entry.createdAt <= now - DINGTALK_CARD_PENDING_TTL_MS;
1014
+ || (entry.interaction.blocking !== true && entry.createdAt <= now - DINGTALK_CARD_PENDING_TTL_MS);
1009
1015
  if (!expired)
1010
1016
  continue;
1011
1017
  entry.invalidated = true;
@@ -1399,7 +1399,12 @@ export function buildCardV2(interaction, opts) {
1399
1399
  buttons.forEach((btn, idx) => {
1400
1400
  const buttonEl = {
1401
1401
  tag: 'button',
1402
- text: { tag: 'plain_text', content: btn.label },
1402
+ text: {
1403
+ tag: 'plain_text',
1404
+ content: kind.kind === 'action' && kind.recommendedOption === btn.key
1405
+ ? `💡 ${btn.label}`
1406
+ : btn.label,
1407
+ },
1403
1408
  type: btn.style === 'danger' ? 'danger' : btn.style === 'primary' ? 'primary' : 'default',
1404
1409
  action_type: 'form_submit',
1405
1410
  name: `btn_${idx}`,