antechamber 1.0.9 → 1.0.12

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 CHANGED
@@ -7,6 +7,27 @@ and this project uses [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.12] - 2026-10-21
11
+
12
+ ### Fixed
13
+
14
+ - Stop saving child-task spawn specs in the session registry; OmO's task records already hold them.
15
+ This shrinks the registry by about a third in sessions with hundreds of child tasks.
16
+
17
+ ## [1.0.11] - 2026-10-21
18
+
19
+ ### Fixed
20
+
21
+ - Cut the event-loop stall of each session registry write by reusing the in-memory registry while
22
+ the file is unchanged, cloning only the entries a batch reads, and writing compact JSON.
23
+
24
+ ## [1.0.10] - 2026-10-21
25
+
26
+ ### Fixed
27
+
28
+ - Keep sessions with many child tasks responsive by ignoring unchanged task snapshots and saving
29
+ each snapshot's changes in one registry write.
30
+
10
31
  ## [1.0.9] - 2026-10-21
11
32
 
12
33
  ### Fixed
@@ -91,10 +91,14 @@ export declare class SessionStore {
91
91
  readonly stateDir: string;
92
92
  private readonly file;
93
93
  private entries;
94
+ /** The registry bytes `entries` was parsed from or last written as; equal bytes on disk mean the cache is current. */
95
+ private raw;
94
96
  private batchState;
95
97
  constructor(stateDir?: string);
96
98
  private load;
97
- private readEntries;
99
+ private committed;
100
+ private readRaw;
101
+ private parseEntries;
98
102
  private withRegistryLock;
99
103
  private persist;
100
104
  private mutate;
@@ -371,6 +371,17 @@ function registryEntry(value) {
371
371
  return false;
372
372
  return true;
373
373
  }
374
+ /**
375
+ * OmO keeps a task's spawn spec in its own task record, which is where the adapter reads it
376
+ * (adoption, eval-agent matching). The registry copy was never read back and made up about a third
377
+ * of the file in sessions with hundreds of children.
378
+ */
379
+ function persistedEntry(entry) {
380
+ if (entry.task === undefined || !("spawn_spec" in entry.task.details))
381
+ return entry;
382
+ const { spawn_spec: _spec, ...details } = entry.task.details;
383
+ return { ...entry, task: { ...entry.task, details } };
384
+ }
374
385
  function errorCode(error) {
375
386
  return error instanceof Error && "code" in error ? String(error.code) : undefined;
376
387
  }
@@ -378,6 +389,8 @@ export class SessionStore {
378
389
  stateDir;
379
390
  file;
380
391
  entries;
392
+ /** The registry bytes `entries` was parsed from or last written as; equal bytes on disk mean the cache is current. */
393
+ raw;
381
394
  batchState;
382
395
  constructor(stateDir) {
383
396
  const dir = stateDir ?? envSetting("STATE_DIR") ?? defaultStateDir();
@@ -386,26 +399,41 @@ export class SessionStore {
386
399
  }
387
400
  load() {
388
401
  if (this.batchState !== undefined) {
389
- this.batchState.entries ??= structuredClone((this.entries ??= this.readEntries()));
402
+ // Shallow: changes replace whole entries, and get() clones an entry before handing it out.
403
+ this.batchState.entries ??= new Map(this.committed());
390
404
  return this.batchState.entries;
391
405
  }
406
+ return this.committed();
407
+ }
408
+ committed() {
392
409
  if (this.entries !== undefined)
393
410
  return this.entries;
394
- this.entries = this.readEntries();
411
+ this.raw = this.readRaw();
412
+ this.entries = this.parseEntries(this.raw);
395
413
  return this.entries;
396
414
  }
397
- readEntries() {
415
+ readRaw() {
416
+ try {
417
+ return readFileSync(this.file);
418
+ }
419
+ catch {
420
+ return undefined;
421
+ }
422
+ }
423
+ parseEntries(raw) {
398
424
  const entries = new Map();
425
+ if (raw === undefined)
426
+ return entries;
399
427
  try {
400
- const raw = JSON.parse(readFileSync(this.file, "utf8"));
401
- if (!record(raw))
428
+ const parsed = JSON.parse(raw.toString("utf8"));
429
+ if (!record(parsed))
402
430
  return entries;
403
- for (const [id, value] of Object.entries(raw))
431
+ for (const [id, value] of Object.entries(parsed))
404
432
  if (registryEntry(value))
405
433
  entries.set(id, value);
406
434
  }
407
435
  catch {
408
- /* missing or corrupt — start empty */
436
+ /* corrupt — start empty */
409
437
  }
410
438
  return entries;
411
439
  }
@@ -450,11 +478,13 @@ export class SessionStore {
450
478
  }
451
479
  }
452
480
  persist(entries) {
453
- const obj = Object.fromEntries(entries);
481
+ const obj = Object.fromEntries([...entries].map(([id, entry]) => [id, persistedEntry(entry)]));
454
482
  const tmp = `${this.file}.${process.pid}.tmp`;
455
- writeFileSync(tmp, JSON.stringify(obj, null, 2), { mode: REGISTRY_FILE_MODE });
483
+ const data = Buffer.from(JSON.stringify(obj));
484
+ writeFileSync(tmp, data, { mode: REGISTRY_FILE_MODE });
456
485
  chmodSync(tmp, REGISTRY_FILE_MODE);
457
486
  renameSync(tmp, this.file);
487
+ return data;
458
488
  }
459
489
  mutate(change) {
460
490
  if (this.batchState !== undefined) {
@@ -463,10 +493,14 @@ export class SessionStore {
463
493
  return;
464
494
  }
465
495
  this.withRegistryLock(() => {
466
- const entries = this.readEntries();
467
- if (change(entries))
468
- this.persist(entries);
496
+ // Another process may have written since; only parse again when the bytes differ.
497
+ const raw = this.readRaw();
498
+ const entries = this.entries !== undefined && raw !== undefined && this.raw !== undefined && raw.equals(this.raw)
499
+ ? new Map(this.entries)
500
+ : this.parseEntries(raw);
501
+ const written = change(entries) ? this.persist(entries) : raw;
469
502
  this.entries = entries;
503
+ this.raw = written;
470
504
  });
471
505
  }
472
506
  /** Synchronous restoration reads its own changes, then merges them under one short-lived write lock. */
@@ -474,7 +508,7 @@ export class SessionStore {
474
508
  if (this.batchState !== undefined)
475
509
  return action();
476
510
  const changes = [];
477
- const batch = { entries: undefined, changes };
511
+ const batch = { entries: undefined, isolated: new WeakSet(), changes };
478
512
  this.batchState = batch;
479
513
  try {
480
514
  const result = action();
@@ -495,7 +529,14 @@ export class SessionStore {
495
529
  }
496
530
  }
497
531
  get(ocSessionId) {
498
- return this.load().get(ocSessionId);
532
+ const entries = this.load();
533
+ const entry = entries.get(ocSessionId);
534
+ if (this.batchState === undefined || entry === undefined || this.batchState.isolated.has(entry))
535
+ return entry;
536
+ const staged = structuredClone(entry);
537
+ entries.set(ocSessionId, staged);
538
+ this.batchState.isolated.add(staged);
539
+ return staged;
499
540
  }
500
541
  idFor(piSessionId) {
501
542
  return [...this.load()].find(([, entry]) => entry.piSessionId === piSessionId)?.[0];
package/dist/subagents.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /** OmO task cards and read-only views of their independently persisted sessions. */
2
2
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, watch, writeFileSync, } from "node:fs";
3
3
  import { basename, dirname, join } from "node:path";
4
+ import { isDeepStrictEqual } from "node:util";
4
5
  import { piAgentDir } from "./agent-dir.js";
5
6
  import { syncSessionCost } from "./pi-event-translation.js";
6
7
  import { ocIdFor, SessionPersistenceError, SessionStore } from "./session-store.js";
@@ -541,28 +542,31 @@ export class SubagentBridge {
541
542
  observe(parent, ev) {
542
543
  if (ev.name !== "omo.task.updated" || !record(ev.data) || !Array.isArray(ev.data.tasks))
543
544
  return;
544
- for (const row of ev.data.tasks) {
545
- if (!record(row) || !taskId(row.task_id))
546
- continue;
547
- if (typeof row.parent_session_id === "string" &&
548
- row.parent_session_id !== (parent.piSessionId ?? parent.restored?.piSessionId))
549
- continue;
550
- const id = this.sessionId(parent, row.task_id);
551
- if (!this.children.has(id))
552
- this.restore(id);
553
- const child = this.children.get(id);
554
- if (!child || child.session.info.parentID !== parent.info.id)
555
- continue;
556
- const data = snapshotState(row);
557
- // OmO re-sends every row ~150 ms after each progress event of any task; only changes matter.
558
- if (Object.entries(data).every(([key, value]) => child.task.details[key] === value))
559
- continue;
560
- this.wire(parent);
561
- if (!this.applyNative(child, data))
562
- continue;
563
- child.live = true;
564
- this.refresh(child, true, false);
565
- }
545
+ const rows = ev.data.tasks;
546
+ this.store.batch(() => withTaskLookups(this.store, () => {
547
+ for (const row of rows) {
548
+ if (!record(row) || !taskId(row.task_id))
549
+ continue;
550
+ if (typeof row.parent_session_id === "string" &&
551
+ row.parent_session_id !== (parent.piSessionId ?? parent.restored?.piSessionId))
552
+ continue;
553
+ const id = this.sessionId(parent, row.task_id);
554
+ if (!this.children.has(id))
555
+ this.restore(id);
556
+ const child = this.children.get(id);
557
+ if (!child || child.session.info.parentID !== parent.info.id)
558
+ continue;
559
+ const data = snapshotState(row);
560
+ // OmO re-sends every row ~150 ms after each progress event of any task; only changes matter.
561
+ if (Object.entries(data).every(([key, value]) => isDeepStrictEqual(child.task.details[key], value)))
562
+ continue;
563
+ this.wire(parent);
564
+ if (!this.applyNative(child, data))
565
+ continue;
566
+ child.live = true;
567
+ this.refresh(child, true, false);
568
+ }
569
+ }));
566
570
  }
567
571
  /**
568
572
  * Merge task state reported by OmO (record, snapshot) unless it is older than what is known: an
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-compat",
3
- "version": "1.0.9",
3
+ "version": "1.0.12",
4
4
  "description": "OpenChamber extension: displays OmO tool calls the way OmO means them when OpenChamber runs on antechamber",
5
5
  "private": true,
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-graph",
3
- "version": "1.0.9",
3
+ "version": "1.0.12",
4
4
  "description": "OpenChamber extension: live, interactive graph of the OmO sub-agents behind the current session",
5
5
  "private": true,
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-work",
3
- "version": "1.0.9",
3
+ "version": "1.0.12",
4
4
  "description": "OpenChamber 2.x Work Status section: OmO todo phases, monitors, eval cells and background work of the current session",
5
5
  "private": true,
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber",
3
- "version": "1.0.9",
3
+ "version": "1.0.12",
4
4
  "description": "OpenCode-compatible HTTP/SSE server that lets OpenChamber drive the pi coding agent (and OmO) over RPC",
5
5
  "type": "module",
6
6
  "license": "MIT",