Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/.release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "1.15.2"
".": "1.15.3"
}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [1.15.3](https://github.com/trycompai/crm/compare/v1.15.2...v1.15.3) (2026-08-21)


### Fixes

* **app:** prevent url param collision between fields sheet and table filter ([#175](https://github.com/trycompai/crm/issues/175)) ([1cebe1e](https://github.com/trycompai/crm/commit/1cebe1e5b3087007c4eac8b18add38ff9e8dd69b))
* stop a finished enrichment reading as failed ([#173](https://github.com/trycompai/crm/issues/173)) ([2946580](https://github.com/trycompai/crm/commit/2946580afdd43419ce08165a0d55ba9d084c554e))

## [1.15.2](https://github.com/trycompai/crm/compare/v1.15.1...v1.15.2) (2026-08-20)


Expand Down
21 changes: 16 additions & 5 deletions apps/agent/agent/lib/brand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { db, EnrichmentStatus } from "@crm/db";
import { mirrorBrandImages } from "./brand-images";
import { brandToUpdate, filledFields, stillFillable } from "./brand-mapping";
import { brandByDomain, contextDevEnabled } from "./context-dev";
import { UNLESS_COMPLETE } from "./enrichment";

export type BrandResult = {
enriched: boolean;
Expand Down Expand Up @@ -65,15 +66,20 @@ export async function runBrand({
}

if (!company.domain) {
await settle(companyId, EnrichmentStatus.SKIPPED, "No domain to look up.");
await settle(
companyId,
EnrichmentStatus.SKIPPED,
"No domain to look up.",
UNLESS_COMPLETE,
);
return { enriched: false, reason: "No domain on this company." };
}

const charge = spend(2);
if (!charge.ok) return { enriched: false, reason: charge.reason };

await db.company.update({
where: { id: companyId },
await db.company.updateMany({
where: { id: companyId, ...UNLESS_COMPLETE },
data: {
enrichmentStatus: EnrichmentStatus.RUNNING,
enrichmentError: null,
Expand Down Expand Up @@ -157,13 +163,18 @@ export function brandOutcome(result: BrandResult): string {
return `Filled ${filled.join(", ")}.${mirrored.length > 0 ? ` Copied ${mirrored.length} image(s) in-house.` : ""}`;
}

type SettleGuard =
| typeof UNLESS_COMPLETE
| { enrichmentStatus: EnrichmentStatus };

async function settle(
companyId: string,
status: EnrichmentStatus,
error: string,
guard: SettleGuard = { enrichmentStatus: EnrichmentStatus.RUNNING },
): Promise<void> {
await db.company.update({
where: { id: companyId },
await db.company.updateMany({
where: { id: companyId, ...guard },
data: { enrichmentStatus: status, enrichmentError: error },
});
}
81 changes: 67 additions & 14 deletions apps/agent/agent/lib/enrichment.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,83 @@
import { db, EnrichmentStatus, type Prisma } from "@crm/db";
import { ownsCompanyStatus, ownsContactStatus } from "@crm/db/agent-tasks";
import type { TaskSubject } from "./tasks";

type StatusGuard =
| EnrichmentStatus
| { not: EnrichmentStatus }
| { in: EnrichmentStatus[] };

type SettleGuard = {
enrichmentStatus?: EnrichmentStatus;
enrichmentStatus?: StatusGuard;
OR?: Array<{
enrichmentStatus: EnrichmentStatus;
updatedAt?: { lt: Date };
}>;
};

type OwnedColumns = {
contactId: string | null;
companyId: string | null;
};

export const UNLESS_COMPLETE = {
enrichmentStatus: { not: EnrichmentStatus.COMPLETE },
} as const;

function ownedColumns(subject: TaskSubject): OwnedColumns {
return {
contactId:
subject.contactId && ownsContactStatus(subject.kind)
? subject.contactId
: null,
companyId:
subject.companyId && ownsCompanyStatus(subject.kind)
? subject.companyId
: null,
};
}

export async function markRunning(subject: TaskSubject): Promise<void> {
await write(subject, EnrichmentStatus.RUNNING, null, false);
await write(ownedColumns(subject), EnrichmentStatus.RUNNING, null, {
...UNLESS_COMPLETE,
});
}

export async function settle(
subject: TaskSubject,
status: EnrichmentStatus,
error?: string,
): Promise<void> {
await write(subject, status, error ?? null, true);
const owned = ownedColumns(subject);
if (!owned.contactId && !owned.companyId) return;

await write(owned, status, error ?? null, await settleable(subject, status));
}

async function write(
subject: TaskSubject,
owned: OwnedColumns,
status: EnrichmentStatus,
error: string | null,
onlyIfRunning: boolean,
guard: SettleGuard,
): Promise<void> {
if (!subject.contactId && !subject.companyId) return;
if (!owned.contactId && !owned.companyId) return;

const data = {
enrichmentStatus: status,
enrichmentError: error,
enrichedAt: status === EnrichmentStatus.COMPLETE ? new Date() : undefined,
};

const guard: SettleGuard = onlyIfRunning
? await settleable(subject, status)
: {};

if (subject.contactId) {
if (owned.contactId) {
await db.contact.updateMany({
where: { id: subject.contactId, ...guard },
where: { id: owned.contactId, ...guard },
data,
});
}

if (subject.companyId) {
if (owned.companyId) {
await db.company.updateMany({
where: { id: subject.companyId, ...guard },
where: { id: owned.companyId, ...guard },
data,
});
}
Expand All @@ -59,6 +88,30 @@ async function settleable(
status: EnrichmentStatus,
): Promise<SettleGuard> {
const running = { enrichmentStatus: EnrichmentStatus.RUNNING };

if (status === EnrichmentStatus.COMPLETE) {
const done = {
enrichmentStatus: {
in: [EnrichmentStatus.RUNNING, EnrichmentStatus.COMPLETE],
},
};

const endedAt = await taskEndedAt(subject.id);
if (!endedAt) return done;
if (await hasOpenRequest(subject)) return done;

return {
OR: [
running,
{ enrichmentStatus: EnrichmentStatus.COMPLETE },
{
enrichmentStatus: EnrichmentStatus.PENDING,
updatedAt: { lt: endedAt },
},
],
};
}

if (status !== EnrichmentStatus.FAILED) return running;

const endedAt = await taskEndedAt(subject.id);
Expand Down
12 changes: 10 additions & 2 deletions apps/agent/agent/lib/stale-tasks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { db, EnrichmentStatus, type Prisma } from "@crm/db";
import { isEnrichmentKind, MAX_ATTEMPTS } from "@crm/db/agent-tasks";
import {
MAX_ATTEMPTS,
ownsCompanyStatus,
ownsContactStatus,
} from "@crm/db/agent-tasks";
import { DISPATCH } from "./dispatch-config";
import { settle } from "./enrichment";
import { retireExhausted, type TaskSubject } from "./tasks";
Expand Down Expand Up @@ -214,12 +218,16 @@ function finishedElsewhere(
task: OpenTask,
completed: Map<string, Date>,
): boolean {
if (!isEnrichmentKind(task.kind)) return false;
if (task.attempts === 0 || task.startedAt === null) return false;

const subjectId = task.contactId ?? task.companyId;
if (!subjectId) return false;

const owns = task.contactId
? ownsContactStatus(task.kind)
: ownsCompanyStatus(task.kind);
if (!owns) return false;

const enrichedAt = completed.get(subjectId);
if (!enrichedAt) return false;

Expand Down
10 changes: 8 additions & 2 deletions apps/agent/test/close-task.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { db, EnrichmentStatus } from "@crm/db";
import { closeTask, taskToken } from "../agent/channels/crm";

const kind = "test-close-task";
const kind = "identify";
const email = `close-task-${crypto.randomUUID()}@example.test`;

const taskIds: string[] = [];

async function clear() {
await db.agentTask.deleteMany({ where: { kind } });
if (taskIds.length > 0) {
await db.agentTask.deleteMany({ where: { id: { in: taskIds.splice(0) } } });
}
await db.contact.deleteMany({ where: { email } });
}

Expand Down Expand Up @@ -36,6 +40,8 @@ async function running() {
select: { id: true },
});

taskIds.push(task.id);

return { contactId: contact.id, taskId: task.id };
}

Expand Down
Loading