antechamber 1.0.30 → 1.0.31

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,18 @@ and this project uses [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.31] - 2026-10-29
11
+
12
+ ### Fixed
13
+
14
+ - Show the MCP servers and their count before a session runs. A directory with no running agent used to report no servers,
15
+ so the sidebar's "MCP servers" count sat at 0 in a new chat, on a dormant project and after the idle reaper stopped the
16
+ last agent. The servers are now read by a short-lived agent started for that read alone.
17
+ - Keep that list current: antechamber announces `mcp.status.changed` when an agent starts, stops or reports new MCP state,
18
+ so OpenChamber re-reads instead of keeping a stale count (it treats an answer as fresh for a minute).
19
+ - Stop a sign-in from failing with "pi process disposed" when OmO answers `auth-start` only while `auth-complete` is
20
+ already running. The agent started for the sign-in was stopped mid-command; the completion now owns it until it ends.
21
+
10
22
  ## [1.0.30] - 2026-10-28
11
23
 
12
24
  - update
package/dist/mcp.d.ts CHANGED
@@ -72,6 +72,11 @@ export declare function v2McpStatus(server: OmoMcpServer): V2McpStatus;
72
72
  * all connected within about a second, each change announced by `loaded_surfaces_changed`.
73
73
  */
74
74
  export declare function waitForMcpServers(pi: PiRpcClient, budgetMs: number, signal?: AbortSignal): Promise<void>;
75
+ /**
76
+ * An agent started, stopped or reported new MCP state. Every directory a client read is told to read
77
+ * again: with the cross-directory fallback, one agent can change the answer for all of them.
78
+ */
79
+ export declare function mcpSurfacesChanged(broadcast: RouteServices["broadcast"]): void;
75
80
  /** The URL OmO prints on its own line after naming the server (`auth` and `auth-start` both do). */
76
81
  export declare function authorizationUrl(text: string, name: string): string | undefined;
77
82
  /**
@@ -80,7 +85,7 @@ export declare function authorizationUrl(text: string, name: string): string | u
80
85
  * authorization request's own `redirect_uri` and `state`.
81
86
  */
82
87
  export declare function redirectInput(authorization: string, input: string): string | undefined;
83
- /** Stops the agents spawned for sign-ins; live sessions are stopped by their own manager. */
88
+ /** Stops the agents spawned for sign-ins and inventories; live sessions are stopped by their own manager. */
84
89
  export declare function disposeMcpAgents(): Promise<void>;
85
90
  export declare function mcpRoute(services: RouteServices, method: string, path: string, directory: string, body: () => Promise<unknown>): Promise<RouteResult | undefined>;
86
91
  export declare function mcpRouteV2(services: RouteServices, method: string, path: string, directory: string, body: () => Promise<unknown>): Promise<RouteResult | undefined>;
package/dist/mcp.js CHANGED
@@ -39,8 +39,18 @@ const SERVER_NAME = /^[^\s"'\\]+$/;
39
39
  const INTEGRATION_PREFIX = "mcp_";
40
40
  const OAUTH_METHOD_ID = "oauth";
41
41
  const BUILTIN_MCP_PATH = "<builtin:mcp>";
42
+ /** How long an inventory read without a running agent is reused. */
43
+ const MCP_INVENTORY_TTL_MS = 30_000;
44
+ /** A fresh agent answers `get_state` once its extensions loaded, about 1.3 s on a typical install. */
45
+ const MCP_INVENTORY_START_MS = 15_000;
46
+ /** OmO reports each server settling separately at startup; one announcement covers the burst. */
47
+ const MCP_CHANGE_DEBOUNCE_MS = 250;
42
48
  const flows = new Map();
43
49
  const admissions = new Map();
50
+ const watchedDirectories = new Set();
51
+ const inventories = new Map();
52
+ const inventoryAgents = new Set();
53
+ let changeTimer;
44
54
  function isRecord(value) {
45
55
  return typeof value === "object" && value !== null && !Array.isArray(value);
46
56
  }
@@ -141,11 +151,58 @@ export async function waitForMcpServers(pi, budgetMs, signal) {
141
151
  }
142
152
  }
