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.
- package/CHANGELOG.md +22 -0
- package/bin/install-codex-managed-hooks.mjs +26 -3
- package/dist/agents/claude-runner.js +11 -1
- package/dist/agents/codex/diagnostics.js +42 -11
- package/dist/agents/codex-runner.js +27 -3
- package/dist/agents/ecagent-runner.js +17 -7
- package/dist/aun/aid/agentmd.js +13 -2
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/msg/managed-operation.js +16 -0
- package/dist/aun/msg/p2p.js +18 -2
- package/dist/channels/aun.js +113 -7
- package/dist/cli/aun-commands.js +37 -13
- package/dist/cli/daemon-commands.js +10 -4
- package/dist/cli/fs-command.js +1 -1
- package/dist/cli/getmeta-command.js +211 -0
- package/dist/cli/index.js +6 -0
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/memory-command.js +46 -15
- package/dist/cli/thread-command.js +12 -4
- package/dist/config/resolved-config-op.js +25 -8
- package/dist/core/auth/operation-authorizer.js +21 -2
- package/dist/core/channel-runtime.js +1 -1
- package/dist/core/command/command-handler.js +7 -1
- package/dist/core/command/menu-handler.js +2 -0
- package/dist/core/command/topic-slash.js +4 -7
- package/dist/core/event-catalog.js +14 -0
- package/dist/core/ipc-registration.js +11 -1
- package/dist/core/memory/batch-meta.js +1 -0
- package/dist/core/memory/consolidation-context.js +97 -14
- package/dist/core/memory/consolidation-trigger.js +15 -7
- package/dist/core/memory/consolidation.js +55 -0
- package/dist/core/memory/memory-writer.js +20 -4
- package/dist/core/message/aun-ipc-sender.js +19 -1
- package/dist/core/message/message-bridge.js +3 -3
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/peer-mode.js +17 -0
- package/dist/core/message/response-engine.js +15 -4
- package/dist/core/permission/ec-command-parser.js +76 -40
- package/dist/core/permission/tool-policy.js +32 -2
- package/dist/core/relation/peer-identity.js +87 -12
- package/dist/core/session/session-manager.js +56 -1
- package/dist/core/session/session-title.js +26 -1
- package/dist/core/system-session/memory.js +87 -20
- package/dist/core/thread/command-parser.js +6 -1
- package/dist/core/thread/ipc-operation.js +33 -16
- package/dist/core/thread/thread-operation.js +62 -24
- package/dist/core/thread/thread-service.js +246 -32
- package/dist/utils/cross-platform.js +6 -3
- package/kits/docs/evolcore/fs-architecture.md +5 -5
- package/kits/docs/evolcore/fs.md +1 -1
- package/kits/docs/evolcore/group-collaboration.md +1 -1
- package/kits/docs/evolcore/group.md +5 -3
- package/kits/docs/evolcore/memory-consolidation.md +1 -2
- package/kits/docs/evolcore/memory-tags.md +1 -1
- package/kits/docs/evolcore/memory.md +2 -2
- package/kits/docs/evolcore/stats.md +1 -1
- package/kits/docs/evolcore/thread-chain.md +25 -9
- package/kits/eck_manifest.auxiliary.json +9 -0
- package/kits/eck_manifest.json +20 -0
- package/kits/eck_manifest.memory.json +20 -0
- package/kits/rules/01-overview.md +3 -3
- package/kits/rules/07-memory-and-thread.md +1 -1
- package/kits/skills/ec-group/SKILL.md +6 -3
- package/kits/skills/thread-ops/SKILL.md +18 -18
- package/kits/templates/memory-consolidation/notes.md +1 -1
- package/kits/templates/memory-consolidation/workflow.md +36 -1
- package/kits/templates/system-fragments/bash-description.md +9 -0
- package/kits/templates/system-fragments/memory.md +2 -2
- 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
|
-
|
|
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 (
|
|
31
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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.
|
|
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 ??
|
|
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(
|
|
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
|
|
413
|
-
//
|
|
608
|
+
// 不传 threadId 时读主会话消息:主会话的行 threadId 为 ''(历史残留 null
|
|
609
|
+
// 也按主会话处理),否则按 threadId 精确匹配子话题消息。
|
|
414
610
|
if (threadId)
|
|
415
611
|
return m.threadId === threadId;
|
|
416
|
-
return m.threadId
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
973
|
+
/** 全部记录,含 clear 墓碑(content=null)。rev/CAS 计数一律基于这个。 */
|
|
974
|
+
_readAllSummaryLines(filePath) {
|
|
764
975
|
if (!fs.existsSync(filePath))
|
|
765
976
|
return [];
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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/`
|
|
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: `复制
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/kits/docs/evolcore/fs.md
CHANGED
|
@@ -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
|
|
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
|
|
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>'` — @
|
|
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>
|
|
95
|
+
ec group create <from> '研讨群' --visibility private
|
|
96
96
|
|
|
97
97
|
# 自定义群 AID;g- 若出现,是名称的一部分,会完整保留
|
|
98
|
-
ec group create <from>
|
|
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
|
-
|
|
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` |
|
|
25
|
-
| `--speakers` |
|
|
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
|