quadwork 1.6.3 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +6 -1
  2. package/bin/quadwork.js +42 -59
  3. package/bridges/discord/__pycache__/discord_bridge.cpython-314.pyc +0 -0
  4. package/bridges/discord/discord_bridge.py +63 -18
  5. package/out/404.html +1 -1
  6. package/out/__next.__PAGE__.txt +3 -3
  7. package/out/__next._full.txt +14 -14
  8. package/out/__next._head.txt +4 -4
  9. package/out/__next._index.txt +7 -7
  10. package/out/__next._tree.txt +2 -2
  11. package/out/_next/static/KleEGCyPe1ovSiWKr9GnB/_ssgManifest.js +1 -0
  12. package/out/_next/static/chunks/{0ftcj9qreh~p4.js → 0_y-97xg1l.bn.js} +13 -13
  13. package/out/_next/static/chunks/{0-jj8tpbs48x4.js → 0cploqtj5jy4z.js} +1 -1
  14. package/out/_next/static/chunks/0koy9hplvko3w.css +2 -0
  15. package/out/_next/static/chunks/14zyqqpdz2x7j.js +1 -0
  16. package/out/_next/static/chunks/{0o3_.p5ivp5sp.js → 152f2hu-ivy6f.js} +1 -1
  17. package/out/_next/static/media/favicon.05o2q2p4kvnq_.ico +0 -0
  18. package/out/_not-found/__next._full.txt +13 -13
  19. package/out/_not-found/__next._head.txt +4 -4
  20. package/out/_not-found/__next._index.txt +7 -7
  21. package/out/_not-found/__next._not-found.__PAGE__.txt +2 -2
  22. package/out/_not-found/__next._not-found.txt +3 -3
  23. package/out/_not-found/__next._tree.txt +2 -2
  24. package/out/_not-found.html +1 -1
  25. package/out/_not-found.txt +13 -13
  26. package/out/app-shell/__next._full.txt +13 -13
  27. package/out/app-shell/__next._head.txt +4 -4
  28. package/out/app-shell/__next._index.txt +7 -7
  29. package/out/app-shell/__next._tree.txt +2 -2
  30. package/out/app-shell/__next.app-shell.__PAGE__.txt +2 -2
  31. package/out/app-shell/__next.app-shell.txt +3 -3
  32. package/out/app-shell.html +1 -1
  33. package/out/app-shell.txt +13 -13
  34. package/out/favicon.ico +0 -0
  35. package/out/icon.png +0 -0
  36. package/out/icon.svg +11 -4
  37. package/out/index.html +1 -1
  38. package/out/index.txt +14 -14
  39. package/out/project/_/__next._full.txt +14 -14
  40. package/out/project/_/__next._head.txt +4 -4
  41. package/out/project/_/__next._index.txt +7 -7
  42. package/out/project/_/__next._tree.txt +2 -2
  43. package/out/project/_/__next.project.$d$id.__PAGE__.txt +3 -3
  44. package/out/project/_/__next.project.$d$id.txt +3 -3
  45. package/out/project/_/__next.project.txt +3 -3
  46. package/out/project/_/queue/__next._full.txt +14 -14
  47. package/out/project/_/queue/__next._head.txt +4 -4
  48. package/out/project/_/queue/__next._index.txt +7 -7
  49. package/out/project/_/queue/__next._tree.txt +2 -2
  50. package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +3 -3
  51. package/out/project/_/queue/__next.project.$d$id.queue.txt +3 -3
  52. package/out/project/_/queue/__next.project.$d$id.txt +3 -3
  53. package/out/project/_/queue/__next.project.txt +3 -3
  54. package/out/project/_/queue.html +1 -1
  55. package/out/project/_/queue.txt +14 -14
  56. package/out/project/_.html +1 -1
  57. package/out/project/_.txt +14 -14
  58. package/out/quadwork-symbol.svg +15 -0
  59. package/out/settings/__next._full.txt +14 -14
  60. package/out/settings/__next._head.txt +4 -4
  61. package/out/settings/__next._index.txt +7 -7
  62. package/out/settings/__next._tree.txt +2 -2
  63. package/out/settings/__next.settings.__PAGE__.txt +3 -3
  64. package/out/settings/__next.settings.txt +3 -3
  65. package/out/settings.html +1 -1
  66. package/out/settings.txt +14 -14
  67. package/out/setup/__next._full.txt +14 -14
  68. package/out/setup/__next._head.txt +4 -4
  69. package/out/setup/__next._index.txt +7 -7
  70. package/out/setup/__next._tree.txt +2 -2
  71. package/out/setup/__next.setup.__PAGE__.txt +3 -3
  72. package/out/setup/__next.setup.txt +3 -3
  73. package/out/setup.html +1 -1
  74. package/out/setup.txt +14 -14
  75. package/package.json +2 -1
  76. package/server/index.js +77 -20
  77. package/server/queue-watcher.js +20 -11
  78. package/server/routes.discordBridge.test.js +5 -5
  79. package/server/routes.js +141 -212
  80. package/server/routes.telegramBridge.test.js +6 -6
  81. package/templates/config.toml +7 -7
  82. package/out/_next/static/ZT6D996Dw9auBgcm_HHTY/_ssgManifest.js +0 -1
  83. package/out/_next/static/chunks/0j-zyy6.adwtl.css +0 -2
  84. package/out/_next/static/chunks/0n7b.b.q4nmo..js +0 -1
  85. package/out/_next/static/chunks/0~xrqi87fqraz.js +0 -1
  86. package/out/_next/static/chunks/12i404gkhv7q..js +0 -4
  87. package/out/_next/static/media/favicon.0qzfoe774zb3r.ico +0 -0
  88. package/out/project/_/memory/__next._full.txt +0 -21
  89. package/out/project/_/memory/__next._head.txt +0 -6
  90. package/out/project/_/memory/__next._index.txt +0 -8
  91. package/out/project/_/memory/__next._tree.txt +0 -3
  92. package/out/project/_/memory/__next.project.$d$id.memory.__PAGE__.txt +0 -6
  93. package/out/project/_/memory/__next.project.$d$id.memory.txt +0 -5
  94. package/out/project/_/memory/__next.project.$d$id.txt +0 -5
  95. package/out/project/_/memory/__next.project.txt +0 -5
  96. package/out/project/_/memory.html +0 -1
  97. package/out/project/_/memory.txt +0 -21
  98. /package/out/_next/static/{ZT6D996Dw9auBgcm_HHTY → KleEGCyPe1ovSiWKr9GnB}/_buildManifest.js +0 -0
  99. /package/out/_next/static/{ZT6D996Dw9auBgcm_HHTY → KleEGCyPe1ovSiWKr9GnB}/_clientMiddlewareManifest.js +0 -0
