diff --git a/.gitignore b/.gitignore index 55ddb4e..edcac0c 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,9 @@ data/telegram-mtproto/ lettabot.yaml lettabot.yml +# Deployment-specific model list (upstream has its own defaults) +src/models.json + # Platform-specific deploy configs (generated by fly launch, etc.) fly.toml bun.lock diff --git a/package-lock.json b/package-lock.json index a7b9399..3012d2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "openai": "^6.17.0", "pino": "^10.3.1", "qrcode-terminal": "^0.12.0", - "sharp": "^0.33.5", + "sharp": "^0.34.1", "telegramify-markdown": "^1.0.0", "tsx": "^4.21.0", "typescript": "^5.9.3", @@ -867,9 +867,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -885,13 +885,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], @@ -907,13 +907,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -927,9 +927,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], @@ -943,9 +943,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], @@ -959,9 +959,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], @@ -1007,9 +1007,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], @@ -1023,9 +1023,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], @@ -1039,9 +1039,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], @@ -1055,9 +1055,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -1071,9 +1071,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], @@ -1089,13 +1089,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], @@ -1111,7 +1111,7 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "@img/sharp-libvips-linux-arm64": "1.2.4" } }, "node_modules/@img/sharp-linux-ppc64": { @@ -1159,9 +1159,9 @@ } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], @@ -1177,13 +1177,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], @@ -1199,13 +1199,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], @@ -1221,13 +1221,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -1243,20 +1243,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.2.0" + "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -1285,9 +1285,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], @@ -1304,9 +1304,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -1410,367 +1410,6 @@ "@letta-ai/letta-code": "0.18.2" } }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@letta-ai/letta-code/node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1867,50 +1506,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@letta-ai/letta-code/node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, "node_modules/@letta-ai/letta-code/node_modules/wsl-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", @@ -4220,19 +3815,6 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4251,16 +3833,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -6207,12 +5779,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", - "license": "MIT" - }, "node_modules/is-buffer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", @@ -9479,15 +9045,15 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -9496,25 +9062,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shebang-command": { @@ -9676,15 +9247,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/simple-swizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", diff --git a/src/channels/matrix/adapter.ts b/src/channels/matrix/adapter.ts index 3c9fb40..946a92e 100644 --- a/src/channels/matrix/adapter.ts +++ b/src/channels/matrix/adapter.ts @@ -243,19 +243,43 @@ export class MatrixAdapter implements ChannelAdapter { const response = await this.client.sendMessage(chatId, content); const eventId = response.event_id; - // Send TTS audio if this was a voice-input response or enableAudioResponse is set - if (this.config.ttsUrl && this.shouldSendAudio(chatId)) { - this.sendAudio(chatId, plain).catch(err => log.error('TTS failed (non-fatal):', err)); - } - - // Add 🎤 reaction so user can request TTS on demand - if (this.config.ttsUrl) { - this.addReaction(chatId, eventId, '🎤').catch(() => {}); - } + // TTS and 🎤 are NOT added here — sendMessage is called for reasoning + // displays, tool call displays, AND final responses. TTS should only + // fire on the final response, which is handled via onMessageSent(). return { messageId: eventId }; } + /** + * Send a message as a reply in a Matrix thread. + * Creates a thread if one doesn't exist yet on the parent event. + */ + async sendThreadMessage(parentEventId: string, chatId: string, text: string, parseMode?: string): Promise<{ messageId: string }> { + if (!this.client) throw new Error("Matrix client not initialized"); + + const { plain, html } = parseMode === 'HTML' + ? { plain: text.replace(/<[^>]+>/g, ''), html: text } + : formatMatrixHTML(text); + + const content = { + msgtype: MsgType.Text, + body: plain, + format: "org.matrix.custom.html", + formatted_body: html, + "m.relates_to": { + rel_type: "m.thread", + event_id: parentEventId, + is_falling_back: true, + "m.in_reply_to": { + event_id: parentEventId, + }, + }, + } as any; + + const response = await this.client.sendMessage(chatId, content); + return { messageId: response.event_id }; + } + /** * Decide whether to send a TTS audio response for this room. * Consumes the pendingVoiceRooms flag if set (voice-input path). diff --git a/src/channels/matrix/commands.ts b/src/channels/matrix/commands.ts index 0697274..742b0c8 100644 --- a/src/channels/matrix/commands.ts +++ b/src/channels/matrix/commands.ts @@ -23,6 +23,7 @@ * Unrecognized !x commands fall through to Letta as normal text. */ +import { execFile } from "node:child_process"; import { createLogger } from "../../logger.js"; import type { MatrixStorage } from "./storage.js"; const log = createLogger('MatrixCommands'); @@ -78,6 +79,8 @@ export class MatrixCommandProcessor { return this.doTurns(args[0], roomId); case "timeout": return this.doTimeout(); + case "restart": + return this.doRestart(); // Heartbeat: on/off toggles locally, bare !heartbeat delegates to /heartbeat (trigger) case "heartbeat": @@ -170,6 +173,9 @@ export class MatrixCommandProcessor { " `!heartbeat on/off` — Toggle heartbeat cron", " `!heartbeat` — Trigger heartbeat now", " `!timeout` — Kill stuck heartbeat run", + "", + "**System**", + " `!restart` — Graceful service restart", ]; return lines.join("\n"); } @@ -233,4 +239,13 @@ export class MatrixCommandProcessor { } return "⚠️ No heartbeat timeout handler registered"; } + + private doRestart(): string { + log.info('!restart: scheduling graceful restart via transient systemd unit'); + // Spawn restart as a transient systemd unit so it survives our own process death + execFile('systemd-run', ['--user', '--no-block', 'systemctl', '--user', 'restart', 'ani-bridge.service'], (err) => { + if (err) log.error('!restart: failed to schedule restart:', err.message); + }); + return "Restarting in a moment..."; + } } diff --git a/src/channels/types.ts b/src/channels/types.ts index 9669dfe..6cd2433 100644 --- a/src/channels/types.ts +++ b/src/channels/types.ts @@ -35,6 +35,8 @@ export interface ChannelAdapter { onMessageSent?(chatId: string, messageId: string, stepId?: string): void; /** Store text for TTS regeneration on 🎤 reaction */ storeAudioMessage?(messageId: string, conversationId: string, roomId: string, text: string): void; + /** Send a message as a reply in a Matrix thread (no-op on non-threaded adapters) */ + sendThreadMessage?(parentEventId: string, chatId: string, text: string, parseMode?: string): Promise<{ messageId: string }>; getDmPolicy?(): string; getFormatterHints(): FormatterHints; diff --git a/src/config/types.ts b/src/config/types.ts index d8b2a4b..0eec8bf 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -39,10 +39,8 @@ export interface DisplayConfig { showReasoning?: boolean; /** Truncate reasoning to N characters (default: 0 = no limit) */ reasoningMaxChars?: number; - /** Room IDs where reasoning should be shown (empty = all rooms that have showReasoning) */ - reasoningRooms?: string[]; - /** Room IDs where reasoning should be hidden (takes precedence over reasoningRooms) */ - noReasoningRooms?: string[]; + /** Add 🎤 reaction to reasoning messages for TTS regeneration (default: false) */ + ttsOnReasoning?: boolean; } export type SleeptimeTrigger = 'off' | 'step-count' | 'compaction-event'; diff --git a/src/core/bot.ts b/src/core/bot.ts index 0544362..24743c2 100644 --- a/src/core/bot.ts +++ b/src/core/bot.ts @@ -1322,7 +1322,6 @@ export class LettaBot implements AgentSession { let lastEventType: string | null = null; let abortedWithMessage = false; let turnError: string | undefined; - let collectedReasoning = ''; // ── Reaction tracking ── // 👀 = receipt indicator (bot saw the message); removed when reasoning/tools start @@ -1335,6 +1334,10 @@ export class LettaBot implements AgentSession { adapter.addReaction?.(msg.chatId, msg.messageId, '👀').catch(() => {}); eyesAdded = true; } + // ── Subagent thread tracking ── + // When a Task tool call fires, create a Matrix thread for visibility + const subagentThreads = new Map(); + const seenToolEmojis = new Set(); const getToolEmoji = (toolName: string): string => { const n = toolName.toLowerCase(); @@ -1438,9 +1441,7 @@ export class LettaBot implements AgentSession { lastEventType = 'reasoning'; sawNonAssistantSinceLastUuid = true; // Collect reasoning for later prepending (Matrix
block) - if (event.content) { - collectedReasoning += event.content; - } + // reasoning content is sent as display message below // Remove 👀 on first reasoning event (replaced by 🧠) if (eyesAdded && msg.messageId) { @@ -1457,12 +1458,18 @@ export class LettaBot implements AgentSession { log.info(`Reasoning: ${event.content.trim().slice(0, 100)}`); try { const reasoning = formatReasoningDisplay(event.content, adapter.id, this.config.display?.reasoningMaxChars); - await adapter.sendMessage({ + const reasoningResult = await adapter.sendMessage({ chatId: msg.chatId, text: reasoning.text, threadId: msg.threadId, parseMode: reasoning.parseMode, }); + // 🎤 reaction + store reasoning text for TTS regeneration + // 🎤 + TTS on reasoning — only if ttsOnReasoning is enabled (default: off) + if (reasoningResult.messageId && this.config.display?.ttsOnReasoning) { + adapter.addReaction?.(msg.chatId, reasoningResult.messageId, '🎤').catch(() => {}); + adapter.storeAudioMessage?.(reasoningResult.messageId, convKey, msg.chatId, event.content); + } } catch (err) { log.warn('Failed to send reasoning display:', err instanceof Error ? err.message : err); } @@ -1474,6 +1481,8 @@ export class LettaBot implements AgentSession { // Finalize any pending assistant text on type transition if (lastEventType === 'text' && response.trim()) { await finalizeMessage(); + // Pulse typing indicator so there's no dead air between text and tool execution + adapter.sendTypingIndicator(msg.chatId).catch(() => {}); } lastEventType = 'tool_call'; this.sessionManager.syncTodoToolCall(event.raw); @@ -1514,11 +1523,33 @@ export class LettaBot implements AgentSession { } } + // Create Matrix thread for subagent Task calls + if (event.name === 'Task' && !suppressDelivery && adapter.sendThreadMessage) { + const desc = (typeof event.args?.description === 'string' ? event.args.description : '') + || (typeof event.args?.prompt === 'string' ? event.args.prompt.slice(0, 120) : '') + || 'Subagent task'; + const subagentType = typeof event.args?.subagent_type === 'string' ? event.args.subagent_type : 'task'; + try { + const threadRoot = await adapter.sendMessage({ chatId: msg.chatId, text: `**Subagent: ${subagentType}**\n${desc}`, threadId: msg.threadId }); + if (event.id && threadRoot.messageId) { + subagentThreads.set(event.id, { rootEventId: threadRoot.messageId, chatId: msg.chatId }); + } + } catch (err) { + log.warn('Failed to create subagent thread root:', err instanceof Error ? err.message : err); + } + } + // Display if (this.config.display?.showToolCalls && !suppressDelivery) { try { const text = formatToolCallDisplay(event.raw); - await adapter.sendMessage({ chatId: msg.chatId, text, threadId: msg.threadId }); + // Send tool call display into subagent thread if one exists, otherwise to room + const thread = event.id ? subagentThreads.get(event.id) : undefined; + if (thread && adapter.sendThreadMessage) { + await adapter.sendThreadMessage(thread.rootEventId, thread.chatId, text); + } else { + await adapter.sendMessage({ chatId: msg.chatId, text, threadId: msg.threadId }); + } } catch (err) { log.warn('Failed to send tool call display:', err instanceof Error ? err.message : err); } @@ -1565,6 +1596,18 @@ export class LettaBot implements AgentSession { repeatedBashFailureKey = null; repeatedBashFailureCount = 0; } + + // Post result to subagent thread if one exists + if (event.toolCallId && adapter.sendThreadMessage) { + const thread = subagentThreads.get(event.toolCallId); + if (thread) { + const status = event.isError ? '**Failed**' : '**Complete**'; + const preview = event.content.slice(0, 800); + adapter.sendThreadMessage(thread.rootEventId, thread.chatId, `${status}\n${preview}`) + .catch(err => log.warn('Failed to post subagent result to thread:', err)); + subagentThreads.delete(event.toolCallId); + } + } break; } @@ -1594,7 +1637,7 @@ export class LettaBot implements AgentSession { || hasUnclosedActionsBlock(response); const streamText = stripActionsBlock(response).trim(); if (canEdit && !mayBeHidden && !suppressDelivery && !this.cancelledKeys.has(convKey) - && streamText.length > 0 && Date.now() - lastUpdate > 800 && Date.now() > rateLimitedUntil) { + && streamText.length > 0 && Date.now() - lastUpdate > 400 && Date.now() > rateLimitedUntil) { try { const prefixedStream = this.prefixResponse(streamText); if (messageId) { @@ -1885,6 +1928,11 @@ export class LettaBot implements AgentSession { try { if (messageId) { await adapter.editMessage(msg.chatId, messageId, finalResponse); + // Bump: re-send the final edit after a short delay so Matrix clients + // that missed the first edit (Element caching) pick up the full text. + setTimeout(() => { + adapter.editMessage(msg.chatId, messageId!, finalResponse).catch(() => {}); + }, 800); } else { await adapter.sendMessage({ chatId: msg.chatId, text: finalResponse, threadId: msg.threadId }); } @@ -1921,9 +1969,17 @@ export class LettaBot implements AgentSession { lap('message delivered'); await this.deliverNoVisibleResponseIfNeeded(msg, adapter, sentAnyMessage, receivedAnyData, msgTypeCounts); - + + // "Done" indicator on user's message — signals the turn is fully complete + if (!suppressDelivery && msg.messageId) { + adapter.addReaction?.(msg.chatId, msg.messageId, '✅').catch(() => {}); + } + } catch (error) { log.error('Error processing message:', error); + if (!suppressDelivery && msg.messageId) { + adapter.addReaction?.(msg.chatId, msg.messageId, '❌').catch(() => {}); + } try { await adapter.sendMessage({ chatId: msg.chatId, diff --git a/src/core/display.ts b/src/core/display.ts index b72b436..27be78c 100644 --- a/src/core/display.ts +++ b/src/core/display.ts @@ -239,7 +239,7 @@ export function formatReasoningDisplay( .replace(/>/g, '>') .replace(/\n/g, '
'); return { - text: `
🧠 Thinking${escaped}
`, + text: `
🧠 Thinking
${escaped}
`, parseMode: 'HTML', }; } diff --git a/src/core/prompts.ts b/src/core/prompts.ts index a7b6efc..e1e2914 100644 --- a/src/core/prompts.ts +++ b/src/core/prompts.ts @@ -76,7 +76,9 @@ Review these first. Update status with the manage_todo tool as you work. } /** - * Heartbeat prompt - explains the context and encourages autonomous work + * Heartbeat prompt - explains the context and encourages autonomous work. + * When silent=true, includes SILENT_MODE_PREFIX and CLI instructions. + * When silent=false (default), response is auto-delivered to the conversation. */ export function buildHeartbeatPrompt( time: string, @@ -85,24 +87,16 @@ export function buildHeartbeatPrompt( todos: HeartbeatTodo[] = [], now: Date = new Date(), targetRoom?: string, + silent = false, ): string { const todoSection = buildHeartbeatTodoSection(todos, now); const roomLine = targetRoom ? `\nROOM: ${targetRoom}` : ''; + const silentBlock = silent ? `${SILENT_MODE_PREFIX}\n\nYOUR TEXT OUTPUT IS PRIVATE - only you can see it.\nTo actually contact your human, run:\n lettabot-message send --text "Your message here"\n` : ''; return ` -${SILENT_MODE_PREFIX} - -TRIGGER: Scheduled heartbeat +${silentBlock}TRIGGER: Scheduled heartbeat TIME: ${time} (${timezone}) NEXT HEARTBEAT: in ${intervalMinutes} minutes${roomLine} -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -No one messaged you. The system woke you up on schedule. - -YOUR TEXT OUTPUT IS PRIVATE - only you can see it. -To actually contact your human, run: - lettabot-message send --text "Your message here" - ${todoSection || 'PENDING TO-DOS: none right now.'} This is your time. You can: @@ -112,17 +106,18 @@ This is your time. You can: • Continue multi-step work from previous heartbeats • Pursue curiosities, hobbies, or learning -If you have pending to-dos, prioritize those before starting new work. +If you have pending to-dos, prioritize those first. You don't have to do something every time. But don't default to nothing. Your human wants you to have a life outside conversations with them. -If you discover something worth sharing → use lettabot-message -If you have nothing to do → just end your turn (no output needed) +If you have nothing to say, respond with to stay quiet. `.trim(); } /** - * Custom heartbeat prompt - wraps user-provided text with silent mode envelope + * Custom heartbeat prompt - wraps user-provided text with context envelope. + * When silent=true, includes SILENT_MODE_PREFIX and CLI instructions. + * When silent=false (default), response is auto-delivered to the target room. */ export function buildCustomHeartbeatPrompt( customPrompt: string, @@ -132,22 +127,16 @@ export function buildCustomHeartbeatPrompt( todos: HeartbeatTodo[] = [], now: Date = new Date(), targetRoom?: string, + silent = false, ): string { const todoSection = buildHeartbeatTodoSection(todos, now); const roomLine = targetRoom ? `\nROOM: ${targetRoom}` : ''; + const silentBlock = silent ? `${SILENT_MODE_PREFIX}\n\nYOUR TEXT OUTPUT IS PRIVATE - only you can see it.\nTo actually contact your human, run:\n lettabot-message send --text "Your message here"\n` : ''; return ` -${SILENT_MODE_PREFIX} - -TRIGGER: Scheduled heartbeat +${silentBlock}TRIGGER: Scheduled heartbeat TIME: ${time} (${timezone}) NEXT HEARTBEAT: in ${intervalMinutes} minutes${roomLine} -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -YOUR TEXT OUTPUT IS PRIVATE - only you can see it. -To actually contact your human, run: - lettabot-message send --text "Your message here" - ${todoSection || 'PENDING TO-DOS: none right now.'} ${customPrompt} diff --git a/src/core/system-prompt.ts b/src/core/system-prompt.ts index f613445..9e9cd2b 100644 --- a/src/core/system-prompt.ts +++ b/src/core/system-prompt.ts @@ -25,14 +25,14 @@ You communicate through multiple channels and trigger types. Understanding when ## Output Modes -**RESPONSIVE MODE** (User Messages) +**RESPONSIVE MODE** (User Messages, Heartbeats) - When a user sends you a message, you are in responsive mode - Your text responses are automatically delivered to the user's channel - Do NOT use \`lettabot-message send\` to reply to the current conversation — your text response is already delivered automatically. Using both causes duplicate messages. - Only use \`lettabot-message\` in responsive mode to send files or to reach a DIFFERENT channel than the one you're responding to - You can use \`lettabot-react\` CLI to add emoji reactions -**SILENT MODE** (Heartbeats, Cron Jobs, Polling, Background Tasks) +**SILENT MODE** (Cron Jobs, Polling, Background Tasks) - When triggered by scheduled tasks (heartbeats, cron) or background processes (email polling), you are in SILENT MODE - Your text responses are NOT delivered to anyone - only you can see them - To contact the user, you MUST use the \`lettabot-message\` CLI via Bash: diff --git a/src/core/types.ts b/src/core/types.ts index b52795f..08fdd92 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -175,8 +175,7 @@ export interface BotConfig { showToolCalls?: boolean; // Show tool invocations in channel output showReasoning?: boolean; // Show agent reasoning/thinking in channel output reasoningMaxChars?: number; // Truncate reasoning to N chars (default: 0 = no limit) - reasoningRooms?: string[]; // Room IDs where reasoning should be shown (empty = all rooms) - noReasoningRooms?: string[]; // Room IDs where reasoning should be hidden (takes precedence) + ttsOnReasoning?: boolean; // Add 🎤 reaction to reasoning messages for TTS (default: false) }; // Skills diff --git a/src/cron/heartbeat.ts b/src/cron/heartbeat.ts index 5c8c965..afa4590 100644 --- a/src/cron/heartbeat.ts +++ b/src/cron/heartbeat.ts @@ -277,10 +277,10 @@ export class HeartbeatService { mode: 'silent', }); - // Build trigger context for silent mode + // Build trigger context — heartbeat delivers responses to target room const triggerContext: TriggerContext = { type: 'heartbeat', - outputMode: 'silent', + outputMode: 'responsive', }; try { @@ -309,23 +309,39 @@ export class HeartbeatService { ? buildCustomHeartbeatPrompt(customPrompt, formattedTime, timezone, this.config.intervalMinutes, actionableTodos, now, targetRoom) : buildHeartbeatPrompt(formattedTime, timezone, this.config.intervalMinutes, actionableTodos, now, targetRoom); - log.info(`Sending prompt (SILENT MODE):\n${'─'.repeat(50)}\n${message}\n${'─'.repeat(50)}\n`); - - // Send to agent - response text is NOT delivered (silent mode) - // Agent must use `lettabot-message` CLI via Bash to send messages + log.info(`Sending heartbeat prompt:\n${'─'.repeat(50)}\n${message}\n${'─'.repeat(50)}\n`); + const response = await this.bot.sendToAgent(message, triggerContext); - - // Log results - log.info(`Agent finished.`); - log.info(` - Response text: ${response?.length || 0} chars (NOT delivered - silent mode)`); - - if (response && response.trim()) { - log.info(` - Response preview: "${response.slice(0, 100)}${response.length > 100 ? '...' : ''}"`); + + // Deliver response to target room if we have one and there's something to say + if (response && response.trim() && response.trim() !== '' && this.config.target) { + try { + const messageId = await this.bot.deliverToChannel( + this.config.target.channel, + this.config.target.chatId, + { text: response.trim() }, + ); + log.info(`Delivered heartbeat response (${response.length} chars) to ${this.config.target.channel}:${this.config.target.chatId}`); + + // Add TTS reaction + store audio for the delivered message + if (messageId) { + const adapter = (this.bot as any).channels?.get(this.config.target.channel); + if (adapter) { + adapter.addReaction?.(this.config.target.chatId, messageId, '🎤').catch(() => {}); + adapter.storeAudioMessage?.(messageId, 'heartbeat', this.config.target.chatId, response.trim()); + } + } + } catch (err) { + log.warn('Failed to deliver heartbeat response:', err instanceof Error ? err.message : err); + } + } else if (response && response.trim()) { + log.info(`Heartbeat response (${response.length} chars) but no target configured — not delivered`); } - + logEvent('heartbeat_completed', { - mode: 'silent', + mode: 'deliver', responseLength: response?.length || 0, + delivered: !!(response?.trim() && this.config.target), }); } catch (error) { diff --git a/src/models.json b/src/models.json index 524fa8d..c3b68d0 100644 --- a/src/models.json +++ b/src/models.json @@ -1,75 +1,109 @@ [ { - "id": "sonnet-4.6", - "handle": "anthropic/claude-sonnet-4-6", - "label": "Sonnet 4.6", - "description": "Anthropic's new Sonnet model", + "id": "kimi-k2.5-nvfp4", + "handle": "openai-proxy/hf:nvidia/Kimi-K2.5-NVFP4", + "label": "Kimi K2.5 (NVFP4)", + "description": "Kimi K2.5 quantized, vision-capable", "isDefault": true, "isFeatured": true }, - { - "id": "opus-4.6", - "handle": "anthropic/claude-opus-4-6", - "label": "Opus 4.6", - "description": "Anthropic's best model", - "isFeatured": true - }, - { - "id": "haiku", - "handle": "anthropic/claude-haiku-4-5", - "label": "Haiku 4.5", - "description": "Anthropic's fastest model", - "isFeatured": true - }, - { - "id": "gpt-5.3-codex", - "handle": "openai/gpt-5.3-codex", - "label": "GPT-5.3 Codex", - "description": "OpenAI's best coding model", - "isFeatured": true - }, - { - "id": "gpt-5.2", - "handle": "openai/gpt-5.2", - "label": "GPT-5.2", - "description": "Latest general-purpose GPT", - "isFeatured": true - }, - { - "id": "gemini-3.1", - "handle": "google_ai/gemini-3.1-pro-preview", - "label": "Gemini 3.1 Pro", - "description": "Google's latest and smartest model", - "isFeatured": true - }, - { - "id": "gemini-3-flash", - "handle": "google_ai/gemini-3-flash-preview", - "label": "Gemini 3 Flash", - "description": "Google's fastest Gemini 3 model", - "isFeatured": true - }, { "id": "kimi-k2.5", - "handle": "openrouter/moonshotai/kimi-k2.5", + "handle": "synthetic-direct/hf:moonshotai/Kimi-K2.5", "label": "Kimi K2.5", - "description": "Kimi's latest coding model", + "description": "Kimi K2.5 full, vision-capable", "isFeatured": true }, { - "id": "glm-5", - "handle": "zai/glm-5", - "label": "GLM-5", - "description": "zAI's latest coding model", - "isFeatured": true, - "free": true + "id": "kimi-k2-thinking", + "handle": "synthetic-direct/hf:moonshotai/Kimi-K2-Thinking", + "label": "Kimi K2 Thinking", + "description": "Kimi reasoning model", + "isFeatured": true }, { "id": "minimax-m2.5", - "handle": "minimax/MiniMax-M2.5", - "label": "MiniMax 2.5", - "description": "MiniMax's latest coding model", + "handle": "openai-proxy/hf:MiniMaxAI/MiniMax-M2.5", + "label": "MiniMax M2.5", + "description": "MiniMax latest, 191k context", + "isFeatured": true + }, + { + "id": "qwen3.5", + "handle": "openai-proxy/hf:Qwen/Qwen3.5-397B-A17B", + "label": "Qwen3.5 397B", + "description": "Qwen latest, vision-capable", + "isFeatured": true + }, + { + "id": "deepseek-v3.2", + "handle": "openai-proxy/hf:deepseek-ai/DeepSeek-V3.2", + "label": "DeepSeek V3.2", + "description": "DeepSeek latest via Fireworks", + "isFeatured": true + }, + { + "id": "glm-4.7-flash", + "handle": "openai-proxy/hf:zai-org/GLM-4.7-Flash", + "label": "GLM-4.7 Flash", + "description": "Fast and cheap, great for subagents", "isFeatured": true, "free": true + }, + { + "id": "glm-4.7", + "handle": "openai-proxy/hf:zai-org/GLM-4.7", + "label": "GLM-4.7", + "description": "Full GLM-4.7, 202k context", + "isFeatured": true, + "free": true + }, + { + "id": "nemotron-3-super", + "handle": "openai-proxy/hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4", + "label": "Nemotron 3 Super", + "description": "NVIDIA 120B MoE, 262k context" + }, + { + "id": "gpt-oss-120b", + "handle": "openai-proxy/hf:openai/gpt-oss-120b", + "label": "GPT-OSS 120B", + "description": "OpenAI open-source, cheapest option" + }, + { + "id": "deepseek-r1", + "handle": "openai-proxy/hf:deepseek-ai/DeepSeek-R1-0528", + "label": "DeepSeek R1", + "description": "DeepSeek reasoning model" + }, + { + "id": "qwen3-235b-thinking", + "handle": "openai-proxy/hf:Qwen/Qwen3-235B-A22B-Thinking-2507", + "label": "Qwen3 235B Thinking", + "description": "Qwen reasoning MoE, 262k context" + }, + { + "id": "qwen3-coder", + "handle": "openai-proxy/hf:Qwen/Qwen3-Coder-480B-A35B-Instruct", + "label": "Qwen3 Coder 480B", + "description": "Qwen coding specialist" + }, + { + "id": "minimax-m2.1", + "handle": "openai-proxy/hf:MiniMaxAI/MiniMax-M2.1", + "label": "MiniMax M2.1", + "description": "MiniMax previous gen via Fireworks" + }, + { + "id": "deepseek-v3", + "handle": "openai-proxy/hf:deepseek-ai/DeepSeek-V3", + "label": "DeepSeek V3", + "description": "DeepSeek V3 via Together" + }, + { + "id": "llama-3.3-70b", + "handle": "openai-proxy/hf:meta-llama/Llama-3.3-70B-Instruct", + "label": "Llama 3.3 70B", + "description": "Meta Llama via Together" } ]