@tangle-network/agent-eval 0.132.0 → 0.133.1
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 +15 -0
- package/README.md +1 -1
- package/dist/agent-profile-cell-OhuTee9n.js +335 -0
- package/dist/agent-profile-cell-OhuTee9n.js.map +1 -0
- package/dist/analyst/index.js +3 -3
- package/dist/{analyze-runs-BjPn_fOS.js → analyze-runs-B-afTpCv.js} +6 -19
- package/dist/analyze-runs-B-afTpCv.js.map +1 -0
- package/dist/{analyze-runs-AFDI5RI0.d.ts → analyze-runs-DZr7JW-m.d.ts} +2 -2
- package/dist/{analyze-runs-AFDI5RI0.d.ts.map → analyze-runs-DZr7JW-m.d.ts.map} +1 -1
- package/dist/{baseline-HsBvw_dk.js → baseline-DcX5hQDv.js} +2 -70
- package/dist/{baseline-HsBvw_dk.js.map → baseline-DcX5hQDv.js.map} +1 -1
- package/dist/baseline-hG3K85h4.d.ts.map +1 -1
- package/dist/benchmarks/index.d.ts +1 -1
- package/dist/benchmarks/index.js +1 -1
- package/dist/{benchmarks-DTrT3UH-.js → benchmarks-BU7P6PCW.js} +3 -3
- package/dist/{benchmarks-DTrT3UH-.js.map → benchmarks-BU7P6PCW.js.map} +1 -1
- package/dist/builder-eval/index.js +1 -1
- package/dist/campaign/index.d.ts +3 -3
- package/dist/campaign/index.js +2 -2
- package/dist/{campaign-Cx6CfMR4.js → campaign-CnzHQndg.js} +21 -15
- package/dist/{campaign-Cx6CfMR4.js.map → campaign-CnzHQndg.js.map} +1 -1
- package/dist/cli.js +1 -1
- package/dist/{client-aZDHJiKO.d.ts → client-D4F9hdzR.d.ts} +2 -2
- package/dist/{client-aZDHJiKO.d.ts.map → client-D4F9hdzR.d.ts.map} +1 -1
- package/dist/contract/index.d.ts +145 -6
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +1395 -11
- package/dist/contract/index.js.map +1 -1
- package/dist/control.js +1 -1
- package/dist/{cost-ledger-ZAa_P4r0.js → cost-ledger-BrJxbrMy.js} +238 -3
- package/dist/cost-ledger-BrJxbrMy.js.map +1 -0
- package/dist/{default-registry-B1JcpnRv.js → default-registry-D3T9XbuY.js} +3 -3
- package/dist/{default-registry-B1JcpnRv.js.map → default-registry-D3T9XbuY.js.map} +1 -1
- package/dist/{eval-campaign-mDKhkdUq.js → eval-campaign-DXhpZghy.js} +5 -6
- package/dist/{eval-campaign-mDKhkdUq.js.map → eval-campaign-DXhpZghy.js.map} +1 -1
- package/dist/{task-failure-attributes-CQZlB3et.js → extract-usage-2j25whHw.js} +154 -2
- package/dist/extract-usage-2j25whHw.js.map +1 -0
- package/dist/fuzz.js +1 -1
- package/dist/hosted/index.d.ts +1 -1
- package/dist/{index-3cdlURSk2.d.ts → index-3cdlURSk.d.ts} +1 -1
- package/dist/index-3cdlURSk.d.ts.map +1 -0
- package/dist/{index-FpfWFsKm.d.ts → index-Ba636PKl.d.ts} +29 -6
- package/dist/{index-FpfWFsKm.d.ts.map → index-Ba636PKl.d.ts.map} +1 -1
- package/dist/{index-p2TR_iWJ.d.ts → index-Wek5mU0y.d.ts} +3 -3
- package/dist/{index-p2TR_iWJ.d.ts.map → index-Wek5mU0y.d.ts.map} +1 -1
- package/dist/{index-BAvgST_9.d.ts → index-nhIYz9hn.d.ts} +67 -9
- package/dist/index-nhIYz9hn.d.ts.map +1 -0
- package/dist/index.d.ts +55 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +111 -24
- package/dist/index.js.map +1 -1
- package/dist/ledger-core/index.d.ts +2 -2
- package/dist/ledger-core/index.js +2 -2
- package/dist/ledger-core-CPZfcrC2.js +620 -0
- package/dist/ledger-core-CPZfcrC2.js.map +1 -0
- package/dist/{llm-client-BNcP4v08.js → llm-client-ClPW-dWB.js} +2 -2
- package/dist/{llm-client-BNcP4v08.js.map → llm-client-ClPW-dWB.js.map} +1 -1
- package/dist/meta-eval/index.d.ts +215 -2
- package/dist/{index-CXs7QlR5.d.ts.map → meta-eval/index.d.ts.map} +1 -1
- package/dist/meta-eval/index.js +93 -3
- package/dist/meta-eval/index.js.map +1 -1
- package/dist/{mint-D5_87M5L.js → mint-BvkwcYZU.js} +2 -2
- package/dist/{mint-D5_87M5L.js.map → mint-BvkwcYZU.js.map} +1 -1
- package/dist/openapi.json +1 -1
- package/dist/{paired-arms-D9D0wXj2.js → paired-arms-6XItKzd1.js} +2 -2
- package/dist/{paired-arms-D9D0wXj2.js.map → paired-arms-6XItKzd1.js.map} +1 -1
- package/dist/pipelines/index.js +2 -2
- package/dist/profile-cell.js +1 -242
- package/dist/{propose-review-control-Bqb7daEJ.js → propose-review-control-SQ-n9-We.js} +2 -2
- package/dist/{propose-review-control-Bqb7daEJ.js.map → propose-review-control-SQ-n9-We.js.map} +1 -1
- package/dist/{release-report-oWt9f2k-.js → release-report-wuilQkvK.js} +3 -3
- package/dist/{release-report-oWt9f2k-.js.map → release-report-wuilQkvK.js.map} +1 -1
- package/dist/{replay-D18-pBAA.js → replay-CJfGLdx4.js} +4 -5
- package/dist/{replay-D18-pBAA.js.map → replay-CJfGLdx4.js.map} +1 -1
- package/dist/reporting.d.ts +1 -1
- package/dist/reporting.js +4 -4
- package/dist/{reward-hacking-BEvjdUtD.js → reward-hacking-Dl2UBzej.js} +3 -3
- package/dist/{reward-hacking-BEvjdUtD.js.map → reward-hacking-Dl2UBzej.js.map} +1 -1
- package/dist/rl.d.ts +153 -3
- package/dist/rl.d.ts.map +1 -1
- package/dist/rl.js +222 -7
- package/dist/rl.js.map +1 -1
- package/dist/rollout/index.d.ts +1 -1
- package/dist/rollout/index.js +2 -2
- package/dist/{rollout-VEo41J0N.js → rollout-CeTlDrf6.js} +2 -2
- package/dist/{rollout-VEo41J0N.js.map → rollout-CeTlDrf6.js.map} +1 -1
- package/dist/{rubric-predictive-validity-B3xmbmS1.js → rubric-predictive-validity-QG7ydk0s.js} +2 -2
- package/dist/{rubric-predictive-validity-B3xmbmS1.js.map → rubric-predictive-validity-QG7ydk0s.js.map} +1 -1
- package/dist/{run-record-CN8Zd21B.js → run-record-BIwU2wdV.js} +2 -2
- package/dist/{run-record-CN8Zd21B.js.map → run-record-BIwU2wdV.js.map} +1 -1
- package/dist/{semantic-concept-judge-DKCtoOz8.js → semantic-concept-judge-BypLt6Fw.js} +4 -4
- package/dist/{semantic-concept-judge-DKCtoOz8.js.map → semantic-concept-judge-BypLt6Fw.js.map} +1 -1
- package/dist/{server-Dc_lsOYd.js → server-BPqlDBWK.js} +3 -3
- package/dist/{server-Dc_lsOYd.js.map → server-BPqlDBWK.js.map} +1 -1
- package/dist/{skillopt-optimization-method-CQlz8GQM.js → skillopt-optimization-method-BoIzh7Dl.js} +6 -6
- package/dist/{skillopt-optimization-method-CQlz8GQM.js.map → skillopt-optimization-method-BoIzh7Dl.js.map} +1 -1
- package/dist/{skillopt-optimization-method-C9M_lxdo.d.ts → skillopt-optimization-method-CAASpcS3.d.ts} +3 -3
- package/dist/{skillopt-optimization-method-C9M_lxdo.d.ts.map → skillopt-optimization-method-CAASpcS3.d.ts.map} +1 -1
- package/dist/{statistics-CnnxdpOg.js → statistics-DWM_AyLe.js} +90 -76
- package/dist/statistics-DWM_AyLe.js.map +1 -0
- package/dist/statistics-DbvkkDPa.d.ts.map +1 -1
- package/dist/{summary-report-BNs5nmXI.js → summary-report-Ci17nIdU.js} +4 -4
- package/dist/{summary-report-BNs5nmXI.js.map → summary-report-Ci17nIdU.js.map} +1 -1
- package/dist/traces.js +2 -2
- package/dist/wire/index.js +1 -1
- package/package.json +2 -7
- package/dist/analyze-runs-BjPn_fOS.js.map +0 -1
- package/dist/belief-state/index.d.ts +0 -622
- package/dist/belief-state/index.d.ts.map +0 -1
- package/dist/belief-state/index.js +0 -1819
- package/dist/belief-state/index.js.map +0 -1
- package/dist/calibration-CNWWA6K8.js +0 -94
- package/dist/calibration-CNWWA6K8.js.map +0 -1
- package/dist/code-agent-session-BjkMTQ7H.js +0 -1390
- package/dist/code-agent-session-BjkMTQ7H.js.map +0 -1
- package/dist/code-agent-session-D5URqc3_.d.ts +0 -143
- package/dist/code-agent-session-D5URqc3_.d.ts.map +0 -1
- package/dist/cost-ledger-ZAa_P4r0.js.map +0 -1
- package/dist/extract-usage-BrQ8mCLX.js +0 -155
- package/dist/extract-usage-BrQ8mCLX.js.map +0 -1
- package/dist/index-3cdlURSk2.d.ts.map +0 -1
- package/dist/index-BAvgST_9.d.ts.map +0 -1
- package/dist/index-CXs7QlR5.d.ts +0 -217
- package/dist/ledger-core-eqaI3PCD.js +0 -388
- package/dist/ledger-core-eqaI3PCD.js.map +0 -1
- package/dist/metrics-C9YY1OcL.js +0 -239
- package/dist/metrics-C9YY1OcL.js.map +0 -1
- package/dist/off-policy-DvgzvtIx.js +0 -220
- package/dist/off-policy-DvgzvtIx.js.map +0 -1
- package/dist/off-policy-mskQw8Mb.d.ts +0 -153
- package/dist/off-policy-mskQw8Mb.d.ts.map +0 -1
- package/dist/pre-registration-DakwTRXk.js +0 -96
- package/dist/pre-registration-DakwTRXk.js.map +0 -1
- package/dist/profile-cell.js.map +0 -1
- package/dist/runtime-trajectory-1gyaTOoC.js +0 -93
- package/dist/runtime-trajectory-1gyaTOoC.js.map +0 -1
- package/dist/runtime-trajectory-BW9Wszb-.d.ts +0 -50
- package/dist/runtime-trajectory-BW9Wszb-.d.ts.map +0 -1
- package/dist/statistics-CnnxdpOg.js.map +0 -1
- package/dist/task-failure-attributes-CQZlB3et.js.map +0 -1
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
import { t as canonicalize } from "./pre-registration-DakwTRXk.js";
|
|
2
|
-
import { t as Mutex } from "./concurrency-MUjT7VjM.js";
|
|
3
|
-
import { closeSync, constants, existsSync, fsyncSync, linkSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
5
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import { hostname } from "node:os";
|
|
7
|
-
//#region src/ledger-core/atomic-file-lock.ts
|
|
8
|
-
/** Crash-safe filesystem lock shared by campaign persistence and run exclusion. */
|
|
9
|
-
var AtomicFileLockError = class extends Error {
|
|
10
|
-
name = "AtomicFileLockError";
|
|
11
|
-
};
|
|
12
|
-
/** Report whether another lock path prevents acquisition without modifying it. */
|
|
13
|
-
function probeAtomicFileLock(options) {
|
|
14
|
-
if (existsSync(recoveryPath(options.lockPath))) return {
|
|
15
|
-
acquired: false,
|
|
16
|
-
reason: "recovery"
|
|
17
|
-
};
|
|
18
|
-
const state = ownerState(options.lockPath);
|
|
19
|
-
if (state.state === "held") return {
|
|
20
|
-
acquired: false,
|
|
21
|
-
reason: "held",
|
|
22
|
-
holder: state.owner
|
|
23
|
-
};
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Try to acquire a complete, uniquely-owned lock inode.
|
|
28
|
-
*
|
|
29
|
-
* A hard link publishes fully-written owner metadata atomically. Stale-owner
|
|
30
|
-
* removal is serialized so one reclaimer cannot delete a new owner's lock.
|
|
31
|
-
*/
|
|
32
|
-
function tryAcquireAtomicFileLock(options) {
|
|
33
|
-
const pid = options.pid ?? process.pid;
|
|
34
|
-
if (!Number.isSafeInteger(pid) || pid <= 0) throw new AtomicFileLockError("atomic file lock pid must be a positive integer");
|
|
35
|
-
const owner = {
|
|
36
|
-
pid,
|
|
37
|
-
host: hostname(),
|
|
38
|
-
nonce: randomUUID()
|
|
39
|
-
};
|
|
40
|
-
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
41
|
-
if (existsSync(recoveryPath(options.lockPath))) return {
|
|
42
|
-
acquired: false,
|
|
43
|
-
reason: "recovery"
|
|
44
|
-
};
|
|
45
|
-
if (tryLinkOwner(options.lockPath, owner, `acquire.${attempt}`)) return acquired(options.lockPath, owner);
|
|
46
|
-
const holder = ownerState(options.lockPath);
|
|
47
|
-
if (holder.state === "missing") continue;
|
|
48
|
-
if (holder.state === "held") return {
|
|
49
|
-
acquired: false,
|
|
50
|
-
reason: "held",
|
|
51
|
-
holder: holder.owner
|
|
52
|
-
};
|
|
53
|
-
const reclaimPath = recoveryPath(options.lockPath);
|
|
54
|
-
if (!tryLinkOwner(reclaimPath, owner, `reclaim.${attempt}`)) return {
|
|
55
|
-
acquired: false,
|
|
56
|
-
reason: "recovery"
|
|
57
|
-
};
|
|
58
|
-
try {
|
|
59
|
-
const current = ownerState(options.lockPath);
|
|
60
|
-
if (current.state === "held") return {
|
|
61
|
-
acquired: false,
|
|
62
|
-
reason: "held",
|
|
63
|
-
holder: current.owner
|
|
64
|
-
};
|
|
65
|
-
if (current.state === "stale") {
|
|
66
|
-
const tombstone = `${options.lockPath}.stale.${owner.nonce}.${attempt}`;
|
|
67
|
-
try {
|
|
68
|
-
renameSync(options.lockPath, tombstone);
|
|
69
|
-
unlinkSync(tombstone);
|
|
70
|
-
} catch (error) {
|
|
71
|
-
if (!isMissing(error)) throw error;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (tryLinkOwner(options.lockPath, owner, `recovered.${attempt}`)) return acquired(options.lockPath, owner);
|
|
75
|
-
} finally {
|
|
76
|
-
releaseOwnedPath(reclaimPath, owner);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
throw new AtomicFileLockError(`could not acquire atomic file lock ${options.lockPath}`);
|
|
80
|
-
}
|
|
81
|
-
function acquired(lockPath, owner) {
|
|
82
|
-
return {
|
|
83
|
-
acquired: true,
|
|
84
|
-
lock: {
|
|
85
|
-
owner,
|
|
86
|
-
release: () => releaseOwnedPath(lockPath, owner)
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
function tryLinkOwner(lockPath, owner, suffix) {
|
|
91
|
-
const ownerPath = `${lockPath}.${owner.pid}.${owner.nonce}.${suffix}.owner`;
|
|
92
|
-
const descriptor = openSync(ownerPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
|
|
93
|
-
try {
|
|
94
|
-
writeFileSync(descriptor, `${canonicalOwner(owner)}\n`, "utf8");
|
|
95
|
-
fsyncSync(descriptor);
|
|
96
|
-
} catch (error) {
|
|
97
|
-
closeSync(descriptor);
|
|
98
|
-
unlinkIfExists(ownerPath);
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
closeSync(descriptor);
|
|
102
|
-
try {
|
|
103
|
-
linkSync(ownerPath, lockPath);
|
|
104
|
-
return true;
|
|
105
|
-
} catch (error) {
|
|
106
|
-
if (!isAlreadyExists(error)) throw error;
|
|
107
|
-
return false;
|
|
108
|
-
} finally {
|
|
109
|
-
unlinkIfExists(ownerPath);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function ownerState(lockPath) {
|
|
113
|
-
const owner = readOwner(lockPath);
|
|
114
|
-
if (!owner) return { state: "missing" };
|
|
115
|
-
if (owner.host !== hostname()) return {
|
|
116
|
-
state: "held",
|
|
117
|
-
owner
|
|
118
|
-
};
|
|
119
|
-
try {
|
|
120
|
-
process.kill(owner.pid, 0);
|
|
121
|
-
return {
|
|
122
|
-
state: "held",
|
|
123
|
-
owner
|
|
124
|
-
};
|
|
125
|
-
} catch (error) {
|
|
126
|
-
if (isNoSuchProcess(error)) return {
|
|
127
|
-
state: "stale",
|
|
128
|
-
owner
|
|
129
|
-
};
|
|
130
|
-
return {
|
|
131
|
-
state: "held",
|
|
132
|
-
owner
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function readOwner(lockPath) {
|
|
137
|
-
let contents;
|
|
138
|
-
try {
|
|
139
|
-
contents = readFileSync(lockPath, "utf8").trim();
|
|
140
|
-
} catch (error) {
|
|
141
|
-
if (isMissing(error)) return void 0;
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
let value;
|
|
145
|
-
try {
|
|
146
|
-
value = JSON.parse(contents);
|
|
147
|
-
} catch (error) {
|
|
148
|
-
throw invalidOwner(lockPath, error);
|
|
149
|
-
}
|
|
150
|
-
if (!isOwner(value)) throw invalidOwner(lockPath);
|
|
151
|
-
return value;
|
|
152
|
-
}
|
|
153
|
-
function isOwner(value) {
|
|
154
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
155
|
-
const record = value;
|
|
156
|
-
return Object.keys(record).length === 3 && Number.isSafeInteger(record.pid) && Number(record.pid) > 0 && typeof record.host === "string" && record.host.trim().length > 0 && typeof record.nonce === "string" && record.nonce.trim().length > 0;
|
|
157
|
-
}
|
|
158
|
-
function invalidOwner(lockPath, cause) {
|
|
159
|
-
const error = new AtomicFileLockError(`atomic file lock has an invalid owner (${lockPath}); refusing unsafe recovery`);
|
|
160
|
-
if (cause !== void 0) Object.defineProperty(error, "cause", { value: cause });
|
|
161
|
-
return error;
|
|
162
|
-
}
|
|
163
|
-
function releaseOwnedPath(lockPath, owner) {
|
|
164
|
-
const current = readOwner(lockPath);
|
|
165
|
-
if (!current) return;
|
|
166
|
-
if (canonicalOwner(current) !== canonicalOwner(owner)) throw new AtomicFileLockError(`atomic file lock owner changed before release (${lockPath})`);
|
|
167
|
-
try {
|
|
168
|
-
unlinkSync(lockPath);
|
|
169
|
-
} catch (error) {
|
|
170
|
-
if (!isMissing(error)) throw error;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
function canonicalOwner(owner) {
|
|
174
|
-
return JSON.stringify({
|
|
175
|
-
host: owner.host,
|
|
176
|
-
nonce: owner.nonce,
|
|
177
|
-
pid: owner.pid
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
function recoveryPath(lockPath) {
|
|
181
|
-
return `${lockPath}.reclaim`;
|
|
182
|
-
}
|
|
183
|
-
function isAlreadyExists(error) {
|
|
184
|
-
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
185
|
-
}
|
|
186
|
-
function isMissing(error) {
|
|
187
|
-
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
188
|
-
}
|
|
189
|
-
function isNoSuchProcess(error) {
|
|
190
|
-
return error instanceof Error && "code" in error && error.code === "ESRCH";
|
|
191
|
-
}
|
|
192
|
-
function unlinkIfExists(path) {
|
|
193
|
-
try {
|
|
194
|
-
unlinkSync(path);
|
|
195
|
-
} catch (error) {
|
|
196
|
-
if (!isMissing(error)) throw error;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
//#endregion
|
|
200
|
-
//#region src/ledger-core/journal-file.ts
|
|
201
|
-
/** Filesystem durability and cross-process exclusion for append-only journal files. */
|
|
202
|
-
/** Append one already-serialized line; the write is fsynced (file and directory)
|
|
203
|
-
* before returning so an acknowledged append survives a crash. */
|
|
204
|
-
function appendLedgerLine(path, line, context) {
|
|
205
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
206
|
-
const fd = openSync(path, constants.O_CREAT | constants.O_WRONLY | constants.O_APPEND, 384);
|
|
207
|
-
try {
|
|
208
|
-
writeAll(fd, Buffer.from(line, "utf8"), context);
|
|
209
|
-
fsyncSync(fd);
|
|
210
|
-
} finally {
|
|
211
|
-
closeSync(fd);
|
|
212
|
-
}
|
|
213
|
-
fsyncDirectory(dirname(path));
|
|
214
|
-
}
|
|
215
|
-
function withLedgerFileLock(path, context, run) {
|
|
216
|
-
const result = tryWithLedgerFileLock(path, context, run);
|
|
217
|
-
if (!result.acquired) throw context.integrityError(`${context.subject} lock is held (${path})`);
|
|
218
|
-
return result.value;
|
|
219
|
-
}
|
|
220
|
-
function tryWithLedgerFileLock(path, context, run) {
|
|
221
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
222
|
-
const lockPath = `${path}.lock`;
|
|
223
|
-
try {
|
|
224
|
-
const acquisition = tryAcquireAtomicFileLock({ lockPath });
|
|
225
|
-
if (!acquisition.acquired) return { acquired: false };
|
|
226
|
-
try {
|
|
227
|
-
return {
|
|
228
|
-
acquired: true,
|
|
229
|
-
value: run()
|
|
230
|
-
};
|
|
231
|
-
} finally {
|
|
232
|
-
acquisition.lock.release();
|
|
233
|
-
}
|
|
234
|
-
} catch (error) {
|
|
235
|
-
if (error instanceof AtomicFileLockError) throw context.integrityError(error.message, { cause: error });
|
|
236
|
-
throw error;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
function writeAll(fd, bytes, context) {
|
|
240
|
-
let offset = 0;
|
|
241
|
-
while (offset < bytes.byteLength) {
|
|
242
|
-
const written = writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
243
|
-
if (written <= 0) throw context.integrityError("filesystem wrote zero bytes");
|
|
244
|
-
offset += written;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
function fsyncDirectory(path) {
|
|
248
|
-
const fd = openSync(path, constants.O_RDONLY);
|
|
249
|
-
try {
|
|
250
|
-
fsyncSync(fd);
|
|
251
|
-
} finally {
|
|
252
|
-
closeSync(fd);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
//#endregion
|
|
256
|
-
//#region src/ledger-core/journal.ts
|
|
257
|
-
/**
|
|
258
|
-
* Generic durable append-only journal with a SHA-256 hash chain.
|
|
259
|
-
*
|
|
260
|
-
* The file format is canonical JSONL: every row is the canonical JSON encoding
|
|
261
|
-
* (object keys sorted recursively) of one entry, and every entry hashes the
|
|
262
|
-
* previous entry's hash so the log is tamper-evident. Appends are serialized
|
|
263
|
-
* in-process per path and across processes via a lock file, fsynced before
|
|
264
|
-
* acknowledgement, and idempotent by `eventId`. A malformed, non-canonical,
|
|
265
|
-
* truncated, reordered, or conflicting log fails loudly; a bad row is never
|
|
266
|
-
* skipped.
|
|
267
|
-
*
|
|
268
|
-
* Domain vocabulary lives entirely in the consumer's codec: entry schema
|
|
269
|
-
* validation, the constant header fields stamped into every entry, the error
|
|
270
|
-
* taxonomy, and the state machine replayed over verified entries.
|
|
271
|
-
*/
|
|
272
|
-
/** Canonical JSON encoding: object keys sorted recursively. This is the byte
|
|
273
|
-
* form that is hashed and stored, so identical values always encode identically. */
|
|
274
|
-
function canonicalString(value) {
|
|
275
|
-
return JSON.stringify(canonicalize(value));
|
|
276
|
-
}
|
|
277
|
-
function hashCanonical(value) {
|
|
278
|
-
return `sha256:${createHash("sha256").update(canonicalString(value)).digest("hex")}`;
|
|
279
|
-
}
|
|
280
|
-
const journalMutexes = /* @__PURE__ */ new Map();
|
|
281
|
-
function mutexFor(path) {
|
|
282
|
-
const existing = journalMutexes.get(path);
|
|
283
|
-
if (existing) return existing;
|
|
284
|
-
const mutex = new Mutex();
|
|
285
|
-
journalMutexes.set(path, mutex);
|
|
286
|
-
return mutex;
|
|
287
|
-
}
|
|
288
|
-
/** Durable filesystem journal. Construction performs no I/O; `append` and
|
|
289
|
-
* `replay` validate the complete existing file under the locks. */
|
|
290
|
-
var FileLedgerJournal = class {
|
|
291
|
-
path;
|
|
292
|
-
codec;
|
|
293
|
-
mutex;
|
|
294
|
-
constructor(path, codec) {
|
|
295
|
-
this.path = resolve(path);
|
|
296
|
-
this.codec = codec;
|
|
297
|
-
this.mutex = mutexFor(this.path);
|
|
298
|
-
}
|
|
299
|
-
async replay() {
|
|
300
|
-
return this.mutex.runExclusive(() => withLedgerFileLock(this.path, this.codec, () => this.replayLocked()));
|
|
301
|
-
}
|
|
302
|
-
async append(event) {
|
|
303
|
-
return this.mutex.runExclusive(() => withLedgerFileLock(this.path, this.codec, () => {
|
|
304
|
-
const before = this.replayLocked();
|
|
305
|
-
const existing = before.entries.find((entry) => entry.event.eventId === event.eventId);
|
|
306
|
-
if (existing) {
|
|
307
|
-
if (canonicalString(existing.event) !== canonicalString(event)) throw this.codec.conflictError(`eventId ${event.eventId} already exists with different content`);
|
|
308
|
-
return {
|
|
309
|
-
entry: existing,
|
|
310
|
-
appended: false,
|
|
311
|
-
projection: before.projection
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
const material = {
|
|
315
|
-
...this.codec.header,
|
|
316
|
-
sequence: before.entries.length,
|
|
317
|
-
previousHash: before.entries.at(-1)?.entryHash ?? null,
|
|
318
|
-
event
|
|
319
|
-
};
|
|
320
|
-
const entry = {
|
|
321
|
-
...material,
|
|
322
|
-
entryHash: hashCanonical(material)
|
|
323
|
-
};
|
|
324
|
-
const projection = this.project([...before.entries, entry]);
|
|
325
|
-
appendLedgerLine(this.path, `${canonicalString(entry)}\n`, this.codec);
|
|
326
|
-
return {
|
|
327
|
-
entry,
|
|
328
|
-
appended: true,
|
|
329
|
-
projection
|
|
330
|
-
};
|
|
331
|
-
}));
|
|
332
|
-
}
|
|
333
|
-
replayLocked() {
|
|
334
|
-
const entries = this.readEntries();
|
|
335
|
-
return {
|
|
336
|
-
entries,
|
|
337
|
-
projection: this.project(entries)
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
readEntries() {
|
|
341
|
-
if (!existsSync(this.path)) return [];
|
|
342
|
-
const text = readFileSync(this.path, "utf8");
|
|
343
|
-
if (text.length === 0) return [];
|
|
344
|
-
if (!text.endsWith("\n")) throw this.codec.integrityError(`${this.codec.subject} ${this.path} has a truncated final record (missing newline)`);
|
|
345
|
-
const lines = text.slice(0, -1).split("\n");
|
|
346
|
-
const entries = [];
|
|
347
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
348
|
-
const line = lines[index];
|
|
349
|
-
if (line.length === 0) throw this.codec.integrityError(`${this.codec.subject} ${this.path} has a blank row at line ${index + 1}`);
|
|
350
|
-
let raw;
|
|
351
|
-
try {
|
|
352
|
-
raw = JSON.parse(line);
|
|
353
|
-
} catch (error) {
|
|
354
|
-
throw this.codec.integrityError(`${this.codec.subject} ${this.path} has invalid JSON at line ${index + 1}`, { cause: error });
|
|
355
|
-
}
|
|
356
|
-
const entry = this.codec.parseEntry(raw, {
|
|
357
|
-
path: this.path,
|
|
358
|
-
line: index + 1
|
|
359
|
-
});
|
|
360
|
-
if (line !== canonicalString(entry)) throw this.codec.integrityError(`${this.codec.subject} ${this.path} has non-canonical bytes at line ${index + 1}`);
|
|
361
|
-
entries.push(entry);
|
|
362
|
-
}
|
|
363
|
-
return entries;
|
|
364
|
-
}
|
|
365
|
-
project(entries) {
|
|
366
|
-
const projector = this.codec.createProjector();
|
|
367
|
-
const eventIds = /* @__PURE__ */ new Set();
|
|
368
|
-
let expectedPrevious = null;
|
|
369
|
-
for (let index = 0; index < entries.length; index += 1) {
|
|
370
|
-
const entry = entries[index];
|
|
371
|
-
this.codec.checkEntryHeader(entry, index);
|
|
372
|
-
if (entry.sequence !== index) throw this.codec.integrityError(`entry ${entry.event.eventId} has sequence ${entry.sequence}, expected ${index}`);
|
|
373
|
-
if (entry.previousHash !== expectedPrevious) throw this.codec.integrityError(`entry ${entry.event.eventId} does not extend the previous hash`);
|
|
374
|
-
const { entryHash: _entryHash, ...material } = entry;
|
|
375
|
-
const expectedHash = hashCanonical(material);
|
|
376
|
-
if (entry.entryHash !== expectedHash) throw this.codec.integrityError(`entry ${entry.event.eventId} hash mismatch: expected ${expectedHash}, got ${entry.entryHash}`);
|
|
377
|
-
expectedPrevious = entry.entryHash;
|
|
378
|
-
if (eventIds.has(entry.event.eventId)) throw this.codec.integrityError(`duplicate eventId ${entry.event.eventId} in durable ledger`);
|
|
379
|
-
eventIds.add(entry.event.eventId);
|
|
380
|
-
projector.apply(entry, index);
|
|
381
|
-
}
|
|
382
|
-
return projector.finish(entries);
|
|
383
|
-
}
|
|
384
|
-
};
|
|
385
|
-
//#endregion
|
|
386
|
-
export { tryWithLedgerFileLock as a, probeAtomicFileLock as c, appendLedgerLine as i, tryAcquireAtomicFileLock as l, canonicalString as n, withLedgerFileLock as o, hashCanonical as r, AtomicFileLockError as s, FileLedgerJournal as t };
|
|
387
|
-
|
|
388
|
-
//# sourceMappingURL=ledger-core-eqaI3PCD.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ledger-core-eqaI3PCD.js","names":[],"sources":["../src/ledger-core/atomic-file-lock.ts","../src/ledger-core/journal-file.ts","../src/ledger-core/journal.ts"],"sourcesContent":["/** Crash-safe filesystem lock shared by campaign persistence and run exclusion. */\n\nimport { randomUUID } from 'node:crypto'\nimport {\n closeSync,\n constants,\n existsSync,\n fsyncSync,\n linkSync,\n openSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs'\nimport { hostname } from 'node:os'\n\nexport interface AtomicFileLockOwner {\n readonly pid: number\n readonly host: string\n readonly nonce: string\n}\n\nexport interface AtomicFileLock {\n readonly owner: AtomicFileLockOwner\n release(): void\n}\n\nexport type AtomicFileLockUnavailable =\n | { readonly acquired: false; readonly reason: 'held'; readonly holder: AtomicFileLockOwner }\n | { readonly acquired: false; readonly reason: 'recovery' }\n\nexport type AtomicFileLockAcquisition =\n | { readonly acquired: true; readonly lock: AtomicFileLock }\n | AtomicFileLockUnavailable\n\nexport interface AtomicFileLockOptions {\n readonly lockPath: string\n readonly pid?: number\n}\n\nexport class AtomicFileLockError extends Error {\n override readonly name = 'AtomicFileLockError'\n}\n\ntype OwnerState =\n | { readonly state: 'missing' }\n | { readonly state: 'stale'; readonly owner: AtomicFileLockOwner }\n | { readonly state: 'held'; readonly owner: AtomicFileLockOwner }\n\n/** Report whether another lock path prevents acquisition without modifying it. */\nexport function probeAtomicFileLock(\n options: AtomicFileLockOptions,\n): AtomicFileLockUnavailable | null {\n if (existsSync(recoveryPath(options.lockPath))) return { acquired: false, reason: 'recovery' }\n const state = ownerState(options.lockPath)\n if (state.state === 'held') {\n return { acquired: false, reason: 'held', holder: state.owner }\n }\n return null\n}\n\n/**\n * Try to acquire a complete, uniquely-owned lock inode.\n *\n * A hard link publishes fully-written owner metadata atomically. Stale-owner\n * removal is serialized so one reclaimer cannot delete a new owner's lock.\n */\nexport function tryAcquireAtomicFileLock(\n options: AtomicFileLockOptions,\n): AtomicFileLockAcquisition {\n const pid = options.pid ?? process.pid\n if (!Number.isSafeInteger(pid) || pid <= 0) {\n throw new AtomicFileLockError('atomic file lock pid must be a positive integer')\n }\n const owner: AtomicFileLockOwner = { pid, host: hostname(), nonce: randomUUID() }\n for (let attempt = 0; attempt < 8; attempt += 1) {\n if (existsSync(recoveryPath(options.lockPath))) {\n return { acquired: false, reason: 'recovery' }\n }\n if (tryLinkOwner(options.lockPath, owner, `acquire.${attempt}`)) {\n return acquired(options.lockPath, owner)\n }\n\n const holder = ownerState(options.lockPath)\n if (holder.state === 'missing') continue\n if (holder.state === 'held') {\n return { acquired: false, reason: 'held', holder: holder.owner }\n }\n\n const reclaimPath = recoveryPath(options.lockPath)\n if (!tryLinkOwner(reclaimPath, owner, `reclaim.${attempt}`)) {\n return { acquired: false, reason: 'recovery' }\n }\n try {\n const current = ownerState(options.lockPath)\n if (current.state === 'held') {\n return { acquired: false, reason: 'held', holder: current.owner }\n }\n if (current.state === 'stale') {\n const tombstone = `${options.lockPath}.stale.${owner.nonce}.${attempt}`\n try {\n renameSync(options.lockPath, tombstone)\n unlinkSync(tombstone)\n } catch (error) {\n if (!isMissing(error)) throw error\n }\n }\n if (tryLinkOwner(options.lockPath, owner, `recovered.${attempt}`)) {\n return acquired(options.lockPath, owner)\n }\n } finally {\n releaseOwnedPath(reclaimPath, owner)\n }\n }\n\n throw new AtomicFileLockError(`could not acquire atomic file lock ${options.lockPath}`)\n}\n\nfunction acquired(lockPath: string, owner: AtomicFileLockOwner): AtomicFileLockAcquisition {\n return {\n acquired: true,\n lock: {\n owner,\n release: () => releaseOwnedPath(lockPath, owner),\n },\n }\n}\n\nfunction tryLinkOwner(lockPath: string, owner: AtomicFileLockOwner, suffix: string): boolean {\n const ownerPath = `${lockPath}.${owner.pid}.${owner.nonce}.${suffix}.owner`\n const descriptor = openSync(\n ownerPath,\n constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY,\n 0o600,\n )\n try {\n writeFileSync(descriptor, `${canonicalOwner(owner)}\\n`, 'utf8')\n fsyncSync(descriptor)\n } catch (error) {\n closeSync(descriptor)\n unlinkIfExists(ownerPath)\n throw error\n }\n closeSync(descriptor)\n\n try {\n linkSync(ownerPath, lockPath)\n return true\n } catch (error) {\n if (!isAlreadyExists(error)) throw error\n return false\n } finally {\n unlinkIfExists(ownerPath)\n }\n}\n\nfunction ownerState(lockPath: string): OwnerState {\n const owner = readOwner(lockPath)\n if (!owner) return { state: 'missing' }\n if (owner.host !== hostname()) return { state: 'held', owner }\n try {\n process.kill(owner.pid, 0)\n return { state: 'held', owner }\n } catch (error) {\n if (isNoSuchProcess(error)) return { state: 'stale', owner }\n return { state: 'held', owner }\n }\n}\n\nfunction readOwner(lockPath: string): AtomicFileLockOwner | undefined {\n let contents: string\n try {\n contents = readFileSync(lockPath, 'utf8').trim()\n } catch (error) {\n if (isMissing(error)) return undefined\n throw error\n }\n\n let value: unknown\n try {\n value = JSON.parse(contents)\n } catch (error) {\n throw invalidOwner(lockPath, error)\n }\n if (!isOwner(value)) throw invalidOwner(lockPath)\n return value\n}\n\nfunction isOwner(value: unknown): value is AtomicFileLockOwner {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false\n const record = value as Record<string, unknown>\n return (\n Object.keys(record).length === 3 &&\n Number.isSafeInteger(record.pid) &&\n Number(record.pid) > 0 &&\n typeof record.host === 'string' &&\n record.host.trim().length > 0 &&\n typeof record.nonce === 'string' &&\n record.nonce.trim().length > 0\n )\n}\n\nfunction invalidOwner(lockPath: string, cause?: unknown): AtomicFileLockError {\n const error = new AtomicFileLockError(\n `atomic file lock has an invalid owner (${lockPath}); refusing unsafe recovery`,\n )\n if (cause !== undefined) Object.defineProperty(error, 'cause', { value: cause })\n return error\n}\n\nfunction releaseOwnedPath(lockPath: string, owner: AtomicFileLockOwner): void {\n const current = readOwner(lockPath)\n if (!current) return\n if (canonicalOwner(current) !== canonicalOwner(owner)) {\n throw new AtomicFileLockError(`atomic file lock owner changed before release (${lockPath})`)\n }\n try {\n unlinkSync(lockPath)\n } catch (error) {\n if (!isMissing(error)) throw error\n }\n}\n\nfunction canonicalOwner(owner: AtomicFileLockOwner): string {\n return JSON.stringify({ host: owner.host, nonce: owner.nonce, pid: owner.pid })\n}\n\nfunction recoveryPath(lockPath: string): string {\n return `${lockPath}.reclaim`\n}\n\nfunction isAlreadyExists(error: unknown): boolean {\n return error instanceof Error && 'code' in error && error.code === 'EEXIST'\n}\n\nfunction isMissing(error: unknown): boolean {\n return error instanceof Error && 'code' in error && error.code === 'ENOENT'\n}\n\nfunction isNoSuchProcess(error: unknown): boolean {\n return error instanceof Error && 'code' in error && error.code === 'ESRCH'\n}\n\nfunction unlinkIfExists(path: string): void {\n try {\n unlinkSync(path)\n } catch (error) {\n if (!isMissing(error)) throw error\n }\n}\n","/** Filesystem durability and cross-process exclusion for append-only journal files. */\n\nimport { closeSync, constants, fsyncSync, mkdirSync, openSync, writeSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { AtomicFileLockError, tryAcquireAtomicFileLock } from './atomic-file-lock'\n\n/** How a journal binding reports file-layer faults in its own error taxonomy. */\nexport interface LedgerFileContext {\n /** Noun used in error messages, e.g. 'search ledger'. */\n subject: string\n integrityError(message: string, options?: { cause?: unknown }): Error\n}\n\n/** Append one already-serialized line; the write is fsynced (file and directory)\n * before returning so an acknowledged append survives a crash. */\nexport function appendLedgerLine(path: string, line: string, context: LedgerFileContext): void {\n mkdirSync(dirname(path), { recursive: true })\n const fd = openSync(path, constants.O_CREAT | constants.O_WRONLY | constants.O_APPEND, 0o600)\n try {\n writeAll(fd, Buffer.from(line, 'utf8'), context)\n fsyncSync(fd)\n } finally {\n closeSync(fd)\n }\n fsyncDirectory(dirname(path))\n}\n\nexport function withLedgerFileLock<T>(path: string, context: LedgerFileContext, run: () => T): T {\n const result = tryWithLedgerFileLock(path, context, run)\n if (!result.acquired) {\n throw context.integrityError(`${context.subject} lock is held (${path})`)\n }\n return result.value\n}\n\nexport type FileLockResult<T> = { acquired: true; value: T } | { acquired: false }\n\nexport function tryWithLedgerFileLock<T>(\n path: string,\n context: LedgerFileContext,\n run: () => T,\n): FileLockResult<T> {\n mkdirSync(dirname(path), { recursive: true })\n const lockPath = `${path}.lock`\n try {\n const acquisition = tryAcquireAtomicFileLock({ lockPath })\n if (!acquisition.acquired) return { acquired: false }\n try {\n return { acquired: true, value: run() }\n } finally {\n acquisition.lock.release()\n }\n } catch (error) {\n if (error instanceof AtomicFileLockError) {\n throw context.integrityError(error.message, { cause: error })\n }\n throw error\n }\n}\n\nfunction writeAll(fd: number, bytes: Buffer, context: LedgerFileContext): void {\n let offset = 0\n while (offset < bytes.byteLength) {\n const written = writeSync(fd, bytes, offset, bytes.byteLength - offset)\n if (written <= 0) throw context.integrityError('filesystem wrote zero bytes')\n offset += written\n }\n}\n\nfunction fsyncDirectory(path: string): void {\n const fd = openSync(path, constants.O_RDONLY)\n try {\n fsyncSync(fd)\n } finally {\n closeSync(fd)\n }\n}\n","/**\n * Generic durable append-only journal with a SHA-256 hash chain.\n *\n * The file format is canonical JSONL: every row is the canonical JSON encoding\n * (object keys sorted recursively) of one entry, and every entry hashes the\n * previous entry's hash so the log is tamper-evident. Appends are serialized\n * in-process per path and across processes via a lock file, fsynced before\n * acknowledgement, and idempotent by `eventId`. A malformed, non-canonical,\n * truncated, reordered, or conflicting log fails loudly; a bad row is never\n * skipped.\n *\n * Domain vocabulary lives entirely in the consumer's codec: entry schema\n * validation, the constant header fields stamped into every entry, the error\n * taxonomy, and the state machine replayed over verified entries.\n */\n\nimport { createHash } from 'node:crypto'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { resolve } from 'node:path'\nimport { Mutex } from '../concurrency'\nimport { canonicalize } from '../pre-registration'\nimport { appendLedgerLine, type LedgerFileContext, withLedgerFileLock } from './journal-file'\n\nexport type LedgerHash = `sha256:${string}`\n\n/** Canonical JSON encoding: object keys sorted recursively. This is the byte\n * form that is hashed and stored, so identical values always encode identically. */\nexport function canonicalString(value: unknown): string {\n return JSON.stringify(canonicalize(value))\n}\n\nexport function hashCanonical(value: unknown): LedgerHash {\n return `sha256:${createHash('sha256').update(canonicalString(value)).digest('hex')}`\n}\n\n/** Minimum shape of a journal event: the idempotency key. */\nexport interface LedgerEventBase {\n eventId: string\n}\n\n/** Chain envelope every entry carries in addition to the journal's constant\n * header fields. `entryHash` covers the header, sequence, previous hash, and\n * event, so any rewrite of history invalidates every later entry. */\nexport interface LedgerChainFields<Event extends LedgerEventBase> {\n sequence: number\n previousHash: LedgerHash | null\n event: Event\n entryHash: LedgerHash\n}\n\nexport type LedgerEntryOf<Header extends object, Event extends LedgerEventBase> = Header &\n LedgerChainFields<Event>\n\nexport interface LedgerLineContext {\n path: string\n line: number\n}\n\n/** Domain state machine replayed over chain-verified entries in order.\n * `apply` throws (via the codec's error taxonomy) on an invalid transition;\n * `finish` builds the consumer's projection once every entry was applied. */\nexport interface LedgerProjector<Entry, Projection> {\n apply(entry: Entry, index: number): void\n finish(entries: Entry[]): Projection\n}\n\nexport interface LedgerJournalCodec<\n Header extends object,\n Event extends LedgerEventBase,\n Projection,\n> extends LedgerFileContext {\n /** Constant fields stamped into every entry and covered by its hash. */\n header: Header\n conflictError(message: string): Error\n /** Validate one parsed JSON row into a typed entry. Must reject malformed\n * rows and non-canonical event encodings with the binding's own errors;\n * the journal itself verifies the stored bytes and the hash chain. */\n parseEntry(raw: unknown, context: LedgerLineContext): LedgerEntryOf<Header, Event>\n /** Reject an entry whose constant header fields do not match this journal. */\n checkEntryHeader(entry: LedgerEntryOf<Header, Event>, index: number): void\n createProjector(): LedgerProjector<LedgerEntryOf<Header, Event>, Projection>\n}\n\nexport interface LedgerReplayResult<Entry, Projection> {\n entries: Entry[]\n projection: Projection\n}\n\nexport interface LedgerAppendResult<Entry, Projection> {\n entry: Entry\n /** False when the exact event was already durably present. */\n appended: boolean\n projection: Projection\n}\n\n// One async mutex per resolved journal path so concurrent appends from a single\n// process queue up instead of failing on the held cross-process lock file.\nconst journalMutexes = new Map<string, Mutex>()\n\nfunction mutexFor(path: string): Mutex {\n const existing = journalMutexes.get(path)\n if (existing) return existing\n const mutex = new Mutex()\n journalMutexes.set(path, mutex)\n return mutex\n}\n\n/** Durable filesystem journal. Construction performs no I/O; `append` and\n * `replay` validate the complete existing file under the locks. */\nexport class FileLedgerJournal<Header extends object, Event extends LedgerEventBase, Projection> {\n readonly path: string\n private readonly codec: LedgerJournalCodec<Header, Event, Projection>\n private readonly mutex: Mutex\n\n constructor(path: string, codec: LedgerJournalCodec<Header, Event, Projection>) {\n this.path = resolve(path)\n this.codec = codec\n this.mutex = mutexFor(this.path)\n }\n\n async replay(): Promise<LedgerReplayResult<LedgerEntryOf<Header, Event>, Projection>> {\n return this.mutex.runExclusive(() =>\n withLedgerFileLock(this.path, this.codec, () => this.replayLocked()),\n )\n }\n\n async append(\n event: Event,\n ): Promise<LedgerAppendResult<LedgerEntryOf<Header, Event>, Projection>> {\n return this.mutex.runExclusive(() =>\n withLedgerFileLock(this.path, this.codec, () => {\n const before = this.replayLocked()\n const existing = before.entries.find((entry) => entry.event.eventId === event.eventId)\n if (existing) {\n if (canonicalString(existing.event) !== canonicalString(event)) {\n throw this.codec.conflictError(\n `eventId ${event.eventId} already exists with different content`,\n )\n }\n return { entry: existing, appended: false, projection: before.projection }\n }\n\n const material = {\n ...this.codec.header,\n sequence: before.entries.length,\n previousHash: before.entries.at(-1)?.entryHash ?? null,\n event,\n }\n const entry = { ...material, entryHash: hashCanonical(material) }\n\n // Apply the state transition before spending an append: an entry the\n // projector rejects must never reach the durable file.\n const projection = this.project([...before.entries, entry])\n appendLedgerLine(this.path, `${canonicalString(entry)}\\n`, this.codec)\n return { entry, appended: true, projection }\n }),\n )\n }\n\n private replayLocked(): LedgerReplayResult<LedgerEntryOf<Header, Event>, Projection> {\n const entries = this.readEntries()\n return { entries, projection: this.project(entries) }\n }\n\n private readEntries(): LedgerEntryOf<Header, Event>[] {\n if (!existsSync(this.path)) return []\n const text = readFileSync(this.path, 'utf8')\n if (text.length === 0) return []\n if (!text.endsWith('\\n')) {\n throw this.codec.integrityError(\n `${this.codec.subject} ${this.path} has a truncated final record (missing newline)`,\n )\n }\n\n const lines = text.slice(0, -1).split('\\n')\n const entries: LedgerEntryOf<Header, Event>[] = []\n for (let index = 0; index < lines.length; index += 1) {\n const line = lines[index]!\n if (line.length === 0) {\n throw this.codec.integrityError(\n `${this.codec.subject} ${this.path} has a blank row at line ${index + 1}`,\n )\n }\n let raw: unknown\n try {\n raw = JSON.parse(line)\n } catch (error) {\n throw this.codec.integrityError(\n `${this.codec.subject} ${this.path} has invalid JSON at line ${index + 1}`,\n { cause: error },\n )\n }\n const entry = this.codec.parseEntry(raw, { path: this.path, line: index + 1 })\n if (line !== canonicalString(entry)) {\n throw this.codec.integrityError(\n `${this.codec.subject} ${this.path} has non-canonical bytes at line ${index + 1}`,\n )\n }\n entries.push(entry)\n }\n return entries\n }\n\n private project(entries: LedgerEntryOf<Header, Event>[]): Projection {\n const projector = this.codec.createProjector()\n const eventIds = new Set<string>()\n let expectedPrevious: LedgerHash | null = null\n for (let index = 0; index < entries.length; index += 1) {\n const entry = entries[index]!\n this.codec.checkEntryHeader(entry, index)\n if (entry.sequence !== index) {\n throw this.codec.integrityError(\n `entry ${entry.event.eventId} has sequence ${entry.sequence}, expected ${index}`,\n )\n }\n if (entry.previousHash !== expectedPrevious) {\n throw this.codec.integrityError(\n `entry ${entry.event.eventId} does not extend the previous hash`,\n )\n }\n const { entryHash: _entryHash, ...material } = entry\n const expectedHash = hashCanonical(material)\n if (entry.entryHash !== expectedHash) {\n throw this.codec.integrityError(\n `entry ${entry.event.eventId} hash mismatch: expected ${expectedHash}, got ${entry.entryHash}`,\n )\n }\n expectedPrevious = entry.entryHash\n if (eventIds.has(entry.event.eventId)) {\n throw this.codec.integrityError(\n `duplicate eventId ${entry.event.eventId} in durable ledger`,\n )\n }\n eventIds.add(entry.event.eventId)\n projector.apply(entry, index)\n }\n return projector.finish(entries)\n }\n}\n"],"mappings":"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"}
|
package/dist/metrics-C9YY1OcL.js
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
//#region src/metrics.ts
|
|
2
|
-
/** Per-1K token pricing for exact model ids. */
|
|
3
|
-
const MODEL_PRICING = {
|
|
4
|
-
"gpt-4o": {
|
|
5
|
-
input: .0025,
|
|
6
|
-
output: .01
|
|
7
|
-
},
|
|
8
|
-
"gpt-4o-mini": {
|
|
9
|
-
input: 15e-5,
|
|
10
|
-
output: 6e-4
|
|
11
|
-
},
|
|
12
|
-
"gpt-4-turbo": {
|
|
13
|
-
input: .01,
|
|
14
|
-
output: .03
|
|
15
|
-
},
|
|
16
|
-
"claude-sonnet-4-20250514": {
|
|
17
|
-
input: .003,
|
|
18
|
-
output: .015
|
|
19
|
-
},
|
|
20
|
-
"claude-opus-4-20250514": {
|
|
21
|
-
input: .015,
|
|
22
|
-
output: .075
|
|
23
|
-
},
|
|
24
|
-
"claude-3-haiku-20240307": {
|
|
25
|
-
input: 25e-5,
|
|
26
|
-
output: .00125
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
/** Family-level pricing fallbacks (per-1K), matched against a normalized id
|
|
30
|
-
* after exact lookup misses. Ordered — first match wins. Covers the model
|
|
31
|
-
* ids actually used through the Tangle router + cli-bridge harnesses
|
|
32
|
-
* (`claude-code/sonnet`, `opencode/zai-coding-plan/glm-5.1`,
|
|
33
|
-
* `kimi-code/kimi-k2.6`, `deepseek-v4-pro`, `anthropic/claude-sonnet-4-6`, …),
|
|
34
|
-
* none of which appear in the exact table above — without this they priced
|
|
35
|
-
* to a silent $0, blanking every cost/Pareto axis downstream. */
|
|
36
|
-
const FAMILY_PRICING = [
|
|
37
|
-
[/claude.*opus/, {
|
|
38
|
-
input: .015,
|
|
39
|
-
output: .075
|
|
40
|
-
}],
|
|
41
|
-
[/claude.*haiku/, {
|
|
42
|
-
input: 8e-4,
|
|
43
|
-
output: .004
|
|
44
|
-
}],
|
|
45
|
-
[/claude.*sonnet|claude-code|claude-sonnet/, {
|
|
46
|
-
input: .003,
|
|
47
|
-
output: .015
|
|
48
|
-
}],
|
|
49
|
-
[/gpt-4o-mini/, {
|
|
50
|
-
input: 15e-5,
|
|
51
|
-
output: 6e-4
|
|
52
|
-
}],
|
|
53
|
-
[/gpt-5|gpt-4\.1|o[134]\b/, {
|
|
54
|
-
input: .00125,
|
|
55
|
-
output: .01
|
|
56
|
-
}],
|
|
57
|
-
[/gpt-4o|gpt-4/, {
|
|
58
|
-
input: .0025,
|
|
59
|
-
output: .01
|
|
60
|
-
}],
|
|
61
|
-
[/deepseek/, {
|
|
62
|
-
input: 3e-4,
|
|
63
|
-
output: .0011
|
|
64
|
-
}],
|
|
65
|
-
[/glm|zhipu|zai/, {
|
|
66
|
-
input: 6e-4,
|
|
67
|
-
output: .0022
|
|
68
|
-
}],
|
|
69
|
-
[/kimi|moonshot/, {
|
|
70
|
-
input: 6e-4,
|
|
71
|
-
output: .0025
|
|
72
|
-
}],
|
|
73
|
-
[/qwen/, {
|
|
74
|
-
input: 4e-4,
|
|
75
|
-
output: .0012
|
|
76
|
-
}],
|
|
77
|
-
[/gemini.*flash/, {
|
|
78
|
-
input: 1e-4,
|
|
79
|
-
output: 4e-4
|
|
80
|
-
}],
|
|
81
|
-
[/gemini/, {
|
|
82
|
-
input: .00125,
|
|
83
|
-
output: .005
|
|
84
|
-
}],
|
|
85
|
-
[/llama/, {
|
|
86
|
-
input: 2e-4,
|
|
87
|
-
output: 6e-4
|
|
88
|
-
}]
|
|
89
|
-
];
|
|
90
|
-
/** Normalize a model id for pricing: drop a `@snapshot` suffix, lowercase,
|
|
91
|
-
* and keep the final harness/provider-prefixed segment so family regexes
|
|
92
|
-
* match (`opencode/zai-coding-plan/glm-5.1` → `glm-5.1`). */
|
|
93
|
-
function normalizeModelId(model) {
|
|
94
|
-
return (model.split("@")[0] ?? model).trim().toLowerCase();
|
|
95
|
-
}
|
|
96
|
-
/** Resolve pricing for a model id: exact table, then family fallback.
|
|
97
|
-
* Returns null when the id matches nothing (caller decides — never a
|
|
98
|
-
* silent-zero masquerading as a real $0 cost). */
|
|
99
|
-
function resolveModelPricing(model) {
|
|
100
|
-
if (MODEL_PRICING[model]) return MODEL_PRICING[model];
|
|
101
|
-
const id = normalizeModelId(model);
|
|
102
|
-
if (MODEL_PRICING[id]) return MODEL_PRICING[id];
|
|
103
|
-
for (const [pattern, price] of FAMILY_PRICING) if (pattern.test(id)) return price;
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
/** True when `model` has known pricing (exact or family). Lets cost-aware
|
|
107
|
-
* callers distinguish a real $0 from an unpriced model. */
|
|
108
|
-
function isModelPriced(model) {
|
|
109
|
-
return resolveModelPricing(model) !== null;
|
|
110
|
-
}
|
|
111
|
-
const warnedUnpricedModels = /* @__PURE__ */ new Set();
|
|
112
|
-
/** Estimate token count from string length (chars / 4 approximation) */
|
|
113
|
-
function estimateTokens(text) {
|
|
114
|
-
return Math.ceil(text.length / 4);
|
|
115
|
-
}
|
|
116
|
-
/** Calculate cost in USD from token counts and model. Unknown models warn
|
|
117
|
-
* once (not a silent zero) and return 0 so callers that ignore pricing keep
|
|
118
|
-
* working; cost-sensitive callers should gate on {@link isModelPriced}. */
|
|
119
|
-
function estimateCost(inputTokens, outputTokens, model) {
|
|
120
|
-
const pricing = resolveModelPricing(model);
|
|
121
|
-
if (!pricing) {
|
|
122
|
-
if (!warnedUnpricedModels.has(model)) {
|
|
123
|
-
warnedUnpricedModels.add(model);
|
|
124
|
-
console.warn(`estimateCost: no pricing for model "${model}" — returning 0; add it to MODEL_PRICING/FAMILY_PRICING (cost/Pareto axes will be blank until then)`);
|
|
125
|
-
}
|
|
126
|
-
return 0;
|
|
127
|
-
}
|
|
128
|
-
return inputTokens / 1e3 * pricing.input + outputTokens / 1e3 * pricing.output;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* TokenCounter — accumulates token usage and cost across turns.
|
|
132
|
-
*/
|
|
133
|
-
var TokenCounter = class {
|
|
134
|
-
totalInput = 0;
|
|
135
|
-
totalOutput = 0;
|
|
136
|
-
totalCost = 0;
|
|
137
|
-
model;
|
|
138
|
-
constructor(model = "gpt-4o") {
|
|
139
|
-
this.model = model;
|
|
140
|
-
}
|
|
141
|
-
/** Record tokens for a turn, returns per-turn cost */
|
|
142
|
-
record(inputTokens, outputTokens) {
|
|
143
|
-
this.totalInput += inputTokens;
|
|
144
|
-
this.totalOutput += outputTokens;
|
|
145
|
-
const cost = estimateCost(inputTokens, outputTokens, this.model);
|
|
146
|
-
this.totalCost += cost;
|
|
147
|
-
return cost;
|
|
148
|
-
}
|
|
149
|
-
/** Estimate and record from raw text */
|
|
150
|
-
recordFromText(inputText, outputText) {
|
|
151
|
-
const inputTokens = estimateTokens(inputText);
|
|
152
|
-
const outputTokens = estimateTokens(outputText);
|
|
153
|
-
return {
|
|
154
|
-
inputTokens,
|
|
155
|
-
outputTokens,
|
|
156
|
-
cost: this.record(inputTokens, outputTokens)
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
getTotalInput() {
|
|
160
|
-
return this.totalInput;
|
|
161
|
-
}
|
|
162
|
-
getTotalOutput() {
|
|
163
|
-
return this.totalOutput;
|
|
164
|
-
}
|
|
165
|
-
getTotalCost() {
|
|
166
|
-
return this.totalCost;
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
/**
|
|
170
|
-
* MetricsCollector — collects per-turn metrics from the product.
|
|
171
|
-
*
|
|
172
|
-
* After each turn, queries the product's APIs to measure state changes.
|
|
173
|
-
*/
|
|
174
|
-
var MetricsCollector = class {
|
|
175
|
-
client;
|
|
176
|
-
workspaceId;
|
|
177
|
-
metrics = [];
|
|
178
|
-
constructor(client, workspaceId) {
|
|
179
|
-
this.client = client;
|
|
180
|
-
this.workspaceId = workspaceId;
|
|
181
|
-
}
|
|
182
|
-
/** Collect metrics after a turn completes */
|
|
183
|
-
async collect(turn, responseLatencyMs, responseChars, codeBlocksProduced, blocksExtracted, completionCriteriaMet, completionCriteriaTotal, qualityScore, inputTokens = 0, outputTokens = 0, estimatedCostUsd = 0) {
|
|
184
|
-
const state = await this.getState();
|
|
185
|
-
const m = {
|
|
186
|
-
turn,
|
|
187
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
188
|
-
tasks: state.tasks,
|
|
189
|
-
events: state.events,
|
|
190
|
-
proposals: state.proposals,
|
|
191
|
-
vaultFiles: state.vaultFiles.length,
|
|
192
|
-
responseLatencyMs,
|
|
193
|
-
responseChars,
|
|
194
|
-
codeBlocksProduced,
|
|
195
|
-
blocksExtracted,
|
|
196
|
-
qualityScore,
|
|
197
|
-
inputTokens,
|
|
198
|
-
outputTokens,
|
|
199
|
-
estimatedCostUsd,
|
|
200
|
-
totalCostUsd: estimatedCostUsd,
|
|
201
|
-
completionPercent: completionCriteriaTotal > 0 ? completionCriteriaMet / completionCriteriaTotal * 100 : 0
|
|
202
|
-
};
|
|
203
|
-
this.metrics.push(m);
|
|
204
|
-
return m;
|
|
205
|
-
}
|
|
206
|
-
/** Get current product state */
|
|
207
|
-
async getState() {
|
|
208
|
-
const [tasks, events, approvals, vaultFiles] = await Promise.all([
|
|
209
|
-
this.client.getTasks(this.workspaceId),
|
|
210
|
-
this.client.getEvents(this.workspaceId),
|
|
211
|
-
this.client.getApprovals(this.workspaceId),
|
|
212
|
-
this.client.getVaultTree(this.workspaceId)
|
|
213
|
-
]);
|
|
214
|
-
return {
|
|
215
|
-
tasks: tasks.length,
|
|
216
|
-
events: events.length,
|
|
217
|
-
proposals: {
|
|
218
|
-
pending: approvals.filter((a) => a.status === "pending").length,
|
|
219
|
-
approved: approvals.filter((a) => a.status === "approved").length,
|
|
220
|
-
rejected: approvals.filter((a) => a.status === "rejected").length
|
|
221
|
-
},
|
|
222
|
-
vaultFiles,
|
|
223
|
-
codeBlocks: 0,
|
|
224
|
-
generations: 0
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
/** Get all collected metrics */
|
|
228
|
-
getMetrics() {
|
|
229
|
-
return [...this.metrics];
|
|
230
|
-
}
|
|
231
|
-
/** Get convergence curve (completion% over turns) */
|
|
232
|
-
getConvergenceCurve() {
|
|
233
|
-
return this.metrics.map((m) => m.completionPercent);
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
//#endregion
|
|
237
|
-
export { estimateTokens as a, estimateCost as i, MetricsCollector as n, isModelPriced as o, TokenCounter as r, resolveModelPricing as s, MODEL_PRICING as t };
|
|
238
|
-
|
|
239
|
-
//# sourceMappingURL=metrics-C9YY1OcL.js.map
|