antechamber 1.0.0 → 1.0.2

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,28 @@ and this project uses [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.2] - 2026-10-20
11
+
12
+ ### Fixed
13
+
14
+ - Restore main-session costs before listing sessions after a restart, and keep live usage totals cumulative
15
+ across assistant steps.
16
+
17
+ ## [1.0.1] - 2026-10-20
18
+
19
+ ### Added
20
+
21
+ - OmO Work, an OpenChamber 2.x Work Status section (`extensions/omo-work`). For the open session it
22
+ shows OmO's todo list by phase, terminal monitors with their matched lines, detached eval cells
23
+ (output peek and Stop), background terminal sessions and the goal continuation timer (Fire now).
24
+ Its service reaches antechamber through new `/antechamber/work-status` routes, authenticated by a
25
+ token `serve` writes into its run record.
26
+
27
+ ### Changed
28
+
29
+ - On OpenChamber 2.x, OmO monitors are no longer listed as subagent sessions; the OmO Work section
30
+ shows them. OpenChamber 1.24 still sees them as child sessions.
31
+
10
32
  ## [1.0.0] - 2026-10-20
11
33
 
12
34
  - First stable release. No functional changes since 0.2.37.
package/README.md CHANGED
@@ -40,6 +40,9 @@ an **external OpenCode server** and sessions run on OmO.
40
40
  - An optional OpenChamber extension, **OmO Graph**, that draws the OmO
