From a1b1d5eaf185991f5af26be1b4deba2ee407a800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=A2=A8?= Date: Thu, 30 Apr 2026 14:17:16 +0000 Subject: [PATCH] =?UTF-8?q?chore:=20RFC-006=20Phase=204=20cleanup=20?= =?UTF-8?q?=E2=80=94=20delete=20worker-fork-support.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move formatChildExitSummary/formatCapturedStderrTail to worker-runtime.ts - Move ignoreSessionBroadcastSignals to new worker-signals.ts - Delete worker-fork-support.ts (teeCapturedStderr no longer used) - Update .knowledge/worker-isolation.md and architecture.md for WorkerRuntime - All 167 tests pass, biome check clean Closes #283 --- .knowledge/architecture.md | 1 + .knowledge/worker-isolation.md | 10 ++++- packages/daemon/src/sense-worker.ts | 2 +- packages/daemon/src/worker-fork-support.ts | 45 ---------------------- packages/daemon/src/worker-pool.ts | 7 +++- packages/daemon/src/worker-runtime.ts | 18 +++++++++ packages/daemon/src/worker-signals.ts | 17 ++++++++ packages/daemon/src/workflow-manager.ts | 7 +++- packages/daemon/src/workflow-worker.ts | 2 +- 9 files changed, 56 insertions(+), 53 deletions(-) delete mode 100644 packages/daemon/src/worker-fork-support.ts create mode 100644 packages/daemon/src/worker-signals.ts diff --git a/.knowledge/architecture.md b/.knowledge/architecture.md index 16fad6b..517aa9d 100644 --- a/.knowledge/architecture.md +++ b/.knowledge/architecture.md @@ -33,6 +33,7 @@ Senses own both the "what" (compute logic) and the "when" (config-driven schedul - One worker per Workflow type (on-demand) - Workers never talk to each other - All user code runs in isolated Workers; kernel never loads user code directly +- **`WorkerRuntime`** (`packages/daemon/src/worker-runtime.ts`) centralizes fork lifecycle for both sense groups (`worker-pool.ts`) and workflow types (`workflow-manager.ts`); see `.knowledge/worker-isolation.md` ## Storage Systems diff --git a/.knowledge/worker-isolation.md b/.knowledge/worker-isolation.md index f7b26c9..9986b53 100644 --- a/.knowledge/worker-isolation.md +++ b/.knowledge/worker-isolation.md @@ -12,6 +12,12 @@ Kernel (Main Process) └── Workflow Worker (review) ── review workflow instances ``` +### WorkerRuntime (RFC-006) + +Forked worker processes are managed by **`WorkerRuntime`** (`worker-runtime.ts`): one Node child per logical key, cold start, optional respawn after crash, drain/evict, and coordinated shutdown over IPC. **`worker-pool.ts`** (sense groups) and **`workflow-manager.ts`** (workflow types) both configure and delegate to `createWorkerRuntime` instead of owning ad-hoc fork logic. + +Worker **entrypoints** (`sense-worker.ts`, `workflow-worker.ts`) import lightweight helpers only — e.g. `worker-signals.ts` for session broadcast signal handling — so they do not pull in the parent-side runtime module. + ## Isolation Boundaries ### 1. Sense Workers @@ -111,10 +117,10 @@ workflows: ### Process Management #### Signal Handling -Workers ignore session broadcast signals (SIGINT/SIGTERM): +Workers ignore session broadcast signals (SIGINT/SIGTERM) via `ignoreSessionBroadcastSignals()` in `worker-signals.ts`: ```typescript // Workers ignore terminal signals; kernel coordinates shutdown -process.on("SIGINT", () => {}); +process.on("SIGINT", () => {}); process.on("SIGTERM", () => {}); ``` diff --git a/packages/daemon/src/sense-worker.ts b/packages/daemon/src/sense-worker.ts index c917980..8457bc6 100644 --- a/packages/daemon/src/sense-worker.ts +++ b/packages/daemon/src/sense-worker.ts @@ -25,7 +25,7 @@ import type { WorkerToParentMessage } from "./ipc.js"; import { parseParentMessage } from "./ipc.js"; import { executeCompute, loadSenseModule, openSenseDb } from "./sense-runtime.js"; import type { SenseRuntime } from "./sense-runtime.js"; -import { ignoreSessionBroadcastSignals } from "./worker-fork-support.js"; +import { ignoreSessionBroadcastSignals } from "./worker-signals.js"; // --------------------------------------------------------------------------- // IPC helpers diff --git a/packages/daemon/src/worker-fork-support.ts b/packages/daemon/src/worker-fork-support.ts deleted file mode 100644 index 9c5d334..0000000 --- a/packages/daemon/src/worker-fork-support.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { ChildProcess } from "node:child_process"; - -const STDERR_TAIL_MAX_CHARS = 16_384; - -/** - * Forked workers inherit the parent's process group. In foreground `nerve dev`, - * terminal-driven SIGINT/SIGTERM is delivered to the whole group, so workers can exit - * on the default handler before the kernel sends `{ type: "shutdown" }` over IPC. - * Swallow these in worker processes so the parent coordinates shutdown (issue #55). - * Only call when `process.send` is defined (fork IPC); standalone `node …-worker.js` keeps default Ctrl+C behaviour. - */ -export function ignoreSessionBroadcastSignals(): void { - const swallow = (): void => {}; - process.on("SIGINT", swallow); - process.on("SIGTERM", swallow); -} - -export function teeCapturedStderr(child: ChildProcess, tail: { value: string }): void { - const stream = child.stderr; - if (stream === null || stream === undefined) return; - stream.setEncoding("utf8"); - stream.on("data", (chunk: string | Buffer) => { - const text = typeof chunk === "string" ? chunk : chunk.toString("utf8"); - process.stderr.write(text); - tail.value = (tail.value + text).slice(-STDERR_TAIL_MAX_CHARS); - }); -} - -export function formatChildExitSummary(code: number | null, signal: NodeJS.Signals | null): string { - const codeStr = code === null || code === undefined ? "null" : String(code); - if (signal) { - return `code=${codeStr} signal=${signal}`; - } - return `code=${codeStr}`; -} - -export function formatCapturedStderrTail(tail: string, maxChars = 800): string { - const trimmed = tail.trim(); - if (trimmed.length === 0) return ""; - const normalized = trimmed.replace(/\r?\n/g, "\\n"); - if (normalized.length <= maxChars) { - return ` worker_stderr=${normalized}`; - } - return ` worker_stderr=…${normalized.slice(-maxChars)}`; -} diff --git a/packages/daemon/src/worker-pool.ts b/packages/daemon/src/worker-pool.ts index 67d68c8..9769b1e 100644 --- a/packages/daemon/src/worker-pool.ts +++ b/packages/daemon/src/worker-pool.ts @@ -6,8 +6,11 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import type { ComputeMessage } from "./ipc.js"; -import { formatCapturedStderrTail, formatChildExitSummary } from "./worker-fork-support.js"; -import { createWorkerRuntime } from "./worker-runtime.js"; +import { + createWorkerRuntime, + formatCapturedStderrTail, + formatChildExitSummary, +} from "./worker-runtime.js"; export function resolveWorkerScript(): string { const __filename = fileURLToPath(import.meta.url); diff --git a/packages/daemon/src/worker-runtime.ts b/packages/daemon/src/worker-runtime.ts index aaaff47..88c94b9 100644 --- a/packages/daemon/src/worker-runtime.ts +++ b/packages/daemon/src/worker-runtime.ts @@ -8,6 +8,24 @@ import { isPlainRecord } from "@uncaged/nerve-core"; const STDERR_TAIL_MAX_CHARS = 2048; +export function formatChildExitSummary(code: number | null, signal: NodeJS.Signals | null): string { + const codeStr = code === null || code === undefined ? "null" : String(code); + if (signal) { + return `code=${codeStr} signal=${signal}`; + } + return `code=${codeStr}`; +} + +export function formatCapturedStderrTail(tail: string, maxChars = 800): string { + const trimmed = tail.trim(); + if (trimmed.length === 0) return ""; + const normalized = trimmed.replace(/\r?\n/g, "\\n"); + if (normalized.length <= maxChars) { + return ` worker_stderr=${normalized}`; + } + return ` worker_stderr=…${normalized.slice(-maxChars)}`; +} + export type WorkerDrainOpts = { shutdownTimeoutMs: number | null; }; diff --git a/packages/daemon/src/worker-signals.ts b/packages/daemon/src/worker-signals.ts new file mode 100644 index 0000000..b9b20ef --- /dev/null +++ b/packages/daemon/src/worker-signals.ts @@ -0,0 +1,17 @@ +/** + * Worker-process signal handling (fork IPC children only). + * Worker entrypoints import this module — not worker-runtime.ts (parent/kernel code). + */ + +/** + * Forked workers inherit the parent's process group. In foreground `nerve dev`, + * terminal-driven SIGINT/SIGTERM is delivered to the whole group, so workers can exit + * on the default handler before the kernel sends `{ type: "shutdown" }` over IPC. + * Swallow these in worker processes so the parent coordinates shutdown (issue #55). + * Only call when `process.send` is defined (fork IPC); standalone `node …-worker.js` keeps default Ctrl+C behaviour. + */ +export function ignoreSessionBroadcastSignals(): void { + const swallow = (): void => {}; + process.on("SIGINT", swallow); + process.on("SIGTERM", swallow); +} diff --git a/packages/daemon/src/workflow-manager.ts b/packages/daemon/src/workflow-manager.ts index d6c23a6..14e4eaa 100644 --- a/packages/daemon/src/workflow-manager.ts +++ b/packages/daemon/src/workflow-manager.ts @@ -11,8 +11,11 @@ import type { NerveConfig, WorkflowConfig, WorkflowStatus } from "@uncaged/nerve import type { LogStore } from "@uncaged/nerve-store"; import type { KillThreadMessage, StartThreadMessage, ThreadEventMessage } from "./ipc.js"; import { parseWorkerMessage } from "./ipc.js"; -import { formatCapturedStderrTail, formatChildExitSummary } from "./worker-fork-support.js"; -import { createWorkerRuntime } from "./worker-runtime.js"; +import { + createWorkerRuntime, + formatCapturedStderrTail, + formatChildExitSummary, +} from "./worker-runtime.js"; import { DEFAULT_MAX_QUEUE, WORKER_SHUTDOWN_TIMEOUT_MS, diff --git a/packages/daemon/src/workflow-worker.ts b/packages/daemon/src/workflow-worker.ts index c017a4f..29c36e5 100644 --- a/packages/daemon/src/workflow-worker.ts +++ b/packages/daemon/src/workflow-worker.ts @@ -30,7 +30,7 @@ import type { WorkerToParentMessage, } from "./ipc.js"; import { parseParentMessage } from "./ipc.js"; -import { ignoreSessionBroadcastSignals } from "./worker-fork-support.js"; +import { ignoreSessionBroadcastSignals } from "./worker-signals.js"; // --------------------------------------------------------------------------- // IPC helpers -- 2.43.0