@inceptionstack/roundhouse 0.5.16 → 0.5.17

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inceptionstack/roundhouse",
3
- "version": "0.5.16",
3
+ "version": "0.5.17",
4
4
  "type": "module",
5
5
  "description": "Multi-platform chat gateway that routes messages through a configured AI agent",
6
6
  "license": "MIT",
@@ -10,8 +10,9 @@
10
10
  * /topic main — return to default session
11
11
  */
12
12
 
13
- import { readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
13
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, renameSync } from "node:fs";
14
14
  import { join } from "node:path";
15
+ import { randomBytes } from "node:crypto";
15
16
  import { ROUNDHOUSE_DIR } from "../config";
16
17
 
17
18
  const TOPICS_FILE = join(ROUNDHOUSE_DIR, "active-topics.json");
@@ -28,7 +29,9 @@ try {
28
29
  function persistTopics(): void {
29
30
  try {
30
31
  mkdirSync(ROUNDHOUSE_DIR, { recursive: true });
31
- writeFileSync(TOPICS_FILE, JSON.stringify(Object.fromEntries(activeTopics)));
32
+ const tmp = TOPICS_FILE + "." + randomBytes(4).toString("hex");
33
+ writeFileSync(tmp, JSON.stringify(Object.fromEntries(activeTopics)));
34
+ renameSync(tmp, TOPICS_FILE);
32
35
  } catch (e) { console.error("[roundhouse] failed to persist topics:", e); }
33
36
  }
34
37
 
@@ -38,12 +41,12 @@ export function getActiveTopic(chatId: string): string | undefined {
38
41
  return (topic && topic !== "main") ? topic : undefined;
39
42
  }
40
43
 
41
- /** Apply topic override to a resolved agent thread ID. */
44
+ /** Apply topic override to a resolved agent thread ID. Scoped per chat. */
42
45
  export function applyTopicOverride(agentThreadId: string, thread: { id?: string }): string {
43
46
  if (agentThreadId !== "main") return agentThreadId;
44
47
  const chatId = thread.id?.split(":")[1] ?? thread.id ?? "";
45
48
  const topic = getActiveTopic(String(chatId));
46
- return topic ? `topic:${topic}` : agentThreadId;
49
+ return topic ? `topic:${chatId}:${topic}` : agentThreadId;
47
50
  }
48
51
 
49
52
  /** Set the active topic for a chat. */
@@ -56,14 +59,16 @@ export function setActiveTopic(chatId: string, topic: string): void {
56
59
  persistTopics();
57
60
  }
58
61
 
59
- /** Get all known topics from memory-state directory. */
60
- export function listTopics(): string[] {
62
+ /** Get all known topics for a specific chat from memory-state directory. */
63
+ export function listTopics(chatId: string): string[] {
61
64
  const stateDir = join(ROUNDHOUSE_DIR, "memory-state");
65
+ // Files are named topic_c<chatId>_c<topicName>.json (threadIdToDir encoding)
66
+ const prefix = `topic_c${chatId}_c`;
62
67
  try {
63
68
  return readdirSync(stateDir)
64
- .filter(f => f.startsWith("topic_c") && f.endsWith(".json"))
65
- .map(f => f.replace(/^topic_c/, "").replace(/\.json$/, ""))
66
- .map(f => f.replace(/__/g, "_")); // reverse threadIdToDir encoding
69
+ .filter(f => f.startsWith(prefix) && f.endsWith(".json"))
70
+ .map(f => f.slice(prefix.length).replace(/\.json$/, ""))
71
+ .map(f => f.replace(/__/g, "_")); // reverse threadIdToDir underscore encoding
67
72
  } catch {
68
73
  return [];
69
74
  }
@@ -81,6 +86,12 @@ export async function handleTopic(ctx: TopicCommandContext): Promise<void> {
81
86
  // Extract chat ID from thread (for private: "telegram:<chatId>")
82
87
  const chatId = thread.id?.split(":")[1] ?? thread.id;
83
88
 
89
+ // /topic only works in private chats (groups use forum topics instead)
90
+ if (chatId && chatId.startsWith("-")) {
91
+ await postWithFallback(thread, "⚠️ /topic only works in private chats. Use Telegram forum topics for groups.");
92
+ return;
93
+ }
94
+
84
95
  // Parse the topic name from the command
85
96
  const match = text.match(/^\/topic(?:@\S+)?\s+(.+)/i);
86
97
  const topicName = match?.[1]?.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, "");
@@ -88,7 +99,7 @@ export async function handleTopic(ctx: TopicCommandContext): Promise<void> {
88
99
  if (!topicName) {
89
100
  // Show current topic + known topics
90
101
  const current = getActiveTopic(chatId) ?? "main (default)";
91
- const known = listTopics();
102
+ const known = listTopics(chatId);
92
103
  let msg = `📂 Current topic: \`${current}\`\n\n`;
93
104
  if (known.length > 0) {
94
105
  msg += `Known topics: ${known.map(t => `\`${t}\``).join(", ")}\n\n`;