evolcore 0.0.34 → 0.0.35

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 (69) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/install-codex-managed-hooks.mjs +26 -3
  3. package/dist/agents/claude-runner.js +11 -1
  4. package/dist/agents/codex/diagnostics.js +42 -11
  5. package/dist/agents/codex-runner.js +27 -3
  6. package/dist/agents/ecagent-runner.js +17 -7
  7. package/dist/aun/aid/agentmd.js +13 -2
  8. package/dist/aun/aid/client.js +1 -1
  9. package/dist/aun/msg/managed-operation.js +16 -0
  10. package/dist/aun/msg/p2p.js +18 -2
  11. package/dist/channels/aun.js +113 -7
  12. package/dist/cli/aun-commands.js +37 -13
  13. package/dist/cli/daemon-commands.js +10 -4
  14. package/dist/cli/fs-command.js +1 -1
  15. package/dist/cli/getmeta-command.js +211 -0
  16. package/dist/cli/index.js +6 -0
  17. package/dist/cli/managed-command-guard.js +3 -0
  18. package/dist/cli/memory-command.js +46 -15
  19. package/dist/cli/thread-command.js +12 -4
  20. package/dist/config/resolved-config-op.js +25 -8
  21. package/dist/core/auth/operation-authorizer.js +21 -2
  22. package/dist/core/channel-runtime.js +1 -1
  23. package/dist/core/command/command-handler.js +7 -1
  24. package/dist/core/command/menu-handler.js +2 -0
  25. package/dist/core/command/topic-slash.js +4 -7
  26. package/dist/core/event-catalog.js +14 -0
  27. package/dist/core/ipc-registration.js +11 -1
  28. package/dist/core/memory/batch-meta.js +1 -0
  29. package/dist/core/memory/consolidation-context.js +97 -14
  30. package/dist/core/memory/consolidation-trigger.js +15 -7
  31. package/dist/core/memory/consolidation.js +55 -0
  32. package/dist/core/memory/memory-writer.js +20 -4
  33. package/dist/core/message/aun-ipc-sender.js +19 -1
  34. package/dist/core/message/message-bridge.js +3 -3
  35. package/dist/core/message/message-log.js +1 -0
  36. package/dist/core/message/peer-mode.js +17 -0
  37. package/dist/core/message/response-engine.js +15 -4
  38. package/dist/core/permission/ec-command-parser.js +76 -40
  39. package/dist/core/permission/tool-policy.js +32 -2
  40. package/dist/core/relation/peer-identity.js +87 -12
  41. package/dist/core/session/session-manager.js +56 -1
  42. package/dist/core/session/session-title.js +26 -1
  43. package/dist/core/system-session/memory.js +87 -20
  44. package/dist/core/thread/command-parser.js +6 -1
  45. package/dist/core/thread/ipc-operation.js +33 -16
  46. package/dist/core/thread/thread-operation.js +62 -24
  47. package/dist/core/thread/thread-service.js +246 -32
  48. package/dist/utils/cross-platform.js +6 -3
  49. package/kits/docs/evolcore/fs-architecture.md +5 -5
  50. package/kits/docs/evolcore/fs.md +1 -1
  51. package/kits/docs/evolcore/group-collaboration.md +1 -1
  52. package/kits/docs/evolcore/group.md +5 -3
  53. package/kits/docs/evolcore/memory-consolidation.md +1 -2
  54. package/kits/docs/evolcore/memory-tags.md +1 -1
  55. package/kits/docs/evolcore/memory.md +2 -2
  56. package/kits/docs/evolcore/stats.md +1 -1
  57. package/kits/docs/evolcore/thread-chain.md +25 -9
  58. package/kits/eck_manifest.auxiliary.json +9 -0
  59. package/kits/eck_manifest.json +20 -0
  60. package/kits/eck_manifest.memory.json +20 -0
  61. package/kits/rules/01-overview.md +3 -3
  62. package/kits/rules/07-memory-and-thread.md +1 -1
  63. package/kits/skills/ec-group/SKILL.md +6 -3
  64. package/kits/skills/thread-ops/SKILL.md +18 -18
  65. package/kits/templates/memory-consolidation/notes.md +1 -1
  66. package/kits/templates/memory-consolidation/workflow.md +36 -1
  67. package/kits/templates/system-fragments/bash-description.md +9 -0
  68. package/kits/templates/system-fragments/memory.md +2 -2
  69. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.0.35 (2026-10-19)
