evolcore 0.0.32 → 0.0.33

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 (150) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/bin/install-codex-managed-hooks.mjs +124 -37
  3. package/dist/agents/claude-runner.js +29 -2
  4. package/dist/agents/codex/app-server-client.js +3 -0
  5. package/dist/agents/codex/diagnostics.js +76 -44
  6. package/dist/agents/codex-runner.js +23 -19
  7. package/dist/agents/ecagent-runner.js +5 -13
  8. package/dist/aun/group/operations.js +0 -1
  9. package/dist/aun/msg/history.js +1 -0
  10. package/dist/aun/msg/p2p.js +0 -1
  11. package/dist/aun/outbox.js +50 -1
  12. package/dist/channels/aun.js +28 -2
  13. package/dist/channels/daemon.js +257 -2
  14. package/dist/cli/agent-command.js +58 -24
  15. package/dist/cli/agent.js +1 -1
  16. package/dist/cli/aun-commands.js +365 -29
  17. package/dist/cli/contact.js +8 -2
  18. package/dist/cli/daemon-commands.js +81 -12
  19. package/dist/cli/debug-mode.js +154 -0
  20. package/dist/cli/handoff-command.js +3 -0
  21. package/dist/cli/index.js +81 -64
  22. package/dist/cli/init.js +2 -2
  23. package/dist/cli/managed-command-guard.js +5 -0
  24. package/dist/cli/memory-command.js +8 -1
  25. package/dist/cli/queue-command.js +9 -4
  26. package/dist/cli/session-command.js +194 -0
  27. package/dist/cli/skill-command.js +78 -2
  28. package/dist/cli/task-context.js +1 -0
  29. package/dist/cli/thread-command.js +620 -0
  30. package/dist/cli/trigger-command.js +4 -2
  31. package/dist/config/builtin-roles.js +1 -0
  32. package/dist/config/config-field-policy.js +2 -3
  33. package/dist/config/config-manager.js +7 -2
  34. package/dist/config/resolved-config-op.js +3 -0
  35. package/dist/config-store.js +15 -0
  36. package/dist/core/auth/agent-delegation.js +25 -1
  37. package/dist/core/auth/debug-context.js +139 -0
  38. package/dist/core/auth/operation-authorizer.js +24 -1
  39. package/dist/core/auth/operation-catalog.js +33 -0
  40. package/dist/core/background-session/session-cleanup.js +212 -0
  41. package/dist/core/background-session/types.js +10 -0
  42. package/dist/core/bootstrap-messages.js +1 -1
  43. package/dist/core/bootstrap-service.js +6 -3
  44. package/dist/core/command/command-handler.js +60 -24
  45. package/dist/core/command/memory-slash.js +6 -2
  46. package/dist/core/command/menu-catalog.js +48 -1
  47. package/dist/core/command/menu-handler.js +92 -16
  48. package/dist/core/command/menu-protocol.js +24 -1
  49. package/dist/core/command/skill-slash.js +322 -0
  50. package/dist/core/command/slash-gate.js +6 -4
  51. package/dist/core/command/slash-handler.js +193 -13
  52. package/dist/core/command/topic-slash.js +528 -0
  53. package/dist/core/evolagent-registry.js +20 -5
  54. package/dist/core/handoff/runtime.js +5 -11
  55. package/dist/core/handoff/store.js +5 -0
  56. package/dist/core/ipc-registration.js +139 -2
  57. package/dist/core/memory/consolidation-context.js +49 -16
  58. package/dist/core/memory/consolidation-ops-records.js +35 -0
  59. package/dist/core/memory/consolidation-queue-format.js +21 -0
  60. package/dist/core/memory/consolidation.js +174 -180
  61. package/dist/core/message/aun-ipc-sender.js +161 -6
  62. package/dist/core/message/im-renderer.js +149 -6
  63. package/dist/core/message/message-bridge.js +13 -3
  64. package/dist/core/message/message-log.js +1 -0
  65. package/dist/core/message/peer-mode.js +6 -1
  66. package/dist/core/message/response-engine.js +349 -67
  67. package/dist/core/message/subtask-tool-projection.js +140 -0
  68. package/dist/core/model/catalog-service.js +17 -12
  69. package/dist/core/permission/ec-command-parser.js +126 -7
  70. package/dist/core/session/session-idle-monitor.js +249 -22
  71. package/dist/core/session/session-manager.js +54 -9
  72. package/dist/core/skill/command-parser.js +13 -0
  73. package/dist/core/skill/index-generator.js +4 -1
  74. package/dist/core/skill/startup.js +41 -0
  75. package/dist/core/skill/switch-resolver.js +55 -29
  76. package/dist/core/thread/command-parser.js +125 -0
  77. package/dist/core/thread/index.js +1 -0
  78. package/dist/core/thread/ipc-operation.js +95 -0
  79. package/dist/core/thread/thread-context-storage.js +131 -0
  80. package/dist/core/thread/thread-operation.js +258 -0
  81. package/dist/core/thread/thread-route.js +33 -0
  82. package/dist/core/thread/thread-service.js +777 -0
  83. package/dist/core/thread/types.js +1 -0
  84. package/dist/eck/message-renderer.js +35 -0
  85. package/dist/index.js +8 -5
  86. package/dist/ipc.js +89 -10
  87. package/dist/paths.js +23 -0
  88. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  89. package/dist/trigger/authorization.js +57 -28
  90. package/dist/trigger/feedback.js +7 -1
  91. package/dist/trigger/parser.js +63 -1
  92. package/dist/trigger/validation.js +14 -0
  93. package/dist/utils/error-utils.js +22 -1
  94. package/kits/docs/context-assembly.md +1 -0
  95. package/kits/docs/evolcore/INDEX.md +1 -0
  96. package/kits/docs/evolcore/agent-create.md +39 -28
  97. package/kits/docs/evolcore/agent.md +19 -4
  98. package/kits/docs/evolcore/ec-capabilities.md +4 -0
  99. package/kits/docs/evolcore/group-collaboration.md +3 -5
  100. package/kits/docs/evolcore/group.md +3 -4
  101. package/kits/docs/evolcore/memory-consolidation.md +1 -1
  102. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  103. package/kits/docs/evolcore/menu-protocol.md +7 -0
  104. package/kits/docs/evolcore/msg.md +5 -8
  105. package/kits/docs/evolcore/thread-chain.md +267 -0
  106. package/kits/docs/prompt-loading-architecture.md +1 -0
  107. package/kits/eck_manifest.json +61 -26
  108. package/kits/eck_message_manifest.json +169 -10
  109. package/kits/rules/05-venue.md +1 -1
  110. package/kits/rules/07-memory-and-thread.md +202 -0
  111. package/kits/schemas/relation-config.schema.9.json +18 -3
  112. package/kits/schemas/role-config.schema.1.json +6 -6
  113. package/kits/schemas/single-session.schema.1.json +6 -6
  114. package/kits/schemas/single-session.schema.2.json +6 -6
  115. package/kits/schemas/single-session.schema.3.json +6 -6
  116. package/kits/skills/.switches.json +12 -1
  117. package/kits/skills/ec-group/SKILL.md +234 -0
  118. package/kits/skills/ec-skill/SKILL.md +10 -1
  119. package/kits/skills/ec-skill/scripts/cli.mjs +15 -1
  120. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  121. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  122. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  123. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  124. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  125. package/kits/skills/thread-ops/SKILL.md +492 -0
  126. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  127. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  128. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  129. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  130. package/kits/templates/bootstrap-welcome.md +7 -7
  131. package/kits/templates/memory-consolidation/forget.md +5 -0
  132. package/kits/templates/memory-consolidation/generalize.md +5 -0
  133. package/kits/templates/memory-consolidation/profile.md +5 -0
  134. package/kits/templates/memory-consolidation/promote.md +5 -0
  135. package/kits/templates/message-fragments/item.md +2 -1
  136. package/kits/templates/message-fragments/thread-context.md +6 -0
  137. package/kits/templates/message-fragments/trigger-session.md +2 -0
  138. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  139. package/kits/templates/roles/member.json +2 -0
  140. package/kits/templates/roles/visitor.json +2 -0
  141. package/kits/templates/system-fragments/bootstrap.md +66 -72
  142. package/kits/templates/system-fragments/channel.md +3 -6
  143. package/kits/templates/system-fragments/commands.md +1 -0
  144. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  145. package/kits/templates/system-fragments/reply.md +3 -2
  146. package/kits/templates/system-fragments/session.md +5 -14
  147. package/package.json +3 -1
  148. package/kits/docs/evolcore/group-fs.md +0 -17
  149. package/kits/docs/venues/aun-group.md +0 -45
  150. package/kits/docs/venues/group.md +0 -43
