feat: Phase 2 — Thread lifecycle, execution engine, worker, CLI

- types.ts: START/END, RoleMeta, ThreadContext, Role, Moderator, WorkflowDefinition
- engine.ts: executeThread with JSONL persistence + AbortSignal
- worker.ts: per-bundle process, TCP IPC, kill individual threads
- CLI: run/ps/kill/threads/thread/thread rm commands
- 32 tests pass, biome clean

小橘 <xiaoju@shazhou.work>
This commit is contained in:
2026-05-06 04:59:54 +00:00
parent 01e930df8f
commit 7582a88d6b
46 changed files with 2829 additions and 167 deletions
@@ -0,0 +1,38 @@
import { describe, expect, test } from "bun:test";
import {
decodeCrockfordBase32Bits,
decodeCrockfordToUint64,
encodeCrockfordBase32Bits,
encodeUint64AsCrockford,
} from "../src/base32.js";
describe("Crockford Base32", () => {
test("roundtrip 64-bit hash encoding", () => {
const value = 0xef46_db37_51d8_e999n;
const encoded = encodeUint64AsCrockford(value);
expect(encoded.length).toBe(13);
const decoded = decodeCrockfordToUint64(encoded);
expect(decoded.ok).toBe(true);
if (decoded.ok) {
expect(decoded.value).toBe(value);
}
});
test("roundtrip arbitrary bit widths used by ULID (128-bit)", () => {
const rand = 0x1234567890abcdef12n & ((1n << 80n) - 1n);
const payload = (12345n << 80n) | rand;
const encoded = encodeCrockfordBase32Bits(payload, 128);
expect(encoded.length).toBe(26);
const decoded = decodeCrockfordBase32Bits(encoded, 128);
expect(decoded.ok).toBe(true);
if (decoded.ok) {
expect(decoded.value).toBe(payload);
}
});
test("reject invalid characters", () => {
const decoded = decodeCrockfordToUint64("!!!!!!!!!!!!!");
expect(decoded.ok).toBe(false);
});
});
@@ -0,0 +1,66 @@
import { describe, expect, test } from "bun:test";
import { validateWorkflowBundle } from "../src/bundle-validator.js";
describe("validateWorkflowBundle", () => {
test("accepts minimal valid builtin-only bundle", () => {
const source = `import fs from "node:fs";
export default async function run() {
fs.existsSync(".");
return { returnCode: 0, summary: "ok" };
}
`;
const r = validateWorkflowBundle({ filePath: "/tmp/w.esm.js", source });
expect(r.ok).toBe(true);
});
test("rejects wrong filename suffix", () => {
const r = validateWorkflowBundle({
filePath: "/tmp/w.js",
source: "export default async function run() { return { returnCode: 0, summary: '' }; }\n",
});
expect(r.ok).toBe(false);
});
test("rejects missing default export", () => {
const r = validateWorkflowBundle({
filePath: "/tmp/w.esm.js",
source: "export const x = 1;\n",
});
expect(r.ok).toBe(false);
if (!r.ok) {
expect(r.error).toContain("default export");
}
});
test("rejects non-builtin imports", () => {
const r = validateWorkflowBundle({
filePath: "/tmp/w.esm.js",
source:
'import x from "some-package";\nexport default async function run() { return { returnCode: 0, summary: "" }; }\n',
});
expect(r.ok).toBe(false);
});
test("rejects dynamic import", () => {
const r = validateWorkflowBundle({
filePath: "/tmp/w.esm.js",
source:
'export default async function run() { await import("fs"); return { returnCode: 0, summary: "" }; }\n',
});
expect(r.ok).toBe(false);
if (!r.ok) {
expect(r.error).toContain("dynamic import");
}
});
test("rejects require()", () => {
const r = validateWorkflowBundle({
filePath: "/tmp/w.esm.js",
source:
'export default async function run() { require("fs"); return { returnCode: 0, summary: "" }; }\n',
});
expect(r.ok).toBe(false);
});
});
+137
View File
@@ -0,0 +1,137 @@
import { describe, expect, test } from "bun:test";
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { executeThread } from "../src/engine.js";
import { createLogger } from "../src/logger.js";
import { END, type WorkflowDefinition } from "../src/types.js";
type DemoMeta = {
planner: Record<string, unknown>;
coder: Record<string, unknown>;
};
const demoWorkflow: WorkflowDefinition<DemoMeta> = {
name: "demo-flow",
roles: {
planner: async () => ({
content: "plan-body",
meta: { plan: "do-it", files: ["a.ts"] },
}),
coder: async () => ({
content: "code-body",
meta: { diff: "+ok" },
}),
},
moderator: (ctx) => {
if (ctx.steps.length === 0) {
return "planner";
}
if (ctx.steps.length === 1) {
return "coder";
}
return END;
},
};
describe("executeThread", () => {
test("writes RFC-001 `.data.jsonl` start + role records and `.info.jsonl` logs", async () => {
const root = await mkdtemp(join(tmpdir(), "wf-engine-"));
try {
const threadId = "01KQXKW18CT8G75T53R8F4G7YG";
const hash = "C9NMV6V2TQT81";
const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`);
const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`);
await mkdir(join(root, "logs", hash), { recursive: true });
const logger = createLogger({ sink: { kind: "file", path: infoPath } });
const ac = new AbortController();
const result = await executeThread(
demoWorkflow,
"Fix the login redirect bug in #3",
{ isDryRun: false, maxRounds: 5, signal: ac.signal },
{ threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath },
logger,
);
expect(result.returnCode).toBe(0);
const dataText = await readFile(dataPath, "utf8");
const lines = dataText
.trim()
.split("\n")
.filter((l) => l !== "");
expect(lines.length).toBe(3);
const start = JSON.parse(lines[0] ?? "{}") as Record<string, unknown>;
expect(start.name).toBe("demo-flow");
expect(start.hash).toBe(hash);
expect(start.threadId).toBe(threadId);
expect(typeof start.timestamp).toBe("number");
const params = start.parameters as Record<string, unknown>;
expect(params.prompt).toBe("Fix the login redirect bug in #3");
const opts = params.options as Record<string, unknown>;
expect(opts.isDryRun).toBe(false);
expect(opts.maxRounds).toBe(5);
const role1 = JSON.parse(lines[1] ?? "{}") as Record<string, unknown>;
expect(role1.role).toBe("planner");
expect(role1.content).toBe("plan-body");
expect(role1.meta).toEqual({ plan: "do-it", files: ["a.ts"] });
expect(typeof role1.timestamp).toBe("number");
const role2 = JSON.parse(lines[2] ?? "{}") as Record<string, unknown>;
expect(role2.role).toBe("coder");
const infoText = await readFile(infoPath, "utf8");
const infoLines = infoText
.trim()
.split("\n")
.filter((l) => l !== "");
expect(infoLines.length).toBeGreaterThan(0);
const log0 = JSON.parse(infoLines[0] ?? "{}") as Record<string, unknown>;
expect(typeof log0.tag).toBe("string");
expect(String(log0.tag).length).toBe(8);
expect(typeof log0.content).toBe("string");
expect(typeof log0.timestamp).toBe("number");
} finally {
await rm(root, { recursive: true, force: true });
}
});
test("respects maxRounds=0 (start record only)", async () => {
const root = await mkdtemp(join(tmpdir(), "wf-engine-max0-"));
try {
const threadId = "01KQXKW18CT8G75T53R8F4G7YG";
const hash = "C9NMV6V2TQT81";
const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`);
const infoPath = join(root, "logs", hash, `${threadId}.info.jsonl`);
await mkdir(join(root, "logs", hash), { recursive: true });
const logger = createLogger({ sink: { kind: "file", path: infoPath } });
const ac = new AbortController();
const result = await executeThread(
demoWorkflow,
"hello",
{ isDryRun: false, maxRounds: 0, signal: ac.signal },
{ threadId, hash, dataJsonlPath: dataPath, infoJsonlPath: infoPath },
logger,
);
expect(result.returnCode).toBe(0);
const dataText = await readFile(dataPath, "utf8");
const lines = dataText
.trim()
.split("\n")
.filter((l) => l !== "");
expect(lines.length).toBe(1);
} finally {
await rm(root, { recursive: true, force: true });
}
});
});
+24
View File
@@ -0,0 +1,24 @@
import { describe, expect, test } from "bun:test";
import { decodeCrockfordToUint64 } from "../src/base32.js";
import { hashWorkflowBundleBytes } from "../src/hash.js";
describe("hashWorkflowBundleBytes", () => {
test("matches XXH64 reference for empty input", () => {
const encoder = new TextEncoder();
const digest = hashWorkflowBundleBytes(encoder.encode(""));
const decoded = decodeCrockfordToUint64(digest);
expect(decoded.ok).toBe(true);
if (decoded.ok) {
expect(decoded.value).toBe(0xef46_db37_51d8_e999n);
}
});
test("stable for identical content", () => {
const encoder = new TextEncoder();
const data = encoder.encode(
"export default async function run() { return { returnCode: 0, summary: '' }; }\n",
);
expect(hashWorkflowBundleBytes(data)).toBe(hashWorkflowBundleBytes(data));
});
});
@@ -0,0 +1,31 @@
import { describe, expect, test } from "bun:test";
import { mkdir, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createLogger } from "../src/logger.js";
describe("createLogger", () => {
test("writes JSONL records to a file sink", async () => {
const dir = join(tmpdir(), `wf-log-${process.pid}-${Date.now()}`);
await mkdir(dir, { recursive: true });
const logPath = join(dir, "test.log");
const log = createLogger({ sink: { kind: "file", path: logPath } });
log("01ABCDEF", "hello");
const text = await readFile(logPath, "utf8");
const line = text.trim().split("\n")[0];
expect(line).toBeDefined();
const obj = JSON.parse(line ?? "{}") as { tag: string; content: string; timestamp: number };
expect(obj.tag).toBe("01ABCDEF");
expect(obj.content).toBe("hello");
expect(typeof obj.timestamp).toBe("number");
await rm(dir, { recursive: true, force: true });
});
test("rejects invalid tags", () => {
const log = createLogger({ sink: { kind: "stderr" } });
expect(() => log("BAD", "x")).toThrow();
expect(() => log("01abcdefg", "x")).toThrow();
expect(() => log("01ABCDEO", "x")).toThrow();
});
});
@@ -0,0 +1,77 @@
import { describe, expect, test } from "bun:test";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
readWorkflowRegistry,
registerWorkflowVersion,
unregisterWorkflow,
writeWorkflowRegistry,
} from "../src/registry.js";
describe("workflow registry", () => {
test("roundtrips through workflow.yaml", async () => {
const dir = join(tmpdir(), `wf-reg-${process.pid}-${Date.now()}`);
await mkdir(dir, { recursive: true });
const empty = await readWorkflowRegistry(dir);
expect(empty.ok).toBe(true);
if (!empty.ok) {
return;
}
const r1 = registerWorkflowVersion(empty.value, "solve-issue", "AAAAAAAAAAAAA", 100);
const w1 = await writeWorkflowRegistry(dir, r1);
expect(w1.ok).toBe(true);
const back = await readWorkflowRegistry(dir);
expect(back.ok).toBe(true);
if (!back.ok) {
await rm(dir, { recursive: true, force: true });
return;
}
expect(back.value.workflows["solve-issue"]?.hash).toBe("AAAAAAAAAAAAA");
const r2 = registerWorkflowVersion(back.value, "solve-issue", "BBBBBBBBBBBBB", 200);
expect(r2.workflows["solve-issue"]?.history[0]?.hash).toBe("AAAAAAAAAAAAA");
const removed = unregisterWorkflow(r2, "solve-issue");
expect(removed.ok).toBe(true);
if (!removed.ok) {
await rm(dir, { recursive: true, force: true });
return;
}
const w2 = await writeWorkflowRegistry(dir, removed.value);
expect(w2.ok).toBe(true);
const finalRead = await readWorkflowRegistry(dir);
expect(finalRead.ok).toBe(true);
if (finalRead.ok) {
expect(finalRead.value.workflows["solve-issue"]).toBeUndefined();
}
await rm(dir, { recursive: true, force: true });
});
test("treats missing registry as empty", async () => {
const dir = join(tmpdir(), `wf-reg2-${process.pid}-${Date.now()}`);
await mkdir(dir, { recursive: true });
const empty = await readWorkflowRegistry(dir);
expect(empty.ok).toBe(true);
if (empty.ok) {
expect(Object.keys(empty.value.workflows).length).toBe(0);
}
await rm(dir, { recursive: true, force: true });
});
test("parse errors on invalid shape", async () => {
const dir = join(tmpdir(), `wf-reg3-${process.pid}-${Date.now()}`);
await mkdir(dir, { recursive: true });
await writeFile(join(dir, "workflow.yaml"), 'workflows: "broken"\n', "utf8");
const bad = await readWorkflowRegistry(dir);
expect(bad.ok).toBe(false);
await rm(dir, { recursive: true, force: true });
});
});
@@ -0,0 +1,21 @@
import { describe, expect, test } from "bun:test";
import { err, ok } from "../src/result.js";
describe("result helpers", () => {
test("ok wraps value", () => {
const r = ok(42);
expect(r.ok).toBe(true);
if (r.ok) {
expect(r.value).toBe(42);
}
});
test("err wraps error", () => {
const r = err("nope");
expect(r.ok).toBe(false);
if (!r.ok) {
expect(r.error).toBe("nope");
}
});
});
@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test";
describe("RFC-001 thread JSONL shapes", () => {
test("documents the `.data.jsonl` start record + role record keys", () => {
const startRecord = {
name: "solve-issue",
hash: "C9NMV6V2TQT81",
threadId: "01KQXKW18CT8G75T53R8F4G7YG",
parameters: {
prompt: "Fix the login redirect bug in #3",
options: {
isDryRun: false,
maxRounds: 5,
},
},
timestamp: 1714963200000,
};
const roleRecord = {
role: "planner",
content: "Plan: modify auth middleware...",
meta: { plan: "...", files: ["src/auth.ts"] },
timestamp: 1714963201000,
};
expect(Object.keys(startRecord).sort()).toEqual(
["hash", "name", "parameters", "threadId", "timestamp"].sort(),
);
expect(Object.keys(roleRecord).sort()).toEqual(["content", "meta", "role", "timestamp"].sort());
});
test("documents the `.info.jsonl` debug record keys", () => {
const infoRecord = {
tag: "4KNMR2PX",
content: "Loading workflow bundle...",
timestamp: 1714963200500,
};
expect(Object.keys(infoRecord).sort()).toEqual(["content", "tag", "timestamp"].sort());
});
});
+29
View File
@@ -0,0 +1,29 @@
import { describe, expect, test } from "bun:test";
import { decodeCrockfordBase32Bits } from "../src/base32.js";
import { generateUlid } from "../src/ulid.js";
describe("generateUlid", () => {
test("length and decodable Crockford payload", () => {
const id = generateUlid(1_714_963_200_000);
expect(id.length).toBe(26);
const decoded = decodeCrockfordBase32Bits(id, 128);
expect(decoded.ok).toBe(true);
});
test("embeds 48-bit timestamp at the MSB of the 128-bit payload", () => {
const ts = 9_999_888_777_666;
const id = generateUlid(ts);
const decoded = decodeCrockfordBase32Bits(id, 128);
expect(decoded.ok).toBe(true);
if (decoded.ok) {
const recoveredMs = decoded.value >> 80n;
expect(Number(recoveredMs)).toBe(ts);
}
});
test("rejects out-of-range timestamps", () => {
expect(() => generateUlid(-1)).toThrow();
expect(() => generateUlid(2 ** 48)).toThrow();
});
});
+120
View File
@@ -0,0 +1,120 @@
import { describe, expect, test } from "bun:test";
import { spawn } from "node:child_process";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createConnection } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getWorkerHostScriptPath } from "../src/worker-entry-path.js";
const bundleSource = `export default {
name: "demo-flow",
roles: {
planner: async () => ({ content: "p", meta: { plan: "x" } }),
coder: async () => ({ content: "c", meta: { diff: "y" } }),
},
moderator(ctx) {
if (ctx.steps.length === 0) return "planner";
if (ctx.steps.length === 1) return "coder";
return "__end__";
},
};
`;
async function readReadyPort(child: import("node:child_process").ChildProcess): Promise<number> {
return await new Promise((resolve, reject) => {
if (child.stdout === null) {
reject(new Error("missing stdout"));
return;
}
let buf = "";
function cleanup(): void {
child.stdout?.off("data", onData);
child.off("exit", onExit);
}
function onData(chunk: Buffer): void {
buf += chunk.toString("utf8");
const nl = buf.indexOf("\n");
if (nl < 0) {
return;
}
cleanup();
const line = buf.slice(0, nl).trim();
const prefix = "READY ";
if (!line.startsWith(prefix)) {
reject(new Error(`unexpected READY line: ${line}`));
return;
}
resolve(Number(line.slice(prefix.length)));
}
function onExit(code: number | null): void {
cleanup();
reject(new Error(`worker exited before READY (code ${code})`));
}
child.stdout.on("data", onData);
child.on("exit", onExit);
});
}
async function sendJson(port: number, payload: unknown): Promise<void> {
await new Promise<void>((resolve, reject) => {
const socket = createConnection({ host: "127.0.0.1", port }, () => {
socket.write(`${JSON.stringify(payload)}\n`);
socket.end();
});
socket.on("error", reject);
socket.on("close", () => resolve());
});
}
describe("worker process", () => {
test("loads bundle, runs a thread over TCP, then exits when idle", async () => {
const root = await mkdtemp(join(tmpdir(), "wf-worker-"));
try {
const hash = "C9NMV6V2TQT81";
await mkdir(join(root, "bundles"), { recursive: true });
const bundlePath = join(root, "bundles", `${hash}.esm.js`);
await writeFile(bundlePath, bundleSource, "utf8");
const scriptPath = getWorkerHostScriptPath();
const child = spawn(process.execPath, [scriptPath, bundlePath, root, hash], {
stdio: ["ignore", "pipe", "inherit"],
});
if (child.stdout === null) {
throw new Error("missing stdout");
}
const port = await readReadyPort(child);
const threadId = "01KQXKW18CT8G75T53R8F4G7YG";
await sendJson(port, {
type: "run",
threadId,
prompt: "hello",
options: { isDryRun: false, maxRounds: 5 },
});
const exitCode: number = await new Promise((resolve) => {
child.on("exit", (code) => resolve(code ?? 1));
});
expect(exitCode).toBe(0);
const dataPath = join(root, "logs", hash, `${threadId}.data.jsonl`);
const text = await readFile(dataPath, "utf8");
expect(
text
.trim()
.split("\n")
.filter((l) => l !== "").length,
).toBe(3);
} finally {
await rm(root, { recursive: true, force: true });
}
}, 15_000);
});
+8 -5
View File
@@ -1,7 +1,7 @@
import { err, ok, type Result } from "./result.js";
/** Crockford Base32 alphabet (no I, L, O, U). */
export const CROCKFORD_BASE32_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXZ";
/** Crockford Base32 alphabet (no I, L, O, U) — exactly 32 symbols. */
export const CROCKFORD_BASE32_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
const DECODE_MAP: Record<string, number> = (() => {
const map: Record<string, number> = {};
@@ -31,13 +31,16 @@ export function encodeCrockfordBase32Bits(value: bigint, bitLength: number): str
let result = "";
for (let i = 0; i < charCount; i++) {
const shift = totalBits - 5 * (i + 1);
const quintet = Number((shifted >> BigInt(shift)) & 0x1fn);
const quintet = Number((shifted >> BigInt(shift)) & 31n);
result += CROCKFORD_BASE32_ALPHABET[quintet];
}
return result;
}
export function decodeCrockfordBase32Bits(encoded: string, bitLength: number): Result<bigint, Error> {
export function decodeCrockfordBase32Bits(
encoded: string,
bitLength: number,
): Result<bigint, Error> {
if (bitLength <= 0) {
return err(new Error("bitLength must be positive"));
}
@@ -57,7 +60,7 @@ export function decodeCrockfordBase32Bits(encoded: string, bitLength: number): R
if (val === undefined) {
return err(new Error(`invalid Crockford Base32 character: ${ch}`));
}
shifted = (shifted << 5n) | BigInt(val);
shifted = (shifted << 5n) | BigInt(val & 31);
}
return ok(shifted >> BigInt(padBits));
}
+123 -63
View File
@@ -1,7 +1,13 @@
import { isBuiltin } from "node:module";
import type {
CallExpression,
ExportAllDeclaration,
ExportNamedDeclaration,
ImportDeclaration,
Node,
Program,
} from "acorn";
import * as acorn from "acorn";
import type { Node, Program } from "acorn";
import { err, ok, type Result } from "./result.js";
@@ -26,22 +32,36 @@ function isAllowedImportSpecifier(spec: string): boolean {
return isBuiltin(spec);
}
function walk(node: Node, visit: (n: Node) => void): void {
visit(node);
function pushNestedAstNodes(value: unknown, out: Node[]): void {
if (value === null || value === undefined) {
return;
}
if (Array.isArray(value)) {
for (const item of value) {
if (item !== null && typeof item === "object" && "type" in item) {
out.push(item as Node);
}
}
return;
}
if (typeof value === "object" && "type" in value) {
out.push(value as Node);
}
}
function collectChildNodes(node: Node): Node[] {
const children: Node[] = [];
for (const key of Object.keys(node)) {
const val = (node as Record<string, unknown>)[key];
if (val === null || val === undefined) {
continue;
}
if (Array.isArray(val)) {
for (const item of val) {
if (item !== null && typeof item === "object" && "type" in item) {
walk(item as Node, visit);
}
}
} else if (typeof val === "object" && "type" in val) {
walk(val as Node, visit);
}
pushNestedAstNodes(val, children);
}
return children;
}
function walkAst(node: Node, visit: (n: Node) => void): void {
visit(node);
for (const child of collectChildNodes(node)) {
walkAst(child, visit);
}
}
@@ -54,6 +74,85 @@ function programHasDefaultExport(body: readonly Node[]): boolean {
return false;
}
function stringLiteralModuleSpecifier(src: Node): string | null {
if (src.type !== "Literal" || typeof src.value !== "string") {
return null;
}
return src.value;
}
function validateImportDeclaration(node: ImportDeclaration): string | null {
const spec = stringLiteralModuleSpecifier(node.source);
if (spec === null) {
return "only static string import specifiers are allowed";
}
if (!isAllowedImportSpecifier(spec)) {
return `disallowed import specifier "${spec}" (only Node built-ins are allowed)`;
}
return null;
}
function validateExportSource(
src: Node,
staticMessage: string,
disallowedPrefix: string,
): string | null {
const spec = stringLiteralModuleSpecifier(src);
if (spec === null) {
return staticMessage;
}
if (!isAllowedImportSpecifier(spec)) {
return `${disallowedPrefix} "${spec}" (only Node built-ins are allowed)`;
}
return null;
}
function validateExportNamedDeclaration(node: ExportNamedDeclaration): string | null {
if (node.source === null || node.source === undefined) {
return null;
}
return validateExportSource(
node.source,
"only static string re-export specifiers are allowed",
"disallowed re-export specifier",
);
}
function validateExportAllDeclaration(node: ExportAllDeclaration): string | null {
return validateExportSource(
node.source,
"only static string export-all specifiers are allowed",
"disallowed export-all specifier",
);
}
function validateRequireCall(node: CallExpression): string | null {
const callee = node.callee;
if (callee.type === "Identifier" && callee.name === "require") {
return "require() is not allowed in workflow bundles";
}
return null;
}
function bundleConstraintViolationForNode(node: Node): string | null {
if (node.type === "ImportExpression") {
return "dynamic import() is not allowed in workflow bundles";
}
if (node.type === "ImportDeclaration") {
return validateImportDeclaration(node);
}
if (node.type === "ExportNamedDeclaration") {
return validateExportNamedDeclaration(node);
}
if (node.type === "ExportAllDeclaration") {
return validateExportAllDeclaration(node);
}
if (node.type === "CallExpression") {
return validateRequireCall(node);
}
return null;
}
/**
* Validate RFC-001 bundle rules: single-file ESM shape, default export,
* no dynamic `import()`, static imports restricted to Node builtins.
@@ -84,58 +183,19 @@ export function validateWorkflowBundle(input: WorkflowBundleValidationInput): Re
return err("workflow bundle must have a default export");
}
let walkError: string | null = null;
walk(ast, (n) => {
if (walkError !== null) {
let violation: string | null = null;
walkAst(ast, (node) => {
if (violation !== null) {
return;
}
if (n.type === "ImportExpression") {
walkError = "dynamic import() is not allowed in workflow bundles";
return;
}
if (n.type === "ImportDeclaration") {
const src = n.source;
if (src.type !== "Literal" || typeof src.value !== "string") {
walkError = "only static string import specifiers are allowed";
return;
}
if (!isAllowedImportSpecifier(src.value)) {
walkError = `disallowed import specifier "${src.value}" (only Node built-ins are allowed)`;
}
return;
}
if (n.type === "ExportNamedDeclaration" && n.source !== null && n.source !== undefined) {
const src = n.source;
if (src.type !== "Literal" || typeof src.value !== "string") {
walkError = "only static string re-export specifiers are allowed";
return;
}
if (!isAllowedImportSpecifier(src.value)) {
walkError = `disallowed re-export specifier "${src.value}" (only Node built-ins are allowed)`;
}
return;
}
if (n.type === "ExportAllDeclaration") {
const src = n.source;
if (src.type !== "Literal" || typeof src.value !== "string") {
walkError = "only static string export-all specifiers are allowed";
return;
}
if (!isAllowedImportSpecifier(src.value)) {
walkError = `disallowed export-all specifier "${src.value}" (only Node built-ins are allowed)`;
}
return;
}
if (n.type === "CallExpression") {
const c = n.callee;
if (c.type === "Identifier" && c.name === "require") {
walkError = "require() is not allowed in workflow bundles";
}
const next = bundleConstraintViolationForNode(node);
if (next !== null) {
violation = next;
}
});
if (walkError !== null) {
return err(walkError);
if (violation !== null) {
return err(violation);
}
return ok(undefined);
+143
View File
@@ -0,0 +1,143 @@
import { appendFile, mkdir } from "node:fs/promises";
import { dirname } from "node:path";
import type { LogFn } from "./logger.js";
import {
END,
type RoleMeta,
type RoleStep,
START,
type ThreadContext,
type WorkflowDefinition,
} from "./types.js";
export type ExecuteThreadIo = {
threadId: string;
hash: string;
dataJsonlPath: string;
infoJsonlPath: string;
};
export type ExecuteThreadOptions = {
isDryRun: boolean;
maxRounds: number;
signal: AbortSignal;
};
function isRoleNext<M extends RoleMeta>(
next: (keyof M & string) | typeof END,
): next is keyof M & string {
return next !== END;
}
async function appendDataLine(path: string, record: unknown): Promise<void> {
const line = `${JSON.stringify(record)}\n`;
await appendFile(path, line, "utf8");
}
/**
* Execute a workflow thread: moderator loop, role steps, RFC-001 `.data.jsonl` records,
* debug lines via `logger` to `.info.jsonl`.
*/
export async function executeThread<M extends RoleMeta>(
def: WorkflowDefinition<M>,
prompt: string,
options: ExecuteThreadOptions,
io: ExecuteThreadIo,
logger: LogFn,
): Promise<{ returnCode: number; summary: string }> {
await mkdir(dirname(io.dataJsonlPath), { recursive: true });
await mkdir(dirname(io.infoJsonlPath), { recursive: true });
const nowMs = Date.now();
const start: ThreadContext<M>["start"] = {
role: START,
content: prompt,
meta: { maxRounds: options.maxRounds, threadId: io.threadId },
timestamp: nowMs,
};
const startRecord = {
name: def.name,
hash: io.hash,
threadId: io.threadId,
parameters: {
prompt,
options: {
isDryRun: options.isDryRun,
maxRounds: options.maxRounds,
},
},
timestamp: nowMs,
};
await appendDataLine(io.dataJsonlPath, startRecord);
let steps: RoleStep<M>[] = [];
logger("T9HQ2KHM", `thread ${io.threadId} started for workflow ${def.name}`);
while (true) {
if (options.signal.aborted) {
logger("V8JX4NP2", `thread ${io.threadId} aborted`);
return { returnCode: 130, summary: "thread aborted" };
}
if (steps.length >= options.maxRounds) {
logger("R3CW7YBQ", `thread ${io.threadId} stopped at maxRounds=${options.maxRounds}`);
return {
returnCode: 0,
summary: `completed: reached maxRounds (${options.maxRounds})`,
};
}
const ctx: ThreadContext<M> = {
threadId: io.threadId,
start,
steps,
};
const next = def.moderator(ctx);
if (!isRoleNext(next)) {
logger("M5FZ2K8H", `thread ${io.threadId} moderator returned END`);
return { returnCode: 0, summary: "completed: moderator returned END" };
}
const roleFn = def.roles[next];
if (roleFn === undefined) {
logger("K2P8QX9W", `thread ${io.threadId} unknown role ${next}`);
return { returnCode: 1, summary: `unknown role: ${next}` };
}
if (options.signal.aborted) {
logger("V8JX4NP3", `thread ${io.threadId} aborted`);
return { returnCode: 130, summary: "thread aborted" };
}
const result = await roleFn(ctx);
const ts = Date.now();
const step: RoleStep<M> = {
role: next,
content: result.content,
meta: result.meta,
timestamp: ts,
} as RoleStep<M>;
await appendDataLine(io.dataJsonlPath, {
role: step.role,
content: step.content,
meta: step.meta,
timestamp: step.timestamp,
});
steps = [...steps, step];
logger("N7BW4YHQ", `thread ${io.threadId} completed role ${next}`);
if (options.signal.aborted) {
logger("V8JX4NP4", `thread ${io.threadId} aborted`);
return { returnCode: 130, summary: "thread aborted" };
}
}
}
+22 -3
View File
@@ -6,10 +6,15 @@ export {
encodeUint64AsCrockford,
} from "./base32.js";
export { validateWorkflowBundle, type WorkflowBundleValidationInput } from "./bundle-validator.js";
export {
type ExecuteThreadIo,
type ExecuteThreadOptions,
executeThread,
} from "./engine.js";
export { hashWorkflowBundleBytes } from "./hash.js";
export {
createLogger,
type CreateLoggerOptions,
createLogger,
type LogFn,
type LoggerSink,
} from "./logger.js";
@@ -21,12 +26,26 @@ export {
registerWorkflowVersion,
stringifyWorkflowRegistryYaml,
unregisterWorkflow,
workflowRegistryPath,
writeWorkflowRegistry,
type WorkflowHistoryEntry,
type WorkflowRegistryEntry,
type WorkflowRegistryFile,
workflowRegistryPath,
writeWorkflowRegistry,
} from "./registry.js";
export { err, ok, type Result } from "./result.js";
export { getDefaultWorkflowStorageRoot } from "./storage-root.js";
export {
type AgentFn,
END,
type Moderator,
type Role,
type RoleMeta,
type RoleResult,
type RoleStep,
START,
type StartStep,
type ThreadContext,
type WorkflowDefinition,
} from "./types.js";
export { generateUlid } from "./ulid.js";
export { getWorkerHostScriptPath } from "./worker-entry-path.js";
+16 -16
View File
@@ -1,7 +1,11 @@
import { appendFileSync } from "node:fs";
import { CROCKFORD_BASE32_ALPHABET } from "./base32.js";
const TAG_LENGTH = 8;
const TAG_CHAR_SET: ReadonlySet<string> = new Set(CROCKFORD_BASE32_ALPHABET.split(""));
function assertValidLogTag(tag: string): void {
if (tag.length !== TAG_LENGTH) {
throw new Error(`log tag must be exactly ${TAG_LENGTH} characters`);
@@ -12,15 +16,13 @@ function assertValidLogTag(tag: string): void {
throw new Error("log tag validation failed");
}
const upper = ch.toUpperCase();
if (!/[0-9A-HJKMNP-TV-Z]/.test(upper)) {
if (!TAG_CHAR_SET.has(upper)) {
throw new Error(`invalid Crockford Base32 character in log tag: ${ch}`);
}
}
}
export type LoggerSink =
| { kind: "stderr" }
| { kind: "file"; path: string };
export type LoggerSink = { kind: "stderr" } | { kind: "file"; path: string };
export type CreateLoggerOptions = {
sink: LoggerSink;
@@ -33,12 +35,11 @@ export function createLogger(options: CreateLoggerOptions): LogFn {
if (options.sink.kind === "stderr") {
return (tag: string, content: string) => {
assertValidLogTag(tag);
const line =
`${JSON.stringify({
tag: tag.toUpperCase(),
content,
timestamp: Date.now(),
})}\n`;
const line = `${JSON.stringify({
tag: tag.toUpperCase(),
content,
timestamp: Date.now(),
})}\n`;
process.stderr.write(line);
};
}
@@ -46,12 +47,11 @@ export function createLogger(options: CreateLoggerOptions): LogFn {
const filePath = options.sink.path;
return (tag: string, content: string) => {
assertValidLogTag(tag);
const line =
`${JSON.stringify({
tag: tag.toUpperCase(),
content,
timestamp: Date.now(),
})}\n`;
const line = `${JSON.stringify({
tag: tag.toUpperCase(),
content,
timestamp: Date.now(),
})}\n`;
appendFileSync(filePath, line, "utf8");
};
}
@@ -0,0 +1,77 @@
import type {
WorkflowHistoryEntry,
WorkflowRegistryEntry,
WorkflowRegistryFile,
} from "./registry-types.js";
import { err, ok, type Result } from "./result.js";
export function normalizeWorkflowHistoryEntry(
workflowName: string,
index: number,
raw: unknown,
): Result<WorkflowHistoryEntry, Error> {
if (raw === null || typeof raw !== "object") {
return err(new Error(`workflow "${workflowName}" history[${index}] must be a mapping`));
}
const he = raw as Record<string, unknown>;
const hash = he.hash;
const timestamp = he.timestamp;
if (typeof hash !== "string" || typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
return err(
new Error(`workflow "${workflowName}" history[${index}] must have hash and timestamp`),
);
}
return ok({ hash, timestamp });
}
export function normalizeWorkflowRegistryEntry(
workflowName: string,
raw: unknown,
): Result<WorkflowRegistryEntry, Error> {
if (raw === null || typeof raw !== "object") {
return err(new Error(`workflow "${workflowName}" must be a mapping`));
}
const e = raw as Record<string, unknown>;
const hash = e.hash;
const timestamp = e.timestamp;
const historyRaw = e.history;
if (typeof hash !== "string") {
return err(new Error(`workflow "${workflowName}" must have a string hash`));
}
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
return err(new Error(`workflow "${workflowName}" must have a finite numeric timestamp`));
}
if (!Array.isArray(historyRaw)) {
return err(new Error(`workflow "${workflowName}" must have a history array`));
}
const history: WorkflowHistoryEntry[] = [];
for (let i = 0; i < historyRaw.length; i++) {
const item = historyRaw[i];
const next = normalizeWorkflowHistoryEntry(workflowName, i, item);
if (!next.ok) {
return next;
}
history.push(next.value);
}
return ok({ hash, timestamp, history });
}
export function normalizeWorkflowRegistryRoot(raw: unknown): Result<WorkflowRegistryFile, Error> {
if (raw === null || typeof raw !== "object") {
return err(new Error("registry root must be a mapping"));
}
const root = raw as Record<string, unknown>;
const workflowsRaw = root.workflows;
if (workflowsRaw === null || workflowsRaw === undefined || typeof workflowsRaw !== "object") {
return err(new Error('registry must contain a "workflows" mapping'));
}
const workflows: Record<string, WorkflowRegistryEntry> = {};
for (const [name, entryRaw] of Object.entries(workflowsRaw)) {
const entryResult = normalizeWorkflowRegistryEntry(name, entryRaw);
if (!entryResult.ok) {
return entryResult;
}
workflows[name] = entryResult.value;
}
return ok({ workflows });
}
+14
View File
@@ -0,0 +1,14 @@
export type WorkflowHistoryEntry = {
hash: string;
timestamp: number;
};
export type WorkflowRegistryEntry = {
hash: string;
timestamp: number;
history: WorkflowHistoryEntry[];
};
export type WorkflowRegistryFile = {
workflows: Record<string, WorkflowRegistryEntry>;
};
+16 -61
View File
@@ -3,22 +3,19 @@ import { dirname, join } from "node:path";
import { parseDocument, stringify } from "yaml";
import { normalizeWorkflowRegistryRoot } from "./registry-normalize.js";
import type {
WorkflowHistoryEntry,
WorkflowRegistryEntry,
WorkflowRegistryFile,
} from "./registry-types.js";
import { err, ok, type Result } from "./result.js";
export type WorkflowHistoryEntry = {
hash: string;
timestamp: number;
};
export type WorkflowRegistryEntry = {
hash: string;
timestamp: number;
history: WorkflowHistoryEntry[];
};
export type WorkflowRegistryFile = {
workflows: Record<string, WorkflowRegistryEntry>;
};
export type {
WorkflowHistoryEntry,
WorkflowRegistryEntry,
WorkflowRegistryFile,
} from "./registry-types.js";
export function workflowRegistryPath(storageRoot: string): string {
return join(storageRoot, "workflow.yaml");
@@ -28,50 +25,6 @@ function emptyRegistry(): WorkflowRegistryFile {
return { workflows: {} };
}
function normalizeRegistry(raw: unknown): Result<WorkflowRegistryFile, Error> {
if (raw === null || typeof raw !== "object") {
return err(new Error("registry root must be a mapping"));
}
const root = raw as Record<string, unknown>;
const workflowsRaw = root.workflows;
if (workflowsRaw === null || workflowsRaw === undefined || typeof workflowsRaw !== "object") {
return err(new Error('registry must contain a "workflows" mapping'));
}
const workflows: Record<string, WorkflowRegistryEntry> = {};
for (const [name, entryRaw] of Object.entries(workflowsRaw)) {
if (entryRaw === null || typeof entryRaw !== "object") {
return err(new Error(`workflow "${name}" must be a mapping`));
}
const e = entryRaw as Record<string, unknown>;
const hash = e.hash;
const timestamp = e.timestamp;
const historyRaw = e.history;
if (typeof hash !== "string") {
return err(new Error(`workflow "${name}" must have a string hash`));
}
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
return err(new Error(`workflow "${name}" must have a finite numeric timestamp`));
}
if (!Array.isArray(historyRaw)) {
return err(new Error(`workflow "${name}" must have a history array`));
}
const history: WorkflowHistoryEntry[] = [];
for (let i = 0; i < historyRaw.length; i++) {
const h = historyRaw[i];
if (h === null || typeof h !== "object") {
return err(new Error(`workflow "${name}" history[${i}] must be a mapping`));
}
const he = h as Record<string, unknown>;
if (typeof he.hash !== "string" || typeof he.timestamp !== "number" || !Number.isFinite(he.timestamp)) {
return err(new Error(`workflow "${name}" history[${i}] must have hash and timestamp`));
}
history.push({ hash: he.hash, timestamp: he.timestamp });
}
workflows[name] = { hash, timestamp, history };
}
return ok({ workflows });
}
export function parseWorkflowRegistryYaml(text: string): Result<WorkflowRegistryFile, Error> {
if (text.trim() === "") {
return ok(emptyRegistry());
@@ -82,14 +35,16 @@ export function parseWorkflowRegistryYaml(text: string): Result<WorkflowRegistry
} catch (e) {
return err(e instanceof Error ? e : new Error(String(e)));
}
return normalizeRegistry(doc);
return normalizeWorkflowRegistryRoot(doc);
}
export function stringifyWorkflowRegistryYaml(registry: WorkflowRegistryFile): string {
return `${stringify(registry, { indent: 2 })}\n`;
return `${stringify(registry, { indent: 2, defaultStringType: "QUOTE_DOUBLE" })}\n`;
}
export async function readWorkflowRegistry(storageRoot: string): Promise<Result<WorkflowRegistryFile, Error>> {
export async function readWorkflowRegistry(
storageRoot: string,
): Promise<Result<WorkflowRegistryFile, Error>> {
const path = workflowRegistryPath(storageRoot);
let text: string;
try {
+63
View File
@@ -0,0 +1,63 @@
/** Sentinel values for automaton control flow. */
export const START = "__start__" as const;
export const END = "__end__" as const;
/** Maps role names → their meta types. Single generic drives all inference. */
export type RoleMeta = Record<string, Record<string, unknown>>;
/** Typed output of a Role execution. */
export type RoleResult<Meta extends Record<string, unknown>> = {
content: string;
meta: Meta;
};
/** Engine start frame: initial prompt + thread identity. */
export type StartStep = {
role: typeof START;
content: string;
meta: { maxRounds: number; threadId: string };
timestamp: number;
};
/** A completed role step in the thread. */
export type RoleStep<M extends RoleMeta> = {
[K in keyof M & string]: { role: K; meta: M[K]; content: string; timestamp: number };
}[keyof M & string];
/** Thread-scoped context passed to roles and moderator. */
export type ThreadContext<M extends RoleMeta = RoleMeta> = {
threadId: string;
start: StartStep;
steps: RoleStep<M>[];
};
/**
* A Role — receives full thread context, returns typed content + meta.
* Implementation can be an agent, LLM call, script, HTTP request, etc.
*/
export type Role<Meta extends Record<string, unknown>> = (
ctx: ThreadContext,
) => Promise<RoleResult<Meta>>;
/**
* An Agent — raw string output interface for LLM/CLI adapters.
* Structured meta is extracted by the role's extract layer.
*/
export type AgentFn = (ctx: ThreadContext, systemPrompt: string) => Promise<string>;
/**
* The Moderator — a pure routing function.
* Receives the full thread context (start + all prior steps).
* On initial call, `steps` is empty.
* Returns the next role name or END to terminate.
*/
export type Moderator<M extends RoleMeta> = (
ctx: ThreadContext<M>,
) => (keyof M & string) | typeof END;
/** Complete workflow definition as authored by users. */
export type WorkflowDefinition<M extends RoleMeta> = {
name: string;
roles: { [K in keyof M & string]: Role<M[K]> };
moderator: Moderator<M>;
};
@@ -0,0 +1,6 @@
import { fileURLToPath } from "node:url";
/** Absolute path to `worker-host.ts` for spawning bundle worker processes. */
export function getWorkerHostScriptPath(): string {
return fileURLToPath(new URL("./worker.ts", import.meta.url));
}
+295
View File
@@ -0,0 +1,295 @@
import { mkdir, unlink, writeFile } from "node:fs/promises";
import { createServer, type Socket } from "node:net";
import { dirname, join } from "node:path";
import { pathToFileURL } from "node:url";
import { type ExecuteThreadIo, executeThread } from "./engine.js";
import { createLogger } from "./logger.js";
import type { RoleMeta, WorkflowDefinition } from "./types.js";
const bootLog = createLogger({ sink: { kind: "stderr" } });
type RunCommand = {
type: "run";
threadId: string;
prompt: string;
options: { isDryRun: boolean; maxRounds: number };
};
type KillCommand = {
type: "kill";
threadId: string;
};
type ControlCommand = RunCommand | KillCommand;
function parseControlPayload(payload: unknown): ControlCommand | null {
if (payload === null || typeof payload !== "object") {
return null;
}
const rec = payload as Record<string, unknown>;
const type = rec.type;
if (type === "kill") {
const threadId = rec.threadId;
if (typeof threadId !== "string") {
return null;
}
return { type: "kill", threadId };
}
if (type === "run") {
const threadId = rec.threadId;
const prompt = rec.prompt;
const options = rec.options;
if (typeof threadId !== "string" || typeof prompt !== "string") {
return null;
}
if (options === null || typeof options !== "object") {
return null;
}
const optRec = options as Record<string, unknown>;
const isDryRun = optRec.isDryRun;
const maxRounds = optRec.maxRounds;
if (typeof isDryRun !== "boolean" || typeof maxRounds !== "number") {
return null;
}
return {
type: "run",
threadId,
prompt,
options: { isDryRun, maxRounds },
};
}
return null;
}
function parseCommandLine(line: string): ControlCommand | null {
const trimmed = line.trim();
if (trimmed === "") {
return null;
}
let parsed: unknown;
try {
parsed = JSON.parse(trimmed) as unknown;
} catch {
bootLog("S8KQ3WJP", "worker received invalid JSON control line");
return null;
}
return parseControlPayload(parsed);
}
function isWorkflowDefinitionLike(value: unknown): value is WorkflowDefinition<RoleMeta> {
if (value === null || typeof value !== "object") {
return false;
}
const rec = value as Record<string, unknown>;
if (typeof rec.name !== "string") {
return false;
}
if (rec.roles === null || typeof rec.roles !== "object") {
return false;
}
if (typeof rec.moderator !== "function") {
return false;
}
return true;
}
async function readLineFromSocket(socket: Socket): Promise<string | null> {
return await new Promise((resolve) => {
let buf = "";
function onData(chunk: Buffer): void {
buf += chunk.toString("utf8");
const nl = buf.indexOf("\n");
if (nl >= 0) {
cleanup();
resolve(buf.slice(0, nl));
}
}
function onEnd(): void {
cleanup();
resolve(buf === "" ? null : buf);
}
function onError(): void {
cleanup();
resolve(null);
}
function cleanup(): void {
socket.off("data", onData);
socket.off("end", onEnd);
socket.off("error", onError);
}
socket.on("data", onData);
socket.on("end", onEnd);
socket.on("error", onError);
});
}
async function main(): Promise<void> {
const bundlePath = process.argv[2];
const storageRoot = process.argv[3];
const hash = process.argv[4];
if (
bundlePath === undefined ||
storageRoot === undefined ||
hash === undefined ||
bundlePath === "" ||
storageRoot === "" ||
hash === ""
) {
bootLog("H7XN4MKQ", "worker usage: worker <bundlePath> <storageRoot> <hash>");
process.exit(2);
return;
}
// Dynamic import required: user bundle path resolved at runtime
const modUnknown: unknown = await import(pathToFileURL(bundlePath).href);
const modRec = modUnknown as Record<string, unknown>;
const defaultExport = modRec.default;
if (!isWorkflowDefinitionLike(defaultExport)) {
bootLog(
"T4BW9YJX",
"workflow bundle default export must be a WorkflowDefinition { name, roles, moderator }",
);
process.exit(2);
return;
}
const def = defaultExport;
const controllers = new Map<string, AbortController>();
let activeThreads = 0;
let shutdownTimer: ReturnType<typeof setTimeout> | null = null;
const workerCtlPath = join(storageRoot, "workers", `${hash}.json`);
function cancelShutdownTimer(): void {
if (shutdownTimer !== null) {
clearTimeout(shutdownTimer);
shutdownTimer = null;
}
}
function scheduleShutdown(): void {
cancelShutdownTimer();
shutdownTimer = setTimeout(() => {
void unlink(workerCtlPath).catch(() => {});
process.exit(0);
}, 150);
}
function bumpStart(): void {
cancelShutdownTimer();
activeThreads++;
}
function bumpDone(): void {
activeThreads--;
if (activeThreads <= 0) {
activeThreads = 0;
scheduleShutdown();
}
}
async function dispatchCommand(cmd: ControlCommand, socket: Socket | null): Promise<void> {
if (cmd.type === "kill") {
const ac = controllers.get(cmd.threadId);
if (ac !== undefined) {
ac.abort();
bootLog("P9XK2WNQ", `kill requested for thread ${cmd.threadId}`);
}
socket?.end();
return;
}
bumpStart();
const threadId = cmd.threadId;
const runningPath = join(storageRoot, "logs", hash, `${threadId}.running`);
const dataJsonlPath = join(storageRoot, "logs", hash, `${threadId}.data.jsonl`);
const infoJsonlPath = join(storageRoot, "logs", hash, `${threadId}.info.jsonl`);
const io: ExecuteThreadIo = {
threadId,
hash,
dataJsonlPath,
infoJsonlPath,
};
const existing = controllers.get(threadId);
if (existing !== undefined) {
existing.abort();
controllers.delete(threadId);
}
const ac = new AbortController();
controllers.set(threadId, ac);
try {
await mkdir(dirname(runningPath), { recursive: true });
await mkdir(dirname(dataJsonlPath), { recursive: true });
await writeFile(runningPath, "", "utf8");
const logger = createLogger({ sink: { kind: "file", path: infoJsonlPath } });
await executeThread(def, cmd.prompt, { ...cmd.options, signal: ac.signal }, io, logger);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
bootLog("Q3MN8YKW", `thread ${threadId} failed: ${message}`);
} finally {
controllers.delete(threadId);
await unlink(runningPath).catch(() => {});
bumpDone();
socket?.end();
}
}
if (typeof process.send === "function") {
process.on("message", (msg: unknown) => {
const cmd = parseControlPayload(msg);
if (cmd === null) {
return;
}
void dispatchCommand(cmd, null);
});
}
const server = createServer((socket) => {
void (async () => {
const line = await readLineFromSocket(socket);
if (line === null) {
socket.end();
return;
}
const cmd = parseCommandLine(line);
if (cmd === null) {
socket.end();
return;
}
await dispatchCommand(cmd, socket);
})();
});
server.on("error", (err) => {
bootLog("W8YK4NPX", `worker server error: ${err.message}`);
process.exit(1);
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", () => {
resolve();
});
});
const addr = server.address();
if (addr === null || typeof addr === "string") {
bootLog("R9XK4MNW", "worker failed to bind TCP address");
process.exit(1);
return;
}
process.stdout.write(`READY ${addr.port}\n`);
await new Promise<void>(() => {});
}
void main();