@awebai/oats 0.22.6 → 0.22.8

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.
@@ -27,9 +27,12 @@ import { existsSync, mkdirSync, mkdtempSync, writeFileSync, readFileSync, readdi
27
27
  import { join, isAbsolute, dirname } from "node:path";
28
28
  import { tmpdir } from "node:os";
29
29
  import { execFile, spawnSync } from "node:child_process";
30
+ import { reclaimHarvestBranch } from "../lib/harvest-branch.mjs";
30
31
 
31
32
  const out = (o) => { process.stdout.write(JSON.stringify(o) + "\n"); process.exit(0); };
32
33
  const warn = (m) => out({ warning: `oats-okf: ${String(m).slice(0, 300)}` });
34
+ // A reported failure must not exit 0: callers and hooks read the status.
35
+ const warnFail = (m) => { process.stdout.write(JSON.stringify({ warning: `oats-okf: ${String(m).slice(0, 300)}` }) + "\n"); process.exit(1); };
33
36
 
34
37
  // Desktop CLI API v1: `oats okf harvest --json` emits EXACTLY ONE envelope
35
38
  // object on stdout — {schemaVersion:1,ok,result|error} — and a nonzero exit
@@ -38,6 +41,12 @@ const JSON_MODE = process.argv.includes("--json");
38
41
  const jsonOk = (result) => { process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: true, result }) + "\n"); process.exit(0); };
39
42
  const jsonFail = (code, message) => { process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message).slice(0, 300) } }) + "\n"); process.exit(1); };
40
43
 
44
+ // --help/-h never runs a command here either (the kernel answers it from the
45
+ // manifest since 0.22.6; this keeps an older kernel from spawning a harvester).
46
+ if (process.argv.slice(2).some((a) => a === "--help" || a === "-h")) {
47
+ process.stdout.write("oats okf harvest [--json] [--from-record] [--force] promote this instance's pending notes (and record windows) into its soul by spawning a memory-harvest worker; --help never spawns\noats okf status [--json]\n");
48
+ process.exit(0);
49
+ }
41
50
  const event = process.env.OATS_EVENT || process.argv[2];
42
51
  const instance = process.env.OATS_INSTANCE;
43
52
  const home = process.env.OATS_HOME || process.cwd();