package/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.0.33 (2026-10-15)
9
+
10
+ ### 话题与会话
11
+
12
+ - 新增话题管理与跨话题协作,支持查询历史、维护摘要和状态、删除与恢复话题,并通过定时复查跟进跨话题消息。
13
+ - 新增当前主会话和话题的闲置触发设置,可通过聊天命令或菜单单独开启、关闭和调整等待时间。
14
+ - 支持按会话编号查找本地存档,完善话题上下文衔接和后台会话清理。
15
+ - 改进消息中断与任务恢复,避免已提交输入重复执行或未提交输入丢失。
16
+ - 优化后台任务与子智能体的状态展示,将关联任务结果合并到原工具卡片,减少重复提示,并正确展示耗时及完成、失败、停止等状态。
17
+
18
+ ### Agent 与模型
19
+
20
+ - 简化首次设定:沿用已有有效资料,仅确认缺失、歧义或新增内容;人格和头像可稍后完善,正式欢迎文案由 Agent 自主准备。
21
+ - 完善模型推理强度选项,在模型目录未提供能力信息时按基座支持范围提供选项,并统一角色与关系设置。
22
+ - 强化 Codex 安装与启动检查,支持修复失效绑定,切换到另一套有效安装路径前要求明确确认。
23
+ - 改进 Codex 工具结果诊断,区分策略拒绝、用户拒绝和实际执行结果,减少错误归因。
24
+
25
+ ### 权限与运行稳定性
26
+
27
+ - 强化受管命令、话题操作及个人和群文件访问的身份与目标校验,发送被拒时提供更明确的原因。
28
+ - 无人值守任务不再请求交互审批,任务恢复与跨会话交接沿用原有审批策略。
29
+ - 查看任务运行上下文不再消耗一次性授权,避免后续操作重复审批。
30
+ - 修复重启提示被恢复后的进程重复投递的问题,并在无法判断错误原因时保留原始诊断信息。
31
+
8
32
  ## 0.0.32 (2026-10-12)
9
33
 
10
34
  - 统一受管命令的原始参数与审批哈希校验,按会话项目目录解析 Agent 初始化、人格和头像文件,强化授权后的文件访问边界。
@@ -3,7 +3,7 @@
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
- import { randomUUID } from 'node:crypto';
6
+ import { createHash, randomUUID } from 'node:crypto';
7
7
  import { execFileSync } from 'node:child_process';
8
8
  import { fileURLToPath, pathToFileURL } from 'node:url';
9
9
  import { isDeepStrictEqual } from 'node:util';
@@ -285,8 +285,55 @@ export function samePackageRoot(embedded, expected) {
285
285
  return normalize(embedded) === normalize(expected) || resolved(embedded) === resolved(expected);
286
286
  }
287
287
 
