Bifrost/tools/dispatch-agent-requests.mjs
Meta Vens 42728a677a Remove unused postBridgeJson/postDispatchRunJson and their ensureTrailingSlash copies
REC-099: `postBridgeJson` (tools/bifrost-bridge.mjs) and
`postDispatchRunJson` (tools/dispatch-agent-requests.mjs) each occur
exactly once in their file: their own definition, with no caller.
Each was the sole caller of that file's private `ensureTrailingSlash`
copy, which becomes dead in the same cut. The `ensureTrailingSlash`
copies in agent-transport.mjs and governance-threads.mjs are separate,
still-called definitions and were left untouched.

Kill attempts that failed: none.
- `rg -n "postBridgeJson" tools/*.mjs` -> only its own (now removed)
  definition.
- `rg -n "postDispatchRunJson" tools/*.mjs` -> only its own (now
  removed) definition.
- `rg -n "ensureTrailingSlash" tools/bifrost-bridge.mjs` and the same
  against tools/dispatch-agent-requests.mjs -> confirmed each file's
  copy had exactly one caller (the function just removed) before
  cutting it.

Checks run: `node --check tools/bifrost-bridge.mjs` and
`node --check tools/dispatch-agent-requests.mjs` -> both syntax OK. No
package.json/test suite exists for these standalone tool scripts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 14:41:21 +02:00

1023 lines
31 KiB
JavaScript

