antechamber 1.0.24 → 1.0.26

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.
@@ -2,6 +2,7 @@
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { evalAgentSession } from "./subagents.js";
4
4
  import { MONITOR_NOTIFICATION } from "./monitor-sessions.js";
5
+ import { terminalShellId } from "./omo-shells.js";
5
6
  import { applyProviderToolResults, finishProviderToolParts, mapTool, nestedToolParts, providerToolParts, resultText, toolFailed, } from "./tool-map.js";
6
7
  import { TodoBridge } from "./todo-bridge.js";
7
8
  import { readSessionTree, recordTurn } from "./session-tree.js";
@@ -150,11 +151,28 @@ export function compactionMessages(s, key, summary, created, model, thinkingLeve
150
151
  },
151
152
  ];
152
153
  }
154
+ /**
155
+ * Events after which OmO's background work may have started or ended: monitor and wake-source snapshots,
156
+ * tool results that launch work, and the custom messages OmO sends when a background session finishes.
157
+ */
158
+ const SHELL_SYNC_EVENTS = new Set([
159
+ "extension_event",
160
+ "tool_execution_end",
161
+ "turn_end",
162
+ "agent_end",
163
+ "agent_idle",
164
+ "message_end",
165
+ ]);
153
166
  /** Translates per-session events; registry and lifecycle operations stay with SessionManager. */
154
167
  export class PiEventTranslator {
155
168
  services;
156
169
  /** Ended assistant messages whose completion waits on their tools. */
157
170
  awaitingTools = new WeakSet();
171
+ /**
172
+ * Errored assistant messages held open until agent_end says whether OmO retries them. OpenChamber prints a
173
+ * completed message's error as "failed to send message", and v2 cannot retract a `session.step.failed`.
174
+ */
175
+ heldFailures = new WeakMap();
158
176
  constructor(services) {
159
177
  this.services = services;
160
178
  }
@@ -316,6 +334,8 @@ export class PiEventTranslator {
316
334
  default:
317
335
  break; // turn_start, queue_update — future milestones
318
336
  }
337
+ if (SHELL_SYNC_EVENTS.has(String(ev.type)))
338
+ this.services.shells.sync(s);
319
339
  }
