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}