9
+
10
+ ### 权限与记忆
11
+
12
+ - 规范 Claude Code 与 ecagent 的 Bash 调用说明,要求明确命令目标并标注低、中、高风险等级;缺失或格式错误时提供具体提示,避免权限模式不同导致调用说明不一致。
13
+ - agent 级记忆整理会扫描所有关系记忆并按关系归类,避免遗漏其他对端的过期记忆。
14
+ - 首次设定尚未完成时阻止记忆整理会话启动,避免记忆被提前认领后无法正常处理。
15
+ - 细化只读 Shell 操作的拒绝原因,提升受限命令的诊断可读性。
16
+
17
+ ### Codex 与运行稳定性
18
+
19
+ - 保留 Codex 模型服务重连进度,并在最终失败时展示实际重试次数,避免只显示笼统的请求失败。
20
+ - 统一 Codex managed hook 的包根目录与安装器解析,升级监视器暂存运行文件时同步携带 `bin` 目录,提升升级和重启过程的一致性。
21
+ - 补充生命周期启动失败时的 stderr 诊断输出,便于定位启动异常。
22
+ - 修复 `agent.md` 显示名解析,改用 YAML 结构化解析以正确处理引号和特殊字符。
23
+ - 修复话题摘要写入时的游标竞态,确保 `coveredThroughTs` 在追加摘要前读取。
24
+
25
+ ### 消息与依赖兼容
26
+
27
+ - 规范 AUN 原始错误展示,截断错误响应中附带的 HTML 页面,保留有用的错误信息并减少无关输出。
28
+ - 将 AUN SDK 最低运行版本提升至 `0.5.20`,使用经过当前版本验证的 SDK 依赖。
29
+
8
30
  ## 0.0.34 (2026-10-18)
9
31
 
10
32
  ### 模型与上下文
@@ -22,7 +22,25 @@ function hasArgument(name) {
22
22
  return process.argv.includes(name);
23
23
  }
24
24
 
