This commit is contained in:
parent
8bb6072006
commit
f48434932d
18 changed files with 1525 additions and 31 deletions
131
bun.lock
131
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=="],
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
49
packages/flow-backend-convex/README.md
Normal file
49
packages/flow-backend-convex/README.md
Normal 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.
|
||||
30
packages/flow-backend-convex/package.json
Normal file
30
packages/flow-backend-convex/package.json
Normal 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:"
|
||||
}
|
||||
}
|
||||
91
packages/flow-backend-convex/src/backend-model.ts
Normal file
91
packages/flow-backend-convex/src/backend-model.ts
Normal 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"
|
||||
);
|
||||
}
|
||||
50
packages/flow-backend-convex/src/component/_generated/api.ts
Normal file
50
packages/flow-backend-convex/src/component/_generated/api.ts
Normal 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 {};
|
||||
|
|
@ -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
|
||||
>;
|
||||
};
|
||||
};
|
||||
|
|
@ -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>;
|
||||
156
packages/flow-backend-convex/src/component/_generated/server.ts
Normal file
156
packages/flow-backend-convex/src/component/_generated/server.ts
Normal 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>;
|
||||
508
packages/flow-backend-convex/src/component/backend.ts
Normal file
508
packages/flow-backend-convex/src/component/backend.ts
Normal 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));
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { defineComponent } from "convex/server";
|
||||
|
||||
const component = defineComponent("flowBackend");
|
||||
|
||||
export default component;
|
||||
124
packages/flow-backend-convex/src/component/schema.ts
Normal file
124
packages/flow-backend-convex/src/component/schema.ts
Normal 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"]),
|
||||
});
|
||||
19
packages/flow-backend-convex/src/index.ts
Normal file
19
packages/flow-backend-convex/src/index.ts
Normal 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";
|
||||
51
packages/flow-backend-convex/src/types.ts
Normal file
51
packages/flow-backend-convex/src/types.ts
Normal 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;
|
||||
};
|
||||
87
packages/flow-backend-convex/test/backend-model.test.ts
Normal file
87
packages/flow-backend-convex/test/backend-model.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
24
packages/flow-backend-convex/tsconfig.json
Normal file
24
packages/flow-backend-convex/tsconfig.json
Normal 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"]
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue