evolcore 0.0.32 → 0.0.34

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 (214) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +135 -37
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +111 -15
  6. package/dist/agents/codex/app-server-client.js +3 -0
  7. package/dist/agents/codex/diagnostics.js +112 -54
  8. package/dist/agents/codex-runner.js +241 -63
  9. package/dist/agents/ecagent-runner.js +78 -33
  10. package/dist/agents/runner-types.js +5 -6
  11. package/dist/aun/aid/identity.js +1 -1
  12. package/dist/aun/group/operations.js +0 -1
  13. package/dist/aun/msg/history.js +1 -0
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/channels/aun.js +44 -8
  17. package/dist/channels/daemon.js +261 -3
  18. package/dist/cli/agent-command.js +58 -24
  19. package/dist/cli/agent.js +1 -1
  20. package/dist/cli/aun-commands.js +368 -31
  21. package/dist/cli/contact.js +8 -2
  22. package/dist/cli/ctl-command.js +3 -1
  23. package/dist/cli/daemon-commands.js +197 -27
  24. package/dist/cli/debug-mode.js +154 -0
  25. package/dist/cli/fs-command.js +2 -1
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +98 -67
  28. package/dist/cli/init.js +2 -2
  29. package/dist/cli/integrity-command.js +196 -0
  30. package/dist/cli/managed-command-guard.js +5 -0
  31. package/dist/cli/memory-command.js +166 -146
  32. package/dist/cli/queue-command.js +9 -4
  33. package/dist/cli/session-command.js +194 -0
  34. package/dist/cli/skill-command.js +78 -2
  35. package/dist/cli/task-context.js +1 -0
  36. package/dist/cli/thread-command.js +622 -0
  37. package/dist/cli/trigger-command.js +10 -3
  38. package/dist/config/builtin-roles.js +1 -0
  39. package/dist/config/config-field-policy.js +6 -3
  40. package/dist/config/config-manager.js +26 -2
  41. package/dist/config/contact-book-store.js +5 -0
  42. package/dist/config/resolved-config-op.js +3 -0
  43. package/dist/config/role-service.js +13 -0
  44. package/dist/config/runtime-env.js +16 -0
  45. package/dist/config-store.js +45 -0
  46. package/dist/core/audit/log-integrity.js +3 -1
  47. package/dist/core/auth/agent-delegation.js +133 -36
  48. package/dist/core/auth/debug-context.js +139 -0
  49. package/dist/core/auth/delegation-diagnostics.js +414 -0
  50. package/dist/core/auth/operation-authorizer.js +25 -2
  51. package/dist/core/auth/operation-catalog.js +49 -0
  52. package/dist/core/background-session/session-cleanup.js +212 -0
  53. package/dist/core/background-session/types.js +10 -0
  54. package/dist/core/bootstrap-messages.js +1 -1
  55. package/dist/core/bootstrap-service.js +6 -3
  56. package/dist/core/command/agent-control.js +18 -26
  57. package/dist/core/command/command-handler.js +197 -27
  58. package/dist/core/command/daemon-diagnostics.js +1 -0
  59. package/dist/core/command/memory-slash.js +6 -2
  60. package/dist/core/command/menu-catalog.js +51 -1
  61. package/dist/core/command/menu-handler.js +114 -23
  62. package/dist/core/command/menu-protocol.js +25 -1
  63. package/dist/core/command/skill-slash.js +322 -0
  64. package/dist/core/command/slash-gate.js +6 -4
  65. package/dist/core/command/slash-handler.js +195 -13
  66. package/dist/core/command/topic-slash.js +528 -0
  67. package/dist/core/daemon-bootstrap.js +4 -7
  68. package/dist/core/evolagent-registry.js +20 -5
  69. package/dist/core/file-integrity/read.js +61 -0
  70. package/dist/core/file-integrity/store.js +262 -0
  71. package/dist/core/handoff/runtime.js +5 -11
  72. package/dist/core/handoff/store.js +5 -0
  73. package/dist/core/ipc-registration.js +175 -3
  74. package/dist/core/memory/batch-meta.js +132 -0
  75. package/dist/core/memory/consolidation-context.js +83 -29
  76. package/dist/core/memory/consolidation-ops-records.js +49 -16
  77. package/dist/core/memory/consolidation-queue-format.js +21 -0
  78. package/dist/core/memory/consolidation.js +264 -205
  79. package/dist/core/memory/index-manager.js +19 -28
  80. package/dist/core/memory/memory-admin.js +19 -14
  81. package/dist/core/memory/memory-document.js +53 -9
  82. package/dist/core/memory/memory-writer.js +30 -15
  83. package/dist/core/message/aun-ipc-sender.js +161 -6
  84. package/dist/core/message/im-renderer.js +169 -10
  85. package/dist/core/message/message-bridge.js +15 -4
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/peer-mode.js +6 -1
  88. package/dist/core/message/response-engine.js +929 -110
  89. package/dist/core/message/subtask-tool-projection.js +140 -0
  90. package/dist/core/model/catalog-service.js +17 -12
  91. package/dist/core/permission/ec-command-parser.js +217 -9
  92. package/dist/core/permission/protected-paths.js +5 -1
  93. package/dist/core/permission/readonly-shell-query.js +43 -5
  94. package/dist/core/permission/session-path-grants.js +334 -0
  95. package/dist/core/permission/tool-canonical.js +226 -0
  96. package/dist/core/permission/tool-policy.js +30 -62
  97. package/dist/core/role/runtime-policy.js +21 -10
  98. package/dist/core/session/session-fs-store.js +123 -8
  99. package/dist/core/session/session-idle-monitor.js +249 -22
  100. package/dist/core/session/session-manager.js +129 -14
  101. package/dist/core/skill/command-parser.js +13 -0
  102. package/dist/core/skill/index-generator.js +21 -4
  103. package/dist/core/skill/startup.js +41 -0
  104. package/dist/core/skill/switch-resolver.js +55 -29
  105. package/dist/core/system-session/grants.js +40 -0
  106. package/dist/core/system-session/memory.js +222 -0
  107. package/dist/core/system-session/registry.js +74 -0
  108. package/dist/core/system-session/types.js +14 -0
  109. package/dist/core/thread/command-parser.js +125 -0
  110. package/dist/core/thread/index.js +1 -0
  111. package/dist/core/thread/ipc-operation.js +95 -0
  112. package/dist/core/thread/thread-context-storage.js +131 -0
  113. package/dist/core/thread/thread-operation.js +258 -0
  114. package/dist/core/thread/thread-route.js +33 -0
  115. package/dist/core/thread/thread-service.js +770 -0
  116. package/dist/core/thread/types.js +1 -0
  117. package/dist/eck/kit-renderer.js +7 -2
  118. package/dist/eck/manifest-engine.js +24 -4
  119. package/dist/eck/message-renderer.js +35 -0
  120. package/dist/index.js +19 -7
  121. package/dist/ipc.js +140 -50
  122. package/dist/paths.js +93 -0
  123. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  124. package/dist/stats/billing.js +20 -8
  125. package/dist/trigger/authorization.js +57 -28
  126. package/dist/trigger/feedback.js +13 -2
  127. package/dist/trigger/parser.js +70 -1
  128. package/dist/trigger/system-definitions.js +3 -3
  129. package/dist/trigger/validation.js +14 -0
  130. package/dist/utils/cross-platform.js +11 -2
  131. package/dist/utils/error-dict.json +18 -4
  132. package/dist/utils/error-utils.js +143 -1
  133. package/dist/utils/instance-registry.js +13 -8
  134. package/dist/utils/log-writer.js +12 -0
  135. package/dist/utils/process-introspect.js +11 -0
  136. package/kits/docs/context-assembly.md +1 -1
  137. package/kits/docs/evolcore/INDEX.md +1 -0
  138. package/kits/docs/evolcore/agent-create.md +40 -28
  139. package/kits/docs/evolcore/agent.md +21 -6
  140. package/kits/docs/evolcore/config.md +2 -2
  141. package/kits/docs/evolcore/ctl.md +2 -2
  142. package/kits/docs/evolcore/ec-capabilities.md +5 -1
  143. package/kits/docs/evolcore/fs-architecture.md +4 -5
  144. package/kits/docs/evolcore/fs.md +1 -1
  145. package/kits/docs/evolcore/group-collaboration.md +4 -6
  146. package/kits/docs/evolcore/group.md +11 -12
  147. package/kits/docs/evolcore/memory-architecture.md +4 -5
  148. package/kits/docs/evolcore/memory-consolidation.md +19 -15
  149. package/kits/docs/evolcore/memory-tags.md +3 -1
  150. package/kits/docs/evolcore/memory.md +24 -12
  151. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  152. package/kits/docs/evolcore/menu-protocol.md +7 -0
  153. package/kits/docs/evolcore/model.md +16 -0
  154. package/kits/docs/evolcore/msg.md +8 -11
  155. package/kits/docs/evolcore/thread-chain.md +267 -0
  156. package/kits/docs/prompt-loading-architecture.md +1 -1
  157. package/kits/eck_manifest.json +55 -40
  158. package/kits/eck_manifest.memory.json +0 -12
  159. package/kits/eck_message_manifest.json +169 -10
  160. package/kits/rules/01-overview.md +11 -0
  161. package/kits/rules/03-identity.md +6 -3
  162. package/kits/rules/05-venue.md +1 -1
  163. package/kits/rules/06-channel.md +1 -1
  164. package/kits/rules/07-memory-and-thread.md +199 -0
  165. package/kits/schemas/_meta.json +7 -1
  166. package/kits/schemas/agent-config.schema.13.json +1 -1
  167. package/kits/schemas/daemon.schema.7.json +140 -0
  168. package/kits/schemas/relation-config.schema.9.json +18 -3
  169. package/kits/schemas/role-config.schema.1.json +6 -6
  170. package/kits/schemas/single-session.schema.1.json +6 -6
  171. package/kits/schemas/single-session.schema.2.json +6 -6
  172. package/kits/schemas/single-session.schema.3.json +6 -6
  173. package/kits/skills/.switches.json +12 -1
  174. package/kits/skills/ec-group/SKILL.md +234 -0
  175. package/kits/skills/ec-skill/SKILL.md +10 -1
  176. package/kits/skills/ec-skill/scripts/cli.mjs +16 -2
  177. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  178. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  179. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  180. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  181. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  182. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  183. package/kits/skills/thread-ops/SKILL.md +492 -0
  184. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  185. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  186. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  187. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  188. package/kits/templates/bootstrap-welcome.md +7 -7
  189. package/kits/templates/memory-consolidation/forget.md +7 -2
  190. package/kits/templates/memory-consolidation/generalize.md +9 -4
  191. package/kits/templates/memory-consolidation/notes.md +11 -11
  192. package/kits/templates/memory-consolidation/profile.md +8 -3
  193. package/kits/templates/memory-consolidation/promote.md +8 -3
  194. package/kits/templates/memory-consolidation/workflow.md +6 -6
  195. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  196. package/kits/templates/message-fragments/item.md +2 -1
  197. package/kits/templates/message-fragments/thread-context.md +6 -0
  198. package/kits/templates/message-fragments/trigger-session.md +2 -0
  199. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  200. package/kits/templates/roles/member.json +2 -0
  201. package/kits/templates/roles/visitor.json +2 -0
  202. package/kits/templates/system-fragments/bootstrap.md +67 -72
  203. package/kits/templates/system-fragments/channel.md +3 -6
  204. package/kits/templates/system-fragments/commands.md +1 -0
  205. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  206. package/kits/templates/system-fragments/memory.md +16 -9
  207. package/kits/templates/system-fragments/reply.md +7 -6
  208. package/kits/templates/system-fragments/session.md +5 -14
  209. package/package.json +6 -3
  210. package/dist/core/memory/memory-session-policy.js +0 -41
  211. package/dist/core/memory/memory-signature.js +0 -257
  212. package/kits/docs/evolcore/group-fs.md +0 -17
  213. package/kits/docs/venues/aun-group.md +0 -45
  214. package/kits/docs/venues/group.md +0 -43
