- 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
+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",
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(
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 }
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 {