mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-08 10:12:54 +00:00
114 lines
3.1 KiB
TypeScript
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: [] };
|
|
}
|