@@ -1,4 +1,4 @@
1
- import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable, TRANSPORT_EFFORTS, resolveCatalogModel, } from '../core/model/model-catalog.js';
1
+ import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable, } from '../core/model/model-catalog.js';
2
2
  // Ecagent/OpenAI API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
3
3
  import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
4
4
  import fs from 'fs';
@@ -18,6 +18,7 @@ import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
18
18
  import { resolveEcagentConfig } from './baseagent.js';
19
19
  import { buildModelRequestHeaders } from './request-identity.js';
20
20
  import { appendRequestPath } from './request-identity.js';
21
+ import { resolveModelSpec } from '../stats/billing.js';
21
22
  import { getManagedTaskTempDir } from '../cli/task-context.js';
22
23
  import { unsettledInterruptError, withInterruptDeadline, hasTrustedFullAccessContext } from './runner-types.js';
23
24
  const PROVIDER_ID = 'evolcore-gateway';
@@ -436,18 +437,23 @@ async function consumeOpenAiStream(output, body, model) {
436
437
  throw new Error('[network] Gateway stream ended without a terminal event');
437
438
  }
438
439
  function createModel(id, baseUrl) {
439
- const isClaude = id.toLowerCase().startsWith('claude-');
440
+ const modelId = stripContextWindowSuffix(id);
441
+ const isClaude = modelId.toLowerCase().startsWith('claude-');
442
+ const fallback = isClaude
443
+ ? { context_window: 200_000, max_output_tokens: 8_192 }
444
+ : { context_window: 128_000, max_output_tokens: 16_384 };
445
+ const spec = resolveModelSpec(resolveRoot(), id, undefined, fallback);
440
446
  return {
441
- id,
442
- name: id,
447
+ id: modelId,
448
+ name: modelId,
443
449
  api: 'openai-completions',
444
450
  provider: PROVIDER_ID,
445
451
  baseUrl,
446
452
  reasoning: true,
447
453
  input: ['text', 'image'],
448
454
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
449
- contextWindow: isClaude ? 200_000 : 128_000,
450
- maxTokens: isClaude ? 8_192 : 16_384,
455
+ contextWindow: spec.context_window,
456
+ maxTokens: spec.max_output_tokens,
451
457
  compat: {
452
458
  maxTokensField: 'max_tokens',
453
459
  supportsUsageInStreaming: true,
@@ -464,7 +470,11 @@ function isWithin(root, candidate) {
464
470
  const relative = path.relative(root, candidate);
465
471
  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
466
472
  }
467
- export function resolveEcagentToolPath(projectPath, candidate, forWrite = false, bootstrapAgentMdPath, fullaccess = false, personalMdPath) {
473
+ /** 路径是否落在任一显式声明的根之下(含根本身)。 */
474
+ function isWithinAny(candidate, roots) {
475
+ return roots.some(root => isWithin(path.resolve(root), candidate));
476
+ }
477
+ export function resolveEcagentToolPath(projectPath, candidate, forWrite = false, bootstrapAgentMdPath, fullaccess = false, personalMdPath, sessionGrants) {
468
478
  const root = fs.realpathSync(projectPath);
469
479
  const resolved = path.resolve(root, candidate || '.');
470
480
  const allowedBootstrapFile = bootstrapAgentMdPath
@@ -472,7 +482,15 @@ export function resolveEcagentToolPath(projectPath, candidate, forWrite = false,
472
482
  : undefined;
473
483
  const allowedPersonalFile = personalMdPath ? path.resolve(personalMdPath) : undefined;
474
484
  const isManagedIdentityFile = resolved === allowedBootstrapFile || resolved === allowedPersonalFile;
475
- if (!fullaccess && !isWithin(root, resolved) && !isManagedIdentityFile) {
485
+ // 系统会话:模块声明的可读根整体放行,可写文件逐个精确匹配(不含目录)。
486
+ // 声明之外、项目之外的路径仍然拒绝。声明里的路径可能在多个根之间借符号链接互串,
487
+ // 所以放行前统一按真实路径复核,确认真实位置仍落在同一个被授予的边界内。
488
+ // 写只看可写文件(逐个精确匹配),绝不由可读根推导;读只看可读根。
489
+ const sessionWrites = forWrite && (sessionGrants?.writeFiles ?? [])
490
+ .some(file => path.resolve(file) === resolved);
491
+ const sessionReads = !forWrite && isWithinAny(resolved, sessionGrants?.readRoots ?? []);
492
+ const isSessionTarget = sessionReads || sessionWrites;
493
+ if (!fullaccess && !isWithin(root, resolved) && !isManagedIdentityFile && !isSessionTarget) {
476
494
  throw new Error(`Path is outside the project: ${candidate}`);
477
495
  }
478
496
  if (isManagedIdentityFile) {
@@ -494,7 +512,34 @@ export function resolveEcagentToolPath(projectPath, candidate, forWrite = false,
494
512
  while (!fs.existsSync(cursor) && cursor !== path.dirname(cursor))
495
513
  cursor = path.dirname(cursor);
496
514
  const canonical = fs.realpathSync(cursor);
497
- if (!fullaccess && !isWithin(root, canonical))
515
+ if (fullaccess)
516
+ return resolved;
517
+ // 系统会话的写入目标与判定层一致:必须是普通文件,链接一律拒绝。
518
+ // 链接会让 resolved 与真实位置分离,放行等于把洞开到声明之外。
519
+ if (sessionWrites && fs.existsSync(resolved) && fs.lstatSync(resolved).isSymbolicLink()) {
520
+ throw new Error(`Refusing to write through a symbolic link: ${candidate}`);
521
+ }
522
+ // 可写文件尚未创建时,以父目录的真实路径为准;已存在时以文件自身的真实路径为准。
523
+ const canonicalSelf = fs.existsSync(resolved) ? fs.realpathSync(resolved) : canonical;
524
+ if (isSessionTarget) {
525
+ const grantedRoots = [
526
+ ...(sessionReads ? sessionGrants?.readRoots ?? [] : []),
527
+ ...(sessionWrites ? (sessionGrants?.writeFiles ?? []).filter(file => path.resolve(file) === resolved) : []),
528
+ ];
529
+ const canonicalRoots = grantedRoots.flatMap(granted => {
530
+ const inside = fs.existsSync(granted) ? [fs.realpathSync(granted)] : [];
531
+ // 根自身可能尚未创建,或者位于尚未创建的上层目录中;先归到存在的祖先再比。
532
+ let ancestor = granted;
533
+ while (!fs.existsSync(ancestor) && ancestor !== path.dirname(ancestor))
534
+ ancestor = path.dirname(ancestor);
535
+ return [...inside, fs.realpathSync(ancestor)];
536
+ });
537
+ if (!canonicalRoots.some(granted => isWithin(granted, canonical) && isWithin(granted, canonicalSelf))) {
538
+ throw new Error(`Path resolves outside its declared grant: ${candidate}`);
539
+ }
540
+ return resolved;
541
+ }
542
+ if (!isWithin(root, canonical))
498
543
  throw new Error(`Path resolves outside the project: ${candidate}`);
499
544
  return resolved;
500
545
  }
@@ -547,7 +592,7 @@ async function walkFiles(root, signal) {
547
592
  function textResult(text, details = {}) {
548
593
  return { content: [{ type: 'text', text: truncate(text) }], details };
549
594
  }
550
- function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullaccess = false, personalMdPath) {
595
+ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullaccess = false, personalMdPath, sessionGrants) {
551
596
  const read = {
552
597
  name: 'Read', label: 'Read', description: 'Read a UTF-8 text file from the project.',
553
598
  parameters: Type.Object({
@@ -556,7 +601,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
556
601
  limit: Type.Optional(Type.Number({ minimum: 1 })),
557
602
  }),
558
603
  async execute(_id, params, signal) {
559
- const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess, personalMdPath);
604
+ const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess, personalMdPath, sessionGrants);
560
605
  const stat = await fs.promises.stat(file);
561
606
  if (stat.size > MAX_READ_BYTES)
562
607
  throw new Error(`File exceeds ${MAX_READ_BYTES} byte read limit`);
@@ -573,7 +618,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
573
618
  name: 'Glob', label: 'Glob', description: 'Find project files matching a glob pattern.',
574
619
  parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()) }),
575
620
  async execute(_id, params, signal) {
576
- const root = resolveEcagentToolPath(projectPath, params.path ?? '.', false, undefined, fullaccess);
621
+ const root = resolveEcagentToolPath(projectPath, params.path ?? '.', false, undefined, fullaccess, undefined, sessionGrants);
577
622
  const matcher = globRegex(params.pattern.replaceAll('\\', '/'));
578
623
  const files = (await walkFiles(root, signal))
579
624
  .map(file => path.relative(root, file).replaceAll(path.sep, '/'))
@@ -589,7 +634,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
589
634
  case_insensitive: Type.Optional(Type.Boolean()), literal: Type.Optional(Type.Boolean()),
590
635
  }),
591
636
  async execute(_id, params, signal) {
592
- const root = resolveEcagentToolPath(projectPath, params.path ?? '.', false, undefined, fullaccess);
637
+ const root = resolveEcagentToolPath(projectPath, params.path ?? '.', false, undefined, fullaccess, undefined, sessionGrants);
593
638
  const files = (await fs.promises.stat(root)).isFile() ? [root] : await walkFiles(root, signal);
594
639
  const fileMatcher = params.glob ? globRegex(params.glob.replaceAll('\\', '/')) : undefined;
595
640
  const flags = params.case_insensitive ? 'i' : '';
@@ -622,7 +667,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
622
667
  name: 'Write', label: 'Write', description: 'Create or completely overwrite a project file.',
623
668
  parameters: Type.Object({ file_path: Type.String(), content: Type.String() }), executionMode: 'sequential',
624
669
  async execute(_id, params, signal) {
625
- const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath, fullaccess, personalMdPath);
670
+ const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath, fullaccess, personalMdPath, sessionGrants);
626
671
  await fs.promises.mkdir(path.dirname(file), { recursive: true });
627
672
  await fs.promises.writeFile(file, params.content, { encoding: 'utf8', signal });
628
673
  return textResult(`Wrote ${Buffer.byteLength(params.content)} bytes to ${path.relative(projectPath, file)}`, { file });
@@ -635,7 +680,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
635
680
  replace_all: Type.Optional(Type.Boolean()),
636
681
  }), executionMode: 'sequential',
637
682
  async execute(_id, params, signal) {
638
- const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess, personalMdPath);
683
+ const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath, fullaccess, personalMdPath, sessionGrants);
639
684
  const value = await fs.promises.readFile(file, { encoding: 'utf8', signal });
640
685
  if (!params.old_string)
641
686
  throw new Error('old_string must not be empty');
@@ -724,7 +769,7 @@ export class EcagentRunner {
724
769
  configuredHeaders: config.headers,
725
770
  }),
726
771
  };