288
+ function normalizedPath(value) {
289
+ return path.normalize(String(value || '')).replace(/[\\/]+$/, '').toLowerCase();
290
+ }
291
+
292
+ function parseManagedCommand(value, hookPath) {
293
+ if (typeof value !== 'string') return null;
294
+ const match = /^"([^"\r\n]+)"\s+"([^"\r\n]+)"\s+%EVOLCORE_INSTANCE_SOCKET%$/i.exec(value.trim());
295
+ if (!match) return null;
296
+ const nodePath = match[1];
297
+ const commandHookPath = match[2];
298
+ if (!path.isAbsolute(nodePath) || /[%!]/.test(nodePath + commandHookPath)) return null;
299
+ if (normalizedPath(commandHookPath) !== normalizedPath(hookPath)) return null;
300
+ return { nodePath };
301
+ }
302
+
303
+ function runtimeFileMayExist(file) {
304
+ try { return fs.statSync(file).isFile(); }
305
+ catch (error) { return !['ENOENT', 'ENOTDIR'].includes(error?.code); }
306
+ }
307
+
308
+ export function managedBinding(source, launcher, hookPath) {
309
+ const embeddedPackageRoot = launcherPackageRoot(launcher);
310
+ if (!embeddedPackageRoot || !path.isAbsolute(embeddedPackageRoot)
311
+ || launcher.replaceAll('\r\n', '\n') !== managedHookLauncher(embeddedPackageRoot)) return null;
312
+ const retained = removeManagedBlock(source);
313
+ if (retained === source) return null;
314
+ const original = parseToml(source);
315
+ const retainedConfig = parseToml(retained);
316
+ const groups = original.hooks?.PreToolUse;
317
+ const retainedGroups = retainedConfig.hooks?.PreToolUse || [];
318
+ const owned = groups.filter(group => !retainedGroups.some(other => isDeepStrictEqual(group, other)));
319
+ if (owned.length !== 1) return null;
320
+ const group = owned[0];
321
+ if (group.matcher !== '*' || !Array.isArray(group.hooks) || group.hooks.length !== 1) return null;
322
+ const handler = group.hooks[0];
323
+ if (handler.type !== 'command' || handler.async === true) return null;
324
+ const command = parseManagedCommand(handler.command, hookPath);
325
+ const windowsCommand = parseManagedCommand(handler.command_windows, hookPath);
326
+ if (!command || !windowsCommand || normalizedPath(command.nodePath) !== normalizedPath(windowsCommand.nodePath)) return null;
327
+ if (!isDeepStrictEqual(group, { matcher: '*', hooks: [{
328
+ type: 'command', command: handler.command, command_windows: handler.command_windows,
329
+ statusMessage: 'Checking EvolCore session policy',
330
+ }] })) return null;
331
+ if (retainedGroups.some(other => JSON.stringify(other).toLowerCase().includes('pre-tool-use.mjs'))) return null;
332
+ return { embeddedPackageRoot, managedNodePath: command.nodePath };
333
+ }
334
+
288
335
  /** Inspect only the canonical EvolCore managed-hook targets. */
289
- export function inspectManagedHookInstallation({ requirements, managedDir, packageRoot }) {
336
+ export function inspectManagedHookInstallation({ requirements, managedDir, packageRoot, nodePath = process.execPath }) {
290
337
  const canonicalRequirements = path.resolve(requirements);
291
338
  const canonicalManagedDir = path.resolve(managedDir);
292
339
  const hookPath = path.join(canonicalManagedDir, 'pre-tool-use.mjs');
@@ -297,11 +344,12 @@ export function inspectManagedHookInstallation({ requirements, managedDir, packa
297
344
  return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: `${directory} must be a real directory` };
298
345
  }
299
346
  } catch (error) {
300
- if (error?.code !== 'ENOENT') return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: error?.message || String(error) };
347
+ if (error?.code !== 'ENOENT') return { status: 'unavailable', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: error?.message || String(error) };
301
348
  }
302
349
  }
303
350
  const req = regularFile(canonicalRequirements);
304
351
  const hook = regularFile(hookPath);
352
+ if (req.error || hook.error) return { status: 'unavailable', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'unable to inspect requirements or launcher' };
305
353
  if (req.unsafe || hook.unsafe) return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'requirements or launcher is not a regular file' };
306
354
  if (!req.ok && hook.ok) return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'canonical launcher exists but requirements.toml is missing' };
