evolcore 0.0.34 → 0.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/install-codex-managed-hooks.mjs +26 -3
  3. package/dist/agents/claude-runner.js +11 -1
  4. package/dist/agents/codex/diagnostics.js +42 -11
  5. package/dist/agents/codex-runner.js +27 -3
  6. package/dist/agents/ecagent-runner.js +17 -7
  7. package/dist/aun/aid/agentmd.js +13 -2
  8. package/dist/aun/aid/client.js +1 -1
  9. package/dist/aun/msg/managed-operation.js +16 -0
  10. package/dist/aun/msg/p2p.js +18 -2
  11. package/dist/channels/aun.js +113 -7
  12. package/dist/cli/aun-commands.js +37 -13
  13. package/dist/cli/daemon-commands.js +10 -4
  14. package/dist/cli/fs-command.js +1 -1
  15. package/dist/cli/getmeta-command.js +211 -0
  16. package/dist/cli/index.js +6 -0
  17. package/dist/cli/managed-command-guard.js +3 -0
  18. package/dist/cli/memory-command.js +46 -15
  19. package/dist/cli/thread-command.js +12 -4
  20. package/dist/config/resolved-config-op.js +25 -8
  21. package/dist/core/auth/operation-authorizer.js +21 -2
  22. package/dist/core/channel-runtime.js +1 -1
  23. package/dist/core/command/command-handler.js +7 -1
  24. package/dist/core/command/menu-handler.js +2 -0
  25. package/dist/core/command/topic-slash.js +4 -7
  26. package/dist/core/event-catalog.js +14 -0
  27. package/dist/core/ipc-registration.js +11 -1
  28. package/dist/core/memory/batch-meta.js +1 -0
  29. package/dist/core/memory/consolidation-context.js +97 -14
  30. package/dist/core/memory/consolidation-trigger.js +15 -7
  31. package/dist/core/memory/consolidation.js +55 -0
  32. package/dist/core/memory/memory-writer.js +20 -4
  33. package/dist/core/message/aun-ipc-sender.js +19 -1
  34. package/dist/core/message/message-bridge.js +3 -3
  35. package/dist/core/message/message-log.js +1 -0
  36. package/dist/core/message/peer-mode.js +17 -0
  37. package/dist/core/message/response-engine.js +15 -4
  38. package/dist/core/permission/ec-command-parser.js +76 -40
  39. package/dist/core/permission/tool-policy.js +32 -2
  40. package/dist/core/relation/peer-identity.js +87 -12
  41. package/dist/core/session/session-manager.js +56 -1
  42. package/dist/core/session/session-title.js +26 -1
  43. package/dist/core/system-session/memory.js +87 -20
  44. package/dist/core/thread/command-parser.js +6 -1
  45. package/dist/core/thread/ipc-operation.js +33 -16
  46. package/dist/core/thread/thread-operation.js +62 -24
  47. package/dist/core/thread/thread-service.js +246 -32
  48. package/dist/utils/cross-platform.js +6 -3
  49. package/kits/docs/evolcore/fs-architecture.md +5 -5
  50. package/kits/docs/evolcore/fs.md +1 -1
  51. package/kits/docs/evolcore/group-collaboration.md +1 -1
  52. package/kits/docs/evolcore/group.md +5 -3
  53. package/kits/docs/evolcore/memory-consolidation.md +1 -2
  54. package/kits/docs/evolcore/memory-tags.md +1 -1
  55. package/kits/docs/evolcore/memory.md +2 -2
  56. package/kits/docs/evolcore/stats.md +1 -1
  57. package/kits/docs/evolcore/thread-chain.md +25 -9
  58. package/kits/eck_manifest.auxiliary.json +9 -0
  59. package/kits/eck_manifest.json +20 -0
  60. package/kits/eck_manifest.memory.json +20 -0
  61. package/kits/rules/01-overview.md +3 -3
  62. package/kits/rules/07-memory-and-thread.md +1 -1
  63. package/kits/skills/ec-group/SKILL.md +6 -3
  64. package/kits/skills/thread-ops/SKILL.md +18 -18
  65. package/kits/templates/memory-consolidation/notes.md +1 -1
  66. package/kits/templates/memory-consolidation/workflow.md +36 -1
  67. package/kits/templates/system-fragments/bash-description.md +9 -0
  68. package/kits/templates/system-fragments/memory.md +2 -2
  69. package/package.json +1 -1
@@ -259,14 +259,18 @@ async function runManagedMsg(args, formatJson) {
259
259
  }
260
260
  }