727
- this.model = stripContextWindowSuffix(config.model);
772
+ this.model = config.model;
728
773
  this.effort = config.effort;
729
774
  this.agentAid = runtime?.agentAid;
730
775
  this.onSessionIdUpdate = callbacks?.onSessionIdUpdate;
@@ -734,8 +779,8 @@ export class EcagentRunner {
734
779
  throw new Error('ecagent requires an EvolCore Agent AID');
735
780
  return agentSessionsDir(this.agentAid);
736
781
  }
737
- setModel(model) { this.model = stripContextWindowSuffix(model); }
738
- getModel() { return this.model; }
782
+ setModel(model) { this.model = model; }
783
+ getModel() { return stripContextWindowSuffix(this.model); }
739
784
  resolveModelId(model) { return model; }
740
785
  setEffort(effort) { this.effort = effort; }
741
786
  getEffort() { return this.effort; }
@@ -762,15 +807,7 @@ export class EcagentRunner {
762
807
  return catalog.models.map(m => m.id);
763
808
  }
764
809
  async getAvailableEfforts(model) {
765
- const catalog = await this.getModelCatalog();
766
- const efforts = catalogEfforts(catalog, model, 'ecagent');
767
- if (efforts.length > 0)
768
- return efforts;
769
- // The embedded transport advertises reasoning support for every model it
770
- // constructs. Older gateways may omit reasoningLevels from /models; keep
771
- // menu.options aligned with the actual runner capability in that case.
772
- const entry = catalog.models.find(item => item.id === resolveCatalogModel(catalog, model));
773
- return entry && entry.reasoningLevels === undefined ? [...TRANSPORT_EFFORTS.ecagent] : efforts;
810
+ return catalogEfforts(await this.getModelCatalog(), model, 'ecagent');
774
811
  }
