From 06d1237ac5d096c1098a91af505854199e00710e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Sat, 14 Mar 2026 07:41:24 -0600 Subject: [PATCH] fix: replace better-sqlite3 with sql.js (WASM) to fix install on Node 25+ (#356) * fix: normalize .ts import extensions to .js for Node 22.22+ compatibility Node 22.22.0's --experimental-strip-types handles .ts import specifiers differently, causing ERR_INVALID_TYPESCRIPT_SYNTAX in CI. The project convention uses .js specifiers with a custom resolve hook that rewrites them to .ts at test time. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: replace better-sqlite3 with sql.js to eliminate native compilation failures better-sqlite3 requires prebuilt binaries or node-gyp compilation, which fails on newer Node versions (e.g. 25.x) that lack prebuilds. sql.js uses WASM-compiled SQLite with zero native dependencies, making installation reliable across all platforms and Node versions. Closes #355 Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- package-lock.json | 374 ++------------- packages/pi-coding-agent/package.json | 4 +- .../src/resources/extensions/memory/index.ts | 12 +- .../resources/extensions/memory/storage.ts | 431 +++++++++--------- 4 files changed, 238 insertions(+), 583 deletions(-) diff --git a/package-lock.json b/package-lock.json index 03b444326..4be46afa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2167,16 +2167,6 @@ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "license": "MIT" }, - "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/diff": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", @@ -2184,6 +2174,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/emscripten": { + "version": "1.41.5", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz", + "integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/hosted-git-info": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/hosted-git-info/-/hosted-git-info-3.0.5.tgz", @@ -2229,6 +2226,17 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, + "node_modules/@types/sql.js": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/sql.js/-/sql.js-1.4.9.tgz", + "integrity": "sha512-ep8b36RKHlgWPqjNG9ToUrPiwkhwh0AEzy883mO5Xnd+cL6VBH1EvSjBAAuxLUFF2Vn/moE3Me6v9E1Lo+48GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/emscripten": "*", + "@types/node": "*" + } + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -2343,17 +2351,6 @@ "node": ">=10.0.0" } }, - "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -2363,26 +2360,6 @@ "node": "*" } }, - "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/bowser": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", @@ -2401,30 +2378,6 @@ "node": "18 || 20 || >=22" } }, - "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/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -2452,12 +2405,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "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/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -2484,30 +2431,6 @@ } } }, - "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==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", @@ -2610,15 +2533,6 @@ "node": ">=0.10.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/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2751,12 +2665,6 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "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/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -2769,12 +2677,6 @@ "node": ">=12.20.0" } }, - "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/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2867,12 +2769,6 @@ "node": ">= 14" } }, - "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/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -2989,18 +2885,6 @@ "node": ">= 4" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -3132,18 +3016,6 @@ "url": "https://opencollective.com/express" } }, - "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/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -3159,15 +3031,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -3177,24 +3040,12 @@ "node": ">=16 || 14 >=14.17" } }, - "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/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "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/netmask": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", @@ -3204,18 +3055,6 @@ "node": ">= 0.4.0" } }, - "node_modules/node-abi": { - "version": "3.88.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.88.0.tgz", - "integrity": "sha512-At6b4UqIEVudaqPsXjmUO1r/N5BUr4yhDGs5PkBE8/oG5+TfLPhFechiskFsnT6Ql0VfUXbalUUCbfXxtj7K+w==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -3428,33 +3267,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "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/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -3543,35 +3355,6 @@ "once": "^1.3.1" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "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/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3672,51 +3455,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "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/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -3771,21 +3509,18 @@ "node": ">=0.10.0" } }, + "node_modules/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "license": "MIT" + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "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/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -3801,15 +3536,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strnum": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", @@ -3838,34 +3564,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "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-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/token-types": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", @@ -3896,18 +3594,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "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/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3949,12 +3635,6 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, - "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/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -4086,7 +3766,6 @@ "@gsd/pi-tui": "*", "@mariozechner/jiti": "^2.6.2", "@silvia-odwyer/photon-node": "^0.3.4", - "better-sqlite3": "^11.7.0", "chalk": "^5.5.0", "diff": "^8.0.2", "extract-zip": "^2.0.1", @@ -4097,15 +3776,16 @@ "marked": "^15.0.12", "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", + "sql.js": "^1.14.1", "strip-ansi": "^7.1.0", "undici": "^7.19.1", "yaml": "^2.8.2" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.12", "@types/diff": "^7.0.2", "@types/hosted-git-info": "^3.0.5", - "@types/proper-lockfile": "^4.1.4" + "@types/proper-lockfile": "^4.1.4", + "@types/sql.js": "^1.4.9" } }, "packages/pi-tui": { diff --git a/packages/pi-coding-agent/package.json b/packages/pi-coding-agent/package.json index 61010b64f..11e1231c1 100644 --- a/packages/pi-coding-agent/package.json +++ b/packages/pi-coding-agent/package.json @@ -42,11 +42,11 @@ "proper-lockfile": "^4.1.2", "strip-ansi": "^7.1.0", "undici": "^7.19.1", - "better-sqlite3": "^11.7.0", + "sql.js": "^1.14.1", "yaml": "^2.8.2" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.12", + "@types/sql.js": "^1.4.9", "@types/diff": "^7.0.2", "@types/hosted-git-info": "^3.0.5", "@types/proper-lockfile": "^4.1.4" diff --git a/packages/pi-coding-agent/src/resources/extensions/memory/index.ts b/packages/pi-coding-agent/src/resources/extensions/memory/index.ts index a6a6c4351..4565b2831 100644 --- a/packages/pi-coding-agent/src/resources/extensions/memory/index.ts +++ b/packages/pi-coding-agent/src/resources/extensions/memory/index.ts @@ -37,9 +37,9 @@ function getDbPath(): string { let storageInstance: MemoryStorage | null = null; -function getStorage(): MemoryStorage { +async function getStorage(): Promise { if (!storageInstance) { - storageInstance = new MemoryStorage(getDbPath()); + storageInstance = await MemoryStorage.create(getDbPath()); } return storageInstance; } @@ -125,7 +125,7 @@ export default function memoryExtension(api: ExtensionAPI): void { // Fire and forget runStartup( - getStorage(), + await getStorage(), { sessionsDir, memoryDir, @@ -203,7 +203,7 @@ export default function memoryExtension(api: ExtensionAPI): void { "Delete all extracted memories for this project?", ); if (confirmed) { - getStorage().clearForCwd(ctx.cwd); + (await getStorage()).clearForCwd(ctx.cwd); if (existsSync(projectMemoryDir)) { rmSync(projectMemoryDir, { recursive: true, force: true }); } @@ -218,7 +218,7 @@ export default function memoryExtension(api: ExtensionAPI): void { "Re-extract all memories from session history? This may take a while.", ); if (confirmed) { - getStorage().resetAllForCwd(ctx.cwd); + (await getStorage()).resetAllForCwd(ctx.cwd); if (existsSync(projectMemoryDir)) { rmSync(projectMemoryDir, { recursive: true, force: true }); } @@ -231,7 +231,7 @@ export default function memoryExtension(api: ExtensionAPI): void { } case "stats": { - const stats = getStorage().getStats(); + const stats = (await getStorage()).getStats(); const statsText = [ "Memory Pipeline Statistics:", ` Total sessions tracked: ${stats.totalThreads}`, diff --git a/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts b/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts index 1047a5062..dae388960 100644 --- a/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +++ b/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts @@ -7,9 +7,9 @@ * - jobs: lease-based job queue for pipeline phases */ -import Database from "better-sqlite3"; +import initSqlJs, { type Database as SqlJsDatabase } from "sql.js"; import { randomUUID } from "crypto"; -import { existsSync, mkdirSync } from "fs"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { dirname } from "path"; export interface ThreadRow { @@ -44,23 +44,40 @@ export interface JobRow { } export class MemoryStorage { - private db: Database.Database; + private db: SqlJsDatabase; + private dbPath: string; - constructor(dbPath: string) { + private constructor(db: SqlJsDatabase, dbPath: string) { + this.db = db; + this.dbPath = dbPath; + } + + static async create(dbPath: string): Promise { 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("busy_timeout = 5000"); - this.initSchema(); + const SQL = await initSqlJs(); + const buffer = existsSync(dbPath) ? readFileSync(dbPath) : undefined; + const db = buffer ? new SQL.Database(buffer) : new SQL.Database(); + + db.run("PRAGMA journal_mode = WAL"); + db.run("PRAGMA synchronous = NORMAL"); + db.run("PRAGMA busy_timeout = 5000"); + + const storage = new MemoryStorage(db, dbPath); + storage.initSchema(); + return storage; + } + + private persist(): void { + const data = this.db.export(); + writeFileSync(this.dbPath, Buffer.from(data)); } private initSchema(): void { - this.db.exec(` + this.db.run(` CREATE TABLE IF NOT EXISTS threads ( thread_id TEXT PRIMARY KEY, file_path TEXT NOT NULL, @@ -71,15 +88,17 @@ export class MemoryStorage { error_message TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - + ) + `); + this.db.run(` CREATE TABLE IF NOT EXISTS stage1_outputs ( thread_id TEXT PRIMARY KEY, extraction_json TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY (thread_id) REFERENCES threads(thread_id) ON DELETE CASCADE - ); - + ) + `); + this.db.run(` CREATE TABLE IF NOT EXISTS jobs ( id TEXT PRIMARY KEY, phase TEXT NOT NULL, @@ -91,12 +110,28 @@ export class MemoryStorage { error_message TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - - CREATE INDEX IF NOT EXISTS idx_jobs_phase_status ON jobs(phase, status); - CREATE INDEX IF NOT EXISTS idx_threads_status ON threads(status); - CREATE INDEX IF NOT EXISTS idx_threads_cwd ON threads(cwd); + ) `); + this.db.run("CREATE INDEX IF NOT EXISTS idx_jobs_phase_status ON jobs(phase, status)"); + this.db.run("CREATE INDEX IF NOT EXISTS idx_threads_status ON threads(status)"); + this.db.run("CREATE INDEX IF NOT EXISTS idx_threads_cwd ON threads(cwd)"); + this.persist(); + } + + private queryAll(sql: string, params: unknown[] = []): T[] { + const stmt = this.db.prepare(sql); + stmt.bind(params as (string | number | null | Uint8Array)[]); + const rows: T[] = []; + while (stmt.step()) { + rows.push(stmt.getAsObject() as T); + } + stmt.free(); + return rows; + } + + private queryOne(sql: string, params: unknown[] = []): T | undefined { + const rows = this.queryAll(sql, params); + return rows[0]; } /** @@ -116,47 +151,40 @@ export class MemoryStorage { let updated = 0; let skipped = 0; - const selectStmt = this.db.prepare( - "SELECT file_size, file_mtime, status FROM threads WHERE thread_id = ?", - ); - const insertStmt = this.db.prepare(` - INSERT INTO threads (thread_id, file_path, file_size, file_mtime, cwd, status) - VALUES (?, ?, ?, ?, ?, 'pending') - `); - const updateStmt = this.db.prepare(` - UPDATE threads SET file_path = ?, file_size = ?, file_mtime = ?, cwd = ?, - status = 'pending', updated_at = datetime('now') - WHERE thread_id = ? - `); - const insertJobStmt = this.db.prepare(` - INSERT OR IGNORE INTO jobs (id, phase, thread_id, status) - VALUES (?, 'stage1', ?, 'pending') - `); + for (const t of threads) { + const existing = this.queryOne<{ file_size: number; file_mtime: number; status: string }>( + "SELECT file_size, file_mtime, status FROM threads WHERE thread_id = ?", + [t.threadId], + ); - const upsertAll = this.db.transaction(() => { - for (const t of threads) { - const existing = selectStmt.get(t.threadId) as - | { file_size: number; file_mtime: number; status: string } - | undefined; - - if (!existing) { - insertStmt.run(t.threadId, t.filePath, t.fileSize, t.fileMtime, t.cwd); - insertJobStmt.run(randomUUID(), t.threadId); - inserted++; - } else if (existing.file_size !== t.fileSize || existing.file_mtime !== t.fileMtime) { - updateStmt.run(t.filePath, t.fileSize, t.fileMtime, t.cwd, t.threadId); - // Re-enqueue if file changed and previous processing is done - if (existing.status === "done" || existing.status === "error") { - insertJobStmt.run(randomUUID(), t.threadId); - } - updated++; - } else { - skipped++; + if (!existing) { + this.db.run( + "INSERT INTO threads (thread_id, file_path, file_size, file_mtime, cwd, status) VALUES (?, ?, ?, ?, ?, 'pending')", + [t.threadId, t.filePath, t.fileSize, t.fileMtime, t.cwd], + ); + this.db.run( + "INSERT OR IGNORE INTO jobs (id, phase, thread_id, status) VALUES (?, 'stage1', ?, 'pending')", + [randomUUID(), t.threadId], + ); + inserted++; + } else if (existing.file_size !== t.fileSize || existing.file_mtime !== t.fileMtime) { + this.db.run( + "UPDATE threads SET file_path = ?, file_size = ?, file_mtime = ?, cwd = ?, status = 'pending', updated_at = datetime('now') WHERE thread_id = ?", + [t.filePath, t.fileSize, t.fileMtime, t.cwd, t.threadId], + ); + if (existing.status === "done" || existing.status === "error") { + this.db.run( + "INSERT OR IGNORE INTO jobs (id, phase, thread_id, status) VALUES (?, 'stage1', ?, 'pending')", + [randomUUID(), t.threadId], + ); } + updated++; + } else { + skipped++; } - }); + } - upsertAll(); + this.persist(); return { inserted, updated, skipped }; } @@ -172,8 +200,8 @@ export class MemoryStorage { const token = randomUUID(); const expiresAt = new Date(Date.now() + leaseSeconds * 1000).toISOString(); - const claimStmt = this.db.prepare(` - UPDATE jobs SET + this.db.run( + `UPDATE jobs SET status = 'claimed', worker_id = ?, ownership_token = ?, @@ -184,16 +212,16 @@ export class MemoryStorage { WHERE phase = 'stage1' AND (status = 'pending' OR (status = 'claimed' AND lease_expires_at < datetime('now'))) LIMIT ? - ) - `); + )`, + [workerId, token, expiresAt, limit], + ); - const selectStmt = this.db.prepare(` - SELECT id, thread_id FROM jobs - WHERE ownership_token = ? AND status = 'claimed' - `); + const rows = this.queryAll<{ id: string; thread_id: string }>( + "SELECT id, thread_id FROM jobs WHERE ownership_token = ? AND status = 'claimed'", + [token], + ); - claimStmt.run(workerId, token, expiresAt, limit); - const rows = selectStmt.all(token) as Array<{ id: string; thread_id: string }>; + this.persist(); return rows.map((r) => ({ jobId: r.id, @@ -206,53 +234,34 @@ export class MemoryStorage { * Mark a stage1 job as complete and store the extraction output. */ completeStage1Job(threadId: string, output: string): void { - const completeAll = this.db.transaction(() => { - this.db - .prepare( - `UPDATE jobs SET status = 'done', updated_at = datetime('now') - WHERE thread_id = ? AND phase = 'stage1' AND status = 'claimed'`, - ) - .run(threadId); - - this.db - .prepare( - `INSERT OR REPLACE INTO stage1_outputs (thread_id, extraction_json, created_at) - VALUES (?, ?, datetime('now'))`, - ) - .run(threadId, output); - - this.db - .prepare( - `UPDATE threads SET status = 'done', updated_at = datetime('now') - WHERE thread_id = ?`, - ) - .run(threadId); - }); - - completeAll(); + this.db.run( + "UPDATE jobs SET status = 'done', updated_at = datetime('now') WHERE thread_id = ? AND phase = 'stage1' AND status = 'claimed'", + [threadId], + ); + this.db.run( + "INSERT OR REPLACE INTO stage1_outputs (thread_id, extraction_json, created_at) VALUES (?, ?, datetime('now'))", + [threadId, output], + ); + this.db.run( + "UPDATE threads SET status = 'done', updated_at = datetime('now') WHERE thread_id = ?", + [threadId], + ); + this.persist(); } /** * Mark a stage1 job as errored. */ failStage1Job(threadId: string, errorMessage: string): void { - const failAll = this.db.transaction(() => { - this.db - .prepare( - `UPDATE jobs SET status = 'error', error_message = ?, updated_at = datetime('now') - WHERE thread_id = ? AND phase = 'stage1' AND status = 'claimed'`, - ) - .run(errorMessage, threadId); - - this.db - .prepare( - `UPDATE threads SET status = 'error', error_message = ?, updated_at = datetime('now') - WHERE thread_id = ?`, - ) - .run(errorMessage, threadId); - }); - - failAll(); + this.db.run( + "UPDATE jobs SET status = 'error', error_message = ?, updated_at = datetime('now') WHERE thread_id = ? AND phase = 'stage1' AND status = 'claimed'", + [errorMessage, threadId], + ); + this.db.run( + "UPDATE threads SET status = 'error', error_message = ?, updated_at = datetime('now') WHERE thread_id = ?", + [errorMessage, threadId], + ); + this.persist(); } /** @@ -266,73 +275,58 @@ export class MemoryStorage { const token = randomUUID(); const expiresAt = new Date(Date.now() + leaseSeconds * 1000).toISOString(); - const result = this.db.transaction(() => { - // Check if all stage1 jobs are done - const pendingStage1 = this.db - .prepare( - `SELECT COUNT(*) as cnt FROM jobs - WHERE phase = 'stage1' AND status IN ('pending', 'claimed')`, - ) - .get() as { cnt: number }; + const pendingStage1 = this.queryOne<{ cnt: number }>( + "SELECT COUNT(*) as cnt FROM jobs WHERE phase = 'stage1' AND status IN ('pending', 'claimed')", + ); - if (pendingStage1.cnt > 0) { - return null; - } + if (pendingStage1 && pendingStage1.cnt > 0) { + return null; + } - // Check if there's already an active phase2 job - const existingPhase2 = this.db - .prepare( - `SELECT id FROM jobs - WHERE phase = 'stage2' AND status = 'claimed' AND lease_expires_at > datetime('now')`, - ) - .get(); + const existingPhase2 = this.queryOne<{ id: string }>( + "SELECT id FROM jobs WHERE phase = 'stage2' AND status = 'claimed' AND lease_expires_at > datetime('now')", + ); - if (existingPhase2) { - return null; - } + if (existingPhase2) { + return null; + } - // Check if there are any stage1 outputs to consolidate - const outputCount = this.db - .prepare("SELECT COUNT(*) as cnt FROM stage1_outputs") - .get() as { cnt: number }; + const outputCount = this.queryOne<{ cnt: number }>( + "SELECT COUNT(*) as cnt FROM stage1_outputs", + ); - if (outputCount.cnt === 0) { - return null; - } + if (!outputCount || outputCount.cnt === 0) { + return null; + } - const jobId = randomUUID(); - this.db - .prepare( - `INSERT INTO jobs (id, phase, status, worker_id, ownership_token, lease_expires_at) - VALUES (?, 'stage2', 'claimed', ?, ?, ?)`, - ) - .run(jobId, workerId, token, expiresAt); + const jobId = randomUUID(); + this.db.run( + "INSERT INTO jobs (id, phase, status, worker_id, ownership_token, lease_expires_at) VALUES (?, 'stage2', 'claimed', ?, ?, ?)", + [jobId, workerId, token, expiresAt], + ); - return { jobId, ownershipToken: token }; - })(); - - return result; + this.persist(); + return { jobId, ownershipToken: token }; } /** * Complete the phase 2 consolidation job. */ completePhase2Job(jobId: string): void { - this.db - .prepare( - `UPDATE jobs SET status = 'done', updated_at = datetime('now') - WHERE id = ? AND phase = 'stage2'`, - ) - .run(jobId); + this.db.run( + "UPDATE jobs SET status = 'done', updated_at = datetime('now') WHERE id = ? AND phase = 'stage2'", + [jobId], + ); + this.persist(); } /** * Get all stage1 extraction outputs. */ getStage1Outputs(): Array<{ threadId: string; extractionJson: string }> { - const rows = this.db - .prepare("SELECT thread_id, extraction_json FROM stage1_outputs") - .all() as Array<{ thread_id: string; extraction_json: string }>; + const rows = this.queryAll<{ thread_id: string; extraction_json: string }>( + "SELECT thread_id, extraction_json FROM stage1_outputs", + ); return rows.map((r) => ({ threadId: r.thread_id, @@ -344,13 +338,12 @@ export class MemoryStorage { * Get all stage1 outputs for a specific cwd. */ getStage1OutputsForCwd(cwd: string): Array<{ threadId: string; extractionJson: string }> { - const rows = this.db - .prepare( - `SELECT s.thread_id, s.extraction_json FROM stage1_outputs s - INNER JOIN threads t ON t.thread_id = s.thread_id - WHERE t.cwd = ?`, - ) - .all(cwd) as Array<{ thread_id: string; extraction_json: string }>; + const rows = this.queryAll<{ thread_id: string; extraction_json: string }>( + `SELECT s.thread_id, s.extraction_json FROM stage1_outputs s + INNER JOIN threads t ON t.thread_id = s.thread_id + WHERE t.cwd = ?`, + [cwd], + ); return rows.map((r) => ({ threadId: r.thread_id, @@ -362,9 +355,10 @@ export class MemoryStorage { * Get thread info by ID. */ getThread(threadId: string): ThreadRow | undefined { - return this.db.prepare("SELECT * FROM threads WHERE thread_id = ?").get(threadId) as - | ThreadRow - | undefined; + return this.queryOne( + "SELECT * FROM threads WHERE thread_id = ?", + [threadId], + ); } /** @@ -378,24 +372,22 @@ export class MemoryStorage { totalStage1Outputs: number; pendingStage1Jobs: number; } { - const threads = this.db.prepare(` + const threads = this.queryOne<{ total: number; pending: number; done: number; errors: number }>(` SELECT COUNT(*) as total, SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as done, SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as errors FROM threads - `).get() as { total: number; pending: number; done: number; errors: number }; + `)!; - const outputs = this.db.prepare("SELECT COUNT(*) as cnt FROM stage1_outputs").get() as { - cnt: number; - }; + const outputs = this.queryOne<{ cnt: number }>( + "SELECT COUNT(*) as cnt FROM stage1_outputs", + )!; - const pendingJobs = this.db - .prepare( - "SELECT COUNT(*) as cnt FROM jobs WHERE phase = 'stage1' AND status IN ('pending', 'claimed')", - ) - .get() as { cnt: number }; + const pendingJobs = this.queryOne<{ cnt: number }>( + "SELECT COUNT(*) as cnt FROM jobs WHERE phase = 'stage1' AND status IN ('pending', 'claimed')", + )!; return { totalThreads: threads.total, @@ -411,78 +403,61 @@ export class MemoryStorage { * Clear all data (for /memory clear). */ clearAll(): void { - this.db.transaction(() => { - this.db.exec("DELETE FROM stage1_outputs"); - this.db.exec("DELETE FROM jobs"); - this.db.exec("DELETE FROM threads"); - })(); + this.db.run("DELETE FROM stage1_outputs"); + this.db.run("DELETE FROM jobs"); + this.db.run("DELETE FROM threads"); + this.persist(); } /** * Clear data for a specific cwd (for /memory clear in project scope). */ clearForCwd(cwd: string): void { - this.db.transaction(() => { - this.db - .prepare( - `DELETE FROM stage1_outputs WHERE thread_id IN ( - SELECT thread_id FROM threads WHERE cwd = ?)`, - ) - .run(cwd); - this.db - .prepare( - `DELETE FROM jobs WHERE thread_id IN ( - SELECT thread_id FROM threads WHERE cwd = ?)`, - ) - .run(cwd); - this.db.prepare("DELETE FROM threads WHERE cwd = ?").run(cwd); - })(); + this.db.run( + "DELETE FROM stage1_outputs WHERE thread_id IN (SELECT thread_id FROM threads WHERE cwd = ?)", + [cwd], + ); + this.db.run( + "DELETE FROM jobs WHERE thread_id IN (SELECT thread_id FROM threads WHERE cwd = ?)", + [cwd], + ); + this.db.run("DELETE FROM threads WHERE cwd = ?", [cwd]); + this.persist(); } /** * Reset all threads to pending (for /memory rebuild). */ resetAllForCwd(cwd: string): void { - this.db.transaction(() => { - // Delete existing stage1 outputs - this.db - .prepare( - `DELETE FROM stage1_outputs WHERE thread_id IN ( - SELECT thread_id FROM threads WHERE cwd = ?)`, - ) - .run(cwd); + this.db.run( + "DELETE FROM stage1_outputs WHERE thread_id IN (SELECT thread_id FROM threads WHERE cwd = ?)", + [cwd], + ); + this.db.run( + "DELETE FROM jobs WHERE thread_id IN (SELECT thread_id FROM threads WHERE cwd = ?)", + [cwd], + ); + this.db.run( + "UPDATE threads SET status = 'pending', updated_at = datetime('now') WHERE cwd = ?", + [cwd], + ); - // Delete old jobs - this.db - .prepare( - `DELETE FROM jobs WHERE thread_id IN ( - SELECT thread_id FROM threads WHERE cwd = ?)`, - ) - .run(cwd); + const threads = this.queryAll<{ thread_id: string }>( + "SELECT thread_id FROM threads WHERE cwd = ?", + [cwd], + ); - // Reset thread status - this.db - .prepare( - `UPDATE threads SET status = 'pending', updated_at = datetime('now') - WHERE cwd = ?`, - ) - .run(cwd); - - // Re-create stage1 jobs - const threads = this.db - .prepare("SELECT thread_id FROM threads WHERE cwd = ?") - .all(cwd) as Array<{ thread_id: string }>; - - const insertJobStmt = this.db.prepare( + for (const t of threads) { + this.db.run( "INSERT INTO jobs (id, phase, thread_id, status) VALUES (?, 'stage1', ?, 'pending')", + [randomUUID(), t.thread_id], ); - for (const t of threads) { - insertJobStmt.run(randomUUID(), t.thread_id); - } - })(); + } + this.persist(); } close(): void { + this.persist(); this.db.close(); } }