Files
openclaw/extensions/codex/src/app-server/attempt-steering.ts
2026-05-27 18:11:16 +01:00

114 lines
3.1 KiB
TypeScript

import { embeddedAgentLog } from "openclaw/plugin-sdk/agent-harness-runtime";
import type { CodexAppServerClient } from "./client.js";
import type { CodexUserInput } from "./protocol.js";
const CODEX_STEER_ALL_DEBOUNCE_MS = 500;
export type CodexSteeringQueueOptions = {
debounceMs?: number;
};
export function createCodexSteeringQueue(params: {
client: CodexAppServerClient;
threadId: string;
turnId: string;
answerPendingUserInput: (text: string) => boolean;
signal: AbortSignal;
}) {
type PendingSteerText = {
text: string;
resolve: () => void;
reject: (error: unknown) => void;
};
let batchedTexts: PendingSteerText[] = [];
let batchTimer: NodeJS.Timeout | undefined;
let sendChain: Promise<void> = Promise.resolve();
const clearBatchTimer = () => {
if (batchTimer) {
clearTimeout(batchTimer);
batchTimer = undefined;
}
};
const sendTexts = async (texts: string[]) => {
if (texts.length === 0) {
return;
}
if (params.signal.aborted) {
throw new Error("codex app-server steering queue aborted");
}
await params.client.request("turn/steer", {
threadId: params.threadId,
expectedTurnId: params.turnId,
input: texts.map(toCodexTextInput),
});
};
const enqueueSend = (texts: string[]) => {
const send = sendChain.then(() => sendTexts(texts));
sendChain = send.catch((error: unknown) => {
embeddedAgentLog.debug("codex app-server queued steer failed", { error });
});
return send;
};
const flushBatch = () => {
clearBatchTimer();
const items = batchedTexts;
batchedTexts = [];
const send = enqueueSend(items.map((item) => item.text));
void send.then(
() => {
for (const item of items) {
item.resolve();
}
},
(error: unknown) => {
for (const item of items) {
item.reject(error);
}
},
);
return send;
};
return {
async queue(text: string, options?: CodexSteeringQueueOptions) {
if (params.answerPendingUserInput(text)) {
return;
}
return await new Promise<void>((resolve, reject) => {
batchedTexts.push({ text, resolve, reject });
clearBatchTimer();
const debounceMs = normalizeCodexSteerDebounceMs(options?.debounceMs);
batchTimer = setTimeout(() => {
batchTimer = undefined;
void flushBatch().catch(() => undefined);
}, debounceMs);
});
},
async flushPending() {
await flushBatch().catch(() => undefined);
},
cancel() {
clearBatchTimer();
const items = batchedTexts;
batchedTexts = [];
for (const item of items) {
item.reject(new Error("codex app-server steering queue cancelled"));
}
},
};
}
export function normalizeCodexSteerDebounceMs(value: number | undefined): number {
return typeof value === "number" && Number.isFinite(value) && value >= 0
? Math.floor(value)
: CODEX_STEER_ALL_DEBOUNCE_MS;
}
export function toCodexTextInput(text: string): CodexUserInput {
return { type: "text", text, text_elements: [] };
}