evolcore 0.0.20 → 0.0.22

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 (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. package/dist/config/aun-gateway-config.js +0 -2
@@ -1,15 +1,22 @@
1
1
  import path from 'path';
2
+ const WATCH_LOG_FILE = /\.(?:log|jsonl|out)(?:\..+)?$/;
3
+ /** Whether a file is a supported input for the interactive log watcher. */
4
+ export function isWatchLogFile(file) {
5
+ return WATCH_LOG_FILE.test(path.basename(file));
6
+ }
2
7
  /** 去掉轮转后缀(按小时、按日及旧版带连字符日期)。入参可为文件名或绝对路径。 */
3
8
  export function shortLogName(file) {
4
- return path.basename(file, '.log')
9
+ const baseName = path.basename(file);
10
+ const stem = baseName.replace(WATCH_LOG_FILE, '');
11
+ return stem
5
12
  .replace(/-\d{8}(?:-\d{2})?$/, '') // -YYYYMMDD[-HH](按日/小时轮转)
6
13
  .replace(/-\d{4}-\d{2}-\d{2}$/, ''); // -YYYY-MM-DD(按日轮转,如 ts-sdk)
7
14
  }
8
- /** .log 文件名列表推导去重、字母序的类型列表。 */
15
+ /** 从支持的日志文件名列表推导去重、字母序的类型列表。 */
9
16
  export function deriveLogTypes(files) {
10
17
  const set = new Set();
11
18
  for (const f of files) {
12
- if (!f.endsWith('.log'))
19
+ if (!isWatchLogFile(f))
13
20
  continue;
14
21
  set.add(shortLogName(f));
15
22
  }
@@ -32,6 +32,7 @@ export function getBuiltinRolesConfig() {
32
32
  export function getManagementCommandPermissions(role) {
33
33
  return {
34
34
  'contact.read': { allow: true, scopes: ['agent'] },
35
+ 'contact.add': { allow: true, scopes: ['relation'], constraints: { ownPeerOnly: true, privateOnly: true } },
35
36
  'contact.block': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
36
37
  'connect.write': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
37
38
  'connect.access.write': { allow: true, scopes: ['agent'], constraints: { requireAgentOwner: true } },
@@ -2,6 +2,7 @@ const BEHAVIOR_TOP_FIELDS = new Set([
2
2
  'active_baseagent',
3
3
  'chatmode',
4
4
  'mentionMode',
5
+ 'groupSlashRequireMentions',
5
6
  'responseMode',
6
7
  'responseModeParams',
7
8
  'flush_delay',
@@ -17,6 +18,8 @@ const BEHAVIOR_TOP_FIELDS = new Set([
17
18
  const BASEAGENT_BEHAVIOR_FIELDS = new Set([
18
19
  'model',
19
20
  'effort',
21
+ 'auxiliaryModel',
22
+ 'auxiliaryEffort',
20
23
  'reasoning',
21
24
  'agentProgressSummaries',
22
25
  'excludeDynamicSections',
@@ -25,7 +28,7 @@ const BASEAGENT_BEHAVIOR_FIELDS = new Set([
25
28
  'mode',
26
29
  'useVertex',
27
30
  ]);
28
- const SUPPORTED_BASEAGENTS = new Set(['claude', 'codex', 'gemini', 'ecagent', 'hermes']);
31
+ const SUPPORTED_BASEAGENTS = new Set(['claude', 'codex', 'gemini', 'ecagent']);
29
32
  const SENSITIVE_TOP_FIELDS = new Set([
30
33
  '$schema_version',
31
34
  'aid',
@@ -75,10 +78,17 @@ export function resolveConfigFieldRule(fieldPath) {
75
78
  const [, baseagent, leaf] = parts;
76
79
  if (!SUPPORTED_BASEAGENTS.has(baseagent))
77
80
  return { class: 'unknown' };
78
- const permissionKey = `baseagents.${baseagent}.${leaf === 'reasoning' ? 'effort' : leaf}`;
79
- if (leaf === 'model' && parts.length === 3)
81
+ const normalizedLeaf = leaf === 'reasoning'
82
+ ? 'effort'
83
+ : leaf === 'auxiliaryModel'
84
+ ? 'model'
85
+ : leaf === 'auxiliaryEffort'
86
+ ? 'effort'
87
+ : leaf;
88
+ const permissionKey = `baseagents.${baseagent}.${normalizedLeaf}`;
89
+ if ((leaf === 'model' || leaf === 'auxiliaryModel') && parts.length === 3)
80
90
  return scalar(permissionKey, 'string');
81
- if ((leaf === 'effort' || leaf === 'reasoning') && parts.length === 3)
91
+ if ((leaf === 'effort' || leaf === 'reasoning' || leaf === 'auxiliaryEffort') && parts.length === 3)
82
92
  return scalar(permissionKey, 'effort');
83
93
  if (baseagent === 'claude' && (leaf === 'agentProgressSummaries' || leaf === 'excludeDynamicSections') && parts.length === 3) {
84
94
  return scalar(permissionKey, 'boolean');
@@ -101,6 +111,8 @@ export function resolveConfigFieldRule(fieldPath) {
101
111
  return scalar(field, 'nonnegative-number');
102
112
  if (field === 'mentionMode')
103
113
  return scalar(field, 'mention-mode');
114
+ if (field === 'groupSlashRequireMentions')
115
+ return scalar(field, 'boolean');
104
116
  if (field === 'show_activities')
105
117
  return scalar(field, 'show-activities');
106
118
  if (field === 'enable_rich_content')
@@ -199,7 +211,9 @@ export function relationFieldWriteOperation(fieldPath) {
199
211
  return 'mentionmode.update';
200
212
  if (/^baseagents\.[^.]+\.model$/.test(field))
201
213
  return 'model.use';
202
- if (/^baseagents\.[^.]+\.(?:effort|reasoning)$/.test(field)) {
214
+ if (/^baseagents\.[^.]+\.auxiliaryModel$/.test(field))
215
+ return 'model.use';
216
+ if (/^baseagents\.[^.]+\.(?:effort|reasoning|auxiliaryEffort)$/.test(field)) {
203
217
  return 'model.effort';
204
218
  }
205
219
  return undefined;
@@ -29,6 +29,7 @@ import { clearRoleStoreCache, isRoleName } from './role-store.js';
29
29
  import { writeRoleDefinition, writeRoleRegistry } from './role-service.js';
30
30
  import { mutateContactBookTransactionSync } from './contact-book-store.js';
31
31
  import { DAEMON_SCHEMA_NAME } from '../product.js';
32
+ import { normalizeDaemonServices } from './daemon-services.js';
32
33
  // AUN gateway discovery belongs to fastaun. Keep the retired gatewayUrl
33
34
  // compatibility handling beside the rest of the configuration read/write
34
35
  // policy so all config callers share one implementation.
@@ -595,11 +596,13 @@ export function read(target, sel, opts = {}) {
595
596
  raw = stripLegacyAunGatewayConfig(raw, file, message => configWarn(message));
596
597
  // schema 版本迁移(read 时若 $schema_version < current)
597
598
  const migrated = migrateIfNeeded(target, raw, file);
598
- const normalized = target === ConfigTarget.Agent
599
- ? normalizeAgentConfigForRead(migrated, sel?.self)
600
- : target === ConfigTarget.Relation
601
- ? foldLegacyProactiveBlock(migrated)
602
- : migrated;
599
+ const normalized = target === ConfigTarget.Process
600
+ ? normalizeProcessConfigCompat(normalizeDaemonServices(migrated))
601
+ : target === ConfigTarget.Agent
602
+ ? normalizeAgentConfigForRead(migrated, sel?.self)
603
+ : target === ConfigTarget.Relation
604
+ ? foldLegacyProactiveBlock(migrated)
605
+ : migrated;
603
606
  // Agent read compatibility may move the legacy top-level `agents` block to
604
607
  // `baseagents`, so canonicalize effort after that structural migration.
605
608
  const canonicalized = normalizeBaseagentEffortCompat(normalized);
@@ -680,12 +683,11 @@ function normalizeAgentConfigForWrite(value) {
680
683
  if (mutable.aid && !isValidAid(mutable.aid)) {
681
684
  throw new ConfigError('VALIDATION_ERROR', `Invalid aid "${mutable.aid}" (must be a valid multi-level domain like mybot.agentid.pub)`);
682
685
  }
683
- // 规范化 projects 字段:只保留 rootPath 和 defaultPath
686
+ // 规范化 projects 字段:只保留唯一公开项目路径 defaultPath
687
+ // defaults.projects.rootPath 已废弃,不能继续成为有效运行时参数。
684
688
  if (mutable.projects && typeof mutable.projects === 'object') {
685
689
  const projects = mutable.projects;
686
690
  const normalized = {};
687
- if (typeof projects.rootPath === 'string')
688
- normalized.rootPath = projects.rootPath;
689
691
  if (typeof projects.defaultPath === 'string')
690
692
  normalized.defaultPath = projects.defaultPath;
691
693
  if (Object.keys(normalized).length > 0) {
@@ -698,19 +700,58 @@ function normalizeAgentConfigForWrite(value) {
698
700
  return mutable;
699
701
  }
700
702
  function normalizeProcessConfigForWrite(value) {
701
- const configuredAidDomain = value.aun?.defaultAidDomain;
703
+ let normalized;
704
+ try {
705
+ normalized = normalizeProcessConfigCompat(normalizeDaemonServices(value));
706
+ }
707
+ catch (error) {
708
+ throw new ConfigError('DAEMON_SERVICES_MIGRATION_REQUIRED', error instanceof Error ? error.message : String(error));
709
+ }
710
+ const configuredAidDomain = normalized.aun?.defaultAidDomain;
702
711
  if (configuredAidDomain === undefined)
703
- return value;
712
+ return normalized;
704
713
  try {
705
714
  return {
706
- ...value,
707
- aun: { ...value.aun, defaultAidDomain: normalizeAidDomain(configuredAidDomain, 'aun.defaultAidDomain') },
715
+ ...normalized,
716
+ aun: { ...normalized.aun, defaultAidDomain: normalizeAidDomain(configuredAidDomain, 'aun.defaultAidDomain') },
708
717
  };
709
718
  }
710
719
  catch (error) {
711
720
  throw new ConfigError('VALIDATION_ERROR', error instanceof Error ? error.message : String(error));
712
721
  }
713
722
  }
723
+ /**
724
+ * Normalize renamed process-level settings while keeping old daemon.json files
725
+ * readable after an upgrade. New values take precedence when both keys exist.
726
+ */
727
+ export function normalizeProcessConfigCompat(value) {
728
+ if (!value || typeof value !== 'object' || Array.isArray(value))
729
+ return value;
730
+ const idleMonitor = value.idleMonitor;
731
+ const debug = value.debug;
732
+ let changed = false;
733
+ let normalized = value;
734
+ if (idleMonitor && typeof idleMonitor === 'object' && !Array.isArray(idleMonitor)) {
735
+ const nextIdleMonitor = { ...idleMonitor };
736
+ if (nextIdleMonitor.apiRetryTimeout === undefined && nextIdleMonitor.retryAttemptTimeout !== undefined) {
737
+ nextIdleMonitor.apiRetryTimeout = nextIdleMonitor.retryAttemptTimeout;
738
+ changed = true;
739
+ }
740
+ if (Object.prototype.hasOwnProperty.call(nextIdleMonitor, 'retryAttemptTimeout')) {
741
+ delete nextIdleMonitor.retryAttemptTimeout;
742
+ changed = true;
743
+ }
744
+ if (changed)
745
+ normalized = { ...normalized, idleMonitor: nextIdleMonitor };
746
+ }
747
+ if (debug && typeof debug === 'object' && !Array.isArray(debug)
748
+ && Object.prototype.hasOwnProperty.call(debug, 'flusherDiag')) {
749
+ const nextDebug = { ...debug };
750
+ delete nextDebug.flusherDiag;
751
+ normalized = { ...normalized, debug: nextDebug };
752
+ }
753
+ return normalized;
754
+ }
714
755
  function normalizeRelationConfigForWrite(value) {
715
756
  const mutable = normalizeBaseagentEffortCompat({ ...value });
716
757
  normalizeShowActivitiesCompat(mutable);
@@ -892,7 +933,10 @@ function acquireConfigMutationLock(file) {
892
933
  return () => releaseConfigMutationLock(lockDir, ownerFile, token);
893
934
  }
894
935
  catch (error) {
895
- if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY')
936
+ const lockMayAlreadyExist = error?.code === 'EEXIST'
937
+ || error?.code === 'ENOTEMPTY'
938
+ || (process.platform === 'win32' && error?.code === 'EPERM' && fs.existsSync(lockDir));
939
+ if (!lockMayAlreadyExist)
896
940
  throw error;
897
941
  if (reclaimStaleConfigMutationLock(lockDir, ownerFile))
898
942
  continue;
@@ -916,10 +960,10 @@ function reclaimStaleConfigMutationLock(lockDir, ownerFile) {
916
960
  pid = Number(JSON.parse(raw).pid);
917
961
  }
918
962
  catch {
919
- return false;
963
+ return reclaimCorruptConfigMutationLock(lockDir);
920
964
  }
921
965
  if (!Number.isInteger(pid) || pid <= 0)
922
- return false;
966
+ return reclaimCorruptConfigMutationLock(lockDir);
923
967
  try {
924
968
  process.kill(pid, 0);
925
969
  return false;
@@ -938,6 +982,24 @@ function reclaimStaleConfigMutationLock(lockDir, ownerFile) {
938
982
  }
939
983
  }
940
984
  }
985
+ function reclaimCorruptConfigMutationLock(lockDir) {
986
+ try {
987
+ const stat = fs.statSync(lockDir);
988
+ if (Date.now() - stat.mtimeMs < CONFIG_MUTATION_LOCK_WAIT_MS)
989
+ return false;
990
+ const reclaimed = `${lockDir}.reclaimed-corrupt-${crypto.randomBytes(8).toString('hex')}`;
991
+ fs.renameSync(lockDir, reclaimed);
992
+ fs.rmSync(reclaimed, { recursive: true, force: true });
993
+ return true;
994
+ }
995
+ catch (error) {
996
+ if (error?.code === 'ENOENT')
997
+ return true;
998
+ if (error?.code === 'EEXIST' || error?.code === 'ENOTEMPTY' || error?.code === 'EPERM')
999
+ return false;
1000
+ throw error;
1001
+ }
1002
+ }
941
1003
  function releaseConfigMutationLock(lockDir, ownerFile, token) {
942
1004
  try {
943
1005
  const owner = JSON.parse(fs.readFileSync(ownerFile, 'utf8'));
@@ -985,7 +1047,11 @@ function ensureSchemaVersion(value, version) {
985
1047
  }
986
1048
  function validateOrThrow(schema, value, target) {
987
1049
  const ok = schema.validate(value);
988
- const scopeErrors = [...validateProcessOnlyFields(target, value), ...validateProcessAidDomain(target, value)];
1050
+ const scopeErrors = [
1051
+ ...validateProcessOnlyFields(target, value),
1052
+ ...validateProcessAidDomain(target, value),
1053
+ ...validateProcessServiceNames(target, value),
1054
+ ];
989
1055
  if (!ok || scopeErrors.length > 0) {
990
1056
  const errs = [
991
1057
  ...scopeErrors,
@@ -1026,6 +1092,24 @@ function validateProcessAidDomain(target, value) {
1026
1092
  return [`/aun/defaultAidDomain ${error instanceof Error ? error.message : String(error)}`];
1027
1093
  }
1028
1094
  }
1095
+ function validateProcessServiceNames(target, value) {
1096
+ if (target !== ConfigTarget.Process || !value || typeof value !== 'object')
1097
+ return [];
1098
+ const services = value.services;
1099
+ if (!Array.isArray(services))
1100
+ return [];
1101
+ const seen = new Set();
1102
+ const errors = [];
1103
+ services.forEach((service, index) => {
1104
+ if (!service || typeof service !== 'object' || typeof service.name !== 'string')
1105
+ return;
1106
+ if (seen.has(service.name))
1107
+ errors.push(`/services/${index}/name duplicates service name ${JSON.stringify(service.name)}`);
1108
+ else
1109
+ seen.add(service.name);
1110
+ });
1111
+ return errors;
1112
+ }
1029
1113
  /**
1030
1114
  * responseModeParams 桶专项校验。
1031
1115
  *
@@ -1076,8 +1160,28 @@ export function validateConfig(target, value) {
1076
1160
  ? loadSchema('contact-book', 2)
1077
1161
  : loadSchema(TARGET_SCHEMA[target]);
1078
1162
  const withVer = ensureSchemaVersion(value, schema.version);
1079
- const ok = schema.validate(withVer);
1080
- const errors = [...validateProcessOnlyFields(target, withVer), ...validateProcessAidDomain(target, withVer)];
1163
+ let normalized = withVer;
1164
+ try {
1165
+ if (target === ConfigTarget.Process)
1166
+ normalized = normalizeProcessConfigCompat(normalizeDaemonServices(withVer));
1167
+ }
1168
+ catch (error) {
1169
+ return [error instanceof Error ? error.message : String(error)];
1170
+ }
1171
+ const ok = schema.validate(normalized);
1172
+ const errors = [
1173
+ ...validateProcessOnlyFields(target, normalized),
1174
+ ...validateProcessAidDomain(target, normalized),
1175
+ ...validateProcessServiceNames(target, normalized),
1176
+ ];
1177
+ if (target === ConfigTarget.Defaults || target === ConfigTarget.Agent || target === ConfigTarget.Relation) {
1178
+ try {
1179
+ validateResponseModeParams(normalized?.responseModeParams, target);
1180
+ }
1181
+ catch (error) {
1182
+ errors.push(error instanceof ConfigError ? error.message : String(error));
1183
+ }
1184
+ }
1081
1185
  if (ok && errors.length === 0)
1082
1186
  return [];
1083
1187
  return [
@@ -1097,7 +1201,15 @@ export function validateConfigFile(target, sel) {
1097
1201
  return { exists: false, errors: [] };
1098
1202
  // Persisted pre-removal files remain readable: validate the same sanitized
1099
1203
  // view used by runtime reads, while new writes are still rejected by write().
1100
- const sanitized = stripLegacyAunGatewayConfig(value, file, message => configWarn(message));
1204
+ let sanitized;
1205
+ try {
1206
+ sanitized = target === ConfigTarget.Process
1207
+ ? normalizeDaemonServices(stripLegacyAunGatewayConfig(value, file, message => configWarn(message)))
1208
+ : stripLegacyAunGatewayConfig(value, file, message => configWarn(message));
1209
+ }
1210
+ catch (error) {
1211
+ return { exists: true, errors: [error instanceof Error ? error.message : String(error)] };
1212
+ }
1101
1213
  return { exists: true, errors: validateConfig(target, sanitized) };
1102
1214
  }
1103
1215
  function fileCacheAvailable() {
@@ -1159,21 +1271,52 @@ export function resolveAgentConfig(sel, opts = {}) {
1159
1271
  const agentConfig = sel.self ? read(ConfigTarget.Agent, sel, opts) : null;
1160
1272
  const relationConfig = (sel.self && sel.peerKey) ? read(ConfigTarget.Relation, sel, opts) : null;
1161
1273
  const layers = [
1162
- stripStaticOwnerField(read(ConfigTarget.Defaults, undefined, opts)),
1274
+ stripStaticOwnerField(normalizeDefaultsBehaviorCompat(read(ConfigTarget.Defaults, undefined, opts))),
1163
1275
  agentConfig,
1164
1276
  stripRelationRoleData(stripStaticOwnerField(relationConfig)),
1165
1277
  ];
1166
1278
  const merged = mergeLayers(layers, fields);
1167
1279
  return merged;
1168
1280
  }
1281
+ /**
1282
+ * defaults.schema keeps the historical `config` object as the process-wide
1283
+ * response behavior compatibility surface. Make those values participate in
1284
+ * the normal agent/relation merge chain without leaking the compatibility
1285
+ * container itself into EffectiveAgentConfig.
1286
+ *
1287
+ * Explicit canonical fields (if a future schema adds them) always win. A
1288
+ * single default chat mode applies to the three peer categories because this
1289
+ * compatibility field predates the per-category `chatmode` dictionary.
1290
+ */
1291
+ export function normalizeDefaultsBehaviorCompat(defaults) {
1292
+ if (!defaults || !defaults.config || typeof defaults.config !== 'object' || Array.isArray(defaults.config))
1293
+ return defaults;
1294
+ const { config, ...rest } = defaults;
1295
+ const normalized = { ...rest };
1296
+ const chatMode = config.chatMode;
1297
+ if (!normalized.chatmode && (chatMode === 'interactive' || chatMode === 'proactive')) {
1298
+ normalized.chatmode = { private: chatMode, group: chatMode, nothuman: chatMode };
1299
+ }
1300
+ if (normalized.mentionMode === undefined && (config.mentionMode === 'disabled' || config.mentionMode === 'mention-only')) {
1301
+ normalized.mentionMode = config.mentionMode;
1302
+ }
1303
+ if (normalized.show_activities === undefined && ['all', 'text', 'none'].includes(config.show_activities)) {
1304
+ normalized.show_activities = config.show_activities;
1305
+ }
1306
+ return normalized;
1307
+ }
1169
1308
  function stripStaticOwnerField(config) {
1170
1309
  if (!config)
1171
1310
  return config;
1172
1311
  if (!Object.prototype.hasOwnProperty.call(config, 'owners')
1173
1312
  && !Object.prototype.hasOwnProperty.call(config, 'admins')
1174
- && !Object.prototype.hasOwnProperty.call(config, 'access'))
1313
+ && !Object.prototype.hasOwnProperty.call(config, 'access')
1314
+ && !Object.prototype.hasOwnProperty.call(config, 'groupSlashRequireMentions'))
1175
1315
  return config;
1176
- const { owners: _owners, admins: _admins, access: _access, ...rest } = config;
1316
+ // groupSlashRequireMentions is intentionally Agent-only. Relation/defaults
1317
+ // files allow forward-compatible unknown properties, so strip it before the
1318
+ // generic merge chain can treat a manually written value as an override.
1319
+ const { owners: _owners, admins: _admins, access: _access, groupSlashRequireMentions: _groupSlashRequireMentions, ...rest } = config;
1177
1320
  return rest;
1178
1321
  }
1179
1322
  function stripRelationRoleData(config) {
@@ -1212,6 +1355,7 @@ export function resolveEffective(sel, opts = {}) {
1212
1355
  baseagents: config.baseagents,
1213
1356
  chatmode: config.chatmode,
1214
1357
  mentionMode: config.mentionMode,
1358
+ groupSlashRequireMentions: config.groupSlashRequireMentions ?? true,
1215
1359
  model: config.model,
1216
1360
  responseMode: config.responseMode,
1217
1361
  responseModeParams: config.responseModeParams,
@@ -1243,6 +1387,7 @@ const EFFECTIVE_BEHAVIOR_FIELDS = [
1243
1387
  'baseagents',
1244
1388
  'chatmode',
1245
1389
  'mentionMode',
1390
+ 'groupSlashRequireMentions',
1246
1391
  'flush_delay',
1247
1392
  'debounce',
1248
1393
  'show_activities',
@@ -14,6 +14,7 @@ const LOCK_NAME = '.contact-book-mutation.lock';
14
14
  const JOURNAL_NAME = '.contact-book-mutation.json';
15
15
  const LOCK_WAIT_MS = 5_000;
16
16
  const LOCK_RETRY_MS = 20;
17
+ const CORRUPT_LOCK_STALE_MS = LOCK_WAIT_MS;
17
18
  export const EMPTY_CONTACT_REVISION = crypto.createHash('sha256').update('').digest('hex');
18
19
  export class ContactMutationError extends Error {
19
20
  code;
@@ -454,7 +455,10 @@ function acquireAgentLock(selfAid) {
454
455
  return () => releaseAgentLock(lockDir, ownerFile, lockToken);
455
456
  }
456
457
  catch (error) {
457
- if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY')
458
+ const lockMayAlreadyExist = error?.code === 'EEXIST'
459
+ || error?.code === 'ENOTEMPTY'
460
+ || (process.platform === 'win32' && error?.code === 'EPERM' && fs.existsSync(lockDir));
461
+ if (!lockMayAlreadyExist)
458
462
  throw error;
459
463
  if (removeStaleLock(lockDir, ownerFile))
460
464
  continue;
@@ -478,10 +482,10 @@ function removeStaleLock(lockDir, ownerFile) {
478
482
  pid = Number(JSON.parse(rawOwner).pid);
479
483
  }
480
484
  catch {
481
- return false;
485
+ return reclaimCorruptLock(lockDir);
482
486
  }
483
487
  if (!Number.isInteger(pid) || pid <= 0)
484
- return false;
488
+ return reclaimCorruptLock(lockDir);
485
489
  try {
486
490
  process.kill(pid, 0);
487
491
  return false;
@@ -503,6 +507,24 @@ function removeStaleLock(lockDir, ownerFile) {
503
507
  }
504
508
  }
505
509
  }
510
+ function reclaimCorruptLock(lockDir) {
511
+ try {
512
+ const stat = fs.statSync(lockDir);
513
+ if (Date.now() - stat.mtimeMs < CORRUPT_LOCK_STALE_MS)
514
+ return false;
515
+ const staleClaim = `${lockDir}.reclaimed-corrupt-${crypto.randomBytes(8).toString('hex')}`;
516
+ fs.renameSync(lockDir, staleClaim);
517
+ fs.rmSync(staleClaim, { recursive: true, force: true });
518
+ return true;
519
+ }
520
+ catch (error) {
521
+ if (error?.code === 'ENOENT')
522
+ return true;
523
+ if (error?.code === 'EEXIST' || error?.code === 'ENOTEMPTY' || error?.code === 'EPERM')
524
+ return false;
525
+ throw error;
526
+ }
527
+ }
506
528
  function releaseAgentLock(lockDir, ownerFile, lockToken) {
507
529
  try {
508
530
  const owner = JSON.parse(fs.readFileSync(ownerFile, 'utf8'));
@@ -4,7 +4,7 @@ import { isExplicitGroupId } from '../aun/group-identity.js';
4
4
  import { agentConfig } from '../paths.js';
5
5
  import { getContactSnapshot, resolveContactView } from './contact-book.js';
6
6
  import { ContactMutationError } from './contact-book-store.js';
7
- import { mutateContactWithOperation } from './contact-request-service.js';
7
+ import { addContact, mutateContactWithOperation } from './contact-request-service.js';
8
8
  export function resolveContactCommand(argv) {
9
9
  const args = argv[0] === 'contact' ? argv.slice(1) : [...argv];
10
10
  const selfAid = String(argValue(args, '--self') || '').trim();
@@ -39,6 +39,21 @@ export function resolveContactCommand(argv) {
39
39
  },
40
40
  };
41
41
  }
42
+ if (command === 'add') {
43
+ if (!isValidAid(subcommand || '') || isExplicitGroupId(subcommand || '')) {
44
+ return invalid('INVALID_AID', 'primaryId must be a valid individual AID');
45
+ }
46
+ return {
47
+ ok: true,
48
+ command: {
49
+ kind: 'add',
50
+ operationId: 'contact.add',
51
+ selfAid,
52
+ primaryId: subcommand,
53
+ canonicalArgv: canonicalArgv(argv),
54
+ },
55
+ };
56
+ }
42
57
  return invalid('UNKNOWN_COMMAND', `Unknown contact command: ${positional.join(' ')}`);
43
58
  }
44
59
  export async function executeResolvedContactCommand(command, actor = 'local-cli') {
@@ -81,6 +96,22 @@ export async function executeResolvedContactCommand(command, actor = 'local-cli'
81
96
  contactRevision: result.contactRevision,
82
97
  };
83
98
  }
99
+ if (command.kind === 'add') {
100
+ const result = await addContact({
101
+ selfAid: command.selfAid,
102
+ primaryId: command.primaryId,
103
+ actorId: actor,
104
+ });
105
+ return {
106
+ ok: true,
107
+ operation: 'add',
108
+ selfAid: command.selfAid,
109
+ primaryId: command.primaryId,
110
+ status: 'active',
111
+ changed: result.changed,
112
+ contactRevision: result.contactRevision,
113
+ };
114
+ }
84
115
  return { ok: false, code: 'CONTACT_OPERATION_FAILED', error: 'Unsupported contact operation' };
85
116
  }
86
117
  catch (error) {
@@ -38,6 +38,50 @@ export async function mutateContactWithOperation(input) {
38
38
  const { value: _value, ...mutationResult } = result;
39
39
  return mutationResult;
40
40
  }
41
+ /**
42
+ * Add a canonical AUN contact as active through the Contact Book transaction.
43
+ * Existing pending/declined entries are intentionally promoted here, while a
44
+ * blocked entry remains a hard stop and must be explicitly unblocked first.
45
+ */
46
+ export async function addContact(input) {
47
+ const primaryId = String(input.primaryId || '').trim();
48
+ if (!isValidAid(primaryId)) {
49
+ throw new ContactMutationError('INVALID_AID', `Invalid contact AID: ${primaryId}`);
50
+ }
51
+ const at = new Date(input.now ?? Date.now()).toISOString();
52
+ const logFile = agentContactOperations(input.selfAid);
53
+ const result = await mutateContactBookPrepared(input.selfAid, ({ contact, contactRevision }) => {
54
+ if (input.expectedContactRevision !== undefined && input.expectedContactRevision !== contactRevision) {
55
+ throw new ContactMutationError('CONFLICT', 'Contact book changed since it was read', {
56
+ expectedContactRevision: input.expectedContactRevision,
57
+ contactRevision,
58
+ });
59
+ }
60
+ if (contact.contacts[primaryId]?.status === 'blocked') {
61
+ throw new ContactMutationError('CONTACT_BLOCKED', `Contact is blocked: ${primaryId}`);
62
+ }
63
+ const mutations = [
64
+ { type: 'set-status', primaryId, status: 'active' },
65
+ ];
66
+ if (input.displayName !== undefined) {
67
+ mutations.push({ type: 'set-display-name', primaryId, displayName: input.displayName });
68
+ }
69
+ return {
70
+ mutation: mutations.length === 1 ? mutations[0] : { type: 'batch', mutations },
71
+ actor: input.actorId,
72
+ value: undefined,
73
+ beforeCommit: ({ contactRevision: nextRevision }) => operationLogChange(logFile, [operationRecord({
74
+ at,
75
+ op: 'manual-add',
76
+ primaryId,
77
+ actorId: input.actorId,
78
+ contactRevision: nextRevision,
79
+ })]),
80
+ };
81
+ });
82
+ const { value: _value, ...mutationResult } = result;
83
+ return mutationResult;
84
+ }
41
85
  export async function submitContactRequest(input) {
42
86
  const applicantAid = String(input.applicantAid || '').trim();
43
87
  const owners = listAgentOwners(input.selfAid);