baa-conductor


commit
ffda03b
parent
fdfa382
author
codex@macbookpro
date
2026-04-01 19:32:28 +0800 CST
refactor: dedupe renewal utility helpers
7 files changed,  +117, -92
Raw patch view.
  1diff --git a/apps/conductor-daemon/src/index.test.js b/apps/conductor-daemon/src/index.test.js
  2index 7e4bae0409347254c8254e06adc62d930d5e305e..eef834882b46e5d18c389835464ce25e7b45e5b0 100644
  3--- a/apps/conductor-daemon/src/index.test.js
  4+++ b/apps/conductor-daemon/src/index.test.js
  5@@ -13,6 +13,7 @@ import {
  6   ArtifactStore
  7 } from "../../../packages/artifact-db/dist/index.js";
  8 import "./artifacts.test.js";
  9+import "./renewal/utils.test.js";
 10 import { ConductorLocalControlPlane } from "../dist/local-control-plane.js";
 11 import { FirefoxCommandBroker } from "../dist/firefox-bridge.js";
 12 import {
 13diff --git a/apps/conductor-daemon/src/renewal/automation.ts b/apps/conductor-daemon/src/renewal/automation.ts
 14index b1abf7f71d9a999e3980c7e123a6327bf6e760e6..605b5eafbc29e7befe448dcad9fcfebf88f8f1d8 100644
 15--- a/apps/conductor-daemon/src/renewal/automation.ts
 16+++ b/apps/conductor-daemon/src/renewal/automation.ts
 17@@ -6,6 +6,8 @@ import type {
 18   LocalConversationRecord
 19 } from "../../../../packages/artifact-db/dist/index.js";
 20 
 21+import { normalizeOptionalString } from "./utils.js";
 22+
 23 export const CONVERSATION_AUTOMATION_FAILURE_THRESHOLD = 3;
 24 export const CONVERSATION_REPEATED_MESSAGE_THRESHOLD = 3;
 25 export const CONVERSATION_REPEATED_RENEWAL_THRESHOLD = 3;
 26@@ -198,12 +200,3 @@ function normalizeAutomationText(value: string): string {
 27     .replace(/\s+/gu, " ")
 28     .trim();
 29 }
 30-
 31-function normalizeOptionalString(value: string | null | undefined): string | null {
 32-  if (value == null) {
 33-    return null;
 34-  }
 35-
 36-  const normalized = value.trim();
 37-  return normalized === "" ? null : normalized;
 38-}
 39diff --git a/apps/conductor-daemon/src/renewal/conversations.ts b/apps/conductor-daemon/src/renewal/conversations.ts
 40index ffb3e058675ae35444bc8f2e396a7cf9cc4fa08a..14f40335057990f307a9dca08d8419b6a16a5f35 100644
 41--- a/apps/conductor-daemon/src/renewal/conversations.ts
 42+++ b/apps/conductor-daemon/src/renewal/conversations.ts
 43@@ -11,6 +11,10 @@ import type {
 44 
 45 import type { BaaDeliveryRouteSnapshot } from "../artifacts/types.js";
 46 import { buildConversationAutomationResetPatch } from "./automation.js";
 47+import {
 48+  normalizeOptionalString,
 49+  normalizeRequiredString
 50+} from "./utils.js";
 51 
 52 const LOCAL_CONVERSATION_ID_PREFIX = "lc_";
 53 const CONVERSATION_LINK_ID_PREFIX = "link_";
 54@@ -141,7 +145,9 @@ export async function listRenewalConversationDetails(
 55 export async function observeRenewalConversation(
 56   input: ObserveRenewalConversationInput
 57 ): Promise<ObserveRenewalConversationResult> {
 58-  const platform = normalizeRequiredString(input.platform, "platform");
 59+  const platform = normalizeRequiredString(input.platform, "platform", {
 60+    errorStyle: "field"
 61+  });
 62   const observedAt = normalizeTimestamp(input.observedAt, "observedAt");
 63   const clientId = normalizeOptionalString(input.clientId);
 64   const observedRoute = buildObservedRoute(platform, {
 65@@ -198,7 +204,9 @@ export async function observeRenewalConversation(
 66 
 67   conversation = await input.store.upsertLocalConversation({
 68     lastMessageAt: observedAt,
 69-    lastMessageId: normalizeRequiredString(input.assistantMessageId, "assistantMessageId"),
 70+    lastMessageId: normalizeRequiredString(input.assistantMessageId, "assistantMessageId", {
 71+      errorStyle: "field"
 72+    }),
 73     localConversationId: conversation.localConversationId,
 74     platform: conversation.platform,
 75     title: observedRoute.pageTitle ?? undefined,
 76@@ -220,7 +228,9 @@ export async function setRenewalConversationAutomationStatus(input: {
 77   pauseReason?: ConversationPauseReason | null;
 78   store: ArtifactStore;
 79 }): Promise<RenewalConversationDetail> {
 80-  const localConversationId = normalizeRequiredString(input.localConversationId, "localConversationId");
 81+  const localConversationId = normalizeRequiredString(input.localConversationId, "localConversationId", {
 82+    errorStyle: "field"
 83+  });
 84   const existing = await input.store.getLocalConversation(localConversationId);
 85 
 86   if (existing == null) {
 87@@ -526,7 +536,9 @@ async function loadOrCreateLocalConversation(
 88     automationStatus: "manual",
 89     createdAt: input.observedAt,
 90     lastMessageAt: input.observedAt,
 91-    lastMessageId: normalizeRequiredString(input.assistantMessageId, "assistantMessageId"),
 92+    lastMessageId: normalizeRequiredString(input.assistantMessageId, "assistantMessageId", {
 93+      errorStyle: "field"
 94+    }),
 95     localConversationId: buildLocalConversationId(),
 96     platform: input.platform,
 97     title: input.pageTitle,
 98@@ -785,30 +797,11 @@ function scoreConversationLink(
 99   };
100 }
101 
102-function normalizeOptionalString(value: unknown): string | null {
103-  if (typeof value !== "string") {
104-    return null;
105-  }
106-
107-  const normalized = value.trim();
108-  return normalized === "" ? null : normalized;
109-}
110-
111 function normalizePathname(value: string): string {
112   const normalized = value.replace(/\/+$/u, "");
113   return normalized === "" ? "/" : normalized;
114 }
115 
116-function normalizeRequiredString(value: unknown, fieldName: string): string {
117-  const normalized = normalizeOptionalString(value);
118-
119-  if (normalized == null) {
120-    throw new Error(`Field "${fieldName}" must be a non-empty string.`);
121-  }
122-
123-  return normalized;
124-}
125-
126 function normalizeTimestamp(value: unknown, fieldName: string): number {
127   if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
128     throw new Error(`Field "${fieldName}" must be a positive finite number.`);
129diff --git a/apps/conductor-daemon/src/renewal/dispatcher.ts b/apps/conductor-daemon/src/renewal/dispatcher.ts
130index e0d404de5e4d4134ae6f60023ec9271fed9dc64d..c4441df406e45e4656570fa1b2d2cf1d6ba2c11f 100644
131--- a/apps/conductor-daemon/src/renewal/dispatcher.ts
132+++ b/apps/conductor-daemon/src/renewal/dispatcher.ts
133@@ -26,6 +26,11 @@ import {
134   recordAutomationFailureSignal,
135   recordAutomationSuccessSignal
136 } from "./automation.js";
137+import {
138+  isPlainRecord,
139+  normalizeOptionalString,
140+  parseJsonValue
141+} from "./utils.js";
142 
143 const DEFAULT_RECHECK_DELAY_MS = 10_000;
144 const DEFAULT_INTER_JOB_JITTER_MIN_MS = 500;
145@@ -1082,10 +1087,6 @@ function parseDownstreamStatusCode(message: string): number | null {
146   return Number(match[1]);
147 }
148 
149-function isPlainRecord(value: unknown): value is Record<string, unknown> {
150-  return typeof value === "object" && value != null && !Array.isArray(value);
151-}
152-
153 function isRenewalProjectorPayload(value: unknown): value is RenewalProjectorPayload {
154   return (
155     isPlainRecord(value)
156@@ -1109,27 +1110,6 @@ function isRenewalTargetSnapshot(value: unknown): value is RenewalProjectorTarge
157   );
158 }
159 
160-function normalizeOptionalString(value: string | null | undefined): string | null {
161-  if (value == null) {
162-    return null;
163-  }
164-
165-  const normalized = value.trim();
166-  return normalized === "" ? null : normalized;
167-}
168-
169-function parseJsonValue(value: string | null): unknown {
170-  if (value == null) {
171-    return null;
172-  }
173-
174-  try {
175-    return JSON.parse(value);
176-  } catch {
177-    return null;
178-  }
179-}
180-
181 function parseTabId(value: string | null): number | null {
182   const normalized = normalizeOptionalString(value);
183 
184diff --git a/apps/conductor-daemon/src/renewal/projector.ts b/apps/conductor-daemon/src/renewal/projector.ts
185index 1db85df9c3f017cb826d301a16354b20a047fce3..0a9fcf056ac510adb20344f8ac0f3b595199eb82 100644
186--- a/apps/conductor-daemon/src/renewal/projector.ts
187+++ b/apps/conductor-daemon/src/renewal/projector.ts
188@@ -17,6 +17,12 @@ import {
189   recordAutomationFailureSignal,
190   recordRenewalPayloadSignal
191 } from "./automation.js";
192+import {
193+  isPlainRecord,
194+  normalizeOptionalString,
195+  normalizeRequiredString,
196+  parseJsonValue
197+} from "./utils.js";
198 
199 const DEFAULT_CURSOR_STATE_KEY = "renewal.projector.cursor";
200 const DEFAULT_MESSAGE_ROLE = "assistant";
201@@ -644,25 +650,6 @@ function normalizeCursorStateKey(value: string | undefined): string {
202   return normalizeRequiredString(value ?? DEFAULT_CURSOR_STATE_KEY, "cursorStateKey");
203 }
204 
205-function normalizeOptionalString(value: string | null | undefined): string | null {
206-  if (value == null) {
207-    return null;
208-  }
209-
210-  const normalized = value.trim();
211-  return normalized === "" ? null : normalized;
212-}
213-
214-function normalizeRequiredString(value: string | null | undefined, name: string): string {
215-  const normalized = normalizeOptionalString(value);
216-
217-  if (normalized == null) {
218-    throw new Error(`${name} must be a non-empty string.`);
219-  }
220-
221-  return normalized;
222-}
223-
224 function parseJsonRecord(value: string | null): Record<string, unknown> | null {
225   const parsed = parseJsonValue(value);
226   return isPlainRecord(parsed) ? parsed : null;
227@@ -685,18 +672,6 @@ function parseTargetTabId(value: string | null): number | null {
228   return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
229 }
230 
231-function parseJsonValue(value: string | null): unknown {
232-  if (value == null) {
233-    return null;
234-  }
235-
236-  try {
237-    return JSON.parse(value);
238-  } catch {
239-    return null;
240-  }
241-}
242-
243 async function saveCursor(
244   repository: Pick<ControlPlaneRepository, "putSystemState">,
245   cursorStateKey: string,
246@@ -713,10 +688,6 @@ async function saveCursor(
247   });
248 }
249 
250-function isPlainRecord(value: unknown): value is Record<string, unknown> {
251-  return typeof value === "object" && value != null && !Array.isArray(value);
252-}
253-
254 function truncateText(text: string, limit: number): string {
255   if (text.length <= limit) {
256     return text;
257diff --git a/apps/conductor-daemon/src/renewal/utils.test.js b/apps/conductor-daemon/src/renewal/utils.test.js
258new file mode 100644
259index 0000000000000000000000000000000000000000..12dc29ee3503133a3a6fde08d0113bd39f7c46f8
260--- /dev/null
261+++ b/apps/conductor-daemon/src/renewal/utils.test.js
262@@ -0,0 +1,41 @@
263+import assert from "node:assert/strict";
264+import test from "node:test";
265+
266+import {
267+  isPlainRecord,
268+  normalizeOptionalString,
269+  normalizeRequiredString,
270+  parseJsonValue
271+} from "../../dist/renewal/utils.js";
272+
273+test("renewal utils normalize optional and required strings without changing legacy error styles", () => {
274+  assert.equal(normalizeOptionalString("  hello  "), "hello");
275+  assert.equal(normalizeOptionalString("   "), null);
276+  assert.equal(normalizeOptionalString(null), null);
277+  assert.equal(normalizeOptionalString(123), null);
278+
279+  assert.equal(normalizeRequiredString("  value  ", "name"), "value");
280+  assert.throws(
281+    () => normalizeRequiredString("   ", "cursorStateKey"),
282+    /cursorStateKey must be a non-empty string\./u
283+  );
284+  assert.throws(
285+    () => normalizeRequiredString(null, "platform", {
286+      errorStyle: "field"
287+    }),
288+    /Field "platform" must be a non-empty string\./u
289+  );
290+});
291+
292+test("renewal utils parse JSON values and distinguish plain records", () => {
293+  const parsedRecord = parseJsonValue("{\"text\":\"hello\"}");
294+
295+  assert.deepEqual(parsedRecord, {
296+    text: "hello"
297+  });
298+  assert.equal(isPlainRecord(parsedRecord), true);
299+  assert.equal(isPlainRecord(parseJsonValue("[1,2,3]")), false);
300+  assert.equal(isPlainRecord(parseJsonValue("\"hello\"")), false);
301+  assert.equal(isPlainRecord(parseJsonValue(null)), false);
302+  assert.equal(parseJsonValue("{"), null);
303+});
304diff --git a/apps/conductor-daemon/src/renewal/utils.ts b/apps/conductor-daemon/src/renewal/utils.ts
305new file mode 100644
306index 0000000000000000000000000000000000000000..3470777f85ec4943871ed7578075dd04454e7761
307--- /dev/null
308+++ b/apps/conductor-daemon/src/renewal/utils.ts
309@@ -0,0 +1,46 @@
310+interface NormalizeRequiredStringOptions {
311+  errorStyle?: "field" | "plain";
312+}
313+
314+export function normalizeOptionalString(value: unknown): string | null {
315+  if (typeof value !== "string") {
316+    return null;
317+  }
318+
319+  const normalized = value.trim();
320+  return normalized === "" ? null : normalized;
321+}
322+
323+export function normalizeRequiredString(
324+  value: unknown,
325+  name: string,
326+  options: NormalizeRequiredStringOptions = {}
327+): string {
328+  const normalized = normalizeOptionalString(value);
329+
330+  if (normalized == null) {
331+    throw new Error(
332+      options.errorStyle === "field"
333+        ? `Field "${name}" must be a non-empty string.`
334+        : `${name} must be a non-empty string.`
335+    );
336+  }
337+
338+  return normalized;
339+}
340+
341+export function parseJsonValue(value: string | null | undefined): unknown {
342+  if (value == null) {
343+    return null;
344+  }
345+
346+  try {
347+    return JSON.parse(value);
348+  } catch {
349+    return null;
350+  }
351+}
352+
353+export function isPlainRecord(value: unknown): value is Record<string, unknown> {
354+  return typeof value === "object" && value != null && !Array.isArray(value);
355+}