Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code b8649c067a fix(setup-dev-env): add database schema initialization after creating databases
https://sonarly.com/issue/31343?type=bug

The `setup-dev-env.sh` script creates empty PostgreSQL databases but never runs schema migrations (`database:init`), causing the backend to crash with "relation does not exist" errors when started.

Fix: Added database schema initialization as step 4 in `setup-dev-env.sh`, after database creation and `.env` setup.

**Changes to `packages/twenty-utils/setup-dev-env.sh`:**

1. **Updated header comment** (line 11): Added step 4 to the "What it does" documentation.

2. **Added `schema_exists()` helper** (lines 248-257): Queries `information_schema.tables` for the `core` schema in the `default` database. Uses the same `psql`/Docker fallback pattern as the existing `run_psql()` function. This enables idempotency — if the schema already exists (e.g., on re-runs), the heavy `database:reset` step is skipped.

3. **Added schema initialization step** (lines 259-271): When `npx` and `node_modules` are available, runs `npx nx database:reset twenty-server` which:
   - Builds `twenty-server` (Nx dependency)
   - Creates the `core` and `public` schemas with required PostgreSQL extensions
   - Runs all TypeORM migrations to create tables (`appToken`, `keyValuePair`, etc.)
   - Seeds the database with initial development data

   When `node_modules` is not available (rare — e.g., running the script before `yarn install`), a clear message tells the user what command to run manually.

**Changes to `CLAUDE.md`** (line 210): Updated the description to include "initializes the database schema (migrations + seed)" so developers know the script now handles the full setup.

The fix is consistent with how the Cursor environment (`cursor/environment.json`) already chains `database:reset` after the setup script, and matches the CI workflow (`ci-server.yaml`) which runs `database:init:prod` after creating databases.
2026-04-26 15:36:03 +00:00
Charles BochetandGitHub 499067ae14 fix(logic-function): serialize LocalDriver layer builds with cache lock (#20054)
## Summary

Fixes a race condition in `LocalDriver` where concurrent `execute()`
calls for the same application can trash each other's layer build,
causing logic function executions to fail with either:

- `Error: ENOENT: process.cwd failed with error no such file or
directory, the current working directory was likely removed without
changing the working directory, uv_cwd` (the yarn-install child's `cwd`
got wiped mid-run), or
- `ENOENT: no such file or directory, open '…/<pkg>/<file>.js.map'`
raised from yarn's berry link step (files under the deps layer vanish
while yarn is extracting).

Both are thrown from `…/deps/<checksum>/.yarn/releases/yarn-4.9.2.cjs` —
i.e. the yarn process `copyYarnEngineAndBuildDependencies` spawns with
`cwd: buildDirectory`.

## Root cause

`LocalDriver.createLayerIfNotExist` (and `ensureSdkLayer`) both follow a
check-then-act pattern with no mutual exclusion:

```ts
if (await pathExists(depsNodeModulesPath)) return;
await fs.rm(depsLayerPath, { recursive: true, force: true });
await copyDependenciesInMemory(...);
await copyYarnEngineAndBuildDependencies(depsLayerPath); // spawns yarn with cwd=depsLayerPath
```

The deps layer path is shared across all `execute()` calls that match a
given `yarnLockChecksum`. Two concurrent callers (e.g. a webhook
invocation + a cron-triggered logic function firing while the first
run's layer is still being built) both see no `node_modules`, both
`fs.rm` the directory, and one's yarn child ends up with its `cwd` or
its extraction target gone.

## Fix

Wrap the critical sections with `CacheLockService.withLock` — the same
cache-backed lock the Lambda driver already uses for its layer/executor
builds:

- `createLayerIfNotExist` → lock key
`local-driver-deps-layer:${yarnLockChecksum ?? 'default'}`
- `ensureSdkLayer` → lock key
`local-driver-sdk-layer:${workspaceId}:${applicationUniversalIdentifier}`

A fast-path `pathExists` check is kept outside the lock (so warm
executions still skip lock acquisition entirely), and the existence +
staleness check is **repeated inside the lock** so followers no-op after
the leader finishes.

Lock parameters mirror the Lambda driver's layer-build lock (`ttl=120s`,
`retry=500ms`, `maxRetries=240`).