307
355
  if (req.ok && !hook.ok) {
@@ -324,48 +372,81 @@ export function inspectManagedHookInstallation({ requirements, managedDir, packa
324
372
  }
325
373
  }
326
374
  if (!req.ok || !hook.ok) return { status: 'missing', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'requirements or launcher is missing' };
327
- let parsed;
328
- try { parsed = parseToml(fs.readFileSync(canonicalRequirements, 'utf8')); }
329
- catch (error) { return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: `requirements.toml is invalid: ${error?.message || String(error)}` }; }
330
- const configured = parsed.hooks?.windows_managed_dir ?? parsed.hooks?.managed_dir;
331
- const normalized = value => path.normalize(String(value || '')).toLowerCase();
332
- const launcher = fs.readFileSync(hookPath, 'utf8');
333
- const packageMarker = launcher.includes('EVOLCORE_MANAGED_HOOK_LAUNCHER')
334
- && samePackageRoot(launcherPackageRoot(launcher), path.resolve(packageRoot || process.cwd()));
335
- const preToolUse = parsed.hooks?.PreToolUse;
336
- const canonicalCommand = value => {
337
- if (typeof value !== 'string' || !value || /[\r\n;&|<>]/.test(value)) return false;
338
- const withoutSocket = value.replace(/\s+%EVOLCORE_INSTANCE_SOCKET%\s*$/i, '').trim();
339
- return withoutSocket.replace(/["']+$/, '').toLowerCase().endsWith(normalized(hookPath));
340
- };
341
- const loaded = Array.isArray(preToolUse) && preToolUse.some(group => group?.matcher === '*'
342
- && Array.isArray(group.hooks) && group.hooks.some(handler => handler?.type === 'command'
343
- && handler.async !== true && typeof handler.command === 'string'
344
- && canonicalCommand(handler.command)
345
- && canonicalCommand(handler.command_windows ?? handler.commandWindows)));
346
- const valid = parsed.allow_managed_hooks_only === true
347
- && parsed.features?.hooks === true
348
- && normalized(configured) === normalized(canonicalManagedDir)
349
- && packageMarker && loaded;
350
- return valid
351
- ? { status: 'installed', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath }
352
- : { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: 'managed-hook policy is missing, altered, or points outside the canonical target' };
353
- }
354
-
355
- /** Idempotently repair a missing canonical installation; unsafe edits fail closed. */
375
+ let source;
376
+ let launcher;
377
+ try {
378
+ source = fs.readFileSync(canonicalRequirements, 'utf8');
379
+ launcher = fs.readFileSync(hookPath, 'utf8');
380
+ } catch (error) {
381
+ return { status: 'unavailable', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: error?.message || String(error) };
382
+ }
383
+ try {
384
+ const parsed = parseToml(source);
385
+ const binding = managedBinding(source, launcher, hookPath);
386
+ if (!binding || parsed.allow_managed_hooks_only !== true || parsed.features?.hooks !== true
387
+ || normalizedPath(parsed.hooks?.windows_managed_dir) !== normalizedPath(canonicalManagedDir)
388
+ || (parsed.hooks?.managed_dir !== undefined
389
+ && normalizedPath(parsed.hooks.managed_dir) !== normalizedPath(canonicalManagedDir))) {
390
+ throw new Error('managed-hook policy is missing, altered, or points outside the canonical target');
391
+ }
392
+ const { embeddedPackageRoot, managedNodePath } = binding;
393
+ const packageMatches = samePackageRoot(embeddedPackageRoot, path.resolve(packageRoot || process.cwd()));
394
+ const nodeMatches = samePackageRoot(managedNodePath, nodePath);
395
+ const state = {
396
+ requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath,
397
+ managedPackageRoot: embeddedPackageRoot, managedNodePath,
398
+ repairFingerprint: createHash('sha256').update(source).update('\0').update(launcher).digest('hex'),
399
+ };
400
+ if (packageMatches && nodeMatches) return { ...state, status: 'installed' };
401
+ return {
402
+ ...state, status: 'stale',
403
+ repairAmbiguous: runtimeFileMayExist(path.join(embeddedPackageRoot, 'bin', 'codex-managed-hook.mjs'))
404
+ && runtimeFileMayExist(managedNodePath),
405
+ reason: !packageMatches && !nodeMatches
406
+ ? 'managed-hook points to a different EvolCore package root and Node runtime'
407
+ : !packageMatches ? 'managed-hook points to a different EvolCore package root'
408
+ : 'managed-hook points to a different Node runtime',
409
+ };
410
+ } catch (error) {
411
+ return { status: 'unsafe', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, reason: error?.message || String(error) };
412
+ }
413
+ }
414
+
415
+ /** Idempotently repair a missing or stale canonical installation; unsafe edits fail closed. */
356
416
  export function repairManagedHookInstallation(options, acl = windowsAcl) {
357
417
  const { requirements, managedDir, packageRoot, nodePath = process.execPath } = options;
358
418
  const canonicalRequirements = path.resolve(requirements);
359
419
  const canonicalManagedDir = path.resolve(managedDir);
360
420
  const hookPath = path.join(canonicalManagedDir, 'pre-tool-use.mjs');
361
421
  const sourceHook = path.join(packageRoot, 'bin', 'codex-managed-hook.mjs');
362
- const state = inspectManagedHookInstallation({ requirements: canonicalRequirements, managedDir: canonicalManagedDir, packageRoot });
422
+ const state = inspectManagedHookInstallation({ requirements: canonicalRequirements, managedDir: canonicalManagedDir, packageRoot, nodePath });
363
423
  if (state.status === 'installed') return { ...state, repaired: false, warnings: [] };
364
- if (state.status === 'unsafe') throw new Error(`Refusing automatic managed-hook repair: ${state.reason}`);
424
+ if (!['missing', 'stale'].includes(state.status)) throw new Error(`Refusing automatic managed-hook repair: ${state.reason}`);
425
+ if (state.status === 'stale' && (options.expectedFingerprint !== state.repairFingerprint
426
+ || (state.repairAmbiguous && options.confirmRebind !== true))) {
427
+ throw new Error('Refusing automatic managed-hook repair: binding changed or requires explicit confirmation');
428
+ }
365
429
  if (!fs.existsSync(sourceHook)) throw new Error(`Required Hook runtime file was not found: ${sourceHook}`);
366
430
  const previousContent = fs.existsSync(canonicalRequirements) ? fs.readFileSync(canonicalRequirements, 'utf8') : '';
367
431
  const content = buildManagedRequirements(previousContent, { targetDirectory: canonicalManagedDir, nodePath, hookPath });
368
- const warnings = provisionManagedFiles({ requirements: canonicalRequirements, hookPath, sourceHook, nodePath, previousContent, content, launcher: managedHookLauncher(packageRoot) }, acl);
432
+ const inspect = () => inspectManagedHookInstallation({
433
+ requirements: canonicalRequirements, managedDir: canonicalManagedDir, packageRoot, nodePath,
434
+ });
435
+ const warnings = provisionManagedFiles({
436
+ requirements: canonicalRequirements, hookPath, sourceHook, nodePath, previousContent, content,
437
+ launcher: managedHookLauncher(packageRoot),
438
+ beforePublish: () => {
439
+ const latest = inspect();
440
+ if (latest.status !== state.status || latest.repairFingerprint !== state.repairFingerprint
441
+ || latest.repairAmbiguous !== state.repairAmbiguous) {
442
+ throw new Error('Managed-hook state changed before publication; retry the startup check');
443
+ }
444
+ },
445
+ verifyPublished: () => {
446
+ const verified = inspect();
447
+ if (verified.status !== 'installed') throw new Error('Managed-hook verification failed: ' + verified.reason);
448
+ },
449
+ }, acl);
369
450
  return { status: 'installed', requirements: canonicalRequirements, managedDir: canonicalManagedDir, hookPath, repaired: true, warnings };
370
451
  }
371
452
 
