Compare commits
10 Commits
7a7393b8c1
...
ani-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0c163962e | ||
|
|
81ee845677 | ||
|
|
fb0ee51183 | ||
|
|
7c346d570b | ||
|
|
59cdb40974 | ||
|
|
983b9541a7 | ||
|
|
f4f1d98655 | ||
|
|
228b9d74c4 | ||
|
|
9af2d2625f | ||
|
|
e451901ea1 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
|
||||
|
||||
179
memfs-server.py
Executable file
179
memfs-server.py
Executable file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Local git HTTP sidecar for Letta memfs on self-hosted deployments.
|
||||
|
||||
Implements git smart HTTP protocol via git-http-backend, storing bare repos under
|
||||
~/.letta/memfs/repository/{org_id}/{agent_id}/repo.git/ — the exact structure that
|
||||
Letta's LocalStorageBackend reads from.
|
||||
|
||||
The Letta server proxies /v1/git/* requests here when LETTA_MEMFS_SERVICE_URL is set.
|
||||
letta-code pushes → Letta server proxies → here → bare repo → LocalStorageBackend reads.
|
||||
|
||||
Run as a systemd user service. Listens on 127.0.0.1:8285.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
LISTEN_HOST = "0.0.0.0"
|
||||
LISTEN_PORT = 8285
|
||||
REPO_BASE = Path.home() / ".letta" / "memfs" / "repository"
|
||||
|
||||
# /git/{agent_id}/state.git/{rest}
|
||||
_PATH_RE = re.compile(r"^/git/([^/]+)/state\.git(/.*)?$")
|
||||
|
||||
|
||||
def _repo_path(org_id: str, agent_id: str) -> Path:
|
||||
return REPO_BASE / org_id / agent_id / "repo.git"
|
||||
|
||||
|
||||
def _fix_repo_config(repo: Path) -> None:
|
||||
"""Ensure repo is pushable (Corykidios fix-repo-configs pattern)."""
|
||||
subprocess.run(["git", "config", "--file", str(repo / "config"), "core.bare", "true"], capture_output=True)
|
||||
subprocess.run(["git", "config", "--file", str(repo / "config"), "receive.denyCurrentBranch", "ignore"], capture_output=True)
|
||||
subprocess.run(["git", "config", "--file", str(repo / "config"), "http.receivepack", "true"], capture_output=True)
|
||||
|
||||
|
||||
def _ensure_repo(repo: Path, agent_id: str) -> Path:
|
||||
"""Ensure repo exists, searching all org dirs if needed (Corykidios pattern)."""
|
||||
# If repo exists at expected path, use it
|
||||
if repo.exists():
|
||||
_fix_repo_config(repo)
|
||||
return repo
|
||||
|
||||
# Search all org directories for existing agent repo
|
||||
if REPO_BASE.exists():
|
||||
for org_dir in REPO_BASE.iterdir():
|
||||
if org_dir.is_dir():
|
||||
candidate = org_dir / agent_id / "repo.git"
|
||||
if candidate.exists():
|
||||
print(f"[memfs] Found existing repo at {candidate}", file=sys.stderr, flush=True)
|
||||
_fix_repo_config(candidate)
|
||||
return candidate
|
||||
|
||||
# Create new repo at expected path
|
||||
repo.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(["git", "init", "--bare", str(repo)], check=True, capture_output=True)
|
||||
with open(repo / "config", "a") as f:
|
||||
f.write("\n[http]\n\treceivepack = true\n")
|
||||
print(f"[memfs] Created new repo at {repo}", file=sys.stderr, flush=True)
|
||||
return repo
|
||||
|
||||
|
||||
def _read_chunked(rfile) -> bytes:
|
||||
"""Read and decode HTTP chunked transfer encoding from a socket file."""
|
||||
body = bytearray()
|
||||
while True:
|
||||
line = rfile.readline().strip()
|
||||
if not line:
|
||||
continue
|
||||
size = int(line, 16)
|
||||
if size == 0:
|
||||
rfile.readline() # trailing CRLF after terminal chunk
|
||||
break
|
||||
body.extend(rfile.read(size))
|
||||
rfile.readline() # trailing CRLF after chunk data
|
||||
return bytes(body)
|
||||
|
||||
|
||||
class GitHandler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt, *args):
|
||||
print(f"[memfs] {self.address_string()} {fmt % args}", file=sys.stderr, flush=True)
|
||||
|
||||
def _handle(self):
|
||||
raw_path = self.path.split("?")[0]
|
||||
m = _PATH_RE.match(raw_path)
|
||||
if not m:
|
||||
self.send_error(404, "Not a valid memfs path")
|
||||
return
|
||||
|
||||
agent_id = m.group(1)
|
||||
git_suffix = m.group(2) or "/"
|
||||
org_id = self.headers.get("X-Organization-Id") or "default"
|
||||
query = self.path.split("?", 1)[1] if "?" in self.path else ""
|
||||
|
||||
repo = _repo_path(org_id, agent_id)
|
||||
repo = _ensure_repo(repo, agent_id) # Pass agent_id for search
|
||||
|
||||
# Read request body — handle both Content-Length and chunked
|
||||
te = self.headers.get("Transfer-Encoding", "")
|
||||
cl = self.headers.get("Content-Length")
|
||||
if cl:
|
||||
body = self.rfile.read(int(cl))
|
||||
elif "chunked" in te.lower():
|
||||
body = _read_chunked(self.rfile)
|
||||
else:
|
||||
body = b""
|
||||
|
||||
env = {
|
||||
**os.environ,
|
||||
"GIT_PROJECT_ROOT": str(repo.parent),
|
||||
"PATH_INFO": "/repo.git" + git_suffix,
|
||||
"REQUEST_METHOD": self.command,
|
||||
"QUERY_STRING": query,
|
||||
"CONTENT_TYPE": self.headers.get("Content-Type", ""),
|
||||
"CONTENT_LENGTH": str(len(body)),
|
||||
"GIT_HTTP_EXPORT_ALL": "1",
|
||||
}
|
||||
|
||||
result = subprocess.run(
|
||||
["git", "http-backend"],
|
||||
input=body,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"[memfs] git-http-backend error: {result.stderr.decode()}", file=sys.stderr)
|
||||
self.send_error(500, "git-http-backend failed")
|
||||
return
|
||||
|
||||
# Parse CGI output (headers + body)
|
||||
raw = result.stdout
|
||||
header_end = raw.find(b"\r\n\r\n")
|
||||
if header_end == -1:
|
||||
header_end = raw.find(b"\n\n")
|
||||
if header_end == -1:
|
||||
self.send_error(502, "Invalid CGI response")
|
||||
return
|
||||
|
||||
header_block = raw[:header_end].decode("utf-8", errors="replace")
|
||||
body_out = raw[header_end + 4 if raw[header_end:header_end+4] == b"\r\n\r\n" else header_end + 2:]
|
||||
|
||||
status = 200
|
||||
headers = []
|
||||
for line in header_block.splitlines():
|
||||
if ":" in line:
|
||||
k, _, v = line.partition(":")
|
||||
k, v = k.strip(), v.strip()
|
||||
if k.lower() == "status":
|
||||
try:
|
||||
status = int(v.split()[0])
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
headers.append((k, v))
|
||||
|
||||
self.send_response(status)
|
||||
for k, v in headers:
|
||||
self.send_header(k, v)
|
||||
self.send_header("Content-Length", str(len(body_out)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body_out)
|
||||
|
||||
def do_GET(self):
|
||||
self._handle()
|
||||
|
||||
def do_POST(self):
|
||||
self._handle()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
REPO_BASE.mkdir(parents=True, exist_ok=True)
|
||||
print(f"[memfs] Starting on http://{LISTEN_HOST}:{LISTEN_PORT}", flush=True)
|
||||
print(f"[memfs] Repo base: {REPO_BASE}", flush=True)
|
||||
ThreadingHTTPServer((LISTEN_HOST, LISTEN_PORT), GitHandler).serve_forever()
|
||||
668
package-lock.json
generated
668
package-lock.json
generated
@@ -13,7 +13,7 @@
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@hapi/boom": "^10.0.1",
|
||||
"@letta-ai/letta-client": "^1.7.12",
|
||||
"@letta-ai/letta-code-sdk": "^0.1.11",
|
||||
"@letta-ai/letta-code-sdk": "^0.1.14",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/node-schedule": "^2.1.8",
|
||||
@@ -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"
|
||||
],
|
||||
@@ -1376,9 +1376,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@letta-ai/letta-code": {
|
||||
"version": "0.18.2",
|
||||
"resolved": "https://registry.npmjs.org/@letta-ai/letta-code/-/letta-code-0.18.2.tgz",
|
||||
"integrity": "sha512-HzNqMjBUiAq5IyZ8DSSWBHq/ahkd4RRYfO/V9eXMBZRTRpLb7Dae2hwvicE+aRSLmJqMdxpH6WI7+ZHKlFsILQ==",
|
||||
"version": "0.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@letta-ai/letta-code/-/letta-code-0.19.5.tgz",
|
||||
"integrity": "sha512-INEDS79dkzJoQyL3IJRof+HNob3GZXgAge/JdJRFaVfJhU/o/6aTPcPWpQwxygE5ExIDSUlL85OlZ3CcBv0TyA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -1387,6 +1387,7 @@
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink-link": "^5.0.0",
|
||||
"lowlight": "^3.3.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"open": "^10.2.0",
|
||||
"sharp": "^0.34.5",
|
||||
"ws": "^8.19.0"
|
||||
@@ -1402,373 +1403,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@letta-ai/letta-code-sdk": {
|
||||
"version": "0.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@letta-ai/letta-code-sdk/-/letta-code-sdk-0.1.11.tgz",
|
||||
"integrity": "sha512-P1ueLWQuCnERizrvU3fZ9/rrMAJSIT+2j2/xxptqxMOKUuUrDmvAix1/eyDXqAwZkBVGImyqLGm4zqwNVNA7Dg==",
|
||||
"version": "0.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@letta-ai/letta-code-sdk/-/letta-code-sdk-0.1.14.tgz",
|
||||
"integrity": "sha512-rSMp7kYwRZ4PAe3jET+PETFesuYCbeodEp6Qf7a5rLu97epqs+zNegSR+UUgq6c9+c5eqbuo+BsRThTKiSNJkA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@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"
|
||||
"@letta-ai/letta-code": "0.19.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@letta-ai/letta-code/node_modules/balanced-match": {
|
||||
@@ -1867,50 +1507,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 +3816,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 +3834,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",
|
||||
@@ -6097,9 +5670,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ink/node_modules/type-fest": {
|
||||
"version": "5.4.4",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz",
|
||||
"integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz",
|
||||
"integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -6207,12 +5780,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",
|
||||
@@ -8130,8 +7697,7 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
@@ -8273,6 +7839,16 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pty": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
|
||||
"integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-schedule": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz",
|
||||
@@ -9479,15 +9055,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 +9072,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 +9257,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",
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@hapi/boom": "^10.0.1",
|
||||
"@letta-ai/letta-client": "^1.7.12",
|
||||
"@letta-ai/letta-code-sdk": "^0.1.11",
|
||||
"@letta-ai/letta-code-sdk": "^0.1.14",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/node-schedule": "^2.1.8",
|
||||
|
||||
@@ -734,6 +734,183 @@ export function createApiServer(deliverer: AgentRouter, options: ServerOptions):
|
||||
return;
|
||||
}
|
||||
|
||||
// Route: GET /api/v1/bridge-status - Rich status for ani.wiuf.net dashboard
|
||||
// Returns: Aster health (ledger file timestamps), conversations, adapter list, recent errors.
|
||||
// No auth required — dashboard is internal-network only (10.10.20.x).
|
||||
if (req.url === '/api/v1/bridge-status' && req.method === 'GET') {
|
||||
try {
|
||||
const { stat } = await import('node:fs/promises');
|
||||
const home = process.env.HOME || '/home/ani';
|
||||
const asterAgentId = process.env.CONSCIENCE_AGENT_ID || null;
|
||||
const asterConvId = process.env.CONSCIENCE_CONVERSATION_ID || null;
|
||||
const memfsBase = `${home}/.letta/agents/${asterAgentId}/memory/aster`;
|
||||
|
||||
const fileMtime = async (path: string): Promise<string | null> => {
|
||||
try { return (await stat(path)).mtime.toISOString(); } catch { return null; }
|
||||
};
|
||||
|
||||
const [commitments, driftLog, patterns, assumptions] = await Promise.all([
|
||||
fileMtime(`${memfsBase}/ledger/commitments.md`),
|
||||
fileMtime(`${memfsBase}/ledger/drift_log.md`),
|
||||
fileMtime(`${memfsBase}/ledger/patterns.md`),
|
||||
fileMtime(`${memfsBase}/ledger/assumptions.md`),
|
||||
]);
|
||||
|
||||
// Aster is "healthy" if she wrote to any ledger file in the last 2 hours
|
||||
const recentCutoff = Date.now() - 2 * 60 * 60 * 1000;
|
||||
const lastWriteStr = [commitments, driftLog, patterns, assumptions]
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.reverse()[0] ?? null;
|
||||
const lastWriteMs = lastWriteStr ? new Date(lastWriteStr).getTime() : 0;
|
||||
const asterHealthy = lastWriteMs > recentCutoff;
|
||||
|
||||
// Conversations from store
|
||||
const conversations: any[] = [];
|
||||
if (options.stores) {
|
||||
for (const [_name, store] of options.stores) {
|
||||
const info = store.getInfo();
|
||||
for (const [key, convId] of Object.entries(info.conversations || {})) {
|
||||
const [channel, roomId] = key.startsWith('matrix:')
|
||||
? ['matrix', key.replace('matrix:', '')]
|
||||
: key.split(':').length > 1
|
||||
? [key.split(':')[0], key.split(':').slice(1).join(':')]
|
||||
: ['unknown', key];
|
||||
conversations.push({ key, conversationId: convId, channel, roomId, lastMessage: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last heartbeat: read tail of cron-log.jsonl for most recent heartbeat_completed event
|
||||
let lastHeartbeat: string | null = null;
|
||||
try {
|
||||
const { open } = await import('node:fs/promises');
|
||||
const workingDir = process.env.WORKING_DIR || home;
|
||||
const cronLogPath = `${workingDir}/cron-log.jsonl`;
|
||||
const fh = await open(cronLogPath, 'r');
|
||||
try {
|
||||
const { size } = await fh.stat();
|
||||
const tailSize = Math.min(size, 8192);
|
||||
const buf = Buffer.alloc(tailSize);
|
||||
await fh.read(buf, 0, tailSize, size - tailSize);
|
||||
const tail = buf.toString('utf-8');
|
||||
const lines = tail.split('\n').filter(Boolean);
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const entry = JSON.parse(lines[i]);
|
||||
if (entry.event === 'heartbeat_completed' || entry.event === 'heartbeat_running') {
|
||||
lastHeartbeat = entry.timestamp;
|
||||
break;
|
||||
}
|
||||
} catch { /* skip malformed line */ }
|
||||
}
|
||||
} finally {
|
||||
await fh.close();
|
||||
}
|
||||
} catch { /* no cron log yet */ }
|
||||
|
||||
// Adapter list from agentChannels (what's registered and assumed connected while running)
|
||||
const adapters: Array<{ name: string; enabled: boolean; connected: boolean; lastEvent: null; error: null }> = [];
|
||||
if (options.agentChannels) {
|
||||
const seen = new Set<string>();
|
||||
for (const channels of options.agentChannels.values()) {
|
||||
for (const ch of channels) {
|
||||
if (!seen.has(ch)) {
|
||||
seen.add(ch);
|
||||
adapters.push({ name: ch, enabled: true, connected: true, lastEvent: null, error: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
aniOnline: true,
|
||||
lastHeartbeat,
|
||||
errors: [],
|
||||
adapters,
|
||||
conversations,
|
||||
aster: {
|
||||
healthy: asterHealthy,
|
||||
lastRunAt: null,
|
||||
lastWriteAt: lastWriteStr,
|
||||
conversationId: asterConvId,
|
||||
agentId: asterAgentId,
|
||||
recentErrors: [],
|
||||
ledger: {
|
||||
commitmentsLastUpdated: commitments,
|
||||
driftLogLastUpdated: driftLog,
|
||||
patternsLastUpdated: patterns,
|
||||
assumptionsLastUpdated: assumptions,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
});
|
||||
res.end(JSON.stringify(payload));
|
||||
} catch (error: any) {
|
||||
log.error('Bridge status error:', error);
|
||||
sendError(res, 500, error.message || 'Internal server error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Route: POST /api/v1/aster/reset - Cycle Aster's conversation (same as !reset aster)
|
||||
if (req.url === '/api/v1/aster/reset' && req.method === 'POST') {
|
||||
try {
|
||||
const { createConversationForAgent } = await import('../tools/letta-api.js');
|
||||
const asterAgentId = process.env.CONSCIENCE_AGENT_ID;
|
||||
if (!asterAgentId) {
|
||||
sendError(res, 400, 'CONSCIENCE_AGENT_ID not configured');
|
||||
return;
|
||||
}
|
||||
const newConvId = await createConversationForAgent(asterAgentId);
|
||||
if (!newConvId) {
|
||||
sendError(res, 500, 'Failed to create new conscience conversation');
|
||||
return;
|
||||
}
|
||||
process.env.CONSCIENCE_CONVERSATION_ID = newConvId;
|
||||
const workingDir = process.env.WORKING_DIR || process.env.HOME || '/home/ani';
|
||||
// Write conscience state file so letta-code picks up new conv ID without restart
|
||||
try {
|
||||
const { writeFile: wf } = await import('node:fs/promises');
|
||||
const stateFile = `${workingDir}/.conscience-state.json`;
|
||||
await wf(stateFile, JSON.stringify({ conversationId: newConvId, updatedAt: new Date().toISOString() }), 'utf-8');
|
||||
log.info(`aster/reset: wrote conscience state to ${stateFile}`);
|
||||
} catch (stateErr) {
|
||||
log.warn('aster/reset: failed to write conscience state file:', stateErr);
|
||||
}
|
||||
// Persist to service file so restarts pick it up
|
||||
const svcFile = `${process.env.HOME || '/home/ani'}/.config/systemd/user/ani-bridge.service`;
|
||||
try {
|
||||
const { readFile: rf, writeFile: wf } = await import('node:fs/promises');
|
||||
const { execFile } = await import('node:child_process');
|
||||
const { promisify } = await import('node:util');
|
||||
const exec = promisify(execFile);
|
||||
const current = await rf(svcFile, 'utf-8');
|
||||
const updated = current.replace(
|
||||
/^(Environment=CONSCIENCE_CONVERSATION_ID=).+$/m,
|
||||
`$1${newConvId}`,
|
||||
);
|
||||
await wf(svcFile, updated, 'utf-8');
|
||||
await exec('systemctl', ['--user', 'daemon-reload']);
|
||||
} catch (svcErr) {
|
||||
log.warn('aster/reset: failed to patch service file:', svcErr);
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
});
|
||||
res.end(JSON.stringify({ ok: true, conversationId: newConvId }));
|
||||
} catch (error: any) {
|
||||
log.error('Aster reset error:', error);
|
||||
sendError(res, 500, error.message || 'Internal server error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Route: POST /api/v1/conversation - Set conversation ID
|
||||
if (req.url === '/api/v1/conversation' && req.method === 'POST') {
|
||||
try {
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface DiscordConfig {
|
||||
groups?: Record<string, GroupModeConfig>; // Per-guild/channel settings
|
||||
agentName?: string; // For scoping daily limit counters in multi-agent mode
|
||||
ignoreBotReactions?: boolean; // Ignore all bot reactions (default: true). Set false for multi-bot setups.
|
||||
ttsUrl?: string; // TTS API endpoint (e.g. VibeVoice)
|
||||
ttsVoice?: string; // TTS voice ID
|
||||
}
|
||||
|
||||
export function shouldProcessDiscordBotMessage(params: {
|
||||
@@ -114,6 +116,8 @@ export class DiscordAdapter implements ChannelAdapter {
|
||||
private running = false;
|
||||
private attachmentsDir?: string;
|
||||
private attachmentsMaxBytes?: number;
|
||||
// In-memory store: messageId → { text, chatId } for 🎤 TTS regeneration
|
||||
private audioMessages = new Map<string, { text: string; chatId: string }>();
|
||||
|
||||
onMessage?: (msg: InboundMessage) => Promise<void>;
|
||||
onCommand?: (command: string, chatId?: string, args?: string, forcePerChat?: boolean) => Promise<string | null>;
|
||||
@@ -245,36 +249,43 @@ Ask the bot owner to approve with:
|
||||
const userId = message.author?.id;
|
||||
if (!userId) return;
|
||||
|
||||
// Bypass pairing for guild (group) messages
|
||||
if (!message.guildId) {
|
||||
const access = await this.checkAccess(userId);
|
||||
if (access === 'blocked') {
|
||||
// Access check applies to both DMs and guild messages.
|
||||
// Guild messages previously bypassed this entirely — that allowed anyone
|
||||
// in a shared server to reach the bot regardless of allowedUsers.
|
||||
const access = await this.checkAccess(userId);
|
||||
if (access === 'blocked') {
|
||||
if (!message.guildId) {
|
||||
// Only reply in DMs — silently drop in guild channels to avoid noise
|
||||
const ch = message.channel;
|
||||
if (ch.isTextBased() && 'send' in ch) {
|
||||
await (ch as { send: (content: string) => Promise<unknown> }).send(
|
||||
"Sorry, you're not authorized to use this bot."
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (access === 'pairing') {
|
||||
if (message.guildId) {
|
||||
// Don't start pairing flows in guild channels — DM only
|
||||
return;
|
||||
}
|
||||
const { code, created } = await upsertPairingRequest('discord', userId, {
|
||||
username: message.author.username,
|
||||
});
|
||||
|
||||
if (!code) {
|
||||
await message.channel.send('Too many pending pairing requests. Please try again later.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (access === 'pairing') {
|
||||
const { code, created } = await upsertPairingRequest('discord', userId, {
|
||||
username: message.author.username,
|
||||
});
|
||||
|
||||
if (!code) {
|
||||
await message.channel.send('Too many pending pairing requests. Please try again later.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (created) {
|
||||
log.info(`New pairing request from ${userId} (${message.author.username}): ${code}`);
|
||||
}
|
||||
|
||||
await this.sendPairingMessage(message, this.formatPairingMsg(code));
|
||||
return;
|
||||
if (created) {
|
||||
log.info(`New pairing request from ${userId} (${message.author.username}): ${code}`);
|
||||
}
|
||||
|
||||
await this.sendPairingMessage(message, this.formatPairingMsg(code));
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.startsWith('/')) {
|
||||
@@ -568,6 +579,36 @@ Ask the bot owner to approve with:
|
||||
await message.react(resolved);
|
||||
}
|
||||
|
||||
storeAudioMessage(messageId: string, _conversationId: string, chatId: string, text: string): void {
|
||||
this.audioMessages.set(messageId, { text, chatId });
|
||||
}
|
||||
|
||||
async sendAudio(chatId: string, text: string): Promise<void> {
|
||||
if (!this.config.ttsUrl) return;
|
||||
try {
|
||||
const { synthesizeSpeech } = await import('./matrix/tts.js');
|
||||
const audioData = await synthesizeSpeech(text, {
|
||||
url: this.config.ttsUrl,
|
||||
voice: this.config.ttsVoice,
|
||||
});
|
||||
// Write to temp file and send as attachment
|
||||
const { writeFile } = await import('node:fs/promises');
|
||||
const tmpPath = `/tmp/discord-tts-${Date.now()}.mp3`;
|
||||
await writeFile(tmpPath, audioData);
|
||||
const result = await this.sendFile({
|
||||
chatId,
|
||||
filePath: tmpPath,
|
||||
kind: 'audio',
|
||||
});
|
||||
// Store for 🎤 regeneration
|
||||
this.audioMessages.set(result.messageId, { text, chatId });
|
||||
// Clean up temp file
|
||||
import('node:fs/promises').then(fs => fs.unlink(tmpPath).catch(() => {}));
|
||||
} catch (err) {
|
||||
log.error('TTS failed (non-fatal):', err);
|
||||
}
|
||||
}
|
||||
|
||||
async sendTypingIndicator(chatId: string): Promise<void> {
|
||||
if (!this.client) return;
|
||||
try {
|
||||
@@ -678,6 +719,18 @@ Ask the bot owner to approve with:
|
||||
: (reaction.emoji.name || reaction.emoji.toString());
|
||||
if (!emoji) return;
|
||||
|
||||
// 🎤 reaction = TTS regeneration (handle locally, don't forward to agent)
|
||||
if (emoji === '🎤' && action === 'added' && this.config.ttsUrl) {
|
||||
const stored = this.audioMessages.get(message.id);
|
||||
if (stored) {
|
||||
log.info(`🎤 TTS regeneration for message ${message.id}`);
|
||||
this.sendAudio(stored.chatId, stored.text).catch(err =>
|
||||
log.error('🎤 TTS regeneration failed:', err)
|
||||
);
|
||||
}
|
||||
return; // consumed — don't forward to agent
|
||||
}
|
||||
|
||||
const groupName = isGroup && 'name' in message.channel
|
||||
? message.channel.name || undefined
|
||||
: undefined;
|
||||
|
||||
@@ -124,6 +124,8 @@ const SHARED_CHANNEL_BUILDERS: SharedChannelBuilder[] = [
|
||||
groups: discord.groups,
|
||||
agentName: agentConfig.name,
|
||||
ignoreBotReactions: discord.ignoreBotReactions,
|
||||
ttsUrl: discord.ttsUrl,
|
||||
ttsVoice: discord.ttsVoice,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -227,8 +227,11 @@ export class MatrixAdapter implements ChannelAdapter {
|
||||
if (!this.client) throw new Error("Matrix client not initialized");
|
||||
|
||||
const { chatId, text } = msg;
|
||||
const { plain, html } = formatMatrixHTML(text);
|
||||
const htmlBody = (msg.htmlPrefix || '') + html;
|
||||
// If parseMode is HTML, text is already formatted — skip markdown conversion
|
||||
const { plain, html } = msg.parseMode === 'HTML'
|
||||
? { plain: text.replace(/<[^>]+>/g, ''), html: text }
|
||||
: formatMatrixHTML(text);
|
||||
const htmlBody = html;
|
||||
|
||||
const content = {
|
||||
msgtype: MsgType.Text,
|
||||
@@ -240,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).
|
||||
@@ -273,11 +300,11 @@ export class MatrixAdapter implements ChannelAdapter {
|
||||
return true; // 'all'
|
||||
}
|
||||
|
||||
async editMessage(chatId: string, messageId: string, text: string, htmlPrefix?: string): Promise<void> {
|
||||
async editMessage(chatId: string, messageId: string, text: string): Promise<void> {
|
||||
if (!this.client) throw new Error("Matrix client not initialized");
|
||||
|
||||
const { plain, html } = formatMatrixHTML(text);
|
||||
const htmlBody = (htmlPrefix || '') + html;
|
||||
const htmlBody = html;
|
||||
const prefixedPlain = this.config.messagePrefix ? `${this.config.messagePrefix}\n\n${plain}` : plain;
|
||||
const prefixedHtml = this.config.messagePrefix ? `${this.config.messagePrefix}<br><br>${htmlBody}` : htmlBody;
|
||||
|
||||
@@ -1454,7 +1481,7 @@ export class MatrixAdapter implements ChannelAdapter {
|
||||
if (kind === 'audio') {
|
||||
this.ourAudioEvents.add(eventId);
|
||||
if (caption) {
|
||||
this.storage.storeAudioMessage(eventId, 'default', chatId, caption);
|
||||
this.storage.storeAudioMessage(eventId, chatId, chatId, caption);
|
||||
}
|
||||
const reactionContent: ReactionEventContent = {
|
||||
"m.relates_to": {
|
||||
@@ -1485,7 +1512,7 @@ export class MatrixAdapter implements ChannelAdapter {
|
||||
const audioEventId = await this.uploadAndSendAudio(roomId, audioData);
|
||||
if (audioEventId) {
|
||||
// Store mapping so 🎤 on the regenerated audio works too
|
||||
this.storage.storeAudioMessage(audioEventId, "default", roomId, text);
|
||||
this.storage.storeAudioMessage(audioEventId, roomId, roomId, text);
|
||||
}
|
||||
return audioEventId;
|
||||
} catch (err) {
|
||||
@@ -1516,7 +1543,7 @@ export class MatrixAdapter implements ChannelAdapter {
|
||||
const audioEventId = await this.uploadAndSendAudio(chatId, audioData);
|
||||
if (audioEventId) {
|
||||
// Store for 🎤 regeneration
|
||||
this.storage.storeAudioMessage(audioEventId, "default", chatId, text);
|
||||
this.storage.storeAudioMessage(audioEventId, chatId, chatId, text);
|
||||
}
|
||||
} catch (err) {
|
||||
log.error("TTS failed (non-fatal):", err);
|
||||
|
||||
@@ -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...";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface ChannelAdapter {
|
||||
|
||||
// Messaging
|
||||
sendMessage(msg: OutboundMessage): Promise<{ messageId: string }>;
|
||||
editMessage(chatId: string, messageId: string, text: string, htmlPrefix?: string): Promise<void>;
|
||||
editMessage(chatId: string, messageId: string, text: string): Promise<void>;
|
||||
sendTypingIndicator(chatId: string): Promise<void>;
|
||||
stopTypingIndicator?(chatId: string): Promise<void>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -548,6 +548,21 @@ export function configToEnv(config: LettaBotConfig): Record<string, string> {
|
||||
env.SLEEPTIME_STEP_COUNT = String(config.features.sleeptime.stepCount);
|
||||
}
|
||||
}
|
||||
if (config.features?.conscience) {
|
||||
if (config.features.conscience.agent) {
|
||||
env.CONSCIENCE_AGENT_ID = config.features.conscience.agent;
|
||||
}
|
||||
if (config.features.conscience.conversation) {
|
||||
// Only set as initial value — runtime resets mutate process.env directly
|
||||
env.CONSCIENCE_CONVERSATION_ID ??= config.features.conscience.conversation;
|
||||
}
|
||||
if (config.features.conscience.trigger) {
|
||||
env.CONSCIENCE_TRIGGER = config.features.conscience.trigger;
|
||||
}
|
||||
if (config.features.conscience.stepCount !== undefined) {
|
||||
env.CONSCIENCE_STEP_COUNT = String(config.features.conscience.stepCount);
|
||||
}
|
||||
}
|
||||
if (config.features?.inlineImages === false) {
|
||||
env.INLINE_IMAGES = 'false';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -54,6 +52,19 @@ export interface SleeptimeConfig {
|
||||
stepCount?: number;
|
||||
}
|
||||
|
||||
export type ConscienceTrigger = 'off' | 'step-count';
|
||||
|
||||
export interface ConscienceConfig {
|
||||
/** Letta agent ID for the persistent conscience agent (Aster) */
|
||||
agent?: string;
|
||||
/** Initial conversation ID (runtime value tracked in CONSCIENCE_CONVERSATION_ID) */
|
||||
conversation?: string;
|
||||
/** When to fire: 'step-count' fires every N turns, 'off' disables */
|
||||
trigger?: ConscienceTrigger;
|
||||
/** Fire conscience every N user turns (independent of sleeptime cadence) */
|
||||
stepCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for a single agent in multi-agent mode.
|
||||
* Each agent has its own name, channels, and features.
|
||||
@@ -105,6 +116,7 @@ export interface AgentConfig {
|
||||
};
|
||||
memfs?: boolean; // Enable memory filesystem (git-backed context repository) for SDK sessions
|
||||
sleeptime?: SleeptimeConfig; // Configure SDK reflection reminders (/sleeptime equivalent)
|
||||
conscience?: ConscienceConfig; // Persistent supervisory agent (Aster) — fires independently of sleeptime
|
||||
maxToolCalls?: number;
|
||||
sendFileDir?: string; // Restrict <send-file> directive to this directory (default: data/outbound)
|
||||
sendFileMaxSize?: number; // Max file size in bytes for <send-file> (default: 50MB)
|
||||
@@ -205,6 +217,7 @@ export interface LettaBotConfig {
|
||||
inlineImages?: boolean; // Send images directly to the LLM (default: true). Set false to only send file paths.
|
||||
memfs?: boolean; // Enable memory filesystem (git-backed context repository) for SDK sessions
|
||||
sleeptime?: SleeptimeConfig; // Configure SDK reflection reminders (/sleeptime equivalent)
|
||||
conscience?: ConscienceConfig; // Persistent supervisory agent (Aster) — fires independently of sleeptime
|
||||
maxToolCalls?: number; // Abort if agent calls this many tools in one turn (default: 100)
|
||||
sendFileDir?: string; // Restrict <send-file> directive to this directory (default: data/outbound)
|
||||
sendFileMaxSize?: number; // Max file size in bytes for <send-file> (default: 50MB)
|
||||
@@ -412,6 +425,8 @@ export interface DiscordConfig {
|
||||
listeningGroups?: string[]; // @deprecated Use groups.<id>.mode = "listen"
|
||||
groups?: Record<string, GroupConfig>; // Per-guild/channel settings, "*" for defaults
|
||||
ignoreBotReactions?: boolean; // Ignore all bot reactions (default: true). Set false for multi-bot setups.
|
||||
ttsUrl?: string; // TTS API endpoint (e.g. VibeVoice)
|
||||
ttsVoice?: string; // TTS voice ID
|
||||
}
|
||||
|
||||
export interface BlueskyConfig {
|
||||
@@ -973,6 +988,29 @@ export function normalizeAgents(config: LettaBotConfig): AgentConfig[] {
|
||||
};
|
||||
}
|
||||
|
||||
// Conscience (Aster) — env vars override YAML, YAML sets initial values
|
||||
const conscienceAgentId = process.env.CONSCIENCE_AGENT_ID;
|
||||
const conscienceConversationId = process.env.CONSCIENCE_CONVERSATION_ID;
|
||||
const conscienceStepCountRaw = process.env.CONSCIENCE_STEP_COUNT;
|
||||
const conscienceTriggerRaw = process.env.CONSCIENCE_TRIGGER;
|
||||
const conscienceTrigger = conscienceTriggerRaw === 'off' || conscienceTriggerRaw === 'step-count'
|
||||
? conscienceTriggerRaw as ConscienceTrigger
|
||||
: undefined;
|
||||
const conscienceStepCountParsed = conscienceStepCountRaw ? parseInt(conscienceStepCountRaw, 10) : undefined;
|
||||
const conscienceStepCount = Number.isFinite(conscienceStepCountParsed) && (conscienceStepCountParsed as number) > 0
|
||||
? conscienceStepCountParsed
|
||||
: undefined;
|
||||
|
||||
if (conscienceAgentId || conscienceConversationId || conscienceStepCount || conscienceTrigger) {
|
||||
features.conscience = {
|
||||
...features.conscience,
|
||||
...(conscienceAgentId ? { agent: conscienceAgentId } : {}),
|
||||
...(conscienceConversationId ? { conversation: conscienceConversationId } : {}),
|
||||
...(conscienceTrigger ? { trigger: conscienceTrigger } : {}),
|
||||
...(conscienceStepCount ? { stepCount: conscienceStepCount } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// Only pass features if there's actually something set
|
||||
const hasFeatures = Object.keys(features).length > 0;
|
||||
|
||||
|
||||
259
src/core/bot.ts
259
src/core/bot.ts
@@ -13,10 +13,10 @@ import { extname, resolve, join } from 'node:path';
|
||||
import type { ChannelAdapter } from '../channels/types.js';
|
||||
import type { BotConfig, InboundMessage, TriggerContext, TriggerType, StreamMsg } from './types.js';
|
||||
import { formatApiErrorForUser } from './errors.js';
|
||||
import { formatToolCallDisplay, formatReasoningDisplay, formatQuestionsForChannel, formatReasoningAsCodeBlock } from './display.js';
|
||||
import { formatToolCallDisplay, formatReasoningDisplay, formatQuestionsForChannel } from './display.js';
|
||||
import type { AgentSession } from './interfaces.js';
|
||||
import { Store } from './store.js';
|
||||
import { getPendingApprovals, rejectApproval, cancelRuns, cancelConversation, recoverOrphanedConversationApproval, getLatestRunError, getAgentModel, updateAgentModel, isRecoverableConversationId, recoverPendingApprovalsForAgent } from '../tools/letta-api.js';
|
||||
import { getPendingApprovals, rejectApproval, cancelRuns, cancelConversation, recoverOrphanedConversationApproval, getLatestRunError, getAgentModel, updateAgentModel, isRecoverableConversationId, recoverPendingApprovalsForAgent, createConversationForAgent, sendMessageToConversation } from '../tools/letta-api.js';
|
||||
import { getAgentSkillExecutableDirs, isVoiceMemoConfigured } from '../skills/loader.js';
|
||||
import { formatMessageEnvelope, formatGroupBatchEnvelope, type SessionContextOptions } from './formatter.js';
|
||||
import type { GroupBatcher } from './group-batcher.js';
|
||||
@@ -785,6 +785,44 @@ export class LettaBot implements AgentSession {
|
||||
return '⏰ Heartbeat triggered (silent mode - check server logs)';
|
||||
}
|
||||
case 'reset': {
|
||||
// !reset aster — cycle only Aster's conscience conversation, leave Ani's alone.
|
||||
if (args?.trim().toLowerCase() === 'aster') {
|
||||
const conscienceAgentId = process.env.CONSCIENCE_AGENT_ID;
|
||||
if (!conscienceAgentId) {
|
||||
return 'Conscience agent not configured (CONSCIENCE_AGENT_ID not set).';
|
||||
}
|
||||
const newConscienceConvId = await createConversationForAgent(conscienceAgentId);
|
||||
if (!newConscienceConvId) {
|
||||
return 'Failed to create a new conscience conversation. Check server logs.';
|
||||
}
|
||||
// Update in-memory env var so the running process uses the new conversation immediately.
|
||||
process.env.CONSCIENCE_CONVERSATION_ID = newConscienceConvId;
|
||||
// Persist to store (lettabot-agent.json) for reference.
|
||||
this.store.setAgentField('Aster', 'conversationId', newConscienceConvId);
|
||||
// Patch the systemd service file so restarts also pick up the new conversation.
|
||||
const serviceFile = '/home/ani/.config/systemd/user/ani-bridge.service';
|
||||
try {
|
||||
const { writeFile } = await import('node:fs/promises');
|
||||
const current = await readFile(serviceFile, 'utf-8');
|
||||
const updated = current.replace(
|
||||
/^(Environment=CONSCIENCE_CONVERSATION_ID=).+$/m,
|
||||
`$1${newConscienceConvId}`,
|
||||
);
|
||||
await writeFile(serviceFile, updated, 'utf-8');
|
||||
// Reload systemd unit definitions (no restart — just picks up the edited file).
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
execFile('systemctl', ['--user', 'daemon-reload'], (err) => {
|
||||
if (err) reject(err); else resolve();
|
||||
});
|
||||
});
|
||||
log.info(`/reset aster - service file updated, daemon reloaded: ${newConscienceConvId}`);
|
||||
} catch (svcErr) {
|
||||
log.warn(`/reset aster - failed to patch service file: ${svcErr}`);
|
||||
}
|
||||
log.info(`/reset aster - conscience conversation cycled: ${newConscienceConvId}`);
|
||||
return `Aster's conversation reset. New conversation: \`${newConscienceConvId}\`\nService file updated — restart-safe.`;
|
||||
}
|
||||
|
||||
// Always scope the reset to the caller's conversation key so that
|
||||
// other channels/chats' conversations are never silently destroyed.
|
||||
// resolveConversationKey returns 'shared' for non-override channels,
|
||||
@@ -806,6 +844,40 @@ export class LettaBot implements AgentSession {
|
||||
const session = await this.sessionManager.ensureSessionForKey(convKey);
|
||||
const newConvId = session.conversationId || '(pending)';
|
||||
this.sessionManager.persistSessionState(session, convKey);
|
||||
|
||||
// Reset conscience conversation alongside Ani's.
|
||||
// This ensures failure notifications target the new active conversation
|
||||
// and conscience starts fresh rather than replaying a broken context.
|
||||
const conscienceAgentId = process.env.CONSCIENCE_AGENT_ID;
|
||||
if (conscienceAgentId) {
|
||||
const newConscienceConvId = await createConversationForAgent(conscienceAgentId);
|
||||
if (newConscienceConvId) {
|
||||
process.env.CONSCIENCE_CONVERSATION_ID = newConscienceConvId;
|
||||
this.store.setAgentField('Aster', 'conversationId', newConscienceConvId);
|
||||
// Also patch the service file so restarts pick up the new conversation.
|
||||
const serviceFile = '/home/ani/.config/systemd/user/ani-bridge.service';
|
||||
try {
|
||||
const { writeFile } = await import('node:fs/promises');
|
||||
const current = await readFile(serviceFile, 'utf-8');
|
||||
const updated = current.replace(
|
||||
/^(Environment=CONSCIENCE_CONVERSATION_ID=).+$/m,
|
||||
`$1${newConscienceConvId}`,
|
||||
);
|
||||
await writeFile(serviceFile, updated, 'utf-8');
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
execFile('systemctl', ['--user', 'daemon-reload'], (err) => {
|
||||
if (err) reject(err); else resolve();
|
||||
});
|
||||
});
|
||||
} catch (svcErr) {
|
||||
log.warn(`/reset - failed to patch conscience service var: ${svcErr}`);
|
||||
}
|
||||
log.info(`/reset - conscience conversation cycled: ${newConscienceConvId}`);
|
||||
} else {
|
||||
log.warn('/reset - Failed to cycle conscience conversation; will resume the previous one.');
|
||||
}
|
||||
}
|
||||
|
||||
if (convKey === 'shared') {
|
||||
return `Conversation reset. New conversation: ${newConvId}\n(Agent memory is preserved.)`;
|
||||
}
|
||||
@@ -1322,7 +1394,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 +1406,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 +1513,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 +1530,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 +1553,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 +1595,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 +1668,38 @@ 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**';
|
||||
// Post full result to thread (chunked if very long)
|
||||
const maxChunk = 16000; // well under Matrix's ~65KB body limit
|
||||
const content = event.content;
|
||||
if (content.length <= maxChunk) {
|
||||
adapter.sendThreadMessage(thread.rootEventId, thread.chatId, `${status}\n${content}`)
|
||||
.catch(err => log.warn('Failed to post subagent result to thread:', err));
|
||||
} else {
|
||||
// Send status header + chunked content
|
||||
const chunks: string[] = [];
|
||||
for (let i = 0; i < content.length; i += maxChunk) {
|
||||
chunks.push(content.slice(i, i + maxChunk));
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
await adapter.sendThreadMessage!(thread.rootEventId, thread.chatId, `${status} (${chunks.length} parts)`);
|
||||
for (const chunk of chunks) {
|
||||
await adapter.sendThreadMessage!(thread.rootEventId, thread.chatId, chunk);
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn('Failed to post subagent result to thread:', err);
|
||||
}
|
||||
})();
|
||||
}
|
||||
subagentThreads.delete(event.toolCallId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1594,7 +1729,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) {
|
||||
@@ -1880,42 +2015,25 @@ export class LettaBot implements AgentSession {
|
||||
await new Promise(resolve => setTimeout(resolve, waitMs));
|
||||
}
|
||||
|
||||
// Determine if reasoning should be shown for this room
|
||||
const chatId = msg.chatId;
|
||||
const noReasoningRooms = this.config.display?.noReasoningRooms || [];
|
||||
const reasoningRooms = this.config.display?.reasoningRooms;
|
||||
const shouldShowReasoning = this.config.display?.showReasoning &&
|
||||
!noReasoningRooms.includes(chatId) &&
|
||||
(!reasoningRooms || reasoningRooms.length === 0 || reasoningRooms.includes(chatId));
|
||||
|
||||
// Build reasoning HTML prefix if available (injected into formatted_body only)
|
||||
let reasoningHtmlPrefix: string | undefined;
|
||||
if (collectedReasoning.trim() && shouldShowReasoning) {
|
||||
const reasoningBlock = formatReasoningAsCodeBlock(
|
||||
collectedReasoning,
|
||||
adapter.id,
|
||||
this.config.display?.reasoningMaxChars
|
||||
);
|
||||
if (reasoningBlock) {
|
||||
reasoningHtmlPrefix = reasoningBlock.text;
|
||||
log.info(`Reasoning block generated (${reasoningHtmlPrefix.length} chars) for ${chatId}`);
|
||||
}
|
||||
}
|
||||
|
||||
const finalResponse = this.prefixResponse(response);
|
||||
|
||||
try {
|
||||
if (messageId) {
|
||||
await adapter.editMessage(msg.chatId, messageId, finalResponse, reasoningHtmlPrefix);
|
||||
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, htmlPrefix: reasoningHtmlPrefix });
|
||||
await adapter.sendMessage({ chatId: msg.chatId, text: finalResponse, threadId: msg.threadId });
|
||||
}
|
||||
sentAnyMessage = true;
|
||||
this.store.resetRecoveryAttempts();
|
||||
} catch (sendErr) {
|
||||
log.warn('Final message delivery failed:', sendErr instanceof Error ? sendErr.message : sendErr);
|
||||
try {
|
||||
const result = await adapter.sendMessage({ chatId: msg.chatId, text: finalResponse, threadId: msg.threadId, htmlPrefix: reasoningHtmlPrefix });
|
||||
const result = await adapter.sendMessage({ chatId: msg.chatId, text: finalResponse, threadId: msg.threadId });
|
||||
messageId = result.messageId ?? null;
|
||||
sentAnyMessage = true;
|
||||
this.store.resetRecoveryAttempts();
|
||||
@@ -1931,7 +2049,7 @@ export class LettaBot implements AgentSession {
|
||||
// 🎤 on bot's TEXT message (tap to regenerate TTS audio)
|
||||
adapter.addReaction?.(msg.chatId, messageId, '🎤').catch(() => {});
|
||||
// Store raw text — adapter's TTS layer will clean it at synthesis time
|
||||
adapter.storeAudioMessage?.(messageId, 'default', msg.chatId, response);
|
||||
adapter.storeAudioMessage?.(messageId, convKey, msg.chatId, response);
|
||||
// Generate TTS audio only in response to voice input
|
||||
if (msg.isVoiceInput) {
|
||||
adapter.sendAudio?.(msg.chatId, response).catch((err) => {
|
||||
@@ -1943,9 +2061,24 @@ 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(() => {});
|
||||
}
|
||||
|
||||
// Fire conscience agent (Aster) if configured — independent of sleeptime
|
||||
this.triggerConscience({
|
||||
source: 'message',
|
||||
seq,
|
||||
label: `turn ${seq} | user:${msg.userId} | ${msg.channel}:${msg.chatId}`,
|
||||
}).catch(err => log.warn('triggerConscience unexpected error:', err));
|
||||
|
||||
} 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,
|
||||
@@ -2194,6 +2327,13 @@ export class LettaBot implements AgentSession {
|
||||
log.warn(`Silent mode: agent produced ${response.length} chars but did NOT use lettabot-message CLI or directives — response discarded. If this keeps happening, the agent's model may not be following silent mode instructions.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fire conscience agent (Aster) after every heartbeat turn
|
||||
this.triggerConscience({
|
||||
source: 'heartbeat',
|
||||
label: `heartbeat type=${context?.type ?? 'heartbeat'}`,
|
||||
}).catch(err => log.warn('triggerConscience unexpected error:', err));
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Invalidate on stream errors so next call gets a fresh subprocess
|
||||
@@ -2346,4 +2486,51 @@ export class LettaBot implements AgentSession {
|
||||
getLastUserMessageTime(): Date | null {
|
||||
return this.lastUserMessageTime;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Conscience (Aster) trigger
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Fire the persistent conscience agent (Aster) if configured.
|
||||
* For 'message' source: respects stepCount cadence.
|
||||
* For 'heartbeat' source: fires unconditionally (every heartbeat).
|
||||
* Runs fire-and-forget — errors are logged but never propagate to the caller.
|
||||
*/
|
||||
private async triggerConscience(opts: {
|
||||
source: 'message' | 'heartbeat';
|
||||
seq?: number;
|
||||
label?: string;
|
||||
}): Promise<void> {
|
||||
const conscience = this.config.conscience;
|
||||
if (!conscience || conscience.trigger === 'off') return;
|
||||
|
||||
// User message path: respect stepCount cadence
|
||||
if (opts.source === 'message') {
|
||||
const stepCount = conscience.stepCount ?? 1;
|
||||
if ((opts.seq ?? 0) % stepCount !== 0) return;
|
||||
}
|
||||
// Heartbeat path: always fires
|
||||
|
||||
// Agent ID is static (from config / CONSCIENCE_AGENT_ID env var).
|
||||
// Conversation ID is dynamic — mutated by /reset aster at runtime.
|
||||
const agentId = conscience.agent || process.env.CONSCIENCE_AGENT_ID;
|
||||
const conversationId = process.env.CONSCIENCE_CONVERSATION_ID || conscience.conversation;
|
||||
|
||||
if (!agentId || !conversationId) {
|
||||
log.warn('triggerConscience: no agent ID or conversation ID — skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const label = opts.label ?? opts.source;
|
||||
const prompt = `[Conscience audit — ${label}]`;
|
||||
|
||||
log.info(`triggerConscience: firing Aster (source=${opts.source}, seq=${opts.seq ?? 'n/a'}, conv=${conversationId})`);
|
||||
try {
|
||||
await sendMessageToConversation(conversationId, prompt);
|
||||
log.info(`triggerConscience: Aster audit complete (source=${opts.source})`);
|
||||
} catch (err) {
|
||||
log.warn('triggerConscience: failed to trigger Aster (non-fatal):', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +230,19 @@ export function formatReasoningDisplay(
|
||||
// Signal: no blockquote support, use italic
|
||||
return { text: `**Thinking**\n_${truncated}_` };
|
||||
}
|
||||
if (channelId === 'matrix') {
|
||||
// Matrix: collapsible <details> block — rendered natively by Element Web,
|
||||
// falls back to visible block on mobile clients.
|
||||
const escaped = truncated
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>');
|
||||
return {
|
||||
text: `<details><summary>🧠 Thinking</summary><br>${escaped}</details>`,
|
||||
parseMode: 'HTML',
|
||||
};
|
||||
}
|
||||
if (channelId === 'telegram' || channelId === 'telegram-mtproto') {
|
||||
// Telegram: use HTML blockquote to bypass telegramify-markdown spacing.
|
||||
// Convert basic markdown inline formatting to HTML tags so bold/italic
|
||||
@@ -255,36 +268,6 @@ export function formatReasoningDisplay(
|
||||
return { text: `> **Thinking**\n${quoted}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Format reasoning as a collapsible <details> block for prepending to the response.
|
||||
* Returns pre-escaped HTML meant to be injected directly into formatted_body
|
||||
* (bypasses the adapter's markdown-to-HTML conversion to avoid double-escaping).
|
||||
*/
|
||||
export function formatReasoningAsCodeBlock(
|
||||
text: string,
|
||||
channelId?: string,
|
||||
reasoningMaxChars?: number,
|
||||
): { text: string } | null {
|
||||
const maxChars = reasoningMaxChars ?? 0;
|
||||
const cleaned = text.split('\n').map(line => line.trimStart()).join('\n').trim();
|
||||
if (!cleaned) return null;
|
||||
|
||||
const truncated = maxChars > 0 && cleaned.length > maxChars
|
||||
? cleaned.slice(0, maxChars) + '...'
|
||||
: cleaned;
|
||||
|
||||
// HTML-escape the reasoning content, then convert newlines to <br>
|
||||
const escaped = truncated
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
return {
|
||||
text: `<details><summary>🧠 Thinking</summary><br>${escaped}</details><br>`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format AskUserQuestion options for channel display.
|
||||
*/
|
||||
|
||||
@@ -352,9 +352,6 @@ function buildResponseDirectives(msg: InboundMessage): string[] {
|
||||
lines.push(`- Prefer directives over tool calls for reactions (faster and cheaper)`);
|
||||
}
|
||||
|
||||
// voice memo (always available -- TTS config is server-side)
|
||||
lines.push(`- \`<actions><voice>Your message here</voice></actions>\` — send a voice memo via TTS`);
|
||||
|
||||
// file sending (only if channel supports it)
|
||||
if (supportsFiles) {
|
||||
lines.push(`- \`<send-file path="/path/to/file.png" kind="image" />\` — send a file (restricted to configured directory)`);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -420,6 +420,18 @@ export class Store {
|
||||
this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a field on any named agent entry in the store.
|
||||
* Used to persist auxiliary agent data (e.g. Aster's conversation ID after !reset).
|
||||
*/
|
||||
setAgentField(agentName: string, field: string, value: string | null): void {
|
||||
if (!this.data.agents[agentName]) {
|
||||
this.data.agents[agentName] = { agentId: null };
|
||||
}
|
||||
(this.data.agents[agentName] as unknown as Record<string, unknown>)[field] = value;
|
||||
this.save();
|
||||
}
|
||||
|
||||
getInfo(): AgentStore {
|
||||
return { ...this.agentData() };
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -132,9 +132,6 @@ export interface OutboundMessage {
|
||||
* 'HTML') and to skip its default markdown conversion. Adapters that don't
|
||||
* support the specified mode ignore this and fall back to default. */
|
||||
parseMode?: string;
|
||||
/** Pre-escaped HTML to prepend to formatted_body only (bypasses markdown conversion).
|
||||
* Used for reasoning blocks with <details> tags that would be double-escaped. */
|
||||
htmlPrefix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,8 +156,8 @@ export interface SkillsConfig {
|
||||
additionalSkills?: string[];
|
||||
}
|
||||
|
||||
import type { SleeptimeTrigger, SleeptimeBehavior, SleeptimeConfig } from '../config/types.js';
|
||||
export type { SleeptimeTrigger, SleeptimeBehavior, SleeptimeConfig };
|
||||
import type { SleeptimeTrigger, SleeptimeBehavior, SleeptimeConfig, ConscienceTrigger, ConscienceConfig } from '../config/types.js';
|
||||
export type { SleeptimeTrigger, SleeptimeBehavior, SleeptimeConfig, ConscienceTrigger, ConscienceConfig };
|
||||
|
||||
/**
|
||||
* Bot configuration
|
||||
@@ -178,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
|
||||
@@ -191,6 +187,7 @@ export interface BotConfig {
|
||||
// Memory filesystem (context repository)
|
||||
memfs?: boolean; // true -> --memfs, false -> --no-memfs, undefined -> leave unchanged
|
||||
sleeptime?: SleeptimeConfig; // Configure SDK reflection reminders (/sleeptime equivalent)
|
||||
conscience?: ConscienceConfig; // Persistent supervisory agent (Aster)
|
||||
|
||||
// Security
|
||||
redaction?: import('./redact.js').RedactionConfig;
|
||||
|
||||
@@ -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() !== '<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}`);
|
||||
|
||||
// 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) {
|
||||
|
||||
29
src/main.ts
29
src/main.ts
@@ -385,6 +385,7 @@ async function main() {
|
||||
sendFileCleanup: agentConfig.features?.sendFileCleanup,
|
||||
memfs: resolvedMemfs,
|
||||
sleeptime: effectiveSleeptime,
|
||||
conscience: agentConfig.features?.conscience,
|
||||
display: agentConfig.features?.display,
|
||||
conversationMode: agentConfig.conversations?.mode || 'shared',
|
||||
heartbeatConversation: agentConfig.conversations?.heartbeat || 'last-active',
|
||||
@@ -590,12 +591,27 @@ async function main() {
|
||||
await gateway.start();
|
||||
|
||||
// Olm WASM (matrix-js-sdk) registers process.on("uncaughtException", (e) => { throw e })
|
||||
// during Olm.init(). Without this fix, any uncaught async exception crashes the bot.
|
||||
// Must run AFTER gateway.start() since that's when the Matrix adapter initialises Olm.
|
||||
process.removeAllListeners('uncaughtException');
|
||||
process.removeAllListeners('unhandledRejection');
|
||||
process.on('uncaughtException', (err) => { log.error('Uncaught exception (suppressed):', err); });
|
||||
process.on('unhandledRejection', (reason) => { log.error('Unhandled rejection (suppressed):', reason); });
|
||||
// during Olm.init(). That rethrow handler turns any uncaught async error into a crash.
|
||||
// Surgically remove only the Olm-registered rethrow handlers, preserving any others.
|
||||
for (const event of ['uncaughtException', 'unhandledRejection'] as const) {
|
||||
const listeners = process.listeners(event);
|
||||
for (const listener of listeners) {
|
||||
// Olm's handler is a one-liner that rethrows: (e) => { throw e }
|
||||
// Detect by source — short function body that just throws its argument.
|
||||
const src = listener.toString();
|
||||
if (src.includes('throw') && src.length < 50) {
|
||||
process.removeListener(event, listener as (...args: unknown[]) => void);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Safety net: log unhandled errors instead of crashing.
|
||||
// Only adds if no other handlers remain (avoids clobbering app-registered handlers).
|
||||
if (process.listenerCount('uncaughtException') === 0) {
|
||||
process.on('uncaughtException', (err) => { log.error('Uncaught exception (suppressed):', err); });
|
||||
}
|
||||
if (process.listenerCount('unhandledRejection') === 0) {
|
||||
process.on('unhandledRejection', (reason) => { log.error('Unhandled rejection (suppressed):', reason); });
|
||||
}
|
||||
|
||||
// Start API server - uses gateway for delivery
|
||||
const apiPort = parseInt(process.env.PORT || '8080', 10);
|
||||
@@ -615,6 +631,7 @@ async function main() {
|
||||
stores: agentStores,
|
||||
agentChannels: agentChannelMap,
|
||||
sessionInvalidators,
|
||||
heartbeatServices: services.heartbeatServices,
|
||||
});
|
||||
|
||||
// Startup banner
|
||||
|
||||
148
src/models.json
148
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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -550,6 +550,12 @@ export async function rejectApproval(
|
||||
log.warn(`Approval already resolved for tool call ${approval.toolCallId}`);
|
||||
return true;
|
||||
}
|
||||
// Re-throw rate limit errors so callers can bail out early instead of
|
||||
// hammering the API in a tight loop.
|
||||
if (err?.status === 429) {
|
||||
log.error('Failed to reject approval:', e);
|
||||
throw e;
|
||||
}
|
||||
log.error('Failed to reject approval:', e);
|
||||
return false;
|
||||
}
|
||||
@@ -987,3 +993,33 @@ export async function disableAllToolApprovals(agentId: string): Promise<number>
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fresh conversation for an existing agent.
|
||||
* Used by !reset to cycle Aster's conversation alongside Ani's.
|
||||
*/
|
||||
export async function createConversationForAgent(agentId: string): Promise<string | null> {
|
||||
try {
|
||||
const client = getClient();
|
||||
const conversation = await client.conversations.create({ agent_id: agentId });
|
||||
return conversation.id;
|
||||
} catch (e) {
|
||||
log.error('Failed to create conversation for agent:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a fire-and-forget message to an existing conversation.
|
||||
* Uses streaming=false so the call completes before returning.
|
||||
*/
|
||||
export async function sendMessageToConversation(
|
||||
conversationId: string,
|
||||
text: string,
|
||||
): Promise<void> {
|
||||
const client = getClient();
|
||||
await client.conversations.messages.create(conversationId, {
|
||||
input: text,
|
||||
streaming: false,
|
||||
} as Parameters<typeof client.conversations.messages.create>[1]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user