775
812
  registerStream(key, stream) { this.activeStreams.set(key, stream); }
776
813
  cleanupStream(key) {
@@ -893,7 +930,7 @@ export class EcagentRunner {
893
930
  models,
894
931
  model,
895
932
  thinkingLevel: normalizeThinkingLevel(effort),
896
- tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv, request.modelOverride?.bootstrapAgentMdPath, fullaccess, request.modelOverride?.personalMdPath),
933
+ tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv, request.modelOverride?.bootstrapAgentMdPath, fullaccess, request.modelOverride?.personalMdPath, this.permissionContexts.get(request.sessionId)?.systemSessionGrants),
897
934
  systemPrompt: [DEFAULT_SYSTEM_PROMPT, request.systemPromptAppend].filter(Boolean).join('\n\n'),
898
935
  streamOptions: {
899
936
  timeoutMs: this.config.timeoutMs,
@@ -917,7 +954,7 @@ export class EcagentRunner {
917
954
  if (pauseResult === 'cancelled')
918
955
  return { block: true, reason: '工具调用已取消' };
919
956
  }
920
- const decision = await this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input);
957
+ const decision = await this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input, stats.numTurns + 1);
921
958
  return lifecycle.cancelled || signal?.aborted ? { block: true, reason: '工具调用已取消' } : decision;