@@ -475,6 +556,7 @@ export function provisionManagedFiles(options, acl = windowsAcl) {
475
556
  let committed = false;
476
557
  try {
477
558
  fs.writeFileSync(lock, JSON.stringify({ pid: process.pid, started: new Date().toISOString() }));
559
+ options.beforePublish?.();
478
560
  const existing = existingFile(requirements, fs);
479
561
  // Detect edits made after configuration generation, including a second
480
562
  // installer completing just before this process acquired the lock.
@@ -509,11 +591,13 @@ export function provisionManagedFiles(options, acl = windowsAcl) {
509
591
  const target = temporary ? entry.temporary : entry.file;
510
592
  if (fs.readFileSync(target, 'utf8') !== entry.value) throw new Error(`Installed content differs: ${target}`);
511
593
  }
512
- return acl('verify', { entries: [
594
+ const report = acl('verify', { entries: [
513
595
  { path: directory, protected: true, execute: true },
514
596
  ...staged.map(entry => ({ path: temporary ? entry.temporary : entry.file, protected: true })),
515
597
  ...readable,
516
598
  ] });
599
+ if (temporary === false) options.verifyPublished?.();
600
+ return report;
517
601
  };
518
602
  verify(true);
519
603
  let accessReport;
@@ -561,7 +645,10 @@ function main() {
561
645
  if (!fs.statSync(file).isFile()) fail(`Required Hook runtime file was not found: ${file}`);
562
646
  }
563
647
  if (hasArgument('--repair')) {
564
- const result = repairManagedHookInstallation({ requirements, managedDir: targetDirectory, packageRoot, nodePath });
648
+ const result = repairManagedHookInstallation({
649
+ requirements, managedDir: targetDirectory, packageRoot, nodePath,
650
+ expectedFingerprint: argument('--expected-fingerprint'), confirmRebind: hasArgument('--confirm-rebind'),
651
+ });
565
652
  process.stdout.write(`${JSON.stringify(result)}\n`);
566
653
  return;
567
654
  }
@@ -586,6 +586,8 @@ export class AgentRunner {
586
586
  permissionGateway;
587
587
  sendPromptFn;
588
588
  permissionContexts = new Map();
589
+ /** tool_use_id → 模型返回序号(turnCount),供 PreToolUse 钩子把 turn 传给 policyHook */
590
+ toolTurnByCallId = new Map();
589
591
  claudeExecutablePath;
590
592
  /** 每个 session 最近的子进程 stderr 行(环形缓冲),用于子进程崩溃时还原真正原因 */
591
593
  recentStderr = new Map();
@@ -714,6 +716,9 @@ export class AgentRunner {
714
716
  ANTHROPIC_API_KEY: this.apiKey,
715
717
  PATH: process.env.PATH,
716
718
  DISABLE_AUTOUPDATER: '1',
719
+ // 记忆只走 EvolCore(ec memory):关闭 Claude Code 原生 auto-memory 的
720
+ // 读写,避免两套记忆并存污染上下文。
721
+ CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1',
717
722
  ...(anthropicBaseUrl ? { ANTHROPIC_BASE_URL: anthropicBaseUrl } : {}),
718
723
  ...(this.customHeaders
719
724
  ? { ANTHROPIC_CUSTOM_HEADERS: Object.entries(this.customHeaders).map(([name, value]) => `${name}: ${value}`).join('\n') }
@@ -779,6 +784,9 @@ export class AgentRunner {
779
784
  settings: {
780
785
  ...settings,
781
786
  skillOverrides: { ...skillOverrides, eclink: 'off' },
787
+ // 记忆只走 EvolCore(ec memory):关闭 Claude Code 原生 auto-memory,
788
+ // 与 getAgentEnv 的 CLAUDE_CODE_DISABLE_AUTO_MEMORY 双保险。
789
+ autoMemoryEnabled: false,
782
790
  },
783
791
  };
784
792
  }
@@ -1765,6 +1773,8 @@ export class AgentRunner {
1765
1773
  if (content.type === 'tool_use') {
1766
1774
  if (content.id)
1767
1775
  toolUseNames.set(content.id, content.name);
1776
+ if (content.id)
1777
+ this.toolTurnByCallId.set(content.id, turnCount);
1768
1778
  if (content.id && content.name === 'Bash' && content.input?.dangerouslyDisableSandbox !== true
1769
1779
  && resolvePhaseOneExecutionSandbox(permissionMode, this.permissionContexts.get(sessionId)?.role).state !== 'off') {
1770
1780
  sandboxedBashCalls.add(content.id);
@@ -1772,7 +1782,9 @@ export class AgentRunner {
1772
1782
  yield { type: 'tool_use', name: content.name, input: content.input, callId: content.id, turn: turnCount, outputTokens: turnOutputChars, assistantUuid: event.uuid };
1773
1783
  }
1774
1784
  else if (content.type === 'text' && content.text) {
1775
- yield { type: 'text', text: content.text, outputTokens: turnOutputChars, turn: turnCount, assistantUuid: event.uuid };
1785
+ // 携带出身标记:`<synthetic>` assistant 是 API/传输故障的错误回显,
1786
+ // 消费端据此把它与模型正文区分开,不必靠字符串或计数猜。
1787
+ yield { type: 'text', text: content.text, outputTokens: turnOutputChars, turn: turnCount, assistantUuid: event.uuid, synthetic: isSyntheticAssistant || undefined };
1776
1788
  }
1777
1789
  }
1778
1790
  }
@@ -1787,6 +1799,8 @@ export class AgentRunner {
1787
1799
  : block.content != null ? JSON.stringify(block.content) : '';
1788
1800
  if (block.tool_use_id)
1789
1801
  emittedToolResultIds.add(block.tool_use_id);
1802
+ if (block.tool_use_id)
1803
+ this.toolTurnByCallId.delete(block.tool_use_id);
1790
1804
  if (sandboxedBashCalls.delete(block.tool_use_id) && block.is_error === true
1791
1805
  && isSandboxInitializationToolResult(block.content)) {
1792
1806
  const { detail } = await this.recordSandboxInfrastructureFailure(sessionId, block.tool_use_id, permissionMode, new Error(resultContent), [], 'session');
@@ -2359,7 +2373,20 @@ export class AgentRunner {
2359
2373
  });
2360
2374
  }
2361
2375
  // proactive 模式行为策略(首次工具调用必须是 ec msg send)
2362
- const policyResult = this.permissionContexts.get(sessionId)?.policyHook?.(toolName, toolInput);
2376
+ const sessionPolicyHook = this.permissionContexts.get(sessionId)?.policyHook;
2377
+ let policyResult;
2378
+ if (sessionPolicyHook) {
2379
+ // 钩子可能先于流里的 tool_use 事件到达:按 tool_use_id 取模型返回序号,取不到短等(最多 200ms)
2380
+ const policyCallId = (input.tool_use_id ?? input.toolUseID);
2381
+ let policyTurn = policyCallId ? this.toolTurnByCallId.get(policyCallId) : undefined;
2382
+ for (let waited = 0; policyCallId && policyTurn === undefined && waited < 200; waited += 20) {
2383
+ await new Promise(resolve => setTimeout(resolve, 20));
2384
+ policyTurn = this.toolTurnByCallId.get(policyCallId);
2385
+ }
2386
+ if (policyCallId)
2387
+ this.toolTurnByCallId.delete(policyCallId);
2388
+ policyResult = sessionPolicyHook(toolName, toolInput, { turn: policyTurn });
2389
+ }
2363
2390
  if (policyResult?.block) {
2364
2391
  await recordPreflightDenial(toolName, toolInput, undefined, input.tool_use_id ?? input.toolUseID, policyResult.policyCode ?? 'session_policy_hook');
2365
2392
  return hookDecision('deny', policyResult.reason || '当前会话策略拒绝此工具调用', updatedInput);
@@ -745,6 +745,9 @@ export class CodexAppServerClient {
745
745
  }
746
746
  buildProcessArgs() {
747
747
  const args = ['app-server', '--listen', 'stdio://'];
748
+ // 记忆只走 EvolCore(ec memory):显式关闭 Codex 原生 memories,
749
+ // 不依赖其默认值(当前 stable 默认 false,此处写死防未来翻转)。
750
+ args.push('--config', 'features.memories=false');
748
751
  if (this.options.enableRequestUserInput !== false) {
749
752
  args.push('--enable', 'default_mode_request_user_input');
750
753
  }
@@ -2,7 +2,9 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
+ import { createHash } from 'node:crypto';
5
6
  import { parse as parseToml } from 'smol-toml';
7
+ const managedHookInstaller = await import(new URL('../../../bin/install-codex-managed-hooks.mjs', import.meta.url).href);
6
8
  export function checkCodexProjectPath(value, options = {}) {
7
9
  const configured = typeof value === 'string' ? value.trim() : '';
8
10
  if (!configured) {
@@ -180,19 +182,6 @@ function realDirectoryState(directory) {
180
182
  return 'unavailable';
181
183
  }
182
184
  }
183
- /** Read back the package root that managedHookLauncher() embedded. */
184
- function launcherPackageRoot(launcher) {
185
- const match = /^\s*const packageRoot = ("(?:[^"\\]|\\.)*")\s*;/m.exec(launcher);
186
- if (!match)
187
- return null;
188
- try {
189
- const value = JSON.parse(match[1]);
190
- return typeof value === 'string' && value ? value : null;
191
- }
192
- catch {
193
- return null;
194
- }
195
- }
196
185
  /**
197
186
  * A `npm link` install reaches the package through a symlinked global path, so
198
187
  * the launcher and the running CLI legitimately spell the same package root
@@ -213,6 +202,20 @@ function samePackageRoot(embedded, expected) {
213
202
  };
214
203
  return normalize(embedded) === normalize(expected) || resolved(embedded) === resolved(expected);
215
204
  }
205
+ function normalizedPath(value) {
206
+ return path.normalize(String(value || '')).replace(/[\\/]+$/, '').toLowerCase();
207
+ }
208
+ function runtimeFileMayExist(file) {
209
+ try {
210
+ return fs.statSync(file).isFile();
211
+ }
212
+ catch (error) {
213
+ return !['ENOENT', 'ENOTDIR'].includes(error?.code || '');
214
+ }
215
+ }
216
+ function managedBinding(source, launcher, hookPath) {
217
+ return managedHookInstaller.managedBinding(source, launcher, hookPath);
218
+ }
216
219
  /** Read-only inspection of the machine-wide Windows Codex managed hook. */
217
220
  export function inspectCodexManagedHook(options = {}) {
218
221
  const paths = codexManagedHookPaths(options);
@@ -283,32 +286,35 @@ export function inspectCodexManagedHook(options = {}) {
283
286
  catch (error) {
284
287
  return { status: 'unavailable', requirements, managedDir, hookPath, reason: error instanceof Error ? error.message : String(error) };
285
288
  }
286
- const normalized = (value) => path.normalize(String(value || '')).toLowerCase();
287
289
  const expectedPackageRoot = path.resolve(options.packageRoot || process.cwd());
288
- const launcherValid = launcher.includes('EVOLCORE_MANAGED_HOOK_LAUNCHER')
289
- && samePackageRoot(launcherPackageRoot(launcher), expectedPackageRoot);
290
- const configuredDir = config.hooks?.windows_managed_dir ?? config.hooks?.managed_dir;
291
- const groups = config.hooks?.PreToolUse;
292
- const canonicalCommand = (value) => {
293
- if (typeof value !== 'string' || !value || /[\r\n;&|<>]/.test(value))
294
- return false;
295
- const withoutSocket = value.replace(/\s+%EVOLCORE_INSTANCE_SOCKET%\s*$/i, '').trim();
296
- return withoutSocket.replace(/["']+$/, '').toLowerCase().endsWith(normalized(hookPath));
297
- };
298
- const hookLoaded = Array.isArray(groups) && groups.some((group) => group?.matcher === '*'
299
- && Array.isArray(group.hooks) && group.hooks.some((handler) => {
300
- const command = typeof handler?.command === 'string' ? handler.command : '';
301
- const windowsCommand = typeof handler?.command_windows === 'string' ? handler.command_windows : '';
302
- return handler?.type === 'command' && handler.async !== true
303
- && canonicalCommand(command) && canonicalCommand(windowsCommand);
304
- }));
305
- const valid = config.allow_managed_hooks_only === true
306
- && config.features?.hooks === true
307
- && normalized(configuredDir) === normalized(managedDir)
308
- && launcherValid && hookLoaded;
309
- return valid
310
- ? { status: 'installed', requirements, managedDir, hookPath }
311
- : { status: 'unsafe', requirements, managedDir, hookPath, reason: 'managed-hook policy is missing, altered, or points outside the canonical target' };
290
+ try {
291
+ const binding = managedBinding(source, launcher, hookPath);
292
+ if (!binding || config.allow_managed_hooks_only !== true || config.features?.hooks !== true
293
+ || normalizedPath(config.hooks?.windows_managed_dir) !== normalizedPath(managedDir)
294
+ || (config.hooks?.managed_dir !== undefined
295
+ && normalizedPath(config.hooks.managed_dir) !== normalizedPath(managedDir))) {
296
+ throw new Error('managed-hook policy is missing, altered, or points outside the canonical target');
297
+ }
298
+ const { embeddedPackageRoot, managedNodePath } = binding;
299
+ const packageMatches = samePackageRoot(embeddedPackageRoot, expectedPackageRoot);
300
+ const nodeMatches = samePackageRoot(managedNodePath, options.nodePath || process.execPath);
301
+ const repairFingerprint = createHash('sha256').update(source).update('\0').update(launcher).digest('hex');
302
+ if (packageMatches && nodeMatches)
303
+ return { status: 'installed', requirements, managedDir, hookPath };
304
+ return {
305
+ status: 'stale', requirements, managedDir, hookPath,
306
+ managedPackageRoot: embeddedPackageRoot, managedNodePath, repairFingerprint,
307
+ repairAmbiguous: runtimeFileMayExist(path.join(embeddedPackageRoot, 'bin', 'codex-managed-hook.mjs'))
308
+ && runtimeFileMayExist(managedNodePath),
309
+ reason: !packageMatches && !nodeMatches
310
+ ? 'managed-hook points to a different EvolCore package root and Node runtime'
311
+ : !packageMatches ? 'managed-hook points to a different EvolCore package root'
312
+ : 'managed-hook points to a different Node runtime',
313
+ };
314
+ }
315
+ catch (error) {
316
+ return { status: 'unsafe', requirements, managedDir, hookPath, reason: error instanceof Error ? error.message : String(error) };
317
+ }
312
318
  }
313
319
  function powershellLiteral(value) {
314
320
  return `'${value.replaceAll("'", "''")}'`;
@@ -319,7 +325,10 @@ function windowsArgument(value) {
319
325
  }
320
326
  export function buildCodexManagedHookElevationCommand(options) {
321
327
  const argumentLine = [options.installer, '--package-root', options.packageRoot,
322
- '--requirements', options.requirements, '--managed-dir', options.managedDir, '--repair']
328
+ '--requirements', options.requirements, '--managed-dir', options.managedDir,
329
+ '--node-path', options.nodePath, '--repair',
330
+ ...(options.expectedFingerprint ? ['--expected-fingerprint', options.expectedFingerprint] : []),
331
+ ...(options.confirmRebind ? ['--confirm-rebind'] : [])]
323
332
  .map(windowsArgument).join(' ');
324
333
  return [
325
334
  "$ErrorActionPreference = 'Stop'",
@@ -334,11 +343,11 @@ export function buildCodexManagedHookElevationCommand(options) {
334
343
  ].join('\n');
335
344
  }
336
345
  /**
337
- * Ensure the machine-wide Windows hook before starting the daemon. Missing
338
- * files are repaired through one UAC elevation; altered policy remains
346
+ * Ensure the machine-wide Windows hook before starting the daemon. Missing or
347
+ * stale files are repaired through one UAC elevation; altered policy remains
339
348
  * fail-closed and is never overwritten automatically.
340
349
  */
341
- export function ensureCodexManagedHook(options) {
350
+ export async function ensureCodexManagedHook(options) {
342
351
  const platformName = options.platform ?? process.platform;
343
352
  const paths = codexManagedHookPaths({
344
353
  platform: platformName,
@@ -358,8 +367,27 @@ export function ensureCodexManagedHook(options) {
358
367
  let state = inspectCodexManagedHook(inspectOptions);
359
368
  if (state.status === 'installed')
360
369
  return { ...state, repaired: false };
361
- if (state.status !== 'missing')
370
+ if (state.status !== 'missing' && state.status !== 'stale')
362
371
  return { ...state, repaired: false };
372
+ let confirmedRebind = false;
373
+ if (state.status === 'stale' && state.repairAmbiguous) {
374
+ try {
375
+ confirmedRebind = await options.repairDecision?.(state) === true;
376
+ }
377
+ catch (error) {
378
+ return { ...state, repaired: false, error: error instanceof Error ? error.message : String(error) };
379
+ }
380
+ }
381
+ if (state.status === 'stale' && state.repairAmbiguous && !confirmedRebind) {
382
+ return { ...state, repaired: false, cancelled: true, error: 'managed-hook repair cancelled or requires confirmation' };
383
+ }
384
+ const rechecked = inspectCodexManagedHook(inspectOptions);
385
+ if (rechecked.status === 'installed')
386
+ return { ...rechecked, repaired: false };
387
+ if (rechecked.status !== state.status || rechecked.repairFingerprint !== state.repairFingerprint
388
+ || rechecked.repairAmbiguous !== state.repairAmbiguous) {
389
+ return { ...rechecked, repaired: false, error: 'managed-hook state changed during the startup check; retry to confirm the current binding' };
390
+ }
363
391
  const installer = path.join(path.resolve(options.packageRoot), 'bin', 'install-codex-managed-hooks.mjs');
364
392
  if (!fs.existsSync(installer)) {
365
393
  return { ...state, repaired: false, error: `managed-hook installer is missing: ${installer}` };
@@ -370,6 +398,10 @@ export function ensureCodexManagedHook(options) {
370
398
  nodePath: process.execPath, installer: script, packageRoot,
371
399
  requirements: path.resolve(inspectOptions.requirements),
372
400
  managedDir: path.resolve(inspectOptions.managedDir),
401
+ ...(state.status === 'stale' ? {
402
+ expectedFingerprint: state.repairFingerprint,
403
+ confirmRebind: confirmedRebind,
404
+ } : {}),
373
405
  });
374
406
  const result = spawnSync(powershell, [
375
407
  // Do not use -NonInteractive here: Start-Process -Verb RunAs must be
@@ -385,7 +417,7 @@ export function ensureCodexManagedHook(options) {
385
417
  }
386
418
  return { ok: true };
387
419
  });
388
- options.onInstallRequired?.();
420
+ options.onInstallRequired?.(state);
389
421
  let elevated;
390
422
  try {
391
423
  elevated = runElevated(installer, path.resolve(options.packageRoot));
@@ -177,10 +177,10 @@ function parseManagedPreToolUseDenial(value) {
177
177
  reason = 'EvolCore PreToolUse policy denied this tool call';
178
178
  const policyCode = /当前汇报命令(?:未通过规范解析|不符合当前会话)/.test(reason)
179
179
  ? 'proactive_send_invalid'
180
- : /工具调用已达到\s*\d+\s*次/.test(reason)
181
- ? 'proactive_tool_report_required'
182
- : /请先用\s+ec\s+(?:group\s+send|msg\s+send)/.test(reason)
183
- ? 'proactive_first_send_required'
180
+ : /连续违规\s*\d+\s*次,任务已中断/.test(reason)
181
+ ? 'proactive_circuit_break'
182
+ : /未包含意图表达,本轮只允许\s+ec\s+(?:group\s+send|msg\s+send)/.test(reason)
183
+ ? 'proactive_intent_required'
184
184
  : 'managed_pre_tool_use_denied';
185
185
  return { policyCode, reason };
186
186
  }
@@ -2206,12 +2206,21 @@ export class CodexRunner {
2206
2206
  const pending = [...this.unconfirmedInterrupts.entries()].filter(([, record]) => record.sessionId === sessionId || (threadId && record.threadId === threadId));
2207
2207
  await Promise.all(pending.map(async ([key, record]) => {
2208
2208
  try {
2209
- // Local stream closure and unsubscribe do not prove the backend turn
2210
- // stopped. Check its exact identity before allowing another writer.
2211
- const response = await withInterruptDeadline(record.client.threadRead(record.threadId, true, 2_000), 'Codex interrupt verification', 2_000);
2212
- const turn = response.thread?.turns?.find(turn => turn.id === record.turnId);
2209
+ const response = await withInterruptDeadline(record.client.threadRead(record.threadId, false, 2_000), 'Codex interrupt verification', 2_000);
2210
+ const threadStatus = response.thread?.status?.type;
2211
+ if (threadStatus === 'idle' || threadStatus === 'notLoaded') {
2212
+ if (this.unconfirmedInterrupts.get(key) === record) {
2213
+ this.unconfirmedInterrupts.delete(key);
2214
+ record.unsubscribe?.();
2215
+ }
2216
+ return;
2217
+ }
2218
+ if (threadStatus !== undefined)
2219
+ return;
2220
+ const history = await withInterruptDeadline(record.client.threadRead(record.threadId, true, 2_000), 'Codex interrupt history verification', 2_000);
2221
+ const turn = history.thread?.turns?.find(turn => turn.id === record.turnId);
2213
2222
  if (turn && ['completed', 'failed', 'interrupted'].includes(String(turn.status))
2214
- && !response.thread?.turns?.some(turn => turn.status === 'inProgress')
2223
+ && !history.thread?.turns?.some(turn => turn.status === 'inProgress')
2215
2224
  && this.unconfirmedInterrupts.get(key) === record) {
2216
2225
  this.unconfirmedInterrupts.delete(key);
2217
2226
  record.unsubscribe?.();
@@ -4965,17 +4974,12 @@ export class CodexRunner {
4965
4974
  }
4966
4975
  : undefined;
4967
4976
  const itemErrorText = outputTextForManagedHook(typeof item.error === 'string' ? item.error : item.error?.message);
4968
- const hookErrorText = /(?:Command|Tool call) blocked by PreToolUse hook\s*:/i.test(itemErrorText)
4969
- ? itemErrorText
4970
- : undefined;
4971
- const managedHookDenial = (item.status === 'failed' || item.status === 'declined' || hookErrorText)
4972
- ? parseManagedPreToolUseDenial([
4973
- item.aggregatedOutput,
4974
- item.output,
4975
- item.result,
4976
- hookErrorText,
4977
- ].filter(value => value !== undefined && value !== null).map(outputTextForManagedHook).join('\n'))
4977
+ const hookOutput = item.type === 'commandExecution' && item.exitCode == null && !itemErrorText
4978
+ && (item.status === 'failed' || item.status === 'declined')
4979
+ ? [item.aggregatedOutput, item.output, item.result].map(outputTextForManagedHook).find(text => /^\s*(?:Script failed\s+)?(?:Script error:\s*)?(?:Command|Tool call) blocked by PreToolUse hook\s*:/i.test(text))
4978
4980
  : undefined;
4981
+ const managedHookDenial = parseManagedPreToolUseDenial(itemErrorText)
4982
+ ?? parseManagedPreToolUseDenial(hookOutput);
4979
4983
  const managedHookFields = !denial && managedHookDenial
4980
4984
  ? {
4981
4985
  isError: true,