25
- function fail(message) {
25
+ /** Path where the elevated installer writes a structured failure record for the non-elevated parent. */
26
+ function installerFailurePath(requirements) {
27
+ const configDir = requirements
28
+ ? path.dirname(path.resolve(requirements))
29
+ : path.join(process.env.ProgramData || path.join(os.homedir(), 'ProgramData'), 'OpenAI', 'Codex');
30
+ return path.join(configDir, '.evolcore-install-failure.json');
31
+ }
32
+
33
+ function writeFailureRecord(requirements, message) {
34
+ try {
35
+ const record = { time: new Date().toISOString(), message };
36
+ fs.writeFileSync(installerFailurePath(requirements), JSON.stringify(record) + '\n', { mode: 0o644 });
37
+ } catch {
38
+ // Best-effort; parent falls back to "installer exited with code 1" if missing.
39
+ }
40
+ }
41
+
42
+ function fail(message, requirements) {
43
+ writeFailureRecord(requirements, message);
26
44
  process.stderr.write(`[evolcore] ${message}\n`);
27
45
  process.exit(1);
28
46
  }
@@ -653,7 +671,7 @@ function main() {
653
671
  const hookPath = path.join(targetDirectory, 'pre-tool-use.mjs');
654
672
  const sourceHook = path.join(packageRoot, 'bin', 'codex-managed-hook.mjs');
655
673
  for (const file of [sourceHook, nodePath]) {
656
- if (!fs.statSync(file).isFile()) fail(`Required Hook runtime file was not found: ${file}`);
674
+ if (!fs.statSync(file).isFile()) fail(`Required Hook runtime file was not found: ${file}`, requirements);
657
675
  }
658
676
  if (hasArgument('--repair')) {
659
677
  const result = repairManagedHookInstallation({
@@ -674,7 +692,12 @@ function main() {
674
692
  }
675
693
 
676
694
  if (process.argv[1] && pathToFileURL(fs.realpathSync(process.argv[1])).href === import.meta.url) {
695
+ // Resolve requirements now so writeFailureRecord has a path even if main()
696
+ // throws before the local `requirements` variable is assigned.
697
+ const earlyRequirements = argument('--requirements')
698
+ ? path.resolve(argument('--requirements'))
699
+ : path.join(process.env.ProgramData || path.join(os.homedir(), 'ProgramData'), 'OpenAI', 'Codex', 'requirements.toml');
677
700
  try { main(); } catch (error) {
678
- fail(error instanceof Error ? error.message : String(error));
701
+ fail(error instanceof Error ? error.message : String(error), earlyRequirements);
679
702
  }
680
703
  }
@@ -15,7 +15,7 @@ import crypto from 'crypto';
15
15
  import { logger } from '../utils/logger.js';
16
16
  import { createClaudeStartupArgumentsTooLongError, isClaudeStartupArgumentsTooLong, createClaudeCodeStartupError, isClaudeCodeStartupProcessError, } from '../utils/error-utils.js';
17
17
  import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
18
- import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
18
+ import { checkBashDescription, checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
19
19
  import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
20
20
  import { summarizeToolInput, summarizeToolInputForAudit } from '../utils/tool-summary.js';
21
21
  import { encodePath } from '../utils/cross-platform.js';
@@ -2391,6 +2391,16 @@ export class AgentRunner {
2391
2391
  delete toolInput.pages;
2392
2392
  }
2393
2393
  let updatedInput = toolInput === originalInput ? undefined : toolInput;
2394
+ // Bash description 格式约定先于一切放行分支(含系统会话与 fullaccess)。
2395
+ // TODO(bash-description): 该约束缺少验证——实测模型被拒后不会补 description 重试,
2396
+ // 话题链等链路会被整条堵死。在补齐测试与重试引导前先短路放行,恢复时删除本开关。
2397
+ if (process.env.EVOLCORE_BASH_DESCRIPTION_ENFORCE === '1') {
2398
+ const descriptionCheck = checkBashDescription(toolName, toolInput);
2399
+ if (descriptionCheck) {
2400
+ await recordPreflightDenial(toolName, toolInput, descriptionCheck, input.tool_use_id ?? input.toolUseID);
2401
+ return hookDecision('deny', descriptionCheck.message, updatedInput);
2402
+ }
2403
+ }
2394
2404
  // 系统会话:模块判定即最终结论,不进入下面任何按角色 / permissionMode 的分支
2395
2405
  const systemSessionCheck = this.permissionContexts.get(sessionId)?.systemSessionCheck;
2396
2406
  if (systemSessionCheck) {
@@ -3,8 +3,10 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { createHash } from 'node:crypto';
6
+ import { createRequire } from 'node:module';
7
+ import { fileURLToPath } from 'node:url';
6
8
  import { parse as parseToml } from 'smol-toml';
7
- const managedHookInstaller = await import(new URL('../../../bin/install-codex-managed-hooks.mjs', import.meta.url).href);
9
+ import { TARGET_PACKAGE_ROOT_ENV } from '../../product.js';
8
10
  export function checkCodexProjectPath(value, options = {}) {
9
11
  const configured = typeof value === 'string' ? value.trim() : '';
10
12
  if (!configured) {
@@ -239,9 +241,6 @@ function runtimeFileMayExist(file) {
239
241
  return !['ENOENT', 'ENOTDIR'].includes(error?.code || '');
240
242
  }
241
243
  }
242
- function managedBinding(source, launcher, hookPath) {
243
- return managedHookInstaller.managedBinding(source, launcher, hookPath);
244
- }
245
244
  /** Read-only inspection of the machine-wide Windows Codex managed hook. */
246
245
  export function inspectCodexManagedHook(options = {}) {
247
246
  const paths = codexManagedHookPaths(options);
@@ -250,6 +249,21 @@ export function inspectCodexManagedHook(options = {}) {
250
249
  const requirements = path.resolve(options.requirements || paths.requirements);
251
250
  const managedDir = path.resolve(options.managedDir || paths.managedDir);
252
251
  const hookPath = path.join(managedDir, 'pre-tool-use.mjs');
252
+ let managedHookInstaller;
253
+ try {
254
+ const installer = fileURLToPath(new URL('../../../bin/install-codex-managed-hooks.mjs', import.meta.url));
255
+ managedHookInstaller = createRequire(import.meta.url)(installer);
256
+ if (typeof managedHookInstaller.managedBinding !== 'function') {
257
+ throw new Error('managedBinding export is unavailable');
258
+ }
259
+ }
260
+ catch (error) {
261
+ const detail = error instanceof Error ? error.message : String(error);
262
+ return {
263
+ status: 'unsafe', requirements, managedDir, hookPath,
264
+ reason: `managed-hook installer could not be loaded: ${detail}`,
265
+ };
266
+ }
253
267
  const configDirState = realDirectoryState(path.dirname(requirements));
254
268
  const managedDirState = realDirectoryState(managedDir);
255
269
  if ([configDirState, managedDirState].includes('unsafe')) {
@@ -312,9 +326,9 @@ export function inspectCodexManagedHook(options = {}) {
312
326
  catch (error) {
313
327
  return { status: 'unavailable', requirements, managedDir, hookPath, reason: error instanceof Error ? error.message : String(error) };
314
328
  }
315
- const expectedPackageRoot = path.resolve(options.packageRoot || process.cwd());
329
+ const expectedPackageRoot = path.resolve(process.env[TARGET_PACKAGE_ROOT_ENV] || options.packageRoot || process.cwd());
316
330
  try {
317
- const binding = managedBinding(source, launcher, hookPath);
331
+ const binding = managedHookInstaller.managedBinding(source, launcher, hookPath);
318
332
  if (!binding || config.allow_managed_hooks_only !== true || config.features?.hooks !== true
319
333
  || normalizedPath(config.hooks?.windows_managed_dir) !== normalizedPath(managedDir)
320
334
  || (config.hooks?.managed_dir !== undefined
@@ -382,9 +396,10 @@ export async function ensureCodexManagedHook(options) {
382
396
  });
383
397
  if (!paths)
384
398
  return { status: 'skipped', reason: 'Windows managed hooks are not applicable on this platform' };
399
+ const packageRoot = process.env[TARGET_PACKAGE_ROOT_ENV] || options.packageRoot;
385
400
  const inspectOptions = {
386
401
  platform: platformName,
387
- packageRoot: options.packageRoot,
402
+ packageRoot,
388
403
  requirements: options.requirements || paths.requirements,
389
404
  managedDir: options.managedDir || paths.managedDir,
390
405
  programData: options.programData,
@@ -414,7 +429,9 @@ export async function ensureCodexManagedHook(options) {
414
429
  || rechecked.repairAmbiguous !== state.repairAmbiguous) {
415
430
  return { ...rechecked, repaired: false, error: 'managed-hook state changed during the startup check; retry to confirm the current binding' };
416
431
  }
417
- const installer = path.join(path.resolve(options.packageRoot), 'bin', 'install-codex-managed-hooks.mjs');
432
+ const installer = process.env[TARGET_PACKAGE_ROOT_ENV]
433
+ ? fileURLToPath(new URL('../../../bin/install-codex-managed-hooks.mjs', import.meta.url))
434
+ : path.join(path.resolve(packageRoot), 'bin', 'install-codex-managed-hooks.mjs');
418
435
  if (!fs.existsSync(installer)) {
419
436
  return { ...state, repaired: false, error: `managed-hook installer is missing: ${installer}` };
420
437
  }
@@ -438,15 +455,29 @@ export async function ensureCodexManagedHook(options) {
438
455
  if (result.error)
439
456
  return { ok: false, detail: result.error.message };
440
457
  if (result.status !== 0) {
441
- const detail = (result.stderr || '').replace(/#<\s*CLIXML[\s\S]*?<\/Objs>\s*/gi, '').trim();
442
- return { ok: false, detail: detail || `installer exited with code ${result.status}` };
458
+ const stderrDetail = (result.stderr || '').replace(/#<\s*CLIXML[\s\S]*?<\/Objs>\s*/gi, '').trim();
459
+ // The elevated child process runs in a separate window and its stderr is
460
+ // not captured through the Start-Process pipe. Read the structured failure
461
+ // record the installer writes to ProgramData before exiting.
462
+ let fileDetail;
463
+ try {
464
+ const failurePath = path.join(path.dirname(path.resolve(inspectOptions.requirements || paths.requirements)), '.evolcore-install-failure.json');
465
+ const raw = fs.readFileSync(failurePath, 'utf8');
466
+ const parsed = JSON.parse(raw);
467
+ if (typeof parsed.message === 'string' && parsed.message)
468
+ fileDetail = parsed.message;
469
+ fs.rmSync(failurePath, { force: true });
470
+ }
471
+ catch { /* best-effort */ }
472
+ const detail = fileDetail || stderrDetail || `installer exited with code ${result.status}`;
473
+ return { ok: false, detail };
443
474
  }
444
475
  return { ok: true };
445
476
  });
446
477
  options.onInstallRequired?.(state);
447
478
  let elevated;
448
479
  try {
449
- elevated = runElevated(installer, path.resolve(options.packageRoot));
480
+ elevated = runElevated(installer, path.resolve(packageRoot));
450
481
  }
451
482
  catch (error) {
452
483
  elevated = { ok: false, detail: error instanceof Error ? error.message : String(error) };
@@ -88,6 +88,24 @@ class AsyncEventQueue {
88
88
  }
89
89
  }
90
90
  }
91
+ function parseCodexRetryProgress(message) {
92
+ const match = message.match(/\bReconnecting\.\.\.\s*(\d+)\s*\/\s*(\d+)\b/i);
93
+ if (!match)
94
+ return undefined;
95
+ const count = Number(match[1]);
96
+ const limit = Number(match[2]);
97
+ if (!Number.isSafeInteger(count) || !Number.isSafeInteger(limit) || count < 1 || limit < count)
98
+ return undefined;
99
+ return { count, limit };
100
+ }
101
+ function createCodexRetryMetadataError(message, state) {
102
+ const error = new Error(message);
103
+ if (state.modelRetryCount !== undefined)
104
+ error.codexRetryCount = state.modelRetryCount;
105
+ if (state.modelRetryLimit !== undefined)
106
+ error.codexRetryLimit = state.modelRetryLimit;
107
+ return error;
108
+ }
91
109
  function fallbackErrorCodeForToolDenial(decisionSource, policyCode) {
92
110
  if (decisionSource === 'policy' && policyCode === 'apply_patch_invalid_argument')
93
111
  return 'INVALID_ARGUMENT';
@@ -4931,7 +4949,7 @@ export class CodexRunner {
4931
4949
  lastReportedUsage: state.tokenUsage?.last,
4932
4950
  })}`);
4933
4951
  if (isRetryableError(new Error(turn.error.message))) {
4934
- throw new Error(turn.error.message);
4952
+ throw createCodexRetryMetadataError(turn.error.message, state);
4935
4953
  }
4936
4954
  yield { type: 'error', error: turn.error.message, errorType: toAgentErrorType(turn.error.message) };
4937
4955
  }
@@ -4953,6 +4971,11 @@ export class CodexRunner {
4953
4971
  // Codex owns retries within this turn. Throwing here closes our subscription
4954
4972
  // before its recovery can finish; runQuery must not submit the input again.
4955
4973
  if (params.willRetry === true) {
4974
+ const retryProgress = parseCodexRetryProgress(message);
4975
+ if (retryProgress) {
4976
+ state.modelRetryCount = Math.max(state.modelRetryCount ?? 0, retryProgress.count);
4977
+ state.modelRetryLimit = retryProgress.limit;
4978
+ }
4956
4979
  yield {
4957
4980
  type: 'notice', severity: 'warn', subtype: 'codex-model-retry',
4958
4981
  message: isOutputTokenLimitError(new Error(message))
@@ -4961,8 +4984,9 @@ export class CodexRunner {
4961
4984
  };
4962
4985
  break;
4963
4986
  }
4964
- if (isRetryableError(new Error(message)))
4965
- throw new Error(message);
4987
+ if (isRetryableError(new Error(message))) {
4988
+ throw createCodexRetryMetadataError(message, state);
4989
+ }
4966
4990
  yield { type: 'error', error: message, errorType: toAgentErrorType(message) };
4967
4991
  break;
4968
4992
  }
@@ -10,7 +10,7 @@ import { DEFAULT_PERMISSION_MODE } from '../types.js';
10
10
  import { agentSessionsDir, resolveRoot } from '../paths.js';
11
11
  import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
12
12
  import { normalizeExecutionPermissionMode, normalizePermissionMode } from '../core/permission/mode.js';
13
- import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
13
+ import { checkBashDescription, checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
14
14
  import { logger } from '../utils/logger.js';
15
15
  import { ErrorType } from '../utils/error-utils.js';
16
16
  import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
@@ -700,7 +700,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
700
700
  name: 'Bash', label: 'Bash', description: 'Run a shell command in the project.',
701
701
  parameters: Type.Object({
702
702
  command: Type.String(), timeout: Type.Optional(Type.Number({ minimum: 1, maximum: 600_000 })),
703
- description: Type.Optional(Type.String()),
703
+ description: Type.Optional(Type.String({ description: '<命令目标描述> [<风险等级>],风险等级为 低/中/高 之一,例如 "查看工作区未提交的改动 [低]"。缺失或格式不符时调用被拒。' })),
704
704
  }), executionMode: 'sequential',
705
705
  async execute(_id, params, signal) {
706
706
  const result = await env.exec(params.command, {
@@ -1214,11 +1214,6 @@ export class EcagentRunner {
1214
1214
  }
1215
1215
  async authorizeTool(sessionId, projectPath, mode, toolName, input, turn) {
1216
1216
  const permissionContext = this.permissionContexts.get(sessionId);
1217
- if (mode === 'fullaccess') {
1218
- return hasTrustedFullAccessContext(permissionContext)
1219
- ? {}
1220
- : { block: true, reason: 'Untrusted fullaccess execution context' };
1221
- }
1222
1217
  const recordBlockedOperation = async (policyCode, operationInput = input, matchedPath) => {
1223
1218
  const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
1224
1219
  auditToolPreflightDenial({
@@ -1257,6 +1252,21 @@ export class EcagentRunner {
1257
1252
  logger.warn(`[EcagentRunner] failed to record blocked operation: session=${sessionId} tool=${toolName} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
1258
1253
  }
1259
1254
  };
1255
+ // Bash description 格式约定先于一切放行分支(含 fullaccess 与系统会话)。
1256
+ // TODO(bash-description): 与 claude-runner 同因——该约束缺少验证,先短路放行;
1257
+ // 恢复强制时把两个 runner 的开关一起打开。
1258
+ if (process.env.EVOLCORE_BASH_DESCRIPTION_ENFORCE === '1') {
1259
+ const descriptionCheck = checkBashDescription(toolName, input);
1260
+ if (descriptionCheck) {
1261
+ await recordBlockedOperation(descriptionCheck.policyCode);
1262
+ return { block: true, reason: descriptionCheck.message };
1263
+ }
1264
+ }
1265
+ if (mode === 'fullaccess') {
1266
+ return hasTrustedFullAccessContext(permissionContext)
1267
+ ? {}
1268
+ : { block: true, reason: 'Untrusted fullaccess execution context' };
1269
+ }
1260
1270
  // 系统会话:模块判定即最终结论,不进入 preflight / permissionMode 分支
1261
1271
  if (permissionContext?.systemSessionCheck) {
1262
1272
  const decision = permissionContext.systemSessionCheck(toolName, input);
@@ -179,8 +179,19 @@ export function parseAgentDisplayName(content) {
179
179
  const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
180
180
  if (!frontmatter)
181
181
  return undefined;
182
- const name = frontmatter.match(/^name:\s*["']?(.+?)["']?\s*$/m)?.[1]?.trim();
183
- return name || undefined;
182
+ try {
183
+ const document = parseDocument(frontmatter);
184
+ if (document.errors.length > 0)
185
+ return undefined;
186
+ const fields = document.toJS();
187
+ if (!fields || typeof fields !== 'object' || Array.isArray(fields))
188
+ return undefined;
189
+ const name = fields.name;
190
+ return typeof name === 'string' ? name.trim() || undefined : undefined;
191
+ }
192
+ catch {
193
+ return undefined;
194
+ }
184
195
  }
185
196
  function displayNameKey(aid, aunPath) {
186
197
  const root = path.resolve(aunPath ?? resolveRoot());
@@ -39,7 +39,7 @@ export function suppressSdkLogs() {
39
39
  };
40
40
  }
41
41
  // ==================== SDK package ====================
42
- export const MIN_AUN_CORE_SDK = [0, 5, 17];
42
+ export const MIN_AUN_CORE_SDK = [0, 5, 20];
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));
@@ -597,6 +597,9 @@ export class ManagedMsgOperationError extends Error {
597
597
  }
598
598
  const HISTORY_VALUE_FLAGS = new Set([
599
599
  '--session', '--limit', '--before', '--after', '--direction', '--format', '--channel-key', '--peer-id',
600
+ // 话题过滤是托管协作的常规用法(文档 `thread-chain.md` 明确写了
601
+ // `ec msg history --thread <id>`);只会收窄结果集,不扩大可读范围。
602
+ '--thread',
600
603
  ]);
601
604
  function failMsg(code, reason) {
602
605
  return { ok: false, code, reason };
@@ -720,6 +723,15 @@ export function resolveManagedMsgOperation(argv) {
720
723
  return failMsg('INVALID_ARGUMENT', '--after must be earlier than --before');
721
724
  }
722
725
  const target = channelKey === undefined ? String(positionals[0] || '').replace(/^@/, '') : undefined;
726
+ const threadId = values.get('--thread');
727
+ if (threadId !== undefined) {
728
+ if (kind !== 'history')
729
+ return failMsg('INVALID_ARGUMENT', '--thread is only supported for history');
730
+ if (channelKey !== undefined)
731
+ return failMsg('INVALID_ARGUMENT', '--thread cannot be combined with --channel-key/--peer-id');
732
+ if (!/^thread-[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(threadId))
733
+ return failMsg('INVALID_ARGUMENT', 'invalid --thread id');
734
+ }
723
735
  const canonicalArgv = ['msg', kind, from];
724
736
  if (target !== undefined)
725
737
  canonicalArgv.push(target);
@@ -735,6 +747,8 @@ export function resolveManagedMsgOperation(argv) {
735
747
  canonicalArgv.push('--after', values.get('--after'));
736
748
  if (values.has('--direction'))
737
749
  canonicalArgv.push('--direction', values.get('--direction'));
750
+ if (threadId !== undefined)
751
+ canonicalArgv.push('--thread', threadId);
738
752
  return {
739
753
  ok: true,
740
754
  command: {
@@ -749,6 +763,7 @@ export function resolveManagedMsgOperation(argv) {
749
763
  before,
750
764
  after,
751
765
  direction,
766
+ ...(threadId !== undefined ? { threadId } : {}),
752
767
  },
753
768
  };
754
769
  }
@@ -766,6 +781,7 @@ export async function executeManagedMsgOperation(command) {
766
781
  before: command.before,
767
782
  after: command.after,
768
783
  direction: command.direction,
784
+ threadId: command.threadId,
769
785
  })
770
786
  : await msgOnline({
771
787
  from: command.from,
@@ -279,8 +279,24 @@ export async function msgSend(args) {
279
279
  catch { /* ignore */ }
280
280
  }
281
281
  // 2. 决定 chatmode(遵循来源1-3)
282
- // 私聊:非 human 对端 → proactive,human 对端 → interactive
283
- const chatmode = peerIdentity.isAgent ? 'proactive' : 'interactive';
282
+ // 私聊:非 human 对端 → proactive;人类对端只有 owner/admin 才 interactive,
283
+ // 其余(含角色未知)按 proactive 保守处理。判定逻辑集中在 peer-mode。
284
+ const { chatmodeForPeerIdentity } = await import('../../core/message/peer-mode.js');
285
+ const { resolvePeerRoleDetail } = await import('../../config/peer-role-resolver.js');
286
+ let peerRole = null;
287
+ try {
288
+ peerRole = resolvePeerRoleDetail({
289
+ selfAid: args.from,
290
+ channelKey: `aun#${args.from}#main`,
291
+ channelType: 'aun',
292
+ chatType: 'private',
293
+ actorId: args.to,
294
+ conversationId: args.to,
295
+ peerType: peerIdentity.type,
296
+ }).effectiveRole;
297
+ }
298
+ catch { /* 角色解析失败时按未知处理(proactive) */ }
299
+ const chatmode = chatmodeForPeerIdentity(peerIdentity, peerRole);
284
300
  // 3. 构建 payload
285
301
  if (!payload) {
286
302
  payload = buildSimplePayload(args.body);
@@ -557,10 +557,23 @@ function setIfDefined(target, key, value) {
557
557
  if (value !== undefined)
558
558
  target[key] = value;
559
559
  }
560
+ function truncateRawErrorForDisplay(raw) {
561
+ if (typeof raw !== 'string' || !raw.trim())
562
+ return undefined;
563
+ const text = raw.trim();
564
+ const htmlStart = text.toLowerCase().indexOf('<html');
565
+ return htmlStart >= 0 ? text.slice(0, htmlStart).trimEnd() : text;
566
+ }
567
+ function projectedRawError(raw, displayRaw) {
568
+ if (typeof raw !== 'string')
569
+ return raw;
570
+ return raw.trim() ? displayRaw : raw;
571
+ }
560
572
  function appendRawErrorText(text, raw) {
561
- if (typeof raw !== 'string' || !raw.trim() || text.includes(raw))
573
+ const displayRaw = truncateRawErrorForDisplay(raw);
574
+ if (!displayRaw || text.includes(displayRaw))
562
575
  return text;
563
- return `${text}\n原因:${raw}`;
576
+ return `${text}\n原因:${displayRaw}`;
564
577
  }
565
578
  /**
566
579
  * 把 AUNChannel 投递的 opts 映射成渠道无关的 InboundMessage。
@@ -667,6 +680,8 @@ export class AUNChannel {
667
680
  traceWriter = null;
668
681
  eventBus = null;
669
682
  queuedHandler = null;
683
+ /** `topic-renamed` 消费器,由 runtime 注入(见 setThreadRenameHandler)。 */
684
+ threadRenameHandler = null;
670
685
  groupInviteHandler;
671
686
  groupInviteConnectedHandler;
672
687
  groupInviteDeliveryGate;
@@ -1396,6 +1411,44 @@ export class AUNChannel {
1396
1411
  return sanitized;
1397
1412
  return undefined;
1398
1413
  }
1414
+ /**
1415
+ * 处理对端 `topic-renamed` 通知:校验字段后交给注入的改名消费器落索引,
1416
+ * 成功后发 `thread:renamed` 让前端刷新。全程不进入 Agent 消息队列。
1417
+ * 通知是尽力而为的,任何失败只记日志,不影响后续消息处理。
1418
+ */
1419
+ applyInboundTopicRename(params) {
1420
+ const payload = params.payload && typeof params.payload === 'object' ? params.payload : null;
1421
+ const threadId = typeof payload?.threadId === 'string' ? payload.threadId : undefined;
1422
+ const name = this.sanitizeTopicName(payload?.topicName);
1423
+ const nameUpdatedAt = typeof payload?.nameUpdatedAt === 'number' && Number.isFinite(payload.nameUpdatedAt)
1424
+ ? payload.nameUpdatedAt : undefined;
1425
+ const applier = this.threadRenameHandler;
1426
+ if (!applier || !threadId || !name || nameUpdatedAt === undefined || !params.peer) {
1427
+ logger.debug(`${this.logPrefix()} topic-renamed ignored: peer=${params.peer || '<none>'} thread=${threadId || '<none>'} hasName=${!!name}`);
1428
+ return;
1429
+ }
1430
+ void applier({
1431
+ selfAid: params.selfAid, peer: params.peer, peerKind: params.peerKind,
1432
+ threadId, name, nameUpdatedAt,
1433
+ }).then(result => {
1434
+ if (!result.applied) {
1435
+ logger.debug(`${this.logPrefix()} topic-renamed not applied (${result.reason ?? 'unknown'}): thread=${threadId}`);
1436
+ return;
1437
+ }
1438
+ this.eventBus?.publish?.({
1439
+ type: 'thread:renamed',
1440
+ sessionId: result.sessionId,
1441
+ peer: params.peer,
1442
+ threadId,
1443
+ name,
1444
+ nameUpdatedAt,
1445
+ by: params.senderAid ?? '',
1446
+ });
1447
+ logger.info(`${this.logPrefix()} topic-renamed applied: thread=${threadId} name=${JSON.stringify(name)}`);
1448
+ }).catch(error => {
1449
+ logger.warn(`${this.logPrefix()} topic-renamed failed: ${error instanceof Error ? error.message : String(error)}`);
1450
+ });
1451
+ }
1399
1452
  buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, label, topicStatus) {
1400
1453
  if (!groupId)
1401
1454
  throw new Error('buildGroupReplyContext requires groupId');
@@ -2524,6 +2577,14 @@ export class AUNChannel {
2524
2577
  if (p2pPayloadType === 'text') {
2525
2578
  import('../utils/msg-diag.js').then(m => m.diagLog('aun_in', { aid: this.config.aid, from: fromAid, to: this.config.aid, msgId: messageId, text })).catch(() => { });
2526
2579
  }
2580
+ // topic-renamed:对端话题改名通知。只更新本端话题索引,不进 Agent 队列。
2581
+ if (p2pPayloadType === 'topic-renamed') {
2582
+ this.applyInboundTopicRename({
2583
+ payload, selfAid: this.config.aid, peer: fromAid, peerKind: 'private',
2584
+ senderAid: fromAid,
2585
+ });
2586
+ return;
2587
+ }
2527
2588
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
2528
2589
  if (p2pPayloadType === 'action_card_reply') {
2529
2590
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
@@ -2655,6 +2716,14 @@ export class AUNChannel {
2655
2716
  logger.debug(`${this.logPrefix()} Group dropped observer control envelope: mid=${messageId}`);
2656
2717
  return;
2657
2718
  }
2719
+ // topic-renamed:群内话题改名通知。只更新本端索引,不进 Agent 队列。
2720
+ if (inboundType === 'topic-renamed') {
2721
+ this.applyInboundTopicRename({
2722
+ payload, selfAid: this.config.aid, peer: groupId || senderAid, peerKind: 'group',
2723
+ senderAid,
2724
+ });
2725
+ return;
2726
+ }
2658
2727
  const payloadMentionEntries = this.parsePayloadMentionsOrReject(payload, 'group.inbound', messageId);
2659
2728
  if (!payloadMentionEntries)
2660
2729
  return;
@@ -3765,6 +3834,13 @@ export class AUNChannel {
3765
3834
  }, delayMs);
3766
3835
  }
3767
3836
  // ── Public API (same interface as before) ───────────────────
3837
+ /**
3838
+ * 对端发来的话题改名通知(`topic-renamed`)。由 runtime 注入,交给
3839
+ * ThreadService.applyRemoteRename 落本地索引;不进入 Agent 消息队列。
3840
+ */
3841
+ setThreadRenameHandler(handler) {
3842
+ this.threadRenameHandler = handler;
3843
+ }
3768
3844
  setEventBus(bus) {
3769
3845
  // 重新订阅前先解掉旧的——避免 reload/重连后 listener 累积
3770
3846
  if (this.eventBus && this.queuedHandler && typeof this.eventBus.unsubscribe === 'function') {
@@ -4611,7 +4687,23 @@ export class AUNChannel {
4611
4687
  return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
4612
4688
  }
4613
4689
  const info = await this.fetchPeerInfo(args.to);
4614
- const chatmode = info.type === 'human' ? 'interactive' : 'proactive';
4690
+ // 与 p2p 发送路径共用同一判定:非人类 → proactive;人类需 owner/admin 才 interactive。
4691
+ const { chatmodeForPeerIdentity } = await import('../core/message/peer-mode.js');
4692
+ let peerRole = null;
4693
+ try {
4694
+ const { resolvePeerRoleDetail } = await import('../config/peer-role-resolver.js');
4695
+ peerRole = resolvePeerRoleDetail({
4696
+ selfAid: this.config.aid,
4697
+ channelKey: `aun#${this.config.aid}#main`,
4698
+ channelType: 'aun',
4699
+ chatType: 'private',
4700
+ actorId: args.to,
4701
+ conversationId: args.to,
4702
+ peerType: info.type ?? undefined,
4703
+ }).effectiveRole;
4704
+ }
4705
+ catch { /* 角色解析失败按未知处理 */ }
4706
+ const chatmode = chatmodeForPeerIdentity({ isMachine: info.type !== 'human', type: info.type ?? undefined }, peerRole);
4615
4707
  if (!finalPayload.chatmode)
4616
4708
  finalPayload.chatmode = chatmode;
4617
4709
  const parentCausation = normalizeCausation(args.causation);
@@ -6426,14 +6518,16 @@ export class AUNChannelPlugin {
6426
6518
  return;
6427
6519
  }
6428
6520
  case 'system.error': {
6521
+ const raw = truncateRawErrorForDisplay(payload.raw);
6522
+ const projectedRaw = projectedRawError(payload.raw, raw);
6429
6523
  const errorPayload = {
6430
6524
  type: payload.recoverable === true ? 'notice' : 'error',
6431
6525
  ...taskBase(),
6432
6526
  subtype: payload.subtype,
6433
6527
  text: payload.recoverable === true
6434
6528
  ? payload.text
6435
- : appendRawErrorText(payload.text, payload.raw),
6436
- ...(payload.raw !== undefined ? { raw: payload.raw } : {}),
6529
+ : appendRawErrorText(payload.text, raw),
6530
+ ...(projectedRaw !== undefined ? { raw: projectedRaw } : {}),
6437
6531
  ...(payload.reason !== undefined ? { reason: payload.reason } : {}),
6438
6532
  ...(payload.recoverable === true
6439
6533
  ? { severity: 'warn' }
@@ -6443,14 +6537,16 @@ export class AUNChannelPlugin {
6443
6537
  return;
6444
6538
  }
6445
6539
  case 'result.error': {
6540
+ const raw = truncateRawErrorForDisplay(payload.raw);
6541
+ const projectedRaw = projectedRawError(payload.raw, raw);
6446
6542
  const errorPayload = {
6447
6543
  type: 'error',
6448
6544
  ...taskBase(),
6449
- text: appendRawErrorText(payload.text, payload.raw),
6545
+ text: appendRawErrorText(payload.text, raw),
6450
6546
  };
6451
6547
  setIfDefined(errorPayload, 'subtype', payload.subtype);
6452
6548
  setIfDefined(errorPayload, 'reason', payload.reason);
6453
- setIfDefined(errorPayload, 'raw', payload.raw);
6549
+ setIfDefined(errorPayload, 'raw', projectedRaw);
6454
6550
  await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6455
6551
  return;
6456
6552
  }
@@ -6776,6 +6872,16 @@ export class AUNChannelPlugin {
6776
6872
  },
6777
6873
  registerHooks(hookCtx) {
6778
6874
  channel.setEventBus(hookCtx.eventBus);
6875
+ // 话题改名通知消费器:直接改本端索引,不经过 Agent 队列。
6876
+ channel.setThreadRenameHandler(async (input) => {
6877
+ const { ThreadService } = await import('../core/thread/thread-service.js');
6878
+ const service = new ThreadService(hookCtx.sessionManager);
6879
+ const result = await service.applyRemoteRename(input);
6880
+ const session = result.applied
6881
+ ? service.findInIndex(input.selfAid, input.peer, input.threadId)?.sessionId
6882
+ : undefined;
6883
+ return { applied: result.applied, reason: result.reason, sessionId: session };
6884
+ });
6779
6885
  if (channel.setOnChannelDown) {
6780
6886
  channel.setOnChannelDown(() => {
6781
6887
  hookCtx.eventBus.publish({