ppxans-harness 2.4.0 → 3.2.0
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/LICENSE +201 -201
- package/README.md +218 -265
- package/bin/ppx-channels.js +2 -2
- package/bin/ppx-serve.js +5 -5
- package/bin/ppx-setup.js +124 -0
- package/bin/ppx-web.js +140 -0
- package/bin/ppx.js +2 -2
- package/config/identity.md +6 -6
- package/config/ishiki.md +16 -16
- package/config/ppx.json +15 -151
- package/config/ppx.json.example +143 -0
- package/package.json +17 -10
- package/skills/.usage.json +6 -0
- package/skills/agent-professional-training/SKILL.md +94 -0
- package/skills/brainstorm/SKILL.md +24 -0
- package/skills/cupid-lover-comms/SKILL.md +37 -0
- package/skills/debug/SKILL.md +26 -0
- package/skills/plan/SKILL.md +25 -0
- package/skills/ponytail/SKILL.md +25 -0
- package/skills/ppx-memory/SKILL.md +91 -0
- package/skills/ppx-memory/scripts/cli.js +192 -0
- package/skills/ppx-memory/scripts/experience.js +133 -0
- package/skills/ppx-memory/scripts/fact-store.js +842 -0
- package/skills/ppx-memory/scripts/l0.js +52 -0
- package/skills/ppx-memory/scripts/l2.js +146 -0
- package/skills/ppx-memory/scripts/l3.js +112 -0
- package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
- package/skills/ppx-memory/scripts/pii.js +42 -0
- package/skills/ppx-memory/scripts/schema.js +80 -0
- package/skills/ppx-memory/scripts/session.js +398 -0
- package/skills/ppx-memory/scripts/similarity.js +43 -0
- package/skills/ppx-memory/scripts/store.js +116 -0
- package/skills/ppx-memory/scripts/wal.js +38 -0
- package/skills/ppx-selfheal/SKILL.md +24 -0
- package/skills/ppx-selfheal/scripts/cli.js +80 -0
- package/skills/ppx-selfheal/scripts/healer.js +184 -0
- package/skills/ppx-selfheal/scripts/logger.js +17 -0
- package/skills/ppx-selfheal/scripts/store.js +116 -0
- package/skills/prompt-depth-kit/SKILL.md +28 -0
- package/skills/session-naming/SKILL.md +36 -0
- package/skills/verify/SKILL.md +25 -0
- package/src/agent/index.js +1340 -717
- package/src/agent/prompts.js +47 -3
- package/src/aml-server.js +197 -151
- package/src/ans/eviction.js +123 -143
- package/src/ans/guard.js +159 -120
- package/src/ans/lifecycle.js +96 -93
- package/src/ans/proactive.js +112 -129
- package/src/ans/reward.js +95 -111
- package/src/ans/values.js +15 -15
- package/src/audit/audit-chain.js +43 -7
- package/src/audit/verifier.js +157 -120
- package/src/bus/circuit-breaker.js +9 -1
- package/src/bus/runtime-bus.js +107 -93
- package/src/channels/base.js +57 -34
- package/src/channels/feishu.js +118 -126
- package/src/channels/http.js +1090 -592
- package/src/channels/index.js +111 -110
- package/src/channels/log.js +29 -29
- package/src/channels/wechat-crypto.js +73 -74
- package/src/channels/wechat.js +191 -197
- package/src/channels/workspace.js +94 -0
- package/src/channels-cli.js +126 -124
- package/src/cli.js +129 -120
- package/src/commands/index.js +142 -0
- package/src/config/channels.js +137 -170
- package/src/config/index.js +277 -224
- package/src/config/placeholder.js +31 -0
- package/src/config/providers.js +148 -188
- package/src/config/settings.js +156 -182
- package/src/core/policy.js +69 -18
- package/src/core/trace.js +8 -6
- package/src/edit/editblock.js +266 -0
- package/src/edit/snapshot.js +67 -0
- package/src/evidence/index.js +153 -0
- package/src/evolve/playbook.js +9 -10
- package/src/hooks/index.js +113 -0
- package/src/llm/client.js +188 -446
- package/src/llm/dsml.js +74 -74
- package/src/llm/embedder.js +41 -35
- package/src/llm/fence.js +52 -105
- package/src/llm/index.js +4 -4
- package/src/llm/local-embedder.js +94 -0
- package/src/llm/presets.js +113 -0
- package/src/llm/pricing.js +93 -0
- package/src/llm/retry.js +73 -73
- package/src/llm/router.js +92 -97
- package/src/mcp/admin.js +326 -0
- package/src/mcp/client.js +487 -375
- package/src/mcp/http.js +203 -0
- package/src/mcp/index.js +116 -116
- package/src/mcp/server.js +392 -0
- package/src/mcp/tasks.js +133 -0
- package/src/memory/asset-hub.js +6 -11
- package/src/memory/canvas.js +2 -5
- package/src/memory/compaction.js +28 -28
- package/src/memory/experience.js +133 -122
- package/src/memory/fact-store.js +914 -698
- package/src/memory/failure-episode.js +20 -11
- package/src/memory/fork.js +17 -8
- package/src/memory/index.js +8 -6
- package/src/memory/l0.js +53 -52
- package/src/memory/l2.js +145 -130
- package/src/memory/l3.js +111 -111
- package/src/memory/legion-board.js +71 -0
- package/src/memory/memory-ticker.js +239 -240
- package/src/memory/session.js +398 -397
- package/src/memory/sqlite-store.js +581 -0
- package/src/mode/blackboard.js +49 -49
- package/src/mode/graph.js +42 -41
- package/src/mode/index.js +64 -64
- package/src/mode/legion.js +54 -51
- package/src/mode/plan-exec.js +50 -50
- package/src/mode/router.js +28 -40
- package/src/orchestrator/agent-worker.js +69 -69
- package/src/orchestrator/dag.js +90 -83
- package/src/orchestrator/experts.js +76 -0
- package/src/orchestrator/index.js +1 -1
- package/src/orchestrator/legion.js +179 -187
- package/src/orchestrator/supervisor.js +6 -8
- package/src/permissions/index.js +378 -0
- package/src/persona/index.js +28 -29
- package/src/plugin/builtin.js +313 -212
- package/src/plugin/context.js +80 -79
- package/src/plugin/index.js +62 -62
- package/src/plugin/v3.js +73 -0
- package/src/protocol/index.js +148 -0
- package/src/repomap/index.js +309 -0
- package/src/review/index.js +393 -0
- package/src/seam/registry.js +3 -0
- package/src/seam/shell.js +55 -55
- package/src/security/injection.js +79 -0
- package/src/selfheal/evolve.js +67 -67
- package/src/selfheal/healer.js +184 -167
- package/src/selfheal/run.js +9 -9
- package/src/server.js +63 -60
- package/src/services/diagnose.js +180 -0
- package/src/services/learning-service.js +9 -0
- package/src/services/memory-health.js +34 -6
- package/src/services/memory-service.js +42 -9
- package/src/services/triage.js +138 -0
- package/src/session/parts.js +76 -0
- package/src/session/projection.js +73 -0
- package/src/session/rollout.js +54 -0
- package/src/session/turn.js +137 -0
- package/src/skills/lint.js +72 -0
- package/src/skills/loader.js +231 -150
- package/src/skills/search.js +58 -0
- package/src/skills/verify.js +95 -100
- package/src/tools/advanced.js +388 -352
- package/src/tools/builtin.js +384 -297
- package/src/tools/catalog.js +283 -159
- package/src/tools/command-guard.js +112 -112
- package/src/tools/custom.js +47 -47
- package/src/tools/delegate.js +383 -297
- package/src/tools/document.js +254 -253
- package/src/tools/git.js +151 -0
- package/src/tools/governance.js +47 -20
- package/src/tools/index.js +16 -11
- package/src/tools/methods.js +178 -178
- package/src/tools/ocr.js +59 -59
- package/src/tools/sandbox-worker.js +40 -0
- package/src/tools/sandbox.js +92 -0
- package/src/tools/seam.js +162 -125
- package/src/tools/selfmod.js +196 -176
- package/src/tools/v3.js +225 -0
- package/src/tools/vad.js +176 -0
- package/src/tools/voice.js +238 -0
- package/src/utils/async.js +14 -0
- package/src/utils/config-file.js +53 -0
- package/src/utils/crashguard.js +88 -0
- package/src/utils/http.js +53 -0
- package/src/utils/id.js +8 -0
- package/src/utils/json-state.js +33 -0
- package/src/utils/logger.js +17 -17
- package/src/utils/ndjson.js +25 -0
- package/src/utils/pii.js +42 -42
- package/src/utils/rate-limit.js +50 -0
- package/src/utils/schema.js +80 -0
- package/src/utils/similarity.js +43 -0
- package/src/utils/store.js +170 -108
- package/src/utils/text.js +15 -15
- package/src/utils/trace.js +153 -153
- package/src/utils/wal.js +39 -0
- package/src/utils/winutf8.js +16 -15
- package/src/wiki/index.js +170 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// src/memory/session.js - 会话事件日志 (吸收 dsh "会话即唯一事实源")
|
|
2
|
+
// 新架构: 不可变 append-only 事件日志, 每条 = {seq, ts, type, data}
|
|
3
|
+
// - 模型可见历史 = deriveMessages() 从日志投影 (无可变状态, 仅从日志重建)
|
|
4
|
+
// - replay() 回放完整事件流 | fork() 从边界派生新会话
|
|
5
|
+
// - 兼容旧 get/set/has/delete 接口 (agent 无需全量改动)
|
|
6
|
+
// 吸收自 DeepSeek Harness: "model-visible means logged" (能进模型的必须能从日志重建)
|
|
7
|
+
//
|
|
8
|
+
// v1.1.x 第九轮 review P2: default 主会话按天分片 (default-YYYY-MM-DD.jsonl),
|
|
9
|
+
// 单文件不再无限增长。设计要点:
|
|
10
|
+
// - 仅 key === "default" 走按天分片; 非 default 会话保持单文件 (不扩散改动面)
|
|
11
|
+
// - 命名: default-YYYY-MM-DD.jsonl (取事件 ts 所在本地自然日, 与 eventsByDay/logicalDay 一致)
|
|
12
|
+
// - 兼容旧文件: 若历史遗留 default.jsonl, 读取时纳入合并, 不丢历史
|
|
13
|
+
// - seq 跨天连续递增: 同一 default 会话所有分片共用一个 seq 序列, 不从 1 重头数
|
|
14
|
+
// (否则 compaction 的 upToSeq / fork / replay 会错乱)
|
|
15
|
+
// - 所有读取路径把多天分片合并成按 seq(ts) 升序的单一事件流
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { ensureDir, logicalDay } from "./store.js";
|
|
19
|
+
|
|
20
|
+
// 事件类型集 (对齐 dsh 事件域: user/assistant/tool/system)
|
|
21
|
+
export const EVENTS = {
|
|
22
|
+
USER: "user/message",
|
|
23
|
+
ASSISTANT: "assistant/message",
|
|
24
|
+
SYSTEM: "system",
|
|
25
|
+
TOOL_CALL: "tool/call",
|
|
26
|
+
TOOL_RESULT: "tool/result",
|
|
27
|
+
COMPACTION: "compaction/summary",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// default 会话按天分片: default-YYYY-MM-DD.jsonl
|
|
31
|
+
const DEFAULT_SHARD_RE = /^default-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
32
|
+
|
|
33
|
+
export class SessionStore {
|
|
34
|
+
constructor(dataDir) {
|
|
35
|
+
this.dir = path.join(dataDir, "sessions");
|
|
36
|
+
ensureDir(this.dir);
|
|
37
|
+
this._logs = new Map(); // key -> event[] (不可变, append-only)
|
|
38
|
+
this._nextSeq = new Map(); // key -> 下一个 seq
|
|
39
|
+
this._flushedSeq = new Map(); // key -> 已落盘的最大 seq (增量追加用)
|
|
40
|
+
this._loadAll();
|
|
41
|
+
// --- 派生缓存 (v1.1.1 性能优化) ---
|
|
42
|
+
// eventsByDay / deriveCompacted 在每一轮/每一工具轮都被调用, 旧实现每次全量扫描 O(T)。
|
|
43
|
+
// 事件日志是 append-only 不可变, 一个 key 的 events 数组只增不改(set/rename/fork 会换新数组)。
|
|
44
|
+
// · deriveCompacted: 以"数组引用标识"做增量缓存——数组没换就只把尾部新事件追加进 msgs, 命中 O(Δ);
|
|
45
|
+
// 数组被替换(重命名/重建)则重算。compaction 事件追加时检测到也整体重算(它会重投影历史)。
|
|
46
|
+
// · eventsByDay: 版本门控的全库按天缓存, 版本变化时整库重扫一次(每轮兜底, 仍远优于每次 O(T))。
|
|
47
|
+
this._version = 0; // 全局版本: 任何 write 递增
|
|
48
|
+
this._dayCache = new Map(); // day -> 该天结果 (当期 version)
|
|
49
|
+
this._dayCacheAt = 0; // 生成该缓存时的 _version
|
|
50
|
+
// deriveCompacted 增量缓存: key -> { arrRef, msgs, lastCompSeq }
|
|
51
|
+
this._compactedCache = new Map();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_bump() { this._version += 1; }
|
|
55
|
+
|
|
56
|
+
_safe(key) { return String(key || "default").replace(/[^\w.-]/g, "_"); }
|
|
57
|
+
// 旧单文件路径 (仅非 default 会话使用)
|
|
58
|
+
_file(key) { return path.join(this.dir, this._safe(key) + ".jsonl"); }
|
|
59
|
+
|
|
60
|
+
_log(key) {
|
|
61
|
+
const k = this._safe(key);
|
|
62
|
+
if (!this._logs.has(k)) { this._logs.set(k, []); this._nextSeq.set(k, 0); }
|
|
63
|
+
return this._logs.get(k);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 判断文件名是否为 default 的日分片
|
|
67
|
+
_isShard(fname) { return DEFAULT_SHARD_RE.test(fname); }
|
|
68
|
+
// default 分片文件路径 (day 形如 YYYY-MM-DD)
|
|
69
|
+
_shardFile(day) { return path.join(this.dir, `default-${day}.jsonl`); }
|
|
70
|
+
// 事件 ts 归属的本地自然日 (复用 utils/store.logicalDay, 与 eventsByDay 一致, 避免时区错位;
|
|
71
|
+
// 2026-09-18 重构: 原先这里手写了一份与 logicalDay 相同的实现, 双份易漂移)
|
|
72
|
+
_dayOf(ts) {
|
|
73
|
+
return logicalDay(new Date(ts));
|
|
74
|
+
}
|
|
75
|
+
// 列出 default 相关的所有文件 (旧单文件 + 各日分片)
|
|
76
|
+
_defaultFiles() {
|
|
77
|
+
let files = [];
|
|
78
|
+
try {
|
|
79
|
+
files = fs.readdirSync(this.dir)
|
|
80
|
+
.filter((f) => f.endsWith(".jsonl") && (f === "default.jsonl" || this._isShard(f)));
|
|
81
|
+
} catch {}
|
|
82
|
+
return files.map((f) => path.join(this.dir, f));
|
|
83
|
+
}
|
|
84
|
+
_removeDefaultFiles() {
|
|
85
|
+
for (const f of this._defaultFiles()) { try { fs.rmSync(f, { force: true }); } catch {} }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 读取单个 jsonl 文件的事件序列
|
|
89
|
+
_readEventsFromFile(file) {
|
|
90
|
+
const events = [];
|
|
91
|
+
try {
|
|
92
|
+
for (const l of fs.readFileSync(file, "utf8").split("\n").filter(Boolean)) {
|
|
93
|
+
try { const e = JSON.parse(l); if (e && e.seq && e.type && e.data) events.push(e); } catch {}
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
return events;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_loadAll() {
|
|
100
|
+
let files = [];
|
|
101
|
+
try { files = fs.readdirSync(this.dir).filter((f) => f.endsWith(".jsonl")); } catch { return; }
|
|
102
|
+
for (const f of files) {
|
|
103
|
+
// default 的旧单文件 + 日分片 → 归并到同一个 key="default"
|
|
104
|
+
if (f === "default.jsonl" || this._isShard(f)) {
|
|
105
|
+
const events = this._readEventsFromFile(path.join(this.dir, f));
|
|
106
|
+
if (events.length) {
|
|
107
|
+
const k = "default";
|
|
108
|
+
const cur = this._logs.get(k) || [];
|
|
109
|
+
this._logs.set(k, cur.concat(events));
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const k = f.replace(/\.jsonl$/, "");
|
|
114
|
+
const events = this._readEventsFromFile(path.join(this.dir, f));
|
|
115
|
+
if (events.length) {
|
|
116
|
+
events.sort((a, b) => a.seq - b.seq);
|
|
117
|
+
this._logs.set(k, events);
|
|
118
|
+
this._nextSeq.set(k, events[events.length - 1].seq);
|
|
119
|
+
this._flushedSeq.set(k, events[events.length - 1].seq);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// default: 跨文件合并后按 seq(ts 兜底) 升序 + 统一 next/flushed seq (跨天连续递增)
|
|
123
|
+
if (this._logs.has("default")) {
|
|
124
|
+
const evs = this._logs.get("default");
|
|
125
|
+
evs.sort((a, b) => a.seq - b.seq || a.ts - b.ts);
|
|
126
|
+
this._nextSeq.set("default", evs[evs.length - 1].seq);
|
|
127
|
+
this._flushedSeq.set("default", evs[evs.length - 1].seq);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 追加事件 (唯一写入路径, append-only, 永不重写/裁剪日志文件)
|
|
132
|
+
// ts 可选: 测试/回溯时可注入固定时间戳; 缺省用 Date.now() (向后兼容)
|
|
133
|
+
// 追加事件 (唯一写入路径, append-only, 永不重写/裁剪日志文件)
|
|
134
|
+
// ts 可选: 测试/回溯时可注入固定时间戳; 缺省用 Date.now() (向后兼容)
|
|
135
|
+
// opts.skipFlush: true 时延后落盘 (调用方需随后显式 flush(key) 或下次 append), 用于一次性批量写入
|
|
136
|
+
append(key, type, data, ts, { skipFlush = false } = {}) {
|
|
137
|
+
const k = this._safe(key);
|
|
138
|
+
const seq = (this._nextSeq.get(k) || 0) + 1;
|
|
139
|
+
const ev = { seq, ts: Number.isFinite(ts) ? ts : Date.now(), type, data };
|
|
140
|
+
this._log(k).push(ev);
|
|
141
|
+
this._nextSeq.set(k, seq);
|
|
142
|
+
if (!skipFlush) this._flush(k);
|
|
143
|
+
this._bump(); // 使派生缓存失效
|
|
144
|
+
return ev;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 显式落盘: 把 skipFlush 延后的待写事件写入磁盘 (供批量写入后调用)
|
|
148
|
+
flush(key) { this._flush(this._safe(key)); }
|
|
149
|
+
|
|
150
|
+
// 从日志投影模型可见历史 (user/assistant), 无可变状态
|
|
151
|
+
deriveMessages(key) {
|
|
152
|
+
return this._log(this._safe(key))
|
|
153
|
+
.filter((e) => e.type === EVENTS.USER || e.type === EVENTS.ASSISTANT)
|
|
154
|
+
.map((e) => ({ role: e.type === EVENTS.USER ? "user" : "assistant", content: e.data?.content }));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 投影「压缩后」的模型可见历史: 最后一条 compaction 事件之前 (seq <= upToSeq) 的消息被摘要替换
|
|
158
|
+
// 吸收 OpenClaw compaction: 摘要作为单一 surface node 替换被压缩区间, 日志本身不可变
|
|
159
|
+
// v1.1.1: 增量缓存——数组引用没变时只 O(Δ) 追加尾部消息; 数组被替换或追加 compaction 时整体重算。
|
|
160
|
+
deriveCompacted(key) {
|
|
161
|
+
const k = this._safe(key);
|
|
162
|
+
const evs = this._log(k);
|
|
163
|
+
const cached = this._compactedCache.get(k);
|
|
164
|
+
// 数组引用未变 (append-only 稳定), 且无新 compaction: 只把尾部新 user/assistant 追加进 msgs
|
|
165
|
+
if (cached && cached.arrRef === evs) {
|
|
166
|
+
if (cached.consumed === evs.length) return cached.msgs; // 无新增, 直接命中
|
|
167
|
+
// 检查新增区间是否含 compaction (compaction 会重投影历史, 需整体重算)
|
|
168
|
+
for (let i = cached.consumed; i < evs.length; i++) {
|
|
169
|
+
if (evs[i].type === EVENTS.COMPACTION) return this._deriveCompactedFrom(evs, k);
|
|
170
|
+
}
|
|
171
|
+
// 只有 user/assistant 追加: 把可见的尾部消息追加进 msgs
|
|
172
|
+
for (let i = cached.consumed; i < evs.length; i++) {
|
|
173
|
+
const e = evs[i];
|
|
174
|
+
if (e.type === EVENTS.USER || e.type === EVENTS.ASSISTANT) {
|
|
175
|
+
cached.msgs.push({ role: e.type === EVENTS.USER ? "user" : "assistant", content: e.data?.content });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
cached.consumed = evs.length;
|
|
179
|
+
return cached.msgs;
|
|
180
|
+
}
|
|
181
|
+
// 无缓存 / 数组被替换 (set/rename/fork/delete): 整体重算
|
|
182
|
+
return this._deriveCompactedFrom(evs, k);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_deriveCompactedFrom(evs, k) {
|
|
186
|
+
let lastComp = null;
|
|
187
|
+
for (const e of evs) if (e.type === EVENTS.COMPACTION) lastComp = e;
|
|
188
|
+
const upToSeq = lastComp?.data?.upToSeq || 0;
|
|
189
|
+
const msgs = [];
|
|
190
|
+
let consumed = 0;
|
|
191
|
+
if (lastComp?.data?.summary) {
|
|
192
|
+
msgs.push({ role: "system", content: String(lastComp.data.summary) });
|
|
193
|
+
}
|
|
194
|
+
for (const e of evs) {
|
|
195
|
+
consumed++;
|
|
196
|
+
if (e.seq <= upToSeq) continue;
|
|
197
|
+
if (e.type === EVENTS.USER || e.type === EVENTS.ASSISTANT) {
|
|
198
|
+
msgs.push({ role: e.type === EVENTS.USER ? "user" : "assistant", content: e.data?.content });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
this._compactedCache.set(k, { arrRef: evs, msgs, consumed, lastCompSeq: lastComp?.seq || 0 });
|
|
202
|
+
return msgs;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 完整事件流 (回放/审计/轨迹)
|
|
206
|
+
replay(key) { return [...this._log(this._safe(key))]; }
|
|
207
|
+
|
|
208
|
+
// L0 只读代理 / 按天审计: 遍历所有会话, 派生某天的对话事件 (消除 l0/*.jsonl 重复)
|
|
209
|
+
// v1.1.1: 全库按天缓存, 版本未变则零成本; 保持"本地自然日"语义 (与 _dayOf/logicalDay 一致, 避免时区错位)
|
|
210
|
+
eventsByDay(day) {
|
|
211
|
+
const dayStr = String(day).slice(0, 10);
|
|
212
|
+
const dayMs = new Date(dayStr + "T00:00:00").getTime();
|
|
213
|
+
if (Number.isNaN(dayMs)) return [];
|
|
214
|
+
// 缓存: 版本未变时按 day 命中 (append 后 _bump 使整个缓存失效, 下轮重扫一次)
|
|
215
|
+
if (this._dayCacheAt === this._version && this._dayCache.size) {
|
|
216
|
+
const hit = this._dayCache.get(dayStr);
|
|
217
|
+
if (hit) return hit;
|
|
218
|
+
}
|
|
219
|
+
// 全库扫描一次, 按"事件 ts 的本地自然日"聚合缓存所有天 → 之后所有 eventsByDay 调用都命中
|
|
220
|
+
const byDay = new Map();
|
|
221
|
+
for (const [key, events] of this._logs) {
|
|
222
|
+
for (const e of events) {
|
|
223
|
+
if (e.type !== EVENTS.USER && e.type !== EVENTS.ASSISTANT) continue;
|
|
224
|
+
const d = this._dayOf(e.ts);
|
|
225
|
+
let arr = byDay.get(d);
|
|
226
|
+
if (!arr) { arr = []; byDay.set(d, arr); }
|
|
227
|
+
arr.push({ sessionKey: key, role: e.type === EVENTS.USER ? "user" : "assistant", content: e.data?.content, timestamp: e.ts });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
this._dayCache = byDay;
|
|
231
|
+
this._dayCacheAt = this._version;
|
|
232
|
+
// 2026-09-18 修复: 每一天的缓存数组都必须在入缓存前排好时间序 ——
|
|
233
|
+
// 原实现只对当次请求的 day 排序, 其余天命中缓存时返回的是事件追加顺序 (乱序)
|
|
234
|
+
for (const arr of byDay.values()) arr.sort((a, b) => a.timestamp - b.timestamp);
|
|
235
|
+
const out = byDay.get(dayStr) || [];
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 返回某自然日的对话事件(带 seq/type/data), 供 memory-ticker 按 seq 去重滚动归档
|
|
240
|
+
// (防止 _compileDaily_Rolling 每次把今日全文再次追加, 造成 longterm 内容重复累积)
|
|
241
|
+
replayDay(day) {
|
|
242
|
+
const dayStr = String(day).slice(0, 10);
|
|
243
|
+
const out = [];
|
|
244
|
+
for (const [, events] of this._logs) {
|
|
245
|
+
for (const e of events) {
|
|
246
|
+
if (e.type !== EVENTS.USER && e.type !== EVENTS.ASSISTANT) continue;
|
|
247
|
+
if (this._dayOf(e.ts) !== dayStr) continue;
|
|
248
|
+
out.push(e);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
out.sort((a, b) => a.seq - b.seq);
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// fork: 从 boundarySeq 及更早派生新会话 (吸收 dsh fork 语义, 保留不可变源)
|
|
256
|
+
fork(fromKey, boundarySeq, toKey) {
|
|
257
|
+
const keep = this._log(this._safe(fromKey)).filter((e) => e.seq <= boundarySeq);
|
|
258
|
+
const k = this._safe(toKey);
|
|
259
|
+
// 目标为 default 时先清掉旧分片, 再按边界重建 (避免 keep 追加到已有同 seq 行造成重复)
|
|
260
|
+
if (k === "default") this._removeDefaultFiles();
|
|
261
|
+
this._logs.set(k, [...keep]);
|
|
262
|
+
this._nextSeq.set(k, keep.length ? keep[keep.length - 1].seq : 0);
|
|
263
|
+
this._flushedSeq.delete(k);
|
|
264
|
+
this._flush(k);
|
|
265
|
+
this._bump();
|
|
266
|
+
return keep;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// count: 事件条数
|
|
270
|
+
count(key) { return this._log(this._safe(key)).length; }
|
|
271
|
+
|
|
272
|
+
// 列出所有会话: {key, count, lastTs, title} (title 取首条 user 消息前 20 字)
|
|
273
|
+
// 供 Web UI 多会话管理 (P1#6)
|
|
274
|
+
list() {
|
|
275
|
+
const out = [];
|
|
276
|
+
for (const [key, events] of this._logs) {
|
|
277
|
+
if (!events.length) continue;
|
|
278
|
+
const firstUser = events.find((e) => e.type === EVENTS.USER);
|
|
279
|
+
const title = firstUser?.data?.content ? String(firstUser.data.content).slice(0, 20) : key;
|
|
280
|
+
out.push({
|
|
281
|
+
key,
|
|
282
|
+
count: events.length,
|
|
283
|
+
lastTs: events[events.length - 1].ts,
|
|
284
|
+
title,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
out.sort((a, b) => (b.lastTs || 0) - (a.lastTs || 0));
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---- 兼容旧接口 (agent/CLI 仍可用) ----
|
|
292
|
+
get(key) { return this.deriveMessages(key); }
|
|
293
|
+
set(key, history) {
|
|
294
|
+
const k = this._safe(key);
|
|
295
|
+
// default 为分片存储: 清掉内存前先删旧分片, 保证重建后磁盘与内存一致 (无残留旧文件)
|
|
296
|
+
if (k === "default") this._removeDefaultFiles();
|
|
297
|
+
this._logs.delete(k); this._nextSeq.set(k, 0); this._flushedSeq.delete(k);
|
|
298
|
+
for (const m of (history || [])) this.append(k, m.role === "user" ? EVENTS.USER : EVENTS.ASSISTANT, { content: m.content });
|
|
299
|
+
return history;
|
|
300
|
+
}
|
|
301
|
+
has(key) { return this._logs.has(this._safe(key)); }
|
|
302
|
+
// 重命名会话: 复制事件到新 key 并删除旧 key (保留 seq 顺序) [P1#6]
|
|
303
|
+
rename(fromKey, toKey) {
|
|
304
|
+
const f = this._safe(fromKey), t = this._safe(toKey);
|
|
305
|
+
if (f === t) return false;
|
|
306
|
+
if (!this._logs.has(f)) return false;
|
|
307
|
+
if (this._logs.has(t)) return false; // 目标已存在, 拒绝覆盖防数据丢失 [复审 P1]
|
|
308
|
+
const events = this._logs.get(f);
|
|
309
|
+
this._logs.set(t, [...events]);
|
|
310
|
+
const lastSeq = events.length ? events[events.length - 1].seq : 0;
|
|
311
|
+
this._nextSeq.set(t, lastSeq);
|
|
312
|
+
this._flushedSeq.delete(t);
|
|
313
|
+
this._flush(t);
|
|
314
|
+
this.delete(f);
|
|
315
|
+
this._bump();
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
delete(key) {
|
|
319
|
+
const k = this._safe(key);
|
|
320
|
+
this._logs.delete(k); this._nextSeq.delete(k); this._flushedSeq.delete(k);
|
|
321
|
+
try {
|
|
322
|
+
if (k === "default") {
|
|
323
|
+
// 删除 default 所有分片 (旧单文件 + 各日 shard)
|
|
324
|
+
this._removeDefaultFiles();
|
|
325
|
+
} else {
|
|
326
|
+
fs.rmSync(this._file(k), { force: true });
|
|
327
|
+
}
|
|
328
|
+
} catch {}
|
|
329
|
+
this._bump();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 清理过期会话: 删除超过 maxAgeDays 天未活跃的非 default 会话文件
|
|
333
|
+
// 返回删除的会话 key 列表。default 主会话始终保留 (防误删主对话历史)
|
|
334
|
+
// 分片说明: default 的各日分片在 _logs 中只对应一个 key="default", 不会被误当独立会话删除
|
|
335
|
+
pruneOld({ maxAgeDays = 30, keep = [] } = {}) {
|
|
336
|
+
const now = Date.now();
|
|
337
|
+
const cutoff = now - maxAgeDays * 86400000;
|
|
338
|
+
const keepSet = new Set([...keep, "default"].map((k) => this._safe(k)));
|
|
339
|
+
const removed = [];
|
|
340
|
+
for (const [key, events] of this._logs) {
|
|
341
|
+
if (keepSet.has(key)) continue;
|
|
342
|
+
const lastTs = events.length ? events[events.length - 1].ts : 0;
|
|
343
|
+
if (!lastTs || lastTs < cutoff) {
|
|
344
|
+
this.delete(key);
|
|
345
|
+
removed.push(key);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return removed;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 增量落盘: 只追加 flushedSeq 之后的新事件 (P1#5, 消除大会话全量重写)
|
|
352
|
+
// default 按天落盘: 只把当天分片写到对应 default-YYYY-MM-DD.jsonl, 非当前天文件不再改写
|
|
353
|
+
_flush(key) {
|
|
354
|
+
const k = this._safe(key);
|
|
355
|
+
const evs = this._logs.get(k) || [];
|
|
356
|
+
if (!evs.length) return;
|
|
357
|
+
const flushed = this._flushedSeq.get(k) || 0;
|
|
358
|
+
const pending = evs.filter((e) => e.seq > flushed);
|
|
359
|
+
if (!pending.length) return;
|
|
360
|
+
try {
|
|
361
|
+
if (k === "default") this._flushDaily(pending);
|
|
362
|
+
else this._flushLegacy(k, pending, flushed);
|
|
363
|
+
this._flushedSeq.set(k, evs[evs.length - 1].seq);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
// v1.0.9: 落盘失败不再静默 (磁盘满/权限丢失消息不可见), 至少留日志
|
|
366
|
+
try { console.warn(`[session] 会话 ${k} 落盘失败: ${e.message}`); } catch {}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 非 default 会话: 原单文件增量写 (首次全量覆盖重建, 之后追加)
|
|
371
|
+
_flushLegacy(k, pending, flushed) {
|
|
372
|
+
const line = pending.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
373
|
+
if (flushed === 0) {
|
|
374
|
+
fs.writeFileSync(this._file(k), line, "utf8");
|
|
375
|
+
} else {
|
|
376
|
+
fs.appendFileSync(this._file(k), line, "utf8");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// default 会话: 按事件 ts 归属的天分片增量写
|
|
381
|
+
// - 按天分组: 每个文件只追加自己那天的行
|
|
382
|
+
// - 当天文件已有则追加, 首次创建则写入新文件
|
|
383
|
+
_flushDaily(pending) {
|
|
384
|
+
const byDay = new Map();
|
|
385
|
+
for (const e of pending) {
|
|
386
|
+
const d = this._dayOf(e.ts);
|
|
387
|
+
let g = byDay.get(d);
|
|
388
|
+
if (!g) { g = []; byDay.set(d, g); }
|
|
389
|
+
g.push(e);
|
|
390
|
+
}
|
|
391
|
+
for (const [d, group] of byDay) {
|
|
392
|
+
const file = this._shardFile(d);
|
|
393
|
+
const line = group.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
394
|
+
if (fs.existsSync(file)) fs.appendFileSync(file, line, "utf8");
|
|
395
|
+
else fs.writeFileSync(file, line, "utf8"); // 新的一天文件首次写入
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/utils/similarity.js - 中文文本相似度的公共基元 (唯一实现, 2026-09-18 重构收敛)
|
|
2
|
+
// 背景: "字符级 bigram + 交集" 的相似度思路在 fact-store / experience / eviction 三处各写一份,
|
|
3
|
+
// 集合运算 (交集大小 / Jaccard / overlap) 完全重复。这里只收敛「集合级运算 + 字符 bigram 分词」,
|
|
4
|
+
// 各模块保留自己的分词策略与阈值 (fact-store 用 scope 感知的 _bigramSet, 语义不同, 不强行合并)。
|
|
5
|
+
// 零依赖, 纯函数。
|
|
6
|
+
|
|
7
|
+
// 字符级 bigram: 去空白 + 按码点取相邻两字符 (对中文简繁/词序变化有容错)
|
|
8
|
+
// 与 experience/_bigrams、eviction/_bigramSet 原实现一致 (Array.from 按码点切分, astral 字符不劈半)
|
|
9
|
+
export function charBigrams(s) {
|
|
10
|
+
const chars = Array.from(String(s ?? "").replace(/\s+/g, ""));
|
|
11
|
+
const out = new Set();
|
|
12
|
+
for (let i = 0; i < chars.length - 1; i++) out.add(chars[i] + chars[i + 1]);
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 集合交集大小
|
|
17
|
+
export function intersectSize(A, B) {
|
|
18
|
+
let inter = 0;
|
|
19
|
+
for (const x of A) if (B.has(x)) inter++;
|
|
20
|
+
return inter;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Jaccard 系数: 交集 / 并集 (0~1)
|
|
24
|
+
export function setJaccard(A, B) {
|
|
25
|
+
if (!A.size || !B.size) return 0;
|
|
26
|
+
const union = A.size + B.size - intersectSize(A, B);
|
|
27
|
+
return union ? intersectSize(A, B) / union : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// overlap 系数: 交集 / 较短集合 (0~1) —— 对"共享核心词但词序/措辞大变"比 Jaccard 更敏感
|
|
31
|
+
export function setOverlap(A, B) {
|
|
32
|
+
if (!A.size || !B.size) return 0;
|
|
33
|
+
return intersectSize(A, B) / Math.min(A.size, B.size);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 文本级便捷封装 (字符 bigram)
|
|
37
|
+
export function overlapCoefficient(a, b) {
|
|
38
|
+
return setOverlap(charBigrams(a), charBigrams(b));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function jaccardCoefficient(a, b) {
|
|
42
|
+
return setJaccard(charBigrams(a), charBigrams(b));
|
|
43
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// src/utils/store.js - 零依赖文件存储 (JSON + 原子写)
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
|
|
6
|
+
export function ensureDir(dir) {
|
|
7
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
8
|
+
return dir;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function atomicWrite(file, data) {
|
|
12
|
+
ensureDir(path.dirname(file));
|
|
13
|
+
const tmp = file + "." + crypto.randomBytes(4).toString("hex") + ".tmp";
|
|
14
|
+
fs.writeFileSync(tmp, data, "utf8");
|
|
15
|
+
// v1.0.8: rename 覆盖已存在文件在 Windows 并发下可能 EPERM/EEXIST (短窗口), 重试 3 次;
|
|
16
|
+
// 不再降级为非原子直接写 (并发双写可交错损坏文件), 重试仍失败则抛错由调用方处理
|
|
17
|
+
for (let attempt = 0; ; attempt++) {
|
|
18
|
+
try {
|
|
19
|
+
fs.renameSync(tmp, file);
|
|
20
|
+
return;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
if (attempt >= 2) {
|
|
23
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
24
|
+
throw new Error(`原子写失败: ${file} (${e.message})`);
|
|
25
|
+
}
|
|
26
|
+
const end = Date.now() + 30;
|
|
27
|
+
while (Date.now() < end) {} // 短延迟后重试
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readJson(file, fallback = null) {
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(file)) return fallback;
|
|
35
|
+
let raw = fs.readFileSync(file, "utf8");
|
|
36
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1); // 去 BOM
|
|
37
|
+
return JSON.parse(raw);
|
|
38
|
+
} catch {
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function writeJson(file, obj) {
|
|
44
|
+
atomicWrite(file, JSON.stringify(obj, null, 2));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 简单跨进程文件锁 (零依赖, 同步): 原子创建 .lock 文件, 临界区内执行 fn, finally 释放
|
|
48
|
+
// 用于跨 agent 共享文件的"读-改-写"临界区 (防并发覆盖, 如共享经验库)
|
|
49
|
+
// 锁是同步 busy-wait (低频写场景可接受); 超时视为陈旧锁 (崩溃残留) 强取
|
|
50
|
+
export function withFileLock(file, fn, { timeoutMs = 3000, pollMs = 20 } = {}) {
|
|
51
|
+
const lock = file + ".lock";
|
|
52
|
+
const acquire = () => {
|
|
53
|
+
try {
|
|
54
|
+
const fd = fs.openSync(lock, "wx"); // 'wx': 已存在则抛错 (原子)
|
|
55
|
+
try { fs.writeSync(fd, String(process.pid)); } catch {}
|
|
56
|
+
fs.closeSync(fd);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
while (!acquire()) {
|
|
64
|
+
if (Date.now() - start > timeoutMs) {
|
|
65
|
+
try { fs.rmSync(lock, { force: true }); } catch {} // 陈旧锁强取
|
|
66
|
+
if (!acquire()) throw new Error("文件锁获取超时: " + lock);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const end = Date.now() + pollMs;
|
|
70
|
+
while (Date.now() < end) {} // 同步短暂等待
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return fn();
|
|
74
|
+
} finally {
|
|
75
|
+
try { fs.rmSync(lock, { force: true }); } catch {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readText(file, fallback = "") {
|
|
80
|
+
try {
|
|
81
|
+
if (!fs.existsSync(file)) return fallback;
|
|
82
|
+
return fs.readFileSync(file, "utf8");
|
|
83
|
+
} catch {
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function writeText(file, text) {
|
|
89
|
+
atomicWrite(file, text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function appendLine(file, line) {
|
|
93
|
+
ensureDir(path.dirname(file));
|
|
94
|
+
fs.appendFileSync(file, line + "\n", "utf8");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function nowISO() {
|
|
98
|
+
return new Date().toISOString();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function logicalDay(d = new Date()) {
|
|
102
|
+
// 逻辑日 (本地时区): 今日/归档/按天检索统一用本地年月日
|
|
103
|
+
// 与 eventsByDay() 的本地解析保持一致, 避免 8 小时时区偏移导致按天检索错位
|
|
104
|
+
const y = d.getFullYear();
|
|
105
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
106
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
107
|
+
return `${y}-${m}-${day}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 按逻辑日切分 JSONL 的日志目录 (工具轨迹 utils/trace.js 与事件流 core/trace.js 共用)。
|
|
111
|
+
// 两个写入器必须落在同一目录, 用同一函数取路径可避免字面量各写一份后悄悄漂移。
|
|
112
|
+
export function logsDir(dataDir, sub = "traces") {
|
|
113
|
+
const dir = path.join(dataDir, "logs", sub);
|
|
114
|
+
ensureDir(dir);
|
|
115
|
+
return dir;
|
|
116
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/utils/wal.js - 追加式变更日志 (Write-Ahead Log) 轻量实现
|
|
2
|
+
// 用途: 记忆数据的高频增量落盘。每次变更 append 一行事件到 <file>.wal (单行 JSON, 原子追加),
|
|
3
|
+
// 达到阈值或显式 flush 时全量 compact 到主文件 + 清空 WAL。
|
|
4
|
+
// 崩溃安全: 主文件 = 最后一次 flush 快照, WAL = 快照后的增量; 启动时重放 WAL 恢复。
|
|
5
|
+
// 重放幂等: 事件按 id upsert/remove, 重复应用无害 (flush 后崩溃最多重放已落盘变更)。
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { ensureDir } from "./store.js";
|
|
9
|
+
|
|
10
|
+
export function walFileOf(file) {
|
|
11
|
+
return file + ".wal";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 追加一条事件 (JSON 行)。appendFileSync 单行追加在正常写入下原子; 失败抛错由调用方降级 (内存态仍正确, 下次 flush 补齐)。
|
|
15
|
+
export function appendWal(walFile, evt) {
|
|
16
|
+
ensureDir(path.dirname(walFile));
|
|
17
|
+
fs.appendFileSync(walFile, JSON.stringify(evt) + "\n", "utf8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 读 WAL: 逐行解析; 尾部半行 (崩溃中断写入) 静默丢弃; 返回事件数组
|
|
21
|
+
export function readWal(walFile) {
|
|
22
|
+
if (!fs.existsSync(walFile)) return [];
|
|
23
|
+
let raw;
|
|
24
|
+
try { raw = fs.readFileSync(walFile, "utf8"); } catch { return []; }
|
|
25
|
+
if (!raw) return [];
|
|
26
|
+
const events = [];
|
|
27
|
+
for (const line of raw.split("\n")) {
|
|
28
|
+
const t = line.trim();
|
|
29
|
+
if (!t) continue;
|
|
30
|
+
try { events.push(JSON.parse(t)); } catch { break; } // 半行丢弃, 后面的已不可信
|
|
31
|
+
}
|
|
32
|
+
return events;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 清空 WAL (compact 成功后调用; 与 append 同一锁内执行, 防丢事件)
|
|
36
|
+
export function truncateWal(walFile) {
|
|
37
|
+
try { fs.rmSync(walFile, { force: true }); } catch {}
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ppx-selfheal
|
|
3
|
+
description: 皮皮虾自愈引擎的使用与判读:启动体检、损坏数据修复、崩溃恢复、残留清理。出现数据异常、启动失败、疑似崩溃退出时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 皮皮虾自愈引擎 (ppx-selfheal)
|
|
7
|
+
|
|
8
|
+
## 流程
|
|
9
|
+
|
|
10
|
+
1. **跑体检**:`selfheal_run` 触发启动检查(补建缺失目录、修复损坏 JSON、清理残留)。
|
|
11
|
+
2. **读结果**:关注 `integrity.json` 的 `clean` 字段 —— 上次 `clean === false` 说明上回是崩溃退出,本轮会做残留清理。
|
|
12
|
+
3. **损坏隔离而非删除**:损坏的 `facts.json` 会被改名 `.corrupt-<ts>` 保留,修复后确认无用再手工清理。
|
|
13
|
+
4. **阈值门禁**:发布前用 `npm run selfheal` 跑基准,`PPX_MIN_SELFHEAL` 可设通过阈值(默认 7/7)。
|
|
14
|
+
5. **判读自诊断**:`self_diagnose` 聚合审计链与失败案例库,输出"症状 → 根因 → 处方"。
|
|
15
|
+
|
|
16
|
+
## 反合理化
|
|
17
|
+
|
|
18
|
+
- "重启一下就好了"——先看 integrity.json 弄清是崩溃还是正常退出,否则同一故障会反复。
|
|
19
|
+
- "损坏文件直接删"——自愈的语义是隔离保留,绕过它等于放弃取证能力。
|
|
20
|
+
- "自愈通过就万事大吉"——自愈保证的是数据可用,不代表业务逻辑正确。
|
|
21
|
+
|
|
22
|
+
## 验证
|
|
23
|
+
|
|
24
|
+
必须提供:① 自愈基准分数(`npm run selfheal`);② 真实数据目录(非 `root/data`)已被体检的证据;③ 有损坏时的隔离文件名。
|