#!/usr/bin/env node
import { spawn, spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const bifrostRoot = resolve(scriptDir, "..");
const transportCli = resolve(bifrostRoot, "tools", "agent-transport.mjs");
const bridgeCli = resolve(bifrostRoot, "tools", "bifrost-bridge.mjs");
const defaultStatusDir = resolve(bifrostRoot, ".bifrost", "agent-dispatch");
const defaultProjectsRoot = resolve(bifrostRoot, "..");
const defaultAquariumChannelId = "1501196543150264332";
const defaultPersonaName = "Bifrost";
const defaultPersonaAvatarUrl =
"https://raw.githubusercontent.com/GameCult/Bifrost/main/src/Bifrost.Web/wwwroot/img/bifrost-profile.png";
async function main() {
loadBifrostLocalEnv();
const [command, ...rawArgs] = process.argv.slice(2);
const options = parseArgs(rawArgs);
if (!command || command === "help" || command === "--help" || command === "-h") {
printHelp();
return;
}
if (command === "dispatch" || command === "run-claimed") {
ensureDispatchReceiptGate(options);
}
switch (command) {
case "dispatch":
await dispatchQueuedRequests(options);
return;
case "run-claimed":
await runClaimedRequest(options);
return;
default:
throw new Error(`Unknown command "${command}". Run "node tools/dispatch-agent-requests.mjs help".`);
}
}
function loadBifrostLocalEnv() {
if (process.env.BIFROST_SKIP_LOCAL_ENV === "true") {
return;
}
loadLocalEnv(resolve(bifrostRoot, ".env"));
loadLocalEnv(resolve(defaultProjectsRoot, "VoidBot", ".env"));
}
async function dispatchQueuedRequests(options) {
const max = parseInteger(options.max ?? "1", "max");
const statusDir = resolveOptionPath(options["status-dir"] ?? defaultStatusDir);
await mkdir(statusDir, { recursive: true });
const dispatched = [];
for (let index = 0; index < max; index += 1) {
const request = claimNextRequest(options);
if (!request) {
break;
}
const repoRoot = resolveRepoRoot(request, options);
const runDir = resolve(statusDir, request.id);
mkdirSync(runDir, { recursive: true });
const requestPath = resolve(runDir, "request.json");
const promptPath = resolve(runDir, "prompt.md");
const logPath = resolve(runDir, "codex.log");
writeFileSync(requestPath, `${JSON.stringify(request, null, 2)}\n`, "utf8");
writeFileSync(promptPath, buildCodexPrompt(request, repoRoot), "utf8");
const child = spawn(
process.execPath,
[
fileURLToPath(import.meta.url),
"run-claimed",
"--request-file", requestPath,
"--repo-root", repoRoot,
"--prompt-file", promptPath,
"--log", logPath,
...optionalArg("--codex-executable", options["codex-executable"]),
...optionalArg("--codex-exec-args", options["codex-exec-args"]),
...optionalArg("--model", options.model),
...optionalArg("--reasoning-effort", options["reasoning-effort"]),
...optionalArg("--sandbox", options.sandbox),
...optionalArg("--launch-mode", options["launch-mode"]),
...optionalArg("--channel-id", options["channel-id"]),
...optionalArg("--persona-name", options["persona-name"]),
...optionalArg("--persona-avatar-url", options["persona-avatar-url"]),
...(options["no-discord"] === "true" ? ["--no-discord", "true"] : []),
],
{
cwd: bifrostRoot,
detached: true,
stdio: "ignore",
windowsHide: true,
},
);
child.unref();
const dispatchRecord = {
requestId: request.id,
title: request.title,
repoRoot,
promptPath,
logPath,
pid: child.pid,
dispatchedAt: new Date().toISOString(),
};
writeFileSync(resolve(runDir, "dispatch.json"), `${JSON.stringify(dispatchRecord, null, 2)}\n`, "utf8");
dispatched.push(dispatchRecord);
}
printJson({
ok: true,
dispatchedCount: dispatched.length,
dispatched,
});
}
function ensureDispatchReceiptGate(options) {
if (wantsUnreceiptedActivity(options)) {
throw new Error(
"Bifrost dispatch cannot use --allow-unreceipted-activity or BIFROST_ALLOW_UNRECEIPTED_ACTIVITY. " +
"Use the typed Bifrost CultCache/CultMesh dispatch store instead of launching off-book work.",
);
}
if (hasRemovedHttpBridgeConfig()) {
throw new Error(
"BIFROST_BRIDGE_BASE_URL / BIFROST_BRIDGE_TOKEN were removed. " +
"Bifrost dispatch receipts are written to the typed CultCache/CultMesh request/run artifacts.",
);
}
}
function hasRemovedHttpBridgeConfig() {
return Boolean(optionalString(process.env.BIFROST_BRIDGE_BASE_URL) || optionalString(process.env.BIFROST_BRIDGE_TOKEN));
}
function wantsUnreceiptedActivity(options) {
return options["allow-unreceipted-activity"] === "true" || process.env.BIFROST_ALLOW_UNRECEIPTED_ACTIVITY === "true";
}
async function runClaimedRequest(options) {
const requestPath = resolveOptionPath(requireOption(options, "request-file"));
const repoRoot = resolveOptionPath(requireOption(options, "repo-root"));
const promptPath = resolveOptionPath(requireOption(options, "prompt-file"));
const logPath = resolveOptionPath(requireOption(options, "log"));
const request = JSON.parse(await readFile(requestPath, "utf8"));
const launchMode = options["launch-mode"] ?? process.env.BIFROST_CODEX_LAUNCH_MODE ?? "app-server";
if (launchMode === "codex-exec") {
await runClaimedViaCodexExec(request, repoRoot, promptPath, logPath, options);
return;
}
if (launchMode !== "app-server") {
throw new Error(`Unsupported --launch-mode "${launchMode}". Use app-server or codex-exec.`);
}
await runClaimedViaAppServer(request, repoRoot, promptPath, logPath, options);
}
async function runClaimedViaCodexExec(request, repoRoot, promptPath, logPath, options) {
const codexExecutable = options["codex-executable"] ?? process.env.CODEX_EXECUTABLE ?? "codex";
const codexExecArgs = splitCommandArgs(options["codex-exec-args"] ?? process.env.CODEX_EXEC_ARGS ?? "");
const model = options.model ?? process.env.CODEX_MODEL ?? "gpt-5.4";
const reasoningEffort = options["reasoning-effort"] ?? process.env.CODEX_MODEL_REASONING_EFFORT ?? "medium";
const sandbox = options.sandbox ?? "workspace-write";
const startedAt = new Date().toISOString();
const resultPath = resolve(dirname(logPath), "result.json");
await mkdir(dirname(logPath), { recursive: true });
const bridgeContextEnv = buildBridgeContextEnv(request, dirname(logPath));
appendLog(logPath, `started ${startedAt}`);
appendLog(logPath, `request ${request.id}: ${request.title}`);
appendLog(logPath, `repoRoot ${repoRoot}`);
let dispatchRun = null;
try {
dispatchRun = await beginDispatchRun(request, {
launchMode: "codex-exec",
workerProcessId: process.pid,
threadId: "",
turnId: "",
logPath,
resultPath,
note: "Codex dispatch process started.",
});
const prompt = await readFile(promptPath, "utf8");
const result = spawnSync(
codexExecutable,
[
...codexExecArgs,
"exec",
"-m", model,
"-c", 'approval_policy="never"',
"-c", `model_reasoning_effort=${JSON.stringify(reasoningEffort)}`,
"--skip-git-repo-check",
"-s", sandbox,
"-",
],
{
cwd: repoRoot,
input: prompt,
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
env: {
...process.env,
...bridgeContextEnv,
},
windowsHide: true,
},
);
appendLog(logPath, result.stdout ?? "");
appendLog(logPath, result.stderr ?? "");
const launchError = result.error ? summarizeError(result.error) : "";
const ok = result.status === 0 && !launchError;
const closeStatus = ok ? "completed" : "cancelled";
const note = ok
? `Codex dispatch completed from Bifrost. Log: ${logPath}`
: launchError
? `Codex dispatch failed before completion: ${launchError}. Log: ${logPath}`
: `Codex dispatch failed with exit ${result.status ?? "unknown"}. Log: ${logPath}`;
const close = runNodeJson([
transportCli,
"close",
"--id", request.id,
"--status", closeStatus,
"--note", note,
], bifrostRoot);
if (launchError) {
await dispatchRun?.fail({
threadId: "",
turnId: "",
resultPath,
note,
error: launchError,
});
} else {
await dispatchRun?.complete({
status: ok ? "Completed" : "Cancelled",
threadId: "",
turnId: "",
resultPath,
note,
});
}
await writeDispatchResult(logPath, {
requestId: request.id,
ok,
exitCode: result.status,
error: launchError,
launchMode: "codex-exec",
startedAt,
finishedAt: new Date().toISOString(),
close,
});
} catch (error) {
const message = summarizeError(error);
const note = `Codex dispatch failed before completion: ${message}. Log: ${logPath}`;
const close = runNodeJson([
transportCli,
"close",
"--id", request.id,
"--status", "cancelled",
"--note", note,
], bifrostRoot);
await dispatchRun?.fail({
threadId: "",
turnId: "",
resultPath,
note,
error: message,
});
await writeDispatchResult(logPath, {
requestId: request.id,
ok: false,
error: message,
launchMode: "codex-exec",
startedAt,
finishedAt: new Date().toISOString(),
close,
});
throw error;
}
}
async function runClaimedViaAppServer(request, repoRoot, promptPath, logPath, options) {
const model = options.model ?? process.env.CODEX_MODEL ?? "gpt-5.4";
const reasoningEffort = options["reasoning-effort"] ?? process.env.CODEX_MODEL_REASONING_EFFORT ?? "medium";
const sandbox = options.sandbox ?? "workspace-write";
const startedAt = new Date().toISOString();
const prompt = await readFile(promptPath, "utf8");
const resultPath = resolve(dirname(logPath), "result.json");
await mkdir(dirname(logPath), { recursive: true });
const client = new CodexAppServerClient({
logPath,
command: resolveCodexCommand(options),
env: buildBridgeContextEnv(request, dirname(logPath)),
});
appendLog(logPath, `started ${startedAt}`);
appendLog(logPath, `request ${request.id}: ${request.title}`);
appendLog(logPath, `repoRoot ${repoRoot}`);
appendLog(logPath, "launchMode app-server");
let threadId;
let turnId;
const dispatchRun = await beginDispatchRun(request, {
launchMode: "app-server",
workerProcessId: process.pid,
threadId: "",
turnId: "",
logPath,
resultPath,
note: "Codex app-server launch started.",
});
try {
await client.start();
await client.request("initialize", {
clientInfo: {
name: "bifrost-dispatcher",
title: "Bifrost Dispatcher",
version: "0.1.0",
},
capabilities: {
experimentalApi: true,
},
});
const threadStart = await client.request("thread/start", {
cwd: repoRoot,
approvalPolicy: "never",
approvalsReviewer: "user",
sandbox,
model,
personality: "pragmatic",
ephemeral: false,
sessionStartSource: "startup",
experimentalRawEvents: false,
persistExtendedHistory: true,
});
threadId = threadStart.thread?.id;
if (!threadId) {
throw new Error("Codex app-server thread/start returned no thread id.");
}
const turnStart = await client.request("turn/start", {
threadId,
cwd: repoRoot,
approvalPolicy: "never",
approvalsReviewer: "user",
sandboxPolicy: sandboxPolicyFromMode(sandbox),
model,
effort: reasoningEffort,
personality: "pragmatic",
input: [
{
type: "text",
text: prompt,
text_elements: [],
},
],
});
turnId = turnStart.turn?.id;
if (!turnId) {
throw new Error("Codex app-server turn/start returned no turn id.");
}
await writeFile(
resolve(dirname(logPath), "app-server-dispatch.json"),
`${JSON.stringify({
requestId: request.id,
launchMode: "app-server",
threadId,
turnId,
startedAt,
}, null, 2)}\n`,
"utf8",
);
if (options["no-discord"] !== "true") {
postDispatchReceipt(request, { threadId, turnId, startedAt }, options);
}
const completed = await client.waitForTurnCompleted(threadId, turnId);
const ok = completed.turn?.status === "completed";
const closeStatus = ok ? "completed" : "cancelled";
const note = ok
? `Codex app turn completed for ${threadId}/${turnId}.`
: `Codex app turn ended as ${completed.turn?.status ?? "unknown"} for ${threadId}/${turnId}.`;
const close = runNodeJson([
transportCli,
"close",
"--id", request.id,
"--status", closeStatus,
"--note", note,
], bifrostRoot);
await dispatchRun?.complete({
status: ok ? "Completed" : "Cancelled",
threadId,
turnId,
resultPath,
note,
});
await writeDispatchResult(logPath, {
requestId: request.id,
ok,
launchMode: "app-server",
threadId,
turnId,
startedAt,
finishedAt: new Date().toISOString(),
close,
});
} catch (error) {
const message = summarizeError(error);
appendLog(logPath, message);
if (!threadId || !turnId) {
releaseClaimedRequest(request.id, `Visible Codex app turn did not start: ${message}`);
} else {
runNodeJson([
transportCli,
"close",
"--id", request.id,
"--status", "cancelled",
"--note", `Codex app dispatch failed after turn start for ${threadId}/${turnId}.`,
], bifrostRoot);
}
await dispatchRun?.fail({
threadId: threadId ?? "",
turnId: turnId ?? "",
resultPath,
note: "Codex dispatch failed.",
error: message,
});
await writeDispatchResult(logPath, {
requestId: request.id,
ok: false,
launchMode: "app-server",
threadId,
turnId,
startedAt,
finishedAt: new Date().toISOString(),
error: message,
});
process.exitCode = 1;
} finally {
client.stop();
}
}
function claimNextRequest(options) {
const repo = optionalString(options.repo);
const agent = optionalString(options.agent);
if (!repo || repo === "*") {
const queued = runNodeJson([
transportCli,
"list",
"--status", "queued",
...optionalArg("--store", options.store),
], bifrostRoot);
const next = queued[0];
if (!next) {
return null;
}
return runNodeJson([
transportCli,
"claim",
"--repo", next.targetRepoName,
...optionalArg("--agent", agent ?? next.targetAgentIdentity),
"--claimed-by", options["claimed-by"] ?? "bifrost-dispatcher",
...optionalArg("--store", options.store),
], bifrostRoot);
}
const args = [
transportCli,
"claim",
"--repo", repo,
...optionalArg("--agent", agent),
"--claimed-by", options["claimed-by"] ?? "bifrost-dispatcher",
...optionalArg("--store", options.store),
];
return runNodeJson(args, bifrostRoot);
}
function resolveRepoRoot(request, options) {
if (options["repo-root"]) {
return resolveOptionPath(options["repo-root"]);
}
const projectsRoot = resolveOptionPath(options["projects-root"] ?? defaultProjectsRoot);
return resolve(projectsRoot, request.targetRepoName);
}
function buildCodexPrompt(request, repoRoot) {
return `You are ${request.targetAgentIdentity ?? "Codex"}, receiving a Bifrost-dispatched update request.
Target repo: ${request.targetRepoName}
Target workspace: ${repoRoot}
Bifrost request id: ${request.id}
Priority: ${request.priority}
Work the request in this workspace. Prefer a small coherent change, create or update tests/docs when appropriate, and leave the repo in a reviewable state. If this is only an analysis/proposal request, produce the artifact requested. Bifrost will close the transport request after this Codex turn exits.
Transport policy: do not push to GitHub, open a pull request, or publish external changes unless this specific request explicitly asks for that. A Bifrost-dispatched turn may prepare local changes and report the commit/PR command it would run, but the bridge owns publication policy.
If this request explicitly authorizes a governed crossing and you use \`tools/bifrost-bridge.mjs\`, the dispatch runtime already provides bridge provenance for request \`${request.id}\`. Preserve it.
${request.requestMarkdown}
`;
}
function postDispatchReceipt(request, dispatchRecord, options) {
const channelId = resolveDispatchReceiptChannelId(options);
const personaName = options["persona-name"] ?? process.env.BIFROST_DISCORD_PERSONA_NAME ?? defaultPersonaName;
const personaAvatarUrl =
optionalString(options["persona-avatar-url"]) ??
optionalString(process.env.BIFROST_DISCORD_PERSONA_AVATAR_URL) ??
optionalString(process.env.DISCORD_PERSONA_AVATAR_URL_BIFROST) ??
defaultPersonaAvatarUrl;
const content = renderDispatchReceiptContent(request);
runNodeJson([
bridgeCli,
"discord-post",
"--channel-id", channelId,
"--persona-name", personaName,
"--content", content,
"--source-kind", "bifrost_agent_transport_request",
"--source-id", request.id,
"--authority-ref", "bifrost_dispatch_started_receipt",
...optionalArg("--target-repository-full-name", request.targetRepositoryFullName),
...optionalArg("--persona-avatar-url", personaAvatarUrl),
...bridgeRecoveryArgs(options),
], bifrostRoot);
}
function resolveDispatchReceiptChannelId(options) {
return (
optionalString(options["channel-id"]) ??
optionalString(process.env.BIFROST_DISCORD_CHANNEL_ID) ??
optionalString(process.env.DISCORD_BIFROST_CHANNEL_ID) ??
defaultAquariumChannelId
);
}
function bridgeRecoveryArgs(options) {
return [];
}
function renderDispatchReceiptContent(request) {
const topic = summarizeRequestTopic(request);
const actor = request.targetAgentIdentity
? `${request.targetAgentIdentity} / ${request.targetRepoName}`
: request.targetRepoName;
return [
`Bifrost Codex dispatch started`,
``,
`**${request.targetRepoName}: ${topic}**`,
`Request: \`${request.id}\``,
`Target: ${actor}`,
`Status: claimed by dispatcher`,
`Codex job: started`,
``,
`Codex has started a turn. The claim is now specific enough to inspect, and the work is no longer sitting unclaimed in chat.`,
`Keep discussion here if it sharpens this request; Bifrost will carry the result back across the bridge.`,
].join("\n");
}
function buildBridgeContextEnv(request, runtimeDir) {
const hooksPath = resolve(bifrostRoot, "tools", "git-hooks");
const gitGatePath = resolve(bifrostRoot, "tools", "git-gate");
const githubConfigDir = resolve(runtimeDir, "github-gh-config");
const gitGlobalConfigPath = resolve(runtimeDir, "github-gitconfig");
mkdirSync(githubConfigDir, { recursive: true });
if (!existsSync(gitGlobalConfigPath)) {
writeFileSync(gitGlobalConfigPath, "", "utf8");
}
return {
BIFROST_BRIDGE_SOURCE_KIND: "bifrost_agent_transport_request",
BIFROST_BRIDGE_SOURCE_ID: request.id,
BIFROST_BRIDGE_AUTHORITY_REF: "bifrost_dispatch_execution",
BIFROST_ENFORCE_GITHUB_GATE: "true",
BIFROST_LOCK_RECOVERY_HATCHES: "true",
BIFROST_GIT_EXECUTABLE: resolve(gitGatePath, "git.cmd"),
BIFROST_GH_EXECUTABLE: resolve(gitGatePath, "gh.cmd"),
BIFROST_NODE_EXECUTABLE: process.execPath,
...buildGitHooksConfigEnv(hooksPath),
...buildPathPrependEnv(gitGatePath),
GH_CONFIG_DIR: githubConfigDir,
GIT_CONFIG_GLOBAL: gitGlobalConfigPath,
GH_TOKEN: "",
GITHUB_TOKEN: "",
GIT_ASKPASS: "",
SSH_ASKPASS: "",
SSH_AUTH_SOCK: "",
GIT_TERMINAL_PROMPT: "0",
GCM_INTERACTIVE: "never",
};
}
function buildGitHooksConfigEnv(hooksPath) {
const entries = [
["core.hooksPath", hooksPath],
["credential.helper", ""],
["credential.interactive", "never"],
];
const existingCount = Number.parseInt(process.env.GIT_CONFIG_COUNT ?? "", 10);
const startIndex = Number.isInteger(existingCount) && existingCount >= 0 ? existingCount : 0;
const env = {
GIT_CONFIG_COUNT: String(startIndex + entries.length),
};
entries.forEach(([key, value], index) => {
const slot = startIndex + index;
env[`GIT_CONFIG_KEY_${slot}`] = key;
env[`GIT_CONFIG_VALUE_${slot}`] = value;
});
return env;
}
function buildPathPrependEnv(directory) {
const currentPath = process.env.Path ?? process.env.PATH ?? "";
const nextPath = currentPath ? `${directory};${currentPath}` : directory;
return {
PATH: nextPath,
Path: nextPath,
};
}
class CodexAppServerClient {
constructor({ command, env, logPath }) {
this.command = command;
this.env = env;
this.logPath = logPath;
this.child = undefined;
this.nextId = 1;
this.buffer = "";
this.pending = new Map();
this.turnWaiters = [];
this.completedTurns = new Map();
}
async start() {
this.child = spawn(this.command.exe, [...this.command.args, "app-server", "--listen", "stdio://"], {
cwd: bifrostRoot,
env: {
...process.env,
...this.env,
},
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
});
this.child.stdout.on("data", (chunk) => this.readStdout(chunk.toString()));
this.child.stderr.on("data", (chunk) => appendLog(this.logPath, chunk.toString()));
this.child.on("exit", (code, signal) => {
const error = new Error(`codex app-server exited with ${code ?? signal ?? "unknown"}`);
for (const waiter of this.pending.values()) {
waiter.reject(error);
}
this.pending.clear();
for (const waiter of this.turnWaiters) {
waiter.reject(error);
}
this.turnWaiters = [];
this.completedTurns.clear();
});
this.child.on("error", (error) => {
for (const waiter of this.pending.values()) {
waiter.reject(error);
}
this.pending.clear();
for (const waiter of this.turnWaiters) {
waiter.reject(error);
}
this.turnWaiters = [];
this.completedTurns.clear();
});
}
request(method, params) {
if (!this.child?.stdin.writable) {
return Promise.reject(new Error("codex app-server stdin is not writable."));
}
const id = this.nextId;
this.nextId += 1;
appendLog(this.logPath, `client-request ${method}#${id}`);
return new Promise((resolvePromise, reject) => {
const timer = setTimeout(() => {
this.pending.delete(id);
reject(new Error(`Timed out waiting for ${method}#${id}`));
}, 60000);
this.pending.set(id, {
resolve: (value) => {
clearTimeout(timer);
resolvePromise(value);
},
reject: (error) => {
clearTimeout(timer);
reject(error);
},
});
this.child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
});
}
waitForTurnCompleted(threadId, turnId) {
const key = `${threadId}:${turnId}`;
const completed = this.completedTurns.get(key);
if (completed) {
this.completedTurns.delete(key);
return Promise.resolve(completed);
}
return new Promise((resolvePromise, reject) => {
this.turnWaiters.push({ threadId, turnId, resolve: resolvePromise, reject });
});
}
readStdout(text) {
this.buffer += text;
while (true) {
const newline = this.buffer.indexOf("\n");
if (newline === -1) {
return;
}
const line = this.buffer.slice(0, newline).trim();
this.buffer = this.buffer.slice(newline + 1);
if (line) {
this.handleMessage(line);
}
}
}
handleMessage(line) {
let message;
try {
message = JSON.parse(line);
} catch {
appendLog(this.logPath, `app-server non-json stdout: ${line}`);
return;
}
if (Object.hasOwn(message, "id") && (Object.hasOwn(message, "result") || Object.hasOwn(message, "error"))) {
const waiter = this.pending.get(message.id);
if (!waiter) {
appendLog(this.logPath, `app-server response for unknown id ${message.id}`);
return;
}
this.pending.delete(message.id);
if (message.error) {
waiter.reject(new Error(JSON.stringify(message.error)));
} else {
waiter.resolve(message.result);
}
return;
}
appendLog(this.logPath, `app-server ${message.method ?? "message"}`);
if (message.method === "turn/completed") {
const params = message.params ?? {};
const completedTurnId = params.turn?.id;
const key = `${params.threadId ?? ""}:${completedTurnId ?? ""}`;
const remaining = [];
let resolved = false;
for (const waiter of this.turnWaiters) {
if (waiter.threadId === params.threadId && waiter.turnId === completedTurnId) {
waiter.resolve(params);
resolved = true;
} else {
remaining.push(waiter);
}
}
this.turnWaiters = remaining;
if (!resolved && params.threadId && completedTurnId) {
this.completedTurns.set(key, params);
}
}
}
stop() {
this.child?.kill();
}
}
function resolveCodexCommand(options) {
const executable = options["codex-executable"] ?? process.env.CODEX_EXECUTABLE;
const extraArgs = splitCommandArgs(options["codex-exec-args"] ?? process.env.CODEX_EXEC_ARGS ?? "");
if (executable) {
return { exe: executable, args: extraArgs };
}
const codexJs = resolve(process.env.APPDATA ?? "", "npm", "node_modules", "@openai", "codex", "bin", "codex.js");
if (existsSync(codexJs)) {
return { exe: process.execPath, args: [codexJs] };
}
return { exe: "codex", args: [] };
}
function sandboxPolicyFromMode(mode) {
if (mode === "danger-full-access") {
return { type: "dangerFullAccess" };
}
if (mode === "read-only") {
return { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false };
}
return {
type: "workspaceWrite",
writableRoots: [],
readOnlyAccess: { type: "fullAccess" },
networkAccess: false,
excludeTmpdirEnvVar: false,
excludeSlashTmp: false,
};
}
function releaseClaimedRequest(id, note) {
runNodeJson([
transportCli,
"release",
"--id", id,
"--note", note,
], bifrostRoot);
}
async function writeDispatchResult(logPath, result) {
await writeFile(
resolve(dirname(logPath), "result.json"),
`${JSON.stringify(result, null, 2)}\n`,
"utf8",
);
}
async function beginDispatchRun(request, input) {
void request;
void input;
return null;
}
function summarizeError(error) {
if (error instanceof Error) {
return error.stack ?? error.message;
}
return String(error);
}
function summarizeRequestTopic(request) {
const title = String(request.title ?? "").trim();
if (title && !/^Recent\b/i.test(title)) {
return title;
}
const text = String(request.requestMarkdown ?? "");
const section = text.match(/## What Needs To Be Done\s+([\s\S]*?)(?:\n## |\n```|$)/i)?.[1]?.trim();
const firstParagraph = section
?.split(/\n\s*\n/)
.map((entry) => entry.replace(/\s+/g, " ").trim())
.find(Boolean);
if (firstParagraph) {
return summarizeAction(firstParagraph);
}
const heading = text.match(/^#\s+(.+)$/m)?.[1]?.trim();
return truncate(heading || "routed update request", 96);
}
function truncate(value, maxLength) {
return value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;
}
function summarizeAction(value) {
const normalized = value.replace(/\s+/g, " ").trim();
const match =
normalized.match(/^Implement the ([^:]+?) as /i) ??
normalized.match(/^Update ([^:]+?) from /i) ??
normalized.match(/^Add (.+?)(?:\.|$)/i);
return truncate(stripTrailingRepoName(match?.[1] ?? normalized), 96);
}
function stripTrailingRepoName(value) {
return value.replace(/\s+for\s+[A-Z][A-Za-z0-9._-]+$/g, "");
}
function runNodeJson(args, cwd) {
const result = spawnSync(process.execPath, args, {
cwd,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
timeout: 30000,
});
if (result.status !== 0) {
const reason = result.error?.message ?? result.stderr ?? result.stdout;
throw new Error(`node ${args.join(" ")} failed with ${result.status ?? "unknown"}:\n${reason}`);
}
return JSON.parse(result.stdout);
}
function appendLog(path, text) {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, `${text ?? ""}\n`, { encoding: "utf8", flag: "a" });
}
function parseArgs(args) {
const options = {};
for (let index = 0; index < args.length; index += 1) {
const token = args[index];
if (!token.startsWith("--")) {
throw new Error(`Unexpected argument "${token}".`);
}
const key = token.slice(2);
const next = args[index + 1];
if (!next || next.startsWith("--")) {
options[key] = "true";
continue;
}
options[key] = next;
index += 1;
}
return options;
}
function requireOption(options, name) {
const value = optionalString(options[name]);
if (!value) {
throw new Error(`Missing required option --${name}.`);
}
return value;
}
function optionalArg(flag, value) {
const normalized = optionalString(value);
return normalized ? [flag, normalized] : [];
}
function optionalString(value) {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
function parseInteger(value, name) {
const parsed = Number.parseInt(value, 10);
if (!Number.isInteger(parsed) || parsed < 1) {
throw new Error(`--${name} must be a positive integer.`);
}
return parsed;
}
function splitCommandArgs(value) {
if (typeof value !== "string" || value.trim().length === 0) {
return [];
}
return value
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
}
function resolveOptionPath(path) {
return resolve(process.cwd(), path);
}
function printJson(value) {
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
}
function loadLocalEnv(path) {
if (!existsSync(path)) {
return;
}
for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) {
continue;
}
const separator = trimmed.indexOf("=");
if (separator === -1) {
continue;
}
const key = trimmed.slice(0, separator).trim();
if (process.env[key]) {
continue;
}
let value = trimmed.slice(separator + 1).trim();
if (
value.length >= 2 &&
((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
) {
value = value.slice(1, -1);
}
process.env[key] = value;
}
}
function printHelp() {
process.stdout.write(`Bifrost agent request dispatcher
Commands:
dispatch Claim queued Bifrost requests and launch detached Codex turns
run-claimed Internal worker for one claimed request
Example:
node tools/dispatch-agent-requests.mjs dispatch --repo AquaSynth --agent aqua --max 1
`);
}
main().catch((error) => {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
});