Q203: automation rules that never ran are removed, so none can claim to be enabled

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 11:17:17 -04:00
1 parent a64b44e3ea
commit 9f44e0bdee
14 files changed
+23 -119

No files matched your search

@@ -5,7 +5,6 @@ const id = z.string();
const nullableId = z.string().nullable();
const actor = z.enum(["employee", "agent"]);
const sender = z.enum(["employee", "agent", "board", "system"]);
const settings = z.record(z.string(), z.unknown());
export const collaborationContracts = {
createDocument: z.strictObject({
@@ -27,15 +26,6 @@ export const collaborationContracts = {
}) satisfies z.ZodType<C.UpdateKnowledgeEntryInput>,
deleteKnowledgeEntry: z.strictObject({ orgId: id, entryId: id, deletedByEmployeeId: id }) satisfies z.ZodType<C.DeleteKnowledgeEntryInput>,
listKnowledgeRevisions: z.strictObject({ orgId: id, entryId: id }) satisfies z.ZodType<C.ListKnowledgeRevisionsInput>,
createAutomationRule: z.strictObject({
orgId: id, name: z.string(), triggerEvent: z.string(), conditions: settings, actionType: z.string(), actionConfig: settings,
priority: z.number(), enabled: z.boolean(),
}) satisfies z.ZodType<C.CreateAutomationRuleInput>,
updateAutomationRule: z.strictObject({
orgId: id, ruleId: id, name: z.string(), triggerEvent: z.string(), conditions: settings, actionType: z.string(), actionConfig: settings,
priority: z.number(), enabled: z.boolean(),
}) satisfies z.ZodType<C.UpdateAutomationRuleInput>,
deleteAutomationRule: z.strictObject({ orgId: id, ruleId: id }) satisfies z.ZodType<C.DeleteAutomationRuleInput>,
createMeeting: z.strictObject({
orgId: id, title: z.string(), meetingType: z.string(), scheduledAt: z.string().nullable(), durationMinutes: z.number(), agenda: z.string(),
}) satisfies z.ZodType<C.CreateMeetingInput>,
-5
View File
@@ -23,7 +23,6 @@ import { OperationsService } from "../services/operations-service";
import { TaskExecutionService } from "../services/task-execution-service";
import { DocumentService } from "../services/document-service";
import { KnowledgeService } from "../services/knowledge-service";
import { AutomationService } from "../services/automation-service";
import { MeetingService } from "../services/meeting-service";
import { SprintService } from "../services/sprint-service";
import { ComposioConnectionService } from "../services/composio/composio-connection-service";
@@ -92,7 +91,6 @@ export function registerIpcHandlers(deps: IpcDeps): void {
const operations = new OperationsService(deps.connection.db);
const documents = new DocumentService(deps.connection.db);
const knowledge = new KnowledgeService(deps.connection.db);
const automation = new AutomationService(deps.connection.db);
const meetingService = new MeetingService(deps.connection.db);
const sprintService = new SprintService(deps.connection.db);
const onboarding = new OnboardingService({
@@ -242,9 +240,6 @@ export function registerIpcHandlers(deps: IpcDeps): void {
revision: r.revision, topic: r.topic, category: r.category, content: r.content, importance: r.importance,
changedByType: r.changed_by_type, changedById: r.changed_by_id, reason: r.reason, createdAt: r.created_at,
}))));
handle("createAutomationRule", (input) => mutate(() => automation.create(input)));
handle("updateAutomationRule", (input) => mutate(() => automation.update(input)));
handle("deleteAutomationRule", (input) => mutate(() => automation.delete(input)));
handle("createMeeting", (input) => mutate(() => meetingService.create(input)));
handle("deleteMeeting", (input) => mutate(() => meetingService.delete(input)));
handle("createSprint", (input) => mutate(() => sprintService.create(input)));
@@ -0,0 +1,5 @@
// Automation rules were saved but never ran; the records are removed rather than kept as a promise.
export const dropAutomationRulesSchema = `
drop index if exists idx_automation_org;
drop table if exists automation_rules;
`;
+2
View File
@@ -106,6 +106,7 @@ import { workLanguageSchema } from "./migrations/104-work-language";
import { onboardingProgressSchema } from "./migrations/105-onboarding-progress";
import { viewStateSchema } from "./migrations/106-view-state";
import { searchPendingSchema } from "./migrations/107-search-pending";
import { dropAutomationRulesSchema } from "./migrations/108-drop-automation-rules";
interface Migration {
version: number;
@@ -221,6 +222,7 @@ const MIGRATIONS: Migration[] = [
{ version: 105, name: "onboarding_progress", sql: onboardingProgressSchema },
{ version: 106, name: "view_state", sql: viewStateSchema },
{ version: 107, name: "search_pending", sql: searchPendingSchema },
{ version: 108, name: "drop_automation_rules", sql: dropAutomationRulesSchema },
];
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
-44
View File
@@ -1,44 +0,0 @@
import type { DatabaseSync } from "node:sqlite";
import type { CreateAutomationRuleInput, UpdateAutomationRuleInput, DeleteAutomationRuleInput } from "../../shared/commands";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
export class AutomationService {
private readonly audit: AuditService;
constructor(private readonly db: DatabaseSync) {
this.audit = new AuditService(db);
}
create(input: CreateAutomationRuleInput): string {
const id = createId("aut");
const now = new Date().toISOString();
this.db.prepare(`
insert into automation_rules(id, org_id, name, trigger_event, condition_json, action_type, action_config_json, priority, enabled, fire_count, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
`).run(id, input.orgId, input.name.trim(), input.triggerEvent, JSON.stringify(input.conditions), input.actionType, JSON.stringify(input.actionConfig), input.priority, input.enabled ? 1 : 0, now, now);
this.audit.record({
orgId: input.orgId,
entityType: "automation_rule",
entityId: id,
actorType: "system",
actorId: null,
action: "created",
payload: { name: input.name.trim(), triggerEvent: input.triggerEvent },
});
return id;
}
update(input: UpdateAutomationRuleInput): void {
const result = this.db.prepare(`
update automation_rules set name = ?, trigger_event = ?, condition_json = ?, action_type = ?, action_config_json = ?, priority = ?, enabled = ?, updated_at = ?
where id = ? and org_id = ?
`).run(input.name.trim(), input.triggerEvent, JSON.stringify(input.conditions), input.actionType, JSON.stringify(input.actionConfig), input.priority, input.enabled ? 1 : 0, new Date().toISOString(), input.ruleId, input.orgId);
if (result.changes === 0) throw new Error("Automation rule was not found.");
}
delete(input: DeleteAutomationRuleInput): void {
const result = this.db.prepare("delete from automation_rules where id = ? and org_id = ?").run(input.ruleId, input.orgId);
if (result.changes === 0) throw new Error("Automation rule was not found.");
}
}
-2
View File
@@ -151,8 +151,6 @@ export class SnapshotService {
auditEvents: recentInOrg<AppSnapshot["auditEvents"][number]>(this.db, "audit_events", "created_at desc", 200, activeOrgId),
documents: list(this.db, "documents", "updated_at desc"),
knowledgeEntries: list(this.db, "knowledge_entries", "updated_at desc"),
automationRules: list<AppSnapshot["automationRules"][number]>(this.db, "automation_rules", "priority asc")
.map((rule) => ({ ...rule, enabled: toBool(rule.enabled) })),
meetings: list(this.db, "meetings", "scheduled_at desc"),
sprints: list(this.db, "sprints", "start_date desc"),
connectorAccounts: list(this.db, "connector_accounts", "toolkit_slug asc"),
-3
View File
@@ -44,9 +44,6 @@ const api: DesktopApi = {
updateKnowledgeEntry: (input) => invoke(IPC_CHANNELS.updateKnowledgeEntry, input),
listDocumentVersions: (input) => invoke(IPC_CHANNELS.listDocumentVersions, input),
listKnowledgeRevisions: (input) => invoke(IPC_CHANNELS.listKnowledgeRevisions, input),
createAutomationRule: (input) => invoke(IPC_CHANNELS.createAutomationRule, input),
updateAutomationRule: (input) => invoke(IPC_CHANNELS.updateAutomationRule, input),
deleteAutomationRule: (input) => invoke(IPC_CHANNELS.deleteAutomationRule, input),
createMeeting: (input) => invoke(IPC_CHANNELS.createMeeting, input),
deleteMeeting: (input) => invoke(IPC_CHANNELS.deleteMeeting, input),
createSprint: (input) => invoke(IPC_CHANNELS.createSprint, input),
-28
View File
@@ -104,34 +104,6 @@ export interface DeleteKnowledgeEntryInput {
deletedByEmployeeId: string;
}
export interface CreateAutomationRuleInput {
orgId: string;
name: string;
triggerEvent: string;
conditions: Record<string, unknown>;
actionType: string;
actionConfig: Record<string, unknown>;
priority: number;
enabled: boolean;
}
export interface UpdateAutomationRuleInput {
orgId: string;
ruleId: string;
name: string;
triggerEvent: string;
conditions: Record<string, unknown>;
actionType: string;
actionConfig: Record<string, unknown>;
priority: number;
enabled: boolean;
}
export interface DeleteAutomationRuleInput {
orgId: string;
ruleId: string;
}
export interface CreateMeetingInput {
orgId: string;
title: string;
-6
View File
@@ -17,7 +17,6 @@ import type {
UpdateAgentInput,
UpdateEmployeeProfileInput,
CreateApprovalInput,
CreateAutomationRuleInput,
CreateDocumentInput,
CreateGoalInput,
CreateMeetingInput,
@@ -32,7 +31,6 @@ import type {
ListKnowledgeRevisionsInput,
CreateProjectInput,
CreateWorkspaceInput,
DeleteAutomationRuleInput,
DeleteDocumentInput,
DeleteTaskInput,
SetGoalDueDateInput,
@@ -41,7 +39,6 @@ import type {
UpdateWorkspaceInput,
DeleteKnowledgeEntryInput,
ResumeGoalInput,
UpdateAutomationRuleInput,
UpdateDocumentInput,
UpdateTaskInput,
CreateChatSpaceInput,
@@ -164,9 +161,6 @@ export interface DesktopApi {
updateKnowledgeEntry(input: UpdateKnowledgeEntryInput): Promise<DesktopResult<AppSnapshot>>;
listDocumentVersions(input: ListDocumentVersionsInput): Promise<DesktopResult<DocumentVersionRecord[]>>;
listKnowledgeRevisions(input: ListKnowledgeRevisionsInput): Promise<DesktopResult<KnowledgeRevisionRecord[]>>;
createAutomationRule(input: CreateAutomationRuleInput): Promise<DesktopResult<AppSnapshot>>;
updateAutomationRule(input: UpdateAutomationRuleInput): Promise<DesktopResult<AppSnapshot>>;
deleteAutomationRule(input: DeleteAutomationRuleInput): Promise<DesktopResult<AppSnapshot>>;
createMeeting(input: CreateMeetingInput): Promise<DesktopResult<AppSnapshot>>;
deleteMeeting(input: DeleteMeetingInput): Promise<DesktopResult<AppSnapshot>>;
createSprint(input: CreateSprintInput): Promise<DesktopResult<AppSnapshot>>;
-15
View File
@@ -539,18 +539,3 @@ export interface ConnectorGrantRecord {
createdAt: string;
}
export interface AutomationRuleRecord {
id: string;
orgId: string;
name: string;
triggerEvent: string;
conditionJson: string;
actionType: string;
actionConfigJson: string;
priority: number;
enabled: boolean;
lastFiredAt: string | null;
fireCount: number;
createdAt: string;
updatedAt: string;
}
-3
View File
@@ -77,9 +77,6 @@ export const IPC_CHANNELS = {
updateKnowledgeEntry: "agentcompany:update-knowledge-entry",
listDocumentVersions: "agentcompany:list-document-versions",
listKnowledgeRevisions: "agentcompany:list-knowledge-revisions",
createAutomationRule: "agentcompany:create-automation-rule",
updateAutomationRule: "agentcompany:update-automation-rule",
deleteAutomationRule: "agentcompany:delete-automation-rule",
createMeeting: "agentcompany:create-meeting",
deleteMeeting: "agentcompany:delete-meeting",
createSprint: "agentcompany:create-sprint",
-2
View File
@@ -19,7 +19,6 @@ import type {
AgentRecord,
ApprovalRecord,
AuditEventRecord,
AutomationRuleRecord,
ConnectorAccountRecord,
ConnectorGrantRecord,
DocumentRecord,
@@ -127,7 +126,6 @@ export interface AppSnapshot {
auditEvents: AuditEventRecord[];
documents: DocumentRecord[];
knowledgeEntries: KnowledgeEntryRecord[];
automationRules: AutomationRuleRecord[];
meetings: MeetingRecord[];
sprints: SprintRecord[];
connectorAccounts: ConnectorAccountRecord[];
+1 -1
View File
@@ -36,7 +36,7 @@ describe("domain services", () => {
const connection = createConnection();
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107]);
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108]);
connection.close();
});
+15
View File
@@ -0,0 +1,15 @@
import { join } from "node:path";
import { tmpdir } from "node:os";
import { randomUUID } from "node:crypto";
import { expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { IPC_CHANNELS } from "../../src/shared/ipc";
// A record type nothing runs or shows is not kept: saving it would only claim it works.
test("automation rules are gone: no table, no channels", () => {
const connection = new DatabaseConnection(join(tmpdir(), `agentcompany-new-${randomUUID()}.db`));
const tables = connection.db.prepare("select name from sqlite_master where type = 'table'").all() as Array<{ name: string }>;
connection.close();
expect(tables.map((row) => row.name)).not.toContain("automation_rules");
expect(Object.keys(IPC_CHANNELS).filter((key) => /automation/i.test(key))).toEqual([]);
});