@circuitwall/jarela 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +5 -5
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +14 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/v1/access/[identity]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/access/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/compact/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/display-filters/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/thread/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/agents/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/allowed-sites/[hostname]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/allowed-sites/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/artifacts/lifecycle/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/[remote_jid]/route.js +18 -7
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/route.js +18 -7
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js +27 -33
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/[route_id]/route.js +28 -12
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/route.js +28 -12
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js +9 -2
- package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/route.js +19 -7
- package/.next/standalone/.next/server/app/api/v1/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/dashboard/metrics/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/dashboard/pricing/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/dashboard/pricing-task/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/documents/search/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/documents/settings/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/documents/sources/[id]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/documents/sources/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/env-sync/allowlist/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/env-sync/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/events/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/events/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/agents/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/browser/activate/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/foreground/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/history/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/poll/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/progress/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/result/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/retry/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/status/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/tabs/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/fill/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/refine/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/turn/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/config/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/secrets/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/harnesses/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/harnesses/default/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/harnesses/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/health/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/integrations/[name]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/callback/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/start/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/status/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/callback/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/start/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/status/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/lifecycle/adoption/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/maps/embed/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/mcp-servers/[name]/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/mcp-servers/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/memory/[namespace]/[key]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/memory/policy/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/memory/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/memory/search/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/models/[name]/compact-threads/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/models/[name]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/models/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/packages/defaults/[id]/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/packages/reload/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/packages/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/page-capture/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/pending-actions/[id]/approve/route.js +27 -12
- package/.next/standalone/.next/server/app/api/v1/profile/location/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/profile/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +585 -58
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/probe/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/providers/github-copilot/auth/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/providers/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/proxy-config/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/redaction/init/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/redaction/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/idle-timeout/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/pin/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/state/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/unlock/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/verify-pin/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/repos/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/repos/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/tasks/[agent_id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/tasks/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/activity/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/context-pin/route.js +12 -6
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js +125 -26
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +1 -2
- package/.next/standalone/.next/server/app/api/v1/threads/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/tools/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/voice/transcribe/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/voice/tts/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/watchers/[id]/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/watchers/[id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/watchers/route.js +1 -1
- package/.next/standalone/.next/server/app/page.js +27323 -27034
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup/page.js +8 -0
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +5 -5
- package/.next/standalone/.next/server/chunks/1388.js +106 -36
- package/.next/standalone/.next/server/chunks/2068.js +47 -10
- package/.next/standalone/.next/server/chunks/210.js +6 -6
- package/.next/standalone/.next/server/chunks/2447.js +10 -5
- package/.next/standalone/.next/server/chunks/2653.js +80 -1
- package/.next/standalone/.next/server/chunks/3042.js +10 -8
- package/.next/standalone/.next/server/chunks/{4540.js → 309.js} +104 -38
- package/.next/standalone/.next/server/chunks/{2350.js → 3272.js} +104 -37
- package/.next/standalone/.next/server/chunks/3524.js +150 -67
- package/.next/standalone/.next/server/chunks/3566.js +1 -1
- package/.next/standalone/.next/server/chunks/4189.js +1 -1
- package/.next/standalone/.next/server/chunks/4596.js +28 -4
- package/.next/standalone/.next/server/chunks/46.js +91 -31
- package/.next/standalone/.next/server/chunks/466.js +53 -7
- package/.next/standalone/.next/server/chunks/4955.js +30 -3
- package/.next/standalone/.next/server/chunks/5585.js +28 -4
- package/.next/standalone/.next/server/chunks/5908.js +63 -42
- package/.next/standalone/.next/server/chunks/6142.js +10 -2
- package/.next/standalone/.next/server/chunks/8461.js +10 -8
- package/.next/standalone/.next/server/chunks/9032.js +5 -5
- package/.next/standalone/.next/server/chunks/9164.js +487 -177
- package/.next/standalone/.next/server/chunks/9304.js +51 -12
- package/.next/standalone/.next/server/chunks/{423.js → 9590.js} +84 -24
- package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
- package/.next/standalone/.next/server/middleware.js +89 -21
- package/.next/standalone/.next/server/pages/404.html +14 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/{1395-e4a99912b8f83044.js → 1395-bfe1f3ef760c0f50.js} +54 -8
- package/.next/standalone/.next/static/chunks/app/{layout-9bc0b10127cb7d42.js → layout-7b24ad13faa3944b.js} +69 -2
- package/.next/standalone/.next/static/chunks/app/{page-213a623b37226452.js → page-2ef8d75d40eb4311.js} +10643 -10295
- package/.next/standalone/.next/static/css/c160ea862133acd2.css +5 -0
- package/.next/standalone/docs/adoption/changesets.json +2805 -0
- package/.next/standalone/package.json +1 -1
- package/CHANGELOG.md +52 -0
- package/api/client.models.test.ts +10 -0
- package/api/client.startup.test.ts +129 -0
- package/api/client.ts +51 -9
- package/api/thread-metadata.test.ts +72 -0
- package/api/types.ts +6 -3
- package/app/api/v1/providers/[provider]/models/route.test.ts +216 -6
- package/app/api/v1/providers/[provider]/models/route.ts +259 -40
- package/app/api/v1/threads/[thread_id]/route.ts +26 -23
- package/app/api/v1/threads/[thread_id]/run/route.ts +0 -1
- package/app/globals.css +119 -29
- package/app/layout.tsx +12 -0
- package/components/agents/AgentsPanel.tsx +5 -13
- package/components/chat/ChatView.tsx +9 -18
- package/components/chat/InputBar.tsx +1 -1
- package/components/chat/MessageBubble.tsx +13 -1
- package/components/chat/MessageList.test.tsx +45 -4
- package/components/chat/MessageList.tsx +123 -89
- package/components/chat/ToolList.tsx +88 -84
- package/components/chat/useThreadActivity.test.tsx +2 -2
- package/components/chat/useThreadActivity.ts +2 -2
- package/components/chat/useThreadData.ts +1 -30
- package/components/chat/useThreadMetadataPolling.test.tsx +128 -0
- package/components/chat/useThreadMetadataPolling.ts +50 -0
- package/components/chat/useUserProfileAndAgent.test.tsx +31 -0
- package/components/chat/useUserProfileAndAgent.ts +13 -2
- package/components/dashboard/DashboardPanel.tsx +1 -1
- package/components/dashboard/Series.tsx +34 -21
- package/components/dashboard/SummaryCards.tsx +1 -1
- package/components/integrations/NetworkSection.tsx +2 -2
- package/components/layout/AppShell.tsx +22 -7
- package/components/layout/MenuPanel.tsx +14 -24
- package/components/profile/ProfilePanel.tsx +2 -2
- package/components/settings/AppearancePanel.tsx +70 -3
- package/components/settings/SettingsPanel.tsx +2 -2
- package/components/tools/ToolsPanel.tsx +2 -7
- package/components/ui/AppearancePreferences.test.tsx +68 -0
- package/components/ui/BootScreen.test.tsx +44 -0
- package/components/ui/BootScreen.tsx +24 -11
- package/components/ui/NotificationStatus.test.tsx +2 -2
- package/components/ui/NotificationStatus.tsx +5 -5
- package/components/ui/SubTabBar.tsx +2 -3
- package/contexts/ThemeContext.tsx +84 -1
- package/hooks/useAgentSession.test.ts +12 -1
- package/hooks/useAgentSession.ts +6 -4
- package/hooks/useUrlSync.test.ts +13 -0
- package/hooks/useUrlSync.ts +0 -4
- package/lib/agents/citation-checker.test.ts +40 -2
- package/lib/agents/citation-checker.ts +56 -11
- package/lib/agents/context-boundary.ts +5 -6
- package/lib/agents/instruction-edits.test.ts +120 -0
- package/lib/agents/instruction-edits.ts +65 -42
- package/lib/agents/prepare/request.ts +0 -2
- package/lib/agents/prepare/system-prompt.test.ts +10 -0
- package/lib/agents/prepare/system-prompt.ts +8 -2
- package/lib/agents/proposals.test.ts +76 -0
- package/lib/agents/proposals.ts +13 -5
- package/lib/agents/run-thread.auto-boundary.test.ts +125 -5
- package/lib/agents/run-thread.ts +24 -9
- package/lib/agents/sqlite-checkpoint-saver.test.ts +91 -0
- package/lib/agents/warm-summary-background.ts +22 -2
- package/lib/agents/warm-topics.test.ts +155 -1
- package/lib/agents/warm-topics.ts +27 -7
- package/lib/bridges/dispatcher.ts +1 -0
- package/lib/bridges/message-role.test.ts +81 -0
- package/lib/bridges/message-role.ts +23 -10
- package/lib/bridges/types.ts +6 -0
- package/lib/bridges/whatsapp-link.test.ts +32 -0
- package/lib/bridges/whatsapp-link.ts +21 -0
- package/lib/bridges/whatsapp.inbound.test.ts +239 -0
- package/lib/bridges/whatsapp.sender.test.ts +80 -1
- package/lib/bridges/whatsapp.ts +76 -38
- package/lib/db/migrations.test.ts +61 -0
- package/lib/db/migrations.ts +13 -0
- package/lib/embeddings/performance.test.ts +114 -0
- package/lib/pricing/snapshot.test.ts +70 -0
- package/lib/pricing/snapshot.ts +10 -2
- package/lib/proxy/dispatcher.test.ts +119 -1
- package/lib/proxy/dispatcher.ts +22 -9
- package/lib/stores/agent-configs.ts +10 -5
- package/lib/stores/bridges.test.ts +81 -0
- package/lib/stores/bridges.ts +9 -8
- package/lib/stores/dashboard-metrics-cache.test.ts +99 -0
- package/lib/stores/dashboard-metrics.ts +30 -4
- package/lib/stores/message-embedding-jobs.test.ts +27 -0
- package/lib/stores/message-embedding-jobs.ts +5 -3
- package/lib/stores/message-usage.test.ts +21 -1
- package/lib/stores/pricing-cache.test.ts +31 -0
- package/lib/stores/pricing.ts +3 -0
- package/lib/stores/proxy-config.test.ts +120 -0
- package/lib/stores/proxy-config.ts +22 -4
- package/lib/stores/threads.ts +12 -8
- package/lib/tools/communications/calendar.test.ts +43 -2
- package/lib/tools/communications/calendar.ts +2 -0
- package/lib/tools/communications/gmail.test.ts +51 -1
- package/lib/tools/communications/gmail.ts +44 -2
- package/lib/tools/communications/outlook-calendar.ts +2 -0
- package/lib/tools/communications/outlook.test.ts +33 -2
- package/lib/tools/communications/outlook.ts +2 -0
- package/lib/tools/filesystem/edit-match.test.ts +133 -0
- package/lib/tools/filesystem/edit-match.ts +7 -2
- package/lib/tools/packages/default-packages.test.ts +21 -0
- package/lib/tools/packages/optional-package.test.ts +72 -0
- package/lib/tools/packages/optional-package.ts +33 -4
- package/lib/tools/support/result-refs.test.ts +131 -0
- package/lib/tools/support/result-refs.ts +41 -9
- package/lib/ui/navigate.test.ts +13 -0
- package/lib/ui/navigate.ts +10 -2
- package/lib/utils/source-link.test.ts +51 -0
- package/lib/utils/source-link.ts +45 -0
- package/package.json +1 -1
- package/.next/standalone/.next/static/css/312542a5725ba0d5.css +0 -5
- /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → 544OFJV-q0RC6Gjgje4LT}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → 544OFJV-q0RC6Gjgje4LT}/_ssgManifest.js +0 -0
package/lib/bridges/whatsapp.ts
CHANGED
|
@@ -29,12 +29,13 @@ import {
|
|
|
29
29
|
bumpRouteLastSeenTs,
|
|
30
30
|
findRoute,
|
|
31
31
|
getBridge,
|
|
32
|
-
|
|
32
|
+
getRouteLastSeenTsMap,
|
|
33
33
|
} from "@/lib/stores/bridges";
|
|
34
34
|
import { ensureWhatsAppAuthDir, removeWhatsAppAuth } from "./modules/whatsapp/auth-state";
|
|
35
35
|
import type { BridgeAdapter, ChatInfo, InboundEvent, InboundHandler, StatusHandler, InboundMessage, StatusUpdate } from "./types";
|
|
36
36
|
import type { ContentPart } from "@/lib/tools/runtime/types";
|
|
37
37
|
import { storeCanonicalBridgeAttachment } from "./attachment-store";
|
|
38
|
+
import { whatsappChatLink } from "./whatsapp-link";
|
|
38
39
|
import { errorMessage } from "@/lib/utils/error";
|
|
39
40
|
|
|
40
41
|
// Baileys + qrcode are dev-time-installed peer libs. We never import their
|
|
@@ -285,12 +286,12 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
285
286
|
// adapter should re-deliver to agents) and the legacy
|
|
286
287
|
// `replace`/`update` events that arrive via messages.update.
|
|
287
288
|
if (payload.type !== "notify" && payload.type !== "append") return;
|
|
288
|
-
// On a fresh adapter boot the
|
|
289
|
-
// persisted. Read
|
|
290
|
-
//
|
|
291
|
-
const
|
|
292
|
-
?
|
|
293
|
-
:
|
|
289
|
+
// On a fresh adapter boot the watermarks are whatever the last run
|
|
290
|
+
// persisted. Read them once per batch (one cheap SQLite call) so old
|
|
291
|
+
// appends are rejected without per-message queries.
|
|
292
|
+
const watermarks = payload.type === "append"
|
|
293
|
+
? getRouteLastSeenTsMap(this.bridge_id)
|
|
294
|
+
: null;
|
|
294
295
|
for (const raw of payload.messages ?? []) {
|
|
295
296
|
const m = raw as {
|
|
296
297
|
key?: { remoteJid?: string; remoteJidAlt?: string; fromMe?: boolean; id?: string; participant?: string };
|
|
@@ -324,18 +325,20 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
324
325
|
const ts = typeof m.messageTimestamp === "number"
|
|
325
326
|
? m.messageTimestamp * 1000
|
|
326
327
|
: (m.messageTimestamp?.low ?? 0) * 1000 || Date.now();
|
|
327
|
-
// Watermark gate: on `append` batches, skip anything
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
if (
|
|
328
|
+
// Watermark gate: on `append` batches, skip anything at or below
|
|
329
|
+
// this chat's own persisted watermark. This prevents the server's
|
|
330
|
+
// reconnect replay from re-delivering messages we already routed in
|
|
331
|
+
// a previous process lifetime. `notify` events skip this check —
|
|
332
|
+
// they're live and may legitimately arrive out of order.
|
|
333
|
+
const watermark = watermarks?.get(remote_jid) ?? 0;
|
|
334
|
+
if (watermark > 0 && ts > 0 && ts <= watermark) {
|
|
334
335
|
continue;
|
|
335
336
|
}
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
|
|
337
|
+
// pushName names the *sender*: in groups that's a participant, and on
|
|
338
|
+
// fromMe messages it's the user's own name. Only a counterpart's
|
|
339
|
+
// pushName in a 1:1 chat is a valid label for the chat itself.
|
|
340
|
+
const chatPushName = is_group || m.key.fromMe ? null : (m.pushName ?? null);
|
|
341
|
+
this.observeChat(remote_jid, chatPushName, ts);
|
|
339
342
|
|
|
340
343
|
// Unwrap protocol envelopes (view-once, ephemeral, etc.) so we can
|
|
341
344
|
// see the underlying payload uniformly, then turn it into a text
|
|
@@ -357,7 +360,7 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
357
360
|
const participant_jid = is_group && m.key.participant
|
|
358
361
|
? normalizeUserJid(m.key.participant)
|
|
359
362
|
: null;
|
|
360
|
-
const chat_name = this.chats.get(remote_jid)?.name ??
|
|
363
|
+
const chat_name = this.chats.get(remote_jid)?.name ?? chatPushName;
|
|
361
364
|
const sender = resolveWhatsAppSender({
|
|
362
365
|
fromMe: m.key.fromMe === true,
|
|
363
366
|
isGroup: is_group,
|
|
@@ -422,7 +425,7 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
422
425
|
};
|
|
423
426
|
const chatJid = pickContactChatJid(ct);
|
|
424
427
|
if (!chatJid) continue;
|
|
425
|
-
const name = ct.name
|
|
428
|
+
const name = [ct.name, ct.verifiedName, ct.notify].find((n) => n?.trim()) ?? null;
|
|
426
429
|
this.observeChat(chatJid, name, null);
|
|
427
430
|
}
|
|
428
431
|
});
|
|
@@ -457,7 +460,7 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
457
460
|
for (const ct of list) {
|
|
458
461
|
const chatJid = pickContactChatJid(ct);
|
|
459
462
|
if (!chatJid) continue;
|
|
460
|
-
this.observeChat(chatJid, ct.name
|
|
463
|
+
this.observeChat(chatJid, [ct.name, ct.verifiedName, ct.notify].find((n) => n?.trim()) ?? null, null);
|
|
461
464
|
}
|
|
462
465
|
});
|
|
463
466
|
|
|
@@ -592,6 +595,10 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
592
595
|
this.pushStatus({ status: "disconnected", qr_data_url: null, error: null });
|
|
593
596
|
}
|
|
594
597
|
|
|
598
|
+
chatLink(remote_jid: string): string | null {
|
|
599
|
+
return whatsappChatLink(remote_jid);
|
|
600
|
+
}
|
|
601
|
+
|
|
595
602
|
async sendText(remote_jid: string, text: string): Promise<void> {
|
|
596
603
|
// Hard guard: refuse to send to any chat whose route is in silent_mode.
|
|
597
604
|
// The dispatcher already short-circuits before calling sendText, but
|
|
@@ -912,8 +919,11 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
912
919
|
});
|
|
913
920
|
attachments.push(ref);
|
|
914
921
|
const sizeKb = Math.max(1, Math.round(buf.length / 1024));
|
|
922
|
+
// Sender-supplied document names can carry newlines or brackets;
|
|
923
|
+
// describe the attachment by the store's sanitized name.
|
|
924
|
+
const shownName = oneLine(("filename" in ref && ref.filename) || filename);
|
|
915
925
|
text = (text ? text + "\n" : "")
|
|
916
|
-
+ `[Attached ${label}: ${
|
|
926
|
+
+ `[Attached ${label}: ${shownName} (${mime}, ${sizeKb} KB); retained as a Jarela file reference.]`;
|
|
917
927
|
} catch (err) {
|
|
918
928
|
const m = errorMessage(err);
|
|
919
929
|
console.warn(`[bridge ${this.bridge_id}] failed to spill ${label} from ${remote_jid}: ${m}`);
|
|
@@ -975,8 +985,8 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
975
985
|
if (inner.locationMessage) {
|
|
976
986
|
const { degreesLatitude, degreesLongitude, name, address } = inner.locationMessage;
|
|
977
987
|
const parts = [`lat=${degreesLatitude ?? "?"}`, `lng=${degreesLongitude ?? "?"}`];
|
|
978
|
-
if (name) parts.push(`name
|
|
979
|
-
if (address) parts.push(`address
|
|
988
|
+
if (name) parts.push(`name=${JSON.stringify(oneLine(name))}`);
|
|
989
|
+
if (address) parts.push(`address=${JSON.stringify(oneLine(address))}`);
|
|
980
990
|
text = (text ? text + "\n" : "") + `[location: ${parts.join(" ")}]`;
|
|
981
991
|
}
|
|
982
992
|
|
|
@@ -986,14 +996,14 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
986
996
|
}
|
|
987
997
|
|
|
988
998
|
if (inner.contactMessage) {
|
|
989
|
-
const name = inner.contactMessage.displayName ?? "unknown";
|
|
990
|
-
const vcard = inner.contactMessage.vcard ?? "";
|
|
999
|
+
const name = oneLine(inner.contactMessage.displayName ?? "") || "unknown";
|
|
1000
|
+
const vcard = stripVcardBinary(inner.contactMessage.vcard ?? "");
|
|
991
1001
|
text = (text ? text + "\n" : "") + `[contact: ${name}]\n${vcard}`;
|
|
992
1002
|
}
|
|
993
1003
|
|
|
994
1004
|
if (inner.contactsArrayMessage) {
|
|
995
1005
|
const names = (inner.contactsArrayMessage.contacts ?? [])
|
|
996
|
-
.map((c) => c?.displayName ?? "unknown")
|
|
1006
|
+
.map((c) => oneLine(c?.displayName ?? "") || "unknown")
|
|
997
1007
|
.join(", ");
|
|
998
1008
|
text = (text ? text + "\n" : "") + `[contacts: ${names}]`;
|
|
999
1009
|
}
|
|
@@ -1005,8 +1015,9 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
1005
1015
|
if (!remote_jid) return;
|
|
1006
1016
|
const existing = this.chats.get(remote_jid);
|
|
1007
1017
|
// Prefer the most recently-observed name (group subjects rename; users
|
|
1008
|
-
// change push names). Don't overwrite a known name with
|
|
1009
|
-
|
|
1018
|
+
// change push names). Don't overwrite a known name with a missing or
|
|
1019
|
+
// blank one.
|
|
1020
|
+
const nextName = name?.trim() || existing?.name || null;
|
|
1010
1021
|
const nextTs = ts && (!existing?.last_message_at || ts > existing.last_message_at)
|
|
1011
1022
|
? ts
|
|
1012
1023
|
: existing?.last_message_at ?? null;
|
|
@@ -1026,6 +1037,30 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
1026
1037
|
get status(): StatusUpdate["status"] { return this.currentStatus; }
|
|
1027
1038
|
}
|
|
1028
1039
|
|
|
1040
|
+
export function oneLine(value: string): string {
|
|
1041
|
+
return value.replace(/[\s\u0085\u2028\u2029]+/g, " ").trim();
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Phones embed contact photos and logos as base64 vCard properties, often
|
|
1046
|
+
* tens of KB of noise in the agent's context. Drop those properties,
|
|
1047
|
+
* including their folded continuation lines (RFC 6350 §3.2), and keep the
|
|
1048
|
+
* readable fields.
|
|
1049
|
+
*/
|
|
1050
|
+
export function stripVcardBinary(vcard: string): string {
|
|
1051
|
+
const out: string[] = [];
|
|
1052
|
+
let skipping = false;
|
|
1053
|
+
for (const line of vcard.split(/\r\n|\r|\n/)) {
|
|
1054
|
+
if (/^[ \t]/.test(line)) {
|
|
1055
|
+
if (!skipping) out.push(line);
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
skipping = /^(?:[\w-]+\.)?(?:PHOTO|LOGO|SOUND|KEY)[;:]/i.test(line);
|
|
1059
|
+
if (!skipping) out.push(line);
|
|
1060
|
+
}
|
|
1061
|
+
return out.join("\n").trim();
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1029
1064
|
// Minimal pino-compatible silent logger. Baileys uses `logger.child()`
|
|
1030
1065
|
// chaining internally, so we return ourselves for every child call.
|
|
1031
1066
|
function makeSilentLogger(): unknown {
|
|
@@ -1070,7 +1105,7 @@ const MEDIA_TYPE_ALLOWLIST: Record<"image" | "audio" | "video" | "document", Rea
|
|
|
1070
1105
|
]),
|
|
1071
1106
|
};
|
|
1072
1107
|
|
|
1073
|
-
function sanitizeMediaType(
|
|
1108
|
+
export function sanitizeMediaType(
|
|
1074
1109
|
raw: string | undefined,
|
|
1075
1110
|
kind: "image" | "audio" | "video" | "document",
|
|
1076
1111
|
fallback: string,
|
|
@@ -1088,7 +1123,7 @@ function sanitizeMediaType(
|
|
|
1088
1123
|
* in several envelope variants (view-once, ephemeral, V2 versions); see
|
|
1089
1124
|
* `unwrapMessage` for how we collapse them.
|
|
1090
1125
|
*/
|
|
1091
|
-
type WAMessageContent = {
|
|
1126
|
+
export type WAMessageContent = {
|
|
1092
1127
|
conversation?: string;
|
|
1093
1128
|
extendedTextMessage?: { text?: string };
|
|
1094
1129
|
stickerMessage?: { mimetype?: string; isAnimated?: boolean };
|
|
@@ -1113,7 +1148,7 @@ type WAMessageContent = {
|
|
|
1113
1148
|
* payload. Bounded recursion — these envelopes never nest deeper than 2
|
|
1114
1149
|
* levels in practice, but we cap at 5 as defence in depth.
|
|
1115
1150
|
*/
|
|
1116
|
-
function unwrapMessage(msg: WAMessageContent | undefined, depth = 0): WAMessageContent | undefined {
|
|
1151
|
+
export function unwrapMessage(msg: WAMessageContent | undefined, depth = 0): WAMessageContent | undefined {
|
|
1117
1152
|
if (!msg || depth > 5) return msg;
|
|
1118
1153
|
if (msg.ephemeralMessage?.message) return unwrapMessage(msg.ephemeralMessage.message, depth + 1);
|
|
1119
1154
|
if (msg.viewOnceMessage?.message) return unwrapMessage(msg.viewOnceMessage.message, depth + 1);
|
|
@@ -1128,7 +1163,7 @@ function unwrapMessage(msg: WAMessageContent | undefined, depth = 0): WAMessageC
|
|
|
1128
1163
|
* country-code + number with no separators, no leading '+'. Accepts the
|
|
1129
1164
|
* common human formats: "+1 (555) 123-4567", "5511 99999-0000", etc.
|
|
1130
1165
|
*/
|
|
1131
|
-
function normalizePhoneDigits(input: string): string {
|
|
1166
|
+
export function normalizePhoneDigits(input: string): string {
|
|
1132
1167
|
return input.replace(/\D+/g, "");
|
|
1133
1168
|
}
|
|
1134
1169
|
|
|
@@ -1142,7 +1177,7 @@ function normalizePhoneDigits(input: string): string {
|
|
|
1142
1177
|
* the `@s.whatsapp.net` form — without this normalization, every inbound
|
|
1143
1178
|
* `@lid` message would silently fail to match its route.
|
|
1144
1179
|
*/
|
|
1145
|
-
function pickRoutableJid(primary: string, alt: string | undefined): string {
|
|
1180
|
+
export function pickRoutableJid(primary: string, alt: string | undefined): string {
|
|
1146
1181
|
const isRoutable = (j: string | undefined) =>
|
|
1147
1182
|
!!j && (j.endsWith("@s.whatsapp.net") || j.endsWith("@g.us"));
|
|
1148
1183
|
if (isRoutable(primary)) return primary;
|
|
@@ -1160,6 +1195,9 @@ export function resolveWhatsAppSender(input: {
|
|
|
1160
1195
|
chatName: string | null;
|
|
1161
1196
|
participantName: string | null;
|
|
1162
1197
|
}): { senderJid: string; senderName: string } {
|
|
1198
|
+
// Platforms send blank display names ("" or whitespace) for contacts that
|
|
1199
|
+
// never set one; treat those as missing so the fallbacks still apply.
|
|
1200
|
+
const named = (...names: Array<string | null>) => names.find((n) => !!n && n.trim().length > 0)?.trim();
|
|
1163
1201
|
if (input.fromMe) {
|
|
1164
1202
|
return {
|
|
1165
1203
|
senderJid: input.selfJid ?? "me",
|
|
@@ -1170,12 +1208,12 @@ export function resolveWhatsAppSender(input: {
|
|
|
1170
1208
|
const senderJid = input.participantJid ?? input.remoteJid;
|
|
1171
1209
|
return {
|
|
1172
1210
|
senderJid,
|
|
1173
|
-
senderName: input.participantName
|
|
1211
|
+
senderName: named(input.participantName, input.pushName) ?? senderJid,
|
|
1174
1212
|
};
|
|
1175
1213
|
}
|
|
1176
1214
|
return {
|
|
1177
1215
|
senderJid: input.remoteJid,
|
|
1178
|
-
senderName: input.pushName
|
|
1216
|
+
senderName: named(input.pushName, input.chatName) ?? input.remoteJid,
|
|
1179
1217
|
};
|
|
1180
1218
|
}
|
|
1181
1219
|
|
|
@@ -1198,7 +1236,7 @@ export function resolveWhatsAppSender(input: {
|
|
|
1198
1236
|
* Returns null for JIDs that aren't routable chats (self broadcast,
|
|
1199
1237
|
* status@broadcast, malformed ids, empty input).
|
|
1200
1238
|
*/
|
|
1201
|
-
function pickContactChatJid(ct: {
|
|
1239
|
+
export function pickContactChatJid(ct: {
|
|
1202
1240
|
id?: string;
|
|
1203
1241
|
phoneNumber?: string;
|
|
1204
1242
|
lid?: string;
|
|
@@ -1220,7 +1258,7 @@ function pickContactChatJid(ct: {
|
|
|
1220
1258
|
* routable user JID — `sendMessage` won't deliver to a JID with `:NN`.
|
|
1221
1259
|
* Returns null if the input is malformed.
|
|
1222
1260
|
*/
|
|
1223
|
-
function normalizeUserJid(id: unknown): string | null {
|
|
1261
|
+
export function normalizeUserJid(id: unknown): string | null {
|
|
1224
1262
|
if (typeof id !== "string") return null;
|
|
1225
1263
|
// Strip any device suffix (":NN") before the '@'.
|
|
1226
1264
|
const at = id.indexOf("@");
|
|
@@ -1234,7 +1272,7 @@ function normalizeUserJid(id: unknown): string | null {
|
|
|
1234
1272
|
return `${user}@${normHost}`;
|
|
1235
1273
|
}
|
|
1236
1274
|
|
|
1237
|
-
function humanList(items: string[], max: number): string {
|
|
1275
|
+
export function humanList(items: string[], max: number): string {
|
|
1238
1276
|
const clean = items.filter((s) => s && s.trim().length > 0);
|
|
1239
1277
|
if (clean.length === 0) return "(none)";
|
|
1240
1278
|
if (clean.length <= max) return clean.join(", ");
|
|
@@ -18,6 +18,67 @@ afterAll(() => {
|
|
|
18
18
|
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
describe("responsive transcript indexes (ADR-0103)", () => {
|
|
22
|
+
it("restores missing indexes on upgrade and remains idempotent", async () => {
|
|
23
|
+
const db = getDb();
|
|
24
|
+
const names = [
|
|
25
|
+
"idx_messages_thread_seq", "idx_messages_created_at",
|
|
26
|
+
"idx_threads_updated", "idx_message_usage_thread_created",
|
|
27
|
+
"idx_memory_namespace_updated", "idx_memory_updated",
|
|
28
|
+
"idx_runtime_operations_thread_state", "idx_signal_events_thread",
|
|
29
|
+
"idx_signal_events_operation", "idx_signal_deliveries_lease",
|
|
30
|
+
"idx_background_results_expiry", "idx_message_embedding_jobs_lease",
|
|
31
|
+
];
|
|
32
|
+
for (const name of names) db.exec(`DROP INDEX IF EXISTS ${name}`);
|
|
33
|
+
const { runMigrations } = await import("./migrations");
|
|
34
|
+
runMigrations(db);
|
|
35
|
+
runMigrations(db);
|
|
36
|
+
const indexes = db.prepare("SELECT name FROM sqlite_master WHERE type='index'").all()
|
|
37
|
+
.map((row) => row.name);
|
|
38
|
+
expect(indexes).toEqual(expect.arrayContaining(names));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it.each([
|
|
42
|
+
["SELECT rowid, msg_id, content FROM messages WHERE thread_id=? ORDER BY rowid DESC LIMIT ?", ["thread", 51]],
|
|
43
|
+
["SELECT rowid, msg_id, content FROM messages WHERE thread_id=? AND rowid<? ORDER BY rowid DESC LIMIT ?", ["thread", 100, 51]],
|
|
44
|
+
["SELECT rowid, msg_id, content FROM messages WHERE thread_id=? AND rowid>? ORDER BY rowid ASC LIMIT ?", ["thread", 100, 50]],
|
|
45
|
+
["SELECT content FROM messages WHERE created_at>=? ORDER BY created_at ASC", ["2026-10-01"]],
|
|
46
|
+
["SELECT * FROM threads WHERE agent_id=? ORDER BY updated_at DESC LIMIT ?", ["agent", 50]],
|
|
47
|
+
["SELECT * FROM threads ORDER BY updated_at DESC LIMIT ? OFFSET ?", [50, 0]],
|
|
48
|
+
])("uses ordered indexes for %s", (query, params) => {
|
|
49
|
+
const plan = getDb().prepare(`EXPLAIN QUERY PLAN ${query}`).all(...params)
|
|
50
|
+
.map((row) => String(row.detail)).join("; ");
|
|
51
|
+
expect(plan).not.toMatch(/USE TEMP B-TREE|SCAN (?:messages|threads)(?! USING INDEX)/);
|
|
52
|
+
expect(plan).toMatch(/USING INDEX/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it.each([
|
|
56
|
+
["SELECT * FROM message_usage WHERE thread_id=? ORDER BY created_at DESC LIMIT 1", ["thread"], "idx_message_usage_thread_created"],
|
|
57
|
+
["SELECT namespace,key,value FROM memory_store WHERE namespace=? ORDER BY updated_at DESC LIMIT ?", ["facts", 50], "idx_memory_namespace_updated"],
|
|
58
|
+
["SELECT namespace,key,value FROM memory_store ORDER BY updated_at DESC LIMIT ?", [50], "idx_memory_updated"],
|
|
59
|
+
["SELECT rowid FROM runtime_operations WHERE thread_id=? AND state='accepted'", ["thread"], "idx_runtime_operations_thread_state"],
|
|
60
|
+
["SELECT rowid FROM signal_events WHERE thread_id=?", ["thread"], "idx_signal_events_thread"],
|
|
61
|
+
["SELECT rowid FROM signal_events WHERE operation_id=?", ["operation"], "idx_signal_events_operation"],
|
|
62
|
+
["SELECT key FROM background_tool_results WHERE expires_at<=?", [1000], "idx_background_results_expiry"],
|
|
63
|
+
["SELECT event_id FROM signal_deliveries WHERE state='leased' AND lease_until<=?", [1000], "idx_signal_deliveries_lease"],
|
|
64
|
+
["SELECT message_id FROM message_embedding_jobs WHERE state='processing' AND lease_until_ms<=?", [1000], "idx_message_embedding_jobs_lease"],
|
|
65
|
+
])("indexes secondary lookup %s", (query, params, index) => {
|
|
66
|
+
const plan = getDb().prepare(`EXPLAIN QUERY PLAN ${query}`).all(...params)
|
|
67
|
+
.map((row) => String(row.detail)).join("; ");
|
|
68
|
+
expect(plan).not.toMatch(/USE TEMP B-TREE/);
|
|
69
|
+
expect(plan).toContain(index);
|
|
70
|
+
if (!query.includes("FROM memory_store ORDER BY")) expect(plan).toContain("SEARCH");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("reuses the unique agent index instead of adding a redundant ordering index", () => {
|
|
74
|
+
const indexes = getDb().prepare("PRAGMA index_list(threads)").all();
|
|
75
|
+
expect(indexes).toEqual(expect.arrayContaining([
|
|
76
|
+
expect.objectContaining({ name: "idx_threads_agent_id", unique: 1 }),
|
|
77
|
+
]));
|
|
78
|
+
expect(indexes.some((row) => row.name === "idx_threads_agent_updated")).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
21
82
|
describe("warm summary limit migration", () => {
|
|
22
83
|
it("adds the default limit to existing agents", async () => {
|
|
23
84
|
const db = getDb();
|
package/lib/db/migrations.ts
CHANGED
|
@@ -35,6 +35,9 @@ export function runMigrations(db: DatabaseSync): void {
|
|
|
35
35
|
-- (getMessages, getRecentMessagesWindow, getMessagesPage, clear).
|
|
36
36
|
-- Without this every read full-scans the messages table.
|
|
37
37
|
CREATE INDEX IF NOT EXISTS idx_messages_thread_id ON messages(thread_id, created_at);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_messages_thread_seq ON messages(thread_id);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_threads_updated ON threads(updated_at DESC);
|
|
38
41
|
CREATE TABLE IF NOT EXISTS runtime_operations (
|
|
39
42
|
id TEXT PRIMARY KEY,
|
|
40
43
|
kind TEXT NOT NULL,
|
|
@@ -58,6 +61,9 @@ export function runMigrations(db: DatabaseSync): void {
|
|
|
58
61
|
created_at INTEGER NOT NULL,
|
|
59
62
|
UNIQUE(kind,operation_id,agent_id,thread_id)
|
|
60
63
|
);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_runtime_operations_thread_state ON runtime_operations(thread_id,state);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_signal_events_thread ON signal_events(thread_id);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_signal_events_operation ON signal_events(operation_id);
|
|
61
67
|
CREATE TABLE IF NOT EXISTS signal_deliveries (
|
|
62
68
|
event_id TEXT PRIMARY KEY REFERENCES signal_events(id) ON DELETE CASCADE,
|
|
63
69
|
wake_eligible INTEGER NOT NULL DEFAULT 0,
|
|
@@ -72,6 +78,7 @@ export function runMigrations(db: DatabaseSync): void {
|
|
|
72
78
|
);
|
|
73
79
|
CREATE INDEX IF NOT EXISTS idx_signal_events_target ON signal_events(agent_id,thread_id,seq);
|
|
74
80
|
CREATE INDEX IF NOT EXISTS idx_signal_deliveries_due ON signal_deliveries(state,available_at);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_signal_deliveries_lease ON signal_deliveries(state,lease_until);
|
|
75
82
|
CREATE TABLE IF NOT EXISTS background_tool_results (
|
|
76
83
|
key TEXT PRIMARY KEY REFERENCES runtime_operations(id) ON DELETE CASCADE,
|
|
77
84
|
thread_id TEXT NOT NULL REFERENCES threads(thread_id) ON DELETE CASCADE,
|
|
@@ -83,6 +90,7 @@ export function runMigrations(db: DatabaseSync): void {
|
|
|
83
90
|
expires_at INTEGER NOT NULL
|
|
84
91
|
);
|
|
85
92
|
CREATE INDEX IF NOT EXISTS idx_background_results_owner ON background_tool_results(thread_id,expires_at);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_background_results_expiry ON background_tool_results(expires_at);
|
|
86
94
|
CREATE TABLE IF NOT EXISTS memory_store (
|
|
87
95
|
namespace TEXT NOT NULL,
|
|
88
96
|
key TEXT NOT NULL,
|
|
@@ -91,6 +99,8 @@ export function runMigrations(db: DatabaseSync): void {
|
|
|
91
99
|
updated_at TEXT NOT NULL,
|
|
92
100
|
PRIMARY KEY (namespace, key)
|
|
93
101
|
);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_memory_namespace_updated ON memory_store(namespace,updated_at DESC);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_memory_updated ON memory_store(updated_at DESC);
|
|
94
104
|
CREATE TABLE IF NOT EXISTS model_configs (
|
|
95
105
|
name TEXT PRIMARY KEY,
|
|
96
106
|
provider TEXT NOT NULL,
|
|
@@ -660,6 +670,7 @@ function ensureMessageUsageTable(db: DatabaseSync): void {
|
|
|
660
670
|
);
|
|
661
671
|
CREATE INDEX IF NOT EXISTS idx_message_usage_created_at ON message_usage(created_at);
|
|
662
672
|
CREATE INDEX IF NOT EXISTS idx_message_usage_agent_id ON message_usage(agent_id);
|
|
673
|
+
CREATE INDEX IF NOT EXISTS idx_message_usage_thread_created ON message_usage(thread_id,created_at DESC);
|
|
663
674
|
`);
|
|
664
675
|
}
|
|
665
676
|
|
|
@@ -703,6 +714,8 @@ function ensureMessageEmbeddingJobsTable(db: DatabaseSync): void {
|
|
|
703
714
|
);
|
|
704
715
|
CREATE INDEX IF NOT EXISTS idx_message_embedding_jobs_due
|
|
705
716
|
ON message_embedding_jobs(state, available_at_ms, lease_until_ms);
|
|
717
|
+
CREATE INDEX IF NOT EXISTS idx_message_embedding_jobs_lease
|
|
718
|
+
ON message_embedding_jobs(state, lease_until_ms);
|
|
706
719
|
INSERT OR IGNORE INTO message_embedding_jobs (message_id, created_at, updated_at)
|
|
707
720
|
SELECT msg_id, created_at, created_at FROM messages WHERE embedding IS NULL;
|
|
708
721
|
`);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { afterAll, expect, it, vi } from "vitest";
|
|
4
|
+
import { LOCAL_EMBEDDING_MODEL_ID } from "./constants";
|
|
5
|
+
|
|
6
|
+
// Opt-in, local-only benchmark. No user database, credentials, or downloads.
|
|
7
|
+
const state = vi.hoisted(() => ({ db: null as DatabaseSync | null }));
|
|
8
|
+
vi.mock("@/lib/db", () => ({
|
|
9
|
+
getDb: () => {
|
|
10
|
+
if (!state.db) throw new Error("benchmark database not initialized");
|
|
11
|
+
return state.db;
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("@/lib/providers", () => ({
|
|
15
|
+
getProvider: () => { throw new Error("remote providers are forbidden in this benchmark"); },
|
|
16
|
+
}));
|
|
17
|
+
vi.mock("@/lib/stores/model-config", () => ({
|
|
18
|
+
getModelConfig: () => null,
|
|
19
|
+
getDefaultModelConfig: () => null,
|
|
20
|
+
getModelParams: () => ({}),
|
|
21
|
+
listModelConfigs: () => [],
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const previousDownload = process.env.JARELA_LOCAL_EMBEDDING_DOWNLOAD;
|
|
25
|
+
afterAll(() => {
|
|
26
|
+
state.db?.close();
|
|
27
|
+
if (previousDownload === undefined) delete process.env.JARELA_LOCAL_EMBEDDING_DOWNLOAD;
|
|
28
|
+
else process.env.JARELA_LOCAL_EMBEDDING_DOWNLOAD = previousDownload;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.skipIf(process.env.JARELA_BENCHMARK_EMBEDDINGS !== "1")(
|
|
32
|
+
"reports local embedding and synthetic memory-search latency",
|
|
33
|
+
async () => {
|
|
34
|
+
process.env.JARELA_LOCAL_EMBEDDING_DOWNLOAD = "0";
|
|
35
|
+
const { getLocalModelStatus } = await import("./local-model");
|
|
36
|
+
expect(getLocalModelStatus().state, "Install the local model before running this offline benchmark").toBe("ready");
|
|
37
|
+
const { runMigrations } = await import("@/lib/db/migrations");
|
|
38
|
+
state.db = new DatabaseSync(":memory:");
|
|
39
|
+
runMigrations(state.db);
|
|
40
|
+
const { setLocalEmbeddingsEnabled, setEmbeddingVectorsSignature } = await import("@/lib/stores/app-settings");
|
|
41
|
+
setLocalEmbeddingsEnabled(true);
|
|
42
|
+
setEmbeddingVectorsSignature(LOCAL_EMBEDDING_MODEL_ID);
|
|
43
|
+
const { embedLocally } = await import("./local");
|
|
44
|
+
const { embedQueryOne, searchMemory, _resetEmbeddingCache, _resetEmbedCaches, reembedStaleVectors } = await import("./index");
|
|
45
|
+
const results: Array<{ operation: string; samples: number; median_ms: number; p95_ms: number }> = [];
|
|
46
|
+
async function time(operation: string, count: number, run: (index: number) => Promise<void>) {
|
|
47
|
+
const elapsed: number[] = [];
|
|
48
|
+
for (let i = 0; i < count; i++) {
|
|
49
|
+
const start = performance.now();
|
|
50
|
+
await run(i);
|
|
51
|
+
elapsed.push(performance.now() - start);
|
|
52
|
+
}
|
|
53
|
+
elapsed.sort((a, b) => a - b);
|
|
54
|
+
results.push({
|
|
55
|
+
operation, samples: count,
|
|
56
|
+
median_ms: Number(elapsed[Math.floor(elapsed.length / 2)].toFixed(3)),
|
|
57
|
+
p95_ms: Number(elapsed[Math.ceil(elapsed.length * 0.95) - 1].toFixed(3)),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const query = "What is the deployment rollback procedure?";
|
|
61
|
+
const validate = (vector: number[] | null) => {
|
|
62
|
+
expect(vector).toHaveLength(384);
|
|
63
|
+
expect(vector?.every(Number.isFinite)).toBe(true);
|
|
64
|
+
};
|
|
65
|
+
await time("cold pipeline + first query inference", 1, async () => {
|
|
66
|
+
validate((await embedLocally([query], "query"))[0]);
|
|
67
|
+
});
|
|
68
|
+
await time("warm pipeline, uncached short query", 10, async (i) => {
|
|
69
|
+
validate((await embedLocally([`${query} revision ${i}`], "query"))[0]);
|
|
70
|
+
});
|
|
71
|
+
await time("warm pipeline, 16 short passages", 3, async () => {
|
|
72
|
+
const vectors = await embedLocally(Array.from({ length: 16 }, (_, i) => `Deployment step ${i}: preserve backups and verify health.`), "passage");
|
|
73
|
+
expect(vectors).toHaveLength(16);
|
|
74
|
+
vectors.forEach(validate);
|
|
75
|
+
});
|
|
76
|
+
await embedQueryOne(query);
|
|
77
|
+
await time("query embedding content-cache hit", 20, async () => { validate(await embedQueryOne(query)); });
|
|
78
|
+
const vector = await embedQueryOne(query);
|
|
79
|
+
const encoded = JSON.stringify(vector);
|
|
80
|
+
const db = state.db;
|
|
81
|
+
db.prepare("INSERT INTO threads(thread_id,agent_id,created_at,updated_at) VALUES ('bench','bench',?,?)")
|
|
82
|
+
.run("2026-01-01", "2026-01-01");
|
|
83
|
+
const memory = db.prepare("INSERT INTO memory_store(namespace,key,value,created_at,updated_at,embedding) VALUES ('benchmark',?,?,?,?,?)");
|
|
84
|
+
const message = db.prepare("INSERT INTO messages(msg_id,thread_id,role,content,created_at,embedding) VALUES (?,'bench','assistant',?,?,?)");
|
|
85
|
+
const job = db.prepare("INSERT INTO message_embedding_jobs(message_id,source_hash,state,created_at,updated_at,model_signature) VALUES (?,?,'done','2026-01-01','2026-01-01',?)");
|
|
86
|
+
for (const rows of [1_000, 10_000]) {
|
|
87
|
+
db.exec("DELETE FROM memory_store WHERE namespace='benchmark'; DELETE FROM message_embedding_jobs; DELETE FROM messages; BEGIN");
|
|
88
|
+
for (let i = 0; i < rows / 2; i++) {
|
|
89
|
+
const content = `Synthetic deployment rollback procedure ${i}: restore backup, verify health, then resume traffic.`;
|
|
90
|
+
memory.run(`key-${i}`, content, "2026-01-01", "2026-01-01", encoded);
|
|
91
|
+
message.run(`msg-${i}`, content, "2026-01-01", encoded);
|
|
92
|
+
job.run(`msg-${i}`, "synthetic", LOCAL_EMBEDDING_MODEL_ID);
|
|
93
|
+
}
|
|
94
|
+
db.exec("COMMIT");
|
|
95
|
+
_resetEmbedCaches();
|
|
96
|
+
await time(`${rows} vectors: first search, cold row cache`, 1, async () => {
|
|
97
|
+
const hits = await searchMemory(query, { literal: true, limit: 10 });
|
|
98
|
+
expect(hits).toHaveLength(10);
|
|
99
|
+
});
|
|
100
|
+
await reembedStaleVectors(384);
|
|
101
|
+
await time(`${rows} vectors: warm rows + cached query`, 10, async () => {
|
|
102
|
+
expect(await searchMemory(query, { literal: true, limit: 10 })).toHaveLength(10);
|
|
103
|
+
});
|
|
104
|
+
await time(`${rows} vectors: warm rows + new query inference`, 5, async (i) => {
|
|
105
|
+
_resetEmbeddingCache();
|
|
106
|
+
expect(await searchMemory(`${query} check ${i}`, { literal: true, limit: 10 })).toHaveLength(10);
|
|
107
|
+
});
|
|
108
|
+
await reembedStaleVectors(384);
|
|
109
|
+
}
|
|
110
|
+
console.table(results);
|
|
111
|
+
console.log("Synthetic repeated 384-dimensional vectors; measures scan/sort cost, not retrieval quality. No HTTP transport or provider network latency.");
|
|
112
|
+
},
|
|
113
|
+
120_000,
|
|
114
|
+
);
|
|
@@ -324,3 +324,73 @@ describe("pricing snapshot refresh", () => {
|
|
|
324
324
|
expect(modelRate?.output_per_1m_usd).toBe(0.28);
|
|
325
325
|
});
|
|
326
326
|
});
|
|
327
|
+
|
|
328
|
+
describe("pricing snapshot edge cases", () => {
|
|
329
|
+
beforeEach(() => {
|
|
330
|
+
vi.resetModules();
|
|
331
|
+
readFileMock.mockReset();
|
|
332
|
+
mkdirMock.mockReset();
|
|
333
|
+
writeFileMock.mockReset();
|
|
334
|
+
vi.unstubAllGlobals();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
afterEach(() => {
|
|
338
|
+
vi.unstubAllGlobals();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("refreshes a snapshot stamped in the future instead of trusting it forever", async () => {
|
|
342
|
+
readFileMock.mockResolvedValueOnce(JSON.stringify({
|
|
343
|
+
generated_at: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
|
|
344
|
+
disclaimer: "skewed",
|
|
345
|
+
ttl_days: 3,
|
|
346
|
+
sources: [],
|
|
347
|
+
}));
|
|
348
|
+
vi.stubGlobal("fetch", vi.fn(async () => new Response("input $1 / 1M tokens output $2 / 1M tokens", { status: 200 })));
|
|
349
|
+
|
|
350
|
+
const { refreshPricingSnapshot } = await import("./snapshot");
|
|
351
|
+
const res = await refreshPricingSnapshot({ providers: ["cohere"] });
|
|
352
|
+
|
|
353
|
+
expect(res).toMatchObject({ refreshed: true, reason: "stale" });
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("treats a corrupt snapshot file as missing", async () => {
|
|
357
|
+
readFileMock.mockResolvedValueOnce("{not json");
|
|
358
|
+
vi.stubGlobal("fetch", vi.fn(async () => new Response("input $1 / 1M tokens output $2 / 1M tokens", { status: 200 })));
|
|
359
|
+
|
|
360
|
+
const { refreshPricingSnapshot } = await import("./snapshot");
|
|
361
|
+
const res = await refreshPricingSnapshot({ providers: ["cohere"] });
|
|
362
|
+
|
|
363
|
+
expect(res).toMatchObject({ refreshed: true, reason: "missing" });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("parses each source's search results independently and skips Google's own links", async () => {
|
|
367
|
+
readFileMock.mockRejectedValueOnce(new Error("ENOENT"));
|
|
368
|
+
const link = (u: string) => `<a href="/url?q=${encodeURIComponent(u)}&sa=U">x</a>`;
|
|
369
|
+
const anthropicSearch = "<html>" + "x".repeat(4000)
|
|
370
|
+
+ link("https://www.google.com/intl/en/policies/")
|
|
371
|
+
+ Array.from({ length: 9 }, (_, i) => link(`https://a${i}.example.com/pricing`)).join("")
|
|
372
|
+
+ "</html>";
|
|
373
|
+
const cohereSearch = `<html>${link("https://c.example.com/pricing")}</html>`;
|
|
374
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
|
375
|
+
const url = String(input);
|
|
376
|
+
if (url.startsWith("https://www.google.com/search")) {
|
|
377
|
+
if (url.includes("Anthropic")) return new Response(anthropicSearch, { status: 200 });
|
|
378
|
+
// Only Cohere's first query has results, so a leaked regex cursor cannot be masked by a retry.
|
|
379
|
+
return new Response(url.includes("1M") ? cohereSearch : "<html></html>", { status: 200 });
|
|
380
|
+
}
|
|
381
|
+
if (/^https:\/\/(a0|c)\.example\.com\//.test(url)) {
|
|
382
|
+
return new Response("input $3 / 1M tokens output $15 / 1M tokens", { status: 200 });
|
|
383
|
+
}
|
|
384
|
+
return new Response("not found", { status: 404 });
|
|
385
|
+
});
|
|
386
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
387
|
+
|
|
388
|
+
const { refreshPricingSnapshot } = await import("./snapshot");
|
|
389
|
+
const res = await refreshPricingSnapshot({ force: true, providers: ["anthropic", "cohere"] });
|
|
390
|
+
const byId = new Map(res.snapshot.sources.map((s) => [s.id, s]));
|
|
391
|
+
|
|
392
|
+
expect(byId.get("anthropic")).toMatchObject({ ok: true, resolved_url: "https://a0.example.com/pricing" });
|
|
393
|
+
expect(byId.get("cohere")).toMatchObject({ ok: true, resolved_url: "https://c.example.com/pricing" });
|
|
394
|
+
expect(fetchMock.mock.calls.map(([u]) => String(u))).not.toContain("https://www.google.com/intl/en/policies/");
|
|
395
|
+
});
|
|
396
|
+
});
|
package/lib/pricing/snapshot.ts
CHANGED
|
@@ -85,6 +85,7 @@ const PRICE_LINE_RE = new RegExp(
|
|
|
85
85
|
);
|
|
86
86
|
|
|
87
87
|
const GOOGLE_RESULT_RE = /href="\/url\?q=([^"&]+)[^"]*"/gi;
|
|
88
|
+
const GOOGLE_OWN_HOST_RE = /^(?:www|accounts|support|maps|policies|webcache)?\.?google\.[a-z.]+$/i;
|
|
88
89
|
|
|
89
90
|
type FetchAttempt = {
|
|
90
91
|
url: string;
|
|
@@ -168,7 +169,8 @@ export async function refreshPricingSnapshot(opts?: { force?: boolean; ttlDays?:
|
|
|
168
169
|
const existing = await readPricingSnapshot();
|
|
169
170
|
if (!force && existing) {
|
|
170
171
|
const ageDays = (Date.now() - Date.parse(existing.generated_at)) / (24 * 60 * 60 * 1000);
|
|
171
|
-
|
|
172
|
+
// A future generated_at (clock skew, copied snapshot) must not pin the cache as fresh.
|
|
173
|
+
if (Number.isFinite(ageDays) && ageDays >= 0 && ageDays < ttlDays) {
|
|
172
174
|
return { refreshed: false, reason: "fresh", snapshot: existing };
|
|
173
175
|
}
|
|
174
176
|
}
|
|
@@ -332,6 +334,8 @@ function parseGoogleResultLinks(html: string): string[] {
|
|
|
332
334
|
const seen = new Set<string>();
|
|
333
335
|
|
|
334
336
|
let m: RegExpExecArray | null;
|
|
337
|
+
// Shared /g regex: an early break leaves lastIndex mid-string for the next page.
|
|
338
|
+
GOOGLE_RESULT_RE.lastIndex = 0;
|
|
335
339
|
while ((m = GOOGLE_RESULT_RE.exec(html)) !== null) {
|
|
336
340
|
const raw = m[1] ?? "";
|
|
337
341
|
let url = "";
|
|
@@ -341,7 +345,11 @@ function parseGoogleResultLinks(html: string): string[] {
|
|
|
341
345
|
continue;
|
|
342
346
|
}
|
|
343
347
|
if (!/^https?:\/\//i.test(url)) continue;
|
|
344
|
-
|
|
348
|
+
try {
|
|
349
|
+
if (GOOGLE_OWN_HOST_RE.test(new URL(url).hostname)) continue;
|
|
350
|
+
} catch {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
345
353
|
if (seen.has(url)) continue;
|
|
346
354
|
seen.add(url);
|
|
347
355
|
out.push(url);
|