143
153
  async function directoryServers(sessions, directory) {
144
- // A directory whose sessions are all dormant would list nothing. Its servers are mostly
145
- // the user's global and built-in ones, so any running session gives a better answer.
146
- const fallback = sessions.liveSessions()[0]?.pi;
147
- const live = liveSession(sessions, directory) ?? (fallback === undefined ? undefined : { pi: fallback });
148
- return live === undefined ? [] : loadedServers(live.pi).catch(() => []);
154
+ watchedDirectories.add(directory);
155
+ // A directory whose sessions are all dormant reads a running session elsewhere: its servers are
156
+ // mostly the user's global and built-in ones. An agent that just exited is skipped.
157
+ const agents = new Set([liveSession(sessions, directory)?.pi, ...sessions.liveSessions().map((s) => s.pi)]);
158
+ for (const pi of agents) {
159
+ if (pi === undefined)
160
+ continue;
161
+ try {
162
+ return await loadedServers(pi);
163
+ }
164
+ catch { }
165
+ }
166
+ return inventory(directory);
167
+ }
168
+ /** With no running agent, a short-lived `--no-session` agent reads the inventory, shared for a while. */
169
+ function inventory(directory) {
170
+ const cached = inventories.get(directory);
171
+ if (cached !== undefined && Date.now() - cached.at < MCP_INVENTORY_TTL_MS)
172
+ return cached.servers;
173
+ const servers = readInventory(directory).catch((error) => {
174
+ if (inventories.get(directory)?.servers === servers)
175
+ inventories.delete(directory);
176
+ console.error(`[mcp] listing servers for ${directory} failed: ${errorMessage(error)}`);
177
+ return [];
178
+ });
179
+ inventories.set(directory, { at: Date.now(), servers });
180
+ return servers;
181
+ }
182
+ async function readInventory(directory) {
183
+ const pi = new PiRpcClient({ cwd: directory, args: ["--no-session"] });
184
+ inventoryAgents.add(pi);
185
+ try {
186
+ await pi.command({ type: "get_state" }, MCP_INVENTORY_START_MS);
187
+ return await loadedServers(pi);
188
+ }
189
+ finally {
190
+ inventoryAgents.delete(pi);
191
+ await pi.dispose();
192
+ }
193
+ }
194
+ /**
195
+ * An agent started, stopped or reported new MCP state. Every directory a client read is told to read
196
+ * again: with the cross-directory fallback, one agent can change the answer for all of them.
197
+ */
198
+ export function mcpSurfacesChanged(broadcast) {
199
+ inventories.clear();
200
+ clearTimeout(changeTimer);
201
+ changeTimer = setTimeout(() => {
202
+ for (const directory of watchedDirectories)
203
+ broadcast(directory, { type: "mcp.tools.changed", properties: { server: "" } });
204
+ }, MCP_CHANGE_DEBOUNCE_MS);
205
+ changeTimer.unref();
149
206
  }
150
207
  /** The builtin MCP command's name in this agent; builds without `sourceInfo` only know it as `mcp`. */
