baa-conductor

git clone 

commit
1bddb21
parent
f08e7dd
author
im_wower
date
2026-03-28 02:07:58 +0800 CST
fix: harden baa extraction and in-memory dedupe
4 files changed,  +121, -4
M apps/conductor-daemon/src/index.test.js
+53, -0
 1@@ -13,6 +13,8 @@ import { FirefoxCommandBroker } from "../dist/firefox-bridge.js";
 2 import {
 3   BaaInstructionCenter,
 4   BaaLiveInstructionIngest,
 5+  InMemoryBaaInstructionDeduper,
 6+  InMemoryBaaLiveInstructionMessageDeduper,
 7   BrowserRequestPolicyController,
 8   ConductorDaemon,
 9   ConductorRuntime,
10@@ -542,6 +544,57 @@ test(
11   }
12 );
13 
14+test("BAA instruction extraction ignores unterminated baa blocks and keeps closed ones", () => {
15+  const message = [
16+    "```baa",
17+    "@conductor::describe",
18+    "```",
19+    "",
20+    "```baa",
21+    "@conductor::exec::printf 'broken-tail'"
22+  ].join("\n");
23+
24+  const blocks = extractBaaInstructionBlocks(message);
25+
26+  assert.equal(blocks.length, 1);
27+  assert.equal(blocks[0].blockIndex, 0);
28+  assert.equal(parseBaaInstructionBlock(blocks[0]).tool, "describe");
29+});
30+
31+test("InMemoryBaaInstructionDeduper evicts the oldest keys when maxSize is exceeded", () => {
32+  const deduper = new InMemoryBaaInstructionDeduper({
33+    maxSize: 2
34+  });
35+
36+  deduper.add({
37+    dedupeKey: "sha256:key-1"
38+  });
39+  deduper.add({
40+    dedupeKey: "sha256:key-2"
41+  });
42+  deduper.add({
43+    dedupeKey: "sha256:key-3"
44+  });
45+
46+  assert.equal(deduper.has("sha256:key-1"), false);
47+  assert.equal(deduper.has("sha256:key-2"), true);
48+  assert.equal(deduper.has("sha256:key-3"), true);
49+});
50+
51+test("InMemoryBaaLiveInstructionMessageDeduper evicts the oldest keys when maxSize is exceeded", () => {
52+  const deduper = new InMemoryBaaLiveInstructionMessageDeduper({
53+    maxSize: 2
54+  });
55+
56+  deduper.add("sha256:msg-1");
57+  deduper.add("sha256:msg-2");
58+  deduper.add("sha256:msg-3");
59+
60+  assert.equal(deduper.has("sha256:msg-1"), false);
61+  assert.equal(deduper.has("sha256:msg-2"), true);
62+  assert.equal(deduper.has("sha256:msg-3"), true);
63+});
64+
65 test("BAA instruction normalization keeps auditable fields and stable dedupe keys", () => {
66   const source = {
67     assistantMessageId: "msg-001",
M apps/conductor-daemon/src/instructions/dedupe.ts
+34, -0
 1@@ -9,16 +9,38 @@ import type {
 2 } from "./types.js";
 3 import { sortBaaJsonValue, stableStringifyBaaJson } from "./types.js";
 4 
 5+const DEFAULT_IN_MEMORY_BAA_INSTRUCTION_DEDUPER_MAX_SIZE = 10_000;
 6+
 7+function normalizeInMemoryDeduperMaxSize(maxSize: number | null | undefined): number {
 8+  if (typeof maxSize !== "number" || !Number.isFinite(maxSize)) {
 9+    return DEFAULT_IN_MEMORY_BAA_INSTRUCTION_DEDUPER_MAX_SIZE;
10+  }
11+
12+  const normalized = Math.trunc(maxSize);
13+  return normalized > 0 ? normalized : DEFAULT_IN_MEMORY_BAA_INSTRUCTION_DEDUPER_MAX_SIZE;
14+}
15+
16 export interface BaaInstructionDeduper {
17   add(instruction: BaaInstructionEnvelope): Promise<void> | void;
18   has(dedupeKey: string): Promise<boolean> | boolean;
19 }
20 
21+export interface InMemoryBaaInstructionDeduperOptions {
22+  maxSize?: number;
23+}
24+
25 export class InMemoryBaaInstructionDeduper implements BaaInstructionDeduper {
26   private readonly keys = new Set<string>();
27+  private readonly maxSize: number;
28+
29+  constructor(options: InMemoryBaaInstructionDeduperOptions = {}) {
30+    this.maxSize = normalizeInMemoryDeduperMaxSize(options.maxSize);
31+  }
32 
33   add(instruction: BaaInstructionEnvelope): void {
34+    this.keys.delete(instruction.dedupeKey);
35     this.keys.add(instruction.dedupeKey);
36+    this.evictOverflow();
37   }
38 
39   clear(): void {
40@@ -28,6 +50,18 @@ export class InMemoryBaaInstructionDeduper implements BaaInstructionDeduper {
41   has(dedupeKey: string): boolean {
42     return this.keys.has(dedupeKey);
43   }
44+
45+  private evictOverflow(): void {
46+    while (this.keys.size > this.maxSize) {
47+      const oldestKey = this.keys.values().next().value;
48+
49+      if (typeof oldestKey !== "string") {
50+        return;
51+      }
52+
53+      this.keys.delete(oldestKey);
54+    }
55+  }
56 }
57 
58 export function buildBaaInstructionDedupeBasis(
M apps/conductor-daemon/src/instructions/extract.ts
+0, -4
 1@@ -55,9 +55,5 @@ export function extractBaaInstructionBlocks(text: string): BaaExtractedBlock[] {
 2     pending.contentLines.push(line);
 3   }
 4 
 5-  if (pending?.isBaa) {
 6-    throw new BaaInstructionExtractError("Unterminated ```baa code block.");
 7-  }
 8-
 9   return blocks;
10 }
M apps/conductor-daemon/src/instructions/ingest.ts
+34, -0
 1@@ -87,11 +87,33 @@ export interface BaaLiveInstructionIngestOptions {
 2   snapshotStore?: BaaLiveInstructionSnapshotStore | null;
 3 }
 4 
 5+const DEFAULT_IN_MEMORY_BAA_LIVE_MESSAGE_DEDUPER_MAX_SIZE = 10_000;
 6+
 7+function normalizeInMemoryMessageDeduperMaxSize(maxSize: number | null | undefined): number {
 8+  if (typeof maxSize !== "number" || !Number.isFinite(maxSize)) {
 9+    return DEFAULT_IN_MEMORY_BAA_LIVE_MESSAGE_DEDUPER_MAX_SIZE;
10+  }
11+
12+  const normalized = Math.trunc(maxSize);
13+  return normalized > 0 ? normalized : DEFAULT_IN_MEMORY_BAA_LIVE_MESSAGE_DEDUPER_MAX_SIZE;
14+}
15+
16+export interface InMemoryBaaLiveInstructionMessageDeduperOptions {
17+  maxSize?: number;
18+}
19+
20 export class InMemoryBaaLiveInstructionMessageDeduper implements BaaLiveInstructionMessageDeduper {
21   private readonly keys = new Set<string>();
22+  private readonly maxSize: number;
23+
24+  constructor(options: InMemoryBaaLiveInstructionMessageDeduperOptions = {}) {
25+    this.maxSize = normalizeInMemoryMessageDeduperMaxSize(options.maxSize);
26+  }
27 
28   add(dedupeKey: string): void {
29+    this.keys.delete(dedupeKey);
30     this.keys.add(dedupeKey);
31+    this.evictOverflow();
32   }
33 
34   clear(): void {
35@@ -101,6 +123,18 @@ export class InMemoryBaaLiveInstructionMessageDeduper implements BaaLiveInstruct
36   has(dedupeKey: string): boolean {
37     return this.keys.has(dedupeKey);
38   }
39+
40+  private evictOverflow(): void {
41+    while (this.keys.size > this.maxSize) {
42+      const oldestKey = this.keys.values().next().value;
43+
44+      if (typeof oldestKey !== "string") {
45+        return;
46+      }
47+
48+      this.keys.delete(oldestKey);
49+    }
50+  }
51 }
52 
53 function buildInstructionDescriptor(target: string, tool: string): string {