@voybio/ace-swarm 0.1.0 → 0.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/README.md +60 -20
- package/assets/scripts/ace-hook-dispatch.mjs +1 -1
- package/dist/ace-server-instructions.js +2 -2
- package/dist/cli.js +7 -7
- package/dist/helpers.js +1 -1
- package/dist/store/ace-packed-store.d.ts +65 -26
- package/dist/store/ace-packed-store.js +448 -261
- package/dist/store/bootstrap-store.d.ts +1 -1
- package/dist/store/bootstrap-store.js +4 -4
- package/dist/store/catalog-builder.js +3 -3
- package/dist/store/importer.d.ts +2 -2
- package/dist/store/importer.js +2 -2
- package/dist/store/materializers/hook-context-materializer.d.ts +1 -1
- package/dist/store/materializers/hook-context-materializer.js +1 -1
- package/dist/store/materializers/host-file-materializer.js +1 -1
- package/dist/store/skills-install.d.ts +1 -1
- package/dist/store/skills-install.js +3 -3
- package/dist/store/state-reader.js +7 -12
- package/dist/store/store-artifacts.js +74 -36
- package/dist/store/store-snapshot.js +3 -3
- package/dist/store/types.d.ts +4 -2
- package/dist/store/types.js +4 -2
- package/dist/tools-framework.js +2 -2
- package/dist/tui/dashboard.d.ts +1 -1
- package/dist/tui/dashboard.js +7 -2
- package/dist/tui/index.js +10 -0
- package/package.json +4 -6
|
@@ -1,258 +1,437 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AcePackedStore
|
|
2
|
+
* AcePackedStore — ACEPACK v2
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Single-file binary store. One file, no subdirectories, no companion files.
|
|
5
|
+
* Events grow in the columnar section indefinitely (Option A). compact() only
|
|
6
|
+
* reclaims dead space in the KV chunk region — it does NOT reset or archive
|
|
7
|
+
* events. Historical event replay across all past runs is always available.
|
|
6
8
|
*
|
|
7
|
-
* File
|
|
8
|
-
* [ Header 64 bytes ] magic(8) version(4) flags(4) index_offset(8) index_length(8) reserved(32)
|
|
9
|
-
* [ Chunk region ] chunk_0: [uint32 length][bytes] ... chunk_N: [uint32 length][bytes]
|
|
10
|
-
* [ Index region ] UTF-8 JSON: { key → { offset, length } }
|
|
9
|
+
* File layout:
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* ┌─────────────────────────────────────────────────────────┐
|
|
12
|
+
* │ Header (128 bytes, big-endian) │
|
|
13
|
+
* │ magic "ACEPACK\0" · version uint32 · flags uint32 │
|
|
14
|
+
* │ kv_index_offset uint64 · kv_index_length uint64 │
|
|
15
|
+
* │ kv_chunk_end uint64 │
|
|
16
|
+
* │ evt_offset uint64 · evt_length uint64 │
|
|
17
|
+
* │ evt_count uint32 · evt_base_id uint32 │
|
|
18
|
+
* │ reserved (zeros) │
|
|
19
|
+
* ├─────────────────────────────────────────────────────────┤
|
|
20
|
+
* │ KV Chunk Region (append-only, random-access) │
|
|
21
|
+
* │ knowledge/agents/{name}/{file} → instruction text │
|
|
22
|
+
* │ knowledge/skills/{name}/{file} → skill content │
|
|
23
|
+
* │ topology/{kind} → JSON array │
|
|
24
|
+
* │ state/{...} → runtime state blobs │
|
|
25
|
+
* │ meta/{...} → schema version etc. │
|
|
26
|
+
* │ Each chunk: [uint32 length BE][bytes] │
|
|
27
|
+
* ├─────────────────────────────────────────────────────────┤
|
|
28
|
+
* │ Columnar Event Section (rewritten on every commit) │
|
|
29
|
+
* │ uint32 count │
|
|
30
|
+
* │ int64[N] timestamps (epoch ms, big-endian) │
|
|
31
|
+
* │ uint8[N] kinds (EntityKind enum) │
|
|
32
|
+
* │ uint8[N] sources (ContentSource enum) │
|
|
33
|
+
* │ uint8[N] flags (0x01=deleted) │
|
|
34
|
+
* │ [pad to 4-byte alignment] │
|
|
35
|
+
* │ uint32[N] pay_offsets (relative to pool start) │
|
|
36
|
+
* │ uint32[N] pay_lengths │
|
|
37
|
+
* │ uint32 pool_length │
|
|
38
|
+
* │ bytes[M] payload pool (UTF-8 JSON: {key,payload,...})│
|
|
39
|
+
* ├─────────────────────────────────────────────────────────┤
|
|
40
|
+
* │ KV Index (JSON, rewritten on commit) │
|
|
41
|
+
* │ { "knowledge/agents/ace-ops/AGENT.md": {offset, length}, ... }
|
|
42
|
+
* └─────────────────────────────────────────────────────────┘
|
|
16
43
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
44
|
+
* Event overhead: ~19 bytes fixed + payload (vs ~150 bytes full JSON).
|
|
45
|
+
* Timestamp/kind scanning reads only the fixed-width columns, never the pool.
|
|
19
46
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
47
|
+
* compact() removes dead KV space (overwritten keys) via atomic tmp-rename.
|
|
48
|
+
* Events are preserved across compact() — they accumulate for the workspace
|
|
49
|
+
* lifetime. See HOT_COLD_EVENT_TIERING.md for a future branch that adds
|
|
50
|
+
* BGZF batch archiving for very long-lived workspaces.
|
|
51
|
+
*
|
|
52
|
+
* Backward compat: v1 files (64-byte header) are migrated on first commit().
|
|
53
|
+
* v1 events live in core/log/ KV blobs and are pulled into the columnar section.
|
|
23
54
|
*/
|
|
24
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync
|
|
25
|
-
import { open as fsOpen } from "fs/promises";
|
|
26
|
-
import { dirname } from "path";
|
|
27
|
-
import { MAGIC, STORE_VERSION, HEADER_SIZE, ContentSource, EntityKind, } from "./types.js";
|
|
28
|
-
|
|
55
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "node:fs";
|
|
56
|
+
import { open as fsOpen } from "node:fs/promises";
|
|
57
|
+
import { dirname } from "node:path";
|
|
58
|
+
import { MAGIC, STORE_VERSION, HEADER_SIZE, HEADER_SIZE_V1, ContentSource, EntityKind, } from "./types.js";
|
|
59
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
60
|
+
// Header field byte offsets (big-endian throughout)
|
|
61
|
+
const H_KV_IDX_OFF = 16; // uint64: KV index offset
|
|
62
|
+
const H_KV_IDX_LEN = 24; // uint64: KV index length
|
|
63
|
+
const H_KV_CHUNK_END = 32; // uint64: end of KV chunk region (v2 only)
|
|
64
|
+
const H_EVT_OFF = 40; // uint64: event section offset (v2 only)
|
|
65
|
+
const H_EVT_LEN = 48; // uint64: event section length (v2 only)
|
|
66
|
+
const H_EVT_CNT = 56; // uint32: event count (v2 only)
|
|
67
|
+
const H_EVT_BASE = 60; // uint32: event base id (v2 only)
|
|
29
68
|
const TEXT = new TextEncoder();
|
|
30
69
|
const DECODE = new TextDecoder();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
for (let i = 0; i < s.length; i++) {
|
|
34
|
-
h ^= s.charCodeAt(i);
|
|
35
|
-
h = Math.imul(h, 16777619) >>> 0;
|
|
36
|
-
}
|
|
37
|
-
return h >>> 0;
|
|
38
|
-
}
|
|
39
|
-
function uint32ToBytes(n) {
|
|
70
|
+
// ── Byte helpers ──────────────────────────────────────────────────────────────
|
|
71
|
+
function u32be(n) {
|
|
40
72
|
const b = new Uint8Array(4);
|
|
41
73
|
new DataView(b.buffer).setUint32(0, n, false);
|
|
42
74
|
return b;
|
|
43
75
|
}
|
|
44
|
-
function
|
|
45
|
-
return new DataView(b.buffer, b.byteOffset).getUint32(offset, false);
|
|
46
|
-
}
|
|
47
|
-
function uint64ToBytes(n) {
|
|
48
|
-
// JS numbers are safe up to 2^53 — sufficient for file offsets
|
|
76
|
+
function u64be(n) {
|
|
49
77
|
const b = new Uint8Array(8);
|
|
50
78
|
const dv = new DataView(b.buffer);
|
|
51
79
|
dv.setUint32(0, Math.floor(n / 2 ** 32), false);
|
|
52
80
|
dv.setUint32(4, n >>> 0, false);
|
|
53
81
|
return b;
|
|
54
82
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
return dv.getUint32(offset, false) * 2 ** 32 + dv.getUint32(offset + 4, false);
|
|
58
|
-
}
|
|
59
|
-
async function readExact(handle, buffer, position) {
|
|
60
|
-
let offset = 0;
|
|
61
|
-
while (offset < buffer.length) {
|
|
62
|
-
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, position + offset);
|
|
63
|
-
if (bytesRead <= 0) {
|
|
64
|
-
throw new Error("AcePackedStore: unexpected EOF while reading chunk");
|
|
65
|
-
}
|
|
66
|
-
offset += bytesRead;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
async function writeExact(handle, buffer, position) {
|
|
70
|
-
const source = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
|
|
71
|
-
let offset = 0;
|
|
72
|
-
while (offset < source.length) {
|
|
73
|
-
const { bytesWritten } = await handle.write(source, offset, source.length - offset, position + offset);
|
|
74
|
-
if (bytesWritten <= 0) {
|
|
75
|
-
throw new Error("AcePackedStore: failed to write chunk");
|
|
76
|
-
}
|
|
77
|
-
offset += bytesWritten;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
function acquireLock(lockPath) {
|
|
81
|
-
if (existsSync(lockPath)) {
|
|
82
|
-
const raw = readFileSync(lockPath, "utf8");
|
|
83
|
-
try {
|
|
84
|
-
const rec = JSON.parse(raw);
|
|
85
|
-
const age = Date.now() - rec.ts;
|
|
86
|
-
if (age < LOCK_TIMEOUT_MS) {
|
|
87
|
-
// Check if PID is alive
|
|
88
|
-
try {
|
|
89
|
-
process.kill(rec.pid, 0);
|
|
90
|
-
throw new Error(`AcePackedStore: locked by PID ${rec.pid} (${age}ms ago)`);
|
|
91
|
-
}
|
|
92
|
-
catch (e) {
|
|
93
|
-
// PID not alive — stale lock, fall through to break it
|
|
94
|
-
if (e.code !== "ESRCH")
|
|
95
|
-
throw e;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
// Unparseable lock — break it
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
writeFileSync(lockPath, JSON.stringify({ pid: process.pid, ts: Date.now() }), "utf8");
|
|
83
|
+
function readU32(b, off = 0) {
|
|
84
|
+
return new DataView(b.buffer, b.byteOffset).getUint32(off, false);
|
|
104
85
|
}
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
unlinkSync(lockPath);
|
|
109
|
-
}
|
|
110
|
-
catch { /* ignore */ }
|
|
111
|
-
}
|
|
86
|
+
function readU64(b, off = 0) {
|
|
87
|
+
const dv = new DataView(b.buffer, b.byteOffset);
|
|
88
|
+
return dv.getUint32(off, false) * 2 ** 32 + dv.getUint32(off + 4, false);
|
|
112
89
|
}
|
|
113
|
-
|
|
114
|
-
function writeHeader(buf, indexOffset, indexLength) {
|
|
115
|
-
// magic
|
|
90
|
+
function writeHeaderV2(buf, h) {
|
|
116
91
|
for (let i = 0; i < 8; i++)
|
|
117
92
|
buf[i] = MAGIC.charCodeAt(i);
|
|
118
|
-
|
|
119
|
-
buf.set(
|
|
120
|
-
|
|
121
|
-
buf.set(
|
|
122
|
-
|
|
123
|
-
buf.set(
|
|
124
|
-
|
|
125
|
-
buf.set(
|
|
126
|
-
|
|
93
|
+
buf.set(u32be(STORE_VERSION), 8);
|
|
94
|
+
buf.set(u32be(0), 12);
|
|
95
|
+
buf.set(u64be(h.kvIndexOffset), H_KV_IDX_OFF);
|
|
96
|
+
buf.set(u64be(h.kvIndexLength), H_KV_IDX_LEN);
|
|
97
|
+
buf.set(u64be(h.kvChunkEnd), H_KV_CHUNK_END);
|
|
98
|
+
buf.set(u64be(h.evtOffset), H_EVT_OFF);
|
|
99
|
+
buf.set(u64be(h.evtLength), H_EVT_LEN);
|
|
100
|
+
buf.set(u32be(h.evtCount), H_EVT_CNT);
|
|
101
|
+
buf.set(u32be(h.evtBaseId), H_EVT_BASE);
|
|
127
102
|
}
|
|
128
|
-
function
|
|
103
|
+
function readHeaderV2(buf) {
|
|
129
104
|
const magic = DECODE.decode(buf.slice(0, 8));
|
|
130
105
|
if (magic !== MAGIC)
|
|
131
|
-
throw new Error("AcePackedStore: invalid magic
|
|
106
|
+
throw new Error("AcePackedStore: invalid magic — not an ACEPACK file");
|
|
107
|
+
const version = readU32(buf, 8);
|
|
108
|
+
const isV2 = version >= 2;
|
|
132
109
|
return {
|
|
133
|
-
version
|
|
134
|
-
|
|
135
|
-
|
|
110
|
+
version,
|
|
111
|
+
kvIndexOffset: readU64(buf, H_KV_IDX_OFF),
|
|
112
|
+
kvIndexLength: readU64(buf, H_KV_IDX_LEN),
|
|
113
|
+
kvChunkEnd: isV2 ? readU64(buf, H_KV_CHUNK_END) : readU64(buf, H_KV_IDX_OFF),
|
|
114
|
+
evtOffset: isV2 ? readU64(buf, H_EVT_OFF) : 0,
|
|
115
|
+
evtLength: isV2 ? readU64(buf, H_EVT_LEN) : 0,
|
|
116
|
+
evtCount: isV2 ? readU32(buf, H_EVT_CNT) : 0,
|
|
117
|
+
evtBaseId: isV2 ? readU32(buf, H_EVT_BASE) : 0,
|
|
136
118
|
};
|
|
137
119
|
}
|
|
120
|
+
// ── Columnar event section serialiser ────────────────────────────────────────
|
|
121
|
+
function serializeEventSection(events) {
|
|
122
|
+
const N = events.length;
|
|
123
|
+
const payloadBytes = events.map(e => TEXT.encode(e.blob));
|
|
124
|
+
const poolSize = payloadBytes.reduce((s, b) => s + b.length, 0);
|
|
125
|
+
const pad = (4 - (N % 4)) % 4;
|
|
126
|
+
// Layout: [4] count + [8N] ts + [N] kind + [N] source + [N] flags + [pad]
|
|
127
|
+
// + [4N] pay_off + [4N] pay_len + [4] pool_len + [M] pool
|
|
128
|
+
const size = 4 + 8 * N + N + N + N + pad + 4 * N + 4 * N + 4 + poolSize;
|
|
129
|
+
const buf = new Uint8Array(size);
|
|
130
|
+
const dv = new DataView(buf.buffer);
|
|
131
|
+
let pos = 0;
|
|
132
|
+
dv.setUint32(pos, N, false);
|
|
133
|
+
pos += 4;
|
|
134
|
+
for (const e of events) {
|
|
135
|
+
dv.setUint32(pos, Math.floor(e.ts / 2 ** 32), false);
|
|
136
|
+
dv.setUint32(pos + 4, e.ts >>> 0, false);
|
|
137
|
+
pos += 8;
|
|
138
|
+
}
|
|
139
|
+
for (const e of events)
|
|
140
|
+
buf[pos++] = e.kind;
|
|
141
|
+
for (const e of events)
|
|
142
|
+
buf[pos++] = e.source;
|
|
143
|
+
for (const e of events)
|
|
144
|
+
buf[pos++] = e.flags;
|
|
145
|
+
pos += pad;
|
|
146
|
+
let poolOff = 0;
|
|
147
|
+
for (const pb of payloadBytes) {
|
|
148
|
+
dv.setUint32(pos, poolOff, false);
|
|
149
|
+
pos += 4;
|
|
150
|
+
poolOff += pb.length;
|
|
151
|
+
}
|
|
152
|
+
for (const pb of payloadBytes) {
|
|
153
|
+
dv.setUint32(pos, pb.length, false);
|
|
154
|
+
pos += 4;
|
|
155
|
+
}
|
|
156
|
+
dv.setUint32(pos, poolSize, false);
|
|
157
|
+
pos += 4;
|
|
158
|
+
for (const pb of payloadBytes) {
|
|
159
|
+
buf.set(pb, pos);
|
|
160
|
+
pos += pb.length;
|
|
161
|
+
}
|
|
162
|
+
return buf;
|
|
163
|
+
}
|
|
164
|
+
function deserializeEventSection(buf) {
|
|
165
|
+
if (buf.length < 4)
|
|
166
|
+
return [];
|
|
167
|
+
const dv = new DataView(buf.buffer, buf.byteOffset);
|
|
168
|
+
const N = dv.getUint32(0, false);
|
|
169
|
+
if (N === 0)
|
|
170
|
+
return [];
|
|
171
|
+
let pos = 4;
|
|
172
|
+
const ts = [];
|
|
173
|
+
for (let i = 0; i < N; i++) {
|
|
174
|
+
ts.push(dv.getUint32(pos, false) * 2 ** 32 + dv.getUint32(pos + 4, false));
|
|
175
|
+
pos += 8;
|
|
176
|
+
}
|
|
177
|
+
const kinds = [];
|
|
178
|
+
for (let i = 0; i < N; i++)
|
|
179
|
+
kinds.push(buf[pos++]);
|
|
180
|
+
const sources = [];
|
|
181
|
+
for (let i = 0; i < N; i++)
|
|
182
|
+
sources.push(buf[pos++]);
|
|
183
|
+
const flags = [];
|
|
184
|
+
for (let i = 0; i < N; i++)
|
|
185
|
+
flags.push(buf[pos++]);
|
|
186
|
+
pos += (4 - (N % 4)) % 4; // skip pad
|
|
187
|
+
const payOffs = [];
|
|
188
|
+
for (let i = 0; i < N; i++) {
|
|
189
|
+
payOffs.push(dv.getUint32(pos, false));
|
|
190
|
+
pos += 4;
|
|
191
|
+
}
|
|
192
|
+
const payLens = [];
|
|
193
|
+
for (let i = 0; i < N; i++) {
|
|
194
|
+
payLens.push(dv.getUint32(pos, false));
|
|
195
|
+
pos += 4;
|
|
196
|
+
}
|
|
197
|
+
pos += 4; // pool_length (implicit from payLens sum)
|
|
198
|
+
const poolStart = pos;
|
|
199
|
+
return Array.from({ length: N }, (_, i) => ({
|
|
200
|
+
ts: ts[i],
|
|
201
|
+
kind: kinds[i],
|
|
202
|
+
source: sources[i],
|
|
203
|
+
flags: flags[i],
|
|
204
|
+
blob: DECODE.decode(buf.slice(poolStart + payOffs[i], poolStart + payOffs[i] + payLens[i])),
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
// ── File I/O helpers ──────────────────────────────────────────────────────────
|
|
208
|
+
async function readExact(fh, buf, position) {
|
|
209
|
+
let off = 0;
|
|
210
|
+
while (off < buf.length) {
|
|
211
|
+
const { bytesRead } = await fh.read(buf, off, buf.length - off, position + off);
|
|
212
|
+
if (bytesRead <= 0)
|
|
213
|
+
throw new Error("AcePackedStore: unexpected EOF");
|
|
214
|
+
off += bytesRead;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function writeExact(fh, data, position) {
|
|
218
|
+
let off = 0;
|
|
219
|
+
while (off < data.length) {
|
|
220
|
+
const { bytesWritten } = await fh.write(data, off, data.length - off, position + off);
|
|
221
|
+
if (bytesWritten <= 0)
|
|
222
|
+
throw new Error("AcePackedStore: write failed");
|
|
223
|
+
off += bytesWritten;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
138
226
|
// ── AcePackedStore ────────────────────────────────────────────────────────────
|
|
139
227
|
export class AcePackedStore {
|
|
140
228
|
storePath = "";
|
|
141
|
-
lockPath = "";
|
|
142
229
|
readOnly = false;
|
|
143
|
-
index = new Map();
|
|
144
|
-
chunkEnd = HEADER_SIZE; // byte offset immediately after last chunk
|
|
145
230
|
fh = null;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
//
|
|
231
|
+
// KV region
|
|
232
|
+
kvIndex = new Map();
|
|
233
|
+
kvChunkEnd = HEADER_SIZE; // end of KV chunk region in file
|
|
234
|
+
// Event log (in-memory)
|
|
235
|
+
committed = []; // loaded from file
|
|
236
|
+
pending = []; // written since last commit
|
|
237
|
+
evtBaseId = 0; // events [0..evtBaseId] are in the archive
|
|
238
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
|
149
239
|
async open(path, opts = {}) {
|
|
150
240
|
this.storePath = path;
|
|
151
|
-
this.lockPath = path + ".lock";
|
|
152
241
|
this.readOnly = opts.readOnly ?? false;
|
|
153
|
-
if (!this.readOnly) {
|
|
154
|
-
acquireLock(this.lockPath);
|
|
155
|
-
}
|
|
156
242
|
if (!existsSync(path)) {
|
|
157
243
|
if (this.readOnly)
|
|
158
|
-
throw new Error(`AcePackedStore:
|
|
159
|
-
await this.
|
|
244
|
+
throw new Error(`AcePackedStore: file not found: ${path}`);
|
|
245
|
+
await this._initNew(path);
|
|
160
246
|
}
|
|
161
247
|
else {
|
|
162
|
-
await this.
|
|
248
|
+
await this._loadExisting(path);
|
|
163
249
|
}
|
|
164
250
|
this.fh = await fsOpen(path, this.readOnly ? "r" : "r+");
|
|
165
251
|
}
|
|
166
|
-
async
|
|
252
|
+
async _initNew(path) {
|
|
167
253
|
mkdirSync(dirname(path), { recursive: true });
|
|
254
|
+
const evtBytes = serializeEventSection([]);
|
|
255
|
+
const idxBytes = TEXT.encode("{}");
|
|
256
|
+
const evtOff = HEADER_SIZE;
|
|
257
|
+
const kvIdxOff = evtOff + evtBytes.length;
|
|
258
|
+
const totalSize = kvIdxOff + idxBytes.length;
|
|
168
259
|
const header = new Uint8Array(HEADER_SIZE);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
260
|
+
writeHeaderV2(header, {
|
|
261
|
+
kvIndexOffset: kvIdxOff,
|
|
262
|
+
kvIndexLength: idxBytes.length,
|
|
263
|
+
kvChunkEnd: HEADER_SIZE,
|
|
264
|
+
evtOffset: evtOff,
|
|
265
|
+
evtLength: evtBytes.length,
|
|
266
|
+
evtCount: 0,
|
|
267
|
+
evtBaseId: 0,
|
|
268
|
+
});
|
|
269
|
+
const out = new Uint8Array(totalSize);
|
|
270
|
+
out.set(header, 0);
|
|
271
|
+
out.set(evtBytes, evtOff);
|
|
272
|
+
out.set(idxBytes, kvIdxOff);
|
|
273
|
+
writeFileSync(path, out);
|
|
274
|
+
this.kvChunkEnd = HEADER_SIZE;
|
|
275
|
+
this.kvIndex = new Map();
|
|
276
|
+
this.committed = [];
|
|
277
|
+
this.pending = [];
|
|
278
|
+
this.evtBaseId = 0;
|
|
279
|
+
}
|
|
280
|
+
async _loadExisting(path) {
|
|
180
281
|
const file = readFileSync(path);
|
|
181
|
-
if (file.length <
|
|
282
|
+
if (file.length < HEADER_SIZE_V1)
|
|
182
283
|
throw new Error("AcePackedStore: file too short");
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
this.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
284
|
+
const hdr = readHeaderV2(file.slice(0, Math.max(HEADER_SIZE, file.length)));
|
|
285
|
+
const isV1 = hdr.version < 2;
|
|
286
|
+
// Load KV index
|
|
287
|
+
const idxRaw = file.slice(hdr.kvIndexOffset, hdr.kvIndexOffset + hdr.kvIndexLength);
|
|
288
|
+
const idxObj = JSON.parse(DECODE.decode(idxRaw));
|
|
289
|
+
this.kvIndex = new Map(Object.entries(idxObj));
|
|
290
|
+
this.kvChunkEnd = hdr.kvChunkEnd;
|
|
291
|
+
if (isV1) {
|
|
292
|
+
// v1 → v2 migration: pull events from core/log/ KV entries
|
|
293
|
+
const logKeys = [...this.kvIndex.keys()]
|
|
294
|
+
.filter(k => k.startsWith("core/log/") && k !== "core/log/__seq")
|
|
295
|
+
.sort();
|
|
296
|
+
this.committed = [];
|
|
297
|
+
for (const lk of logKeys) {
|
|
298
|
+
const entry = await this._readKvBlobDirect(file, lk);
|
|
299
|
+
if (!entry)
|
|
300
|
+
continue;
|
|
301
|
+
try {
|
|
302
|
+
const e = JSON.parse(entry);
|
|
303
|
+
this.committed.push({
|
|
304
|
+
ts: e.ts,
|
|
305
|
+
kind: e.kind,
|
|
306
|
+
source: e.content_source,
|
|
307
|
+
flags: e.flags ?? 0,
|
|
308
|
+
blob: JSON.stringify({ key: e.key, payload: e.payload, parent_id: e.parent_id }),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
catch { /* skip malformed */ }
|
|
312
|
+
}
|
|
313
|
+
// Remove core/log/ and meta/log_entry_seq from KV (they go to columnar section)
|
|
314
|
+
for (const k of [...this.kvIndex.keys()]) {
|
|
315
|
+
if (k.startsWith("core/log/") || k === "meta/log_entry_seq")
|
|
316
|
+
this.kvIndex.delete(k);
|
|
317
|
+
}
|
|
318
|
+
this.evtBaseId = 0;
|
|
319
|
+
// force commit to write v2 format (pending is non-empty from migration)
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// v2: load event section
|
|
323
|
+
if (hdr.evtLength > 0) {
|
|
324
|
+
const evtBuf = file.slice(hdr.evtOffset, hdr.evtOffset + hdr.evtLength);
|
|
325
|
+
this.committed = deserializeEventSection(evtBuf);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
this.committed = [];
|
|
329
|
+
}
|
|
330
|
+
this.evtBaseId = hdr.evtBaseId;
|
|
331
|
+
}
|
|
332
|
+
this.pending = [];
|
|
333
|
+
}
|
|
334
|
+
/** Read a KV blob directly from a loaded file buffer (used during migration). */
|
|
335
|
+
async _readKvBlobDirect(file, key) {
|
|
336
|
+
const entry = this.kvIndex.get(key);
|
|
337
|
+
if (!entry)
|
|
338
|
+
return undefined;
|
|
339
|
+
const start = entry.offset + 4;
|
|
340
|
+
const end = start + entry.length;
|
|
341
|
+
if (end > file.length)
|
|
342
|
+
return undefined;
|
|
343
|
+
return DECODE.decode(file.slice(start, end));
|
|
192
344
|
}
|
|
193
345
|
async commit() {
|
|
194
|
-
if (this.readOnly || !this.fh
|
|
346
|
+
if (this.readOnly || !this.fh)
|
|
195
347
|
return;
|
|
196
|
-
|
|
348
|
+
// Merge pending into committed
|
|
349
|
+
this.committed.push(...this.pending);
|
|
350
|
+
this.pending = [];
|
|
351
|
+
// Serialize event section
|
|
352
|
+
const evtBytes = serializeEventSection(this.committed);
|
|
353
|
+
// Write KV index after event section
|
|
354
|
+
const idxJson = JSON.stringify(Object.fromEntries(this.kvIndex));
|
|
197
355
|
const idxBytes = TEXT.encode(idxJson);
|
|
198
|
-
|
|
199
|
-
|
|
356
|
+
const evtOff = this.kvChunkEnd;
|
|
357
|
+
const kvIdxOff = evtOff + evtBytes.length;
|
|
358
|
+
await writeExact(this.fh, evtBytes, evtOff);
|
|
359
|
+
await writeExact(this.fh, idxBytes, kvIdxOff);
|
|
360
|
+
await this.fh.truncate(kvIdxOff + idxBytes.length);
|
|
200
361
|
// Update header
|
|
201
362
|
const header = new Uint8Array(HEADER_SIZE);
|
|
202
|
-
|
|
363
|
+
writeHeaderV2(header, {
|
|
364
|
+
kvIndexOffset: kvIdxOff,
|
|
365
|
+
kvIndexLength: idxBytes.length,
|
|
366
|
+
kvChunkEnd: this.kvChunkEnd,
|
|
367
|
+
evtOffset: evtOff,
|
|
368
|
+
evtLength: evtBytes.length,
|
|
369
|
+
evtCount: this.committed.length,
|
|
370
|
+
evtBaseId: this.evtBaseId,
|
|
371
|
+
});
|
|
203
372
|
await writeExact(this.fh, header, 0);
|
|
204
|
-
// Truncate any stale trailing bytes
|
|
205
|
-
await this.fh.truncate(this.chunkEnd + idxBytes.length);
|
|
206
373
|
await this.fh.datasync();
|
|
207
|
-
this.dirty = false;
|
|
208
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* Compacts the KV chunk region — removes dead space left by overwritten keys.
|
|
377
|
+
* Events are NOT reset or archived; they accumulate in the columnar section
|
|
378
|
+
* for the workspace lifetime (Option A — grow forever).
|
|
379
|
+
* See HOT_COLD_EVENT_TIERING.md for the future branch that adds BGZF archiving.
|
|
380
|
+
*/
|
|
209
381
|
async compact() {
|
|
210
|
-
if (!this.storePath)
|
|
382
|
+
if (!this.storePath || this.readOnly)
|
|
211
383
|
return;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
let writeOffset = HEADER_SIZE;
|
|
384
|
+
// Flush pending events into committed so they survive the rewrite
|
|
385
|
+
this.committed.push(...this.pending);
|
|
386
|
+
this.pending = [];
|
|
387
|
+
// Rebuild KV chunk region with only live keys (removes dead space)
|
|
217
388
|
const srcFile = readFileSync(this.storePath);
|
|
389
|
+
const liveKv = new Map();
|
|
218
390
|
const chunks = [];
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
writeOffset += 4 + length; // uint32 length prefix + data
|
|
391
|
+
let writeOff = HEADER_SIZE;
|
|
392
|
+
for (const [key, { offset, length }] of this.kvIndex) {
|
|
393
|
+
chunks.push(srcFile.slice(offset + 4, offset + 4 + length));
|
|
394
|
+
liveKv.set(key, { offset: writeOff, length });
|
|
395
|
+
writeOff += 4 + length;
|
|
225
396
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
397
|
+
this.kvIndex = liveKv;
|
|
398
|
+
this.kvChunkEnd = writeOff;
|
|
399
|
+
// Serialize events (preserved in full) + new KV index
|
|
400
|
+
const evtBytes = serializeEventSection(this.committed);
|
|
401
|
+
const idxBytes = TEXT.encode(JSON.stringify(Object.fromEntries(liveKv)));
|
|
402
|
+
const evtOff = writeOff;
|
|
403
|
+
const kvIdxOff = evtOff + evtBytes.length;
|
|
404
|
+
const totalSize = kvIdxOff + idxBytes.length;
|
|
229
405
|
const out = new Uint8Array(totalSize);
|
|
230
406
|
const header = new Uint8Array(HEADER_SIZE);
|
|
231
|
-
|
|
232
|
-
|
|
407
|
+
writeHeaderV2(header, {
|
|
408
|
+
kvIndexOffset: kvIdxOff,
|
|
409
|
+
kvIndexLength: idxBytes.length,
|
|
410
|
+
kvChunkEnd: writeOff,
|
|
411
|
+
evtOffset: evtOff,
|
|
412
|
+
evtLength: evtBytes.length,
|
|
413
|
+
evtCount: this.committed.length,
|
|
414
|
+
evtBaseId: this.evtBaseId,
|
|
415
|
+
});
|
|
416
|
+
out.set(header, 0);
|
|
233
417
|
let pos = HEADER_SIZE;
|
|
234
418
|
for (const chunk of chunks) {
|
|
235
|
-
out.set(
|
|
236
|
-
|
|
237
|
-
pos
|
|
419
|
+
out.set(u32be(chunk.length), pos);
|
|
420
|
+
pos += 4;
|
|
421
|
+
out.set(chunk, pos);
|
|
422
|
+
pos += chunk.length;
|
|
238
423
|
}
|
|
239
|
-
out.set(
|
|
424
|
+
out.set(evtBytes, evtOff);
|
|
425
|
+
out.set(idxBytes, kvIdxOff);
|
|
426
|
+
const tmpPath = this.storePath + ".tmp";
|
|
240
427
|
writeFileSync(tmpPath, out);
|
|
241
|
-
|
|
242
|
-
const tmpBuf = readFileSync(tmpPath);
|
|
243
|
-
readHeader(tmpBuf.slice(0, HEADER_SIZE)); // throws if invalid
|
|
244
|
-
// Atomic rename
|
|
428
|
+
readHeaderV2(readFileSync(tmpPath).slice(0, HEADER_SIZE)); // validate
|
|
245
429
|
if (this.fh) {
|
|
246
430
|
await this.fh.close();
|
|
247
431
|
this.fh = null;
|
|
248
432
|
}
|
|
249
433
|
renameSync(tmpPath, this.storePath);
|
|
250
|
-
releaseLock(lockPath);
|
|
251
|
-
acquireLock(lockPath);
|
|
252
434
|
this.fh = await fsOpen(this.storePath, "r+");
|
|
253
|
-
this.index = liveIndex;
|
|
254
|
-
this.chunkEnd = writeOffset;
|
|
255
|
-
this.dirty = false;
|
|
256
435
|
}
|
|
257
436
|
async close() {
|
|
258
437
|
await this.commit();
|
|
@@ -260,83 +439,95 @@ export class AcePackedStore {
|
|
|
260
439
|
await this.fh.close();
|
|
261
440
|
this.fh = null;
|
|
262
441
|
}
|
|
263
|
-
if (!this.readOnly)
|
|
264
|
-
releaseLock(this.lockPath);
|
|
265
442
|
}
|
|
266
|
-
// ──
|
|
443
|
+
// ── Core KV ─────────────────────────────────────────────────────────────────
|
|
267
444
|
async get(key) {
|
|
268
|
-
const entry = this.
|
|
269
|
-
if (!entry)
|
|
445
|
+
const entry = this.kvIndex.get(key);
|
|
446
|
+
if (!entry || !this.fh)
|
|
270
447
|
return undefined;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
await readExact(this.fh, dataBuf, entry.offset + 4);
|
|
275
|
-
return dataBuf;
|
|
448
|
+
const buf = new Uint8Array(entry.length);
|
|
449
|
+
await readExact(this.fh, buf, entry.offset + 4);
|
|
450
|
+
return buf;
|
|
276
451
|
}
|
|
277
452
|
async set(key, value) {
|
|
278
453
|
if (this.readOnly)
|
|
279
|
-
throw new Error("AcePackedStore:
|
|
454
|
+
throw new Error("AcePackedStore: read-only");
|
|
280
455
|
if (!this.fh)
|
|
281
|
-
throw new Error("AcePackedStore:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
await writeExact(this.fh,
|
|
285
|
-
|
|
286
|
-
this.
|
|
287
|
-
this.chunkEnd += 4 + value.length;
|
|
288
|
-
this.dirty = true;
|
|
456
|
+
throw new Error("AcePackedStore: not open");
|
|
457
|
+
// Append chunk to KV chunk region (at kvChunkEnd)
|
|
458
|
+
await writeExact(this.fh, u32be(value.length), this.kvChunkEnd);
|
|
459
|
+
await writeExact(this.fh, value, this.kvChunkEnd + 4);
|
|
460
|
+
this.kvIndex.set(key, { offset: this.kvChunkEnd, length: value.length });
|
|
461
|
+
this.kvChunkEnd += 4 + value.length;
|
|
289
462
|
}
|
|
290
463
|
async delete(key) {
|
|
291
|
-
const existed = this.
|
|
292
|
-
this.
|
|
293
|
-
if (existed)
|
|
294
|
-
this.dirty = true;
|
|
464
|
+
const existed = this.kvIndex.has(key);
|
|
465
|
+
this.kvIndex.delete(key);
|
|
295
466
|
return existed;
|
|
296
467
|
}
|
|
297
468
|
async *list() {
|
|
298
|
-
for (const key of this.
|
|
469
|
+
for (const key of this.kvIndex.keys())
|
|
299
470
|
yield key;
|
|
300
471
|
}
|
|
301
|
-
// ── JSON / Blob convenience
|
|
472
|
+
// ── JSON / Blob convenience ──────────────────────────────────────────────────
|
|
302
473
|
async getJSON(key) {
|
|
303
474
|
const raw = await this.get(key);
|
|
304
475
|
if (!raw)
|
|
305
476
|
return undefined;
|
|
306
|
-
|
|
477
|
+
try {
|
|
478
|
+
return JSON.parse(DECODE.decode(raw));
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
307
483
|
}
|
|
308
484
|
async setJSON(key, value) {
|
|
309
485
|
await this.set(key, TEXT.encode(JSON.stringify(value)));
|
|
310
486
|
}
|
|
311
487
|
async getBlob(key) {
|
|
312
488
|
const raw = await this.get(key);
|
|
313
|
-
|
|
314
|
-
return undefined;
|
|
315
|
-
return DECODE.decode(raw);
|
|
489
|
+
return raw ? DECODE.decode(raw) : undefined;
|
|
316
490
|
}
|
|
317
491
|
async setBlob(key, content) {
|
|
318
492
|
await this.set(key, TEXT.encode(content));
|
|
319
493
|
}
|
|
320
|
-
// ── Unified
|
|
494
|
+
// ── Unified event log ────────────────────────────────────────────────────────
|
|
321
495
|
async appendEntry(entry) {
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
496
|
+
const id = this.evtBaseId + this.committed.length + this.pending.length + 1;
|
|
497
|
+
const ts = Date.now();
|
|
498
|
+
const rec = {
|
|
499
|
+
ts,
|
|
500
|
+
kind: entry.kind,
|
|
501
|
+
source: entry.content_source,
|
|
502
|
+
flags: entry.flags ?? 0,
|
|
503
|
+
blob: JSON.stringify({ key: entry.key, payload: entry.payload, parent_id: entry.parent_id }),
|
|
504
|
+
};
|
|
505
|
+
this.pending.push(rec);
|
|
506
|
+
return { id, ts, kind: entry.kind, content_source: entry.content_source, key: entry.key, payload: entry.payload, flags: entry.flags };
|
|
331
507
|
}
|
|
332
508
|
async getEntries(filter) {
|
|
509
|
+
const all = [...this.committed, ...this.pending];
|
|
333
510
|
const results = [];
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
511
|
+
let id = this.evtBaseId + 1;
|
|
512
|
+
for (const rec of all) {
|
|
513
|
+
let parsed;
|
|
514
|
+
try {
|
|
515
|
+
parsed = JSON.parse(rec.blob);
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
id++;
|
|
339
519
|
continue;
|
|
520
|
+
}
|
|
521
|
+
const entry = {
|
|
522
|
+
id: id++,
|
|
523
|
+
ts: rec.ts,
|
|
524
|
+
kind: rec.kind,
|
|
525
|
+
content_source: rec.source,
|
|
526
|
+
key: parsed.key,
|
|
527
|
+
payload: parsed.payload,
|
|
528
|
+
parent_id: parsed.parent_id,
|
|
529
|
+
flags: rec.flags,
|
|
530
|
+
};
|
|
340
531
|
if (filter?.kind !== undefined && entry.kind !== filter.kind)
|
|
341
532
|
continue;
|
|
342
533
|
if (filter?.content_source !== undefined && entry.content_source !== filter.content_source)
|
|
@@ -349,80 +540,76 @@ export class AcePackedStore {
|
|
|
349
540
|
continue;
|
|
350
541
|
results.push(entry);
|
|
351
542
|
}
|
|
352
|
-
return results
|
|
543
|
+
return results;
|
|
353
544
|
}
|
|
354
|
-
async getEntry(
|
|
355
|
-
|
|
545
|
+
async getEntry(_key) {
|
|
546
|
+
// Events are no longer in KV — scan by id if key is a core/log/ path
|
|
547
|
+
const match = _key.match(/^core\/log\/(\d+)$/);
|
|
548
|
+
if (!match)
|
|
549
|
+
return undefined;
|
|
550
|
+
const targetId = parseInt(match[1], 10);
|
|
551
|
+
const all = await this.getEntries();
|
|
552
|
+
return all.find(e => e.id === targetId);
|
|
356
553
|
}
|
|
357
|
-
// ── Topology
|
|
554
|
+
// ── Topology ─────────────────────────────────────────────────────────────────
|
|
358
555
|
async getTopology(kind) {
|
|
359
556
|
return (await this.getJSON(`topology/${kind}`)) ?? [];
|
|
360
557
|
}
|
|
361
558
|
async setTopology(kind, entries) {
|
|
362
559
|
await this.setJSON(`topology/${kind}`, entries);
|
|
363
560
|
}
|
|
364
|
-
// ── Knowledge: agents
|
|
561
|
+
// ── Knowledge: agents ────────────────────────────────────────────────────────
|
|
365
562
|
async getAgentInstruction(agent, file) {
|
|
366
563
|
return this.getBlob(`knowledge/agents/${agent}/${file}`);
|
|
367
564
|
}
|
|
368
565
|
async setAgentInstruction(agent, file, content, source = ContentSource.Package) {
|
|
369
566
|
const key = `knowledge/agents/${agent}/${file}`;
|
|
370
567
|
await this.setBlob(key, content);
|
|
371
|
-
await this.appendEntry({
|
|
372
|
-
kind: EntityKind.AgentInstruction,
|
|
373
|
-
content_source: source,
|
|
374
|
-
key,
|
|
375
|
-
payload: { agent, file, length: content.length },
|
|
376
|
-
});
|
|
568
|
+
await this.appendEntry({ kind: EntityKind.AgentInstruction, content_source: source, key, payload: { agent, file, length: content.length } });
|
|
377
569
|
}
|
|
378
570
|
async listAgents() {
|
|
379
571
|
const agents = new Set();
|
|
380
|
-
for (const
|
|
381
|
-
if (
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
384
|
-
agents.add(
|
|
572
|
+
for (const k of this.kvIndex.keys()) {
|
|
573
|
+
if (k.startsWith("knowledge/agents/")) {
|
|
574
|
+
const p = k.split("/");
|
|
575
|
+
if (p[2])
|
|
576
|
+
agents.add(p[2]);
|
|
385
577
|
}
|
|
386
578
|
}
|
|
387
579
|
return [...agents].sort();
|
|
388
580
|
}
|
|
389
|
-
// ── Knowledge: skills
|
|
581
|
+
// ── Knowledge: skills ────────────────────────────────────────────────────────
|
|
390
582
|
async getSkillContent(skill, file) {
|
|
391
583
|
return this.getBlob(`knowledge/skills/${skill}/${file}`);
|
|
392
584
|
}
|
|
393
585
|
async setSkillContent(skill, file, content, source = ContentSource.Package) {
|
|
394
586
|
const key = `knowledge/skills/${skill}/${file}`;
|
|
395
587
|
await this.setBlob(key, content);
|
|
396
|
-
await this.appendEntry({
|
|
397
|
-
kind: EntityKind.SkillDefinition,
|
|
398
|
-
content_source: source,
|
|
399
|
-
key,
|
|
400
|
-
payload: { skill, file, length: content.length },
|
|
401
|
-
});
|
|
588
|
+
await this.appendEntry({ kind: EntityKind.SkillDefinition, content_source: source, key, payload: { skill, file, length: content.length } });
|
|
402
589
|
}
|
|
403
590
|
async listSkills() {
|
|
404
591
|
const skills = new Set();
|
|
405
|
-
for (const
|
|
406
|
-
if (
|
|
407
|
-
const
|
|
408
|
-
if (
|
|
409
|
-
skills.add(
|
|
592
|
+
for (const k of this.kvIndex.keys()) {
|
|
593
|
+
if (k.startsWith("knowledge/skills/")) {
|
|
594
|
+
const p = k.split("/");
|
|
595
|
+
if (p[2])
|
|
596
|
+
skills.add(p[2]);
|
|
410
597
|
}
|
|
411
598
|
}
|
|
412
599
|
return [...skills].sort();
|
|
413
600
|
}
|
|
414
|
-
// ──
|
|
415
|
-
/** Dead space ratio
|
|
601
|
+
// ── Introspection ─────────────────────────────────────────────────────────────
|
|
602
|
+
/** Dead space ratio in the KV chunk region. Used to decide if compaction is needed. */
|
|
416
603
|
get deadSpaceRatio() {
|
|
417
|
-
|
|
604
|
+
const totalKv = this.kvChunkEnd - HEADER_SIZE;
|
|
605
|
+
if (totalKv <= 0)
|
|
418
606
|
return 0;
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
return totalSize > 0 ? 1 - liveSize / totalSize : 0;
|
|
607
|
+
const liveKv = [...this.kvIndex.values()].reduce((s, e) => s + 4 + e.length, 0);
|
|
608
|
+
return Math.max(0, 1 - liveKv / totalKv);
|
|
422
609
|
}
|
|
423
|
-
/**
|
|
610
|
+
/** Total events in log (committed + pending). */
|
|
424
611
|
get entryCount() {
|
|
425
|
-
return this.
|
|
612
|
+
return this.committed.length + this.pending.length;
|
|
426
613
|
}
|
|
427
614
|
}
|
|
428
615
|
// ── Factory ────────────────────────────────────────────────────────────────────
|