evolcore 0.0.11 → 0.0.13

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 (164) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/codex-managed-hook.mjs +80 -0
  3. package/bin/ec-safe-output.js +26 -7
  4. package/dist/agents/claude-runner.js +86 -7
  5. package/dist/agents/codex-app-server-client.js +174 -20
  6. package/dist/agents/codex-runner.js +375 -92
  7. package/dist/agents/ecagent-runner.js +39 -4
  8. package/dist/agents/gemini-runner.js +1 -0
  9. package/dist/aun/aid/client.js +11 -21
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/managed-operation.js +107 -0
  12. package/dist/aun/aid/store.js +13 -7
  13. package/dist/aun/group-fs-download.js +56 -0
  14. package/dist/aun/group-identity.js +1 -2
  15. package/dist/aun/msg/group.js +91 -44
  16. package/dist/aun/msg/index.js +1 -2
  17. package/dist/aun/msg/managed-operation.js +865 -0
  18. package/dist/aun/msg/mention-schema.js +20 -0
  19. package/dist/aun/msg/p2p.js +7 -0
  20. package/dist/aun/msg/upload.js +16 -1
  21. package/dist/aun/outbox.js +31 -2
  22. package/dist/aun/rpc/client.js +66 -0
  23. package/dist/aun/rpc/index.js +1 -2
  24. package/dist/aun/storage/{upload.js → client.js} +11 -2
  25. package/dist/aun/storage/index.js +1 -2
  26. package/dist/channels/aun.js +655 -240
  27. package/dist/channels/daemon.js +13 -0
  28. package/dist/cli/agent-command.js +56 -8
  29. package/dist/cli/agent.js +108 -31
  30. package/dist/cli/aun-commands.js +270 -69
  31. package/dist/cli/bench.js +12 -3
  32. package/dist/cli/cli-argv.js +10 -0
  33. package/dist/cli/config.js +9 -17
  34. package/dist/cli/ctl-command.js +1 -1
  35. package/dist/cli/daemon-commands.js +70 -55
  36. package/dist/cli/fs-command.js +79 -10
  37. package/dist/cli/handoff-command.js +3 -0
  38. package/dist/cli/index.js +30 -2
  39. package/dist/cli/init.js +77 -8
  40. package/dist/cli/managed-command-guard.js +41 -0
  41. package/dist/cli/model.js +11 -4
  42. package/dist/cli/queue-command.js +53 -1
  43. package/dist/cli/response.js +71 -0
  44. package/dist/cli/restart-monitor.js +7 -32
  45. package/dist/cli/trigger-command.js +11 -0
  46. package/dist/config/access-policy.js +1 -2
  47. package/dist/config/builtin-roles.js +9 -0
  48. package/dist/config/config-manager.js +21 -1
  49. package/dist/config/contact-book-store.js +11 -3
  50. package/dist/config/contact-request-service.js +161 -15
  51. package/dist/config/mention-mode.js +8 -24
  52. package/dist/config/peer-role-resolver.js +6 -2
  53. package/dist/config/role-migration-startup.js +53 -0
  54. package/dist/config/role-store.js +1 -1
  55. package/dist/core/auth/agent-delegation.js +1 -2
  56. package/dist/core/auth/auth-gateway.js +29 -1
  57. package/dist/core/auth/authorization-audit.js +15 -0
  58. package/dist/core/auth/operation-authorizer.js +31 -7
  59. package/dist/core/auth/operation-catalog.js +196 -25
  60. package/dist/core/bootstrap-messages.js +25 -0
  61. package/dist/core/bootstrap-service.js +64 -4
  62. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  63. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  64. package/dist/core/capability/skill-discovery.js +55 -0
  65. package/dist/core/channel-loader.js +1 -1
  66. package/dist/core/command/cli-intent-parser.js +9 -0
  67. package/dist/core/command/command-handler.js +650 -42
  68. package/dist/core/command/connect-menu.js +15 -0
  69. package/dist/core/command/group-menu.js +4 -4
  70. package/dist/core/command/menu-catalog.js +370 -0
  71. package/dist/core/command/menu-handler.js +230 -51
  72. package/dist/core/command/menu-protocol.js +94 -2
  73. package/dist/core/command/slash-gate.js +5 -2
  74. package/dist/core/command/slash-handler.js +99 -31
  75. package/dist/core/daemon-file-cache.js +2 -1
  76. package/dist/core/data-migration.js +9 -2
  77. package/dist/core/event-catalog.js +6 -0
  78. package/dist/core/evolagent.js +4 -2
  79. package/dist/core/handoff/dispatcher.js +5 -2
  80. package/dist/core/handoff/runtime.js +157 -22
  81. package/dist/core/handoff/store.js +29 -1
  82. package/dist/core/handoff/types.js +1 -0
  83. package/dist/core/interaction-router.js +10 -0
  84. package/dist/core/message/im-renderer.js +19 -0
  85. package/dist/core/message/logical-queue-bridge.js +1 -0
  86. package/dist/core/message/mention-schema.js +126 -0
  87. package/dist/core/message/message-bridge.js +298 -88
  88. package/dist/core/message/message-log.js +3 -0
  89. package/dist/core/message/message-queue.js +268 -39
  90. package/dist/core/message/message-utils.js +81 -4
  91. package/dist/core/message/response-engine.js +161 -77
  92. package/dist/core/message/stream-debouncer.js +10 -2
  93. package/dist/core/permission/approval-gateway.js +1 -0
  94. package/dist/core/permission/ec-command-parser.js +243 -18
  95. package/dist/core/permission/index.js +1 -1
  96. package/dist/core/permission/mode.js +15 -0
  97. package/dist/core/permission/readonly-shell-query.js +148 -8
  98. package/dist/core/permission/sandbox-runtime.js +43 -1
  99. package/dist/core/permission/tool-policy.js +322 -57
  100. package/dist/core/protected-paths.js +114 -4
  101. package/dist/core/session/session-manager.js +42 -4
  102. package/dist/core/session/session-mapper.js +20 -1
  103. package/dist/eck/detect.js +18 -1
  104. package/dist/eck/group-rules-sync.js +15 -44
  105. package/dist/eck/manifest-engine.js +39 -13
  106. package/dist/index.js +541 -180
  107. package/dist/ipc.js +232 -10
  108. package/dist/paths.js +3 -0
  109. package/dist/response-system/coordinator.js +6 -4
  110. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  111. package/dist/response-system/modes/single-session/index.js +14 -1
  112. package/dist/response-system/selector.js +6 -5
  113. package/dist/trigger/anomaly-store.js +4 -0
  114. package/dist/trigger/feedback.js +64 -11
  115. package/dist/trigger/manager.js +21 -1
  116. package/dist/trigger/parser.js +14 -2
  117. package/dist/trigger/scheduler.js +1 -0
  118. package/dist/trigger/script-executor.js +1 -0
  119. package/dist/trigger/validation.js +67 -6
  120. package/dist/utils/codex-app-server-registry.js +90 -0
  121. package/dist/utils/codex-cli.js +5 -1
  122. package/dist/utils/cross-platform.js +15 -0
  123. package/dist/utils/ecweb-supervisor.js +332 -0
  124. package/dist/utils/error-utils.js +10 -0
  125. package/dist/utils/npm-ops.js +5 -12
  126. package/dist/utils/tool-summary.js +11 -0
  127. package/kits/docs/channels/aun.md +1 -1
  128. package/kits/docs/context-assembly.md +2 -2
  129. package/kits/docs/evolcore/agent.md +11 -6
  130. package/kits/docs/evolcore/aid.md +14 -0
  131. package/kits/docs/evolcore/trigger.md +17 -1
  132. package/kits/docs/prompt-loading-architecture.md +1 -2
  133. package/kits/docs/venues/group.md +1 -1
  134. package/kits/eck_manifest.json +0 -12
  135. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  136. package/kits/rules/04-relation.md +1 -1
  137. package/kits/rules/05-venue.md +2 -2
  138. package/kits/schemas/role-config.schema.1.json +1 -0
  139. package/kits/schemas/single-session.schema.1.json +3 -3
  140. package/kits/schemas/single-session.schema.2.json +3 -3
  141. package/kits/templates/roles/admin.json +27 -0
  142. package/kits/templates/roles/member.json +34 -1
  143. package/kits/templates/roles/visitor.json +35 -3
  144. package/kits/templates/system-fragments/bootstrap.md +1 -1
  145. package/kits/templates/system-fragments/channel.md +1 -1
  146. package/kits/templates/system-fragments/session.md +1 -1
  147. package/package.json +2 -2
  148. package/dist/aun/msg/payload-type.js +0 -27
  149. package/dist/aun/rpc/caller.js +0 -42
  150. package/dist/aun/rpc/connection.js +0 -25
  151. package/dist/aun/storage/manage.js +0 -10
  152. package/dist/config/access-policy-domain.js +0 -18
  153. package/dist/config/config-batch-get.js +0 -11
  154. package/dist/config/owner-policy.js +0 -4
  155. package/dist/config/role-config-v4-startup.js +0 -32
  156. package/dist/config/role-config-v5-startup.js +0 -27
  157. package/dist/core/auth/trigger-authorization.js +0 -15
  158. package/dist/core/command/evol-menu-version-gate.js +0 -39
  159. package/dist/core/interaction-registration.js +0 -10
  160. package/dist/core/permission/execution-sandbox.js +0 -16
  161. package/dist/core/relation/peer-key.js +0 -1
  162. package/dist/core/session/session-key.js +0 -24
  163. package/dist/eck/baseagent-caps.js +0 -18
  164. package/dist/eck/rules-loader.js +0 -28
