@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.6",
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
- for (let i = done; i < turns.length; i++) {
169
- this.addTurn(turns[i], streamId, effectiveOwner);
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, turns.length);
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.