151
208
  async function mcpCommandName(pi) {
@@ -279,14 +336,16 @@ function release(flow) {
279
336
  .dispose()
280
337
  .catch((error) => console.error(`[mcp] stopping an auth agent failed: ${errorMessage(error)}`));
281
338
  }
282
- /** Stops the agents spawned for sign-ins; live sessions are stopped by their own manager. */
339
+ /** Stops the agents spawned for sign-ins and inventories; live sessions are stopped by their own manager. */
283
340
  export async function disposeMcpAgents() {
341
+ clearTimeout(changeTimer);
342
+ inventories.clear();
284
343
  const dedicated = [...flows.values()].filter((flow) => flow.s === undefined && !flow.released);
285
344
  for (const flow of dedicated) {
286
345
  flow.released = true;
287
346
  clearTimeout(flow.expiry);
288
347
  }
289
- await Promise.all(dedicated.map((flow) => flow.pi.dispose()));
348
+ await Promise.all([...dedicated.map((flow) => flow.pi.dispose()), ...[...inventoryAgents].map((pi) => pi.dispose())]);
290
349
  }
291
350
  async function flowStatus(flow, name) {
292
351
  if (flow.server !== undefined || !agentCurrent(flow))
@@ -359,6 +418,7 @@ async function startFlow(services, directory, name, command) {
359
418
  done: Promise.resolve(),
360
419
  running: true,
361
420
  authorized: false,
421
+ completing: false,
362
422
  released: false,
363
423
  };
364
424
  if (live === undefined) {
@@ -380,7 +440,8 @@ async function startFlow(services, directory, name, command) {
380
440
  .then(async () => {
381
441
  if (!flow.authorized)
382
442
  return;
383
- if (live === undefined) {
443
+ // A completion in flight set `authorized` and owns the agent: stopping it now would kill its command.
444
+ if (live === undefined && !flow.completing) {
384
445
  const server = await currentStatus(pi, name).catch(() => undefined);
385
446
  if (server !== undefined)
386
447
  flow.server = server;
@@ -448,6 +509,7 @@ async function completeFlow(services, directory, name, input) {
448
509
  // OmO consumes the pending state on any answer, so a dedicated agent has nothing left to wait for.
449
510
  let attempted = false;
450
511
  let server;
512
+ flow.completing = true;
451
513
  try {
452
514
  if (flow.running && flow.command === "auth") {
453
515
  const target = new URL(redirect);
@@ -483,6 +545,7 @@ async function completeFlow(services, directory, name, input) {
483
545
  return fail(400, "McpAuthError", flow.error);
484
546
  }
485
547
  finally {
548
+ flow.completing = false;
486
549
  if (attempted)
487
550
  release(flow);
488
551
  }
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /** HTTP/SSE composition root for the OpenCode API backed by pi. */
2
2
  import { createHash, randomBytes } from "node:crypto";
3
3
  import { createServer } from "node:http";
4
- import { disposeMcpAgents } from "./mcp.js";
4
+ import { disposeMcpAgents, mcpSurfacesChanged } from "./mcp.js";
5
5
  import { debugLog } from "./misc-routes.js";
6
6
  import { SessionManager } from "./sessions.js";
7
7
  import { loadCatalog } from "./catalog.js";
@@ -30,6 +30,7 @@ export class AdapterServer {
30
30
  this.options = options;
31
31
  this.sse = new SSE(options, this.sseClients, (id) => this.sessions.monitors.owns(id));
32
32
  this.sessions = new SessionManager((directory, event) => this.broadcast(directory, event), undefined, options.defaultDirectory);
33
+ this.sessions.onMcpSurfacesChanged = () => mcpSurfacesChanged((directory, event) => this.broadcast(directory, event));
33
34
  this.server = createServer((req, res) => void this.route(req, res));
34
35
  this.server.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
35
36
  }
@@ -116,6 +116,8 @@ export declare class SessionManager {
116
116
  private scanCache;
117
117
  /** freshness of the all-directories scan (see listAll). */
118
118
  private scanAllCache;
119
+ /** Called when an agent starts, stops or reports a change to its MCP servers. */
120
+ onMcpSurfacesChanged: (() => void) | undefined;
119
121
  constructor(emit: EventSink, store?: SessionStore, defaultDirectory?: string | undefined, lifecycleOptions?: LifecycleOptions);
120
122
  create(directory: string, title?: string, parentID?: string, options?: {
121
123
  id?: string;
package/dist/sessions.js CHANGED
@@ -75,6 +75,8 @@ export class SessionManager {
75
75
  scanCache = new Map();
76
76
  /** freshness of the all-directories scan (see listAll). */
77
77
  scanAllCache;
78
+ /** Called when an agent starts, stops or reports a change to its MCP servers. */
79
+ onMcpSurfacesChanged;
78
80
  constructor(emit, store, defaultDirectory, lifecycleOptions = {}) {
79
81
  this.emit = emit;
80
82
  this.defaultDirectory = defaultDirectory;
@@ -258,6 +260,8 @@ export class SessionManager {
258
260
  const onEvent = (payload) => {
259
261
  if (s.pi !== pi)
260
262
  return;
263
+ if (payload.type === "loaded_surfaces_changed")
264
+ this.onMcpSurfacesChanged?.();
261
265
  this.lifecycle.touch(s);
262
266
  s.approvals?.observeQuestion(payload);
263
267
  try {
@@ -278,6 +282,7 @@ export class SessionManager {
278
282
  const onExit = (code, signal) => {
279
283
  if (s.pi !== pi)
280
284
  return;
285
+ this.onMcpSurfacesChanged?.();
281
286
  // An agent that stopped on its own (timeout, broken pipe, crash) must say why: a notice card in the
282
287
  // transcript, the same error on the reply it interrupted, and a replay after an adapter restart.
283
288
  // Pending commands reject with the same PiStoppedError; prompt admission leaves it to this handler.
@@ -315,6 +320,7 @@ export class SessionManager {
315
320
  pi.onEvent(onEvent);
316
321
  pi.onExtensionUI(onExtensionUI);
317
322
  pi.onExit(onExit);
323
+ this.onMcpSurfacesChanged?.();
318
324
  s.unwirePi = () => {
319
325
  pi.off("event", onEvent);
320
326
  pi.off("extension_ui_request", onExtensionUI);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-compat",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
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.30",
3
+ "version": "1.0.31",
4
4
  "description": "OpenChamber extension: live, interactive graph of the OmO sub-agents behind the current session",
5
5
  "private": true,
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-work",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
4
4
  "description": "OpenChamber 2.x Work Status section: OmO todo phases, monitors, eval cells and background work of the current session",
5
5
  "private": true,
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
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",