@rivet-dev/agentos-core 0.2.22 → 0.4.0
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/README.md +9 -9
- package/dist/agent-os.d.ts +71 -198
- package/dist/agent-os.js +480 -1206
- package/dist/agentos-package.d.ts +15 -4
- package/dist/agentos-package.js +1 -22
- package/dist/cron/cron-manager.js +0 -27
- package/dist/cron/types.d.ts +0 -12
- package/dist/default-software/coreutils.aospkg +4 -0
- package/dist/default-software/default-software.json +10 -0
- package/dist/default-software/diffutils.aospkg +0 -0
- package/dist/default-software/findutils.aospkg +0 -0
- package/dist/default-software/gawk.aospkg +0 -0
- package/dist/default-software/grep.aospkg +0 -0
- package/dist/default-software/gzip.aospkg +0 -0
- package/dist/default-software/sed.aospkg +0 -0
- package/dist/default-software/tar.aospkg +0 -0
- package/dist/default-software.d.ts +6 -5
- package/dist/default-software.js +17 -6
- package/dist/generated/VmLimitsConfig.d.ts +0 -2
- package/dist/host-dir-mount.d.ts +1 -1
- package/dist/host-dir-mount.js +1 -1
- package/dist/language-execution.d.ts +6 -2
- package/dist/options-schema.d.ts +10 -48
- package/dist/options-schema.js +10 -47
- package/dist/output-replay.d.ts +18 -0
- package/dist/output-replay.js +75 -0
- package/dist/packages.d.ts +0 -9
- package/dist/packages.js +0 -39
- package/dist/process-forest.d.ts +7 -0
- package/dist/process-forest.js +24 -0
- package/dist/runtime-compat.d.ts +13 -0
- package/dist/runtime-compat.js +3 -9
- package/dist/sidecar/binary.d.ts +1 -1
- package/dist/sidecar/binary.js +4 -4
- package/dist/sidecar/native-process-client.d.ts +1 -1
- package/dist/sidecar/rpc-client.d.ts +12 -18
- package/dist/sidecar/rpc-client.js +385 -188
- package/dist/types.d.ts +3 -11
- package/package.json +9 -45
- package/dist/generated/AcpLimitsConfig.d.ts +0 -20
- package/dist/generated/AcpLimitsConfig.js +0 -2
- package/dist/json-rpc.d.ts +0 -50
- package/dist/json-rpc.js +0 -22
- package/dist/session-api.d.ts +0 -180
- package/dist/session-api.js +0 -1
- package/dist/sidecar/agentos-protocol.d.ts +0 -719
- package/dist/sidecar/agentos-protocol.js +0 -1331
package/dist/agent-os.js
CHANGED
|
@@ -8,140 +8,12 @@ import { SidecarRejectedError } from "@rivet-dev/agentos-runtime-core/sidecar-er
|
|
|
8
8
|
import { hostFunctionDescription, resolveHostFunctions, } from "@rivet-dev/agentos-runtime-core/host-functions";
|
|
9
9
|
import { zodToJsonSchema } from "@rivet-dev/agentos-runtime-core/host-functions-zod";
|
|
10
10
|
import { parseAgentOsOptions } from "./options-schema.js";
|
|
11
|
+
import { buildProcessForest } from "./process-forest.js";
|
|
11
12
|
import { getSandboxDisposeHooks, resolveSandboxOptions, } from "./sandbox.js";
|
|
12
13
|
import { resolvePublishedSidecarBinary } from "./sidecar/binary.js";
|
|
14
|
+
import { replayPage, replayPageLimits, replayWirePageLimits, } from "./output-replay.js";
|
|
13
15
|
import { findCargoBinary, resolveCargoBinary } from "./sidecar/cargo.js";
|
|
14
|
-
const
|
|
15
|
-
const ACP_EXTENSION_NAMESPACE = "dev.rivet.agent-os.acp";
|
|
16
|
-
const SHELL_DISPOSE_TIMEOUT_MS = 5_000;
|
|
17
|
-
const PROCESS_OUTPUT_EVENT_LIMIT = 1_024;
|
|
18
|
-
function safeWireU64(value) {
|
|
19
|
-
const result = Number(value);
|
|
20
|
-
if (!Number.isSafeInteger(result) || result < 0) {
|
|
21
|
-
throw new RangeError(`wire integer ${value} exceeds JavaScript's safe range`);
|
|
22
|
-
}
|
|
23
|
-
return result;
|
|
24
|
-
}
|
|
25
|
-
function decodeDurableSessionInfo(value) {
|
|
26
|
-
return {
|
|
27
|
-
sessionId: value.sessionId,
|
|
28
|
-
agent: value.agent,
|
|
29
|
-
cwd: value.cwd,
|
|
30
|
-
additionalDirectories: JSON.parse(value.additionalDirectories),
|
|
31
|
-
state: JSON.parse(value.state),
|
|
32
|
-
latestSequence: safeWireU64(value.latestSequence),
|
|
33
|
-
title: value.title ?? undefined,
|
|
34
|
-
metadata: value.metadata === null ? undefined : JSON.parse(value.metadata),
|
|
35
|
-
createdAt: value.createdAt,
|
|
36
|
-
updatedAt: value.updatedAt,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
const PERMISSION_TERMINAL_REASONS = new Set([
|
|
40
|
-
"already_resolved",
|
|
41
|
-
"prompt_cancelled",
|
|
42
|
-
"adapter_exited",
|
|
43
|
-
"session_deleted",
|
|
44
|
-
"vm_shutdown",
|
|
45
|
-
"request_not_found",
|
|
46
|
-
]);
|
|
47
|
-
function permissionTerminalReason(value) {
|
|
48
|
-
if (value !== null &&
|
|
49
|
-
PERMISSION_TERMINAL_REASONS.has(value)) {
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
throw new Error(`invalid permission terminal reason: ${value ?? "missing"}`);
|
|
53
|
-
}
|
|
54
|
-
function decodeDurableSessionEvent(value) {
|
|
55
|
-
const envelope = {
|
|
56
|
-
durability: "durable",
|
|
57
|
-
sessionId: value.sessionId,
|
|
58
|
-
sequence: safeWireU64(value.sequence),
|
|
59
|
-
timestamp: value.timestamp,
|
|
60
|
-
};
|
|
61
|
-
switch (value.event.tag) {
|
|
62
|
-
case "AcpDurableSessionUpdate": {
|
|
63
|
-
const update = JSON.parse(value.event.val.update);
|
|
64
|
-
const { sessionUpdate: type, ...payload } = update;
|
|
65
|
-
return {
|
|
66
|
-
...envelope,
|
|
67
|
-
type,
|
|
68
|
-
...payload,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
case "AcpDurablePermissionRequest": {
|
|
72
|
-
const request = JSON.parse(value.event.val.request);
|
|
73
|
-
const { sessionId: _sessionId, ...payload } = request;
|
|
74
|
-
return {
|
|
75
|
-
...envelope,
|
|
76
|
-
type: "permission_request",
|
|
77
|
-
requestId: value.event.val.requestId,
|
|
78
|
-
...payload,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
case "AcpDurablePermissionResponse": {
|
|
82
|
-
const status = value.event.val.status;
|
|
83
|
-
if (status !== "accepted" && status !== "not_pending") {
|
|
84
|
-
throw new Error(`invalid permission response event status: ${status}`);
|
|
85
|
-
}
|
|
86
|
-
const response = JSON.parse(value.event.val.response);
|
|
87
|
-
return {
|
|
88
|
-
...envelope,
|
|
89
|
-
type: "permission_response",
|
|
90
|
-
requestId: value.event.val.requestId,
|
|
91
|
-
...response,
|
|
92
|
-
status,
|
|
93
|
-
...(value.event.val.reason === null
|
|
94
|
-
? {}
|
|
95
|
-
: { reason: permissionTerminalReason(value.event.val.reason) }),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
function normalizeSessionCapabilities(value) {
|
|
101
|
-
const capabilities = toRecord(value);
|
|
102
|
-
const prompt = toRecord(capabilities.promptCapabilities);
|
|
103
|
-
const mcp = toRecord(capabilities.mcpCapabilities);
|
|
104
|
-
const session = toRecord(capabilities.sessionCapabilities);
|
|
105
|
-
const extensions = Object.fromEntries(Object.entries(capabilities).filter(([key]) => ![
|
|
106
|
-
"loadSession",
|
|
107
|
-
"promptCapabilities",
|
|
108
|
-
"mcpCapabilities",
|
|
109
|
-
"sessionCapabilities",
|
|
110
|
-
].includes(key)));
|
|
111
|
-
return {
|
|
112
|
-
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
113
|
-
loadSession: capabilities.loadSession === true,
|
|
114
|
-
...(Object.keys(prompt).length > 0
|
|
115
|
-
? {
|
|
116
|
-
prompt: {
|
|
117
|
-
audio: prompt.audio === true || undefined,
|
|
118
|
-
embeddedContext: prompt.embeddedContext === true || undefined,
|
|
119
|
-
image: prompt.image === true || undefined,
|
|
120
|
-
},
|
|
121
|
-
}
|
|
122
|
-
: {}),
|
|
123
|
-
...(Object.keys(mcp).length > 0
|
|
124
|
-
? {
|
|
125
|
-
mcp: {
|
|
126
|
-
http: mcp.http === true || undefined,
|
|
127
|
-
sse: mcp.sse === true || undefined,
|
|
128
|
-
},
|
|
129
|
-
}
|
|
130
|
-
: {}),
|
|
131
|
-
...(Object.keys(session).length > 0
|
|
132
|
-
? {
|
|
133
|
-
session: {
|
|
134
|
-
list: typeof session.list === "object" || undefined,
|
|
135
|
-
resume: typeof session.resume === "object" || undefined,
|
|
136
|
-
close: typeof session.close === "object" || undefined,
|
|
137
|
-
delete: typeof session.delete === "object" || undefined,
|
|
138
|
-
additionalDirectories: typeof session.additionalDirectories === "object" || undefined,
|
|
139
|
-
},
|
|
140
|
-
}
|
|
141
|
-
: {}),
|
|
142
|
-
...(Object.keys(extensions).length > 0 ? { extensions } : {}),
|
|
143
|
-
};
|
|
144
|
-
}
|
|
16
|
+
const VM_DISPOSE_DRAIN_TIMEOUT_MS = 5_000;
|
|
145
17
|
async function waitForTrackedExitPromises(promises, timeoutMs) {
|
|
146
18
|
if (promises.length === 0) {
|
|
147
19
|
return;
|
|
@@ -168,60 +40,9 @@ import { resolveDefaultSoftware } from "./default-software.js";
|
|
|
168
40
|
import { snapshotVirtualFilesystem, sortFilesystemEntries, } from "./filesystem-snapshot.js";
|
|
169
41
|
import { serializeMountConfigForSidecar, } from "./js-bridge.js";
|
|
170
42
|
import { createSnapshotExport, } from "./layers.js";
|
|
171
|
-
import {
|
|
43
|
+
import { allowAll } from "./runtime-compat.js";
|
|
172
44
|
import { serializePermissionsForSidecar } from "./sidecar/permissions.js";
|
|
173
45
|
import { createAgentOsSidecarClient, NativeSidecarKernelProxy, SidecarProcess, serializeRootFilesystemForSidecar, } from "./sidecar/rpc-client.js";
|
|
174
|
-
function defaultAgentStderrHandler(event) {
|
|
175
|
-
process.stderr.write(event.chunk);
|
|
176
|
-
}
|
|
177
|
-
function defaultAgentExitHandler(event) {
|
|
178
|
-
process.stderr.write(`[agentos] agent adapter exited unexpectedly: session=${event.sessionId} agent=${event.agentType} exitCode=${event.exitCode ?? "unknown"}; restore explicitly before retrying\n`);
|
|
179
|
-
}
|
|
180
|
-
class AcpDispatchError extends Error {
|
|
181
|
-
code;
|
|
182
|
-
data;
|
|
183
|
-
constructor(code, message, data) {
|
|
184
|
-
super(message);
|
|
185
|
-
this.name = "AcpDispatchError";
|
|
186
|
-
this.code = code;
|
|
187
|
-
this.data = data;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
function toJsonRpcNotification(value) {
|
|
191
|
-
if (!value ||
|
|
192
|
-
typeof value !== "object" ||
|
|
193
|
-
Array.isArray(value) ||
|
|
194
|
-
value.jsonrpc !== "2.0" ||
|
|
195
|
-
typeof value.method !== "string") {
|
|
196
|
-
throw new Error("Invalid JSON-RPC notification from sidecar");
|
|
197
|
-
}
|
|
198
|
-
return value;
|
|
199
|
-
}
|
|
200
|
-
function toJsonRpcResponse(value) {
|
|
201
|
-
if (!value ||
|
|
202
|
-
typeof value !== "object" ||
|
|
203
|
-
Array.isArray(value) ||
|
|
204
|
-
value.jsonrpc !== "2.0" ||
|
|
205
|
-
!(typeof value.id === "number" ||
|
|
206
|
-
typeof value.id === "string" ||
|
|
207
|
-
value.id === null)) {
|
|
208
|
-
throw new Error("Invalid JSON-RPC response from sidecar");
|
|
209
|
-
}
|
|
210
|
-
return value;
|
|
211
|
-
}
|
|
212
|
-
function toJsonRpcRequest(value) {
|
|
213
|
-
if (!value ||
|
|
214
|
-
typeof value !== "object" ||
|
|
215
|
-
Array.isArray(value) ||
|
|
216
|
-
value.jsonrpc !== "2.0" ||
|
|
217
|
-
!(typeof value.id === "number" ||
|
|
218
|
-
typeof value.id === "string" ||
|
|
219
|
-
value.id === null) ||
|
|
220
|
-
typeof value.method !== "string") {
|
|
221
|
-
throw new Error("Invalid JSON-RPC request from ACP callback");
|
|
222
|
-
}
|
|
223
|
-
return value;
|
|
224
|
-
}
|
|
225
46
|
function toRecord(value) {
|
|
226
47
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
227
48
|
? value
|
|
@@ -250,6 +71,8 @@ function normalizePackageRef(value) {
|
|
|
250
71
|
return undefined;
|
|
251
72
|
}
|
|
252
73
|
const CLOSED_SHELL_ID_RETENTION_LIMIT = 2048;
|
|
74
|
+
const TERMINAL_LIMIT = 1024;
|
|
75
|
+
const PROCESS_REGISTRY_LIMIT = 1024;
|
|
253
76
|
class BoundedSet {
|
|
254
77
|
limit;
|
|
255
78
|
#entries = new Map();
|
|
@@ -464,18 +287,8 @@ const KERNEL_POSIX_BOOTSTRAP_DIR_METADATA = {
|
|
|
464
287
|
"/var/run": { mode: "777", uid: 0, gid: 0 },
|
|
465
288
|
"/var/tmp": { mode: "1777", uid: 0, gid: 0 },
|
|
466
289
|
};
|
|
467
|
-
// Runtime commands that get a `/bin/<cmd>` stub at bootstrap so the guest shell
|
|
468
|
-
// resolves them on PATH (e.g. `sh -c "python ..."`, pipelines). The sidecar
|
|
469
|
-
// intercepts these by name and routes them to the embedded V8 / Pyodide runtime.
|
|
470
|
-
const RUNTIME_BOOTSTRAP_COMMANDS = [
|
|
471
|
-
"node",
|
|
472
|
-
"npm",
|
|
473
|
-
"npx",
|
|
474
|
-
"python",
|
|
475
|
-
"python3",
|
|
476
|
-
];
|
|
477
290
|
const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url));
|
|
478
|
-
const SIDECAR_BINARY = join(REPO_ROOT, "target/debug/agentos-sidecar");
|
|
291
|
+
const SIDECAR_BINARY = join(REPO_ROOT, "target/debug/agentos-native-sidecar");
|
|
479
292
|
const SIDECAR_BUILD_INPUTS = [
|
|
480
293
|
join(REPO_ROOT, "Cargo.toml"),
|
|
481
294
|
join(REPO_ROOT, "Cargo.lock"),
|
|
@@ -483,8 +296,6 @@ const SIDECAR_BUILD_INPUTS = [
|
|
|
483
296
|
join(REPO_ROOT, "crates/build-support"),
|
|
484
297
|
join(REPO_ROOT, "crates/execution"),
|
|
485
298
|
join(REPO_ROOT, "crates/kernel"),
|
|
486
|
-
join(REPO_ROOT, "crates/agentos-protocol"),
|
|
487
|
-
join(REPO_ROOT, "crates/agentos-sidecar"),
|
|
488
299
|
join(REPO_ROOT, "crates/native-sidecar"),
|
|
489
300
|
join(REPO_ROOT, "crates/native-sidecar-core"),
|
|
490
301
|
join(REPO_ROOT, "crates/sidecar-protocol"),
|
|
@@ -653,13 +464,13 @@ function ensureNativeSidecarBinary() {
|
|
|
653
464
|
if (sidecarBinaryNeedsBuild()) {
|
|
654
465
|
const cargoBinary = findCargoBinary();
|
|
655
466
|
if (cargoBinary) {
|
|
656
|
-
execFileSync(cargoBinary, ["build", "-q", "-p", "agentos-sidecar"], {
|
|
467
|
+
execFileSync(cargoBinary, ["build", "-q", "-p", "agentos-native-sidecar"], {
|
|
657
468
|
cwd: REPO_ROOT,
|
|
658
469
|
stdio: "pipe",
|
|
659
470
|
});
|
|
660
471
|
}
|
|
661
472
|
else if (!existsSync(SIDECAR_BINARY)) {
|
|
662
|
-
execFileSync(resolveCargoBinary(), ["build", "-q", "-p", "agentos-sidecar"], {
|
|
473
|
+
execFileSync(resolveCargoBinary(), ["build", "-q", "-p", "agentos-native-sidecar"], {
|
|
663
474
|
cwd: REPO_ROOT,
|
|
664
475
|
stdio: "pipe",
|
|
665
476
|
});
|
|
@@ -944,8 +755,15 @@ async function handleHostCallback(request, context) {
|
|
|
944
755
|
error: `unsupported sidecar request type: ${payload.type}`,
|
|
945
756
|
};
|
|
946
757
|
}
|
|
947
|
-
|
|
948
|
-
|
|
758
|
+
if (payload.callback_key === "agentos") {
|
|
759
|
+
const command = parseHostCommandCallbackInput(payload.input);
|
|
760
|
+
if (!command || command.command !== payload.callback_key) {
|
|
761
|
+
return {
|
|
762
|
+
type: "host_callback_result",
|
|
763
|
+
invocation_id: payload.invocation_id,
|
|
764
|
+
error: `Invalid registry callback for "${payload.callback_key}"`,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
949
767
|
try {
|
|
950
768
|
return {
|
|
951
769
|
type: "host_callback_result",
|
|
@@ -969,8 +787,9 @@ async function handleHostCallback(request, context) {
|
|
|
969
787
|
error: `Unknown host function "${payload.callback_key}"`,
|
|
970
788
|
};
|
|
971
789
|
}
|
|
972
|
-
// The sidecar
|
|
973
|
-
//
|
|
790
|
+
// The sidecar authorizes this exact callback key before forwarding it. Never
|
|
791
|
+
// reinterpret direct callback input as a registry command: a valid function
|
|
792
|
+
// schema may itself accept command-shaped data.
|
|
974
793
|
const parsed = definition.inputSchema.safeParse(payload.input);
|
|
975
794
|
if (!parsed.success) {
|
|
976
795
|
return {
|
|
@@ -1594,22 +1413,25 @@ function mapExecutionCompletedEvent(event) {
|
|
|
1594
1413
|
export class AgentOs {
|
|
1595
1414
|
#kernel;
|
|
1596
1415
|
sidecar;
|
|
1597
|
-
_durableSessionEventHandlers = new Map();
|
|
1598
|
-
_agentExitHandlers = new Map();
|
|
1599
1416
|
_processes = new Map();
|
|
1600
1417
|
_languageProcesses = new Map();
|
|
1601
1418
|
_languageProcessIds = new Map();
|
|
1419
|
+
_pendingProcessRegistrations = 0;
|
|
1602
1420
|
_executionOutputHandlers = new Map();
|
|
1603
1421
|
_executionCompletedHandlers = new Map();
|
|
1422
|
+
// No completion event can arrive after the sidecar exits, so its close
|
|
1423
|
+
// rejects every execution still waiting for one.
|
|
1424
|
+
_pendingCompletionRejects = new Set();
|
|
1604
1425
|
_shells = new Map();
|
|
1605
1426
|
// Value is the recorded exit code (undefined until/unless the exit
|
|
1606
1427
|
// resolves) so waitShell can still report it after the entry is dropped.
|
|
1607
1428
|
_closedShellIds = new BoundedSet(CLOSED_SHELL_ID_RETENTION_LIMIT);
|
|
1429
|
+
_closedShellErrors = new BoundedSet(CLOSED_SHELL_ID_RETENTION_LIMIT);
|
|
1608
1430
|
_pendingShellExitPromises = new Set();
|
|
1609
1431
|
_shellCounter = 0;
|
|
1610
|
-
_acpTerminals = new Map();
|
|
1611
|
-
_acpTerminalCounter = 0;
|
|
1612
1432
|
_softwareRoots;
|
|
1433
|
+
_installedSoftware = new Map();
|
|
1434
|
+
_vmMutationActive = false;
|
|
1613
1435
|
_cronManager;
|
|
1614
1436
|
_hostFunctions = [];
|
|
1615
1437
|
_hostFunctionReference = "";
|
|
@@ -1621,8 +1443,7 @@ export class AgentOs {
|
|
|
1621
1443
|
_sidecarSession;
|
|
1622
1444
|
_sidecarVm;
|
|
1623
1445
|
_disposeSidecarEventListener;
|
|
1624
|
-
|
|
1625
|
-
_agentExitHandler;
|
|
1446
|
+
_disposeSidecarCloseListener;
|
|
1626
1447
|
_limitWarningHandler;
|
|
1627
1448
|
_disposeHooks = [];
|
|
1628
1449
|
/**
|
|
@@ -1714,31 +1535,16 @@ export class AgentOs {
|
|
|
1714
1535
|
software = {
|
|
1715
1536
|
list: this._listSoftware.bind(this),
|
|
1716
1537
|
link: this._linkSoftware.bind(this),
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
};
|
|
1721
|
-
sessions = {
|
|
1722
|
-
open: this._openSession.bind(this),
|
|
1723
|
-
get: this._getSession.bind(this),
|
|
1724
|
-
list: this._listSessions.bind(this),
|
|
1725
|
-
delete: this._deleteSession.bind(this),
|
|
1726
|
-
unload: this._unloadSession.bind(this),
|
|
1727
|
-
prompt: this._prompt.bind(this),
|
|
1728
|
-
cancelPrompt: this._cancelPrompt.bind(this),
|
|
1729
|
-
respondPermission: this._respondPermission.bind(this),
|
|
1730
|
-
readHistory: this._readHistory.bind(this),
|
|
1731
|
-
getConfig: this._getSessionConfig.bind(this),
|
|
1732
|
-
setConfigOption: this._setSessionConfigOption.bind(this),
|
|
1733
|
-
getCapabilities: this._getSessionCapabilities.bind(this),
|
|
1734
|
-
getAgentInfo: this._getSessionAgentInfo.bind(this),
|
|
1538
|
+
install: this._installSoftware.bind(this),
|
|
1539
|
+
uninstall: this._uninstallSoftware.bind(this),
|
|
1540
|
+
installed: this._installedSoftwareList.bind(this),
|
|
1735
1541
|
};
|
|
1736
1542
|
cron = {
|
|
1737
1543
|
schedule: this._scheduleCron.bind(this),
|
|
1738
1544
|
list: this._listCronJobs.bind(this),
|
|
1739
1545
|
cancel: this._cancelCronJob.bind(this),
|
|
1740
1546
|
};
|
|
1741
|
-
constructor(kernel, sidecar, softwareRoots, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm,
|
|
1547
|
+
constructor(kernel, sidecar, softwareRoots, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm, limitWarningHandler) {
|
|
1742
1548
|
this.#kernel = kernel;
|
|
1743
1549
|
this.sidecar = sidecar;
|
|
1744
1550
|
this._softwareRoots = softwareRoots;
|
|
@@ -1748,12 +1554,14 @@ export class AgentOs {
|
|
|
1748
1554
|
this._sidecarClient = sidecarClient;
|
|
1749
1555
|
this._sidecarSession = sidecarSession;
|
|
1750
1556
|
this._sidecarVm = sidecarVm;
|
|
1751
|
-
this._agentStderrHandler = agentStderrHandler;
|
|
1752
|
-
this._agentExitHandler = agentExitHandler;
|
|
1753
1557
|
this._limitWarningHandler = limitWarningHandler;
|
|
1754
1558
|
this._disposeSidecarEventListener = this._sidecarClient.onEvent((event) => {
|
|
1755
1559
|
this._handleSidecarEvent(event);
|
|
1756
1560
|
});
|
|
1561
|
+
this._disposeSidecarCloseListener = this._sidecarClient.onClose((error) => {
|
|
1562
|
+
for (const reject of this._pendingCompletionRejects)
|
|
1563
|
+
reject(error);
|
|
1564
|
+
});
|
|
1757
1565
|
agentOsRuntimeAdmins.set(this, {
|
|
1758
1566
|
kernel,
|
|
1759
1567
|
rootView: rootFilesystem,
|
|
@@ -1767,18 +1575,12 @@ export class AgentOs {
|
|
|
1767
1575
|
static async getSharedSidecar(options = {}) {
|
|
1768
1576
|
return getSharedAgentOsSidecarInternal(options);
|
|
1769
1577
|
}
|
|
1770
|
-
static async create(
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
//
|
|
1776
|
-
// @agentos-software/* dependencies (e.g. common), each default-exporting
|
|
1777
|
-
// its registry-built descriptor. Agent packages are NOT projected here —
|
|
1778
|
-
// openSession({ agent: id }) links the matching agent dependency into the running
|
|
1779
|
-
// VM on first use, so agent closures (and pi's V8 snapshot bundle) only
|
|
1780
|
-
// enter VMs that run them. Unbuilt packages throw with build
|
|
1781
|
-
// instructions; opt out via defaultSoftware: false.
|
|
1578
|
+
static async create(options,
|
|
1579
|
+
/** @internal Product wrappers select a sidecar-owned default profile here. */
|
|
1580
|
+
defaultsProfile = "agent_os") {
|
|
1581
|
+
options = parseAgentOsOptions(options);
|
|
1582
|
+
// Default software is resolved from immutable `.aospkg` files vendored in
|
|
1583
|
+
// this package. Runtime package resolution never consults npm.
|
|
1782
1584
|
const defaultSoftware = options?.defaultSoftware === false ? [] : resolveDefaultSoftware();
|
|
1783
1585
|
const software = options?.defaultSoftware === false
|
|
1784
1586
|
? (options.software ?? [])
|
|
@@ -1802,9 +1604,7 @@ export class AgentOs {
|
|
|
1802
1604
|
return [{ path: ref.path }];
|
|
1803
1605
|
});
|
|
1804
1606
|
// All package software is projected into `/opt/agentos` by the sidecar. The
|
|
1805
|
-
// client stages nothing host-side and parses
|
|
1806
|
-
// sidecar owns agent resolution, agent enumeration, and agent snapshot
|
|
1807
|
-
// bundle loading from the projected package dirs.
|
|
1607
|
+
// client stages nothing host-side and parses no package manifests.
|
|
1808
1608
|
const localMounts = await resolveCompatLocalMounts(options?.mounts);
|
|
1809
1609
|
// Keys become command names, so resolve and check them before anything
|
|
1810
1610
|
// else in `create()` allocates a sidecar or a sandbox.
|
|
@@ -1823,10 +1623,6 @@ export class AgentOs {
|
|
|
1823
1623
|
// runtime `linkSoftware` appends to that live dir). The client no longer
|
|
1824
1624
|
// stages packages host-side.
|
|
1825
1625
|
const hostFunctionBootstrapCommands = collectHostFunctionBootstrapCommands(hostFunctions);
|
|
1826
|
-
const bootstrapCommands = [
|
|
1827
|
-
...RUNTIME_BOOTSTRAP_COMMANDS,
|
|
1828
|
-
...hostFunctionBootstrapCommands,
|
|
1829
|
-
];
|
|
1830
1626
|
const bootstrapLower = createKernelBootstrapLower(options?.rootFilesystem);
|
|
1831
1627
|
let hostFunctionReference = "";
|
|
1832
1628
|
let rootBridge = null;
|
|
@@ -1842,7 +1638,9 @@ export class AgentOs {
|
|
|
1842
1638
|
cleanedUp = true;
|
|
1843
1639
|
};
|
|
1844
1640
|
try {
|
|
1845
|
-
const env =
|
|
1641
|
+
const env = options?.environment !== undefined
|
|
1642
|
+
? { ...options.environment }
|
|
1643
|
+
: getBaseEnvironment();
|
|
1846
1644
|
// Guest command paths. The sidecar owns the `/opt/agentos` projection and
|
|
1847
1645
|
// reports the exact projected package commands after `configureVm`.
|
|
1848
1646
|
// HostFunction-shim commands are added below.
|
|
@@ -1856,16 +1654,27 @@ export class AgentOs {
|
|
|
1856
1654
|
client = shared.client;
|
|
1857
1655
|
const session = shared.session;
|
|
1858
1656
|
nativeSession = session;
|
|
1859
|
-
const
|
|
1657
|
+
const hostPermissions = options?.permissions ?? {
|
|
1658
|
+
...allowAll,
|
|
1659
|
+
binding: "allow",
|
|
1660
|
+
};
|
|
1661
|
+
const sidecarPermissions = options?.permissions
|
|
1662
|
+
? serializePermissionsForSidecar(options.permissions)
|
|
1663
|
+
: undefined;
|
|
1860
1664
|
const createVmConfig = {
|
|
1861
|
-
|
|
1665
|
+
defaultsProfile,
|
|
1666
|
+
...(options?.environment !== undefined
|
|
1667
|
+
? { env: { ...options.environment } }
|
|
1668
|
+
: {}),
|
|
1862
1669
|
database: options?.database,
|
|
1863
1670
|
...(options?.user ? { user: options.user } : {}),
|
|
1864
1671
|
rootFilesystem: serializeRootFilesystemForSidecar(options?.rootFilesystem, bootstrapLower),
|
|
1865
|
-
permissions: sidecarPermissions,
|
|
1672
|
+
...(sidecarPermissions ? { permissions: sidecarPermissions } : {}),
|
|
1866
1673
|
limits: options?.limits,
|
|
1867
1674
|
loopbackExemptPorts: options?.loopbackExemptPorts ?? [],
|
|
1868
|
-
|
|
1675
|
+
...(hostFunctionBootstrapCommands.length > 0
|
|
1676
|
+
? { bootstrapCommands: hostFunctionBootstrapCommands }
|
|
1677
|
+
: {}),
|
|
1869
1678
|
...(options?.rootFilesystem?.type === "native"
|
|
1870
1679
|
? {
|
|
1871
1680
|
nativeRoot: {
|
|
@@ -1912,7 +1721,7 @@ export class AgentOs {
|
|
|
1912
1721
|
event.ownership.vm_id === nativeVm.vmId, 10_000);
|
|
1913
1722
|
const configuredVm = await client.configureVm(session, nativeVm, {
|
|
1914
1723
|
mounts: sidecarMounts,
|
|
1915
|
-
permissions:
|
|
1724
|
+
permissions: undefined,
|
|
1916
1725
|
commandPermissions: {},
|
|
1917
1726
|
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1918
1727
|
packages: sidecarPackages,
|
|
@@ -1937,7 +1746,7 @@ export class AgentOs {
|
|
|
1937
1746
|
cwd: "/workspace",
|
|
1938
1747
|
localMounts,
|
|
1939
1748
|
sidecarMounts,
|
|
1940
|
-
permissions:
|
|
1749
|
+
permissions: undefined,
|
|
1941
1750
|
commandPermissions: {},
|
|
1942
1751
|
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1943
1752
|
// Retained for runtime mount reconfigures: `configure_vm` is
|
|
@@ -1963,7 +1772,7 @@ export class AgentOs {
|
|
|
1963
1772
|
kernel,
|
|
1964
1773
|
rootView: rootBridge.createRootView(),
|
|
1965
1774
|
sidecarMounts,
|
|
1966
|
-
sidecarPermissions,
|
|
1775
|
+
sidecarPermissions: undefined,
|
|
1967
1776
|
commandPermissions: {},
|
|
1968
1777
|
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1969
1778
|
sidecarClient: client,
|
|
@@ -2014,7 +1823,7 @@ export class AgentOs {
|
|
|
2014
1823
|
createVm: async () => createVmAdmin(),
|
|
2015
1824
|
});
|
|
2016
1825
|
const vmAdmin = sidecarLease.admin;
|
|
2017
|
-
const vm = new AgentOs(vmAdmin.kernel, sidecar, [], vmAdmin.hostMounts, vmAdmin.env, vmAdmin.rootView, vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm, options?.
|
|
1826
|
+
const vm = new AgentOs(vmAdmin.kernel, sidecar, [], vmAdmin.hostMounts, vmAdmin.env, vmAdmin.rootView, vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm, options?.onLimitWarning);
|
|
2018
1827
|
vm._sidecarLease = sidecarLease;
|
|
2019
1828
|
vm._hostFunctions = vmAdmin.hostFunctions;
|
|
2020
1829
|
vm._hostFunctionReference = vmAdmin.hostFunctionReference;
|
|
@@ -2052,10 +1861,7 @@ export class AgentOs {
|
|
|
2052
1861
|
let executionId;
|
|
2053
1862
|
const earlyOutput = [];
|
|
2054
1863
|
const completedBeforeAdmission = new Set();
|
|
2055
|
-
|
|
2056
|
-
const completion = new Promise((resolve) => {
|
|
2057
|
-
resolveCompletion = resolve;
|
|
2058
|
-
});
|
|
1864
|
+
const { completion, resolve: resolveCompletion, release: releaseCompletion, } = this._pendingCompletion();
|
|
2059
1865
|
let warnedAboutEarlyEvents = false;
|
|
2060
1866
|
const warnAboutEarlyEvents = () => {
|
|
2061
1867
|
if (warnedAboutEarlyEvents)
|
|
@@ -2092,7 +1898,7 @@ export class AgentOs {
|
|
|
2092
1898
|
}
|
|
2093
1899
|
if (event.executionId === executionId) {
|
|
2094
1900
|
completed = true;
|
|
2095
|
-
resolveCompletion
|
|
1901
|
+
resolveCompletion();
|
|
2096
1902
|
}
|
|
2097
1903
|
});
|
|
2098
1904
|
let response;
|
|
@@ -2102,11 +1908,13 @@ export class AgentOs {
|
|
|
2102
1908
|
catch (error) {
|
|
2103
1909
|
unsubscribeOutput();
|
|
2104
1910
|
unsubscribeCompleted();
|
|
1911
|
+
releaseCompletion();
|
|
2105
1912
|
throw error;
|
|
2106
1913
|
}
|
|
2107
1914
|
if (response.type !== "execution_accepted") {
|
|
2108
1915
|
unsubscribeOutput();
|
|
2109
1916
|
unsubscribeCompleted();
|
|
1917
|
+
releaseCompletion();
|
|
2110
1918
|
throw new Error(`unexpected execution response: ${response.type}`);
|
|
2111
1919
|
}
|
|
2112
1920
|
const descriptor = response.response.execution;
|
|
@@ -2117,7 +1925,7 @@ export class AgentOs {
|
|
|
2117
1925
|
}
|
|
2118
1926
|
completed = completedBeforeAdmission.has(executionId);
|
|
2119
1927
|
if (completed)
|
|
2120
|
-
resolveCompletion
|
|
1928
|
+
resolveCompletion();
|
|
2121
1929
|
const abort = () => {
|
|
2122
1930
|
// Admitted executions report cancellation through their structured result.
|
|
2123
1931
|
void this._cancelExecution(response.response.operationId).catch((error) => {
|
|
@@ -2130,6 +1938,7 @@ export class AgentOs {
|
|
|
2130
1938
|
const cleanup = () => {
|
|
2131
1939
|
unsubscribeOutput();
|
|
2132
1940
|
unsubscribeCompleted();
|
|
1941
|
+
releaseCompletion();
|
|
2133
1942
|
options.signal?.removeEventListener("abort", abort);
|
|
2134
1943
|
};
|
|
2135
1944
|
if (background) {
|
|
@@ -2165,44 +1974,45 @@ export class AgentOs {
|
|
|
2165
1974
|
if ("contextId" in options) {
|
|
2166
1975
|
throw new Error("contextId is not supported for spawned processes; contexts are for attached operations");
|
|
2167
1976
|
}
|
|
1977
|
+
const releaseRegistrySlot = this._reserveProcessRegistrySlot();
|
|
2168
1978
|
const executionId = `process-${randomUUID()}`;
|
|
2169
1979
|
const internalOptions = {
|
|
2170
1980
|
...options,
|
|
2171
1981
|
output: options.output,
|
|
2172
1982
|
};
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
this._languageProcesses.set(admitted.pid, {
|
|
2181
|
-
executionId: admitted.executionId,
|
|
2182
|
-
descriptor,
|
|
2183
|
-
outputHandlers: new Set(),
|
|
2184
|
-
exitHandlers: new Set(),
|
|
2185
|
-
});
|
|
2186
|
-
this._languageProcessIds.set(admitted.executionId, admitted.pid);
|
|
2187
|
-
const reconcileCompletion = async () => {
|
|
2188
|
-
if (!this._languageProcesses.get(admitted.pid)?.exit) {
|
|
2189
|
-
await this._waitProcess(admitted.pid);
|
|
2190
|
-
}
|
|
2191
|
-
};
|
|
2192
|
-
if (admitted.completed)
|
|
2193
|
-
await reconcileCompletion();
|
|
2194
|
-
else {
|
|
2195
|
-
void admitted.completion
|
|
2196
|
-
.then(reconcileCompletion)
|
|
2197
|
-
.catch((error) => console.error("[agentos] failed to reconcile spawned process completion", error));
|
|
2198
|
-
}
|
|
2199
|
-
if (options.signal) {
|
|
2200
|
-
const abort = () => {
|
|
2201
|
-
void this._killProcess(admitted.pid);
|
|
1983
|
+
try {
|
|
1984
|
+
const admitted = (await this._executionOperation(buildPayload(internalOptions, executionId), internalOptions, true));
|
|
1985
|
+
const descriptor = {
|
|
1986
|
+
pid: admitted.pid,
|
|
1987
|
+
state: "running",
|
|
1988
|
+
language,
|
|
1989
|
+
startedAtMs: admitted.createdAtMs,
|
|
2202
1990
|
};
|
|
2203
|
-
|
|
1991
|
+
this._languageProcesses.set(admitted.pid, {
|
|
1992
|
+
executionId: admitted.executionId,
|
|
1993
|
+
descriptor,
|
|
1994
|
+
outputHandlers: new Set(),
|
|
1995
|
+
exitHandlers: new Set(),
|
|
1996
|
+
});
|
|
1997
|
+
this._languageProcessIds.set(admitted.executionId, admitted.pid);
|
|
1998
|
+
releaseRegistrySlot();
|
|
1999
|
+
const reconcileCompletion = async () => {
|
|
2000
|
+
if (!this._languageProcesses.get(admitted.pid)?.exit) {
|
|
2001
|
+
await this._waitProcess(admitted.pid);
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
if (admitted.completed)
|
|
2005
|
+
await reconcileCompletion();
|
|
2006
|
+
else {
|
|
2007
|
+
void admitted.completion
|
|
2008
|
+
.then(reconcileCompletion)
|
|
2009
|
+
.catch((error) => console.error("[agentos] failed to reconcile spawned process completion", error));
|
|
2010
|
+
}
|
|
2011
|
+
return (this._languageProcesses.get(admitted.pid)?.descriptor ?? descriptor);
|
|
2012
|
+
}
|
|
2013
|
+
finally {
|
|
2014
|
+
releaseRegistrySlot();
|
|
2204
2015
|
}
|
|
2205
|
-
return this._languageProcesses.get(admitted.pid)?.descriptor ?? descriptor;
|
|
2206
2016
|
}
|
|
2207
2017
|
async _exec(command, options = {}) {
|
|
2208
2018
|
return (await this._executionOperation({
|
|
@@ -2564,12 +2374,9 @@ export class AgentOs {
|
|
|
2564
2374
|
return response.response.executions.map(mapExecutionDescriptor);
|
|
2565
2375
|
}
|
|
2566
2376
|
async _waitExecutionResult(executionId) {
|
|
2567
|
-
|
|
2568
|
-
const completion = new Promise((resolve) => {
|
|
2569
|
-
resolveCompletion = resolve;
|
|
2570
|
-
});
|
|
2377
|
+
const { completion, resolve: resolveCompletion, release: releaseCompletion, } = this._pendingCompletion();
|
|
2571
2378
|
const unsubscribe = this._onExecutionCompleted(executionId, () => {
|
|
2572
|
-
resolveCompletion
|
|
2379
|
+
resolveCompletion();
|
|
2573
2380
|
});
|
|
2574
2381
|
let response;
|
|
2575
2382
|
try {
|
|
@@ -2587,6 +2394,7 @@ export class AgentOs {
|
|
|
2587
2394
|
}
|
|
2588
2395
|
finally {
|
|
2589
2396
|
unsubscribe();
|
|
2397
|
+
releaseCompletion();
|
|
2590
2398
|
}
|
|
2591
2399
|
if (response.type !== "execution_completed") {
|
|
2592
2400
|
throw new Error(`unexpected waitExecution response: ${response.type}`);
|
|
@@ -2679,6 +2487,24 @@ export class AgentOs {
|
|
|
2679
2487
|
this._executionOutputHandlers.delete(executionId);
|
|
2680
2488
|
};
|
|
2681
2489
|
}
|
|
2490
|
+
/** A completion signal that rejects if the sidecar exits before it resolves. */
|
|
2491
|
+
_pendingCompletion() {
|
|
2492
|
+
let resolve;
|
|
2493
|
+
let reject;
|
|
2494
|
+
const completion = new Promise((res, rej) => {
|
|
2495
|
+
resolve = res;
|
|
2496
|
+
reject = rej;
|
|
2497
|
+
});
|
|
2498
|
+
// Callers await the signal only on some paths; an unawaited rejection
|
|
2499
|
+
// must not surface as an unhandled rejection.
|
|
2500
|
+
completion.catch(() => { });
|
|
2501
|
+
this._pendingCompletionRejects.add(reject);
|
|
2502
|
+
return {
|
|
2503
|
+
completion,
|
|
2504
|
+
resolve,
|
|
2505
|
+
release: () => this._pendingCompletionRejects.delete(reject),
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2682
2508
|
_onExecutionCompleted(executionId, handler) {
|
|
2683
2509
|
const handlers = this._executionCompletedHandlers.get(executionId) ?? new Set();
|
|
2684
2510
|
handlers.add(handler);
|
|
@@ -2696,8 +2522,7 @@ export class AgentOs {
|
|
|
2696
2522
|
args,
|
|
2697
2523
|
startedAtMs: Date.now(),
|
|
2698
2524
|
retainEvents,
|
|
2699
|
-
|
|
2700
|
-
nextSequence: 0,
|
|
2525
|
+
processId: proc.processId,
|
|
2701
2526
|
signal: undefined,
|
|
2702
2527
|
exit: undefined,
|
|
2703
2528
|
outputHandlers,
|
|
@@ -2709,16 +2534,15 @@ export class AgentOs {
|
|
|
2709
2534
|
// requires exited processes to stay queryable (running:false, exitCode set).
|
|
2710
2535
|
// `_processes` is a process table for this VM's lifetime; it is freed wholesale
|
|
2711
2536
|
// in dispose(). (H5: the leak was that dispose() never cleared it.)
|
|
2712
|
-
void proc
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
h(exit);
|
|
2537
|
+
void proc
|
|
2538
|
+
.wait()
|
|
2539
|
+
.then((code) => {
|
|
2540
|
+
this._recordProcessExit(proc.pid, code);
|
|
2541
|
+
})
|
|
2542
|
+
.catch((error) => {
|
|
2543
|
+
// A failed wait is not an exit event. Explicit process.wait calls retain
|
|
2544
|
+
// the error, and callers may still signal an unconfirmed live process.
|
|
2545
|
+
console.error(`[agentOS] process ${proc.pid} exit observation failed`, error);
|
|
2722
2546
|
});
|
|
2723
2547
|
return {
|
|
2724
2548
|
pid: proc.pid,
|
|
@@ -2727,46 +2551,123 @@ export class AgentOs {
|
|
|
2727
2551
|
startedAtMs: entry.startedAtMs,
|
|
2728
2552
|
};
|
|
2729
2553
|
}
|
|
2554
|
+
_recordProcessExit(pid, exitCode) {
|
|
2555
|
+
const entry = this._processes.get(pid);
|
|
2556
|
+
if (!entry || entry.exit)
|
|
2557
|
+
return;
|
|
2558
|
+
const exit = {
|
|
2559
|
+
pid,
|
|
2560
|
+
exitCode,
|
|
2561
|
+
outcome: entry.signal ? "signalled" : "exited",
|
|
2562
|
+
...(entry.signal ? { signal: entry.signal } : {}),
|
|
2563
|
+
};
|
|
2564
|
+
entry.exit = exit;
|
|
2565
|
+
for (const handler of entry.exitHandlers) {
|
|
2566
|
+
try {
|
|
2567
|
+
handler(exit);
|
|
2568
|
+
}
|
|
2569
|
+
catch (error) {
|
|
2570
|
+
console.error("[agentOS] process exit handler failed", error);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
_reserveProcessRegistrySlot() {
|
|
2575
|
+
this._pruneExitedProcesses(this._pendingProcessRegistrations + 1);
|
|
2576
|
+
const retained = this._processes.size + this._languageProcesses.size;
|
|
2577
|
+
const admitted = retained + this._pendingProcessRegistrations + 1;
|
|
2578
|
+
if (admitted > PROCESS_REGISTRY_LIMIT) {
|
|
2579
|
+
throw Object.assign(new Error(`process registry limit ${PROCESS_REGISTRY_LIMIT} reached; wait for an exited process to be evicted or raise PROCESS_REGISTRY_LIMIT`), {
|
|
2580
|
+
code: "ERR_AGENTOS_RESOURCE_LIMIT",
|
|
2581
|
+
limitName: "process_registry_entries",
|
|
2582
|
+
configuredLimit: PROCESS_REGISTRY_LIMIT,
|
|
2583
|
+
requested: admitted,
|
|
2584
|
+
configurationPath: "PROCESS_REGISTRY_LIMIT",
|
|
2585
|
+
unit: "processes",
|
|
2586
|
+
scope: "vm",
|
|
2587
|
+
operation: "process.spawn",
|
|
2588
|
+
retryable: true,
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
this._pendingProcessRegistrations += 1;
|
|
2592
|
+
if (admitted >= PROCESS_REGISTRY_LIMIT * 0.8) {
|
|
2593
|
+
console.warn("[agentos] process registry admission approaches PROCESS_REGISTRY_LIMIT", { admitted, limit: PROCESS_REGISTRY_LIMIT });
|
|
2594
|
+
}
|
|
2595
|
+
let active = true;
|
|
2596
|
+
return () => {
|
|
2597
|
+
if (!active)
|
|
2598
|
+
return;
|
|
2599
|
+
active = false;
|
|
2600
|
+
this._pendingProcessRegistrations -= 1;
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
_pruneExitedProcesses(reserveSlots) {
|
|
2604
|
+
const retained = this._processes.size + this._languageProcesses.size;
|
|
2605
|
+
const target = Math.max(0, PROCESS_REGISTRY_LIMIT - reserveSlots);
|
|
2606
|
+
let removeCount = retained - target;
|
|
2607
|
+
if (removeCount <= 0)
|
|
2608
|
+
return;
|
|
2609
|
+
const exited = [
|
|
2610
|
+
...[...this._processes.entries()]
|
|
2611
|
+
.filter(([, entry]) => entry.exit !== undefined)
|
|
2612
|
+
.map(([pid, entry]) => ({
|
|
2613
|
+
kind: "process",
|
|
2614
|
+
pid,
|
|
2615
|
+
startedAtMs: entry.startedAtMs,
|
|
2616
|
+
})),
|
|
2617
|
+
...[...this._languageProcesses.entries()]
|
|
2618
|
+
.filter(([, entry]) => entry.exit !== undefined)
|
|
2619
|
+
.map(([pid, entry]) => ({
|
|
2620
|
+
kind: "language",
|
|
2621
|
+
pid,
|
|
2622
|
+
startedAtMs: entry.descriptor.startedAtMs,
|
|
2623
|
+
})),
|
|
2624
|
+
].sort((left, right) => left.startedAtMs - right.startedAtMs || left.pid - right.pid);
|
|
2625
|
+
for (const entry of exited) {
|
|
2626
|
+
if (removeCount <= 0)
|
|
2627
|
+
break;
|
|
2628
|
+
if (entry.kind === "process") {
|
|
2629
|
+
this._processes.delete(entry.pid);
|
|
2630
|
+
}
|
|
2631
|
+
else {
|
|
2632
|
+
const language = this._languageProcesses.get(entry.pid);
|
|
2633
|
+
if (language)
|
|
2634
|
+
this._languageProcessIds.delete(language.executionId);
|
|
2635
|
+
this._languageProcesses.delete(entry.pid);
|
|
2636
|
+
}
|
|
2637
|
+
removeCount -= 1;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2730
2640
|
async _spawnProcess(command, args = [], options = {}) {
|
|
2641
|
+
const releaseRegistrySlot = this._reserveProcessRegistrySlot();
|
|
2731
2642
|
const outputHandlers = new Set();
|
|
2732
2643
|
const exitHandlers = new Set();
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2644
|
+
let proc;
|
|
2645
|
+
try {
|
|
2646
|
+
proc = this.#kernel.spawn(command, args, {
|
|
2647
|
+
cwd: options.cwd,
|
|
2648
|
+
env: options.env,
|
|
2649
|
+
stdin: options.stdin,
|
|
2650
|
+
timeout: options.timeoutMs,
|
|
2651
|
+
streamStdin: true,
|
|
2652
|
+
retainOutput: options.output?.retainEvents ?? false,
|
|
2653
|
+
onStdout: (data, metadata) => {
|
|
2654
|
+
options?.onStdout?.(data);
|
|
2655
|
+
for (const h of outputHandlers) {
|
|
2656
|
+
h({ pid: proc.pid, stream: "stdout", data, ...metadata });
|
|
2657
|
+
}
|
|
2658
|
+
},
|
|
2659
|
+
onStderr: (data, metadata) => {
|
|
2660
|
+
options?.onStderr?.(data);
|
|
2661
|
+
for (const h of outputHandlers) {
|
|
2662
|
+
h({ pid: proc.pid, stream: "stderr", data, ...metadata });
|
|
2663
|
+
}
|
|
2664
|
+
},
|
|
2746
2665
|
});
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
timeout: options.timeoutMs,
|
|
2753
|
-
streamStdin: true,
|
|
2754
|
-
onStdout: (data) => {
|
|
2755
|
-
recordOutput("stdout", data);
|
|
2756
|
-
options?.onStdout?.(data);
|
|
2757
|
-
for (const h of outputHandlers) {
|
|
2758
|
-
h({ pid: proc.pid, stream: "stdout", data });
|
|
2759
|
-
}
|
|
2760
|
-
},
|
|
2761
|
-
onStderr: (data) => {
|
|
2762
|
-
recordOutput("stderr", data);
|
|
2763
|
-
options?.onStderr?.(data);
|
|
2764
|
-
for (const h of outputHandlers) {
|
|
2765
|
-
h({ pid: proc.pid, stream: "stderr", data });
|
|
2766
|
-
}
|
|
2767
|
-
},
|
|
2768
|
-
});
|
|
2769
|
-
return this._trackProcess(proc, command, args, options.output?.retainEvents ?? false, outputHandlers, exitHandlers);
|
|
2666
|
+
return this._trackProcess(proc, command, args, options.output?.retainEvents ?? false, outputHandlers, exitHandlers);
|
|
2667
|
+
}
|
|
2668
|
+
finally {
|
|
2669
|
+
releaseRegistrySlot();
|
|
2670
|
+
}
|
|
2770
2671
|
}
|
|
2771
2672
|
spawn(command, args = [], options) {
|
|
2772
2673
|
return this.process.spawn(command, args, options);
|
|
@@ -2825,6 +2726,8 @@ export class AgentOs {
|
|
|
2825
2726
|
if (language.exit)
|
|
2826
2727
|
return language.exit;
|
|
2827
2728
|
const result = await this._waitExecutionResult(language.executionId);
|
|
2729
|
+
if (language.exit)
|
|
2730
|
+
return language.exit;
|
|
2828
2731
|
const exit = {
|
|
2829
2732
|
pid,
|
|
2830
2733
|
outcome: result.outcome === "timed_out"
|
|
@@ -3009,25 +2912,29 @@ export class AgentOs {
|
|
|
3009
2912
|
* leaving the mount silently host-only.
|
|
3010
2913
|
*/
|
|
3011
2914
|
async _mountFs(descriptor) {
|
|
3012
|
-
this.
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
2915
|
+
return this._withVmMutation(async () => {
|
|
2916
|
+
this._assertSafeAbsolutePath(descriptor.path);
|
|
2917
|
+
if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
|
|
2918
|
+
throw new Error("portable dynamic mounts require the native sidecar");
|
|
2919
|
+
}
|
|
2920
|
+
await this.#kernel.mountDescriptor({
|
|
2921
|
+
guestPath: descriptor.path,
|
|
2922
|
+
readOnly: descriptor.readOnly ?? false,
|
|
2923
|
+
plugin: {
|
|
2924
|
+
id: descriptor.plugin.id,
|
|
2925
|
+
config: descriptor.plugin.config ?? {},
|
|
2926
|
+
},
|
|
2927
|
+
});
|
|
3023
2928
|
});
|
|
3024
2929
|
}
|
|
3025
2930
|
async _unmountFs(path) {
|
|
3026
|
-
this.
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
2931
|
+
return this._withVmMutation(async () => {
|
|
2932
|
+
this._assertSafeAbsolutePath(path);
|
|
2933
|
+
if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
|
|
2934
|
+
throw new Error("portable dynamic mounts require the native sidecar");
|
|
2935
|
+
}
|
|
2936
|
+
await this.#kernel.unmountDescriptor(path);
|
|
2937
|
+
});
|
|
3031
2938
|
}
|
|
3032
2939
|
async _listMounts() {
|
|
3033
2940
|
if (!(this.#kernel instanceof NativeSidecarKernelProxy))
|
|
@@ -3143,8 +3050,8 @@ export class AgentOs {
|
|
|
3143
3050
|
}
|
|
3144
3051
|
/**
|
|
3145
3052
|
* Fetch an HTTP endpoint inside the VM while preserving duplicate response
|
|
3146
|
-
* headers and arbitrary binary bodies.
|
|
3147
|
-
*
|
|
3053
|
+
* headers and arbitrary binary bodies. This compatibility surface supports
|
|
3054
|
+
* fetch-like callers that proxy requests into a guest VM.
|
|
3148
3055
|
*/
|
|
3149
3056
|
async fetch(port, request) {
|
|
3150
3057
|
const url = new URL(request.url);
|
|
@@ -3210,6 +3117,25 @@ export class AgentOs {
|
|
|
3210
3117
|
});
|
|
3211
3118
|
}
|
|
3212
3119
|
_openTerminal(options) {
|
|
3120
|
+
// Include closed-but-not-yet-exited terminals: close is not proof of exit.
|
|
3121
|
+
if (this._pendingShellExitPromises.size >= TERMINAL_LIMIT) {
|
|
3122
|
+
throw Object.assign(new Error(`terminal limit ${TERMINAL_LIMIT} reached; wait for existing terminals to exit or raise TERMINAL_LIMIT`), {
|
|
3123
|
+
code: "ERR_AGENTOS_RESOURCE_LIMIT",
|
|
3124
|
+
limitName: "active_terminals",
|
|
3125
|
+
configuredLimit: TERMINAL_LIMIT,
|
|
3126
|
+
requested: TERMINAL_LIMIT + 1,
|
|
3127
|
+
configurationPath: "TERMINAL_LIMIT",
|
|
3128
|
+
unit: "terminals",
|
|
3129
|
+
scope: "vm",
|
|
3130
|
+
retryable: true,
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
if (this._pendingShellExitPromises.size + 1 >= TERMINAL_LIMIT * 0.8) {
|
|
3134
|
+
console.warn("[agentos] terminal admission approaches TERMINAL_LIMIT; close unused terminals", {
|
|
3135
|
+
active: this._pendingShellExitPromises.size + 1,
|
|
3136
|
+
limit: TERMINAL_LIMIT,
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3213
3139
|
const shellId = `shell-${++this._shellCounter}`;
|
|
3214
3140
|
this._closedShellIds.delete(shellId);
|
|
3215
3141
|
const dataHandlers = new Set();
|
|
@@ -3249,9 +3175,17 @@ export class AgentOs {
|
|
|
3249
3175
|
handler({ shellId, exitCode });
|
|
3250
3176
|
return exitCode;
|
|
3251
3177
|
}, (error) => {
|
|
3178
|
+
this._closedShellErrors.add(shellId, error);
|
|
3252
3179
|
finalize();
|
|
3253
3180
|
throw error;
|
|
3254
3181
|
});
|
|
3182
|
+
// Preserve rejection for wait() without leaving an unobserved promise.
|
|
3183
|
+
void entry.exitPromise.catch((error) => {
|
|
3184
|
+
console.error("[agentos] terminal ended without a confirmed exit", {
|
|
3185
|
+
shellId,
|
|
3186
|
+
error,
|
|
3187
|
+
});
|
|
3188
|
+
});
|
|
3255
3189
|
this._pendingShellExitPromises.add(entry.exitPromise);
|
|
3256
3190
|
this._shells.set(shellId, entry);
|
|
3257
3191
|
return { shellId };
|
|
@@ -3317,6 +3251,9 @@ export class AgentOs {
|
|
|
3317
3251
|
_waitTerminal(shellId) {
|
|
3318
3252
|
const entry = this._shells.get(shellId);
|
|
3319
3253
|
if (!entry) {
|
|
3254
|
+
if (this._closedShellErrors.has(shellId)) {
|
|
3255
|
+
return Promise.reject(this._closedShellErrors.get(shellId));
|
|
3256
|
+
}
|
|
3320
3257
|
const exitCode = this._closedShellIds.get(shellId);
|
|
3321
3258
|
if (exitCode !== undefined)
|
|
3322
3259
|
return Promise.resolve(exitCode);
|
|
@@ -3374,10 +3311,10 @@ export class AgentOs {
|
|
|
3374
3311
|
/** Returns info about all processes spawned via spawn(). */
|
|
3375
3312
|
async _listProcesses() {
|
|
3376
3313
|
return [
|
|
3377
|
-
...[...this._processes.values()].map(({ proc, command, startedAtMs }) => ({
|
|
3314
|
+
...[...this._processes.values()].map(({ proc, command, startedAtMs, exit }) => ({
|
|
3378
3315
|
pid: proc.pid,
|
|
3379
3316
|
command,
|
|
3380
|
-
state: proc.exitCode
|
|
3317
|
+
state: exit || proc.exitCode !== null ? "exited" : "running",
|
|
3381
3318
|
startedAtMs,
|
|
3382
3319
|
})),
|
|
3383
3320
|
...[...this._languageProcesses.values()].map((entry) => entry.descriptor),
|
|
@@ -3392,38 +3329,32 @@ export class AgentOs {
|
|
|
3392
3329
|
}
|
|
3393
3330
|
/** Returns processes organized as a tree using ppid relationships. */
|
|
3394
3331
|
async _processTree() {
|
|
3395
|
-
const
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3332
|
+
const records = this.#kernel instanceof NativeSidecarKernelProxy
|
|
3333
|
+
? this.#kernel.snapshotProcessTopology()
|
|
3334
|
+
: this._listAllProcesses().map((info) => ({
|
|
3335
|
+
info,
|
|
3336
|
+
key: `kernel:${info.pid}`,
|
|
3337
|
+
parentKey: `kernel:${info.ppid}`,
|
|
3338
|
+
}));
|
|
3339
|
+
const roots = buildProcessForest(records, (proc, children) => ({
|
|
3340
|
+
pid: proc.pid,
|
|
3341
|
+
ppid: proc.ppid,
|
|
3342
|
+
command: proc.command,
|
|
3343
|
+
state: proc.status,
|
|
3344
|
+
startedAtMs: proc.startTime,
|
|
3345
|
+
children,
|
|
3346
|
+
}));
|
|
3347
|
+
const kernelKeys = new Set(records.map((record) => record.key));
|
|
3408
3348
|
for (const process of this._languageProcesses.values()) {
|
|
3409
|
-
|
|
3410
|
-
|
|
3349
|
+
// Language admission returns a raw kernel PID and bypasses the proxy's
|
|
3350
|
+
// synthetic spawn registry. A matching display PID is not identity.
|
|
3351
|
+
if (!kernelKeys.has(`kernel:${process.descriptor.pid}`)) {
|
|
3352
|
+
roots.push({
|
|
3411
3353
|
...process.descriptor,
|
|
3412
3354
|
children: [],
|
|
3413
3355
|
});
|
|
3414
3356
|
}
|
|
3415
3357
|
}
|
|
3416
|
-
// Wire: attach each node to its parent
|
|
3417
|
-
const roots = [];
|
|
3418
|
-
for (const node of nodeMap.values()) {
|
|
3419
|
-
const parent = node.ppid === undefined ? undefined : nodeMap.get(node.ppid);
|
|
3420
|
-
if (parent) {
|
|
3421
|
-
parent.children.push(node);
|
|
3422
|
-
}
|
|
3423
|
-
else {
|
|
3424
|
-
roots.push(node);
|
|
3425
|
-
}
|
|
3426
|
-
}
|
|
3427
3358
|
return roots;
|
|
3428
3359
|
}
|
|
3429
3360
|
/** Returns info about a specific process by PID. Throws if not found. */
|
|
@@ -3438,15 +3369,29 @@ export class AgentOs {
|
|
|
3438
3369
|
return {
|
|
3439
3370
|
pid: entry.proc.pid,
|
|
3440
3371
|
command: entry.command,
|
|
3441
|
-
state: entry.proc.exitCode
|
|
3372
|
+
state: entry.exit || entry.proc.exitCode !== null ? "exited" : "running",
|
|
3442
3373
|
startedAtMs: entry.startedAtMs,
|
|
3443
3374
|
};
|
|
3444
3375
|
}
|
|
3445
3376
|
async _signalProcess(pid, signal) {
|
|
3446
3377
|
const language = this._languageProcesses.get(pid);
|
|
3447
3378
|
if (language) {
|
|
3379
|
+
if (language.exit)
|
|
3380
|
+
return;
|
|
3448
3381
|
language.signal = signal;
|
|
3449
|
-
|
|
3382
|
+
try {
|
|
3383
|
+
await this._signalExecution(language.executionId, signal);
|
|
3384
|
+
}
|
|
3385
|
+
catch (error) {
|
|
3386
|
+
if (!(error instanceof SidecarRejectedError) ||
|
|
3387
|
+
error.detail.code !== "execution_not_running") {
|
|
3388
|
+
throw error;
|
|
3389
|
+
}
|
|
3390
|
+
// The execution can finish between the local state check and the
|
|
3391
|
+
// signal request. Reconcile its final state and preserve kill/signal's
|
|
3392
|
+
// documented already-exited no-op behavior.
|
|
3393
|
+
await this._waitProcess(pid);
|
|
3394
|
+
}
|
|
3450
3395
|
return;
|
|
3451
3396
|
}
|
|
3452
3397
|
const entry = this._processes.get(pid);
|
|
@@ -3471,24 +3416,21 @@ export class AgentOs {
|
|
|
3471
3416
|
await this._resizeExecutionPty(language.executionId, size);
|
|
3472
3417
|
}
|
|
3473
3418
|
async _readProcessOutput(pid, options = {}) {
|
|
3419
|
+
const { maxEvents } = replayPageLimits(options);
|
|
3474
3420
|
const language = this._languageProcesses.get(pid);
|
|
3475
3421
|
if (language) {
|
|
3476
3422
|
const page = await this._readExecutionOutput(language.executionId, {
|
|
3423
|
+
limit: maxEvents,
|
|
3477
3424
|
cursor: options.after === undefined ? undefined : `1:${options.after + 1}`,
|
|
3478
3425
|
});
|
|
3479
|
-
|
|
3426
|
+
const replay = replayPage(pid, page.events.map((event) => ({
|
|
3480
3427
|
pid,
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
})),
|
|
3488
|
-
nextCursor: page.nextCursor,
|
|
3489
|
-
hasMore: page.hasMore,
|
|
3490
|
-
truncated: page.truncated,
|
|
3491
|
-
};
|
|
3428
|
+
sequence: event.sequence,
|
|
3429
|
+
channel: event.channel,
|
|
3430
|
+
chunk: event.chunk,
|
|
3431
|
+
timestampMs: event.timestampMs,
|
|
3432
|
+
})), options, page.truncated ? (options.after ?? -1) + 1 : undefined, page.hasMore);
|
|
3433
|
+
return { ...replay, exitCode: language.exit?.exitCode ?? null };
|
|
3492
3434
|
}
|
|
3493
3435
|
const entry = this._processes.get(pid);
|
|
3494
3436
|
if (!entry)
|
|
@@ -3496,315 +3438,125 @@ export class AgentOs {
|
|
|
3496
3438
|
if (!entry.retainEvents) {
|
|
3497
3439
|
throw new Error(`Process ${pid} was not spawned with output.retainEvents enabled`);
|
|
3498
3440
|
}
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
return {
|
|
3502
|
-
pid,
|
|
3503
|
-
events,
|
|
3504
|
-
nextCursor: String(events.at(-1)?.sequence ?? after),
|
|
3505
|
-
hasMore: false,
|
|
3506
|
-
truncated: entry.events.length === PROCESS_OUTPUT_EVENT_LIMIT &&
|
|
3507
|
-
after < (entry.events[0]?.sequence ?? 0) - 1,
|
|
3508
|
-
};
|
|
3509
|
-
}
|
|
3510
|
-
async _openSession(input) {
|
|
3511
|
-
const response = await this._sendAcpRequest({
|
|
3512
|
-
tag: "AcpOpenSessionRequest",
|
|
3513
|
-
val: {
|
|
3514
|
-
sessionId: input.sessionId ?? null,
|
|
3515
|
-
agent: input.agent,
|
|
3516
|
-
cwd: input.cwd ?? null,
|
|
3517
|
-
additionalDirectories: input.additionalDirectories === undefined
|
|
3518
|
-
? null
|
|
3519
|
-
: JSON.stringify(input.additionalDirectories),
|
|
3520
|
-
env: input.env === undefined ? null : JSON.stringify(input.env),
|
|
3521
|
-
mcpServers: input.mcpServers === undefined
|
|
3522
|
-
? null
|
|
3523
|
-
: JSON.stringify(input.mcpServers),
|
|
3524
|
-
permissionPolicy: input.permissionPolicy ?? null,
|
|
3525
|
-
skipOsInstructions: input.skipOsInstructions ?? null,
|
|
3526
|
-
additionalInstructions: combineInstructions(input.additionalInstructions, this._hostFunctionReference) ?? null,
|
|
3527
|
-
},
|
|
3528
|
-
});
|
|
3529
|
-
if (response.tag !== "AcpOpenSessionResponse") {
|
|
3530
|
-
throw new Error(`unexpected openSession response: ${response.tag}`);
|
|
3441
|
+
if (!entry.processId) {
|
|
3442
|
+
throw new Error(`Process ${pid} does not expose a sidecar replay identity`);
|
|
3531
3443
|
}
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3444
|
+
const wireLimits = replayWirePageLimits(options);
|
|
3445
|
+
const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, {
|
|
3446
|
+
type: "read_process_output",
|
|
3447
|
+
process_id: entry.processId,
|
|
3448
|
+
...(options.after === undefined ? {} : { after: options.after }),
|
|
3449
|
+
max_events: wireLimits.maxEvents,
|
|
3450
|
+
max_bytes: wireLimits.maxBytes,
|
|
3537
3451
|
});
|
|
3538
|
-
if (response.
|
|
3539
|
-
throw new Error(`unexpected
|
|
3452
|
+
if (response.type !== "process_output_page") {
|
|
3453
|
+
throw new Error(`unexpected readProcessOutput response: ${response.type}`);
|
|
3540
3454
|
}
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
});
|
|
3548
|
-
if (response.tag !== "AcpListDurableSessionsResponse") {
|
|
3549
|
-
throw new Error(`unexpected listSessions response: ${response.tag}`);
|
|
3455
|
+
const exitCode = response.response.exitCode;
|
|
3456
|
+
if (exitCode !== null && this._processes.get(pid) === entry) {
|
|
3457
|
+
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
3458
|
+
this.#kernel.reconcileReplayExit(entry.processId, exitCode);
|
|
3459
|
+
}
|
|
3460
|
+
this._recordProcessExit(pid, exitCode);
|
|
3550
3461
|
}
|
|
3551
3462
|
return {
|
|
3552
|
-
|
|
3553
|
-
|
|
3463
|
+
pid,
|
|
3464
|
+
exitCode,
|
|
3465
|
+
events: response.response.events.map((event) => ({
|
|
3466
|
+
pid,
|
|
3467
|
+
sequence: safeProtocolNumber(event.sequence, "process output sequence"),
|
|
3468
|
+
channel: event.channel.toLowerCase(),
|
|
3469
|
+
chunk: new Uint8Array(event.chunk),
|
|
3470
|
+
timestampMs: safeProtocolNumber(event.timestampMs, "process output timestamp"),
|
|
3471
|
+
})),
|
|
3472
|
+
nextCursor: response.response.nextCursor === null
|
|
3473
|
+
? null
|
|
3474
|
+
: safeProtocolNumber(response.response.nextCursor, "process output cursor"),
|
|
3475
|
+
hasMore: response.response.hasMore,
|
|
3476
|
+
truncated: response.response.truncated,
|
|
3554
3477
|
};
|
|
3555
3478
|
}
|
|
3556
|
-
async
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
val: { sessionId: input.sessionId ?? null },
|
|
3560
|
-
});
|
|
3561
|
-
if (response.tag !== "AcpDeleteSessionResponse") {
|
|
3562
|
-
throw new Error(`unexpected deleteSession response: ${response.tag}`);
|
|
3479
|
+
async _withVmMutation(operation) {
|
|
3480
|
+
if (this._vmMutationActive) {
|
|
3481
|
+
throw new Error("invalid_state: another VM mount or software mutation is already in progress; wait for it to finish before retrying");
|
|
3563
3482
|
}
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
tag: "AcpUnloadSessionRequest",
|
|
3568
|
-
val: { sessionId: input?.sessionId ?? null },
|
|
3569
|
-
});
|
|
3570
|
-
if (response.tag !== "AcpUnloadSessionResponse") {
|
|
3571
|
-
throw new Error(`unexpected unloadSession response: ${response.tag}`);
|
|
3483
|
+
this._vmMutationActive = true;
|
|
3484
|
+
try {
|
|
3485
|
+
return await operation();
|
|
3572
3486
|
}
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
const response = await this._sendAcpRequest({
|
|
3576
|
-
tag: "AcpPromptRequest",
|
|
3577
|
-
val: {
|
|
3578
|
-
sessionId: input.sessionId ?? null,
|
|
3579
|
-
idempotencyKey: input.idempotencyKey ?? null,
|
|
3580
|
-
content: JSON.stringify(input.content),
|
|
3581
|
-
},
|
|
3582
|
-
});
|
|
3583
|
-
if (response.tag !== "AcpPromptResponse") {
|
|
3584
|
-
throw new Error(`unexpected prompt response: ${response.tag}`);
|
|
3487
|
+
finally {
|
|
3488
|
+
this._vmMutationActive = false;
|
|
3585
3489
|
}
|
|
3586
|
-
return {
|
|
3587
|
-
sessionId: response.val.sessionId,
|
|
3588
|
-
message: response.val.message === null ? null : JSON.parse(response.val.message),
|
|
3589
|
-
stopReason: response.val.stopReason,
|
|
3590
|
-
};
|
|
3591
3490
|
}
|
|
3592
|
-
async
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
return { status: response.val.status };
|
|
3605
|
-
}
|
|
3606
|
-
async _respondPermission(input) {
|
|
3607
|
-
const response = await this._sendAcpRequest({
|
|
3608
|
-
tag: "AcpRespondPermissionRequest",
|
|
3609
|
-
val: {
|
|
3610
|
-
sessionId: input.sessionId,
|
|
3611
|
-
requestId: input.requestId,
|
|
3612
|
-
optionId: input.optionId,
|
|
3613
|
-
},
|
|
3614
|
-
});
|
|
3615
|
-
if (response.tag !== "AcpRespondPermissionResponse") {
|
|
3616
|
-
throw new Error(`unexpected respondPermission response: ${response.tag}`);
|
|
3617
|
-
}
|
|
3618
|
-
if (response.val.status !== "accepted" &&
|
|
3619
|
-
response.val.status !== "not_pending") {
|
|
3620
|
-
throw new Error(`invalid respondPermission status: ${response.val.status}`);
|
|
3621
|
-
}
|
|
3622
|
-
if (response.val.status === "accepted") {
|
|
3623
|
-
if (response.val.reason !== null) {
|
|
3624
|
-
throw new Error("accepted permission response must not include a reason");
|
|
3491
|
+
async _linkSoftware(descriptor) {
|
|
3492
|
+
return this._withVmMutation(async () => {
|
|
3493
|
+
// Forward to the sidecar, which owns the `/opt/agentos` projection and
|
|
3494
|
+
// appends the package to its live host-backed staging dir; the commands
|
|
3495
|
+
// appear under `/opt/agentos/bin` immediately. The sidecar rejects a
|
|
3496
|
+
// duplicate command, surfaced here as a thrown error.
|
|
3497
|
+
const commands = await this._sidecarClient.linkPackage(this._sidecarSession, this._sidecarVm, descriptor);
|
|
3498
|
+
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
3499
|
+
this.#kernel.registerCommandGuestPaths(new Map(commands.projectedCommands.map((command) => [
|
|
3500
|
+
command.name,
|
|
3501
|
+
command.guestPath,
|
|
3502
|
+
])));
|
|
3625
3503
|
}
|
|
3626
|
-
return { status: "accepted" };
|
|
3627
|
-
}
|
|
3628
|
-
return {
|
|
3629
|
-
status: "not_pending",
|
|
3630
|
-
reason: permissionTerminalReason(response.val.reason),
|
|
3631
|
-
};
|
|
3632
|
-
}
|
|
3633
|
-
async _readHistory(input) {
|
|
3634
|
-
const response = await this._sendAcpRequest({
|
|
3635
|
-
tag: "AcpReadHistoryRequest",
|
|
3636
|
-
val: {
|
|
3637
|
-
sessionId: input?.sessionId ?? null,
|
|
3638
|
-
before: input?.before === undefined ? null : BigInt(input.before),
|
|
3639
|
-
after: input?.after === undefined ? null : BigInt(input.after),
|
|
3640
|
-
limit: input?.limit ?? null,
|
|
3641
|
-
},
|
|
3642
3504
|
});
|
|
3643
|
-
if (response.tag !== "AcpHistoryPageResponse") {
|
|
3644
|
-
throw new Error(`unexpected readHistory response: ${response.tag}`);
|
|
3645
|
-
}
|
|
3646
|
-
return {
|
|
3647
|
-
events: response.val.events.map(decodeDurableSessionEvent),
|
|
3648
|
-
hasMoreBefore: response.val.hasMoreBefore,
|
|
3649
|
-
hasMoreAfter: response.val.hasMoreAfter,
|
|
3650
|
-
};
|
|
3651
|
-
}
|
|
3652
|
-
async _getSessionConfig(input) {
|
|
3653
|
-
const response = await this._sendAcpRequest({
|
|
3654
|
-
tag: "AcpGetSessionConfigRequest",
|
|
3655
|
-
val: { sessionId: input?.sessionId ?? null },
|
|
3656
|
-
});
|
|
3657
|
-
if (response.tag !== "AcpSessionConfigResponse") {
|
|
3658
|
-
throw new Error(`unexpected getSessionConfig response: ${response.tag}`);
|
|
3659
|
-
}
|
|
3660
|
-
return {
|
|
3661
|
-
revision: safeWireU64(response.val.revision),
|
|
3662
|
-
options: JSON.parse(response.val.options),
|
|
3663
|
-
};
|
|
3664
|
-
}
|
|
3665
|
-
async _setSessionConfigOption(input) {
|
|
3666
|
-
const response = await this._sendAcpRequest({
|
|
3667
|
-
tag: "AcpSetSessionConfigOptionRequest",
|
|
3668
|
-
val: {
|
|
3669
|
-
sessionId: input.sessionId ?? null,
|
|
3670
|
-
configId: input.configId,
|
|
3671
|
-
value: JSON.stringify(input.value),
|
|
3672
|
-
},
|
|
3673
|
-
});
|
|
3674
|
-
if (response.tag !== "AcpSessionConfigResponse") {
|
|
3675
|
-
throw new Error(`unexpected setSessionConfigOption response: ${response.tag}`);
|
|
3676
|
-
}
|
|
3677
|
-
return {
|
|
3678
|
-
revision: safeWireU64(response.val.revision),
|
|
3679
|
-
options: JSON.parse(response.val.options),
|
|
3680
|
-
};
|
|
3681
3505
|
}
|
|
3682
|
-
async
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3506
|
+
async _installSoftware(source) {
|
|
3507
|
+
return this._withVmMutation(async () => {
|
|
3508
|
+
const wireSource = source.type === "url"
|
|
3509
|
+
? {
|
|
3510
|
+
type: "url",
|
|
3511
|
+
url: source.url,
|
|
3512
|
+
expected_digest: source.expectedDigest,
|
|
3513
|
+
}
|
|
3514
|
+
: {
|
|
3515
|
+
type: "path",
|
|
3516
|
+
path: source.path,
|
|
3517
|
+
expected_digest: source.expectedDigest,
|
|
3518
|
+
};
|
|
3519
|
+
const result = await this._sidecarClient.installPackage(this._sidecarSession, this._sidecarVm, { source: wireSource });
|
|
3520
|
+
const packageInfo = result.package;
|
|
3521
|
+
const installed = {
|
|
3522
|
+
packageId: packageInfo.package_id,
|
|
3523
|
+
digest: packageInfo.digest,
|
|
3524
|
+
sizeBytes: packageInfo.size,
|
|
3525
|
+
packageName: packageInfo.package_name,
|
|
3526
|
+
version: packageInfo.version,
|
|
3527
|
+
commands: [...packageInfo.commands],
|
|
3528
|
+
};
|
|
3529
|
+
this._installedSoftware.set(installed.packageId, installed);
|
|
3530
|
+
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
3531
|
+
this.#kernel.registerCommandGuestPaths(new Map(result.projected_commands.map((command) => [
|
|
3532
|
+
command.name,
|
|
3533
|
+
command.guest_path,
|
|
3534
|
+
])));
|
|
3535
|
+
}
|
|
3536
|
+
return { ...installed, commands: [...installed.commands] };
|
|
3686
3537
|
});
|
|
3687
|
-
if (response.tag !== "AcpSessionCapabilitiesResponse") {
|
|
3688
|
-
throw new Error(`unexpected getSessionCapabilities response: ${response.tag}`);
|
|
3689
|
-
}
|
|
3690
|
-
return response.val.capabilities === null
|
|
3691
|
-
? null
|
|
3692
|
-
: normalizeSessionCapabilities(JSON.parse(response.val.capabilities));
|
|
3693
3538
|
}
|
|
3694
|
-
async
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3539
|
+
async _uninstallSoftware(packageId) {
|
|
3540
|
+
return this._withVmMutation(async () => {
|
|
3541
|
+
const installed = this._installedSoftware.get(packageId);
|
|
3542
|
+
if (!installed)
|
|
3543
|
+
throw new Error(`Software not found: ${packageId}`);
|
|
3544
|
+
const removedCommands = await this._sidecarClient.unlinkPackage(this._sidecarSession, this._sidecarVm, packageId);
|
|
3545
|
+
this._installedSoftware.delete(packageId);
|
|
3546
|
+
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
3547
|
+
this.#kernel.unregisterCommandGuestPaths(removedCommands);
|
|
3548
|
+
}
|
|
3549
|
+
return { ...installed, commands: [...installed.commands] };
|
|
3698
3550
|
});
|
|
3699
|
-
if (response.tag !== "AcpSessionAgentInfoResponse") {
|
|
3700
|
-
throw new Error(`unexpected getSessionAgentInfo response: ${response.tag}`);
|
|
3701
|
-
}
|
|
3702
|
-
return response.val.agentInfo === null
|
|
3703
|
-
? null
|
|
3704
|
-
: JSON.parse(response.val.agentInfo);
|
|
3705
|
-
}
|
|
3706
|
-
/** @deprecated Use `sessions.open()`. */
|
|
3707
|
-
openSession(input) {
|
|
3708
|
-
return this.sessions.open(input);
|
|
3709
|
-
}
|
|
3710
|
-
/** @deprecated Use `sessions.get()`. */
|
|
3711
|
-
getSession(input) {
|
|
3712
|
-
return this.sessions.get(input);
|
|
3713
|
-
}
|
|
3714
|
-
/** @deprecated Use `sessions.list()`. */
|
|
3715
|
-
listSessions(input) {
|
|
3716
|
-
return this.sessions.list(input);
|
|
3717
|
-
}
|
|
3718
|
-
/** @deprecated Use `sessions.delete()`. */
|
|
3719
|
-
deleteSession(input = {}) {
|
|
3720
|
-
return this.sessions.delete(input);
|
|
3721
|
-
}
|
|
3722
|
-
/** @deprecated Use `sessions.unload()`. */
|
|
3723
|
-
unloadSession(input) {
|
|
3724
|
-
return this.sessions.unload(input);
|
|
3725
|
-
}
|
|
3726
|
-
/** @deprecated Use `sessions.prompt()`. */
|
|
3727
|
-
prompt(input) {
|
|
3728
|
-
return this.sessions.prompt(input);
|
|
3729
3551
|
}
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
/** @deprecated Use `sessions.respondPermission()`. */
|
|
3735
|
-
respondPermission(input) {
|
|
3736
|
-
return this.sessions.respondPermission(input);
|
|
3737
|
-
}
|
|
3738
|
-
/** @deprecated Use `sessions.readHistory()`. */
|
|
3739
|
-
readHistory(input) {
|
|
3740
|
-
return this.sessions.readHistory(input);
|
|
3741
|
-
}
|
|
3742
|
-
/** @deprecated Use `sessions.getConfig()`. */
|
|
3743
|
-
getSessionConfig(input) {
|
|
3744
|
-
return this.sessions.getConfig(input);
|
|
3745
|
-
}
|
|
3746
|
-
/** @deprecated Use `sessions.setConfigOption()`. */
|
|
3747
|
-
setSessionConfigOption(input) {
|
|
3748
|
-
return this.sessions.setConfigOption(input);
|
|
3749
|
-
}
|
|
3750
|
-
/** @deprecated Use `sessions.getCapabilities()`. */
|
|
3751
|
-
getSessionCapabilities(input) {
|
|
3752
|
-
return this.sessions.getCapabilities(input);
|
|
3753
|
-
}
|
|
3754
|
-
/** @deprecated Use `sessions.getAgentInfo()`. */
|
|
3755
|
-
getSessionAgentInfo(input) {
|
|
3756
|
-
return this.sessions.getAgentInfo(input);
|
|
3757
|
-
}
|
|
3758
|
-
/**
|
|
3759
|
-
* Dynamically link a software package into the RUNNING VM. The package's
|
|
3760
|
-
* `bin/` commands appear under `/opt/agentos/bin` (on `$PATH`) and its `share/man`
|
|
3761
|
-
* pages under MANPATH immediately — the `/opt/agentos` mount is host-backed, so
|
|
3762
|
-
* writing into its staging dir is reflected live with no reboot. An `agent`
|
|
3763
|
-
* block registers the package for `openSession({ agent: name })`. Persists for the VM's
|
|
3764
|
-
* lifetime (and across a snapshot iff the volume persists).
|
|
3765
|
-
*/
|
|
3766
|
-
async _linkSoftware(descriptor) {
|
|
3767
|
-
// Forward to the sidecar, which owns the `/opt/agentos` projection and
|
|
3768
|
-
// appends the package to its live host-backed staging dir; the commands
|
|
3769
|
-
// appear under `/opt/agentos/bin` immediately. The sidecar rejects a
|
|
3770
|
-
// duplicate command, surfaced here as a thrown error.
|
|
3771
|
-
const commands = await this._sidecarClient.linkPackage(this._sidecarSession, this._sidecarVm, descriptor);
|
|
3772
|
-
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
3773
|
-
this.#kernel.registerCommandGuestPaths(new Map(commands.projectedCommands.map((command) => [
|
|
3774
|
-
command.name,
|
|
3775
|
-
command.guestPath,
|
|
3776
|
-
])));
|
|
3777
|
-
// Retain the linked package for runtime mount reconfigures:
|
|
3778
|
-
// `configure_vm` is replace-on-write, so a later `mountFs` that
|
|
3779
|
-
// resent only the boot packages would unproject this one.
|
|
3780
|
-
this.#kernel.registerLinkedPackage(descriptor);
|
|
3781
|
-
}
|
|
3782
|
-
// The client parses no manifests: an `agent` block in the linked package is
|
|
3783
|
-
// picked up by the sidecar (it owns the projected `/opt/agentos` and answers
|
|
3784
|
-
// openSession/listAgents from it). Nothing to record client-side.
|
|
3552
|
+
_installedSoftwareList() {
|
|
3553
|
+
return [...this._installedSoftware.values()]
|
|
3554
|
+
.sort((left, right) => left.packageId.localeCompare(right.packageId))
|
|
3555
|
+
.map((software) => ({ ...software, commands: [...software.commands] }));
|
|
3785
3556
|
}
|
|
3786
3557
|
async _listSoftware() {
|
|
3787
3558
|
return this._sidecarClient.providedCommands(this._sidecarSession, this._sidecarVm);
|
|
3788
3559
|
}
|
|
3789
|
-
/**
|
|
3790
|
-
* Returns all registered agents with their installation status. Thin forwarder:
|
|
3791
|
-
* sends `AcpListAgentsRequest` and maps the response. The sidecar enumerates the
|
|
3792
|
-
* projected `/opt/agentos` packages (the client parses no manifests). Every such
|
|
3793
|
-
* agent is a package materialized into the VM, so `installed` is always `true`.
|
|
3794
|
-
*/
|
|
3795
|
-
async _listAgents() {
|
|
3796
|
-
const response = await this._sendAcpRequest({
|
|
3797
|
-
tag: "AcpListAgentsRequest",
|
|
3798
|
-
val: { reserved: false },
|
|
3799
|
-
});
|
|
3800
|
-
if (response.tag !== "AcpListAgentsResponse") {
|
|
3801
|
-
throw new Error(`unexpected list_agents response: ${response.tag}`);
|
|
3802
|
-
}
|
|
3803
|
-
return response.val.agents.map((agent) => ({
|
|
3804
|
-
id: agent.id,
|
|
3805
|
-
installed: agent.installed,
|
|
3806
|
-
}));
|
|
3807
|
-
}
|
|
3808
3560
|
/** @deprecated Use `software.link()`. */
|
|
3809
3561
|
linkSoftware(descriptor) {
|
|
3810
3562
|
return this.software.link(descriptor);
|
|
@@ -3813,63 +3565,14 @@ export class AgentOs {
|
|
|
3813
3565
|
listSoftware() {
|
|
3814
3566
|
return this.software.list();
|
|
3815
3567
|
}
|
|
3816
|
-
/** @deprecated Use `agents.list()`. */
|
|
3817
|
-
listAgents() {
|
|
3818
|
-
return this.agents.list();
|
|
3819
|
-
}
|
|
3820
|
-
_recordAgentStderr(event) {
|
|
3821
|
-
if (!event.sessionId) {
|
|
3822
|
-
return;
|
|
3823
|
-
}
|
|
3824
|
-
const handler = this._agentStderrHandler;
|
|
3825
|
-
if (!handler) {
|
|
3826
|
-
return;
|
|
3827
|
-
}
|
|
3828
|
-
try {
|
|
3829
|
-
handler({
|
|
3830
|
-
sessionId: event.sessionId,
|
|
3831
|
-
agentType: event.agentType,
|
|
3832
|
-
processId: event.processId,
|
|
3833
|
-
pid: null,
|
|
3834
|
-
chunk: new Uint8Array(event.chunk),
|
|
3835
|
-
});
|
|
3836
|
-
}
|
|
3837
|
-
catch (error) {
|
|
3838
|
-
console.error("AgentOS stderr handler failed", error);
|
|
3839
|
-
}
|
|
3840
|
-
}
|
|
3841
|
-
_recordAgentExit(event) {
|
|
3842
|
-
const publicEvent = {
|
|
3843
|
-
sessionId: event.sessionId,
|
|
3844
|
-
agentType: event.agentType,
|
|
3845
|
-
processId: event.processId,
|
|
3846
|
-
pid: event.pid,
|
|
3847
|
-
exitCode: event.exitCode,
|
|
3848
|
-
restart: event.restart,
|
|
3849
|
-
restartCount: event.restartCount,
|
|
3850
|
-
maxRestarts: event.maxRestarts,
|
|
3851
|
-
};
|
|
3852
|
-
const handler = this._agentExitHandler;
|
|
3853
|
-
if (handler) {
|
|
3854
|
-
try {
|
|
3855
|
-
handler(publicEvent);
|
|
3856
|
-
}
|
|
3857
|
-
catch (error) {
|
|
3858
|
-
console.error("AgentOS agent-exit handler failed", error);
|
|
3859
|
-
}
|
|
3860
|
-
}
|
|
3861
|
-
for (const key of ["*", event.sessionId]) {
|
|
3862
|
-
for (const subscription of this._agentExitHandlers.get(key) ?? []) {
|
|
3863
|
-
try {
|
|
3864
|
-
subscription(publicEvent);
|
|
3865
|
-
}
|
|
3866
|
-
catch (error) {
|
|
3867
|
-
console.error("AgentOS agent-exit subscription failed", error);
|
|
3868
|
-
}
|
|
3869
|
-
}
|
|
3870
|
-
}
|
|
3871
|
-
}
|
|
3872
3568
|
_handleSidecarEvent(event) {
|
|
3569
|
+
// onEvent is process-wide: execution IDs are only unique inside one VM.
|
|
3570
|
+
// Keep sibling VMs' output/completion and warnings on their own handles.
|
|
3571
|
+
if (event.ownership.scope === "vm" &&
|
|
3572
|
+
(event.ownership.connection_id !== this._sidecarSession.connectionId ||
|
|
3573
|
+
event.ownership.session_id !== this._sidecarSession.sessionId ||
|
|
3574
|
+
event.ownership.vm_id !== this._sidecarVm.vmId))
|
|
3575
|
+
return;
|
|
3873
3576
|
if (event.payload.type === "execution_output") {
|
|
3874
3577
|
const output = mapExecutionOutputEvent(event.payload.event);
|
|
3875
3578
|
const pid = this._languageProcessIds.get(output.executionId);
|
|
@@ -3880,6 +3583,8 @@ export class AgentOs {
|
|
|
3880
3583
|
pid,
|
|
3881
3584
|
stream: output.channel === "stderr" ? "stderr" : "stdout",
|
|
3882
3585
|
data: output.chunk,
|
|
3586
|
+
sequence: output.sequence,
|
|
3587
|
+
timestampMs: output.timestampMs,
|
|
3883
3588
|
};
|
|
3884
3589
|
for (const handler of process.outputHandlers) {
|
|
3885
3590
|
try {
|
|
@@ -3945,10 +3650,6 @@ export class AgentOs {
|
|
|
3945
3650
|
}
|
|
3946
3651
|
return;
|
|
3947
3652
|
}
|
|
3948
|
-
if (event.payload.type === "ext") {
|
|
3949
|
-
this._handleAcpExtEvent(event.payload.envelope);
|
|
3950
|
-
return;
|
|
3951
|
-
}
|
|
3952
3653
|
if (event.payload.type !== "structured") {
|
|
3953
3654
|
return;
|
|
3954
3655
|
}
|
|
@@ -3977,71 +3678,6 @@ export class AgentOs {
|
|
|
3977
3678
|
console.error("AgentOS limit-warning handler failed", error);
|
|
3978
3679
|
}
|
|
3979
3680
|
}
|
|
3980
|
-
_handleAcpExtEvent(envelope) {
|
|
3981
|
-
if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
|
|
3982
|
-
return;
|
|
3983
|
-
}
|
|
3984
|
-
try {
|
|
3985
|
-
const event = decodeAcpEvent(envelope.payload);
|
|
3986
|
-
switch (event.tag) {
|
|
3987
|
-
case "AcpDurableSessionEvent": {
|
|
3988
|
-
this._emitDurableSessionEvent(decodeDurableSessionEvent(event.val));
|
|
3989
|
-
return;
|
|
3990
|
-
}
|
|
3991
|
-
case "AcpEphemeralSessionUpdateEvent": {
|
|
3992
|
-
const update = JSON.parse(event.val.update);
|
|
3993
|
-
const { sessionUpdate: type, ...payload } = update;
|
|
3994
|
-
this._emitDurableSessionEvent({
|
|
3995
|
-
durability: "ephemeral",
|
|
3996
|
-
type,
|
|
3997
|
-
sessionId: event.val.sessionId,
|
|
3998
|
-
afterSequence: safeWireU64(event.val.afterSequence),
|
|
3999
|
-
...payload,
|
|
4000
|
-
});
|
|
4001
|
-
return;
|
|
4002
|
-
}
|
|
4003
|
-
case "AcpSessionEvent":
|
|
4004
|
-
return;
|
|
4005
|
-
case "AcpAgentStderrEvent": {
|
|
4006
|
-
this._recordAgentStderr(event.val);
|
|
4007
|
-
return;
|
|
4008
|
-
}
|
|
4009
|
-
case "AcpAgentExitedEvent": {
|
|
4010
|
-
this._recordAgentExit(event.val);
|
|
4011
|
-
return;
|
|
4012
|
-
}
|
|
4013
|
-
}
|
|
4014
|
-
}
|
|
4015
|
-
catch (error) {
|
|
4016
|
-
console.error("AgentOS failed to decode an ACP sidecar event", error);
|
|
4017
|
-
}
|
|
4018
|
-
}
|
|
4019
|
-
_emitDurableSessionEvent(entry) {
|
|
4020
|
-
for (const handler of this._durableSessionEventHandlers.get(entry.sessionId) ?? []) {
|
|
4021
|
-
try {
|
|
4022
|
-
handler(entry);
|
|
4023
|
-
}
|
|
4024
|
-
catch (error) {
|
|
4025
|
-
console.error("AgentOS session event handler failed", error);
|
|
4026
|
-
}
|
|
4027
|
-
}
|
|
4028
|
-
}
|
|
4029
|
-
async _sendAcpRequest(request) {
|
|
4030
|
-
const envelope = await this._sidecarClient.extensionRequest(this._sidecarSession, this._sidecarVm, {
|
|
4031
|
-
namespace: ACP_EXTENSION_NAMESPACE,
|
|
4032
|
-
payload: encodeAcpRequest(request),
|
|
4033
|
-
});
|
|
4034
|
-
if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
|
|
4035
|
-
throw new Error(`unexpected ACP Ext namespace: ${envelope.namespace}`);
|
|
4036
|
-
}
|
|
4037
|
-
const response = decodeAcpResponse(envelope.payload);
|
|
4038
|
-
if (response.tag === "AcpErrorResponse") {
|
|
4039
|
-
const error = new Error(response.val.message);
|
|
4040
|
-
error.code = response.val.code;
|
|
4041
|
-
throw error;
|
|
4042
|
-
}
|
|
4043
|
-
return response;
|
|
4044
|
-
}
|
|
4045
3681
|
_installSidecarRequestHandler() {
|
|
4046
3682
|
const context = {
|
|
4047
3683
|
hostFunctions: this._hostFunctions,
|
|
@@ -4057,386 +3693,15 @@ export class AgentOs {
|
|
|
4057
3693
|
filesystem: this.#kernel.vfs,
|
|
4058
3694
|
});
|
|
4059
3695
|
case "ext":
|
|
4060
|
-
return
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
type: "ext_result",
|
|
4068
|
-
envelope: {
|
|
4069
|
-
namespace: envelope.namespace,
|
|
4070
|
-
payload: Buffer.from("unknown extension namespace", "utf8"),
|
|
4071
|
-
},
|
|
4072
|
-
};
|
|
4073
|
-
}
|
|
4074
|
-
const callback = decodeAcpCallback(envelope.payload);
|
|
4075
|
-
switch (callback.tag) {
|
|
4076
|
-
case "AcpHostRequestCallback": {
|
|
4077
|
-
const response = await this._dispatchAcpSidecarRequest(toJsonRpcRequest(JSON.parse(callback.val.request)));
|
|
4078
|
-
return {
|
|
4079
|
-
type: "ext_result",
|
|
4080
|
-
envelope: {
|
|
4081
|
-
namespace: ACP_EXTENSION_NAMESPACE,
|
|
4082
|
-
payload: encodeAcpCallbackResponse({
|
|
4083
|
-
tag: "AcpHostRequestCallbackResponse",
|
|
4084
|
-
val: {
|
|
4085
|
-
response: JSON.stringify(response),
|
|
4086
|
-
},
|
|
4087
|
-
}),
|
|
4088
|
-
},
|
|
4089
|
-
};
|
|
4090
|
-
}
|
|
4091
|
-
}
|
|
4092
|
-
}
|
|
4093
|
-
async _dispatchAcpSidecarRequest(request) {
|
|
4094
|
-
try {
|
|
4095
|
-
const result = await this._handleSupportedAcpSidecarRequest(request);
|
|
4096
|
-
return {
|
|
4097
|
-
jsonrpc: "2.0",
|
|
4098
|
-
id: request.id,
|
|
4099
|
-
result,
|
|
4100
|
-
};
|
|
4101
|
-
}
|
|
4102
|
-
catch (error) {
|
|
4103
|
-
if (error instanceof AcpDispatchError) {
|
|
4104
|
-
return {
|
|
4105
|
-
jsonrpc: "2.0",
|
|
4106
|
-
id: request.id,
|
|
4107
|
-
error: {
|
|
4108
|
-
code: error.code,
|
|
4109
|
-
message: error.message,
|
|
4110
|
-
...(error.data ? { data: error.data } : {}),
|
|
4111
|
-
},
|
|
4112
|
-
};
|
|
4113
|
-
}
|
|
4114
|
-
return {
|
|
4115
|
-
jsonrpc: "2.0",
|
|
4116
|
-
id: request.id,
|
|
4117
|
-
error: {
|
|
4118
|
-
code: -32603,
|
|
4119
|
-
message: error instanceof Error ? error.message : String(error),
|
|
4120
|
-
},
|
|
4121
|
-
};
|
|
4122
|
-
}
|
|
4123
|
-
}
|
|
4124
|
-
async _handleSupportedAcpSidecarRequest(request) {
|
|
4125
|
-
const params = this._acpParams(request);
|
|
4126
|
-
switch (request.method) {
|
|
4127
|
-
case "fs/read":
|
|
4128
|
-
case "fs/read_text_file":
|
|
4129
|
-
return this._handleAcpReadFile(params);
|
|
4130
|
-
case "fs/write":
|
|
4131
|
-
case "fs/write_text_file":
|
|
4132
|
-
return this._handleAcpWriteFile(params);
|
|
4133
|
-
case "fs/readDir":
|
|
4134
|
-
case "fs/read_dir":
|
|
4135
|
-
return this._handleAcpReadDir(params);
|
|
4136
|
-
case "terminal/create":
|
|
4137
|
-
return this._handleAcpCreateTerminal(params);
|
|
4138
|
-
case "terminal/write":
|
|
4139
|
-
return this._handleAcpWriteTerminal(params);
|
|
4140
|
-
case "terminal/output":
|
|
4141
|
-
case "terminal/read":
|
|
4142
|
-
return this._handleAcpReadTerminal(params);
|
|
4143
|
-
case "terminal/wait_for_exit":
|
|
4144
|
-
case "terminal/waitForExit":
|
|
4145
|
-
return this._handleAcpWaitForTerminalExit(params);
|
|
4146
|
-
case "terminal/kill":
|
|
4147
|
-
return this._handleAcpKillTerminal(params);
|
|
4148
|
-
case "terminal/release":
|
|
4149
|
-
case "terminal/close":
|
|
4150
|
-
return this._handleAcpReleaseTerminal(params);
|
|
4151
|
-
case "terminal/resize":
|
|
4152
|
-
return this._handleAcpResizeTerminal(params);
|
|
4153
|
-
default:
|
|
4154
|
-
throw new AcpDispatchError(-32601, `Method not found: ${request.method}`, {
|
|
4155
|
-
method: request.method,
|
|
4156
|
-
});
|
|
4157
|
-
}
|
|
4158
|
-
}
|
|
4159
|
-
_acpParams(request) {
|
|
4160
|
-
if (!request.params) {
|
|
4161
|
-
return {};
|
|
4162
|
-
}
|
|
4163
|
-
if (typeof request.params !== "object" ||
|
|
4164
|
-
request.params === null ||
|
|
4165
|
-
Array.isArray(request.params)) {
|
|
4166
|
-
throw new AcpDispatchError(-32602, `${request.method} requires object params`);
|
|
4167
|
-
}
|
|
4168
|
-
return request.params;
|
|
4169
|
-
}
|
|
4170
|
-
_requireAcpStringParam(params, name, method) {
|
|
4171
|
-
const value = params[name];
|
|
4172
|
-
if (typeof value !== "string") {
|
|
4173
|
-
throw new AcpDispatchError(-32602, `${method} requires a string ${name}`);
|
|
4174
|
-
}
|
|
4175
|
-
return value;
|
|
4176
|
-
}
|
|
4177
|
-
_optionalAcpStringParam(params, name, method) {
|
|
4178
|
-
const value = params[name];
|
|
4179
|
-
if (value === undefined || value === null) {
|
|
4180
|
-
return undefined;
|
|
4181
|
-
}
|
|
4182
|
-
if (typeof value !== "string") {
|
|
4183
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} to be a string when provided`);
|
|
4184
|
-
}
|
|
4185
|
-
return value;
|
|
4186
|
-
}
|
|
4187
|
-
_optionalAcpNumberParam(params, name, method) {
|
|
4188
|
-
const value = params[name];
|
|
4189
|
-
if (value === undefined || value === null) {
|
|
4190
|
-
return undefined;
|
|
4191
|
-
}
|
|
4192
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
4193
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} to be a number when provided`);
|
|
4194
|
-
}
|
|
4195
|
-
return value;
|
|
4196
|
-
}
|
|
4197
|
-
_optionalAcpStringArrayParam(params, name, method) {
|
|
4198
|
-
const value = params[name];
|
|
4199
|
-
if (value === undefined || value === null) {
|
|
4200
|
-
return undefined;
|
|
4201
|
-
}
|
|
4202
|
-
if (!Array.isArray(value) ||
|
|
4203
|
-
value.some((entry) => typeof entry !== "string")) {
|
|
4204
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} to be an array of strings when provided`);
|
|
4205
|
-
}
|
|
4206
|
-
return [...value];
|
|
4207
|
-
}
|
|
4208
|
-
_optionalAcpEnvParam(params, name, method) {
|
|
4209
|
-
const value = params[name];
|
|
4210
|
-
if (value === undefined || value === null) {
|
|
4211
|
-
return undefined;
|
|
4212
|
-
}
|
|
4213
|
-
if (Array.isArray(value)) {
|
|
4214
|
-
const env = {};
|
|
4215
|
-
for (const entry of value) {
|
|
4216
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
4217
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} entries to be { name, value } objects`);
|
|
4218
|
-
}
|
|
4219
|
-
const record = entry;
|
|
4220
|
-
if (typeof record.name !== "string" ||
|
|
4221
|
-
typeof record.value !== "string") {
|
|
4222
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} entries to be { name, value } objects`);
|
|
4223
|
-
}
|
|
4224
|
-
env[record.name] = record.value;
|
|
4225
|
-
}
|
|
4226
|
-
return env;
|
|
4227
|
-
}
|
|
4228
|
-
if (typeof value !== "object") {
|
|
4229
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} to be an object or name/value array`);
|
|
4230
|
-
}
|
|
4231
|
-
const env = {};
|
|
4232
|
-
for (const [key, entryValue] of Object.entries(value)) {
|
|
4233
|
-
if (typeof entryValue !== "string") {
|
|
4234
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} values to be strings`);
|
|
3696
|
+
return {
|
|
3697
|
+
type: "ext_result",
|
|
3698
|
+
envelope: {
|
|
3699
|
+
namespace: request.payload.envelope.namespace,
|
|
3700
|
+
payload: Buffer.from("extension handlers are not configured", "utf8"),
|
|
3701
|
+
},
|
|
3702
|
+
};
|
|
4235
3703
|
}
|
|
4236
|
-
env[key] = entryValue;
|
|
4237
|
-
}
|
|
4238
|
-
return env;
|
|
4239
|
-
}
|
|
4240
|
-
_requireAcpTerminal(params, method) {
|
|
4241
|
-
const terminalId = this._requireAcpStringParam(params, "terminalId", method);
|
|
4242
|
-
const terminal = this._acpTerminals.get(terminalId);
|
|
4243
|
-
if (!terminal) {
|
|
4244
|
-
throw new AcpDispatchError(-32602, `ACP terminal not found: ${terminalId}`);
|
|
4245
|
-
}
|
|
4246
|
-
return terminal;
|
|
4247
|
-
}
|
|
4248
|
-
_appendAcpTerminalOutput(terminal, data) {
|
|
4249
|
-
const chunk = Buffer.from(data).toString("utf8");
|
|
4250
|
-
if (!chunk) {
|
|
4251
|
-
return;
|
|
4252
|
-
}
|
|
4253
|
-
terminal.output += chunk;
|
|
4254
|
-
if (Number.isFinite(terminal.outputByteLimit) &&
|
|
4255
|
-
terminal.outputByteLimit >= 0 &&
|
|
4256
|
-
terminal.output.length > terminal.outputByteLimit) {
|
|
4257
|
-
terminal.output = terminal.output.slice(terminal.output.length - terminal.outputByteLimit);
|
|
4258
|
-
terminal.truncated = true;
|
|
4259
|
-
}
|
|
4260
|
-
}
|
|
4261
|
-
async _handleAcpReadFile(params) {
|
|
4262
|
-
const method = "fs/read";
|
|
4263
|
-
const path = this._requireAcpStringParam(params, "path", method);
|
|
4264
|
-
const line = this._optionalAcpNumberParam(params, "line", method);
|
|
4265
|
-
const limit = this._optionalAcpNumberParam(params, "limit", method);
|
|
4266
|
-
const encoding = this._optionalAcpStringParam(params, "encoding", method);
|
|
4267
|
-
const bytes = await this.readFile(path);
|
|
4268
|
-
if (encoding === "base64") {
|
|
4269
|
-
return { content: Buffer.from(bytes).toString("base64") };
|
|
4270
|
-
}
|
|
4271
|
-
const text = new TextDecoder().decode(bytes);
|
|
4272
|
-
if (line === undefined && limit === undefined) {
|
|
4273
|
-
return { content: text };
|
|
4274
|
-
}
|
|
4275
|
-
const startLine = Math.max(1, Math.trunc(line ?? 1));
|
|
4276
|
-
const lineLimit = limit === undefined
|
|
4277
|
-
? Number.POSITIVE_INFINITY
|
|
4278
|
-
: Math.max(0, Math.trunc(limit));
|
|
4279
|
-
return {
|
|
4280
|
-
content: text
|
|
4281
|
-
.split("\n")
|
|
4282
|
-
.slice(startLine - 1, startLine - 1 + lineLimit)
|
|
4283
|
-
.join("\n"),
|
|
4284
|
-
};
|
|
4285
|
-
}
|
|
4286
|
-
async _handleAcpWriteFile(params) {
|
|
4287
|
-
const method = "fs/write";
|
|
4288
|
-
const path = this._requireAcpStringParam(params, "path", method);
|
|
4289
|
-
const content = this._requireAcpStringParam(params, "content", method);
|
|
4290
|
-
const encoding = this._optionalAcpStringParam(params, "encoding", method);
|
|
4291
|
-
await this.writeFile(path, encoding === "base64" ? Buffer.from(content, "base64") : content);
|
|
4292
|
-
return null;
|
|
4293
|
-
}
|
|
4294
|
-
async _handleAcpReadDir(params) {
|
|
4295
|
-
const method = "fs/readDir";
|
|
4296
|
-
const path = this._requireAcpStringParam(params, "path", method);
|
|
4297
|
-
const entries = await this._vfs().readDirWithTypes(path);
|
|
4298
|
-
return {
|
|
4299
|
-
entries: entries
|
|
4300
|
-
.filter((entry) => entry.name !== "." && entry.name !== "..")
|
|
4301
|
-
.map((entry) => ({
|
|
4302
|
-
name: entry.name,
|
|
4303
|
-
path: path === "/" ? `/${entry.name}` : `${path}/${entry.name}`,
|
|
4304
|
-
type: entry.isSymbolicLink
|
|
4305
|
-
? "symlink"
|
|
4306
|
-
: entry.isDirectory
|
|
4307
|
-
? "directory"
|
|
4308
|
-
: "file",
|
|
4309
|
-
})),
|
|
4310
|
-
};
|
|
4311
|
-
}
|
|
4312
|
-
_handleAcpCreateTerminal(params) {
|
|
4313
|
-
const method = "terminal/create";
|
|
4314
|
-
const command = this._requireAcpStringParam(params, "command", method);
|
|
4315
|
-
const args = this._optionalAcpStringArrayParam(params, "args", method);
|
|
4316
|
-
const env = this._optionalAcpEnvParam(params, "env", method);
|
|
4317
|
-
const cwd = this._optionalAcpStringParam(params, "cwd", method);
|
|
4318
|
-
const cols = this._optionalAcpNumberParam(params, "cols", method);
|
|
4319
|
-
const rows = this._optionalAcpNumberParam(params, "rows", method);
|
|
4320
|
-
const outputByteLimit = Math.max(0, Math.trunc(this._optionalAcpNumberParam(params, "outputByteLimit", method) ??
|
|
4321
|
-
1_048_576));
|
|
4322
|
-
const terminalId = `acp-terminal-${++this._acpTerminalCounter}`;
|
|
4323
|
-
const terminal = {
|
|
4324
|
-
handle: this.#kernel.openShell({
|
|
4325
|
-
command,
|
|
4326
|
-
...(args ? { args } : {}),
|
|
4327
|
-
...(env ? { env } : {}),
|
|
4328
|
-
...(cwd ? { cwd } : {}),
|
|
4329
|
-
...(cols !== undefined ? { cols: Math.trunc(cols) } : {}),
|
|
4330
|
-
...(rows !== undefined ? { rows: Math.trunc(rows) } : {}),
|
|
4331
|
-
}),
|
|
4332
|
-
output: "",
|
|
4333
|
-
truncated: false,
|
|
4334
|
-
outputByteLimit,
|
|
4335
|
-
exitCode: null,
|
|
4336
|
-
waitPromise: Promise.resolve(0),
|
|
4337
|
-
};
|
|
4338
|
-
terminal.handle.onData = (data) => {
|
|
4339
|
-
this._appendAcpTerminalOutput(terminal, data);
|
|
4340
|
-
};
|
|
4341
|
-
terminal.waitPromise = terminal.handle.wait().then((exitCode) => {
|
|
4342
|
-
terminal.exitCode = exitCode;
|
|
4343
|
-
return exitCode;
|
|
4344
3704
|
});
|
|
4345
|
-
this._acpTerminals.set(terminalId, terminal);
|
|
4346
|
-
return { terminalId };
|
|
4347
|
-
}
|
|
4348
|
-
async _handleAcpWriteTerminal(params) {
|
|
4349
|
-
const method = "terminal/write";
|
|
4350
|
-
const terminal = this._requireAcpTerminal(params, method);
|
|
4351
|
-
const data = this._requireAcpStringParam(params, "data", method);
|
|
4352
|
-
const encoding = this._optionalAcpStringParam(params, "encoding", method);
|
|
4353
|
-
await terminal.handle.write(encoding === "base64" ? Buffer.from(data, "base64") : data);
|
|
4354
|
-
return null;
|
|
4355
|
-
}
|
|
4356
|
-
_handleAcpReadTerminal(params) {
|
|
4357
|
-
const terminal = this._requireAcpTerminal(params, "terminal/output");
|
|
4358
|
-
return {
|
|
4359
|
-
output: terminal.output,
|
|
4360
|
-
truncated: terminal.truncated,
|
|
4361
|
-
...(terminal.exitCode !== null
|
|
4362
|
-
? {
|
|
4363
|
-
exitStatus: {
|
|
4364
|
-
exitCode: terminal.exitCode,
|
|
4365
|
-
signal: null,
|
|
4366
|
-
},
|
|
4367
|
-
}
|
|
4368
|
-
: {}),
|
|
4369
|
-
};
|
|
4370
|
-
}
|
|
4371
|
-
async _handleAcpWaitForTerminalExit(params) {
|
|
4372
|
-
const terminal = this._requireAcpTerminal(params, "terminal/wait_for_exit");
|
|
4373
|
-
const exitCode = await terminal.waitPromise;
|
|
4374
|
-
return { exitCode, signal: null };
|
|
4375
|
-
}
|
|
4376
|
-
_handleAcpKillTerminal(params) {
|
|
4377
|
-
const method = "terminal/kill";
|
|
4378
|
-
const terminal = this._requireAcpTerminal(params, method);
|
|
4379
|
-
const signal = this._optionalAcpNumberParam(params, "signal", method) ?? 15;
|
|
4380
|
-
terminal.handle.kill(Math.trunc(signal));
|
|
4381
|
-
return null;
|
|
4382
|
-
}
|
|
4383
|
-
_handleAcpReleaseTerminal(params) {
|
|
4384
|
-
const method = "terminal/release";
|
|
4385
|
-
const terminalId = this._requireAcpStringParam(params, "terminalId", method);
|
|
4386
|
-
const terminal = this._acpTerminals.get(terminalId);
|
|
4387
|
-
if (!terminal) {
|
|
4388
|
-
throw new AcpDispatchError(-32602, `ACP terminal not found: ${terminalId}`);
|
|
4389
|
-
}
|
|
4390
|
-
if (terminal.exitCode === null) {
|
|
4391
|
-
terminal.handle.kill();
|
|
4392
|
-
}
|
|
4393
|
-
this._acpTerminals.delete(terminalId);
|
|
4394
|
-
return null;
|
|
4395
|
-
}
|
|
4396
|
-
_handleAcpResizeTerminal(params) {
|
|
4397
|
-
const method = "terminal/resize";
|
|
4398
|
-
const terminal = this._requireAcpTerminal(params, method);
|
|
4399
|
-
const cols = this._optionalAcpNumberParam(params, "cols", method);
|
|
4400
|
-
const rows = this._optionalAcpNumberParam(params, "rows", method);
|
|
4401
|
-
if (cols === undefined || rows === undefined) {
|
|
4402
|
-
throw new AcpDispatchError(-32602, `${method} requires numeric cols and rows`);
|
|
4403
|
-
}
|
|
4404
|
-
terminal.handle.resize(Math.trunc(cols), Math.trunc(rows));
|
|
4405
|
-
return null;
|
|
4406
|
-
}
|
|
4407
|
-
onSessionEvent(sessionIdOrHandler, maybeHandler) {
|
|
4408
|
-
const sessionId = typeof sessionIdOrHandler === "string" ? sessionIdOrHandler : "main";
|
|
4409
|
-
const handler = typeof sessionIdOrHandler === "function"
|
|
4410
|
-
? sessionIdOrHandler
|
|
4411
|
-
: maybeHandler;
|
|
4412
|
-
if (!handler) {
|
|
4413
|
-
throw new TypeError("onSessionEvent requires a handler");
|
|
4414
|
-
}
|
|
4415
|
-
const handlers = this._durableSessionEventHandlers.get(sessionId) ?? new Set();
|
|
4416
|
-
handlers.add(handler);
|
|
4417
|
-
this._durableSessionEventHandlers.set(sessionId, handlers);
|
|
4418
|
-
return () => {
|
|
4419
|
-
handlers.delete(handler);
|
|
4420
|
-
if (handlers.size === 0) {
|
|
4421
|
-
this._durableSessionEventHandlers.delete(sessionId);
|
|
4422
|
-
}
|
|
4423
|
-
};
|
|
4424
|
-
}
|
|
4425
|
-
onAgentExit(sessionIdOrHandler, maybeHandler) {
|
|
4426
|
-
const sessionId = typeof sessionIdOrHandler === "string" ? sessionIdOrHandler : "*";
|
|
4427
|
-
const handler = typeof sessionIdOrHandler === "function"
|
|
4428
|
-
? sessionIdOrHandler
|
|
4429
|
-
: maybeHandler;
|
|
4430
|
-
if (!handler)
|
|
4431
|
-
throw new TypeError("onAgentExit requires a handler");
|
|
4432
|
-
const handlers = this._agentExitHandlers.get(sessionId) ?? new Set();
|
|
4433
|
-
handlers.add(handler);
|
|
4434
|
-
this._agentExitHandlers.set(sessionId, handlers);
|
|
4435
|
-
return () => {
|
|
4436
|
-
handlers.delete(handler);
|
|
4437
|
-
if (handlers.size === 0)
|
|
4438
|
-
this._agentExitHandlers.delete(sessionId);
|
|
4439
|
-
};
|
|
4440
3705
|
}
|
|
4441
3706
|
// ── Cron ────────────────────────────────────────────────────
|
|
4442
3707
|
/** Schedule a cron job. Returns a handle with the job ID and a cancel method. */
|
|
@@ -4469,29 +3734,38 @@ export class AgentOs {
|
|
|
4469
3734
|
}
|
|
4470
3735
|
async dispose() {
|
|
4471
3736
|
this._cronManager.dispose();
|
|
4472
|
-
|
|
3737
|
+
const errors = [];
|
|
3738
|
+
for (const entry of this._shells.values()) {
|
|
4473
3739
|
entry.handle.kill();
|
|
4474
3740
|
}
|
|
4475
3741
|
const shellExitPromises = [...this._pendingShellExitPromises];
|
|
3742
|
+
const runningLanguageProcesses = [
|
|
3743
|
+
...this._languageProcesses.entries(),
|
|
3744
|
+
].filter(([, process]) => !process.exit);
|
|
3745
|
+
const languageExitPromises = runningLanguageProcesses.map(([, process]) => new Promise((resolve) => {
|
|
3746
|
+
const handleExit = () => {
|
|
3747
|
+
process.exitHandlers.delete(handleExit);
|
|
3748
|
+
resolve();
|
|
3749
|
+
};
|
|
3750
|
+
process.exitHandlers.add(handleExit);
|
|
3751
|
+
}));
|
|
3752
|
+
const languageKillResults = await Promise.allSettled(runningLanguageProcesses.map(([pid]) => this._killProcess(pid)));
|
|
3753
|
+
errors.push(...languageKillResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
|
|
3754
|
+
await waitForTrackedExitPromises([...shellExitPromises, ...languageExitPromises], VM_DISPOSE_DRAIN_TIMEOUT_MS);
|
|
4476
3755
|
this._shells.clear();
|
|
4477
|
-
const terminalExitPromises = [];
|
|
4478
|
-
for (const terminal of this._acpTerminals.values()) {
|
|
4479
|
-
terminal.handle.kill();
|
|
4480
|
-
terminalExitPromises.push(terminal.waitPromise.then(() => undefined, () => undefined));
|
|
4481
|
-
}
|
|
4482
|
-
this._acpTerminals.clear();
|
|
4483
3756
|
this._processes.clear();
|
|
3757
|
+
this._languageProcesses.clear();
|
|
3758
|
+
this._languageProcessIds.clear();
|
|
4484
3759
|
this._executionOutputHandlers.clear();
|
|
4485
3760
|
this._executionCompletedHandlers.clear();
|
|
4486
|
-
await waitForTrackedExitPromises([...shellExitPromises, ...terminalExitPromises], SHELL_DISPOSE_TIMEOUT_MS);
|
|
4487
3761
|
this._disposeSidecarEventListener();
|
|
3762
|
+
this._disposeSidecarCloseListener();
|
|
4488
3763
|
const sidecarLease = this._sidecarLease;
|
|
4489
3764
|
this._sidecarLease = null;
|
|
4490
3765
|
const disposeVm = sidecarLease
|
|
4491
3766
|
? sidecarLease.dispose()
|
|
4492
3767
|
: this.#kernel.dispose();
|
|
4493
3768
|
const hooks = this._disposeHooks.splice(0);
|
|
4494
|
-
const errors = [];
|
|
4495
3769
|
try {
|
|
4496
3770
|
await disposeVm;
|
|
4497
3771
|
}
|