evolcore 0.0.33 → 0.0.34
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 +37 -0
- package/bin/ec.js +3 -2
- package/bin/install-codex-managed-hooks.mjs +12 -1
- package/dist/agents/baseagent-availability.js +20 -36
- package/dist/agents/claude-runner.js +82 -13
- package/dist/agents/codex/diagnostics.js +37 -11
- package/dist/agents/codex-runner.js +218 -44
- package/dist/agents/ecagent-runner.js +73 -20
- package/dist/agents/runner-types.js +5 -6
- package/dist/aun/aid/identity.js +1 -1
- package/dist/channels/aun.js +16 -6
- package/dist/channels/daemon.js +6 -3
- package/dist/cli/aun-commands.js +3 -2
- package/dist/cli/ctl-command.js +3 -1
- package/dist/cli/daemon-commands.js +120 -19
- package/dist/cli/fs-command.js +2 -1
- package/dist/cli/index.js +20 -6
- package/dist/cli/integrity-command.js +196 -0
- package/dist/cli/memory-command.js +158 -145
- package/dist/cli/thread-command.js +5 -3
- package/dist/cli/trigger-command.js +6 -1
- package/dist/config/config-field-policy.js +4 -0
- package/dist/config/config-manager.js +19 -0
- package/dist/config/contact-book-store.js +5 -0
- package/dist/config/role-service.js +13 -0
- package/dist/config/runtime-env.js +16 -0
- package/dist/config-store.js +30 -0
- package/dist/core/audit/log-integrity.js +3 -1
- package/dist/core/auth/agent-delegation.js +108 -35
- package/dist/core/auth/delegation-diagnostics.js +414 -0
- package/dist/core/auth/operation-authorizer.js +1 -1
- package/dist/core/auth/operation-catalog.js +16 -0
- package/dist/core/command/agent-control.js +18 -26
- package/dist/core/command/command-handler.js +138 -4
- package/dist/core/command/daemon-diagnostics.js +1 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +22 -7
- package/dist/core/command/menu-protocol.js +1 -0
- package/dist/core/command/slash-handler.js +2 -0
- package/dist/core/daemon-bootstrap.js +4 -7
- package/dist/core/file-integrity/read.js +61 -0
- package/dist/core/file-integrity/store.js +262 -0
- package/dist/core/ipc-registration.js +36 -1
- package/dist/core/memory/batch-meta.js +132 -0
- package/dist/core/memory/consolidation-context.js +35 -14
- package/dist/core/memory/consolidation-ops-records.js +14 -16
- package/dist/core/memory/consolidation.js +91 -26
- package/dist/core/memory/index-manager.js +19 -28
- package/dist/core/memory/memory-admin.js +19 -14
- package/dist/core/memory/memory-document.js +53 -9
- package/dist/core/memory/memory-writer.js +30 -15
- package/dist/core/message/im-renderer.js +20 -4
- package/dist/core/message/message-bridge.js +2 -1
- package/dist/core/message/response-engine.js +584 -47
- package/dist/core/permission/ec-command-parser.js +93 -4
- package/dist/core/permission/protected-paths.js +5 -1
- package/dist/core/permission/readonly-shell-query.js +43 -5
- package/dist/core/permission/session-path-grants.js +334 -0
- package/dist/core/permission/tool-canonical.js +226 -0
- package/dist/core/permission/tool-policy.js +30 -62
- package/dist/core/role/runtime-policy.js +21 -10
- package/dist/core/session/session-fs-store.js +123 -8
- package/dist/core/session/session-manager.js +75 -5
- package/dist/core/skill/index-generator.js +17 -3
- package/dist/core/system-session/grants.js +40 -0
- package/dist/core/system-session/memory.js +222 -0
- package/dist/core/system-session/registry.js +74 -0
- package/dist/core/system-session/types.js +14 -0
- package/dist/core/thread/thread-service.js +13 -20
- package/dist/eck/kit-renderer.js +7 -2
- package/dist/eck/manifest-engine.js +24 -4
- package/dist/index.js +11 -2
- package/dist/ipc.js +51 -40
- package/dist/paths.js +70 -0
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/feedback.js +6 -1
- package/dist/trigger/parser.js +8 -1
- package/dist/trigger/system-definitions.js +3 -3
- package/dist/utils/cross-platform.js +11 -2
- package/dist/utils/error-dict.json +18 -4
- package/dist/utils/error-utils.js +121 -0
- package/dist/utils/instance-registry.js +13 -8
- package/dist/utils/log-writer.js +12 -0
- package/dist/utils/process-introspect.js +11 -0
- package/kits/docs/context-assembly.md +0 -1
- package/kits/docs/evolcore/agent-create.md +5 -4
- package/kits/docs/evolcore/agent.md +2 -2
- package/kits/docs/evolcore/config.md +2 -2
- package/kits/docs/evolcore/ctl.md +2 -2
- package/kits/docs/evolcore/ec-capabilities.md +1 -1
- package/kits/docs/evolcore/fs-architecture.md +4 -5
- package/kits/docs/evolcore/fs.md +1 -1
- package/kits/docs/evolcore/group-collaboration.md +2 -2
- package/kits/docs/evolcore/group.md +10 -10
- package/kits/docs/evolcore/memory-architecture.md +4 -5
- package/kits/docs/evolcore/memory-consolidation.md +18 -14
- package/kits/docs/evolcore/memory-tags.md +3 -1
- package/kits/docs/evolcore/memory.md +24 -12
- package/kits/docs/evolcore/model.md +16 -0
- package/kits/docs/evolcore/msg.md +7 -7
- package/kits/docs/evolcore/thread-chain.md +5 -5
- package/kits/docs/prompt-loading-architecture.md +0 -1
- package/kits/eck_manifest.json +0 -20
- package/kits/eck_manifest.memory.json +0 -12
- package/kits/rules/01-overview.md +11 -0
- package/kits/rules/03-identity.md +6 -3
- package/kits/rules/06-channel.md +1 -1
- package/kits/rules/07-memory-and-thread.md +12 -15
- package/kits/schemas/_meta.json +7 -1
- package/kits/schemas/agent-config.schema.13.json +1 -1
- package/kits/schemas/daemon.schema.7.json +140 -0
- package/kits/skills/ec-group/SKILL.md +4 -4
- package/kits/skills/ec-skill/scripts/cli.mjs +1 -1
- package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
- package/kits/templates/memory-consolidation/forget.md +2 -2
- package/kits/templates/memory-consolidation/generalize.md +4 -4
- package/kits/templates/memory-consolidation/notes.md +11 -11
- package/kits/templates/memory-consolidation/profile.md +3 -3
- package/kits/templates/memory-consolidation/promote.md +3 -3
- package/kits/templates/memory-consolidation/workflow.md +6 -6
- package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
- package/kits/templates/system-fragments/bootstrap.md +4 -3
- package/kits/templates/system-fragments/channel.md +2 -2
- package/kits/templates/system-fragments/memory.md +16 -9
- package/kits/templates/system-fragments/reply.md +6 -6
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +4 -3
- package/dist/core/memory/memory-session-policy.js +0 -41
- package/dist/core/memory/memory-signature.js +0 -257
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import { integrityDbPath, integritySnapshotDir, resolveRoot } from '../../paths.js';
|
|
6
|
+
import { logger } from '../../utils/logger.js';
|
|
7
|
+
// 项目是 ESM,裸 require 不存在;与 codex-session-file-adapter 同款懒加载
|
|
8
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
9
|
+
// ── SQLite 懒加载 ─────────────────────────────────────────────────────────────
|
|
10
|
+
let _db = null;
|
|
11
|
+
const CREATE_TABLE_SQL = `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS file_integrity (
|
|
13
|
+
file_path TEXT PRIMARY KEY,
|
|
14
|
+
sha256 TEXT NOT NULL,
|
|
15
|
+
registered_at INTEGER NOT NULL,
|
|
16
|
+
last_verified_at INTEGER,
|
|
17
|
+
registrar_aid TEXT
|
|
18
|
+
)
|
|
19
|
+
`;
|
|
20
|
+
function getDb() {
|
|
21
|
+
if (_db)
|
|
22
|
+
return _db;
|
|
23
|
+
// node:sqlite (DatabaseSync) 在 Node 22.5+ 可用,懒加载兼容低版本
|
|
24
|
+
let DatabaseSync;
|
|
25
|
+
try {
|
|
26
|
+
({ DatabaseSync } = requireFromHere('node:sqlite'));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error('[file-integrity] node:sqlite 不可用,需要 Node.js 22.5+');
|
|
30
|
+
}
|
|
31
|
+
fs.mkdirSync(path.dirname(integrityDbPath()), { recursive: true });
|
|
32
|
+
_db = new DatabaseSync(integrityDbPath());
|
|
33
|
+
// CLI 进程和 daemon 可能同时打开同一个库:先设等待,再切 WAL
|
|
34
|
+
_db.exec('PRAGMA busy_timeout = 5000');
|
|
35
|
+
_db.exec('PRAGMA journal_mode = WAL');
|
|
36
|
+
_db.exec(CREATE_TABLE_SQL);
|
|
37
|
+
return _db;
|
|
38
|
+
}
|
|
39
|
+
// ── 快照路径规范化 ────────────────────────────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* 台账主键:绝对路径 + 正斜杠;Windows 文件系统大小写不敏感,再统一转小写,
|
|
42
|
+
* 避免 `Evol` / `evol` 这类同一文件两种写法登记成两条。
|
|
43
|
+
*/
|
|
44
|
+
function recordKey(filePath) {
|
|
45
|
+
const normalized = path.resolve(filePath).replace(/\\/g, '/');
|
|
46
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
47
|
+
}
|
|
48
|
+
function snapshotPath(key) {
|
|
49
|
+
const rootKey = recordKey(resolveRoot());
|
|
50
|
+
if (key.startsWith(rootKey + '/')) {
|
|
51
|
+
return path.join(integritySnapshotDir(), key.slice(rootKey.length + 1));
|
|
52
|
+
}
|
|
53
|
+
// 路径在 home 外(如包内 kits/),用 key 的 sha256 命名,防路径穿越和盘符冒号
|
|
54
|
+
const hash = crypto.createHash('sha256').update(key).digest('hex');
|
|
55
|
+
return path.join(integritySnapshotDir(), hash);
|
|
56
|
+
}
|
|
57
|
+
// ── SHA-256 计算 ──────────────────────────────────────────────────────────────
|
|
58
|
+
function computeSha256(bytes) {
|
|
59
|
+
return crypto.createHash('sha256').update(bytes).digest('hex');
|
|
60
|
+
}
|
|
61
|
+
/** 快照存在且内容与台账一致,才算可恢复 */
|
|
62
|
+
function snapshotIntact(key, expected) {
|
|
63
|
+
const snap = snapshotPath(key);
|
|
64
|
+
return fs.existsSync(snap) && computeSha256(fs.readFileSync(snap)) === expected;
|
|
65
|
+
}
|
|
66
|
+
// ── 原子写文件(tmp → rename) ────────────────────────────────────────────────
|
|
67
|
+
function atomicWrite(destPath, bytes) {
|
|
68
|
+
// 带 pid:恢复写盘不在事务里,daemon 与 CLI 同时恢复同一文件时不能共用临时名
|
|
69
|
+
const tmpPath = `${destPath}.integrity-tmp-${process.pid}`;
|
|
70
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
71
|
+
fs.writeFileSync(tmpPath, bytes);
|
|
72
|
+
fs.renameSync(tmpPath, destPath);
|
|
73
|
+
}
|
|
74
|
+
// ── 日志(复用 daemon logger,固定前缀便于过滤) ─────────────────────────────
|
|
75
|
+
function short(hash) {
|
|
76
|
+
return hash.slice(0, 12);
|
|
77
|
+
}
|
|
78
|
+
// ── 核心 API ──────────────────────────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* 登记文件到台账并备份快照。
|
|
81
|
+
* 在文件写入完成后由写入方调用,建立或更新可信基线。
|
|
82
|
+
*/
|
|
83
|
+
export function registerFile(filePath, aid) {
|
|
84
|
+
const key = recordKey(filePath);
|
|
85
|
+
const bytes = fs.readFileSync(filePath);
|
|
86
|
+
registerBytes(key, bytes, aid, fs.statSync(filePath).mtime);
|
|
87
|
+
logger.info(`[file-integrity] registered path=${key} sha256=${short(computeSha256(bytes))}${aid ? ` aid=${aid}` : ''}`);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* node:sqlite 没有 transaction() 包装,手写 BEGIN/COMMIT:
|
|
91
|
+
* 快照写失败则 ROLLBACK,台账不会指向一份不存在的快照。
|
|
92
|
+
*/
|
|
93
|
+
function registerBytes(key, bytes, aid, mtime) {
|
|
94
|
+
const sha256 = computeSha256(bytes);
|
|
95
|
+
const now = Math.floor(Date.now() / 1000);
|
|
96
|
+
const db = getDb();
|
|
97
|
+
db.exec('BEGIN');
|
|
98
|
+
try {
|
|
99
|
+
db.prepare(`INSERT INTO file_integrity (file_path, sha256, registered_at, registrar_aid)
|
|
100
|
+
VALUES (?, ?, ?, ?)
|
|
101
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
102
|
+
sha256 = excluded.sha256,
|
|
103
|
+
registered_at = excluded.registered_at,
|
|
104
|
+
registrar_aid = excluded.registrar_aid`).run(key, sha256, now, aid ?? null);
|
|
105
|
+
const snap = snapshotPath(key);
|
|
106
|
+
atomicWrite(snap, bytes);
|
|
107
|
+
// 快照带上原文件 mtime,恢复时还原回去;记忆的排序和超期判断都读 mtime
|
|
108
|
+
if (mtime)
|
|
109
|
+
fs.utimesSync(snap, mtime, mtime);
|
|
110
|
+
db.exec('COMMIT');
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
try {
|
|
114
|
+
db.exec('ROLLBACK');
|
|
115
|
+
}
|
|
116
|
+
catch { /* ignore */ }
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 校验文件完整性。
|
|
122
|
+
*
|
|
123
|
+
* - 文件不存在 → missing
|
|
124
|
+
* - 台账无记录 → 用本次读到的字节自动登记,返回 verified
|
|
125
|
+
* - sha256 匹配 → verified,更新 last_verified_at
|
|
126
|
+
* - 不匹配且 restoreOnFail → 快照 sha256 与台账一致则原子恢复,返回 restored;否则 tampered
|
|
127
|
+
* - 不匹配且未要求恢复 → tampered
|
|
128
|
+
*
|
|
129
|
+
* 返回的 bytes 就是本次校验过的内容,调用方直接使用,不要再读一次文件(TOCTOU)。
|
|
130
|
+
*/
|
|
131
|
+
export function verifyFile(filePath, opts) {
|
|
132
|
+
const key = recordKey(filePath);
|
|
133
|
+
if (!fs.existsSync(filePath))
|
|
134
|
+
return { status: 'missing' };
|
|
135
|
+
const bytes = fs.readFileSync(filePath);
|
|
136
|
+
const actual = computeSha256(bytes);
|
|
137
|
+
const db = getDb();
|
|
138
|
+
const row = db.prepare('SELECT sha256 FROM file_integrity WHERE file_path = ?').get(key);
|
|
139
|
+
if (!row) {
|
|
140
|
+
// 用已读字节登记,避免二次读文件时内容已变
|
|
141
|
+
registerBytes(key, bytes, undefined, fs.statSync(filePath).mtime);
|
|
142
|
+
logger.info(`[file-integrity] auto-registered path=${key} sha256=${short(actual)}`);
|
|
143
|
+
return { status: 'verified', bytes };
|
|
144
|
+
}
|
|
145
|
+
if (actual === row.sha256) {
|
|
146
|
+
db.prepare('UPDATE file_integrity SET last_verified_at = ? WHERE file_path = ?')
|
|
147
|
+
.run(Math.floor(Date.now() / 1000), key);
|
|
148
|
+
return { status: 'verified', bytes };
|
|
149
|
+
}
|
|
150
|
+
const snap = snapshotPath(key);
|
|
151
|
+
const snapExists = fs.existsSync(snap);
|
|
152
|
+
if (opts?.restoreOnFail && snapExists) {
|
|
153
|
+
const snapBytes = fs.readFileSync(snap);
|
|
154
|
+
if (computeSha256(snapBytes) === row.sha256) {
|
|
155
|
+
atomicWrite(filePath, snapBytes);
|
|
156
|
+
// 还原成登记时的 mtime,不让恢复动作把文件"续期"成刚写入
|
|
157
|
+
try {
|
|
158
|
+
const { mtime } = fs.statSync(snap);
|
|
159
|
+
fs.utimesSync(filePath, mtime, mtime);
|
|
160
|
+
}
|
|
161
|
+
catch { /* best-effort */ }
|
|
162
|
+
logger.warn(`[file-integrity] restored path=${key} expected=${short(row.sha256)} actual=${short(actual)}`);
|
|
163
|
+
return { status: 'restored', bytes: snapBytes };
|
|
164
|
+
}
|
|
165
|
+
logger.error(`[file-integrity] snapshot-corrupted path=${key}`);
|
|
166
|
+
return { status: 'tampered', restorable: false, expected: row.sha256, actual };
|
|
167
|
+
}
|
|
168
|
+
const restorable = snapshotIntact(key, row.sha256);
|
|
169
|
+
logger.warn(`[file-integrity] tampered path=${key} expected=${short(row.sha256)} actual=${short(actual)} restorable=${restorable}`);
|
|
170
|
+
return { status: 'tampered', restorable, expected: row.sha256, actual };
|
|
171
|
+
}
|
|
172
|
+
/** 只读查询台账记录,不读文件,供诊断和 CLI 使用。 */
|
|
173
|
+
export function queryRecord(filePath) {
|
|
174
|
+
const row = getDb().prepare('SELECT file_path, sha256, registered_at, last_verified_at, registrar_aid FROM file_integrity WHERE file_path = ?').get(recordKey(filePath));
|
|
175
|
+
return row ?? null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 文件经正规渠道 rename 后调用:台账主键和快照一起搬到新路径,sha256 不变。
|
|
179
|
+
* 搬移前后的篡改都会在新路径上被校验出来。rename 后不要对新路径重新 registerFile,
|
|
180
|
+
* 那会把搬移前的篡改登记成新基线。
|
|
181
|
+
*/
|
|
182
|
+
export function moveRecord(fromPath, toPath) {
|
|
183
|
+
const fromKey = recordKey(fromPath);
|
|
184
|
+
const toKey = recordKey(toPath);
|
|
185
|
+
if (fromKey === toKey)
|
|
186
|
+
return;
|
|
187
|
+
const db = getDb();
|
|
188
|
+
const row = db.prepare('SELECT 1 FROM file_integrity WHERE file_path = ?').get(fromKey);
|
|
189
|
+
if (!row) {
|
|
190
|
+
// 源文件从未登记过,没有可搬的基线;新路径下次 verify 时自动登记
|
|
191
|
+
logger.warn(`[file-integrity] move-without-record from=${fromKey} to=${toKey}`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const fromSnap = snapshotPath(fromKey);
|
|
195
|
+
const toSnap = snapshotPath(toKey);
|
|
196
|
+
db.exec('BEGIN');
|
|
197
|
+
try {
|
|
198
|
+
db.prepare('DELETE FROM file_integrity WHERE file_path = ?').run(toKey);
|
|
199
|
+
db.prepare('UPDATE file_integrity SET file_path = ? WHERE file_path = ?').run(toKey, fromKey);
|
|
200
|
+
if (fs.existsSync(fromSnap)) {
|
|
201
|
+
fs.mkdirSync(path.dirname(toSnap), { recursive: true });
|
|
202
|
+
fs.renameSync(fromSnap, toSnap);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
fs.rmSync(toSnap, { force: true });
|
|
206
|
+
}
|
|
207
|
+
db.exec('COMMIT');
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
try {
|
|
211
|
+
db.exec('ROLLBACK');
|
|
212
|
+
}
|
|
213
|
+
catch { /* ignore */ }
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
logger.info(`[file-integrity] moved from=${fromKey} to=${toKey}`);
|
|
217
|
+
}
|
|
218
|
+
/** 文件经正规渠道删除后调用:删掉台账记录和快照。 */
|
|
219
|
+
export function unregisterFile(filePath) {
|
|
220
|
+
const key = recordKey(filePath);
|
|
221
|
+
const result = getDb().prepare('DELETE FROM file_integrity WHERE file_path = ?').run(key);
|
|
222
|
+
fs.rmSync(snapshotPath(key), { force: true });
|
|
223
|
+
if (Number(result?.changes ?? 0) > 0)
|
|
224
|
+
logger.info(`[file-integrity] unregistered path=${key}`);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* 登记一段**尚未落盘**的内容。写盘之前调用。
|
|
228
|
+
*
|
|
229
|
+
* 为什么不在写完之后登记:写方和读方可能不在同一个进程(daemon 与 CLI)。
|
|
230
|
+
* 若先改盘再登记,中间那个窗口里另一个进程读到的就是"新内容 + 旧台账",
|
|
231
|
+
* 会被判成篡改并回滚掉刚写进去的合法内容。
|
|
232
|
+
*
|
|
233
|
+
* 配置类文件不靠 mtime 判断超期,所以这里用登记时刻作快照 mtime。
|
|
234
|
+
*/
|
|
235
|
+
export function registerContent(filePath, content, aid) {
|
|
236
|
+
const key = recordKey(filePath);
|
|
237
|
+
const bytes = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf-8');
|
|
238
|
+
registerBytes(key, bytes, aid);
|
|
239
|
+
}
|
|
240
|
+
/** 只读检查,不改台账、不恢复文件,也不更新 last_verified_at。供诊断和 CLI 使用。 */
|
|
241
|
+
export function checkFile(filePath) {
|
|
242
|
+
const key = recordKey(filePath);
|
|
243
|
+
if (!fs.existsSync(filePath))
|
|
244
|
+
return { status: 'missing' };
|
|
245
|
+
const actual = computeSha256(fs.readFileSync(filePath));
|
|
246
|
+
const row = getDb().prepare('SELECT sha256 FROM file_integrity WHERE file_path = ?').get(key);
|
|
247
|
+
if (!row)
|
|
248
|
+
return { status: 'unregistered', actual };
|
|
249
|
+
if (row.sha256 === actual)
|
|
250
|
+
return { status: 'verified', expected: row.sha256, actual };
|
|
251
|
+
return { status: 'tampered', expected: row.sha256, actual, restorable: snapshotIntact(key, row.sha256) };
|
|
252
|
+
}
|
|
253
|
+
/** 仅供测试:关闭并重置 SQLite 连接缓存 */
|
|
254
|
+
export function _resetDb() {
|
|
255
|
+
if (_db) {
|
|
256
|
+
try {
|
|
257
|
+
_db.close();
|
|
258
|
+
}
|
|
259
|
+
catch { /* ignore */ }
|
|
260
|
+
_db = null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -24,6 +24,7 @@ import { registerConsolidationCheckExecutor, registerConsolidationLaunchExecutor
|
|
|
24
24
|
import { createTriggerBaseagentResolver, registerTriggerIpcExecutor } from '../trigger/ipc-service.js';
|
|
25
25
|
import { createThreadOperationExecutor } from './thread/ipc-operation.js';
|
|
26
26
|
import { isLinkedInstall } from '../utils/npm-ops.js';
|
|
27
|
+
import { buildSystemSessionContext, getSystemSessionModule } from './system-session/registry.js';
|
|
27
28
|
export function registerDaemonIpc({ ipcServer, agentRegistry, agentDelegationRegistry, sessionManager, statsCollector, aidStatsCollector, responseEngine, messageQueue, agentMap, commandHandler, channelInstances, eventBus, handoffRuntime, reloadCoordinator, agentApplicationService, agentInitializationService, enqueueConsolidation, completeAgentBootstrap, contactBinds, bindService, diagService, getProcessOwners, getControlAidState, daemonAid, triggerControlToken, triggerSchedulers, daemonTriggerOwner, primaryAgent, triggerAuthorization, }) {
|
|
28
29
|
const authorizeManagedAgentOperation = createDelegationAuthorizer({
|
|
29
30
|
agentDelegationRegistry,
|
|
@@ -359,6 +360,31 @@ export function registerRuntimeIpcProviders({ ipcServer, channelInstances, messa
|
|
|
359
360
|
});
|
|
360
361
|
logger.info('[Memory] 开发仓安装:已启用 ec memory --debug <sessionId>');
|
|
361
362
|
}
|
|
363
|
+
ipcServer.setSystemSessionCommandGate(async ({ sessionId, delegationToken, delegationCommandHash, type, argv }) => {
|
|
364
|
+
// 只读查询,不消耗已审批命令
|
|
365
|
+
const validation = agentDelegationRegistry.validateForContext(delegationToken, sessionId, delegationCommandHash);
|
|
366
|
+
const marker = validation.ok ? validation.grant.systemSession : undefined;
|
|
367
|
+
if (!validation.ok || !marker)
|
|
368
|
+
return undefined;
|
|
369
|
+
const module = getSystemSessionModule(marker.type);
|
|
370
|
+
if (!module)
|
|
371
|
+
return { allow: false, code: 'system_session_unknown', reason: `未注册的系统会话类型: ${marker.type}` };
|
|
372
|
+
// 无 argv 的请求只放行任务上下文查询(ec 命令取自身上下文用)
|
|
373
|
+
if (!argv) {
|
|
374
|
+
return type === 'task-runtime-context'
|
|
375
|
+
? { allow: true }
|
|
376
|
+
: { allow: false, code: 'system_session_command_forbidden', reason: `系统会话不允许调用 ${type}` };
|
|
377
|
+
}
|
|
378
|
+
const session = await sessionManager.getSessionById(sessionId);
|
|
379
|
+
const ctx = buildSystemSessionContext({
|
|
380
|
+
marker,
|
|
381
|
+
sessionId,
|
|
382
|
+
selfAid: validation.grant.selfAid,
|
|
383
|
+
projectPath: session?.projectPath ?? '',
|
|
384
|
+
metadata: session?.metadata,
|
|
385
|
+
});
|
|
386
|
+
return module.checkCommand(argv, ctx);
|
|
387
|
+
});
|
|
362
388
|
ipcServer.setTaskRuntimeContextAuthorizer(({ sessionId, delegationToken, delegationCommandHash, consumeApproval }) => {
|
|
363
389
|
if (consumeApproval && !agentDelegationRegistry.validate)
|
|
364
390
|
return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Command authorization is unavailable' };
|
|
@@ -367,8 +393,17 @@ export function registerRuntimeIpcProviders({ ipcServer, channelInstances, messa
|
|
|
367
393
|
: agentDelegationRegistry.validateForContext(delegationToken, sessionId, delegationCommandHash);
|
|
368
394
|
return authorization.ok
|
|
369
395
|
? { ok: true }
|
|
370
|
-
: {
|
|
396
|
+
: {
|
|
397
|
+
ok: false,
|
|
398
|
+
code: authorization.code,
|
|
399
|
+
error: authorization.reason,
|
|
400
|
+
diagnosticCause: authorization.diagnosticCause,
|
|
401
|
+
...(authorization.diagnosticEventId ? { diagnosticEventId: authorization.diagnosticEventId } : {}),
|
|
402
|
+
};
|
|
371
403
|
});
|
|
404
|
+
if (agentDelegationRegistry.recordDiagnosticFailure) {
|
|
405
|
+
ipcServer.setDelegationFailureRecorder(agentDelegationRegistry.recordDiagnosticFailure.bind(agentDelegationRegistry));
|
|
406
|
+
}
|
|
372
407
|
ipcServer.setCodexPreToolUseExecutor(async (params) => {
|
|
373
408
|
for (const runner of new Set(agentMap.values())) {
|
|
374
409
|
if (typeof runner?.evaluatePreToolUse !== 'function')
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* batch-meta.ts — 整理批次元数据文件(batch.md)
|
|
3
|
+
*
|
|
4
|
+
* 格式:YAML frontmatter + 正文(feedback 内容)
|
|
5
|
+
*
|
|
6
|
+
* ---
|
|
7
|
+
* executionSessionId: memory_20261015T000000_abc
|
|
8
|
+
* selfAid: dddd.agentid.pub
|
|
9
|
+
* mode: notes
|
|
10
|
+
* ...
|
|
11
|
+
* ---
|
|
12
|
+
*
|
|
13
|
+
* ## 整理反馈
|
|
14
|
+
*
|
|
15
|
+
* <feedback 正文>
|
|
16
|
+
*
|
|
17
|
+
* 在 batchDir 创建时写入初始元数据(status=running),会话结束时更新状态、填入 feedback。
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
const BATCH_MD = 'batch.md';
|
|
22
|
+
const FEEDBACK_HEADING = '## 整理反馈';
|
|
23
|
+
// ── frontmatter 序列化 / 解析 ──────────────────────────────────────────────
|
|
24
|
+
function serializeFrontmatter(meta) {
|
|
25
|
+
const lines = ['---'];
|
|
26
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
27
|
+
if (v === undefined)
|
|
28
|
+
continue;
|
|
29
|
+
lines.push(`${k}: ${String(v)}`);
|
|
30
|
+
}
|
|
31
|
+
lines.push('---');
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
34
|
+
function parseFrontmatter(content) {
|
|
35
|
+
if (!content.startsWith('---'))
|
|
36
|
+
return { meta: {}, body: content };
|
|
37
|
+
const end = content.indexOf('\n---', 3);
|
|
38
|
+
if (end === -1)
|
|
39
|
+
return { meta: {}, body: content };
|
|
40
|
+
const block = content.slice(4, end);
|
|
41
|
+
const body = content.slice(end + 4).replace(/^\n/, '');
|
|
42
|
+
const meta = {};
|
|
43
|
+
for (const line of block.split('\n')) {
|
|
44
|
+
const sep = line.indexOf(':');
|
|
45
|
+
if (sep === -1)
|
|
46
|
+
continue;
|
|
47
|
+
const key = line.slice(0, sep).trim();
|
|
48
|
+
const val = line.slice(sep + 1).trim();
|
|
49
|
+
if (val === 'true')
|
|
50
|
+
meta[key] = true;
|
|
51
|
+
else if (val === 'false')
|
|
52
|
+
meta[key] = false;
|
|
53
|
+
else if (val === '')
|
|
54
|
+
meta[key] = undefined;
|
|
55
|
+
else
|
|
56
|
+
meta[key] = val;
|
|
57
|
+
}
|
|
58
|
+
return { meta: meta, body };
|
|
59
|
+
}
|
|
60
|
+
function batchMdPath(batchDir) {
|
|
61
|
+
return path.join(batchDir, BATCH_MD);
|
|
62
|
+
}
|
|
63
|
+
// ── 公开 API ───────────────────────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* 创建 batchDir 后立即写入初始 batch.md(status=running,无 feedback 正文)。
|
|
66
|
+
* 文件已存在时跳过(重入保护)。
|
|
67
|
+
*/
|
|
68
|
+
export function writeBatchMeta(batchDir, meta) {
|
|
69
|
+
try {
|
|
70
|
+
fs.mkdirSync(batchDir, { recursive: true });
|
|
71
|
+
const filePath = batchMdPath(batchDir);
|
|
72
|
+
if (fs.existsSync(filePath))
|
|
73
|
+
return;
|
|
74
|
+
const content = serializeFrontmatter(meta) + '\n';
|
|
75
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
76
|
+
}
|
|
77
|
+
catch { /* 失败静默,不影响主流程 */ }
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 会话结束时更新 batch.md:
|
|
81
|
+
* - 合并 frontmatter 字段(status / finishedAt / success / agentSessionId)
|
|
82
|
+
* - 如果 feedbackText 非空,写入或替换「## 整理反馈」段落
|
|
83
|
+
*/
|
|
84
|
+
export function updateBatchMeta(batchDir, updates) {
|
|
85
|
+
try {
|
|
86
|
+
const filePath = batchMdPath(batchDir);
|
|
87
|
+
if (!fs.existsSync(filePath))
|
|
88
|
+
return;
|
|
89
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
90
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
91
|
+
// 合并字段
|
|
92
|
+
const updated = {
|
|
93
|
+
...meta,
|
|
94
|
+
status: updates.status,
|
|
95
|
+
finishedAt: updates.finishedAt,
|
|
96
|
+
success: updates.success,
|
|
97
|
+
...(updates.agentSessionId ? { agentSessionId: updates.agentSessionId } : {}),
|
|
98
|
+
};
|
|
99
|
+
// 更新 feedback 正文
|
|
100
|
+
let newBody = body;
|
|
101
|
+
if (updates.feedbackText) {
|
|
102
|
+
const feedbackBlock = `${FEEDBACK_HEADING}\n\n${updates.feedbackText}\n`;
|
|
103
|
+
const headingIdx = newBody.indexOf(FEEDBACK_HEADING);
|
|
104
|
+
if (headingIdx !== -1) {
|
|
105
|
+
// 替换已有段落
|
|
106
|
+
newBody = newBody.slice(0, headingIdx) + feedbackBlock;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
newBody = (newBody ? newBody.trimEnd() + '\n\n' : '') + feedbackBlock;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const result = serializeFrontmatter(updated) + '\n' + (newBody ? '\n' + newBody : '');
|
|
113
|
+
fs.writeFileSync(filePath, result, 'utf-8');
|
|
114
|
+
}
|
|
115
|
+
catch { /* 失败静默 */ }
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 读取 batch.md 的 frontmatter(解析失败返回 undefined)。
|
|
119
|
+
*/
|
|
120
|
+
export function readBatchMeta(batchDir) {
|
|
121
|
+
try {
|
|
122
|
+
const filePath = batchMdPath(batchDir);
|
|
123
|
+
if (!fs.existsSync(filePath))
|
|
124
|
+
return undefined;
|
|
125
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
126
|
+
const { meta } = parseFrontmatter(raw);
|
|
127
|
+
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* consolidation-context.ts — 整理上下文 MD 文档序列化(v3)
|
|
3
3
|
*
|
|
4
|
-
* 把一次整理所需的全部信息序列化成 Markdown
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
4
|
+
* 把一次整理所需的全部信息序列化成 Markdown,写一处:
|
|
5
|
+
* - $EVOLCORE_HOME/memory/consolidation-batches/<批次目录>/context.md
|
|
6
|
+
*
|
|
7
|
+
* 任务书只有一个落点。此前另写一份到会话运行目录(sessionRuntimeDir,缺省回落
|
|
8
|
+
* $EVOLCORE_HOME/tmp),导致落点随触发路径漂移,且该目录的读取范围不由任务书所在
|
|
9
|
+
* 的整理会话决定——整理会话读自己的任务书会被只读判定拦成 workspace_escape。
|
|
10
|
+
* 见 docs/memory/session-profile-design.md §5.1。
|
|
7
11
|
*
|
|
8
12
|
* 批次目录命名:<YYYYMMDDTHHmmss>-<selfAid>-<mode>/
|
|
9
13
|
* 批次目录内:context.md(任务书)、ops.jsonl(操作流水)、threads/(话题快照)
|
|
@@ -15,6 +19,8 @@ import { resolveRoot, resolvePaths } from '../../paths.js';
|
|
|
15
19
|
import { DEFAULT_FORGET_AFTER_DAYS, collectCandidates, isDueForReview } from './staleness.js';
|
|
16
20
|
import { chatDirPath } from '../session/session-fs-store.js';
|
|
17
21
|
import { getMemoryArray, getMemoryString, parseMemoryDocument } from './memory-document.js';
|
|
22
|
+
import { writeBatchMeta } from './batch-meta.js';
|
|
23
|
+
import { feedbackFilePath } from './consolidation-ops-records.js';
|
|
18
24
|
export const CONSOLIDATION_MODES = ['notes', 'generalize', 'promote', 'profile', 'forget'];
|
|
19
25
|
/**
|
|
20
26
|
* 模式 → 允许写入的作用域(不含 forget)。
|
|
@@ -421,8 +427,6 @@ export function serializeConsolidationContext(input) {
|
|
|
421
427
|
migrateLegacyConsolidationBatches();
|
|
422
428
|
const scopes = resolveScopeContexts(input);
|
|
423
429
|
const now = new Date();
|
|
424
|
-
// 会话内文件名用:去掉所有分隔符,无 T,14 字符(YYYYMMDDHHmmss)
|
|
425
|
-
const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
426
430
|
const forgetAfterDays = input.forgetAfterDays ?? DEFAULT_FORGET_AFTER_DAYS;
|
|
427
431
|
for (const ctx of scopes) {
|
|
428
432
|
fillCandidates(ctx, input.mode, forgetAfterDays);
|
|
@@ -435,6 +439,9 @@ export function serializeConsolidationContext(input) {
|
|
|
435
439
|
fs.mkdirSync(batchRoot, { recursive: true });
|
|
436
440
|
const batchDir = fs.mkdtempSync(path.join(batchRoot, `${prettyTs}-${selfAid}-${input.mode}-`));
|
|
437
441
|
fs.mkdirSync(batchDir, { recursive: true });
|
|
442
|
+
// 反馈文件的父目录先建好:整理会话只被授权写这一个文件,无权建目录,
|
|
443
|
+
// 而各 runner 的补丁器 / 文件写入工具在父目录不存在时会失败。
|
|
444
|
+
fs.mkdirSync(path.dirname(feedbackFilePath(batchDir)), { recursive: true });
|
|
438
445
|
// ── 话题快照(notes / compress / session-new 模式,有 peerId 时生成)───────
|
|
439
446
|
let historyJsonlPath;
|
|
440
447
|
let threadFiles;
|
|
@@ -512,21 +519,35 @@ export function serializeConsolidationContext(input) {
|
|
|
512
519
|
lines.push('', '---', '');
|
|
513
520
|
}
|
|
514
521
|
lines.push('请按照本次会话注入的模式手册完成整理。');
|
|
522
|
+
lines.push('');
|
|
523
|
+
lines.push('整理结束后,用文件写入工具(Write / Edit)把本次整理的整体反馈写入以下**反馈文件路径**:');
|
|
524
|
+
lines.push('');
|
|
525
|
+
lines.push(`\`${feedbackFilePath(batchDir)}\``);
|
|
526
|
+
lines.push('');
|
|
527
|
+
lines.push('这是本次会话唯一可以写的文件(文件已建好,直接写即可);写别的路径会被拒绝。');
|
|
528
|
+
lines.push('未写入则收尾时回退到默认统计模板(模式、触发方式、耗时、新建/更新/归档条数等)。');
|
|
515
529
|
const text = lines.join('\n');
|
|
516
|
-
// ──
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
521
|
-
const filePath = path.join(tmpDir, `memory-consolidation-${ts}.md`);
|
|
522
|
-
fs.writeFileSync(filePath, text, 'utf-8');
|
|
523
|
-
// ── 写批次目录(长期留存)────────────────────────────────────────────────
|
|
530
|
+
// ── 写批次目录(任务书唯一落点)────────────────────────────────────────────
|
|
531
|
+
// 不再写会话运行目录:任务书落在 $EVOLCORE_HOME/tmp 或 %TEMP%/ec-session-<hash>/
|
|
532
|
+
// 会同时踩两个坑——落点随触发路径而变,且会话运行时目录的读取范围不由任务书
|
|
533
|
+
// 所在的会话决定(见 docs/memory/session-profile-design.md §5.1)。
|
|
524
534
|
const contextFilePath = path.join(batchDir, 'context.md');
|
|
525
535
|
try {
|
|
526
536
|
fs.writeFileSync(contextFilePath, text, 'utf-8');
|
|
527
537
|
}
|
|
528
538
|
catch { /* 批次任务书写入失败不影响整理 */ }
|
|
529
|
-
|
|
539
|
+
// 写入初始 batch.md 元数据
|
|
540
|
+
writeBatchMeta(batchDir, {
|
|
541
|
+
selfAid: input.taskCtx.selfAid ?? 'unknown',
|
|
542
|
+
...(input.taskCtx.peerKey ? { peerKey: input.taskCtx.peerKey } : {}),
|
|
543
|
+
...(input.ownerAid ? { ownerAid: input.ownerAid } : {}),
|
|
544
|
+
...(input.taskCtx.baseagent ? { baseagent: input.taskCtx.baseagent } : {}),
|
|
545
|
+
mode: input.mode,
|
|
546
|
+
triggeredBy: input.triggeredBy,
|
|
547
|
+
startedAt: now.toISOString(),
|
|
548
|
+
status: 'running',
|
|
549
|
+
});
|
|
550
|
+
return { filePath: contextFilePath, batchDir, contextFilePath, scopes, historyJsonlPath, threadFiles, chatDir,
|
|
530
551
|
...(threadFiles?.length ? { snapshotThroughTs: now.getTime() } : {}) };
|
|
531
552
|
}
|
|
532
553
|
/** 会话结束后回填本次产出,追加到任务书末尾。 */
|
|
@@ -38,7 +38,7 @@ export function summarizeOpsRecords(batchDir) {
|
|
|
38
38
|
try {
|
|
39
39
|
for (const line of fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean)) {
|
|
40
40
|
const entry = JSON.parse(line);
|
|
41
|
-
if (entry.op === 'summary' || entry.op === 'session-start' || entry.op === 'session-end'
|
|
41
|
+
if (entry.op === 'summary' || entry.op === 'session-start' || entry.op === 'session-end')
|
|
42
42
|
continue;
|
|
43
43
|
if (!entry.ok) {
|
|
44
44
|
summary.errors++;
|
|
@@ -56,25 +56,23 @@ export function summarizeOpsRecords(batchDir) {
|
|
|
56
56
|
return summary;
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
59
|
+
* 整理会话的反馈文件路径。任务书给出的路径、系统会话模块声明的唯一可写文件、
|
|
60
|
+
* 收尾读取与清理都以此为准,不要在别处重新拼接。
|
|
61
|
+
*/
|
|
62
|
+
export function feedbackFilePath(batchDir) {
|
|
63
|
+
return path.join(batchDir, 'scratch', 'feedback.txt');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 读整理会话写入的反馈文本:<batchDir>/scratch/feedback.txt。
|
|
67
|
+
* 文件不存在或内容为空时返回 undefined,收尾回退默认统计模板。
|
|
61
68
|
*/
|
|
62
69
|
export function readFeedbackText(batchDir) {
|
|
63
|
-
const filePath = path.join(batchDir, 'ops.jsonl');
|
|
64
|
-
if (!fs.existsSync(filePath))
|
|
65
|
-
return undefined;
|
|
66
70
|
try {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (entry.op === 'feedback-set') {
|
|
71
|
-
const text = entry.text;
|
|
72
|
-
if (typeof text === 'string' && text.trim())
|
|
73
|
-
return text;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
71
|
+
const text = fs.readFileSync(feedbackFilePath(batchDir), 'utf-8').trim();
|
|
72
|
+
if (text)
|
|
73
|
+
return text;
|
|
76
74
|
}
|
|
77
|
-
catch { /*
|
|
75
|
+
catch { /* 未写反馈属正常情况 */ }
|
|
78
76
|
return undefined;
|
|
79
77
|
}
|
|
80
78
|
/**
|