- 零第三方依赖,Node >=20 原生 ESM - 团队式编排引擎:拆解/路由/执行/审查/合并全真实 LLM - 阶段心跳、单一权威清单守卫、all-keys-failed 如实上报 - H4 会话视图/amend/watchdog 有界重试/产物区 artifacts.json - H5 零依赖三栏控制台
79 lines
2.6 KiB
JavaScript
79 lines
2.6 KiB
JavaScript
/**
|
||
* WAL / journal(M4):关键动作先追加 JSONL 落盘再执行;重放幂等。
|
||
* state/wal-<owner>.jsonl:{seq,ts,type,taskId,key,data}
|
||
* 重放重建:lastSeq、幂等键集合、每任务事件流(供崩溃恢复 DAG 进度)。
|
||
*/
|
||
import { openSync, appendFileSync, readFileSync, existsSync, mkdirSync, closeSync } from "node:fs";
|
||
import { resolve } from "node:path";
|
||
|
||
export class WAL {
|
||
constructor(stateRoot, owner) {
|
||
mkdirSync(stateRoot, { recursive: true });
|
||
this.owner = owner;
|
||
this.file = resolve(stateRoot, `wal-${owner.replace(/[^A-Za-z0-9_-]/g, "_")}.jsonl`);
|
||
this.seq = 0;
|
||
this.keys = new Set();
|
||
this.byTask = new Map();
|
||
this._recover();
|
||
}
|
||
|
||
_recover() {
|
||
if (!existsSync(this.file)) return;
|
||
const lines = readFileSync(this.file, "utf8").split(/\r?\n/).filter(Boolean);
|
||
for (const line of lines) {
|
||
let e;
|
||
try {
|
||
e = JSON.parse(line);
|
||
} catch {
|
||
continue; // 尾部截断行跳过
|
||
}
|
||
this.seq = Math.max(this.seq, e.seq || 0);
|
||
if (e.key) this.keys.add(e.key);
|
||
if (e.taskId) {
|
||
if (!this.byTask.has(e.taskId)) this.byTask.set(e.taskId, []);
|
||
this.byTask.get(e.taskId).push(e);
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 先写 journal。key 为幂等键:重复 key 返回既有 seq,不重复落盘。 */
|
||
append(type, { taskId = null, key = null, data = {} } = {}) {
|
||
if (key && this.keys.has(key)) {
|
||
return { duplicated: true, seq: this.seq };
|
||
}
|
||
const entry = { seq: ++this.seq, ts: Date.now(), owner: this.owner, type, taskId, key, data };
|
||
const fd = openSync(this.file, "a");
|
||
try {
|
||
appendFileSync(fd, JSON.stringify(entry) + "\n");
|
||
} finally {
|
||
closeSync(fd); // 关闭触发刷盘,保证 kill -9 后可重放
|
||
}
|
||
if (key) this.keys.add(key);
|
||
if (taskId) {
|
||
if (!this.byTask.has(taskId)) this.byTask.set(taskId, []);
|
||
this.byTask.get(taskId).push(entry);
|
||
}
|
||
return { duplicated: false, entry, seq: this.seq };
|
||
}
|
||
|
||
has(key) {
|
||
return this.keys.has(key);
|
||
}
|
||
|
||
events(taskId) {
|
||
return this.byTask.get(taskId) || [];
|
||
}
|
||
|
||
/** 崩溃恢复视角:已 claim 但未 settled 的任务及其进度。 */
|
||
recoverInFlight() {
|
||
const out = [];
|
||
for (const [taskId, events] of this.byTask) {
|
||
const types = new Set(events.map((e) => e.type));
|
||
if (types.has("task.claimed") && !types.has("task.settled")) {
|
||
out.push({ taskId, events: events.map((e) => ({ type: e.type, key: e.key, data: e.data })) });
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
}
|