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
@@ -3,12 +3,29 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { agentSessionsDir, agentDir, agentThreadIndexPath, agentThreadSummariesDir, agentThreadSummaryPath } from '../../paths.js';
5
5
  import { atomicWriteJson, atomicReadJson } from '../../utils/atomic-write.js';
6
- import { appendJsonl, appendJsonlDurable, readAllJsonlLines, readLastJsonlLine, readLastJsonlLineStrict, chatDirPath, tryParseChannelSessionKey } from '../session/session-fs-store.js';
7
- import { sanitizeSessionTitle } from '../session/session-title.js';
6
+ import { appendJsonl, appendJsonlDurable, readAllJsonlLines, readLastJsonlLine, readLastJsonlLineStrict, chatDirPath, scanChatDirs, tryParseChannelSessionKey } from '../session/session-fs-store.js';
7
+ import { sanitizeSessionTitle, SESSION_TITLE_MAX_LEN, SESSION_TITLE_STORED_MAX_LEN, SESSION_TITLE_ELLIPSIS } from '../session/session-title.js';
8
8
  import { ThreadContextStorage } from './thread-context-storage.js';
9
9
  const THREAD_ID_RE = /^thread-[0-9]+-[0-9a-f]{4}-[0-9a-f]{4}$/;
10
10
  const RESERVED_THREAD_IDS = new Set(['current', 'main', 'all']);
11
- const THREAD_NAME_MAX_LEN = 80;
11
+ /**
12
+ * 话题名长度校验:自动生成与手动改名共用 `session-title.ts` 的两个常量。
13
+ *
14
+ * - `SESSION_TITLE_MAX_LEN`(80):正文上限,`sanitizeSessionTitle` 按它截断并补省略号;
15
+ * - `SESSION_TITLE_STORED_MAX_LEN`(83):80 字符正文 + `...`,落盘上限。
16
+ *
17
+ * 校验按落盘上限而不是正文上限:系统自动生成的话题名最长就是 83 字符,
18
+ * 按 80 拒绝会让这些名字「改不回去」。超过 83 的直接拒绝并报出当前长度,
19
+ * 不静默截断——让调用方以为完整名称生效了比报错更糟。
20
+ */
21
+ function assertThreadNameLength(rawName, sanitized) {
22
+ const raw = rawName.trim();
23
+ if (raw.length <= SESSION_TITLE_STORED_MAX_LEN)
24
+ return;
25
+ throw new ThreadServiceError(`话题名称超过 ${SESSION_TITLE_STORED_MAX_LEN} 字符上限(当前 ${raw.length} 字符,含标点;`
26
+ + `其中前 ${SESSION_TITLE_MAX_LEN} 字符为正文,末尾可带 ${SESSION_TITLE_ELLIPSIS} 省略号)。`
27
+ + `请缩写后重试`, 'NAME_TOO_LONG');
28
+ }
12
29
  const LIST_DEFAULT_LIMIT = 10;
13
30
  const LIST_MAX_LIMIT = 200;
