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
@@ -1464,6 +1464,9 @@ export function routeField(topField, scope) {
1464
1464
  */
1465
1465
  export function routeFieldPath(fieldPath, scope) {
1466
1466
  const topField = fieldPath.split('.')[0];
1467
+ if (topField === 'sessionLoop') {
1468
+ throw new ConfigError('SESSION_LOOP_MANAGED', 'sessionLoop is managed through the dedicated /loop or session Menu entry');
1469
+ }
1467
1470
  if (scope === 'process')
1468
1471
  return routeIn(DAEMON_SCHEMA_NAME, ConfigTarget.Process, topField);
1469
1472
  if (scope === 'defaults') {
@@ -1541,8 +1544,10 @@ export function listFields(scope) {
1541
1544
  const out = [];
1542
1545
  const add = (name, target) => {
1543
1546
  const s = loadSchema(name);
1544
- for (const f of s.fields.keys())
1545
- out.push(mkRoute(s, target, f));
1547
+ for (const f of s.fields.keys()) {
1548
+ if (f !== 'sessionLoop')
1549
+ out.push(mkRoute(s, target, f));
1550
+ }
1546
1551
  };
1547
1552
  if (scope === 'process') {
1548
1553
  add(DAEMON_SCHEMA_NAME, ConfigTarget.Process);
@@ -90,6 +90,9 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
90
90
  const field = business.values[0].trim();
91
91
  if (!field)
92
92
  return { ok: false, code: 'MISSING_ARG', reason: `config ${subcommand} requires a field` };
93
+ if (field === 'sessionLoop' || field.startsWith('sessionLoop.')) {
94
+ return { ok: false, code: 'SESSION_LOOP_MANAGED', reason: 'sessionLoop is managed through the dedicated /loop or session Menu entry' };
95
+ }
93
96
  if (field === 'permissionMode') {
94
97
  return {
95
98
  ok: false,
@@ -60,6 +60,9 @@ export function loadDaemonConfig() {
60
60
  export function isRestartRecoveryEnabled(config = loadDaemonConfig()) {
61
61
  return config.restartRecovery?.enabled === true;
62
62
  }
63
+ export function isSkillWatchEnabled(config = loadDaemonConfig()) {
64
+ return config.skills?.watch === true;
65
+ }
63
66
  let eckSnapshotsConfigCache;
64
67
  /** Parse the process-level snapshot gate once for the current daemon lifecycle. */
65
68
  export function initializeEckSnapshotsConfig(config = loadDaemonConfig()) {
@@ -103,6 +106,18 @@ function validateDaemonConfig(value, validateAidDomain) {
103
106
  throw new Error('daemon.json.restartRecovery.enabled must be a boolean');
104
107
  }
105
108
  }
109
+ if (value.skills !== undefined) {
110
+ if (!value.skills || typeof value.skills !== 'object' || Array.isArray(value.skills)) {
111
+ throw new Error('daemon.json.skills must be an object');
112
+ }
113
+ const unknownSkillsFields = Object.keys(value.skills).filter(key => key !== 'watch');
114
+ if (unknownSkillsFields.length > 0) {
115
+ throw new Error('daemon.json.skills contains unsupported fields: ' + unknownSkillsFields.join(', '));
116
+ }
117
+ if (value.skills.watch !== undefined && typeof value.skills.watch !== 'boolean') {
118
+ throw new Error('daemon.json.skills.watch must be a boolean');
119
+ }
120
+ }
106
121
  const minEvolVersion = value.aun?.minEvolVersion;
107
122
  if (minEvolVersion !== undefined && !parseStableSemver(minEvolVersion)) {
108
123
  throw new Error('daemon.json.aun.minEvolVersion must use stable X.Y.Z format');
@@ -5,6 +5,7 @@ import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
5
5
  import { logger } from '../../utils/logger.js';
6
6
  import { canonicalizeEvolcoreCommandArgv } from '../permission/ec-command-parser.js';
7
7
  import { MAIN_CLI_BIN } from '../../product.js';
8
+ import { DebugDelegationRegistry, DEBUG_TOKEN_PREFIX } from './debug-context.js';
8
9
  export const AGENT_DELEGATION_TOKEN_ENV = 'EVOLCORE_DELEGATION_TOKEN';
9
10
  export const CODEX_SESSION_TOKEN_ENV = 'EVOLCORE_CODEX_SESSION_TOKEN';
10
11
  export const AGENT_DELEGATION_COMMAND_TTL_MS = 5 * 60_000;
@@ -24,9 +25,11 @@ export class AgentDelegationRegistry {
24
25
  grantsByHash = new Map();
25
26
  activeHashBySession = new Map();
26
27
  approvedCommands = new Map();
28
+ debugRegistry = new DebugDelegationRegistry();
27
29
  getRuntimeEpoch() {
28
30
  return this.runtimeEpoch;
29
31
  }
32
+ /** 发行正常 task delegation token */
30
33
  issue(input) {
31
34
  if (input.executionIdentity && !isValidFullAccessExecutionIdentity(input.executionIdentity)) {
32
35
  throw new Error('invalid fullaccess task execution identity');
@@ -38,11 +41,21 @@ export class AgentDelegationRegistry {
38
41
  this.activeHashBySession.set(input.sessionId, tokenHash);
39
42
  return token;
40
43
  }
44
+ /** 发行 debug token(dbg: 前缀),仅 debug 模式下调用 */
45
+ issueDebug(input) {
46
+ return this.debugRegistry.issue(input);
47
+ }
41
48
  validate(token, sessionId, commandHash) {
49
+ if (token?.startsWith(DEBUG_TOKEN_PREFIX)) {
50
+ return this.debugRegistry.validate(token, sessionId);
51
+ }
42
52
  return this.validateToken(token, sessionId, commandHash, true);
43
53
  }
44
54
  /** Authenticate a CLI context lookup without spending its approved operation. */
45
55
  validateForContext(token, sessionId, commandHash) {
56
+ if (token?.startsWith(DEBUG_TOKEN_PREFIX)) {
57
+ return this.debugRegistry.validate(token, sessionId);
58
+ }
46
59
  return this.validateToken(token, sessionId, commandHash, false);
47
60
  }
48
61
  validateToken(token, sessionId, commandHash, consumeApproval) {
@@ -211,7 +224,18 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
211
224
  fromControlChannel: false,
212
225
  ...trustedExecutionIdentityInput(grant),
213
226
  });
214
- const subject = { ...builtSubject, peerKey: grant.peerKey };
227
+ // 跨话题发送:目标 relation 由话题推断,按目标话题的会话类型和 relation 校验
228
+ // 「当前对端/当前群」约束;角色仍沿用来源会话,不因目标而提权。
229
+ const subject = input.threadRouted
230
+ ? {
231
+ ...builtSubject,
232
+ chatType: (input.scope === 'group' ? 'group' : 'private'),
233
+ channelId: input.to,
234
+ conversationId: input.to,
235
+ actorId: input.scope === 'group' ? builtSubject.actorId : input.to,
236
+ peerKey: formatPeerKey(grant.channelType, input.to),
237
+ }
238
+ : { ...builtSubject, peerKey: grant.peerKey };
215
239
  const decision = authorizeOperation({
216
240
  source: 'agent-tool',
217
241
  intent: {
@@ -0,0 +1,139 @@
1
+ /**
2
+ * debug-context.ts — 调试模式共享逻辑
3
+ *
4
+ * 供两端共用,不依赖 src/cli/:
5
+ * - CLI 侧:src/cli/debug-mode.ts(resolveDebugContext)
6
+ * - daemon 侧:src/ipc.ts(debug-issue-token IPC handler)
7
+ *
8
+ * 包含:
9
+ * - DebugDelegationRegistry:独立 grant 通道,token 带 dbg: 前缀,不影响主 registry
10
+ * - buildDebugTaskContext:从 Session 重建 TaskRuntimeContext(从 launch-consolidation.ts 迁移)
11
+ */
12
+ import crypto from 'crypto';
13
+ import { formatPeerKey } from '../relation/peer-identity.js';
14
+ import { logger } from '../../utils/logger.js';
15
+ // ---------------------------------------------------------------------------
16
+ // DebugDelegationRegistry
17
+ // ---------------------------------------------------------------------------
18
+ /** Token TTL for debug grants — longer than production (30 min vs 5 min). */
19
+ const DEBUG_TOKEN_TTL_MS = 30 * 60_000;
20
+ /** Prefix that identifies a debug delegation token at the validation entry point. */
21
+ export const DEBUG_TOKEN_PREFIX = 'dbg:';
22
+ function sha256(data) {
23
+ return crypto.createHash('sha256').update(data).digest('hex');
24
+ }
25
+ /**
26
+ * Standalone delegation registry for debug mode.
27
+ *
28
+ * Uses a separate Map (debugGrantsByHash) so debug tokens never touch
29
+ * activeHashBySession in AgentDelegationRegistry, and issuing a debug token
30
+ * never triggers revokeSession on a live task.
31
+ *
32
+ * Token format: `dbg:<32-byte random hex>`
33
+ * Hash is computed over the raw part (after the prefix) only.
34
+ */
35
+ export class DebugDelegationRegistry {
36
+ debugGrantsByHash = new Map();
37
+ issue(input) {
38
+ const raw = crypto.randomBytes(32).toString('hex');
39
+ const token = `${DEBUG_TOKEN_PREFIX}${raw}`;
40
+ const tokenHash = sha256(raw);
41
+ const grant = {
42
+ tokenHash,
43
+ sessionId: input.sessionId,
44
+ taskId: `debug-${Date.now()}`,
45
+ actorId: input.actorId ?? input.selfAid,
46
+ channel: input.channel,
47
+ channelType: input.channelType,
48
+ chatType: input.chatType,
49
+ selfAid: input.selfAid,
50
+ peerKey: input.peerKey,
51
+ issuedRole: input.issuedRole ?? 'debug',
52
+ expiresAt: Date.now() + DEBUG_TOKEN_TTL_MS,
53
+ };
54
+ this.debugGrantsByHash.set(tokenHash, grant);
55
+ logger.debug(`[DebugDelegation] issued token for session=${input.sessionId}`);
56
+ return token;
57
+ }
58
+ validate(token, sessionId) {
59
+ const raw = token.startsWith(DEBUG_TOKEN_PREFIX) ? token.slice(DEBUG_TOKEN_PREFIX.length) : token;
60
+ const tokenHash = sha256(raw);
61
+ const grant = this.debugGrantsByHash.get(tokenHash);
62
+ if (!grant) {
63
+ return { ok: false, code: 'INVALID_DELEGATION', reason: 'unknown debug token', diagnosticCause: 'unknown_token' };
64
+ }
65
+ if (grant.sessionId !== sessionId) {
66
+ return { ok: false, code: 'INVALID_DELEGATION', reason: 'session mismatch', diagnosticCause: 'session_mismatch' };
67
+ }
68
+ if (Date.now() > grant.expiresAt) {
69
+ this.debugGrantsByHash.delete(tokenHash);
70
+ return { ok: false, code: 'INVALID_DELEGATION', reason: 'debug token expired', diagnosticCause: 'task_replaced' };
71
+ }
72
+ return { ok: true, grant };
73
+ }
74
+ /** Remove all grants for a session (called when session is unbound). */
75
+ revokeSession(sessionId) {
76
+ for (const [hash, grant] of this.debugGrantsByHash) {
77
+ if (grant.sessionId === sessionId) {
78
+ this.debugGrantsByHash.delete(hash);
79
+ }
80
+ }
81
+ }
82
+ /** Prune expired grants. */
83
+ pruneExpired() {
84
+ const now = Date.now();
85
+ for (const [hash, grant] of this.debugGrantsByHash) {
86
+ if (now > grant.expiresAt)
87
+ this.debugGrantsByHash.delete(hash);
88
+ }
89
+ }
90
+ }
91
+ export function buildDebugTaskContext(input) {
92
+ const { session } = input;
93
+ const meta = (session.metadata ?? {});
94
+ const sessionType = session.sessionType ?? meta.sessionType ?? 'main';
95
+ // channel = 类型名;channelKey = 完整实例 key。二者分开,
96
+ // 与 originFromActorSession 的 `session.metadata?.channelKey || session.channel` 同源。
97
+ let channel = session.channelType || session.channel;
98
+ let channelKey = meta.channelKey || session.channel;
99
+ let chatType = session.chatType || 'private';
100
+ let peerId = meta.peerId;
101
+ let peerKeyId = chatType === 'group'
102
+ ? (meta.groupId || session.channelId)
103
+ : (meta.peerId || session.channelId);
104
+ let threadId = session.threadId || undefined;
105
+ if (session.channelId?.startsWith('trigger:') && session.selfAID && input.getTrigger) {
106
+ const triggerId = session.channelId.slice('trigger:'.length);
107
+ const origin = input.getTrigger(session.selfAID, triggerId)?.origin;
108
+ if (origin?.channelType) {
109
+ channel = origin.channelType;
110
+ if (origin.channelKey)
111
+ channelKey = origin.channelKey;
112
+ chatType = origin.delivery?.chatType ?? 'private';
113
+ peerId = origin.peerId;
114
+ peerKeyId = chatType === 'group' ? origin.channelId : (origin.peerId ?? origin.channelId);
115
+ threadId = origin.threadId;
116
+ }
117
+ }
118
+ return {
119
+ sessionId: session.id,
120
+ taskId: `debug-${Date.now()}`,
121
+ messageId: `debug-msg-${Date.now()}`,
122
+ baseagent: session.baseagent,
123
+ channel,
124
+ channelKey,
125
+ channelId: session.channelId,
126
+ chatType,
127
+ selfAid: session.selfAID,
128
+ peerId,
129
+ peerKey: channel && peerKeyId ? formatPeerKey(channel, peerKeyId) : undefined,
130
+ peerName: meta.peerName,
131
+ peerType: meta.peerType,
132
+ threadId,
133
+ sessionType,
134
+ memoryMode: meta.memoryMode,
135
+ memoryScope: meta.memoryScope,
136
+ consolidationBatchDir: meta.consolidationBatchDir ?? meta.consolidationLogBase,
137
+ sessionRuntimeDir: input.sessionRuntimeDir,
138
+ };
139
+ }
@@ -331,6 +331,15 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
331
331
  };
332
332
  }
333
333
  let matchResult = matchCommandPermission(operation, opMeta.category, roleDef.commandPermissions || {}, opMeta.dangerous);
334
+ if (!matchResult && operation.startsWith('ec.thread.') && opMeta.category === 'read' && intent.scope === 'relation') {
335
+ const permission = roleDef.commandPermissions?.['session.topic.list'];
336
+ if (permission?.allow)
337
+ matchResult = {
338
+ permission: { ...permission, scopes: ['relation'], constraints: { ...permission.constraints, currentRelationOnly: true, targetCurrentAgentOnly: true } },
339
+ matchedRule: 'session.topic.list',
340
+ rank: 1000,
341
+ };
342
+ }
334
343
  // Agent self-service contact add deliberately reuses the same low-trust
335
344
  // relation boundary as ec.msg.send. Older per-Agent role files may still
336
345
  // contain the historical contact.* deny without an explicit contact.add
@@ -461,6 +470,14 @@ function requiresDaemonOwner(ctx, resolvedConfigCommand) {
461
470
  if (operation === 'agent.reload') {
462
471
  return !isCurrentAgentTarget(ctx);
463
472
  }
473
+ // Same shape as agent.reload: the current Agent's owner/admin refresh their
474
+ // own skill index; refreshing another Agent's index needs the daemon owner.
475
+ if (operation === 'skill.refresh' && hasDaemonOwnerRole(ctx)) {
476
+ return true;
477
+ }
478
+ if (operation === 'skill.refresh') {
479
+ return !isCurrentAgentTarget(ctx);
480
+ }
464
481
  if (operation.startsWith('trigger.')) {
465
482
  if (ctx.fromControlChannel)
466
483
  return true;
@@ -478,7 +495,7 @@ function hasDaemonOwnerRole(ctx) {
478
495
  || ctx.processRole === 'daemon-service';
479
496
  }
480
497
  function daemonOwnerDenialReasonCode(ctx) {
481
- if ((ctx.intent.operation === 'agent.show' || ctx.intent.operation === 'agent.reload')
498
+ if ((ctx.intent.operation === 'agent.show' || ctx.intent.operation === 'agent.reload' || ctx.intent.operation === 'skill.refresh')
482
499
  && ctx.intent.scope !== 'control'
483
500
  && !ctx.fromControlChannel
484
501
  && !isCurrentAgentTarget(ctx)) {
@@ -894,6 +911,12 @@ function checkOwnPeer(ctx) {
894
911
  ? { ok: true }
895
912
  : { ok: false, reason: 'Only the current group relation can be targeted' };
896
913
  }
914
+ // For ec.msg.send / ec.msg.file, args.peer is the recipient (to), not a
915
+ // self-targeting field. Cross-topic sends are checked against their indexed
916
+ // target session by the managed message operation.
917
+ const isMsgSend = ctx.intent.operation === 'ec.msg.send' || ctx.intent.operation === 'ec.msg.file';
918
+ if (isMsgSend)
919
+ return { ok: true };
897
920
  if (ctx.actorId) {
898
921
  if (requestedPeerId && requestedPeerId !== ctx.actorId) {
899
922
  return { ok: false, reason: ownPeerOnlyDenialReason(ctx, requestedPeerId) };
@@ -307,6 +307,22 @@ const OPERATIONS = [
307
307
  description: '查看当前会话状态',
308
308
  sources: ['slash', 'menu', 'agent-tool'],
309
309
  },
310
+ {
311
+ id: 'session.loop.current',
312
+ category: 'read',
313
+ dangerous: false,
314
+ defaultScopes: ['relation'],
315
+ description: '查看当前主会话或话题的 idle 触发设置',
316
+ sources: ['slash', 'menu', 'ecweb', 'control'],
317
+ },
318
+ {
319
+ id: 'session.loop.update',
320
+ category: 'write-own',
321
+ dangerous: false,
322
+ defaultScopes: ['relation'],
323
+ description: '更新当前主会话或话题的 idle 触发设置',
324
+ sources: ['slash', 'menu', 'ecweb', 'control'],
325
+ },
310
326
  {
311
327
  id: 'session.topic.current',
312
328
  category: 'read',
@@ -767,6 +783,23 @@ const OPERATIONS = [
767
783
  description: '重载 agent 配置',
768
784
  sources: ['slash', 'menu', 'agent-tool', 'control'],
769
785
  },
786
+ {
787
+ id: 'skill.refresh',
788
+ category: 'write-agent',
789
+ dangerous: false,
790
+ defaultScopes: ['agent', 'control'],
791
+ description: '重建技能索引文件',
792
+ sources: ['agent-tool', 'control'],
793
+ },
794
+ ...['list', 'list-peers', 'show', 'history', 'check-index', 'summary-show', 'summary-log', 'create', 'rename', 'label', 'status', 'delete', 'summary-set', 'summary-clear', 'rebuild-index'].map(action => ({
795
+ id: `ec.thread.${action}`,
796
+ category: (['list', 'list-peers', 'show', 'history', 'check-index', 'summary-show', 'summary-log'].includes(action)
797
+ ? 'read' : action === 'rebuild-index' ? 'write-agent' : 'write-own'),
798
+ dangerous: false,
799
+ defaultScopes: ['relation', 'agent'],
800
+ description: `${action} managed conversation threads`,
801
+ sources: ['agent-tool'],
802
+ })),
770
803
  {
771
804
  id: 'agent.bootstrapComplete',
772
805
  category: 'write-agent',
@@ -0,0 +1,212 @@
1
+ /**
2
+ * session-cleanup.ts — 独立后台会话(bg-session)和 trigger 会话的清理机制
3
+ *
4
+ * 三种自动清理模式:
5
+ * immediate — 会话结束时同步清理
6
+ * on_next_launch — 下次同 kind 会话启动时清理上一次的残留(默认)
7
+ * expire — 会话结束时写登记表,启动时扫描到期条目清理
8
+ *
9
+ * 登记表路径:$EVOLCORE_HOME/data/session-cleanup/expire-<YYYYMMDD>.jsonl
10
+ * 详见 docs/background-session/session-cleanup-policy.md
11
+ */
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { resolveRoot } from '../../paths.js';
15
+ import { logger } from '../../utils/logger.js';
16
+ // ── 登记表工具 ──────────────────────────────────────────────────────────────
17
+ function cleanupDir() {
18
+ return path.join(resolveRoot(), 'data', 'session-cleanup');
19
+ }
20
+ function expireFilePath(date) {
21
+ const y = date.getFullYear();
22
+ const m = String(date.getMonth() + 1).padStart(2, '0');
23
+ const d = String(date.getDate()).padStart(2, '0');
24
+ return path.join(cleanupDir(), `expire-${y}${m}${d}.jsonl`);
25
+ }
26
+ function doneFilePath(expirePath) {
27
+ return expirePath.replace(/\.jsonl$/, '.done.jsonl');
28
+ }
29
+ /**
30
+ * 把一条超期记录写进今天的登记表。
31
+ * 幂等:同一 channelId 写多次无害,runExpireCleanup 会逐条处理。
32
+ */
33
+ export function appendExpireEntry(entry) {
34
+ try {
35
+ const dir = cleanupDir();
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ const filePath = expireFilePath(new Date(entry.expireAt));
38
+ fs.appendFileSync(filePath, JSON.stringify(entry) + '\n', 'utf-8');
39
+ }
40
+ catch (e) {
41
+ logger.warn(`[SessionCleanup] 写入超期登记表失败: ${e?.message}`);
42
+ }
43
+ }
44
+ /**
45
+ * 清理一条 evolcore session 记录(移入 _trash/)。
46
+ * runner 侧日志不在这里处理(runner 接口尚未提供 deleteSession)。
47
+ */
48
+ export async function cleanupSessionRecord(target, cleaner) {
49
+ try {
50
+ await cleaner.softDeleteSession(target.channelId);
51
+ logger.debug(`[SessionCleanup] 已清理 session 记录: channelId=${target.channelId}` +
52
+ `${target.agentAid ? ` aid=${target.agentAid}` : ''}`);
53
+ }
54
+ catch (e) {
55
+ logger.warn(`[SessionCleanup] 清理 session 记录失败 channelId=${target.channelId}: ${e?.message}`);
56
+ }
57
+ }
58
+ // ── expire 模式:启动时扫描 ──────────────────────────────────────────────────
59
+ /**
60
+ * 扫描 data/session-cleanup/ 下所有登记表,清理已到期的条目。
61
+ * 在任意 bg-session 或 trigger 会话启动时调用,成本低(只读少量小文件)。
62
+ */
63
+ export async function runExpireCleanup(cleaner, now = Date.now()) {
64
+ const dir = cleanupDir();
65
+ if (!fs.existsSync(dir))
66
+ return;
67
+ let files;
68
+ try {
69
+ files = fs.readdirSync(dir).filter(f => /^expire-\d{8}\.jsonl$/.test(f));
70
+ }
71
+ catch {
72
+ return;
73
+ }
74
+ const todayStr = (() => {
75
+ const d = new Date(now);
76
+ const y = d.getFullYear();
77
+ const m = String(d.getMonth() + 1).padStart(2, '0');
78
+ const day = String(d.getDate()).padStart(2, '0');
79
+ return `expire-${y}${m}${day}.jsonl`;
80
+ })();
81
+ for (const file of files) {
82
+ // 跳过未来日期的文件(expireAt 不可能到期)
83
+ const fileDate = file.slice(7, 15); // expire-YYYYMMDD.jsonl → YYYYMMDD
84
+ const todayDate = todayStr.slice(7, 15);
85
+ if (fileDate > todayDate)
86
+ continue;
87
+ const filePath = path.join(dir, file);
88
+ const donePath = doneFilePath(filePath);
89
+ let lines;
90
+ try {
91
+ lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(l => l.trim());
92
+ }
93
+ catch {
94
+ continue;
95
+ }
96
+ // 读取已处理的 channelId 集合
97
+ const doneIds = new Set();
98
+ if (fs.existsSync(donePath)) {
99
+ try {
100
+ for (const line of fs.readFileSync(donePath, 'utf-8').split('\n').filter(l => l.trim())) {
101
+ const entry = JSON.parse(line);
102
+ if (entry.channelId)
103
+ doneIds.add(entry.channelId);
104
+ }
105
+ }
106
+ catch { /* 损坏的 done 文件,跳过去重,重复清理幂等 */ }
107
+ }
108
+ let processedCount = 0;
109
+ // 文件里是否还有未到期条目;有则保留文件,下次再扫
110
+ let hasPending = false;
111
+ for (const line of lines) {
112
+ let entry;
113
+ try {
114
+ entry = JSON.parse(line);
115
+ }
116
+ catch {
117
+ continue;
118
+ }
119
+ if (!entry.channelId || !entry.expireAt)
120
+ continue;
121
+ if (entry.expireAt > now) {
122
+ hasPending = true;
123
+ continue;
124
+ } // 未到期
125
+ if (doneIds.has(entry.channelId))
126
+ continue; // 已处理
127
+ await cleanupSessionRecord({ sessionId: entry.sessionId, channelId: entry.channelId, agentAid: entry.agentAid }, cleaner);
128
+ // 标记已处理
129
+ try {
130
+ fs.appendFileSync(donePath, JSON.stringify({ channelId: entry.channelId, ts: now }) + '\n', 'utf-8');
131
+ }
132
+ catch { /* 写失败无妨,下次会重复处理,cleanupSessionRecord 幂等 */ }
133
+ doneIds.add(entry.channelId);
134
+ processedCount++;
135
+ }
136
+ if (processedCount > 0) {
137
+ logger.debug(`[SessionCleanup] expire 扫描:清理 ${processedCount} 条到期记录(${file})`);
138
+ }
139
+ // 整天文件全部处理完时,删除两个文件。
140
+ // 不能用 doneIds.size 与行数比较:同一 channelId 重复登记时两者永远不等,文件会残留。
141
+ if (!hasPending) {
142
+ try {
143
+ fs.unlinkSync(filePath);
144
+ }
145
+ catch { /* ignore */ }
146
+ try {
147
+ fs.unlinkSync(donePath);
148
+ }
149
+ catch { /* ignore */ }
150
+ }
151
+ }
152
+ }
153
+ // ── on_next_launch 模式:按 kind 清理历史会话 ────────────────────────────────
154
+ /**
155
+ * 记录一条 on_next_launch 清理登记,供下次启动时使用。
156
+ * 存在一个独立的 JSONL 文件中,按 kind 分文件存储。
157
+ */
158
+ function nextLaunchFilePath(kind) {
159
+ const safeKind = kind.replace(/[^a-zA-Z0-9\-_]/g, '_');
160
+ return path.join(cleanupDir(), `next-launch-${safeKind}.jsonl`);
161
+ }
162
+ /**
163
+ * 记录一条「下次启动时清理」的会话信息。
164
+ * 在会话正常或异常结束时调用。
165
+ */
166
+ export function recordNextLaunchCleanup(kind, entry) {
167
+ try {
168
+ const dir = cleanupDir();
169
+ fs.mkdirSync(dir, { recursive: true });
170
+ const filePath = nextLaunchFilePath(kind);
171
+ fs.appendFileSync(filePath, JSON.stringify(entry) + '\n', 'utf-8');
172
+ }
173
+ catch (e) {
174
+ logger.warn(`[SessionCleanup] 写入 on_next_launch 登记失败 kind=${kind}: ${e?.message}`);
175
+ }
176
+ }
177
+ /**
178
+ * 清理同 kind 的所有历史会话记录,然后清空登记文件。
179
+ * 在新会话启动前调用(on_next_launch 模式)。
180
+ */
181
+ export async function cleanupPreviousByKind(kind, cleaner) {
182
+ const filePath = nextLaunchFilePath(kind);
183
+ if (!fs.existsSync(filePath))
184
+ return;
185
+ let lines;
186
+ try {
187
+ lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(l => l.trim());
188
+ }
189
+ catch {
190
+ return;
191
+ }
192
+ for (const line of lines) {
193
+ let entry;
194
+ try {
195
+ entry = JSON.parse(line);
196
+ }
197
+ catch {
198
+ continue;
199
+ }
200
+ if (!entry.channelId)
201
+ continue;
202
+ await cleanupSessionRecord({ sessionId: entry.sessionId, channelId: entry.channelId, agentAid: entry.agentAid }, cleaner);
203
+ }
204
+ // 清空登记文件(下一次重新积累)
205
+ try {
206
+ fs.unlinkSync(filePath);
207
+ }
208
+ catch { /* ignore */ }
209
+ if (lines.length > 0) {
210
+ logger.debug(`[SessionCleanup] on_next_launch:已清理 ${lines.length} 条历史记录(kind=${kind})`);
211
+ }
212
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * background-session/types.ts — 独立后台会话的接口定义
3
+ *
4
+ * 独立后台会话(bg-session)是由 daemon 内部逻辑主动拉起的 AI 会话,
5
+ * 不属于用户交互,也不挂在 trigger 调度系统下。
6
+ * 整理会话、话题总结会话等都通过这套接口拉起。
7
+ *
8
+ * 详细机制见 docs/background-session/background-session-launcher.md
9
+ */
10
+ export {};
@@ -53,7 +53,7 @@ export function renderPostBootstrapWelcome(aid, owner, ownerName) {
53
53
  }
54
54
  const config = loadAgent(aid);
55
55
  if (config && resolveAgentLifecycle(config) === 'bootstrapping' && config.initialization?.mode !== 'supplied') {
56
- throw new Error('WELCOME_REQUIRED: Owner-confirmed welcome.md is required before activation');
56
+ throw new Error('WELCOME_REQUIRED: Agent-prepared welcome.md is required before activation');
57
57
  }
58
58
  return generateWelcomeMessage({
59
59
  channelType: 'aun',
@@ -25,14 +25,17 @@ export async function completeBootstrapWithWelcome(service, aid, welcome, sessio
25
25
  }
26
26
  if (lifecycle === 'bootstrapping') {
27
27
  // Existing per-Owner records are immutable retry snapshots. New interactive
28
- // Bootstrap completions require the text saved after Owner confirmation.
28
+ // Bootstrap completions require the welcome text prepared by the Agent.
29
29
  const config = loadAgent(aid);
30
30
  const deliveries = config?.owners?.length ? postBootstrapWelcomeStatus(aid, config.owners).deliveries : [];
31
31
  const alreadyPrepared = deliveries.length > 0 && deliveries.every(delivery => delivery.status !== 'not_prepared');
32
32
  if (config?.initialization?.mode !== 'supplied' && !alreadyPrepared) {
33
33
  try {
34
34
  if (readAgentWelcomeTemplate(aid) === null) {
35
- return { ok: false, code: 'WELCOME_REQUIRED', error: '请先在 Bootstrap 对话中生成欢迎文案,经 Owner 确认后保存 welcome.md', reloaded: false };
35
+ return {
36
+ ok: false, code: 'WELCOME_REQUIRED', reloaded: false,
37
+ error: '请先自主生成正式欢迎文案并保存 welcome.md,无需 Owner 确认',
38
+ };
36
39
  }
37
40
  }
38
41
  catch (error) {
@@ -446,7 +449,7 @@ export class BootstrapService {
446
449
  const templatePath = path.join(kitsTemplatesDir(), 'bootstrap-welcome.md');
447
450
  const template = fs.existsSync(templatePath)
448
451
  ? fs.readFileSync(templatePath, 'utf-8')
449
- : '你好,我是 {{agentName}}。请帮我确认显示名、简介和标签。';
452
+ : '你好,我是 {{agentName}}。已有的有效基础信息会直接沿用;请补充缺失的显示名、简介或标签。persona 和头像可选,之后也可以完善。';
450
453
  return renderTemplate(template, vars).trim();
451
454
  }
452
455
  resolveAgentDisplayName(aid) {