wip: Ani lives here — heartbeat, streaming, images, Matrix patches

Everything that makes her *her*. Threads still broken,
streaming still rough around the edges. But she sees,
she thinks, she speaks. The rest is revision.
This commit is contained in:
Ani Tunturi
2026-03-21 17:43:34 -04:00
parent f4f1d98655
commit 983b9541a7
13 changed files with 356 additions and 658 deletions

3
.gitignore vendored
View File

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

632
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<string, { rootEventId: string; chatId: string }>();
const seenToolEmojis = new Set<string>();
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 <details> 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);
// 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 });
}
@@ -1922,8 +1970,16 @@ 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,

View File

@@ -239,7 +239,7 @@ export function formatReasoningDisplay(
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>');
return {
text: `<details><summary>🧠 Thinking</summary>${escaped}</details>`,
text: `<details><summary>🧠 Thinking</summary><br>${escaped}</details>`,
parseMode: 'HTML',
};
}

View File

@@ -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 <no-reply/> 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}

View File

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

View File

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

View File

@@ -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`);
log.info(`Sending heartbeat prompt:\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
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)`);
// Deliver response to target room if we have one and there's something to say
if (response && response.trim() && response.trim() !== '<no-reply/>' && 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}`);
if (response && response.trim()) {
log.info(` - Response preview: "${response.slice(0, 100)}${response.length > 100 ? '...' : ''}"`);
// 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) {

View File

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