@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.
Files changed (47) hide show
  1. package/README.md +9 -9
  2. package/dist/agent-os.d.ts +71 -198
  3. package/dist/agent-os.js +480 -1206
  4. package/dist/agentos-package.d.ts +15 -4
  5. package/dist/agentos-package.js +1 -22
  6. package/dist/cron/cron-manager.js +0 -27
  7. package/dist/cron/types.d.ts +0 -12
  8. package/dist/default-software/coreutils.aospkg +4 -0
  9. package/dist/default-software/default-software.json +10 -0
  10. package/dist/default-software/diffutils.aospkg +0 -0
  11. package/dist/default-software/findutils.aospkg +0 -0
  12. package/dist/default-software/gawk.aospkg +0 -0
  13. package/dist/default-software/grep.aospkg +0 -0
  14. package/dist/default-software/gzip.aospkg +0 -0
  15. package/dist/default-software/sed.aospkg +0 -0
  16. package/dist/default-software/tar.aospkg +0 -0
  17. package/dist/default-software.d.ts +6 -5
  18. package/dist/default-software.js +17 -6
  19. package/dist/generated/VmLimitsConfig.d.ts +0 -2
  20. package/dist/host-dir-mount.d.ts +1 -1
  21. package/dist/host-dir-mount.js +1 -1
  22. package/dist/language-execution.d.ts +6 -2
  23. package/dist/options-schema.d.ts +10 -48
  24. package/dist/options-schema.js +10 -47
  25. package/dist/output-replay.d.ts +18 -0
  26. package/dist/output-replay.js +75 -0
  27. package/dist/packages.d.ts +0 -9
  28. package/dist/packages.js +0 -39
  29. package/dist/process-forest.d.ts +7 -0
  30. package/dist/process-forest.js +24 -0
  31. package/dist/runtime-compat.d.ts +13 -0
  32. package/dist/runtime-compat.js +3 -9
  33. package/dist/sidecar/binary.d.ts +1 -1
  34. package/dist/sidecar/binary.js +4 -4
  35. package/dist/sidecar/native-process-client.d.ts +1 -1
  36. package/dist/sidecar/rpc-client.d.ts +12 -18
  37. package/dist/sidecar/rpc-client.js +385 -188
  38. package/dist/types.d.ts +3 -11
  39. package/package.json +9 -45
  40. package/dist/generated/AcpLimitsConfig.d.ts +0 -20
  41. package/dist/generated/AcpLimitsConfig.js +0 -2
  42. package/dist/json-rpc.d.ts +0 -50
  43. package/dist/json-rpc.js +0 -22
  44. package/dist/session-api.d.ts +0 -180
  45. package/dist/session-api.js +0 -1
  46. package/dist/sidecar/agentos-protocol.d.ts +0 -719
  47. 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 ACP_PROTOCOL_VERSION = 1;
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 { decodeAcpCallback, decodeAcpEvent, decodeAcpResponse, encodeAcpCallbackResponse, encodeAcpRequest, } from "./sidecar/agentos-protocol.js";
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
- const command = parseHostCommandCallbackInput(payload.input);
948
- if (command) {
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 checks the `hostFunction` permission scope before it forwards a
973
- // host function call, so a call that reaches the host is already permitted.
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
- _agentStderrHandler;
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
- agents = {
1719
- list: this._listAgents.bind(this),
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, agentStderrHandler, agentExitHandler, limitWarningHandler) {
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(callerOptions) {
1771
- // The generic exists only so each `execute` infers its input from its own
1772
- // `inputSchema`. Past this point the concrete schemas carry no meaning, so
1773
- // the rest of `create()` works with the plain option type.
1774
- let options = parseAgentOsOptions(callerOptions);
1775
- // Default software is FULLY DYNAMIC: this package's own NON-agent
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 NO package manifests: the
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 = getBaseEnvironment();
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 sidecarPermissions = serializePermissionsForSidecar(options?.permissions);
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
- env,
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
- bootstrapCommands,
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: sidecarPermissions,
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: sidecarPermissions,
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?.onAgentStderr ?? defaultAgentStderrHandler, options?.onAgentExit ?? defaultAgentExitHandler, options?.onLimitWarning);
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
- let resolveCompletion;
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
- const admitted = (await this._executionOperation(buildPayload(internalOptions, executionId), internalOptions, true));
2174
- const descriptor = {
2175
- pid: admitted.pid,
2176
- state: "running",
2177
- language,
2178
- startedAtMs: admitted.createdAtMs,
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
- options.signal.addEventListener("abort", abort, { once: true });
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
- let resolveCompletion;
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
- events: [],
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.wait().then((code) => {
2713
- const exit = {
2714
- pid: proc.pid,
2715
- outcome: entry.signal ? "signalled" : "exited",
2716
- exitCode: code,
2717
- ...(entry.signal ? { signal: entry.signal } : {}),
2718
- };
2719
- entry.exit = exit;
2720
- for (const h of exitHandlers)
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
- const recordOutput = (channel, data) => {
2734
- const entry = this._processes.get(proc.pid);
2735
- if (!entry?.retainEvents)
2736
- return;
2737
- if (entry.events.length >= PROCESS_OUTPUT_EVENT_LIMIT) {
2738
- entry.events.shift();
2739
- }
2740
- entry.events.push({
2741
- pid: proc.pid,
2742
- sequence: entry.nextSequence++,
2743
- channel,
2744
- chunk: data,
2745
- timestampMs: Date.now(),
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
- const proc = this.#kernel.spawn(command, args, {
2749
- cwd: options.cwd,
2750
- env: options.env,
2751
- stdin: options.stdin,
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._assertSafeAbsolutePath(descriptor.path);
3013
- if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
3014
- throw new Error("portable dynamic mounts require the native sidecar");
3015
- }
3016
- await this.#kernel.mountDescriptor({
3017
- guestPath: descriptor.path,
3018
- readOnly: descriptor.readOnly ?? false,
3019
- plugin: {
3020
- id: descriptor.plugin.id,
3021
- config: descriptor.plugin.config ?? {},
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._assertSafeAbsolutePath(path);
3027
- if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
3028
- throw new Error("portable dynamic mounts require the native sidecar");
3029
- }
3030
- await this.#kernel.unmountDescriptor(path);
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. agentOS Apps uses this compatibility
3147
- * surface for its direct actor API.
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 === null ? "running" : "exited",
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 all = this._listAllProcesses();
3396
- const nodeMap = new Map();
3397
- // Index: create a tree node for each process
3398
- for (const proc of all) {
3399
- nodeMap.set(proc.pid, {
3400
- pid: proc.pid,
3401
- ppid: proc.ppid,
3402
- command: proc.command,
3403
- state: proc.status,
3404
- startedAtMs: proc.startTime,
3405
- children: [],
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
- if (!nodeMap.has(process.descriptor.pid)) {
3410
- nodeMap.set(process.descriptor.pid, {
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 === null ? "running" : "exited",
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
- await this._signalExecution(language.executionId, signal);
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
- return {
3426
+ const replay = replayPage(pid, page.events.map((event) => ({
3480
3427
  pid,
3481
- events: page.events.map((event) => ({
3482
- pid,
3483
- sequence: event.sequence,
3484
- channel: event.channel,
3485
- chunk: event.chunk,
3486
- timestampMs: event.timestampMs,
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
- const after = options.after ?? -1;
3500
- const events = entry.events.filter((event) => event.sequence > after);
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
- async _getSession(input) {
3534
- const response = await this._sendAcpRequest({
3535
- tag: "AcpGetDurableSessionRequest",
3536
- val: { sessionId: input?.sessionId ?? null },
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.tag !== "AcpGetDurableSessionResponse") {
3539
- throw new Error(`unexpected getSession response: ${response.tag}`);
3452
+ if (response.type !== "process_output_page") {
3453
+ throw new Error(`unexpected readProcessOutput response: ${response.type}`);
3540
3454
  }
3541
- return decodeDurableSessionInfo(response.val.session);
3542
- }
3543
- async _listSessions(input) {
3544
- const response = await this._sendAcpRequest({
3545
- tag: "AcpListDurableSessionsRequest",
3546
- val: { cursor: input?.cursor ?? null, limit: input?.limit ?? null },
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
- sessions: response.val.sessions.map(decodeDurableSessionInfo),
3553
- nextCursor: response.val.nextCursor,
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 _deleteSession(input = {}) {
3557
- const response = await this._sendAcpRequest({
3558
- tag: "AcpDeleteSessionRequest",
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
- async _unloadSession(input) {
3566
- const response = await this._sendAcpRequest({
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
- async _prompt(input) {
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 _cancelPrompt(input) {
3593
- const response = await this._sendAcpRequest({
3594
- tag: "AcpCancelPromptRequest",
3595
- val: { sessionId: input?.sessionId ?? null },
3596
- });
3597
- if (response.tag !== "AcpCancelPromptResponse") {
3598
- throw new Error(`unexpected cancelPrompt response: ${response.tag}`);
3599
- }
3600
- if (response.val.status !== "cancelled" &&
3601
- response.val.status !== "no_active_prompt") {
3602
- throw new Error(`invalid cancelPrompt status: ${response.val.status}`);
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 _getSessionCapabilities(input) {
3683
- const response = await this._sendAcpRequest({
3684
- tag: "AcpGetSessionCapabilitiesRequest",
3685
- val: { sessionId: input?.sessionId ?? null },
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 _getSessionAgentInfo(input) {
3695
- const response = await this._sendAcpRequest({
3696
- tag: "AcpGetSessionAgentInfoRequest",
3697
- val: { sessionId: input?.sessionId ?? null },
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
- /** @deprecated Use `sessions.cancelPrompt()`. */
3731
- cancelPrompt(input) {
3732
- return this.sessions.cancelPrompt(input);
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 this._handleAcpExtSidecarRequest(request.payload.envelope);
4061
- }
4062
- });
4063
- }
4064
- async _handleAcpExtSidecarRequest(envelope) {
4065
- if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
4066
- return {
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
- for (const [id, entry] of this._shells) {
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
  }