antechamber 0.2.4 → 0.2.6

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.
@@ -0,0 +1,48 @@
1
+ import type { ManagedSession } from "./sessions.js";
2
+ import type { OCEvent, OCMessageWithParts } from "./types.js";
3
+ /** Names OmO's terminal extension passes to `pi.rpc.emit` (core/extensions/builtin/monitor-state-event.js). */
4
+ export declare const MONITOR_STATE_EVENT = "terminal_monitor_state";
5
+ export declare const MONITOR_ENDED_EVENT = "terminal_monitor_ended";
6
+ export interface MonitorSnapshot {
7
+ id: string;
8
+ description: string;
9
+ command: string | null;
10
+ filter: string | null;
11
+ paused: boolean;
12
+ persistent: boolean;
13
+ startedAtMs: number;
14
+ deadlineMs: number | null;
15
+ fireCount: number;
16
+ lastFiredAtMs: number | null;
17
+ }
18
+ interface MonitorEnding {
19
+ reason: string;
20
+ exitCode: number | null;
21
+ endedAtMs: number;
22
+ }
23
+ export declare function parseMonitor(value: unknown): MonitorSnapshot | undefined;
24
+ export declare function monitorSummary(monitor: MonitorSnapshot, ending?: MonitorEnding): string;
25
+ export declare class MonitorSessions {
26
+ private readonly sessions;
27
+ private readonly emit;
28
+ private readonly watches;
29
+ /** Children the user deleted; a later snapshot of the same watch must not resurrect them. */
30
+ private readonly dismissed;
31
+ constructor(sessions: Map<string, ManagedSession>, emit: (directory: string, event: OCEvent) => void);
32
+ wire(parent: ManagedSession): void;
33
+ owns(id: string): boolean;
34
+ messages(id: string): OCMessageWithParts[];
35
+ observe(parent: ManagedSession, ev: Record<string, unknown>): void;
36
+ /** The agent process is gone: OmO's in-process watches died with it. */
37
+ stopParent(parentID: string): void;
38
+ /** Forget a deleted child, or every child of a deleted parent. */
39
+ remove(id: string): void;
40
+ private running;
41
+ private childID;
42
+ private applyState;
43
+ private applyEnded;
44
+ private end;
45
+ private create;
46
+ private publish;
47
+ }
48
+ export {};
@@ -0,0 +1,295 @@
1
+ /**
2
+ * OmO terminal monitors as read-only child sessions. OpenChamber has no surface for
3
+ * background watches, but it already lists child sessions under Subagents and gives each
4
+ * a live dot, so a watch is busy while OmO reports it and idle once it ends.
5
+ */
6
+ import { getAgentName } from "./agent-name.js";
7
+ import { ocIdFor } from "./session-store.js";
8
+ /** Names OmO's terminal extension passes to `pi.rpc.emit` (core/extensions/builtin/monitor-state-event.js). */
9
+ export const MONITOR_STATE_EVENT = "terminal_monitor_state";
10
+ export const MONITOR_ENDED_EVENT = "terminal_monitor_ended";
11
+ function record(value) {
12
+ return value !== null && typeof value === "object" && !Array.isArray(value);
13
+ }
14
+ function finite(value) {
15
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
16
+ }
17
+ function text(value) {
18
+ return typeof value === "string" && value !== "" ? value : null;
19
+ }
20
+ export function parseMonitor(value) {
21
+ if (!record(value) || typeof value.id !== "string" || value.id === "")
22
+ return undefined;
23
+ const startedAtMs = finite(value.startedAtMs);
24
+ if (startedAtMs === null)
25
+ return undefined;
26
+ return {
27
+ id: value.id,
28
+ description: text(value.description) ?? value.id,
29
+ command: text(value.command),
30
+ filter: text(value.filter),
31
+ paused: value.paused === true,
32
+ persistent: value.persistent === true,
33
+ startedAtMs,
34
+ deadlineMs: finite(value.deadlineMs),
35
+ fireCount: finite(value.fireCount) ?? 0,
36
+ lastFiredAtMs: finite(value.lastFiredAtMs),
37
+ };
38
+ }
39
+ function endingLabel(ending) {
40
+ switch (ending.reason) {
41
+ case "exit":
42
+ return ending.exitCode === null ? "command exited" : `command exited with code ${ending.exitCode}`;
43
+ case "timeout":
44
+ return "deadline reached";
45
+ case "killed":
46
+ return "stopped";
47
+ case "disposed":
48
+ return "suspended by an OmO shutdown or reload";
49
+ case "agent-exit":
50
+ return "OmO process exited";
51
+ default:
52
+ return "ended";
53
+ }
54
+ }
55
+ function iso(ms) {
56
+ return ms === null ? undefined : new Date(ms).toISOString();
57
+ }
58
+ export function monitorSummary(monitor, ending) {
59
+ const deadline = monitor.persistent ? "none (persistent)" : iso(monitor.deadlineMs);
60
+ return [
61
+ `Status: ${ending ? endingLabel(ending) : monitor.paused ? "paused" : "watching"}`,
62
+ monitor.filter !== null ? `Filter: ${monitor.filter}` : undefined,
63
+ `Events: ${monitor.fireCount}`,
64
+ monitor.lastFiredAtMs !== null ? `Last event: ${iso(monitor.lastFiredAtMs)}` : undefined,
65
+ `Started: ${iso(monitor.startedAtMs)}`,
66
+ ending ? `Ended: ${iso(ending.endedAtMs)}` : deadline !== undefined ? `Deadline: ${deadline}` : undefined,
67
+ ]
68
+ .filter((line) => line !== undefined)
69
+ .join("\n");
70
+ }
71
+ export class MonitorSessions {
72
+ sessions;
73
+ emit;
74
+ watches = new Map();
75
+ /** Children the user deleted; a later snapshot of the same watch must not resurrect them. */
76
+ dismissed = new Set();
77
+ constructor(sessions, emit) {
78
+ this.sessions = sessions;
79
+ this.emit = emit;
80
+ }
81
+ wire(parent) {
82
+ parent.hasBackgroundWork = () => this.running(parent.info.id);
83
+ }
84
+ owns(id) {
85
+ return this.watches.has(id);
86
+ }
87
+ messages(id) {
88
+ return this.watches.get(id)?.session.messages ?? [];
89
+ }
90
+ observe(parent, ev) {
91
+ if (!record(ev.data))
92
+ return;
93
+ if (ev.name === MONITOR_STATE_EVENT && Array.isArray(ev.data.monitors))
94
+ this.applyState(parent, ev.data.monitors);
95
+ else if (ev.name === MONITOR_ENDED_EVENT)
96
+ this.applyEnded(parent, ev.data);
97
+ }
98
+ /** The agent process is gone: OmO's in-process watches died with it. */
99
+ stopParent(parentID) {
100
+ for (const watch of this.watches.values()) {
101
+ if (watch.session.info.parentID !== parentID || watch.ending !== undefined)
102
+ continue;
103
+ this.end(watch, { reason: "agent-exit", exitCode: null, endedAtMs: Date.now() });
104
+ }
105
+ }
106
+ /** Forget a deleted child, or every child of a deleted parent. */
107
+ remove(id) {
108
+ for (const [childID, watch] of this.watches) {
109
+ if (childID !== id && watch.session.info.parentID !== id)
110
+ continue;
111
+ this.watches.delete(childID);
112
+ this.dismissed.add(childID);
113
+ if (this.sessions.get(childID) !== watch.session)
114
+ continue;
115
+ this.sessions.delete(childID);
116
+ this.emit(watch.session.info.directory, { type: "session.deleted", properties: { info: watch.session.info } });
117
+ }
118
+ }
119
+ running(parentID) {
120
+ for (const watch of this.watches.values()) {
121
+ if (watch.session.info.parentID === parentID && watch.ending === undefined)
122
+ return true;
123
+ }
124
+ return false;
125
+ }
126
+ childID(parent, monitorID) {
127
+ return ocIdFor(`${parent.info.id}/monitor/${monitorID}`);
128
+ }
129
+ applyState(parent, entries) {
130
+ const live = new Set();
131
+ for (const entry of entries) {
132
+ const monitor = parseMonitor(entry);
133
+ if (!monitor)
134
+ continue;
135
+ const id = this.childID(parent, monitor.id);
136
+ live.add(id);
137
+ if (this.dismissed.has(id))
138
+ continue;
139
+ const watch = this.watches.get(id);
140
+ if (watch === undefined)
141
+ this.create(parent, id, monitor);
142
+ else {
143
+ watch.monitor = monitor;
144
+ delete watch.ending;
145
+ this.publish(watch);
146
+ }
147
+ }
148
+ // A snapshot lists every live watch; one that is missing has ended. Its reason may follow.
149
+ for (const [id, watch] of this.watches) {
150
+ if (watch.session.info.parentID !== parent.info.id || watch.ending !== undefined || live.has(id))
151
+ continue;
152
+ this.end(watch, { reason: "ended", exitCode: null, endedAtMs: Date.now() });
153
+ }
154
+ }
155
+ applyEnded(parent, data) {
156
+ if (typeof data.id !== "string")
157
+ return;
158
+ const watch = this.watches.get(this.childID(parent, data.id));
159
+ if (!watch)
160
+ return;
161
+ const fireCount = finite(data.fireCount);
162
+ if (fireCount !== null)
163
+ watch.monitor.fireCount = fireCount;
164
+ this.end(watch, {
165
+ reason: typeof data.reason === "string" ? data.reason : "ended",
166
+ exitCode: finite(data.exitCode),
167
+ endedAtMs: finite(data.endedAtMs) ?? Date.now(),
168
+ });
169
+ }
170
+ end(watch, ending) {
171
+ watch.ending = ending;
172
+ this.publish(watch);
173
+ }
174
+ create(parent, id, monitor) {
175
+ const now = Date.now();
176
+ const session = {
177
+ info: {
178
+ id,
179
+ parentID: parent.info.id,
180
+ projectID: parent.info.projectID,
181
+ directory: parent.info.directory,
182
+ title: `Monitor: ${monitor.description}`,
183
+ version: parent.info.version,
184
+ time: { created: monitor.startedAtMs, updated: now },
185
+ },
186
+ pi: undefined,
187
+ messages: [],
188
+ model: parent.model,
189
+ streaming: undefined,
190
+ status: { type: "idle" },
191
+ toolByCallId: new Map(),
192
+ tree: { messageEntries: {} },
193
+ hasPendingInteraction: () => false,
194
+ hasRunningChildren: () => false,
195
+ };
196
+ const watch = { session, monitor };
197
+ this.watches.set(id, watch);
198
+ this.sessions.set(id, session);
199
+ this.emit(session.info.directory, { type: "session.created", properties: { info: session.info } });
200
+ this.publish(watch);
201
+ }
202
+ publish(watch) {
203
+ const { session, monitor, ending } = watch;
204
+ const directory = session.info.directory;
205
+ const sessionID = session.info.id;
206
+ const base = sessionID.replace(/^ses_/, "");
207
+ const model = session.model ?? { providerID: "omo", modelID: "monitor" };
208
+ const started = monitor.startedAtMs;
209
+ const command = monitor.command ?? monitor.description;
210
+ const input = { command, description: monitor.description };
211
+ const output = monitorSummary(monitor, ending);
212
+ const tool = {
213
+ id: `prt_${base}_tool`,
214
+ sessionID,
215
+ messageID: `msg_${base}_1`,
216
+ type: "tool",
217
+ callID: `call_${base}`,
218
+ tool: "bash",
219
+ state: ending
220
+ ? {
221
+ status: "completed",
222
+ input,
223
+ output,
224
+ title: monitor.description,
225
+ metadata: { monitor, ending },
226
+ time: { start: started, end: ending.endedAtMs },
227
+ }
228
+ : {
229
+ status: "running",
230
+ input,
231
+ title: monitor.description,
232
+ metadata: { monitor, output },
233
+ time: { start: started },
234
+ },
235
+ };
236
+ const messages = [
237
+ {
238
+ info: {
239
+ id: `msg_${base}_0`,
240
+ sessionID,
241
+ role: "user",
242
+ time: { created: started },
243
+ agent: getAgentName(),
244
+ model,
245
+ },
246
+ parts: [
247
+ {
248
+ id: `prt_${base}_request`,
249
+ sessionID,
250
+ messageID: `msg_${base}_0`,
251
+ type: "text",
252
+ text: "The agent started a background monitor",
253
+ synthetic: true,
254
+ time: { start: started, end: started },
255
+ },
256
+ ],
257
+ },
258
+ {
259
+ info: {
260
+ id: `msg_${base}_1`,
261
+ sessionID,
262
+ role: "assistant",
263
+ time: { created: started, ...(ending ? { completed: ending.endedAtMs } : {}) },
264
+ parentID: `msg_${base}_0`,
265
+ modelID: model.modelID,
266
+ providerID: model.providerID,
267
+ mode: getAgentName(),
268
+ path: { cwd: directory, root: directory },
269
+ cost: 0,
270
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
271
+ ...(ending ? { finish: "stop" } : {}),
272
+ },
273
+ parts: [tool],
274
+ },
275
+ ];
276
+ const before = new Map((session.messages ?? []).map((message) => [message.info.id, JSON.stringify(message)]));
277
+ session.messages = messages;
278
+ for (const message of messages) {
279
+ if (before.get(message.info.id) === JSON.stringify(message))
280
+ continue;
281
+ this.emit(directory, { type: "message.updated", properties: { info: message.info } });
282
+ for (const part of message.parts)
283
+ this.emit(directory, { type: "message.part.updated", properties: { part } });
284
+ }
285
+ const status = ending ? "idle" : "busy";
286
+ if (session.status.type === status)
287
+ return;
288
+ session.status = { type: status };
289
+ session.info.time.updated = Date.now();
290
+ this.emit(directory, { type: "session.updated", properties: { info: session.info } });
291
+ this.emit(directory, { type: "session.status", properties: { sessionID, status: session.status } });
292
+ if (status === "idle")
293
+ this.emit(directory, { type: "session.idle", properties: { sessionID } });
294
+ }
295
+ }
@@ -3,6 +3,7 @@ import type { OCAssistantMessage, OCMessageWithParts, OCModelRef, OCEvent, OCSes
3
3
  import type { ManagedSession } from "./sessions.js";
4
4
  import type { SessionStore } from "./session-store.js";
5
5
  import type { SubagentBridge } from "./subagents.js";
6
+ import type { MonitorSessions } from "./monitor-sessions.js";
6
7
  import { type TodoUpdated } from "./todo-bridge.js";
7
8
  export interface PiUsage {
8
9
  input?: number;
@@ -38,6 +39,7 @@ export declare function compactionMessages(s: ManagedSession, key: string, summa
38
39
  interface PiEventServices {
39
40
  readonly emit: (directory: string, event: OCEvent | TodoUpdated) => void;
40
41
  readonly subagents: SubagentBridge;
42
+ readonly monitors: MonitorSessions;
41
43
  readonly store: Pick<SessionStore, "update">;
42
44
  readonly dirtyDiffs: Set<string>;
43
45
  readonly ensureMessages: (s: ManagedSession) => OCMessageWithParts[];
@@ -284,6 +284,9 @@ export class PiEventTranslator {
284
284
  case "compaction_end":
285
285
  this.onCompactionEvent(s, ev);
286
286
  break;
287
+ case "extension_event":
288
+ this.services.monitors.observe(s, ev);
289
+ break;
287
290
  default:
288
291
  break; // turn_start, queue_update — future milestones
289
292
  }
package/dist/pi-rpc.js CHANGED
@@ -93,6 +93,8 @@ export class PiRpcClient extends EventEmitter {
93
93
  ...(process.env.SENPI_RPC_CLIENT_CAPABILITIES ?? "").split(","),
94
94
  "auto_title_sessions",
95
95
  "question",
96
+ // Extension `pi.rpc.emit` frames, e.g. the terminal extension's monitor state.
97
+ "extension_events",
96
98
  ]
97
99
  .map((value) => value.trim())
98
100
  .filter(Boolean)),
package/dist/routes-v1.js CHANGED
@@ -3,6 +3,7 @@ import { execFile } from "node:child_process";
3
3
  import { homedir } from "node:os";
4
4
  import { errorBody, json } from "./http.js";
5
5
  import { miscRoute } from "./misc-routes.js";
6
+ import { mcpRoute } from "./mcp.js";
6
7
  import { getAgentName } from "./agent-name.js";
7
8
  import { nativePromptInput, parsePromptParts } from "./prompt-parts.js";
8
9
  import { RequestBodyError } from "./http.js";
@@ -63,6 +64,11 @@ function parseV1Prompt(body, defaultModel) {
63
64
  export async function handleV1(services, req, res, url, path, method, directory) {
64
65
  if (await handleV1GlobalRoute(services, res, path, method))
65
66
  return;
67
+ const mcp = await mcpRoute(services, method, path, directory, () => services.body(req));
68
+ if (mcp !== undefined) {
69
+ json(res, mcp.status, mcp.body);
70
+ return;
71
+ }
66
72
  // --- sessions ---
67
73
  const misc = await miscRoute(method, path, url, directory);
68
74
  if (misc !== undefined) {
@@ -376,10 +382,6 @@ export async function handleV1(services, req, res, url, path, method, directory)
376
382
  json(res, 200, []);
377
383
  return;
378
384
  }
379
- if (path === "/mcp" && method === "GET") {
380
- json(res, 200, {});
381
- return;
382
- }
383
385
  if (path === "/command" && method === "GET") {
384
386
  json(res, 200, (await getCommands(directory)).map((command) => ({
385
387
  ...command,
package/dist/routes-v2.js CHANGED
@@ -6,6 +6,7 @@ import { promisify } from "node:util";
6
6
  import { errorBody, json, RequestBodyError } from "./http.js";
7
7
  import { nativePromptInput, parsePromptParts, v2PromptAttachments, v2PromptParts } from "./prompt-parts.js";
8
8
  import { debugLog, miscRoute } from "./misc-routes.js";
9
+ import { mcpRouteV2 } from "./mcp.js";
9
10
  import { OPENCODE_API_VERSION } from "./version.js";
10
11
  import { toV2Permission } from "./approvals.js";
11
12
  import { formQuestionAnswer, questionFormID, toQuestionForm } from "./question-forms.js";
@@ -192,8 +193,12 @@ export async function handleV2(services, req, res, url, path, method, directory)
192
193
  res.writeHead(204).end();
193
194
  return;
194
195
  }
195
- if (path === "/api/integration" && method === "GET") {
196
- json(res, 200, withLocation([]));
196
+ const mcp = await mcpRouteV2(services, method, path, directory, () => services.body(req));
197
+ if (mcp !== undefined) {
198
+ if (mcp.status === 204)
199
+ res.writeHead(204).end();
200
+ else
201
+ json(res, mcp.status, mcp.body);
197
202
  return;
198
203
  }
199
204
  const integrationMatch = path.match(/^\/api\/integration\/([^/]+)$/);
@@ -226,9 +231,10 @@ export async function handleV2(services, req, res, url, path, method, directory)
226
231
  });
227
232
  return;
228
233
  }
229
- // Read-only inventories OpenChamber loads at bootstrap: OmO has no background shells,
230
- // web-search providers or MCP servers, so each is an honest empty list (mutations stay 400).
231
- if ((path === "/api/shell" || path === "/api/websearch/provider" || path === "/api/mcp") && method === "GET") {
234
+ // Read-only inventories OpenChamber loads at bootstrap. OmO has no OpenCode-style shells or
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).
237
+ if ((path === "/api/shell" || path === "/api/websearch/provider") && method === "GET") {
232
238
  json(res, 200, withLocation([]));
233
239
  return;
234
240
  }
@@ -9,6 +9,7 @@ import { ApprovalBridge, type PendingPermission, type PendingQuestion } from "./
9
9
  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
+ import { MonitorSessions } from "./monitor-sessions.js";
12
13
  import { SessionStore, type TitleSource } from "./session-store.js";
13
14
  import type { OCAssistantMessage, OCEvent, OCMessageWithParts, OCModelRef, OCPart, OCPromptPart, OCSession, OCSessionStatus, OCToolPart } from "./types.js";
14
15
  export { ADAPTER_VERSION } from "./version.js";
@@ -65,6 +66,8 @@ export interface ManagedSession extends ThinkingState {
65
66
  /** Hooks wired by the approvals and child-task bridges. */
66
67
  hasPendingInteraction: () => boolean;
67
68
  hasRunningChildren: () => boolean;
69
+ /** Live OmO background work (monitors) that keeps the agent process from idle eviction. */
70
+ hasBackgroundWork?: () => boolean;
68
71
  }
69
72
  type EventSink = (directory: string, event: OCEvent | TodoUpdated) => void;
70
73
  export declare class SessionManager {
@@ -82,6 +85,7 @@ export declare class SessionManager {
82
85
  private readonly store;
83
86
  readonly lifecycle: SessionLifecycle;
84
87
  readonly subagents: SubagentBridge;
88
+ readonly monitors: MonitorSessions;
85
89
  /** directory → last scan freshness (pi dir mtime + wall clock). */
86
90
  private scanCache;
87
91
  /** freshness of the all-directories scan (see listAll). */
@@ -100,6 +104,8 @@ export declare class SessionManager {
100
104
  private wirePi;
101
105
  /** pi writes `<ts>_<sessionId>.jsonl` at startup — poll briefly for it. */
102
106
  private resolvePiFile;
107
+ /** Sessions with a running agent in `directory`, most recently used first; never revives a dormant one. */
108
+ liveSessions(directory: string): ManagedSession[];
103
109
  list(directory?: string): OCSession[];
104
110
  /**
105
111
  * Every restorable session on disk (all directories) plus live ones. Backs the
package/dist/sessions.js CHANGED
@@ -16,6 +16,7 @@ import { getAgentName, getExposedAgentNames } from "./agent-name.js";
16
16
  import { RequestBodyError } from "./http.js";
17
17
  import { bindMessageEntries, durableShellTree, forkConversation, getEntries, navigate, readSessionTree, revertConversation, SessionTreeError, unrevertConversation, } from "./session-tree.js";
18
18
  import { SubagentBridge } from "./subagents.js";
19
+ import { MonitorSessions } from "./monitor-sessions.js";
19
20
  import { sessionDiff } from "./session-diff.js";
20
21
  import { announceSessionNotice, slashTitlePrompt } from "./session-commands.js";
21
22
  import { ocIdFor, piSessionDir, piSessionsRootMtime, scanAllPiSessions, scanPiSessions, SessionStore, readSessionTitle, } from "./session-store.js";
@@ -40,6 +41,7 @@ export class SessionManager {
40
41
  store;
41
42
  lifecycle;
42
43
  subagents;
44
+ monitors;
43
45
  /** directory → last scan freshness (pi dir mtime + wall clock). */
44
46
  scanCache = new Map();
45
47
  /** freshness of the all-directories scan (see listAll). */
@@ -50,9 +52,11 @@ export class SessionManager {
50
52
  this.store = store ?? new SessionStore();
51
53
  this.lifecycle = new SessionLifecycle(() => this.sessions.values(), lifecycleOptions);
52
54
  this.subagents = new SubagentBridge(this.store, this.sessions, this.emit, (session, raw) => parsePiSessionText(session, getAgentName(), raw));
55
+ this.monitors = new MonitorSessions(this.sessions, this.emit);
53
56
  this.events = new PiEventTranslator({
54
57
  emit: this.emit,
55
58
  subagents: this.subagents,
59
+ monitors: this.monitors,
56
60
  store: this.store,
57
61
  dirtyDiffs: this.dirtyDiffs,
58
62
  ensureMessages: (s) => this.ensureMessages(s),
@@ -122,6 +126,7 @@ export class SessionManager {
122
126
  this.sessions.set(info.id, managed);
123
127
  published = true;
124
128
  this.subagents.wire(managed);
129
+ this.monitors.wire(managed);
125
130
  this.lifecycle.touch(managed);
126
131
  this.wirePi(managed, pi);
127
132
  if (options.model !== undefined)
@@ -236,6 +241,7 @@ export class SessionManager {
236
241
  });
237
242
  delete s.interruptRequested;
238
243
  this.subagents.stopParent(s.info.id);
244
+ this.monitors.stopParent(s.info.id);
239
245
  void this.lifecycle
240
246
  .dormant(s)
241
247
  .catch((error) => console.error("[lifecycle] exit cleanup failed:", error));
@@ -276,6 +282,14 @@ export class SessionManager {
276
282
  }
277
283
  return undefined;
278
284
  }
285
+ /** Sessions with a running agent in `directory`, most recently used first; never revives a dormant one. */
286
+ liveSessions(directory) {
287
+ if (this.disposed)
288
+ return [];
289
+ return [...this.sessions.values()]
290
+ .filter((s) => s.pi !== undefined && s.info.directory === directory)
291
+ .sort((a, b) => this.lifecycle.lastUsed(b) - this.lifecycle.lastUsed(a));
292
+ }
279
293
  list(directory) {
280
294
  this.refreshNativeTitles();
281
295
  this.subagents.restoreAll();
@@ -319,8 +333,9 @@ export class SessionManager {
319
333
  this.sessions.delete(sessionId);
320
334
  this.emit(s.info.directory, { type: "session.deleted", properties: { info: s.info } });
321
335
  await this.lifecycle.dormant(s);
322
- const piPath = this.subagents.owns(sessionId) ? undefined : (s.piPath ?? s.restored?.piPath);
336
+ const piPath = this.subagents.owns(sessionId) || this.monitors.owns(sessionId) ? undefined : (s.piPath ?? s.restored?.piPath);
323
337
  this.subagents.remove(sessionId);
338
+ this.monitors.remove(sessionId);
324
339
  if (piPath !== undefined) {
325
340
  try {
326
341
  unlinkSync(piPath);
@@ -1133,6 +1148,8 @@ export class SessionManager {
1133
1148
  ensureMessages(s) {
1134
1149
  if (this.subagents.owns(s.info.id))
1135
1150
  return this.subagents.messages(s.info.id);
1151
+ if (this.monitors.owns(s.info.id))
1152
+ return this.monitors.messages(s.info.id);
1136
1153
  if (s.messages === undefined) {
1137
1154
  const admissions = s.tree.localShellTurns?.length;
1138
1155
  s.messages = parsePiSessionFile(s, getAgentName());
@@ -1260,6 +1277,7 @@ export class SessionManager {
1260
1277
  if (reg?.connectorOwned === true)
1261
1278
  this.createdSessions.set(sc.piSessionId, sc.directory);
1262
1279
  this.subagents.wire(managed);
1280
+ this.monitors.wire(managed);
1263
1281
  return managed;
1264
1282
  }
1265
1283
  /** Resolve a bare session id (e.g. deep link after restart) via known directories. */
package/dist/types.d.ts CHANGED
@@ -265,6 +265,11 @@ export type OCEvent = {
265
265
  } | {
266
266
  type: "server.connected";
267
267
  properties: Record<string, unknown>;
268
+ } | {
269
+ type: "mcp.tools.changed";
270
+ properties: {
271
+ server: string;
272
+ };
268
273
  } | {
269
274
  type: "session.created";
270
275
  properties: {
package/dist/v2.js CHANGED
@@ -390,6 +390,8 @@ export class V2EventTranslator {
390
390
  sessionID: ev.properties.sessionID,
391
391
  }),
392
392
  ];
393
+ case "mcp.tools.changed":
394
+ return [this.frame(directory, "mcp.status.changed", { server: ev.properties.server })];
393
395
  default:
394
396
  return [];
395
397
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-graph",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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",