baa-conductor


baa-conductor / apps / conductor-daemon / src / renewal
codex@macbookpro  ·  2026-04-01

automation.ts

  1import { createHash } from "node:crypto";
  2import type {
  3  ArtifactStore,
  4  ConversationAutomationStatus,
  5  ConversationPauseReason,
  6  LocalConversationRecord
  7} from "../../../../packages/artifact-db/dist/index.js";
  8
  9import { normalizeOptionalString } from "./utils.js";
 10
 11export const CONVERSATION_AUTOMATION_FAILURE_THRESHOLD = 3;
 12export const CONVERSATION_REPEATED_MESSAGE_THRESHOLD = 3;
 13export const CONVERSATION_REPEATED_RENEWAL_THRESHOLD = 3;
 14
 15export interface ConversationAutomationResetOptions {
 16  automationStatus: ConversationAutomationStatus;
 17}
 18
 19export function buildConversationAutomationResetPatch(
 20  options: ConversationAutomationResetOptions
 21): {
 22  automationStatus: ConversationAutomationStatus;
 23  consecutiveFailureCount: number;
 24  lastError: null;
 25  lastMessageFingerprint: null;
 26  lastRenewalFingerprint: null;
 27  pauseReason: null;
 28  pausedAt: null;
 29  repeatedMessageCount: number;
 30  repeatedRenewalCount: number;
 31} {
 32  return {
 33    automationStatus: options.automationStatus,
 34    consecutiveFailureCount: 0,
 35    lastError: null,
 36    lastMessageFingerprint: null,
 37    lastRenewalFingerprint: null,
 38    pauseReason: null,
 39    pausedAt: null,
 40    repeatedMessageCount: 0,
 41    repeatedRenewalCount: 0
 42  };
 43}
 44
 45export function buildAutomationTextFingerprint(text: string): string {
 46  return createHash("sha256")
 47    .update(normalizeAutomationText(text))
 48    .digest("hex");
 49}
 50
 51export async function pauseConversationAutomation(input: {
 52  conversation: LocalConversationRecord;
 53  force?: boolean;
 54  lastError?: string | null;
 55  observedAt: number;
 56  reason: ConversationPauseReason;
 57  store: Pick<ArtifactStore, "upsertLocalConversation">;
 58}): Promise<LocalConversationRecord> {
 59  const force = input.force === true;
 60  const existingPauseReason = input.conversation.pauseReason;
 61
 62  if (!force && input.conversation.automationStatus === "paused" && existingPauseReason != null) {
 63    return input.store.upsertLocalConversation({
 64      lastError: normalizeOptionalString(input.lastError) ?? input.conversation.lastError,
 65      localConversationId: input.conversation.localConversationId,
 66      platform: input.conversation.platform,
 67      updatedAt: input.observedAt
 68    });
 69  }
 70
 71  return input.store.upsertLocalConversation({
 72    automationStatus: "paused",
 73    lastError: normalizeOptionalString(input.lastError) ?? input.conversation.lastError,
 74    localConversationId: input.conversation.localConversationId,
 75    pauseReason: input.reason,
 76    pausedAt: input.observedAt,
 77    platform: input.conversation.platform,
 78    updatedAt: input.observedAt
 79  });
 80}
 81
 82export async function recordAssistantMessageAutomationSignal(input: {
 83  conversation: LocalConversationRecord;
 84  observedAt: number;
 85  rawText: string;
 86  store: Pick<ArtifactStore, "upsertLocalConversation">;
 87}): Promise<LocalConversationRecord> {
 88  const fingerprint = buildAutomationTextFingerprint(input.rawText);
 89  const repeatedMessageCount =
 90    input.conversation.lastMessageFingerprint === fingerprint
 91      ? input.conversation.repeatedMessageCount + 1
 92      : 1;
 93  const updatedConversation = await input.store.upsertLocalConversation({
 94    lastMessageFingerprint: fingerprint,
 95    localConversationId: input.conversation.localConversationId,
 96    platform: input.conversation.platform,
 97    repeatedMessageCount,
 98    updatedAt: input.observedAt
 99  });
100
101  if (repeatedMessageCount < CONVERSATION_REPEATED_MESSAGE_THRESHOLD) {
102    return updatedConversation;
103  }
104
105  return pauseConversationAutomation({
106    conversation: updatedConversation,
107    lastError: `repeated assistant message detected (${repeatedMessageCount}x)`,
108    observedAt: input.observedAt,
109    reason: "repeated_message",
110    store: input.store
111  });
112}
113
114export async function recordAutomationFailureSignal(input: {
115  conversation: LocalConversationRecord;
116  errorMessage: string;
117  observedAt: number;
118  store: Pick<ArtifactStore, "upsertLocalConversation">;
119}): Promise<LocalConversationRecord> {
120  const lastError = normalizeOptionalString(input.errorMessage) ?? "automation_failure";
121  const consecutiveFailureCount = input.conversation.consecutiveFailureCount + 1;
122  const updatedConversation = await input.store.upsertLocalConversation({
123    consecutiveFailureCount,
124    lastError,
125    localConversationId: input.conversation.localConversationId,
126    platform: input.conversation.platform,
127    updatedAt: input.observedAt
128  });
129
130  if (consecutiveFailureCount < CONVERSATION_AUTOMATION_FAILURE_THRESHOLD) {
131    return updatedConversation;
132  }
133
134  return pauseConversationAutomation({
135    conversation: updatedConversation,
136    lastError,
137    observedAt: input.observedAt,
138    reason: "execution_failure",
139    store: input.store
140  });
141}
142
143export async function recordAutomationSuccessSignal(input: {
144  conversation: LocalConversationRecord;
145  observedAt: number;
146  store: Pick<ArtifactStore, "upsertLocalConversation">;
147}): Promise<LocalConversationRecord> {
148  if (input.conversation.consecutiveFailureCount === 0 && input.conversation.lastError == null) {
149    return input.conversation;
150  }
151
152  return input.store.upsertLocalConversation({
153    consecutiveFailureCount: 0,
154    lastError: null,
155    localConversationId: input.conversation.localConversationId,
156    platform: input.conversation.platform,
157    updatedAt: input.observedAt
158  });
159}
160
161export async function recordRenewalPayloadSignal(input: {
162  conversation: LocalConversationRecord;
163  observedAt: number;
164  payloadText: string;
165  store: Pick<ArtifactStore, "upsertLocalConversation">;
166}): Promise<LocalConversationRecord> {
167  const fingerprint = buildAutomationTextFingerprint(input.payloadText);
168  const repeatedRenewalCount =
169    input.conversation.lastRenewalFingerprint === fingerprint
170      ? input.conversation.repeatedRenewalCount + 1
171      : 1;
172  const updatedConversation = await input.store.upsertLocalConversation({
173    lastRenewalFingerprint: fingerprint,
174    localConversationId: input.conversation.localConversationId,
175    platform: input.conversation.platform,
176    repeatedRenewalCount,
177    updatedAt: input.observedAt
178  });
179
180  if (repeatedRenewalCount < CONVERSATION_REPEATED_RENEWAL_THRESHOLD) {
181    return updatedConversation;
182  }
183
184  return pauseConversationAutomation({
185    conversation: updatedConversation,
186    lastError: `repeated renewal payload detected (${repeatedRenewalCount}x)`,
187    observedAt: input.observedAt,
188    reason: "repeated_renewal",
189    store: input.store
190  });
191}
192
193function normalizeAutomationText(value: string): string {
194  return value
195    .toLowerCase()
196    .replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gu, "<uuid>")
197    .replace(/\b\d{4}-\d{2}-\d{2}(?:[ t]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?)?(?:z|[+-]\d{2}:?\d{2})?\b/gu, "<ts>")
198    .replace(/\b(?:msg|conv|job|req|run|tab|trace)[-_:/]?[a-z0-9]{4,}\b/gu, "<id>")
199    .replace(/\b\d{4,}\b/gu, "<n>")
200    .replace(/\s+/gu, " ")
201    .trim();
202}