@selesai/code 0.13.39 → 0.13.40
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/dist/core/intercom-rpc.d.ts +135 -0
- package/dist/core/intercom-rpc.js +64 -0
- package/dist/core/release-rpc.d.ts +137 -0
- package/dist/core/release-rpc.js +77 -0
- package/dist/core/resource-loader.d.ts +3 -0
- package/dist/core/resource-loader.js +3 -0
- package/dist/core/subagent-rpc.d.ts +110 -0
- package/dist/core/subagent-rpc.js +60 -0
- package/dist/extensions/agent-browser.ts +9 -0
- package/dist/extensions/pi-intercom/README.md +15 -0
- package/dist/extensions/pi-intercom/broker/broker.ts +277 -13
- package/dist/extensions/pi-intercom/broker/client.ts +6 -2
- package/dist/extensions/pi-intercom/broker/mailbox-persistence.test.ts +523 -0
- package/dist/extensions/pi-intercom/broker/paths.test.ts +4 -4
- package/dist/extensions/pi-intercom/broker/paths.ts +4 -2
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +1 -3
- package/dist/extensions/pi-intercom/extension-api.ts +10 -0
- package/dist/extensions/pi-intercom/index.ts +188 -8
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +280 -0
- package/dist/extensions/pi-intercom/package.json +1 -1
- package/dist/extensions/pi-intercom/release-readiness.test.ts +116 -0
- package/dist/extensions/pi-intercom/release-readiness.ts +93 -0
- package/dist/extensions/pi-subagents/docs/extension-api.md +8 -0
- package/dist/extensions/pi-subagents/docs/observability.md +8 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +51 -1
- package/dist/extensions/pi-subagents/src/extension/observability.ts +164 -0
- package/dist/extensions/pi-subagents/src/extension/release-readiness.ts +367 -0
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +8 -1
- package/dist/extensions/pi-subagents/src/integrations/pi-web-session-liveness.ts +7 -1
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +1 -44
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +6 -0
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +13 -3
- package/dist/extensions/pi-subagents/src/runs/background/result-delivery-ownership.ts +269 -7
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +11 -10
- package/dist/extensions/pi-subagents/src/runs/background/retained-nested-route-tracker.ts +15 -3
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +64 -0
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -6
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -1
- package/dist/extensions/pi-subagents/src/shared/owner-record.ts +107 -0
- package/dist/extensions/pi-subagents/src/shared/process-identity.ts +87 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +9 -0
- package/dist/extensions/pi-subagents/test/integration/async-attention-clearing.test.ts +191 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/integration/stop-all-background.test.ts +142 -0
- package/dist/extensions/pi-subagents/test/support/fake-child-session.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/observability.test.ts +198 -0
- package/dist/extensions/pi-subagents/test/unit/process-identity.test.ts +75 -0
- package/dist/extensions/pi-subagents/test/unit/release-readiness.test.ts +715 -0
- package/dist/extensions/pi-subagents/test/unit/result-takeover.test.ts +332 -0
- package/dist/extensions/tokenin-onboarding.ts +4 -2
- package/dist/index.d.ts +6 -1
- package/dist/index.js +7 -0
- package/dist/main.d.ts +6 -0
- package/dist/main.js +1 -1
- package/dist/modes/index.d.ts +1 -1
- package/dist/modes/rpc/intercom-bridge.d.ts +14 -0
- package/dist/modes/rpc/intercom-bridge.js +75 -0
- package/dist/modes/rpc/intercom-bridge.test.d.ts +1 -0
- package/dist/modes/rpc/intercom-bridge.test.js +65 -0
- package/dist/modes/rpc/release-bridge.d.ts +35 -0
- package/dist/modes/rpc/release-bridge.js +159 -0
- package/dist/modes/rpc/release-bridge.test.d.ts +1 -0
- package/dist/modes/rpc/release-bridge.test.js +347 -0
- package/dist/modes/rpc/rpc-client.d.ts +34 -10
- package/dist/modes/rpc/rpc-client.js +45 -2
- package/dist/modes/rpc/rpc-intercom-transport.test.d.ts +1 -0
- package/dist/modes/rpc/rpc-intercom-transport.test.js +85 -0
- package/dist/modes/rpc/rpc-mode.d.ts +1 -13
- package/dist/modes/rpc/rpc-mode.js +239 -8
- package/dist/modes/rpc/rpc-release-transport.test.d.ts +1 -0
- package/dist/modes/rpc/rpc-release-transport.test.js +329 -0
- package/dist/modes/rpc/rpc-subagent-transport.test.d.ts +1 -0
- package/dist/modes/rpc/rpc-subagent-transport.test.js +60 -0
- package/dist/modes/rpc/rpc-types.d.ts +12 -2
- package/dist/modes/rpc/rpc-types.js +0 -6
- package/dist/modes/rpc/subagent-bridge.d.ts +23 -0
- package/dist/modes/rpc/subagent-bridge.js +93 -0
- package/dist/modes/rpc/test-fixtures/intercom-host.d.ts +1 -0
- package/dist/modes/rpc/test-fixtures/intercom-host.js +13 -0
- package/docs/rpc-subagent.schema.json +491 -0
- package/docs/rpc.md +210 -1
- package/package.json +2 -1
|
@@ -173,6 +173,49 @@ export class RpcClient {
|
|
|
173
173
|
/**
|
|
174
174
|
* Get current session state.
|
|
175
175
|
*/
|
|
176
|
+
/** Read-only native subagent snapshot; never invokes a model. */
|
|
177
|
+
async subagentStatus() {
|
|
178
|
+
return this.getData(await this.send({ type: "subagent_status", version: 1 }));
|
|
179
|
+
}
|
|
180
|
+
/** Authoritative "is it safe to stop this process now" answer; never invokes a model. */
|
|
181
|
+
async releaseReadiness() {
|
|
182
|
+
return this.getData(await this.send({ type: "release_readiness", version: 1 }));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Atomic "release if safe". `released: true` means the process has committed to a gentle exit (code 0, no abort) and rejects
|
|
186
|
+
* every further command except `release`/`release_readiness`; `released: false` carries the blockers and the process keeps running.
|
|
187
|
+
* "Nobody answered" (no extension contributors) is never released.
|
|
188
|
+
*/
|
|
189
|
+
async release() {
|
|
190
|
+
return this.getData(await this.send({ type: "release", version: 1 }));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* DESTRUCTIVE kill switch for an explicit user "Quit anyway": aborts the turn (retry, compaction, bash, queued messages), stops every
|
|
194
|
+
* async/detached subagent run of this session (detached runners terminate on their own), disarms this session's session-only
|
|
195
|
+
* schedules (paused, project schedules untouched) and cancels pending outbound intercom asks. Per-item failures are `ok:false`
|
|
196
|
+
* in `stopped`; `remaining` is the readiness computed after the stop. Idempotent; refused while the session is releasing.
|
|
197
|
+
*/
|
|
198
|
+
async stopAllBackground() {
|
|
199
|
+
return this.getData(await this.send({ type: "stop_all_background", version: 1 }));
|
|
200
|
+
}
|
|
201
|
+
async intercomStatus() {
|
|
202
|
+
return this.getData(await this.send({ type: "intercom_status" }));
|
|
203
|
+
}
|
|
204
|
+
async intercomList() {
|
|
205
|
+
return this.getData(await this.send({ type: "intercom_list" }));
|
|
206
|
+
}
|
|
207
|
+
async intercomPending() {
|
|
208
|
+
return this.getData(await this.send({ type: "intercom_pending" }));
|
|
209
|
+
}
|
|
210
|
+
async intercomSend(to, options) {
|
|
211
|
+
return this.getData(await this.send({ type: "intercom_send", to, ...options }));
|
|
212
|
+
}
|
|
213
|
+
async intercomAsk(to, options) {
|
|
214
|
+
return this.getData(await this.send({ type: "intercom_ask", to, ...options }, (options.timeoutMs ?? 600_000) + 5000));
|
|
215
|
+
}
|
|
216
|
+
async intercomReply(options) {
|
|
217
|
+
return this.getData(await this.send({ type: "intercom_reply", ...options }));
|
|
218
|
+
}
|
|
176
219
|
async getState() {
|
|
177
220
|
const response = await this.send({ type: "get_state" });
|
|
178
221
|
return this.getData(response);
|
|
@@ -652,7 +695,7 @@ export class RpcClient {
|
|
|
652
695
|
}
|
|
653
696
|
this.pendingRequests.clear();
|
|
654
697
|
}
|
|
655
|
-
async send(command) {
|
|
698
|
+
async send(command, timeoutMs = 35_000) {
|
|
656
699
|
const childProcess = this.process;
|
|
657
700
|
const stdin = childProcess?.stdin;
|
|
658
701
|
if (!childProcess || !stdin) {
|
|
@@ -677,7 +720,7 @@ export class RpcClient {
|
|
|
677
720
|
const timeout = setTimeout(() => {
|
|
678
721
|
this.pendingRequests.delete(id);
|
|
679
722
|
reject(new Error(`Timeout waiting for response to ${command.type}. Stderr: ${this.stderr}`));
|
|
680
|
-
},
|
|
723
|
+
}, timeoutMs);
|
|
681
724
|
this.pendingRequests.set(id, {
|
|
682
725
|
resolve: (response) => {
|
|
683
726
|
clearTimeout(timeout);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { RpcClient } from "./rpc-client.js";
|
|
8
|
+
// Run after npm run build: exercises built SDK host JSONL transport, avoiding unrelated CLI onboarding.
|
|
9
|
+
describe("Intercom RPC JSONL transport", () => {
|
|
10
|
+
it("starts the bundled standalone broker without a host-package alias", async () => {
|
|
11
|
+
const agentDir = mkdtempSync(join(tmpdir(), "rpc-broker-"));
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const tsxCli = join(dirname(require.resolve("tsx")), "cli.mjs");
|
|
14
|
+
const broker = spawn(process.execPath, [tsxCli, resolve("dist/extensions/pi-intercom/broker/broker.ts")], {
|
|
15
|
+
cwd: agentDir, env: { ...process.env, SELESAI_CODING_AGENT_DIR: agentDir }, stdio: ["ignore", "pipe", "pipe"],
|
|
16
|
+
});
|
|
17
|
+
let stderr = "";
|
|
18
|
+
broker.stderr.on("data", chunk => { stderr += chunk.toString(); });
|
|
19
|
+
try {
|
|
20
|
+
await new Promise((resolveReady, reject) => {
|
|
21
|
+
const timer = setTimeout(() => reject(new Error(`Standalone broker startup timed out: ${stderr}`)), 5000);
|
|
22
|
+
broker.once("error", error => { clearTimeout(timer); reject(error); });
|
|
23
|
+
broker.once("exit", code => { clearTimeout(timer); reject(new Error(`Standalone broker exited ${code}: ${stderr}`)); });
|
|
24
|
+
broker.stdout.on("data", chunk => {
|
|
25
|
+
if (chunk.toString().includes("Intercom broker started")) {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
resolveReady();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
expect(stderr).not.toContain("ERR_MODULE_NOT_FOUND");
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
if (broker.pid !== undefined && broker.exitCode === null && broker.signalCode === null) {
|
|
35
|
+
const exited = new Promise(resolveExit => broker.once("exit", () => resolveExit()));
|
|
36
|
+
broker.kill("SIGTERM");
|
|
37
|
+
await exited;
|
|
38
|
+
}
|
|
39
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
it("returns missing-backend errors without a model turn", async () => {
|
|
43
|
+
const agentDir = mkdtempSync(join(tmpdir(), "rpc-missing-"));
|
|
44
|
+
const client = new RpcClient({ cliPath: resolve("dist/modes/rpc/test-fixtures/intercom-host.js"), cwd: agentDir,
|
|
45
|
+
env: { SELESAI_CODING_AGENT_DIR: agentDir } });
|
|
46
|
+
try {
|
|
47
|
+
await client.start();
|
|
48
|
+
await expect(client.intercomList()).rejects.toThrow("Intercom extension unavailable");
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await client.stop();
|
|
52
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
it("streams connection events while idle and rebinds on new session/reload", async () => {
|
|
56
|
+
const agentDir = mkdtempSync(join(tmpdir(), "rpc-live-"));
|
|
57
|
+
const client = new RpcClient({ cliPath: resolve("dist/modes/rpc/test-fixtures/intercom-host.js"), cwd: agentDir,
|
|
58
|
+
env: { SELESAI_CODING_AGENT_DIR: agentDir, RPC_INTERCOM_EXTENSION: resolve("dist/extensions/pi-intercom/index.ts"), PI_INTERCOM_STABLE_ID: "", SELESAI_SUBAGENT_ORCHESTRATOR_TARGET: "", SELESAI_SUBAGENT_RUN_ID: "" } });
|
|
59
|
+
const events = [];
|
|
60
|
+
const unsubscribe = client.onEvent(event => events.push(event));
|
|
61
|
+
try {
|
|
62
|
+
await client.start();
|
|
63
|
+
const first = await client.intercomList();
|
|
64
|
+
expect(first.sessions.some(peer => peer.id === first.sessionId)).toBe(true);
|
|
65
|
+
expect((await client.intercomStatus()).connected).toBe(true);
|
|
66
|
+
expect((await client.intercomPending()).asks).toEqual([]);
|
|
67
|
+
expect(events.some(event => event.type === "intercom_connection" && event.connected)).toBe(true);
|
|
68
|
+
expect(events.some(event => event.type === "agent_start")).toBe(false);
|
|
69
|
+
await client.newSession();
|
|
70
|
+
const next = await client.intercomList();
|
|
71
|
+
expect(next.sessionId).not.toBe(first.sessionId);
|
|
72
|
+
await client.reload();
|
|
73
|
+
const reloaded = await client.intercomList();
|
|
74
|
+
expect(reloaded.sessionId).toBe(next.sessionId);
|
|
75
|
+
expect(reloaded.sessions.filter(peer => peer.id === next.sessionId)).toHaveLength(1);
|
|
76
|
+
await expect(client.intercomSend(reloaded.sessionId, { message: "Self must fail" })).rejects.toThrow("current session");
|
|
77
|
+
expect(events.some(event => event.type === "agent_start")).toBe(false);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
unsubscribe();
|
|
81
|
+
await client.stop();
|
|
82
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -1,17 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RPC mode: Headless operation with JSON stdin/stdout protocol.
|
|
3
|
-
*
|
|
4
|
-
* Used for embedding the agent in other applications.
|
|
5
|
-
* Receives commands as JSON on stdin, outputs events and responses as JSON on stdout.
|
|
6
|
-
*
|
|
7
|
-
* Protocol:
|
|
8
|
-
* - Commands: JSON objects with `type` field, optional `id` for correlation
|
|
9
|
-
* - Responses: JSON objects with `type: "response"`, `command`, `success`, and optional `data`/`error`
|
|
10
|
-
* - Events: AgentSessionEvent objects streamed as they occur
|
|
11
|
-
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
|
-
*/
|
|
13
1
|
import type { AgentSessionRuntime } from "../../core/agent-session-runtime.ts";
|
|
14
|
-
export type { RpcCommand, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcResponse, RpcSessionInfo, RpcSessionState, RpcSlashCommand, PrototypePhase, QuicktypePhase, WorkflowState, } from "./rpc-types.ts";
|
|
2
|
+
export type { RpcCommand, RpcEvent, IntercomRpcCommand, IntercomRpcResponse, IntercomRpcEvent, IntercomRpcStatus, IntercomRpcDelivery, IntercomRpcMessageOptions, IntercomRpcDataMap, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcResponse, RpcSessionInfo, RpcSessionState, RpcSlashCommand, PrototypePhase, QuicktypePhase, WorkflowState, } from "./rpc-types.ts";
|
|
15
3
|
/**
|
|
16
4
|
* Run in RPC mode.
|
|
17
5
|
* Listens for JSON commands on stdin, outputs events and responses on stdout.
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { RpcSubagentBridge } from "./subagent-bridge.js";
|
|
2
|
+
import { RpcReleaseBridge, validateReleaseCommand } from "./release-bridge.js";
|
|
3
|
+
import { RELEASING_BLOCKER_KIND } from "../../core/release-rpc.js";
|
|
1
4
|
/**
|
|
2
5
|
* RPC mode: Headless operation with JSON stdin/stdout protocol.
|
|
3
6
|
*
|
|
@@ -10,6 +13,7 @@
|
|
|
10
13
|
* - Events: AgentSessionEvent objects streamed as they occur
|
|
11
14
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
15
|
*/
|
|
16
|
+
import { RpcIntercomBridge } from "./intercom-bridge.js";
|
|
13
17
|
import * as crypto from "node:crypto";
|
|
14
18
|
import { spawnSync } from "node:child_process";
|
|
15
19
|
import { chmodSync, copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
@@ -42,6 +46,9 @@ export async function runRpcMode(runtimeHost) {
|
|
|
42
46
|
let session = runtimeHost.session;
|
|
43
47
|
let unsubscribe;
|
|
44
48
|
let unsubscribeBackpressure;
|
|
49
|
+
let intercomBridge;
|
|
50
|
+
let subagentBridge;
|
|
51
|
+
let releaseBridge;
|
|
45
52
|
const output = (obj) => {
|
|
46
53
|
writeRawStdout(serializeJsonLine(obj));
|
|
47
54
|
};
|
|
@@ -95,6 +102,27 @@ export async function runRpcMode(runtimeHost) {
|
|
|
95
102
|
const pendingExtensionRequests = new Map();
|
|
96
103
|
// Prompts buffered while compacting (queueWhileCompacting) — flushed after compaction_end.
|
|
97
104
|
const compactionPromptQueue = [];
|
|
105
|
+
// In-flight session transitions (switch/new/fork/reload/extension rebind) and prompts still inside preflight or
|
|
106
|
+
// run, tracked so release_readiness never answers "safe" in a window where session state is being swapped.
|
|
107
|
+
let sessionTransitions = 0;
|
|
108
|
+
let promptsInFlight = 0;
|
|
109
|
+
const withTransition = async (fn) => {
|
|
110
|
+
sessionTransitions++;
|
|
111
|
+
try {
|
|
112
|
+
return await fn();
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
sessionTransitions--;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const TRANSITION_COMMANDS = new Set([
|
|
119
|
+
"new_session", "handoff_new", "switch_session", "fork", "clone", "import_jsonl", "reload", "navigate_tree",
|
|
120
|
+
]);
|
|
121
|
+
// Atomic `release` command: once committed (synchronously, in the same step as the readiness check) every other command is
|
|
122
|
+
// refused and the process exits gently right after the response is flushed. `releasedReadiness` is what the commit was based on.
|
|
123
|
+
let releasing = false;
|
|
124
|
+
let releaseShutdownPending = false;
|
|
125
|
+
let releasedReadiness;
|
|
98
126
|
// Shutdown request flag
|
|
99
127
|
let shutdownRequested = false;
|
|
100
128
|
let shuttingDown = false;
|
|
@@ -302,16 +330,43 @@ export async function runRpcMode(runtimeHost) {
|
|
|
302
330
|
runtimeHost.setRebindSession(async () => {
|
|
303
331
|
await rebindSession();
|
|
304
332
|
});
|
|
305
|
-
const
|
|
333
|
+
const bindObservationBridges = () => {
|
|
334
|
+
intercomBridge?.dispose();
|
|
335
|
+
subagentBridge?.dispose();
|
|
336
|
+
releaseBridge?.dispose();
|
|
306
337
|
session = runtimeHost.session;
|
|
338
|
+
const epoch = crypto.randomUUID();
|
|
339
|
+
const bus = session.resourceLoader.getEventBus?.();
|
|
340
|
+
intercomBridge = new RpcIntercomBridge(bus, output, session.sessionId, epoch);
|
|
341
|
+
subagentBridge = new RpcSubagentBridge(bus, output, { version: 1, sessionId: session.sessionId, epoch });
|
|
342
|
+
releaseBridge = new RpcReleaseBridge(bus, { version: 1, sessionId: session.sessionId, epoch });
|
|
343
|
+
};
|
|
344
|
+
const reloadSession = () => withTransition(reloadSessionUnguarded);
|
|
345
|
+
const reloadSessionUnguarded = async () => {
|
|
346
|
+
intercomBridge?.dispose();
|
|
347
|
+
subagentBridge?.dispose();
|
|
348
|
+
releaseBridge?.dispose();
|
|
349
|
+
let rebound = false;
|
|
350
|
+
try {
|
|
351
|
+
await session.reload({ beforeSessionStart: () => { bindObservationBridges(); rebound = true; } });
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
if (!rebound)
|
|
355
|
+
bindObservationBridges();
|
|
356
|
+
subagentBridge?.initialize();
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
const rebindSession = () => withTransition(rebindSessionUnguarded);
|
|
360
|
+
const rebindSessionUnguarded = async () => {
|
|
361
|
+
bindObservationBridges();
|
|
307
362
|
await session.bindExtensions({
|
|
308
363
|
uiContext: createExtensionUIContext(),
|
|
309
364
|
mode: "rpc",
|
|
310
365
|
commandContextActions: {
|
|
311
366
|
waitForIdle: () => session.waitForIdle(),
|
|
312
|
-
newSession: async (options) => runtimeHost.newSession(options),
|
|
367
|
+
newSession: async (options) => withTransition(() => runtimeHost.newSession(options)),
|
|
313
368
|
fork: async (entryId, forkOptions) => {
|
|
314
|
-
const result = await runtimeHost.fork(entryId, forkOptions);
|
|
369
|
+
const result = await withTransition(() => runtimeHost.fork(entryId, forkOptions));
|
|
315
370
|
return { cancelled: result.cancelled };
|
|
316
371
|
},
|
|
317
372
|
navigateTree: async (targetId, options) => {
|
|
@@ -324,10 +379,10 @@ export async function runRpcMode(runtimeHost) {
|
|
|
324
379
|
return { cancelled: result.cancelled };
|
|
325
380
|
},
|
|
326
381
|
switchSession: async (sessionPath, options) => {
|
|
327
|
-
return runtimeHost.switchSession(sessionPath, options);
|
|
382
|
+
return withTransition(() => runtimeHost.switchSession(sessionPath, options));
|
|
328
383
|
},
|
|
329
384
|
reload: async () => {
|
|
330
|
-
await
|
|
385
|
+
await reloadSession();
|
|
331
386
|
},
|
|
332
387
|
},
|
|
333
388
|
shutdownHandler: () => {
|
|
@@ -337,6 +392,7 @@ export async function runRpcMode(runtimeHost) {
|
|
|
337
392
|
output({ type: "extension_error", extensionPath: err.extensionPath, event: err.event, error: err.error });
|
|
338
393
|
},
|
|
339
394
|
});
|
|
395
|
+
subagentBridge?.initialize();
|
|
340
396
|
unsubscribe?.();
|
|
341
397
|
unsubscribeBackpressure?.();
|
|
342
398
|
unsubscribe = session.subscribe((event) => {
|
|
@@ -389,10 +445,168 @@ export async function runRpcMode(runtimeHost) {
|
|
|
389
445
|
};
|
|
390
446
|
await rebindSession();
|
|
391
447
|
registerSignalHandlers();
|
|
448
|
+
/**
|
|
449
|
+
* Release blockers owned by this process (not by an extension). Fail closed: if a probe throws, report it
|
|
450
|
+
* instead of answering "safe". Extension blockers are collected by RpcReleaseBridge.
|
|
451
|
+
*/
|
|
452
|
+
const coreReleaseBlockers = () => {
|
|
453
|
+
const blockers = [];
|
|
454
|
+
const block = (kind, detail) => blockers.push({ source: "core", kind, detail });
|
|
455
|
+
if (releasing)
|
|
456
|
+
block(RELEASING_BLOCKER_KIND, "A release was committed; this process is shutting down");
|
|
457
|
+
if (sessionTransitions > 0)
|
|
458
|
+
block("session_transition", "Session switch, reload or extension rebind in progress");
|
|
459
|
+
if (promptsInFlight > 0)
|
|
460
|
+
block("prompt_in_flight", `${promptsInFlight} prompt command(s) still being processed`);
|
|
461
|
+
const probe = (kind, check) => {
|
|
462
|
+
try {
|
|
463
|
+
check();
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
block("contributor_error", `${kind} probe failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
probe("streaming", () => { if (session.isStreaming)
|
|
470
|
+
block("streaming", "Agent run in progress"); });
|
|
471
|
+
probe("compacting", () => { if (session.isCompacting)
|
|
472
|
+
block("compacting", "Compaction or branch summarization in progress"); });
|
|
473
|
+
probe("retrying", () => { if (session.isRetrying)
|
|
474
|
+
block("retrying", `Auto-retry in progress (attempt ${session.retryAttempt})`); });
|
|
475
|
+
probe("queued_messages", () => {
|
|
476
|
+
const steering = session.getSteeringMessages().length;
|
|
477
|
+
const followUp = session.getFollowUpMessages().length;
|
|
478
|
+
if (session.agent.hasQueuedMessages() || session.pendingMessageCount > 0)
|
|
479
|
+
block("queued_messages", `Queued messages: ${steering} steering, ${followUp} follow-up`);
|
|
480
|
+
});
|
|
481
|
+
probe("pending_tool_calls", () => {
|
|
482
|
+
const pending = session.agent.state.pendingToolCalls.size;
|
|
483
|
+
if (pending > 0)
|
|
484
|
+
block("pending_tool_calls", `${pending} tool call(s) in flight`);
|
|
485
|
+
});
|
|
486
|
+
probe("pending_bash", () => {
|
|
487
|
+
if (session.isBashRunning)
|
|
488
|
+
block("pending_bash", "Bash command running");
|
|
489
|
+
if (session.hasPendingBashMessages)
|
|
490
|
+
block("pending_bash", "Bash output awaiting flush into the session");
|
|
491
|
+
});
|
|
492
|
+
if (pendingExtensionRequests.size > 0)
|
|
493
|
+
block("extension_dialog", `${pendingExtensionRequests.size} extension dialog(s) awaiting a response`);
|
|
494
|
+
if (compactionPromptQueue.length > 0)
|
|
495
|
+
block("compaction_prompt_queue", `${compactionPromptQueue.length} prompt(s) buffered until compaction ends`);
|
|
496
|
+
return blockers;
|
|
497
|
+
};
|
|
498
|
+
/**
|
|
499
|
+
* Core half of `stop_all_background`: everything this process runs for the session itself. Mirrors the `abort` command (agent
|
|
500
|
+
* turn, retry, compaction, branch summary) and additionally kills a running `!bash`, drops queued steer/follow-up messages and
|
|
501
|
+
* buffered compaction prompts so nothing restarts the turn after the abort. Only what was actually active is reported, so an
|
|
502
|
+
* idle session yields no items. Foreground subagent runs belong to the turn: they are killed through the turn's abort signal.
|
|
503
|
+
* Extension dialogs are not touched (they are the host's UI, not background work).
|
|
504
|
+
*/
|
|
505
|
+
const stopCoreWork = async () => {
|
|
506
|
+
const items = [];
|
|
507
|
+
const active = (kind, probe) => {
|
|
508
|
+
try {
|
|
509
|
+
return probe();
|
|
510
|
+
}
|
|
511
|
+
catch (e) {
|
|
512
|
+
items.push({ source: "core", kind, detail: `${kind} probe failed: ${e instanceof Error ? e.message : String(e)}`, ok: false, error: "probe_failed" });
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
const turn = active("turn", () => session.isStreaming);
|
|
517
|
+
const retry = active("retry", () => session.isRetrying);
|
|
518
|
+
const compaction = active("compaction", () => session.isCompacting);
|
|
519
|
+
const bash = active("bash", () => session.isBashRunning);
|
|
520
|
+
let queued = { steering: 0, followUp: 0 };
|
|
521
|
+
try {
|
|
522
|
+
queued = { steering: session.getSteeringMessages().length, followUp: session.getFollowUpMessages().length };
|
|
523
|
+
}
|
|
524
|
+
catch { /* reported by the abort below */ }
|
|
525
|
+
const bufferedPrompts = compactionPromptQueue.length;
|
|
526
|
+
const done = (kind, detail, failure) => {
|
|
527
|
+
const message = failure === undefined ? undefined : (failure instanceof Error ? failure.message : String(failure)).slice(0, 256);
|
|
528
|
+
items.push({ source: "core", kind, detail: message === undefined ? detail : `${detail} failed: ${message}`, ok: message === undefined, ...(message === undefined ? {} : { error: message }) });
|
|
529
|
+
};
|
|
530
|
+
// Order matters: drop queued work first so the abort's idle transition cannot start the next queued turn.
|
|
531
|
+
let queueFailure;
|
|
532
|
+
try {
|
|
533
|
+
compactionPromptQueue.length = 0;
|
|
534
|
+
session.clearQueue();
|
|
535
|
+
}
|
|
536
|
+
catch (e) {
|
|
537
|
+
queueFailure = e;
|
|
538
|
+
}
|
|
539
|
+
if (queued.steering + queued.followUp > 0)
|
|
540
|
+
done("queued_messages", `Dropped ${queued.steering} steering and ${queued.followUp} follow-up message(s)`, queueFailure);
|
|
541
|
+
if (bufferedPrompts > 0)
|
|
542
|
+
done("compaction_prompt_queue", `Dropped ${bufferedPrompts} prompt(s) buffered until compaction ends`, queueFailure);
|
|
543
|
+
let bashFailure;
|
|
544
|
+
try {
|
|
545
|
+
session.abortBash();
|
|
546
|
+
}
|
|
547
|
+
catch (e) {
|
|
548
|
+
bashFailure = e;
|
|
549
|
+
}
|
|
550
|
+
if (bash)
|
|
551
|
+
done("bash", "Aborted running bash command", bashFailure);
|
|
552
|
+
let abortFailure;
|
|
553
|
+
try {
|
|
554
|
+
session.abortBranchSummary();
|
|
555
|
+
session.abortCompaction();
|
|
556
|
+
await session.abort();
|
|
557
|
+
}
|
|
558
|
+
catch (e) {
|
|
559
|
+
abortFailure = e;
|
|
560
|
+
}
|
|
561
|
+
if (turn)
|
|
562
|
+
done("turn", "Aborted in-flight agent turn", abortFailure);
|
|
563
|
+
if (retry)
|
|
564
|
+
done("retry", "Cancelled auto-retry", abortFailure);
|
|
565
|
+
if (compaction)
|
|
566
|
+
done("compaction", "Aborted compaction or branch summarization", abortFailure);
|
|
567
|
+
if (abortFailure !== undefined && !turn && !retry && !compaction)
|
|
568
|
+
done("turn", "Abort", abortFailure);
|
|
569
|
+
return items;
|
|
570
|
+
};
|
|
392
571
|
// Handle a single command
|
|
393
572
|
const handleCommand = async (command) => {
|
|
394
573
|
const id = command.id;
|
|
574
|
+
// Committed release: nothing new may start. Only the release probes still answer.
|
|
575
|
+
if (releasing && command.type !== "release" && command.type !== "release_readiness") {
|
|
576
|
+
return error(id, command.type, "Session is releasing");
|
|
577
|
+
}
|
|
395
578
|
switch (command.type) {
|
|
579
|
+
case "subagent_status":
|
|
580
|
+
return subagentBridge.request(command);
|
|
581
|
+
case "release_readiness":
|
|
582
|
+
return releaseBridge.request(command, coreReleaseBlockers());
|
|
583
|
+
case "stop_all_background":
|
|
584
|
+
// Destructive kill switch for "Quit anyway". Refused above while `releasing`. The core abort and every extension's stop
|
|
585
|
+
// start synchronously inside the bridge and are awaited together under one ~5s budget.
|
|
586
|
+
return releaseBridge.stopAll(command, stopCoreWork, coreReleaseBlockers);
|
|
587
|
+
case "release": {
|
|
588
|
+
// ONE synchronous section: readiness check (core + extension broadcast) and commitment, no `await` in between.
|
|
589
|
+
// The shutdown itself runs from handleInputLine after the response is written and flushed.
|
|
590
|
+
if (releasing) {
|
|
591
|
+
// Idempotent: a repeated (valid) release reports the committed decision.
|
|
592
|
+
const invalid = validateReleaseCommand(command);
|
|
593
|
+
if (invalid)
|
|
594
|
+
return error(id, command.type, invalid);
|
|
595
|
+
return success(id, "release", { released: true, readiness: releasedReadiness });
|
|
596
|
+
}
|
|
597
|
+
return releaseBridge.release(command, coreReleaseBlockers(), (readiness) => {
|
|
598
|
+
releasing = true;
|
|
599
|
+
releasedReadiness = readiness;
|
|
600
|
+
releaseShutdownPending = true;
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
case "intercom_status":
|
|
604
|
+
case "intercom_list":
|
|
605
|
+
case "intercom_send":
|
|
606
|
+
case "intercom_ask":
|
|
607
|
+
case "intercom_reply":
|
|
608
|
+
case "intercom_pending":
|
|
609
|
+
return intercomBridge.request(command);
|
|
396
610
|
// =================================================================
|
|
397
611
|
// Prompting
|
|
398
612
|
// =================================================================
|
|
@@ -415,6 +629,7 @@ export async function runRpcMode(runtimeHost) {
|
|
|
415
629
|
compactionPromptQueue.push(queued);
|
|
416
630
|
return undefined;
|
|
417
631
|
}
|
|
632
|
+
promptsInFlight++;
|
|
418
633
|
void session
|
|
419
634
|
.prompt(command.message, {
|
|
420
635
|
images: command.images,
|
|
@@ -431,6 +646,9 @@ export async function runRpcMode(runtimeHost) {
|
|
|
431
646
|
if (!preflightSucceeded) {
|
|
432
647
|
output(error(id, "prompt", e.message));
|
|
433
648
|
}
|
|
649
|
+
})
|
|
650
|
+
.finally(() => {
|
|
651
|
+
promptsInFlight--;
|
|
434
652
|
});
|
|
435
653
|
return undefined;
|
|
436
654
|
}
|
|
@@ -492,6 +710,7 @@ export async function runRpcMode(runtimeHost) {
|
|
|
492
710
|
thinkingLevel: session.thinkingLevel,
|
|
493
711
|
isStreaming: session.isStreaming,
|
|
494
712
|
isCompacting: session.isCompacting,
|
|
713
|
+
isRetrying: session.isRetrying,
|
|
495
714
|
steeringMode: session.steeringMode,
|
|
496
715
|
followUpMode: session.followUpMode,
|
|
497
716
|
sessionFile: session.sessionFile,
|
|
@@ -1008,7 +1227,7 @@ export async function runRpcMode(runtimeHost) {
|
|
|
1008
1227
|
catch {
|
|
1009
1228
|
// best-effort
|
|
1010
1229
|
}
|
|
1011
|
-
await
|
|
1230
|
+
await reloadSession();
|
|
1012
1231
|
return success(id, "factory_reset_settings");
|
|
1013
1232
|
}
|
|
1014
1233
|
catch (resetError) {
|
|
@@ -1276,7 +1495,7 @@ export async function runRpcMode(runtimeHost) {
|
|
|
1276
1495
|
return error(id, "reload", "Wait for compaction to finish before reloading.");
|
|
1277
1496
|
}
|
|
1278
1497
|
try {
|
|
1279
|
-
await
|
|
1498
|
+
await reloadSession();
|
|
1280
1499
|
return success(id, "reload");
|
|
1281
1500
|
}
|
|
1282
1501
|
catch (reloadError) {
|
|
@@ -1396,6 +1615,9 @@ export async function runRpcMode(runtimeHost) {
|
|
|
1396
1615
|
}
|
|
1397
1616
|
unsubscribe?.();
|
|
1398
1617
|
unsubscribeBackpressure?.();
|
|
1618
|
+
intercomBridge?.dispose();
|
|
1619
|
+
subagentBridge?.dispose();
|
|
1620
|
+
releaseBridge?.dispose();
|
|
1399
1621
|
await runtimeHost.dispose();
|
|
1400
1622
|
detachInput();
|
|
1401
1623
|
process.stdin.pause();
|
|
@@ -1434,11 +1656,17 @@ export async function runRpcMode(runtimeHost) {
|
|
|
1434
1656
|
}
|
|
1435
1657
|
const command = parsed;
|
|
1436
1658
|
try {
|
|
1437
|
-
const response = await handleCommand(command);
|
|
1659
|
+
const response = await (TRANSITION_COMMANDS.has(command.type) ? withTransition(() => handleCommand(command)) : handleCommand(command));
|
|
1438
1660
|
if (response) {
|
|
1439
1661
|
output(response);
|
|
1440
1662
|
await waitForRawStdoutBackpressure();
|
|
1441
1663
|
}
|
|
1664
|
+
if (releaseShutdownPending) {
|
|
1665
|
+
// The release response is out: flush it fully, then take the normal gentle shutdown path (extensions disposed, exit 0).
|
|
1666
|
+
releaseShutdownPending = false;
|
|
1667
|
+
await flushRawStdout();
|
|
1668
|
+
await shutdown();
|
|
1669
|
+
}
|
|
1442
1670
|
await checkShutdownRequested();
|
|
1443
1671
|
}
|
|
1444
1672
|
catch (commandError) {
|
|
@@ -1447,6 +1675,9 @@ export async function runRpcMode(runtimeHost) {
|
|
|
1447
1675
|
}
|
|
1448
1676
|
};
|
|
1449
1677
|
const onInputEnd = () => {
|
|
1678
|
+
// A committed release already owns the shutdown (response flush, then dispose); a second shutdown() would cut it short.
|
|
1679
|
+
if (releasing)
|
|
1680
|
+
return;
|
|
1450
1681
|
void shutdown();
|
|
1451
1682
|
};
|
|
1452
1683
|
process.stdin.on("end", onInputEnd);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|