320
340
  onTodoEntry(s, ev) {
321
341
  s.todoBridge ??= new TodoBridge(s.info.id);
@@ -335,9 +355,11 @@ export class PiEventTranslator {
335
355
  const timestamp = "timestamp" in value ? value.timestamp : undefined;
336
356
  const parsed = typeof timestamp === "string" ? Date.parse(timestamp) : timestamp;
337
357
  const created = typeof parsed === "number" && Number.isFinite(parsed) ? parsed : Date.now();
338
- const part = completeBackgroundEval(messages, result, created);
358
+ const part = completeBackgroundEval(messages, result, created, s.pi?.startedAtMs);
339
359
  const prefix = `msg_background_${result.key}_`;
340
360
  const unbound = messages.findLast((message) => message.info.id.startsWith(prefix) && s.tree.messageEntries[message.info.id] === undefined);
361
+ if (source === "message")
362
+ this.services.shells.noteEnding(s, result);
341
363
  // A finished shell also ends the wait of the card that launched it (once, from the live message).
342
364
  if (result.kind === "terminal" && source === "message")
343
365
  for (const waiting of releaseBackground(messages, result.callID, created))
@@ -377,8 +399,11 @@ export class PiEventTranslator {
377
399
  this.setStatus(s, { type: "busy" });
378
400
  return;
379
401
  case "agent_end":
380
- if (ev.willRetry === true)
381
- return; // auto-retry keeps the session busy
402
+ if (ev.willRetry === true) {
403
+ // auto-retry keeps the session busy, and the failed step it retries is not a failure to report.
404
+ this.completeHeldFailures(s, false);
405
+ return;
406
+ }
382
407
  if (s.interruptRequested)
383
408
  this.finishInterruptedRun(s, { name: "MessageAbortedError", data: { message: "Cancelled" } });
384
409
  s.approvals?.finishTurn();
@@ -582,15 +607,19 @@ export class PiEventTranslator {
582
607
  s.streaming.message.modelID = s.model.modelID;
583
608
  }
584
609
  applyUsage(s.streaming.message, ended.usage);
585
- Object.assign(s.streaming.message, piStopReason(ended.stopReason, ended.errorMessage));
610
+ const stop = piStopReason(ended.stopReason, ended.errorMessage);
611
+ if (ended.stopReason === "error")
612
+ this.heldFailures.set(s.streaming.message, stop);
613
+ else
614
+ Object.assign(s.streaming.message, stop);
586
615
  // OpenCode completes an assistant message only after its tools settle, and
587
616
  // OpenChamber marks active tools under a completed message as Interrupted.
588
617
  // pi ends a toolUse message before executing its tool calls, so defer its
589
618
  // completion to the last tool result (applyToolResults), the next step, or
590
- // the run end. Errored/aborted messages never run their tools.
619
+ // the run end. Errored/aborted messages never run their tools; an errored one completes at agent_end.
591
620
  if (ended.stopReason === "toolUse" && hasActiveTools(s, s.streaming.message.id))
592
621
  this.awaitingTools.add(s.streaming.message);
593
- else
622
+ else if (ended.stopReason !== "error")
594
623
  s.streaming.message.time.completed = Date.now();
595
624
  this.services.emit(s.info.directory, { type: "message.updated", properties: { info: s.streaming.message } });
596
625
  this.finishStreaming(s);
@@ -885,7 +914,7 @@ export class PiEventTranslator {
885
914
  continue;
886
915
  s.toolByCallId.delete(callID);
887
916
  s.todoBridge?.finish(part, toolFailed(tr));
888
- finishToolPart(part, tr, callID, Date.now());
917
+ finishToolPart(part, tr, callID, Date.now(), s.pi?.startedAtMs);
889
918
  if (touchedFiles([part]).length > 0)
890
919
  this.services.dirtyDiffs.add(s.info.id);
891
920
  this.emitPart(s, part);
@@ -904,6 +933,11 @@ export class PiEventTranslator {
904
933
  onNestedTools(s, parent, details) {
905
934
  const nested = nestedToolParts(parent, details, {
906
935
  monitor: (args) => this.services.monitors.findChild(s.info.id, args),
936
+ monitorShell: (childID) => {
937
+ const bashId = this.services.monitors.terminalId(childID);
938
+ const epoch = s.pi?.startedAtMs;
939
+ return bashId !== undefined && epoch !== undefined ? terminalShellId(epoch, bashId) : undefined;
940
+ },
907
941
  agent: (args, claimed, after) => evalAgentSession(s.piSessionId ?? s.restored?.piSessionId, args, claimed, after),
908
942
  });
909
943
  if (nested.length === 0)
@@ -946,8 +980,23 @@ export class PiEventTranslator {
946
980
  s.toolByCallId.clear();
947
981
  this.completeAwaitingTools(s); // the run is over
948
982
  }
983
+ /** Complete held errored messages: with their error when the run failed, without it when OmO retries the step. */
984
+ completeHeldFailures(s, failed) {
985
+ const now = Date.now();
986
+ for (const { info } of s.messages ?? []) {
987
+ const stop = this.heldFailures.get(info);
988
+ if (stop === undefined || info.role !== "assistant")
989
+ continue;
990
+ this.heldFailures.delete(info);
991
+ if (failed)
992
+ Object.assign(info, stop);
993
+ info.time.completed = now;
994
+ this.services.emit(s.info.directory, { type: "message.updated", properties: { info } });
995
+ }
996
+ }
949
997
  /** Complete every ended message of this session still waiting on its tools. */
950
998
  completeAwaitingTools(s) {
999
+ this.completeHeldFailures(s, true);
951
1000
  const now = Date.now();
952
1001
  for (const { info } of s.messages ?? []) {
953
1002
  if (!this.awaitingTools.has(info) || info.role !== "assistant")
package/dist/routes-v2.js CHANGED
@@ -14,6 +14,8 @@ import { getAgentName } from "./agent-name.js";
14
14
  import { vcsRead, VcsError } from "./vcs.js";
15
15
  import { announceSessionError, compactSession, getCommands, unsupportedShare } from "./session-commands.js";
16
16
  import { defaultV2Model, flattenModels, toV2Message, toV2Session } from "./v2.js";
17
+ import { SHELL_CANCELLATION_KEY } from "./omo-shells.js";
18
+ import { WorkStatusError } from "./work-status.js";
17
19
  import { agentDescriptors, modelRefFromBody, numberParam, OPENCODE_COMPAT_VERSION, promptAsync, runSessionShell, runSessionSlash, sessionDirectory, sessionMetadata, } from "./route-services.js";
18
20
  async function v2Agents(services) {
19
21
  return (await agentDescriptors(services)).map(({ model, variant, ...agent }) => ({
@@ -169,6 +171,41 @@ async function handleV2Bootstrap(services, res, path, method, directory) {
169
171
  }
170
172
  return false;
171
173
  }
174
+ /** Shell ids named by OpenChamber's stop note (background-shell.ts `shellCancellationNote`). */
175
+ function cancelledShells(metadata) {
176
+ const note = metadata?.[SHELL_CANCELLATION_KEY];
177
+ if (note === null || typeof note !== "object")
178
+ return [];
179
+ const ids = "shellIDs" in note ? note.shellIDs : "shellID" in note ? [note.shellID] : [];
180
+ return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
181
+ }
182
+ async function handleShell(services, res, id, output, method, url, withLocation) {
183
+ const shells = services.sessions.shells;
184
+ const info = shells.get(id);
185
+ if (info === undefined) {
186
+ json(res, 404, errorBody("NotFoundError", `Shell not found: ${id}`));
187
+ return;
188
+ }
189
+ try {
190
+ if (method === "DELETE") {
191
+ await shells.stop(id);
192
+ res.writeHead(204).end();
193
+ }
194
+ else if (output) {
195
+ const cursor = numberParam(url.searchParams.get("cursor"));
196
+ const limit = numberParam(url.searchParams.get("limit"));
197
+ // @opencode/client returns the body and OpenChamber reads `.data` (lib/opencode/client.ts readShellOutput).
198
+ json(res, 200, withLocation(await shells.output(id, cursor, limit)));
199
+ }
200
+ else
201
+ json(res, 200, withLocation(info));
202
+ }
203
+ catch (error) {
204
+ if (!(error instanceof WorkStatusError))
205
+ throw error;
206
+ json(res, error.status, errorBody(error.status === 404 ? "NotFoundError" : "UnsupportedError", error.message));
207
+ }
208
+ }
172
209
  export async function handleV2(services, req, res, url, path, method, directory) {
173
210
  const location = { directory };
174
211
  const withLocation = (data) => ({ location, data });
@@ -235,10 +272,18 @@ export async function handleV2(services, req, res, url, path, method, directory)
235
272
  });
236
273
  return;
237
274
  }
238
- // Read-only inventories OpenChamber loads at bootstrap. OmO has no OpenCode-style shells or
239
- // web-search providers, so each is an honest empty list (mutations stay 400);
240
- // OmO monitors appear in the OmO Work status section instead (work-status.ts).
241
- if ((path === "/api/shell" || path === "/api/websearch/provider") && method === "GET") {
275
+ // OmO's background work (monitors, background terminal sessions, detached eval cells) as OpenCode shells.
276
+ if (path === "/api/shell" && method === "GET") {
277
+ json(res, 200, withLocation(services.sessions.shells.list(directory)));
278
+ return;
279
+ }
280
+ const shellMatch = path.match(/^\/api\/shell\/([^/]+)(\/output)?$/);
281
+ if (shellMatch && (method === "GET" || (method === "DELETE" && shellMatch[2] === undefined))) {
282
+ await handleShell(services, res, decodeURIComponent(shellMatch[1]), shellMatch[2] !== undefined, method, url, withLocation);
283
+ return;
284
+ }
285
+ // OmO has no web-search providers: an honest empty list.
286
+ if (path === "/api/websearch/provider" && method === "GET") {
242
287
  json(res, 200, withLocation([]));
243
288
  return;
244
289
  }
@@ -666,6 +711,13 @@ export async function handleV2(services, req, res, url, path, method, directory)
666
711
  return;
667
712
  }
668
713
  const metadata = sessionMetadata(body.metadata);
714
+ const unstoppable = cancelledShells(metadata).filter((id) => !services.sessions.shells.stoppable(id));
715
+ if (unstoppable.length > 0) {
716
+ // OpenChamber sends this note before it removes the shells and stops nothing when the note fails:
717
+ // refuse it rather than tell the agent about a stop that cannot happen.
718
+ json(res, 400, errorBody("UnsupportedError", `OmO cannot stop ${unstoppable.join(", ")}: only eval cells can be stopped`));
719
+ return;
720
+ }
669
721
  const messageID = typeof body.id === "string" && body.id !== "" ? body.id : `msg_${randomUUID().replaceAll("-", "")}`;
670
722
  const text = body.text;
671
723
  promptAsync(services, managed, text, undefined, true, messageID, body.delivery ?? "steer", undefined, metadata);
@@ -11,6 +11,7 @@ import { type SessionTreeState } from "./session-tree.js";
11
11
  import { SubagentBridge } from "./subagents.js";
12
12
  import { MonitorSessions } from "./monitor-sessions.js";
13
13
  import { WorkStatusBridge } from "./work-status.js";
14
+ import { OmoShells } from "./omo-shells.js";
14
15
  import { SessionStore, type TitleSource } from "./session-store.js";
15
16
  import type { OCAssistantMessage, OCEvent, OCMessageWithParts, OCModelRef, OCPart, OCPromptPart, OCSession, OCSessionStatus, OCToolPart } from "./types.js";
16
17
  export { ADAPTER_VERSION } from "./version.js";
@@ -75,6 +76,8 @@ export interface ManagedSession extends ThinkingState {
75
76
  hasBackgroundWork?: () => boolean;
76
77
  /** Live wake sources OmO publishes over RPC (detached eval cells), which also block idle eviction. */
77
78
  hasWakeSources?: () => boolean;
79
+ /** antechamber stopped the agent process itself (lifecycle `dormant`): its in-process work died with it. */
80
+ onDormant?: () => void;
78
81
  /** Why the agent last stopped on its own; persisted and replayed as a notice until the next prompt. */
79
82
  lastStop?: {
80
83
  message: string;
@@ -101,6 +104,7 @@ export declare class SessionManager {
101
104
  readonly subagents: SubagentBridge;
102
105
  readonly monitors: MonitorSessions;
103
106
  readonly workStatus: WorkStatusBridge;
107
+ readonly shells: OmoShells;
104
108
  /** directory → last scan freshness (pi dir mtime + wall clock). */
105
109
  private scanCache;
106
110
  /** freshness of the all-directories scan (see listAll). */
@@ -229,6 +233,8 @@ export declare class SessionManager {
229
233
  /** Adopt every restorable pi session on disk, regardless of directory. */
230
234
  private ensureAllScanned;
231
235
  private adopt;
236
+ /** The same cleanup as an agent exit, for a process antechamber disposed after unwiring its exit handler. */
237
+ private wireDormant;
232
238
  /** Resolve a bare session id (e.g. deep link after restart) via known directories. */
233
239
  private resolveRestored;
234
240
  /** Registry write for adapter-owned metadata (title override, pi path). */
package/dist/sessions.js CHANGED
@@ -18,6 +18,7 @@ import { bindMessageEntries, branchEntries, durableShellTree, followExternalAppe
18
18
  import { SubagentBridge } from "./subagents.js";
19
19
  import { MonitorSessions } from "./monitor-sessions.js";
20
20
  import { WorkStatusBridge } from "./work-status.js";
21
+ import { OmoShells } from "./omo-shells.js";
21
22
  import { sessionDiff } from "./session-diff.js";
22
23
  import { announceSessionNotice, noticeMessage, skillCommandText, slashTitlePrompt } from "./session-commands.js";
23
24
  import { ocIdFor, piSessionDir, piSessionsRootMtime, scanAllPiSessions, scanPiSessions, SessionStore, readSessionFileMeta, } from "./session-store.js";
@@ -64,6 +65,7 @@ export class SessionManager {
64
65
  subagents;
65
66
  monitors;
66
67
  workStatus;
68
+ shells;
67
69
  /** directory → last scan freshness (pi dir mtime + wall clock). */
68
70
  scanCache = new Map();
69
71
  /** freshness of the all-directories scan (see listAll). */
@@ -76,11 +78,13 @@ export class SessionManager {
76
78
  this.subagents = new SubagentBridge(this.store, this.sessions, this.emit, (session, raw) => parsePiSessionText(session, getAgentName(), raw));
77
79
  this.monitors = new MonitorSessions(this.sessions, this.emit);
78
80
  this.workStatus = new WorkStatusBridge(this.monitors);
81
+ this.shells = new OmoShells(this.sessions, this.monitors, this.workStatus, this.emit);
79
82
  this.events = new PiEventTranslator({
80
83
  emit: this.emit,
81
84
  subagents: this.subagents,
82
85
  monitors: this.monitors,
83
86
  workStatus: this.workStatus,
87
+ shells: this.shells,
84
88
  store: this.store,
85
89
  dirtyDiffs: this.dirtyDiffs,
86
90
  ensureMessages: (s) => this.ensureMessages(s),
@@ -152,6 +156,7 @@ export class SessionManager {
152
156
  this.subagents.wire(managed);
153
157
  this.monitors.wire(managed);
154
158
  this.workStatus.wire(managed);
159
+ this.wireDormant(managed);
155
160
  this.lifecycle.touch(managed);
156
161
  this.wirePi(managed, pi);
157
162
  if (options.model !== undefined)
@@ -291,6 +296,7 @@ export class SessionManager {
291
296
  this.subagents.finishCalls(s.info.id, true);
292
297
  this.monitors.stopParent(s.info.id);
293
298
  this.workStatus.stopParent(s.info.id);
299
+ this.shells.stopParent(s.info.id);
294
300
  void this.lifecycle
295
301
  .dormant(s)
296
302
  .catch((error) => console.error("[lifecycle] exit cleanup failed:", error));
@@ -401,6 +407,7 @@ export class SessionManager {
401
407
  const child = this.subagents.remove(sessionId);
402
408
  this.monitors.remove(sessionId);
403
409
  this.workStatus.stopParent(sessionId);
410
+ this.shells.stopParent(sessionId);
404
411
  if (piPath !== undefined) {
405
412
  try {
406
413
  unlinkSync(piPath);
@@ -1484,8 +1491,17 @@ export class SessionManager {
1484
1491
  this.subagents.wire(managed);
1485
1492
  this.monitors.wire(managed);
1486
1493
  this.workStatus.wire(managed);
1494
+ this.wireDormant(managed);
1487
1495
  return managed;
1488
1496
  }
1497
+ /** The same cleanup as an agent exit, for a process antechamber disposed after unwiring its exit handler. */
1498
+ wireDormant(s) {
1499
+ s.onDormant = () => {
1500
+ this.monitors.stopParent(s.info.id);
1501
+ this.workStatus.stopParent(s.info.id);
1502
+ this.shells.stopParent(s.info.id);
1503
+ };
1504
+ }
1489
1505
  /** Resolve a bare session id (e.g. deep link after restart) via known directories. */
1490
1506
  resolveRestored(sessionId) {
1491
1507
  const reg = this.store.get(sessionId);
@@ -32,6 +32,8 @@ export declare function mapTool(omoName: string, args: unknown, result?: ToolRes
32
32
  export interface NestedSessions {
33
33
  /** The monitor child session for a recorded `monitor` call. */
34
34
  monitor?: (args: Record<string, unknown>) => string | undefined;
35
+ /** The OpenCode shell id of a monitor child session's watch while it runs (omo-shells.ts). */
36
+ monitorShell?: (sessionID: string) => string | undefined;
35
37
  /** The subagent a recorded `agent()` call started; `claimed` keeps repeated calls apart. */
36
38
  agent?: (args: Record<string, unknown>, claimed: Set<string>, after: number) => EvalAgent | null | undefined;
37
39
  }
package/dist/tool-map.js CHANGED
@@ -200,6 +200,9 @@ export function nestedToolParts(parent, details, sessions = {}) {
200
200
  const command = typeof args.command === "string" ? args.command : "";
201
201
  const label = typeof args.description === "string" && args.description !== "" ? args.description : command;
202
202
  mapped = nestedSubagent({ title: "Monitor", subtitle: label }, { description: `Monitor: ${label}`, prompt: command, type: "monitor" }, sessionID);
203
+ const shellID = sessions.monitorShell?.(sessionID);
204
+ if (shellID !== undefined)
205
+ mapped.metadata = { ...mapped.metadata, backgroundShellID: shellID };
203
206
  }
204
207
  else if (entry.name === "task") {
205
208
  const text = (value) => (typeof value === "string" && value !== "" ? value : undefined);
package/dist/types.d.ts CHANGED
@@ -231,6 +231,24 @@ export type OCSessionStatus = {
231
231
  message: string;
232
232
  next: number;
233
233
  };
234
+ export type OCShellStatus = "running" | "exited" | "timeout" | "killed";
235
+ /** OpenCode 2.x `Shell.Info` (@opencode/schema 2.1.4 shell.js); `metadata.sessionID` ties it to a session. */
236
+ export interface OCShellInfo {
237
+ id: string;
238
+ status: OCShellStatus;
239
+ command: string;
240
+ cwd: string;
241
+ shell: string;
242
+ file: string;
243
+ exit?: number;
244
+ metadata: {
245
+ sessionID: string;
246
+ } & Record<string, unknown>;
247
+ time: {
248
+ started: number;
249
+ completed?: number;
250
+ };
251
+ }
234
252
  export type OCEvent = {
235
253
  type: "session.inbox.enqueued";
236
254
  properties: {
@@ -356,6 +374,18 @@ export type OCEvent = {
356
374
  truncated: boolean;
357
375
  };
358
376
  };
377
+ } | {
378
+ type: "shell.created";
379
+ properties: {
380
+ info: OCShellInfo;
381
+ };
382
+ } | {
383
+ type: "shell.exited";
384
+ properties: {
385
+ id: string;
386
+ status: OCShellStatus;
387
+ exit?: number;
388
+ };
359
389
  } | {
360
390
  type: "session.deleted";
361
391
  properties: {
package/dist/v2.js CHANGED
@@ -184,6 +184,25 @@ export function toV2AssistantMessage(info) {
184
184
  function v2ToolName(tool) {
185
185
  return tool === "bash" ? "shell" : tool;
186
186
  }
187
+ /**
188
+ * A card whose call left OmO work running (background-results.ts `backgroundShellID`), as OpenCode 2.x settles a
189
+ * call it moved to the background: completed at once with `{status: "running", shellID}` in its metadata, so
190
+ * OpenChamber 2.x draws the row from the shell (background-shell.ts `readBackgroundShellID`) and lists the shell
191
+ * as a background command. A card still waiting on that work is running here, with its result already known.
192
+ */
193
+ function backgroundShell(part) {
194
+ const st = part.state;
195
+ if (st.status !== "running" && st.status !== "completed")
196
+ return undefined;
197
+ const { backgroundShellID: shellID, backgroundWaits, backgroundFailed, output, ...metadata } = st.metadata ?? {};
198
+ if (typeof shellID !== "string")
199
+ return undefined;
200
+ if (st.status === "completed")
201
+ return { output: st.output, metadata: { ...metadata, status: "running", shellID } };
202
+ if (backgroundFailed !== false || !Array.isArray(backgroundWaits))
203
+ return undefined;
204
+ return { output: typeof output === "string" ? output : "", metadata: { ...metadata, status: "running", shellID } };
205
+ }
187
206
  function toolContent(part) {
188
207
  const st = part.state;
189
208
  const name = v2ToolName(part.tool);
@@ -202,9 +221,21 @@ function toolContent(part) {
202
221
  ...(end !== undefined ? { completed: end } : {}),
203
222
  },
204
223
  };
224
+ const background = st.status === "running" ? backgroundShell(part) : undefined;
205
225
  if (st.status === "pending") {
206
226
  base.state = { status: "streaming", input: st.raw ?? "", metadata: {} };
207
227
  }
228
+ else if (background !== undefined) {
229
+ base.time = { created: st.time.start, ran: st.time.start, completed: st.time.start };
230
+ base.state = {
231
+ status: "completed",
232
+ input: st.input,
233
+ content: background.output !== "" ? [{ type: "text", text: background.output }] : [],
234
+ output: background.output,
235
+ title: st.status === "running" ? (st.title ?? part.tool) : part.tool,
236
+ metadata: background.metadata,
237
+ };
238
+ }
208
239
  else if (st.status === "running") {
209
240
  base.state = {
210
241
  status: "running",
@@ -220,7 +251,7 @@ function toolContent(part) {
220
251
  content: st.output !== "" ? [{ type: "text", text: st.output }] : [],
221
252
  output: st.output,
222
253
  title: st.title,
223
- metadata: st.metadata ?? {},
254
+ metadata: backgroundShell(part)?.metadata ?? st.metadata ?? {},
224
255
  };
225
256
  }
226
257
  else {
@@ -256,6 +287,8 @@ export class V2EventTranslator {
256
287
  case "session.compaction.failed":
257
288
  case "session.shell.started":
258
289
  case "session.shell.ended":
290
+ case "shell.created":
291
+ case "shell.exited":
259
292
  return [this.frame(directory, ev.type, ev.properties)];
260
293
  case "message.removed":
261
294
  return [
@@ -610,6 +643,19 @@ export class V2EventTranslator {
610
643
  ts.called = true;
611
644
  frames.push(this.frame(directory, "session.tool.called", { ...base, input: state.input, executed: false }));
612
645
  }
646
+ const background = backgroundShell(part);
647
+ if (background !== undefined) {
648
+ if (!ts.ended) {
649
+ ts.ended = true;
650
+ frames.push(this.frame(directory, "session.tool.success", {
651
+ ...base,
652
+ content: background.output !== "" ? [{ type: "text", text: background.output }] : [],
653
+ metadata: background.metadata,
654
+ executed: false,
655
+ }));
656
+ }
657
+ return frames;
658
+ }
613
659
  // `called` carries no metadata (eval card headers, live output); 2.x reads it while running only from progress.
614
660
  if (state.status === "running" && state.metadata !== undefined) {
615
661
  const metadata = safeJson(state.metadata);
@@ -80,6 +80,8 @@ export declare class WorkStatusBridge {
80
80
  */
81
81
  wire(s: ManagedSession): void;
82
82
  observe(s: ManagedSession, ev: Record<string, unknown>): void;
83
+ /** Detached eval cells codemode reports running or queued. */
84
+ liveCells(sessionID: string): WakeSourceItem[];
83
85
  /** The agent process exited: its in-process wake sources and timers died with it. */
84
86
  stopParent(sessionID: string): void;
85
87
  snapshot(s: ManagedSession): Promise<WorkStatus>;
@@ -194,6 +194,10 @@ export class WorkStatusBridge {
194
194
  work.goal = { ...(work.goal ?? { armed: false }), wakeSources: counts };
195
195
  }
196
196
  }
197
+ /** Detached eval cells codemode reports running or queued. */
198
+ liveCells(sessionID) {
199
+ return this.work.get(sessionID)?.wakeSources.get(CODEMODE_WAKE_SOURCE)?.items ?? [];
200
+ }
197
201
  /** The agent process exited: its in-process wake sources and timers died with it. */
198
202
  stopParent(sessionID) {
199
203
  this.work.delete(sessionID);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-compat",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
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.24",
3
+ "version": "1.0.26",
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",
@@ -10,16 +10,14 @@ For the open session it shows:
10
10
 
11
11
  - **Todo**: OmO's todo list grouped by phase, with progress per phase. The
12
12
  phase in progress is open; click any phase to open or close it.
13
- - **Monitors**: each terminal monitor with its state, age and event count.
14
- Click one for its command, filter, deadline and latest matched lines. On
15
- OpenChamber 2.x, monitors appear here instead of as subagent sessions.
16
- - **Eval cells**: detached eval cells with their language, state and running
17
- time. Click one to follow its output; **Stop** stops a running or queued
18
- cell.
19
- - **Background**: background terminal sessions that are still running.
20
13
  - **Goal**: the goal continuation timer, what it waits on, and **Fire now**.
21
14
  - **Other activity**: any other wake source OmO reports.
22
15
 
16
+ Terminal monitors, detached eval cells and background terminal sessions are not
17
+ listed here: antechamber reports them to OpenChamber as background commands,
18
+ so the session reads as working while they run and OpenChamber lists them above
19
+ the composer with their output (and Stop, for eval cells).
20
+
23
21
  The section sizes itself to its content, up to OpenChamber's 320 px limit, and
24
22
  scrolls inside beyond that.
25
23
 
@@ -64,7 +62,7 @@ section (sandboxed iframe) --serviceRequest /status?session=ses_...--> OpenChamb
64
62
  `antechamber serve` writes `<state dir>/run/<pid>.json` (mode 0600) with its
65
63
  URL and a random token that opens only the `/antechamber/work-status` routes. The service
66
64
  picks the running server whose agent holds the session and relays the snapshot
67
- and the Stop and Fire now actions. It keeps no state. See [docs/DESIGN.md](docs/DESIGN.md) for the snapshot, refresh,
65
+ and the Fire now action. It keeps no state. See [docs/DESIGN.md](docs/DESIGN.md) for the snapshot, refresh,
68
66
  server choice and security boundary, and
69
67
  [docs/architecture.md](../../docs/architecture.md#omo-work-status-section) for
70
68
  where each piece of data comes from in OmO.
@@ -77,13 +75,8 @@ the extension's OpenChamber storage.
77
75
 
78
76
  ## Limitations
79
77
 
80
- OmO does not expose these over RPC, so the section cannot offer them:
81
-
82
- - stopping a monitor or a background terminal session;
83
- - continuation holds (the loop guard publishes them in-process only).
84
-
85
- Eval cells need OmO's codemode. Without it, or while the agent process is not
86
- running, the section lists the cells OmO last reported, without output or Stop.
78
+ OmO does not publish continuation holds over RPC (the loop guard keeps them
79
+ in-process), so the section cannot show them.
87
80
 
88
81
  ## Development
89
82