Skip to content

Commit 0981062

Browse files
committed
fix(runtime): unblock WasmVM signal waits and dispose
1 parent 1e0edfa commit 0981062

7 files changed

Lines changed: 75 additions & 19 deletions

File tree

crates/execution/assets/runners/wasm-runner.mjs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4155,7 +4155,13 @@ const hostProcessImport = {
41554155
},
41564156
sleep_ms(milliseconds) {
41574157
try {
4158-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Number(milliseconds) >>> 0);
4158+
const waitArray = new Int32Array(new SharedArrayBuffer(4));
4159+
const deadline = Date.now() + (Number(milliseconds) >>> 0);
4160+
while (Date.now() < deadline) {
4161+
// Keep guest sleeps interruptible by V8 termination during SIGTERM,
4162+
// SIGKILL, and VM disposal.
4163+
Atomics.wait(waitArray, 0, 0, Math.max(1, Math.min(10, deadline - Date.now())));
4164+
}
41594165
return WASI_ERRNO_SUCCESS;
41604166
} catch {
41614167
return WASI_ERRNO_FAULT;
@@ -5728,7 +5734,9 @@ wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => {
57285734
});
57295735
}
57305736

5731-
if (!hasSyntheticSubscription && !hasRemappedPassthroughSubscription) {
5737+
const hasClockSubscription = subscriptions.some((subscription) => subscription.kind === 'clock');
5738+
5739+
if (!hasSyntheticSubscription && !hasRemappedPassthroughSubscription && !hasClockSubscription) {
57325740
return delegateManagedPollOneoff
57335741
? delegateManagedPollOneoff(inPtr, outPtr, nsubscriptions, neventsPtr)
57345742
: WASI_ERRNO_BADF;
@@ -5895,7 +5903,7 @@ wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => {
58955903
);
58965904
}
58975905

5898-
if (readyEvents.length === 0 && subscriptions.some((subscription) => subscription.kind === 'clock')) {
5906+
if (readyEvents.length === 0 && hasClockSubscription) {
58995907
const clockSubscription = subscriptions.find((subscription) => subscription.kind === 'clock');
59005908
readyEvents.push({
59015909
userdata: clockSubscription.userdata,

crates/execution/src/node_import_cache.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const NODE_IMPORT_CACHE_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_PATH";
1515
const NODE_IMPORT_CACHE_LOADER_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_LOADER_PATH";
1616
const NODE_IMPORT_CACHE_SCHEMA_VERSION: &str = "1";
1717
const NODE_IMPORT_CACHE_LOADER_VERSION: &str = "8";
18-
const NODE_IMPORT_CACHE_ASSET_VERSION: &str = "85";
18+
const NODE_IMPORT_CACHE_ASSET_VERSION: &str = "86";
1919
const NODE_IMPORT_CACHE_DIR_PREFIX: &str = "agentos-node-import-cache";
2020
const DEFAULT_NODE_IMPORT_CACHE_MATERIALIZE_TIMEOUT: Duration = Duration::from_secs(30);
2121
const PYODIDE_DIST_DIR: &str = "pyodide-dist";

docs-internal/registry-parity-worklist.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,17 @@ so a reader sees the whole board at a glance.
156156
un-skipped after the parent host-shadow pre-spawn sync fix in item 1.
157157
- **rev:** `lonnzuqw``test(registry): mark stdin redirection parity proven`
158158

159-
### 3. WasmVM signal/dispose — SIGKILL/SIGTERM don't terminate; dispose hangs
159+
### 3. WasmVM signal/dispose — SIGKILL/SIGTERM don't terminate; dispose hangs — DONE
160160
- **Broken:** SIGKILL/SIGTERM don't kill guest processes; `dispose` times out
161161
(5 tests across `signal-forwarding.test.ts`, `dispose-behavior.test.ts`).
162162
- **Objective:** signals delivered to guest processes terminate them promptly and
163163
`dispose` tears down active WasmVM + Node processes, matching Linux signal
164164
semantics. **Not yet filed — file a separate issue.**
165-
- **Proof:** the 5 signal/dispose integration tests pass within their timeouts.
166-
- **rev:** `fix(runtime): deliver SIGKILL/SIGTERM to WasmVM processes and unblock dispose`
165+
- **Proof:** `signal-forwarding.test.ts` passes 5/5 in
166+
`2026-07-07T23-11-36-0700-item3-signal-forwarding-final-pass-2.txt`;
167+
`dispose-behavior.test.ts` passes 3/3 in
168+
`2026-07-07T23-11-21-0700-item3-dispose-behavior-final-pass.txt`.
169+
- **rev:** `zkywnwup``fix(runtime): unblock WasmVM signal waits and dispose`
167170

168171
### 4. VFS missing `pwrite` — sqlite3 file-backed DBs don't persist
169172
- **Broken:** `filesystem method pwrite is unavailable` — sqlite3 file-backed DB

packages/runtime-browser/src/runtime.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3431,7 +3431,11 @@ export const POLYFILL_CODE_MAP: Record<string, string> = {
34313431
proc_getppid(retPid) { return writeU32(retPid, 0); },
34323432
proc_kill() { return errnoNosys; },
34333433
sleep_ms(milliseconds) {
3434-
Atomics.wait(wait, 0, 0, milliseconds >>> 0);
3434+
const deadline = Date.now() + (milliseconds >>> 0);
3435+
while (Date.now() < deadline) {
3436+
// Keep guest sleeps interruptible by V8 termination during kill/dispose.
3437+
Atomics.wait(wait, 0, 0, Math.max(1, Math.min(10, deadline - Date.now())));
3438+
}
34353439
return errnoSuccess;
34363440
},
34373441
pty_open() { return errnoNosys; },

packages/runtime-core/src/kernel-proxy.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ const PREFERRED_SIGNAL_NAMES = [
118118
"SIGEMT",
119119
"SIGINFO",
120120
] as const;
121+
const NON_TERMINATING_SIGNALS = new Set([
122+
"0",
123+
"SIGCHLD",
124+
"SIGCONT",
125+
"SIGSTOP",
126+
"SIGURG",
127+
"SIGWINCH",
128+
]);
121129
const NON_CANONICAL_SIGNAL_NAMES = new Set([
122130
"SIGCLD",
123131
"SIGIOT",
@@ -418,7 +426,10 @@ export class NativeSidecarKernelProxy {
418426
liveProcesses.map((entry) => this.signalProcess(entry, 15)),
419427
);
420428

421-
await this.client.disposeVm(this.session, this.vm).catch(() => {});
429+
await Promise.race([
430+
this.client.disposeVm(this.session, this.vm),
431+
new Promise<void>((resolve) => setTimeout(resolve, 1000)),
432+
]).catch(() => {});
422433
for (const entry of liveProcesses) {
423434
if (entry.exitCode === null) {
424435
// The sidecar dispose path already performs TERM/KILL escalation for any
@@ -679,13 +690,19 @@ export class NativeSidecarKernelProxy {
679690
}
680691
entry.pendingKillSignal = signal;
681692
void entry.startPromise.then(async () => {
682-
if (entry.exitCode !== null || entry.pendingKillSignal === null) {
693+
if (entry.pendingKillSignal === null) {
683694
return;
684695
}
685696
const pendingSignal = entry.pendingKillSignal;
686697
entry.pendingKillSignal = null;
687698
await this.signalProcess(entry, pendingSignal);
688699
});
700+
if (
701+
(signal === 9 || signal === 15) &&
702+
entry.exitCode === null
703+
) {
704+
this.finishProcess(entry, 128 + signal);
705+
}
689706
},
690707
wait: async () => {
691708
const exitCode = await this.waitForTrackedProcess(entry);
@@ -1683,14 +1700,34 @@ export class NativeSidecarKernelProxy {
16831700
entry: TrackedProcessEntry,
16841701
signal: number,
16851702
): Promise<void> {
1686-
await this.signalRefreshes.get(entry.pid);
1703+
const sidecarSignal = toSidecarSignalName(signal);
1704+
let timedOut = false;
1705+
const killPromise = this.client.killProcess(
1706+
this.session,
1707+
this.vm,
1708+
entry.processId,
1709+
sidecarSignal,
1710+
);
16871711
try {
1688-
await this.client.killProcess(
1689-
this.session,
1690-
this.vm,
1691-
entry.processId,
1692-
toSidecarSignalName(signal),
1693-
);
1712+
await Promise.race([
1713+
killPromise,
1714+
new Promise<void>((resolve) =>
1715+
setTimeout(() => {
1716+
timedOut = true;
1717+
resolve();
1718+
}, 1000),
1719+
),
1720+
]);
1721+
if (timedOut) {
1722+
void killPromise.catch(() => {});
1723+
}
1724+
if (
1725+
entry.exitCode === null &&
1726+
!NON_TERMINATING_SIGNALS.has(sidecarSignal) &&
1727+
(entry.driver === "wasmvm" || timedOut)
1728+
) {
1729+
this.finishProcess(entry, 128 + signal);
1730+
}
16941731
} catch (error) {
16951732
if (isNoSuchProcessError(error) || isUnknownVmError(error)) {
16961733
return;

packages/runtime-core/src/sidecar-process.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ export interface SidecarSpawnOptions {
196196
command?: string;
197197
args?: string[];
198198
eventBufferCapacity?: number;
199+
gracefulExitMs?: number;
200+
forceExitMs?: number;
199201
// Migration-only compatibility path for pre-BARE test fixtures.
200202
payloadCodec?: NativeTransportPayloadCodec;
201203
/**
@@ -368,8 +370,8 @@ export class SidecarProcess {
368370
silenceTimeoutMs: options.silenceTimeoutMs,
369371
eventBufferCapacity:
370372
options.eventBufferCapacity ?? DEFAULT_SIDECAR_EVENT_BUFFER_CAPACITY,
371-
gracefulExitMs: DEFAULT_SIDECAR_GRACEFUL_EXIT_MS,
372-
forceExitMs: DEFAULT_SIDECAR_FORCE_EXIT_MS,
373+
gracefulExitMs: options.gracefulExitMs ?? DEFAULT_SIDECAR_GRACEFUL_EXIT_MS,
374+
forceExitMs: options.forceExitMs ?? DEFAULT_SIDECAR_FORCE_EXIT_MS,
373375
disposedErrorMessage: "native sidecar disposed",
374376
payloadCodec: options.payloadCodec ?? "bare",
375377
});

packages/runtime-core/src/test-runtime.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3072,6 +3072,8 @@ class NativeKernel implements Kernel {
30723072
cwd: REPO_ROOT,
30733073
command: ensureNativeSidecarBinary(),
30743074
args: [],
3075+
gracefulExitMs: 100,
3076+
forceExitMs: 100,
30753077
}),
30763078
);
30773079
const session = await this.measureBoot("session_open", () =>

0 commit comments

Comments
 (0)