auramaxx 0.0.17 → 0.0.19
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/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +70 -70
- package/.next/app-path-routes-manifest.json +2 -2
- package/.next/build-manifest.json +5 -5
- package/.next/prerender-manifest.json +30 -30
- package/.next/react-loadable-manifest.json +16 -16
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +5 -5
- package/.next/server/app/api/[...doc]/page.js.nft.json +1 -1
- package/.next/server/app/api/[...doc]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agent-requests/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/apps/install/route.js.nft.json +1 -1
- package/.next/server/app/api/apps/install/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/apps/manifests/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/apps/static/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/docs/plain/route.js.nft.json +1 -1
- package/.next/server/app/api/docs/plain/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/events/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/import-from-openclaw/[channel]/route.js.nft.json +1 -1
- package/.next/server/app/api/import-from-openclaw/[channel]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/import-from-openclaw/route.js.nft.json +1 -1
- package/.next/server/app/api/import-from-openclaw/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/import-from-openclaw/validate/[channel]/route.js.nft.json +1 -1
- package/.next/server/app/api/import-from-openclaw/validate/[channel]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/page.js.nft.json +1 -1
- package/.next/server/app/api/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/version/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/workspace/[id]/apps/[wid]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/workspace/[id]/apps/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/workspace/[id]/export/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/workspace/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/workspace/config/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/workspace/import/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/workspace/route_client-reference-manifest.js +1 -1
- package/.next/server/app/app-legacy-do-not-use/page.js +3 -3
- package/.next/server/app/app-legacy-do-not-use/page.js.nft.json +1 -1
- package/.next/server/app/app-legacy-do-not-use/page_client-reference-manifest.js +1 -1
- package/.next/server/app/app-legacy-do-not-use.html +1 -1
- package/.next/server/app/app-legacy-do-not-use.rsc +6 -6
- package/.next/server/app/approve/[actionId]/page.js.nft.json +1 -1
- package/.next/server/app/approve/[actionId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/docs/[...doc]/page.js.nft.json +1 -1
- package/.next/server/app/docs/[...doc]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/docs/page.js.nft.json +1 -1
- package/.next/server/app/docs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/health/page.js.nft.json +1 -1
- package/.next/server/app/health/page_client-reference-manifest.js +1 -1
- package/.next/server/app/health.html +1 -1
- package/.next/server/app/health.rsc +5 -5
- package/.next/server/app/hello/page.js.nft.json +1 -1
- package/.next/server/app/hello/page_client-reference-manifest.js +1 -1
- package/.next/server/app/hello.html +1 -1
- package/.next/server/app/hello.rsc +5 -5
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +6 -6
- package/.next/server/app/page.js +9 -3
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/privacy/page.js.nft.json +1 -1
- package/.next/server/app/privacy/page_client-reference-manifest.js +1 -1
- package/.next/server/app/privacy.html +1 -1
- package/.next/server/app/privacy.rsc +5 -5
- package/.next/server/app/share/[token]/page.js.nft.json +1 -1
- package/.next/server/app/share/[token]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/terms/page.js.nft.json +1 -1
- package/.next/server/app/terms/page_client-reference-manifest.js +1 -1
- package/.next/server/app/terms.html +1 -1
- package/.next/server/app/terms.rsc +5 -5
- package/.next/server/app/yo/page.js +1 -1
- package/.next/server/app/yo/page.js.nft.json +1 -1
- package/.next/server/app/yo/page_client-reference-manifest.js +1 -1
- package/.next/server/app/yo.html +1 -1
- package/.next/server/app/yo.rsc +6 -6
- package/.next/server/app-paths-manifest.json +2 -2
- package/.next/server/chunks/1196.js +1 -1
- package/.next/server/chunks/{6415.js → 2123.js} +2 -2
- package/.next/server/chunks/2234.js +1 -1
- package/.next/server/chunks/4224.js +1 -0
- package/.next/server/chunks/4600.js +1 -1
- package/.next/server/chunks/5678.js +1 -1
- package/.next/server/chunks/5991.js +7 -0
- package/.next/server/chunks/6470.js +1 -1
- package/.next/server/chunks/7585.js +1 -1
- package/.next/server/chunks/7935.js +19 -11
- package/.next/server/chunks/8468.js +1 -1
- package/.next/server/chunks/{1350.js → 9323.js} +3 -3
- package/.next/server/chunks/9971.js +1 -1
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{1168.aaac1edbb597fe5a.js → 1168.bb1c93965872e980.js} +1 -1
- package/.next/static/chunks/2891-bf010357841c9ce9.js +1 -0
- package/.next/static/chunks/{2927.7e00cc878d9a3f52.js → 2927.3976471e61df966e.js} +1 -1
- package/.next/static/chunks/{3573-1b41d7b8a000d015.js → 3573-667bc4c8f56afcff.js} +1 -1
- package/.next/static/chunks/{3a91511d-ba215c0b5dc21ba9.js → 3a91511d-da2c74cad6c6fc33.js} +2 -2
- package/.next/static/chunks/4114-a375178fd94c4d8a.js +1 -0
- package/.next/static/chunks/{4256.48407d9abad5ea33.js → 4256.aa831e4d8c3f65bc.js} +1 -1
- package/.next/static/chunks/{4919-7e7cdd5efc9f2110.js → 4919-0e698cd95504e355.js} +1 -1
- package/.next/static/chunks/{5336-233ec7ab3807267c.js → 5336-b775f449d7433604.js} +1 -1
- package/.next/static/chunks/{7338-3ce17a93614f1d77.js → 5362-2e8fcdd0bc6273ca.js} +4 -4
- package/.next/static/chunks/67-cb9bbb430499e803.js +1 -0
- package/.next/static/chunks/{8273-8e92d34180669ca9.js → 8273-3b9d882c5dd6a9a8.js} +1 -1
- package/.next/static/chunks/8291-8f6fb6554e692552.js +1 -0
- package/.next/static/chunks/8370-6537b6b27df58b23.js +1 -0
- package/.next/static/chunks/app/app-legacy-do-not-use/page-efa348da049c5873.js +1 -0
- package/.next/static/chunks/app/{error-63cbe72522239cff.js → error-04a18fd49a89ee17.js} +1 -1
- package/.next/static/chunks/app/layout-7093dda0d106f34d.js +1 -0
- package/.next/static/chunks/app/page-0ec7cf48550f71e6.js +1 -0
- package/.next/static/chunks/app/yo/{page-cf5d6540ea31c31c.js → page-48930746b135037e.js} +1 -1
- package/.next/static/chunks/{webpack-79ad58260e9b10b4.js → webpack-15ec0114c9d658c2.js} +1 -1
- package/.next/static/css/35991998d1b5eff0.css +3 -0
- package/.next/trace +28 -28
- package/README.md +2 -2
- package/bin/auramaxx.js +87 -3
- package/package.json +8 -4
- package/prisma/migrations/20260301225214_social_tables/migration.sql +81 -0
- package/prisma/migrations/20260302120000_add_notification_hash/migration.sql +8 -0
- package/prisma/migrations/20260302160000_add_public_key_hex/migration.sql +5 -0
- package/prisma/migrations/20260302170000_add_inbound_author_public_key/migration.sql +5 -0
- package/prisma/schema.prisma +272 -177
- package/shared/credential-field-schema.ts +10 -0
- package/skills/auramaxx/SKILL.md +33 -4
- package/skills/auramaxx/docs/AGENT_SETUP.md +7 -2
- package/src/app/UnlockPageClient.tsx +448 -43
- package/src/app/app-legacy-do-not-use/page.tsx +5 -3
- package/src/app/globals.css +1 -1
- package/src/components/AgentPicker.tsx +278 -0
- package/src/components/CreateAgentModal.tsx +192 -0
- package/src/components/NotificationDrawer.tsx +15 -13
- package/src/components/agent/AgentSidebar.tsx +2 -19
- package/src/components/agent/CredentialAgent.tsx +70 -37
- package/src/components/agent/CredentialForm.tsx +32 -7
- package/src/components/agent/CredentialList.tsx +149 -72
- package/src/components/agents/AgentsView.tsx +1035 -0
- package/src/components/design-system/TextAreaInput.tsx +78 -0
- package/src/components/design-system/index.ts +1 -0
- package/src/components/hub/HubView.tsx +58 -0
- package/src/components/layout/LeftRail.tsx +333 -40
- package/src/components/layout/ViewShell.tsx +125 -0
- package/src/components/layout/index.ts +1 -0
- package/src/components/social/PrimaryHubRegistrationGate.tsx +260 -0
- package/src/components/social/SocialView.tsx +904 -0
- package/src/hooks/useSocialNotifications.ts +123 -0
- package/src/lib/events.ts +10 -1
- package/src/lib/social-client.ts +319 -0
- package/src/lib/view-registry.ts +22 -4
- package/src/server/cli/commands/agent.ts +33 -7
- package/src/server/cli/commands/create.ts +209 -0
- package/src/server/cli/commands/onboard.ts +39 -0
- package/src/server/cli/commands/play.ts +68 -0
- package/src/server/cli/commands/register-agent.ts +181 -0
- package/src/server/cli/commands/social.ts +648 -0
- package/src/server/cli/commands/start.ts +4 -3
- package/src/server/cron/index.ts +5 -9
- package/src/server/cron/jobs/social-inbound.ts +596 -0
- package/src/server/cron/jobs/social-outbound.ts +70 -0
- package/src/server/index.ts +14 -0
- package/src/server/lib/agent-profiles.ts +10 -1
- package/src/server/lib/cold.ts +0 -45
- package/src/server/lib/credential-import.ts +53 -47
- package/src/server/lib/defaults.ts +11 -0
- package/src/server/lib/feature-flags.ts +3 -0
- package/src/server/lib/hub-auth.ts +159 -0
- package/src/server/lib/migrate-agent-profiles.ts +96 -0
- package/src/server/lib/notifications.ts +12 -1
- package/src/server/lib/permissions.ts +10 -0
- package/src/server/lib/project-scope.ts +33 -5
- package/src/server/lib/social/create.ts +176 -0
- package/src/server/lib/social/global-aura-id.ts +115 -0
- package/src/server/lib/social/hub-rpc-client.ts +99 -0
- package/src/server/lib/social/hub-session.ts +6 -0
- package/src/server/lib/social/public-key.ts +32 -0
- package/src/server/lib/social/register.ts +162 -0
- package/src/server/lib/social/sign.ts +95 -0
- package/src/server/lib/social/sync.ts +301 -0
- package/src/server/mcp/server.ts +142 -18
- package/src/server/mcp/tools.ts +552 -0
- package/src/server/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/src/server/routes/agent-hub.ts +276 -0
- package/src/server/routes/agent-profiles.ts +145 -19
- package/src/server/routes/credentials.ts +49 -15
- package/src/server/routes/heartbeat.ts +3 -7
- package/src/server/routes/passkey.ts +7 -1
- package/src/server/routes/social.ts +869 -0
- package/src/server/routes/unlock.ts +17 -1
- package/src/server/routes/verified-credentials.ts +312 -0
- package/src/server/test-data/template.db +0 -0
- package/src/server/test-data/worker-1/agent-primary.json +12 -0
- package/src/server/test-data/worker-1/auramaxx.db +0 -0
- package/src/server/test-data/worker-1/backups/auramaxx.db.20260305_011404.bak +0 -0
- package/src/server/test-data/worker-1/backups/credentials.20260305_011404.cred-00ntf5h3.json +24 -0
- package/src/server/test-data/worker-1/backups/credentials.20260305_011404.cred-dh8fa9k3.json +24 -0
- package/src/server/test-data/worker-1/backups/credentials.20260305_011404.cred-prfhlyow.json +25 -0
- package/src/server/test-data/worker-1/backups/credentials.20260305_011404.cred-w26jd6zi.json +22 -0
- package/src/server/test-data/worker-1/config.json +33 -0
- package/src/server/test-data/worker-1/test.db +0 -0
- package/src/server/test-data/worker-10/agent-primary.json +12 -0
- package/src/server/test-data/worker-10/config.json +33 -0
- package/src/server/test-data/worker-10/test.db +0 -0
- package/src/server/test-data/worker-11/agent-primary.json +12 -0
- package/src/server/test-data/worker-11/config.json +33 -0
- package/src/server/test-data/worker-11/test.db +0 -0
- package/src/server/test-data/worker-12/agent-primary.json +12 -0
- package/src/server/test-data/worker-12/config.json +33 -0
- package/src/server/test-data/worker-12/credentials/cred-hboagned.json +25 -0
- package/src/server/test-data/worker-12/credentials/cred-nt14xd9t.json +24 -0
- package/src/server/test-data/worker-12/credentials/cred-qn59xefc.json +24 -0
- package/src/server/test-data/worker-12/test.db +0 -0
- package/src/server/test-data/worker-13/agent-primary.json +12 -0
- package/src/server/test-data/worker-13/approval-context.json +56 -0
- package/src/server/test-data/worker-13/approval-context.key +1 -0
- package/src/server/test-data/worker-13/config.json +33 -0
- package/src/server/test-data/worker-13/credentials-recently-deleted/cred-jn9yqy2m.json +20 -0
- package/src/server/test-data/worker-13/test.db +0 -0
- package/src/server/test-data/worker-14/agent-primary.json +12 -0
- package/src/server/test-data/worker-14/config.json +33 -0
- package/src/server/test-data/worker-14/test.db +0 -0
- package/src/server/test-data/worker-15/agent-primary.json +12 -0
- package/src/server/test-data/worker-15/config.json +33 -0
- package/src/server/test-data/worker-15/test.db +0 -0
- package/src/server/test-data/worker-15/update-check-4b305e76.json +5 -0
- package/src/server/test-data/worker-2/agent-primary.json +12 -0
- package/src/server/test-data/worker-2/config.json +33 -0
- package/src/server/test-data/worker-2/test.db +0 -0
- package/src/server/test-data/worker-3/agent-primary.json +12 -0
- package/src/server/test-data/worker-3/config.json +33 -0
- package/src/server/test-data/worker-3/test.db +0 -0
- package/src/server/test-data/worker-4/agent-primary.json +12 -0
- package/src/server/test-data/worker-4/config.json +33 -0
- package/src/server/test-data/worker-4/credentials/cred-bm6p74u5.json +24 -0
- package/src/server/test-data/worker-4/credentials/cred-r8eqb07i.json +25 -0
- package/src/server/test-data/worker-4/credentials/cred-ziv49y9h.json +24 -0
- package/src/server/test-data/worker-4/test.db +0 -0
- package/src/server/test-data/worker-5/agent-primary.json +12 -0
- package/src/server/test-data/worker-5/config.json +33 -0
- package/src/server/test-data/worker-5/credentials/cred-15gyfeht.json +25 -0
- package/src/server/test-data/worker-5/credentials/cred-lp0ocnfc.json +24 -0
- package/src/server/test-data/worker-5/credentials/cred-zfbwfxw0.json +24 -0
- package/src/server/test-data/worker-5/test.db +0 -0
- package/src/server/test-data/worker-6/agent-primary.json +12 -0
- package/src/server/test-data/worker-6/config.json +33 -0
- package/src/server/test-data/worker-6/credentials/cred-dx8li3ue.json +24 -0
- package/src/server/test-data/worker-6/credentials/cred-myeskqky.json +24 -0
- package/src/server/test-data/worker-6/credentials/cred-tam01qdt.json +25 -0
- package/src/server/test-data/worker-6/credentials/cred-wdh6rslw.json +22 -0
- package/src/server/test-data/worker-6/credentials-archive/cred-4wg53v1y.json +25 -0
- package/src/server/test-data/worker-6/test.db +0 -0
- package/src/server/test-data/worker-7/agent-primary.json +12 -0
- package/src/server/test-data/worker-7/config.json +33 -0
- package/src/server/test-data/worker-7/credentials/cred-ji7ec57d.json +25 -0
- package/src/server/test-data/worker-7/credentials/cred-mmgj0ypo.json +24 -0
- package/src/server/test-data/worker-7/credentials/cred-nb0ic93x.json +24 -0
- package/src/server/test-data/worker-7/test.db +0 -0
- package/src/server/test-data/worker-8/agent-primary.json +12 -0
- package/src/server/test-data/worker-8/config.json +33 -0
- package/src/server/test-data/worker-8/test.db +0 -0
- package/src/server/test-data/worker-9/agent-primary.json +12 -0
- package/src/server/test-data/worker-9/test.db +0 -0
- package/src/server/tests/cli/bin-entrypoint.test.ts +99 -0
- package/src/server/tests/cli/social.test.ts +83 -0
- package/src/server/tests/cron/social-inbound-followers.unit.test.ts +164 -0
- package/src/server/tests/cron/social-inbound-staleness.test.ts +193 -0
- package/src/server/tests/cron/social-outbound.test.ts +252 -0
- package/src/server/tests/endpoints/agent.test.ts +0 -16
- package/src/server/tests/lib/credential-field-schema.test.ts +448 -0
- package/src/server/tests/lib/project-scope.test.ts +49 -0
- package/src/server/tests/lib/social/create.test.ts +207 -0
- package/src/server/tests/lib/social/sign.test.ts +163 -0
- package/src/server/tests/lib/social/sync.test.ts +198 -0
- package/src/server/tests/mcp/server.test.ts +1 -3
- package/src/server/tests/mcp/tools.test.ts +219 -7
- package/src/server/tsconfig.tsbuildinfo +1 -1
- package/tsconfig.json +1 -0
- package/.next/server/chunks/2460.js +0 -7
- package/.next/server/chunks/5246.js +0 -1
- package/.next/static/chunks/3667-1db7bd03948e60df.js +0 -1
- package/.next/static/chunks/5442-b5bb869e832e8967.js +0 -1
- package/.next/static/chunks/6872-a8f1ad5fc35a1c35.js +0 -1
- package/.next/static/chunks/7394-4bdb1feefad1a74a.js +0 -1
- package/.next/static/chunks/7587-8542eae6e5139d27.js +0 -1
- package/.next/static/chunks/app/app-legacy-do-not-use/page-569fed5d1a2bfe79.js +0 -1
- package/.next/static/chunks/app/layout-af8d9969c7aeb758.js +0 -1
- package/.next/static/chunks/app/page-9bd0ec3ba4aa6cea.js +0 -1
- package/.next/static/css/88f92e1578b1e5d9.css +0 -3
- package/shared/agent-profile-schema.ts +0 -81
- package/src/internal/security/frontend-api-authorization-audit.md +0 -206
- package/src/server/lib/agent-profile-records.ts +0 -72
- /package/.next/static/{QPzVnl7GToqgYBq14V_iC → yEruVJH_cuht5hu5_1R00}/_buildManifest.js +0 -0
- /package/.next/static/{QPzVnl7GToqgYBq14V_iC → yEruVJH_cuht5hu5_1R00}/_ssgManifest.js +0 -0
package/src/server/cron/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cron Server Entry Point
|
|
3
|
-
* Runs background jobs:
|
|
3
|
+
* Runs background jobs: native price sync, social sync.
|
|
4
4
|
* Connects to the same DB as the wallet server.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -9,11 +9,9 @@ import pino from 'pino';
|
|
|
9
9
|
import { getDbUrl } from '../lib/config';
|
|
10
10
|
import { preloadCache, getDefaultSync } from '../lib/defaults';
|
|
11
11
|
import { Scheduler } from './scheduler';
|
|
12
|
-
import { balanceSyncJob } from './jobs/balance-sync';
|
|
13
12
|
import { nativePriceJob } from './jobs/native-price';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { strategyRunnerJob } from './jobs/strategy-runner';
|
|
13
|
+
import { socialInboundJob } from './jobs/social-inbound';
|
|
14
|
+
import { socialOutboundJob } from './jobs/social-outbound';
|
|
17
15
|
import type { CronContext } from './job';
|
|
18
16
|
|
|
19
17
|
// Set DATABASE_URL before creating PrismaClient
|
|
@@ -108,10 +106,8 @@ async function main(): Promise<void> {
|
|
|
108
106
|
// Build scheduler
|
|
109
107
|
const scheduler = new Scheduler();
|
|
110
108
|
scheduler.register(nativePriceJob);
|
|
111
|
-
scheduler.register(
|
|
112
|
-
scheduler.register(
|
|
113
|
-
scheduler.register(orphanCleanupJob);
|
|
114
|
-
scheduler.register(strategyRunnerJob);
|
|
109
|
+
scheduler.register(socialInboundJob);
|
|
110
|
+
scheduler.register(socialOutboundJob);
|
|
115
111
|
|
|
116
112
|
// Start all jobs
|
|
117
113
|
await scheduler.startAll(ctx);
|
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social Inbound Sync Job
|
|
3
|
+
* =======================
|
|
4
|
+
* Pulls messages from the hub into local InboundMessage cache.
|
|
5
|
+
* Two modes:
|
|
6
|
+
* - snapshot: cold start or stale cursor (> 1 hour behind)
|
|
7
|
+
* - incremental: normal operation, pulls from global event log
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CronJob, CronContext } from '../job';
|
|
11
|
+
import { isEnabled } from '../../lib/feature-flags';
|
|
12
|
+
import { getHubUrl } from '../../lib/defaults';
|
|
13
|
+
import { createNotification } from '../../lib/notifications';
|
|
14
|
+
import { getAgentMnemonic } from '../../lib/cold';
|
|
15
|
+
import { blake3 } from '@noble/hashes/blake3.js';
|
|
16
|
+
import { tryCallHubWithSessionAuth } from '../../lib/hub-auth';
|
|
17
|
+
import { normalizeHubPublicKey } from '../../lib/social/public-key';
|
|
18
|
+
const EVENT_BATCH_LIMIT = 500;
|
|
19
|
+
const STALENESS_THRESHOLD = 10_000; // If agent is this many events behind, fall back to snapshot
|
|
20
|
+
const RELEVANT_STREAMS = ['feed', 'followers', 'notifications'] as const;
|
|
21
|
+
|
|
22
|
+
// ─── Hub response types ──────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
interface HubEvent {
|
|
25
|
+
seq: number;
|
|
26
|
+
hash: string;
|
|
27
|
+
authorAuraId: number;
|
|
28
|
+
authorPublicKey: string;
|
|
29
|
+
type: string;
|
|
30
|
+
body: string; // JSON-encoded
|
|
31
|
+
timestamp: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface EventsResponse {
|
|
35
|
+
events: HubEvent[];
|
|
36
|
+
latestSeq: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SnapshotMessage {
|
|
40
|
+
hash: string;
|
|
41
|
+
authorAuraId: number;
|
|
42
|
+
authorPublicKey?: string;
|
|
43
|
+
type: string;
|
|
44
|
+
body: string;
|
|
45
|
+
timestamp: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface SnapshotFollowEntry {
|
|
49
|
+
auraId: number;
|
|
50
|
+
publicKey: string;
|
|
51
|
+
timestamp: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface SnapshotResponse {
|
|
55
|
+
followers: SnapshotFollowEntry[];
|
|
56
|
+
following: SnapshotFollowEntry[];
|
|
57
|
+
feed: SnapshotMessage[];
|
|
58
|
+
notifications: SnapshotMessage[];
|
|
59
|
+
latestSeq: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface SnapshotCursorResponse {
|
|
63
|
+
since: number;
|
|
64
|
+
seq: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/** Check if an authorPublicKey is someone this agent follows. */
|
|
70
|
+
async function isFollowing(
|
|
71
|
+
agentId: string,
|
|
72
|
+
authorPublicKey: string,
|
|
73
|
+
ctx: CronContext,
|
|
74
|
+
): Promise<boolean> {
|
|
75
|
+
if (!authorPublicKey) return false;
|
|
76
|
+
|
|
77
|
+
// Fast path: check inbound cache (populated by snapshot sync)
|
|
78
|
+
const inbound = await ctx.prisma.inboundMessage.findFirst({
|
|
79
|
+
where: {
|
|
80
|
+
agentId,
|
|
81
|
+
type: 'link_add',
|
|
82
|
+
body: { contains: `"followeePublicKey":"${authorPublicKey}"` },
|
|
83
|
+
},
|
|
84
|
+
select: { id: true },
|
|
85
|
+
});
|
|
86
|
+
if (inbound) return true;
|
|
87
|
+
|
|
88
|
+
// Fallback: check our own outbound follows in SocialMessage
|
|
89
|
+
const outboundFollow = await ctx.prisma.socialMessage.findFirst({
|
|
90
|
+
where: {
|
|
91
|
+
agentId,
|
|
92
|
+
type: 'link_add',
|
|
93
|
+
body: { contains: `"followeePublicKey":"${authorPublicKey}"` },
|
|
94
|
+
},
|
|
95
|
+
orderBy: { createdAt: 'desc' },
|
|
96
|
+
select: { createdAt: true },
|
|
97
|
+
});
|
|
98
|
+
if (!outboundFollow) return false;
|
|
99
|
+
|
|
100
|
+
// Ensure no subsequent unfollow
|
|
101
|
+
const outboundUnfollow = await ctx.prisma.socialMessage.findFirst({
|
|
102
|
+
where: {
|
|
103
|
+
agentId,
|
|
104
|
+
type: 'link_remove',
|
|
105
|
+
body: { contains: `"followeePublicKey":"${authorPublicKey}"` },
|
|
106
|
+
},
|
|
107
|
+
orderBy: { createdAt: 'desc' },
|
|
108
|
+
select: { createdAt: true },
|
|
109
|
+
});
|
|
110
|
+
if (outboundUnfollow && outboundUnfollow.createdAt > outboundFollow.createdAt) return false;
|
|
111
|
+
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Check if a message references one of this agent's outbound post hashes. */
|
|
116
|
+
async function referencesMyPost(
|
|
117
|
+
agentId: string,
|
|
118
|
+
body: string,
|
|
119
|
+
ctx: CronContext,
|
|
120
|
+
): Promise<boolean> {
|
|
121
|
+
// Look for postHash or targetPostHash in the body
|
|
122
|
+
let parsed: Record<string, unknown>;
|
|
123
|
+
try {
|
|
124
|
+
parsed = JSON.parse(body);
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const refHash = (parsed.postHash ?? parsed.targetPostHash ?? parsed.parentPostHash) as string | undefined;
|
|
129
|
+
if (!refHash) return false;
|
|
130
|
+
|
|
131
|
+
const myMsg = await ctx.prisma.socialMessage.findFirst({
|
|
132
|
+
where: { agentId, hash: refHash },
|
|
133
|
+
select: { id: true },
|
|
134
|
+
});
|
|
135
|
+
return myMsg !== null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Check if this is a credential_add for me (by publicKey). */
|
|
139
|
+
function isCredentialForMe(body: string, myPublicKeyHex: string): boolean {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(body);
|
|
142
|
+
return parsed.ownerPublicKey === myPublicKeyHex;
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Check if this is a link_add targeting us (by publicKey). */
|
|
149
|
+
function isFollowingMe(body: string, myPublicKeyHex: string): boolean {
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(body);
|
|
152
|
+
return parsed.followeePublicKey === myPublicKeyHex;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Handle remove types: delete matching InboundMessage by target hash. */
|
|
159
|
+
async function handleRemove(
|
|
160
|
+
agentId: string,
|
|
161
|
+
type: string,
|
|
162
|
+
body: string,
|
|
163
|
+
ctx: CronContext,
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
let parsed: Record<string, unknown>;
|
|
166
|
+
try {
|
|
167
|
+
parsed = JSON.parse(body);
|
|
168
|
+
} catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let targetHash: string | undefined;
|
|
173
|
+
let targetType: string | undefined;
|
|
174
|
+
|
|
175
|
+
if (type === 'post_remove') {
|
|
176
|
+
targetHash = parsed.targetPostHash as string;
|
|
177
|
+
targetType = 'post_add';
|
|
178
|
+
} else if (type === 'reaction_remove') {
|
|
179
|
+
targetHash = parsed.postHash as string;
|
|
180
|
+
targetType = 'reaction_add';
|
|
181
|
+
} else if (type === 'link_remove') {
|
|
182
|
+
// For link_remove, find the matching link_add by followeePublicKey in body
|
|
183
|
+
const followeePublicKey = parsed.followeePublicKey as string;
|
|
184
|
+
if (followeePublicKey) {
|
|
185
|
+
await ctx.prisma.inboundMessage.deleteMany({
|
|
186
|
+
where: {
|
|
187
|
+
agentId,
|
|
188
|
+
type: 'link_add',
|
|
189
|
+
body: { contains: `"followeePublicKey":"${followeePublicKey}"` },
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (targetHash && targetType) {
|
|
197
|
+
await ctx.prisma.inboundMessage.deleteMany({
|
|
198
|
+
where: { agentId, type: targetType, hash: targetHash },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Notification generation ─────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
async function createSocialNotification(
|
|
206
|
+
agentId: string,
|
|
207
|
+
publicKeyHex: string,
|
|
208
|
+
event: HubEvent,
|
|
209
|
+
ctx: CronContext,
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
let title: string | undefined;
|
|
212
|
+
let message: string | undefined;
|
|
213
|
+
let socialType: string | undefined;
|
|
214
|
+
|
|
215
|
+
// Reaction on my post
|
|
216
|
+
if (event.type === 'reaction_add' && await referencesMyPost(agentId, event.body, ctx)) {
|
|
217
|
+
socialType = 'reaction';
|
|
218
|
+
const actorLabel = event.authorAuraId ? `#${event.authorAuraId}` : 'Someone';
|
|
219
|
+
title = 'Reaction on your post';
|
|
220
|
+
message = `${actorLabel} reacted to your post`;
|
|
221
|
+
}
|
|
222
|
+
// Reply to my post
|
|
223
|
+
else if (event.type === 'post_add') {
|
|
224
|
+
let parsed: Record<string, unknown> | undefined;
|
|
225
|
+
try { parsed = JSON.parse(event.body); } catch { /* skip */ }
|
|
226
|
+
if (parsed?.parentPostHash && await referencesMyPost(agentId, event.body, ctx)) {
|
|
227
|
+
socialType = 'reply';
|
|
228
|
+
const actorLabel = event.authorAuraId ? `#${event.authorAuraId}` : 'Someone';
|
|
229
|
+
title = 'Reply to your post';
|
|
230
|
+
message = `${actorLabel} replied to your post`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// New follower
|
|
234
|
+
else if (event.type === 'link_add' && isFollowingMe(event.body, publicKeyHex)) {
|
|
235
|
+
socialType = 'follow';
|
|
236
|
+
const actorLabel = event.authorAuraId ? `#${event.authorAuraId}` : 'Someone';
|
|
237
|
+
title = 'New follower';
|
|
238
|
+
message = `${actorLabel} started following you`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!title || !message || !socialType) return;
|
|
242
|
+
|
|
243
|
+
const notification = await createNotification({
|
|
244
|
+
type: 'info',
|
|
245
|
+
category: 'social',
|
|
246
|
+
title,
|
|
247
|
+
message,
|
|
248
|
+
hash: event.hash,
|
|
249
|
+
source: 'system',
|
|
250
|
+
agentId,
|
|
251
|
+
metadata: {
|
|
252
|
+
socialType,
|
|
253
|
+
actorAuraId: event.authorAuraId,
|
|
254
|
+
actorPublicKey: event.authorPublicKey,
|
|
255
|
+
eventHash: event.hash,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (notification) {
|
|
260
|
+
await ctx.emit('notification:created', {
|
|
261
|
+
agentId,
|
|
262
|
+
notificationId: notification.id,
|
|
263
|
+
category: 'social',
|
|
264
|
+
title,
|
|
265
|
+
});
|
|
266
|
+
ctx.log.debug({ agentId, socialType, hash: event.hash }, 'Social notification created');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── Snapshot mode ───────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
/** Generate a deterministic hash for synthetic follow/follower messages. */
|
|
273
|
+
function syntheticHash(prefix: string, agentId: string, publicKey: string): string {
|
|
274
|
+
const input = `${prefix}:${agentId}:${publicKey}`;
|
|
275
|
+
return Buffer.from(blake3(new TextEncoder().encode(input))).toString('hex');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function runSnapshot(
|
|
279
|
+
agentId: string,
|
|
280
|
+
hubUrl: string,
|
|
281
|
+
mnemonic: string,
|
|
282
|
+
subscriptionId: string,
|
|
283
|
+
ctx: CronContext,
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
const cursor = await tryCallHubWithSessionAuth<SnapshotCursorResponse>(
|
|
286
|
+
hubUrl,
|
|
287
|
+
'sync.snapshotCursor',
|
|
288
|
+
{},
|
|
289
|
+
mnemonic,
|
|
290
|
+
{ log: ctx.log },
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Get personalized snapshot by publicKey
|
|
294
|
+
const identity = await ctx.prisma.agentProfile.findUnique({ where: { agentId } });
|
|
295
|
+
const pubKey = identity?.publicKeyHex ? normalizeHubPublicKey(identity.publicKeyHex) : '';
|
|
296
|
+
const snapshot = await tryCallHubWithSessionAuth<SnapshotResponse>(
|
|
297
|
+
hubUrl,
|
|
298
|
+
'sync.snapshot',
|
|
299
|
+
{ publicKey: pubKey },
|
|
300
|
+
mnemonic,
|
|
301
|
+
{ log: ctx.log },
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
if (!snapshot) {
|
|
305
|
+
ctx.log.warn({ agentId, hubUrl }, 'Snapshot fetch failed, will retry');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let upsertCount = 0;
|
|
310
|
+
|
|
311
|
+
const upsert = async (hash: string, authorAuraId: number, authorPublicKey: string, type: string, body: string, timestamp: number) => {
|
|
312
|
+
await ctx.prisma.inboundMessage.upsert({
|
|
313
|
+
where: { hash_hubUrl: { hash, hubUrl } },
|
|
314
|
+
create: { agentId, hash, hubUrl, authorAuraId, authorPublicKey, type, body, timestamp },
|
|
315
|
+
update: {},
|
|
316
|
+
}).catch((err: unknown) => {
|
|
317
|
+
ctx.log.debug({ err, hash }, 'Snapshot upsert failed (likely race)');
|
|
318
|
+
});
|
|
319
|
+
upsertCount++;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// following → synthetic link_add (we follow them)
|
|
323
|
+
for (const entry of snapshot.following ?? []) {
|
|
324
|
+
const hash = syntheticHash('following', agentId, entry.publicKey);
|
|
325
|
+
const body = JSON.stringify({ followeePublicKey: entry.publicKey, followeeAuraId: entry.auraId });
|
|
326
|
+
await upsert(hash, entry.auraId, pubKey, 'link_add', body, entry.timestamp);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// followers → synthetic link_add with followeePublicKey = our key
|
|
330
|
+
for (const entry of snapshot.followers ?? []) {
|
|
331
|
+
const hash = syntheticHash('follower', agentId, entry.publicKey);
|
|
332
|
+
const body = JSON.stringify({ followeePublicKey: pubKey, followerPublicKey: entry.publicKey });
|
|
333
|
+
await upsert(hash, entry.auraId, entry.publicKey, 'link_add', body, entry.timestamp);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// feed → direct upsert
|
|
337
|
+
for (const msg of snapshot.feed ?? []) {
|
|
338
|
+
await upsert(msg.hash, msg.authorAuraId, msg.authorPublicKey ?? '', msg.type, msg.body, msg.timestamp);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// notifications → direct upsert
|
|
342
|
+
for (const msg of snapshot.notifications ?? []) {
|
|
343
|
+
await upsert(msg.hash, msg.authorAuraId, msg.authorPublicKey ?? '', msg.type, msg.body, msg.timestamp);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Set cursor to latest global seq and switch to incremental
|
|
347
|
+
const latestSeq = snapshot.latestSeq ?? cursor?.seq ?? 0;
|
|
348
|
+
await ctx.prisma.hubSubscription.update({
|
|
349
|
+
where: { id: subscriptionId },
|
|
350
|
+
data: {
|
|
351
|
+
inboundSeq: latestSeq,
|
|
352
|
+
inboundMode: 'incremental',
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
ctx.log.info(
|
|
357
|
+
{ agentId, hubUrl, upsertCount, seq: latestSeq },
|
|
358
|
+
'Snapshot sync complete, switching to incremental',
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ─── Incremental mode ────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
/** Pull events once from the hub starting at the lowest agent cursor. */
|
|
365
|
+
async function fetchEventBatch(
|
|
366
|
+
sinceSeq: number,
|
|
367
|
+
hubUrl: string,
|
|
368
|
+
publicKey: string,
|
|
369
|
+
mnemonic: string,
|
|
370
|
+
ctx: CronContext,
|
|
371
|
+
): Promise<{ events: HubEvent[]; latestSeq: number } | null> {
|
|
372
|
+
const params: Record<string, unknown> = {
|
|
373
|
+
sinceSeq,
|
|
374
|
+
limit: EVENT_BATCH_LIMIT,
|
|
375
|
+
publicKey: normalizeHubPublicKey(publicKey),
|
|
376
|
+
streams: RELEVANT_STREAMS,
|
|
377
|
+
};
|
|
378
|
+
return tryCallHubWithSessionAuth<EventsResponse>(
|
|
379
|
+
hubUrl,
|
|
380
|
+
'sync.events.relevant',
|
|
381
|
+
params,
|
|
382
|
+
mnemonic,
|
|
383
|
+
{ log: ctx.log },
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Fan-out: filter a batch of events for a single agent and persist relevant ones. */
|
|
388
|
+
async function processEventsForAgent(
|
|
389
|
+
agentId: string,
|
|
390
|
+
publicKeyHex: string,
|
|
391
|
+
events: HubEvent[],
|
|
392
|
+
hubUrl: string,
|
|
393
|
+
ctx: CronContext,
|
|
394
|
+
): Promise<number> {
|
|
395
|
+
let maxSeq = 0;
|
|
396
|
+
|
|
397
|
+
for (const event of events) {
|
|
398
|
+
const isRemove = event.type.endsWith('_remove');
|
|
399
|
+
|
|
400
|
+
if (isRemove) {
|
|
401
|
+
await handleRemove(agentId, event.type, event.body, ctx);
|
|
402
|
+
} else {
|
|
403
|
+
// Filter: should we cache this message?
|
|
404
|
+
let shouldCache = false;
|
|
405
|
+
|
|
406
|
+
// Is this a credential_add for my publicKey?
|
|
407
|
+
if (event.type === 'credential_add' && isCredentialForMe(event.body, publicKeyHex)) {
|
|
408
|
+
shouldCache = true;
|
|
409
|
+
}
|
|
410
|
+
// Is this a link_add targeting my publicKey? (new follower)
|
|
411
|
+
else if (event.type === 'link_add' && isFollowingMe(event.body, publicKeyHex)) {
|
|
412
|
+
shouldCache = true;
|
|
413
|
+
}
|
|
414
|
+
// Is authorPublicKey someone I follow? (feed content)
|
|
415
|
+
else if (await isFollowing(agentId, event.authorPublicKey, ctx)) {
|
|
416
|
+
shouldCache = true;
|
|
417
|
+
}
|
|
418
|
+
// Is this a reaction/reply to one of my posts? (notification)
|
|
419
|
+
else if (await referencesMyPost(agentId, event.body, ctx)) {
|
|
420
|
+
shouldCache = true;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (shouldCache) {
|
|
424
|
+
await ctx.prisma.inboundMessage.upsert({
|
|
425
|
+
where: { hash_hubUrl: { hash: event.hash, hubUrl } },
|
|
426
|
+
create: {
|
|
427
|
+
agentId,
|
|
428
|
+
hash: event.hash,
|
|
429
|
+
hubUrl,
|
|
430
|
+
authorAuraId: event.authorAuraId,
|
|
431
|
+
authorPublicKey: event.authorPublicKey ?? '',
|
|
432
|
+
type: event.type,
|
|
433
|
+
body: event.body,
|
|
434
|
+
timestamp: event.timestamp,
|
|
435
|
+
},
|
|
436
|
+
update: {},
|
|
437
|
+
}).catch((err: unknown) => {
|
|
438
|
+
ctx.log.debug({ err, hash: event.hash }, 'Incremental upsert failed (likely race)');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Generate social notifications for notification-worthy events
|
|
442
|
+
await createSocialNotification(agentId, publicKeyHex, event, ctx);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (event.seq > maxSeq) maxSeq = event.seq;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return maxSeq;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Incremental sync for a single agent against a single hub.
|
|
454
|
+
* Pull events from the hub and filter/cache locally.
|
|
455
|
+
*/
|
|
456
|
+
async function runIncremental(
|
|
457
|
+
agentId: string,
|
|
458
|
+
publicKeyHex: string,
|
|
459
|
+
currentSeq: number,
|
|
460
|
+
hubUrl: string,
|
|
461
|
+
mnemonic: string,
|
|
462
|
+
subscriptionId: string,
|
|
463
|
+
ctx: CronContext,
|
|
464
|
+
): Promise<void> {
|
|
465
|
+
let seq = currentSeq;
|
|
466
|
+
let batchCount = 0;
|
|
467
|
+
|
|
468
|
+
do {
|
|
469
|
+
const data = await fetchEventBatch(seq, hubUrl, publicKeyHex, mnemonic, ctx);
|
|
470
|
+
if (!data) break;
|
|
471
|
+
if (!data.events || data.events.length === 0) {
|
|
472
|
+
if (data.latestSeq > seq) {
|
|
473
|
+
seq = data.latestSeq;
|
|
474
|
+
}
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
batchCount = data.events.length;
|
|
479
|
+
|
|
480
|
+
const maxSeq = await processEventsForAgent(agentId, publicKeyHex, data.events, hubUrl, ctx);
|
|
481
|
+
if (maxSeq > seq) seq = maxSeq;
|
|
482
|
+
if (batchCount < EVENT_BATCH_LIMIT && data.latestSeq > seq) {
|
|
483
|
+
seq = data.latestSeq;
|
|
484
|
+
}
|
|
485
|
+
} while (batchCount === EVENT_BATCH_LIMIT);
|
|
486
|
+
|
|
487
|
+
if (seq > currentSeq) {
|
|
488
|
+
await ctx.prisma.hubSubscription.update({
|
|
489
|
+
where: { id: subscriptionId },
|
|
490
|
+
data: { inboundSeq: seq },
|
|
491
|
+
});
|
|
492
|
+
ctx.log.debug({ agentId, hubUrl, oldSeq: currentSeq, newSeq: seq }, 'Incremental sync advanced cursor');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ─── Job Definition ──────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
export const socialInboundJob: CronJob = {
|
|
499
|
+
id: 'social-inbound',
|
|
500
|
+
name: 'Social Inbound Sync',
|
|
501
|
+
intervalKey: 'social_inbound_interval',
|
|
502
|
+
defaultInterval: 5_000,
|
|
503
|
+
|
|
504
|
+
async run(ctx: CronContext): Promise<void> {
|
|
505
|
+
if (!isEnabled('SOCIAL')) return;
|
|
506
|
+
|
|
507
|
+
// Sync inbound cache for every local agent with a social identity.
|
|
508
|
+
const agents = await ctx.prisma.agentProfile.findMany({
|
|
509
|
+
where: { publicKeyHex: { not: null } },
|
|
510
|
+
orderBy: { createdAt: 'asc' },
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
if (agents.length === 0) return;
|
|
514
|
+
|
|
515
|
+
for (const agent of agents) {
|
|
516
|
+
const agentId = agent.agentId;
|
|
517
|
+
const publicKeyHex = agent.publicKeyHex;
|
|
518
|
+
if (!publicKeyHex) continue;
|
|
519
|
+
|
|
520
|
+
const mnemonic = getAgentMnemonic(agentId);
|
|
521
|
+
if (!mnemonic) {
|
|
522
|
+
ctx.log.warn({ agentId }, 'Skipping social inbound sync because agent is locked');
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Iterate over all subscribed hubs for this agent.
|
|
527
|
+
const subscriptions = await ctx.prisma.hubSubscription.findMany({
|
|
528
|
+
where: { agentId },
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Fallback: if no subscriptions exist yet (pre-migration), create one
|
|
532
|
+
// from the legacy single-hub config so existing users aren't broken.
|
|
533
|
+
if (subscriptions.length === 0) {
|
|
534
|
+
const defaultHubUrl = getHubUrl();
|
|
535
|
+
const sub = await ctx.prisma.hubSubscription.upsert({
|
|
536
|
+
where: { agentId_hubUrl: { agentId, hubUrl: defaultHubUrl } },
|
|
537
|
+
create: {
|
|
538
|
+
agentId,
|
|
539
|
+
hubUrl: defaultHubUrl,
|
|
540
|
+
label: 'AuraMaxx',
|
|
541
|
+
auraId: null,
|
|
542
|
+
inboundSeq: agent.inboundSeq,
|
|
543
|
+
inboundMode: agent.inboundMode,
|
|
544
|
+
},
|
|
545
|
+
update: {},
|
|
546
|
+
});
|
|
547
|
+
subscriptions.push(sub);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
for (const sub of subscriptions) {
|
|
551
|
+
const hubUrl = sub.hubUrl;
|
|
552
|
+
const currentSeq = sub.inboundSeq ?? 0;
|
|
553
|
+
const mode = sub.inboundMode;
|
|
554
|
+
|
|
555
|
+
// Fetch snapshot-cursor for this (agent, hub) pair.
|
|
556
|
+
// Do not share cursor responses across agents because auth/whitelist
|
|
557
|
+
// can differ by caller key.
|
|
558
|
+
const snapshotCursor = await tryCallHubWithSessionAuth<SnapshotCursorResponse>(
|
|
559
|
+
hubUrl,
|
|
560
|
+
'sync.snapshotCursor',
|
|
561
|
+
{},
|
|
562
|
+
mnemonic,
|
|
563
|
+
{ log: ctx.log },
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
const isStale =
|
|
567
|
+
snapshotCursor !== null &&
|
|
568
|
+
currentSeq > 0 &&
|
|
569
|
+
mode === 'incremental' &&
|
|
570
|
+
(snapshotCursor.seq - currentSeq) > STALENESS_THRESHOLD;
|
|
571
|
+
|
|
572
|
+
const needsSnapshot =
|
|
573
|
+
mode !== 'incremental' ||
|
|
574
|
+
currentSeq === 0 ||
|
|
575
|
+
isStale;
|
|
576
|
+
|
|
577
|
+
if (isStale) {
|
|
578
|
+
ctx.log.info(
|
|
579
|
+
{ agentId, hubUrl, currentSeq, hubSeq: snapshotCursor!.seq, threshold: STALENESS_THRESHOLD },
|
|
580
|
+
'Stale cursor, falling back to snapshot mode',
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
if (needsSnapshot) {
|
|
586
|
+
await runSnapshot(agentId, hubUrl, mnemonic, sub.id, ctx);
|
|
587
|
+
} else {
|
|
588
|
+
await runIncremental(agentId, publicKeyHex, currentSeq, hubUrl, mnemonic, sub.id, ctx);
|
|
589
|
+
}
|
|
590
|
+
} catch (err) {
|
|
591
|
+
ctx.log.error({ err, agentId, hubUrl }, 'Inbound sync failed for hub');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social Outbound Sync Job
|
|
3
|
+
* ========================
|
|
4
|
+
* Batches pending SocialMessages and POSTs them to the hub.
|
|
5
|
+
* Handles 207 multi-status response: accepted, duplicate, rejected, error.
|
|
6
|
+
* Exponential backoff on failures (max 5 minutes).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CronJob, CronContext } from '../job';
|
|
10
|
+
import type { SocialMessage } from '@prisma/client';
|
|
11
|
+
import { isEnabled } from '../../lib/feature-flags';
|
|
12
|
+
import { syncSocialMessagesNow } from '../../lib/social/sync';
|
|
13
|
+
import { getHubUrl } from '../../lib/defaults';
|
|
14
|
+
const BATCH_LIMIT = 200;
|
|
15
|
+
|
|
16
|
+
// --- Job Definition ---
|
|
17
|
+
|
|
18
|
+
export const socialOutboundJob: CronJob = {
|
|
19
|
+
id: 'social-outbound',
|
|
20
|
+
name: 'Social Outbound Sync',
|
|
21
|
+
intervalKey: 'social_outbound_interval',
|
|
22
|
+
defaultInterval: 2_000,
|
|
23
|
+
|
|
24
|
+
async run(ctx: CronContext): Promise<void> {
|
|
25
|
+
if (!isEnabled('SOCIAL')) return;
|
|
26
|
+
|
|
27
|
+
// Query pending messages ready to sync
|
|
28
|
+
const pending = await ctx.prisma.socialMessage.findMany({
|
|
29
|
+
where: {
|
|
30
|
+
syncStatus: 'pending',
|
|
31
|
+
OR: [
|
|
32
|
+
{ nextRetryAt: null },
|
|
33
|
+
{ nextRetryAt: { lte: new Date() } },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
orderBy: { createdAt: 'asc' },
|
|
37
|
+
take: BATCH_LIMIT,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (pending.length === 0) return;
|
|
41
|
+
|
|
42
|
+
// Group by (agentId, hubUrl) — each group syncs to its respective hub
|
|
43
|
+
const groups = new Map<string, SocialMessage[]>();
|
|
44
|
+
for (const msg of pending) {
|
|
45
|
+
const key = `${msg.agentId}\0${msg.hubUrl}`;
|
|
46
|
+
const list = groups.get(key) ?? [];
|
|
47
|
+
list.push(msg);
|
|
48
|
+
groups.set(key, list);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const [key, messages] of groups) {
|
|
52
|
+
const sepIdx = key.indexOf('\0');
|
|
53
|
+
const agentId = key.slice(0, sepIdx);
|
|
54
|
+
const msgHubUrl = key.slice(sepIdx + 1);
|
|
55
|
+
const hubUrl = msgHubUrl || getHubUrl(); // "" = default hub
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await syncSocialMessagesNow({
|
|
59
|
+
messages,
|
|
60
|
+
transientErrorMode: 'retry',
|
|
61
|
+
prismaClient: ctx.prisma,
|
|
62
|
+
hubUrl,
|
|
63
|
+
log: ctx.log,
|
|
64
|
+
});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
ctx.log.error({ err, agentId, hubUrl }, 'Outbound sync failed for agent/hub');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|