From e71a3a3d42e06d34839e21143769636a1448e142 Mon Sep 17 00:00:00 2001 From: vilmire Date: Mon, 25 May 2026 17:25:40 +0900 Subject: [PATCH 1/2] feat: add BeadsDB mesh queue backend --- package-lock.json | 400 +++++++++++++++++- packages/daemon-core/package.json | 2 + packages/daemon-core/src/mesh/beads-db.ts | 155 +++++++ .../daemon-core/src/mesh/mesh-work-queue.ts | 60 +-- .../daemon-core/test/mesh/mesh-events.test.ts | 6 +- .../test/mesh/mesh-work-queue.test.ts | 44 +- 6 files changed, 605 insertions(+), 62 deletions(-) create mode 100644 packages/daemon-core/src/mesh/beads-db.ts diff --git a/package-lock.json b/package-lock.json index 3a6ab698..09f5d728 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "adhdev", - "version": "0.9.81", + "version": "0.9.82-rc.62", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "adhdev", - "version": "0.9.81", + "version": "0.9.82-rc.62", "license": "AGPL-3.0", "workspaces": [ "packages/*" @@ -1989,6 +1989,16 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -2521,6 +2531,26 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.8", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", @@ -2534,6 +2564,40 @@ "node": ">=6.0.0" } }, + "node_modules/better-sqlite3": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", + "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -2592,6 +2656,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -3203,11 +3291,25 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4.0.0" @@ -3275,7 +3377,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -3380,6 +3481,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -3563,6 +3673,15 @@ "node": ">=18.0.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -3680,6 +3799,12 @@ } } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -3731,6 +3856,12 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.3.4", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", @@ -3840,6 +3971,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3978,6 +4115,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3988,7 +4145,6 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, "license": "ISC" }, "node_modules/inline-style-parser": { @@ -5513,11 +5669,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5546,6 +5713,12 @@ "node": ">= 18" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/mlly": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz", @@ -5607,6 +5780,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -5616,6 +5795,30 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -5900,6 +6103,33 @@ } } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -5923,6 +6153,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.15.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", @@ -5966,7 +6206,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -6072,6 +6311,20 @@ "react-dom": ">=16.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -6334,6 +6587,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -6510,6 +6783,51 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -6569,6 +6887,15 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -6615,7 +6942,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6725,6 +7051,40 @@ "node": ">=18" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -6926,6 +7286,18 @@ "fsevents": "~2.3.3" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", @@ -7145,6 +7517,12 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -8080,12 +8458,13 @@ }, "packages/daemon-core": { "name": "@adhdev/daemon-core", - "version": "0.9.81", + "version": "0.9.82-rc.62", "license": "AGPL-3.0-or-later", "dependencies": { "@adhdev/session-host-core": "*", "@agentclientprotocol/sdk": "^0.16.1", "@xterm/xterm": "^6.0.0", + "better-sqlite3": "^12.10.0", "chalk": "^5.3.0", "chokidar": "^4.0.3", "conf": "^13.0.0", @@ -8094,6 +8473,7 @@ "ws": "^8.19.0" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/js-yaml": "^4.0.9", "@types/node": "^22.0.0", "@types/ws": "^8.18.1", @@ -8107,7 +8487,7 @@ }, "packages/daemon-standalone": { "name": "@adhdev/daemon-standalone", - "version": "0.9.81", + "version": "0.9.82-rc.62", "hasInstallScript": true, "license": "AGPL-3.0-or-later", "dependencies": { diff --git a/packages/daemon-core/package.json b/packages/daemon-core/package.json index 79e5bb3e..7ccaf21f 100644 --- a/packages/daemon-core/package.json +++ b/packages/daemon-core/package.json @@ -49,6 +49,7 @@ "@adhdev/session-host-core": "*", "@agentclientprotocol/sdk": "^0.16.1", "@xterm/xterm": "^6.0.0", + "better-sqlite3": "^12.10.0", "chalk": "^5.3.0", "chokidar": "^4.0.3", "conf": "^13.0.0", @@ -60,6 +61,7 @@ "@adhdev/ghostty-vt-node": "*" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/js-yaml": "^4.0.9", "@types/node": "^22.0.0", "@types/ws": "^8.18.1", diff --git a/packages/daemon-core/src/mesh/beads-db.ts b/packages/daemon-core/src/mesh/beads-db.ts new file mode 100644 index 00000000..d049a97c --- /dev/null +++ b/packages/daemon-core/src/mesh/beads-db.ts @@ -0,0 +1,155 @@ +import Database from 'better-sqlite3'; +import { existsSync, mkdirSync, readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { getLedgerDir } from './mesh-ledger.js'; +import type { MeshTaskStatus, MeshWorkQueueEntry } from './mesh-work-queue.js'; + +function safeMeshId(meshId: string): string { + return meshId.replace(/[^a-zA-Z0-9_-]/g, '_'); +} + +function legacyQueuePath(meshId: string): string { + return join(getLedgerDir(), `${safeMeshId(meshId)}.queue.json`); +} + +export class BeadsDB { + private static instance: BeadsDB | undefined; + private readonly db: Database.Database; + private readonly migratedMeshIds = new Set(); + + private constructor(dbPath: string) { + const dir = dirname(dbPath); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + + this.db = new Database(dbPath); + this.db.pragma('journal_mode = WAL'); + this.db.pragma('synchronous = NORMAL'); + this.db.pragma('foreign_keys = ON'); + this.db.pragma('busy_timeout = 5000'); + this.migrate(); + } + + static getInstance(): BeadsDB { + if (!this.instance) { + this.instance = new BeadsDB(join(getLedgerDir(), 'beads.db')); + } + return this.instance; + } + + static resetForTests(): void { + this.instance?.close(); + this.instance = undefined; + } + + close(): void { + this.db.close(); + } + + transaction(fn: () => T): T { + return this.db.transaction(fn).immediate(); + } + + private migrate(): void { + this.db.exec(` + CREATE TABLE IF NOT EXISTS mesh_queue ( + id TEXT PRIMARY KEY, + mesh_id TEXT NOT NULL, + status TEXT NOT NULL, + target_node_id TEXT, + target_session_id TEXT, + assigned_node_id TEXT, + assigned_session_id TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + payload TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_mesh_queue_mesh_status_created + ON mesh_queue(mesh_id, status, created_at); + CREATE INDEX IF NOT EXISTS idx_mesh_queue_assignment + ON mesh_queue(mesh_id, assigned_node_id, assigned_session_id, status); + `); + } + + private ensureLegacyQueueMigrated(meshId: string): void { + if (this.migratedMeshIds.has(meshId)) return; + this.migratedMeshIds.add(meshId); + + const count = this.db + .prepare('SELECT COUNT(*) AS count FROM mesh_queue WHERE mesh_id = ?') + .get(meshId) as { count: number }; + if (count.count > 0) return; + + const path = legacyQueuePath(meshId); + if (!existsSync(path)) return; + + try { + const entries = JSON.parse(readFileSync(path, 'utf-8')) as MeshWorkQueueEntry[]; + if (!Array.isArray(entries)) return; + const insert = this.db.prepare(` + INSERT OR REPLACE INTO mesh_queue ( + id, mesh_id, status, target_node_id, target_session_id, + assigned_node_id, assigned_session_id, created_at, updated_at, payload + ) VALUES ( + @id, @meshId, @status, @targetNodeId, @targetSessionId, + @assignedNodeId, @assignedSessionId, @createdAt, @updatedAt, @payload + ) + `); + for (const entry of entries) { + insert.run(this.toRow(entry)); + } + } catch { + return; + } + } + + getQueueEntries(meshId: string, statuses?: MeshTaskStatus[]): MeshWorkQueueEntry[] { + this.ensureLegacyQueueMigrated(meshId); + if (statuses?.length) { + const placeholders = statuses.map(() => '?').join(', '); + const rows = this.db + .prepare(`SELECT payload FROM mesh_queue WHERE mesh_id = ? AND status IN (${placeholders}) ORDER BY created_at ASC`) + .all(meshId, ...statuses) as Array<{ payload: string }>; + return rows.map(row => JSON.parse(row.payload) as MeshWorkQueueEntry); + } + const rows = this.db + .prepare('SELECT payload FROM mesh_queue WHERE mesh_id = ? ORDER BY created_at ASC') + .all(meshId) as Array<{ payload: string }>; + return rows.map(row => JSON.parse(row.payload) as MeshWorkQueueEntry); + } + + replaceQueue(meshId: string, queue: MeshWorkQueueEntry[]): void { + const deleteStmt = this.db.prepare('DELETE FROM mesh_queue WHERE mesh_id = ?'); + const insert = this.db.prepare(` + INSERT INTO mesh_queue ( + id, mesh_id, status, target_node_id, target_session_id, + assigned_node_id, assigned_session_id, created_at, updated_at, payload + ) VALUES ( + @id, @meshId, @status, @targetNodeId, @targetSessionId, + @assignedNodeId, @assignedSessionId, @createdAt, @updatedAt, @payload + ) + `); + deleteStmt.run(meshId); + for (const entry of queue) insert.run(this.toRow(entry)); + } + + deleteQueue(meshId: string): void { + this.db.prepare('DELETE FROM mesh_queue WHERE mesh_id = ?').run(meshId); + this.migratedMeshIds.delete(meshId); + } + + private toRow(entry: MeshWorkQueueEntry): Record { + return { + id: entry.id, + meshId: entry.meshId, + status: entry.status, + targetNodeId: entry.targetNodeId ?? null, + targetSessionId: entry.targetSessionId ?? null, + assignedNodeId: entry.assignedNodeId ?? null, + assignedSessionId: entry.assignedSessionId ?? null, + createdAt: entry.createdAt, + updatedAt: entry.updatedAt, + payload: JSON.stringify(entry), + }; + } +} diff --git a/packages/daemon-core/src/mesh/mesh-work-queue.ts b/packages/daemon-core/src/mesh/mesh-work-queue.ts index 5dea804c..d89cb630 100644 --- a/packages/daemon-core/src/mesh/mesh-work-queue.ts +++ b/packages/daemon-core/src/mesh/mesh-work-queue.ts @@ -1,9 +1,7 @@ -import { existsSync, writeFileSync, readFileSync, openSync, closeSync, unlinkSync } from 'fs'; -import { join } from 'path'; import { randomUUID } from 'crypto'; -import { getLedgerDir } from './mesh-ledger.js'; import { requireMeshHostQueueOwner } from './mesh-host-ownership.js'; import type { RepoMeshDaemonRole } from '../repo-mesh-types.js'; +import { BeadsDB } from './beads-db.js'; export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled'; export type MeshActiveTaskStatus = Extract; @@ -99,50 +97,16 @@ export interface MeshQueueMutationOptions { ownerRole?: RepoMeshDaemonRole; } -function getQueuePath(meshId: string): string { - const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_'); - return join(getLedgerDir(), `${safe}.queue.json`); -} - -function getLockPath(meshId: string): string { - const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_'); - return join(getLedgerDir(), `${safe}.queue.lock`); -} - -/** - * Simple advisory file lock using O_EXCL (atomic create) for queue mutations. - * Retries up to 10 times at 30 ms intervals; proceeds without lock on timeout - * to prevent deadlock (best-effort — far better than no locking at all). - */ -function withQueueLock(meshId: string, fn: () => T): T { - const lockPath = getLockPath(meshId); - let fd = -1; - for (let i = 0; i < 10; i++) { - try { fd = openSync(lockPath, 'wx'); break; } catch { - const deadline = Date.now() + 30; - while (Date.now() < deadline) { /* spin */ } - } - } - try { return fn(); } finally { - if (fd !== -1) try { closeSync(fd); } catch { /* noop */ } - try { unlinkSync(lockPath); } catch { /* already removed */ } - } +function withQueueLock(_meshId: string, fn: () => T): T { + return BeadsDB.getInstance().transaction(fn); } function readQueue(meshId: string): MeshWorkQueueEntry[] { - const path = getQueuePath(meshId); - if (!existsSync(path)) return []; - try { - const content = readFileSync(path, 'utf-8'); - return JSON.parse(content) as MeshWorkQueueEntry[]; - } catch { - return []; - } + return BeadsDB.getInstance().getQueueEntries(meshId); } function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void { - const path = getQueuePath(meshId); - writeFileSync(path, JSON.stringify(queue, null, 2), 'utf-8'); + BeadsDB.getInstance().replaceQueue(meshId, queue); } /** @@ -408,3 +372,17 @@ export function getMeshQueueStats(meshId: string): MeshWorkQueueStats { })), }; } + +export function __replaceMeshQueueForTests(meshId: string, queue: MeshWorkQueueEntry[]): void { + BeadsDB.getInstance().transaction(() => { + BeadsDB.getInstance().replaceQueue(meshId, queue); + }); +} + +export function __clearMeshQueueForTests(meshId: string): void { + BeadsDB.getInstance().deleteQueue(meshId); +} + +export function __resetBeadsDBForTests(): void { + BeadsDB.resetForTests(); +} diff --git a/packages/daemon-core/test/mesh/mesh-events.test.ts b/packages/daemon-core/test/mesh/mesh-events.test.ts index 77cf7861..225c8e40 100644 --- a/packages/daemon-core/test/mesh/mesh-events.test.ts +++ b/packages/daemon-core/test/mesh/mesh-events.test.ts @@ -21,7 +21,7 @@ vi.mock('../../src/detection/cli-detector.js', () => ({ })) import { handleMeshForwardEvent, setupMeshEventForwarding, triggerMeshQueue } from '../../src/mesh/mesh-events.js' -import { claimNextTask, enqueueTask, getQueue } from '../../src/mesh/mesh-work-queue.js' +import { __clearMeshQueueForTests, __resetBeadsDBForTests, claimNextTask, enqueueTask, getQueue } from '../../src/mesh/mesh-work-queue.js' import { getLedgerDir, readLedgerEntries, appendLedgerEntry, getLedgerSummary } from '../../src/mesh/mesh-ledger.js' function createComponents(meshId = 'mesh_inline_1') { @@ -69,6 +69,8 @@ function createComponents(meshId = 'mesh_inline_1') { function cleanupMeshFiles(meshId: string) { const queuePath = path.join(getLedgerDir(), `${meshId}.queue.json`) const ledgerPath = path.join(getLedgerDir(), `${meshId}.jsonl`) + __clearMeshQueueForTests(meshId) + __resetBeadsDBForTests() if (fs.existsSync(queuePath)) fs.unlinkSync(queuePath) if (fs.existsSync(ledgerPath)) fs.unlinkSync(ledgerPath) } @@ -137,7 +139,6 @@ describe('setupMeshEventForwarding', () => { it('marks the assigned queue task completed when a completion event only carries instanceId', () => { const meshId = `mesh_completion_fallback_${Date.now()}` - const queuePath = path.join(getLedgerDir(), `${meshId}.queue.json`) try { meshConfigMocks.getMesh.mockReturnValue({ id: meshId, @@ -190,7 +191,6 @@ describe('setupMeshEventForwarding', () => { }) } finally { cleanupMeshFiles(meshId) - if (fs.existsSync(queuePath)) fs.unlinkSync(queuePath) } }) diff --git a/packages/daemon-core/test/mesh/mesh-work-queue.test.ts b/packages/daemon-core/test/mesh/mesh-work-queue.test.ts index f0008ff6..4fc61406 100644 --- a/packages/daemon-core/test/mesh/mesh-work-queue.test.ts +++ b/packages/daemon-core/test/mesh/mesh-work-queue.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it, beforeEach, afterEach } from 'vitest'; import * as path from 'path'; import * as fs from 'fs'; -import * as os from 'os'; import { enqueueTask, getQueue, @@ -10,7 +9,10 @@ import { updateSessionTaskStatus, cancelTask, requeueTask, - getMeshQueueStats + getMeshQueueStats, + __clearMeshQueueForTests, + __replaceMeshQueueForTests, + __resetBeadsDBForTests } from '../../src/mesh/mesh-work-queue.js'; import { getLedgerDir } from '../../src/mesh/mesh-ledger.js'; @@ -19,12 +21,15 @@ describe('Mesh Work Queue (GUPP)', () => { const queuePath = path.join(getLedgerDir(), `${meshId}.queue.json`); beforeEach(() => { + __clearMeshQueueForTests(meshId); if (fs.existsSync(queuePath)) { fs.unlinkSync(queuePath); } }); afterEach(() => { + __clearMeshQueueForTests(meshId); + __resetBeadsDBForTests(); if (fs.existsSync(queuePath)) { fs.unlinkSync(queuePath); } @@ -40,6 +45,29 @@ describe('Mesh Work Queue (GUPP)', () => { expect(queue[0].id).to.equal(task.id); }); + it('imports an existing JSON queue into BeadsDB on first read', () => { + const now = new Date().toISOString(); + const legacyTask = { + id: 'legacy-task-1', + meshId, + message: 'legacy queued task', + status: 'pending' as const, + createdAt: now, + updatedAt: now, + }; + fs.writeFileSync(queuePath, JSON.stringify([legacyTask], null, 2), 'utf-8'); + __resetBeadsDBForTests(); + + const queue = getQueue(meshId); + + expect(queue).to.have.length(1); + expect(queue[0]).to.deep.include({ + id: 'legacy-task-1', + message: 'legacy queued task', + status: 'pending', + }); + }); + it('blocks queue ownership mutations from member daemons when ownership is declared', () => { expect(() => enqueueTask(meshId, 'member task', { ownerRole: 'member' } as any)) .to.throw(/Mesh Host/); @@ -164,7 +192,7 @@ describe('Mesh Work Queue (GUPP)', () => { // Manually assign both tasks to the same session (bypassing claimNextTask guard) // to simulate the edge case where assignment tracking drifts - const queue = JSON.parse(fs.readFileSync(queuePath, 'utf-8')); + const queue = getQueue(meshId); const now = Date.now(); queue[0].status = 'assigned'; queue[0].assignedNodeId = 'node1'; @@ -178,7 +206,7 @@ describe('Mesh Work Queue (GUPP)', () => { queue[1].dispatchTimestamp = new Date(now).toISOString(); queue[1].updatedAt = new Date(now).toISOString(); - fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2)); + __replaceMeshQueueForTests(meshId, queue); // Completion should target the most recently dispatched task (t2) const completed = updateSessionTaskStatus(meshId, 'session1', 'completed'); @@ -198,7 +226,7 @@ describe('Mesh Work Queue (GUPP)', () => { const t1 = enqueueTask(meshId, 'legacy task 1'); const t2 = enqueueTask(meshId, 'legacy task 2'); - const queue = JSON.parse(fs.readFileSync(queuePath, 'utf-8')); + const queue = getQueue(meshId); const now = Date.now(); queue[0].status = 'assigned'; queue[0].assignedNodeId = 'node1'; @@ -212,7 +240,7 @@ describe('Mesh Work Queue (GUPP)', () => { // No dispatchTimestamp — legacy entry queue[1].updatedAt = new Date(now).toISOString(); - fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2)); + __replaceMeshQueueForTests(meshId, queue); const completed = updateSessionTaskStatus(meshId, 'session1', 'completed'); expect(completed?.id).to.equal(t2.id); @@ -226,7 +254,7 @@ describe('Mesh Work Queue (GUPP)', () => { const olderTask = enqueueTask(meshId, 'older task'); const newerTask = enqueueTask(meshId, 'newer continuation task'); - const queue = JSON.parse(fs.readFileSync(queuePath, 'utf-8')); + const queue = getQueue(meshId); const now = Date.now(); const staleCompletionAt = new Date(now).toISOString(); @@ -242,7 +270,7 @@ describe('Mesh Work Queue (GUPP)', () => { queue[1].dispatchTimestamp = new Date(now + 5000).toISOString(); queue[1].updatedAt = new Date(now + 5000).toISOString(); - fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2)); + __replaceMeshQueueForTests(meshId, queue); const completed = updateSessionTaskStatus(meshId, 'session1', 'completed', { occurredAt: staleCompletionAt }); expect(completed).to.be.null; From 5a99ab25f9adadbd19b2e4011c890a71ee84151b Mon Sep 17 00:00:00 2001 From: vilmire Date: Mon, 25 May 2026 18:53:51 +0900 Subject: [PATCH 2/2] fix: keep mesh status queue summary fresh --- packages/daemon-core/src/commands/router.ts | 6 ++- packages/daemon-core/src/index.ts | 2 +- packages/daemon-core/src/mesh/beads-db.ts | 8 +++ .../daemon-core/src/mesh/mesh-work-queue.ts | 4 ++ .../test/commands/mesh-status.test.ts | 51 +++++++++++++++++++ packages/mcp-server/src/tools/mesh-tools.ts | 3 ++ 6 files changed, 71 insertions(+), 3 deletions(-) diff --git a/packages/daemon-core/src/commands/router.ts b/packages/daemon-core/src/commands/router.ts index 5ba92284..8b5f2fd5 100644 --- a/packages/daemon-core/src/commands/router.ts +++ b/packages/daemon-core/src/commands/router.ts @@ -53,6 +53,7 @@ import { import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js'; import { getSessionCompletionMarker } from '../status/snapshot.js'; import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js'; +import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js'; import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js'; import { homedir } from 'os'; import { join as pathJoin, resolve as pathResolve } from 'path'; @@ -1706,7 +1707,7 @@ export class DaemonCommandRouter { * the mesh doesn't exist in the local meshes.json file. */ private inlineMeshCache = new Map(); /** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */ - private aggregateMeshStatusCache = new Map(); + private aggregateMeshStatusCache = new Map(); /** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */ private runningRefineJobs = new Map(); /** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */ @@ -1796,6 +1797,7 @@ export class DaemonCommandRouter { private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null { const cached = this.aggregateMeshStatusCache.get(meshId); if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null; + if (cached.queueRevision !== getMeshQueueRevision(meshId)) return null; let snapshot = this.cloneJsonValue(cached.snapshot); snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options); if (shouldRefreshStalePendingAggregate(snapshot, options)) return null; @@ -1840,7 +1842,7 @@ export class DaemonCommandRouter { returnedAt: new Date(builtAt).toISOString(), }, }; - this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next) }); + this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next), queueRevision: getMeshQueueRevision(meshId) }); return next; } diff --git a/packages/daemon-core/src/index.ts b/packages/daemon-core/src/index.ts index ed1ccade..e46f40e3 100644 --- a/packages/daemon-core/src/index.ts +++ b/packages/daemon-core/src/index.ts @@ -186,7 +186,7 @@ export { buildMeshLedgerReconciliationEvidence, buildMeshLedgerReplicaEvidence } export type { MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshLedgerReplicaStatus } from './mesh/mesh-ledger-reconciliation.js'; // ── Mesh Work Queue (GUPP) ── -export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, normalizeMeshTaskMode, validateMeshTaskModeRequest } from './mesh/mesh-work-queue.js'; +export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest } from './mesh/mesh-work-queue.js'; export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult } from './mesh/mesh-work-queue.js'; export { buildMeshActiveWork, buildMeshActiveWorkSummary } from './mesh/mesh-active-work.js'; export type { MeshActiveWorkRecord, MeshActiveWorkStatus, MeshActiveWorkSummary, MeshActiveWorkSource } from './mesh/mesh-active-work.js'; diff --git a/packages/daemon-core/src/mesh/beads-db.ts b/packages/daemon-core/src/mesh/beads-db.ts index d049a97c..e677747a 100644 --- a/packages/daemon-core/src/mesh/beads-db.ts +++ b/packages/daemon-core/src/mesh/beads-db.ts @@ -118,6 +118,14 @@ export class BeadsDB { return rows.map(row => JSON.parse(row.payload) as MeshWorkQueueEntry); } + getQueueRevision(meshId: string): string { + this.ensureLegacyQueueMigrated(meshId); + const rows = this.db + .prepare('SELECT id, status, updated_at FROM mesh_queue WHERE mesh_id = ? ORDER BY id ASC') + .all(meshId) as Array<{ id: string; status: string; updated_at: string }>; + return rows.map(row => `${row.id}:${row.status}:${row.updated_at}`).join('|'); + } + replaceQueue(meshId: string, queue: MeshWorkQueueEntry[]): void { const deleteStmt = this.db.prepare('DELETE FROM mesh_queue WHERE mesh_id = ?'); const insert = this.db.prepare(` diff --git a/packages/daemon-core/src/mesh/mesh-work-queue.ts b/packages/daemon-core/src/mesh/mesh-work-queue.ts index d89cb630..8d97d99f 100644 --- a/packages/daemon-core/src/mesh/mesh-work-queue.ts +++ b/packages/daemon-core/src/mesh/mesh-work-queue.ts @@ -153,6 +153,10 @@ export function getQueue(meshId: string, opts?: { status?: MeshTaskStatus[] }): return queue; } +export function getMeshQueueRevision(meshId: string): string { + return BeadsDB.getInstance().getQueueRevision(meshId); +} + /** * Find the next pending task that this node is allowed to claim, and mark it as assigned. */ diff --git a/packages/daemon-core/test/commands/mesh-status.test.ts b/packages/daemon-core/test/commands/mesh-status.test.ts index 2e4af72c..1c6fc118 100644 --- a/packages/daemon-core/test/commands/mesh-status.test.ts +++ b/packages/daemon-core/test/commands/mesh-status.test.ts @@ -826,6 +826,57 @@ describe('mesh_status', () => { } }) + it('does not reuse cached mesh_status after queue storage changes outside the router', async () => { + const configDir = await mkdtemp(join(tmpdir(), 'mesh-status-queue-cache-')) + const { dir, repoRoot } = await createTempGitRepo('mesh-status-queue-revision-') + const previousConfigDir = process.env.ADHDEV_CONFIG_DIR + + try { + process.env.ADHDEV_CONFIG_DIR = configDir + const { createMesh, addNode } = await import('../../src/config/mesh-config.js') + const { enqueueTask, __resetBeadsDBForTests } = await import('../../src/mesh/mesh-work-queue.js') + __resetBeadsDBForTests() + + const mesh = createMesh({ + name: 'Queue Cache Mesh', + repoIdentity: 'github.com/acme/queue-cache', + defaultBranch: 'master', + }) + addNode(mesh.id, { workspace: repoRoot, repoRoot }) + + const { router, sessionHostControl } = createRouter() + const initial = await router.execute('mesh_status', { meshId: mesh.id, refresh: true }) as any + expect(initial.success).toBe(true) + expect(initial.queue.summary.total).toBe(0) + expect(initial.sourceOfTruth.aggregateSnapshot.cached).toBe(false) + + sessionHostControl.listSessions.mockClear() + enqueueTask(mesh.id, 'queue task created by another mesh process') + + const afterQueueChange = await router.execute('mesh_status', { meshId: mesh.id }) as any + + expect(afterQueueChange.success).toBe(true) + expect(afterQueueChange.queue.summary.total).toBe(1) + expect(afterQueueChange.queue.tasks).toHaveLength(1) + expect(afterQueueChange.queue.tasks[0]).toEqual(expect.objectContaining({ + message: 'queue task created by another mesh process', + status: 'pending', + })) + expect(afterQueueChange.sourceOfTruth.aggregateSnapshot).toMatchObject({ + cached: false, + refreshReason: 'stale_pending_cache_refresh', + }) + expect(sessionHostControl.listSessions).toHaveBeenCalledTimes(1) + } finally { + const { __resetBeadsDBForTests } = await import('../../src/mesh/mesh-work-queue.js') + __resetBeadsDBForTests() + if (previousConfigDir === undefined) delete process.env.ADHDEV_CONFIG_DIR + else process.env.ADHDEV_CONFIG_DIR = previousConfigDir + await rm(configDir, { recursive: true, force: true }) + await rm(dir, { recursive: true, force: true }) + } + }) + it('refreshes instead of returning a stale pending cache hit when direct peer truth is required', async () => { const { dir, repoRoot } = await createTempGitRepo('mesh-status-stale-cache-refresh-') try { diff --git a/packages/mcp-server/src/tools/mesh-tools.ts b/packages/mcp-server/src/tools/mesh-tools.ts index 34f3d719..f4a6903a 100644 --- a/packages/mcp-server/src/tools/mesh-tools.ts +++ b/packages/mcp-server/src/tools/mesh-tools.ts @@ -2213,6 +2213,9 @@ export async function meshQueueCancel( if (!taskId) return JSON.stringify({ success: false, error: 'task_id required' }); const task = cancelTask(ctx.mesh.id, taskId, { reason: args.reason }); if (!task) return JSON.stringify({ success: false, error: `Queue task '${taskId}' not found` }); + if (isLocalTransport(ctx.transport)) { + ctx.transport.command('trigger_mesh_queue', { meshId: ctx.mesh.id }).catch(() => {}); + } return JSON.stringify({ success: true, task }, null, 2); } catch (e: any) { return JSON.stringify({ success: false, error: e.message });