Add Convex flow backend component
Some checks failed
ci / check (push) Failing after 19s

This commit is contained in:
matamune 2026-05-13 12:59:04 +00:00
parent 8bb6072006
commit f48434932d
Signed by: matamune
GPG key ID: 3BB8E7D3B968A324
18 changed files with 1525 additions and 31 deletions

131
bun.lock
View file

@ -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=="],

View file

@ -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:

View file

@ -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"
}
}

View file

@ -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.

View file

@ -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:"
}
}

View file

@ -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<string, unknown> {
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"
);
}

View file

@ -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<any, "public">
> = 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<any, "internal">
> = anyApi as any;
export const components = componentsGeneric() as unknown as {};

View file

@ -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<Name extends string | undefined = string | undefined> =
{
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
>;
};
};

View file

@ -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<DataModel>;
/**
* The type of a document stored in Convex.
*
* @typeParam TableName - A string literal type of the table name (like "users").
*/
export type Doc<TableName extends TableNames> = 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<TableName extends TableNames | SystemTableNames> =
GenericId<TableName>;
/**
* 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<typeof schema>;

View file

@ -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<DataModel, "public"> = 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<DataModel, "internal"> =
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<DataModel, "public"> = 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<DataModel, "internal"> =
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<DataModel, "public"> = 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<DataModel, "internal"> =
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<DataModel>;
/**
* 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<DataModel>;
/**
* 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<DataModel>;
/**
* 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<DataModel>;
/**
* 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<DataModel>;

View file

@ -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));
}

View file

@ -0,0 +1,5 @@
import { defineComponent } from "convex/server";
const component = defineComponent("flowBackend");
export default component;

View file

@ -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"]),
});

View file

@ -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";

View file

@ -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<ConvexFlowRunStatus, "queued">;
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<string, unknown>;
steps: SyncedFlowStep[];
};
export type ClaimedConvexFlowRun<TPayload = unknown> = {
runId: string;
attemptId: string;
leaseToken: string;
leaseExpiresAt: number;
flowName: string;
stepName: string;
runner: FlowStep["runner"];
event: FlowEvent<TPayload>;
};
export type DispatchConvexFlowEventResult = {
status: "accepted" | "duplicate";
eventId: string;
runIds: string[];
matched: number;
};
export type CompleteConvexFlowRunInput = {
attemptId: string;
leaseToken: string;
result: FlowResult;
};

View file

@ -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,
});
});

View file

@ -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"]
}

View file

@ -30,8 +30,8 @@ export async function runBunStep(options: RunBunStepOptions): Promise<FlowResult
subprocess.stdin.end();
const timer = setTimeout(() => 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) {