✨ server: add runtime entrypoints for api and hooks - #1226
Conversation
🦋 Changeset detectedLatest commit: fb91a71 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Warning Review limit reached
Next review available in: 21 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (10)
WalkthroughAdded a WhatsApp chat assistant, webhook, and authenticated association API. Added database support, runtime entrypoints, service configuration, lifecycle wiring, package exports, evaluations, and tests for chat and existing API and hook binaries. ChangesChat service
Runtime entrypoints
Estimated code review effort: 5 (Critical) | ~120 minutes Merge Risk: 🔴 Critical · up to This change adds API and webhook entrypoints, but the current version may fail to start, accept unsigned webhook requests, and send invalid WhatsApp requests. Merge should be blocked until the startup, authentication, and request-format issues are fixed; dependency-policy and evaluation follow-ups also remain. Sequence Diagram(s)sequenceDiagram
participant WhatsApp
participant ChatWebhook
participant Assistant
participant Redis
participant Database
WhatsApp->>ChatWebhook: send signed message webhook
ChatWebhook->>Database: resolve credential by whatsappId
ChatWebhook->>Redis: load conversation memory
ChatWebhook->>Assistant: generate response with request context
Assistant-->>ChatWebhook: return response and links
ChatWebhook->>WhatsApp: send reply
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## allow #1226 +/- ##
==========================================
- Coverage 74.56% 74.17% -0.39%
==========================================
Files 285 296 +11
Lines 14295 14496 +201
Branches 5071 5110 +39
==========================================
+ Hits 10659 10753 +94
- Misses 3282 3387 +105
- Partials 354 356 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 96beaa79e9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| export default define({ | ||
| common: ["redis-url", "sentry-dsn"], | ||
| crema: ["redis-address", "redis-password", "redis-username"], | ||
| services: { |
There was a problem hiding this comment.
Set APP_DOMAIN on the new Cloud Run services
When these entries are materialized by infra/index.ts:65-68, the container gets APP_STACK/NODE_ENV/SENTRY_DSN but no APP_DOMAIN. server/utils/appOrigin.ts derives origins from common/domain.js:1-2, which falls back to sandbox.exactly.app, so any non-sandbox Cloud Run api/hook service will use the sandbox origin for CORS/auth and for activity/block webhook registration. Pass the deployment domain into these service envs before enabling them.
Useful? React with 👍 / 👎.
| common: ["redis-url", "sentry-dsn"], | ||
| crema: ["redis-address", "redis-password", "redis-username"], | ||
| services: { | ||
| api: { |
There was a problem hiding this comment.
Pass ALCHEMY_ACTIVITY_ID to the api service
For the new split api service, this config doesn't set ALCHEMY_ACTIVITY_ID; unlike the monolith, it no longer runs hooks/activity in the same process to update server/utils/activityWebhook.ts:7-10. When server/workers/subscribe/queue.ts:20-22 takes its direct Alchemy fallback after a queue enqueue failure, webhookId is undefined, so new accounts are not subscribed to activity webhooks (the caller then swallows the error in server/utils/createCredential.ts:86). Add the same env: { ALCHEMY_ACTIVITY_ID: "alchemyActivityId" } used by the subscribe worker to the api service.
Useful? React with 👍 / 👎.
| segmentKey, | ||
| walletExtensionSecret, | ||
| ]) => | ||
| api({ |
There was a problem hiding this comment.
Preserve the /api prefix in the api entrypoint
When the new api service is reached through the existing client URL shape, requests still include the /api prefix (src/utils/server.ts:74), but this entrypoint serves the bare api.app whose routes are /activity, /card, etc.; the monolith previously mounted it with app.route("/api", api.app) in server/index.ts:116. Without either mounting under /api here or configuring a path-stripping proxy, every existing /api/* request to this service will 404.
Useful? React with 👍 / 👎.
| secret("activity-postgres-url"), | ||
| secret("redis-url"), | ||
| ]).then(([alchemyKey, onesignalKey, postgresUrl, redisUrl]) => | ||
| activity({ alchemyKey, onesignalKey, postgresUrl, redisUrl }), |
There was a problem hiding this comment.
Preserve hook path prefixes in the hook entrypoints
The split hook services supervise each bare hook app, and those apps only register POST "/"; the monolith mounted them under /hooks/<name> (server/index.ts:117-122), and activity/block even self-register webhook URLs at ${appOrigin}/hooks/activity and ${appOrigin}/hooks/block. If these new services receive the existing provider callbacks at /hooks/<name>, the requests will miss the only route and return 404 unless the entrypoint mounts the app at the same prefix or the ingress strips it.
Useful? React with 👍 / 👎.
| secret("block-alchemy-webhooks-key"), | ||
| secret("block-onesignal-api-key"), | ||
| secret("redis-url"), | ||
| signer("executor"), |
There was a problem hiding this comment.
Use the keeper signer for block execution
signer("executor") selects a new KMS key named for executor, but the block hook is still performing the keeper-backed executions that the monolith wired with executor: keeper (server/index.ts:62-66), and the plugin validates these operations against the keeper role (contracts/src/ExaPlugin.sol:373-408). In Cloud Run, proposals and withdrawals signed by an executor key that is not the deployed keeper will revert instead of being processed; wire this to the keeper signer or authorize the new key on-chain.
Useful? React with 👍 / 👎.
| secret("panda-sardine-api-key"), | ||
| secret("sardine-api-url"), | ||
| secret("panda-segment-write-key"), | ||
| signer("settler"), |
There was a problem hiding this comment.
Use the keeper signer for panda settlement
signer("settler") selects a distinct KMS key, while the monolith passed the deployed keeper as settler (server/index.ts:87-97) and the Panda hook sends keeper-restricted collectDebit/collectInstallments calls (server/hooks/panda.ts:1192 and server/hooks/panda.ts:1235; contracts/src/ExaPlugin.sol:398-402). On Cloud Run, debit and installment settlements signed by an unauthorized settler key will revert, so wire this to the keeper signer or authorize the new key on-chain.
Useful? React with 👍 / 👎.
96beaa7 to
cafb421
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cafb421425
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| services: { | ||
| api: { |
There was a problem hiding this comment.
Provision module prerequisites before deploying services
When this commit is deployed to a stack that has not been manually pre-provisioned, every added service depends on identities, secrets, and signer keys that are created only by infra/meta.ts from modules.services (infra/meta.ts:185-210,251-263), while the checked deployment path in .github/workflows/server-deploy.yaml:38-41 runs only the ordinary stack. Consequently, infra/index.ts looks up service accounts such as ${stack}-api before they exist, so the Pulumi deployment fails instead of creating these services; run/update the meta stack as an explicit prerequisite in the deployment workflow.
Useful? React with 👍 / 👎.
| ], | ||
| }, | ||
| activity: { secrets: ["alchemy-webhooks-key", "onesignal-api-key", "postgres-url"] }, | ||
| block: { secrets: ["alchemy-webhooks-key", "onesignal-api-key"], signers: ["executor"] }, |
There was a problem hiding this comment.
Avoid running two block schedulers during the service rollout
When the new block service starts with an authorized executor and Redis contains pending withdrawals or proposals, it loads and schedules every entry in server/hooks/block.ts:89-101, but the checked deployment workflow still deploys the DigitalOcean monolith in parallel (.github/workflows/server-deploy.yaml:42-56), where the same block hook performs the same startup scan. The mutex in block.ts is process-local, so both processes can submit the same keeper operation and race to remove the Redis entry, producing competing on-chain executions and duplicate side effects; the rollout must stop the monolith scheduler or add a distributed claim/lock before enabling this service.
Useful? React with 👍 / 👎.
| panda: { | ||
| secrets: ["onesignal-api-key", "panda-api-key", "postgres-url", "sardine-api-key", "segment-write-key"], | ||
| shared: ["panda-api-url", "sardine-api-url"], | ||
| signers: ["settler", "issuer"], |
There was a problem hiding this comment.
Preserve CPU for Panda's post-response work
When a non-transaction Panda webhook needs forwarding, server/hooks/panda.ts:150-154 starts publish() without awaiting it and returns the response immediately, while this Cloud Run service receives neither an always-on CPU setting nor a committed pandaResources override through infra/index.ts:62,117. Under Cloud Run's default request-scoped CPU allocation, the instance can be throttled or terminated as soon as that response completes, interrupting the downstream webhook calls and retries in publish(); await or queue this work, or deploy the service with instance-based CPU allocation.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b15d51093
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| ], | ||
| }, | ||
| activity: { secrets: ["alchemy-webhooks-key", "onesignal-api-key", "postgres-url"] }, | ||
| block: { secrets: ["alchemy-webhooks-key", "onesignal-api-key"], signers: ["executor"] }, |
There was a problem hiding this comment.
Provision instance-based CPU for block timers
When this Cloud Run service becomes the sole block scheduler, proposals and withdrawals are persisted and then executed by detached timers in server/hooks/block.ts:482-507 and 598-612. The checked service definition reads only an optional blockResources override, but no such override is committed, so Cloud Run's default request-scoped CPU can be throttled or the instance can scale to zero after the webhook response completes; with no subsequent request, the Redis entry remains pending indefinitely. Configure instance-based CPU (for example, cpuIdle: false) or move the delayed execution to a worker.
Useful? React with 👍 / 👎.
| secret("block-alchemy-webhooks-key"), | ||
| secret("block-onesignal-api-key"), | ||
| secret("redis-url"), | ||
| signer("executor"), |
There was a problem hiding this comment.
Pass the fallback signing key to the block hook
When a block instance cold-starts while Alchemy's webhook-management API is temporarily unavailable, initializeAlchemy only suppresses the failure if its signing-key set is already populated (server/hooks/block.ts:102-104). This entrypoint never passes blockKey, unlike the monolith's ALCHEMY_BLOCK_KEY wiring, so the readiness promise rejects and the HTTP server never starts even though the existing signing key could validate incoming callbacks. Provision the block signing key and pass it into the hook.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4cbc9cfe3a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| shared: [ | ||
| "bridge-api-url", | ||
| "manteca-api-url", | ||
| "panda-api-url", | ||
| "pax-api-url", | ||
| "persona-api-url", | ||
| "sardine-api-url", | ||
| ], |
There was a problem hiding this comment.
Grant the api service access to the chat secrets
The api entrypoint now reads chat-identity-key, chat-whatsapp-access-token, and chat-whatsapp-phone-number-id, but this service configuration grants its account access only to the listed api-prefixed secrets and URL shares. On Cloud Run, the Promise.all in server/api/bin.ts therefore rejects with Secret Manager permission errors and the api service never starts; add these three chat secrets to the api service's shared secrets.
Useful? React with 👍 / 👎.
| "whatsapp-phone-number-id", | ||
| "whatsapp-verify-token", | ||
| ], | ||
| shared: ["chat-identity-key"], |
There was a problem hiding this comment.
Remove the duplicate chat identity secret
For the chat module, identity-key is first prefixed to chat-identity-key by modules() and then the same name is added again through shared. Although the meta stack deduplicates secret creation, infra/index.ts:85-93 creates one SecretIamMember per list entry using the same logical name, so Pulumi encounters duplicate chat-chat-identity-key-access resources and cannot deploy the services.
Useful? React with 👍 / 👎.
| whatsappId: text("whatsapp_id"), | ||
| }, | ||
| ({ account, bridgeId }) => [uniqueIndex("account_index").on(account), uniqueIndex("bridge_id_index").on(bridgeId)], | ||
| ({ account, bridgeId, whatsappId }) => [ | ||
| uniqueIndex("account_index").on(account), | ||
| uniqueIndex("bridge_id_index").on(bridgeId), | ||
| uniqueIndex("whatsapp_id_index").on(whatsappId), |
There was a problem hiding this comment.
Add a migration for the WhatsApp credential column
This adds whatsapp_id and its unique index only to the Drizzle schema, with no migration in the commit. Existing databases therefore lack the column, causing both the association queries and the chat webhook's credential lookup to fail at runtime as soon as these routes are used; generate and include the corresponding database migration. .agents/rules/server.mdL28-L32
Useful? React with 👍 / 👎.
| authSecret: parse(pipe(string("auth"), nonEmpty("auth")), env.AUTH_SECRET), | ||
| bridgeKey: parse(pipe(string("bridge key"), nonEmpty("bridge key")), env.BRIDGE_API_KEY), | ||
| bridgeUrl: parse(pipe(string("bridge url"), nonEmpty("bridge url")), env.BRIDGE_API_URL), | ||
| chatKey: parse(pipe(string("chat"), nonEmpty("chat")), env.CHAT_IDENTITY_KEY), |
There was a problem hiding this comment.
Add the chat variables to the monolith deployment
The checked .github/workflows/server-deploy.yaml still deploys the DigitalOcean monolith from .do/app.yaml, whose complete server environment list has none of CHAT_IDENTITY_KEY, WHATSAPP_PHONE_NUMBER_ID, WHATSAPP_ACCESS_TOKEN, or ANTHROPIC_API_KEY. This new required parse therefore throws while importing server/index.ts—before the HTTP server starts—on the sandbox, base, and production deployment paths; add the chat configuration to that service or do not initialize chat in the monolith.
Useful? React with 👍 / 👎.
| app: { path: "" }, | ||
| handoff: { path: "", intro: "You can continue this in the app." }, | ||
| associate: { | ||
| path: "/whatsapp", |
There was a problem hiding this comment.
Add the route targeted by association links
Every association tool response sends the user to ${appOrigin}/whatsapp, but a repo-wide search for whatsapp and inspection of the Expo Router tree found no src/app/whatsapp route and no client code invoking the new /api/chat endpoint. The server's static rewrites consequently have no exported page to serve at this URL, so users following either the associate or move link reach a 404 and cannot complete the association flow.
Useful? React with 👍 / 👎.
| vValidator("json", event, validatorHook({ code: "bad chat" })), | ||
| async (c) => { | ||
| // TODO implement queue | ||
| const messages = new Map(parse(c.req.valid("json")).map((message) => [message.id, message] as const)); |
There was a problem hiding this comment.
Deduplicate message IDs across webhook deliveries
When WhatsApp redelivers a payload after a timeout or non-2xx response, this request-local Map is reconstructed and no longer remembers IDs from the earlier delivery. For example, if one sender group succeeds and another rejects, line 125 makes the whole webhook response fail even though the successful group already sent its reply; the retry then invokes the model and sends that reply again. Persist processed message IDs or move the work to an idempotent queue.
Useful? React with 👍 / 👎.
| "generate:broadcasts": "[ \"$CHAIN_ID\" != 31337 ] || NODE_ENV=development tsx -e 'require(\"./test/anvil\").default({ provide: () => undefined }).then((teardown) => teardown())'", | ||
| "db:push": "drizzle-kit push", | ||
| "e2e": "tsx script/e2e.ts", | ||
| "eval:chat": "tsx --env-file=.env test/evals/chat.eval.ts", |
There was a problem hiding this comment.
Make the chat eval run without a .env file
This target unconditionally asks tsx to load .env, but the repository deliberately provides and permits no such file, so pnpm nx eval:chat server exits on the missing file before starting the evaluation in the supported development environment. Remove the file dependency and consume explicitly supplied runtime configuration or project defaults instead.
AGENTS.md reference: AGENTS.md:L14-L14
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 46da69cc8d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
|
|
||
| function verify(body: string, signature?: string, secret?: string) { | ||
| if (!secret) return true; |
There was a problem hiding this comment.
Require the WhatsApp webhook secret
When the monolith is made to start but WHATSAPP_APP_SECRET remains absent—as it currently is in .do/app.yaml—this branch treats every request as authenticated. This is distinct from the already-noted missing startup variables: an Internet caller can POST fabricated message events, trigger Anthropic work and credential lookups, and make the service send WhatsApp messages to attacker-selected recipients; make the secret required and fail startup rather than failing open. .agents/rules/server.mdL61-L65
Useful? React with 👍 / 👎.
| responses: { | ||
| 200: { description: "The id is available to associate." }, | ||
| 400: { description: "Bad token, associated with another credential, or this credential already has one." }, | ||
| }, |
There was a problem hiding this comment.
Add JSON response schemas for the chat routes
When the OpenAPI specification is generated, these description-only responses omit the actual JSON contracts, including the success and conflict codes and the confirmed whatsappId; the POST response block has the same problem. As a result, generated documentation and consumers cannot derive the endpoint's response types despite this being a schema-first API, so define Valibot response schemas and attach them with resolver. .agents/rules/server.mdL12-L15
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 46da69cc8d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
|
|
||
| function verify(body: string, signature?: string, secret?: string) { | ||
| if (!secret) return true; |
There was a problem hiding this comment.
Require the WhatsApp webhook secret
When the monolith is made to start but WHATSAPP_APP_SECRET remains absent—as it currently is in .do/app.yaml—this branch treats every request as authenticated. This is distinct from the already-noted missing startup variables: an Internet caller can POST fabricated message events, trigger Anthropic work and credential lookups, and make the service send WhatsApp messages to attacker-selected recipients; make the secret required and fail startup rather than failing open. .agents/rules/server.mdL61-L65
Useful? React with 👍 / 👎.
| responses: { | ||
| 200: { description: "The id is available to associate." }, | ||
| 400: { description: "Bad token, associated with another credential, or this credential already has one." }, | ||
| }, |
There was a problem hiding this comment.
Add JSON response schemas for the chat routes
When the OpenAPI specification is generated, these description-only responses omit the actual JSON contracts, including the success and conflict codes and the confirmed whatsappId; the POST response block has the same problem. As a result, generated documentation and consumers cannot derive the endpoint's response types despite this being a schema-first API, so define Valibot response schemas and attach them with resolver. .agents/rules/server.mdL12-L15
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 5
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b9313286-d080-4efb-8f5f-3849fb86278a
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (26)
.changeset/warm-otters-associate.mdcspell.jsoninfra/utils/modules.tspackage.jsonpnpm-workspace.yamlserver/api/bin.tsserver/api/chat.tsserver/api/index.tsserver/database/schema.tsserver/hooks/bin/chat.tsserver/hooks/chat.tsserver/index.tsserver/instrument.cjsserver/package.jsonserver/script/openapi.tsserver/test/api/api.test.tsserver/test/api/bin.test.tsserver/test/api/chat.test.tsserver/test/evals/chat.eval.tsserver/test/hooks/chat.test.tsserver/test/hooks/hook.test.tsserver/test/utils/assistant.test.tsserver/test/utils/chat.test.tsserver/utils/assistant.tsserver/utils/chat.tsserver/vitest.config.mts
| shared: [ | ||
| "bridge-api-url", | ||
| "manteca-api-url", | ||
| "panda-api-url", | ||
| "pax-api-url", | ||
| "persona-api-url", | ||
| "sardine-api-url", | ||
| ], |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Authorize the API service to read the chat secrets.
server/api/bin.ts resolves chat-identity-key, chat-whatsapp-access-token, and chat-whatsapp-phone-number-id. This module exposes only API-prefixed, common, and listed shared secrets. Secret resolution will reject during API startup, and supervise will terminate the process.
Add the three chat secret names to services.api.shared.
Proposed fix
shared: [
"bridge-api-url",
+ "chat-identity-key",
+ "chat-whatsapp-access-token",
+ "chat-whatsapp-phone-number-id",
"manteca-api-url",
"panda-api-url",
"pax-api-url",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| shared: [ | |
| "bridge-api-url", | |
| "manteca-api-url", | |
| "panda-api-url", | |
| "pax-api-url", | |
| "persona-api-url", | |
| "sardine-api-url", | |
| ], | |
| shared: [ | |
| "bridge-api-url", | |
| "chat-identity-key", | |
| "chat-whatsapp-access-token", | |
| "chat-whatsapp-phone-number-id", | |
| "manteca-api-url", | |
| "panda-api-url", | |
| "pax-api-url", | |
| "persona-api-url", | |
| "sardine-api-url", | |
| ], |
| ignoreWorkspaceRootCheck: true | ||
| includeWorkspaceRoot: true | ||
| minimumReleaseAge: 1440 | ||
| minimumReleaseAgeExclude: ["@mastra/*"] |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 3 'minimumReleaseAge|minimumReleaseAgeExclude' pnpm-workspace.yaml
rg -n -C 2 '"`@mastra/`' server/package.json
fd -a '^pnpm-lock\.yaml$' . -x rg -n -C 1 '`@mastra/`' {}Repository: exactly/exa
Length of output: 8469
🌐 Web query:
pnpm minimumReleaseAgeExclude package pattern version syntax documentation
💡 Result:
In pnpm, the minimumReleaseAgeExclude setting is used to exempt specific dependencies from the minimumReleaseAge restriction [1][2]. This configuration is defined in your pnpm-workspace.yaml file [3][2]. The setting supports a flexible syntax that allows you to specify packages by name, pattern (glob), or specific version [4][2]. Supported Syntax and Patterns: 1. Package Name: You can exclude an entire package regardless of its version [2]. minimumReleaseAgeExclude: - webpack 2. Glob Patterns: You can use wildcards to exclude all packages within a scope or pattern [2][5]. minimumReleaseAgeExclude: - '@myorg/*' 3. Version Pinning: You can exclude a specific version of a package [4][2]. minimumReleaseAgeExclude: - nx@21.6.5 4. Version Ranges: You can use standard disjunction syntax (||) to exclude multiple specific versions [2][6]. minimumReleaseAgeExclude: - webpack@4.47.0 || 5.102.1 When configured, these exclusions allow pnpm to install the specified packages immediately, bypassing the wait time typically enforced by minimumReleaseAge [1][2]. Note that minimumReleaseAgeExclude was introduced in pnpm v10.16.0 [2].
Citations:
- 1: https://pnpm.io/blog/releases/10.16
- 2: https://pnpm.io/10.x/settings
- 3: https://pnpm.io/settings
- 4: https://charpeni.com/blog/protecting-against-compromised-packages-with-minimum-release-age
- 5: feat:
minimumReleaseAgeExcludesupport patterns pnpm/pnpm#9984 - 6:
minimumReleaseAgeExclude: Add support for version range pnpm/pnpm#9985
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
text = Path("pnpm-workspace.yaml").read_text()
for key in ("minimumReleaseAge", "minimumReleaseAgeExclude"):
m = re.search(rf"(?m)^{re.escape(key)}:.*$", text)
print(f"{key}: {m.group(0) if m else 'not found'}")
lock = Path("pnpm-lock.yaml").read_text()
names = sorted(set(re.findall(r"(?m)^\s*'(`@mastra/`[^']+)@([^':(]+)", lock)))
print("lockfile Mastra package/version keys:")
for name, version in names:
print(f" {name}@{version}")
PYRepository: exactly/exa
Length of output: 1932
Restrict the release-age bypass to required package versions.
@mastra/* bypasses the 24-hour policy for every current and future package version in the @mastra scope. Replace it with exact package@version selectors for the versions required by this update.
| function verify(body: string, signature?: string, secret?: string) { | ||
| if (!secret) return true; | ||
| if (!signature) return false; | ||
| const expected = Buffer.from(`sha256=${createHmac("sha256", secret).update(body).digest("hex")}`); | ||
| const received = Buffer.from(signature); | ||
| return received.length === expected.length && timingSafeEqual(received, expected); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Fail closed when webhook signing configuration is absent.
verify accepts every request when secret is absent. server/index.ts can pass an absent WHATSAPP_APP_SECRET into this branch. An unauthenticated caller can then submit a valid payload that invokes the assistant and outbound WhatsApp delivery.
server/hooks/chat.ts#L139-L145: Returnfalsewhensecretis absent, or require a non-optional secret in the hook configuration.server/index.ts#L83-L92: ParseWHATSAPP_APP_SECRETas a non-empty required value beforecreateChat.server/test/hooks/chat.test.ts#L72-L76: Add a request test that confirms an unsigned payload is rejected when no signing secret is configured.
Based on learnings, validate required environment variables at module import time and throw when they are absent.
📍 Affects 3 files
server/hooks/chat.ts#L139-L145(this comment)server/index.ts#L83-L92server/test/hooks/chat.test.ts#L72-L76
Source: Learnings
| const requestContext = new RequestContext<InferPublicSchema<typeof context>>([ | ||
| ["account", associated ? account : undefined], | ||
| ["credentialId", associated ? "credential" : undefined], | ||
| ["whatsappId", whatsappId], | ||
| ]); | ||
| const messages = [ | ||
| ...history.flatMap(({ user, assistant: answer }) => [ | ||
| { role: "user" as const, content: user }, | ||
| { role: "assistant" as const, content: answer }, | ||
| ]), | ||
| { role: "user" as const, content: input }, | ||
| ]; | ||
| const { text, toolCalls, totalUsage } = await reply(messages, { requestContext }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 4 'async function reply|agent\.generate|AgentExecutionOptions' server/utils/assistant.ts
rg -n -C 4 'new RedisStore|reply\(|requestContext|memory:|thread:|resource:' server/test/evals/chat.eval.ts
fd -a '^pnpm-lock\.yaml$' . -x rg -n -C 2 '`@mastra/`(core|memory|redis)' {}Repository: exactly/exa
Length of output: 9919
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- assistant setup and memory configuration ---'
sed -n '1,180p' server/utils/assistant.ts
rg -n -C 5 'Memory|memory|RedisStore|resource|thread|generate\(' server --glob '*.ts' --glob '*.tsx'
printf '%s\n' '--- evaluation structure and memory cases ---'
sed -n '1,80p' server/test/evals/chat.eval.ts
sed -n '250,370p' server/test/evals/chat.eval.ts
rg -n -C 8 'name:.*memory|memory' server/test/evals/chat.eval.ts
printf '%s\n' '--- local Mastra type/source references, if present ---'
git ls-files | rg '(^|/)(node_modules|packages)/.*(`@mastra`|mastra)' || true
rg -n -C 4 'interface AgentExecutionOptions|type AgentExecutionOptions|memory\??:|thread\??:|resource\??:' . --glob '*.d.ts' --glob '*.ts' --glob '*.tsx' --glob '!pnpm-lock.yaml' || trueRepository: exactly/exa
Length of output: 50368
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- production memory identifiers ---'
rg -n -C 10 'memory: \{ resource, thread \}|resource:.*thread:|thread:.*resource:|assistant\(' server/hooks server --glob '*.ts' --glob '!server/test/**' --glob '!server/test/evals/chat.eval.ts' --glob '!server/utils/assistant.ts'
printf '%s\n' '--- memory evaluation cases and runner ---'
sed -n '20,175p' server/test/evals/chat.eval.ts
sed -n '280,355p' server/test/evals/chat.eval.ts
sed -n '355,430p' server/test/evals/chat.eval.ts
printf '%s\n' '--- assistant tests covering memory options ---'
sed -n '80,120p' server/test/utils/assistant.test.ts
sed -n '1,45p' server/test/hooks/chat.test.ts
rg -n -C 6 'reply\(|memory:|resource|thread' server/test/hooks/chat.test.ts server/test/utils/assistant.test.tsRepository: exactly/exa
Length of output: 23907
🌐 Web query:
Mastra v1.59 Agent memory AgentExecutionOptions memory resource thread behavior missing resource thread
💡 Result:
In Mastra, memory and thread behavior are managed through the AgentExecutionOptions interface, which allows you to define how an agent interacts with conversation persistence [1][2][3]. The core identifiers for memory management are the resource and the thread [1][4][2]: 1. Resource (resourceId): This typically identifies the user or entity (e.g., a specific user ID) [1][2]. It serves as a persistent scope for data like working memory and observational logs [4][5]. 2. Thread (threadId): This identifies a specific conversation session [1][2]. If you are experiencing issues with thread behavior or memory, ensure both identifiers are correctly provided in your agent call [1][2]: const response = await agent.generate('Your prompt', { memory: { thread: 'your-thread-id', resource: 'your-resource-id', }, }) Regarding scoping behavior: - Resource-Scoped Memory (Default): If agents share the same resourceId, they can share observations, working memory, and embeddings across different threads [4][5]. - Thread-Scoped Memory: If agents use the same resourceId and threadId, they share the full message history, allowing for tight coupling where agents build upon each other's outputs [4]. Working memory can be explicitly set to 'thread' scope if you need isolation per conversation [5]. If a thread is missing or memory is not persisting as expected, verify that: - Your storage provider is correctly configured for the memory instance [6][7]. - The same resourceId and threadId are being passed consistently for the relevant context [4]. - You are not accidentally switching between thread-scoped and resource-scoped memory configurations, as they are isolated from one another [5]. Note that "v1.59" appears to be a versioning reference; Mastra continues to evolve its memory capabilities, including "vnext" features for RAG and working memory improvements [7]. Always ensure your implementation aligns with the documented Memory class and AgentExecutionOptions for your specific version [6][1][3].
Citations:
- 1: https://mastra.ai/reference/agents/generate
- 2: https://github.com/mastra-ai/mastra/blob/main/docs/src/content/en/reference/agents/agent.mdx
- 3: https://www.mintlify.com/mastra-ai/mastra/api/agent-options
- 4: https://github.com/mastra-ai/mastra/blob/main/docs/src/content/en/docs/memory/overview.mdx
- 5: https://github.com/mastra-ai/mastra/blob/main/docs/src/content/en/docs/memory/working-memory.mdx
- 6: https://mastra.ai/docs/memory/overview
- 7: https://mastra.ai/blog/use-rag-for-agent-memory
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from urllib.request import urlopen
urls = [
"https://raw.githubusercontent.com/mastra-ai/mastra/main/packages/core/src/agent/agent.ts",
"https://raw.githubusercontent.com/mastra-ai/mastra/main/packages/core/src/agent/index.ts",
]
terms = ("memory", "resource", "thread", "generate")
for url in urls:
print(f"--- {url} ---")
try:
text = urlopen(url, timeout=10).read().decode()
except Exception as exc:
print(type(exc).__name__, exc)
continue
lines = text.splitlines()
for index, line in enumerate(lines):
if any(term in line.lower() for term in terms):
start = max(0, index - 3)
end = min(len(lines), index + 4)
print("\n".join(f"{i + 1}:{lines[i]}" for i in range(start, end)))
print("---")
PYRepository: exactly/exa
Length of output: 631
🌐 Web query:
Mastra Agent.generate without memory resource thread missing memory options throws error or disables memory
💡 Result:
In Mastra, when you configure an agent with memory but provide only a resourceId (or missing/undefined threadId) in the agent.generate() call, the operation may throw an error or fail to persist data because Mastra requires both a resourceId and a threadId to properly scope and store conversation history [1][2][3][4]. The error "A resourceId and a threadId must be provided when using Memory" typically occurs because the agent's memory preparation step fails to validate the required identifiers [3]. Mastra organizes conversations by these two identifiers, and providing only one is insufficient for the system to determine where to read from or write to [5][6]. To resolve this issue: 1. Ensure you are passing both resource (the owner/user entity) and thread (the conversation session ID) in the memory object when calling agent.generate() [7][5][8]. 2. If you are working in an environment (such as datasets or middleware) where only a resourceId is automatically available, you must explicitly supply a threadId (e.g., by generating a unique ID) to complete the required memory configuration [3]. 3. Verify that the agent instance itself has been correctly initialized with a Memory instance that includes a configured storage backend [4][9]. Without a storage backend, memory features are effectively disabled, and attempting to use them may lead to errors [4][10]. If you provide a threadId and resourceId but find that memory is not working as expected (e.g., nothing is saved), check for circular dependencies in your code that might result in the memory configuration being passed as undefined to the agent [1].
Citations:
- 1: [DISCORD:1391750763398692984] Agent memory. ThreadId is undefined mastra-ai/mastra#5718
- 2: https://www.answeroverflow.com/m/1398108185524568216
- 3: fix(core): give each dataset experiment item its own memory thread when the context only carries a resource id mastra-ai/mastra#20844
- 4: https://github.com/mastra-ai/skills/blob/main/skills/mastra/references/common-errors.md
- 5: https://mastra.ai/docs/memory/overview
- 6: https://mastra.ai/docs/memory/message-history
- 7: https://mastra.ai/reference/agents/generate
- 8: https://www.mintlify.com/mastra-ai/mastra/api/agent-options
- 9: https://github.com/mastra-ai/mastra/blob/main/docs/src/content/en/reference/memory/memory-class.mdx
- 10: https://mastra.ai/docs/agent-builder/memory
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from urllib.request import urlopen
urls = [
"https://raw.githubusercontent.com/mastra-ai/mastra/main/packages/core/src/agent/agent.ts",
"https://raw.githubusercontent.com/mastra-ai/mastra/main/packages/core/src/agent/index.ts",
]
terms = ("memory", "resource", "thread", "generate")
for url in urls:
print(f"--- {url} ---")
try:
text = urlopen(url, timeout=10).read().decode()
except Exception as exc:
print(type(exc).__name__, exc)
continue
lines = text.splitlines()
for index, line in enumerate(lines):
if any(term in line.lower() for term in terms):
start = max(0, index - 3)
end = min(len(lines), index + 4)
print("\n".join(f"{i + 1}:{lines[i]}" for i in range(start, end)))
print("---")
PYRepository: exactly/exa
Length of output: 631
Pass memory scopes to the evaluations.
assistant uses the Redis-backed Memory, but these calls omit memory.resource and memory.thread. Mastra requires both identifiers for persisted memory. Add an isolated scope for each case and add a multi-turn case that uses the same scope across both calls to test Redis-backed retrieval and conversation isolation.
| async function send(recipient: string, text: string) { | ||
| await request(object({}), `/${from}/messages`, { | ||
| messaging_product: "whatsapp", | ||
| recipient_type: "individual", | ||
| recipient, | ||
| type: "text", | ||
| text: { body: text }, | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- server/utils/chat.ts ---'
cat -n server/utils/chat.ts
printf '%s\n' '--- chat.send usages ---'
rg -n -C 5 'chat\.send|send\(' server/api server/utils server/test 2>/dev/null || true
printf '%s\n' '--- request helper definitions/usages ---'
rg -n -C 4 'function request|const request|export .*request|request\(' server
printf '%s\n' '--- related tests ---'
fd -t f . server | rg 'chat|test' | head -80Repository: exactly/exa
Length of output: 50367
🌐 Web query:
WhatsApp Cloud API send text message request body recipient field to recipient_type
💡 Result:
In the WhatsApp Cloud API, the recipient_type field is a required parameter within the request body when sending messages [1][2]. It defines the nature of the message recipient and must be set to one of the following two values [1][3][2]: individual: Used for 1:1 messaging. When this is specified, the to field should contain the recipient's phone number [1][2]. group: Used for group messaging. When this is specified, the to field should contain the group-id [1][2]. This field is part of the common syntax required for all send message requests to the /messages endpoint [3][4]. For example, when sending a standard text message, the request body is structured as follows [3][5][6]: { "messaging_product": "whatsapp", "recipient_type": "individual", "to": "<WHATSAPP_USER_PHONE_NUMBER>", "type": "text", "text": { "body": "<BODY_TEXT>" } }
Citations:
- 1: https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages
- 2: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/message-api
- 3: https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages/
- 4: https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/send-messages
- 5: https://developers.facebook.com/docs/whatsapp/cloud-api/messages/text-messages/
- 6: https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/text-messages
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- server/api/chat.ts ---'
sed -n '70,120p' server/api/chat.ts
printf '%s\n' '--- server test layout ---'
fd -t d -d 3 . server/test | sort
printf '%s\n' '--- chat-related tests and fetch mocks ---'
rg -n -C 5 'WhatsApp|whatsapp|graph\.facebook|globalThis\.fetch|fetch\(' server/test | head -240
printf '%s\n' '--- package test configuration ---'
rg -n -C 3 'vitest|server/test|test' package.json vitest.config.* 2>/dev/null || trueRepository: exactly/exa
Length of output: 22717
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
import json
payload = {
"messaging_product": "whatsapp",
"recipient_type": "individual",
"recipient": "+15551234567",
"type": "text",
"text": {"body": "test"},
}
print("serialized payload:", json.dumps(payload, separators=(",", ":")))
print("has required `to`:", "to" in payload)
print("has nonstandard `recipient`:", "recipient" in payload)
PYRepository: exactly/exa
Length of output: 355
Use to for the WhatsApp recipient.
The WhatsApp Cloud API requires to; recipient is not a valid field. Without to, Meta rejects the request and the validation code is not sent. Update the existing assertion in server/test/hooks/chat.test.ts.
proposed fix
- recipient,
+ to: recipient,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function send(recipient: string, text: string) { | |
| await request(object({}), `/${from}/messages`, { | |
| messaging_product: "whatsapp", | |
| recipient_type: "individual", | |
| recipient, | |
| type: "text", | |
| text: { body: text }, | |
| }); | |
| async function send(recipient: string, text: string) { | |
| await request(object({}), `/${from}/messages`, { | |
| messaging_product: "whatsapp", | |
| recipient_type: "individual", | |
| to: recipient, | |
| type: "text", | |
| text: { body: text }, | |
| }); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fb91a71f07
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| v.array( | ||
| v.object({ | ||
| id: v.string(), | ||
| from_user_id: v.string(), |
There was a problem hiding this comment.
Accept standard WhatsApp sender fields
When Meta delivers the normal phone-number webhook payload, the sender is in messages[].from; from_user_id is the scoped-ID variant used for certain users. Requiring only from_user_id makes Valibot reject the former payloads with 400 before parse() runs, so affected users' inbound messages never reach the assistant. Accept both sender-field variants and normalize them before processing.
Useful? React with 👍 / 👎.
|
|
||
| const failure = associate.error | ||
| ? associate.error instanceof APIError && associate.error.text === "bad code" | ||
| ? t("That code isn't correct. Check it and try again.") |
There was a problem hiding this comment.
Do not invite retry after consuming the code
When a user mistypes the verification code once, the API has already removed the pending verification via getdel in server/api/chat.ts, but this message keeps the user on the code screen and explicitly tells them to try again. Every subsequent submission therefore returns no verification, making the advertised retry impossible; either preserve the pending value on a mismatch or immediately direct the user through the resend flow.
Useful? React with 👍 / 👎.
| if ("token" in payload) { | ||
| const whatsappId = await chat.decode(payload.token).catch(() => null); | ||
| if (!whatsappId) return c.json({ code: "bad token" }, 400); | ||
| if (!(await redis.set(`chat:cooldown:${whatsappId}`, "1", "PX", 60_000, "NX"))) { |
There was a problem hiding this comment.
Distinguish another credential's cooldown
When the same WhatsApp ID requested a code for a different credential during the previous minute, this shared cooldown returns 429 before creating chat:${credentialId} for the current account. The client treats every 429 as evidence that its code was sent and opens the verification screen, but any submitted code then returns no verification; this is especially reachable while moving a number between accounts. Return a distinct result unless a pending verification exists for the current credential, or make the client handle this case as an unsent code.
Useful? React with 👍 / 👎.
| queryKey: ["chat", "preflight", value], | ||
| queryFn: () => { | ||
| if (!value) throw new Error("missing token"); | ||
| return preflightChat(value); |
There was a problem hiding this comment.
Preserve the association link through sign-in
When the link is opened on a fresh browser or device with no cached credential, this main-group screen calls preflightChat() immediately, whose auth() path attempts a WebAuthn assertion using the default method. There is no redirect to the authentication screens or preserved return URL, so new users and fresh SIWE users only see the generic connection failure and cannot complete the association. Gate this query on authentication and route unauthenticated users through sign-in or account creation while preserving the token.
Useful? React with 👍 / 👎.
| <Button | ||
| primary | ||
| onPress={() => { | ||
| openBrowser("https://wa.me", { external: true }).catch(reportError); // TODO append bot number |
There was a problem hiding this comment.
Include the bot number in the WhatsApp CTA
When a user completes the association or needs to request a replacement link, this CTA passes the bare https://wa.me root to Linking.openURL. Without a recipient number it cannot deep-link to the Exa bot conversation, so the user cannot perform the next action described by the surrounding copy. Build the URL with the bot's public WhatsApp number.
Useful? React with 👍 / 👎.
| <YStack gap="$s6" paddingHorizontal="$s6" width="100%"> | ||
| <Feature | ||
| icon={<FileChartColumn size={24} color="$uiBrandSecondary" />} | ||
| title={t("Check your account balance")} |
There was a problem hiding this comment.
Do not advertise an unavailable balance action
When a user follows this advertised capability and asks the bot for their balance, the assistant registers no balance or portfolio tool in server/utils/assistant.ts:28-66 and receives only the account address; no RPC or API balance lookup occurs. The backend therefore cannot supply the promised balance through WhatsApp. Add the corresponding tool before presenting this capability, or remove the claim.
Useful? React with 👍 / 👎.
Summary by CodeRabbit