14
31
  export class ThreadServiceError extends Error {
@@ -25,12 +42,41 @@ function newThreadId() {
25
42
  const rand = crypto.randomBytes(2).toString('hex');
26
43
  return `thread-${ms}-${seq}-${rand}`;
27
44
  }
45
+ /**
46
+ * 读取话题索引,兼容 4d938a6f(话题数据目录收敛)之前的旧路径。
47
+ *
48
+ * 旧文件位于 `data/thread-index.json`,新路径是 `data/threads/index.json`。
49
+ * 那次改动只搬了路径、没写迁移,导致老 agent 的索引在新路径上不存在,
50
+ * 依赖索引的命令(peers / list / rename ...)看着像"没有话题"。
51
+ *
52
+ * 新索引缺失或为空时回退读旧文件,转换后写回新路径。写回是幂等的:
53
+ * 成功一次之后新索引存在,后续不会再走旧文件;旧文件保持原样,不删除。
54
+ */
28
55
  function readIndex(indexPath) {
29
56
  const raw = atomicReadJson(indexPath);
30
- if (!raw || !Array.isArray(raw.threads)) {
31
- return { version: 1, threads: [] };
57
+ if (raw && Array.isArray(raw.threads) && raw.threads.length > 0) {
58
+ return raw;
59
+ }
60
+ const legacyPath = path.join(path.dirname(path.dirname(indexPath)), 'thread-index.json');
61
+ if (legacyPath !== indexPath) {
62
+ const legacy = atomicReadJson(legacyPath);
63
+ if (legacy && Array.isArray(legacy.threads) && legacy.threads.length > 0) {
64
+ const migrated = {
65
+ version: 1,
66
+ threads: legacy.threads,
67
+ };
68
+ // 顺手落盘;失败不影响本次读取(例如目录只读),下次仍能走回退。
69
+ try {
70
+ fs.mkdirSync(path.dirname(indexPath), { recursive: true });
71
+ atomicWriteJson(indexPath, migrated);
72
+ }
73
+ catch { /* 写失败不阻断读取 */ }
74
+ return migrated;
75
+ }
32
76
  }
33
- return raw;
77
+ if (raw && Array.isArray(raw.threads))
78
+ return raw;
79
+ return { version: 1, threads: [] };
34
80
  }
35
81
  /**
36
82
  * 把用户传入的 peer 归一成索引里实际存储的形态(对端在该渠道下的原生 ID)。
@@ -117,9 +163,7 @@ export class ThreadService {
117
163
  const sanitized = sanitizeSessionTitle(name);
118
164
  if (!sanitized)
119
165
  throw new ThreadServiceError('话题名称不能为空', 'INVALID_NAME');
120
- if (sanitized.length > THREAD_NAME_MAX_LEN) {
121
- throw new ThreadServiceError(`话题名称超过 ${THREAD_NAME_MAX_LEN} 字符`, 'NAME_TOO_LONG');
122
- }
166
+ assertThreadNameLength(name, sanitized);
123
167
  // Check for duplicate name on same peer
124
168
  const index = this.readThreadIndex(selfAid);
125
169
  const dup = index.threads.find(t => t.peer === peer && t.name === sanitized);
@@ -176,8 +220,20 @@ export class ThreadService {
176
220
  const sanitized = sanitizeSessionTitle(name);
177
221
  if (!sanitized)
178
222
  throw new ThreadServiceError('话题名称不能为空', 'INVALID_NAME');
179
- if (sanitized.length > THREAD_NAME_MAX_LEN) {
180
- throw new ThreadServiceError(`话题名称超过 ${THREAD_NAME_MAX_LEN} 字符`, 'NAME_TOO_LONG');
223
+ assertThreadNameLength(name, sanitized);
224
+ // 主会话(threadId 为空)从不进话题索引,它由会话文件产生。
225
+ // 改名直接落到 active session 上,不走索引与快照。
226
+ // 群里主会话名只有 admin/owner 能改,且在授权层已经拦过;这里照常落本地。
227
+ if (!threadId) {
228
+ const main = await this.showMainSession(selfAid, peer);
229
+ if (!main)
230
+ throw new ThreadServiceError(`与 ${peer} 无活跃主会话`, 'TOPIC_NOT_FOUND');
231
+ if ((main.name ?? '') === sanitized)
232
+ return { changed: false, name: sanitized };
233
+ const renamed = await this.sessionManager.renameSession(main.sessionId, sanitized);
234
+ if (!renamed)
235
+ throw new ThreadServiceError(`主会话改名失败: ${main.sessionId}`, 'RENAME_FAILED');
236
+ return { changed: true, name: sanitized, nameUpdatedAt: Date.now() };
181
237
  }
182
238
  const index = this.readThreadIndex(selfAid);
183
239
  const entry = index.threads.find(t => t.peer === peer && t.threadId === threadId);
@@ -185,8 +241,10 @@ export class ThreadService {
185
241
  throw new ThreadServiceError(`话题不存在: ${threadId}`, 'TOPIC_NOT_FOUND');
186
242
  if (entry.name === sanitized)
187
243
  return { changed: false, name: sanitized };
244
+ const nameUpdatedAt = Date.now();
188
245
  entry.name = sanitized;
189
- entry.lastActiveAt = Date.now();
246
+ entry.nameUpdatedAt = nameUpdatedAt;
247
+ entry.lastActiveAt = nameUpdatedAt;
190
248
  this.writeThreadIndex(selfAid, index);
191
249
  this.appendThreadSnapshot(selfAid, entry);
192
250
  // Sync to session
@@ -194,7 +252,50 @@ export class ThreadService {
194
252
  if (session) {
195
253
  await this.sessionManager.renameSession(session.id, sanitized);
196
254
  }
197
- return { changed: true, name: sanitized };
255
+ return { changed: true, name: sanitized, nameUpdatedAt };
256
+ }
257
+ /**
258
+ * 应用对端发来的改名通知(`topic-renamed`)。
259
+ *
260
+ * 判据是最新更新时间先到先赢:通知里的 `nameUpdatedAt` 只有**严格大于**本地
261
+ * 记录时才生效。对端尚未建档时按通知内容建条目(与会话自动建档一致),
262
+ * 这样后续跨话题交互能看到对端已命名的话题。
263
+ */
264
+ async applyRemoteRename(input) {
265
+ const sanitized = sanitizeSessionTitle(input.name);
266
+ if (!sanitized || !input.threadId)
267
+ return { applied: false, reason: 'invalid' };
268
+ const peer = normalizeThreadPeer(input.peer) ?? input.peer;
269
+ const index = this.readThreadIndex(input.selfAid);
270
+ const entry = index.threads.find(t => t.peer === peer && t.threadId === input.threadId);
271
+ const current = entry?.nameUpdatedAt ?? 0;
272
+ if (input.nameUpdatedAt <= current)
273
+ return { applied: false, reason: 'stale' };
274
+ if (!entry) {
275
+ index.threads.push({
276
+ threadId: input.threadId,
277
+ sessionId: '',
278
+ peer,
279
+ channelType: 'aun',
280
+ peerKind: input.peerKind ?? 'private',
281
+ name: sanitized,
282
+ nameUpdatedAt: input.nameUpdatedAt,
283
+ label: null,
284
+ topicStatus: 'pending',
285
+ createdAt: input.nameUpdatedAt,
286
+ lastActiveAt: input.nameUpdatedAt,
287
+ });
288
+ this.writeThreadIndex(input.selfAid, index);
289
+ return { applied: true };
290
+ }
291
+ entry.name = sanitized;
292
+ entry.nameUpdatedAt = input.nameUpdatedAt;
293
+ this.writeThreadIndex(input.selfAid, index);
294
+ this.appendThreadSnapshot(input.selfAid, entry);
295
+ const session = await this.sessionManager.getThreadSession(entryChannelType(entry), entry.peer, input.threadId, input.selfAid);
296
+ if (session)
297
+ await this.sessionManager.renameSession(session.id, sanitized);
298
+ return { applied: true };
198
299
  }
199
300
  // ── P1a.1.6: 话题标签 ──
200
301
  async labelThread(input) {
@@ -237,6 +338,10 @@ export class ThreadService {
237
338
  * 活跃时间取自该对端消息日志的最后一行,读尾行(不整文件加载);
238
339
  * 无消息日志或读取失败时退回索引里的管理动作时间。
239
340
  * 内部话题(daemon 命名空间的会话记录)不是真实联系人,不在此列。
341
+ *
342
+ * 候选集 = 索引条目 ∪ 磁盘上真实存在的 chat 目录:
343
+ * 主会话从不进索引(它由会话文件而非 createThread 产生),只靠索引会漏掉
344
+ * "只聊过主会话"的对端。磁盘扫描兜住这一类,threadCount 至少记 1。
240
345
  */
241
346
  listPeers(selfAid) {
242
347
  const index = this.readThreadIndex(selfAid);
@@ -252,6 +357,7 @@ export class ThreadService {
252
357
  threadCount: 1,
253
358
  lastActiveAt: entry.lastActiveAt,
254
359
  channelType: entryChannelType(entry),
360
+ indexed: true,
255
361
  });
256
362
  }
257
363
  else {
@@ -260,6 +366,45 @@ export class ThreadService {
260
366
  existing.lastActiveAt = entry.lastActiveAt;
261
367
  }
262
368
  }
369
+ // 索引之外的兜底:磁盘上真实存在、且有消息或会话存档的 chat 目录。
370
+ // 只加索引里没有的对端——索引已经有了的,子话题数已算过,不再重复计主会话。
371
+ // 从目录名解析 peer 与 channelType,避免信赖可能过期的索引字段。
372
+ try {
373
+ const agentsRoot = path.dirname(agentDir(selfAid));
374
+ for (const chat of scanChatDirs(agentsRoot)) {
375
+ if (chat.selfAID !== selfAid)
376
+ continue;
377
+ const peer = chat.channelId;
378
+ if (!peer || isInternalThreadPeer(peer))
379
+ continue;
380
+ const existing = map.get(peer);
381
+ if (existing) {
382
+ // 索引里有的对端:只更正渠道类型(索引历史条目可能缺字段)
383
+ if (!existing.channelType)
384
+ existing.channelType = chat.channelType;
385
+ continue;
386
+ }
387
+ // 无消息、无会话存档的空目录不算对端
388
+ const hasPayload = fs.existsSync(path.join(chat.dirPath, 'messages.jsonl'))
389
+ || fs.existsSync(path.join(chat.dirPath, 'active.json'));
390
+ if (!hasPayload)
391
+ continue;
392
+ // 用当前主会话判定群/私聊;没有会话文件时按目录名兜底为私聊
393
+ const active = this.sessionManager
394
+ ? this.sessionManager.getActiveSessionSync(chat.channelType, peer, chat.channelType, selfAid)
395
+ : undefined;
396
+ map.set(peer, {
397
+ peerKind: active?.chatType === 'group' ? 'group' : 'private',
398
+ threadCount: 1,
399
+ lastActiveAt: active?.updatedAt ?? 0,
400
+ channelType: chat.channelType,
401
+ indexed: false,
402
+ });
403
+ }
404
+ }
405
+ catch {
406
+ // 扫描失败不影响索引结果
407
+ }
263
408
  return Array.from(map.entries())
264
409
  .map(([peer, v]) => {
265
410
  // 真实活跃时间 = 消息日志末行的时间戳。
@@ -296,6 +441,55 @@ export class ThreadService {
296
441
  return false;
297
442
  return true;
298
443
  });
444
+ // 主会话是 threadId 为 `''` 的话题:它由会话文件而非 createThread 产生,
445
+ // 所以从不进话题索引。只列子话题会让「和某对端聊过什么」在列表里看不到主线,
446
+ // 而 `history <peer>` 不传 threadId 读的恰恰是主会话消息。指定对端时补一条
447
+ // 合成记录,让 list 与 history 的口径一致。
448
+ if (peer) {
449
+ const hasMain = threads.some(t => !t.threadId);
450
+ if (!hasMain) {
451
+ const chatDir = this.chatDirFor(selfAid, peer);
452
+ const logPath = path.join(chatDir, 'messages.jsonl');
453
+ let lastTs = 0;
454
+ if (fs.existsSync(logPath)) {
455
+ try {
456
+ for (const m of readAllJsonlLines(logPath)) {
457
+ if (!m.threadId && typeof m.ts === 'number' && m.ts > lastTs)
458
+ lastTs = m.ts;
459
+ }
460
+ }
461
+ catch { /* skip corrupt file */ }
462
+ }
463
+ // 群标识与私聊 AID 语法相同,只能靠会话记录里的 chatType 判定;
464
+ // 没有会话文件时退回私聊(`ec thread list <peer>` 的常见用法)。
465
+ const active = this.sessionManager.getActiveSessionSync('aun', peer, 'aun', selfAid);
466
+ const peerKind = active?.chatType === 'group' ? 'group' : 'private';
467
+ const synthetic = {
468
+ threadId: '',
469
+ sessionId: active?.id ?? '',
470
+ peer,
471
+ channelType: 'aun',
472
+ peerKind,
473
+ // 主会话有自己的名字(创建时写 `默认会话`,可经 ec thread rename 修改)。
474
+ // 读真实值而不是写死 `主会话`:否则列表里的名字和会话存档对不上,
475
+ // 用户也没法从列表确认改名是否生效。`[主会话]` 前缀由 CLI 渲染。
476
+ name: active?.name || '主会话',
477
+ label: null,
478
+ topicStatus: 'pending',
479
+ createdAt: lastTs,
480
+ lastActiveAt: lastTs,
481
+ mainSession: true,
482
+ internal: false,
483
+ msgTotal: 0,
484
+ msg24h: 0,
485
+ lastMsgSnippet: '',
486
+ };
487
+ // 该对端没有任何主会话往来时不造空行;状态/标签过滤对主会话也无意义,
488
+ // 只在未过滤时加入。
489
+ if (lastTs > 0 && !status && !label && !unlabeled)
490
+ threads = [synthetic, ...threads];
491
+ }
492
+ }
299
493
  // 先读消息日志并派生真实活跃时间,再排序、再分页——
300
494
  // 否则 recent 排序用的是索引里的管理动作时间,与实际展示的活跃时间不一致。
301
495
  const ts24h = Date.now() - 86400000;
@@ -315,7 +509,7 @@ export class ThreadService {
315
509
  ? (payload.text ?? payload.content ?? '')
316
510
  : (m.content ?? '');
317
511
  msgs.push({
318
- threadId: m.threadId ?? null,
512
+ threadId: m.threadId ?? '',
319
513
  ts: m.ts ?? 0,
320
514
  snippet: String(text).replace(/\n/g, ' ').slice(0, 50),
321
515
  });
@@ -332,10 +526,12 @@ export class ThreadService {
332
526
  const lastMsgOf = new Map();
333
527
  for (const t of threads) {
334
528
  const msgs = (peerMessages.get(`${entryChannelType(t)}\u0000${t.peer}`) ?? [])
335
- .filter(m => m.threadId === t.threadId);
529
+ .filter(m => (m.threadId ?? '') === (t.threadId ?? ''));
336
530
  const lastMsg = msgs.reduce((acc, m) => (!acc || m.ts > acc.ts) ? m : acc, null);
531
+ // 合成的主会话条目用 `main` 作 threadId,日志里主会话记的是 null。
532
+ const key = t.threadId ?? '';
337
533
  if (lastMsg)
338
- lastMsgOf.set(t.threadId, lastMsg);
534
+ lastMsgOf.set(key, lastMsg);
339
535
  }
340
536
  if (sort === 'created') {
341
537
  threads.sort((a, b) => b.createdAt - a.createdAt);
@@ -349,10 +545,10 @@ export class ThreadService {
349
545
  threads = threads.slice(offset, offset + limit);
350
546
  threads = threads.map(t => {
351
547
  const msgs = (peerMessages.get(`${entryChannelType(t)}\u0000${t.peer}`) ?? [])
352
- .filter(m => m.threadId === t.threadId);
548
+ .filter(m => (m.threadId ?? '') === (t.threadId ?? ''));
353
549
  const msgTotal = msgs.length;
354
550
  const msg24h = msgs.filter(m => m.ts >= ts24h).length;
355
- const lastMsg = lastMsgOf.get(t.threadId);
551
+ const lastMsg = lastMsgOf.get(t.threadId ?? '');
356
552
  const lastActiveAt = lastMsg?.ts || t.lastActiveAt;
357
553
  // 读该话题的 summary(当前最新版本)
358
554
  const summaryEntry = this.showSummary(selfAid, t.sessionId);
@@ -409,11 +605,11 @@ export class ThreadService {
409
605
  return { peer, messages: [] };
410
606
  const messages = readAllJsonlLines(logPath)
411
607
  .filter((m) => {
412
- // threadId 为 undefined/null 时读主会话消息(threadId === null 的行),
413
- // 否则按 threadId 精确匹配子话题消息。
608
+ // 不传 threadId 时读主会话消息:主会话的行 threadId 为 ''(历史残留 null
609
+ // 也按主会话处理),否则按 threadId 精确匹配子话题消息。
414
610
  if (threadId)
415
611
  return m.threadId === threadId;
416
- return m.threadId == null;
612
+ return !m.threadId;
417
613
  })
418
614
  .map((m) => {
419
615
  // 消息体优先取 payloadSummary.text(加密消息的明文摘要),
@@ -695,16 +891,16 @@ export class ThreadService {
695
891
  * 作为"这条摘要覆盖到哪"的游标。下次总结时用它作 `history --after` 起点,
696
892
  * 只读增量。话题为空(或主会话无消息)时不记录该字段。
697
893
  */
698
- setSummary(selfAid, sessionId, content, ifRev) {
894
+ async setSummary(selfAid, sessionId, content, ifRev) {
699
895
  const dir = agentThreadSummariesDir(selfAid);
700
896
  fs.mkdirSync(dir, { recursive: true });
701
897
  const filePath = agentThreadSummaryPath(selfAid, sessionId);
702
- const existing = this._readSummaryLines(filePath);
898
+ const coveredThroughTs = await this.resolveCoveredThroughTs(selfAid, sessionId);
899
+ const existing = this._readAllSummaryLines(filePath);
703
900
  const currentRev = existing.length;
704
901
  if (ifRev !== undefined && ifRev !== currentRev) {
705
902
  throw new ThreadServiceError(`summary CAS 冲突:期望版本 ${ifRev},当前版本 ${currentRev}`, 'SUMMARY_CAS_CONFLICT');
706
903
  }
707
- const coveredThroughTs = this.resolveCoveredThroughTs(selfAid, sessionId);
708
904
  const entry = {
709
905
  rev: currentRev + 1,
710
906
  content,
@@ -719,12 +915,25 @@ export class ThreadService {
719
915
  *
720
916
  * 话题内总结时这就是"当前消息",跨话题总结时(先读全增量再写)也是同一语义。
721
917
  * 话题不存在或无消息时返回 undefined——此时摘要没有游标,下次总结只能全量读。
918
+ *
919
+ * 主会话不进话题索引(它由会话文件产生),索引里查不到时按 sessionId 回退到
920
+ * session 本身:peer 取 channelId,threadId 取 session.threadId(主会话为空串)。
921
+ * 不回退的话主会话摘要永远没有游标,增量总结在主会话上不成立。
722
922
  */
723
- resolveCoveredThroughTs(selfAid, sessionId) {
923
+ async resolveCoveredThroughTs(selfAid, sessionId) {
724
924
  const entry = this.readThreadIndex(selfAid).threads.find(t => t.sessionId === sessionId);
725
- if (!entry)
925
+ let peer = entry?.peer;
926
+ let threadId = entry?.threadId;
927
+ if (!entry) {
928
+ const session = await this.sessionManager.getSessionById(sessionId);
929
+ if (!session)
930
+ return undefined;
931
+ peer = session.channelId;
932
+ threadId = session.threadId ?? '';
933
+ }
934
+ if (!peer)
726
935
  return undefined;
727
- const { messages } = this._readThreadMessages(selfAid, entry.peer, entry.threadId);
936
+ const { messages } = this._readThreadMessages(selfAid, peer, threadId);
728
937
  if (messages.length === 0)
729
938
  return undefined;
730
939
  return messages[messages.length - 1].timestamp;
@@ -753,18 +962,23 @@ export class ThreadService {
753
962
  */
754
963
  clearSummary(selfAid, sessionId) {
755
964
  const filePath = agentThreadSummaryPath(selfAid, sessionId);
756
- const existing = this._readSummaryLines(filePath);
965
+ // 计数必须含墓碑:rev 是文件行序号,过滤后会与已有墓碑撞号。
966
+ const existing = this._readAllSummaryLines(filePath);
757
967
  if (existing.length === 0)
758
968
  return; // 已无内容,幂等
759
969
  const dir = agentThreadSummariesDir(selfAid);
760
970
  fs.mkdirSync(dir, { recursive: true });
761
971
  appendJsonl(filePath, { rev: existing.length + 1, content: null, updatedAt: Date.now() });
762
972
  }
763
- _readSummaryLines(filePath) {
973
+ /** 全部记录,含 clear 墓碑(content=null)。rev/CAS 计数一律基于这个。 */
974
+ _readAllSummaryLines(filePath) {
764
975
  if (!fs.existsSync(filePath))
765
976
  return [];
766
- const all = readAllJsonlLines(filePath);
767
- // 过滤掉 clear 墓碑(content=null),只返回实际写入的版本
768
- return all.filter(l => l.content !== null);
977
+ return readAllJsonlLines(filePath);
978
+ }
979
+ /** 只含实际写入的版本,不含 clear 墓碑。仅用于展示历史。 */
980
+ _readSummaryLines(filePath) {
981
+ return this._readAllSummaryLines(filePath)
982
+ .filter(l => l.content !== null);
769
983
  }
770
984
  }
@@ -344,9 +344,9 @@ export async function launchMonitorAndConfirm(args, env, isAliveInRegistry, hasO
344
344
  * in a checkout: `npm install -g` swaps the whole package directory, and the
345
345
  * monitor has ~170 lazy `await import()` sites, so a module it has not loaded
346
346
  * yet would come from the new version while the rest is still the old one.
347
- * Staging `dist/` elsewhere keeps its module graph on one consistent version.
347
+ * Staging `dist/` and `bin/` keeps the package-owned code on one version.
348
348
  *
349
- * Only `dist/` and `package.json` are copied (a few MB). `node_modules` is
349
+ * Only `dist/`, `bin/` and `package.json` are copied (a few MB). `node_modules` is
350
350
  * linked, because copying it is hundreds of MB. The link degrades in steps so
351
351
  * that no platform is left without a path forward:
352
352
  *
@@ -379,6 +379,9 @@ export function prepareMonitorStage(packageRoot, stageParent) {
379
379
  try {
380
380
  fs.mkdirSync(stageDir, { recursive: true });
381
381
  fs.cpSync(distSource, path.join(stageDir, 'dist'), { recursive: true });
382
+ const binSource = path.join(packageRoot, 'bin');
383
+ if (fs.existsSync(binSource))
384
+ fs.cpSync(binSource, path.join(stageDir, 'bin'), { recursive: true });
382
385
  const pkgJson = path.join(packageRoot, 'package.json');
383
386
  if (fs.existsSync(pkgJson))
384
387
  fs.copyFileSync(pkgJson, path.join(stageDir, 'package.json'));
@@ -388,7 +391,7 @@ export function prepareMonitorStage(packageRoot, stageParent) {
388
391
  fs.rmSync(stageDir, { recursive: true, force: true });
389
392
  }
390
393
  catch { }
391
- return { stageDir: null, linkMode: 'none', note: `复制 dist/ 失败:${error.message}` };
394
+ return { stageDir: null, linkMode: 'none', note: `复制 monitor 运行文件失败:${error.message}` };
392
395
  }
393
396
  if (!fs.existsSync(modulesSource)) {
394
397
  // Dependencies hoisted elsewhere; nothing to link, node resolves upward.
@@ -650,7 +650,7 @@ lrwxrwxrwx slides.pdf → /private/q2/slides.pdf r-- public
650
650
  ```bash
651
651
  ec fs ls --pending <AID>: # 查看待审批
652
652
  ec fs approve <AID>: --request-id req_xxx
653
- ec fs reject <AID>: --request-id req_xxx --note "需脱敏"
653
+ ec fs reject <AID>: --request-id req_xxx --note '需脱敏'
654
654
  ```
655
655
 
656
656
  ---
@@ -1118,20 +1118,20 @@ forward 历史全部保留,任何时候都能再前滚。
1118
1118
  ec collab submit alice.aid.pub:/projects/myapp schema.json ./schema.json --base-version 1
1119
1119
 
1120
1120
  # alice 打快照——只动了内容,自动 PATCH
1121
- ec collab snapshot create alice.aid.pub:/projects/myapp -m "修正 schema 字段"
1121
+ ec collab snapshot create alice.aid.pub:/projects/myapp -m '修正 schema 字段'
1122
1122
  # → 2.3.1 (bump=patch, changed=[schema.json])
1123
1123
 
1124
1124
  # alice 新增 deploy.md(doc 集合变化)后打快照——自动 MINOR
1125
1125
  ec collab create alice.aid.pub:/projects/myapp deploy.md ./deploy.md
1126
- ec collab snapshot create alice.aid.pub:/projects/myapp -m "加部署文档"
1126
+ ec collab snapshot create alice.aid.pub:/projects/myapp -m '加部署文档'
1127
1127
  # → 2.4.0 (bump=minor)
1128
1128
 
1129
1129
  # 里程碑,手动标大版本
1130
- ec collab snapshot create alice.aid.pub:/projects/myapp --major -m "v3 设计定稿"
1130
+ ec collab snapshot create alice.aid.pub:/projects/myapp --major -m 'v3 设计定稿'
1131
1131
  # → 3.0.0
1132
1132
 
1133
1133
  # 方向错了,非破坏回滚到 2.3.1
1134
- ec collab snapshot restore alice.aid.pub:/projects/myapp 2.3.1 -m "回退到 schema 修正版"
1134
+ ec collab snapshot restore alice.aid.pub:/projects/myapp 2.3.1 -m '回退到 schema 修正版'
1135
1135
  # → 各 @current 重指回 2.3.1 的 target;deploy.md 的 @current 撤下(版本文件仍在)
1136
1136
  # 追加新快照 3.0.1,记录 "restored from 2.3.1";3.0.0 历史保留
1137
1137
 
@@ -77,7 +77,7 @@ ec fs ln -s alice.agentid.pub:/private/report.md alice.agentid.pub:/public/repor
77
77
  ec fs chmod +r alice.agentid.pub:/public/report.md
78
78
  ec fs setfacl alice.agentid.pub:/private/report.md -m aid:bob.agentid.pub:r
79
79
  ec fs token issue alice.agentid.pub:/public/report.md --expires 2026-12-31
80
- ec fs find alice.agentid.pub:/private/ --name "*.md"
80
+ ec fs find alice.agentid.pub:/private/ --name '*.md'
81
81
  ec fs rm alice.agentid.pub:/private/report.md
82
82
  ec fs df alice.agentid.pub:
83
83
  ```
@@ -46,7 +46,7 @@
46
46
  群名称必填。创建前根据群名称确定一个简短、唯一的 group AID(同样使用 `{name}.{issuer}`,不要四级域名),并给出一句群简介;名称、AID 和简介应能体现协作目标。推荐默认使用私有群和 `invite_only`,先建群再按名单邀请:
47
47
 
48
48
  ```bash
49
- ec group create coordinator.agentid.pub "Project Alpha 协作" \
49
+ ec group create coordinator.agentid.pub 'Project Alpha 协作' \
50
50
  --visibility private \
51
51
  --description "Project Alpha 的设计、实现和复核协作群" \
52
52
  --join-mode invite_only
@@ -33,7 +33,7 @@ ec group ack <from> <group-id> <seq> [--app <name>]
33
33
  ```
34
34
 
35
35
  @ 成员:
36
- - `--mention '<aid1>,<aid2>'` — @ 某个成员。写法固定为单引号包住、逗号分隔,可多次传入,每个 aid 都过格式校验
36
+ - `--mention '<aid1>,<aid2>'` — @ 某个成员。单引号包住、逗号分隔,可多次传入,每个 aid 都过格式校验
37
37
  - `--mention-all` — @ 所有人
38
38
 
39
39
  ```bash
@@ -92,10 +92,10 @@ ec group create <from> '<name>' [--visibility public|private] [--description '<d
92
92
 
93
93
  # 不指定 --group-id 时,默认名称为 g- 加五位随机数字(10000–99999)
94
94
  # 例如返回 g-12345.agentid.cn;issuer 以群服务实际返回为准
95
- ec group create <from> "研讨群" --visibility private
95
+ ec group create <from> '研讨群' --visibility private
96
96
 
97
97
  # 自定义群 AID;g- 若出现,是名称的一部分,会完整保留
98
- ec group create <from> "本体研讨" --group-id ontology.agentid.cn
98
+ ec group create <from> '本体研讨' --group-id ontology.agentid.cn
99
99
 
100
100
  # 列出我加入的群
101
101
  ec group list <from> [--size <N>]
@@ -190,6 +190,8 @@ ec group invite <from> <group-id> <member-aid> [<member-aid>...]
190
190
  ec group kick <from> <group-id> <member-aid>
191
191
 
192
192
  # 列出群成员
193
+ # 参数顺序固定为「先发送方 AID,后群 AID」——群 ID 永远在第 2 个位置。
194
+ # 写反时命令会直接指出,并给出可复制的正确写法。
193
195
  ec group members <from> <group-id> [--page <N>] [--size <N>]
194
196
 
195
197
  # 查看在线成员
@@ -245,8 +245,7 @@ daemon 按整理会话的模式校验 `--scope`:
245
245
  |---|---|---|
246
246
  | 12 | `memory` | 与主会话共用的 `$KITS_FRAGMENTS/memory.md`,按 `sessionType=memory` 渲染整理分支:三个写命令、模式权限、禁止事项 |
247
247
  | 20 | `memory-workflow` | 整理流程:读上下文、查重、取舍、update 与 create --replaces 的选择、收尾 |
248
- | 25 | `persona` | 当前 agent 的人格 |
249
- | 26 | `personal-profile` | 当前 agent 的个人资料与工作背景 |
248
+ | 25 | `persona` | 当前 agent 的人格与个人资料(合并自原 `personal-profile` 段) |
250
249
  | 27 | `peer-profile` | **当前对端画像**(`profile` 标签的记忆) |
251
250
  | 31 | `memory-mode-*` | 五份模式手册,按 `memoryMode` 五选一 |
252
251
  | 36/37/38 | 三份记忆索引 | agent / owner / relation |
@@ -55,7 +55,7 @@ tags: [important, long-term, rule, verified]
55
55
 
56
56
  **打标的地方**:整理会话写正式记忆(`ec memory create --tags 'a,b'`,事后用 `ec memory update --tags 'a,b'` 整体替换),主会话写 note(`ec memory note --tags 'a,b'`)。note 的标签是临时的,整理会话读取后决定去留。
57
57
 
58
- 写法固定为单引号包住、逗号分隔:`--tags 'to-generalize,urgent'`。
58
+ 写法为单引号包住、逗号分隔:`--tags 'to-generalize,urgent'`。
59
59
 
60
60
  **清标的两种方式**:
61
61
 
@@ -21,8 +21,8 @@ ec memory note --slug <slug> [--tags '<tag1>,<tag2>'] [--speakers '<id1>,<id2>']
21
21
  | 参数 | 说明 |
22
22
  |------|------|
23
23
  | `--slug` | **必填**。字母/数字/连字符,1-64 字符,当前关系内唯一,后续 update / drop 靠它定位 |
24
- | `--tags` | 可选。写法固定为 `--tags 'tag1,tag2'`:单引号包住,逗号分隔。取值见 `$KITS_DOCS/evolcore/memory-tags.md` |
25
- | `--speakers` | 可选,**群聊专用**。写法固定为 `--speakers '<aid1>,<aid2>'`:单引号包住,逗号分隔。填本条 note 涉及的发言人 AID 或渠道原生 ID。整理会话会据此把发言人相关信息分流写入各发言人自己的 relation 记忆,而不是只落在群的 relation 下。私聊场景无需填写。 |
24
+ | `--tags` | 可选。`--tags 'tag1,tag2'`:单引号包住,逗号分隔。取值见 `$KITS_DOCS/evolcore/memory-tags.md` |
25
+ | `--speakers` | 可选,**群聊专用**。`--speakers '<aid1>,<aid2>'`:单引号包住,逗号分隔。填本条 note 涉及的发言人 AID 或渠道原生 ID。整理会话会据此把发言人相关信息分流写入各发言人自己的 relation 记忆,而不是只落在群的 relation 下。私聊场景无需填写。 |
26
26
  | 正文 | 必填。写清事实本身即可,不用组织成索引描述 |
27
27
 
28
28
  ```bash
@@ -67,7 +67,7 @@ ec stats --rebuild
67
67
  ## 直接 SQL 查询
68
68
 
69
69
  ```bash
70
- ec stats --sql "SELECT model, COUNT(*) AS cnt FROM usage_events GROUP BY model"
70
+ ec stats --sql 'SELECT model, COUNT(*) AS cnt FROM usage_events GROUP BY model'
71
71
  ```
72
72
 
73
73
  仅允许 SELECT 查询(只读安全)。