`cacheLockService` is now passed to `LocalDriver` from
`LogicFunctionDriverFactory` (it was already injected there for the
Lambda driver).
2026-04-26 12:44:40 +00:00
nitinandGitHub ca84d28157 [AI] ai usage line chart date gap filling (#20048)
https://github.com/user-attachments/assets/ecd37dfb-daae-41c3-8316-a9d923463eca
2026-04-26 12:41:16 +00:00
nitinandGitHub d1a4902460 [AI] Drop 'serialization' from tool output naming (#20052)
didnt made sense anymore -- we dont really 'serialize'
2026-04-26 14:10:01 +02:00
Paul RastoinandGitHub 89ad87aa64 Make twenty-front build env agnostic (#20055)
## Introduction
In aim to reduce and optimize the number of twenty-front build we do
during our cd process and allow twenty-front build promotion

### Build time

**Nothing is baked.** The `build/` directory is a clean, env-agnostic
artifact. `index.html` contains the empty placeholder:

```html
<script id="twenty-env-config">
  window._env_ = {
    // This will be overwritten
  };
</script>
```

The JS bundles contain no hardcoded server URL.

---

### Deploy mode 1: Frontend served by the backend (Docker / NestJS)

1. Container starts, NestJS boots in `main.ts`
2. `generateFrontConfig()` runs, reads `process.env.SERVER_URL`
3. Rewrites `dist/front/index.html`, replacing the placeholder with:
   ```html
   <script id="twenty-env-config">
     window._env_ = {
       REACT_APP_SERVER_BASE_URL: "https://api.example.com"
     };
   </script>
   ```
4. NestJS serves the static `dist/front/` directory
5. Browser loads `index.html`, `window._env_` is set before the app JS
executes
6. `src/config/index.ts` reads `window._env_.REACT_APP_SERVER_BASE_URL`
and uses it

---

### Deploy mode 2: Frontend served standalone (CDN / nginx / static
server)

1. Take the `build/` artifact as-is
2. Before serving, run at deploy time:
   ```bash
REACT_APP_SERVER_BASE_URL=https://api.example.com sh
./scripts/inject-runtime-env.sh
   ```
3. This does the same `sed` replacement on `build/index.html`
4. Serve the `build/` directory with your static server of choice
5. Same resolution in the browser:
`window._env_.REACT_APP_SERVER_BASE_URL` is picked up by
`src/config/index.ts`

---

### Fallback: no injection at all

If neither mechanism runs (e.g. local dev with `vite dev`),
`window._env_.REACT_APP_SERVER_BASE_URL` is `undefined`, and
`getDefaultUrl()` kicks in:
- **Localhost**: returns `http://localhost:3000`
- **Non-localhost**: returns same-origin (`window.location.origin`)
2026-04-26 07:05:54 +00:00
Charles BochetandGitHub 0c5c9c844e feat(github-connector): exclude self-reviews from review counts (#20050)
## Summary

- Adds an `isSelfReview` boolean to the consolidated `PullRequestReview`
record (`true` when the reviewer is the same contributor as the PR
author).
- Filters `isSelfReview === true` rows out of the top-reviewers
leaderboard (`top-contributors`) and per-contributor review stats
(`contributor-stats`) so contributors are credited only for reviews on
**other people's** PRs.
- Both the live ingestion path (`fetch-prs`) and the recompute job
(`recompute-pull-request-reviews`) now thread `prAuthorId` to
`buildConsolidatedRow`, so re-running either is sufficient to backfill
existing rows — no extra migration needed.

## Test plan

- [x] `yarn test
src/modules/github/pull-request-review/utils/consolidate-reviews.integration-test.ts`
— 20 tests passing, including new coverage for the `isSelfReview` helper
and `buildConsolidatedRow` payload.
- [ ] After merge: re-run `fetch-prs` (or
`recompute-pull-request-reviews`) once on a target workspace to backfill
`isSelfReview` on existing `PullRequestReview` rows, then verify the
top-reviewers leaderboard no longer credits self-reviews.


Made with [Cursor](https://cursor.com)
2026-04-25 23:34:01 +02:00
Charles BochetandGitHub 41571ea377 feat(front-component-renderer): forward offset/movement coordinates on serialised events (#20046)
## Summary

Adds `offsetX`, `offsetY`, `movementX`, `movementY` to
`SerializedEventData` and the host event serialiser so apps can reason
about element-relative pointer positions without trying to read the host
element's bounding rect (which is impossible from a remote-DOM worker).

## Motivation

I was building a front-component with click-to-drop-pin and trackpad
pan/zoom (custom OSM tile renderer). Two real bugs surfaced from the
current event-serialisation surface:

1. **Wheel pan/zoom was broken.** The host already forwards
`deltaX`/`deltaY`, but app authors naturally read them off the
React-style event handler argument as `e.deltaX`/`e.deltaY`. Because
remote-DOM bridges everything via `RemoteEvent extends
CustomEvent<Detail>`, the payload actually arrives at `e.detail.deltaX`.
Reading the wrong place gives `undefined`, and `undefined < 0 ===
false`, so every wheel notch zoomed in the same direction. App code now
uses `e.detail`, but this was a sharp papercut worth flagging in docs /
a helper (separate change).

2. **Element-local click coords are unobtainable from a worker.** With
only `clientX/Y`, an app needs the stage's bounding rect to translate
viewport coordinates to local — which can't be read across the worker
boundary. `offsetX`/`offsetY` close that gap with a one-read solution.
`movementX`/`movementY` round out the set for any future drag-style
interactions if `mousemove` later joins the allow-list.
2026-04-25 10:12:28 +00:00
570038ad65 chore: sync AI model catalog from models.dev (#20045)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-25 08:26:54 +02:00
38 changed files with 493 additions and 71 deletions
+1 -1
View File
@@ -207,7 +207,7 @@ All dev environments (Claude Code web, Cursor, local) use one script:
bash packages/twenty-utils/setup-dev-env.sh
```
This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, and copies `.env` files. Idempotent — safe to run multiple times.
This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, copies `.env` files, and initializes the database schema (migrations + seed). Idempotent — safe to run multiple times.
- `--docker` — force Docker mode (uses `packages/twenty-docker/docker-compose.dev.yml`)
- `--down` — stop services
@@ -1,6 +1,6 @@
{
"name": "github-connector",
"version": "0.1.0",
"version": "0.2.0",
"license": "MIT",
"engines": {
"node": "^24.5.0",
@@ -77,6 +77,7 @@ describe('PullRequestReview object', () => {
expect(names).toContain('firstSubmittedAt');
expect(names).toContain('lastSubmittedAt');
expect(names).toContain('eventCount');
expect(names).toContain('isSelfReview');
expect(names).toContain('reviewer');
expect(names).toContain('pullRequest');
expect(names).toContain('reviewEvents');
@@ -298,7 +298,12 @@ const handler = async (
const res = await client.query({
pullRequestReviews: {
__args: {
filter: { reviewerId: { eq: contributorId } },
filter: {
and: [
{ reviewerId: { eq: contributorId } },
{ isSelfReview: { eq: false } },
],
},
orderBy: [{ firstSubmittedAt: 'DescNullsLast' }],
first: PAGE_SIZE,
after: cursor,
@@ -198,6 +198,7 @@ const handler = async (
const res = await client.query({
pullRequestReviews: {
__args: {
filter: { isSelfReview: { eq: false } },
orderBy: [{ firstSubmittedAt: 'DescNullsLast' }],
first: PAGE_SIZE,
after: cursor,
@@ -15,5 +15,6 @@ export async function batchUpsertConsolidatedReviews(
eventCount: true,
reviewerId: true,
pullRequestId: true,
isSelfReview: true,
}) as Promise<PullRequestReviewRow[]>;
}
@@ -23,7 +23,10 @@ type ReviewEventNode = {
reviewerId: string | null;
pullRequestId: string | null;
reviewer: { ghLogin: string | null } | null;
pullRequest: { githubNumber: number | null } | null;
pullRequest: {
githubNumber: number | null;
authorId: string | null;
} | null;
};
type GroupContext = {
@@ -31,6 +34,7 @@ type GroupContext = {
reviewerId: string | null;
prNumber: number | null;
reviewerLogin: string | null;
prAuthorId: string | null;
events: { state: ReviewEventState; submittedAt: string | null }[];
};
@@ -68,7 +72,7 @@ const handler = async (_event: RoutePayload<unknown>) => {
reviewerId: true,
pullRequestId: true,
reviewer: { ghLogin: true },
pullRequest: { githubNumber: true },
pullRequest: { githubNumber: true, authorId: true },
},
},
pageInfo: { hasNextPage: true, endCursor: true },
@@ -95,6 +99,7 @@ const handler = async (_event: RoutePayload<unknown>) => {
reviewerId: node.reviewerId,
prNumber: node.pullRequest?.githubNumber ?? null,
reviewerLogin: node.reviewer?.ghLogin ?? null,
prAuthorId: node.pullRequest?.authorId ?? null,
events: [],
};
groups.set(key, group);
@@ -105,6 +110,9 @@ const handler = async (_event: RoutePayload<unknown>) => {
if (group.prNumber === null && node.pullRequest?.githubNumber != null) {
group.prNumber = node.pullRequest.githubNumber;
}
if (group.prAuthorId === null && node.pullRequest?.authorId) {
group.prAuthorId = node.pullRequest.authorId;
}
group.events.push({
state: node.state,
submittedAt: node.submittedAt,
@@ -123,6 +131,7 @@ const handler = async (_event: RoutePayload<unknown>) => {
prNumber: group.prNumber,
reviewerLogin: group.reviewerLogin,
events: group.events,
prAuthorId: group.prAuthorId,
}),
);
}
@@ -24,6 +24,9 @@ export const REVIEW_EVENT_COUNT_FIELD_UNIVERSAL_IDENTIFIER =
export const PULL_REQUEST_REVIEW_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
'b4d61187-1b78-5d6e-9752-79f994ff6d55';
export const REVIEW_IS_SELF_REVIEW_FIELD_UNIVERSAL_IDENTIFIER =
'c1f3e8a2-9b5d-4e7f-8a6c-2d4b6f8e1a93';
enum ReviewState {
APPROVED = 'APPROVED',
CHANGES_REQUESTED = 'CHANGES_REQUESTED',
@@ -116,5 +119,15 @@ export default defineObject({
icon: 'IconHash',
defaultValue: 0,
},
{
universalIdentifier: REVIEW_IS_SELF_REVIEW_FIELD_UNIVERSAL_IDENTIFIER,
name: 'isSelfReview',
type: FieldType.BOOLEAN,
label: 'Self review',
description:
'True when the reviewer is the same contributor as the PR author. Self reviews are excluded from review-count aggregations (top reviewers, contributor stats, etc.) so contributors are credited only for reviews on other people\u2019s PRs.',
icon: 'IconUserCheck',
defaultValue: false,
},
],
});
@@ -8,4 +8,5 @@ export type PullRequestReviewRow = {
eventCount?: number | null;
reviewerId?: string | null;
pullRequestId?: string | null;
isSelfReview?: boolean | null;
};
@@ -12,6 +12,7 @@ export type ConsolidatedReviewUpsertInput = {
eventCount: number;
reviewerId: string | null;
pullRequestId: string;
isSelfReview: boolean;
};
export type BuildConsolidatedRowParams = {
@@ -20,8 +21,23 @@ export type BuildConsolidatedRowParams = {
prNumber: number | null;
reviewerLogin: string | null;
events: ReviewEventForConsolidation[];
/**
* Author of the PR being reviewed. When non-null and equal to `reviewerId`
* the consolidated row is flagged as a self-review so downstream
* aggregations (top reviewers, contributor stats, etc.) can exclude it.
*/
prAuthorId?: string | null;
};
export const isSelfReview = (
reviewerId: string | null,
prAuthorId: string | null | undefined,
): boolean =>
reviewerId !== null &&
prAuthorId !== null &&
prAuthorId !== undefined &&
reviewerId === prAuthorId;
export const buildReviewKey = (
pullRequestId: string,
reviewerId: string | null,
@@ -49,5 +65,6 @@ export const buildConsolidatedRow = (
eventCount: verdict.eventCount,
reviewerId: params.reviewerId,
pullRequestId: params.pullRequestId,
isSelfReview: isSelfReview(params.reviewerId, params.prAuthorId ?? null),
};
};
@@ -8,6 +8,7 @@ import {
buildConsolidatedRow,
buildReviewKey,
buildConsolidatedTitle,
isSelfReview,
} from 'src/modules/github/pull-request-review/utils/build-consolidated-row';
const evt = (
@@ -140,6 +141,71 @@ describe('buildConsolidatedRow', () => {
eventCount: 3,
reviewerId: 'rev-2',
pullRequestId: 'pr-1',
isSelfReview: false,
});
});
it('flags isSelfReview when prAuthorId equals reviewerId', () => {
const row = buildConsolidatedRow({
pullRequestId: 'pr-1',
reviewerId: 'alice',
prNumber: 7,
reviewerLogin: 'alice',
prAuthorId: 'alice',
events: [evt('COMMENTED', '2025-04-01T10:00:00Z')],
});
expect(row.isSelfReview).toBe(true);
});
it('does not flag isSelfReview when prAuthorId differs from reviewerId', () => {
const row = buildConsolidatedRow({
pullRequestId: 'pr-1',
reviewerId: 'alice',
prNumber: 7,
reviewerLogin: 'alice',
prAuthorId: 'bob',
events: [evt('APPROVED', '2025-04-01T10:00:00Z')],
});
expect(row.isSelfReview).toBe(false);
});
it('does not flag isSelfReview when prAuthorId is missing', () => {
const row = buildConsolidatedRow({
pullRequestId: 'pr-1',
reviewerId: 'alice',
prNumber: 7,
reviewerLogin: 'alice',
events: [evt('APPROVED', '2025-04-01T10:00:00Z')],
});
expect(row.isSelfReview).toBe(false);
});
it('does not flag isSelfReview when reviewerId is null (ghost reviewer)', () => {
const row = buildConsolidatedRow({
pullRequestId: 'pr-1',
reviewerId: null,
prNumber: 7,
reviewerLogin: null,
prAuthorId: null,
events: [evt('COMMENTED', '2025-04-01T10:00:00Z')],
});
expect(row.isSelfReview).toBe(false);
});
});
describe('isSelfReview', () => {
it('returns true only when both ids are present and equal', () => {
expect(isSelfReview('a', 'a')).toBe(true);
});
it('returns false when ids differ', () => {
expect(isSelfReview('a', 'b')).toBe(false);
});
it('returns false when either side is null/undefined', () => {
expect(isSelfReview(null, 'a')).toBe(false);
expect(isSelfReview('a', null)).toBe(false);
expect(isSelfReview('a', undefined)).toBe(false);
expect(isSelfReview(null, null)).toBe(false);
});
});
@@ -110,9 +110,16 @@ const handler = async (event: RoutePayload<FetchPrsPayload>) => {
reviewerId: string | null;
prNumber: number;
reviewerLogin: string | null;
prAuthorId: string | null;
events: { state: ReviewEventState; submittedAt: string | null }[];
};
const authorIdByPullRequestId = new Map<string, string | null>();
for (const pr of prData) {
const id = prIdByNumber.get(pr.githubNumber);
if (id) authorIdByPullRequestId.set(id, pr.authorId);
}
let skippedReviews = 0;
const reviewEventData: ReviewEventInput[] = [];
const groups = new Map<GroupKey, GroupContext>();
@@ -141,6 +148,7 @@ const handler = async (event: RoutePayload<FetchPrsPayload>) => {
reviewerId,
prNumber: pr.number,
reviewerLogin: review.author?.login ?? null,
prAuthorId: authorIdByPullRequestId.get(pullRequestId) ?? null,
events: [],
};
groups.set(key, group);
@@ -177,6 +185,7 @@ const handler = async (event: RoutePayload<FetchPrsPayload>) => {
prNumber: group.prNumber,
reviewerLogin: group.reviewerLogin,
events: group.events,
prAuthorId: group.prAuthorId,
}),
);
const consolidatedRecords = await timed(
-7
View File
@@ -49,8 +49,6 @@ RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk tw
FROM common-deps AS twenty-front-build
ARG REACT_APP_SERVER_BASE_URL
COPY ./packages/twenty-front /app/packages/twenty-front
COPY ./packages/twenty-front-component-renderer /app/packages/twenty-front-component-renderer
COPY ./packages/twenty-ui /app/packages/twenty-ui
@@ -138,9 +136,6 @@ USER 1000
FROM twenty-server AS twenty
ARG REACT_APP_SERVER_BASE_URL
ENV REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL
COPY --chown=1000 --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
LABEL org.opencontainers.image.description="Twenty image with backend and frontend."
@@ -232,7 +227,6 @@ RUN mkdir -p /data/postgres /data/redis /app/packages/twenty-server/.local-stora
&& chown -R postgres:postgres /data/postgres \
&& chown 1000:1000 /data/redis /app/packages/twenty-server/.local-storage
ARG REACT_APP_SERVER_BASE_URL
ARG APP_VERSION=0.0.0
ENV S6_KEEP_ENV=1
@@ -241,7 +235,6 @@ ENV PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default \
REDIS_URL=redis://localhost:6379 \
STORAGE_TYPE=local \
APP_SECRET=twenty-app-dev-secret-not-for-production \
REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL \
APP_VERSION=$APP_VERSION \
NODE_ENV=development \
NODE_PORT=2020 \
@@ -10,6 +10,10 @@ export type SerializedEventData = {
pageY?: number;
screenX?: number;
screenY?: number;
offsetX?: number;
offsetY?: number;
movementX?: number;
movementY?: number;
button?: number;
buttons?: number;
key?: string;
@@ -81,6 +81,12 @@ const serializeEvent = (event: unknown): SerializedEventData => {
if ('pageY' in domEvent) serialized.pageY = domEvent.pageY as number;
if ('screenX' in domEvent) serialized.screenX = domEvent.screenX as number;
if ('screenY' in domEvent) serialized.screenY = domEvent.screenY as number;
if ('offsetX' in domEvent) serialized.offsetX = domEvent.offsetX as number;
if ('offsetY' in domEvent) serialized.offsetY = domEvent.offsetY as number;
if ('movementX' in domEvent)
serialized.movementX = domEvent.movementX as number;
if ('movementY' in domEvent)
serialized.movementY = domEvent.movementY as number;
if ('button' in domEvent) serialized.button = domEvent.button as number;
if ('buttons' in domEvent) serialized.buttons = domEvent.buttons as number;
+2 -2
View File
@@ -3,8 +3,8 @@
"private": true,
"type": "module",
"scripts": {
"build": "NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 npx vite build && sh ./scripts/inject-runtime-env.sh",
"build:sourcemaps": "NODE_ENV=production VITE_BUILD_SOURCEMAP=true NODE_OPTIONS=--max-old-space-size=8192 npx vite build && sh ./scripts/inject-runtime-env.sh",
"build": "NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 npx vite build",
"build:sourcemaps": "NODE_ENV=production VITE_BUILD_SOURCEMAP=true NODE_OPTIONS=--max-old-space-size=8192 npx vite build",
"start:prod": "NODE_ENV=production npx serve -s build",
"tsup": "npx tsup"
},
@@ -1,5 +1,10 @@
#!/bin/sh
if [ -z "$REACT_APP_SERVER_BASE_URL" ]; then
echo "Error: REACT_APP_SERVER_BASE_URL is not set."
exit 1
fi
echo "Injecting runtime environment variables into index.html..."
CONFIG_BLOCK=$(cat << EOF
+1 -3
View File
@@ -18,6 +18,4 @@ const getDefaultUrl = () => {
};
export const REACT_APP_SERVER_BASE_URL =
window._env_?.REACT_APP_SERVER_BASE_URL ||
process.env.REACT_APP_SERVER_BASE_URL ||
getDefaultUrl();
window._env_?.REACT_APP_SERVER_BASE_URL || getDefaultUrl();
@@ -62,7 +62,8 @@ export const SettingsAdminAI = () => {
const billing = useAtomStateValue(billingState);
const isBillingEnabled = billing?.isBillingEnabled ?? false;
const hasEnterpriseAccess =
isBillingEnabled || currentWorkspace?.hasValidEnterpriseKey === true;
isBillingEnabled ||
currentWorkspace?.hasValidEnterpriseValidityToken === true;
const [usagePeriod, setUsagePeriod] = useState<PeriodPreset>('30d');
const periodOptions = getPeriodOptions();
const usageDates = getPeriodDates(usagePeriod);
@@ -7,9 +7,9 @@ import { SettingsEnterpriseFeatureGateCard } from '@/settings/components/Setting
import { UsageBreakdownPieSection } from '@/settings/usage/components/UsageBreakdownPieSection';
import { UsageByUserTableSection } from '@/settings/usage/components/UsageByUserTableSection';
import { UsageDailyChartSection } from '@/settings/usage/components/UsageDailyChartSection';
import { UsageSectionSkeleton } from '@/settings/usage/components/UsageSectionSkeleton';
import { AI_OPERATION_TYPES } from '@/settings/usage/constants/AiOperationTypes';
import { useUsageAnalyticsData } from '@/settings/usage/hooks/useUsageAnalyticsData';
import { UsageSectionSkeleton } from '@/settings/usage/components/UsageSectionSkeleton';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { t } from '@lingui/core/macro';
import { SettingsPath } from 'twenty-shared/types';
@@ -25,7 +25,8 @@ export const SettingsAiUsageTab = () => {
const isClickHouseConfigured = useAtomStateValue(isClickHouseConfiguredState);
const hasEnterpriseAccess =
isBillingEnabled || currentWorkspace?.hasValidEnterpriseKey === true;
isBillingEnabled ||
currentWorkspace?.hasValidEnterpriseValidityToken === true;
const shouldSkipQuery = !hasEnterpriseAccess || !isClickHouseConfigured;
@@ -1,8 +1,10 @@
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
export const mockedApolloClient = new ApolloClient({
link: new HttpLink({
uri: process.env.REACT_APP_SERVER_BASE_URL + '/metadata',
uri: REACT_APP_SERVER_BASE_URL + '/metadata',
}),
cache: new InMemoryCache(),
});
@@ -1,8 +1,10 @@
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
export const mockedApolloCoreClient = new ApolloClient({
link: new HttpLink({
uri: process.env.REACT_APP_SERVER_BASE_URL + '/graphql',
uri: REACT_APP_SERVER_BASE_URL + '/graphql',
}),
cache: new InMemoryCache(),
});
-5
View File
@@ -20,7 +20,6 @@ export default defineConfig(({ mode }) => {
const env = loadEnv(mode, __dirname, '');
const {
REACT_APP_SERVER_BASE_URL,
VITE_BUILD_SOURCEMAP,
VITE_HOST,
SSL_CERT_PATH,
@@ -232,11 +231,7 @@ export default defineConfig(({ mode }) => {
envPrefix: 'REACT_APP_',
define: {
_env_: {
REACT_APP_SERVER_BASE_URL,
},
'process.env': {
REACT_APP_SERVER_BASE_URL,
IS_DEBUG_MODE,
IS_DEV_ENV: mode === 'development' ? 'true' : 'false',
},
@@ -14,6 +14,7 @@ import {
} from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-driver.interface';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
import { LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/logic-function-executor-tmpdir-folder';
import { ConsoleListener } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/intercept-console';
import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/temporary-dir-manager';
@@ -22,10 +23,18 @@ import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-
import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/application/application-package/utils/copy-yarn-engine-and-build-dependencies';
import type { LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service';
import type { SdkClientArchiveService } from 'src/engine/core-modules/sdk-client/sdk-client-archive.service';
import type { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
const LAYER_BUILD_LOCK_TTL_MS = 120_000;
const LAYER_BUILD_LOCK_RETRY_MS = 500;
const LAYER_BUILD_LOCK_MAX_RETRIES = 240;
const LAYER_BUILD_READY_SENTINEL = '.twenty-layer-ready';
export interface LocalDriverOptions {
logicFunctionResourceService: LogicFunctionResourceService;
sdkClientArchiveService: SdkClientArchiveService;
cacheLockService: CacheLockService;
workspaceCacheService: WorkspaceCacheService;
}
const pathExists = async (targetPath: string): Promise<boolean> => {
@@ -41,10 +50,14 @@ const pathExists = async (targetPath: string): Promise<boolean> => {
export class LocalDriver implements LogicFunctionDriver {
private readonly logicFunctionResourceService: LogicFunctionResourceService;
private readonly sdkClientArchiveService: SdkClientArchiveService;
private readonly cacheLockService: CacheLockService;
private readonly workspaceCacheService: WorkspaceCacheService;
constructor(options: LocalDriverOptions) {
this.logicFunctionResourceService = options.logicFunctionResourceService;
this.sdkClientArchiveService = options.sdkClientArchiveService;
this.cacheLockService = options.cacheLockService;
this.workspaceCacheService = options.workspaceCacheService;
}
private getDepsLayerPath(flatApplication: FlatApplication): string {
@@ -75,23 +88,40 @@ export class LocalDriver implements LogicFunctionDriver {
applicationUniversalIdentifier: string;
}): Promise<void> {
const depsLayerPath = this.getDepsLayerPath(flatApplication);
const depsNodeModulesPath = join(depsLayerPath, 'node_modules');
const depsReadySentinelPath = join(
depsLayerPath,
LAYER_BUILD_READY_SENTINEL,
);
const nodeModulesExist = await pathExists(depsNodeModulesPath);
if (nodeModulesExist) {
if (await pathExists(depsReadySentinelPath)) {
return;
}
// Wipe any partial leftovers from a previously failed build
await fs.rm(depsLayerPath, { recursive: true, force: true });
const lockKey = `local-driver-deps-layer:${flatApplication.yarnLockChecksum ?? 'default'}`;
await this.logicFunctionResourceService.copyDependenciesInMemory({
applicationUniversalIdentifier,
workspaceId: flatApplication.workspaceId,
inMemoryFolderPath: depsLayerPath,
});
await copyYarnEngineAndBuildDependencies(depsLayerPath);
await this.cacheLockService.withLock(
async () => {
if (await pathExists(depsReadySentinelPath)) {
return;
}
await fs.rm(depsLayerPath, { recursive: true, force: true });
await this.logicFunctionResourceService.copyDependenciesInMemory({
applicationUniversalIdentifier,
workspaceId: flatApplication.workspaceId,
inMemoryFolderPath: depsLayerPath,
});
await copyYarnEngineAndBuildDependencies(depsLayerPath);
await fs.writeFile(depsReadySentinelPath, '');
},
lockKey,
{
ttl: LAYER_BUILD_LOCK_TTL_MS,
ms: LAYER_BUILD_LOCK_RETRY_MS,
maxRetries: LAYER_BUILD_LOCK_MAX_RETRIES,
},
);
}
private async ensureSdkLayer({
@@ -106,28 +136,57 @@ export class LocalDriver implements LogicFunctionDriver {
applicationUniversalIdentifier,
});
const sdkNodeModulesPath = join(sdkLayerPath, 'node_modules');
const sdkReadySentinelPath = join(sdkLayerPath, LAYER_BUILD_READY_SENTINEL);
const nodeModulesExist = await pathExists(sdkNodeModulesPath);
if (nodeModulesExist && !flatApplication.isSdkLayerStale) {
if (
(await pathExists(sdkReadySentinelPath)) &&
!flatApplication.isSdkLayerStale
) {
return;
}
await fs.rm(sdkLayerPath, { recursive: true, force: true });
const lockKey = `local-driver-sdk-layer:${flatApplication.workspaceId}:${applicationUniversalIdentifier}`;
const sdkPackagePath = join(sdkNodeModulesPath, 'twenty-client-sdk');
await this.cacheLockService.withLock(
async () => {
const { flatApplicationMaps } =
await this.workspaceCacheService.getOrRecompute(
flatApplication.workspaceId,
['flatApplicationMaps'],
);
const freshFlatApplication =
flatApplicationMaps.byId[flatApplication.id];
const isStale = freshFlatApplication?.isSdkLayerStale ?? true;
await this.sdkClientArchiveService.downloadAndExtractToPackage({
workspaceId: flatApplication.workspaceId,
applicationId: flatApplication.id,
applicationUniversalIdentifier,
targetPackagePath: sdkPackagePath,
});
if ((await pathExists(sdkReadySentinelPath)) && !isStale) {
return;
}
await this.sdkClientArchiveService.markSdkLayerFresh({
applicationId: flatApplication.id,
workspaceId: flatApplication.workspaceId,
});
await fs.rm(sdkLayerPath, { recursive: true, force: true });
const sdkPackagePath = join(sdkNodeModulesPath, 'twenty-client-sdk');
await this.sdkClientArchiveService.downloadAndExtractToPackage({
workspaceId: flatApplication.workspaceId,
applicationId: flatApplication.id,
applicationUniversalIdentifier,
targetPackagePath: sdkPackagePath,
});
await this.sdkClientArchiveService.markSdkLayerFresh({
applicationId: flatApplication.id,
workspaceId: flatApplication.workspaceId,
});
await fs.writeFile(sdkReadySentinelPath, '');
},
lockKey,
{
ttl: LAYER_BUILD_LOCK_TTL_MS,
ms: LAYER_BUILD_LOCK_RETRY_MS,
maxRetries: LAYER_BUILD_LOCK_MAX_RETRIES,
},
);
}
async transpile({
@@ -17,6 +17,7 @@ import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
import { ConfigGroupHashService } from 'src/engine/core-modules/twenty-config/services/config-group-hash.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@Injectable()
export class LogicFunctionDriverFactory extends DriverFactoryBase<LogicFunctionDriver> {
@@ -26,6 +27,7 @@ export class LogicFunctionDriverFactory extends DriverFactoryBase<LogicFunctionD
private readonly logicFunctionResourceService: LogicFunctionResourceService,
private readonly sdkClientArchiveService: SdkClientArchiveService,
private readonly cacheLockService: CacheLockService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {
super(twentyConfigService, configGroupHashService);
}
@@ -51,6 +53,8 @@ export class LogicFunctionDriverFactory extends DriverFactoryBase<LogicFunctionD
return new LocalDriver({
logicFunctionResourceService: this.logicFunctionResourceService,
sdkClientArchiveService: this.sdkClientArchiveService,
cacheLockService: this.cacheLockService,
workspaceCacheService: this.workspaceCacheService,
});
case LogicFunctionDriverType.LAMBDA: {
@@ -7,6 +7,7 @@ import { LogicFunctionTriggerModule } from 'src/engine/core-modules/logic-functi
import { LogicFunctionExecutorModule } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.module';
import { SdkClientModule } from 'src/engine/core-modules/sdk-client/sdk-client.module';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@Global()
@Module({})
@@ -21,6 +22,7 @@ export class LogicFunctionModule {
LogicFunctionTriggerModule,
LogicFunctionExecutorModule,
SdkClientModule,
WorkspaceCacheModule,
],
providers: [LogicFunctionDriverFactory],
exports: [
@@ -8,5 +8,5 @@ export type ToolRetrievalOptions = {
// Apply output compaction (strip nulls/empty values) to dispatch results
// before returning. Chat enables this to reduce token usage in the
// conversation context; MCP and workflow agents leave raw output intact.
serializeOutput?: boolean;
compactOutput?: boolean;
};
@@ -1,4 +1,4 @@
import { stripEmptyValues } from 'src/engine/core-modules/tool-provider/output-serialization/strip-empty-values.util';
import { stripEmptyValues } from 'src/engine/core-modules/tool-provider/output-transforms/strip-empty-values.util';
describe('stripEmptyValues', () => {
it('should remove null values', () => {
@@ -7,7 +7,7 @@ import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/
import { type ToolRetrievalOptions } from 'src/engine/core-modules/tool-provider/interfaces/tool-retrieval-options.type';
import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/tool-providers.token';
import { compactToolOutput } from 'src/engine/core-modules/tool-provider/output-serialization/compact-tool-output.util';
import { compactToolOutput } from 'src/engine/core-modules/tool-provider/output-transforms/compact-tool-output.util';
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
import { type LearnToolsAspect } from 'src/engine/core-modules/tool-provider/tools/learn-tools.tool';
import { type ToolContext } from 'src/engine/core-modules/tool-provider/types/tool-context.type';
@@ -107,12 +107,12 @@ export class ToolRegistryService {
options?: {
wrapWithErrorContext?: boolean;
includeLoadingMessage?: boolean;
serializeOutput?: boolean;
compactOutput?: boolean;
},
): ToolSet {
const toolSet: ToolSet = {};
const includeLoadingMessage = options?.includeLoadingMessage ?? true;
const serializeOutput = options?.serializeOutput ?? false;
const compactOutput = options?.compactOutput ?? false;
for (const descriptor of descriptors) {
const baseSchema = descriptor.inputSchema as Record<string, unknown>;
@@ -133,7 +133,7 @@ export class ToolRegistryService {
context,
);
return serializeOutput
return compactOutput
? (compactToolOutput(result) as ToolOutput)
: result;
};
@@ -170,7 +170,7 @@ export class ToolRegistryService {
context: ToolContext,
options?: {
includeLoadingMessage?: boolean;
serializeOutput?: boolean;
compactOutput?: boolean;
},
): Promise<ToolSet> {
const fullContext = this.buildContextFromToolContext(context);
@@ -190,7 +190,7 @@ export class ToolRegistryService {
return this.hydrateToolSet(descriptors, fullContext, {
includeLoadingMessage: options?.includeLoadingMessage,
serializeOutput: options?.serializeOutput,
compactOutput: options?.compactOutput,
});
}
@@ -236,7 +236,7 @@ export class ToolRegistryService {
toolName: string,
args: Record<string, unknown> | undefined,
context: ToolContext,
options?: { serializeOutput?: boolean },
options?: { compactOutput?: boolean },
): Promise<ToolOutput> {
try {
const fullContext = this.buildContextFromToolContext(context);
@@ -258,7 +258,7 @@ export class ToolRegistryService {
fullContext,
);
return options?.serializeOutput
return options?.compactOutput
? (compactToolOutput(result) as ToolOutput)
: result;
} catch (error) {
@@ -286,7 +286,7 @@ export class ToolRegistryService {
excludeTools,
wrapWithErrorContext,
includeLoadingMessage,
serializeOutput,
compactOutput,
} = options;
const categorySet = categories ? new Set(categories) : undefined;
@@ -321,7 +321,7 @@ export class ToolRegistryService {
const toolSet = this.hydrateToolSet(filteredDescriptors, context, {
wrapWithErrorContext,
includeLoadingMessage,
serializeOutput,
compactOutput,
});
this.logger.log(
@@ -49,7 +49,7 @@ export const createExecuteToolTool = (
context: ToolContext,
options?: {
excludeTools?: Set<string>;
serializeOutput?: boolean;
compactOutput?: boolean;
},
) => ({
description:
@@ -67,7 +67,7 @@ export const createExecuteToolTool = (
}
return toolRegistry.resolveAndExecute(toolName, args, context, {
serializeOutput: options?.serializeOutput,
compactOutput: options?.compactOutput,
});
},
});
@@ -4,6 +4,7 @@ import { Injectable } from '@nestjs/common';
import { ClickHouseService } from 'src/database/clickHouse/clickHouse.service';
import { formatDateForClickHouse } from 'src/database/clickHouse/clickHouse.util';
import { fillUsageTimeSeriesGaps } from 'src/engine/core-modules/usage/utils/fill-usage-time-series-gaps.util';
import { toDisplayCredits } from 'src/engine/core-modules/usage/utils/to-display-credits.util';
import { toDollars } from 'src/engine/core-modules/usage/utils/to-dollars.util';
@@ -230,9 +231,15 @@ export class UsageAnalyticsService {
},
);
return rows.map((row) => ({
const points = rows.map((row) => ({
date: row.date,
creditsUsed: row.creditsUsedMicro,
}));
return fillUsageTimeSeriesGaps({
rows: points,
periodStart,
periodEnd,
});
}
}
@@ -0,0 +1,138 @@
import { fillUsageTimeSeriesGaps } from 'src/engine/core-modules/usage/utils/fill-usage-time-series-gaps.util';
describe('fillUsageTimeSeriesGaps', () => {
it('should return all-zero series across the period when no rows are returned', () => {
const result = fillUsageTimeSeriesGaps({
rows: [],
periodStart: new Date('2026-04-20T00:00:00.000Z'),
periodEnd: new Date('2026-04-23T23:59:59.999Z'),
});
expect(result).toEqual([
{ date: '2026-04-20', creditsUsed: 0 },
{ date: '2026-04-21', creditsUsed: 0 },
{ date: '2026-04-22', creditsUsed: 0 },
{ date: '2026-04-23', creditsUsed: 0 },
]);
});
it('should fill internal gaps with zero while preserving real values', () => {
const result = fillUsageTimeSeriesGaps({
rows: [
{ date: '2026-04-20', creditsUsed: 100 },
{ date: '2026-04-22', creditsUsed: 250 },
],
periodStart: new Date('2026-04-20T00:00:00.000Z'),
periodEnd: new Date('2026-04-23T23:59:59.999Z'),
});
expect(result).toEqual([
{ date: '2026-04-20', creditsUsed: 100 },
{ date: '2026-04-21', creditsUsed: 0 },
{ date: '2026-04-22', creditsUsed: 250 },
{ date: '2026-04-23', creditsUsed: 0 },
]);
});
it('should pad trailing dates with zero when data stops before period end (Félix bug)', () => {
const result = fillUsageTimeSeriesGaps({
rows: [
{ date: '2026-04-15', creditsUsed: 50 },
{ date: '2026-04-16', creditsUsed: 75 },
],
periodStart: new Date('2026-04-15T00:00:00.000Z'),
periodEnd: new Date('2026-04-20T23:59:59.999Z'),
});
expect(result).toEqual([
{ date: '2026-04-15', creditsUsed: 50 },
{ date: '2026-04-16', creditsUsed: 75 },
{ date: '2026-04-17', creditsUsed: 0 },
{ date: '2026-04-18', creditsUsed: 0 },
{ date: '2026-04-19', creditsUsed: 0 },
{ date: '2026-04-20', creditsUsed: 0 },
]);
});
it('should pad leading dates with zero when data starts after period start', () => {
const result = fillUsageTimeSeriesGaps({
rows: [{ date: '2026-04-22', creditsUsed: 99 }],
periodStart: new Date('2026-04-20T00:00:00.000Z'),
periodEnd: new Date('2026-04-23T23:59:59.999Z'),
});
expect(result).toEqual([
{ date: '2026-04-20', creditsUsed: 0 },
{ date: '2026-04-21', creditsUsed: 0 },
{ date: '2026-04-22', creditsUsed: 99 },
{ date: '2026-04-23', creditsUsed: 0 },
]);
});
it('should return data unchanged when every day in period is already present', () => {
const rows = [
{ date: '2026-04-20', creditsUsed: 1 },
{ date: '2026-04-21', creditsUsed: 2 },
{ date: '2026-04-22', creditsUsed: 3 },
];
const result = fillUsageTimeSeriesGaps({
rows,
periodStart: new Date('2026-04-20T00:00:00.000Z'),
periodEnd: new Date('2026-04-22T23:59:59.999Z'),
});
expect(result).toEqual(rows);
});
it('should return a single entry when period spans one day', () => {
const result = fillUsageTimeSeriesGaps({
rows: [{ date: '2026-04-23', creditsUsed: 42 }],
periodStart: new Date('2026-04-23T00:00:00.000Z'),
periodEnd: new Date('2026-04-23T23:59:59.999Z'),
});
expect(result).toEqual([{ date: '2026-04-23', creditsUsed: 42 }]);
});
it('should return an empty array when periodStart is after periodEnd', () => {
const result = fillUsageTimeSeriesGaps({
rows: [],
periodStart: new Date('2026-04-25T00:00:00.000Z'),
periodEnd: new Date('2026-04-20T23:59:59.999Z'),
});
expect(result).toEqual([]);
});
it('should treat periodEnd as exclusive to match SQL `timestamp < periodEnd` semantics', () => {
const result = fillUsageTimeSeriesGaps({
rows: [{ date: '2026-04-23', creditsUsed: 10 }],
periodStart: new Date('2026-04-22T00:00:00.000Z'),
periodEnd: new Date('2026-04-24T00:00:00.000Z'),
});
expect(result).toEqual([
{ date: '2026-04-22', creditsUsed: 0 },
{ date: '2026-04-23', creditsUsed: 10 },
]);
});
it('should return dates in ascending order', () => {
const result = fillUsageTimeSeriesGaps({
rows: [
{ date: '2026-04-22', creditsUsed: 5 },
{ date: '2026-04-20', creditsUsed: 1 },
],
periodStart: new Date('2026-04-20T00:00:00.000Z'),
periodEnd: new Date('2026-04-23T23:59:59.999Z'),
});
expect(result.map((point) => point.date)).toEqual([
'2026-04-20',
'2026-04-21',
'2026-04-22',
'2026-04-23',
]);
});
});
@@ -0,0 +1,47 @@
import { type Temporal } from 'temporal-polyfill';
import {
isPlainDateAfter,
isPlainDateBeforeOrEqual,
parseToPlainDateOrThrow,
} from 'twenty-shared/utils';
import { type UsageTimeSeriesPoint } from 'src/engine/core-modules/usage/services/usage-analytics.service';
type FillUsageTimeSeriesGapsParams = {
rows: UsageTimeSeriesPoint[];
periodStart: Date;
periodEnd: Date;
};
export const fillUsageTimeSeriesGaps = ({
rows,
periodStart,
periodEnd,
}: FillUsageTimeSeriesGapsParams): UsageTimeSeriesPoint[] => {
const startDate = parseToPlainDateOrThrow(periodStart.toISOString());
const lastIncludedInstant = new Date(periodEnd.getTime() - 1);
const endDate = parseToPlainDateOrThrow(lastIncludedInstant.toISOString());
if (isPlainDateAfter(startDate, endDate)) {
return [];
}
const rowsByDate = new Map<string, UsageTimeSeriesPoint>();
for (const row of rows) {
rowsByDate.set(row.date, row);
}
const filled: UsageTimeSeriesPoint[] = [];
let currentDateCursor: Temporal.PlainDate = startDate;
while (isPlainDateBeforeOrEqual(currentDateCursor, endDate)) {
const key = currentDateCursor.toString();
const existing = rowsByDate.get(key);
filled.push(existing ?? { date: key, creditsUsed: 0 });
currentDateCursor = currentDateCursor.add({ days: 1 });
}
return filled;
};
@@ -138,7 +138,7 @@ export class ChatExecutionService {
const preloadedTools = await this.toolRegistry.getToolsByName(
AI_CHAT_TOOL_NAMES_TO_PRELOAD,
toolContext,
{ serializeOutput: true },
{ compactOutput: true },
);
const resolvedModelId = modelId ?? workspace.smartModel;
@@ -186,7 +186,7 @@ export class ChatExecutionService {
[EXECUTE_TOOL_TOOL_NAME]: createExecuteToolTool(
this.toolRegistry,
toolContext,
{ serializeOutput: true },
{ compactOutput: true },
),
[LOAD_SKILL_TOOL_NAME]: createLoadSkillTool(
(skillNames) =>
@@ -55,6 +55,12 @@
"inputCostPerMillionTokens": 5,
"outputCostPerMillionTokens": 30,
"cachedInputCostPerMillionTokens": 0.5,
"longContextCost": {
"inputCostPerMillionTokens": 10,
"outputCostPerMillionTokens": 45,
"thresholdTokens": 200000,
"cachedInputCostPerMillionTokens": 1
},
"contextWindowTokens": 1050000,
"maxOutputTokens": 130000,
"modalities": ["image", "pdf"],
+29
View File
@@ -8,6 +8,7 @@
# 1. Starts Postgres + Redis (local services or Docker, auto-detected)
# 2. Creates 'default' and 'test' databases
# 3. Copies .env.example -> .env for front and server
# 4. Initializes database schema (runs migrations + seeds)
#
# Usage (from repo root):
# bash packages/twenty-utils/setup-dev-env.sh # start + configure
@@ -241,6 +242,34 @@ else
done
fi
# =============================================================================
# 4. Initialize database schema (build server, run migrations, seed)
# =============================================================================
schema_exists() {
local query="SELECT 1 FROM information_schema.tables WHERE table_schema = 'core' LIMIT 1;"
if command -v psql &>/dev/null; then
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d default -tAc "$query" 2>/dev/null | grep -q 1
elif can_use_docker && docker compose -f "$COMPOSE_FILE" ps --quiet db 2>/dev/null | grep -q .; then
docker compose -f "$COMPOSE_FILE" exec -T db psql -U postgres -d default -tAc "$query" 2>/dev/null | grep -q 1
else
return 1
fi
}
if command -v npx &>/dev/null && [ -d node_modules ]; then
if schema_exists; then
ok "Database schema already initialized"
else
info "Initializing database schema (this may take a minute)..."
npx nx database:reset twenty-server
ok "Database schema initialized"
fi
else
echo ""
echo " NOTE: node_modules not found — skipping database schema initialization."
echo " Run 'yarn install' then 'npx nx database:reset twenty-server' to initialize the schema."
fi
# =============================================================================
echo ""
echo "Dev environment ready."