922
959
  });
923
960
  const unsubscribe = harness.subscribe(event => this.mapHarnessEvent(queue, event, stats, model, effort));
@@ -1175,7 +1212,7 @@ export class EcagentRunner {
1175
1212
  model: model.id, effort,
1176
1213
  };
1177
1214
  }
1178
- async authorizeTool(sessionId, projectPath, mode, toolName, input) {
1215
+ async authorizeTool(sessionId, projectPath, mode, toolName, input, turn) {
1179
1216
  const permissionContext = this.permissionContexts.get(sessionId);
1180
1217
  if (mode === 'fullaccess') {
1181
1218
  return hasTrustedFullAccessContext(permissionContext)
@@ -1220,7 +1257,15 @@ export class EcagentRunner {
1220
1257
  logger.warn(`[EcagentRunner] failed to record blocked operation: session=${sessionId} tool=${toolName} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
1221
1258
  }
1222
1259
  };
1223
- const policy = permissionContext?.policyHook?.(toolName, input);
1260
+ // 系统会话:模块判定即最终结论,不进入 preflight / permissionMode 分支
1261
+ if (permissionContext?.systemSessionCheck) {
1262
+ const decision = permissionContext.systemSessionCheck(toolName, input);
1263
+ if (decision.allow)
1264
+ return {};
1265
+ await recordBlockedOperation(decision.code);
1266
+ return { block: true, reason: decision.reason };
1267
+ }
1268
+ const policy = permissionContext?.policyHook?.(toolName, input, { turn });
1224
1269
  if (policy?.block) {
1225
1270
  await recordBlockedOperation(policy.policyCode ?? 'session_policy_hook');
1226
1271
  return { block: true, reason: policy.reason || '当前会话策略拒绝此工具调用' };
@@ -1247,7 +1292,7 @@ export class EcagentRunner {
1247
1292
  const checkedInput = preflight.input;
1248
1293
  if (mode === 'readonly') {
1249
1294
  const decision = checkReadonly(toolName, checkedInput, projectPath, {
1250
- sessionId, managedTempDir: permissionContext?.managedTempDir, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
1295
+ sessionId, managedTempDir: permissionContext?.managedTempDir, grantedReadRoots: permissionContext?.grantedReadRoots, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
1251
1296
  allowReadonlySourceDiagnostics: permissionContext?.allowReadonlySourceDiagnostics === true,
1252
1297
  });
1253
1298
  if (decision.behavior === 'deny') {
@@ -1359,7 +1404,7 @@ export class EcagentRunner {
1359
1404
  ? cached
1360
1405
  : await this.openExistingRuntime(agentSessionId, cwd);
1361
1406
  this.onCompactStart?.(sessionId);
1362
- const compactModel = stripContextWindowSuffix(modelOverride?.model || this.model);
1407
+ const compactModel = modelOverride?.model || this.model;
1363
1408
  const compactEffort = modelOverride?.effortMode === 'model_default'
1364
1409
  ? undefined
1365
1410
  : modelOverride?.effort ?? this.effort;
@@ -136,10 +136,9 @@ export function isOneMillionContextModel(model) {
136
136
  return ONE_M_CONTEXT_MODEL_PREFIXES.some(prefix => base === prefix || base.startsWith(`${prefix}-`));
137
137
  }
138
138
  /**
139
- * EvolCore's conservative local context-window catalog: 1M models = 1,000,000;
140
- * every other model falls back to 200,000 until provider metadata is available.
141
- * This is an operational guardrail, not a claim about an upstream provider's
142
- * advertised capacity.
139
+ * Hardcoded context-window fallback for models missing from the parameter table:
140
+ * known 1M models use 1,000,000; every other model falls back to 200,000.
141
+ * This is an operational guardrail, not a claim about advertised capacity.
143
142
  */
144
143
  export function realContextWindowForModel(model) {
145
144
  return isOneMillionContextModel(model) ? 1000000 : 200000;
@@ -159,8 +158,8 @@ export function autoCompactWindowForModel(_model) {
159
158
  * at the right point for small-context models (200k/128k), where EvolCore's
160
159
  * fixed 650k threshold would never trigger.
161
160
  */
162
- export function sdkAutoCompactWindowForModel(model) {
163
- const contextWindow = realContextWindowForModel(model);
161
+ export function sdkAutoCompactWindowForModel(model, resolvedContextWindow) {
162
+ const contextWindow = resolvedContextWindow ?? realContextWindowForModel(model);
164
163
  const reserve = Math.min(10_000, Math.floor(contextWindow * 0.05));
165
164
  return Math.min(650_000, Math.max(1, contextWindow - reserve));
166
165
  }
@@ -224,7 +224,7 @@ export async function aidCreate(aid, opts) {
224
224
  // 新注册流程:AIDStore.register → downloadCaRoot → load → authenticate
225
225
  const store = await getAidStore({ slotId: SLOT.cli, aunPath });
226
226
  try {
227
- const regResult = await store.register(aid);
227
+ const regResult = await store.register(aid, { type: 'agent' });
228
228
  if (!regResult.ok) {
229
229
  const e = new Error(regResult.error.message);
230
230
  e.code = regResult.error.code;
@@ -117,7 +117,6 @@ async function tryDaemonGroupSend(args, payload, runtimeContext, file) {
117
117
  mentions: args.mentions,
118
118
  encrypt: args.encrypt,
119
119
  thread,
120
- returnPolicy: args.returnPolicy,
121
120
  originSessionId: runtimeContext.sessionId,
122
121
  originMessageId: runtimeContext.messageId,
123
122
  delegationToken: readDelegationCredential(),
@@ -171,6 +171,7 @@ function readHistoryScopes(input) {
171
171
  .filter(({ entry, sessionIds }) => sessionIds === undefined
172
172
  || (typeof entry.sessionId === 'string' && sessionIds.has(entry.sessionId)))
173
173
  .filter(({ entry }) => input.direction === 'all' || entry.dir === input.direction)
174
+ .filter(({ entry }) => input.args.threadId === undefined || entry.threadId === input.args.threadId)
174
175
  .filter(({ entry }) => input.args.after === undefined || entry.ts > input.args.after)
175
176
  .filter(({ entry }) => input.args.before === undefined || entry.ts < input.args.before)
176
177
  .sort((left, right) => left.entry.ts - right.entry.ts || left.scope - right.scope)
@@ -134,7 +134,6 @@ async function tryDaemonMsgSend(args, payload, runtimeContext, file) {
134
134
  ...(payload ? { payload } : { file }),
135
135
  encrypt: args.encrypt,
136
136
  thread,
137
- returnPolicy: args.returnPolicy,
138
137
  handoffContext: args.handoffContext,
139
138
  originSessionId: runtimeContext.sessionId,
140
139
  originMessageId: runtimeContext.messageId,
@@ -64,6 +64,24 @@ function isExpired(entry) {
64
64
  return false;
65
65
  return Date.now() - entry.ts > entry.ttl;
66
66
  }
67
+ /**
68
+ * A process-scoped entry whose enqueuing process is gone. Replaying it would
69
+ * deliver stale live feedback — typically the "restarting, back in ~5s" notice
70
+ * that a successful restart has already superseded with its own confirmation.
71
+ *
72
+ * A stale entry is not `isExpired()`: it needs `drainOnce` to retire it with a
73
+ * reason of its own, otherwise the send log would report a semantic drop as a
74
+ * timeout.
75
+ */
76
+ export function isStaleProcessScoped(entry) {
77
+ if (!entry.processScoped)
78
+ return false;
79
+ // A record without a pid cannot prove same-process ownership. Treat it as
80
+ // replayable so the ordinary route/expiry rules stay in charge.
81
+ if (typeof entry.enqueuedByPid !== 'number')
82
+ return false;
83
+ return entry.enqueuedByPid !== process.pid;
84
+ }
67
85
  export function isDeliveryForChannel(value, channelId) {
68
86
  return isDeliveryTargetForChannel(value, channelId);
69
87
  }
@@ -129,6 +147,7 @@ export function enqueue(aid, opts) {
129
147
  delivery,
130
148
  dedupeKey: opts.dedupeKey,
131
149
  critical: opts.critical,
150
+ ...(opts.processScoped ? { processScoped: true, enqueuedByPid: process.pid } : {}),
132
151
  type: opts.type,
133
152
  contentKind: opts.contentKind,
134
153
  payload: opts.payload,
@@ -273,6 +292,19 @@ function markExpired(entry) {
273
292
  code: entry.lastErrorCode,
274
293
  };
275
294
  }
295
+ /**
296
+ * Drop a process-scoped entry left behind by a previous process. The terminal
297
+ * record is retained so the send log still shows why it never went out.
298
+ */
299
+ function markSupersededProcessScoped(entry) {
300
+ entry.lastError = 'process-scoped delivery superseded by a new daemon process';
301
+ entry.lastErrorCode = 'OUTBOX_PROCESS_SUPERSEDED';
302
+ entry.terminal = {
303
+ at: Date.now(),
304
+ error: entry.lastError,
305
+ code: entry.lastErrorCode,
306
+ };
307
+ }
276
308
  /** Mark a durable entry terminal while keeping its reason for diagnostics. */
277
309
  export function markTerminal(aid, id, failure, submitted, queue = submitted ? entryQueue(submitted) : 'default') {
278
310
  const entries = readEntries(aid, queue);
@@ -336,6 +368,7 @@ async function drainOnce(aid, sender, queue) {
336
368
  let expired = 0;
337
369
  let failed = 0;
338
370
  let permanent = 0;
371
+ let superseded = 0;
339
372
  const remaining = [];
340
373
  for (const entry of entries) {
341
374
  // Cancellation or another sender may have changed an
@@ -350,6 +383,16 @@ async function drainOnce(aid, sender, queue) {
350
383
  }
351
384
  Object.assign(entry, currentEntry);
352
385
  }
386
+ if (isStaleProcessScoped(entry)) {
387
+ // Deliberately not counted as `expired`: this is a semantic drop, not a
388
+ // timeout, and the drained-line diagnostic should not conflate them.
389
+ // The terminal record also retires the entry from `hasPending()`, so a
390
+ // later drain does not rediscover it.
391
+ superseded++;
392
+ markSupersededProcessScoped(entry);
393
+ remaining.push(entry);
394
+ continue;
395
+ }
353
396
  if (isExpired(entry)) {
354
397
  expired++;
355
398
  markExpired(entry);
@@ -457,7 +500,13 @@ async function drainOnce(aid, sender, queue) {
457
500
  }];
458
501
  });
459
502
  writeEntries(aid, retained, queue);
460
- return { sent, expired, failed, ...(permanent > 0 ? { permanent } : {}) };
503
+ return {
504
+ sent,
505
+ expired,
506
+ failed,
507
+ ...(permanent > 0 ? { permanent } : {}),
508
+ ...(superseded > 0 ? { superseded } : {}),
509
+ };
461
510
  }
462
511
  export async function drain(aid, sender, queue = 'default') {
463
512
  const drainKey = `${queue}:${aid}`;
@@ -557,6 +557,11 @@ function setIfDefined(target, key, value) {
557
557
  if (value !== undefined)
558
558
  target[key] = value;
559
559
  }
560
+ function appendRawErrorText(text, raw) {
561
+ if (typeof raw !== 'string' || !raw.trim() || text.includes(raw))
562
+ return text;
563
+ return `${text}\n原因:${raw}`;
564
+ }
560
565
  /**
561
566
  * 把 AUNChannel 投递的 opts 映射成渠道无关的 InboundMessage。
562
567
  *
@@ -4413,6 +4418,9 @@ export class AUNChannel {
4413
4418
  };
4414
4419
  if (evolCommand)
4415
4420
  base.ext = { evolCommand };
4421
+ // Same diagnostic dictionary as text payloads (sessionId/agentSessionId/model).
4422
+ if (context?.debug)
4423
+ base.debug = { ...context.debug, sentAt: localTimestamp() };
4416
4424
  return withAunAttention(base);
4417
4425
  }
4418
4426
  async sendReliableStructured(channelId, payload, context, logText) {
@@ -4496,6 +4504,9 @@ export class AUNChannel {
4496
4504
  delivery,
4497
4505
  dedupeKey: operationId,
4498
4506
  critical: routedContext.metadata?.criticalDelivery === true,
4507
+ // Live feedback about the running process (for example the pre-restart
4508
+ // notice) must never be replayed by the process that replaces it.
4509
+ processScoped: routedContext.metadata?.processScopedDelivery === true,
4499
4510
  type: 'text',
4500
4511
  text: finalText,
4501
4512
  context: routedContext,
@@ -4565,6 +4576,16 @@ export class AUNChannel {
4565
4576
  };
4566
4577
  }
4567
4578
  /** Daemon-side transport for `ec msg send` running inside an agent task. */
4579
+ /**
4580
+ * Agent 经 CLI 委托发送(ec msg send / ec group send)时占用本轮序号,
4581
+ * 与回复路径共用 sentCount,保证同一轮内 seqInTurn 连续。
4582
+ */
4583
+ nextSeqInTurn(channelId, threadId) {
4584
+ const turnKey = `${channelId}\0${threadId ?? ''}`;
4585
+ const seqInTurn = (this.sentCount.get(turnKey) || 0) + 1;
4586
+ this.sentCount.set(turnKey, seqInTurn);
4587
+ return seqInTurn;
4588
+ }
4568
4589
  async sendDaemonMsg(args) {
4569
4590
  if (!args.to) {
4570
4591
  return { ok: false, error: 'aun-msg-send requires a private target', code: 'UNSUPPORTED_TARGET' };
@@ -5271,6 +5292,9 @@ export class AUNChannel {
5271
5292
  delete payloadWithChatmode.thread_id;
5272
5293
  if (context?.threadId && !payloadWithChatmode.thread_id)
5273
5294
  payloadWithChatmode.thread_id = context.threadId;
5295
+ // Same diagnostic dictionary as text payloads (sessionId/agentSessionId/model).
5296
+ if (context?.debug)
5297
+ payloadWithChatmode.debug = { ...context.debug, sentAt: localTimestamp() };
5274
5298
  const params = {
5275
5299
  context: { type: 'task', id: taskId },
5276
5300
  payload: this.limitThoughtPayload(payloadWithChatmode),
@@ -5847,8 +5871,12 @@ export class AUNChannel {
5847
5871
  }
5848
5872
  return { status: 'permanent', error: `unsupported outbox entry type: ${entry.type}`, code: 'UNSUPPORTED_OUTBOX_TYPE' };
5849
5873
  }, queue);
5850
- if (result.sent > 0 || result.expired > 0 || result.permanent) {
5851
- logger.info(`${this.logPrefix()} ${queue} outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed} permanent=${result.permanent ?? 0}`);
5874
+ if (result.sent > 0 || result.expired > 0 || result.permanent || result.superseded) {
5875
+ const superseded = result.superseded ? ` superseded=${result.superseded}` : '';
5876
+ logger.info(`${this.logPrefix()} ${queue} outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed} permanent=${result.permanent ?? 0}${superseded}`);
5877
+ }
5878
+ if (result.superseded) {
5879
+ logger.info(`${this.logPrefix()} ${queue} outbox: dropped ${result.superseded} process-scoped entr${result.superseded === 1 ? 'y' : 'ies'} left by a previous daemon process`);
5852
5880
  }
5853
5881
  };
5854
5882
  await drainQueue('default');
@@ -5962,6 +5990,9 @@ export class AUNChannel {
5962
5990
  statusPayload.chatmode = context.metadata.chatmode;
5963
5991
  if (context?.replyToMessageId)
5964
5992
  statusPayload.ref_message_id = context.replyToMessageId;
5993
+ // Same diagnostic dictionary as text payloads (sessionId/agentSessionId/model).
5994
+ if (context?.debug)
5995
+ statusPayload.debug = { ...context.debug, sentAt: localTimestamp() };
5965
5996
  const notifyOptions = { ttlMs: 60_000 };
5966
5997
  if (delivery.chatType === 'group')
5967
5998
  notifyOptions.groupId = delivery.groupId;
@@ -6389,6 +6420,7 @@ export class AUNChannelPlugin {
6389
6420
  text: payload.text,
6390
6421
  severity: 'info',
6391
6422
  };
6423
+ setIfDefined(noticePayload, 'raw', payload.raw);
6392
6424
  setIfDefined(noticePayload, 'subtype', payload.subtype);
6393
6425
  await channel.sendReliableStructured(channelId, noticePayload, replyCtx, payload.text);
6394
6426
  return;
@@ -6398,10 +6430,14 @@ export class AUNChannelPlugin {
6398
6430
  type: payload.recoverable === true ? 'notice' : 'error',
6399
6431
  ...taskBase(),
6400
6432
  subtype: payload.subtype,
6401
- text: payload.text,
6433
+ text: payload.recoverable === true
6434
+ ? payload.text
6435
+ : appendRawErrorText(payload.text, payload.raw),
6436
+ ...(payload.raw !== undefined ? { raw: payload.raw } : {}),
6437
+ ...(payload.reason !== undefined ? { reason: payload.reason } : {}),
6402
6438
  ...(payload.recoverable === true
6403
6439
  ? { severity: 'warn' }
6404
- : { message: payload.text, user_message: payload.text, recoverable: false }),
6440
+ : { recoverable: false }),
6405
6441
  };
6406
6442
  await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6407
6443
  return;
@@ -6410,11 +6446,11 @@ export class AUNChannelPlugin {
6410
6446
  const errorPayload = {
6411
6447
  type: 'error',
6412
6448
  ...taskBase(),
6413
- reason: payload.reason,
6414
- text: payload.text,
6415
- message: payload.text,
6416
- user_message: payload.text,
6449
+ text: appendRawErrorText(payload.text, payload.raw),
6417
6450
  };
6451
+ setIfDefined(errorPayload, 'subtype', payload.subtype);
6452
+ setIfDefined(errorPayload, 'reason', payload.reason);
6453
+ setIfDefined(errorPayload, 'raw', payload.raw);
6418
6454
  await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6419
6455
  return;
6420
6456
  }