antechamber 0.2.34 → 0.2.35

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,16 @@ and this project uses [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.35] - 2026-10-19
11
+
12
+ ### Fixed
13
+
14
+ - Keep OmO background subagents running in OpenChamber after the parent turn is stopped or its agent
15
+ process exits. Child status now follows OmO's `omo.task.updated` snapshots and task records, and a
16
+ child an older version wrongly marked cancelled recovers from its running record. Stopping the turn
17
+ no longer unblocks fork and revert while a background subagent runs; the error now names the
18
+ running subagent instead of asking to stop the turn.
19
+
10
20
  ## [0.2.34] - 2026-10-19
11
21
 
12
22
  ### Fixed
@@ -310,6 +310,7 @@ export class PiEventTranslator {
310
310
  break;
311
311
  case "extension_event":
312
312
  this.services.monitors.observe(s, ev);
313
+ this.services.subagents.observe(s, ev);
313
314
  break;
314
315
  default:
315
316
  break; // turn_start, queue_update — future milestones
package/dist/sessions.js CHANGED
@@ -251,7 +251,8 @@ export class SessionManager {
251
251
  }
252
252
  delete s.interruptRequested;
253
253
  this.events.settleBackground(s);
254
- this.subagents.stopParent(s.info.id);
254
+ // Its task cards end with the run; the tasks themselves may still run in OmO's shared host.
255
+ this.subagents.finishCalls(s.info.id, true);
255
256
  this.monitors.stopParent(s.info.id);
256
257
  void this.lifecycle
257
258
  .dormant(s)
@@ -500,9 +501,13 @@ export class SessionManager {
500
501
  if (!s)
501
502
  throw new SessionTreeError(`unknown session: ${sessionId}`);
502
503
  return this.operation(s, async () => {
503
- if (s.status.type !== "idle" || s.hasPendingInteraction() || s.hasRunningChildren()) {
504
+ if (s.status.type !== "idle" || s.hasPendingInteraction()) {
504
505
  throw new SessionTreeError("Session is busy; stop the turn before changing the conversation");
505
506
  }
507
+ // Stopping the turn does not stop a background subagent; it ends on its own or when cancelled.
508
+ if (s.hasRunningChildren()) {
509
+ throw new SessionTreeError("A subagent of this session is still running; wait for it to finish before changing the conversation");
510
+ }
506
511
  if (!s.pi)
507
512
  await this.revive(s);
508
513
  const pi = s.pi;
@@ -712,7 +717,8 @@ export class SessionManager {
712
717
  }
713
718
  s.cancelShell?.();
714
719
  s.approvals?.cancelAll();
715
- this.subagents.stopParent(sessionId);
720
+ // Background tasks outlive an abort; OmO itself cancels the ones the turn was waiting on.
721
+ this.subagents.finishCalls(sessionId, true);
716
722
  return true;
717
723
  }
718
724
  permissions(sessionId) {
@@ -57,6 +57,19 @@ export declare class SubagentBridge {
57
57
  start(parent: ManagedSession, original: OCToolPart, args: Record<string, unknown>): void;
58
58
  result(parent: ManagedSession, callID: string, result: ToolResult, ended: boolean): boolean;
59
59
  wake(parent: ManagedSession, event: Record<string, unknown>): void;
60
+ /**
61
+ * OmO's task bridge publishes `omo.task.updated` over the RPC extension channel: a replacement
62
+ * snapshot of every task under the parent, sent on attach (after OmO reconciles detached workers)
63
+ * and on each task store change (omo-task.js `W6`). A task missing from it is unknown, not ended:
64
+ * the snapshot is capped at 256 tasks.
65
+ */
66
+ observe(parent: ManagedSession, ev: Record<string, unknown>): void;
67
+ /**
68
+ * Merge task state reported by OmO (record, snapshot) unless it is older than what is known: an
69
+ * older update, or a running state that predates a terminal one OmO already reported. Terminal
70
+ * states are only ever taken from OmO, so nothing the adapter inferred can block a running task.
71
+ */
72
+ private applyNative;
60
73
  discover(parent: ManagedSession, messages: OCMessageWithParts[]): void;
61
74
  private discoverParts;
62
75
  private create;
@@ -90,7 +103,18 @@ export declare class SubagentBridge {
90
103
  private persist;
91
104
  private emitPart;
92
105
  private closeWatchers;
106
+ /**
107
+ * End the parent's task cards that have no running child. A card whose child runs is left to OmO:
108
+ * an abort cancels only a task the turn was still waiting on, and OmO reports that cancellation
109
+ * like any other end (omo-task.js `UZ`, "parent turn aborted"); background tasks keep running.
110
+ * After an abort the call stays tracked, because its tool result can still arrive.
111
+ */
93
112
  finishCalls(parentID: string, cancelled?: boolean): void;
113
+ /**
114
+ * Stop showing a deleted parent's children as live. Their status is not touched: OmO tasks outlive
115
+ * their parent's turn and process (shared-host workers detach, omo-task.js `DK`/`jK`), so only OmO
116
+ * can say a task ended.
117
+ */
94
118
  stopParent(parentID: string): void;
95
119
  /** Forget a deleted child and leave a registry tombstone; false when `id` is not a child. */
96
120
  remove(id: string): boolean;
package/dist/subagents.js CHANGED
@@ -29,6 +29,22 @@ function active(status) {
29
29
  function revivable(details) {
30
30
  return (details.residency_state === "resident" && ["completed", "error", "interrupted"].includes(String(details.status)));
31
31
  }
32
+ function timestamp(value) {
33
+ return typeof value === "string" ? Date.parse(value) : Number.NaN;
34
+ }
35
+ /**
36
+ * The lifecycle fields of an `omo.task.updated` row. `live_progress` changes on every event and is
37
+ * not task state; a text field OmO cut to its 32 000-character limit (`*_truncated`) must not
38
+ * replace the full value the task record carries.
39
+ */
40
+ function snapshotState(row) {
41
+ const { live_progress: _progress, final_response_truncated: responseCut, error_message_truncated: errorCut, description_truncated: _descriptionCut, ...data } = row;
42
+ if (responseCut === true)
43
+ delete data.final_response;
44
+ if (errorCut === true)
45
+ delete data.error_message;
46
+ return data;
47
+ }
32
48
  function terminal(status) {
33
49
  return ["completed", "failed", "error", "cancelled", "canceled", "killed", "lost", "denied", "plan_error"].includes(String(status));
34
50
  }
@@ -371,6 +387,50 @@ export class SubagentBridge {
371
387
  this.refresh(child, true, false);
372
388
  }
373
389
  }
390
+ /**
391
+ * OmO's task bridge publishes `omo.task.updated` over the RPC extension channel: a replacement
392
+ * snapshot of every task under the parent, sent on attach (after OmO reconciles detached workers)
393
+ * and on each task store change (omo-task.js `W6`). A task missing from it is unknown, not ended:
394
+ * the snapshot is capped at 256 tasks.
395
+ */
396
+ observe(parent, ev) {
397
+ if (ev.name !== "omo.task.updated" || !record(ev.data) || !Array.isArray(ev.data.tasks))
398
+ return;
399
+ for (const row of ev.data.tasks) {
400
+ if (!record(row) || !taskId(row.task_id))
401
+ continue;
402
+ const id = ocIdFor(row.task_id);
403
+ if (!this.children.has(id))
404
+ this.restore(id);
405
+ const child = this.children.get(id);
406
+ if (!child)
407
+ continue;
408
+ const data = snapshotState(row);
409
+ // OmO re-sends every row ~150 ms after each progress event of any task; only changes matter.
410
+ if (Object.entries(data).every(([key, value]) => child.task.details[key] === value))
411
+ continue;
412
+ this.wire(parent);
413
+ if (!this.applyNative(child, data))
414
+ continue;
415
+ child.live = true;
416
+ this.refresh(child, true, false);
417
+ }
418
+ }
419
+ /**
420
+ * Merge task state reported by OmO (record, snapshot) unless it is older than what is known: an
421
+ * older update, or a running state that predates a terminal one OmO already reported. Terminal
422
+ * states are only ever taken from OmO, so nothing the adapter inferred can block a running task.
423
+ */
424
+ applyNative(child, data) {
425
+ const updated = timestamp(data.updated_at);
426
+ const known = timestamp(child.task.details.updated_at);
427
+ if (updated < known)
428
+ return false;
429
+ if (active(data.status) && terminal(child.task.details.status) && updated <= (child.task.terminalAt ?? 0))
430
+ return false;
431
+ child.task.details = { ...child.task.details, ...data };
432
+ return true;
433
+ }
374
434
  discover(parent, messages) {
375
435
  this.discoverParts(parent, messages.flatMap((message) => message.parts), false);
376
436
  }
@@ -582,8 +642,12 @@ export class SubagentBridge {
582
642
  }
583
643
  refresh(child, publish, useRecord = true) {
584
644
  const archive = this.archive(child.task.id);
585
- const found = this.reconcileTaskRecord(child, archive, useRecord);
586
- const running = active(child.task.details.status) && (found !== undefined || child.live);
645
+ const found = this.reconcileTaskRecord(child, useRecord);
646
+ // OmO stops in-process workers with their parent and leaves them `running` but `persisted_only`
647
+ // until the revived parent reconciles them (omo-task.js `DK`/`jK`); they are not running.
648
+ const running = active(child.task.details.status) &&
649
+ child.task.details.residency_state !== "persisted_only" &&
650
+ (found !== undefined || child.live);
587
651
  const state = {
588
652
  previousMessages: child.session.messages ?? [],
589
653
  previousStatus: child.session.status.type,
@@ -599,16 +663,13 @@ export class SubagentBridge {
599
663
  this.persist(child);
600
664
  this.publishRefresh(child, state, publish);
601
665
  }
602
- reconcileTaskRecord(child, archive, useRecord) {
666
+ reconcileTaskRecord(child, useRecord) {
603
667
  const found = findTask(child.task.id, child.task.parentPiId);
604
668
  if (!found)
605
669
  return undefined;
606
670
  child.recordPath = found.path;
607
- const updated = typeof found.data.updated_at === "string" ? Date.parse(found.data.updated_at) : NaN;
608
- const retainedTerminal = Math.max(child.task.terminalAt ?? 0, existsSync(archive) ? statSync(archive).mtimeMs : 0);
609
- const staleRunning = active(found.data.status) && terminal(child.task.details.status) && updated <= retainedTerminal;
610
- if (useRecord && !staleRunning)
611
- child.task.details = { ...child.task.details, ...found.data };
671
+ if (useRecord)
672
+ this.applyNative(child, found.data);
612
673
  const host = found.data.host_session;
613
674
  if (record(host) && typeof host.session_path === "string") {
614
675
  child.livePath = host.session_path;
@@ -806,15 +867,24 @@ export class SubagentBridge {
806
867
  child.watchers.delete(dir);
807
868
  }
808
869
  }
870
+ /**
871
+ * End the parent's task cards that have no running child. A card whose child runs is left to OmO:
872
+ * an abort cancels only a task the turn was still waiting on, and OmO reports that cancellation
873
+ * like any other end (omo-task.js `UZ`, "parent turn aborted"); background tasks keep running.
874
+ * After an abort the call stays tracked, because its tool result can still arrive.
875
+ */
809
876
  finishCalls(parentID, cancelled = false) {
810
877
  for (const [key, parts] of this.calls) {
811
878
  if (!key.startsWith(`${parentID}|`))
812
879
  continue;
813
880
  const parent = this.sessions.get(parentID);
881
+ let waiting = false;
814
882
  for (const part of parts) {
815
883
  const childID = part.state.status !== "pending" ? part.state.metadata?.sessionID : undefined;
816
- if (!cancelled && typeof childID === "string" && this.children.get(childID)?.session.status.type === "busy")
884
+ if (typeof childID === "string" && this.children.get(childID)?.session.status.type === "busy") {
885
+ waiting = true;
817
886
  continue;
887
+ }
818
888
  updateTaskParts([part], {
819
889
  isError: true,
820
890
  details: { status: cancelled ? "cancelled" : "failed" },
@@ -823,19 +893,21 @@ export class SubagentBridge {
823
893
  if (parent)
824
894
  this.emitPart(parent, part);
825
895
  }
826
- this.calls.delete(key);
896
+ if (!cancelled || !waiting)
897
+ this.calls.delete(key);
827
898
  }
828
899
  }
900
+ /**
901
+ * Stop showing a deleted parent's children as live. Their status is not touched: OmO tasks outlive
902
+ * their parent's turn and process (shared-host workers detach, omo-task.js `DK`/`jK`), so only OmO
903
+ * can say a task ended.
904
+ */
829
905
  stopParent(parentID) {
830
906
  this.finishCalls(parentID, true);
831
907
  for (const child of this.children.values()) {
832
908
  if (child.session.info.parentID !== parentID)
833
909
  continue;
834
910
  this.stopParent(child.session.info.id);
835
- if (child.session.status.type === "busy") {
836
- child.task.details = { ...child.task.details, status: "cancelled" };
837
- this.refresh(child, true, false);
838
- }
839
911
  this.closeWatchers(child);
840
912
  }
841
913
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-compat",
3
- "version": "0.2.34",
3
+ "version": "0.2.35",
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": "0.2.34",
3
+ "version": "0.2.35",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber",
3
- "version": "0.2.34",
3
+ "version": "0.2.35",
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",