alvin-bot 4.11.0 β 4.12.1
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 +148 -0
- package/README.md +144 -16
- package/dist/handlers/commands.js +52 -1
- package/dist/handlers/message.js +69 -17
- package/dist/handlers/platform-message.js +45 -11
- package/dist/handlers/stuck-timer.js +54 -0
- package/dist/index.js +5 -0
- package/dist/paths.js +5 -0
- package/dist/platforms/slack.js +67 -3
- package/dist/providers/claude-sdk-provider.js +25 -0
- package/dist/services/personality.js +66 -34
- package/dist/services/session-persistence.js +40 -5
- package/dist/services/session.js +48 -0
- package/dist/services/workspaces.js +247 -0
- package/dist/web/server.js +25 -0
- package/package.json +1 -1
- package/skills/social-fetch/SKILL.md +385 -0
- package/skills/webcheck/SKILL.md +150 -0
- package/test/claude-sdk-tool-use-id.test.ts +180 -0
- package/test/memory-stress-restart.test.ts +2 -1
- package/test/multi-session-stress.test.ts +255 -0
- package/test/platform-session-key.test.ts +69 -0
- package/test/session-persistence.test.ts +8 -5
- package/test/slack-progress-ticker.test.ts +123 -0
- package/test/stuck-timer.test.ts +116 -0
- package/test/sync-task-timeout.test.ts +153 -0
- package/test/system-prompt-background-hint.test.ts +17 -0
- package/test/telegram-workspace-command.test.ts +78 -0
- package/test/workspaces.test.ts +196 -0
- package/web/public/index.html +9 -0
- package/web/public/js/app.js +44 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,154 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Alvin Bot are documented here.
|
|
4
4
|
|
|
5
|
+
## [4.12.1] β 2026-04-15
|
|
6
|
+
|
|
7
|
+
### π Patch: Sync sub-agent timeout + workspace command menu
|
|
8
|
+
|
|
9
|
+
Three issues from v4.12.0 production use, fixed:
|
|
10
|
+
|
|
11
|
+
- **Fix (Bug 1)**: `Task`/`Agent` tool calls without `run_in_background: true` were false-aborted after 10 minutes. The Claude Agent SDK runs synchronous sub-agents entirely inside the tool call β the parent stream emits no intermediate chunks during that time, so the flat 10-minute stuck-timer fired on legitimate long-running work. The new task-aware stuck timer detects sync Task/Agent tool calls (tracked by `toolUseId`) and automatically escalates the idle timeout to 120 minutes (configurable via `ALVIN_SYNC_AGENT_IDLE_TIMEOUT_MINUTES`). Once the matching `tool_result` arrives, the timer reverts to the normal 10-minute idle detection for genuine SDK hangs.
|
|
12
|
+
|
|
13
|
+
- **Mitigation (Bug 2)**: The `BACKGROUND_SUBAGENT_HINT` in `src/services/personality.ts` was rewritten with `β οΈ CRITICAL` framing, a concrete decision-tree structure, an aggressive ~30 second threshold (down from "2 minutes"), and an explicit warning about the Telegram session-blocking consequence. The goal is to get Claude to reliably set `run_in_background: true` when sub-agents will take more than a few seconds, so the main Telegram session doesn't stay blocked while the sub-agent works. This is defense-in-depth on top of the Bug 1 fix β the timer prevents false aborts regardless of Claude's compliance; the strengthened hint reduces how often main-session blocking happens in the first place. Compliance is monitored empirically via logs.
|
|
14
|
+
|
|
15
|
+
- **Fix (Bug 3)**: `/workspace` and `/workspaces` were registered as Telegram command handlers in v4.12.0 but not added to the `bot.api.setMyCommands` array, so they didn't appear in Telegram's auto-complete menu (the list that pops up when you type `/`). Added both, plus a new "π§ Workspaces" block in the `/help` text.
|
|
16
|
+
|
|
17
|
+
#### Architecture details
|
|
18
|
+
|
|
19
|
+
**NEW `src/handlers/stuck-timer.ts`**: Pure state machine `createStuckTimer({normalMs, extendedMs, onTimeout})` returning `{reset, enterSync, exitSync, cancel}`. Testable in isolation without grammy/session/provider mocks via `vi.useFakeTimers()`. 8 unit tests cover normal fire, enterSync extends, exitSync returns, multi-pending, unknown-id no-op, cancel, reset-while-extended, idempotent enterSync.
|
|
20
|
+
|
|
21
|
+
**Protocol change in `src/providers/types.ts` + `claude-sdk-provider.ts`**: `StreamChunk` gains a new additive optional field `runInBackground?: boolean`. The provider extracts it from `block.input.run_in_background` **before** the existing 500-char JSON truncation on `toolInput` β this is load-bearing because for long prompts the serialized input can exceed 500 chars, and naive post-truncation parsing would lose the flag and misclassify sync tasks as async. `toolUseId` is now also yielded on `tool_use` chunks (previously only on `tool_result`) so the consumer can correlate tool_use β tool_result for sync tracking. 4 contract-pin tests mock `@anthropic-ai/claude-agent-sdk` with scripted assistant messages to verify the extraction logic.
|
|
22
|
+
|
|
23
|
+
**Critical ordering in `message.ts`**: State mutation of the pending-sync-task set (`stuckTimer.enterSync` / `stuckTimer.exitSync`) happens **before** `stuckTimer.reset()` in the for-await loop, so the timer arms with the post-mutation state. Inline comment added documenting this invariant.
|
|
24
|
+
|
|
25
|
+
#### Known limitation (not fixed in v4.12.1)
|
|
26
|
+
|
|
27
|
+
A Nanosecond-race where the stuck timer fires the same moment a `tool_result` arrives (fundamentally unfixable without `check-before-fire` semantics in `setTimeout`). With the 120-minute extended window the race requires the tool_result to arrive at exactly 120:00:00.000 β practically irrelevant. A proper fix would require rewriting the timer as a state machine with a pre-fire check, deferred to v4.13.0 if it ever matters.
|
|
28
|
+
|
|
29
|
+
#### Testing
|
|
30
|
+
|
|
31
|
+
**350 tests total** (330 baseline from v4.12.0 + 20 new). All green, TSC clean.
|
|
32
|
+
|
|
33
|
+
- 8 `test/stuck-timer.test.ts` β pure state-machine unit tests
|
|
34
|
+
- 4 `test/claude-sdk-tool-use-id.test.ts` β contract pins for `toolUseId` + `runInBackground` on tool_use chunks
|
|
35
|
+
- 3 new assertions in `test/system-prompt-background-hint.test.ts` (CRITICAL framing, Telegram blocking, 30-second threshold)
|
|
36
|
+
- 5 `test/sync-task-timeout.test.ts` β integration tests over realistic timing scales + regression guard for the pre-fix flat-timeout behavior
|
|
37
|
+
|
|
38
|
+
Live verification after release: local bot restart, Telegram `/` auto-complete shows `/workspace` + `/workspaces`, `curl https://api.telegram.org/bot$TOKEN/getMyCommands` returns the new entries.
|
|
39
|
+
|
|
40
|
+
#### Files changed
|
|
41
|
+
|
|
42
|
+
- **NEW**: `src/handlers/stuck-timer.ts`
|
|
43
|
+
- **NEW tests**: `test/stuck-timer.test.ts`, `test/claude-sdk-tool-use-id.test.ts`, `test/sync-task-timeout.test.ts`
|
|
44
|
+
- **Modified**: `src/providers/types.ts` (`StreamChunk.runInBackground`), `src/providers/claude-sdk-provider.ts` (extract `runInBackground` before truncation, yield `toolUseId` on tool_use), `src/handlers/message.ts` (`createStuckTimer` integration + task-aware flow), `src/services/personality.ts` (`BACKGROUND_SUBAGENT_HINT` rewrite), `src/handlers/commands.ts` (setMyCommands + `/help`), `test/system-prompt-background-hint.test.ts` (3 new assertions)
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## [4.12.0] β 2026-04-13
|
|
49
|
+
|
|
50
|
+
### π§ Multi-Session + Slack Interface β parallel contexts, per-channel workspaces
|
|
51
|
+
|
|
52
|
+
A colleague's feature request the same day v4.11.0 shipped: *"Multiple Session und Interface ΓΌber Slack β wie bei OpenClaw. Du hast mehrere parallele Sessions, die den jeweiligen Kontext voneinander nicht kennen aber in sich einen bestimmten Kontext und Zweck haben. Sie hatten dabei Zugriff auf das gesamte Knowledge (Skills + Memory). Und konnten bei Bedarf eigene agents starten."*
|
|
53
|
+
|
|
54
|
+
The ultra-analysis revealed Alvin was already ~80% built for this: the Slack adapter existed (355 LOC with `@slack/bolt@4.6.0`), the platform abstraction was clean, `buildSessionKey()` already supported `per-channel` mode, `session.workingDir` was already per-session, sub-agents were already async and session-isolated (v4.10.0), and memory/skills were already globally shared. **The single blocker: one line in `platform-message.ts` that bypassed `buildSessionKey` with a naive `hashUserId(userId)`, collapsing every non-Telegram channel from the same user into one session.**
|
|
55
|
+
|
|
56
|
+
This release adds a thin workspace layer on top plus Slack polish. **No breaking changes** β if no workspaces are configured, pre-v4.12 behavior is preserved exactly.
|
|
57
|
+
|
|
58
|
+
#### P0 #1 β Session-Key Fix (`src/handlers/platform-message.ts`)
|
|
59
|
+
|
|
60
|
+
`handlePlatformMessage` now routes through `buildSessionKey(msg.platform, msg.chatId, msg.userId)` instead of `hashUserId(msg.userId)`. On Slack with `SESSION_MODE=per-channel`, each channel gets its own session. Cross-channel isolation is automatic.
|
|
61
|
+
|
|
62
|
+
`buildSessionKey` signature widened from `userId: number` to `userId: string | number` so Slack user IDs (`U01ABC...`) pass through unchanged.
|
|
63
|
+
|
|
64
|
+
**6 unit tests** covering per-channel / per-channel-peer / per-user modes, cross-channel isolation, cross-platform isolation, and backwards compat with numeric Telegram user IDs.
|
|
65
|
+
|
|
66
|
+
#### P0 #2 β Workspace Registry (`src/services/workspaces.ts`, NEW)
|
|
67
|
+
|
|
68
|
+
Loads `~/.alvin-bot/workspaces/*.md` markdown files with YAML frontmatter. Each workspace has: `name`, `purpose`, `cwd`, optional `color`/`emoji`, explicit `channels: []` array for ID-based mapping, and a markdown body that becomes the system prompt override.
|
|
69
|
+
|
|
70
|
+
Hot-reload via `fs.watch()` with 500 ms debounce β same pattern as `src/services/skills.ts`. Changes to workspace files are picked up without a bot restart.
|
|
71
|
+
|
|
72
|
+
Public API: `loadWorkspaces`, `reloadWorkspaces`, `listWorkspaces`, `getWorkspace`, `getDefaultWorkspace`, `matchWorkspaceForChannel`, `resolveWorkspaceOrDefault`, `initWorkspaces`, `startWorkspaceWatcher`, `stopWorkspaceWatcher`.
|
|
73
|
+
|
|
74
|
+
**13 unit tests** covering default fallback, single/multi-workspace load, `~` expansion in cwd, channel-ID match, channel-name match, hot-reload, non-`.md` file skipping, malformed frontmatter resilience, missing directory graceful handling.
|
|
75
|
+
|
|
76
|
+
#### P0 #3 β Workspace Resolver Integration (`src/handlers/platform-message.ts`, `src/handlers/message.ts`)
|
|
77
|
+
|
|
78
|
+
Both the platform handler (Slack/Discord/WhatsApp) and the Telegram main handler now resolve the incoming message to a workspace before building the system prompt. If the session's `workspaceName` changed vs. the previous turn, `workingDir` is updated and persisted via `session-persistence` (v4.11.0).
|
|
79
|
+
|
|
80
|
+
`buildSystemPrompt` and `buildSmartSystemPrompt` gained a new optional `workspacePersona` parameter that injects a `## Workspace Persona` section into the system prompt. Empty string = no-op (default workspace).
|
|
81
|
+
|
|
82
|
+
`UserSession` gained a new `workspaceName: string | null` field. Persisted across restarts via the new v2 envelope format in `sessions.json` (backwards compatible with v4.11 flat format β the loader auto-detects).
|
|
83
|
+
|
|
84
|
+
#### P0 #4 β Slack Setup Documentation (`docs/install/slack-setup.md`, `docs/install/slack-manifest.json`)
|
|
85
|
+
|
|
86
|
+
Step-by-step guide: create Slack App from manifest β Socket Mode β App-Level Token β Bot Token β `~/.alvin-bot/.env` β restart β invite bot β create workspace files. Covers troubleshooting for common issues. The `slack-manifest.json` is copy-paste-ready: pre-configured bot user, all required scopes, event subscriptions, Socket Mode enabled. Both files are gitignored (Ali's docs/install/ convention) and ship via GitHub Release assets.
|
|
87
|
+
|
|
88
|
+
#### P1 #1 β Slack Progress Ticker (`src/platforms/slack.ts`)
|
|
89
|
+
|
|
90
|
+
`SlackAdapter.sendText()` now returns the message `ts` so callers can hold on to it. New `SlackAdapter.editMessage(chatId, messageId, newText)` wraps `chat.update`. Fail-silent: if Slack API errors, the ticker degrades gracefully and the full message still arrives at query end.
|
|
91
|
+
|
|
92
|
+
`PlatformAdapter` interface: `sendText` return type widened from `void` to `string | void`, optional `editMessage` method added. Existing adapters (Telegram, WhatsApp, Discord, Signal) that don't implement `editMessage` are unaffected.
|
|
93
|
+
|
|
94
|
+
**3 unit tests** with mocked `@slack/bolt` covering `chat.update` call, `sendText` ts return, and graceful failure handling.
|
|
95
|
+
|
|
96
|
+
#### P1 #2 β Slack Typing Status + Channel Name Resolution (`src/platforms/slack.ts`)
|
|
97
|
+
|
|
98
|
+
`SlackAdapter.setTyping()` now calls `assistant.threads.setStatus` so Slack shows "Alvin is thinkingβ¦" under the message during long queries. Silently no-ops in channels where the assistant scope isn't granted.
|
|
99
|
+
|
|
100
|
+
New `SlackAdapter.getChannelName(channelId)` resolves + caches channel names via `conversations.info`. `platform-message.ts` detects this helper via duck-typing on the adapter and passes the resolved name to `resolveWorkspaceOrDefault` β enabling channel-name matching (`#alev-b` β `workspaces/alev-b.md`) without hardcoding the Slack type in the platform handler.
|
|
101
|
+
|
|
102
|
+
#### P1 #3 β Telegram `/workspace` + `/workspaces` Commands
|
|
103
|
+
|
|
104
|
+
Feature parity for Telegram. `/workspaces` lists all configured workspaces with emojis, purposes, and the active one marked β
. `/workspace <name>` switches the active workspace for the Telegram user; next message uses the new persona and cwd. `/workspace default` resets.
|
|
105
|
+
|
|
106
|
+
New `session.ts` exports: `getTelegramWorkspace(userId)` / `setTelegramWorkspace(userId, name)` + a module-level `telegramWorkspaces` map persisted via a new v2 envelope format in `sessions.json` (backwards compatible with v4.11 flat format).
|
|
107
|
+
|
|
108
|
+
**5 new unit tests** covering getter/setter/null-clear, persistence roundtrip, and v4.11 flat-format backwards compat.
|
|
109
|
+
|
|
110
|
+
#### P1 #4 β Per-Workspace Cost Aggregation (`src/services/session.ts`)
|
|
111
|
+
|
|
112
|
+
New `getCostByWorkspace()` helper aggregates `session.totalCost` by `session.workspaceName` across all active sessions in memory. Returns per-workspace totals for cost, session count, message count, and tool use count. Used by the Web UI workspace cards.
|
|
113
|
+
|
|
114
|
+
Sessions with `workspaceName === null` aggregate under `"default"` in the breakdown.
|
|
115
|
+
|
|
116
|
+
#### P1 #5 β Web UI Workspace Cards (`src/web/server.ts`, `web/public/index.html`, `web/public/js/app.js`)
|
|
117
|
+
|
|
118
|
+
New `GET /api/workspaces` endpoint returns the workspace registry merged with `getCostByWorkspace()`. Dashboard SPA gains a "π§ Workspaces" page in the Data section of the sidebar (between Sessions and Files). Cards show emoji, name, purpose, cwd, channel mappings, session count, message count, and cumulative cost β color-coded via workspace frontmatter `color` field.
|
|
119
|
+
|
|
120
|
+
Default workspace is always included even when no user configs exist, so the UI always shows at least one card.
|
|
121
|
+
|
|
122
|
+
#### Architecture Decisions
|
|
123
|
+
|
|
124
|
+
- **Workspace is channel-scoped, not thread-scoped.** Slack channel = workspace. Threads within a channel are continuations of the same session.
|
|
125
|
+
- **Memory stays global.** All workspaces share `MEMORY.md`, the Hub memory, and the embeddings index.
|
|
126
|
+
- **Provider stays global.** Per-workspace provider override deferred to v4.13.
|
|
127
|
+
- **`@slack/bolt@^4.6.0`** is a regular dep, already in `package.json` from a previous branch.
|
|
128
|
+
- **Backwards compat is absolute.** If no workspaces exist, `resolveWorkspaceOrDefault` returns the default workspace with empty persona + global cwd. v4.11 flat-format `sessions.json` files still load without migration.
|
|
129
|
+
- **v2 envelope format**: `sessions.json` is now `{ version: 2, sessions: {...}, telegramWorkspaces: {...} }`. Loader auto-detects and handles both legacy flat format and new envelope.
|
|
130
|
+
|
|
131
|
+
#### Testing
|
|
132
|
+
|
|
133
|
+
**330 tests total** (292 baseline from v4.11 + 38 new). All green. TSC clean.
|
|
134
|
+
|
|
135
|
+
- 6 platform-session-key unit tests
|
|
136
|
+
- 14 workspaces unit + integration tests
|
|
137
|
+
- 3 slack-progress-ticker tests (mocked @slack/bolt)
|
|
138
|
+
- 5 telegram-workspace-command tests
|
|
139
|
+
- 10 multi-session end-to-end stress tests
|
|
140
|
+
|
|
141
|
+
**Live verified** via `tmp/live-multi-session.mjs` probe against the real `dist/`: 5 parallel workspaces, 5 simulated Slack channels, full persistence roundtrip with v2 envelope, cost aggregation, hot-reload picking up new workspace files, channel-name fallback, telegramWorkspaces map persistence. **All 7 phases passed.**
|
|
142
|
+
|
|
143
|
+
#### Files changed
|
|
144
|
+
|
|
145
|
+
- **NEW code:** `src/services/workspaces.ts`
|
|
146
|
+
- **NEW tests:** `test/platform-session-key.test.ts`, `test/workspaces.test.ts`, `test/slack-progress-ticker.test.ts`, `test/telegram-workspace-command.test.ts`, `test/multi-session-stress.test.ts`
|
|
147
|
+
- **NEW docs (gitignored, in Release assets):** `docs/install/slack-setup.md`, `docs/install/slack-manifest.json`
|
|
148
|
+
- **Modified:** `src/handlers/platform-message.ts`, `src/handlers/message.ts`, `src/handlers/commands.ts`, `src/platforms/slack.ts`, `src/platforms/types.ts`, `src/services/session.ts`, `src/services/session-persistence.ts`, `src/services/personality.ts`, `src/paths.ts`, `src/index.ts`, `src/web/server.ts`, `web/public/index.html`, `web/public/js/app.js`
|
|
149
|
+
- **Plan:** `docs/superpowers/plans/2026-04-13-multi-session-slack.md`
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
5
153
|
## [4.11.0] β 2026-04-13
|
|
6
154
|
|
|
7
155
|
### π§ Memory Persistence + Smart Loading β sessions survive restart, memory is layered
|
package/README.md
CHANGED
|
@@ -62,20 +62,23 @@ Alvin Bot is an open-source, self-hosted AI agent that lives where you chat. Bui
|
|
|
62
62
|
- **Heartbeat Monitor** β Pings providers every 5 minutes, auto-failover after 2 failures, auto-recovery
|
|
63
63
|
- **User-Configurable Fallback Order** β Rearrange provider priority via Telegram (`/fallback`), Web UI, or API
|
|
64
64
|
- **Adjustable Thinking** β From quick answers (`/effort low`) to deep analysis (`/effort max`)
|
|
65
|
-
- **Persistent Memory** β Remembers across sessions via vector-indexed knowledge base
|
|
65
|
+
- **Persistent Memory** β Remembers across sessions via vector-indexed knowledge base; session state (Claude SDK resume tokens, conversation history, language, effort) survives bot restarts (v4.11.0)
|
|
66
|
+
- **Multi-Session Workspaces** β Run multiple parallel, context-isolated sessions on the same bot β one per Slack channel or per Telegram `/workspace` β each with its own working directory, purpose, and persona. Memory, skills, and sub-agents stay globally shared (v4.12.0). [How-to β](#-multi-session-workspaces-v4120)
|
|
67
|
+
- **Background Sub-Agents** β Claude autonomously uses `run_in_background: true` for long audits/research; main session stays responsive, results deliver as separate messages (v4.10.0)
|
|
66
68
|
- **Smart Tool Discovery** β Scans your system at startup, knows exactly what CLI tools, plugins, and APIs are available
|
|
67
|
-
- **Skill System** β
|
|
69
|
+
- **Skill System** β 12 built-in SKILL.md files (code, data analysis, email, docs, research, sysadmin, browse, etc.) auto-activate based on message context
|
|
68
70
|
- **Self-Awareness** β Knows it IS the AI model β won't call external APIs for tasks it can do itself
|
|
69
|
-
- **Automatic Language Detection** β Detects user language (EN/DE) and adapts; learns preference over time
|
|
71
|
+
- **Automatic Language Detection** β Detects user language (EN/DE/ES/FR) and adapts; learns preference over time
|
|
70
72
|
|
|
71
73
|
### π¬ Multi-Platform
|
|
72
74
|
- **Telegram** β Full-featured with streaming, inline keyboards, voice, photos, documents
|
|
75
|
+
- **Slack** β Socket Mode bot via `@slack/bolt`, DMs + @mentions, file attachments, reactions, `assistant.threads.setStatus` typing indicator. **One channel = one isolated workspace.** See [Multi-Session Workspaces](#-multi-session-workspaces-v4120) below.
|
|
73
76
|
- **WhatsApp** β Via WhatsApp Web: self-chat as AI notepad, group whitelist with per-contact access control, full media support (photos, docs, audio, video)
|
|
74
77
|
- **WhatsApp Group Approval** β Owner gets approval requests via Telegram (or WhatsApp DM fallback) before the bot responds to group messages. Silent β group members see nothing.
|
|
75
78
|
- **Discord** β Server bot with mention/reply detection, slash commands
|
|
76
79
|
- **Signal** β Via signal-cli REST API with voice transcription
|
|
77
80
|
- **Terminal** β Rich TUI with ANSI colors and streaming (`alvin-bot tui`)
|
|
78
|
-
- **Web UI** β Full dashboard with chat, settings, file manager, terminal
|
|
81
|
+
- **Web UI** β Full dashboard with chat, settings, file manager, terminal, workspace overview
|
|
79
82
|
|
|
80
83
|
### π§ Capabilities
|
|
81
84
|
- **52+ Built-in Tools** β Shell, files, email, screenshots, PDF, media, git, system control
|
|
@@ -244,6 +247,8 @@ If your AI provider isn't working, run `doctor` β it tests the actual API conn
|
|
|
244
247
|
| `/remember <text>` | Save to memory |
|
|
245
248
|
| `/export` | Export conversation |
|
|
246
249
|
| `/dir <path>` | Change working directory |
|
|
250
|
+
| `/workspaces` | List all configured workspaces (v4.12.0) |
|
|
251
|
+
| `/workspace [name]` | Show or switch the active workspace β `/workspace default` resets (v4.12.0) |
|
|
247
252
|
| `/status` | Current session & cost info |
|
|
248
253
|
| `/setup` | Configure API keys & platforms |
|
|
249
254
|
| `/system <prompt>` | Set custom system prompt |
|
|
@@ -258,15 +263,19 @@ If your AI provider isn't working, run `doctor` β it tests the actual API conn
|
|
|
258
263
|
## ποΈ Architecture
|
|
259
264
|
|
|
260
265
|
```
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
ββββββββββββ ββββββββββββ
|
|
266
|
-
β Telegram β β
|
|
267
|
-
ββββββ¬ββββββ ββββββ¬ββββββ
|
|
268
|
-
β
|
|
269
|
-
|
|
266
|
+
ββββββββββββββββ
|
|
267
|
+
β Web UI β (Dashboard, Workspaces, Chat, Settings)
|
|
268
|
+
ββββββββ¬ββββββββ
|
|
269
|
+
β HTTP/WS
|
|
270
|
+
ββββββββββββ βββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
|
|
271
|
+
β Telegram β β Slack β β WhatsApp β β Discord β β Signal β
|
|
272
|
+
ββββββ¬ββββββ βββββ¬ββββ ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬ββββββ
|
|
273
|
+
β β β β β
|
|
274
|
+
ββββββββββββββ΄ββββββββββββ΄ββββββββββββββ΄βββββββββββββββ
|
|
275
|
+
β
|
|
276
|
+
βββββββββββ΄βββββββββββ
|
|
277
|
+
β Workspace Resolver β (per-channel context: cwd + persona)
|
|
278
|
+
βββββββββββ¬βββββββββββ
|
|
270
279
|
β
|
|
271
280
|
ββββββββ΄ββββββββ
|
|
272
281
|
β Engine β (Query routing, fallback)
|
|
@@ -302,14 +311,15 @@ alvin-bot/
|
|
|
302
311
|
β βββ config.ts # Configuration
|
|
303
312
|
β βββ handlers/ # Message & command handlers
|
|
304
313
|
β βββ middleware/ # Auth & access control
|
|
305
|
-
β βββ platforms/ # Telegram, WhatsApp, Discord, Signal adapters
|
|
314
|
+
β βββ platforms/ # Telegram, Slack, WhatsApp, Discord, Signal adapters
|
|
306
315
|
β βββ providers/ # AI provider implementations
|
|
307
|
-
β βββ services/ # Memory, voice, cron, plugins, tool discovery
|
|
316
|
+
β βββ services/ # Memory, voice, cron, plugins, workspaces, tool discovery
|
|
308
317
|
β βββ tui/ # Terminal UI
|
|
309
318
|
β βββ web/ # Web server, APIs, setup wizard
|
|
310
319
|
βββ web/public/ # Web UI (HTML/CSS/JS, zero build step)
|
|
311
320
|
βββ plugins/ # Plugin directory (6 built-in)
|
|
312
321
|
βββ docs/
|
|
322
|
+
β βββ install/ # Setup guides (macOS, Windows, Slack)
|
|
313
323
|
β βββ custom-models.json # Custom model configurations
|
|
314
324
|
βββ TOOLS.md # Custom tool definitions (Markdown)
|
|
315
325
|
βββ SOUL.md # Agent personality
|
|
@@ -319,6 +329,89 @@ alvin-bot/
|
|
|
319
329
|
|
|
320
330
|
---
|
|
321
331
|
|
|
332
|
+
## π§ Multi-Session Workspaces (v4.12.0)
|
|
333
|
+
|
|
334
|
+
**Run multiple parallel Alvin sessions on the same bot β one per project, context-isolated, memory shared.** Think Claude Coworker, but on your own machine with your own tools. Each workspace has its own working directory, purpose, and optional persona. Sub-agents spawned in one workspace stay in that workspace. Memory, skills, and the knowledge base are globally shared across all of them.
|
|
335
|
+
|
|
336
|
+
### Why you'd want this
|
|
337
|
+
|
|
338
|
+
Without workspaces, Alvin has one big blob of context. If you ask about Alev-B deployment right after debugging a trading bot, Claude pollutes one context with the other. Workspaces solve this: **Slack channel = session**, or on Telegram, **`/workspace alev-b` = session**. Each one has its own Claude SDK `resume` token, history, and current project CLAUDE.md loaded via its working directory.
|
|
339
|
+
|
|
340
|
+
### How it works
|
|
341
|
+
|
|
342
|
+
1. **Drop a markdown file** into `~/.alvin-bot/workspaces/<name>.md` with YAML frontmatter.
|
|
343
|
+
2. **Alvin hot-reloads** the workspace registry (no restart needed β same pattern as skills).
|
|
344
|
+
3. On **Slack**, workspaces resolve by explicit channel ID first, then by channel name match (`#alev-b` β `workspaces/alev-b.md`, case-insensitive).
|
|
345
|
+
4. On **Telegram**, run `/workspace <name>` to switch β next message uses the new persona and cwd.
|
|
346
|
+
5. Nothing configured? Alvin falls back to the "default" workspace exactly like pre-v4.12 β **no breaking changes**.
|
|
347
|
+
|
|
348
|
+
### Example workspace file
|
|
349
|
+
|
|
350
|
+
Create `~/.alvin-bot/workspaces/alev-b.md`:
|
|
351
|
+
|
|
352
|
+
```markdown
|
|
353
|
+
---
|
|
354
|
+
purpose: Alev-B consulting website dev
|
|
355
|
+
cwd: ~/Projects/alev-b-website
|
|
356
|
+
emoji: "π’"
|
|
357
|
+
color: "#6366f1"
|
|
358
|
+
channels: ["C01ABCDEF"]
|
|
359
|
+
---
|
|
360
|
+
You are focused on the Alev-B consulting website. Stack: React + Express +
|
|
361
|
+
Drizzle + MySQL. Production VPS 72.62.34.230, deploy via rsync. Prefer
|
|
362
|
+
concise, directly actionable answers about features, deployment, and
|
|
363
|
+
Stripe integration.
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
The `cwd` auto-loads the project-specific `CLAUDE.md` via Claude SDK's `settingSources: ["user", "project"]`, so each workspace inherits its project's conventions automatically. `channels` is optional β omit it to match by filename.
|
|
367
|
+
|
|
368
|
+
### Slack setup (5 minutes)
|
|
369
|
+
|
|
370
|
+
1. Download the setup guide + manifest from the [latest release](https://github.com/alvbln/Alvin-Bot/releases/latest):
|
|
371
|
+
- `slack-setup.md` β step-by-step instructions with screenshots
|
|
372
|
+
- `slack-manifest.json` β copy-paste ready Slack App manifest
|
|
373
|
+
2. Create a Slack App from the manifest at https://api.slack.com/apps β **Create New App** β **From an app manifest**
|
|
374
|
+
3. Enable Socket Mode, generate an **App-Level Token** (starts with `xapp-`)
|
|
375
|
+
4. Install the app to your workspace, copy the **Bot User OAuth Token** (starts with `xoxb-`)
|
|
376
|
+
5. Add both to `~/.alvin-bot/.env`:
|
|
377
|
+
```bash
|
|
378
|
+
SLACK_APP_TOKEN=xapp-1-...
|
|
379
|
+
SLACK_BOT_TOKEN=xoxb-...
|
|
380
|
+
SLACK_ALLOWED_USERS=U01ABCDEF # optional, comma-separated
|
|
381
|
+
```
|
|
382
|
+
6. Restart Alvin. You should see `π¬ Slack connected (Alvin @ YourWorkspace)` in the log.
|
|
383
|
+
7. Invite Alvin to channels with `/invite @Alvin`. DMs work without an invite.
|
|
384
|
+
|
|
385
|
+
### Telegram `/workspace` commands
|
|
386
|
+
|
|
387
|
+
| Command | Effect |
|
|
388
|
+
|---|---|
|
|
389
|
+
| `/workspaces` | List all configured workspaces with emojis and purposes (active one marked β
) |
|
|
390
|
+
| `/workspace` | Show the currently active workspace |
|
|
391
|
+
| `/workspace <name>` | Switch to `<name>` β next message uses its persona and cwd |
|
|
392
|
+
| `/workspace default` | Reset to the default workspace (global cwd, no persona) |
|
|
393
|
+
|
|
394
|
+
Workspace selection is per Telegram user, persisted across bot restarts via `~/.alvin-bot/state/sessions.json` (v2 envelope format, backwards compatible with v4.11).
|
|
395
|
+
|
|
396
|
+
### Web UI
|
|
397
|
+
|
|
398
|
+
The dashboard has a dedicated **π§ Workspaces** tab (Data section in the sidebar). Each workspace shows as a color-coded card with emoji, purpose, cwd, mapped channels, session count, message count, and cumulative cost. Useful for spotting which project is burning the most tokens.
|
|
399
|
+
|
|
400
|
+
Or query directly:
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
curl -s http://localhost:3100/api/workspaces | jq
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Architecture guarantees
|
|
407
|
+
|
|
408
|
+
- **Memory is global.** Facts Alvin learns in `#alev-b` are visible in `#homes` via the shared `MEMORY.md` and embeddings index. Per-workspace memory layer is on the v4.13 roadmap.
|
|
409
|
+
- **Sub-agents are per-session.** Each workspace can spawn its own `run_in_background` agents β results come back to the same channel automatically (v4.10.0).
|
|
410
|
+
- **Session state survives restart.** Claude SDK `resume` tokens, conversation history, language, effort, and `workspaceName` all persist via `session-persistence.ts` (v4.11.0).
|
|
411
|
+
- **Backwards compatible.** If you don't create any workspace files, everything behaves exactly like v4.11. Upgrade is a no-op.
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
322
415
|
## βοΈ Configuration
|
|
323
416
|
|
|
324
417
|
### Environment Variables
|
|
@@ -345,13 +438,21 @@ WHATSAPP_ENABLED=true # Enable WhatsApp (needs Chrome)
|
|
|
345
438
|
DISCORD_TOKEN=<token> # Enable Discord
|
|
346
439
|
SIGNAL_API_URL=<url> # Signal REST API URL
|
|
347
440
|
SIGNAL_NUMBER=<number> # Signal phone number
|
|
441
|
+
SLACK_BOT_TOKEN=xoxb-... # Slack Bot User OAuth Token (Socket Mode)
|
|
442
|
+
SLACK_APP_TOKEN=xapp-1-... # Slack App-Level Token (connections:write scope)
|
|
443
|
+
SLACK_ALLOWED_USERS=U01... # Optional: comma-separated Slack user IDs allowlist
|
|
444
|
+
|
|
445
|
+
# Multi-Session (v4.12.0)
|
|
446
|
+
SESSION_MODE=per-channel # per-user (default) | per-channel | per-channel-peer
|
|
447
|
+
# per-channel gives each Slack channel / group its own isolated session
|
|
348
448
|
|
|
349
449
|
# Optional
|
|
350
|
-
WORKING_DIR=~ # Default working directory
|
|
450
|
+
WORKING_DIR=~ # Default working directory (used when no workspace is resolved)
|
|
351
451
|
MAX_BUDGET_USD=5.0 # Cost limit per session
|
|
352
452
|
WEB_PORT=3100 # Web UI port
|
|
353
453
|
WEB_PASSWORD=<password> # Web UI auth (optional)
|
|
354
454
|
CHROME_PATH=/path/to/chrome # Custom Chrome path (for WhatsApp)
|
|
455
|
+
MEMORY_EXTRACTION_DISABLED=1 # Opt out of v4.11.0 auto-fact-extraction in compaction
|
|
355
456
|
```
|
|
356
457
|
|
|
357
458
|
### Custom Models
|
|
@@ -531,6 +632,33 @@ alvin-bot version # Show version
|
|
|
531
632
|
- [ ] One-line install script
|
|
532
633
|
- [x] Docker Compose polish (production-ready `docker-compose.yml`)
|
|
533
634
|
- [x] **Phase 13** β npm publish (security audit)
|
|
635
|
+
- [x] **Phase 14** β Async Sub-Agents (v4.10.0)
|
|
636
|
+
- [x] `run_in_background: true` system prompt hint for Claude SDK
|
|
637
|
+
- [x] Async-agent watcher polling `outputFile` JSONL, delivering results as separate messages
|
|
638
|
+
- [x] Session-bound sub-agents (each session spawns its own background workers)
|
|
639
|
+
- [x] **Phase 15** β Memory Persistence + Smart Loading (v4.11.0)
|
|
640
|
+
- [x] Session persistence across bot restarts (debounced atomic flush, v2 envelope)
|
|
641
|
+
- [x] SDK memory injection (MEMORY.md in every system prompt, not just tool-call dependent)
|
|
642
|
+
- [x] Semantic recall on SDK first-turn via embeddings
|
|
643
|
+
- [x] Layered memory stack (L0 identity / L1 preferences / L2 projects / L3 vector search)
|
|
644
|
+
- [x] Auto-fact extraction during compaction (Mem0-style)
|
|
645
|
+
- [x] **Phase 16** β Multi-Session + Slack Interface (v4.12.0)
|
|
646
|
+
- [x] Session-key fix: platform-message.ts routes through `buildSessionKey()`
|
|
647
|
+
- [x] Workspace registry with hot-reload (`~/.alvin-bot/workspaces/*.md`)
|
|
648
|
+
- [x] Workspace resolver in platform handlers (per-channel persona + cwd)
|
|
649
|
+
- [x] Slack adapter polish: progress ticker (`chat.update`), typing status (`assistant.threads.setStatus`), channel name cache
|
|
650
|
+
- [x] Telegram `/workspace` + `/workspaces` commands (feature parity)
|
|
651
|
+
- [x] Per-workspace cost aggregation + Web UI workspace cards
|
|
652
|
+
- [x] Slack setup guide + copy-paste app manifest (in GitHub Release assets)
|
|
653
|
+
- [ ] **Phase 17** β Memory + Workspace polish (v4.13.0+)
|
|
654
|
+
- [ ] SQLite migration of the embeddings index (currently 128 MB JSON)
|
|
655
|
+
- [ ] Per-workspace memory layer (additive over global) β facts learned in `#alev-b` stay in `alev-b` unless explicitly promoted to global
|
|
656
|
+
- [ ] Per-workspace provider override (`provider:` in frontmatter) β e.g. Alev-B uses Claude Opus, JobSnack uses cheap Gemini
|
|
657
|
+
- [ ] Per-workspace skill allowlist β scope Apple Notes to personal workspace, sysadmin only to devops workspace, etc.
|
|
658
|
+
- [ ] Multi-User Slack (real `per-channel-peer` mode) β different users in the same Slack channel get their own sub-sessions
|
|
659
|
+
- [ ] Workspace cloning / templates β `/workspace clone alev-b as homes-dev` spins up a new workspace from an existing one
|
|
660
|
+
- [ ] Slack slash commands (`/alvin workspace`, `/alvin status`, `/alvin new`) β native Slack command integration via Bolt
|
|
661
|
+
- [ ] Daily log decay / archive β older daily logs move to cold storage after N days
|
|
534
662
|
|
|
535
663
|
---
|
|
536
664
|
|
|
@@ -2,7 +2,8 @@ import { InlineKeyboard, InputFile } from "grammy";
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path, { resolve } from "path";
|
|
4
4
|
import os from "os";
|
|
5
|
-
import { getSession, resetSession, markSessionDirty } from "../services/session.js";
|
|
5
|
+
import { getSession, resetSession, markSessionDirty, getTelegramWorkspace, setTelegramWorkspace } from "../services/session.js";
|
|
6
|
+
import { listWorkspaces, getWorkspace } from "../services/workspaces.js";
|
|
6
7
|
import { getRegistry } from "../engine.js";
|
|
7
8
|
import { reloadSoul } from "../services/personality.js";
|
|
8
9
|
import { parseDuration, createReminder, listReminders, cancelReminder } from "../services/reminders.js";
|
|
@@ -109,6 +110,10 @@ export function registerCommands(bot) {
|
|
|
109
110
|
`/effort β Set reasoning depth\n` +
|
|
110
111
|
`/voice β Voice replies on/off\n` +
|
|
111
112
|
`/dir <path> β Working directory\n\n` +
|
|
113
|
+
`π§ *Workspaces*\n` +
|
|
114
|
+
`/workspaces β List all workspaces\n` +
|
|
115
|
+
`/workspace <name> β Switch active workspace\n` +
|
|
116
|
+
`/workspace default β Reset to default\n\n` +
|
|
112
117
|
`π¨ *Extras*\n` +
|
|
113
118
|
`/imagine <prompt> β Generate image\n` +
|
|
114
119
|
`/remind <time> <text> β Set reminder\n` +
|
|
@@ -148,6 +153,8 @@ export function registerCommands(bot) {
|
|
|
148
153
|
{ command: "version", description: "Show Alvin Bot version" },
|
|
149
154
|
{ command: "new", description: "Start new session" },
|
|
150
155
|
{ command: "dir", description: "Change working directory" },
|
|
156
|
+
{ command: "workspaces", description: "List all workspaces" },
|
|
157
|
+
{ command: "workspace", description: "Switch active workspace" },
|
|
151
158
|
{ command: "web", description: "Quick web search" },
|
|
152
159
|
{ command: "imagine", description: "Generate image (e.g. /imagine A fox)" },
|
|
153
160
|
{ command: "remind", description: "Set reminder (e.g. /remind 30m Text)" },
|
|
@@ -425,6 +432,50 @@ export function registerCommands(bot) {
|
|
|
425
432
|
markSessionDirty(userId);
|
|
426
433
|
await ctx.reply(`β
Effort: ${EFFORT_LABELS[session.effort]}`);
|
|
427
434
|
});
|
|
435
|
+
// v4.12.0 P1 #3 β Multi-workspace support on Telegram
|
|
436
|
+
bot.command("workspaces", async (ctx) => {
|
|
437
|
+
const userId = ctx.from.id;
|
|
438
|
+
const active = getTelegramWorkspace(userId) ?? "default";
|
|
439
|
+
const all = listWorkspaces();
|
|
440
|
+
if (all.length === 0) {
|
|
441
|
+
await ctx.reply("π§ No workspaces configured.\n\n" +
|
|
442
|
+
"Create one by adding a file at `~/.alvin-bot/workspaces/<name>.md` " +
|
|
443
|
+
"with YAML frontmatter. See docs/install/slack-setup.md for the format.", { parse_mode: "Markdown" });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const lines = [`π§ *Workspaces* (active: \`${active}\`)`, ""];
|
|
447
|
+
for (const ws of all) {
|
|
448
|
+
const marker = ws.name === active ? "β
" : (ws.emoji ?? "βͺοΈ");
|
|
449
|
+
const purpose = ws.purpose || "(no purpose)";
|
|
450
|
+
lines.push(`${marker} \`${ws.name}\` β ${purpose}`);
|
|
451
|
+
}
|
|
452
|
+
lines.push("");
|
|
453
|
+
lines.push("Switch with: `/workspace <name>` Β· Reset: `/workspace default`");
|
|
454
|
+
await ctx.reply(lines.join("\n"), { parse_mode: "Markdown" });
|
|
455
|
+
});
|
|
456
|
+
bot.command("workspace", async (ctx) => {
|
|
457
|
+
const userId = ctx.from.id;
|
|
458
|
+
const arg = ctx.match?.trim();
|
|
459
|
+
if (!arg) {
|
|
460
|
+
const active = getTelegramWorkspace(userId) ?? "default";
|
|
461
|
+
const ws = active === "default" ? null : getWorkspace(active);
|
|
462
|
+
const purpose = ws?.purpose || "global default β no persona, global cwd";
|
|
463
|
+
await ctx.reply(`π§ Active workspace: *${active}*\n_${purpose}_\n\nUse \`/workspaces\` to see all available.`, { parse_mode: "Markdown" });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (arg === "default" || arg === "reset") {
|
|
467
|
+
setTelegramWorkspace(userId, null);
|
|
468
|
+
await ctx.reply("β
Switched to the default workspace.");
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const ws = getWorkspace(arg);
|
|
472
|
+
if (!ws) {
|
|
473
|
+
await ctx.reply(`β Workspace \`${arg}\` not found.\nUse \`/workspaces\` to list available ones.`, { parse_mode: "Markdown" });
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
setTelegramWorkspace(userId, arg);
|
|
477
|
+
await ctx.reply(`β
Switched to workspace *${ws.emoji ?? "π§"} ${ws.name}*\n_${ws.purpose || "(no purpose set)"}_\n\nNext message will use this workspace's persona and cwd (\`${ws.cwd}\`).`, { parse_mode: "Markdown" });
|
|
478
|
+
});
|
|
428
479
|
// Inline keyboard callback for effort switching
|
|
429
480
|
bot.callbackQuery(/^effort:(.+)$/, async (ctx) => {
|
|
430
481
|
const level = ctx.match[1];
|
package/dist/handlers/message.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { InputFile } from "grammy";
|
|
2
2
|
import fs from "fs";
|
|
3
|
-
import { getSession, addToHistory, trackProviderUsage, buildSessionKey } from "../services/session.js";
|
|
3
|
+
import { getSession, addToHistory, trackProviderUsage, buildSessionKey, getTelegramWorkspace } from "../services/session.js";
|
|
4
|
+
import { resolveWorkspaceOrDefault, getWorkspace } from "../services/workspaces.js";
|
|
4
5
|
import { TelegramStreamer } from "../services/telegram.js";
|
|
5
6
|
import { getRegistry } from "../engine.js";
|
|
6
7
|
import { textToSpeech } from "../services/voice.js";
|
|
@@ -16,6 +17,7 @@ import { emitUserMessage as broadcastUserMessage, emitResponseStart as broadcast
|
|
|
16
17
|
import { t } from "../i18n.js";
|
|
17
18
|
import { isHarmlessTelegramError } from "../util/telegram-error-filter.js";
|
|
18
19
|
import { handleToolResultChunk } from "./async-agent-chunk-handler.js";
|
|
20
|
+
import { createStuckTimer } from "./stuck-timer.js";
|
|
19
21
|
/**
|
|
20
22
|
* Stuck-only timeout β NO absolute cap.
|
|
21
23
|
*
|
|
@@ -36,6 +38,26 @@ import { handleToolResultChunk } from "./async-agent-chunk-handler.js";
|
|
|
36
38
|
*/
|
|
37
39
|
const STUCK_TIMEOUT_MINUTES = Number(process.env.ALVIN_STUCK_TIMEOUT_MINUTES) || 10;
|
|
38
40
|
const STUCK_TIMEOUT_MS = STUCK_TIMEOUT_MINUTES * 60 * 1000;
|
|
41
|
+
/**
|
|
42
|
+
* v4.12.1 β Task-aware stuck timeout for sync Task/Agent tool calls.
|
|
43
|
+
*
|
|
44
|
+
* When Claude calls the Task/Agent tool WITHOUT run_in_background: true,
|
|
45
|
+
* the Claude Agent SDK runs the sub-agent synchronously inside the tool
|
|
46
|
+
* call. The parent stream emits NO intermediate chunks during that time
|
|
47
|
+
* β it's silent until the sub-agent finishes and the final tool_result
|
|
48
|
+
* arrives. With the normal STUCK_TIMEOUT_MS (10 min), this triggered a
|
|
49
|
+
* false abort on legitimate long-running sub-agents.
|
|
50
|
+
*
|
|
51
|
+
* The new approach: track pending sync Task/Agent tool calls by their
|
|
52
|
+
* toolUseId, and while any are active, escalate the idle timeout to
|
|
53
|
+
* SYNC_AGENT_IDLE_TIMEOUT_MS (default 120 min, env-configurable). After
|
|
54
|
+
* the matching tool_result arrives, revert to the normal timeout.
|
|
55
|
+
*
|
|
56
|
+
* The normal 10-min timeout still applies for genuine SDK hangs (no
|
|
57
|
+
* sync tool call active, no chunks arriving).
|
|
58
|
+
*/
|
|
59
|
+
const SYNC_AGENT_IDLE_TIMEOUT_MINUTES = Number(process.env.ALVIN_SYNC_AGENT_IDLE_TIMEOUT_MINUTES) || 120;
|
|
60
|
+
const SYNC_AGENT_IDLE_TIMEOUT_MS = SYNC_AGENT_IDLE_TIMEOUT_MINUTES * 60 * 1000;
|
|
39
61
|
/** Checkpoint reminder thresholds β kept in sync with
|
|
40
62
|
* src/providers/claude-sdk-provider.ts (where the actual hint injection
|
|
41
63
|
* happens). We mirror the check here so the session telemetry knows
|
|
@@ -164,21 +186,23 @@ export async function handleMessage(ctx) {
|
|
|
164
186
|
const typingInterval = setInterval(() => {
|
|
165
187
|
ctx.api.sendChatAction(ctx.chat.id, "typing").catch(() => { });
|
|
166
188
|
}, 4000);
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
189
|
+
// v4.12.1 β Task-aware stuck timer. Normal mode (STUCK_TIMEOUT_MS)
|
|
190
|
+
// fires after 10 min of silence. When a sync Task/Agent tool call is
|
|
191
|
+
// active (tracked by toolUseId in the for-await loop below), the
|
|
192
|
+
// timeout escalates to SYNC_AGENT_IDLE_TIMEOUT_MS (120 min) so
|
|
193
|
+
// legitimate long-running sub-agents that emit no intermediate chunks
|
|
194
|
+
// don't get falsely aborted. See src/handlers/stuck-timer.ts.
|
|
195
|
+
const stuckTimer = createStuckTimer({
|
|
196
|
+
normalMs: STUCK_TIMEOUT_MS,
|
|
197
|
+
extendedMs: SYNC_AGENT_IDLE_TIMEOUT_MS,
|
|
198
|
+
onTimeout: () => {
|
|
175
199
|
if (session.abortController && !session.abortController.signal.aborted) {
|
|
176
200
|
timedOut = true;
|
|
177
201
|
session.abortController.abort();
|
|
178
202
|
}
|
|
179
|
-
},
|
|
180
|
-
};
|
|
181
|
-
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
stuckTimer.reset();
|
|
182
206
|
try {
|
|
183
207
|
// React with π€ to show we're thinking
|
|
184
208
|
await react(ctx, "π€");
|
|
@@ -226,10 +250,22 @@ export async function handleMessage(ctx) {
|
|
|
226
250
|
// and rehydrated sessions where the persisted snapshot lacked a sessionId.
|
|
227
251
|
// After the first SDK turn, Claude resumes via SDK session_id and already
|
|
228
252
|
// carries the recalled context β no need for another search per turn.
|
|
253
|
+
//
|
|
254
|
+
// v4.12.0 β Resolve the user's active Telegram workspace (if any) and
|
|
255
|
+
// forward the persona to buildSmartSystemPrompt. If the workspace
|
|
256
|
+
// changed since last turn, update session's workingDir + workspaceName.
|
|
257
|
+
const activeWsName = getTelegramWorkspace(userId);
|
|
258
|
+
const workspace = activeWsName
|
|
259
|
+
? (getWorkspace(activeWsName) ?? resolveWorkspaceOrDefault("telegram", String(userId), undefined))
|
|
260
|
+
: resolveWorkspaceOrDefault("telegram", String(userId), undefined);
|
|
261
|
+
if (session.workspaceName !== workspace.name) {
|
|
262
|
+
session.workspaceName = workspace.name;
|
|
263
|
+
session.workingDir = workspace.cwd;
|
|
264
|
+
}
|
|
229
265
|
const chatIdStr = String(ctx.chat.id);
|
|
230
266
|
const skillContext = buildSkillContext(text);
|
|
231
267
|
const isFirstSDKTurn = isSDK && session.sessionId === null;
|
|
232
|
-
const systemPrompt = (await buildSmartSystemPrompt(isSDK, session.language, text, chatIdStr, isFirstSDKTurn)) + skillContext;
|
|
268
|
+
const systemPrompt = (await buildSmartSystemPrompt(isSDK, session.language, text, chatIdStr, isFirstSDKTurn, workspace.systemPromptOverride)) + skillContext;
|
|
233
269
|
// Track the user turn in history regardless of provider type. This keeps
|
|
234
270
|
// the fallback path (Ollama etc.) aware of what was said on SDK turns.
|
|
235
271
|
addToHistory(userId, { role: "user", content: text });
|
|
@@ -291,8 +327,25 @@ export async function handleMessage(ctx) {
|
|
|
291
327
|
// not in the tool_result text). See Fix #17 Stage 2.
|
|
292
328
|
let lastAgentToolUseInput;
|
|
293
329
|
for await (const chunk of registry.queryWithFallback(queryOpts)) {
|
|
294
|
-
//
|
|
295
|
-
|
|
330
|
+
// v4.12.1 β Update pending-sync-task state FIRST so the timer's
|
|
331
|
+
// next reset picks up the new state. This ordering is load-bearing:
|
|
332
|
+
// reversing it means the timer rearms with stale state. A sync
|
|
333
|
+
// Task/Agent tool call switches the stuck timer to extended mode
|
|
334
|
+
// (120 min) to tolerate the silent gap until tool_result arrives.
|
|
335
|
+
if (chunk.type === "tool_use" &&
|
|
336
|
+
(chunk.toolName === "Task" || chunk.toolName === "Agent") &&
|
|
337
|
+
chunk.toolUseId &&
|
|
338
|
+
chunk.runInBackground !== true) {
|
|
339
|
+
stuckTimer.enterSync(chunk.toolUseId);
|
|
340
|
+
}
|
|
341
|
+
else if (chunk.type === "tool_result" && chunk.toolUseId) {
|
|
342
|
+
// Any tool_result may match a pending sync entry. Set.delete is
|
|
343
|
+
// a no-op if the id isn't in the set β safe for async results.
|
|
344
|
+
stuckTimer.exitSync(chunk.toolUseId);
|
|
345
|
+
}
|
|
346
|
+
// Any chunk is progress β reset the stuck timer (now with
|
|
347
|
+
// updated pending-sync state so the correct timeout is armed).
|
|
348
|
+
stuckTimer.reset();
|
|
296
349
|
switch (chunk.type) {
|
|
297
350
|
case "text":
|
|
298
351
|
finalText = chunk.text || "";
|
|
@@ -460,8 +513,7 @@ export async function handleMessage(ctx) {
|
|
|
460
513
|
}
|
|
461
514
|
}
|
|
462
515
|
finally {
|
|
463
|
-
|
|
464
|
-
clearTimeout(stuckTimer);
|
|
516
|
+
stuckTimer.cancel();
|
|
465
517
|
clearInterval(typingInterval);
|
|
466
518
|
session.isProcessing = false;
|
|
467
519
|
session.abortController = null;
|