@inceptionstack/roundhouse 0.5.14 → 0.5.16
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/CHANGELOG.md +34 -0
- package/package.json +1 -1
- package/src/gateway/commands.ts +8 -0
- package/src/gateway/gateway.ts +8 -2
- package/src/gateway/helpers.ts +22 -3
- package/src/gateway/topic-command.ts +106 -0
- package/src/gateway/whats-new.ts +4 -5
- package/src/transports/telegram/bot-commands.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@inceptionstack/roundhouse` are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.5.14] — 2026-05-10
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **"What's New" notification** — after `/update` + restart, startup message shows changelog highlights from the new version
|
|
9
|
+
- Command dispatch registry — cleaner gateway routing (−13 lines)
|
|
10
|
+
- Status helpers extracted (`formatUptime`, `checkAvailableUpdate`)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- COMMAND_REGISTRY type safety (`CommandContext` not `any`)
|
|
14
|
+
- CHANGELOG.md now included in published npm package
|
|
15
|
+
|
|
16
|
+
## [0.5.13] — 2026-05-10
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **soul.md + user.md persona injection** — agent identity + user context, auto-reloads on file change
|
|
20
|
+
- tools.md now hints agent to check `~/.roundhouse/workspace/later.md`
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- XML injection: escape `</persona>` in user-supplied persona files
|
|
24
|
+
- `mkdirSync` before `writeSettings` (fixes fresh-install crash)
|
|
25
|
+
- mtime check uses `!==` instead of `>` (catches deletions)
|
|
26
|
+
- `/later@BotName` suffix now stripped in group chats
|
|
27
|
+
|
|
28
|
+
## [0.5.12] — 2026-05-10
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **Inline keyboard for /model** — 8 frontier Bedrock models (2-column, 4-row layout)
|
|
32
|
+
- Models: Claude Opus 4.7, Opus 4.6, Sonnet 4.6, Haiku 4.5, DeepSeek R1, Llama 4, Nova Pro, Mistral Large
|
|
33
|
+
|
|
34
|
+
## [0.5.11] — 2026-05-09
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- **/later command** — quick-capture ideas to `~/.roundhouse/workspace/later.md`
|
|
38
|
+
|
|
5
39
|
## [0.5.10] — 2026-05-09
|
|
6
40
|
|
|
7
41
|
### Fixed
|
package/package.json
CHANGED
package/src/gateway/commands.ts
CHANGED
|
@@ -68,6 +68,14 @@ export async function handleUpdate(ctx: CommandContext): Promise<void> {
|
|
|
68
68
|
await thread.post("⚠️ /update requires an allowlist to be configured.");
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
|
+
// Seed .last-version with current version BEFORE update, so new code detects the change on restart
|
|
72
|
+
try {
|
|
73
|
+
const { ROUNDHOUSE_DIR, ROUNDHOUSE_VERSION } = await import("../config");
|
|
74
|
+
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
75
|
+
const { join } = await import("node:path");
|
|
76
|
+
mkdirSync(ROUNDHOUSE_DIR, { recursive: true });
|
|
77
|
+
writeFileSync(join(ROUNDHOUSE_DIR, ".last-version"), ROUNDHOUSE_VERSION + "\n");
|
|
78
|
+
} catch {}
|
|
71
79
|
console.log(`[roundhouse] /update requested by @${authorName} in thread=${thread.id}`);
|
|
72
80
|
const progress = await createProgressMessage(thread, "📦 Checking for updates...");
|
|
73
81
|
try {
|
package/src/gateway/gateway.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { handleStreaming as _handleStream } from "./streaming";
|
|
|
25
25
|
import { handleNew, handleRestart, handleUpdate, handleCompact, handleStatus, handleStop, handleVerbose, handleDoctor, handleCrons, type CommandContext } from "./commands";
|
|
26
26
|
import { handleModel, handleModelAction, MODEL_ACTION_ID } from "./model-command";
|
|
27
27
|
import { handleLater } from "./later-command";
|
|
28
|
+
import { handleTopic, applyTopicOverride } from "./topic-command";
|
|
28
29
|
import { TelegramAdapter } from "../transports";
|
|
29
30
|
import type { TransportAdapter } from "../transports";
|
|
30
31
|
import { hostname } from "node:os";
|
|
@@ -215,7 +216,7 @@ export class Gateway {
|
|
|
215
216
|
|
|
216
217
|
// ── Unified handler ────────────────────────────
|
|
217
218
|
const handle = async (thread: any, message: any) => {
|
|
218
|
-
|
|
219
|
+
let agentThreadId = applyTopicOverride(resolveAgentThreadId(thread, message), thread);
|
|
219
220
|
const userText = message.text ?? "";
|
|
220
221
|
const authorName = message.author?.userName ?? message.author?.userId ?? "?";
|
|
221
222
|
const rawAttachments = message.attachments ?? [];
|
|
@@ -267,13 +268,18 @@ export class Gateway {
|
|
|
267
268
|
return;
|
|
268
269
|
}
|
|
269
270
|
|
|
271
|
+
if (isCommandWithArgs(trimmed, "/topic") || isCommand(trimmed, "/topic")) {
|
|
272
|
+
await handleTopic({ thread, text: trimmed, postWithFallback: (t, txt) => this.postWithFallback(t, txt) });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
270
276
|
// Dispatch to agent turn handler
|
|
271
277
|
await this.handleAgentTurn(thread, agentThreadId, userText, rawAttachments, verboseThreads, threadLocks, abortControllers);
|
|
272
278
|
};
|
|
273
279
|
|
|
274
280
|
// ── Wire Chat SDK events ───────────────────────
|
|
275
281
|
const handleOrAbort = async (thread: any, message: any) => {
|
|
276
|
-
|
|
282
|
+
let agentThreadId = applyTopicOverride(resolveAgentThreadId(thread, message), thread);
|
|
277
283
|
const text = (message.text ?? "").trim();
|
|
278
284
|
// /stop — abort the in-flight agent run immediately
|
|
279
285
|
if (isCommand(text, "/stop")) {
|
package/src/gateway/helpers.ts
CHANGED
|
@@ -51,21 +51,40 @@ function getChatId(thread: any, message: any): string {
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Resolve the agent-facing thread ID from a chat message.
|
|
54
|
-
* Private/DM → "main", group → "group:<chatId>"
|
|
54
|
+
* Private/DM → "main", group → "group:<chatId>", forum topic → "group:<chatId>:topic:<topicId>"
|
|
55
55
|
*/
|
|
56
56
|
export function resolveAgentThreadId(thread: any, message: any): string {
|
|
57
57
|
const chatType = String(message?.chat?.type ?? thread?.chat?.type ?? thread?.type ?? "").toLowerCase();
|
|
58
|
+
|
|
59
|
+
// Extract topic ID if present (forum threads)
|
|
60
|
+
const topicId = message?.message_thread_id ?? thread?.messageThreadId ?? extractTopicFromThreadId(thread?.id);
|
|
61
|
+
|
|
58
62
|
if (["private", "dm", "direct", "im"].includes(chatType)) return "main";
|
|
59
|
-
if (["group", "supergroup", "channel"].includes(chatType))
|
|
63
|
+
if (["group", "supergroup", "channel"].includes(chatType)) {
|
|
64
|
+
const base = `group:${getChatId(thread, message)}`;
|
|
65
|
+
return topicId ? `${base}:topic:${topicId}` : base;
|
|
66
|
+
}
|
|
60
67
|
|
|
61
68
|
const telegramChatId = telegramChatIdFromThreadId(thread?.id);
|
|
62
69
|
if (telegramChatId !== null) {
|
|
63
|
-
|
|
70
|
+
const base = telegramChatId < 0 ? `group:${telegramChatId}` : "main";
|
|
71
|
+
return (topicId && telegramChatId < 0) ? `${base}:topic:${topicId}` : base;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
return String(thread?.id ?? "main");
|
|
67
75
|
}
|
|
68
76
|
|
|
77
|
+
/** Extract message_thread_id from thread ID string like "telegram:chatId:topicId" */
|
|
78
|
+
function extractTopicFromThreadId(threadId: string | undefined): number | undefined {
|
|
79
|
+
if (!threadId) return undefined;
|
|
80
|
+
const parts = threadId.split(":");
|
|
81
|
+
if (parts.length === 3 && parts[0] === "telegram") {
|
|
82
|
+
const parsed = parseInt(parts[2], 10);
|
|
83
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
69
88
|
// ── System Resources ─────────────────────────────────
|
|
70
89
|
|
|
71
90
|
export interface SystemResources {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gateway/topic-command.ts — Named topic sessions in private chats
|
|
3
|
+
*
|
|
4
|
+
* Allows switching between independent conversations via /topic <name>.
|
|
5
|
+
* Each topic has its own agent session (memory, context, thread).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* /topic deploy — switch to "deploy" topic (creates if new)
|
|
9
|
+
* /topic — show current topic + list all
|
|
10
|
+
* /topic main — return to default session
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { ROUNDHOUSE_DIR } from "../config";
|
|
16
|
+
|
|
17
|
+
const TOPICS_FILE = join(ROUNDHOUSE_DIR, "active-topics.json");
|
|
18
|
+
|
|
19
|
+
/** Active topic per chat (chatId → topicName). "main" means default. */
|
|
20
|
+
let activeTopics = new Map<string, string>();
|
|
21
|
+
|
|
22
|
+
// Load persisted topics on module init
|
|
23
|
+
try {
|
|
24
|
+
const data = JSON.parse(readFileSync(TOPICS_FILE, "utf8"));
|
|
25
|
+
activeTopics = new Map(Object.entries(data));
|
|
26
|
+
} catch { /* first run or corrupt — start fresh */ }
|
|
27
|
+
|
|
28
|
+
function persistTopics(): void {
|
|
29
|
+
try {
|
|
30
|
+
mkdirSync(ROUNDHOUSE_DIR, { recursive: true });
|
|
31
|
+
writeFileSync(TOPICS_FILE, JSON.stringify(Object.fromEntries(activeTopics)));
|
|
32
|
+
} catch (e) { console.error("[roundhouse] failed to persist topics:", e); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Get the active topic for a chat. Returns undefined if on default "main". */
|
|
36
|
+
export function getActiveTopic(chatId: string): string | undefined {
|
|
37
|
+
const topic = activeTopics.get(chatId);
|
|
38
|
+
return (topic && topic !== "main") ? topic : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Apply topic override to a resolved agent thread ID. */
|
|
42
|
+
export function applyTopicOverride(agentThreadId: string, thread: { id?: string }): string {
|
|
43
|
+
if (agentThreadId !== "main") return agentThreadId;
|
|
44
|
+
const chatId = thread.id?.split(":")[1] ?? thread.id ?? "";
|
|
45
|
+
const topic = getActiveTopic(String(chatId));
|
|
46
|
+
return topic ? `topic:${topic}` : agentThreadId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Set the active topic for a chat. */
|
|
50
|
+
export function setActiveTopic(chatId: string, topic: string): void {
|
|
51
|
+
if (topic === "main" || topic === "off" || topic === "") {
|
|
52
|
+
activeTopics.delete(chatId);
|
|
53
|
+
} else {
|
|
54
|
+
activeTopics.set(chatId, topic);
|
|
55
|
+
}
|
|
56
|
+
persistTopics();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Get all known topics from memory-state directory. */
|
|
60
|
+
export function listTopics(): string[] {
|
|
61
|
+
const stateDir = join(ROUNDHOUSE_DIR, "memory-state");
|
|
62
|
+
try {
|
|
63
|
+
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
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface TopicCommandContext {
|
|
73
|
+
thread: { id: string };
|
|
74
|
+
text: string;
|
|
75
|
+
postWithFallback: (thread: any, text: string) => Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function handleTopic(ctx: TopicCommandContext): Promise<void> {
|
|
79
|
+
const { thread, text, postWithFallback } = ctx;
|
|
80
|
+
|
|
81
|
+
// Extract chat ID from thread (for private: "telegram:<chatId>")
|
|
82
|
+
const chatId = thread.id?.split(":")[1] ?? thread.id;
|
|
83
|
+
|
|
84
|
+
// Parse the topic name from the command
|
|
85
|
+
const match = text.match(/^\/topic(?:@\S+)?\s+(.+)/i);
|
|
86
|
+
const topicName = match?.[1]?.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, "");
|
|
87
|
+
|
|
88
|
+
if (!topicName) {
|
|
89
|
+
// Show current topic + known topics
|
|
90
|
+
const current = getActiveTopic(chatId) ?? "main (default)";
|
|
91
|
+
const known = listTopics();
|
|
92
|
+
let msg = `📂 Current topic: \`${current}\`\n\n`;
|
|
93
|
+
if (known.length > 0) {
|
|
94
|
+
msg += `Known topics: ${known.map(t => `\`${t}\``).join(", ")}\n\n`;
|
|
95
|
+
}
|
|
96
|
+
msg += `Switch with: \`/topic <name>\`\nReturn to default: \`/topic main\``;
|
|
97
|
+
await postWithFallback(thread, msg);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setActiveTopic(chatId, topicName);
|
|
102
|
+
const display = topicName === "main" || topicName === "off" ? "main (default)" : topicName;
|
|
103
|
+
const isNew = topicName !== "main" && topicName !== "off";
|
|
104
|
+
const emoji = isNew ? "📂" : "🏠";
|
|
105
|
+
await postWithFallback(thread, `${emoji} Switched to topic: \`${display}\`\n\nAgent context is now independent for this topic.`);
|
|
106
|
+
}
|
package/src/gateway/whats-new.ts
CHANGED
|
@@ -48,12 +48,11 @@ export function checkVersionChange(): string | null {
|
|
|
48
48
|
// No change
|
|
49
49
|
if (lastVersion === ROUNDHOUSE_VERSION) return null;
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
if (!lastVersion) return null;
|
|
53
|
-
|
|
54
|
-
// Version changed — this is an update
|
|
51
|
+
// Version changed (or first run after feature was added) — show what's new
|
|
55
52
|
const changelog = getLatestChangelog();
|
|
56
|
-
const header =
|
|
53
|
+
const header = lastVersion
|
|
54
|
+
? `🆕 Updated: v${lastVersion} → v${ROUNDHOUSE_VERSION}`
|
|
55
|
+
: `🆕 What's new in v${ROUNDHOUSE_VERSION}`;
|
|
57
56
|
if (!changelog) return header;
|
|
58
57
|
return `${header}\n\n${changelog}`;
|
|
59
58
|
}
|
|
@@ -15,6 +15,7 @@ export const BOT_COMMANDS: BotCommand[] = [
|
|
|
15
15
|
{ command: "compact", description: "Compact context window" },
|
|
16
16
|
{ command: "model", description: "Show or switch AI model" },
|
|
17
17
|
{ command: "later", description: "Save an idea for later" },
|
|
18
|
+
{ command: "topic", description: "Switch conversation topic" },
|
|
18
19
|
{ command: "verbose", description: "Toggle verbose tool output" },
|
|
19
20
|
{ command: "stop", description: "Stop the current agent run" },
|
|
20
21
|
{ command: "restart", description: "Restart agent process" },
|