



















@@ -36,6 +36,9 @@ const describeLive = LIVE && ACP_BIND_LIVE ? describe : describe.skip;
36363737const CONNECT_TIMEOUT_MS = 90_000;
3838const LIVE_TIMEOUT_MS = 240_000;
39+const ACP_CRON_MCP_PROBE_MAX_ATTEMPTS = 2;
40+const ACP_CRON_MCP_PROBE_VERIFY_POLLS = 5;
41+const ACP_CRON_MCP_PROBE_VERIFY_POLL_MS = 1_000;
3942const DEFAULT_LIVE_CODEX_MODEL = "gpt-5.5";
4043const DEFAULT_LIVE_PARENT_MODEL = "openai/gpt-5.4";
4144type LiveAcpAgent = "claude" | "codex" | "droid" | "gemini" | "opencode";
@@ -150,6 +153,10 @@ function shouldRequireBoundAssistantTranscript(liveAgent: LiveAcpAgent): boolean
150153);
151154}
152155156+function shouldRequireCronMcpProbe(): boolean {
157+return isTruthyEnvValue(process.env.OPENCLAW_LIVE_ACP_BIND_REQUIRE_CRON);
158+}
159+153160function normalizeOpenAiModelRef(value: string): string {
154161const trimmed = value.trim();
155162if (!trimmed) {
@@ -287,24 +294,30 @@ async function bindConversationAndWait(params: {
287294doctor?: () => Promise<{ message?: string; details?: string[] }>;
288295}
289296| undefined;
290-if (runtime?.probeAvailability) {
291-await runtime.probeAvailability().catch(() => {});
292-}
293-if (!(backend?.healthy?.() ?? false)) {
294-if (runtime?.doctor && (attempt === 1 || attempt % 6 === 0)) {
295-const report = await runtime.doctor().catch((error) => ({
296-message: error instanceof Error ? error.message : String(error),
297-details: [],
298-}));
299-logLiveStep(
300-`acpx doctor before bind attempt ${attempt}: ${report.message ?? "unknown"}${
301- report.details?.length ? ` (${report.details.join("; ")})` : ""
302- }`,
303-);
297+const backendUnavailable = !backend || (backend.healthy && !backend.healthy());
298+if (backendUnavailable) {
299+if (runtime?.probeAvailability) {
300+await runtime.probeAvailability().catch(() => {});
301+}
302+const backendReadyAfterProbe = backend && (!backend.healthy || backend.healthy());
303+if (backendReadyAfterProbe) {
304+logLiveStep(`acpx backend became healthy before bind attempt ${attempt}`);
305+} else {
306+if (runtime?.doctor && (attempt === 1 || attempt % 6 === 0)) {
307+const report = await runtime.doctor().catch((error) => ({
308+message: error instanceof Error ? error.message : String(error),
309+details: [],
310+}));
311+logLiveStep(
312+`acpx doctor before bind attempt ${attempt}: ${report.message ?? "unknown"}${
313+ report.details?.length ? ` (${report.details.join("; ")})` : ""
314+ }`,
315+);
316+}
317+logLiveStep(`acpx backend still unhealthy before bind attempt ${attempt}`);
318+await sleep(5_000);
319+continue;
304320}
305-logLiveStep(`acpx backend still unhealthy before bind attempt ${attempt}`);
306-await sleep(5_000);
307-continue;
308321}
309322310323await sendChatAndWait({
@@ -463,6 +476,25 @@ async function waitForAssistantTurn(params: {
463476);
464477}
465478479+async function pollCronJobVisibleViaCli(params: {
480+port: number;
481+token: string;
482+env: NodeJS.ProcessEnv;
483+expectedName: string;
484+expectedMessage: string;
485+}): Promise<{ job?: Awaited<ReturnType<typeof assertCronJobVisibleViaCli>>; pollsUsed: number }> {
486+for (let verifyAttempt = 0; verifyAttempt < ACP_CRON_MCP_PROBE_VERIFY_POLLS; verifyAttempt += 1) {
487+const job = await assertCronJobVisibleViaCli(params);
488+if (job) {
489+return { job, pollsUsed: verifyAttempt + 1 };
490+}
491+if (verifyAttempt < ACP_CRON_MCP_PROBE_VERIFY_POLLS - 1) {
492+await sleep(ACP_CRON_MCP_PROBE_VERIFY_POLL_MS);
493+}
494+}
495+return { pollsUsed: ACP_CRON_MCP_PROBE_VERIFY_POLLS };
496+}
497+466498describeLive("gateway live (ACP bind)", () => {
467499it(
468500"binds a synthetic Slack DM conversation to a live ACP session and reroutes the next turn",
@@ -852,9 +884,10 @@ describeLive("gateway live (ACP bind)", () => {
852884agentId: liveAgent,
853885sessionKey: spawnedSessionKey,
854886});
887+const requireCronMcpProbe = shouldRequireCronMcpProbe();
855888let cronJobId: string | undefined;
856889let lastCronAssistantText = "";
857-for (let attempt = 0; attempt < 2; attempt += 1) {
890+for (let attempt = 0; attempt < ACP_CRON_MCP_PROBE_MAX_ATTEMPTS; attempt += 1) {
858891await sendChatAndWait({
859892 client,
860893sessionKey: originalSessionKey,
@@ -876,7 +909,7 @@ describeLive("gateway live (ACP bind)", () => {
876909cronHistory = await waitForAssistantText({
877910 client,
878911sessionKey: spawnedSessionKey,
879-timeoutMs: liveAgent === "claude" ? 90_000 : 45_000,
912+timeoutMs: 20_000,
880913contains: cronProbe.name,
881914});
882915} catch {
@@ -885,13 +918,14 @@ describeLive("gateway live (ACP bind)", () => {
885918if (cronHistory) {
886919lastCronAssistantText = cronHistory.lastAssistantText;
887920}
888-const createdJob = await assertCronJobVisibleViaCli({
921+const verifyResult = await pollCronJobVisibleViaCli({
889922 port,
890923 token,
891924env: process.env,
892925expectedName: cronProbe.name,
893926expectedMessage: cronProbe.message,
894927});
928+const createdJob = verifyResult.job;
895929if (createdJob) {
896930assertCronJobMatches({
897931job: createdJob,
@@ -906,10 +940,15 @@ describeLive("gateway live (ACP bind)", () => {
906940}
907941break;
908942}
909-if (attempt === 1) {
910-if (liveAgent !== "claude") {
943+logLiveStep(
944+`cron mcp job not observed after attempt ${String(
945+ attempt + 1,
946+ )}; polls=${String(verifyResult.pollsUsed)}`,
947+);
948+if (attempt === ACP_CRON_MCP_PROBE_MAX_ATTEMPTS - 1) {
949+if (!requireCronMcpProbe) {
911950logLiveStep(
912-`cron mcp job ${cronProbe.name} not observed for ${liveAgent}; continuing after bind/image verification`,
951+`cron mcp job ${cronProbe.name} not observed; continuing after bind/image verification`,
913952);
914953break;
915954}
@@ -921,7 +960,7 @@ describeLive("gateway live (ACP bind)", () => {
921960}
922961}
923962if (!cronJobId) {
924-if (liveAgent !== "claude") {
963+if (!requireCronMcpProbe) {
925964return;
926965}
927966throw new Error(`acp cron cli verify did not create job ${cronProbe.name}`);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。