antechamber 1.0.30 → 1.0.32
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 +23 -0
- package/dist/mcp.d.ts +6 -1
- package/dist/mcp.js +71 -8
- package/dist/pi-rpc.d.ts +2 -0
- package/dist/pi-rpc.js +13 -1
- package/dist/server.js +2 -1
- package/dist/session-commands.d.ts +2 -0
- package/dist/session-commands.js +53 -4
- package/dist/sessions.d.ts +2 -0
- package/dist/sessions.js +12 -1
- package/extensions/omo-compat/package.json +1 -1
- package/extensions/omo-dag/package.json +1 -1
- package/extensions/omo-work/package.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project uses [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.32] - 2026-10-29
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Stop killing the agent when a shell command run from the composer takes longer than a minute, such as an
|
|
15
|
+
interactive login. A user shell now runs until its command exits or you press Stop; only a shell that OmO still
|
|
16
|
+
has not answered a minute after Stop stops the agent.
|
|
17
|
+
- Show a shell command's output live in its card while it runs, so a link or code a command prints before it waits
|
|
18
|
+
(such as a login URL) can be copied. Before, the card stayed empty until the command ended. A command you stop
|
|
19
|
+
keeps the output it printed.
|
|
20
|
+
|
|
21
|
+
## [1.0.31] - 2026-10-29
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Show the MCP servers and their count before a session runs. A directory with no running agent used to report no servers,
|
|
26
|
+
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
|
|
27
|
+
last agent. The servers are now read by a short-lived agent started for that read alone.
|
|
28
|
+
- Keep that list current: antechamber announces `mcp.status.changed` when an agent starts, stops or reports new MCP state,
|
|
29
|
+
so OpenChamber re-reads instead of keeping a stale count (it treats an answer as fresh for a minute).
|
|
30
|
+
- Stop a sign-in from failing with "pi process disposed" when OmO answers `auth-start` only while `auth-complete` is
|
|
31
|
+
already running. The agent started for the sign-in was stopped mid-command; the completion now owns it until it ends.
|
|
32
|
+
|
|
10
33
|
## [1.0.30] - 2026-10-28
|
|
11
34
|
|
|
12
35
|
- 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
|
-
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
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
|
-
|
|
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/pi-rpc.d.ts
CHANGED
|
@@ -137,6 +137,8 @@ export declare class PiRpcClient extends EventEmitter {
|
|
|
137
137
|
probe<T = unknown>(cmd: Omit<PiRpcCommand, "id">, timeoutMs?: number, signal?: AbortSignal): Promise<PiRpcResponse<T>>;
|
|
138
138
|
/** Send a command and wait for its correlated response. A timeout stops the agent unless `stopOnTimeout` is false. */
|
|
139
139
|
command<T = unknown>(cmd: Omit<PiRpcCommand, "id">, timeoutMs?: number, signal?: AbortSignal, onDispatch?: () => void, stopOnTimeout?: boolean): Promise<PiRpcResponse<T>>;
|
|
140
|
+
/** Asks OmO to stop the user shell; a pending `bash` still unanswered after the ordinary deadline means a hung agent. */
|
|
141
|
+
abortBash(): void;
|
|
140
142
|
/** Fire-and-forget command (no response correlation). */
|
|
141
143
|
notify(cmd: Omit<PiRpcCommand, "id">): void;
|
|
142
144
|
dispose(): Promise<void>;
|
package/dist/pi-rpc.js
CHANGED
|
@@ -403,6 +403,10 @@ export class PiRpcClient extends EventEmitter {
|
|
|
403
403
|
const expectedCommand = String(cmd.type);
|
|
404
404
|
const deadlineMs = timeoutMs ?? RPC_COMMAND_TIMEOUT_MS;
|
|
405
405
|
const waitsForWork = expectedCommand === "prompt" && timeoutMs === undefined;
|
|
406
|
+
// A user `bash` answers when its command exits: OmO awaits `executeBash` with no time limit (senpi
|
|
407
|
+
// 2026.10.25-2, dist/modes/rpc/connection-handler.js `case "bash"`), and an interactive command such as a
|
|
408
|
+
// login waits on the user. Without a caller deadline it gets one only once `abortBash` asks OmO to stop it.
|
|
409
|
+
const waitsForShell = expectedCommand === "bash" && timeoutMs === undefined;
|
|
406
410
|
return new Promise((resolve, reject) => {
|
|
407
411
|
let timer;
|
|
408
412
|
let admissionTimer;
|
|
@@ -431,7 +435,8 @@ export class PiRpcClient extends EventEmitter {
|
|
|
431
435
|
if (!waitsForWork || !(this.agentActive || this.compacting))
|
|
432
436
|
timer = setTimeout(() => expire(deadlineMs), deadlineMs);
|
|
433
437
|
};
|
|
434
|
-
|
|
438
|
+
if (!waitsForShell)
|
|
439
|
+
armDeadline();
|
|
435
440
|
if (waitsForWork)
|
|
436
441
|
admissionTimer = setTimeout(() => expire(PROMPT_ADMISSION_TIMEOUT_MS), PROMPT_ADMISSION_TIMEOUT_MS);
|
|
437
442
|
this.pending.set(id, {
|
|
@@ -443,12 +448,19 @@ export class PiRpcClient extends EventEmitter {
|
|
|
443
448
|
},
|
|
444
449
|
reject: rejectCommand,
|
|
445
450
|
...(waitsForWork ? { onWorkState: armDeadline } : {}),
|
|
451
|
+
...(waitsForShell ? { onAbortBash: () => timer === undefined && armDeadline() } : {}),
|
|
446
452
|
});
|
|
447
453
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
448
454
|
this.write({ ...cmd, id });
|
|
449
455
|
onDispatch?.();
|
|
450
456
|
});
|
|
451
457
|
}
|
|
458
|
+
/** Asks OmO to stop the user shell; a pending `bash` still unanswered after the ordinary deadline means a hung agent. */
|
|
459
|
+
abortBash() {
|
|
460
|
+
this.write({ type: "abort_bash" });
|
|
461
|
+
for (const request of this.pending.values())
|
|
462
|
+
request.onAbortBash?.();
|
|
463
|
+
}
|
|
452
464
|
/** Fire-and-forget command (no response correlation). */
|
|
453
465
|
notify(cmd) {
|
|
454
466
|
this.write(cmd);
|
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
|
}
|
|
@@ -54,6 +54,8 @@ type CompactResult = {
|
|
|
54
54
|
kind: "error";
|
|
55
55
|
message: string;
|
|
56
56
|
};
|
|
57
|
+
/** Appends a chunk and keeps the newest `limit` chars, starting at a line boundary when one leaves anything. */
|
|
58
|
+
export declare function appendLiveOutput(previous: string, delta: string, limit?: number): string;
|
|
57
59
|
export declare const unsupportedShare: {
|
|
58
60
|
readonly name: "UnsupportedError";
|
|
59
61
|
readonly data: {
|
package/dist/session-commands.js
CHANGED
|
@@ -2,6 +2,22 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { PiRpcClient, UpstreamProtocolError } from "./pi-rpc.js";
|
|
3
3
|
import { getAgentName } from "./agent-name.js";
|
|
4
4
|
import { nativePromptInput, parsePromptParts } from "./prompt-parts.js";
|
|
5
|
+
/**
|
|
6
|
+
* Live output kept on a running shell card: the newest part. Every snapshot carries all of it, so it is kept small;
|
|
7
|
+
* the finished card gets OmO's full output.
|
|
8
|
+
*/
|
|
9
|
+
const LIVE_SHELL_OUTPUT_CHARS = 64 * 1024;
|
|
10
|
+
/** Output chunks arriving within this window reach clients as one card snapshot. */
|
|
11
|
+
const LIVE_SHELL_OUTPUT_FLUSH_MS = 100;
|
|
12
|
+
/** Appends a chunk and keeps the newest `limit` chars, starting at a line boundary when one leaves anything. */
|
|
13
|
+
export function appendLiveOutput(previous, delta, limit = LIVE_SHELL_OUTPUT_CHARS) {
|
|
14
|
+
const output = previous + delta;
|
|
15
|
+
if (output.length <= limit)
|
|
16
|
+
return output;
|
|
17
|
+
const tail = output.slice(output.length - limit);
|
|
18
|
+
const lineStart = tail.indexOf("\n") + 1;
|
|
19
|
+
return lineStart > 0 && lineStart < tail.length ? tail.slice(lineStart) : tail;
|
|
20
|
+
}
|
|
5
21
|
// OpenChamber drops rejected Share actions, so emit a visible session notice too.
|
|
6
22
|
export const unsupportedShare = {
|
|
7
23
|
name: "UnsupportedError",
|
|
@@ -311,13 +327,35 @@ export async function runShellCommand(context) {
|
|
|
311
327
|
const afterEntryId = session.tree.leafId ?? null;
|
|
312
328
|
const controller = new AbortController();
|
|
313
329
|
let interrupted = false;
|
|
330
|
+
// OmO streams user bash output as `bash_execution_update {id: executionId, delta}` frames before its reply
|
|
331
|
+
// (agent-session.js `executeBash`; real RPC probe on 2026-10-28). Showing them live lets the user act on
|
|
332
|
+
// output while the command waits, such as a login link.
|
|
333
|
+
let streaming;
|
|
334
|
+
let liveOutput = "";
|
|
335
|
+
let flush;
|
|
336
|
+
const onOutput = (event) => {
|
|
337
|
+
const [part] = message.parts;
|
|
338
|
+
if (event.type !== "bash_execution_update" || event.id !== part.callID || typeof event.delta !== "string")
|
|
339
|
+
return;
|
|
340
|
+
if (part.state.status !== "running")
|
|
341
|
+
return;
|
|
342
|
+
liveOutput = appendLiveOutput(liveOutput, event.delta);
|
|
343
|
+
part.state.metadata = { ...part.state.metadata, output: liveOutput };
|
|
344
|
+
flush ??= setTimeout(() => {
|
|
345
|
+
flush = undefined;
|
|
346
|
+
if (part.state.status === "running")
|
|
347
|
+
shellEvent(context, { type: "message.part.updated", properties: { part } });
|
|
348
|
+
}, LIVE_SHELL_OUTPUT_FLUSH_MS);
|
|
349
|
+
};
|
|
350
|
+
// A stopped card keeps what the command printed, such as a link the user still needs.
|
|
351
|
+
const streamed = () => (liveOutput === "" ? {} : { output: liveOutput });
|
|
314
352
|
session.cancelShell = (byRestart = false) => {
|
|
315
353
|
interrupted = byRestart;
|
|
316
354
|
delete session.cancelShell;
|
|
317
355
|
// Agent abort does not stop user bash. Dispatch the dedicated RPC first,
|
|
318
356
|
// then release our waiter; a correlated late response is discarded.
|
|
319
357
|
if (dispatch[message.info.id])
|
|
320
|
-
session.pi?.
|
|
358
|
+
session.pi?.abortBash();
|
|
321
359
|
controller.abort();
|
|
322
360
|
};
|
|
323
361
|
sessions.appendMessage(session.info.id, user);
|
|
@@ -332,8 +370,10 @@ export async function runShellCommand(context) {
|
|
|
332
370
|
});
|
|
333
371
|
try {
|
|
334
372
|
await sessions.update(session.info.id, {});
|
|
335
|
-
const response = await sessions.sessionRpc(session.info.id, { type: "bash", command }, controller.signal, () => {
|
|
373
|
+
const response = await sessions.sessionRpc(session.info.id, { type: "bash", command, executionId: message.parts[0].callID }, controller.signal, () => {
|
|
336
374
|
dispatch[message.info.id] = true;
|
|
375
|
+
streaming = session.pi;
|
|
376
|
+
streaming?.onEvent(onOutput);
|
|
337
377
|
// Only actual dispatch admissions are durable; queued attempts stay in the live cache.
|
|
338
378
|
(session.tree.localShellTurns ??= []).push({ afterEntryId, messages: [user, message] });
|
|
339
379
|
// A native result can land beyond the last synchronized leaf.
|
|
@@ -351,7 +391,11 @@ export async function runShellCommand(context) {
|
|
|
351
391
|
}
|
|
352
392
|
const result = parseShellResult(response.data);
|
|
353
393
|
if (result.cancelled) {
|
|
354
|
-
failShellMessage(message, command, "Command cancelled", {
|
|
394
|
+
failShellMessage(message, command, "Command cancelled", {
|
|
395
|
+
...streamed(),
|
|
396
|
+
cancelled: true,
|
|
397
|
+
exitCode: result.exitCode,
|
|
398
|
+
});
|
|
355
399
|
}
|
|
356
400
|
else {
|
|
357
401
|
completeShellMessage(message, command, result);
|
|
@@ -361,7 +405,10 @@ export async function runShellCommand(context) {
|
|
|
361
405
|
}
|
|
362
406
|
catch (error) {
|
|
363
407
|
if (controller.signal.aborted) {
|
|
364
|
-
failShellMessage(message, command, interrupted ? "Interrupted by restart" : "Command cancelled",
|
|
408
|
+
failShellMessage(message, command, interrupted ? "Interrupted by restart" : "Command cancelled", {
|
|
409
|
+
...streamed(),
|
|
410
|
+
...(interrupted ? { interrupted: true } : { cancelled: true }),
|
|
411
|
+
});
|
|
365
412
|
emitShellMessage(context, message);
|
|
366
413
|
return { kind: "completed", message };
|
|
367
414
|
}
|
|
@@ -370,6 +417,8 @@ export async function runShellCommand(context) {
|
|
|
370
417
|
throw error;
|
|
371
418
|
}
|
|
372
419
|
finally {
|
|
420
|
+
streaming?.off("event", onOutput);
|
|
421
|
+
clearTimeout(flush);
|
|
373
422
|
delete session.cancelShell;
|
|
374
423
|
const state = message.parts[0].state;
|
|
375
424
|
const metadata = state.status === "pending" ? undefined : state.metadata;
|
package/dist/sessions.d.ts
CHANGED
|
@@ -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);
|
|
@@ -833,7 +839,12 @@ export class SessionManager {
|
|
|
833
839
|
if (pi === undefined)
|
|
834
840
|
throw new Error(`session cannot be resumed: ${sessionId}`);
|
|
835
841
|
this.lifecycle.touch(s);
|
|
836
|
-
|
|
842
|
+
// A user shell runs until its command exits or the user stops it (`PiRpcClient.abortBash`).
|
|
843
|
+
const timeout = command.type === "compact"
|
|
844
|
+
? COMPACT_RPC_TIMEOUT_MS
|
|
845
|
+
: command.type === "bash"
|
|
846
|
+
? undefined
|
|
847
|
+
: RPC_COMMAND_TIMEOUT_MS;
|
|
837
848
|
// OmO cancels a compaction whenever an MCP server's tools register, which a freshly revived agent
|
|
838
849
|
// does for about a second; start it once they have. The retries below remain the backstop.
|
|
839
850
|
if (command.type === "compact")
|