import { createHash, randomUUID } from "node:crypto";
import { lstatSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
import * as Schema from "typebox/schema";
export const RLM_CONTRACT_FORMAT_VERSION = 1;
export const RLM_RESULT_FILE_NAME = "rlm-result.json";
export const RLM_CONTRACT_FILE_NAME = ".rlm-contract.json";
const MAX_SCHEMA_BYTES = 64 * 1024;
const MAX_CONTRACT_BYTES = 256 * 1024;
const MAX_RESULT_BYTES = 1024 * 1024;
const MAX_PROJECTED_RESULT_BYTES = 32 * 1024;
export interface RlmPayloadContextPolicy {
payload: boolean;
notify: boolean;
}
export interface RlmResultContextPolicy {
fields: string[];
}
export interface RlmChildContextContract {
terminal?: RlmPayloadContextPolicy;
direct_messages?: RlmPayloadContextPolicy;
observe?: boolean;
result?: RlmResultContextPolicy;
}
export interface RlmResultContract {
schema: Record<string, unknown>;
sink: { kind: "file" };
required: boolean;
}
export interface RlmChildContract {
result?: RlmResultContract;
context?: RlmChildContextContract;
}
export type RlmResultTurnStatus = "active" | "settled" | "failed";
interface PersistedRlmResultTurn {
turn_key: string;
generation: number;
status: RlmResultTurnStatus;
provider_completed: boolean;
replied_to_parent: boolean;
notice_admitted: boolean;
error?: string;
}
interface PersistedRlmResultState {
next_generation: number;
latest_settled?: number;
turn?: PersistedRlmResultTurn;
}
interface PersistedRlmChildContract extends RlmChildContract {
format_version: typeof RLM_CONTRACT_FORMAT_VERSION;
child_id: string;
session_name: string;
session_id?: string;
result_state?: PersistedRlmResultState;
}
export interface PreparedRlmChildContract {
childId: string;
sessionName: string;
sessionId?: string;
sessionDir: string;
contractPath: string;
resultState?: PersistedRlmResultState;
resultPath?: string;
contract: RlmChildContract;
}
export interface RlmResultTurn {
turnKey: string;
generation: number;
path: string;
status: RlmResultTurnStatus;
providerCompleted: boolean;
repliedToParent: boolean;
noticeAdmitted: boolean;
error?: string;
}
export interface RlmResultTurnSettlement {
prepared: PreparedRlmChildContract;
turn: RlmResultTurn;
error?: string;
}
export interface RlmResultRead {
child_id: string;
generation: number;
path: string;
sha256: string;
result: Record<string, unknown>;
}
export interface RlmResultSubmission {
child_id: string;
generation: number;
path: string;
sha256: string;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function assertKnownKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {
const unknown = Object.keys(value).filter((key) => !keys.includes(key));
if (unknown.length > 0) throw new Error(`${label} has unsupported fields: ${unknown.sort().join(", ")}`);
}
function cloneJsonRecord(value: Record<string, unknown>, label: string, maxBytes: number): Record<string, unknown> {
assertJsonValue(value, label);
let encoded: string;
try {
encoded = JSON.stringify(value);
} catch (error) {
throw new Error(`${label} must be JSON-serializable: ${error instanceof Error ? error.message : String(error)}`);
}
if (Buffer.byteLength(encoded) > maxBytes) throw new Error(`${label} exceeds ${maxBytes} bytes`);
const parsed: unknown = JSON.parse(encoded);
if (!isRecord(parsed)) throw new Error(`${label} must be an object`);
return parsed;
}
const JSON_SCHEMA_TYPES = new Set(["array", "boolean", "integer", "null", "number", "object", "string"]);
const JSON_SCHEMA_KEYS = new Set([
"$schema",
"additionalProperties",
"allOf",
"anyOf",
"const",
"default",
"description",
"enum",
"exclusiveMaximum",
"exclusiveMinimum",
"items",
"maxItems",
"maxLength",
"maxProperties",
"maximum",
"minItems",
"minLength",
"minProperties",
"minimum",
"multipleOf",
"not",
"oneOf",
"properties",
"required",
"title",
"type",
"uniqueItems",
]);
function assertJsonValue(value: unknown, label: string, depth = 0): void {
if (depth > 32) throw new Error(`${label} exceeds the maximum JSON nesting depth`);
if (value === null || typeof value === "string" || typeof value === "boolean") return;
if (typeof value === "number") {
if (!Number.isFinite(value)) throw new Error(`${label} contains a non-finite number`);
if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
throw new Error(`${label} contains an integer outside the JavaScript safe range`);
}
return;
}
if (Array.isArray(value)) {
value.forEach((item, index) => {
assertJsonValue(item, `${label}[${index}]`, depth + 1);
});
return;
}
if (isRecord(value)) {
for (const [key, item] of Object.entries(value)) assertJsonValue(item, `${label}.${key}`, depth + 1);
return;
}
throw new Error(`${label} contains a non-JSON value`);
}
function assertNonNegativeInteger(value: unknown, label: string): void {
if (!Number.isInteger(value) || (value as number) < 0) throw new Error(`${label} must be a non-negative integer`);
}
function assertFiniteNumber(value: unknown, label: string): void {
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${label} must be a finite number`);
}
function assertSupportedSchema(value: unknown, label: string, depth = 0): void {
if (!isRecord(value)) throw new Error(`${label} must be an object`);
if (depth > 16) throw new Error(`${label} exceeds the supported schema nesting depth`);
assertKnownKeys(value, [...JSON_SCHEMA_KEYS], label);
if (value.type !== undefined) {
const types = Array.isArray(value.type) ? value.type : [value.type];
if (types.length === 0 || !types.every((type) => typeof type === "string" && JSON_SCHEMA_TYPES.has(type))) {
throw new Error(`${label}.type is unsupported`);
}
}
if (value.properties !== undefined) {
if (!isRecord(value.properties)) throw new Error(`${label}.properties must be an object`);
for (const [key, schema] of Object.entries(value.properties)) {
assertSupportedSchema(schema, `${label}.properties.${key}`, depth + 1);
}
}
if (value.required !== undefined) {
if (!Array.isArray(value.required) || !value.required.every((field) => typeof field === "string")) {
throw new Error(`${label}.required must be an array of strings`);
}
if (new Set(value.required).size !== value.required.length) {
throw new Error(`${label}.required must not contain duplicates`);
}
}
if (value.additionalProperties !== undefined && typeof value.additionalProperties !== "boolean") {
assertSupportedSchema(value.additionalProperties, `${label}.additionalProperties`, depth + 1);
}
if (value.items !== undefined) assertSupportedSchema(value.items, `${label}.items`, depth + 1);
for (const keyword of ["allOf", "anyOf", "oneOf"] as const) {
const schemas = value[keyword];
if (schemas === undefined) continue;
if (!Array.isArray(schemas) || schemas.length === 0) throw new Error(`${label}.${keyword} must be non-empty`);
schemas.forEach((schema, index) => {
assertSupportedSchema(schema, `${label}.${keyword}[${index}]`, depth + 1);
});
}
if (value.not !== undefined) assertSupportedSchema(value.not, `${label}.not`, depth + 1);
if (value.enum !== undefined && (!Array.isArray(value.enum) || value.enum.length === 0)) {
throw new Error(`${label}.enum must be a non-empty array`);
}
for (const keyword of [
"minItems",
"maxItems",
"minLength",
"maxLength",
"minProperties",
"maxProperties",
] as const) {
if (value[keyword] !== undefined) assertNonNegativeInteger(value[keyword], `${label}.${keyword}`);
}
for (const keyword of ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"] as const) {
if (value[keyword] !== undefined) assertFiniteNumber(value[keyword], `${label}.${keyword}`);
}
if (value.multipleOf !== undefined && (value.multipleOf as number) <= 0) {
throw new Error(`${label}.multipleOf must be greater than zero`);
}
if (value.uniqueItems !== undefined && typeof value.uniqueItems !== "boolean") {
throw new Error(`${label}.uniqueItems must be a boolean`);
}
for (const keyword of ["$schema", "description", "title"] as const) {
if (value[keyword] !== undefined && typeof value[keyword] !== "string") {
throw new Error(`${label}.${keyword} must be a string`);
}
}
}
function normalizeSchema(value: unknown): Record<string, unknown> {
if (!isRecord(value)) throw new Error("rlm.run result.schema must be an object");
const schema = cloneJsonRecord(value, "rlm.run result.schema", MAX_SCHEMA_BYTES);
assertSupportedSchema(schema, "rlm.run result.schema");
if (schema.type !== "object") throw new Error('rlm.run result.schema type must be "object"');
if (!isRecord(schema.properties)) throw new Error("rlm.run result.schema properties must be an object");
if (schema.required !== undefined) {
if (!Array.isArray(schema.required) || !schema.required.every((field) => typeof field === "string")) {
throw new Error("rlm.run result.schema required must be an array of strings");
}
for (const field of schema.required) {
if (!Object.hasOwn(schema.properties, field)) {
throw new Error(`rlm.run result.schema requires unknown field ${field}`);
}
}
}
return schema;
}
function normalizeResultContract(value: unknown): RlmResultContract | undefined {
if (value === undefined) return undefined;
if (!isRecord(value)) throw new Error("rlm.run result must be an object");
assertKnownKeys(value, ["schema", "sink", "required"], "rlm.run result");
const sink = value.sink ?? { kind: "file" };
if (!isRecord(sink) || sink.kind !== "file" || Object.keys(sink).some((key) => key !== "kind")) {
throw new Error('rlm.run result.sink must be {"kind": "file"}');
}
const required = value.required ?? true;
if (typeof required !== "boolean") throw new Error("rlm.run result.required must be a boolean");
return { schema: normalizeSchema(value.schema), sink: { kind: "file" }, required };
}
function normalizePayloadPolicy(value: unknown, label: string): RlmPayloadContextPolicy | undefined {
if (value === undefined) return undefined;
if (!isRecord(value)) throw new Error(`${label} must be an object`);
assertKnownKeys(value, ["payload", "notify"], label);
if (typeof value.payload !== "boolean") throw new Error(`${label}.payload must be a boolean`);
const notify = value.notify ?? true;
if (typeof notify !== "boolean") throw new Error(`${label}.notify must be a boolean`);
if (value.payload && !notify) throw new Error(`${label}.notify cannot be false while payload is true`);
return { payload: value.payload, notify };
}
function normalizeResultContextPolicy(
value: unknown,
result: RlmResultContract | undefined,
): RlmResultContextPolicy | undefined {
if (value === undefined) return undefined;
if (!result) throw new Error("rlm.run context.result requires a result contract");
if (!isRecord(value)) throw new Error("rlm.run context.result must be an object");
assertKnownKeys(value, ["fields"], "rlm.run context.result");
if (!Array.isArray(value.fields) || !value.fields.every((field) => typeof field === "string" && field.length > 0)) {
throw new Error("rlm.run context.result.fields must be an array of non-empty strings");
}
const fields = [...new Set(value.fields)];
if (fields.length !== value.fields.length)
throw new Error("rlm.run context.result.fields must not contain duplicates");
const properties = result.schema.properties as Record<string, unknown>;
for (const field of fields) {
if (!Object.hasOwn(properties, field)) {
throw new Error(`rlm.run context.result admits unknown schema field ${field}`);
}
}
return { fields };
}
function normalizeContextContract(
value: unknown,
result: RlmResultContract | undefined,
): RlmChildContextContract | undefined {
if (value === undefined) return undefined;
if (!isRecord(value)) throw new Error("rlm.run context must be an object");
assertKnownKeys(value, ["terminal", "direct_messages", "observe", "result"], "rlm.run context");
if (value.observe !== undefined && typeof value.observe !== "boolean") {
throw new Error("rlm.run context.observe must be a boolean");
}
return {
...(value.terminal !== undefined
? { terminal: normalizePayloadPolicy(value.terminal, "rlm.run context.terminal")! }
: {}),
...(value.direct_messages !== undefined
? { direct_messages: normalizePayloadPolicy(value.direct_messages, "rlm.run context.direct_messages")! }
: {}),
...(value.observe !== undefined ? { observe: value.observe } : {}),
...(value.result !== undefined ? { result: normalizeResultContextPolicy(value.result, result)! } : {}),
};
}
export const DEFAULT_GUARDED_CONTEXT: RlmChildContextContract = {
terminal: { payload: false, notify: true },
direct_messages: { payload: false, notify: true },
observe: false,
};
/**
* Guarded children communicate through their declared result contract only:
* terminal and direct-message payloads are withheld behind fixed host notices
* and observation previews are stripped. Explicit context entries override the
* corresponding default; unlisted entries inherit the guarded default.
*/
export function normalizeRlmChildContract(
result: unknown,
context: unknown,
options: { guardedByDefault?: boolean } = {},
): RlmChildContract | undefined {
const normalizedResult = normalizeResultContract(result);
const guarded = options.guardedByDefault === true && normalizedResult !== undefined;
const mergedContext = guarded ? { ...DEFAULT_GUARDED_CONTEXT, ...(isRecord(context) ? context : {}) } : context;
const normalizedContext = normalizeContextContract(mergedContext, normalizedResult);
if (!normalizedResult && !normalizedContext) return undefined;
return {
...(normalizedResult ? { result: normalizedResult } : {}),
...(normalizedContext ? { context: normalizedContext } : {}),
};
}
function resultPathForGeneration(sessionDir: string, generation: number): string {
return join(sessionDir, generation === 0 ? RLM_RESULT_FILE_NAME : `rlm-result-${generation}.json`);
}
function resultSealPathForGeneration(sessionDir: string, generation: number): string {
return `${resultPathForGeneration(sessionDir, generation)}.settled`;
}
function pathExists(path: string): boolean {
try {
lstatSync(path);
return true;
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
throw error;
}
}
function preparedForGeneration(prepared: PreparedRlmChildContract, generation: number): PreparedRlmChildContract {
if (!prepared.contract.result || !prepared.resultState) throw new Error("RLM child has no result contract");
if (!Number.isSafeInteger(generation) || generation < 0 || generation >= prepared.resultState.next_generation) {
throw new Error(`RLM result generation ${generation} is not available`);
}
return { ...prepared, resultPath: resultPathForGeneration(prepared.sessionDir, generation) };
}
function persistedContract(prepared: PreparedRlmChildContract): PersistedRlmChildContract {
return {
format_version: RLM_CONTRACT_FORMAT_VERSION,
child_id: prepared.childId,
session_name: prepared.sessionName,
...(prepared.sessionId ? { session_id: prepared.sessionId } : {}),
...(prepared.resultState ? { result_state: prepared.resultState } : {}),
...prepared.contract,
};
}
function replaceContractFile(path: string, value: PersistedRlmChildContract): void {
const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
writeFileSync(
temporary,
`${JSON.stringify(value)}
`,
{ encoding: "utf8", flag: "wx", mode: 0o600 },
);
renameSync(temporary, path);
}
export function prepareRlmChildContract(
sessionDir: string,
childId: string,
sessionName: string,
contract: RlmChildContract | undefined,
): PreparedRlmChildContract | undefined {
if (!contract) return undefined;
const resolvedDir = resolve(sessionDir);
const prepared: PreparedRlmChildContract = {
childId,
sessionName,
sessionDir: resolvedDir,
contractPath: join(resolvedDir, RLM_CONTRACT_FILE_NAME),
resultState: { next_generation: 0 },
...(contract.result ? { resultPath: resultPathForGeneration(resolvedDir, 0) } : {}),
contract,
};
writeFileSync(
prepared.contractPath,
`${JSON.stringify(persistedContract(prepared))}
`,
{
encoding: "utf8",
flag: "wx",
mode: 0o600,
},
);
return prepared;
}
export function bindRlmChildContractSession(
prepared: PreparedRlmChildContract,
sessionId: string,
): PreparedRlmChildContract {
const bound = { ...prepared, sessionId };
replaceContractFile(bound.contractPath, persistedContract(bound));
return bound;
}
export function currentRlmResultTurn(prepared: PreparedRlmChildContract): RlmResultTurn | undefined {
const turn = prepared.resultState?.turn;
return turn
? {
turnKey: turn.turn_key,
generation: turn.generation,
path: prepared.contract.result ? resultPathForGeneration(prepared.sessionDir, turn.generation) : "",
status: turn.status,
providerCompleted: turn.provider_completed,
repliedToParent: turn.replied_to_parent,
noticeAdmitted: turn.notice_admitted,
...(turn.error ? { error: turn.error } : {}),
}
: undefined;
}
export function beginRlmChildResultTurn(
prepared: PreparedRlmChildContract,
turnKey: string,
): { prepared: PreparedRlmChildContract; turn: RlmResultTurn } {
if (!prepared.resultState) throw new Error("RLM child has no persisted turn state");
if (!turnKey) throw new Error("RLM result turn key must not be empty");
const current = prepared.resultState.turn;
if (current?.turn_key === turnKey) {
const turn = currentRlmResultTurn(prepared)!;
return { prepared, turn };
}
if (current?.status === "active") {
throw new Error(`RLM result turn ${current.turn_key} is still active`);
}
const generation = prepared.resultState.next_generation;
if (generation >= Number.MAX_SAFE_INTEGER) throw new Error("RLM result generation limit reached");
const resultPath = prepared.contract.result ? resultPathForGeneration(prepared.sessionDir, generation) : undefined;
if (
resultPath &&
(pathExists(resultPath) || pathExists(resultSealPathForGeneration(prepared.sessionDir, generation)))
) {
throw new Error(`RLM result generation ${generation} already exists without a matching turn`);
}
const turnState: PersistedRlmResultTurn = {
turn_key: turnKey,
generation,
status: "active",
provider_completed: false,
replied_to_parent: false,
notice_admitted: false,
};
const advanced: PreparedRlmChildContract = {
...prepared,
resultState: {
...prepared.resultState,
next_generation: generation + 1,
turn: turnState,
},
...(resultPath ? { resultPath } : {}),
};
replaceContractFile(advanced.contractPath, persistedContract(advanced));
return { prepared: advanced, turn: currentRlmResultTurn(advanced)! };
}
export function completeRlmChildResultTurn(
prepared: PreparedRlmChildContract,
turnKey: string,
repliedToParent: boolean,
): { prepared: PreparedRlmChildContract; turn: RlmResultTurn } {
const turn = prepared.resultState?.turn;
if (!turn || turn.turn_key !== turnKey) throw new Error(`Unknown RLM result turn ${turnKey}`);
if (turn.status !== "active") return { prepared, turn: currentRlmResultTurn(prepared)! };
const completed: PreparedRlmChildContract = {
...prepared,
resultState: {
...prepared.resultState!,
turn: {
...turn,
provider_completed: true,
replied_to_parent: turn.replied_to_parent || repliedToParent,
},
},
};
replaceContractFile(completed.contractPath, persistedContract(completed));
return { prepared: completed, turn: currentRlmResultTurn(completed)! };
}
export function markRlmChildResultTurnNotified(
prepared: PreparedRlmChildContract,
turnKey: string,
): PreparedRlmChildContract {
const turn = prepared.resultState?.turn;
if (!turn || turn.turn_key !== turnKey) throw new Error(`Unknown RLM result turn ${turnKey}`);
if (turn.notice_admitted) return prepared;
const notified: PreparedRlmChildContract = {
...prepared,
resultState: { ...prepared.resultState!, turn: { ...turn, notice_admitted: true } },
};
replaceContractFile(notified.contractPath, persistedContract(notified));
return notified;
}
export function failRlmChildResultTurn(
prepared: PreparedRlmChildContract,
turnKey: string,
error?: string,
): PreparedRlmChildContract {
const turn = prepared.resultState?.turn;
if (!turn || turn.turn_key !== turnKey) throw new Error(`Unknown RLM result turn ${turnKey}`);
if (turn.status === "settled") return prepared;
const normalizedError = error?.slice(0, 4096);
if (turn.status === "failed" && (!normalizedError || turn.error === normalizedError)) return prepared;
const failed: PreparedRlmChildContract = {
...prepared,
resultState: {
...prepared.resultState!,
turn: {
...turn,
status: "failed",
...(normalizedError ? { error: normalizedError } : {}),
},
},
};
replaceContractFile(failed.contractPath, persistedContract(failed));
return failed;
}
export function loadRlmChildContract(sessionDir: string): PreparedRlmChildContract | undefined {
const resolvedDir = resolve(sessionDir);
const contractPath = join(resolvedDir, RLM_CONTRACT_FILE_NAME);
let raw: string;
try {
const stats = lstatSync(contractPath);
if (!stats.isFile() || stats.isSymbolicLink()) {
throw new Error(`RLM child contract is not a regular file: ${contractPath}`);
}
if (stats.size > MAX_CONTRACT_BYTES) {
throw new Error(`RLM child contract exceeds ${MAX_CONTRACT_BYTES} bytes`);
}
raw = readFileSync(contractPath, "utf8");
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") return undefined;
throw error;
}
const value: unknown = JSON.parse(raw);
if (!isRecord(value) || value.format_version !== RLM_CONTRACT_FORMAT_VERSION) {
throw new Error(`Invalid RLM child contract at ${contractPath}`);
}
assertKnownKeys(
value,
["format_version", "child_id", "session_name", "session_id", "result_state", "result", "context"],
"persisted RLM child contract",
);
if (
typeof value.child_id !== "string" ||
!value.child_id ||
typeof value.session_name !== "string" ||
!value.session_name ||
(value.session_id !== undefined && (typeof value.session_id !== "string" || !value.session_id))
) {
throw new Error(`Invalid RLM child identity at ${contractPath}`);
}
const contract = normalizeRlmChildContract(value.result, value.context) ?? {};
let resultState: PersistedRlmResultState | undefined;
{
const rawState = value.result_state ?? { next_generation: 0 };
if (!isRecord(rawState)) throw new Error(`Invalid RLM result state at ${contractPath}`);
assertKnownKeys(rawState, ["next_generation", "latest_settled", "turn"], "persisted RLM result state");
if (!Number.isSafeInteger(rawState.next_generation) || (rawState.next_generation as number) < 0) {
throw new Error(`Invalid RLM result generation state at ${contractPath}`);
}
const nextGeneration = rawState.next_generation as number;
const latestSettled = rawState.latest_settled;
if (!contract.result && latestSettled !== undefined) {
throw new Error(`Result generation without an RLM result contract at ${contractPath}`);
}
if (
latestSettled !== undefined &&
(!Number.isSafeInteger(latestSettled) ||
(latestSettled as number) < 0 ||
(latestSettled as number) >= nextGeneration)
) {
throw new Error(`Invalid latest RLM result generation at ${contractPath}`);
}
let turn: PersistedRlmResultTurn | undefined;
if (rawState.turn !== undefined) {
if (!isRecord(rawState.turn)) throw new Error(`Invalid RLM result turn at ${contractPath}`);
assertKnownKeys(
rawState.turn,
["turn_key", "generation", "status", "provider_completed", "replied_to_parent", "notice_admitted", "error"],
"persisted RLM result turn",
);
if (
typeof rawState.turn.turn_key !== "string" ||
!rawState.turn.turn_key ||
!Number.isSafeInteger(rawState.turn.generation) ||
(rawState.turn.generation as number) < 0 ||
(rawState.turn.generation as number) >= nextGeneration ||
!(["active", "settled", "failed"] as unknown[]).includes(rawState.turn.status) ||
(rawState.turn.provider_completed !== undefined && typeof rawState.turn.provider_completed !== "boolean") ||
(rawState.turn.replied_to_parent !== undefined && typeof rawState.turn.replied_to_parent !== "boolean") ||
(rawState.turn.notice_admitted !== undefined && typeof rawState.turn.notice_admitted !== "boolean") ||
(rawState.turn.error !== undefined &&
(typeof rawState.turn.error !== "string" || rawState.turn.error.length > 4096))
) {
throw new Error(`Invalid RLM result turn at ${contractPath}`);
}
const status = rawState.turn.status as RlmResultTurnStatus;
turn = {
turn_key: rawState.turn.turn_key,
generation: rawState.turn.generation as number,
status,
provider_completed:
rawState.turn.provider_completed === undefined ? status !== "active" : rawState.turn.provider_completed,
replied_to_parent: rawState.turn.replied_to_parent === true,
notice_admitted: rawState.turn.notice_admitted === true,
...(typeof rawState.turn.error === "string" ? { error: rawState.turn.error } : {}),
};
}
resultState = {
next_generation: nextGeneration,
...(typeof latestSettled === "number" ? { latest_settled: latestSettled } : {}),
...(turn ? { turn } : {}),
};
}
const resultGeneration = resultState?.turn?.generation ?? resultState?.next_generation;
return {
childId: value.child_id,
sessionName: value.session_name,
...(typeof value.session_id === "string" ? { sessionId: value.session_id } : {}),
sessionDir: resolvedDir,
contractPath,
...(resultState ? { resultState } : {}),
...(contract.result && resultGeneration !== undefined
? { resultPath: resultPathForGeneration(resolvedDir, resultGeneration) }
: {}),
contract,
};
}
export function formatRlmResultContractInstructions(prepared: PreparedRlmChildContract | undefined): string {
if (!prepared?.resultPath || !prepared.contract.result) return "";
const generation = prepared.resultState?.turn?.generation;
const generationLabel = generation === undefined ? "" : ` for result generation ${generation}`;
return [
"",
"[host result contract]",
prepared.contract.result.required
? `Submit exactly one JSON object with \`await rlm.submit_result(value)\`${generationLabel} before finishing.`
: `Structured result submission is optional${generationLabel}. If you produce one, submit exactly one JSON object with \`await rlm.submit_result(value)\`.`,
"The host validates and persists any submitted result. It must match this JSON Schema:",
JSON.stringify(prepared.contract.result.schema),
].join("\n");
}
function resultValidationError(schema: Record<string, unknown>, value: unknown): string | undefined {
let valid: boolean;
try {
valid = Schema.Check(schema as Schema.XSchema, value);
} catch (error) {
throw new Error(
`RLM result schema could not be evaluated: ${error instanceof Error ? error.message : String(error)}`,
);
}
if (valid) return undefined;
const [, errors] = Schema.Errors(schema as Schema.XSchema, value);
return errors
.slice(0, 3)
.map((error) => `${error.instancePath || "/"} ${error.message}`)
.join("; ");
}
export function submitRlmChildResult(prepared: PreparedRlmChildContract, value: unknown): RlmResultSubmission {
const resultContract = prepared.contract.result;
const turn = prepared.resultState?.turn;
if (!prepared.resultPath || !resultContract || !turn) throw new Error("RLM child has no active result turn");
if (turn.status !== "active") throw new Error(`RLM result turn ${turn.turn_key} is already ${turn.status}`);
if (!isRecord(value)) throw new Error("RLM result must be a JSON object");
const normalized = cloneJsonRecord(value, "RLM result", MAX_RESULT_BYTES);
const validationError = resultValidationError(resultContract.schema, normalized);
if (validationError !== undefined) {
throw new Error(`RLM result does not match its schema${validationError ? `: ${validationError}` : ""}`);
}
const encoded = Buffer.from(`${JSON.stringify(normalized)}
`);
try {
writeFileSync(prepared.resultPath, encoded, { flag: "wx", mode: 0o600 });
} catch (error) {
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
const existing = readResultFile(prepared.resultPath);
if (!existing.equals(encoded)) {
throw new Error(`RLM result generation ${turn.generation} was already submitted with different content`);
}
}
return {
child_id: prepared.childId,
generation: turn.generation,
path: prepared.resultPath,
sha256: createHash("sha256").update(encoded).digest("hex"),
};
}
function readResultFile(path: string): Buffer {
const stats = lstatSync(path);
if (!stats.isFile() || stats.isSymbolicLink()) throw new Error(`RLM result is not a regular file: ${path}`);
if (stats.size > MAX_RESULT_BYTES) throw new Error(`RLM result exceeds ${MAX_RESULT_BYTES} bytes`);
return readFileSync(path);
}
function validatedRlmChildResult(prepared: PreparedRlmChildContract): {
bytes: Buffer;
path: string;
value: Record<string, unknown>;
} {
const resultContract = prepared.contract.result;
if (!prepared.resultPath || !resultContract) throw new Error("RLM child has no result contract");
const bytes = readResultFile(prepared.resultPath);
let value: unknown;
try {
value = JSON.parse(bytes.toString("utf8"));
} catch (error) {
throw new Error(`RLM result is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
}
assertJsonValue(value, "RLM result");
if (!isRecord(value)) throw new Error("RLM result must be a JSON object");
const validationError = resultValidationError(resultContract.schema, value);
if (validationError !== undefined) {
throw new Error(`RLM result does not match its schema${validationError ? `: ${validationError}` : ""}`);
}
return { bytes, path: prepared.resultPath, value };
}
function resultSeal(prepared: PreparedRlmChildContract, generation: number, sha256: string): Buffer {
return Buffer.from(
`${JSON.stringify({
child_id: prepared.childId,
...(prepared.sessionId ? { session_id: prepared.sessionId } : {}),
generation,
sha256,
})}
`,
);
}
function assertSettledResult(prepared: PreparedRlmChildContract, generation: number, bytes: Buffer): void {
const sealPath = resultSealPathForGeneration(prepared.sessionDir, generation);
const seal = readResultFile(sealPath);
const expected = resultSeal(prepared, generation, createHash("sha256").update(bytes).digest("hex"));
if (!seal.equals(expected)) throw new Error(`RLM result generation ${generation} has an invalid settlement seal`);
}
export function settleRlmChildResultTurn(prepared: PreparedRlmChildContract, turnKey: string): RlmResultTurnSettlement {
const turn = prepared.resultState?.turn;
if (!turn || turn.turn_key !== turnKey) throw new Error(`Unknown RLM result turn ${turnKey}`);
if (turn.status === "settled") return { prepared, turn: currentRlmResultTurn(prepared)! };
if (turn.status === "failed") {
return { prepared, turn: currentRlmResultTurn(prepared)!, error: `RLM result turn ${turnKey} already failed` };
}
if (!prepared.contract.result) {
const settled: PreparedRlmChildContract = {
...prepared,
resultState: {
...prepared.resultState!,
turn: { ...turn, status: "settled" },
},
};
replaceContractFile(settled.contractPath, persistedContract(settled));
return { prepared: settled, turn: currentRlmResultTurn(settled)! };
}
let validated: ReturnType<typeof validatedRlmChildResult> | undefined;
try {
validated = validatedRlmChildResult(preparedForGeneration(prepared, turn.generation));
} catch (error) {
const missing = error instanceof Error && "code" in error && error.code === "ENOENT";
if (!missing || prepared.contract.result?.required) {
const message = error instanceof Error ? error.message : String(error);
const failed = failRlmChildResultTurn(prepared, turnKey, message);
return {
prepared: failed,
turn: currentRlmResultTurn(failed)!,
error: message,
};
}
}
if (validated) {
const sha256 = createHash("sha256").update(validated.bytes).digest("hex");
const sealPath = resultSealPathForGeneration(prepared.sessionDir, turn.generation);
const encodedSeal = resultSeal(prepared, turn.generation, sha256);
try {
writeFileSync(sealPath, encodedSeal, { flag: "wx", mode: 0o600 });
} catch (error) {
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
const existing = readResultFile(sealPath);
if (!existing.equals(encodedSeal)) throw new Error(`RLM result generation ${turn.generation} seal conflicts`);
}
}
const settled: PreparedRlmChildContract = {
...prepared,
resultState: {
...prepared.resultState!,
...(validated ? { latest_settled: turn.generation } : {}),
turn: { ...turn, status: "settled" },
},
};
replaceContractFile(settled.contractPath, persistedContract(settled));
return { prepared: settled, turn: currentRlmResultTurn(settled)! };
}
export function validateRlmChildResult(prepared: PreparedRlmChildContract, generation?: number): void {
const selectedGeneration = generation ?? prepared.resultState?.latest_settled;
if (selectedGeneration === undefined) throw new Error("RLM child has no settled result");
const selected = preparedForGeneration(prepared, selectedGeneration);
const { bytes } = validatedRlmChildResult(selected);
assertSettledResult(prepared, selectedGeneration, bytes);
}
export function readRlmChildResult(
prepared: PreparedRlmChildContract,
requestedFields?: readonly string[],
generation?: number,
): RlmResultRead {
const resultPolicy = prepared.contract.context?.result;
if (!resultPolicy) throw new Error("RLM child contract does not admit result fields into parent context");
const fields = requestedFields ?? resultPolicy.fields;
if (new Set(fields).size !== fields.length || fields.some((field) => !resultPolicy.fields.includes(field))) {
throw new Error("Requested RLM result fields exceed the child context contract");
}
const selectedGeneration = generation ?? prepared.resultState?.latest_settled;
if (selectedGeneration === undefined) throw new Error("RLM child has no settled result");
const selected = preparedForGeneration(prepared, selectedGeneration);
const { bytes, path, value } = validatedRlmChildResult(selected);
assertSettledResult(prepared, selectedGeneration, bytes);
const projected = Object.fromEntries(
fields.filter((field) => Object.hasOwn(value, field)).map((field) => [field, value[field]]),
);
if (Buffer.byteLength(JSON.stringify(projected)) > MAX_PROJECTED_RESULT_BYTES) {
throw new Error(`Projected RLM result exceeds ${MAX_PROJECTED_RESULT_BYTES} bytes`);
}
return {
child_id: prepared.childId,
generation: selectedGeneration,
path,
sha256: createHash("sha256").update(bytes).digest("hex"),
result: projected,
};
}