baa-conductor

git clone 

commit
7c8558f
parent
98db481
author
im_wower
date
2026-03-30 05:52:28 +0800 CST
Merge branch fix/opt-002 (resolved conflicts: withRouteTimeout + multi-platform)
4 files changed,  +193, -34
M apps/conductor-daemon/src/index.test.js
+59, -2
 1@@ -23,10 +23,13 @@ import {
 2   BrowserRequestPolicyController,
 3   ConductorDaemon,
 4   ConductorRuntime,
 5+  DEFAULT_BAA_EXEC_INSTRUCTION_TIMEOUT_MS,
 6+  DEFAULT_BAA_INSTRUCTION_TIMEOUT_MS,
 7   PersistentBaaInstructionDeduper,
 8   PersistentBaaLiveInstructionMessageDeduper,
 9   PersistentBaaLiveInstructionSnapshotStore,
10   createFetchControlApiClient,
11+  executeBaaInstruction,
12   extractBaaInstructionBlocks,
13   handleConductorHttpRequest,
14   normalizeBaaInstruction,
15@@ -758,7 +761,8 @@ test("routeBaaInstruction maps browser send/current targets to the existing loca
16       key: `local.browser.${platform}.send`,
17       method: "POST",
18       path: `/v1/browser/${platform}/send`,
19-      requiresSharedToken: false
20+      requiresSharedToken: false,
21+      timeoutMs: DEFAULT_BAA_INSTRUCTION_TIMEOUT_MS
22     });
23 
24     const currentRoute = routeBaaInstruction(createInstructionEnvelope({
25@@ -770,11 +774,64 @@ test("routeBaaInstruction maps browser send/current targets to the existing loca
26       key: `local.browser.${platform}.current`,
27       method: "GET",
28       path: `/v1/browser/${platform}/current`,
29-      requiresSharedToken: false
30+      requiresSharedToken: false,
31+      timeoutMs: DEFAULT_BAA_INSTRUCTION_TIMEOUT_MS
32     });
33   }
34 });
35 
36+test("routeBaaInstruction applies a 60s default timeout to conductor exec", () => {
37+  const execRoute = routeBaaInstruction(createInstructionEnvelope({
38+    params: "printf 'exec-timeout-default'",
39+    tool: "exec"
40+  }));
41+
42+  assert.equal(execRoute.timeoutMs, DEFAULT_BAA_EXEC_INSTRUCTION_TIMEOUT_MS);
43+  assert.deepEqual(execRoute.body, {
44+    command: "printf 'exec-timeout-default'",
45+    timeoutMs: DEFAULT_BAA_EXEC_INSTRUCTION_TIMEOUT_MS
46+  });
47+});
48+
49+test("executeBaaInstruction returns a structured timeout failure when the local handler stalls", async () => {
50+  const { controlPlane, snapshot } = await createLocalApiFixture();
51+  const instruction = createInstructionEnvelope({
52+    params: "printf 'timeout-protected'",
53+    tool: "exec"
54+  });
55+  const route = {
56+    ...routeBaaInstruction(instruction),
57+    timeoutMs: 20
58+  };
59+
60+  try {
61+    const result = await executeBaaInstruction(
62+      instruction,
63+      route,
64+      {
65+        repository: null,
66+        sharedToken: "local-shared-token",
67+        snapshotLoader: () => snapshot
68+      },
69+      {
70+        requestHandler: () => new Promise(() => {})
71+      }
72+    );
73+
74+    assert.equal(result.ok, false);
75+    assert.equal(result.httpStatus, 504);
76+    assert.equal(result.error, "execution_timeout");
77+    assert.equal(result.message, "Local instruction execution timed out after 20ms.");
78+    assert.deepEqual(result.details, {
79+      timeout_ms: 20
80+    });
81+    assert.equal(result.route.key, "local.exec");
82+    assert.ok(result.artifact);
83+  } finally {
84+    controlPlane.close();
85+  }
86+});
87+
88 test("BaaInstructionCenter runs the Phase 1 execution loop and dedupes replayed messages", async () => {
89   const { controlPlane, repository, sharedToken, snapshot } = await createLocalApiFixture();
90   const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-instruction-center-"));
M apps/conductor-daemon/src/instructions/executor.ts
+85, -11
  1@@ -3,6 +3,11 @@ import {
  2   handleConductorHttpRequest,
  3   type ConductorLocalApiContext
  4 } from "../local-api.js";
  5+import type { ConductorHttpRequest, ConductorHttpResponse } from "../http-types.js";
  6+import {
  7+  DEFAULT_BAA_EXEC_INSTRUCTION_TIMEOUT_MS,
  8+  DEFAULT_BAA_INSTRUCTION_TIMEOUT_MS
  9+} from "./router.js";
 10 import type {
 11   BaaInstructionEnvelope,
 12   BaaInstructionExecutionResult,
 13@@ -12,12 +17,29 @@ import type {
 14 } from "./types.js";
 15 import { isBaaJsonValue } from "./types.js";
 16 
 17+export interface ExecuteBaaInstructionOptions {
 18+  requestHandler?: (
 19+    request: ConductorHttpRequest,
 20+    context: ConductorLocalApiContext
 21+  ) => Promise<ConductorHttpResponse>;
 22+}
 23+
 24+class BaaInstructionExecutionTimeoutError extends Error {
 25+  readonly timeoutMs: number;
 26+
 27+  constructor(timeoutMs: number) {
 28+    super(`Local instruction execution timed out after ${timeoutMs}ms.`);
 29+    this.timeoutMs = timeoutMs;
 30+  }
 31+}
 32+
 33 function toExecutionFailure(
 34   instruction: BaaInstructionEnvelope,
 35   route: BaaInstructionRoute,
 36   message: string,
 37   error = "execution_failed",
 38-  details: BaaJsonValue | null = null
 39+  details: BaaJsonValue | null = null,
 40+  httpStatus = 500
 41 ): BaaInstructionExecutionResult {
 42   return {
 43     artifact: null,
 44@@ -25,7 +47,7 @@ function toExecutionFailure(
 45     dedupeKey: instruction.dedupeKey,
 46     details,
 47     error,
 48-    httpStatus: 500,
 49+    httpStatus,
 50     instructionId: instruction.instructionId,
 51     message,
 52     ok: false,
 53@@ -40,6 +62,35 @@ function toExecutionFailure(
 54   };
 55 }
 56 
 57+function resolveExecutionTimeoutMs(route: BaaInstructionRoute): number {
 58+  if (Number.isInteger(route.timeoutMs) && route.timeoutMs > 0) {
 59+    return route.timeoutMs;
 60+  }
 61+
 62+  return route.key === "local.exec"
 63+    ? DEFAULT_BAA_EXEC_INSTRUCTION_TIMEOUT_MS
 64+    : DEFAULT_BAA_INSTRUCTION_TIMEOUT_MS;
 65+}
 66+
 67+function withRequestTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
 68+  return new Promise((resolve, reject) => {
 69+    const timeoutId = setTimeout(() => {
 70+      reject(new BaaInstructionExecutionTimeoutError(timeoutMs));
 71+    }, timeoutMs);
 72+
 73+    promise.then(
 74+      (value) => {
 75+        clearTimeout(timeoutId);
 76+        resolve(value);
 77+      },
 78+      (error) => {
 79+        clearTimeout(timeoutId);
 80+        reject(error);
 81+      }
 82+    );
 83+  });
 84+}
 85+
 86 function normalizeJsonBodyValue(value: unknown): BaaJsonValue | null {
 87   return isBaaJsonValue(value) ? value : null;
 88 }
 89@@ -166,9 +217,11 @@ async function withExecutionArtifact(
 90 export async function executeBaaInstruction(
 91   instruction: BaaInstructionEnvelope,
 92   route: BaaInstructionRoute,
 93-  context: ConductorLocalApiContext
 94+  context: ConductorLocalApiContext,
 95+  options: ExecuteBaaInstructionOptions = {}
 96 ): Promise<BaaInstructionExecutionResult> {
 97   try {
 98+    const timeoutMs = resolveExecutionTimeoutMs(route);
 99     const headers: Record<string, string> = {
100       "content-type": "application/json"
101     };
102@@ -177,14 +230,18 @@ export async function executeBaaInstruction(
103       headers.authorization = `Bearer ${context.sharedToken}`;
104     }
105 
106-    const response = await handleConductorHttpRequest(
107-      {
108-        body: route.body == null ? null : JSON.stringify(route.body),
109-        headers,
110-        method: route.method,
111-        path: route.path
112-      },
113-      context
114+    const requestHandler = options.requestHandler ?? handleConductorHttpRequest;
115+    const response = await withRequestTimeout(
116+      requestHandler(
117+        {
118+          body: route.body == null ? null : JSON.stringify(route.body),
119+          headers,
120+          method: route.method,
121+          path: route.path
122+        },
123+        context
124+      ),
125+      timeoutMs
126     );
127 
128     let parsedBody: unknown = null;
129@@ -235,6 +292,23 @@ export async function executeBaaInstruction(
130       tool: instruction.tool
131     }, instruction, context);
132   } catch (error) {
133+    if (error instanceof BaaInstructionExecutionTimeoutError) {
134+      return withExecutionArtifact(
135+        toExecutionFailure(
136+          instruction,
137+          route,
138+          error.message,
139+          "execution_timeout",
140+          {
141+            timeout_ms: error.timeoutMs
142+          },
143+          504
144+        ),
145+        instruction,
146+        context
147+      );
148+    }
149+
150     const message = error instanceof Error ? error.message : String(error);
151     return withExecutionArtifact(toExecutionFailure(instruction, route, message), instruction, context);
152   }
M apps/conductor-daemon/src/instructions/router.ts
+48, -21
  1@@ -5,6 +5,9 @@ import type {
  2 } from "./types.js";
  3 import { isBaaJsonObject } from "./types.js";
  4 
  5+export const DEFAULT_BAA_INSTRUCTION_TIMEOUT_MS = 30_000;
  6+export const DEFAULT_BAA_EXEC_INSTRUCTION_TIMEOUT_MS = 60_000;
  7+
  8 export class BaaInstructionRouteError extends Error {
  9   readonly blockIndex: number;
 10   readonly stage = "route";
 11@@ -66,11 +69,35 @@ function requireNoParams(instruction: BaaInstructionEnvelope): void {
 12   }
 13 }
 14 
 15+function withRouteTimeout(
 16+  route: Omit<BaaInstructionRoute, "timeoutMs">,
 17+  timeoutMs = DEFAULT_BAA_INSTRUCTION_TIMEOUT_MS
 18+): BaaInstructionRoute {
 19+  return {
 20+    ...route,
 21+    timeoutMs
 22+  };
 23+}
 24+
 25+function applyDefaultExecTimeout(body: BaaJsonObject): BaaJsonObject {
 26+  if (
 27+    Object.prototype.hasOwnProperty.call(body, "timeoutMs")
 28+    || Object.prototype.hasOwnProperty.call(body, "timeout_ms")
 29+  ) {
 30+    return body;
 31+  }
 32+
 33+  return {
 34+    ...body,
 35+    timeoutMs: DEFAULT_BAA_EXEC_INSTRUCTION_TIMEOUT_MS
 36+  };
 37+}
 38+
 39 function normalizeExecBody(instruction: BaaInstructionEnvelope): BaaJsonObject {
 40   if (typeof instruction.params === "string") {
 41-    return {
 42+    return applyDefaultExecTimeout({
 43       command: requireNonEmptyStringParam(instruction, "command", true)
 44-    };
 45+    });
 46   }
 47 
 48   const params = requireJsonObjectParams(instruction);
 49@@ -83,7 +110,7 @@ function normalizeExecBody(instruction: BaaInstructionEnvelope): BaaJsonObject {
 50     );
 51   }
 52 
 53-  return params;
 54+  return applyDefaultExecTimeout(params);
 55 }
 56 
 57 function normalizeFileReadBody(instruction: BaaInstructionEnvelope): BaaJsonObject {
 58@@ -162,64 +189,64 @@ function routeLocalInstruction(instruction: BaaInstructionEnvelope): BaaInstruct
 59   switch (instruction.tool) {
 60     case "describe":
 61       requireNoParams(instruction);
 62-      return {
 63+      return withRouteTimeout({
 64         body: null,
 65         key: "local.describe",
 66         method: "GET",
 67         path: "/describe",
 68         requiresSharedToken: false
 69-      };
 70+      });
 71     case "describe/business":
 72       requireNoParams(instruction);
 73-      return {
 74+      return withRouteTimeout({
 75         body: null,
 76         key: "local.describe.business",
 77         method: "GET",
 78         path: "/describe/business",
 79         requiresSharedToken: false
 80-      };
 81+      });
 82     case "describe/control":
 83       requireNoParams(instruction);
 84-      return {
 85+      return withRouteTimeout({
 86         body: null,
 87         key: "local.describe.control",
 88         method: "GET",
 89         path: "/describe/control",
 90         requiresSharedToken: false
 91-      };
 92+      });
 93     case "status":
 94       requireNoParams(instruction);
 95-      return {
 96+      return withRouteTimeout({
 97         body: null,
 98         key: "local.status",
 99         method: "GET",
100         path: "/v1/status",
101         requiresSharedToken: false
102-      };
103+      });
104     case "exec":
105-      return {
106+      return withRouteTimeout({
107         body: normalizeExecBody(instruction),
108         key: "local.exec",
109         method: "POST",
110         path: "/v1/exec",
111         requiresSharedToken: true
112-      };
113+      }, DEFAULT_BAA_EXEC_INSTRUCTION_TIMEOUT_MS);
114     case "files/read":
115-      return {
116+      return withRouteTimeout({
117         body: normalizeFileReadBody(instruction),
118         key: "local.files.read",
119         method: "POST",
120         path: "/v1/files/read",
121         requiresSharedToken: true
122-      };
123+      });
124     case "files/write":
125-      return {
126+      return withRouteTimeout({
127         body: normalizeFileWriteBody(instruction),
128         key: "local.files.write",
129         method: "POST",
130         path: "/v1/files/write",
131         requiresSharedToken: true
132-      };
133+      });
134     default:
135       throw new BaaInstructionRouteError(
136         instruction.blockIndex,
137@@ -234,22 +261,22 @@ function routeBrowserInstruction(
138 ): BaaInstructionRoute {
139   switch (instruction.tool) {
140     case "send":
141-      return {
142+      return withRouteTimeout({
143         body: normalizeBrowserSendBody(instruction),
144         key: `local.browser.${platform}.send`,
145         method: "POST",
146         path: `/v1/browser/${platform}/send`,
147         requiresSharedToken: false
148-      };
149+      });
150     case "current":
151       requireNoParams(instruction);
152-      return {
153+      return withRouteTimeout({
154         body: null,
155         key: `local.browser.${platform}.current`,
156         method: "GET",
157         path: `/v1/browser/${platform}/current`,
158         requiresSharedToken: false
159-      };
160+      });
161     default:
162       throw new BaaInstructionRouteError(
163         instruction.blockIndex,
M apps/conductor-daemon/src/instructions/types.ts
+1, -0
1@@ -65,6 +65,7 @@ export interface BaaInstructionRoute {
2   method: "GET" | "POST";
3   path: string;
4   requiresSharedToken: boolean;
5+  timeoutMs: number;
6 }
7 
8 export type BaaInstructionDeniedStage = "policy" | "route";