antechamber 1.0.8 → 1.0.10

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,20 @@ and this project uses [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.10] - 2026-10-21
11
+
12
+ ### Fixed
13
+
14
+ - Keep sessions with many child tasks responsive by ignoring unchanged task snapshots and saving
15
+ each snapshot's changes in one registry write.
16
+
17
+ ## [1.0.9] - 2026-10-21
18
+
19
+ ### Fixed
20
+
21
+ - Reduce repeated native-title scans and task-record lookups while restoring and listing sessions,
22
+ preserving first-list costs, child-session identities and later native updates.
23
+
10
24
  ## [1.0.8] - 2026-10-20
11
25
 
12
26
  ### Fixed
@@ -107,6 +107,7 @@ export declare class SessionManager {
107
107
  private rollbackCreate;
108
108
  private applyNativeTitle;
109
109
  private refreshNativeTitles;
110
+ private refreshNativeTitle;
110
111
  private wirePi;
111
112
  /** pi writes `<ts>_<sessionId>.jsonl` at startup — poll briefly for it. */
112
113
  private resolvePiFile;
package/dist/sessions.js CHANGED
@@ -209,13 +209,15 @@ export class SessionManager {
209
209
  this.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
210
210
  }
211
211
  refreshNativeTitles() {
212
- for (const s of this.sessions.values()) {
213
- if (s.pi !== undefined || s.restored === undefined || !existsSync(s.restored.piPath))
214
- continue;
215
- const metadata = readSessionTitle(s.restored.piPath, s.restored.piSessionId);
216
- if (metadata.titleSource === "native")
217
- this.applyNativeTitle(s, metadata.title);
218
- }
212
+ for (const s of this.sessions.values())
213
+ this.refreshNativeTitle(s);
214
+ }
215
+ refreshNativeTitle(s) {
216
+ if (s.pi !== undefined || s.restored === undefined || !existsSync(s.restored.piPath))
217
+ return;
218
+ const metadata = readSessionTitle(s.restored.piPath, s.restored.piSessionId);
219
+ if (metadata.titleSource === "native")
220
+ this.applyNativeTitle(s, metadata.title);
219
221
  }
220
222
  wirePi(s, pi) {
221
223
  s.unwirePi?.();
@@ -345,7 +347,7 @@ export class SessionManager {
345
347
  return undefined;
346
348
  const live = this.sessions.get(sessionId);
347
349
  if (live !== undefined) {
348
- this.refreshNativeTitles();
350
+ this.refreshNativeTitle(live);
349
351
  this.sessionInfo(live);
350
352
  return live;
351
353
  }
@@ -51,8 +51,6 @@ export declare class SubagentBridge {
51
51
  private readonly children;
52
52
  private readonly calls;
53
53
  private readonly removed;
54
- /** Parent pi session of each task record already read; a record's parent never changes. */
55
- private readonly taskParents;
56
54
  constructor(store: SessionStore, sessions: Map<string, ManagedSession>, emit: (directory: string, event: OCEvent) => void, parse: ParseHistory);
57
55
  private archive;
58
56
  private sessionId;
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";
@@ -211,9 +212,81 @@ function childRecord(id, path) {
211
212
  const data = jsonObject(path);
212
213
  return data?.task_id === id && typeof data.parent_session_id === "string" ? data : undefined;
213
214
  }
215
+ // History parsing calls the exported helpers synchronously, including from nested child discovery.
216
+ let taskLookupPass;
217
+ function withTaskLookups(store, action) {
218
+ if (taskLookupPass?.store === store)
219
+ return action();
220
+ const previous = taskLookupPass;
221
+ const pass = {
222
+ store,
223
+ childIds: store.childIds(),
224
+ identities: new Map(),
225
+ childByPi: new Map(),
226
+ piByChild: new Map(),
227
+ };
228
+ try {
229
+ taskLookupPass = pass;
230
+ for (const id of pass.childIds) {
231
+ const entry = store.get(id);
232
+ if (entry?.task)
233
+ indexChildIdentity(store, id, entry.task, entry.deleted ? undefined : entry.piSessionId);
234
+ }
235
+ return action();
236
+ }
237
+ finally {
238
+ taskLookupPass = previous;
239
+ }
240
+ }
241
+ function indexChildIdentity(store, id, task, piSessionId) {
242
+ const pass = taskLookupPass;
243
+ if (pass?.store !== store)
244
+ return;
245
+ let parents = pass.identities.get(task.id);
246
+ if (parents === undefined)
247
+ pass.identities.set(task.id, (parents = new Map()));
248
+ if (!parents.has(task.parentPiId))
249
+ parents.set(task.parentPiId, id);
250
+ if (piSessionId !== undefined) {
251
+ const previous = pass.piByChild.get(id);
252
+ if (previous !== undefined && previous !== piSessionId && pass.childByPi.get(previous) === id)
253
+ pass.childByPi.delete(previous);
254
+ pass.piByChild.set(id, piSessionId);
255
+ pass.childByPi.set(piSessionId, id);
256
+ }
257
+ }
258
+ function indexedTaskRecords() {
259
+ const pass = taskLookupPass;
260
+ if (pass === undefined)
261
+ return undefined;
262
+ if (pass.records !== undefined)
263
+ return pass.records;
264
+ const all = [];
265
+ const byId = new Map();
266
+ const byParent = new Map();
267
+ for (const { id, path } of taskFiles()) {
268
+ const data = childRecord(id, path);
269
+ if (data === undefined)
270
+ continue;
271
+ const row = { id, path, data };
272
+ all.push(row);
273
+ const sameId = byId.get(id) ?? [];
274
+ sameId.push(row);
275
+ byId.set(id, sameId);
276
+ const parent = String(data.parent_session_id);
277
+ const sameParent = byParent.get(parent) ?? [];
278
+ sameParent.push(row);
279
+ byParent.set(parent, sameParent);
280
+ }
281
+ return (pass.records = { all, byId, byParent });
282
+ }
214
283
  /** Keep bookmarks for known tasks; a bare native id is only safe while it has one parent. */
215
- export function taskSessionId(id, parentPiId, store = new SessionStore()) {
216
- for (const sessionID of store.childIds()) {
284
+ export function taskSessionId(id, parentPiId, store = taskLookupPass?.store ?? new SessionStore()) {
285
+ const pass = taskLookupPass?.store === store ? taskLookupPass : undefined;
286
+ const known = pass?.identities.get(id)?.get(parentPiId);
287
+ if (known !== undefined)
288
+ return known;
289
+ for (const sessionID of pass === undefined ? store.childIds() : []) {
217
290
  const task = store.get(sessionID)?.task;
218
291
  if (task?.id === id && task.parentPiId === parentPiId)
219
292
  return sessionID;
@@ -228,6 +301,10 @@ export function taskSessionId(id, parentPiId, store = new SessionStore()) {
228
301
  const qualifiedID = ocIdFor(`${parentPiId}:${id}`);
229
302
  if (store.get(qualifiedID))
230
303
  return qualifiedID;
304
+ const indexed = indexedTaskRecords();
305
+ if (indexed !== undefined) {
306
+ return indexed.byId.get(id)?.some(({ data }) => data.parent_session_id !== parentPiId) ? qualifiedID : legacyID;
307
+ }
231
308
  for (const root of taskStores()) {
232
309
  const data = childRecord(id, join(root, "tasks", `${id}.json`));
233
310
  if (data && data.parent_session_id !== parentPiId)
@@ -236,10 +313,11 @@ export function taskSessionId(id, parentPiId, store = new SessionStore()) {
236
313
  return legacyID;
237
314
  }
238
315
  /** Reverse lookup never resolves an unbound, ambiguous bare id by store enumeration order. */
239
- export function findTaskBySession(sessionId, store = new SessionStore()) {
316
+ export function findTaskBySession(sessionId, store = taskLookupPass?.store ?? new SessionStore()) {
240
317
  let found;
241
- for (const { id, path } of taskFiles()) {
242
- const data = childRecord(id, path);
318
+ const rows = indexedTaskRecords()?.all ?? taskFiles();
319
+ for (const { id, path, data: cached } of rows) {
320
+ const data = cached ?? childRecord(id, path);
243
321
  if (!data || taskSessionId(id, String(data.parent_session_id), store) !== sessionId)
244
322
  continue;
245
323
  if (found && found.data.parent_session_id !== data.parent_session_id)
@@ -270,10 +348,11 @@ export function evalAgentSession(parentPiId, args, claimed, after) {
270
348
  if (parentPiId === undefined || typeof args.prompt !== "string")
271
349
  return undefined;
272
350
  let best;
273
- for (const { id, path } of taskFiles()) {
351
+ const rows = indexedTaskRecords()?.byParent.get(parentPiId) ?? (taskLookupPass ? [] : taskFiles());
352
+ for (const { id, path, data: cached } of rows) {
274
353
  if (claimed.has(id))
275
354
  continue;
276
- const data = childRecord(id, path);
355
+ const data = cached ?? childRecord(id, path);
277
356
  if (data?.parent_session_id !== parentPiId)
278
357
  continue;
279
358
  const spec = record(data.spawn_spec) ? data.spawn_spec : {};
@@ -306,6 +385,9 @@ export function evalAgentSession(parentPiId, args, claimed, after) {
306
385
  export function findTask(id, parentPiId) {
307
386
  if (!taskId(id))
308
387
  return undefined;
388
+ const indexed = indexedTaskRecords();
389
+ if (indexed !== undefined)
390
+ return indexed.byId.get(id)?.find(({ data }) => data.parent_session_id === parentPiId);
309
391
  for (const store of taskStores()) {
310
392
  const path = join(store, "tasks", `${id}.json`);
311
393
  const data = jsonObject(path);
@@ -391,8 +473,6 @@ export class SubagentBridge {
391
473
  children = new Map();
392
474
  calls = new Map();
393
475
  removed = new Set();
394
- /** Parent pi session of each task record already read; a record's parent never changes. */
395
- taskParents = new Map();
396
476
  constructor(store, sessions, emit, parse) {
397
477
  this.store = store;
398
478
  this.sessions = sessions;
@@ -462,28 +542,31 @@ export class SubagentBridge {
462
542
  observe(parent, ev) {
463
543
  if (ev.name !== "omo.task.updated" || !record(ev.data) || !Array.isArray(ev.data.tasks))
464
544
  return;
465
- for (const row of ev.data.tasks) {
466
- if (!record(row) || !taskId(row.task_id))
467
- continue;
468
- if (typeof row.parent_session_id === "string" &&
469
- row.parent_session_id !== (parent.piSessionId ?? parent.restored?.piSessionId))
470
- continue;
471
- const id = this.sessionId(parent, row.task_id);
472
- if (!this.children.has(id))
473
- this.restore(id);
474
- const child = this.children.get(id);
475
- if (!child || child.session.info.parentID !== parent.info.id)
476
- continue;
477
- const data = snapshotState(row);
478
- // OmO re-sends every row ~150 ms after each progress event of any task; only changes matter.
479
- if (Object.entries(data).every(([key, value]) => child.task.details[key] === value))
480
- continue;
481
- this.wire(parent);
482
- if (!this.applyNative(child, data))
483
- continue;
484
- child.live = true;
485
- this.refresh(child, true, false);
486
- }
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
+ }));
487
570
  }
488
571
  /**
489
572
  * Merge task state reported by OmO (record, snapshot) unless it is older than what is known: an
@@ -501,7 +584,7 @@ export class SubagentBridge {
501
584
  return true;
502
585
  }
503
586
  discover(parent, messages) {
504
- this.discoverParts(parent, messages.flatMap((message) => message.parts), false);
587
+ withTaskLookups(this.store, () => this.discoverParts(parent, messages.flatMap((message) => message.parts), false));
505
588
  }
506
589
  discoverParts(parent, parts, live) {
507
590
  this.wire(parent);
@@ -562,23 +645,26 @@ export class SubagentBridge {
562
645
  };
563
646
  this.children.set(session.info.id, child);
564
647
  this.sessions.set(session.info.id, session);
648
+ indexChildIdentity(this.store, session.info.id, task);
565
649
  this.wire(session);
566
650
  this.emit(session.info.directory, { type: "session.created", properties: { info: session.info } });
567
651
  return child;
568
652
  }
569
653
  restore(id) {
570
- const known = this.children.get(id);
571
- if (known)
572
- return known.session;
573
- const entry = this.store.get(id);
574
- if (!entry)
575
- return this.removed.has(id) ? undefined : this.adopt(id);
576
- if (entry.deleted || !entry.task || !entry.parentID)
577
- return undefined;
578
- const child = this.create(this.detachedParent(entry.parentID, entry.directory, entry), entry.task, id);
579
- child.session.info.title = entry.title ?? child.session.info.title;
580
- this.refresh(child, false);
581
- return child.session;
654
+ return withTaskLookups(this.store, () => {
655
+ const known = this.children.get(id);
656
+ if (known)
657
+ return known.session;
658
+ const entry = this.store.get(id);
659
+ if (!entry)
660
+ return this.removed.has(id) ? undefined : this.adopt(id);
661
+ if (entry.deleted || !entry.task || !entry.parentID)
662
+ return undefined;
663
+ const child = this.create(this.detachedParent(entry.parentID, entry.directory, entry), entry.task, id);
664
+ child.session.info.title = entry.title ?? child.session.info.title;
665
+ this.refresh(child, false);
666
+ return child.session;
667
+ });
582
668
  }
583
669
  /**
584
670
  * Tasks spawned outside a `task` tool call (e.g. `agent()` inside an `eval`
@@ -623,6 +709,8 @@ export class SubagentBridge {
623
709
  return this.sessions.get(id) ?? (entry ? this.detachedParent(id, entry.directory, entry) : undefined);
624
710
  }
625
711
  childIdsByPi() {
712
+ if (taskLookupPass?.store === this.store)
713
+ return taskLookupPass.childByPi;
626
714
  const byPi = new Map();
627
715
  for (const id of this.store.childIds()) {
628
716
  const entry = this.store.get(id);
@@ -656,26 +744,20 @@ export class SubagentBridge {
656
744
  };
657
745
  }
658
746
  restoreAll() {
659
- for (const id of this.store.childIds())
660
- this.restore(id);
661
- const childByPi = this.childIdsByPi();
662
- for (const { id, path } of taskFiles()) {
663
- let parentPi = this.taskParents.get(path);
664
- if (parentPi === undefined) {
665
- const parent = childRecord(id, path)?.parent_session_id;
666
- if (typeof parent !== "string")
747
+ withTaskLookups(this.store, () => {
748
+ for (const id of taskLookupPass?.childIds ?? [])
749
+ this.restore(id);
750
+ const childByPi = this.childIdsByPi();
751
+ for (const { id, path, data } of indexedTaskRecords()?.all ?? []) {
752
+ const parentPi = String(data.parent_session_id);
753
+ const sessionId = taskSessionId(id, parentPi, this.store);
754
+ if (this.children.has(sessionId) || this.removed.has(sessionId) || this.store.get(sessionId))
755
+ continue;
756
+ if (!this.parentFor(parentPi, childByPi))
667
757
  continue;
668
- this.taskParents.set(path, (parentPi = parent));
669
- }
670
- const sessionId = taskSessionId(id, parentPi, this.store);
671
- if (this.children.has(sessionId) || this.removed.has(sessionId) || this.store.get(sessionId))
672
- continue;
673
- if (!this.parentFor(parentPi, childByPi))
674
- continue;
675
- const data = childRecord(id, path);
676
- if (data)
677
758
  this.adopt(sessionId, { path, data });
678
- }
759
+ }
760
+ });
679
761
  }
680
762
  owns(id) {
681
763
  return this.children.has(id);
@@ -936,6 +1018,7 @@ export class SubagentBridge {
936
1018
  created: info.time.created,
937
1019
  updated: info.time.updated,
938
1020
  });
1021
+ indexChildIdentity(this.store, info.id, child.task, child.session.piSessionId ?? child.task.id);
939
1022
  }
940
1023
  catch (error) {
941
1024
  if (!(error instanceof SessionPersistenceError))
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-compat",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
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.8",
3
+ "version": "1.0.10",
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.8",
3
+ "version": "1.0.10",
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.8",
3
+ "version": "1.0.10",
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",