Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions crates/execution/assets/runners/wasm-runner.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4155,7 +4155,13 @@ const hostProcessImport = {
},
sleep_ms(milliseconds) {
try {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Number(milliseconds) >>> 0);
const waitArray = new Int32Array(new SharedArrayBuffer(4));
const deadline = Date.now() + (Number(milliseconds) >>> 0);
while (Date.now() < deadline) {
// Keep guest sleeps interruptible by V8 termination during SIGTERM,
// SIGKILL, and VM disposal.
Atomics.wait(waitArray, 0, 0, Math.max(1, Math.min(10, deadline - Date.now())));
}
return WASI_ERRNO_SUCCESS;
} catch {
return WASI_ERRNO_FAULT;
Expand Down Expand Up @@ -5728,7 +5734,9 @@ wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => {
});
}

if (!hasSyntheticSubscription && !hasRemappedPassthroughSubscription) {
const hasClockSubscription = subscriptions.some((subscription) => subscription.kind === 'clock');

if (!hasSyntheticSubscription && !hasRemappedPassthroughSubscription && !hasClockSubscription) {
return delegateManagedPollOneoff
? delegateManagedPollOneoff(inPtr, outPtr, nsubscriptions, neventsPtr)
: WASI_ERRNO_BADF;
Expand Down Expand Up @@ -5895,7 +5903,7 @@ wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => {
);
}

if (readyEvents.length === 0 && subscriptions.some((subscription) => subscription.kind === 'clock')) {
if (readyEvents.length === 0 && hasClockSubscription) {
const clockSubscription = subscriptions.find((subscription) => subscription.kind === 'clock');
readyEvents.push({
userdata: clockSubscription.userdata,
Expand Down
2 changes: 1 addition & 1 deletion crates/execution/src/node_import_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const NODE_IMPORT_CACHE_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_PATH";
const NODE_IMPORT_CACHE_LOADER_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_LOADER_PATH";
const NODE_IMPORT_CACHE_SCHEMA_VERSION: &str = "1";
const NODE_IMPORT_CACHE_LOADER_VERSION: &str = "8";
const NODE_IMPORT_CACHE_ASSET_VERSION: &str = "85";
const NODE_IMPORT_CACHE_ASSET_VERSION: &str = "86";
const NODE_IMPORT_CACHE_DIR_PREFIX: &str = "agentos-node-import-cache";
const DEFAULT_NODE_IMPORT_CACHE_MATERIALIZE_TIMEOUT: Duration = Duration::from_secs(30);
const PYODIDE_DIST_DIR: &str = "pyodide-dist";
Expand Down
9 changes: 6 additions & 3 deletions docs-internal/registry-parity-worklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,17 @@ so a reader sees the whole board at a glance.
un-skipped after the parent host-shadow pre-spawn sync fix in item 1.
- **rev:** `lonnzuqw` — `test(registry): mark stdin redirection parity proven`

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

### 4. VFS missing `pwrite` — sqlite3 file-backed DBs don't persist
- **Broken:** `filesystem method pwrite is unavailable` — sqlite3 file-backed DB
Expand Down
6 changes: 5 additions & 1 deletion packages/runtime-browser/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3431,7 +3431,11 @@ export const POLYFILL_CODE_MAP: Record<string, string> = {
proc_getppid(retPid) { return writeU32(retPid, 0); },
proc_kill() { return errnoNosys; },
sleep_ms(milliseconds) {
Atomics.wait(wait, 0, 0, milliseconds >>> 0);
const deadline = Date.now() + (milliseconds >>> 0);
while (Date.now() < deadline) {
// Keep guest sleeps interruptible by V8 termination during kill/dispose.
Atomics.wait(wait, 0, 0, Math.max(1, Math.min(10, deadline - Date.now())));
}
return errnoSuccess;
},
pty_open() { return errnoNosys; },
Expand Down
55 changes: 46 additions & 9 deletions packages/runtime-core/src/kernel-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ const PREFERRED_SIGNAL_NAMES = [
"SIGEMT",
"SIGINFO",
] as const;
const NON_TERMINATING_SIGNALS = new Set([
"0",
"SIGCHLD",
"SIGCONT",
"SIGSTOP",
"SIGURG",
"SIGWINCH",
]);
const NON_CANONICAL_SIGNAL_NAMES = new Set([
"SIGCLD",
"SIGIOT",
Expand Down Expand Up @@ -418,7 +426,10 @@ export class NativeSidecarKernelProxy {
liveProcesses.map((entry) => this.signalProcess(entry, 15)),
);

await this.client.disposeVm(this.session, this.vm).catch(() => {});
await Promise.race([
this.client.disposeVm(this.session, this.vm),
new Promise<void>((resolve) => setTimeout(resolve, 1000)),
]).catch(() => {});
for (const entry of liveProcesses) {
if (entry.exitCode === null) {
// The sidecar dispose path already performs TERM/KILL escalation for any
Expand Down Expand Up @@ -679,13 +690,19 @@ export class NativeSidecarKernelProxy {
}
entry.pendingKillSignal = signal;
void entry.startPromise.then(async () => {
if (entry.exitCode !== null || entry.pendingKillSignal === null) {
if (entry.pendingKillSignal === null) {
return;
}
const pendingSignal = entry.pendingKillSignal;
entry.pendingKillSignal = null;
await this.signalProcess(entry, pendingSignal);
});
if (
(signal === 9 || signal === 15) &&
entry.exitCode === null
) {
this.finishProcess(entry, 128 + signal);
}
},
wait: async () => {
const exitCode = await this.waitForTrackedProcess(entry);
Expand Down Expand Up @@ -1683,14 +1700,34 @@ export class NativeSidecarKernelProxy {
entry: TrackedProcessEntry,
signal: number,
): Promise<void> {
await this.signalRefreshes.get(entry.pid);
const sidecarSignal = toSidecarSignalName(signal);
let timedOut = false;
const killPromise = this.client.killProcess(
this.session,
this.vm,
entry.processId,
sidecarSignal,
);
try {
await this.client.killProcess(
this.session,
this.vm,
entry.processId,
toSidecarSignalName(signal),
);
await Promise.race([
killPromise,
new Promise<void>((resolve) =>
setTimeout(() => {
timedOut = true;
resolve();
}, 1000),
),
]);
if (timedOut) {
void killPromise.catch(() => {});
}
if (
entry.exitCode === null &&
!NON_TERMINATING_SIGNALS.has(sidecarSignal) &&
(entry.driver === "wasmvm" || timedOut)
) {
this.finishProcess(entry, 128 + signal);
}
} catch (error) {
if (isNoSuchProcessError(error) || isUnknownVmError(error)) {
return;
Expand Down
6 changes: 4 additions & 2 deletions packages/runtime-core/src/sidecar-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ export interface SidecarSpawnOptions {
command?: string;
args?: string[];
eventBufferCapacity?: number;
gracefulExitMs?: number;
forceExitMs?: number;
// Migration-only compatibility path for pre-BARE test fixtures.
payloadCodec?: NativeTransportPayloadCodec;
/**
Expand Down Expand Up @@ -368,8 +370,8 @@ export class SidecarProcess {
silenceTimeoutMs: options.silenceTimeoutMs,
eventBufferCapacity:
options.eventBufferCapacity ?? DEFAULT_SIDECAR_EVENT_BUFFER_CAPACITY,
gracefulExitMs: DEFAULT_SIDECAR_GRACEFUL_EXIT_MS,
forceExitMs: DEFAULT_SIDECAR_FORCE_EXIT_MS,
gracefulExitMs: options.gracefulExitMs ?? DEFAULT_SIDECAR_GRACEFUL_EXIT_MS,
forceExitMs: options.forceExitMs ?? DEFAULT_SIDECAR_FORCE_EXIT_MS,
disposedErrorMessage: "native sidecar disposed",
payloadCodec: options.payloadCodec ?? "bare",
});
Expand Down
2 changes: 2 additions & 0 deletions packages/runtime-core/src/test-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3072,6 +3072,8 @@ class NativeKernel implements Kernel {
cwd: REPO_ROOT,
command: ensureNativeSidecarBinary(),
args: [],
gracefulExitMs: 100,
forceExitMs: 100,
}),
);
const session = await this.measureBoot("session_open", () =>
Expand Down