41
41
  sub-agents behind the open session as a live, clickable graph (see
42
42
  [OpenChamber extension](#openchamber-extension-omo-graph)).
43
+ - An optional OpenChamber 2.x Work Status section, **OmO Work**, for the
44
+ session's todo phases, monitors, eval cells and background work (see
45
+ [OmO Work](#openchamber-extension-omo-work)).
43
46
 
44
47
  ## Supported backends
45
48
 
@@ -234,6 +237,24 @@ OpenChamber clones the repository root, which is antechamber itself, not the
234
237
  extension, and refuses it as an invalid manifest. Details:
235
238
  [extensions/omo-dag/README.md](extensions/omo-dag/README.md).
236
239
 
240
+ ## OpenChamber extension: OmO Work
241
+
242
+ `extensions/omo-work` adds an **OmO Work** section to OpenChamber 2.x's Work
243
+ Status panel (OpenChamber 2.3.2 or newer; 1.x has no extension sections). For
244
+ the open session it shows the OmO todo list grouped by phase, terminal
245
+ monitors with their matched lines, detached eval cells with their output and a
246
+ Stop button, background terminal sessions, and the goal continuation timer
247
+ with a Fire now button. On 2.x, monitors appear here instead of as subagent
248
+ sessions.
249
+
250
+ Add it like OmO Graph: the folder `antechamber setup` prints
251
+ (`.../extensions/omo-work`, built by `npm run build`), or
252
+ `https://github.com/eliottness/antechamber/releases/latest/download/antechamber-omo-work.zip`.
253
+ Approve its local service. It needs an antechamber version that writes a
254
+ work-status token into its run record (this release or newer) and an
255
+ OpenChamber restart after upgrading. Details:
256
+ [extensions/omo-work/README.md](extensions/omo-work/README.md).
257
+
237
258
  ## OpenChamber extension: OmO Compatibility
238
259
 
239
260
  `extensions/omo-compat` is a second, code-free extension for OpenChamber 1.24
@@ -28,10 +28,16 @@ export async function serve(options) {
28
28
  process.on("SIGTERM", shutdown);
29
29
  await server.start();
30
30
  try {
31
- forget = recordRunningServer(envSetting("STATE_DIR") ?? defaultStateDir(homedir(), fs.existsSync), { pid: process.pid, version: ADAPTER_VERSION, url: server.url, started: new Date().toISOString() }, fs);
31
+ forget = recordRunningServer(envSetting("STATE_DIR") ?? defaultStateDir(homedir(), fs.existsSync), {
32
+ pid: process.pid,
33
+ version: ADAPTER_VERSION,
34
+ url: server.url,
35
+ started: new Date().toISOString(),
36
+ token: server.extensionToken,
37
+ }, fs);
32
38
  }
33
39
  catch (error) {
34
- // Only doctor reads the record; serving must not depend on it.
40
+ // Doctor and the OmO Work extension read the record; serving must not depend on it.
35
41
  console.error(`antechamber: cannot record the running version: ${error instanceof Error ? error.message : String(error)}`);
36
42
  }
37
43
  if (compatible)
@@ -18,6 +18,12 @@ function omoGraphExtensionDir(ctx) {
18
18
  const built = ["package.json", "panel/main.js", "service/main.js"].every((file) => ctx.fs.existsSync(join(dir, file)));
19
19
  return built ? dir : undefined;
20
20
  }
21
+ /** The bundled OmO Work status-section extension folder, when this installation has its built files. */
22
+ function omoWorkExtensionDir(ctx) {
23
+ const dir = resolve(dirname(ctx.cliPath), "..", "extensions", "omo-work");
24
+ const built = ["package.json", "status/main.js", "service/main.js"].every((file) => ctx.fs.existsSync(join(dir, file)));
25
+ return built ? dir : undefined;
26
+ }
21
27
  /** The bundled OmO Compatibility extension folder; it is a manifest only, so it needs no build. */
22
28
  function omoCompatExtensionDir(ctx) {
23
29
  const dir = resolve(dirname(ctx.cliPath), "..", "extensions", "omo-compat");
@@ -179,6 +185,10 @@ export function setup(options, ctx = createContext()) {
179
185
  if (extension !== undefined) {
180
186
  report(`Optional OmO Graph extension: in OpenChamber, Settings \u2192 Extensions \u2192 Add \u2192 Folder: ${extension}`);
181
187
  }
188
+ const work = omoWorkExtensionDir(ctx);
189
+ if (work !== undefined) {
190
+ report(`Optional OmO Work section (todo phases, monitors, eval cells; OpenChamber 2.x): Settings \u2192 Extensions \u2192 Add \u2192 Folder: ${work}`);
191
+ }
182
192
  const compat = omoCompatExtensionDir(ctx);
183
193
  if (compat !== undefined) {
184
194
  report(`Optional OmO Compatibility extension (eval card headers): Settings \u2192 Extensions \u2192 Add \u2192 Folder: ${compat}`);
package/dist/http.d.ts CHANGED
@@ -13,11 +13,14 @@ export declare function errorBody(name: string, message: string): {
13
13
  };
14
14
  };
15
15
  export declare function json(res: ServerResponse, status: number, body: unknown): void;
16
+ /** Routes the OmO Work extension service calls with the run-record token instead of the server password. */
17
+ export declare const EXTENSION_ROUTE_PREFIX = "/antechamber/work-status/";
16
18
  export declare class Http {
17
19
  private serverOrigin?;
18
20
  private readonly credentials;
19
21
  private readonly maxBodyBytes;
20
- constructor(serverOrigin?: (() => string) | undefined);
22
+ private readonly extensionToken;
23
+ constructor(serverOrigin?: (() => string) | undefined, extensionToken?: string);
21
24
  allowsUpgrade(req: IncomingMessage): boolean;
22
25
  route(req: IncomingMessage, res: ServerResponse, handle: () => Promise<void>): Promise<void>;
23
26
  body(req: IncomingMessage): Promise<unknown>;
package/dist/http.js CHANGED
@@ -45,12 +45,17 @@ export function json(res, status, body) {
45
45
  const payload = JSON.stringify(body);
46
46
  res.writeHead(status, { "Content-Type": "application/json" }).end(payload);
47
47
  }
48
+ /** Routes the OmO Work extension service calls with the run-record token instead of the server password. */
49
+ export const EXTENSION_ROUTE_PREFIX = "/antechamber/work-status/";
48
50
  export class Http {
49
51
  serverOrigin;
50
52
  credentials;
51
53
  maxBodyBytes;
52
- constructor(serverOrigin) {
54
+ extensionToken;
55
+ constructor(serverOrigin, extensionToken) {
53
56
  this.serverOrigin = serverOrigin;
57
+ this.extensionToken =
58
+ extensionToken === undefined ? undefined : createHash("sha256").update(`Bearer ${extensionToken}`).digest();
54
59
  const maxBodyBytes = Number(envSetting("MAX_BODY_BYTES"));
55
60
  this.maxBodyBytes = Number.isSafeInteger(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
56
61
  const password = serverPassword();
@@ -182,6 +187,14 @@ export class Http {
182
187
  authorized(req) {
183
188
  if (this.credentials === undefined)
184
189
  return true;
190
+ if (this.extensionToken !== undefined && // The router resolves dot segments, %2e and backslashes, so the check must see the same path.
191
+ new URL(req.url ?? "/", "http://localhost").pathname.startsWith(EXTENSION_ROUTE_PREFIX)) {
192
+ const supplied = createHash("sha256")
193
+ .update(req.headers.authorization ?? "")
194
+ .digest();
195
+ if (timingSafeEqual(this.extensionToken, supplied))
196
+ return true;
197
+ }
185
198
  const match = req.headers.authorization?.match(/^Basic\s+([A-Za-z0-9+/]+=*)$/i);
186
199
  const supplied = createHash("sha256")
187
200
  .update(Buffer.from(match?.[1] ?? "", "base64"))
@@ -17,11 +17,19 @@ export interface MonitorSnapshot {
17
17
  fireCount: number;
18
18
  lastFiredAtMs: number | null;
19
19
  }
20
- interface MonitorEnding {
20
+ export interface MonitorEnding {
21
21
  reason: string;
22
22
  exitCode: number | null;
23
23
  endedAtMs: number;
24
24
  }
25
+ /** One watch of a parent, for the Work Status section. */
26
+ export interface MonitorView {
27
+ monitor: MonitorSnapshot;
28
+ ending?: MonitorEnding & {
29
+ label: string;
30
+ };
31
+ lines: string[];
32
+ }
25
33
  export declare function parseMonitor(value: unknown): MonitorSnapshot | undefined;
26
34
  export declare function monitorSummary(monitor: MonitorSnapshot, ending?: MonitorEnding): string;
27
35
  export declare class MonitorSessions {
@@ -33,6 +41,8 @@ export declare class MonitorSessions {
33
41
  constructor(sessions: Map<string, ManagedSession>, emit: (directory: string, event: OCEvent) => void);
34
42
  wire(parent: ManagedSession): void;
35
43
  owns(id: string): boolean;
44
+ /** A parent's watches, live first then the newest ended ones, with their latest matched lines. */
45
+ list(parentID: string, maxEnded: number, maxLines: number): MonitorView[];
36
46
  messages(id: string): OCMessageWithParts[];
37
47
  observe(parent: ManagedSession, ev: Record<string, unknown>): void;
38
48
  /** A hidden `senpi-monitor:notification` custom message: append its lines to each watch it names. */
@@ -55,4 +65,3 @@ export declare class MonitorSessions {
55
65
  private create;
56
66
  private publish;
57
67
  }
58
- export {};
@@ -88,6 +88,20 @@ export class MonitorSessions {
88
88
  owns(id) {
89
89
  return this.watches.has(id);
90
90
  }
91
+ /** A parent's watches, live first then the newest ended ones, with their latest matched lines. */
92
+ list(parentID, maxEnded, maxLines) {
93
+ const watches = [...this.watches.values()].filter((watch) => watch.session.info.parentID === parentID);
94
+ const live = watches.filter((watch) => watch.ending === undefined);
95
+ const ended = watches
96
+ .filter((watch) => watch.ending !== undefined)
97
+ .sort((a, b) => (b.ending?.endedAtMs ?? 0) - (a.ending?.endedAtMs ?? 0))
98
+ .slice(0, maxEnded);
99
+ return [...live.sort((a, b) => b.monitor.startedAtMs - a.monitor.startedAtMs), ...ended].map(({ monitor, ending, lines }) => ({
100
+ monitor: { ...monitor },
101
+ ...(ending ? { ending: { ...ending, label: endingLabel(ending) } } : {}),
102
+ lines: lines.slice(-maxLines),
103
+ }));
104
+ }
91
105
  messages(id) {
92
106
  return this.watches.get(id)?.session.messages ?? [];
93
107
  }
@@ -151,12 +165,13 @@ export class MonitorSessions {
151
165
  for (const [childID, watch] of this.watches) {
152
166
  if (childID !== id && watch.session.info.parentID !== id)
153
167
  continue;
168
+ // Emitted while the watch is still owned, so the v2 stream keeps hiding it.
169
+ if (this.sessions.get(childID) === watch.session) {
170
+ this.emit(watch.session.info.directory, { type: "session.deleted", properties: { info: watch.session.info } });
171
+ this.sessions.delete(childID);
172
+ }
154
173
  this.watches.delete(childID);
155
174
  this.dismissed.add(childID);
156
- if (this.sessions.get(childID) !== watch.session)
157
- continue;
158
- this.sessions.delete(childID);
159
- this.emit(watch.session.info.directory, { type: "session.deleted", properties: { info: watch.session.info } });
160
175
  }
161
176
  }
162
177
  running(parentID) {
@@ -4,6 +4,7 @@ import type { ManagedSession } from "./sessions.js";
4
4
  import type { SessionStore } from "./session-store.js";
5
5
  import { type SubagentBridge } from "./subagents.js";
6
6
  import { type MonitorSessions } from "./monitor-sessions.js";
7
+ import type { WorkStatusBridge } from "./work-status.js";
7
8
  import { type TodoUpdated } from "./todo-bridge.js";
8
9
  export interface PiUsage {
9
10
  input?: number;
@@ -40,6 +41,7 @@ interface PiEventServices {
40
41
  readonly emit: (directory: string, event: OCEvent | TodoUpdated) => void;
41
42
  readonly subagents: SubagentBridge;
42
43
  readonly monitors: MonitorSessions;
44
+ readonly workStatus: Pick<WorkStatusBridge, "observe">;
43
45
  readonly store: Pick<SessionStore, "update">;
44
46
  readonly dirtyDiffs: Set<string>;
45
47
  readonly ensureMessages: (s: ManagedSession) => OCMessageWithParts[];
@@ -311,6 +311,7 @@ export class PiEventTranslator {
311
311
  case "extension_event":
312
312
  this.services.monitors.observe(s, ev);
313
313
  this.services.subagents.observe(s, ev);
314
+ this.services.workStatus.observe(s, ev);
314
315
  break;
315
316
  default:
316
317
  break; // turn_start, queue_update — future milestones
package/dist/routes-v2.js CHANGED
@@ -233,7 +233,7 @@ export async function handleV2(services, req, res, url, path, method, directory)
233
233
  }
234
234
  // Read-only inventories OpenChamber loads at bootstrap. OmO has no OpenCode-style shells or
235
235
  // web-search providers, so each is an honest empty list (mutations stay 400);
236
- // OmO monitors appear as child sessions instead (monitor-sessions.ts).
236
+ // OmO monitors appear in the OmO Work status section instead (work-status.ts).
237
237
  if ((path === "/api/shell" || path === "/api/websearch/provider") && method === "GET") {
238
238
  json(res, 200, withLocation([]));
239
239
  return;
@@ -328,7 +328,9 @@ export async function handleV2(services, req, res, url, path, method, directory)
328
328
  const parentID = url.searchParams.get("parentID");
329
329
  const order = url.searchParams.get("order") === "asc" ? "asc" : "desc";
330
330
  const limit = numberParam(url.searchParams.get("limit"));
331
- let infos = dir !== null && dir !== "" ? services.sessions.list(dir) : services.sessions.listAll();
331
+ let infos = (dir !== null && dir !== "" ? services.sessions.list(dir) : services.sessions.listAll()).filter(
332
+ // OmO monitors are a child session only for v1 clients; 2.x shows them in the OmO Work section.
333
+ (session) => !services.sessions.monitors.owns(session.id));
332
334
  if (search)
333
335
  infos = infos.filter((s) => s.title.toLowerCase().includes(search.toLowerCase()));
334
336
  if (parentID !== null)
@@ -373,7 +375,7 @@ export async function handleV2(services, req, res, url, path, method, directory)
373
375
  if (path === "/api/session/active" && method === "GET") {
374
376
  const data = {};
375
377
  for (const [id, status] of Object.entries(services.sessions.statuses())) {
376
- if (status.type !== "idle")
378
+ if (status.type !== "idle" && !services.sessions.monitors.owns(id))
377
379
  data[id] = { type: "running" };
378
380
  }
379
381
  json(res, 200, { data });
@@ -9,6 +9,8 @@ export interface RunningServer {
9
9
  readonly version: string;
10
10
  readonly url: string;
11
11
  readonly started: string;
12
+ /** Bearer token for the `/antechamber/work-status` routes; the file is 0600 in a 0700 directory. */
13
+ readonly token?: string;
12
14
  }
13
15
  type Fs = Pick<typeof fs, "existsSync" | "mkdirSync" | "readdirSync" | "readFileSync" | "rmSync" | "writeFileSync">;
14
16
  /** Writes the record and returns its remover; a crash leaves a stale record that readers skip by PID. */
package/dist/server.d.ts CHANGED
@@ -20,6 +20,8 @@ export declare class AdapterServer {
20
20
  private nextWebSocketEventID;
21
21
  private catalogPromise;
22
22
  private actualPort;
23
+ /** Bearer token for the `/antechamber/work-status` routes, published in the run record for the OmO Work extension. */
24
+ readonly extensionToken: string;
23
25
  private readonly http;
24
26
  constructor(options: AdapterServerOptions);
25
27
  private catalog;
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** HTTP/SSE composition root for the OpenCode API backed by pi. */
2
- import { createHash } from "node:crypto";
2
+ import { createHash, randomBytes } from "node:crypto";
3
3
  import { createServer } from "node:http";
4
4
  import { debugLog } from "./misc-routes.js";
5
5
  import { SessionManager } from "./sessions.js";
@@ -9,6 +9,7 @@ import { SSE } from "./sse.js";
9
9
  import { handleV1 } from "./routes-v1.js";
10
10
  import { handleV2 } from "./routes-v2.js";
11
11
  import { selfUpdate } from "./self-update.js";
12
+ import { handleWorkStatus } from "./work-status.js";
12
13
  export { isLoopbackHost, serverPassword } from "./http.js";
13
14
  export class AdapterServer {
14
15
  options;
@@ -21,10 +22,12 @@ export class AdapterServer {
21
22
  nextWebSocketEventID = 0;
22
23
  catalogPromise;
23
24
  actualPort;
24
- http = new Http(() => this.url);
25
+ /** Bearer token for the `/antechamber/work-status` routes, published in the run record for the OmO Work extension. */
26
+ extensionToken = randomBytes(32).toString("base64url");
27
+ http = new Http(() => this.url, this.extensionToken);
25
28
  constructor(options) {
26
29
  this.options = options;
27
- this.sse = new SSE(options, this.sseClients);
30
+ this.sse = new SSE(options, this.sseClients, (id) => this.sessions.monitors.owns(id));
28
31
  this.sessions = new SessionManager((directory, event) => this.broadcast(directory, event), undefined, options.defaultDirectory);
29
32
  this.server = createServer((req, res) => void this.route(req, res));
30
33
  this.server.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
@@ -197,6 +200,10 @@ export class AdapterServer {
197
200
  this.handleSSE(req, res, "v2", directory, directoryQuery === null && headerDirectory === undefined);
198
201
  return;
199
202
  }
203
+ if (path.startsWith("/antechamber/work-status/")) {
204
+ await handleWorkStatus(this.sessions, req, res, url, method);
205
+ return;
206
+ }
200
207
  const services = {
201
208
  sessions: this.sessions,
202
209
  defaultDirectory: this.options.defaultDirectory,
@@ -10,6 +10,7 @@ import type { TodoBridge, Todo, TodoUpdated } from "./todo-bridge.js";
10
10
  import { type SessionTreeState } from "./session-tree.js";
11
11
  import { SubagentBridge } from "./subagents.js";
12
12
  import { MonitorSessions } from "./monitor-sessions.js";
13
+ import { WorkStatusBridge } from "./work-status.js";
13
14
  import { SessionStore, type TitleSource } from "./session-store.js";
14
15
  import type { OCAssistantMessage, OCEvent, OCMessageWithParts, OCModelRef, OCPart, OCPromptPart, OCSession, OCSessionStatus, OCToolPart } from "./types.js";
15
16
  export { ADAPTER_VERSION } from "./version.js";
@@ -90,6 +91,7 @@ export declare class SessionManager {
90
91
  readonly lifecycle: SessionLifecycle;
91
92
  readonly subagents: SubagentBridge;
92
93
  readonly monitors: MonitorSessions;
94
+ readonly workStatus: WorkStatusBridge;
93
95
  /** directory → last scan freshness (pi dir mtime + wall clock). */
94
96
  private scanCache;
95
97
  /** freshness of the all-directories scan (see listAll). */
@@ -117,6 +119,7 @@ export declare class SessionManager {
117
119
  * global/recent view; `list(directory)` stays the per-project source of truth.
118
120
  */
119
121
  listAll(): OCSession[];
122
+ private sessionInfo;
120
123
  get(sessionId: string): ManagedSession | undefined;
121
124
  remove(sessionId: string): Promise<boolean>;
122
125
  private removeLive;
package/dist/sessions.js CHANGED
@@ -17,6 +17,7 @@ import { RequestBodyError } from "./http.js";
17
17
  import { bindMessageEntries, durableShellTree, followExternalAppends, forkConversation, getEntries, navigate, recordTurn, readSessionTree, revertConversation, SessionTreeError, unrevertConversation, } from "./session-tree.js";
18
18
  import { SubagentBridge } from "./subagents.js";
19
19
  import { MonitorSessions } from "./monitor-sessions.js";
20
+ import { WorkStatusBridge } from "./work-status.js";
20
21
  import { sessionDiff } from "./session-diff.js";
21
22
  import { announceSessionNotice, skillCommandText, slashTitlePrompt } from "./session-commands.js";
22
23
  import { ocIdFor, piSessionDir, piSessionsRootMtime, scanAllPiSessions, scanPiSessions, SessionStore, readSessionTitle, } from "./session-store.js";
@@ -45,6 +46,7 @@ export class SessionManager {
45
46
  lifecycle;
46
47
  subagents;
47
48
  monitors;
49
+ workStatus;
48
50
  /** directory → last scan freshness (pi dir mtime + wall clock). */
49
51
  scanCache = new Map();
50
52
  /** freshness of the all-directories scan (see listAll). */
@@ -56,10 +58,12 @@ export class SessionManager {
56
58
  this.lifecycle = new SessionLifecycle(() => this.sessions.values(), lifecycleOptions);
57
59
  this.subagents = new SubagentBridge(this.store, this.sessions, this.emit, (session, raw) => parsePiSessionText(session, getAgentName(), raw));
58
60
  this.monitors = new MonitorSessions(this.sessions, this.emit);
61
+ this.workStatus = new WorkStatusBridge(this.monitors);
59
62
  this.events = new PiEventTranslator({
60
63
  emit: this.emit,
61
64
  subagents: this.subagents,
62
65
  monitors: this.monitors,
66
+ workStatus: this.workStatus,
63
67
  store: this.store,
64
68
  dirtyDiffs: this.dirtyDiffs,
65
69
  ensureMessages: (s) => this.ensureMessages(s),
@@ -254,6 +258,7 @@ export class SessionManager {
254
258
  // Its task cards end with the run; the tasks themselves may still run in OmO's shared host.
255
259
  this.subagents.finishCalls(s.info.id, true);
256
260
  this.monitors.stopParent(s.info.id);
261
+ this.workStatus.stopParent(s.info.id);
257
262
  void this.lifecycle
258
263
  .dormant(s)
259
264
  .catch((error) => console.error("[lifecycle] exit cleanup failed:", error));
@@ -307,10 +312,10 @@ export class SessionManager {
307
312
  this.subagents.restoreAll();
308
313
  if (directory !== undefined) {
309
314
  this.ensureScanned(directory);
310
- return [...this.sessions.values()].map((s) => s.info).filter((s) => s.directory === directory);
315
+ return [...this.sessions.values()].filter((s) => s.info.directory === directory).map((s) => this.sessionInfo(s));
311
316
  }
312
317
  // Unfiltered: keep the old semantics — live sessions only.
313
- return [...this.sessions.values()].filter((s) => s.restored === undefined).map((s) => s.info);
318
+ return [...this.sessions.values()].filter((s) => s.restored === undefined).map((s) => this.sessionInfo(s));
314
319
  }
315
320
  /**
316
321
  * Every restorable session on disk (all directories) plus live ones. Backs the
@@ -321,7 +326,12 @@ export class SessionManager {
321
326
  this.refreshNativeTitles();
322
327
  this.ensureAllScanned();
323
328
  this.subagents.restoreAll();
324
- return [...this.sessions.values()].map((s) => s.info);
329
+ return [...this.sessions.values()].map((s) => this.sessionInfo(s));
330
+ }
331
+ sessionInfo(s) {
332
+ if (s.restored !== undefined)
333
+ this.ensureMessages(s);
334
+ return s.info;
325
335
  }
326
336
  get(sessionId) {
327
337
  if (this.disposed)
@@ -329,9 +339,13 @@ export class SessionManager {
329
339
  const live = this.sessions.get(sessionId);
330
340
  if (live !== undefined) {
331
341
  this.refreshNativeTitles();
342
+ this.sessionInfo(live);
332
343
  return live;
333
344
  }
334
- return this.subagents.restore(sessionId) ?? this.resolveRestored(sessionId);
345
+ const restored = this.subagents.restore(sessionId) ?? this.resolveRestored(sessionId);
346
+ if (restored !== undefined)
347
+ this.sessionInfo(restored);
348
+ return restored;
335
349
  }
336
350
  async remove(sessionId) {
337
351
  return this.lifecycle.run(() => this.removeLive(sessionId));
@@ -348,6 +362,7 @@ export class SessionManager {
348
362
  const piPath = this.subagents.owns(sessionId) || this.monitors.owns(sessionId) ? undefined : (s.piPath ?? s.restored?.piPath);
349
363
  const child = this.subagents.remove(sessionId);
350
364
  this.monitors.remove(sessionId);
365
+ this.workStatus.stopParent(sessionId);
351
366
  if (piPath !== undefined) {
352
367
  try {
353
368
  unlinkSync(piPath);
package/dist/sse.d.ts CHANGED
@@ -7,10 +7,14 @@ export type SSESubscriber = {
7
7
  directory?: string;
8
8
  global: boolean;
9
9
  };
10
+ /** The session an event belongs to, when it names one. */
11
+ export declare function eventSessionID(event: OCEvent | TodoUpdated): string | undefined;
10
12
  export declare class SSE {
11
13
  private options;
12
14
  /** SSE subscribers tagged by protocol flavor and explicit directory scope. */
13
15
  readonly clients: Map<ServerResponse, SSESubscriber>;
16
+ /** Sessions v2 clients must not see (OmO monitor children, shown by the OmO Work section). */
17
+ private hiddenFromV2;
14
18
  private v2events;
15
19
  private nextEventID;
16
20
  private replay;
@@ -18,7 +22,9 @@ export declare class SSE {
18
22
  defaultDirectory: string;
19
23
  },
20
24
  /** SSE subscribers tagged by protocol flavor and explicit directory scope. */
21
- clients: Map<ServerResponse, SSESubscriber>);
25
+ clients: Map<ServerResponse, SSESubscriber>,
26
+ /** Sessions v2 clients must not see (OmO monitor children, shown by the OmO Work section). */
27
+ hiddenFromV2?: (sessionID: string) => boolean);
22
28
  close(): void;
23
29
  broadcast(directory: string, event: OCEvent | TodoUpdated): void;
24
30
  writeSSE(res: ServerResponse, frame: string): boolean;
package/dist/sse.js CHANGED
@@ -5,17 +5,35 @@ import { V2EventTranslator } from "./v2.js";
5
5
  const MAX_SSE_SUBSCRIBERS = 64;
6
6
  const MAX_SSE_PENDING_BYTES = 8 * 1024 * 1024;
7
7
  const MAX_SSE_REPLAY_RECORDS = 1024;
8
+ /** The session an event belongs to, when it names one. */
9
+ export function eventSessionID(event) {
10
+ const properties = event.properties;
11
+ const nested = (key) => {
12
+ const value = properties[key];
13
+ return value !== null && typeof value === "object" ? value : undefined;
14
+ };
15
+ if (event.type === "session.created" || event.type === "session.updated" || event.type === "session.deleted")
16
+ return event.properties.info.id;
17
+ const info = nested("info");
18
+ const part = nested("part");
19
+ const id = properties.sessionID ?? info?.sessionID ?? part?.sessionID;
20
+ return typeof id === "string" ? id : undefined;
21
+ }
8
22
  export class SSE {
9
23
  options;
10
24
  clients;
25
+ hiddenFromV2;
11
26
  v2events = new V2EventTranslator();
12
27
  nextEventID = 0;
13
28
  replay = [];
14
29
  constructor(options,
15
30
  /** SSE subscribers tagged by protocol flavor and explicit directory scope. */
16
- clients) {
31
+ clients,
32
+ /** Sessions v2 clients must not see (OmO monitor children, shown by the OmO Work section). */
33
+ hiddenFromV2 = () => false) {
17
34
  this.options = options;
18
35
  this.clients = clients;
36
+ this.hiddenFromV2 = hiddenFromV2;
19
37
  }
20
38
  close() {
21
39
  for (const res of this.clients.keys()) {
@@ -24,9 +42,12 @@ export class SSE {
24
42
  }
25
43
  }
26
44
  broadcast(directory, event) {
27
- const v2Frames = event.type === "todo.updated"
28
- ? [{ id: `evt_${randomUUID()}`, type: event.type, location: { directory }, data: event.properties }]
29
- : this.v2events.translate(directory, event);
45
+ const hidden = this.hiddenFromV2(eventSessionID(event) ?? "");
46
+ const v2Frames = hidden
47
+ ? []
48
+ : event.type === "todo.updated"
49
+ ? [{ id: `evt_${randomUUID()}`, type: event.type, location: { directory }, data: event.properties }]
50
+ : this.v2events.translate(directory, event);
30
51
  const records = v2Frames.length === 0
31
52
  ? [this.record(directory, { v1: event })]
32
53
  : v2Frames.map((v2, index) => this.record(directory, index === 0 ? { v1: event, v2 } : { v2 }));
@@ -5,6 +5,14 @@ export interface Todo {
5
5
  readonly status: "pending" | "in_progress" | "completed" | "cancelled";
6
6
  readonly priority: "medium";
7
7
  }
8
+ /** One todo phase as OmO groups it, in its own order (the Work Status section's waves). */
9
+ export interface TodoPhase {
10
+ readonly name: string;
11
+ readonly tasks: ReadonlyArray<{
12
+ readonly content: string;
13
+ readonly status: Todo["status"];
14
+ }>;
15
+ }
8
16
  export interface TodoUpdated {
9
17
  type: "todo.updated";
10
18
  properties: {
@@ -12,11 +20,14 @@ export interface TodoUpdated {
12
20
  todos: Todo[];
13
21
  };
14
22
  }
23
+ /** The `senpi.todo-state` phases, keeping only well-formed phases and tasks. */
24
+ export declare function todoPhases(data: unknown): TodoPhase[];
15
25
  export declare function flattenTodos(sessionID: string, data: unknown): Todo[];
16
26
  /** Per-session snapshots; each card retains the state produced by its own call. */
17
27
  export declare class TodoBridge {
18
28
  private readonly sessionID;
19
29
  todos: Todo[];
30
+ phases: TodoPhase[];
20
31
  private awaitingState;
21
32
  constructor(sessionID: string);
22
33
  track(part: OCToolPart, args: unknown): void;