diff --git a/bun.lock b/bun.lock index e962170..3f3fe41 100644 --- a/bun.lock +++ b/bun.lock @@ -95,6 +95,19 @@ "typescript": "^5.9.2", }, }, + "packages/flow-backend-convex": { + "name": "@peezy.tech/flow-backend-convex", + "version": "0.1.0", + "dependencies": { + "@peezy.tech/flow-runtime": "workspace:*", + "convex": "^1.38.0", + }, + "devDependencies": { + "@types/bun": "catalog:", + "@types/node": "catalog:", + "typescript": "catalog:", + }, + }, "packages/flow-runtime": { "name": "@peezy.tech/flow-runtime", "version": "0.1.0", @@ -233,57 +246,57 @@ "@ecies/ciphers": ["@ecies/ciphers@0.2.6", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="], "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], @@ -339,6 +352,8 @@ "@peezy.tech/codex-flows": ["@peezy.tech/codex-flows@workspace:packages/codex-client"], + "@peezy.tech/flow-backend-convex": ["@peezy.tech/flow-backend-convex@workspace:packages/flow-backend-convex"], + "@peezy.tech/flow-runtime": ["@peezy.tech/flow-runtime@workspace:packages/flow-runtime"], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], @@ -545,6 +560,8 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "convex": ["convex@1.38.0", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0", "ws": "8.18.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "@clerk/react": "^6.4.3", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "@clerk/react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-122AC6y5lUS7mr39cluLw9+TOtRX5d/XxeivHhHObs/NTXoVvOnIgDzexVcxaz6Rk0oLFSoydSR1rDCltEz/0A=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], @@ -609,7 +626,7 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + "esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -899,6 +916,8 @@ "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], @@ -1067,7 +1086,7 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], @@ -1093,6 +1112,8 @@ "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "@discordjs/ws/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], @@ -1131,6 +1152,8 @@ "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + "vite/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1155,6 +1178,58 @@ "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/docs/flows.md b/docs/flows.md index 60f3f9b..b4ccd4a 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -193,6 +193,20 @@ running Codex or shell work executes. A future Convex backend should: This keeps Patchbay dispatch-only, keeps Convex durable, and keeps process-heavy work on infrastructure that can run Codex, Bun, Git, Cargo, and system tools. +The reusable component package now lives at +`packages/flow-backend-convex`. It owns only generic flow control-plane state: +synced manifests, events, runs, attempts, leases, compact output events, and +final results. Installing apps should expose their own service-authenticated +wrapper functions and keep domain-specific completion in app code. For example, +the 2D pet game keeps asset registration, payment state, and minting outside the +generic backend. + +The first component version stores readable progress chunks in +`flowOutputEvents`. If durable long-form transcripts become important, add +`@convex-dev/persistent-text-streaming` as a child component and attach a stream +id to each run attempt; canonical run state should remain in the flow backend +tables. + ## Codex Release Flows The upstream `openai/codex` release event fans out to two flow packages: diff --git a/package.json b/package.json index 2455dd0..78616f0 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,6 @@ "replay:thread": "bun scripts/run-code-mode-in-new-thread.ts", "start": "bun run --filter web preview", "start:discord:debug:commentary": "bun run --filter codex-discord-bridge start:debug:commentary", - "test": "bun run --filter @peezy.tech/codex-flows test && bun run --filter @peezy.tech/flow-runtime test && bun run --filter codex-flow-systemd-local test && bun run --filter codex-app-cli test && bun run --filter codex-discord-bridge test" + "test": "bun run --filter @peezy.tech/codex-flows test && bun run --filter @peezy.tech/flow-runtime test && bun run --filter @peezy.tech/flow-backend-convex test && bun run --filter codex-flow-systemd-local test && bun run --filter codex-app-cli test && bun run --filter codex-discord-bridge test" } } diff --git a/packages/flow-backend-convex/README.md b/packages/flow-backend-convex/README.md new file mode 100644 index 0000000..31291fe --- /dev/null +++ b/packages/flow-backend-convex/README.md @@ -0,0 +1,49 @@ +# @peezy.tech/flow-backend-convex + +Reusable Convex backend primitives for `codex-flows`. + +This package is the extracted version of the backend shape proven in +`2d-codex-pet-game`: Convex stores generic flow events, matching runs, run +attempts, leases, results, and compact output events. Process-heavy execution +still happens in an external worker that claims runs and executes `flow.toml` +steps through `@peezy.tech/flow-runtime`. + +## Component Boundary + +The component owns generic flow state only: + +- synced flow manifests +- accepted `FlowEvent` records +- queued/running/completed/failed/canceled run records +- leased run attempts +- structured output events +- final result payloads + +Installing apps own authentication and domain state. An app should expose +service-authenticated wrapper functions for external workers, then call this +component from those wrappers. Domain-specific completion, such as generated +asset registration or minting, should stay in app code. + +## Component Install + +```ts +// convex/convex.config.ts +import flowBackend from "@peezy.tech/flow-backend-convex/convex.config.js"; +import { defineApp } from "convex/server"; + +const app = defineApp(); +app.use(flowBackend); + +export default app; +``` + +The app wrapper functions can call the installed component functions through +`components.flowBackend`. The worker-facing API should stay app-owned so each +deployment can enforce its own service secret, identity, or ACL. + +## Current Transcript Strategy + +The first component stores output chunks in `flowOutputEvents`. A future version +can add `@convex-dev/persistent-text-streaming` as a child component and map +each run attempt to a durable transcript stream. The canonical control state +should remain in this component's tables either way. diff --git a/packages/flow-backend-convex/package.json b/packages/flow-backend-convex/package.json new file mode 100644 index 0000000..8a464e0 --- /dev/null +++ b/packages/flow-backend-convex/package.json @@ -0,0 +1,30 @@ +{ + "name": "@peezy.tech/flow-backend-convex", + "version": "0.1.0", + "description": "Reusable Convex component for durable codex-flow event, run, lease, and result state.", + "type": "module", + "private": true, + "license": "Apache-2.0", + "exports": { + ".": "./src/index.ts", + "./convex.config": "./src/component/convex.config.ts", + "./convex.config.js": "./src/component/convex.config.ts", + "./_generated/component.js": { + "types": "./src/component/_generated/component.ts" + } + }, + "scripts": { + "build": "tsc --noEmit", + "check:types": "tsc --noEmit", + "test": "bun test test/*.test.ts" + }, + "dependencies": { + "@peezy.tech/flow-runtime": "workspace:*", + "convex": "^1.38.0" + }, + "devDependencies": { + "@types/bun": "catalog:", + "@types/node": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/flow-backend-convex/src/backend-model.ts b/packages/flow-backend-convex/src/backend-model.ts new file mode 100644 index 0000000..6489346 --- /dev/null +++ b/packages/flow-backend-convex/src/backend-model.ts @@ -0,0 +1,91 @@ +import type { FlowEvent } from "@peezy.tech/flow-runtime"; +import type { DispatchConvexFlowEventResult, SyncedFlowManifest } from "./types.ts"; + +export type StoredFlowRunInput = { + eventId: string; + flowName: string; + stepName: string; + replayNonce?: string; +}; + +export function flowRunId(input: StoredFlowRunInput): string { + return [ + "run", + safeId(input.eventId), + safeId(input.flowName), + safeId(input.stepName), + ...(input.replayNonce ? [safeId(input.replayNonce), "replay"] : []), + ].join(":"); +} + +export function normalizeFlowEvent(value: unknown): FlowEvent { + if (!isRecord(value) || typeof value.id !== "string" || typeof value.type !== "string") { + throw new Error("FlowEvent requires string id and type"); + } + return { + receivedAt: typeof value.receivedAt === "string" ? value.receivedAt : new Date().toISOString(), + payload: "payload" in value ? value.payload : {}, + ...value, + } as FlowEvent; +} + +export function matchingManifestSteps( + manifests: SyncedFlowManifest[], + event: FlowEvent, +): Array<{ manifest: SyncedFlowManifest; step: SyncedFlowManifest["steps"][number] }> { + const matches: Array<{ manifest: SyncedFlowManifest; step: SyncedFlowManifest["steps"][number] }> = []; + for (const manifest of manifests) { + for (const step of manifest.steps) { + if (step.trigger?.type === event.type) { + matches.push({ manifest, step }); + } + } + } + return matches; +} + +export function duplicateDispatchResult(eventId: string, runIds: string[]): DispatchConvexFlowEventResult { + return { + status: "duplicate", + eventId, + runIds, + matched: 0, + }; +} + +export function acceptedDispatchResult( + eventId: string, + runIds: string[], + matched: number, +): DispatchConvexFlowEventResult { + return { + status: "accepted", + eventId, + runIds, + matched, + }; +} + +export function clampLimit(value: number | undefined): number { + if (!value || !Number.isFinite(value)) { + return 50; + } + return Math.max(1, Math.min(500, Math.trunc(value))); +} + +export function leaseMs(value: number | undefined): number { + return Math.max(10_000, Math.min(value ?? 120_000, 30 * 60_000)); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function safeId(value: string): string { + return ( + value + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, "-") + .replace(/^-+|-+$/g, "") || "item" + ); +} diff --git a/packages/flow-backend-convex/src/component/_generated/api.ts b/packages/flow-backend-convex/src/component/_generated/api.ts new file mode 100644 index 0000000..c177ff9 --- /dev/null +++ b/packages/flow-backend-convex/src/component/_generated/api.ts @@ -0,0 +1,50 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type * as backend from "../backend.js"; + +import type { + ApiFromModules, + FilterApi, + FunctionReference, +} from "convex/server"; +import { anyApi, componentsGeneric } from "convex/server"; + +const fullApi: ApiFromModules<{ + backend: typeof backend; +}> = anyApi as any; + +/** + * A utility for referencing Convex functions in your app's public API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +export const api: FilterApi< + typeof fullApi, + FunctionReference +> = anyApi as any; + +/** + * A utility for referencing Convex functions in your app's internal API. + * + * Usage: + * ```js + * const myFunctionReference = internal.myModule.myFunction; + * ``` + */ +export const internal: FilterApi< + typeof fullApi, + FunctionReference +> = anyApi as any; + +export const components = componentsGeneric() as unknown as {}; diff --git a/packages/flow-backend-convex/src/component/_generated/component.ts b/packages/flow-backend-convex/src/component/_generated/component.ts new file mode 100644 index 0000000..c0cb204 --- /dev/null +++ b/packages/flow-backend-convex/src/component/_generated/component.ts @@ -0,0 +1,151 @@ +/* eslint-disable */ +/** + * Generated `ComponentApi` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { FunctionReference } from "convex/server"; + +/** + * A utility for referencing a Convex component's exposed API. + * + * Useful when expecting a parameter like `components.myComponent`. + * Usage: + * ```ts + * async function myFunction(ctx: QueryCtx, component: ComponentApi) { + * return ctx.runQuery(component.someFile.someQuery, { ...args }); + * } + * ``` + */ +export type ComponentApi = + { + backend: { + appendRunOutput: FunctionReference< + "mutation", + "internal", + { + attemptId: string; + kind: "system" | "stdout" | "stderr" | "agent"; + leaseToken: string; + text: string; + }, + any, + Name + >; + cancelRun: FunctionReference< + "mutation", + "internal", + { runId: string }, + any, + Name + >; + claimRun: FunctionReference< + "mutation", + "internal", + { leaseMs?: number; workerId: string }, + any, + Name + >; + completeRun: FunctionReference< + "mutation", + "internal", + { attemptId: string; leaseToken: string; result: any }, + any, + Name + >; + dispatchEvent: FunctionReference< + "mutation", + "internal", + { + event: { + id: string; + occurredAt?: string; + payload: any; + receivedAt?: string; + source?: string; + type: string; + }; + }, + any, + Name + >; + failRun: FunctionReference< + "mutation", + "internal", + { attemptId: string; error: string; leaseToken: string }, + any, + Name + >; + getEvent: FunctionReference< + "query", + "internal", + { eventId: string }, + any, + Name + >; + getRun: FunctionReference< + "query", + "internal", + { runId: string }, + any, + Name + >; + heartbeatRun: FunctionReference< + "mutation", + "internal", + { attemptId: string; leaseMs?: number; leaseToken: string }, + any, + Name + >; + listEvents: FunctionReference< + "query", + "internal", + { limit?: number; type?: string }, + any, + Name + >; + listRuns: FunctionReference< + "query", + "internal", + { + eventId?: string; + limit?: number; + status?: "queued" | "running" | "completed" | "failed" | "canceled"; + }, + any, + Name + >; + replayEvent: FunctionReference< + "mutation", + "internal", + { eventId: string }, + any, + Name + >; + syncFlowManifest: FunctionReference< + "mutation", + "internal", + { + config?: any; + description?: string; + name: string; + root?: string; + steps: Array<{ + cwd?: string; + name: string; + runner: "bun" | "code-mode"; + script: string; + timeoutMs: number; + trigger?: { schema?: string; schemaJson?: any; type: string }; + }>; + version: number; + }, + any, + Name + >; + }; + }; diff --git a/packages/flow-backend-convex/src/component/_generated/dataModel.ts b/packages/flow-backend-convex/src/component/_generated/dataModel.ts new file mode 100644 index 0000000..f97fd19 --- /dev/null +++ b/packages/flow-backend-convex/src/component/_generated/dataModel.ts @@ -0,0 +1,60 @@ +/* eslint-disable */ +/** + * Generated data model types. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { + DataModelFromSchemaDefinition, + DocumentByName, + TableNamesInDataModel, + SystemTableNames, +} from "convex/server"; +import type { GenericId } from "convex/values"; +import schema from "../schema.js"; + +/** + * The names of all of your Convex tables. + */ +export type TableNames = TableNamesInDataModel; + +/** + * The type of a document stored in Convex. + * + * @typeParam TableName - A string literal type of the table name (like "users"). + */ +export type Doc = DocumentByName< + DataModel, + TableName +>; + +/** + * An identifier for a document in Convex. + * + * Convex documents are uniquely identified by their `Id`, which is accessible + * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). + * + * Documents can be loaded using `db.get(tableName, id)` in query and mutation functions. + * + * IDs are just strings at runtime, but this type can be used to distinguish them from other + * strings when type checking. + * + * @typeParam TableName - A string literal type of the table name (like "users"). + */ +export type Id = + GenericId; + +/** + * A type describing your Convex data model. + * + * This type includes information about what tables you have, the type of + * documents stored in those tables, and the indexes defined on them. + * + * This type is used to parameterize methods like `queryGeneric` and + * `mutationGeneric` to make them type-safe. + */ +export type DataModel = DataModelFromSchemaDefinition; diff --git a/packages/flow-backend-convex/src/component/_generated/server.ts b/packages/flow-backend-convex/src/component/_generated/server.ts new file mode 100644 index 0000000..739b02f --- /dev/null +++ b/packages/flow-backend-convex/src/component/_generated/server.ts @@ -0,0 +1,156 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { + ActionBuilder, + HttpActionBuilder, + MutationBuilder, + QueryBuilder, + GenericActionCtx, + GenericMutationCtx, + GenericQueryCtx, + GenericDatabaseReader, + GenericDatabaseWriter, +} from "convex/server"; +import { + actionGeneric, + httpActionGeneric, + queryGeneric, + mutationGeneric, + internalActionGeneric, + internalMutationGeneric, + internalQueryGeneric, +} from "convex/server"; +import type { DataModel } from "./dataModel.js"; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const query: QueryBuilder = queryGeneric; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const internalQuery: QueryBuilder = + internalQueryGeneric; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const mutation: MutationBuilder = mutationGeneric; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const internalMutation: MutationBuilder = + internalMutationGeneric; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export const action: ActionBuilder = actionGeneric; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export const internalAction: ActionBuilder = + internalActionGeneric; + +/** + * Define an HTTP action. + * + * The wrapped function will be used to respond to HTTP requests received + * by a Convex deployment if the requests matches the path and method where + * this action is routed. Be sure to route your httpAction in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument + * and a Fetch API `Request` object as its second. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. + */ +export const httpAction: HttpActionBuilder = httpActionGeneric; + +/** + * A set of services for use within Convex query functions. + * + * The query context is passed as the first argument to any Convex query + * function run on the server. + * + * If you're using code generation, use the `QueryCtx` type in `convex/_generated/server.d.ts` instead. + */ +export type QueryCtx = GenericQueryCtx; + +/** + * A set of services for use within Convex mutation functions. + * + * The mutation context is passed as the first argument to any Convex mutation + * function run on the server. + * + * If you're using code generation, use the `MutationCtx` type in `convex/_generated/server.d.ts` instead. + */ +export type MutationCtx = GenericMutationCtx; + +/** + * A set of services for use within Convex action functions. + * + * The action context is passed as the first argument to any Convex action + * function run on the server. + */ +export type ActionCtx = GenericActionCtx; + +/** + * An interface to read from the database within Convex query functions. + * + * The two entry points are {@link DatabaseReader.get}, which fetches a single + * document by its {@link Id}, or {@link DatabaseReader.query}, which starts + * building a query. + */ +export type DatabaseReader = GenericDatabaseReader; + +/** + * An interface to read from and write to the database within Convex mutation + * functions. + * + * Convex guarantees that all writes within a single mutation are + * executed atomically, so you never have to worry about partial writes leaving + * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) + * for the guarantees Convex provides your functions. + */ +export type DatabaseWriter = GenericDatabaseWriter; diff --git a/packages/flow-backend-convex/src/component/backend.ts b/packages/flow-backend-convex/src/component/backend.ts new file mode 100644 index 0000000..ed4a306 --- /dev/null +++ b/packages/flow-backend-convex/src/component/backend.ts @@ -0,0 +1,508 @@ +import { v } from "convex/values"; +import { mutation, query } from "./_generated/server.js"; +import { flowEventArg, flowStepArg } from "./schema.js"; + +const runStatusArg = v.union( + v.literal("queued"), + v.literal("running"), + v.literal("completed"), + v.literal("failed"), + v.literal("canceled"), +); + +export const syncFlowManifest = mutation({ + args: { + name: v.string(), + version: v.number(), + description: v.optional(v.string()), + root: v.optional(v.string()), + config: v.optional(v.any()), + steps: v.array(flowStepArg), + }, + handler: async (ctx, args) => { + const now = Date.now(); + const existing = await ctx.db + .query("flowManifests") + .withIndex("by_name", (q) => q.eq("name", args.name)) + .unique(); + const manifest = { + name: args.name, + version: args.version, + description: args.description, + root: args.root, + config: args.config, + steps: args.steps, + syncedAt: now, + updatedAt: now, + }; + if (existing) { + await ctx.db.patch(existing._id, manifest); + return { manifestId: existing._id, status: "updated" }; + } + return { + manifestId: await ctx.db.insert("flowManifests", manifest), + status: "created", + }; + }, +}); + +export const dispatchEvent = mutation({ + args: { + event: flowEventArg, + }, + handler: async (ctx, args) => { + return dispatchFlowEvent(ctx, { event: args.event }); + }, +}); + +export const replayEvent = mutation({ + args: { + eventId: v.string(), + }, + handler: async (ctx, args) => { + const existing = await ctx.db + .query("flowEvents") + .withIndex("by_event_id", (q) => q.eq("eventId", args.eventId)) + .unique(); + if (!existing) { + throw new Error(`Unknown flow event: ${args.eventId}`); + } + return dispatchFlowEvent(ctx, { + event: existing.raw, + replayNonce: String(Date.now()), + }); + }, +}); + +export const claimRun = mutation({ + args: { + workerId: v.string(), + leaseMs: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const now = Date.now(); + const expired = await ctx.db + .query("flowRunAttempts") + .withIndex("by_status_lease", (q) => + q.eq("status", "running").lt("leaseExpiresAt", now), + ) + .first(); + if (expired) { + await ctx.db.patch(expired._id, { + status: "failed", + error: "Lease expired before worker heartbeat.", + updatedAt: now, + completedAt: now, + }); + const expiredRun = await runById(ctx, expired.runId); + if (expiredRun && expiredRun.status === "running") { + return claimExistingRun(ctx, expiredRun, args.workerId, leaseMs(args.leaseMs)); + } + } + + const queued = await ctx.db + .query("flowRuns") + .withIndex("by_status_created", (q) => q.eq("status", "queued")) + .order("asc") + .first(); + if (!queued) return null; + return claimExistingRun(ctx, queued, args.workerId, leaseMs(args.leaseMs)); + }, +}); + +export const heartbeatRun = mutation({ + args: { + attemptId: v.string(), + leaseToken: v.string(), + leaseMs: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const attempt = await assertAttemptLease(ctx, args.attemptId, args.leaseToken); + const now = Date.now(); + const nextLeaseExpiresAt = now + leaseMs(args.leaseMs); + await ctx.db.patch(attempt._id, { + leaseExpiresAt: nextLeaseExpiresAt, + lastHeartbeatAt: now, + updatedAt: now, + }); + return { status: "running", leaseExpiresAt: nextLeaseExpiresAt }; + }, +}); + +export const appendRunOutput = mutation({ + args: { + attemptId: v.string(), + leaseToken: v.string(), + kind: v.union( + v.literal("system"), + v.literal("stdout"), + v.literal("stderr"), + v.literal("agent"), + ), + text: v.string(), + }, + handler: async (ctx, args) => { + const attempt = await assertAttemptLease(ctx, args.attemptId, args.leaseToken); + return ctx.db.insert("flowOutputEvents", { + attemptId: args.attemptId, + runId: attempt.runId, + kind: args.kind, + text: args.text, + createdAt: Date.now(), + }); + }, +}); + +export const completeRun = mutation({ + args: { + attemptId: v.string(), + leaseToken: v.string(), + result: v.any(), + }, + handler: async (ctx, args) => { + const attempt = await assertAttemptLease(ctx, args.attemptId, args.leaseToken); + const run = await runById(ctx, attempt.runId); + if (!run) throw new Error(`Unknown flow run: ${attempt.runId}`); + const now = Date.now(); + await ctx.db.patch(attempt._id, { + status: "completed", + result: args.result, + updatedAt: now, + completedAt: now, + }); + await ctx.db.patch(run._id, { + status: "completed", + result: args.result, + updatedAt: now, + completedAt: now, + }); + return { status: "completed", runId: attempt.runId }; + }, +}); + +export const failRun = mutation({ + args: { + attemptId: v.string(), + leaseToken: v.string(), + error: v.string(), + }, + handler: async (ctx, args) => { + const attempt = await assertAttemptLease(ctx, args.attemptId, args.leaseToken); + const run = await runById(ctx, attempt.runId); + if (!run) throw new Error(`Unknown flow run: ${attempt.runId}`); + const now = Date.now(); + await ctx.db.patch(attempt._id, { + status: "failed", + error: args.error, + updatedAt: now, + completedAt: now, + }); + await ctx.db.patch(run._id, { + status: "failed", + error: args.error, + updatedAt: now, + completedAt: now, + }); + return { status: "failed", runId: attempt.runId }; + }, +}); + +export const cancelRun = mutation({ + args: { + runId: v.string(), + }, + handler: async (ctx, args) => { + const run = await runById(ctx, args.runId); + if (!run) throw new Error(`Unknown flow run: ${args.runId}`); + if (run.status === "completed") { + throw new Error(`Cannot cancel completed flow run: ${args.runId}`); + } + const now = Date.now(); + await ctx.db.patch(run._id, { + status: "canceled", + updatedAt: now, + completedAt: now, + }); + return { status: "canceled", runId: args.runId }; + }, +}); + +export const listEvents = query({ + args: { + type: v.optional(v.string()), + limit: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const limit = clampLimit(args.limit); + const eventType = args.type; + if (eventType) { + return ctx.db + .query("flowEvents") + .withIndex("by_type_created", (q) => q.eq("type", eventType)) + .order("desc") + .take(limit); + } + return ctx.db.query("flowEvents").order("desc").take(limit); + }, +}); + +export const getEvent = query({ + args: { + eventId: v.string(), + }, + handler: async (ctx, args) => { + const event = await ctx.db + .query("flowEvents") + .withIndex("by_event_id", (q) => q.eq("eventId", args.eventId)) + .unique(); + if (!event) return null; + const runs = await ctx.db + .query("flowRuns") + .withIndex("by_event_id", (q) => q.eq("eventId", args.eventId)) + .collect(); + return { ...event, runs }; + }, +}); + +export const listRuns = query({ + args: { + eventId: v.optional(v.string()), + status: v.optional(runStatusArg), + limit: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const limit = clampLimit(args.limit); + const eventId = args.eventId; + if (eventId) { + return ctx.db + .query("flowRuns") + .withIndex("by_event_id", (q) => q.eq("eventId", eventId)) + .order("desc") + .take(limit); + } + const status = args.status; + if (status) { + return ctx.db + .query("flowRuns") + .withIndex("by_status_created", (q) => q.eq("status", status)) + .order("desc") + .take(limit); + } + return ctx.db.query("flowRuns").order("desc").take(limit); + }, +}); + +export const getRun = query({ + args: { + runId: v.string(), + }, + handler: async (ctx, args) => { + const run = await runById(ctx, args.runId); + if (!run) return null; + const attempts = await ctx.db + .query("flowRunAttempts") + .withIndex("by_run_id", (q) => q.eq("runId", args.runId)) + .collect(); + const output = await ctx.db + .query("flowOutputEvents") + .withIndex("by_run", (q) => q.eq("runId", args.runId)) + .order("asc") + .take(500); + return { ...run, attempts, output }; + }, +}); + +async function dispatchFlowEvent( + ctx: any, + args: { + event: { + id: string; + type: string; + source?: string; + occurredAt?: string; + receivedAt?: string; + payload: any; + }; + replayNonce?: string; + }, +) { + const now = Date.now(); + const event = { + ...args.event, + receivedAt: args.event.receivedAt ?? new Date(now).toISOString(), + }; + const existing = await ctx.db + .query("flowEvents") + .withIndex("by_event_id", (q: any) => q.eq("eventId", event.id)) + .unique(); + if (existing && !args.replayNonce) { + const runs = await ctx.db + .query("flowRuns") + .withIndex("by_event_id", (q: any) => q.eq("eventId", event.id)) + .collect(); + return { + status: "duplicate", + eventId: event.id, + runIds: runs.map((run: any) => run.runId), + matched: 0, + }; + } + + if (!existing) { + await ctx.db.insert("flowEvents", { + eventId: event.id, + type: event.type, + source: event.source, + occurredAt: event.occurredAt, + receivedAt: event.receivedAt, + payload: event.payload, + raw: event, + createdAt: now, + }); + } + + const manifests = await ctx.db.query("flowManifests").collect(); + const matches = []; + for (const manifest of manifests) { + for (const step of manifest.steps) { + if (step.trigger?.type === event.type) { + matches.push({ manifest, step }); + } + } + } + + const runIds: string[] = []; + for (const match of matches) { + const runId = flowRunId(event.id, match.manifest.name, match.step.name, args.replayNonce); + const existingRun = await runById(ctx, runId); + if (existingRun) { + runIds.push(existingRun.runId); + continue; + } + await ctx.db.insert("flowRuns", { + runId, + eventId: event.id, + flowName: match.manifest.name, + flowVersion: match.manifest.version, + stepName: match.step.name, + runner: match.step.runner, + status: "queued", + attemptCount: 0, + createdAt: now, + updatedAt: now, + }); + runIds.push(runId); + } + + return { + status: "accepted", + eventId: event.id, + runIds, + matched: matches.length, + }; +} + +async function claimExistingRun( + ctx: any, + run: any, + workerId: string, + leaseDurationMs: number, +) { + const now = Date.now(); + const attemptNumber = run.attemptCount + 1; + const attemptId = `${run.runId}:attempt:${attemptNumber}`; + const leaseToken = `${attemptId}:${workerId}:${now}`; + await ctx.db.insert("flowRunAttempts", { + attemptId, + runId: run.runId, + eventId: run.eventId, + flowName: run.flowName, + stepName: run.stepName, + attemptNumber, + status: "running", + workerId, + leaseToken, + leaseExpiresAt: now + leaseDurationMs, + lastHeartbeatAt: now, + createdAt: now, + updatedAt: now, + }); + await ctx.db.patch(run._id, { + status: "running", + attemptCount: attemptNumber, + latestAttemptId: attemptId, + startedAt: run.startedAt ?? now, + updatedAt: now, + }); + + const event = await ctx.db + .query("flowEvents") + .withIndex("by_event_id", (q: any) => q.eq("eventId", run.eventId)) + .unique(); + return { + runId: run.runId, + attemptId, + leaseToken, + leaseExpiresAt: now + leaseDurationMs, + flowName: run.flowName, + stepName: run.stepName, + runner: run.runner, + event: event?.raw, + }; +} + +async function assertAttemptLease(ctx: any, attemptId: string, leaseToken: string) { + const attempt = await ctx.db + .query("flowRunAttempts") + .withIndex("by_attempt_id", (q: any) => q.eq("attemptId", attemptId)) + .unique(); + if (!attempt) throw new Error(`Unknown flow run attempt: ${attemptId}`); + if (attempt.status !== "running" || attempt.leaseToken !== leaseToken) { + throw new Error("Flow run attempt is not leased by this worker."); + } + if (attempt.leaseExpiresAt < Date.now()) { + throw new Error("Flow run attempt lease expired."); + } + return attempt; +} + +async function runById(ctx: any, runId: string) { + return ctx.db + .query("flowRuns") + .withIndex("by_run_id", (q: any) => q.eq("runId", runId)) + .unique(); +} + +function flowRunId( + eventId: string, + flowName: string, + stepName: string, + replayNonce?: string, +): string { + return [ + "run", + safeId(eventId), + safeId(flowName), + safeId(stepName), + ...(replayNonce ? [safeId(replayNonce), "replay"] : []), + ].join(":"); +} + +function safeId(value: string): string { + return ( + value + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, "-") + .replace(/^-+|-+$/g, "") + || "item" + ); +} + +function leaseMs(value: number | undefined): number { + return Math.max(10_000, Math.min(value ?? 120_000, 30 * 60_000)); +} + +function clampLimit(value: number | undefined): number { + if (!value || !Number.isFinite(value)) return 50; + return Math.max(1, Math.min(Math.trunc(value), 500)); +} diff --git a/packages/flow-backend-convex/src/component/convex.config.ts b/packages/flow-backend-convex/src/component/convex.config.ts new file mode 100644 index 0000000..ef9d75d --- /dev/null +++ b/packages/flow-backend-convex/src/component/convex.config.ts @@ -0,0 +1,5 @@ +import { defineComponent } from "convex/server"; + +const component = defineComponent("flowBackend"); + +export default component; diff --git a/packages/flow-backend-convex/src/component/schema.ts b/packages/flow-backend-convex/src/component/schema.ts new file mode 100644 index 0000000..33a4e70 --- /dev/null +++ b/packages/flow-backend-convex/src/component/schema.ts @@ -0,0 +1,124 @@ +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; + +const flowStep = v.object({ + name: v.string(), + runner: v.union(v.literal("bun"), v.literal("code-mode")), + script: v.string(), + timeoutMs: v.number(), + cwd: v.optional(v.string()), + trigger: v.optional( + v.object({ + type: v.string(), + schema: v.optional(v.string()), + schemaJson: v.optional(v.any()), + }), + ), +}); + +export const flowEventArg = v.object({ + id: v.string(), + type: v.string(), + source: v.optional(v.string()), + occurredAt: v.optional(v.string()), + receivedAt: v.optional(v.string()), + payload: v.any(), +}); + +export const flowStepArg = flowStep; + +export default defineSchema({ + flowManifests: defineTable({ + name: v.string(), + version: v.number(), + description: v.optional(v.string()), + root: v.optional(v.string()), + config: v.optional(v.any()), + steps: v.array(flowStep), + syncedAt: v.number(), + updatedAt: v.number(), + }).index("by_name", ["name"]), + + flowEvents: defineTable({ + eventId: v.string(), + type: v.string(), + source: v.optional(v.string()), + occurredAt: v.optional(v.string()), + receivedAt: v.string(), + payload: v.any(), + raw: v.any(), + createdAt: v.number(), + }) + .index("by_event_id", ["eventId"]) + .index("by_type_created", ["type", "createdAt"]), + + flowRuns: defineTable({ + runId: v.string(), + eventId: v.string(), + flowName: v.string(), + flowVersion: v.number(), + stepName: v.string(), + runner: v.union(v.literal("bun"), v.literal("code-mode")), + status: v.union( + v.literal("queued"), + v.literal("running"), + v.literal("completed"), + v.literal("failed"), + v.literal("canceled"), + ), + attemptCount: v.number(), + latestAttemptId: v.optional(v.string()), + result: v.optional(v.any()), + error: v.optional(v.string()), + createdAt: v.number(), + updatedAt: v.number(), + startedAt: v.optional(v.number()), + completedAt: v.optional(v.number()), + }) + .index("by_run_id", ["runId"]) + .index("by_event_id", ["eventId"]) + .index("by_status_created", ["status", "createdAt"]), + + flowRunAttempts: defineTable({ + attemptId: v.string(), + runId: v.string(), + eventId: v.string(), + flowName: v.string(), + stepName: v.string(), + attemptNumber: v.number(), + status: v.union( + v.literal("running"), + v.literal("completed"), + v.literal("failed"), + v.literal("canceled"), + ), + workerId: v.string(), + leaseToken: v.string(), + leaseExpiresAt: v.number(), + lastHeartbeatAt: v.number(), + transcriptStreamId: v.optional(v.string()), + result: v.optional(v.any()), + error: v.optional(v.string()), + createdAt: v.number(), + updatedAt: v.number(), + completedAt: v.optional(v.number()), + }) + .index("by_attempt_id", ["attemptId"]) + .index("by_run_id", ["runId"]) + .index("by_status_lease", ["status", "leaseExpiresAt"]), + + flowOutputEvents: defineTable({ + attemptId: v.string(), + runId: v.string(), + kind: v.union( + v.literal("system"), + v.literal("stdout"), + v.literal("stderr"), + v.literal("agent"), + ), + text: v.string(), + createdAt: v.number(), + }) + .index("by_attempt", ["attemptId", "createdAt"]) + .index("by_run", ["runId", "createdAt"]), +}); diff --git a/packages/flow-backend-convex/src/index.ts b/packages/flow-backend-convex/src/index.ts new file mode 100644 index 0000000..66d1ff4 --- /dev/null +++ b/packages/flow-backend-convex/src/index.ts @@ -0,0 +1,19 @@ +export { + acceptedDispatchResult, + clampLimit, + duplicateDispatchResult, + flowRunId, + leaseMs, + matchingManifestSteps, + normalizeFlowEvent, +} from "./backend-model.ts"; +export type { + ClaimedConvexFlowRun, + CompleteConvexFlowRunInput, + ConvexFlowAttemptStatus, + ConvexFlowOutputKind, + ConvexFlowRunStatus, + DispatchConvexFlowEventResult, + SyncedFlowManifest, + SyncedFlowStep, +} from "./types.ts"; diff --git a/packages/flow-backend-convex/src/types.ts b/packages/flow-backend-convex/src/types.ts new file mode 100644 index 0000000..03cf2b1 --- /dev/null +++ b/packages/flow-backend-convex/src/types.ts @@ -0,0 +1,51 @@ +import type { FlowEvent, FlowResult, FlowStep } from "@peezy.tech/flow-runtime"; + +export type ConvexFlowRunStatus = + | "queued" + | "running" + | "completed" + | "failed" + | "canceled"; + +export type ConvexFlowAttemptStatus = Exclude; + +export type ConvexFlowOutputKind = "system" | "stdout" | "stderr" | "agent"; + +export type SyncedFlowStep = FlowStep & { + trigger?: FlowStep["trigger"] & { + schemaJson?: unknown; + }; +}; + +export type SyncedFlowManifest = { + name: string; + version: number; + description?: string; + root?: string; + config?: Record; + steps: SyncedFlowStep[]; +}; + +export type ClaimedConvexFlowRun = { + runId: string; + attemptId: string; + leaseToken: string; + leaseExpiresAt: number; + flowName: string; + stepName: string; + runner: FlowStep["runner"]; + event: FlowEvent; +}; + +export type DispatchConvexFlowEventResult = { + status: "accepted" | "duplicate"; + eventId: string; + runIds: string[]; + matched: number; +}; + +export type CompleteConvexFlowRunInput = { + attemptId: string; + leaseToken: string; + result: FlowResult; +}; diff --git a/packages/flow-backend-convex/test/backend-model.test.ts b/packages/flow-backend-convex/test/backend-model.test.ts new file mode 100644 index 0000000..fb31f9f --- /dev/null +++ b/packages/flow-backend-convex/test/backend-model.test.ts @@ -0,0 +1,87 @@ +import { expect, test } from "bun:test"; +import { + acceptedDispatchResult, + duplicateDispatchResult, + flowRunId, + matchingManifestSteps, + normalizeFlowEvent, +} from "../src/backend-model.ts"; + +test("normalizes generic flow events", () => { + const event = normalizeFlowEvent({ + id: "event-1", + type: "pet-game.player_asset_generation.requested", + payload: { requestId: "request-1" }, + }); + + expect(event).toMatchObject({ + id: "event-1", + type: "pet-game.player_asset_generation.requested", + payload: { requestId: "request-1" }, + }); + expect(typeof event.receivedAt).toBe("string"); +}); + +test("matches synced manifest steps by event type", () => { + const matches = matchingManifestSteps( + [ + { + name: "player-character-asset", + version: 1, + steps: [ + { + name: "generate", + runner: "bun", + script: "exec/generate.ts", + timeoutMs: 1000, + trigger: { type: "pet-game.player_asset_generation.requested" }, + }, + ], + }, + ], + { + id: "event-1", + type: "pet-game.player_asset_generation.requested", + receivedAt: "2026-05-13T00:00:00.000Z", + payload: {}, + }, + ); + + expect(matches.map((match) => `${match.manifest.name}/${match.step.name}`)).toEqual([ + "player-character-asset/generate", + ]); +}); + +test("builds stable run ids and dispatch result shapes", () => { + const first = flowRunId({ + eventId: "event-1", + flowName: "player-character-asset", + stepName: "generate", + }); + const second = flowRunId({ + eventId: "event-1", + flowName: "player-character-asset", + stepName: "generate", + }); + const replay = flowRunId({ + eventId: "event-1", + flowName: "player-character-asset", + stepName: "generate", + replayNonce: "1", + }); + + expect(first).toBe(second); + expect(replay).not.toBe(first); + expect(acceptedDispatchResult("event-1", [first], 1)).toEqual({ + status: "accepted", + eventId: "event-1", + runIds: [first], + matched: 1, + }); + expect(duplicateDispatchResult("event-1", [first])).toEqual({ + status: "duplicate", + eventId: "event-1", + runIds: [first], + matched: 0, + }); +}); diff --git a/packages/flow-backend-convex/tsconfig.json b/packages/flow-backend-convex/tsconfig.json new file mode 100644 index 0000000..3b2441e --- /dev/null +++ b/packages/flow-backend-convex/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "target": "ES2022", + "lib": ["ESNext", "DOM"], + "strict": true, + "skipLibCheck": true, + "noUncheckedIndexedAccess": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "types": ["node", "bun"], + "baseUrl": ".", + "paths": { + "@peezy.tech/flow-runtime": ["../flow-runtime/src/index.ts"] + } + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/packages/flow-runtime/src/runners/bun.ts b/packages/flow-runtime/src/runners/bun.ts index 5bfc80f..fe790a3 100644 --- a/packages/flow-runtime/src/runners/bun.ts +++ b/packages/flow-runtime/src/runners/bun.ts @@ -30,8 +30,8 @@ export async function runBunStep(options: RunBunStepOptions): Promise subprocess.kill("SIGTERM"), options.step.timeoutMs); const [stdout, stderr, exitCode] = await Promise.all([ - subprocess.stdout.text(), - subprocess.stderr.text(), + new Response(subprocess.stdout).text(), + new Response(subprocess.stderr).text(), subprocess.exited, ]).finally(() => clearTimeout(timer)); if (exitCode !== 0) {