261
261
  else {
262
- if (runtime && (runtime.channel !== 'aun' || runtime.chatType !== 'private')) {
262
+ // `--thread` 是话题作用域的读取:目标对端由话题推断,数据是本 agent 自己的
263
+ // 消息日志,因此既不受"当前必须是私聊"限制(群会话里也要能查 A↔C 的话题),
264
+ // 也不受"只能查当前对端"限制。不带 --thread 时保持原有边界。
265
+ const threadScoped = parsed.command.kind === 'history' && !!parsed.command.threadId;
266
+ if (runtime && (runtime.channel !== 'aun' || runtime.chatType !== 'private') && !threadScoped) {
263
267
  failManagedGroup(formatJson, 'SCOPE_MISMATCH', 'ec msg history/online 只能读取当前 AUN 私聊关系');
264
268
  }
265
269
  // owner/admin 可以跨对端读历史(每次查询仍只针对一个对端)。这里只是快速
266
270
  // 预检:角色未知时放行交给 daemon 判定,daemon 侧 handleMsgOperation 才是
267
271
  // 权威关卡,member 由 currentRelationOnly 约束、visitor 直接拒绝。
268
272
  const mayReadCrossPeer = parsed.command.kind === 'history'
269
- && (runtime?.peerRole === undefined || isManagementRole(runtime.peerRole));
273
+ && (threadScoped || runtime?.peerRole === undefined || isManagementRole(runtime.peerRole));
270
274
  if (!mayReadCrossPeer && runtime?.channelId && parsed.command.target !== runtime.channelId.replace(/^@/, '')) {
271
275
  failManagedGroup(formatJson, 'SCOPE_MISMATCH', `只能读取当前 AUN 私聊对端:${runtime.channelId}`);
272
276
  }
@@ -2065,10 +2069,15 @@ Options:
2065
2069
  // ==================== Group ====================
2066
2070
  const GROUP_FROM_AID_FORMAT_EXAMPLE = 'sender.example.invalid';
2067
2071
  const GROUP_AID_FORMAT_EXAMPLE = '12345.example.invalid';
2068
- function printGroupSendAidHint() {
2069
- console.error('正确格式: ec group send <发送方AID> <真实群AID> <正文或文件参数>');
2070
- console.error(`仅格式示例(.example.invalid 是保留的虚构域,不是真实 AID): ec group send ${GROUP_FROM_AID_FORMAT_EXAMPLE} ${GROUP_AID_FORMAT_EXAMPLE} "hello"`);
2071
- console.error('查询真实群 AID: ec group list <发送方AID>');
2072
+ function printGroupArgsHint(sub) {
2073
+ if (sub === 'send') {
2074
+ console.error('正确格式: ec group send <发送方AID> <真实群AID> <正文或文件参数>');
2075
+ }
2076
+ else {
2077
+ console.error(`正确格式: ec group ${sub} <发送方AID> <真实群AID> [选项]`);
2078
+ }
2079
+ console.error(`仅格式示例(.example.invalid 是保留的虚构域,不是真实 AID): ec group ${sub} ${GROUP_FROM_AID_FORMAT_EXAMPLE} ${GROUP_AID_FORMAT_EXAMPLE}`);
2080
+ console.error('顺序固定为「先发送方 AID,后群 AID」;查询真实群 AID: ec group list <发送方AID>');
2072
2081
  }
2073
2082
  function printGroupRulesUsage() {
2074
2083
  console.log(`用法:
@@ -2173,7 +2182,7 @@ Options:
2173
2182
  无值时默认 5m;Trigger 名:check-<目标threadId>-from-<来源threadId>-<唯一后缀>
2174
2183
  取消/调整用返回的 trigger_id(trig_…),show/update/cancel 只认 id
2175
2184
  --trigger-prompt <text> 自定义复查 Trigger prompt 中的用户段(仅 --check-every)
2176
- --mention <...> 发送时 @ 某个成员。写法固定为单引号包住、逗号分隔多个 aid
2185
+ --mention <...> 发送时 @ 某个成员。单引号包住、逗号分隔多个 aid
2177
2186
  (如 --mention 'alice.agentid.pub,bob.agentid.pub'),可多次传入
2178
2187
  --mention-all 发送时 @ 所有人
2179
2188
  --content-type <mime> 显式覆盖 MIME(仅 --file 模式)
@@ -2208,13 +2217,28 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2208
2217
  console.error('❌ 缺少 <from-aid> 参数');
2209
2218
  process.exit(1);
2210
2219
  }
2220
+ // 第 1 个位置参数必须是「发送方 AID」。群 ID 与私聊 AID 语法相同,isValidAid
2221
+ // 会放行,随后在加载证书时抛 CERT_NOT_FOUND——这条路径既不说明原因也不给出
2222
+ // 正确写法。因此先用明确的群 ID 形态判定,把"参数写反了"当场指出来。
2223
+ // 例外:invites 是多值标志/邀请列表,第 1 个位置参数不是 from;create/list 的
2224
+ // 形态不同,也在下面各自的用法里处理,这里只拦截"以群 ID 打头"这一种错位。
2211
2225
  const { isValidAid } = await import('../aun/aid/index.js');
2226
+ if (isExplicitGroupId(from)) {
2227
+ const second = args[2];
2228
+ console.error(`❌ 第 1 个位置参数 <from-aid> 无效:收到了群 ID: ${from}`);
2229
+ if (second && isValidAid(second) && !isExplicitGroupId(second)) {
2230
+ console.error(`检测到参数顺序写反了:第 2 个参数 ${second} 才是发送方 AID。`);
2231
+ console.error(`请改成: ec group ${sub} ${second} ${from}` + (sub === 'send' ? ' <正文>' : ''));
2232
+ }
2233
+ else {
2234
+ console.error('当前命令缺少发送方 AID,后续参数发生了错位。');
2235
+ }
2236
+ printGroupArgsHint(sub);
2237
+ process.exit(1);
2238
+ }
2212
2239
  if (!isValidAid(from)) {
2213
2240
  console.error(`❌ 第 1 个位置参数 <from-aid> 无效: ${from}`);
2214
- if (isExplicitGroupId(from)) {
2215
- console.error('检测到这里填写的是群 ID;当前命令缺少发送方 AID,后续参数发生了错位。');
2216
- }
2217
- printGroupSendAidHint();
2241
+ printGroupArgsHint(sub);
2218
2242
  process.exit(1);
2219
2243
  }
2220
2244
  if ((process.env.EVOLCORE_SESSION_ID || (sub === 'history' && readDelegationCredential())) && sub !== 'send') {
@@ -2286,7 +2310,7 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2286
2310
  const gid = args[2];
2287
2311
  if (!gid) {
2288
2312
  console.error('❌ 缺少第 2 个位置参数 <group-aid>');
2289
- printGroupSendAidHint();
2313
+ printGroupArgsHint(sub);
2290
2314
  process.exit(1);
2291
2315
  }
2292
2316
  try {
@@ -2294,7 +2318,7 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2294
2318
  }
2295
2319
  catch (error) {
2296
2320
  console.error(`❌ 第 2 个位置参数 <group-aid> 不是有效群 AID: ${gid}: ${error.message}`);
2297
- printGroupSendAidHint();
2321
+ printGroupArgsHint(sub);
2298
2322
  process.exit(1);
2299
2323
  }
2300
2324
  };
@@ -25,7 +25,7 @@ import { isNonAgentSessionChannel } from '../core/system-channels.js';
25
25
  import { printCodeStats } from './code-stats.js';
26
26
  import { getProcessStartTime, startTimeMatches } from '../utils/process-introspect.js';
27
27
  import { AGENT_DELEGATION_TOKEN_ENV, readDelegationCredential } from '../core/auth/agent-delegation.js';
28
- import { MAIN_CLI_BIN, RUNTIME_HOME_ENV, WEB_CLI_BIN, WEB_PACKAGE_LATEST, WEB_PACKAGE_NAME } from '../product.js';
28
+ import { MAIN_CLI_BIN, RUNTIME_HOME_ENV, TARGET_PACKAGE_ROOT_ENV, WEB_CLI_BIN, WEB_PACKAGE_LATEST, WEB_PACKAGE_NAME, } from '../product.js';
29
29
  import { rotateStdoutLog } from '../utils/log-writer.js';
30
30
  import { inspectDataMigrationRequirement } from '../core/data-migration.js';
31
31
  import { isInitCancelledError, printInitCancelHint } from './init-cancel.js';
@@ -79,7 +79,7 @@ export async function ensureCodexHookBeforeLifecycle(onRepaired) {
79
79
  });
80
80
  if (!usesCodex)
81
81
  return true;
82
- const packageRoot = getPackageRoot();
82
+ const packageRoot = process.env[TARGET_PACKAGE_ROOT_ENV] || getPackageRoot();
83
83
  const result = await ensureCodexManagedHook({
84
84
  packageRoot,
85
85
  repairDecision: state => confirmCodexHookRebind(state, packageRoot),
@@ -328,6 +328,11 @@ async function launchPackageLifecycle(source, log, options = {}) {
328
328
  const result = await platform.launchMonitorAndConfirm(args, env, pid => scanInstances().restartMonitors.some(item => item.alive && item.record.pid === pid), pid => scanInstances().restartMonitors.some(item => item.alive && item.record.pid !== pid));
329
329
  if (!result.ok && launch.stageDir)
330
330
  platform.cleanupMonitorStage(launch.stageDir);
331
+ if (!result.ok) {
332
+ for (const line of (result.stderrTail ?? '').split(/\r?\n/).filter(Boolean).slice(-8)) {
333
+ console.error(` | ${line}`);
334
+ }
335
+ }
331
336
  return { ok: result.ok, ...(!result.ok ? { reason: result.reason } : {}) };
332
337
  }
333
338
  function requireCompletedDataMigration(root) {
@@ -3304,12 +3309,13 @@ export async function cmdDiagnose(args = []) {
3304
3309
  console.log('[diagnose] - Codex managed hook: 跳过(没有 Agent 使用 Codex)');
3305
3310
  }
3306
3311
  else {
3307
- const hook = inspectCodexManagedHook({ packageRoot: getPackageRoot() });
3312
+ const packageRoot = process.env[TARGET_PACKAGE_ROOT_ENV] || getPackageRoot();
3313
+ const hook = inspectCodexManagedHook({ packageRoot });
3308
3314
  if (hook.status === 'installed') {
3309
3315
  console.log(`[diagnose] ✓ Codex managed hook 已安装: ${hook.hookPath}`);
3310
3316
  if (runHookLaunchProbe) {
3311
3317
  try {
3312
- const launch = runCodexHookLaunchDiagnostic({ packageRoot: getPackageRoot() });
3318
+ const launch = runCodexHookLaunchDiagnostic({ packageRoot });
3313
3319
  if (launch.status === 'ok') {
3314
3320
  console.log('[diagnose] ✓ Codex managed hook 启动探针报告: ' + launch.output);
3315
3321
  for (const result of launch.results) {
@@ -56,7 +56,7 @@ Options:
56
56
  --content-type <mime> 上传时指定内容类型
57
57
  --public 上传后公开可读(personal storage)
58
58
  --visibility <public|private> chmod 可见性
59
- --allow-roles <roles> chmod 角色约束,写法固定为单引号包住、逗号分隔,
59
+ --allow-roles <roles> chmod 角色约束,单引号包住、逗号分隔,
60
60
  如 --allow-roles 'owner,admin'
61
61
  -m aid:<aid>:<perms> setfacl 新增/更新 ACL
62
62
  -x aid:<aid> setfacl 移除 ACL
@@ -0,0 +1,211 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { resolvePaths, agentThreadIndexPath } from '../paths.js';
4
+ import { scanChatDirs, readLastJsonlLine, } from '../core/session/session-fs-store.js';
5
+ const HELP_TEXT = `
6
+ ec getmeta <id> [选项]
7
+
8
+ 按任意一种会话标识查询本地会话的完整元数据(不经过 daemon,直接读存档)。
9
+
10
+ <id> 接受三种形态:
11
+ meta_YYYYMMDD_<ts> ec 会话 id(meta 文件名)
12
+ thread-<...> 话题 id(threadId)
13
+ <uuid> provider 侧会话 id(agentSessionId)
14
+
15
+ 输出字段:
16
+ sessionId / threadId / agentSessionId / peer / selfAID / baseagent
17
+ name / chatType / projectPath / updatedAt / metaFile
18
+ source 命中位置:meta(主会话)/ thread(话题)/ active / trash
19
+ indexed 该会话是否登记在话题索引里
20
+
21
+ 选项:
22
+ --self <aid> 只扫描指定 agent,减少全量扫描开销
23
+ --format json 以 JSON 格式输出
24
+ `.trim();
25
+ function argValue(args, flag) {
26
+ const i = args.indexOf(flag);
27
+ return i >= 0 ? args[i + 1] : undefined;
28
+ }
29
+ function hasFlag(args, flag) {
30
+ return args.includes(flag);
31
+ }
32
+ function readJsonFile(filePath) {
33
+ try {
34
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ /** 该 agent 的话题索引里是否登记了 (peer, threadId, sessionId)。 */
41
+ function isIndexed(selfAID, peer, threadId, sessionId) {
42
+ if (!selfAID)
43
+ return false;
44
+ const raw = readJsonFile(agentThreadIndexPath(selfAID));
45
+ if (!raw?.threads)
46
+ return false;
47
+ return raw.threads.some(t => (t.sessionId === sessionId
48
+ && (t.threadId ?? '') === threadId
49
+ && (peer == null || t.peer === peer)));
50
+ }
51
+ function makeHit(meta, metaFile, source, selfAID, chatPeer) {
52
+ const threadId = meta.threadId ?? '';
53
+ return {
54
+ sessionId: meta.id,
55
+ threadId,
56
+ agentSessionId: meta.agentSessionId ?? null,
57
+ peer: meta.channelId ?? chatPeer ?? null,
58
+ selfAID: meta.selfAID ?? selfAID,
59
+ baseagent: meta.baseagent ?? null,
60
+ name: meta.name ?? null,
61
+ chatType: meta.chatType ?? null,
62
+ projectPath: meta.projectPath ?? null,
63
+ updatedAt: meta.updatedAt ?? null,
64
+ updatedAtStr: meta.updatedAtStr ?? null,
65
+ createdAtStr: meta.createdAtStr ?? null,
66
+ metaFile,
67
+ source,
68
+ indexed: isIndexed(meta.selfAID ?? selfAID, meta.channelId ?? chatPeer ?? null, threadId, meta.id),
69
+ };
70
+ }
71
+ /**
72
+ * id 形态判定。
73
+ * - `meta_...` 按文件名直接命中,不读文件内容
74
+ * - `thread-...` / uuid 需要扫末行比对,成本高但只在必要时发生
75
+ */
76
+ function classifyId(id) {
77
+ if (id.startsWith('meta_'))
78
+ return 'session';
79
+ if (id.startsWith('thread-'))
80
+ return 'thread';
81
+ return 'agentSession';
82
+ }
83
+ async function cmdGetMeta(args) {
84
+ const jsonFormat = hasFlag(args, '--format') && argValue(args, '--format') === 'json'
85
+ || hasFlag(args, '--json');
86
+ // 位置参数 = 第一个不是 flag、也不属于任何 flag 取值的参数。
87
+ // 不能简单取"首个非 - 开头",否则 `--self <aid>` 的值会被当成 id。
88
+ const VALUE_FLAGS = new Set(['--self', '--format']);
89
+ let rawId;
90
+ for (let i = 0; i < args.length; i++) {
91
+ const a = args[i];
92
+ if (a.startsWith('-')) {
93
+ if (VALUE_FLAGS.has(a))
94
+ i++;
95
+ continue;
96
+ }
97
+ rawId = a;
98
+ break;
99
+ }
100
+ if (!rawId) {
101
+ console.error('用法: ec getmeta <sessionId|threadId|agentSessionId> [--self <aid>] [--format json]');
102
+ process.exitCode = 1;
103
+ return;
104
+ }
105
+ const id = rawId.endsWith('.jsonl') ? rawId.slice(0, -'.jsonl'.length) : rawId;
106
+ const selfFilter = argValue(args, '--self');
107
+ const kind = classifyId(id);
108
+ const p = resolvePaths();
109
+ const chatDirs = scanChatDirs(p.sessionsDir).filter(c => !selfFilter || c.selfAID === selfFilter);
110
+ const hits = [];
111
+ for (const chat of chatDirs) {
112
+ // 主会话 meta 在目录根,话题在 _threads/,已删除话题在 _trash/
113
+ const locations = [
114
+ { dir: chat.dirPath, source: 'meta' },
115
+ { dir: path.join(chat.dirPath, '_threads'), source: 'thread' },
116
+ { dir: path.join(chat.dirPath, '_trash'), source: 'trash' },
117
+ ];
118
+ for (const loc of locations) {
119
+ let entries;
120
+ try {
121
+ entries = fs.readdirSync(loc.dir);
122
+ }
123
+ catch {
124
+ continue;
125
+ }
126
+ for (const entry of entries) {
127
+ if (!entry.endsWith('.jsonl') || entry.includes('.fork-history'))
128
+ continue;
129
+ const entryId = entry.slice(0, -'.jsonl'.length);
130
+ // 文件名直接命中时只读这一个文件;否则必须读末行比对
131
+ if (kind === 'session' && entryId !== id)
132
+ continue;
133
+ const filePath = path.join(loc.dir, entry);
134
+ const meta = readLastJsonlLine(filePath);
135
+ if (!meta)
136
+ continue;
137
+ if (kind === 'thread' && (meta.threadId ?? '') !== id)
138
+ continue;
139
+ if (kind === 'agentSession' && (meta.agentSessionId ?? '') !== id)
140
+ continue;
141
+ if (kind === 'session' && meta.id !== id && entryId !== id)
142
+ continue;
143
+ hits.push(makeHit(meta, filePath, loc.source, chat.selfAID, chat.channelId));
144
+ }
145
+ }
146
+ }
147
+ // 兜底:active.json 的 id/threadId/agentSessionId 也可能命中(active 是 meta 的最新快照)
148
+ if (hits.length === 0) {
149
+ for (const chat of chatDirs) {
150
+ const active = readJsonFile(path.join(chat.dirPath, 'active.json'));
151
+ if (!active)
152
+ continue;
153
+ const match = kind === 'session'
154
+ ? active.id === id
155
+ : kind === 'thread'
156
+ ? (active.threadId ?? '') === id
157
+ : (active.agentSessionId ?? '') === id;
158
+ if (!match)
159
+ continue;
160
+ hits.push(makeHit(active, path.join(chat.dirPath, 'active.json'), 'active', chat.selfAID, chat.channelId));
161
+ }
162
+ }
163
+ if (hits.length === 0) {
164
+ if (jsonFormat) {
165
+ console.log(JSON.stringify({ ok: false, id, kind, hits: [] }, null, 2));
166
+ }
167
+ else {
168
+ console.error(`✗ 未找到会话 ${id}(${kind === 'session' ? 'ec 会话 id' : kind === 'thread' ? '话题 id' : 'provider 会话 id'};已扫描 ${chatDirs.length} 个 chat 目录)`);
169
+ }
170
+ process.exitCode = 1;
171
+ return;
172
+ }
173
+ if (jsonFormat) {
174
+ console.log(JSON.stringify({ ok: true, id, kind, hits }, null, 2));
175
+ return;
176
+ }
177
+ const sourceLabel = {
178
+ meta: '主会话 meta 文件',
179
+ thread: '话题 meta 文件',
180
+ active: 'active.json 快照',
181
+ trash: '已删除(_trash)',
182
+ };
183
+ for (const h of hits) {
184
+ console.log(`会话 id: ${h.sessionId}`);
185
+ console.log(` 话题 id: ${h.threadId || '(主会话)'}`);
186
+ console.log(` provider 会话: ${h.agentSessionId ?? '(空)'}`);
187
+ console.log(` 来源: ${sourceLabel[h.source]}${h.source === 'trash' ? ' ⚠ 话题已删除' : ''}`);
188
+ console.log(` 索引: ${h.indexed ? '已登记' : '未登记(仅磁盘存档)'}`);
189
+ console.log(` 名称: ${h.name ?? '(未命名)'}`);
190
+ if (h.chatType)
191
+ console.log(` 类型: ${h.chatType === 'group' ? '群聊' : '单聊'}`);
192
+ console.log(` 对端: ${h.peer ?? ''}`);
193
+ console.log(` 本端 AID: ${h.selfAID ?? ''}`);
194
+ if (h.baseagent)
195
+ console.log(` 基座: ${h.baseagent}`);
196
+ if (h.projectPath)
197
+ console.log(` 项目: ${h.projectPath}`);
198
+ if (h.updatedAtStr)
199
+ console.log(` 最近更新: ${h.updatedAtStr}`);
200
+ console.log(` 文件: ${h.metaFile}`);
201
+ console.log('');
202
+ }
203
+ }
204
+ export async function runGetMetaCommand(args) {
205
+ const sub = args[0];
206
+ if (!sub || sub === 'help' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
207
+ console.log(HELP_TEXT);
208
+ return;
209
+ }
210
+ await cmdGetMeta(args);
211
+ }
package/dist/cli/index.js CHANGED
@@ -37,6 +37,7 @@ Agent 与会话:
37
37
  ctl 当前托管会话的控制命令
38
38
  queue 查询或操作 agent 消息队列
39
39
  session 按 ec 会话 id 查询本地 session 存档(含 agentSessionId)
40
+ getmeta 按会话 id / threadId / agentSessionId 查询会话元数据
40
41
  task 恢复暂停或异常中断的会话任务
41
42
  trigger 管理定时与事件触发器
42
43
  handoff 查询和完成跨会话回流
@@ -449,6 +450,11 @@ export async function main(args) {
449
450
  await runSessionCommand(strippedArgs.slice(1));
450
451
  break;
451
452
  }
453
+ case 'getmeta': {
454
+ const { runGetMetaCommand } = await import('./getmeta-command.js');
455
+ await runGetMetaCommand(strippedArgs.slice(1));
456
+ break;
457
+ }
452
458
  case 'group': {
453
459
  const { suppressSdkLogs } = await import('../aun/aid/index.js');
454
460
  suppressSdkLogs();
@@ -21,6 +21,9 @@ const MANAGED_TASK_COMMANDS = new Set([
21
21
  // `session` 按 ec 会话 id 只读查询本地 session 存档(meta 文件尾行的
22
22
  // agentSessionId 等),不触碰 daemon 与进程生命周期。
23
23
  'session',
24
+ // `getmeta` 按 ec 会话 id / threadId / agentSessionId 之一只读查询本地
25
+ // 会话存档,同样不触碰 daemon 与进程生命周期。
26
+ 'getmeta',
24
27
  // `skill` 管理 EC 技能(安装/开关/校验)。它的主要使用者就是 agent 自己——
25
28
  // ec-skill 技能的 SKILL.md 明确指导 agent 调用它。脚本只读写
26
29
  // `thread` 提供话题链操作,通过 thread.op IPC 鉴权
@@ -11,6 +11,7 @@
11
11
  * 整理会话(sessionType=memory)另有 create / update / archive 三个写命令,
12
12
  * 实现见 core/memory/memory-writer.ts,在这里只做 CLI 分发与权限委托。
13
13
  */
14
+ import fs from 'node:fs';
14
15
  import { resolvePaths } from '../paths.js';
15
16
  import { ipcQuery } from '../ipc.js';
16
17
  import { isHelpFlag, wantsHelp, getArgValue } from './help.js';
@@ -32,8 +33,8 @@ ec memory note --slug <slug> [--tags '<tag1>,<tag2>'] '<content>'
32
33
 
33
34
  选项:
34
35
  --slug <slug> 文件名标识,字母/数字/连字符,长度 1-64(必填)
35
- --tags <...> 标签列表,写法固定为单引号包住、逗号分隔;取值见 \$KITS_DOCS/evolcore/memory-tags.md
36
- --speakers <...> 群聊专用:本条 note 涉及的发言人,写法固定为单引号包住、逗号分隔,
36
+ --tags <...> 标签列表,单引号包住、逗号分隔;取值见 \$KITS_DOCS/evolcore/memory-tags.md
37
+ --speakers <...> 群聊专用:本条 note 涉及的发言人,单引号包住、逗号分隔,
37
38
  如 --speakers '<aid1>,<aid2>';
38
39
  整理会话据此把发言人信息分流写入各自的 relation 记忆
39
40
 
@@ -119,7 +120,13 @@ function assertFormatValues(args) {
119
120
  });
120
121
  }
121
122
  /**
122
- * 解析 `--key value` 与位置参数。未识别的 `--xxx` 一律报错,避免静默忽略。
123
+ * 解析 `--key value` 与位置参数。
124
+ *
125
+ * flag 判定采用**精确匹配已知 flag 集合**,而非 `startsWith('--')`——正文是自由
126
+ * 文本,可能以 `--` 开头(如「--as-session 通道测试」),把它当未知参数拒绝会让
127
+ * 正文受命令行位置影响。不在已知集合里的 token 一律按正文处理。
128
+ *
129
+ * 另支持 POSIX `--` end-of-options:其后所有 token 无条件按正文。
123
130
  */
124
131
  function parseArgs(args, known) {
125
132
  assertFormatValues(args);
@@ -127,10 +134,12 @@ function parseArgs(args, known) {
127
134
  const positional = [];
128
135
  for (let i = 0; i < args.length; i++) {
129
136
  const a = args[i];
130
- if (a.startsWith('--')) {
131
- if (!COMMON_FLAGS.includes(a) && !known.includes(a))
132
- throw new Error(`未知参数: ${a}`);
133
- if (i + 1 >= args.length || args[i + 1].startsWith('--')) {
137
+ if (a === '--') {
138
+ positional.push(...args.slice(i + 1));
139
+ break;
140
+ }
141
+ if (COMMON_FLAGS.includes(a) || known.includes(a)) {
142
+ if (i + 1 >= args.length || args[i + 1] === '--' || isKnownFlagToken(args[i + 1], known)) {
134
143
  // --touch 这类布尔开关不带值
135
144
  flags.set(a, '');
136
145
  continue;
@@ -143,6 +152,9 @@ function parseArgs(args, known) {
143
152
  }
144
153
  return { flags, positional };
145
154
  }
155
+ function isKnownFlagToken(token, known) {
156
+ return COMMON_FLAGS.includes(token) || known.includes(token);
157
+ }
146
158
  function parseTags(raw) {
147
159
  if (raw === undefined)
148
160
  return undefined;
@@ -262,22 +274,41 @@ function parseCliArgs(args, known, json) {
262
274
  * 否则读 EVOLCORE_SESSION_ID。
263
275
  */
264
276
  function requireManagedSession(cmd, flags, json) {
265
- const debugSessionId = flags.get('--debug');
266
- if (flags.has('--debug') && !debugSessionId) {
277
+ const explicitDebugSessionId = flags.get('--debug');
278
+ if (flags.has('--debug') && !explicitDebugSessionId) {
267
279
  fail(json, 'INVALID_ARGUMENT', '--debug 需要提供 sessionId(如 --debug meta_20260910_1789013149542)');
268
280
  }
269
- const sessionId = debugSessionId || process.env.EVOLCORE_SESSION_ID;
281
+ // `ec … --as-session <id>` 由 debug-mode.ts 在进程启动时注入会话环境,
282
+ // 与 `--debug <id>` 是同一种调试身份。判据:注入了 delegation token、
283
+ // 但来自从属会话的 daemon runtime epoch,这是 debug-issue-token 的固定形态
284
+ // (真正的托管任务两者都有)。两者都必须走 daemon 的 debug 分支:
285
+ // 该分支只认本地 control token,不接受 task delegation。
286
+ const asSessionDebug = !explicitDebugSessionId
287
+ && !!process.env.EVOLCORE_SESSION_ID
288
+ && !!readDelegationCredential()
289
+ && !process.env.EVOLCORE_DAEMON_RUNTIME_EPOCH;
290
+ const debugMode = !!explicitDebugSessionId || asSessionDebug;
291
+ const sessionId = explicitDebugSessionId || process.env.EVOLCORE_SESSION_ID;
270
292
  if (!sessionId) {
271
- const message = `EVOLCORE_SESSION_ID 未设置(${cmd} 仅在 evolcore 托管环境中可用;调试可加 --debug <sessionId>)`;
293
+ const message = `EVOLCORE_SESSION_ID 未设置(${cmd} 仅在 evolcore 托管环境中可用;调试可加 --debug <sessionId> 或 --as-session <sessionId>)`;
272
294
  fail(json, 'NO_SESSION', message, { text: `错误: ${message}` });
273
295
  }
274
- if (debugSessionId)
275
- console.error(`[debug] 模拟会话 ${debugSessionId}`);
296
+ if (explicitDebugSessionId)
297
+ console.error(`[debug] 模拟会话 ${explicitDebugSessionId}`);
298
+ else if (asSessionDebug)
299
+ console.error(`[debug] --as-session 会话 ${sessionId}`);
300
+ let controlToken;
301
+ if (debugMode) {
302
+ try {
303
+ controlToken = fs.readFileSync(resolvePaths().controlToken, 'utf8').trim() || undefined;
304
+ }
305
+ catch { /* 交给 daemon 报错 */ }
306
+ }
276
307
  return {
277
308
  sessionId,
278
309
  socketPath: resolvePaths().socket,
279
- delegationToken: readDelegationCredential(),
280
- ...(debugSessionId ? { debug: true } : {}),
310
+ // debug 身份必须用 control token;带 delegationToken 会被 daemon 直接拒绝
311
+ ...(debugMode ? { controlToken, debug: true } : { delegationToken: readDelegationCredential() }),
281
312
  };
282
313
  }
283
314
  // ── note ───────────────────────────────────────────────────────────────────
@@ -128,9 +128,13 @@ function printItem(item) {
128
128
  const running = obj.running ? ' [运行中]' : '';
129
129
  // 内部话题(daemon 会话记录)不是真实联系人,标注出来避免误认
130
130
  const internal = obj.internal ? ' [内部会话记录·无消息]' : '';
131
- console.log(`${obj.peer}/${obj.threadId}${running}${internal}`);
132
- // 名称只放名称本身:状态、标签各自单独一行,避免污染标题(标题会被 summary set --name 引用)
133
- console.log(` 名称: ${obj.name}`);
131
+ // 主会话的 threadId 是空串;显示成 /main 比留一个空尾巴好读。
132
+ console.log(`${obj.peer}/${obj.threadId || 'main'}${running}${internal}`);
133
+ // 名称只放名称本身:状态、标签各自单独一行,避免污染标题(标题会被 summary set --name 引用)。
134
+ // 主会话(threadId 为空)加 `[主会话]` 前缀,与普通话题区分开;前缀只在展示层加,
135
+ // 不进索引与 session 的 name 字段,避免被 summary set --name 当成真实标题。
136
+ const namePrefix = obj.threadId ? '' : '[主会话] ';
137
+ console.log(` 名称: ${namePrefix}${obj.name}`);
134
138
  if (obj.topicStatus)
135
139
  console.log(` 状态: ${obj.topicStatus}`);
136
140
  if (obj.label)
@@ -230,7 +234,11 @@ export async function runThreadCommand(args) {
230
234
  const slash = ref.lastIndexOf('/');
231
235
  if (slash < 0)
232
236
  return { peer: ref };
233
- return { peer: ref.slice(0, slash), threadId: ref.slice(slash + 1) };
237
+ const raw = ref.slice(slash + 1);
238
+ // 主会话的 threadId 是空串;`<peer>/main` 是同义写法,`list` 也这样显示。
239
+ if (raw === 'main')
240
+ return { peer: ref.slice(0, slash), threadId: '' };
241
+ return { peer: ref.slice(0, slash), threadId: raw };
234
242
  }
235
243
  switch (sub) {
236
244
  case 'list': {
@@ -208,7 +208,10 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
208
208
  else {
209
209
  if (!route)
210
210
  return { ok: false, code: 'UNKNOWN_FIELD', reason: `Unknown config field: ${field}` };
211
- value = coerceConfigValue(rawValue, route);
211
+ const coerced = coerceConfigValue(rawValue, route, field);
212
+ if (!coerced.ok)
213
+ return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: coerced.reason };
214
+ value = coerced.value;
212
215
  }
213
216
  }
214
217
  return {
@@ -506,18 +509,32 @@ function buildGlobalCanonicalArgv(command) {
506
509
  argv.push('--format', command.format);
507
510
  return argv;
508
511
  }
509
- function coerceConfigValue(raw, route) {
512
+ function coerceConfigValue(raw, route, field) {
510
513
  if (route.merge === 'list')
511
- return [raw];
514
+ return { ok: true, value: [raw] };
512
515
  if (route.enum)
513
- return raw;
516
+ return { ok: true, value: raw };
517
+ if (route.merge === 'dict' && field === route.field) {
518
+ const trimmed = raw.trim();
519
+ let parsed;
520
+ try {
521
+ parsed = JSON.parse(trimmed);
522
+ }
523
+ catch {
524
+ return { ok: false, reason: `Config field ${route.field} requires a JSON object value` };
525
+ }
526
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
527
+ return { ok: false, reason: `Config field ${route.field} requires a JSON object value` };
528
+ }
529
+ return { ok: true, value: parsed };
530
+ }
514
531
  if (raw === 'true')
515
- return true;
532
+ return { ok: true, value: true };
516
533
  if (raw === 'false')
517
- return false;
534
+ return { ok: true, value: false };
518
535
  if (/^-?\d+(\.\d+)?$/.test(raw))
519
- return Number(raw);
520
- return raw;
536
+ return { ok: true, value: Number(raw) };
537
+ return { ok: true, value: raw };
521
538
  }
522
539
  function parseOwnersValue(raw) {
523
540
  let values;
@@ -331,14 +331,21 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
331
331
  };
332
332
  }
333
333
  let matchResult = matchCommandPermission(operation, opMeta.category, roleDef.commandPermissions || {}, opMeta.dangerous);
334
+ // `ec.thread.*` 是 agent-tool 专用的自数据操作:索引、会话、摘要都在
335
+ // `agents/<self>/data/threads/` 下,目标对端只是数据条目,不是访问边界。
336
+ // 因此只继承 `session.topic.list` 的"读"授权,不继承它的 currentRelationOnly /
337
+ // ownPeerOnly —— 那两条针对的是"当前关系下的会话视图"(menu/slash 场景),
338
+ // 套到话题链上会让 member 无法发现任何第三方话题。
334
339
  if (!matchResult && operation.startsWith('ec.thread.') && opMeta.category === 'read' && intent.scope === 'relation') {
335
340
  const permission = roleDef.commandPermissions?.['session.topic.list'];
336
- if (permission?.allow)
341
+ if (permission?.allow) {
342
+ const { currentRelationOnly: _relOnly, ownPeerOnly: _ownPeer, ...restConstraints } = permission.constraints ?? {};
337
343
  matchResult = {
338
- permission: { ...permission, scopes: ['relation'], constraints: { ...permission.constraints, currentRelationOnly: true, targetCurrentAgentOnly: true } },
344
+ permission: { ...permission, scopes: ['relation'], constraints: { ...restConstraints, targetCurrentAgentOnly: true } },
339
345
  matchedRule: 'session.topic.list',
340
346
  rank: 1000,
341
347
  };
348
+ }
342
349
  }
343
350
  // Agent self-service contact add deliberately reuses the same low-trust
344
351
  // relation boundary as ec.msg.send. Older per-Agent role files may still
@@ -768,6 +775,10 @@ function checkCurrentRelation(ctx, resolvedConfigCommand) {
768
775
  if (!ctx.selfAid || !requestedSelf || requestedSelf !== ctx.selfAid) {
769
776
  return { ok: false, reason: 'Only the current agent relation can be targeted' };
770
777
  }
778
+ // 话题作用域的历史读取(`ec msg history --thread <id>`)目标是话题本身,
779
+ // 不是当前关系的对端;消息日志仍落在调用方自己的会话目录里,因此不比对对端。
780
+ if (ctx.intent.operation === 'ec.msg.history' && stringArg(ctx.intent.args.threadId))
781
+ return { ok: true };
771
782
  const requestedPeer = resolvedConfigCommand?.peerKey || stringArg(ctx.intent.args.peerKey) || stringArg(ctx.intent.args.peer);
772
783
  if (!ctx.peerKey || !requestedPeer) {
773
784
  return { ok: false, reason: 'Current relation context is required' };
@@ -917,6 +928,14 @@ function checkOwnPeer(ctx) {
917
928
  const isMsgSend = ctx.intent.operation === 'ec.msg.send' || ctx.intent.operation === 'ec.msg.file';
918
929
  if (isMsgSend)
919
930
  return { ok: true };
931
+ // Same reasoning for `ec.thread.*`: args.peer is the *indexed peer whose local
932
+ // thread data is being read or written*. All of it lives under
933
+ // `agents/<self>/data/threads/`, so targeting a peer other than the current
934
+ // relation's counterpart does not cross a data boundary — it is the whole
935
+ // point of cross-topic routing. Ownership of the data stays with the caller,
936
+ // which `targetCurrentAgentOnly` already covers.
937
+ if (ctx.intent.operation.startsWith('ec.thread.'))
938
+ return { ok: true };
920
939
  if (ctx.actorId) {
921
940
  if (requestedPeerId && requestedPeerId !== ctx.actorId) {
922
941
  return { ok: false, reason: ownPeerOnlyDenialReason(ctx, requestedPeerId) };
@@ -390,7 +390,7 @@ export function wrapChannelSend(inst, dependencies) {
390
390
  from: owningAgent?.aid ?? envelope.agentName ?? session.selfAID ?? 'self',
391
391
  to: String(target || envelope.channelId),
392
392
  sessionId: session.id,
393
- threadId: envelope.replyContext?.threadId || session.threadId || null,
393
+ threadId: envelope.replyContext?.threadId || session.threadId || '',
394
394
  chatType: isGroup ? 'group' : 'private',
395
395
  groupId: isGroup ? String(session.metadata?.groupId || session.channelId) : null,
396
396
  msgId: `${envelope.taskId || 'out'}_${Date.now()}`,