evolcore 0.0.28 → 0.0.29
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 +12 -0
- package/bin/ec-safe-output.js +270 -54
- package/dist/agents/baseagent-availability.js +37 -1
- package/dist/agents/baseagent.js +19 -9
- package/dist/agents/claude-runner.js +171 -13
- package/dist/agents/codex/app-server-client.js +6 -2
- package/dist/agents/codex/diagnostics.js +4 -3
- package/dist/agents/codex-runner.js +396 -95
- package/dist/agents/gemini-runner.js +35 -17
- package/dist/aun/group/history.js +130 -90
- package/dist/aun/group/operations.js +1 -3
- package/dist/aun/menu/transport.js +5 -3
- package/dist/aun/msg/managed-operation.js +70 -0
- package/dist/aun/msg/p2p.js +1 -3
- package/dist/aun/msg/upload.js +2 -2
- package/dist/aun/outbox.js +1 -37
- package/dist/channels/aun.js +249 -89
- package/dist/channels/daemon.js +18 -1
- package/dist/channels/feishu.js +1 -2
- package/dist/cli/agent-command.js +23 -3
- package/dist/cli/agent-create.js +68 -2
- package/dist/cli/agent.js +12 -2
- package/dist/cli/aun-commands.js +87 -13
- package/dist/cli/cli-argv.js +26 -0
- package/dist/cli/daemon-commands.js +40 -10
- package/dist/cli/data-command.js +4 -4
- package/dist/cli/handoff-command.js +2 -0
- package/dist/cli/index.js +18 -1
- package/dist/cli/managed-command-guard.js +10 -0
- package/dist/cli/memory-command.js +441 -0
- package/dist/cli/restart-monitor.js +62 -6
- package/dist/cli/skill-command.js +41 -0
- package/dist/cli/task-context.js +2 -23
- package/dist/cli/trigger-command.js +9 -1
- package/dist/cli/watch-msg-diag.js +209 -0
- package/dist/config/config-field-policy.js +52 -3
- package/dist/config/config-manager.js +17 -4
- package/dist/config/contact-book-store.js +2 -2
- package/dist/config/mention-mode.js +7 -2
- package/dist/config/merge.js +28 -9
- package/dist/config/resolved-config-op.js +1 -1
- package/dist/config/runtime-env.js +137 -0
- package/dist/config/schema-registry.js +11 -1
- package/dist/config-store.js +10 -10
- package/dist/core/audit/event-key.js +3 -1
- package/dist/core/audit/log-integrity.js +4 -0
- package/dist/core/auth/agent-delegation.js +2 -1
- package/dist/core/auth/auth-gateway.js +13 -6
- package/dist/core/auth/authorization-audit.js +54 -19
- package/dist/core/auth/operation-authorizer.js +17 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/causation/context.js +5 -1
- package/dist/core/command/agent-control.js +1 -2
- package/dist/core/command/command-handler.js +51 -23
- package/dist/core/command/memory-slash.js +270 -0
- package/dist/core/command/menu-catalog.js +1 -0
- package/dist/core/command/menu-handler.js +315 -60
- package/dist/core/command/menu-protocol.js +42 -4
- package/dist/core/command/slash-gate.js +10 -10
- package/dist/core/command/slash-handler.js +206 -64
- package/dist/core/data-migration.js +282 -505
- package/dist/core/event-catalog.js +26 -0
- package/dist/core/evolagent-registry.js +6 -0
- package/dist/core/evolagent.js +0 -10
- package/dist/core/handoff/store.js +1 -20
- package/dist/core/inference/text-inference.js +57 -1
- package/dist/core/memory/consolidation-context.js +505 -0
- package/dist/core/memory/consolidation-ops-records.js +124 -0
- package/dist/core/memory/consolidation-trigger.js +394 -0
- package/dist/core/memory/index-manager.js +308 -0
- package/dist/core/memory/launch-consolidation.js +395 -0
- package/dist/core/memory/memory-admin.js +234 -0
- package/dist/core/memory/memory-document.js +100 -0
- package/dist/core/memory/memory-session-policy.js +41 -0
- package/dist/core/memory/memory-signature.js +257 -0
- package/dist/core/memory/memory-writer.js +432 -0
- package/dist/core/memory/staleness.js +117 -0
- package/dist/core/message/auto-mention-gate.js +454 -0
- package/dist/core/message/im-renderer.js +69 -16
- package/dist/core/message/message-bridge.js +287 -21
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +114 -27
- package/dist/core/message/pending-hints.js +329 -161
- package/dist/core/message/response-engine.js +567 -80
- package/dist/core/permission/ec-command-parser.js +1029 -80
- package/dist/core/permission/protected-paths.js +376 -126
- package/dist/core/permission/sandbox-runtime.js +40 -12
- package/dist/core/permission/tool-error-code.js +96 -13
- package/dist/core/permission/tool-policy.js +276 -23
- package/dist/core/role/runtime-policy.js +3 -7
- package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
- package/dist/core/session/session-fs-store.js +0 -76
- package/dist/core/session/session-idle-monitor.js +355 -0
- package/dist/core/session/session-manager.js +478 -17
- package/dist/core/session/session-mapper.js +33 -16
- package/dist/core/session/session-renew.js +217 -155
- package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
- package/dist/core/skill/global-watcher.js +282 -0
- package/dist/core/skill/index-generator.js +84 -0
- package/dist/core/skill/startup.js +73 -0
- package/dist/core/skill/switch-resolver.js +115 -0
- package/dist/core/task/task-activity-store.js +219 -0
- package/dist/eck/kit-renderer.js +30 -3
- package/dist/eck/manifest-engine.js +26 -4
- package/dist/index.js +708 -18
- package/dist/ipc.js +564 -2
- package/dist/paths.js +30 -9
- package/dist/response-system/engines/v1/proactive-flow.js +116 -15
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +1 -0
- package/dist/trigger/manager.js +0 -10
- package/dist/trigger/scheduler.js +11 -0
- package/dist/trigger/validation.js +22 -0
- package/dist/utils/avatar-upload.js +59 -18
- package/dist/utils/error-dict.json +6 -3
- package/dist/utils/error-utils.js +27 -0
- package/dist/utils/logger.js +32 -10
- package/dist/utils/media-cache.js +84 -0
- package/dist/utils/msg-diag.js +97 -0
- package/dist/utils/welcome.js +37 -8
- package/kits/docs/AUTHORING.md +314 -0
- package/kits/docs/GUIDE.md +2 -0
- package/kits/docs/INDEX.md +2 -0
- package/kits/docs/aun/CHEATSHEET.md +4 -0
- package/kits/docs/aun/SDK.md +60 -0
- package/kits/docs/context-assembly.md +10 -6
- package/kits/docs/evolcore/INDEX.md +17 -3
- package/kits/docs/evolcore/agent-create.md +24 -0
- package/kits/docs/evolcore/agent.md +5 -2
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/ec-capabilities.md +364 -0
- package/kits/docs/evolcore/group.md +34 -18
- package/kits/docs/evolcore/memory-architecture.md +78 -0
- package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
- package/kits/docs/evolcore/memory-consolidation.md +263 -0
- package/kits/docs/evolcore/memory-tags.md +63 -0
- package/kits/docs/evolcore/memory.md +110 -0
- package/kits/docs/evolcore/menu-client.md +27 -11
- package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
- package/kits/docs/evolcore/menu-protocol.md +213 -0
- package/kits/docs/evolcore/msg.md +2 -0
- package/kits/docs/evolcore/trigger.md +7 -5
- package/kits/docs/path-registry.md +6 -8
- package/kits/docs/prompt-loading-architecture.md +5 -3
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +294 -88
- package/kits/eck_manifest.memory.json +257 -0
- package/kits/rules/03-identity.md +8 -8
- package/kits/rules/05-venue.md +4 -2
- package/kits/rules/06-channel.md +1 -1
- package/kits/schemas/_meta.json +276 -47
- package/kits/schemas/agent-config.schema.12.json +38 -2
- package/kits/schemas/agent-config.schema.13.json +591 -0
- package/kits/schemas/daemon.schema.6.json +9 -9
- package/kits/schemas/defaults.schema.5.json +29 -0
- package/kits/schemas/relation-config.schema.8.json +14 -1
- package/kits/schemas/relation-config.schema.9.json +129 -0
- package/kits/schemas/role-config.schema.1.json +3 -3
- package/kits/skills/.switches.json +3 -0
- package/kits/skills/ec-skill/SKILL.md +217 -0
- package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
- package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
- package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
- package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
- package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
- package/kits/templates/decision/auto-mention.md +25 -0
- package/kits/templates/decision/session-renew.md +22 -0
- package/kits/templates/memory-consolidation/forget.md +104 -0
- package/kits/templates/memory-consolidation/generalize.md +76 -0
- package/kits/templates/memory-consolidation/notes.md +133 -0
- package/kits/templates/memory-consolidation/profile.md +58 -0
- package/kits/templates/memory-consolidation/promote.md +72 -0
- package/kits/templates/memory-consolidation/workflow.md +75 -0
- package/kits/templates/system-fragments/bootstrap.md +14 -5
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/group-history.md +6 -0
- package/kits/templates/system-fragments/identity.md +0 -3
- package/kits/templates/system-fragments/memory.md +79 -0
- package/kits/templates/system-fragments/reply.md +1 -0
- package/kits/templates/system-fragments/venue.md +1 -1
- package/package.json +4 -6
- package/dist/trigger/legacy-session-history.js +0 -19
- package/kits/templates/system-fragments/group-unread-history.md +0 -7
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-writer.ts — pending note 与正式记忆的增改归档(v3)
|
|
3
|
+
*
|
|
4
|
+
* 只有整理会话(sessionType=memory)能调用这三个操作,由 ipc.ts 做会话类型与
|
|
5
|
+
* 模式↔作用域的双重校验后委托到这里。本模块只负责文件层面的动作:
|
|
6
|
+
*
|
|
7
|
+
* - createMemory 新建正式记忆:写 consolidated/、签名、归档 --replaces 的旧文件
|
|
8
|
+
* - updateMemory 改标签或续期(--touch),改完重新签名
|
|
9
|
+
* - archiveMemory 归档(唯一的下线方式,没有删除)
|
|
10
|
+
*
|
|
11
|
+
* 签名基础设施已就绪(signMemoryFile),但验签门控尚未开启——
|
|
12
|
+
* 门控设计已存档,待后续按需启用。
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { resolveRoot } from '../../paths.js';
|
|
17
|
+
import { signMemoryFile } from './memory-signature.js';
|
|
18
|
+
import { refreshMemoryMeta } from './index-manager.js';
|
|
19
|
+
import { getMemoryArray, getMemoryString, parseMemoryDocument, serializeMemoryDocument, setMemoryField, } from './memory-document.js';
|
|
20
|
+
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,63}$/;
|
|
21
|
+
/** 与 memory-signature.ts 的 MAX_MEMORY_BYTES 保持一致(10KB)。 */
|
|
22
|
+
const MAX_CONTENT_BYTES = 10 * 1024;
|
|
23
|
+
/** 解析某个作用域的记忆目录。 */
|
|
24
|
+
export function resolveScopeDirs(scope, taskCtx, ownerAid, peerKeyOverride) {
|
|
25
|
+
const selfAid = taskCtx.selfAid;
|
|
26
|
+
if (!selfAid)
|
|
27
|
+
throw new Error('无法确定当前 agent AID(selfAid 为空)');
|
|
28
|
+
const root = resolveRoot();
|
|
29
|
+
if (scope === 'agent') {
|
|
30
|
+
const memoryDir = path.join(root, 'agents', selfAid, 'personal', 'memory');
|
|
31
|
+
return { memoryDir, consolidatedDir: path.join(memoryDir, 'consolidated') };
|
|
32
|
+
}
|
|
33
|
+
if (scope === 'relation') {
|
|
34
|
+
const peerKey = peerKeyOverride ?? taskCtx.peerKey;
|
|
35
|
+
if (!peerKey)
|
|
36
|
+
throw new Error('relation 作用域需要 peerKey(当前会话无对端信息)');
|
|
37
|
+
// 防止路径穿越:peerKey 不能含路径分隔符或 ..
|
|
38
|
+
if (peerKey.includes('..') || peerKey.includes('/') || peerKey.includes('\\')) {
|
|
39
|
+
throw new Error(`INVALID_PEER_KEY: peerKey 格式非法: "${peerKey}"`);
|
|
40
|
+
}
|
|
41
|
+
const memoryDir = path.join(root, 'agents', selfAid, 'relations', peerKey, 'memory');
|
|
42
|
+
return { memoryDir, consolidatedDir: path.join(memoryDir, 'consolidated') };
|
|
43
|
+
}
|
|
44
|
+
if (!ownerAid)
|
|
45
|
+
throw new Error('owner 作用域需要 ownerAid(当前 agent 未配置 owner)');
|
|
46
|
+
const memoryDir = path.join(root, 'memory', 'owner', ownerAid);
|
|
47
|
+
return { memoryDir, consolidatedDir: path.join(memoryDir, 'consolidated') };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 把用户给的路径解析成 consolidated/ 下的绝对路径。
|
|
51
|
+
* 允许 slug / <slug>.md / 相对或绝对路径(须落在本作用域的 consolidated/ 内)。
|
|
52
|
+
*/
|
|
53
|
+
export function resolveConsolidatedPath(dirs, input) {
|
|
54
|
+
const base = path.resolve(dirs.consolidatedDir);
|
|
55
|
+
const looksLikePath = input.includes('/') || input.includes('\\');
|
|
56
|
+
if (!looksLikePath) {
|
|
57
|
+
const slug = input.replace(/\.md$/, '');
|
|
58
|
+
assertSlug(slug);
|
|
59
|
+
return path.join(base, `${slug}.md`);
|
|
60
|
+
}
|
|
61
|
+
// 允许 "consolidated/xxx.md" 这种相对 MEMORY.md 的写法
|
|
62
|
+
const candidates = [path.resolve(input), path.resolve(dirs.memoryDir, input)];
|
|
63
|
+
for (const abs of candidates) {
|
|
64
|
+
const rel = path.relative(base, abs);
|
|
65
|
+
if (!rel.startsWith('..') && !path.isAbsolute(rel))
|
|
66
|
+
return abs;
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`ROLE_ACCESS_DENIED: 只能操作本作用域 consolidated/ 下的记忆(${base}),当前路径越界: ${input}`);
|
|
69
|
+
}
|
|
70
|
+
function assertSlug(slug) {
|
|
71
|
+
if (!SLUG_RE.test(slug)) {
|
|
72
|
+
throw new Error(`slug 格式非法:只允许字母、数字、连字符,长度 1-64,当前值: "${slug}"`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ── shared memory document helpers ─────────────────────────────────────────
|
|
76
|
+
function readMemory(content) {
|
|
77
|
+
return parseMemoryDocument(content);
|
|
78
|
+
}
|
|
79
|
+
function writeSignedMemory(document, selfAid) {
|
|
80
|
+
return signMemoryFile(serializeMemoryDocument(document), selfAid).then(result => result.signed);
|
|
81
|
+
}
|
|
82
|
+
export async function createMemory(req) {
|
|
83
|
+
assertSlug(req.slug);
|
|
84
|
+
if (!req.description || req.description.trim() === '')
|
|
85
|
+
throw new Error('description 不能为空');
|
|
86
|
+
if (!req.content || req.content.trim() === '')
|
|
87
|
+
throw new Error('content 不能为空');
|
|
88
|
+
const bytes = Buffer.byteLength(req.content, 'utf-8');
|
|
89
|
+
if (bytes > MAX_CONTENT_BYTES) {
|
|
90
|
+
throw new Error(`MEMORY_TOO_LARGE: content ${bytes} 字节,超过上限 ${MAX_CONTENT_BYTES} 字节(10KB)。` +
|
|
91
|
+
`一条记忆应当是一句话能说清的结论。内容确实这么多时,请拆成多条分别 create。`);
|
|
92
|
+
}
|
|
93
|
+
const dirs = resolveScopeDirs(req.scope, req.taskCtx, req.ownerAid, req.targetPeerKey);
|
|
94
|
+
fs.mkdirSync(dirs.consolidatedDir, { recursive: true });
|
|
95
|
+
const targetPath = path.join(dirs.consolidatedDir, `${req.slug}.md`);
|
|
96
|
+
const replacePaths = (req.replaces ?? []).map(p => resolveConsolidatedPath(dirs, p));
|
|
97
|
+
// 撞名闸门:目标已存在且没被 --replaces 覆盖 → 拒绝
|
|
98
|
+
if (fs.existsSync(targetPath) && !replacePaths.includes(targetPath)) {
|
|
99
|
+
throw new Error(`SLUG_EXISTS: 目标已存在: ${targetPath}\n` +
|
|
100
|
+
`如果要替换它,请在 --replaces 里列出该路径;如果是新记忆,请换一个 slug。`);
|
|
101
|
+
}
|
|
102
|
+
// profile 唯一性:一个关系只能有一条 profile 记忆
|
|
103
|
+
if (req.tags.includes('profile')) {
|
|
104
|
+
const others = findProfileMemories(dirs, targetPath);
|
|
105
|
+
for (const other of others) {
|
|
106
|
+
if (!replacePaths.includes(other)) {
|
|
107
|
+
throw new Error(`PROFILE_EXISTS: 该作用域已有画像记忆: ${other}\n` +
|
|
108
|
+
`一个关系只能有一条 profile 记忆,重新生成请用 --replaces 指定它。`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const now = new Date().toISOString();
|
|
113
|
+
const createdAt = readExistingCreatedAt(targetPath) ?? now;
|
|
114
|
+
const document = parseMemoryDocument('');
|
|
115
|
+
document.hasFrontmatter = true;
|
|
116
|
+
setMemoryField(document, 'name', req.slug);
|
|
117
|
+
setMemoryField(document, 'description', req.description.trim());
|
|
118
|
+
setMemoryField(document, 'scope', req.scope);
|
|
119
|
+
if (req.scope === 'relation' && req.taskCtx.peerKey)
|
|
120
|
+
setMemoryField(document, 'peerKey', req.taskCtx.peerKey);
|
|
121
|
+
if (req.scope === 'owner' && req.ownerAid)
|
|
122
|
+
setMemoryField(document, 'ownerAid', req.ownerAid);
|
|
123
|
+
setMemoryField(document, 'createdAt', createdAt);
|
|
124
|
+
setMemoryField(document, 'updatedAt', now);
|
|
125
|
+
setMemoryField(document, 'createdBy', 'memory-trigger');
|
|
126
|
+
if (req.scope === 'agent' && req.generalizedFrom)
|
|
127
|
+
setMemoryField(document, 'generalizedFrom', req.generalizedFrom);
|
|
128
|
+
if (req.scope === 'owner' && req.promotedFrom)
|
|
129
|
+
setMemoryField(document, 'promotedFrom', req.promotedFrom);
|
|
130
|
+
if (req.tags.length > 0)
|
|
131
|
+
setMemoryField(document, 'tags', req.tags);
|
|
132
|
+
if (req.keywords?.length)
|
|
133
|
+
setMemoryField(document, 'keywords', req.keywords);
|
|
134
|
+
if (replacePaths.length > 0)
|
|
135
|
+
setMemoryField(document, 'replaces', replacePaths.map(p => path.relative(dirs.memoryDir, p)));
|
|
136
|
+
document.body = req.content.trimEnd();
|
|
137
|
+
const signed = await writeSignedMemory(document, req.taskCtx.selfAid);
|
|
138
|
+
const tmp = targetPath + '.tmp';
|
|
139
|
+
fs.writeFileSync(tmp, signed, 'utf-8');
|
|
140
|
+
fs.renameSync(tmp, targetPath);
|
|
141
|
+
// 归档被替换的旧文件(在写入成功之后做,失败不影响新文件)
|
|
142
|
+
const archived = [];
|
|
143
|
+
for (const p of replacePaths) {
|
|
144
|
+
if (!fs.existsSync(p))
|
|
145
|
+
continue;
|
|
146
|
+
if (path.resolve(p) === path.resolve(targetPath))
|
|
147
|
+
continue;
|
|
148
|
+
const dest = archivePath(dirs.memoryDir, p);
|
|
149
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
150
|
+
try {
|
|
151
|
+
fs.renameSync(p, dest);
|
|
152
|
+
archived.push(dest);
|
|
153
|
+
}
|
|
154
|
+
catch { /* 归档失败不影响主流程,由下次整理清理 */ }
|
|
155
|
+
}
|
|
156
|
+
// 刷新本作用域的 _meta.json(扫描优化用;失败不影响主流程)
|
|
157
|
+
refreshMemoryMeta(dirs.memoryDir);
|
|
158
|
+
return { ok: true, filePath: targetPath, archived, scopeDirs: dirs };
|
|
159
|
+
}
|
|
160
|
+
/** 列出本作用域内除 exceptPath 之外、带 profile 标签的记忆文件。 */
|
|
161
|
+
function findProfileMemories(dirs, exceptPath) {
|
|
162
|
+
if (!fs.existsSync(dirs.consolidatedDir))
|
|
163
|
+
return [];
|
|
164
|
+
const out = [];
|
|
165
|
+
for (const f of fs.readdirSync(dirs.consolidatedDir)) {
|
|
166
|
+
if (!f.endsWith('.md'))
|
|
167
|
+
continue;
|
|
168
|
+
const fp = path.join(dirs.consolidatedDir, f);
|
|
169
|
+
if (path.resolve(fp) === path.resolve(exceptPath))
|
|
170
|
+
continue;
|
|
171
|
+
try {
|
|
172
|
+
const document = readMemory(fs.readFileSync(fp, 'utf-8'));
|
|
173
|
+
if (getMemoryArray(document, 'tags').includes('profile'))
|
|
174
|
+
out.push(fp);
|
|
175
|
+
}
|
|
176
|
+
catch { /* ignore */ }
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
function readExistingCreatedAt(filePath) {
|
|
181
|
+
if (!fs.existsSync(filePath))
|
|
182
|
+
return undefined;
|
|
183
|
+
try {
|
|
184
|
+
return getMemoryString(readMemory(fs.readFileSync(filePath, 'utf-8')), 'createdAt');
|
|
185
|
+
}
|
|
186
|
+
catch { /* ignore */ }
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
/** 归档路径:<memory>/archive/consolidated/<原文件名> */
|
|
190
|
+
function archivePath(memoryDir, filePath) {
|
|
191
|
+
return path.join(memoryDir, 'archive', 'consolidated', path.basename(filePath));
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 把 agent 级或 owner 级记忆降级回上一层(agent→relation 或 owner→agent)。
|
|
195
|
+
* 依据记忆文件 frontmatter 里的 generalizedFrom / promotedFrom 字段找到来源层。
|
|
196
|
+
* 只降一层,可以连续调用进一步降级。
|
|
197
|
+
*
|
|
198
|
+
* 遇到目标路径已有同名文件时,把当前文件归档到 archive/consolidated/,不覆盖也不报错。
|
|
199
|
+
*/
|
|
200
|
+
export async function demoteMemory(req) {
|
|
201
|
+
const selfAid = req.taskCtx.selfAid;
|
|
202
|
+
if (!selfAid)
|
|
203
|
+
throw new Error('无法确定当前 agent AID(selfAid 为空)');
|
|
204
|
+
const root = resolveRoot();
|
|
205
|
+
const srcDirs = resolveScopeDirs(req.scope, req.taskCtx, req.ownerAid);
|
|
206
|
+
const srcPath = path.join(srcDirs.consolidatedDir, `${req.slug}.md`);
|
|
207
|
+
if (!fs.existsSync(srcPath))
|
|
208
|
+
throw new Error(`找不到记忆文件: ${srcPath}`);
|
|
209
|
+
const content = fs.readFileSync(srcPath, 'utf-8');
|
|
210
|
+
const fromRelPath = getMemoryString(readMemory(content), req.scope === 'agent' ? 'generalizedFrom' : 'promotedFrom');
|
|
211
|
+
if (!fromRelPath) {
|
|
212
|
+
throw new Error(`DEMOTE_NO_SOURCE: 记忆文件缺少来源字段(${req.scope === 'agent' ? 'generalizedFrom' : 'promotedFrom'}),` +
|
|
213
|
+
`无法确定降级目标。只有由 generalize/promote 整理会话产出的记忆才能降级。`);
|
|
214
|
+
}
|
|
215
|
+
// 来源路径相对于 agents/<selfAid>/,解析成绝对路径
|
|
216
|
+
const agentRoot = path.join(root, 'agents', selfAid);
|
|
217
|
+
const destPath = path.resolve(agentRoot, fromRelPath);
|
|
218
|
+
// 路径安全检查:目标必须落在当前 agent 目录下
|
|
219
|
+
if (!destPath.startsWith(agentRoot + path.sep)) {
|
|
220
|
+
throw new Error(`DEMOTE_PATH_ERROR: 来源路径越界: ${fromRelPath}`);
|
|
221
|
+
}
|
|
222
|
+
const destMemoryDir = destPath.includes(`${path.sep}consolidated${path.sep}`)
|
|
223
|
+
? destPath.slice(0, destPath.lastIndexOf(`${path.sep}consolidated${path.sep}`))
|
|
224
|
+
: path.dirname(path.dirname(destPath));
|
|
225
|
+
let conflictArchived = false;
|
|
226
|
+
let finalDestPath = destPath;
|
|
227
|
+
if (fs.existsSync(destPath)) {
|
|
228
|
+
// 同名冲突:把当前文件归档,不覆盖,不失败
|
|
229
|
+
const archiveDest = archivePath(srcDirs.memoryDir, srcPath);
|
|
230
|
+
fs.mkdirSync(path.dirname(archiveDest), { recursive: true });
|
|
231
|
+
fs.renameSync(srcPath, archiveDest);
|
|
232
|
+
finalDestPath = archiveDest;
|
|
233
|
+
conflictArchived = true;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
237
|
+
fs.renameSync(srcPath, destPath);
|
|
238
|
+
}
|
|
239
|
+
// 跨作用域搬运,源与目标两个目录都要刷新元数据
|
|
240
|
+
refreshMemoryMeta(srcDirs.memoryDir);
|
|
241
|
+
refreshMemoryMeta(destMemoryDir);
|
|
242
|
+
return {
|
|
243
|
+
ok: true,
|
|
244
|
+
destPath: finalDestPath,
|
|
245
|
+
conflictArchived,
|
|
246
|
+
srcScopeDirs: srcDirs,
|
|
247
|
+
destMemoryDir,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
export async function updateMemory(req) {
|
|
251
|
+
const dirs = resolveScopeDirs(req.scope, req.taskCtx, req.ownerAid);
|
|
252
|
+
const filePath = resolveConsolidatedPath(dirs, req.target);
|
|
253
|
+
if (!fs.existsSync(filePath))
|
|
254
|
+
throw new Error(`找不到记忆文件: ${filePath}`);
|
|
255
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
256
|
+
const document = readMemory(raw);
|
|
257
|
+
const prevTags = getMemoryArray(document, 'tags');
|
|
258
|
+
if (req.tags !== undefined)
|
|
259
|
+
setMemoryField(document, 'tags', req.tags.length > 0 ? req.tags : undefined);
|
|
260
|
+
// 刷新 updatedAt(--touch 的续期语义就是刷新这里 + 文件 mtime)
|
|
261
|
+
const now = new Date().toISOString();
|
|
262
|
+
setMemoryField(document, 'updatedAt', now);
|
|
263
|
+
const signed = await writeSignedMemory(document, req.taskCtx.selfAid);
|
|
264
|
+
const tmp = filePath + '.tmp';
|
|
265
|
+
fs.writeFileSync(tmp, signed, 'utf-8');
|
|
266
|
+
fs.renameSync(tmp, filePath);
|
|
267
|
+
refreshMemoryMeta(dirs.memoryDir);
|
|
268
|
+
return { ok: true, filePath, prevTags, scopeDirs: dirs };
|
|
269
|
+
}
|
|
270
|
+
// ── archive ────────────────────────────────────────────────────────────────
|
|
271
|
+
export async function archiveMemory(req) {
|
|
272
|
+
const dirs = resolveScopeDirs(req.scope, req.taskCtx, req.ownerAid);
|
|
273
|
+
const archived = [];
|
|
274
|
+
for (const t of req.targets) {
|
|
275
|
+
const p = resolveConsolidatedPath(dirs, t);
|
|
276
|
+
if (!fs.existsSync(p))
|
|
277
|
+
throw new Error(`找不到记忆文件: ${p}`);
|
|
278
|
+
const dest = archivePath(dirs.memoryDir, p);
|
|
279
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
280
|
+
fs.renameSync(p, dest);
|
|
281
|
+
archived.push(dest);
|
|
282
|
+
}
|
|
283
|
+
refreshMemoryMeta(dirs.memoryDir);
|
|
284
|
+
return { ok: true, archived, scopeDirs: dirs };
|
|
285
|
+
}
|
|
286
|
+
const NOTE_MAX_CONTENT_BYTES = 4 * 1024;
|
|
287
|
+
function assertMemoryEnabled(memoryConfig) {
|
|
288
|
+
if (memoryConfig?.enableRelation === false) {
|
|
289
|
+
throw new Error('SCOPE_DISABLED: 记忆系统已停用(memory.enableRelation=false)');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
export function relationMemoryDir(taskCtx) {
|
|
293
|
+
const selfAid = taskCtx.selfAid;
|
|
294
|
+
if (!selfAid)
|
|
295
|
+
throw new Error('无法确定当前 agent AID(selfAid 为空)');
|
|
296
|
+
const peerKey = taskCtx.peerKey;
|
|
297
|
+
if (!peerKey)
|
|
298
|
+
throw new Error('relation 作用域需要 peerKey(当前会话无对端信息)');
|
|
299
|
+
if (peerKey.includes('..') || peerKey.includes('/') || peerKey.includes('\\')) {
|
|
300
|
+
throw new Error(`INVALID_PEER_KEY: peerKey 格式非法: "${peerKey}"`);
|
|
301
|
+
}
|
|
302
|
+
return path.join(resolveRoot(), 'agents', selfAid, 'relations', peerKey, 'memory');
|
|
303
|
+
}
|
|
304
|
+
function pendingDir(taskCtx) {
|
|
305
|
+
return path.join(relationMemoryDir(taskCtx), 'pending');
|
|
306
|
+
}
|
|
307
|
+
function assertNoteContent(content) {
|
|
308
|
+
if (!content || content.trim() === '')
|
|
309
|
+
throw new Error('content 不能为空');
|
|
310
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
311
|
+
if (bytes > NOTE_MAX_CONTENT_BYTES)
|
|
312
|
+
throw new Error(`content 超过 ${NOTE_MAX_CONTENT_BYTES} 字节限制(当前 ${bytes} 字节)`);
|
|
313
|
+
}
|
|
314
|
+
function resolvePendingPath(taskCtx, input) {
|
|
315
|
+
const base = path.resolve(pendingDir(taskCtx));
|
|
316
|
+
if (!input.includes('/') && !input.includes('\\')) {
|
|
317
|
+
const slug = input.replace(/\.md$/, '');
|
|
318
|
+
assertSlug(slug);
|
|
319
|
+
return path.join(base, `${slug}.md`);
|
|
320
|
+
}
|
|
321
|
+
const abs = path.resolve(input);
|
|
322
|
+
const rel = path.relative(base, abs);
|
|
323
|
+
if (rel.startsWith('..') || path.isAbsolute(rel))
|
|
324
|
+
throw new Error(`ROLE_ACCESS_DENIED: 只能操作当前关系 pending/ 下的记忆(${base}),当前路径越界: ${input}`);
|
|
325
|
+
return abs;
|
|
326
|
+
}
|
|
327
|
+
function writeAtomic(filePath, content, exclusive = false) {
|
|
328
|
+
const tmpPath = `${filePath}.tmp`;
|
|
329
|
+
try {
|
|
330
|
+
fs.writeFileSync(tmpPath, content, exclusive ? { encoding: 'utf-8', flag: 'wx' } : 'utf-8');
|
|
331
|
+
fs.renameSync(tmpPath, filePath);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
try {
|
|
335
|
+
fs.unlinkSync(tmpPath);
|
|
336
|
+
}
|
|
337
|
+
catch { /* ignore */ }
|
|
338
|
+
if (error?.code === 'EEXIST' && exclusive)
|
|
339
|
+
throw new Error('已有同名的 note 正在写入,请稍后重试或改用 note update');
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
export async function writeMemoryNote(req) {
|
|
344
|
+
assertMemoryEnabled(req.memoryConfig);
|
|
345
|
+
assertNoteContent(req.content);
|
|
346
|
+
assertSlug(req.slug);
|
|
347
|
+
const dir = pendingDir(req.taskCtx);
|
|
348
|
+
const dirCreated = !fs.existsSync(dir);
|
|
349
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
350
|
+
const now = new Date().toISOString();
|
|
351
|
+
const document = parseMemoryDocument('');
|
|
352
|
+
document.hasFrontmatter = true;
|
|
353
|
+
setMemoryField(document, 'name', req.slug);
|
|
354
|
+
setMemoryField(document, 'scope', 'relation');
|
|
355
|
+
setMemoryField(document, 'peerKey', req.taskCtx.peerKey ?? '');
|
|
356
|
+
if (req.taskCtx.chatType)
|
|
357
|
+
setMemoryField(document, 'chatType', req.taskCtx.chatType);
|
|
358
|
+
if (req.speakers?.length)
|
|
359
|
+
setMemoryField(document, 'speakers', req.speakers);
|
|
360
|
+
if (req.taskCtx.sessionId)
|
|
361
|
+
setMemoryField(document, 'evolcoreSessionId', req.taskCtx.sessionId);
|
|
362
|
+
if (req.taskCtx.taskId)
|
|
363
|
+
setMemoryField(document, 'taskId', req.taskCtx.taskId);
|
|
364
|
+
setMemoryField(document, 'createdAt', now);
|
|
365
|
+
setMemoryField(document, 'updatedAt', now);
|
|
366
|
+
setMemoryField(document, 'createdBy', req.taskCtx.baseagent ?? 'agent');
|
|
367
|
+
if (req.tags?.length)
|
|
368
|
+
setMemoryField(document, 'tags', req.tags);
|
|
369
|
+
document.body = req.content.trimEnd();
|
|
370
|
+
const filePath = path.join(dir, `${req.slug}.md`);
|
|
371
|
+
if (fs.existsSync(filePath))
|
|
372
|
+
throw new Error(`SLUG_EXISTS: 该 slug 已存在同名 note: ${filePath}`);
|
|
373
|
+
const content = serializeMemoryDocument(document);
|
|
374
|
+
writeAtomic(filePath, content, true);
|
|
375
|
+
return { ok: true, filePath, dirCreated, fileSize: Buffer.byteLength(content, 'utf-8') };
|
|
376
|
+
}
|
|
377
|
+
export async function updateMemoryNote(req) {
|
|
378
|
+
assertMemoryEnabled(req.memoryConfig);
|
|
379
|
+
const filePath = resolvePendingPath(req.taskCtx, req.target);
|
|
380
|
+
if (!fs.existsSync(filePath))
|
|
381
|
+
throw new Error(`找不到 note: ${filePath}`);
|
|
382
|
+
const document = parseMemoryDocument(fs.readFileSync(filePath, 'utf-8'));
|
|
383
|
+
if (req.newSlug !== undefined) {
|
|
384
|
+
assertSlug(req.newSlug);
|
|
385
|
+
setMemoryField(document, 'name', req.newSlug);
|
|
386
|
+
}
|
|
387
|
+
if (req.tags !== undefined)
|
|
388
|
+
setMemoryField(document, 'tags', req.tags.length ? req.tags : undefined);
|
|
389
|
+
if (req.content !== undefined) {
|
|
390
|
+
assertNoteContent(req.content);
|
|
391
|
+
document.body = req.content;
|
|
392
|
+
}
|
|
393
|
+
setMemoryField(document, 'updatedAt', new Date().toISOString());
|
|
394
|
+
const destPath = req.newSlug === undefined ? filePath : path.join(path.dirname(filePath), `${req.newSlug}.md`);
|
|
395
|
+
if (destPath !== filePath && fs.existsSync(destPath))
|
|
396
|
+
throw new Error(`SLUG_EXISTS: 目标 slug 已存在同名 note: ${destPath}`);
|
|
397
|
+
writeAtomic(destPath, serializeMemoryDocument(document));
|
|
398
|
+
if (destPath !== filePath)
|
|
399
|
+
fs.rmSync(filePath, { force: true });
|
|
400
|
+
return { ok: true, filePath: destPath };
|
|
401
|
+
}
|
|
402
|
+
export async function dropMemoryNote(req) {
|
|
403
|
+
assertMemoryEnabled(req.memoryConfig);
|
|
404
|
+
const filePath = resolvePendingPath(req.taskCtx, req.target);
|
|
405
|
+
if (!fs.existsSync(filePath))
|
|
406
|
+
throw new Error(`找不到 note: ${filePath}`);
|
|
407
|
+
const archiveDir = path.join(path.dirname(filePath), 'archive');
|
|
408
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
409
|
+
const ts = new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').slice(0, 15);
|
|
410
|
+
const dest = path.join(archiveDir, `${ts}-${path.basename(filePath)}`);
|
|
411
|
+
fs.renameSync(filePath, dest);
|
|
412
|
+
return { ok: true, filePath: dest };
|
|
413
|
+
}
|
|
414
|
+
export async function listMemoryNotes(req) {
|
|
415
|
+
assertMemoryEnabled(req.memoryConfig);
|
|
416
|
+
const dir = pendingDir(req.taskCtx);
|
|
417
|
+
if (!fs.existsSync(dir))
|
|
418
|
+
return { ok: true, notes: [] };
|
|
419
|
+
const notes = [];
|
|
420
|
+
for (const name of fs.readdirSync(dir)) {
|
|
421
|
+
if (!name.endsWith('.md'))
|
|
422
|
+
continue;
|
|
423
|
+
try {
|
|
424
|
+
const document = parseMemoryDocument(fs.readFileSync(path.join(dir, name), 'utf-8'));
|
|
425
|
+
const firstLine = document.body.split('\n').map(line => line.trim()).find(Boolean) ?? '';
|
|
426
|
+
notes.push({ slug: getMemoryString(document, 'name') ?? name.replace(/\.md$/, ''), tags: getMemoryArray(document, 'tags'), firstLine: firstLine.length > 80 ? `${firstLine.slice(0, 80)}…` : firstLine, updatedAt: getMemoryString(document, 'updatedAt') });
|
|
427
|
+
}
|
|
428
|
+
catch { /* ignore unreadable notes */ }
|
|
429
|
+
}
|
|
430
|
+
notes.sort((a, b) => (a.updatedAt ?? '').localeCompare(b.updatedAt ?? ''));
|
|
431
|
+
return { ok: true, notes };
|
|
432
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* staleness.ts — 遗忘的超期判据(v3)
|
|
3
|
+
*
|
|
4
|
+
* 两个时间戳分工,是整个遗忘机制的地基:
|
|
5
|
+
*
|
|
6
|
+
* **mtime** —— 「候选集入场券」。超过 forgetAfterDays 就进候选。
|
|
7
|
+
* 进一次候选就刷一次,于是 forgetAfterDays 内不会第二次进候选。
|
|
8
|
+
* **updatedAt** —— 「真正的审查节拍」。决定这条记忆是否真的交给模型审查。
|
|
9
|
+
*
|
|
10
|
+
* 两种记忆的规则不同:
|
|
11
|
+
*
|
|
12
|
+
* 普通记忆
|
|
13
|
+
* mtime 超 forgetAfterDays → 进候选 → updatedAt 也超 forgetAfterDays → 交模型审查
|
|
14
|
+
* 模型续期 → `ec memory update --touch` → mtime 与 updatedAt 同时刷新
|
|
15
|
+
*
|
|
16
|
+
* 长期记忆(带 long-term 标签)
|
|
17
|
+
* mtime 超 forgetAfterDays → 进候选,但【不交给模型】
|
|
18
|
+
* 程序只刷 mtime(updatedAt 不动)→ forgetAfterDays 内不再进候选
|
|
19
|
+
* ……如此反复,直到 updatedAt 超过 30 天才真正交给模型审查
|
|
20
|
+
*
|
|
21
|
+
* 「交模型审查」的门槛固定为 30 天(LONG_TERM_REVIEW_DAYS),不做成配置项。
|
|
22
|
+
*
|
|
23
|
+
* 判据集中在这里,**trigger 与 context 必须共用**——它们如果各写一套,
|
|
24
|
+
* 就会出现「触发了但候选为空」或「评估了不该动的记忆」这类不一致。
|
|
25
|
+
*/
|
|
26
|
+
import fs from 'fs';
|
|
27
|
+
import path from 'path';
|
|
28
|
+
import { getMemoryArray, getMemoryString, parseMemoryDocument } from './memory-document.js';
|
|
29
|
+
/** 普通记忆的遗忘超期天数兜底值,与 MemoryConfig.forgetAfterDays 的默认值一致。 */
|
|
30
|
+
export const DEFAULT_FORGET_AFTER_DAYS = 3;
|
|
31
|
+
/** 长期记忆的固定审查周期(天)。 */
|
|
32
|
+
export const LONG_TERM_REVIEW_DAYS = 30;
|
|
33
|
+
/** 这条记忆是否打了长期标记。 */
|
|
34
|
+
export function isLongTerm(tags) {
|
|
35
|
+
return tags.includes('long-term');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 这条记忆应当在多少天内不被重复审查。
|
|
39
|
+
* 长期记忆用固定 30 天,普通记忆用配置值。
|
|
40
|
+
*/
|
|
41
|
+
export function reviewWindowDays(tags, thresholds) {
|
|
42
|
+
return isLongTerm(tags) ? LONG_TERM_REVIEW_DAYS : thresholds.forgetAfterDays;
|
|
43
|
+
}
|
|
44
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
45
|
+
/** 从 frontmatter 读一个数组字段。 */
|
|
46
|
+
export function readFmArray(content, key) {
|
|
47
|
+
return getMemoryArray(parseMemoryDocument(content), key);
|
|
48
|
+
}
|
|
49
|
+
/** 从 frontmatter 读一个标量字段。 */
|
|
50
|
+
export function readFmValue(content, key) {
|
|
51
|
+
return getMemoryString(parseMemoryDocument(content), key);
|
|
52
|
+
}
|
|
53
|
+
/** 从 frontmatter 读 updatedAt 的毫秒时间戳;读不到返回 undefined。 */
|
|
54
|
+
export function readUpdatedAtMs(content) {
|
|
55
|
+
const raw = readFmValue(content, 'updatedAt');
|
|
56
|
+
if (!raw)
|
|
57
|
+
return undefined;
|
|
58
|
+
const ms = Date.parse(raw);
|
|
59
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
60
|
+
}
|
|
61
|
+
/** 扫目录,返回所有 mtime 已过「候选门票」的记忆。不做超期判定,只收窄集合。 */
|
|
62
|
+
export function collectCandidates(consolidatedDir, thresholds, now = Date.now()) {
|
|
63
|
+
if (!fs.existsSync(consolidatedDir))
|
|
64
|
+
return [];
|
|
65
|
+
let names;
|
|
66
|
+
try {
|
|
67
|
+
names = fs.readdirSync(consolidatedDir).filter(f => f.endsWith('.md'));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const name of names) {
|
|
74
|
+
const filePath = path.join(consolidatedDir, name);
|
|
75
|
+
let content;
|
|
76
|
+
let mtimeMs;
|
|
77
|
+
try {
|
|
78
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
79
|
+
mtimeMs = fs.statSync(filePath).mtimeMs;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// 候选门票:mtime 超 forgetAfterDays。长期记忆不豁免这一关。
|
|
85
|
+
const ticketMs = now - thresholds.forgetAfterDays * DAY_MS;
|
|
86
|
+
if (mtimeMs >= ticketMs)
|
|
87
|
+
continue;
|
|
88
|
+
const tags = readFmArray(content, 'tags');
|
|
89
|
+
const updatedAtMs = readUpdatedAtMs(content) ?? mtimeMs;
|
|
90
|
+
out.push({ name, filePath, tags, mtimeMs, updatedAtMs, content });
|
|
91
|
+
}
|
|
92
|
+
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 这条候选是否**真的该交给模型审查**。
|
|
97
|
+
*
|
|
98
|
+
* 判定落在 updatedAt 上:
|
|
99
|
+
* - 普通记忆:updatedAt 超过 forgetAfterDays
|
|
100
|
+
* - 长期记忆:updatedAt 超过固定的 30 天
|
|
101
|
+
*/
|
|
102
|
+
export function isDueForReview(info, thresholds, now = Date.now()) {
|
|
103
|
+
const windowMs = reviewWindowDays(info.tags, thresholds) * DAY_MS;
|
|
104
|
+
return info.updatedAtMs < now - windowMs;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 只刷 mtime、不动 updatedAt —— 长期记忆的「减速」动作。
|
|
108
|
+
*
|
|
109
|
+
* 效果:这条记忆在 forgetAfterDays 内不会再次进候选,但它**没有**被标记为
|
|
110
|
+
* 「审查过」,所以 updatedAt 继续走,直到超过 30 天才真正交给模型。
|
|
111
|
+
*
|
|
112
|
+
* 这是**程序自己**做的,不暴露成 CLI 参数——否则模型可能拿它假装续期。
|
|
113
|
+
*/
|
|
114
|
+
export function deferCandidate(filePath, now = Date.now()) {
|
|
115
|
+
const t = now / 1000;
|
|
116
|
+
fs.utimesSync(filePath, t, t);
|
|
117
|
+
}
|