@@ -421,6 +430,10 @@ _(the single next action — keep this current; a fresh session on any model res
421
430
  const soulRepo = gitRootOf(realSoul);
422
431
  if (!soulRepo) skip("workspace-mode soul is not inside a git repo — nowhere to deliver a PR");
423
432
  const relSoul = realSoul.slice(soulRepo.length + 1);
433
+ // A leftover memory-harvest/<slug> branch from a merged promotion is
434
+ // deleted first; an unmerged one refuses the harvest with the remedy.
435
+ const reclaimed = reclaimHarvestBranch(soulRepo, `memory-harvest/${slug}`);
436
+ if (reclaimed.action === "deleted") process.stderr.write(`oats-okf: deleted stale harvest branch memory-harvest/${slug} (merged into ${reclaimed.base})\n`);
424
437
  const task = `Harvest the pending notes of live WORKSPACE-MODE instance "${inst}" (agent "${agName}") into its soul — delivered as a PR.\n\n- Source notes: ${notes.length ? `${notesDir} (${notes.join(", ")})` : "none pending"}\n- Your ./work is a dedicated worktree of the soul's home repo (${soulRepo}), branch memory-harvest/${slug}.\n- Soul knowledge bundle to update: ./work/${join(relSoul, "knowledge")}\n- Soul skills dir (for procedure-shaped notes): ./work/${join(relSoul, "skills")}\n- Follow your memory-harvest skill: promote/merge/drop each note, knowledge vs skill routing, index + log discipline, validate the bundle, DELETE processed notes from the source notes/ dir, and commit once (prefixed "memory-harvest:") if anything changed.${recordBrief(recordPlan, packageRuntimeCli())}\n- If you changed anything: push the branch and open a PR (\`git push -u origin memory-harvest/${slug}\` then \`gh pr create --fill\`). Do NOT merge it; the humans/owners of ${soulRepo} review soul changes. If gh is unavailable, push the branch and report the compare URL. A harvest that promoted nothing has nothing to commit, push or open; that is a completed harvest, not a failed one.\n- Finally run \`oats retire ${harvName} --self\` (keep the branch: --self only).`;
425
438
  r = await spawnHarvester(harvestSpawnArgs({
426
439
  slug, parent: inst, repo: soulRepo, work: "worktree",
@@ -458,7 +471,7 @@ _(the single next action — keep this current; a fresh session on any model res
458
471
  out({ meta: { harvestSpawn: r.instance, window: r.tmux?.window }, ...warnings });
459
472
  } catch (e) {
460
473
  if (JSON_MODE) jsonFail(e.code || "E_HARVEST_FAILED", `harvest spawn failed (notes are safe on disk): ${e.message || e}`);
461
- warn(`harvest spawn failed (notes are safe on disk): ${e.message || e}`);
474
+ warnFail(`harvest spawn failed (notes are safe on disk): ${e.message || e}`);
462
475
  }
463
476
  } else if (event === "retire") {
464
477
  // Retirement is intentionally a no-op for knowledge (for now): promotion happens
@@ -0,0 +1,43 @@
1
+ // A workspace-mode harvest delivers its promotion as a PR from a branch named
2
+ // memory-harvest/<slug> in the soul's repository. After that PR merges, the
3
+ // local branch may still exist and the next harvest's spawn would refuse it.
4
+ // A branch fully merged into the base is stale and is deleted before the
5
+ // spawn; an unmerged one is the previous harvester's unfinished work and the
6
+ // harvest refuses with the exact remedy instead of touching it.
7
+ import { execFileSync } from "node:child_process";
8
+
9
+ /** Single-quote shell escaping for the operator remedy: the repo path may hold spaces or shell metacharacters. */
10
+ export function shellQuote(s) { return "'" + String(s).replace(/'/g, "'\\''") + "'"; }
11
+
12
+ function git(repo, args) {
13
+ return execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
14
+ }
15
+
16
+ /** The repository's base branch: origin/HEAD's target when known, else main, else master. */
17
+ export function baseBranchOf(repo) {
18
+ try { const ref = git(repo, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]); if (ref) return ref.replace(/^refs\/remotes\//, ""); } catch { /* no origin/HEAD */ }
19
+ for (const b of ["origin/main", "main", "origin/master", "master"]) {
20
+ try { git(repo, ["rev-parse", "--verify", "--quiet", b]); return b; } catch { /* next */ }
21
+ }
22
+ return undefined;
23
+ }
24
+
25
+ /** Returns { action: "absent" | "deleted", base } or throws E_HARVEST_BRANCH_EXISTS. */
26
+ export function reclaimHarvestBranch(repo, branch) {
27
+ try { git(repo, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]); }
28
+ catch { return { action: "absent" }; }
29
+ const base = baseBranchOf(repo);
30
+ let merged = false;
31
+ if (base) { try { git(repo, ["merge-base", "--is-ancestor", branch, base]); merged = true; } catch { merged = false; } }
32
+ if (!merged) {
33
+ const err = new Error(`branch ${branch} already exists in ${repo} and is not merged into ${base || "any base branch"}: a previous harvest's promotion is unfinished — review and merge or delete it (git -C ${shellQuote(repo)} branch -D ${shellQuote(branch)}) before harvesting again`);
34
+ err.code = "E_HARVEST_BRANCH_EXISTS";
35
+ throw err;
36
+ }
37
+ // -D, not -d: the merge check above is against the BASE (origin/main when
38
+ // present). `branch -d` re-checks against the branch's upstream or the
39
+ // current HEAD instead, so with the soul's local main behind origin/main a
40
+ // branch fully merged upstream would still be refused as "not fully merged".
41
+ git(repo, ["branch", "-D", branch]);
42
+ return { action: "deleted", base };
43
+ }
@@ -1,15 +1,21 @@
1
1
  {
2
2
  "capability": "oats.okf",
3
3
  "command": "okf",
4
- "version": "1.5.1",
5
- "compatibility": { "oats": ">=0.22.3" },
4
+ "version": "1.5.2",
5
+ "compatibility": {
6
+ "oats": ">=0.22.3"
7
+ },
6
8
  "layer": "knowledge",
7
9
  "description": "Knowledge layer via OKF: soul bundles, instance memory (STATE.md/log.md/notes/), continuous post-commit harvest into the soul (commit, PR, or direct-edit for local souls), craft + memory skills, validator.",
8
10
  "requires": [],
9
11
  "settings": {
10
12
  "harvest-runtime": {
11
13
  "default": "pi",
12
- "values": ["pi", "claude", "codex"],
14
+ "values": [
15
+ "pi",
16
+ "claude",
17
+ "codex"
18
+ ],
13
19
  "description": "Harness for the memory harvester, independent of the source instance's runtime."
14
20
  },
15
21
  "harvest-model": {
@@ -167,6 +167,14 @@ start a second harvester while the first one's home exists. The check uses the
167
167
  existing prepared watermark file and does not treat a successful spawn as
168
168
  completed learning.
169
169
 
170
+ ## oats.okf 1.5.2
171
+
172
+ `okf harvest` exits non-zero when it reports a failure (the plain and the
173
+ `--json` forms alike). A leftover `memory-harvest/<slug>` branch from a merged
174
+ promotion is deleted before the next workspace-mode harvest; an unmerged one
175
+ refuses the harvest and names the remedy. `oats okf harvest --help` prints
176
+ usage and never spawns.
177
+
170
178
  ## oats.aweb late joins (1.10.3)
171
179
 
172
180
  `aw team join` at spawn gets 120 s (a slow link is slow, not broken). If the
@@ -33,24 +33,30 @@ Every remembering role must have a tested learning path. Preserve each role's
33
33
  explicit policy: Cjr reviewers exclude accumulated memory; Themis uses
34
34
  reviewed learning. Config discovery alone establishes none of this.
35
35
 
36
- The installed CLI baseline is published OATS 0.22.5 on this Mac and `aweb-agents`,
36
+ The installed CLI baseline is published OATS 0.22.7 on this Mac and `aweb-agents`,
37
37
  including native Pi/Claude/Codex, tmux/Herdr, shared `yolo`, remote Desktop
38
38
  roster/actions, retained-authority binding and corrected deferred retirement.
39
- The installed Mac Desktop 0.22.5 passed published ZIP checksum, strict deep
40
- codesign, packaged renderer and PTY launch checks; the previous 0.22.4 app
41
- is preserved for rollback. Official oats.okf 1.5.1 is published after independent
39
+ The installed Mac Desktop 0.22.7 passed published ZIP checksum and strict deep
40
+ codesign. Its controlled single-instance check passed and the app was closed
41
+ afterwards, with all owned processes verified gone. The previous 0.22.6 app
42
+ is preserved for rollback; earlier renderer/PTY checks remain version-specific.
43
+ Official oats.okf 1.5.1 is published after independent
42
44
  review, adding harvest-runtime selection and detection of unadvanced record
43
45
  plans. Each deployment selects an authenticated harness; without an explicit
44
46
  harvest-model, that harness uses its own configured default. Some prepared
45
47
  teams still use the compatible 1.5.0 package; preserve the exact versions of
46
- each earlier qualification.
48
+ each earlier qualification. Follow-up 1.5.2 is now published with truthful
49
+ failure exits, harmless help and repeat-harvest branch reuse. BeadHub's actual
50
+ deployment has been updated and re-trusted at 1.5.2; its previous lock was 1.5.0.
47
51
 
48
52
  BeadHub, Minerva and Merlin now have managed standing executions with retained
49
53
  identities. BeadHub and Minerva passed separate mail/chat checks; Merlin verified
50
54
  his identity and preserved claims, then received and replied to Minerva's real
51
- mail through the host wake path. This is not completion of all teams: frontend
52
- and Themis encountered failed setup, Docflow still has a running backfill, and
53
- the coordinator handovers remain outstanding. See the current status below;
55
+ mail through the host wake path. Frontend also has a managed execution with
56
+ separate mail/chat proof and reviewed knowledge promotion. This is not completion
57
+ of all teams: TSM is held, Docflow still has a running backfill, and normal
58
+ coordinator learning remains queued. The final lead handover is accepted.
59
+ See the current status below;
54
60
  older evidence records keep the version and outcome of each earlier check.
55
61
 
56
62
  ## Current status and operating limits (2026-09-06)
@@ -70,22 +76,36 @@ no OATS Desktop process remained when lead took the incident snapshot.
70
76
 
71
77
  Lead stopped the capture watcher and residual capture passes, disabled their
72
78
  exact Claude hooks, and stopped the experimental mind follow service. Settings,
73
- service definitions, raw records and learning state are preserved. GUI launch
74
- is paused. Resume with one bounded operation at a time, checking memory between
75
- launches; declining swap alone does not prove sustained stability. Capture stays
76
- disabled until its concurrency fix is independently reviewed and measured. The
77
- first proposed lock was rejected because age-based stealing and initialization
78
- races could still permit overlapping passes.
79
+ service definitions, raw records and learning state are preserved. Continue
80
+ with one bounded operation at a time, checking memory between
81
+ launches; declining swap alone does not prove sustained stability. Published
82
+ 0.22.6 prevents overlapping capture passes with a conservative lock that never
83
+ steals an existing owner; interrupted owners require explicit recovery. Its
84
+ measured full pass still exceeded a 2 GiB RSS budget during indexing and was
85
+ stopped by the monitor. The follow-up streams journal entries instead of loading
86
+ whole arrays. The independently reviewed candidate completed the real index of
87
+ 1.74 million turns in 41.6 seconds, at 809.5 MiB peak RSS with normal memory
88
+ pressure, under a 256 MiB Node old-space budget. That fix is now published and
89
+ installed in 0.22.7. The replacement launchd job runs one background pass every
90
+ 15 minutes, without per-tool hooks or a permanent watcher. Its first real pass
91
+ completed in 101 seconds at 746 MiB peak RSS, with index completion and zero
92
+ aw-log projection failures. The reviewed operator wrapper stops its own child
93
+ on a 2 GiB RSS limit, two elevated memory-pressure samples, or a five-minute
94
+ deadline. It records each run atomically in
95
+ `~/.local/state/oats/capture/status.json`, retaining the previous success time
96
+ through failures. Idle between passes is normal. Interrupted capture locks
97
+ still need explicit owner-checked recovery; the wrapper reports the remedy.
98
+ Experimental mind follow remains paused.
79
99
 
80
100
  | Scope | Verified state | Next boundary |
81
101
  | --- | --- | --- |
82
102
  | Host services | Published aw 1.36.1 installed; normal launchd wake service on Mac and enabled user service on `aweb-agents`; private broker stopped | Investigate repeated reconnect hints and reported read timing without assuming the broker acknowledged mail |
83
- | BeadHub | `beadhub-seat`, retained DID/address and claims; native Codex; independent mail/chat; first reviewed knowledge PR merged at `70c839e` | Repeat harvest exposed a retained merged-branch collision; operator updated the linked soul and removed only the verified merged branch; next cycle waits for a bounded launch slot |
84
- | Cjr | `accountant-minerva` and `coordinator-merlin` live on retained identities; old holders stopped first; claims preserved; real delivery and reviewed learning recorded | Complete the existing log worker's fresh review, one reviewer at a time; no financial authority changes |
85
- | Aweb | Coordinator remains live; old frontend stopped; replacement failed during a timed-out join that completed server-side | Supported cleanup of the retained failed home/orphan binding, then one successor with independent delivery checks |
103
+ | BeadHub | `beadhub-seat`, retained DID/address and claims; native Codex; independent mail/chat; trusted OKF 1.5.2 and aweb 1.10.3; repeat harvest promoted all four notes and retired cleanly | PR #2 at `a111a222` independently reviewed and merged; canonical deployment checkout fast-forwarded with untracked data preserved, so the live soul sees the six-concept bundle |
104
+ | Cjr | `accountant-minerva` and `coordinator-merlin` live on retained identities; old holders stopped first; claims preserved; real delivery and reviewed learning recorded | Log `5bfeefd1` and corrected librarian knowledge `6c91f986` landed with user edits preserved; librarian retired with alias reusable; Merlin's own harvest and health-reader follow-up remain queued |
105
+ | Aweb | Retained `coordinator-aweb` and fresh `frontend-oats` both passed independent mail/chat; frontend knowledge `d649d729` reviewed and landed | Old holders stopped with their channel children before replacement; metadata repair and encryption-key findings tracked separately, with repeated wake-hint diagnosis in progress |
86
106
  | TSM | Prepared souls and owner checkpoints; Themis setup failed before the current hold | **No migration work until Zeus reports deployment finished**; re-inventory with its owner afterwards |
87
107
  | Docflow | Legacy seat and actual mail backfill remain running | Finish backfill and register checks; owner restores mail-ingest afterwards; accountant-sync remains unloaded under its separate export fence |
88
- | Oats/lead | Existing coordinators remain active | Last handovers, with actual stop receipts and all unresolved work carried forward |
108
+ | Oats/lead | Retained `oats-coordinator-coordinator` passed independent mail wake; fresh team-local `lead-operating-lead` passed separate idle-to-wake mail fetch/reply after verified predecessor stop; root packages trusted at OKF 1.5.2 and aweb 1.10.3 | BeadHub accepted the lead and transferred the still-open rollout epic; Oats's provider capacity, historical lead claim reconciliation, coordinator learning and preserved recovery lessons remain follow-ups; cross-team updates use retained global relays |
89
109
  | Remote qualification | Published host service delivered native Claude mail/chat through Herdr; corrected knowledge independently reviewed; source retired with `aliasReusable: true` | Earlier separate fresh-reader cycle passed; latest corrected wake-specific retrieval is still pending |
90
110
 
91
111
  Published aw 1.36.1 is tagged at `bfdb20886080e4ffe1f02b266f6116d12bd100fd`.
@@ -97,10 +117,17 @@ Production same-alias join/delete/rejoin passed, and official oats.aweb 1.10.2
97
117
  reports the released alias result truthfully. Retained standing-seat retirement
98
118
  must still preserve authority.
99
119
 
100
- Desktop 0.22.5 has six validated team roots saved as workspace suggestions,
120
+ Desktop has six validated team roots saved as workspace suggestions,
101
121
  not six running GUI instances. It starts with one workspace and can add others.
102
122
  It is currently closed; visual QA and sustained multi-workspace memory behavior
103
- are not claimed. Native remote Pi authentication and remote Codex remain
123
+ are not claimed. Version 0.22.6 added a single-instance guard and 0.22.7 runs the
124
+ packaged backend as Node. An installed 0.22.7 check served the Oats workspace API;
125
+ a second launch exited successfully while the primary remained, with both
126
+ owned process groups peaking at 589 MiB and normal memory pressure. All test
127
+ processes were stopped and verified absent. The full release gate and all three
128
+ Desktop builds passed on hosted runners; publication succeeded, and the bot's
129
+ version-bump PR permission failure was resolved through reviewed manual PR #8.
130
+ Native remote Pi authentication and remote Codex remain
104
131
  unqualified; the accepted remote harness is Claude.
105
132
 
106
133
  ## Scope inventory
@@ -119,7 +146,7 @@ of continuing seats.
119
146
  | `~/prj/docflow` | Live Claude seat identified itself as local `juan.aweb.ai/alice` on `docflow:juan.aweb.ai` | Owner Juan; finish running mail backfill and register checks before transfer; retain identity, memory and Minerva route; accountant-sync remains deliberately unloaded |
120
147
  | `ai.aweb` on `aweb-agents` | Aweb confirms Athena intentionally inactive; remote legacy home retained | Aweb and oats own archival inspection; do not resurrect as a continuing seat |
121
148
  | `~/awebai/demo-aweb/bob` | Live Pi demo | Aweb owns safe stop and archival disposition; it is not an operating-team migration |
122
- | `~/.turn-record` | Capture and experimental mind services paused after memory incident | Preserve records; review and measure resource fixes before resuming |
149
+ | `~/.turn-record` | Guarded periodic capture enabled and measured; per-tool hooks and experimental mind follow remain disabled | Preserve records; monitor the run artifact; no blind watcher restart |
123
150
 
124
151
  The starting inventory above was checked on 2026-09-05 using harness process
125
152
  working directories and exact custom tmux sockets, without interrupting them.
@@ -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.
@@ -0,0 +1,40 @@
1
+ # OATS v0.22.8
2
+
3
+ Two fixes an operator meets directly: picking a remote workspace in Desktop
4
+ now works, and the bundled catalog pin for the knowledge package catches up
5
+ with what is published.
6
+
7
+ ## Desktop: selecting a discovered remote workspace
8
+
9
+ A registered SSH server's workspaces are discovered after the app starts, so
10
+ the main process's record of what the backend advertises could be seeded
11
+ before that discovery finished. Clicking the remote workspace in the menu then
12
+ looked like nothing happened: the request carried a workspace id the record
13
+ did not recognize, and an unrecognized selection is deliberately pinned back
14
+ to the verified local scope. The workaround was to open Add workspace and
15
+ cancel, whose own refresh repaired the record as a side effect.
16
+
17
+ The main process now learns that record from the successful `/api/panel`
18
+ responses it is already making, which are the same server-owned choices the
19
+ menu is built from, so a workspace that appears late is selectable as soon as
20
+ the menu can show it. No request is added, including for a selection that
21
+ stays unrecognized. Only a successful panel response teaches it: error
22
+ replies and other endpoints cannot. Nothing learned from an outgoing backend
23
+ can survive its replacement: a request that begins while the backend is being
24
+ replaced never teaches, and one that began earlier is refused if a replacement
25
+ started before its reply arrived, so the workspaces a stopped server
26
+ advertised cannot come back. Off-origin resolution is still rejected before
27
+ anything is fetched, and a selection the backend does not advertise is still
28
+ pinned to the local scope.
29
+
30
+ ## Bundled: oats.okf 1.5.2 pin
31
+
32
+ The catalog shipped with the kernel now pins oats.okf v1.5.2, so
33
+ `oats update oats.okf` resolves it without an explicit selector. Until now a
34
+ bare update resolved the pin baked into the installed kernel, which was
35
+ v1.5.1, and reaching 1.5.2 needed `oats update oats.okf --to v1.5.2`.
36
+ Deployments that moved early with the explicit selector are already on the
37
+ same payload and need nothing. 1.5.2 makes a failed harvest exit non-zero,
38
+ stops `okf harvest --help` from running a harvest, and reclaims a leftover
39
+ `memory-harvest/<slug>` branch whose promotion is already merged into the
40
+ base, including when the base is a remote main ahead of the local one.
@@ -2,7 +2,7 @@
2
2
  "packages": {
3
3
  "oats.okf": {
4
4
  "url": "https://github.com/awebai/oats-okf.git",
5
- "ref": "v1.5.1",
5
+ "ref": "v1.5.2",
6
6
  "path": "oats-package"
7
7
  },
8
8
  "oats.aweb": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.22.6",
3
+ "version": "0.22.8",
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.