@@ -11,7 +11,7 @@ const H_CLASS_RELATIVE_PATTERNS = [
11
11
  /^backups\/config(?:\/|$)/,
12
12
  /^\.snapshots(?:\/|$)/,
13
13
  /^CA(?:\/|$)/,
14
- /^(?:AIDs|aids)\/[^/]+\/(?:cert|keys)(?:\/|$)/,
14
+ /^(?:AIDs|aids)\/[^/]+\/(?:cert|keys|private)(?:\/|$)/,
15
15
  /^\.device_id$/,
16
16
  /^\.env$/,
17
17
  /^\.seed(?:\.|$)/,
@@ -33,11 +33,25 @@ const H_CLASS_RELATIVE_PATTERNS = [
33
33
  const H_CLASS_REFERENCE_PATTERNS = [
34
34
  /(?:^|[^A-Za-z0-9_.-])daemon\.json(?=$|[^A-Za-z0-9_.-])/,
35
35
  /(?:^|[^A-Za-z0-9_.-])agents[\\/](?:defaults\.json|[^/\\\s"']+[\\/](?:(?:config|contact)\.json|contact-operations\.jsonl|relations[\\/][^/\\\s"']+[\\/]config\.json))(?=$|[\s"';&|<>)},])/,
36
- /(?:^|[^A-Za-z0-9_.-])(?:backups[\\/]config|\.snapshots|CA|(?:AIDs|aids)[\\/][^/\\\s"']+[\\/](?:cert|keys))(?:[\\/]|$|[\s"';&|<>)},])/,
36
+ /(?:^|[^A-Za-z0-9_.-])(?:backups[\\/]config|\.snapshots|CA|(?:AIDs|aids)[\\/][^/\\\s"']+[\\/](?:cert|keys|private))(?:[\\/]|$|[\s"';&|<>)},])/,
37
37
  /(?:^|[/\\\s"'=<>])(?:\.device_id|\.env|\.seed(?:\.[^/\\\s"']*)?|\.migrated-[^/\\\s"']*|\.lock|daemon\.pid|data[\\/](?:causation-aun\.json(?:_|__)?|sessions|contact-book-audit\.jsonl|feishu-seen-[^/\\\s"']+\.jsonl|[a-z0-9_-]+-greeted-[^/\\\s"']+\.jsonl|message-queue\.json(?:_|__|\.tmp-[^/\\\s"']*)?|daemon[\\/](?:message-queue\.json(?:_|__|\.tmp-[^/\\\s"']*)?|control|migrations)|channel-state|instance[\\/]control\.token|outbox|handoff)|agents[\\/][^/\\\s"']+[\\/](?:sessions|data[\\/](?:channels|handoff|outbox\.jsonl|contact-audit\.jsonl)))(?:[\\/]|$|[\s"';&|<>)},])/,
38
38
  /(?:^|[/\\\s"'=<>])data[\\/]triggers(?:[\\/]|$|[\s"';&|<>)},])/,
39
39
  /(?:^|[/\\\s"'=<>])(?:[^/\\\s"']+\.json_|[^/\\\s"']+\.json\.migrated|defaults_\d+\.json)(?=$|[\s"';&|<>)},])/,
40
40
  ];
41
+ // H-class directory roots are kept separately from the full path matcher so
42
+ // an exact metadata query can distinguish a protected directory from a
43
+ // protected file. Descendants inherit the directory classification unless an
44
+ // existing regular file gives us a more precise answer.
45
+ const H_CLASS_DIRECTORY_RELATIVE_PATTERNS = [
46
+ /^backups\/config(?:\/|$)/,
47
+ /^\.snapshots(?:\/|$)/,
48
+ /^CA(?:\/|$)/,
49
+ /^(?:AIDs|aids)\/[^/]+\/(?:cert|keys|private)(?:\/|$)/,
50
+ /^data\/(?:triggers|sessions|outbox|handoff|channel-state)(?:\/|$)/,
51
+ /^data\/daemon\/(?:control|migrations)(?:\/|$)/,
52
+ /^agents\/[^/]+\/sessions(?:\/|$)/,
53
+ /^agents\/[^/]+\/data\/(?:channels|handoff)(?:\/|$)/,
54
+ ];
41
55
  const L_CLASS_RELATIVE_PATTERNS = [
42
56
  /^agents\/[^/]+\/triggers(?:\/|$)/,
43
57
  /^agents\/[^/]+\/roles(?:\/|$)/,
@@ -68,6 +82,22 @@ function resolveThroughExistingAncestor(value) {
68
82
  return absolute;
69
83
  }
70
84
  }
85
+ /**
86
+ * H-class file names are normally leaves, but treating their descendants as
87
+ * protected as well is important when a malformed or externally-created
88
+ * directory uses a reserved file name (for example `config.json/child`).
89
+ * Such a path cannot expose content from the expected file, and fail-closed
90
+ * classification avoids turning that unusual filesystem state into a bypass.
91
+ */
92
+ function matchesHClassRelative(relative) {
93
+ const segments = relative.split('/').filter(Boolean);
94
+ for (let end = segments.length; end > 0; end--) {
95
+ if (H_CLASS_RELATIVE_PATTERNS.some(pattern => pattern.test(segments.slice(0, end).join('/')))) {
96
+ return true;
97
+ }
98
+ }
99
+ return false;
100
+ }
71
101
  export function isSameOrDescendant(candidate, parent) {
72
102
  const normalizedCandidate = normalizeForComparison(candidate);
73
103
  const normalizedParent = normalizeForComparison(parent);
@@ -85,14 +115,89 @@ export function isHClassPath(value, options = {}) {
85
115
  : path.resolve(options.cwd ?? process.cwd(), value);
86
116
  if (isSameOrDescendant(lexicalCandidate, root)) {
87
117
  const lexicalRelative = path.relative(root, lexicalCandidate).split(path.sep).join('/');
88
- if (H_CLASS_RELATIVE_PATTERNS.some(pattern => pattern.test(lexicalRelative)))
118
+ if (matchesHClassRelative(lexicalRelative))
89
119
  return true;
90
120
  }
91
121
  const candidate = resolveProtectedCandidate(value, options.cwd);
92
122
  if (!isSameOrDescendant(candidate, root))
93
123
  return false;
94
124
  const relative = path.relative(root, candidate).split(path.sep).join('/');
95
- return H_CLASS_RELATIVE_PATTERNS.some(pattern => pattern.test(relative));
125
+ return matchesHClassRelative(relative);
126
+ }
127
+ function hasPathSyntax(value) {
128
+ return /[\0\r\n$`*?\[\]{}<>|;&]/.test(value);
129
+ }
130
+ function lexicalCandidate(value, cwd = process.cwd()) {
131
+ return path.isAbsolute(value) ? path.resolve(value) : path.resolve(cwd, value);
132
+ }
133
+ function resolveWithoutFinalSymlink(value, cwd = process.cwd()) {
134
+ const absolute = lexicalCandidate(value, cwd);
135
+ const basename = path.basename(absolute);
136
+ const parent = resolveThroughExistingAncestor(path.dirname(absolute));
137
+ return basename ? path.join(parent, basename) : parent;
138
+ }
139
+ /** Resolve path ancestors while preserving the final symlink as a literal node. */
140
+ export function resolveProtectedCandidateWithoutFinalSymlink(value, cwd = process.cwd()) {
141
+ return resolveWithoutFinalSymlink(value, cwd);
142
+ }
143
+ function relativeWithinRoot(value, root) {
144
+ if (!isSameOrDescendant(value, root))
145
+ return undefined;
146
+ return path.relative(root, value).split(path.sep).join('/');
147
+ }
148
+ /**
149
+ * Classify one literal filesystem path without interpreting shell syntax.
150
+ * `followFinalSymlink=false` is used by stat/lstat policy so metadata queries
151
+ * describe the link itself rather than exposing the protected target.
152
+ */
153
+ export function classifyProtectedPath(value, options = {}) {
154
+ if (typeof value !== 'string' || !value.trim() || hasPathSyntax(value))
155
+ return 'unknown';
156
+ const root = resolveThroughExistingAncestor(options.root ?? resolveRoot());
157
+ const lexical = resolveWithoutFinalSymlink(value, options.cwd);
158
+ const followFinalSymlink = options.followFinalSymlink !== false;
159
+ const candidate = followFinalSymlink ? resolveProtectedCandidate(value, options.cwd) : lexical;
160
+ const relative = relativeWithinRoot(candidate, root);
161
+ if (relative === undefined)
162
+ return 'ordinary';
163
+ const hMatch = matchesHClassRelative(relative);
164
+ if (hMatch) {
165
+ let directory = H_CLASS_DIRECTORY_RELATIVE_PATTERNS.some(pattern => pattern.test(relative));
166
+ try {
167
+ const statTarget = followFinalSymlink ? candidate : lexical;
168
+ directory = fs.lstatSync(statTarget).isDirectory();
169
+ }
170
+ catch {
171
+ // Missing paths below a protected directory inherit that directory's
172
+ // class; missing exact H-class filenames remain H-class files.
173
+ }
174
+ return directory ? 'h-directory' : 'h-file';
175
+ }
176
+ const lMatch = L_CLASS_RELATIVE_PATTERNS.some(pattern => pattern.test(relative));
177
+ if (lMatch)
178
+ return 'l-path';
179
+ return 'ordinary';
180
+ }
181
+ /** Apply the H/L path-class × access-action matrix to one literal path. */
182
+ export function checkProtectedPathAccess(value, accessKind, options = {}) {
183
+ const pathClass = classifyProtectedPath(value, options);
184
+ if (pathClass === 'unknown')
185
+ return { pathClass, accessKind, allowed: false };
186
+ if (pathClass === 'h-file' || pathClass === 'h-directory') {
187
+ return { pathClass, accessKind, allowed: accessKind === 'metadata' };
188
+ }
189
+ if (pathClass === 'l-path') {
190
+ return {
191
+ pathClass,
192
+ accessKind,
193
+ allowed: accessKind !== 'write' && accessKind !== 'grant',
194
+ };
195
+ }
196
+ return {
197
+ pathClass,
198
+ accessKind,
199
+ allowed: accessKind !== 'write' && accessKind !== 'grant',
200
+ };
96
201
  }
97
202
  export function containsHClassReference(value) {
98
203
  const normalized = value.replace(/\\/g, '/');
@@ -144,10 +249,14 @@ export function getHClassSandboxPatterns(root = resolveRoot()) {
144
249
  path.join('AIDs', '*', 'cert', '**'),
145
250
  path.join('AIDs', '*', 'keys'),
146
251
  path.join('AIDs', '*', 'keys', '**'),
252
+ path.join('AIDs', '*', 'private'),
253
+ path.join('AIDs', '*', 'private', '**'),
147
254
  path.join('aids', '*', 'cert'),
148
255
  path.join('aids', '*', 'cert', '**'),
149
256
  path.join('aids', '*', 'keys'),
150
257
  path.join('aids', '*', 'keys', '**'),
258
+ path.join('aids', '*', 'private'),
259
+ path.join('aids', '*', 'private', '**'),
151
260
  '.device_id',
152
261
  '.env',
153
262
  '.seed',
@@ -272,6 +381,7 @@ export function getExistingHClassMaskTargets(root = resolveRoot()) {
272
381
  continue;
273
382
  addExistingTarget(targets, path.join(aidsRoot, aid.name, 'cert'));
274
383
  addExistingTarget(targets, path.join(aidsRoot, aid.name, 'keys'));
384
+ addExistingTarget(targets, path.join(aidsRoot, aid.name, 'private'));
275
385
  }
276
386
  }
277
387
  catch { }
@@ -2,8 +2,7 @@ import { ensureDir } from '../../utils/atomic-write.js';
2
2
  import { logger } from '../../utils/logger.js';
3
3
  import { encodePath } from '../../utils/cross-platform.js';
4
4
  import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, readJsonFile, readLastJsonlLine, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
5
- import { sessionToFile, fileToSession } from './session-mapper.js';
6
- import { formatSessionKey, DEFAULT_THREAD_ID } from './session-key.js';
5
+ import { sessionToFile, fileToSession, formatSessionKey, DEFAULT_THREAD_ID } from './session-mapper.js';
7
6
  import { tryParseChannelKey } from '../channel-loader.js';
8
7
  import { isSystemControlChannel } from '../system-channels.js';
9
8
  import path from 'path';
@@ -11,7 +10,7 @@ import fs from 'fs';
11
10
  import os from 'os';
12
11
  function newSessionStableMetadata(chatType, ...sources) {
13
12
  const keys = chatType === 'group'
14
- ? ['channelKey', 'groupId', 'groupName', 'dispatchMode']
13
+ ? ['channelKey', 'groupId', 'groupName', 'mentionMode']
15
14
  : ['channelKey', 'peerId', 'peerName', 'peerType'];
16
15
  const metadata = {};
17
16
  for (const source of sources) {
@@ -597,6 +596,14 @@ export class SessionManager {
597
596
  mutated = true;
598
597
  }
599
598
  }
599
+ if (chatType === 'group' && metadata?.groupId) {
600
+ if (!session.metadata)
601
+ session.metadata = {};
602
+ if (session.metadata.groupId !== metadata.groupId) {
603
+ session.metadata.groupId = metadata.groupId;
604
+ mutated = true;
605
+ }
606
+ }
600
607
  if (metadata?.peerType && session.metadata?.peerType !== metadata.peerType) {
601
608
  if (!session.metadata)
602
609
  session.metadata = {};
@@ -636,6 +643,9 @@ export class SessionManager {
636
643
  if (metadata?.peerType) {
637
644
  session.metadata.peerType = metadata.peerType;
638
645
  }
646
+ if (chatType === 'group' && metadata?.groupId) {
647
+ session.metadata.groupId = metadata.groupId;
648
+ }
639
649
  this.persistSession(session, 'sync');
640
650
  return session;
641
651
  }
@@ -702,11 +712,20 @@ export class SessionManager {
702
712
  const existing = this.readMetaLatest(metaPath);
703
713
  if (existing) {
704
714
  const validSessionId = this.validateSessionFile(existing);
715
+ let mutated = false;
705
716
  if (metadata) {
706
717
  const creatorPeerId = existing.metadata?.peerId;
707
718
  existing.metadata = { ...(existing.metadata || {}), ...metadata };
708
719
  if (creatorPeerId && existing.metadata)
709
720
  existing.metadata.peerId = creatorPeerId;
721
+ mutated = true;
722
+ }
723
+ if (chatType && existing.chatType !== chatType) {
724
+ logger.info(`[SessionManager] Updating thread chatType for session ${existing.id}: ${existing.chatType} -> ${chatType}`);
725
+ existing.chatType = chatType;
726
+ mutated = true;
727
+ }
728
+ if (mutated) {
710
729
  this.persistSession(existing, 'none');
711
730
  }
712
731
  return { ...existing, agentSessionId: validSessionId };
@@ -1135,6 +1154,10 @@ export class SessionManager {
1135
1154
  if (!baseagent) {
1136
1155
  throw new Error(`[SessionManager] createNewSession: baseagent is empty for channel=${channel} channelId=${channelId}`);
1137
1156
  }
1157
+ const metadata = newSessionStableMetadata(inheritedChatType, activeMetadata, identityMetadata);
1158
+ if (identityMetadata?.sessionBoundaryOperationId) {
1159
+ metadata.sessionBoundaryOperationId = identityMetadata.sessionBoundaryOperationId;
1160
+ }
1138
1161
  const session = {
1139
1162
  id: generateSessionId(),
1140
1163
  channel,
@@ -1146,7 +1169,7 @@ export class SessionManager {
1146
1169
  baseagent,
1147
1170
  sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
1148
1171
  chatType: inheritedChatType,
1149
- metadata: newSessionStableMetadata(inheritedChatType, activeMetadata, identityMetadata),
1172
+ metadata,
1150
1173
  name: name || '默认会话',
1151
1174
  createdAt: Date.now(),
1152
1175
  updatedAt: Date.now(),
@@ -1163,6 +1186,21 @@ export class SessionManager {
1163
1186
  });
1164
1187
  return session;
1165
1188
  }
1189
+ /**
1190
+ * Create and activate one fresh main session for a durable system operation.
1191
+ * Replaying the same operation returns the already-created session.
1192
+ */
1193
+ async ensureNewMainSessionForOperation(opts) {
1194
+ const active = this.getActiveSessionSync(opts.channel, opts.channelId);
1195
+ if (active?.metadata?.sessionBoundaryOperationId === opts.operationId) {
1196
+ return { session: active, created: false };
1197
+ }
1198
+ const session = await this.createNewSession(opts.channel, opts.channelId, opts.projectPath, undefined, opts.baseagent, {
1199
+ ...(opts.identityMetadata ?? {}),
1200
+ sessionBoundaryOperationId: opts.operationId,
1201
+ });
1202
+ return { session, created: true };
1203
+ }
1166
1204
  async createForkedSession(sourceSession, forkedAgentSessionId, name) {
1167
1205
  const channelType = sourceSession.channelType || sourceSession.channel;
1168
1206
  const threadId = sourceSession.threadId || '';
@@ -1,5 +1,24 @@
1
1
  import { formatTimestamp } from './session-fs-store.js';
2
- import { formatSessionKey, DEFAULT_THREAD_ID } from './session-key.js';
2
+ /** Default topic component used by the stable session routing key. */
3
+ export const DEFAULT_THREAD_ID = 'main';
4
+ export function formatSessionKey(channelType, channelId, threadId) {
5
+ const tid = threadId || DEFAULT_THREAD_ID;
6
+ return `${channelType}#${encodeURIComponent(channelId)}#${encodeURIComponent(tid)}`;
7
+ }
8
+ export function parseSessionKey(key) {
9
+ const first = key.indexOf('#');
10
+ if (first <= 0)
11
+ throw new Error(`Invalid session key: ${key}`);
12
+ const rest = key.slice(first + 1);
13
+ const second = rest.indexOf('#');
14
+ if (second <= 0)
15
+ throw new Error(`Invalid session key (missing threadId): ${key}`);
16
+ return {
17
+ channelType: key.slice(0, first),
18
+ channelId: decodeURIComponent(rest.slice(0, second)),
19
+ threadId: decodeURIComponent(rest.slice(second + 1)),
20
+ };
21
+ }
3
22
  export function sessionToFile(session) {
4
23
  const metadata = {};
5
24
  if (session.metadata) {
@@ -1,6 +1,23 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { BASEAGENT_CAPS } from './baseagent-caps.js';
3
+ export const BASEAGENT_CAPS = {
4
+ 'claude-code': {
5
+ autoLoadsRules: true,
6
+ supportsSystemPrompt: true,
7
+ },
8
+ 'claude': {
9
+ autoLoadsRules: true,
10
+ supportsSystemPrompt: true,
11
+ },
12
+ 'codex': {
13
+ autoLoadsRules: false,
14
+ supportsSystemPrompt: true,
15
+ },
16
+ 'gemini': {
17
+ autoLoadsRules: false,
18
+ supportsSystemPrompt: true,
19
+ },
20
+ };
4
21
  const MAX_DEPTH = 5;
5
22
  export function resolveEckInjection(agentConfig, projectPath, kitsRulesPath) {
6
23
  const caps = BASEAGENT_CAPS[agentConfig.baseAgent]
@@ -1,8 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { createHash } from 'crypto';
4
3
  import { TextDecoder } from 'util';
5
4
  import { getAidStore, isValidAid, loadClient, SLOT } from '../aun/aid/index.js';
5
+ import { downloadGroupFsBytes, GroupFsDownloadError } from '../aun/group-fs-download.js';
6
6
  import { checkGroupIndex as checkGroupIndexRpc, getGroupIndex as getGroupIndexRpc } from '../aun/msg/group-index.js';
7
7
  import { agentRelationsDir } from '../paths.js';
8
8
  import { formatPeerKey } from '../core/relation/peer-identity.js';
@@ -220,7 +220,9 @@ function lazyGroupClient(selfAid) {
220
220
  client: async () => {
221
221
  if (client)
222
222
  return client;
223
- store = await getAidStore({ slotId: SLOT.daemon });
223
+ // This short-lived RPC client must never share the daemon's gateway
224
+ // cursor. A short connection can perform a background pull on connect.
225
+ store = await getAidStore({ slotId: SLOT.groupRules });
224
226
  client = await loadClient(store, selfAid);
225
227
  await client.connect({ connection_kind: 'short', short_ttl_ms: 30000, auto_reconnect: false });
226
228
  return client;
@@ -242,39 +244,15 @@ function lazyGroupClient(selfAid) {
242
244
  };
243
245
  }
244
246
  async function downloadGroupFileBytes(client, remoteRef) {
245
- const ticket = await client.call('group.fs.create_download_ticket', { path: remoteRef });
246
- if (!isRecord(ticket)) {
247
- throw statusError('unreadable', 'group.fs.create_download_ticket returned invalid response');
248
- }
249
- const downloadUrl = stringField(ticket, ['download_url', 'url']);
250
- if (!downloadUrl) {
251
- throw statusError('unreadable', 'group.fs.create_download_ticket did not return download_url');
252
- }
253
- const bytes = await client.group.fs.lowlevel.httpGet(downloadUrl, bearerHeaders(client));
254
- const expectedSha = stringField(ticket, ['sha256']);
255
- if (expectedSha && sha256Hex(bytes).toLowerCase() !== expectedSha.toLowerCase()) {
256
- throw statusError('unreadable', 'download hash verification failed');
247
+ try {
248
+ return (await downloadGroupFsBytes(client, remoteRef)).bytes;
257
249
  }
258
- return bytes;
259
- }
260
- function sha256Hex(bytes) {
261
- return createHash('sha256').update(bytes).digest('hex');
262
- }
263
- function bearerHeaders(client) {
264
- const token = accessTokenFromClient(client);
265
- return token ? { Authorization: `Bearer ${token}` } : undefined;
266
- }
267
- function accessTokenFromClient(client) {
268
- const anyClient = client;
269
- if (typeof anyClient.getAccessToken === 'function') {
270
- const token = stringValue(anyClient.getAccessToken());
271
- if (token)
272
- return token;
250
+ catch (error) {
251
+ if (error instanceof GroupFsDownloadError) {
252
+ throw statusError('unreadable', error.message);
253
+ }
254
+ throw error;
273
255
  }
274
- return stringValue(anyClient.accessToken)
275
- || stringValue(anyClient.access_token)
276
- || stringValue(anyClient._identity?.access_token)
277
- || stringValue(anyClient._sessionParams?.access_token);
278
256
  }
279
257
  function assertUtf8(bytes) {
280
258
  new TextDecoder('utf-8', { fatal: true }).decode(bytes);
@@ -302,6 +280,10 @@ function classifyError(e) {
302
280
  if (e instanceof StatusError)
303
281
  return e.status;
304
282
  const msg = errorMessage(e).toLowerCase();
283
+ // A peer cache miss is a signer/identity lookup failure, not a missing
284
+ // rules file.
285
+ if (msg.includes('peer not found in cache'))
286
+ return 'unreadable';
305
287
  if (msg.includes('forbidden') || msg.includes('unauthorized') || msg.includes('permission'))
306
288
  return 'forbidden';
307
289
  if (msg.includes('not found') || msg.includes('does not exist'))
@@ -314,17 +296,6 @@ function errorMessage(e) {
314
296
  function isRecord(x) {
315
297
  return typeof x === 'object' && x !== null && !Array.isArray(x);
316
298
  }
317
- function stringValue(x) {
318
- return typeof x === 'string' ? x : '';
319
- }
320
- function stringField(obj, keys) {
321
- for (const k of keys) {
322
- const v = obj[k];
323
- if (typeof v === 'string')
324
- return v;
325
- }
326
- return undefined;
327
- }
328
299
  function numberField(obj, keys) {
329
300
  for (const k of keys) {
330
301
  const v = obj[k];
@@ -55,6 +55,10 @@ function loadAndMergeManifest(filename) {
55
55
  for (const s of base.sections)
56
56
  merged.set(s.id, { ...s });
57
57
  for (const s of override.sections) {
58
+ if (s.id === 'relation-group-profile' && merged.has('peer-profile')) {
59
+ logger.warn('[ManifestEngine] Ignoring deprecated patch section "relation-group-profile"; use "peer-profile" instead');
60
+ continue;
61
+ }
58
62
  const existing = merged.get(s.id);
59
63
  merged.set(s.id, existing ? { ...existing, ...s } : s);
60
64
  }
@@ -320,15 +324,35 @@ function resolvePath(rawPath, vars) {
320
324
  const r = resolvePathWithDiag(rawPath, vars);
321
325
  return r.status === 'ok' ? r.resolved : null;
322
326
  }
327
+ function isWithinDirectory(filePath, directory) {
328
+ const relative = path.relative(path.resolve(directory), path.resolve(filePath));
329
+ return relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`));
330
+ }
331
+ /**
332
+ * 随包发布的 kits 内容只在 reload/重启后刷新;personal/relations 及用户自定义
333
+ * manifest 路径属于运行时数据,必须用 mtime 检查,否则同一 daemon 内会长期陈旧。
334
+ */
335
+ function sectionCacheOptions(resolvedPath, vars) {
336
+ if (isWithinDirectory(resolvedPath, kitsDir())) {
337
+ return { policy: 'on-reload', group: 'kits' };
338
+ }
339
+ const personalDir = typeof vars.PERSONAL_DIR === 'string' ? vars.PERSONAL_DIR : null;
340
+ const selfAid = typeof vars.selfAid === 'string' ? vars.selfAid : null;
341
+ if (personalDir && selfAid && isWithinDirectory(resolvedPath, personalDir)) {
342
+ return { policy: 'mtime', group: `agent-files:${selfAid}` };
343
+ }
344
+ return { policy: 'mtime', group: 'eck-runtime' };
345
+ }
323
346
  /** 加载 loop 的 child 子模板文件内容(路径解析 + fileCache)。找不到返回 null。 */
324
347
  export function loadChildTemplate(childFile, vars) {
325
348
  const resolved = resolvePath(childFile, vars);
326
349
  if (!resolved)
327
350
  return null;
328
- return fileCache.getText(resolved, { policy: 'on-reload', group: 'kits' });
351
+ return fileCache.getText(resolved, sectionCacheOptions(resolved, vars));
329
352
  }
330
353
  /**
331
- * 返回 [filePath, rawContent][];按 sessionId 缓存已读文件内容。
354
+ * 返回 [filePath, rawContent][]。kits 文件跨 session 缓存到 reload;运行时文件
355
+ * (personal/relations/用户自定义路径)每次按 mtime 检查。
332
356
  * 若传入 overflowOut 且目录段触发单目录限额(maxFiles/maxBytes),
333
357
  * 会把溢出信息写入 overflowOut.value(不传则不启用限额收集,行为同旧版但仍应用默认上限)。
334
358
  */
@@ -343,7 +367,7 @@ export function loadSectionFiles(section, vars, sessionCache, overflowOut) {
343
367
  return [];
344
368
  const maxFiles = section.maxFiles ?? DIR_MAX_FILES;
345
369
  const maxBytes = section.maxBytes ?? DIR_MAX_BYTES;
346
- const { files, overflow } = readDirectoryFiles(resolved, section.pattern, maxFiles, maxBytes);
370
+ const { files, overflow } = readDirectoryFiles(resolved, section.pattern, maxFiles, maxBytes, vars);
347
371
  if (overflow && overflowOut)
348
372
  overflowOut.value = overflow;
349
373
  return files.map(([name, content]) => [path.join(resolved, name), content]);
@@ -351,31 +375,33 @@ export function loadSectionFiles(section, vars, sessionCache, overflowOut) {
351
375
  return [];
352
376
  }
353
377
  function loadFileSection(filePath, vars, sessionCache) {
354
- void sessionCache; // 内容跨 session 共享,改走全局 fileCache(on-reload)
378
+ void sessionCache; // 内容跨 session 共享,统一走全局 fileCache
355
379
  const resolved = resolvePath(filePath, vars);
356
380
  if (!resolved)
357
381
  return null;
358
- // 内容跨 session 共享:用全局 fileCache(on-reload,reload/重启时失效),
359
- // 不再按 session 重复缓存同一文件内容。
360
- const content = fileCache.getText(resolved, { policy: 'on-reload', group: 'kits' });
382
+ const content = fileCache.getText(resolved, sectionCacheOptions(resolved, vars));
361
383
  return content === null ? null : [resolved, content];
362
384
  }
363
385
  /**
364
386
  * 读取目录下匹配文件,受单目录限额(maxFiles/maxBytes)约束。
365
387
  * 返回已加载文件 + 溢出信息(超限时)。按 fileCache 缓存目录列表与文件内容。
366
388
  */
367
- function readDirectoryFiles(dirPath, pattern, maxFiles, maxBytes) {
389
+ function readDirectoryFiles(dirPath, pattern, maxFiles, maxBytes, vars) {
368
390
  const glob = pattern || '*.md';
369
- // 目录列表 + 各文件内容均走 fileCache(on-reload)。目录列表以 "<dir>|<glob>"
370
- // 为键缓存文件名数组;各文件内容走 fileCache.getText 共享。
371
- const names = fileCache.get(`${dirPath} ${glob}`, () => {
391
+ const cacheOptions = sectionCacheOptions(dirPath, vars);
392
+ const readNames = () => {
372
393
  try {
373
394
  return fs.readdirSync(dirPath).filter(f => matchGlob(f, glob)).sort();
374
395
  }
375
396
  catch {
376
397
  return [];
377
398
  }
378
- }, { policy: 'on-reload', group: 'kits' });
399
+ };
400
+ // synthetic cache key 无法用文件 mtime 门控。kits 目录结构只在 reload 后刷新;
401
+ // 运行时目录直接重扫,确保新增/删除文件在同一 session 内可见。
402
+ const names = cacheOptions.policy === 'on-reload'
403
+ ? fileCache.get(`${dirPath} ${glob}`, readNames, cacheOptions)
404
+ : readNames();
379
405
  const out = [];
380
406
  let usedBytes = 0;
381
407
  let overflow;
@@ -387,7 +413,7 @@ function readDirectoryFiles(dirPath, pattern, maxFiles, maxBytes) {
387
413
  break;
388
414
  }
389
415
  const fp = path.join(dirPath, f);
390
- const content = fileCache.getText(fp, { policy: 'on-reload', group: 'kits' });
416
+ const content = fileCache.getText(fp, sectionCacheOptions(fp, vars));
391
417
  if (content === null)
392
418
  continue;
393
419
  // 字节限额(至少加载一个文件,避免单个超大文件导致空目录)