package/server/routes.js CHANGED
@@ -8,6 +8,8 @@ const fs = require("fs");
8
8
  const path = require("path");
9
9
  const os = require("os");
10
10
 
11
+ const multer = require("multer");
12
+
11
13
  const router = express.Router();
12
14
 
13
15
  const CONFIG_DIR = path.join(os.homedir(), ".quadwork");
@@ -113,18 +115,32 @@ function chatAuthHeaders(token) {
113
115
  }
114
116
 
115
117
  router.get("/api/chat", async (req, res) => {
118
+ const projectId = req.query.project;
116
119
  const apiPath = req.query.path || "/api/messages";
117
- const { url: base, token } = getChattrConfig(req.query.project);
120
+ const { url: base, token } = getChattrConfig(projectId);
118
121
 
119
- const fwd = new URLSearchParams();
120
- for (const [k, v] of Object.entries(req.query)) {
121
- if (k !== "path") fwd.set(k, String(v));
122
- }
123
- if (token) fwd.set("token", token);
122
+ const buildUrl = (tok) => {
123
+ const fwd = new URLSearchParams();
124
+ for (const [k, v] of Object.entries(req.query)) {
125
+ if (k !== "path") fwd.set(k, String(v));
126
+ }
127
+ if (tok) fwd.set("token", tok);
128
+ return `${base}${apiPath}?${fwd.toString()}`;
129
+ };
124
130
 
125
- const url = `${base}${apiPath}?${fwd.toString()}`;
126
131
  try {
127
- const r = await fetch(url, { headers: chatAuthHeaders(token) });
132
+ const r = await fetch(buildUrl(token), { headers: chatAuthHeaders(token) });
133
+ // #448: on 401/403, re-sync the session token from AC and retry
134
+ // once. The stored token may be stale after an AC restart.
135
+ if ((r.status === 401 || r.status === 403) && projectId) {
136
+ try { await syncChattrToken(projectId); } catch {}
137
+ const { token: refreshed } = getChattrConfig(projectId);
138
+ if (refreshed && refreshed !== token) {
139
+ const retry = await fetch(buildUrl(refreshed), { headers: chatAuthHeaders(refreshed) });
140
+ if (!retry.ok) return res.status(retry.status).json({ error: `AgentChattr returned ${retry.status}` });
141
+ return res.json(await retry.json());
142
+ }
143
+ }
128
144
  if (!r.ok) return res.status(r.status).json({ error: `AgentChattr returned ${r.status}` });
129
145
  res.json(await r.json());
130
146
  } catch (err) {
@@ -977,6 +993,53 @@ router.post("/api/chat", async (req, res) => {
977
993
  }
978
994
  });
979
995
 
996
+ // ─── Image upload (#466) ──────────────────────────────────────────────────
997
+
998
+ const UPLOAD_MAX_BYTES = 10 * 1024 * 1024; // 10MB
999
+ const ALLOWED_MIME = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
1000
+ const uploadStorage = multer.diskStorage({
1001
+ destination: (req, _file, cb) => {
1002
+ const projectId = req.query.project || "";
1003
+ if (!projectId || /[/\\]/.test(projectId)) return cb(new Error("Invalid project"));
1004
+ const dir = path.join(CONFIG_DIR, projectId, "uploads");
1005
+ fs.mkdirSync(dir, { recursive: true });
1006
+ cb(null, dir);
1007
+ },
1008
+ filename: (_req, file, cb) => {
1009
+ const ext = path.extname(file.originalname) || ".png";
1010
+ cb(null, `upload-${Date.now()}${ext}`);
1011
+ },
1012
+ });
1013
+ const upload = multer({
1014
+ storage: uploadStorage,
1015
+ limits: { fileSize: UPLOAD_MAX_BYTES },
1016
+ fileFilter: (_req, file, cb) => {
1017
+ if (ALLOWED_MIME.has(file.mimetype)) cb(null, true);
1018
+ else cb(new Error(`Unsupported type: ${file.mimetype}`));
1019
+ },
1020
+ });
1021
+
1022
+ router.post("/api/upload", upload.single("file"), (req, res) => {
1023
+ if (!req.file) return res.status(400).json({ error: "No file uploaded" });
1024
+ return res.json({
1025
+ ok: true,
1026
+ path: req.file.path,
1027
+ name: req.file.filename,
1028
+ });
1029
+ });
1030
+
1031
+ // Serve uploaded images for thumbnail rendering
1032
+ router.get("/api/uploads/:project/:filename", (req, res) => {
1033
+ const { project, filename } = req.params;
1034
+ // Sanitize to prevent directory traversal
1035
+ if (/[/\\]/.test(project) || /[/\\]/.test(filename)) {
1036
+ return res.status(400).json({ error: "Invalid path" });
1037
+ }
1038
+ const filePath = path.join(CONFIG_DIR, project, "uploads", filename);
1039
+ if (!fs.existsSync(filePath)) return res.status(404).json({ error: "Not found" });
1040
+ res.sendFile(filePath);
1041
+ });
1042
+
980
1043
  // ─── Projects (dashboard aggregation) ──────────────────────────────────────
981
1044
 
982
1045
  function ghJson(args) {
@@ -1674,184 +1737,7 @@ router.get("/api/batch-progress", async (req, res) => {
1674
1737
  res.json(data);
1675
1738
  });
1676
1739
 
1677
- // ─── Memory ────────────────────────────────────────────────────────────────
1678
-
1679
- function getProject(projectId) {
1680
- try {
1681
- const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
1682
- return cfg.projects?.find((p) => p.id === projectId) || null;
1683
- } catch {
1684
- return null;
1685
- }
1686
- }
1687
-
1688
- function getMemoryPaths(project) {
1689
- const workDir = project.working_dir || "";
1690
- return {
1691
- cardsDir: project.memory_cards_dir || path.join(workDir, "..", "agent-memory", "archive", "v2", "cards"),
1692
- sharedMemoryPath: project.shared_memory_path || path.join(workDir, "..", "agent-memory", "central", "short-term", "agent-os.md"),
1693
- butlerDir: project.butler_scripts_dir || path.join(workDir, "..", "agent-memory", "scripts"),
1694
- };
1695
- }
1696
-
1697
- function findMdFiles(dir) {
1698
- const results = [];
1699
- if (!fs.existsSync(dir)) return results;
1700
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1701
- const full = path.join(dir, entry.name);
1702
- if (entry.isDirectory()) results.push(...findMdFiles(full));
1703
- else if (entry.name.endsWith(".md")) results.push(full);
1704
- }
1705
- return results;
1706
- }
1707
-
1708
- function parseFrontmatter(content) {
1709
- const fm = {};
1710
- const match = content.match(/^---\n([\s\S]*?)\n---/);
1711
- if (!match) return fm;
1712
- for (const line of match[1].split("\n")) {
1713
- const idx = line.indexOf(":");
1714
- if (idx > 0) {
1715
- const key = line.slice(0, idx).trim();
1716
- let val = line.slice(idx + 1).trim();
1717
- if (val.startsWith("[") && val.endsWith("]")) val = val.slice(1, -1).trim();
1718
- fm[key] = val;
1719
- }
1720
- }
1721
- return fm;
1722
- }
1723
-
1724
- router.get("/api/memory", (req, res) => {
1725
- const projectId = req.query.project || "";
1726
- const action = req.query.action || "cards";
1727
- const project = getProject(projectId);
1728
- if (!project) return res.status(404).json({ error: "Project not found" });
1729
-
1730
- const paths = getMemoryPaths(project);
1731
-
1732
- if (action === "cards") {
1733
- const search = req.query.search || "";
1734
- try {
1735
- const files = findMdFiles(paths.cardsDir);
1736
- const cards = files.map((fullPath) => {
1737
- const content = fs.readFileSync(fullPath, "utf-8");
1738
- const fm = parseFrontmatter(content);
1739
- const relPath = path.relative(paths.cardsDir, fullPath);
1740
- const body = content.replace(/^---\n[\s\S]*?\n---\n?/, "").trim();
1741
- const firstLine = body.split("\n")[0]?.replace(/^#\s*/, "").trim();
1742
- return {
1743
- file: relPath,
1744
- title: firstLine || fm.id || path.basename(fullPath, ".md"),
1745
- date: fm.at || "",
1746
- agent: fm.by || "",
1747
- tags: fm.tags || "",
1748
- content: body,
1749
- };
1750
- });
1751
- cards.sort((a, b) => b.date.localeCompare(a.date));
1752
- if (search) {
1753
- const q = search.toLowerCase();
1754
- return res.json(cards.filter((c) =>
1755
- c.title.toLowerCase().includes(q) || c.agent.toLowerCase().includes(q) || c.tags.toLowerCase().includes(q) || c.content.toLowerCase().includes(q)
1756
- ));
1757
- }
1758
- return res.json(cards);
1759
- } catch {
1760
- return res.json([]);
1761
- }
1762
- }
1763
-
1764
- if (action === "status") {
1765
- const agents = project.agents || {};
1766
- const status = {};
1767
- for (const [id, agent] of Object.entries(agents)) {
1768
- const targetPath = path.join(agent.cwd || "", "shared-memory.md");
1769
- if (fs.existsSync(targetPath)) {
1770
- const stat = fs.statSync(targetPath);
1771
- status[id] = { injected: true, lastModified: stat.mtime.toISOString() };
1772
- } else {
1773
- status[id] = { injected: false, lastModified: null };
1774
- }
1775
- }
1776
- const sourceExists = fs.existsSync(paths.sharedMemoryPath);
1777
- return res.json({ agents: status, sourceExists });
1778
- }
1779
-
1780
- if (action === "shared-memory") {
1781
- try {
1782
- const content = fs.readFileSync(paths.sharedMemoryPath, "utf-8");
1783
- return res.json({ content, path: paths.sharedMemoryPath });
1784
- } catch {
1785
- return res.json({ content: "", path: paths.sharedMemoryPath });
1786
- }
1787
- }
1788
-
1789
- if (action === "settings") {
1790
- return res.json({
1791
- memory_cards_dir: project.memory_cards_dir || "",
1792
- shared_memory_path: project.shared_memory_path || "",
1793
- butler_scripts_dir: project.butler_scripts_dir || "",
1794
- });
1795
- }
1796
-
1797
- res.status(400).json({ error: "Unknown action" });
1798
- });
1799
-
1800
- router.post("/api/memory", (req, res) => {
1801
- const projectId = req.query.project || "";
1802
- const action = req.query.action || "";
1803
- const project = getProject(projectId);
1804
- if (!project) return res.status(404).json({ error: "Project not found" });
1805
-
1806
- const paths = getMemoryPaths(project);
1807
-
1808
- if (action === "butler") {
1809
- const allowed = ["butler-scan.sh", "butler-consolidate.sh", "inject.sh"];
1810
- const command = req.body.command;
1811
- if (!allowed.includes(command)) return res.json({ ok: false, error: `Unknown command: ${command}` });
1812
- const scriptPath = path.join(paths.butlerDir, command);
1813
- if (!fs.existsSync(scriptPath)) return res.json({ ok: false, error: `Script not found: ${scriptPath}` });
1814
- try {
1815
- const output = execFileSync("bash", [scriptPath], {
1816
- encoding: "utf-8",
1817
- timeout: 30000,
1818
- cwd: path.dirname(paths.butlerDir),
1819
- });
1820
- return res.json({ ok: true, output });
1821
- } catch (err) {
1822
- return res.json({ ok: false, error: err.message });
1823
- }
1824
- }
1825
-
1826
- if (action === "save-memory") {
1827
- try {
1828
- const dir = path.dirname(paths.sharedMemoryPath);
1829
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1830
- fs.writeFileSync(paths.sharedMemoryPath, req.body.content);
1831
- return res.json({ ok: true });
1832
- } catch (err) {
1833
- return res.json({ ok: false, error: err.message });
1834
- }
1835
- }
1836
-
1837
- if (action === "save-settings") {
1838
- try {
1839
- const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
1840
- const proj = cfg.projects?.find((p) => p.id === projectId);
1841
- if (!proj) return res.json({ ok: false, error: "Project not found" });
1842
- const s = req.body;
1843
- if (s.memory_cards_dir !== undefined) proj.memory_cards_dir = s.memory_cards_dir || undefined;
1844
- if (s.shared_memory_path !== undefined) proj.shared_memory_path = s.shared_memory_path || undefined;
1845
- if (s.butler_scripts_dir !== undefined) proj.butler_scripts_dir = s.butler_scripts_dir || undefined;
1846
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
1847
- return res.json({ ok: true });
1848
- } catch (err) {
1849
- return res.json({ ok: false, error: err.message });
1850
- }
1851
- }
1852
-
1853
- res.status(400).json({ error: "Unknown action" });
1854
- });
1740
+ // #445: Memory section (agent-memory butler integration) removed.
1855
1741
 
1856
1742
  // ─── Setup ─────────────────────────────────────────────────────────────────
1857
1743
 
@@ -2378,9 +2264,12 @@ router.post("/api/rename", (req, res) => {
2378
2264
  // ─── Telegram ──────────────────────────────────────────────────────────────
2379
2265
 
2380
2266
  const BRIDGE_DIR = path.join(CONFIG_DIR, "agentchattr-telegram");
2267
+ // #444: pin agentchattr-telegram to a known commit (same pattern as
2268
+ // AGENTCHATTR_PIN in bin/quadwork.js for bcurts/agentchattr).
2269
+ const AGENTCHATTR_TELEGRAM_PIN = "4a6b45f1794c612328b9d5ee6d6fcb3f77015abc";
2381
2270
 
2382
2271
  function telegramPidFile(projectId) {
2383
- return path.join(CONFIG_DIR, `telegram-bridge-${projectId}.pid`);
2272
+ return path.join(CONFIG_DIR, `tg-bridge-${projectId}.pid`);
2384
2273
  }
2385
2274
 
2386
2275
  function telegramConfigToml(projectId) {
@@ -2388,7 +2277,7 @@ function telegramConfigToml(projectId) {
2388
2277
  }
2389
2278
 
2390
2279
  // #383: path to a project's AgentChattr config.toml. The install
2391
- // handler patches this file to declare the `telegram-bridge` agent
2280
+ // handler patches this file to declare the `tg` agent
2392
2281
  // so AC's registry accepts the bridge's register call.
2393
2282
  function projectAgentchattrConfigPath(projectId) {
2394
2283
  return path.join(CONFIG_DIR, projectId, "agentchattr", "config.toml");
@@ -2415,7 +2304,12 @@ function resolveProjectAgentchattrUrl(cfg, project) {
2415
2304
  // AC message IDs advances the cursor past the other project's range,
2416
2305
  // silently killing AC→TG forwarding for that project.
2417
2306
  function buildTelegramBridgeToml(tg, projectId) {
2418
- const cursorFile = path.join(CONFIG_DIR, `telegram-bridge-cursor-${projectId}.json`);
2307
+ const cursorFile = path.join(CONFIG_DIR, `tg-bridge-cursor-${projectId}.json`);
2308
+ // #439: migrate old cursor file so the bridge doesn't replay history
2309
+ const oldCursor = path.join(CONFIG_DIR, `telegram-bridge-cursor-${projectId}.json`);
2310
+ if (!fs.existsSync(cursorFile) && fs.existsSync(oldCursor)) {
2311
+ fs.renameSync(oldCursor, cursorFile);
2312
+ }
2419
2313
  return (
2420
2314
  `[telegram]\n` +
2421
2315
  `bot_token = "${tg.bot_token}"\n` +
@@ -2427,14 +2321,18 @@ function buildTelegramBridgeToml(tg, projectId) {
2427
2321
 
2428
2322
  // #383 Bug 3: AC's registry rejects any base name not pre-declared
2429
2323
  // in config.toml with `400 unknown base`. The bridge registers as
2430
- // `telegram-bridge`, so every per-project AC config must declare it.
2431
- // Idempotent: only appends if the section is not already present.
2324
+ // `tg` (#439: renamed from `telegram-bridge`), so every per-project
2325
+ // AC config must declare it. Idempotent: only appends if the section
2326
+ // is not already present. Also migrates old `[agents.telegram-bridge]`.
2432
2327
  function patchAgentchattrConfigForTelegramBridge(tomlText) {
2433
- if (/^\[agents\.telegram-bridge\]\s*$/m.test(tomlText)) {
2434
- return { text: tomlText, changed: false };
2328
+ // #439: migrate old slug if present
2329
+ const original = tomlText;
2330
+ tomlText = tomlText.replace(/^\[agents\.telegram-bridge\]\s*$/m, "[agents.tg]");
2331
+ if (/^\[agents\.tg\]\s*$/m.test(tomlText)) {
2332
+ return { text: tomlText, changed: tomlText !== original };
2435
2333
  }
2436
2334
  const sep = tomlText.length === 0 || tomlText.endsWith("\n") ? "" : "\n";
2437
- const block = `\n[agents.telegram-bridge]\nlabel = "Telegram Bridge"\n`;
2335
+ const block = `\n[agents.tg]\nlabel = "Telegram Bridge"\n`;
2438
2336
  return { text: tomlText + sep + block, changed: true };
2439
2337
  }
2440
2338
 
@@ -2456,7 +2354,7 @@ function buildTelegramBridgeSpawnEnv(parentEnv) {
2456
2354
  // config parse, auth failure) are recoverable instead of
2457
2355
  // /dev/null'd by `stdio: "ignore"`.
2458
2356
  function telegramBridgeLog(projectId) {
2459
- return path.join(CONFIG_DIR, `telegram-bridge-${projectId}.log`);
2357
+ return path.join(CONFIG_DIR, `tg-bridge-${projectId}.log`);
2460
2358
  }
2461
2359
 
2462
2360
  // Tail the last N lines of a file without reading the whole thing
@@ -2676,6 +2574,12 @@ router.post("/api/telegram", async (req, res) => {
2676
2574
  try {
2677
2575
  if (!fs.existsSync(BRIDGE_DIR)) {
2678
2576
  execFileSync("gh", ["repo", "clone", "realproject7/agentchattr-telegram", BRIDGE_DIR], { encoding: "utf-8", timeout: 30000 });
2577
+ // #444: pin to a known commit after clone
2578
+ try {
2579
+ execFileSync("git", ["-C", BRIDGE_DIR, "checkout", "-B", "pinned", AGENTCHATTR_TELEGRAM_PIN], { encoding: "utf-8", timeout: 30000 });
2580
+ } catch {
2581
+ console.warn(`[telegram] WARNING: could not check out agentchattr-telegram pin ${AGENTCHATTR_TELEGRAM_PIN}; falling back to default branch.`);
2582
+ }
2679
2583
  }
2680
2584
  // #380: create the dedicated venv if missing. `python3 -m venv`
2681
2585
  // builds a fresh isolated environment that bypasses PEP 668
@@ -2705,16 +2609,14 @@ router.post("/api/telegram", async (req, res) => {
2705
2609
  `pip output tail:\n${pipOutput.split("\n").slice(-10).join("\n")}`,
2706
2610
  });
2707
2611
  }
2708
- // #383 Bug 3: ensure every known project's AC config declares
2709
- // the `telegram-bridge` agent. Without this, AC's registry
2710
- // rejects the bridge's register call with `400 unknown base`
2711
- // and the bridge enters an infinite re-register loop.
2712
- // Idempotent — append-only, skips configs that already have
2713
- // the section. Does NOT restart AC servers; the operator
2714
- // must click SERVER → Restart to load the new agent slug.
2612
+ // #383 Bug 3 / #457: ensure every known project's AC config
2613
+ // declares the `tg` agent and migrates old `telegram-bridge`
2614
+ // slug. Restarts AC for projects whose config changed so the
2615
+ // new slug loads immediately.
2715
2616
  const patched = [];
2716
2617
  try {
2717
2618
  const cfgAll = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
2619
+ const serverPort = cfgAll.port || 8400;
2718
2620
  for (const proj of cfgAll.projects || []) {
2719
2621
  if (!proj || !proj.id) continue;
2720
2622
  const acPath = projectAgentchattrConfigPath(proj.id);
@@ -2725,6 +2627,14 @@ router.post("/api/telegram", async (req, res) => {
2725
2627
  if (changed) {
2726
2628
  fs.writeFileSync(acPath, text);
2727
2629
  patched.push(proj.id);
2630
+ // #457: restart AC so it loads the new agent slug
2631
+ setTimeout(async () => {
2632
+ try {
2633
+ await fetch(`http://127.0.0.1:${serverPort}/api/agentchattr/${encodeURIComponent(proj.id)}/restart`, {
2634
+ method: "POST",
2635
+ });
2636
+ } catch {}
2637
+ }, 1000);
2728
2638
  }
2729
2639
  } catch {}
2730
2640
  }
@@ -2855,7 +2765,7 @@ router.post("/api/telegram", async (req, res) => {
2855
2765
  const acUrl = resolveProjectAgentchattrUrl(cfg, project);
2856
2766
  if (acUrl) {
2857
2767
  const acPort = new URL(acUrl).port || "8300";
2858
- await fetch(`http://127.0.0.1:${acPort}/api/deregister/telegram-bridge`, {
2768
+ await fetch(`http://127.0.0.1:${acPort}/api/deregister/tg`, {
2859
2769
  method: "POST",
2860
2770
  signal: AbortSignal.timeout(3000),
2861
2771
  }).catch(() => {});
@@ -2938,7 +2848,7 @@ const DISCORD_BRIDGE_SRC = path.join(__dirname, "..", "bridges", "discord");
2938
2848
  const DISCORD_BRIDGE_DIR = path.join(CONFIG_DIR, "agentchattr-discord");
2939
2849
 
2940
2850
  function discordPidFile(projectId) {
2941
- return path.join(CONFIG_DIR, `discord-bridge-${projectId}.pid`);
2851
+ return path.join(CONFIG_DIR, `dc-bridge-${projectId}.pid`);
2942
2852
  }
2943
2853
 
2944
2854
  function discordConfigToml(projectId) {
@@ -2946,11 +2856,16 @@ function discordConfigToml(projectId) {
2946
2856
  }
2947
2857
 
2948
2858
  function discordBridgeLog(projectId) {
2949
- return path.join(CONFIG_DIR, `discord-bridge-${projectId}.log`);
2859
+ return path.join(CONFIG_DIR, `dc-bridge-${projectId}.log`);
2950
2860
  }
2951
2861
 
2952
2862
  function buildDiscordBridgeToml(dc, projectId) {
2953
- const cursorFile = path.join(CONFIG_DIR, `discord-bridge-cursor-${projectId}.json`);
2863
+ const cursorFile = path.join(CONFIG_DIR, `dc-bridge-cursor-${projectId}.json`);
2864
+ // #439: migrate old cursor file so the bridge doesn't replay history
2865
+ const oldCursor = path.join(CONFIG_DIR, `discord-bridge-cursor-${projectId}.json`);
2866
+ if (!fs.existsSync(cursorFile) && fs.existsSync(oldCursor)) {
2867
+ fs.renameSync(oldCursor, cursorFile);
2868
+ }
2954
2869
  return (
2955
2870
  `[discord]\n` +
2956
2871
  `bot_token = "${dc.bot_token}"\n` +
@@ -2961,11 +2876,14 @@ function buildDiscordBridgeToml(dc, projectId) {
2961
2876
  }
2962
2877
 
2963
2878
  function patchAgentchattrConfigForDiscordBridge(tomlText) {
2964
- if (/^\[agents\.discord-bridge\]\s*$/m.test(tomlText)) {
2965
- return { text: tomlText, changed: false };
2879
+ // #439: migrate old slug if present
2880
+ const original = tomlText;
2881
+ tomlText = tomlText.replace(/^\[agents\.discord-bridge\]\s*$/m, "[agents.dc]");
2882
+ if (/^\[agents\.dc\]\s*$/m.test(tomlText)) {
2883
+ return { text: tomlText, changed: tomlText !== original };
2966
2884
  }
2967
2885
  const sep = tomlText.length === 0 || tomlText.endsWith("\n") ? "" : "\n";
2968
- const block = `\n[agents.discord-bridge]\nlabel = "Discord Bridge"\n`;
2886
+ const block = `\n[agents.dc]\nlabel = "Discord Bridge"\n`;
2969
2887
  return { text: tomlText + sep + block, changed: true };
2970
2888
  }
2971
2889
 
@@ -3147,10 +3065,12 @@ router.post("/api/discord", async (req, res) => {
3147
3065
  `pip output tail:\n${pipOutput.split("\n").slice(-10).join("\n")}`,
3148
3066
  });
3149
3067
  }
3150
- // Patch all project AC configs with [agents.discord-bridge]
3068
+ // #457: Patch all project AC configs with [agents.dc] and
3069
+ // migrate old `discord-bridge` slug. Restart AC for changed projects.
3151
3070
  const patched = [];
3152
3071
  try {
3153
3072
  const cfgAll = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
3073
+ const serverPort = cfgAll.port || 8400;
3154
3074
  for (const proj of cfgAll.projects || []) {
3155
3075
  if (!proj || !proj.id) continue;
3156
3076
  const acPath = projectAgentchattrConfigPath(proj.id);
@@ -3161,6 +3081,14 @@ router.post("/api/discord", async (req, res) => {
3161
3081
  if (changed) {
3162
3082
  fs.writeFileSync(acPath, text);
3163
3083
  patched.push(proj.id);
3084
+ // #457: restart AC so it loads the new agent slug
3085
+ setTimeout(async () => {
3086
+ try {
3087
+ await fetch(`http://127.0.0.1:${serverPort}/api/agentchattr/${encodeURIComponent(proj.id)}/restart`, {
3088
+ method: "POST",
3089
+ });
3090
+ } catch {}
3091
+ }, 1000);
3164
3092
  }
3165
3093
  } catch {}
3166
3094
  }
@@ -3246,7 +3174,7 @@ router.post("/api/discord", async (req, res) => {
3246
3174
  const acUrl = resolveProjectAgentchattrUrl(cfg, project);
3247
3175
  if (acUrl) {
3248
3176
  const acPort = new URL(acUrl).port || "8300";
3249
- await fetch(`http://127.0.0.1:${acPort}/api/deregister/discord-bridge`, {
3177
+ await fetch(`http://127.0.0.1:${acPort}/api/deregister/dc`, {
3250
3178
  method: "POST",
3251
3179
  signal: AbortSignal.timeout(3000),
3252
3180
  }).catch(() => {});
@@ -3374,7 +3302,7 @@ module.exports.parseActiveBatch = parseActiveBatch;
3374
3302
  // summarizeItems for the batch-progress fixture test.
3375
3303
  module.exports.buildNoPrRow = buildNoPrRow;
3376
3304
  module.exports.summarizeItems = summarizeItems;
3377
- // #353: expose readLastLines for the telegram-bridge test.
3305
+ // #353: expose readLastLines for the tg-bridge test.
3378
3306
  module.exports.readLastLines = readLastLines;
3379
3307
  // #380: expose checkTelegramBridgePythonDeps so the bridge test can
3380
3308
  // exercise the venv-path interpreter argument round trip.
@@ -3390,6 +3318,7 @@ module.exports.checkDiscordBridgePythonDeps = checkDiscordBridgePythonDeps;
3390
3318
  module.exports.buildDiscordBridgeToml = buildDiscordBridgeToml;
3391
3319
  module.exports.patchAgentchattrConfigForDiscordBridge = patchAgentchattrConfigForDiscordBridge;
3392
3320
  module.exports.buildDiscordBridgeSpawnEnv = buildDiscordBridgeSpawnEnv;
3321
+ module.exports.projectAgentchattrConfigPath = projectAgentchattrConfigPath;
3393
3322
  // #236: expose sendViaWebSocket so the chat-ws-send regression test
3394
3323
  // can verify the ack/body/error paths against a fake AC ws server.
3395
3324
  module.exports.sendViaWebSocket = sendViaWebSocket;
@@ -183,7 +183,7 @@ try {
183
183
  assert.match(toml13, /agentchattr_url = "http:\/\/127\.0\.0\.1:8301"/);
184
184
  // #404: cursor_file must be per-project so multiple bridges
185
185
  // don't clobber each other's position.
186
- assert.match(toml13, /cursor_file = ".*telegram-bridge-cursor-testproject\.json"/);
186
+ assert.match(toml13, /cursor_file = ".*tg-bridge-cursor-testproject\.json"/);
187
187
  // Must NOT emit a separate [agentchattr] section — the bridge
188
188
  // would silently ignore it.
189
189
  assert.equal(toml13.includes("\n[agentchattr]\n"), false);
@@ -196,19 +196,19 @@ try {
196
196
  "[agents.head]\nlabel = \"Head\"\n\n[agents.dev]\nlabel = \"Dev\"\n";
197
197
  const first = patchAgentchattrConfigForTelegramBridge(baseConfig);
198
198
  assert.equal(first.changed, true);
199
- assert.match(first.text, /^\[agents\.telegram-bridge\]$/m);
199
+ assert.match(first.text, /^\[agents\.tg\]$/m);
200
200
  assert.match(first.text, /label = "Telegram Bridge"/);
201
201
  // Running a second time is a no-op.
202
202
  const second = patchAgentchattrConfigForTelegramBridge(first.text);
203
203
  assert.equal(second.changed, false);
204
204
  assert.equal(second.text, first.text);
205
- // A config that was hand-patched during diagnosis is recognized
206
- // as already-correct do not clobber the operator's edit.
205
+ // #439: a config with old slug [agents.telegram-bridge] is migrated
206
+ // to [agents.tg] and flagged as changed.
207
207
  const handPatched =
208
208
  baseConfig + "\n[agents.telegram-bridge]\nlabel = \"Telegram Bridge\"\n";
209
209
  const third = patchAgentchattrConfigForTelegramBridge(handPatched);
210
- assert.equal(third.changed, false);
211
- assert.equal(third.text, handPatched);
210
+ assert.equal(third.changed, true);
211
+ assert.match(third.text, /^\[agents\.tg\]$/m);
212
212
 
213
213
  // 15) #383 Bug 4: buildTelegramBridgeSpawnEnv strips the three
214
214
  // env vars the upstream bridge treats as higher-precedence
@@ -38,15 +38,15 @@ color = "#da7756"
38
38
  label = "Builder"
39
39
 
40
40
  # #383: AC's registry rejects bases not declared in config.toml.
41
- # The Telegram bridge registers as `telegram-bridge`, so every
42
- # per-project AC config must declare it. The bridge has no
43
- # command/cwd of its own — it is a long-running external client
44
- # that posts to AC's HTTP API.
45
- [agents.telegram-bridge]
41
+ # The Telegram bridge registers as `tg` (#439: renamed from
42
+ # `telegram-bridge`), so every per-project AC config must declare
43
+ # it. The bridge has no command/cwd of its own — it is a long-running
44
+ # external client that posts to AC's HTTP API.
45
+ [agents.tg]
46
46
  label = "Telegram Bridge"
47
47
 
48
- # #399: Discord bridge registers as `discord-bridge`.
49
- [agents.discord-bridge]
48
+ # #399/#439: Discord bridge registers as `dc` (renamed from `discord-bridge`).
49
+ [agents.dc]
50
50
  label = "Discord Bridge"
51
51
 
52
52
  [routing]
@@ -1 +0,0 @@
1
- self.__SSG_MANIFEST=new Set(["\u002Fproject\u002F[id]","\u002Fproject\u002F[id]\u002Fmemory","\u002Fproject\u002F[id]\u002Fqueue"]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()