feat(agent): scope field backfill tasks to records missing values - #163
Conversation
…agement - Added support for field backfill tasks in the agent system, allowing for better handling of missing custom field values. - Introduced new API key management features, including creation, revocation, and listing of API keys. - Updated various contracts and routers to accommodate new functionalities and ensure proper validation. - Enhanced existing agent logic to improve task handling and data processing. This commit improves the overall agent capabilities and API security management.
- Updated the logic for determining the deployable status of the agent to only consider the "READY" state, removing the "DEPLOYED" condition. - This change simplifies the deployment criteria and ensures more accurate status handling.
- Updated the way NODE_ENV is accessed and modified in the test utility function to use a type assertion for better clarity and safety. - This change enhances the maintainability of the test code by ensuring consistent access to the environment variable.
…ple files - Added missing imports and adjusted the order of imports for consistency. - Reformatted code for better readability, including consistent indentation and line breaks. - Enhanced the clarity of type definitions and function parameters in various services and routers. - This commit improves overall code maintainability and readability.
There was a problem hiding this comment.
12 issues found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/agent/agent/lib/preamble.ts">
<violation number="1" location="apps/agent/agent/lib/preamble.ts:55">
P2: When another writer fills a field after the task is queued, this instruction can overwrite that value because the agent cannot read current custom values and `set_field_value` is not blank-only. Add a current-value check or enforce blank-only writes for backfill tasks before setting the field.</violation>
</file>
<file name="apps/agent/agent/lib/facts.ts">
<violation number="1" location="apps/agent/agent/lib/facts.ts:13">
P2: Existing APPLIED `seniority` and `function` facts remain absent from `Contact` after this mapping change. Add a one-time migration or repair that copies the current APPLIED fact into each new column, because the normal record and blank-fact paths skip those rows.</violation>
<violation number="2" location="apps/agent/agent/lib/facts.ts:13">
P2: Existing APPLIED seniority/function facts were never written to the contact columns before this change (they were stored only in `contactFact`, since `column` was previously `null`). After deploy, the migration creates the columns but nothing backfills those already-applied values, and `sweepBlankFacts`/`recordFact` both treat a present APPLIED fact as `hasAgentFact`, so `fillsBlank` returns false and the column is left NULL. Contacts that already have an APPLIED seniority/function fact will thus stay missing from the new columns (and from CRM filters on `contact.seniority`/`contact.function`) until the value changes and is re-recorded. Consider a one-time backfill that copies existing APPLIED values into the new columns.</violation>
</file>
<file name="apps/api/src/companies/companies.service.ts">
<violation number="1" location="apps/api/src/companies/companies.service.ts:622">
P2: When a caller sends an unsupported activity value, `activityFilter` builds an invalid `Date` and the company list fails instead of returning a validation error. Restrict `activity` to `ACTIVITY_WINDOWS` in the input schema before calling this helper.</violation>
</file>
<file name="apps/app/components/crm/fields/field-facets.tsx">
<violation number="1" location="apps/app/components/crm/fields/field-facets.tsx:18">
P2: After an admin enables, disables, or creates a filterable field, this `fields.filters` query is not invalidated, so the facet bar stays stale until its query becomes stale or the page reloads. Include `trpc.fields.filters.queryKey()` in the field-settings cache invalidation.</violation>
</file>
<file name="apps/app/app/(app)/[slug]/deals/deals-bulk-actions.tsx">
<violation number="1" location="apps/app/app/(app)/[slug]/deals/deals-bulk-actions.tsx:121">
P0: When switching to Archived while the archived query is still loading, this branch can show `Delete forever` for the selected active deals retained by the placeholder. Bulk purge accepts those IDs without checking their archived state, so users can permanently delete active deals; clear the selection or disable archived actions until archived rows load.</violation>
</file>
<file name="apps/api/src/archive/archive-retention.controller.ts">
<violation number="1" location="apps/api/src/archive/archive-retention.controller.ts:87">
P1: When an expired company has deals, this concurrent sweep can cascade-delete the deals before `DealsService.purge` runs. That skips deal-specific cleanup and reports those deals as failed; clean dependent deals before deleting the company or make the company purge handle their cleanup.</violation>
</file>
<file name="apps/api/src/trpc/list-input.ts">
<violation number="1" location="apps/api/src/trpc/list-input.ts:127">
P2: When a list request contains an unsupported activity value, `activityFilter` produces an `Invalid Date` instead of a valid filter, causing the list query to fail. Validate values against `ACTIVITY_WINDOWS` before calculating the cutoff, or reject invalid values in the list-input schemas.</violation>
</file>
<file name="apps/api/src/agent/agent-trigger.service.ts">
<violation number="1" location="apps/api/src/agent/agent-trigger.service.ts:225">
P2: When pending field-backfill tasks from before this rollout exist, this predicate never finds them because those rows have no record ID. Retire or migrate legacy untargeted tasks during rollout so they cannot still dispatch without a focused record.</violation>
</file>
<file name="package.json">
<violation number="1" location="package.json:49">
P2: These new dependencies are added to the workspace root package.json instead of the workspaces that actually use them. `trpc-to-openapi` is imported only by `apps/api/src/create-app.ts` and `apps/api/src/trpc/openapi.ts`, yet it is not declared in `apps/api/package.json` dependencies, so the API package now relies on a hoisted root dependency without declaring it. `@better-auth/api-key` is already declared in `packages/auth/package.json` at the identical version `1.6.25`, making the root entry a redundant duplicate. Move `trpc-to-openapi` into `apps/api/package.json` dependencies and drop the duplicate `@better-auth/api-key` entry from root so each workspace declares its own dependencies.</violation>
</file>
<file name="apps/app/app/(app)/[slug]/settings/archive-retention.tsx">
<violation number="1" location="apps/app/app/(app)/[slug]/settings/archive-retention.tsx:50">
P2: When the input contains `180.5`, `180days`, or `1e3`, `Number.parseInt` truncates the value and submits a different retention period instead of rejecting it. Parse the complete trimmed string and require an integer before enabling or submitting the mutation.</violation>
</file>
<file name="apps/app/app/(app)/[slug]/settings/api-keys/copy-value.tsx">
<violation number="1" location="apps/app/app/(app)/[slug]/settings/api-keys/copy-value.tsx:8">
P3: This newly added component is an exact copy of the existing `apps/app/app/(app)/[slug]/settings/sso/copy-value.tsx` (identical imports, props, and body). The two settings pages now each maintain a duplicate copy-to-clipboard component. Extract a single shared component (e.g. into a shared settings component location) and import it from both api-keys and sso, so future clipboard/copy changes are fixed in one place.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| }), | ||
| ); | ||
|
|
||
| if (archived) { |
There was a problem hiding this comment.
P0: When switching to Archived while the archived query is still loading, this branch can show Delete forever for the selected active deals retained by the placeholder. Bulk purge accepts those IDs without checking their archived state, so users can permanently delete active deals; clear the selection or disable archived actions until archived rows load.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(app)/[slug]/deals/deals-bulk-actions.tsx, line 121:
<comment>When switching to Archived while the archived query is still loading, this branch can show `Delete forever` for the selected active deals retained by the placeholder. Bulk purge accepts those IDs without checking their archived state, so users can permanently delete active deals; clear the selection or disable archived actions until archived rows load.</comment>
<file context>
@@ -81,20 +84,76 @@ export function DealsBulkActions({
}),
);
+ if (archived) {
+ const archivedPending = restore.isPending || purge.isPending;
+
</file context>
| const before = new Date(Date.now() - retentionDays * DAY_MS); | ||
|
|
||
| const [companies, contacts, deals] = await Promise.all([ | ||
| this.companies.purgeExpired(before), |
There was a problem hiding this comment.
P1: When an expired company has deals, this concurrent sweep can cascade-delete the deals before DealsService.purge runs. That skips deal-specific cleanup and reports those deals as failed; clean dependent deals before deleting the company or make the company purge handle their cleanup.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/archive/archive-retention.controller.ts, line 87:
<comment>When an expired company has deals, this concurrent sweep can cascade-delete the deals before `DealsService.purge` runs. That skips deal-specific cleanup and reports those deals as failed; clean dependent deals before deleting the company or make the company purge handle their cleanup.</comment>
<file context>
@@ -0,0 +1,113 @@
+ const before = new Date(Date.now() - retentionDays * DAY_MS);
+
+ const [companies, contacts, deals] = await Promise.all([
+ this.companies.purgeExpired(before),
+ this.contacts.purgeExpired(before),
+ this.deals.purgeExpired(before),
</file context>
| import { Icon } from "@crm/ui/components/icon"; | ||
| import { toast } from "sonner"; | ||
|
|
||
| export function CopyValue({ value, label }: { value: string; label: string }) { |
There was a problem hiding this comment.
P3: This newly added component is an exact copy of the existing apps/app/app/(app)/[slug]/settings/sso/copy-value.tsx (identical imports, props, and body). The two settings pages now each maintain a duplicate copy-to-clipboard component. Extract a single shared component (e.g. into a shared settings component location) and import it from both api-keys and sso, so future clipboard/copy changes are fixed in one place.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(app)/[slug]/settings/api-keys/copy-value.tsx, line 8:
<comment>This newly added component is an exact copy of the existing `apps/app/app/(app)/[slug]/settings/sso/copy-value.tsx` (identical imports, props, and body). The two settings pages now each maintain a duplicate copy-to-clipboard component. Extract a single shared component (e.g. into a shared settings component location) and import it from both api-keys and sso, so future clipboard/copy changes are fixed in one place.</comment>
<file context>
@@ -0,0 +1,37 @@
+import { Icon } from "@crm/ui/components/icon";
+import { toast } from "sonner";
+
+export function CopyValue({ value, label }: { value: string; label: string }) {
+ const unavailable = () =>
+ toast.error(
</file context>
Parse nuqs's raw query state through a Zod schema once at the boundary instead of casting through unknown; remove now-redundant return type annotations that discarded inferred literal types.
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/app/components/data-table/use-table-query.ts">
<violation number="1" location="apps/app/components/data-table/use-table-query.ts:92">
P2: When a facet has non-empty `facetDefaults`, clearing it now reapplies the default values because this treats `[]` as missing. Preserve a valid empty array and only use `facetDefaults` when the parsed value is absent or invalid.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
| for (const id of facetIds ?? []) { | ||
| filters[id] = values[id] ?? facetDefaults?.[id] ?? "all"; | ||
| const selected = asStringArray(values[id]); | ||
| filters[id] = selected.length > 0 ? selected : (facetDefaults?.[id] ?? []); |
There was a problem hiding this comment.
P2: When a facet has non-empty facetDefaults, clearing it now reapplies the default values because this treats [] as missing. Preserve a valid empty array and only use facetDefaults when the parsed value is absent or invalid.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/data-table/use-table-query.ts, line 92:
<comment>When a facet has non-empty `facetDefaults`, clearing it now reapplies the default values because this treats `[]` as missing. Preserve a valid empty array and only use `facetDefaults` when the parsed value is absent or invalid.</comment>
<file context>
@@ -14,52 +16,80 @@ export type TableQuery<TTab extends string, TFacet extends string = never> = {
- filters[id] =
- (values[id] as string[] | undefined) ?? facetDefaults?.[id] ?? [];
+ const selected = asStringArray(values[id]);
+ filters[id] = selected.length > 0 ? selected : (facetDefaults?.[id] ?? []);
}
for (const [key, selected] of Object.entries(fields)) {
</file context>
| filters[id] = selected.length > 0 ? selected : (facetDefaults?.[id] ?? []); | |
| \t\tfilters[id] = stringArraySchema.safeParse(values[id]).success\n\t\t\t? asStringArray(values[id])\n\t\t\t: (facetDefaults?.[id] ?? []); |
- Removed outdated dependencies from bun.lock and package.json. - Enhanced task management by adding taskKind to focus state and updating related functions. - Improved handling of field backfill tasks, including checks for existing values. - Introduced a new SessionOnlyMiddleware for better session management in API routes. - Updated API key management features, including the addition of a copy value component for easier API key handling. - Refactored various services and components for improved readability and maintainability. This commit enhances the overall functionality and security of the application.
There was a problem hiding this comment.
4 issues found across 50 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/app/components/crm/record-sheet/record-actions.tsx">
<violation number="1" location="apps/app/components/crm/record-sheet/record-actions.tsx:57">
P2: Archiving a company or contact no longer refreshes the activity caches (activities.timeline, timelineCounts, myTasks). The previous cache.removed(record) refreshed activityKeys(), but cache.company(id)/cache.contact(id) do not include them, so the activity timeline can keep showing the archived record's activities until some other refresh. Add an activity invalidation alongside the per-kind refresh to preserve prior behavior.</violation>
</file>
<file name="apps/api/src/companies/companies.service.ts">
<violation number="1" location="apps/api/src/companies/companies.service.ts:508">
P2: When a candidate is restored or deleted after `purgeExpired` selects it, `purge` returns `null`, but `runBulk` counts that resolved call as a success. Count only non-null deletions as succeeded, or make the bulk runner distinguish skipped records from successful purges.</violation>
</file>
<file name="apps/api/src/contacts/contacts.service.ts">
<violation number="1" location="apps/api/src/contacts/contacts.service.ts:466">
P2: When a candidate is restored or removed after the initial retention query, `purge` returns `null`, but `runBulk` counts the resolved callback as succeeded. Make retention bulk accounting distinguish a skipped `null` from a successful deletion.</violation>
</file>
<file name="apps/api/src/deals/deals.service.ts">
<violation number="1" location="apps/api/src/deals/deals.service.ts:469">
P2: When an initially selected deal is restored or deleted before this guarded call, `purge` resolves `null`, but `runBulk` counts it as succeeded. The retention response and log therefore overstate how many deals were deleted; count only non-null purge results or make skipped records explicit failures.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| toast.success( | ||
| `${archived.name || `The ${NOUN[record.kind]}`} was archived.`, | ||
| ); | ||
| void cache[record.kind](record.id); |
There was a problem hiding this comment.
P2: Archiving a company or contact no longer refreshes the activity caches (activities.timeline, timelineCounts, myTasks). The previous cache.removed(record) refreshed activityKeys(), but cache.company(id)/cache.contact(id) do not include them, so the activity timeline can keep showing the archived record's activities until some other refresh. Add an activity invalidation alongside the per-kind refresh to preserve prior behavior.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/crm/record-sheet/record-actions.tsx, line 57:
<comment>Archiving a company or contact no longer refreshes the activity caches (activities.timeline, timelineCounts, myTasks). The previous cache.removed(record) refreshed activityKeys(), but cache.company(id)/cache.contact(id) do not include them, so the activity timeline can keep showing the archived record's activities until some other refresh. Add an activity invalidation alongside the per-kind refresh to preserve prior behavior.</comment>
<file context>
@@ -54,7 +54,7 @@ function useArchiveRecord(record: RecordRef) {
`${archived.name || `The ${NOUN[record.kind]}`} was archived.`,
);
- void cache.removed(record);
+ void cache[record.kind](record.id);
},
onError: (error: { message: string }) => toast.error(error.message),
</file context>
| void cache[record.kind](record.id); | |
| void cache.activity({ settle: "record" }); | |
| void cache[record.kind](record.id); |
|
|
||
| return runBulk( | ||
| expired.map((row) => row.id), | ||
| (id) => this.purge(id, { archivedBefore: before }), |
There was a problem hiding this comment.
P2: When an initially selected deal is restored or deleted before this guarded call, purge resolves null, but runBulk counts it as succeeded. The retention response and log therefore overstate how many deals were deleted; count only non-null purge results or make skipped records explicit failures.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/deals/deals.service.ts, line 469:
<comment>When an initially selected deal is restored or deleted before this guarded call, `purge` resolves `null`, but `runBulk` counts it as succeeded. The retention response and log therefore overstate how many deals were deleted; count only non-null purge results or make skipped records explicit failures.</comment>
<file context>
@@ -441,7 +466,7 @@ export class DealsService {
return runBulk(
expired.map((row) => row.id),
- (id) => this.purge(id),
+ (id) => this.purge(id, { archivedBefore: before }),
);
}
</file context>
|
|
||
| return runBulk( | ||
| expired.map((row) => row.id), | ||
| (id) => this.purge(id, { archivedBefore: before }), |
There was a problem hiding this comment.
P2: When a candidate is restored or deleted after purgeExpired selects it, purge returns null, but runBulk counts that resolved call as a success. Count only non-null deletions as succeeded, or make the bulk runner distinguish skipped records from successful purges.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/companies/companies.service.ts, line 508:
<comment>When a candidate is restored or deleted after `purgeExpired` selects it, `purge` returns `null`, but `runBulk` counts that resolved call as a success. Count only non-null deletions as succeeded, or make the bulk runner distinguish skipped records from successful purges.</comment>
<file context>
@@ -468,7 +505,7 @@ export class CompaniesService {
return runBulk(
expired.map((row) => row.id),
- (id) => this.purge(id),
+ (id) => this.purge(id, { archivedBefore: before }),
);
}
</file context>
|
|
||
| return runBulk( | ||
| expired.map((row) => row.id), | ||
| (id) => this.purge(id, { archivedBefore: before }), |
There was a problem hiding this comment.
P2: When a candidate is restored or removed after the initial retention query, purge returns null, but runBulk counts the resolved callback as succeeded. Make retention bulk accounting distinguish a skipped null from a successful deletion.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/contacts/contacts.service.ts, line 466:
<comment>When a candidate is restored or removed after the initial retention query, `purge` returns `null`, but `runBulk` counts the resolved callback as succeeded. Make retention bulk accounting distinguish a skipped `null` from a successful deletion.</comment>
<file context>
@@ -425,7 +463,7 @@ export class ContactsService {
return runBulk(
expired.map((row) => row.id),
- (id) => this.purge(id),
+ (id) => this.purge(id, { archivedBefore: before }),
);
}
</file context>
…agement - Introduced idempotency key locking for field backfill tasks to prevent race conditions. - Updated the writeField function to handle backfill checks within a transaction. - Added concurrency control for field backfill tasks in the agent dispatch configuration. - Enhanced bulk operation results to include skipped counts for better tracking of processed records. This commit improves the reliability and performance of field backfill operations.
* Implement currency conversion features and enhance deal handling - Introduced a new CurrencyModule to manage currency conversion and rates. - Added ConversionService for handling currency conversions and fetching rates. - Updated DealsService to support base amounts and currency conversion logic. - Enhanced Deal and Dashboard functionalities to include reporting currency and unconverted deals. - Implemented new currency-related contracts and routes for setting reporting currency and manual rates. - Added integration tests to ensure correct handling of currency conversions and deal totals. * Refactor currency rates service to use open.er-api.com - Updated the currency rates service to fetch exchange rates from open.er-api.com, replacing the previous provider frankfurter.dev. - Enhanced error handling to check for unsupported base currencies in the response. - Implemented retry logic for fetching rates with a maximum of two attempts and a reduced timeout. - Cleaned up stale exchange rates for unsupported currencies during the refresh process. - Updated documentation to reflect the new exchange rate provider and its implications. * Enhance currency handling and conversion logic - Introduced baseCurrency to the Deal model to track the currency of baseAmount. - Updated ConversionService to streamline currency conversion processes and improve deal field handling. - Enhanced CurrencyService to enforce permissions for managing currency settings based on user roles. - Refactored DealsService to incorporate base currency logic in deal aggregations and reporting. - Improved DashboardService to accurately reflect open deal values based on the current reporting currency. - Updated integration tests to validate new currency handling features and ensure correct behavior across services. * Enhance currency conversion logic and improve deal handling - Updated `pendingWhere` method in `ConversionService` to explicitly match null `baseCurrency`, ensuring no deals are excluded from totals. - Added integration test to verify that deals with missing currency are correctly handled and updated. - Modified seeding logic to ensure `baseCurrency` is set alongside `baseAmount` for newly created deals, preventing issues with unconverted figures. - Updated documentation to clarify changes in currency handling and the implications for deal visibility. * Refine currency conversion logic and enhance deal handling - Updated `ConversionService` to conditionally clear rates only when `onlyMissing` is false, improving efficiency in handling missing currencies. - Enhanced integration tests to verify correct behavior when dealing with unconverted figures and missing currency rates. - Introduced a new utility function in the deal sheet component to manage currency options, ensuring proper display of unsupported currencies. * Revise agent and API documentation for clarity and structure - Updated AGENTS.md to emphasize the importance of reviewing relevant documentation before starting work, including a new index table for quick reference. - Refined API rules in api.md to clarify logging practices and the separation of intelligence from the API. - Consolidated environment setup instructions into a new setup.md file for better organization and ease of access. - Enhanced currency handling in DashboardService and related tests to ensure accurate reporting and conversion logic. - Improved integration tests to validate new currency handling features and ensure correct behavior across services. * Enhance documentation and introduce new currency handling guidelines - Updated AGENTS.md to include new references for the Agent panel and local setup instructions. - Added a new docs/agent-panel.md file detailing the Agent panel's functionality and usage. - Introduced docs/currency.md to clarify currency handling rules and reporting practices. - Revised environment setup instructions in docs/environment.md for better clarity and organization. * Add anonymous usage telemetry documentation and enhance currency handling in DealSheet * Implement anonymous usage telemetry and enhance related documentation - Added telemetry functionality to track anonymous usage data, including installation metrics and tool usage. - Introduced new environment variables for telemetry configuration in `.env.example`. - Updated `AGENTS.md` to reference the new telemetry documentation. - Created a `TelemetryModule` with services and controllers for managing telemetry data. - Added a settings page for telemetry configuration in the application. - Enhanced error handling and logging for telemetry events across various services. - Removed outdated ADR on telemetry usage from the repository. * Remove telemetry-related components and references from the application - Deleted the TelemetryRouter and its associated service, removing the telemetry status query. - Updated the settings sidebar to eliminate the Telemetry option. - Removed the TelemetrySettingsPage and its related components, including the TelemetryStatus display. - Cleaned up unused imports and references to telemetry throughout the codebase. * Enhance telemetry functionality and improve budget management - Added an 'exhausted' state to the focus management to track when the research budget is depleted. - Updated the spend function to prevent multiple budget exhaustion events from being recorded. - Refactored the rollup service to handle telemetry rollup claims and restore counters more effectively. - Improved error handling in telemetry events to ensure proper reporting and recovery from failures. - Enhanced documentation to clarify the behavior of telemetry when disabled and the implications for data integrity. * Add telemetry support and enhance landing page analytics - Introduced `@crm/telemetry` package to manage telemetry configurations and constants. - Integrated `posthog-js` for analytics on the landing page, ensuring it only runs on allowed domains. - Updated the `LandingAnalytics` component to initialize analytics tracking based on hostname. - Enhanced the `audit` hook to exclude specific event types from archiving. - Improved agent session handling by implementing offline thread management. - Added utility functions for analytics host validation and created tests for the new functionality. - Updated documentation to reflect changes in telemetry usage and landing page analytics. * Update agent panel to use SETTLED_TTL_MS for archive stale time and enhance documentation - Changed the `staleTime` for the archive query in the agent panel from `Infinity` to `SETTLED_TTL_MS` to ensure proper session management. - Updated documentation to clarify the behavior of the archive in relation to session state and stale time handling. * Enhance landing page analytics with CTA event tracking - Introduced `captureLanding` function to track user interactions with the setup prompt and GitHub star buttons. - Updated `SetupPromptButton` and `GitHubStarButton` components to accept a `location` prop for distinguishing between 'hero' and 'closing' CTAs. - Modified `LandingAnalytics` to include new event types for clipboard actions and button clicks. - Enhanced documentation to reflect the new telemetry events and their usage. * Update README with new images and remove outdated ones - Replaced outdated images with new visuals for the landing page, showcasing agents and capabilities. - Removed references to deleted images related to deals, contacts, and companies to streamline documentation. * Refactor README to improve layout of screenshots - Converted individual screenshot sections into a table format for better visual organization. - Updated captions for clarity and conciseness, enhancing the overall presentation of the landing page visuals. * Update README and images for landing page - Removed outdated captions from the README for agents and capabilities images to streamline content. - Updated binary images for agents, capabilities, and hero sections to enhance visual quality on the landing page. * Update README and replace landing hero image - Updated the README to reflect the new image caption for the companies list. - Replaced the outdated landing hero image with a new product shot to enhance visual appeal. - Removed the old landing hero image from the repository. * Update landing page images for agents and capabilities - Replaced existing binary images for agents and capabilities on the landing page to improve visual quality and consistency. - Ensured that the new images align with the recent updates to the README and overall landing page design. * Update landing page images for agents and capabilities to enhance visual quality * Refactor AddButton component in multiple sheets to use ComponentProps for better type safety - Updated the AddButton function in create-company-sheet, create-contact-sheet, create-deal-sheet, and add-sso-provider-sheet to accept props of type ComponentProps from the Button component. - This change enhances type safety and allows for more flexible button properties across different sheets. * Refactor TelemetryService to integrate RollupService for telemetry rollups - Replaced FunnelService with RollupService in TelemetryService to handle telemetry rollups. - Implemented a timer to run rollups hourly, enhancing telemetry data collection. - Updated documentation to reflect changes in telemetry rollup processes and clarify the in-process execution without cron dependencies. * Report installs without a cron, and stop double counting them The install count was reading 1 while 20 databases had migrated. Every "Active installs" tile is built on install_daily, which only ever fired from POST /internal/telemetry/rollup — a route that refuses to run without CRON_SECRET. An install that never configures a cron reported nothing at all, however much it was used. TelemetryService now rolls up in-process, on boot and hourly. The existing row lock on install makes all but the first of those a no-op, and it short-circuits before the aggregation runs, so it is still one set of grouped queries per install per day. The route stays, still behind CRON_SECRET, for a platform cron that would rather drive it; nothing depends on it now. Two ways the same event could arrive twice, both of which the hourly timer would have made more frequent: A rollup wrongly read as failed hands the day back and is re-sent. posthog-node does not reject on a failed send, so the client inferred failure from a module-global error counter that any other capture could move. It now enqueues and awaits flush(), which does throw, and treats either signal as a failure — erring toward a re-send, which is free, over consuming a day whose event never left. A milestone sent before it was recorded, so both the boot sweep and the rollup sweep could send the same step. One install sent first_fact_applied six times. The insert is now the claim: of two sweeps exactly one is told it landed the row, and only that one sends. A failed send deletes the row so the step is retried. Both events also carry a deterministic uuid derived from the install and the day (or the step), so a duplicate that does get out is ingested once. Installs and active installs were always safe — PostHog's unique math is per install per day — but the summed agent-usage properties were not. * Derive the dedupe id with SHA-256 in a v8 uuid CodeQL flags a weak algorithm reached by the install identity, and it is right that the two do not belong in one expression. SHA-1 was there only because RFC 4122 defines v5 that way; nothing depends on being a v5, so this is a SHA-256 digest in a v8 uuid, the slot RFC 9562 leaves for a derivation of one's own. * CMP-1 chore: enrich agentic experience * ci: open pull requests, gate titles and promote releases automatically (trycompai#53) * Lewis/contact and currencies (trycompai#56) * Implement fields management features (trycompai#55) * Lewis/dynamic field fix (trycompai#70) * Refactor query prefetching in Companies, Contacts, and Deals pages to… (trycompai#71) * chore: release main (trycompai#72) * feat(api): add microsoft sign-in and outlook mailbox sync (trycompai#73) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * chore: release release * ci: run release-please on main and document merge order (trycompai#76) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * chore: release main (trycompai#78) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * feat(db): CMP-1 persist durable custom agents (trycompai#67) * feat(agent): CMP-1 add sandboxed builder and runner runtimes (trycompai#60) * refactor(app): CMP-59 harden CRM UI foundations (trycompai#61) * feat(app): CMP-46 add the private agent builder workspace (trycompai#62) * feat(app): CMP-12 review agent drafts before deployment (trycompai#63) * fix(app): CMP-47 consolidate agent builder presentation (trycompai#64) * feat(app): CMP-47 add inline composer context * fix(app): move chat beneath overview in icon rail (trycompai#83) Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com> * fix(ci): tag releases automatically and keep previews off the production schema (trycompai#82) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * chore(main): release 1.4.0 (trycompai#86) * feat(agent): bound agent builder retries and improve chat scrolling (trycompai#89) * fix(app): render agent transcript chronologically with anchored tool results (trycompai#92) Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * fix(agent): declare granted write actions in draft access summary (trycompai#93) Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com> * chore(main): release 1.5.0 (trycompai#91) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(api): warn when the deployed schema does not match schema.prisma (trycompai#88) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * CMP-62 chore: add gh-stack skill (trycompai#96) * chore(main): release 1.5.1 (trycompai#97) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(ci): make the release guard reject only genuinely untagged pull requests (trycompai#105) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * fix(ci): stop the auto-titler downgrading a release The titler regenerated the title on every push once it had written one, so a pull request's subject was whatever its *last* commits looked like. trycompai#105 carried the whole website-tracking feature and was retitled `fix(ci)` by its final push, squashed onto main under that subject, and released as a patch whose notes mention none of it. A generated title is now left alone unless it stops being a conventional commit or stops covering the branch, and no title — generated or typed — may release less than the commits behind it: `floor_of` takes the strongest bump on the branch and `generate` raises its proposal to meet it. A branch holding a `feat` cannot ship as a `fix`, and one holding a breaking change cannot ship without the `!`. Over-releasing is the safe direction; losing a feature out of the changelog is not. * feat(tracking): add website tracking with form capture and attribution A first-party script on the marketing site, a collector in the API, and one rule: a form submission becomes a contact. Page views, click labels and first/last-touch attribution hang off that, with a 90-day retention sweep, an hourly contact cap and a per-minute event budget. The work landed in 815a832. The auto-titler had retitled its pull request `fix(ci)` on the last push, so it squashed onto main under that subject and released as a patch whose notes describe only the guard fix. This commit carries no code — it exists so the changelog and the version say what actually shipped. See docs/tracking.md. * chore(main): release 1.6.0 (trycompai#106) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(ci): make a release one pull request instead of two Shipping was a release pull request into `main` and a promotion pull request into `release`, open at the same time, with a warning on the promotion telling you to merge the other one first. Merge them the wrong way round and you shipped untagged code and left the version behind for the next promotion. Nobody should have to hold that rule in their head to deploy. The tag and the code have to travel together, so the release workflow now does it in one step: when release-please cuts the tag it merges that exact commit into `release` through the merges API. One pull request, no order to remember, and the tag is by construction an ancestor of what shipped. `promote.yml` is gone. A conflict is the one case a human still has to see, and it can only mean somebody committed to `release` directly, so it fails the run and says so rather than quietly leaving production behind. Non-releasable commits now wait for the next release rather than riding a promotion, which is the trade: `release` moves when a tag is cut and at no other time. * fix(ci): fall back to the pushed commit when release-please reports no sha * chore(main): release 1.6.1 (trycompai#108) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * feat(db): add peek script for inspecting database contents (trycompai#110) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * chore(main): release 1.7.0 (trycompai#111) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * feat(agent): apply sourced facts to empty fields automatically (trycompai#112) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * chore(main): release 1.8.0 (trycompai#113) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(ci): ship releases by opening a pull request into release (trycompai#114) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * chore(main): release 1.8.1 (trycompai#115) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(agent): fill blank fields on the dispatch tick instead of sign-in (trycompai#117) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * chore(main): release 1.8.2 (trycompai#118) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * feat(agent): stop suggesting a URL that already matches the field (trycompai#120) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * chore(main): release 1.9.0 (trycompai#121) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * feat(tracking): support installing the tracking tag via Google Tag Manager (trycompai#124) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * chore(main): release 1.10.0 (trycompai#126) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * feat(app): copy the tracking snippet for the selected install method (trycompai#128) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * chore(main): release 1.11.0 (trycompai#129) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * feat: edit a deployed agent, and show what Slack actually granted (CMP-77) (trycompai#109) * chore(main): release 1.12.0 (trycompai#132) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * feat(app): search company dropdowns instead of scrolling them (trycompai#125) * fix(app): show select field values in record tables (trycompai#133) * fix(agent): let the assistant chat read the deal list it is told to use (CMP-77) (trycompai#139) * chore(main): release 1.13.0 (trycompai#136) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * chore: add anti-slop lint rules, dead-code checks and stricter Biome constraints (CMP-80) (trycompai#145) * refactor: clear anti-slop type assertions and conditional object spreads (CMP-81) (trycompai#146) * docs: propose an i18n layer (trycompai#143) * refactor: parse every remaining I/O boundary into a domain type (CMP-82) (trycompai#151) * fix: unblock the test suite and actually install the git hooks (CMP-83) (trycompai#152) * ci: run anti-slop lint in CI and pre-push (CMP-84) (trycompai#153) Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com> * feat: enrichment queue widget (CMP-92) (trycompai#159) * feat(agent): read people from Context.dev instead of RapidAPI (CMP-86) (trycompai#158) Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com> * feat: page the enrichment queue (CMP-92) (trycompai#160) Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com> * chore(main): release 1.14.0 (trycompai#147) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * feat(agent): scope field backfill tasks to records missing values (trycompai#163) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * chore(main): release 1.15.0 (trycompai#164) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(api): serve openapi.json and bundle swagger deps in function build (trycompai#166) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * chore(main): release 1.15.1 (trycompai#167) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Lewis/openapi json (trycompai#169) * docs(api): explain runtime openapi document and vendoring rules (trycompai#170) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * chore(main): release 1.15.2 (trycompai#171) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(app): prevent url param collision between fields sheet and table filter (trycompai#175) Co-authored-by: Lewis Carhart <lewis@trycomp.ai> * fix: stop a finished enrichment reading as failed (trycompai#173) * chore(main): release 1.15.3 (trycompai#176) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * ci: add Vercel deploy workflow Deploys to Vercel on push to main/release branches. Also supports manual trigger via workflow_dispatch with environment selection (preview/production). Uses secrets: VERCEL_API_KEY, VERCEL_ORG_ID, VERCEL_PROJECT_ID --------- Co-authored-by: Lewis Carhart <lewis@trycomp.ai> Co-authored-by: grim <75869731+ripgrim@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: Guzman Pintos <37162183+GuzmanPintos@users.noreply.github.com> Co-authored-by: twinprime19 <38123958+twinprime19@users.noreply.github.com> Co-authored-by: Kiro Agent <244629292+kiro-agent@users.noreply.github.com>
Opened automatically when
lewis/crm-tweaks-hardeningwas pushed.The title is written from the diff and rewritten as you push, because this is squashed onto
mainand the title becomes the commit subject and the changelog line. Retitle it yourself and it is yours — the automation stops touching it.Summary by cubic
Scopes agent field‑backfill to only records missing requested custom fields and makes writes idempotent to avoid overwriting and races. Also adds a REST bridge with OpenAPI via
@nestjs/swagger, API key management, saved views, and archive/restore + purge flows; list/search filters now use arrays and table queries validate at the boundary.Field backfill and data
fieldKeystofield-backfilltask attributes and preamble; instructs tolist_fieldsthen onlyset_field_valuewith evidence.taskKind;writeFieldruns in a transaction, locks an idempotency key, and skips backfill writes if a value already exists.FIELDS_CONFIG; retires never‑targeted tasks; limitsfieldBackfillconcurrency to 8.seniorityandfunctionas columns; bulk results now include askippedcount.Migration
fields: Record<string, string[]>, andarchived: boolean.fieldKeysin agent context and that existing values will be left unchanged, which may return as skipped in bulk results.CRON_SECRETand schedule archive pruning at/internal/archive/prune; prefer/restendpoints viatrpc-to-openapiand use the generated docs with@nestjs/swagger. Session‑only routes rejectAPI-Keyauth.Written for commit fc0f180. Summary will update on new commits.