@awebai/oats 0.22.6 → 0.22.7
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.
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# OATS v0.22.7
|
|
2
|
+
|
|
3
|
+
Two resource fixes: index reconciliation streams journals instead of
|
|
4
|
+
reading each one whole, so a full capture pass fits in memory on a store
|
|
5
|
+
with very large session journals, and the packaged Desktop backend runs as
|
|
6
|
+
Node rather than a second Electron runtime.
|
|
7
|
+
|
|
8
|
+
## Record index: streamed reconciliation
|
|
9
|
+
|
|
10
|
+
`RecordIndex.update()` used to materialize every journal as a buffer plus a
|
|
11
|
+
parsed array before consulting its per-stream cursor; on a real store with
|
|
12
|
+
an 887 MiB journal the indexing phase peaked above 2.3 GiB and had to be
|
|
13
|
+
stopped. The store now offers `iterateStream()`, which reads 64 KiB chunks
|
|
14
|
+
and yields one turn at a time with the same rules as `readStream()`:
|
|
15
|
+
whitespace lines are skipped, an unparseable or `null` interior line is a
|
|
16
|
+
corruption error carrying its absolute byte offset, and an unterminated
|
|
17
|
+
final fragment is a torn tail left for the owner's append repair. The
|
|
18
|
+
cursor, the single transaction and its rollback on corruption are
|
|
19
|
+
unchanged. Measured on the same store: a full pass completes in 42 s with a
|
|
20
|
+
peak of about 810 MiB under a 256 MB Node old-space budget.
|
|
21
|
+
|
|
22
|
+
`readStream()` is unchanged for callers that need arrays.
|
|
23
|
+
|
|
24
|
+
## Desktop backend runs as Node
|
|
25
|
+
|
|
26
|
+
The packaged Desktop app spawns its backend server with its own
|
|
27
|
+
executable, which is Electron. The child now runs with
|
|
28
|
+
`ELECTRON_RUN_AS_NODE=1`, so the backend and the collector processes it
|
|
29
|
+
starts run as plain Node instead of relaunching the app (verified headless
|
|
30
|
+
against the signed 0.22.6 executable: the backend served `/api/version` at
|
|
31
|
+
about 70 MiB). This does not explain the earlier GUI memory incident, whose
|
|
32
|
+
snapshot held no Desktop process; it removes one way to multiply app
|
|
33
|
+
runtimes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.7",
|
|
4
4
|
"description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -163,12 +163,13 @@ export class RecordIndex {
|
|
|
163
163
|
"INSERT INTO index_state (stream, indexed_count) VALUES (?, ?) ON CONFLICT(stream) DO UPDATE SET indexed_count = excluded.indexed_count",
|
|
164
164
|
);
|
|
165
165
|
for (const streamId of this.store.listStreams()) {
|
|
166
|
-
const turns = this.store.readStream(streamId);
|
|
167
166
|
const done = Number(getState.get(streamId)?.indexed_count ?? 0);
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
let count = 0;
|
|
168
|
+
for (const turn of this.store.iterateStream(streamId)) {
|
|
169
|
+
if (count >= done) this.addTurn(turn, streamId, effectiveOwner);
|
|
170
|
+
count++;
|
|
170
171
|
}
|
|
171
|
-
setState.run(streamId,
|
|
172
|
+
setState.run(streamId, count);
|
|
172
173
|
}
|
|
173
174
|
this.db.exec("COMMIT");
|
|
174
175
|
} catch (err) {
|
|
@@ -404,6 +404,43 @@ export class RecordStore {
|
|
|
404
404
|
return parseJournal(readFileSync(path)).turns;
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
// Bulk consumers need one turn at a time, not a buffer and parsed array
|
|
408
|
+
// for an entire historical journal (some exceed 800 MB). Keep at most
|
|
409
|
+
// one complete line plus read chunks, with the same corruption/torn-tail
|
|
410
|
+
// rules as readStream. Joining fragments only at a newline avoids
|
|
411
|
+
// repeatedly copying long lines as each chunk arrives.
|
|
412
|
+
*iterateStream(streamId) {
|
|
413
|
+
let fd;
|
|
414
|
+
try { fd = openSync(this.journalPath(streamId), "r"); }
|
|
415
|
+
catch (e) { if (e.code === "ENOENT") return; throw e; }
|
|
416
|
+
let pieces = [], size = 0, lineStart = 0;
|
|
417
|
+
try {
|
|
418
|
+
for (;;) {
|
|
419
|
+
const chunk = Buffer.allocUnsafe(65536);
|
|
420
|
+
const n = readSync(fd, chunk, 0, chunk.length, null);
|
|
421
|
+
if (n === 0) break;
|
|
422
|
+
let from = 0;
|
|
423
|
+
for (let nl = chunk.indexOf(10, from); nl >= 0 && nl < n; nl = chunk.indexOf(10, from)) {
|
|
424
|
+
pieces.push(chunk.subarray(from, nl + 1));
|
|
425
|
+
size += nl + 1 - from;
|
|
426
|
+
const line = pieces.length === 1 ? pieces[0] : Buffer.concat(pieces, size);
|
|
427
|
+
let parsed;
|
|
428
|
+
try { parsed = parseJournal(line); }
|
|
429
|
+
catch (e) {
|
|
430
|
+
if (e instanceof StoreError) throw new StoreError(`corrupt interior journal line at byte ${lineStart}`);
|
|
431
|
+
throw e;
|
|
432
|
+
}
|
|
433
|
+
lineStart += size;
|
|
434
|
+
pieces = []; size = 0; from = nl + 1;
|
|
435
|
+
yield* parsed.turns;
|
|
436
|
+
}
|
|
437
|
+
if (from < n) { pieces.push(chunk.subarray(from, n)); size += n - from; }
|
|
438
|
+
}
|
|
439
|
+
// A final fragment without a newline is a torn tail, even if its
|
|
440
|
+
// JSON is valid. Readers leave it for the owner's append repair.
|
|
441
|
+
} finally { closeSync(fd); }
|
|
442
|
+
}
|
|
443
|
+
|
|
407
444
|
// Is this a session-content stream (`<owner>~<source>.<session-id>`)?
|
|
408
445
|
// Their journals hold whole conversations and can be large, so bulk
|
|
409
446
|
// reads exclude them unless asked; access them per-thread instead.
|