@hera-al/server 1.6.14 → 1.6.18

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/README.md CHANGED
@@ -2,324 +2,69 @@
2
2
 
3
3
  **The AI that doesn't just chat — it acts, evolves, and thinks ahead.**
4
4
 
5
- Hera is not another chatbot wrapper. It's an autonomous AI platform that combines state-of-the-art language models with unprecedented levels of autonomy, action, and self-evolution. Built on Claude Agent SDK, Hera connects to multiple communication channels while maintaining context, learning from interactions, and proactively managing tasks without constant supervision.
5
+ Hera is an autonomous AI platform built on Claude Agent SDK. Multi-channel gateway (Telegram, WhatsApp, Discord, WebChat), memory that evolves overnight, self-programming skills, distributed execution nodes, and a real-time admin panel.
6
6
 
7
- ---
8
-
9
- ## 🌟 What Makes Hera Different
10
-
11
- ### Beyond Chat: True Autonomy
12
- - **Proactive Actions**: Schedules its own tasks, monitors events, reaches out when needed
13
- - **Self-Programming**: Creates and modifies its own skills, adapts workflows
14
- - **Memory Evolution**: Nightly consolidation transforms daily interactions into long-term knowledge with RDF concept mapping
15
- - **Multi-Modal Intelligence**: Voice (TTS/STT), vision, document processing, web search
16
-
17
- ### Production-Ready Architecture
18
- - **Multi-Channel Gateway**: Telegram, WhatsApp, Discord, WebChat — unified interface, consistent experience
19
- - **Nostromo Control Center**: Advanced admin panel for real-time monitoring, configuration, and orchestration
20
- - **Distributed Execution**: Remote nodes for browser automation, shell execution, cross-platform actions
21
- - **Extensible by Design**: Skills, MCP tools, plugins — choose your level of integration
22
-
23
- ### Built for Developers
24
- - **MIT Licensed Components**: Core libraries available as `@hera-al/*` packages on npm
25
- - **Claude Agent SDK Native**: Full toolkit access, streaming, artifacts, permissions
26
- - **TypeScript Throughout**: Type-safe, maintainable, modern codebase
27
- - **Docker Ready**: One command to production
28
-
29
- ---
30
-
31
- ## 🏗️ Architecture
32
-
33
- ```
34
- ┌─────────────────────────────────────────────────────────────┐
35
- │ COMMUNICATION LAYER │
36
- │ Telegram • WhatsApp • Discord • WebChat • Voice • Browser │
37
- └────────────────────────┬────────────────────────────────────┘
38
-
39
- ┌────────────────────────▼────────────────────────────────────┐
40
- │ HERA GATEWAY │
41
- │ • Session Management • Multi-format rendering │
42
- │ • Permission System • Cron Scheduler │
43
- │ • Memory Pipeline • Nostromo Admin Panel │
44
- └────────────────────────┬────────────────────────────────────┘
45
-
46
- ┌────────────────────────▼────────────────────────────────────┐
47
- │ CLAUDE AGENT SDK │
48
- │ Claude Sonnet 4.5 • Tool orchestration • Streaming │
49
- └────────────────────────┬────────────────────────────────────┘
50
-
51
- ┌───────────────┴───────────────┬─────────────────┐
52
- │ │ │
53
- ┌────────▼────────┐ ┌─────────▼──────┐ ┌───────▼────────┐
54
- │ SKILLS │ │ MCP SERVERS │ │ REMOTE NODES │
55
- │ • Google WS │ │ • Custom │ │ • OSXNode │
56
- │ • xAI Search │ │ • Community │ │ • Browser CDP │
57
- │ • Apple Notes │ │ • External │ │ • Shell Exec │
58
- │ • Dreaming │ │ │ │ │
59
- │ • Custom... │ │ │ │ │
60
- └─────────────────┘ └────────────────┘ └────────────────┘
61
- ```
7
+ Not another chatbot wrapper.
62
8
 
63
9
  ---
64
10
 
65
- ## 🎯 Core Components
66
-
67
- ### Nostromo — The Command Center
68
- Real-time admin panel providing:
69
- - **Live Session Monitoring**: Active conversations, context windows, tool usage
70
- - **Skill Management**: Install, update, configure, and publish skills
71
- - **Cron Orchestration**: Schedule autonomous tasks, view execution history
72
- - **Model Registry**: Switch between Claude, GPT-4, local models on the fly
73
- - **Permission Dashboard**: Approve/deny tool executions, set policies
74
- - **System Health**: Memory usage, API quotas, node connectivity
75
-
76
- ### The Memory System
77
- Three-tier memory architecture:
78
- 1. **Session Memory**: Real-time context during conversations
79
- 2. **Daily Logs**: Markdown files with full conversation history
80
- 3. **Long-Term Memory**: Consolidated knowledge with semantic search and RDF Turtle concept maps
81
-
82
- The `dreaming` skill runs nightly, identifying patterns, extracting insights, and updating the concept graph — Hera literally learns while you sleep.
83
-
84
- ### Skills — Extensibility at Scale
85
- Skills are self-contained capabilities that extend Hera's reach:
86
-
87
- **Bundled Skills:**
88
- - `google-workspace` — Gmail, Drive, Docs, Sheets, Slides (OAuth2 integrated)
89
- - `xai-search` — Web and X (Twitter) search via Grok Responses API
90
- - `apple-notes` & `apple-reminders` — Native macOS integration
91
- - `weather` — Current conditions and forecasts
92
- - `ssh` — Remote server management
93
- - `dreaming` — Nightly memory consolidation and concept mapping
94
- - `the-skill-guardian` — Security scanner for third-party skills (90+ malicious pattern detection)
95
- - `skill-creator` — Meta-skill for creating and packaging new skills
96
-
97
- **Create Your Own:**
98
- ```bash
99
- # Skills live in .claude/skills/<name>/
100
- # SKILL.md defines capabilities
101
- # scripts/ subfolder contains executables
102
- # Full access to Hera's toolkit
103
- ```
104
-
105
- ### MCP Integration
106
- Model Context Protocol (MCP) support allows:
107
- - Connecting to external data sources (databases, APIs, SaaS)
108
- - Using community MCP servers (Notion, Linear, GitHub, etc.)
109
- - Building custom MCP servers for proprietary systems
11
+ ## Highlights
110
12
 
111
- ### Remote Nodes
112
- WebSocket-based execution nodes for distributed actions:
113
- - **StandardNode**: Shell execution on any machine (npm package: `@hera-al/standardnode`)
114
- - **OSXNode**: Native macOS app with SwiftUI interface, markdown rendering, notifications
115
- - **Browser Nodes**: Chrome DevTools Protocol (CDP) automation for web scraping, testing, interaction
13
+ - **True Autonomy** — Schedules tasks, monitors events, creates its own skills
14
+ - **Multi-Channel** Telegram, WhatsApp, Discord, WebChat with unified context
15
+ - **Memory Evolution** Nightly consolidation with semantic search and concept graph
16
+ - **Nostromo** Real-time admin panel for monitoring, configuration, orchestration
17
+ - **Remote Nodes** Shell execution, browser automation, cross-platform actions
18
+ - **Skills System** — Extensible capabilities (Google Workspace, Apple Notes, SSH, weather, and more)
19
+ - **PLASMA** — Event-sourced dynamic UI system for AI-generated interfaces
20
+ - **Pico Agents** — Multi-model subagents (Council pattern) via OpenRouter
21
+ - **Docker Ready** — One command to production
116
22
 
117
23
  ---
118
24
 
119
- ## 🚀 Quick Start
120
-
121
- ### Prerequisites
122
- - Node.js 18+
123
- - Anthropic API key
124
- - (Optional) Telegram bot token, OpenAI API key for voice
125
-
126
- ### Installation
127
-
128
- ```bash
129
- # Clone or install
130
- npm install -g @hera-al/server
131
- ```
132
-
133
- ### Configuration
134
-
135
- Create `.env`:
136
- ```env
137
- # Required
138
- ANTHROPIC_API_KEY=sk-ant-...
139
- DATA_FOLDER=/path/to/data
140
-
141
- # Channels (optional)
142
- TELEGRAM_BOT_TOKEN=123456789:ABC...
143
- WHATSAPP_ENABLED=true
144
-
145
- # Voice (optional)
146
- OPENAI_API_KEY=sk-...
147
- TTS_PROVIDER=openai
148
- TTS_VOICE=echo
149
-
150
- # Nostromo (optional)
151
- NOSTROMO_PORT=3030
152
- NOSTROMO_ENABLED=true
153
- ```
154
-
155
- See `.env.example` for all options.
156
-
157
- ### Launch
25
+ ## Quick Start
158
26
 
159
27
  ```bash
160
- # Start the gateway
161
- npm start
162
-
163
- # Or with Nostromo
164
- npm run server:with-nostromo
165
-
166
- # CLI tools
167
- hera # Interactive management CLI
168
- heraserver # Start server programmatically
169
- hera-install # Installation wizard
28
+ git clone https://github.com/hera-artificial-life/hera-al.git
29
+ cd hera-al
30
+ cp .env.example .env # Add your ANTHROPIC_API_KEY
31
+ ./docker/hera-start.sh
170
32
  ```
171
33
 
172
- Access Nostromo at `http://localhost:3030` (default).
173
-
174
34
  ---
175
35
 
176
- ## 📚 Use Cases
177
-
178
- ### Personal AI Assistant
179
- - Manages emails, calendar, reminders across platforms
180
- - Proactively surfaces relevant information
181
- - Learns preferences and adapts over time
182
-
183
- ### Team Collaboration Hub
184
- - Multi-channel presence (Slack, Discord, Telegram simultaneously)
185
- - Skill-based workflows (CI/CD triggers, code reviews, deployments)
186
- - Shared memory and knowledge base
36
+ ## Documentation
187
37
 
188
- ### Autonomous Automation
189
- - Scheduled tasks via cron (data scraping, report generation, monitoring)
190
- - Self-healing workflows (detect failures, retry with exponential backoff)
191
- - Cross-platform orchestration (Mac → Linux → Cloud)
38
+ Full documentation, architecture details, installation guide, skill development, and contributing guidelines:
192
39
 
193
- ### Research & Development
194
- - Memory consolidation identifies patterns in conversations
195
- - Concept mapping reveals hidden connections
196
- - Export knowledge graphs for analysis
40
+ **📖 [github.com/hera-artificial-life/hera-al](https://github.com/hera-artificial-life/hera-al)**
197
41
 
198
42
  ---
199
43
 
200
- ## 🧩 Ecosystem
44
+ ## Ecosystem
201
45
 
202
- ### Published Packages
46
+ | Package | Description |
47
+ |---------|-------------|
48
+ | `@hera-al/server` | Core gateway |
49
+ | `@hera-al/browser-server` | WebChat UI component |
50
+ | `@hera-al/standardnode` | Remote execution node client |
203
51
 
204
- | Package | Description | npm |
205
- |---------|-------------|-----|
206
- | `@hera-al/server` | Core gateway (coming soon) | — |
207
- | `@hera-al/browser-server` | WebChat UI component | [📦](https://www.npmjs.com/package/@hera-al/browser-server) |
208
- | `@hera-al/standardnode` | Remote execution node client | (coming soon) |
209
-
210
- ### GitHub Organization
211
- - **Main repo**: [hera-artificial-life/hera-al](https://github.com/hera-artificial-life/hera-al)
212
- - **Skill repositories**: Skills developed by the community
52
+ - **GitHub**: [hera-artificial-life](https://github.com/hera-artificial-life)
213
53
  - **OSXNode**: [hera-artificial-life/osxnode](https://github.com/hera-artificial-life/osxnode)
54
+ - **npm**: [@hera-al](https://www.npmjs.com/org/hera-al)
214
55
 
215
56
  ---
216
57
 
217
- ## 🔐 Security
218
-
219
- ### The Skill Guardian
220
- Before installing any third-party skill, run:
221
- ```bash
222
- hera skill-scan <path-to-skill>
223
- ```
224
-
225
- Detects 90+ malicious patterns including:
226
- - Code execution & shell injection
227
- - Data exfiltration & credential theft
228
- - Persistence mechanisms
229
- - Prompt injection attacks
230
-
231
- ### Permission System
232
- Fine-grained control over:
233
- - Tool execution (approve/deny on demand or via policy)
234
- - File system access (read/write boundaries)
235
- - Network requests (allowlist/blocklist domains)
236
- - Skill capabilities (sandbox mode)
237
-
238
- ---
239
-
240
- ## 🌱 Philosophy: Hera vs. OpenClaw
241
-
242
- **OpenClaw** is the open-source framework — maximum flexibility, community-driven, all options on the table.
243
-
244
- **Hera** is the opinionated product — curated experience, cohesive design, decisions made for you. Think Rails vs. Express, or macOS vs. Linux.
245
-
246
- Both are valid. Hera makes choices so you don't have to. The building blocks (npm packages) are MIT licensed. The product (gateway + curated skills + Nostromo) is the Hera experience.
247
-
248
- ---
249
-
250
- ## 🛠️ Development
251
-
252
- ### Project Structure
253
- ```
254
- grabmeabeer/
255
- ├── src/ # Gateway source code
256
- │ ├── channels/ # Telegram, WhatsApp, Discord, WebChat
257
- │ ├── agent/ # Claude SDK integration
258
- │ ├── memory/ # Semantic search, RDF concept map
259
- │ ├── cron/ # Scheduler & job management
260
- │ └── tools/ # Built-in tools (node_exec, TTS, etc.)
261
- ├── browser-server/ # WebChat UI (published to npm)
262
- ├── standardnode/ # Remote node client (published to npm)
263
- ├── .claude/skills/ # Skill directory
264
- ├── bundled/ # Pre-installed skills
265
- ├── installationPkg/ # Installation wizard assets
266
- └── nostromo/ # Admin panel source
267
- ```
268
-
269
- ### Contributing
270
- 1. Fork the repo
271
- 2. Create a feature branch
272
- 3. Write tests (when applicable)
273
- 4. Submit a pull request
274
-
275
- **Skill contributions welcome!** See `.claude/skills/skill-creator/SKILL.md` for the template.
276
-
277
- ---
278
-
279
- ## 📖 Documentation
280
-
281
- - **Installation Guide**: `docs/installation.md` (coming soon)
282
- - **Skill Development**: `.claude/skills/skill-creator/SKILL.md`
283
- - **API Reference**: `docs/api.md` (coming soon)
284
- - **Nostromo Manual**: `docs/nostromo.md` (coming soon)
285
-
286
- ---
287
-
288
- ## 🙏 Credits
289
-
290
- ### Built With
291
- - [Claude Agent SDK](https://github.com/anthropics/anthropic-sdk-typescript) — Anthropic
292
- - [grammY](https://grammy.dev) — Telegram bot framework
293
- - [Hono](https://hono.dev) — Lightweight web framework
294
- - [Zod](https://zod.dev) — TypeScript-first schema validation
295
- - [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) — Fast SQLite bindings
296
-
297
- ### Inspiration
298
- Standing on the shoulders of giants: OpenClaw, AutoGPT, LangChain, and the entire open-source AI community.
299
-
300
- ---
301
-
302
- ## 📄 License
303
-
304
- **MIT License** — see [LICENSE](./LICENSE) for details.
58
+ ## License
305
59
 
306
- Core components (`@hera-al/*` packages) are free and open source. Build, fork, modify, deploy commercially — it's yours.
60
+ **MIT** see [LICENSE](./LICENSE).
307
61
 
308
- The Hera product (curated gateway + skills + Nostromo) represents our vision of what an AI agent should be. Use the components freely; experience Hera as intended.
309
-
310
- ---
311
-
312
- ## 🔗 Links
313
-
314
- - **Website**: Coming soon
315
- - **npm**: [@hera-al](https://www.npmjs.com/org/hera-al)
316
- - **GitHub**: [hera-artificial-life](https://github.com/hera-artificial-life)
317
- - **Author**: TGP (The Good Programmer)
318
- - **Email**: heralife.dev@gmail.com
62
+ Core components are free and open source. Build, fork, modify, deploy commercially.
319
63
 
320
64
  ---
321
65
 
322
66
  <p align="center">
323
67
  <strong>💎 Hera Artificial Life</strong><br>
324
- <em>Opinionated AI. Curated experience. Unprecedented autonomy.</em>
68
+ <em>Opinionated AI. Curated experience. Unprecedented autonomy.</em><br><br>
69
+ <a href="https://github.com/hera-artificial-life/hera-al">GitHub</a> · <a href="https://www.npmjs.com/org/hera-al">npm</a> · heralife.dev@gmail.com
325
70
  </p>
@@ -117,11 +117,15 @@ export declare class SessionAgent {
117
117
  * for compaction thresholds and cost tracking.
118
118
  */
119
119
  private resolvePiConfig;
120
+ /** Regex matching retryable API errors in response text. */
121
+ private static readonly API_RETRY_RE;
120
122
  /**
121
123
  * Send a prompt and wait for the agent's response.
122
124
  * Behavior depends on queueMode when the agent is already busy.
123
125
  *
124
- * Automatically retries on API Error 500 (up to 3 attempts with 2s delay).
126
+ * Automatically retries on transient API errors (500, 502, 503, 529/overloaded)
127
+ * with exponential backoff. Retry parameters are configurable via
128
+ * `agent.apiRetry` in config.yaml.
125
129
  */
126
130
  send(prompt: BuiltPrompt, _retryCount?: number): Promise<AgentResult>;
127
131
  interrupt(): Promise<boolean>;
@@ -1 +1 @@
1
- import{query as e}from"@anthropic-ai/claude-agent-sdk";import{MessageQueue as s}from"./message-queue.js";import{resolveModelId as t}from"../config.js";import{createLogger as i}from"../utils/logger.js";const o=i("SessionAgent");export class SessionAgent{sessionKey;config;queue;queryHandle=null;pendingResponses=[];currentResponse="";currentSessionId;model;queueMode;closed=!1;piProviderConfig=null;outputDone=!1;initialized=!1;opts;collectBuffer=[];lastCollectAt=0;debounceMs;debounceTimer=null;debounceResolve=null;queueCap;dropPolicy;droppedResolvers=[];droppedSummaries=[];sdkSlashCommands=[];channelSender=null;toolUseNotifier=null;typingSetter=null;typingClearer=null;textBlockStreamer=null;pendingTextBlock="";streamedAny=!1;streamedText="";usageRecorder=null;autoApproveTools;pendingPermission=null;pendingQuestion=null;constructor(e,i,n,r,l,a,h,u,c,d,p,g,m,f,y,$,b,v,w,T,S,K,R){this.sessionKey=e,this.config=i,this.currentSessionId=l??"";const x=a??i.agent.model;this.model=x?t(i,x):"",this.queueMode=i.agent.queueMode,this.debounceMs=Math.max(0,i.agent.queueDebounceMs),this.queueCap=Math.max(0,i.agent.queueCap),this.dropPolicy=i.agent.queueDropPolicy,this.autoApproveTools=i.agent.autoApproveTools,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:p?{type:"preset",preset:"claude_code",append:n}:n,...i.agent.maxTurns>0?{maxTurns:i.agent.maxTurns}:{},cwd:i.agent.workspacePath,env:process.env,permissionMode:i.agent.permissionMode,allowDangerouslySkipPermissions:!1,...b?{sandbox:{enabled:!0,autoAllowBashIfSandboxed:!0,network:{allowLocalBinding:!0}}}:{},canUseTool:async(e,s)=>this.handleCanUseTool(e,s),hooks:{PreCompact:[{hooks:[async e=>{const s=e?.trigger??"auto";if(o.info(`[${this.sessionKey}] PreCompact hook fired (trigger=${s})`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e="auto"===s?"The conversation context is getting large — compacting memory to keep things running smoothly.":"Compacting conversation memory...";this.channelSender(t,i,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{o.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const P=i.agent.settingSources;"user"===P?this.opts.settingSources=["user"]:"project"===P?this.opts.settingSources=["project"]:"both"===P&&(this.opts.settingSources=["user","project"]);const k=i.agent.mainFallback;k&&(this.opts.fallbackModel=t(i,k)),i.agent.allowedTools.length>0&&(this.opts.allowedTools=i.agent.allowedTools),i.agent.disallowedTools.length>0&&(this.opts.disallowedTools=i.agent.disallowedTools);const _={};if(Object.keys(i.agent.mcpServers).length>0&&Object.assign(_,i.agent.mcpServers),h&&(_["node-tools"]=h),u&&(_["message-tools"]=u),c&&(_["server-tools"]=c),d&&(_["cron-tools"]=d),f&&(_["tts-tools"]=f),y&&(_["memory-tools"]=y),$&&(_["browser-tools"]=$),v&&(_["pico-tools"]=v),w&&(_["telegram-actions"]=w),T&&(_["a2ui-tools"]=T),S&&(_["dynamic-ui-tools"]=S),K&&(_["plasma-client-tools"]=K),R&&(_["concept-tools"]=R),Object.keys(_).length>0&&(this.opts.mcpServers=_,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(_)){const s=`mcp__${e}__*`;this.opts.allowedTools.includes(s)||this.opts.allowedTools.push(s)}if(l&&(this.opts.resume=l),!1===g&&(this.opts.allowedTools&&this.opts.allowedTools.length>0?this.opts.allowedTools=this.opts.allowedTools.filter(e=>"Task"!==e):(this.opts.disallowedTools||(this.opts.disallowedTools=[]),this.opts.disallowedTools.includes("Task")||this.opts.disallowedTools.push("Task"))),m){const e={};for(const s of i.agent.customSubAgents){if(!s.enabled)continue;const t=s.expandContext?r+"\n\n"+s.prompt:s.prompt;e[s.name]={description:s.description,prompt:t,tools:s.tools,..."inherit"!==s.model?{model:s.model}:{}}}Object.keys(e).length>0&&(this.opts.agents=e)}const A=i.agent.plugins.filter(e=>e.enabled);A.length>0&&(this.opts.options={...this.opts.options,plugins:A.map(e=>({type:"local",path:e.path}))});const C=this.buildEnvForModel(this.model);this.opts.env=C.env,C.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&o.info(`[${e}] Pi engine: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}resolvePiConfig(){const e=this.model,s=this.config.models?.find(s=>s.id===e),t=s?.name??"";let i;const n=this.config.agent.picoAgent;if(n?.enabled&&Array.isArray(n.modelRefs)&&(i=n.modelRefs.find(e=>e.split(":")[0]===t)),!i){const e=this.config.agent.engine;if(!e||"pi"!==e.type||!e.piModelRef)return null;i=e.piModelRef}const r=i.split(":");if(r.length<2)return o.warn(`[${this.currentSessionId}] Invalid piModelRef (missing ':'): ${i}`),null;const l=r[0].trim();let a,h;if(r.length>=3)a=r[1].trim(),h=r.slice(2).join(":").trim(),h.startsWith(a+":")&&(h=h.substring(a.length+1));else{const e=r[1].trim(),s=e.indexOf("/");s>0?(a=e.substring(0,s),h=e.substring(s+1)):(a="openrouter",h=e)}const u=this.config.models?.find(e=>e.name===l);let c,d;u?.baseURL&&u.baseURL.includes("openrouter.ai")&&"openrouter"!==a&&(o.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),o.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`);const p=n?.rollingMemoryModel;if(p){const e=p.split(":");if(e.length>=3)c=e[1].trim(),d=e.slice(2).join(":").trim();else if(2===e.length){const s=e[1].indexOf("/");s>0?(c=e[1].substring(0,s).trim(),d=e[1].substring(s+1).trim()):d=e[1].trim()}d&&o.info(`[${this.currentSessionId}] Summarization model resolved: ${c}/${d}`)}return{provider:a,modelId:h,apiKey:u?.apiKey||void 0,baseUrl:u?.baseURL||void 0,contextWindowTokens:u?.contextWindow||void 0,costInput:u?.costInput||void 0,costOutput:u?.costOutput||void 0,costCacheRead:u?.costCacheRead||void 0,costCacheWrite:u?.costCacheWrite||void 0,summarizationProvider:c,summarizationModelId:d}}async send(e,s){const t=s??0;if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");let i;switch(this.ensureInitialized(),this.queueMode){case"collect":i=await this.sendCollect(e);break;case"steer":i=await this.sendSteer(e);break;default:i=await this.sendDirect(e)}const n=i.response.includes("API Error: 500")||i.fullResponse&&i.fullResponse.includes("API Error: 500");if(n&&t<3){const s=t+1;if(o.warn(`[${this.sessionKey}] API Error 500 detected, retrying (${s}/3)...`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e=`API temporarily unavailable, retrying (attempt ${s}/3)...`;this.channelSender(t,i,e).catch(()=>{})}}}return await new Promise(e=>setTimeout(e,2e3)),this.send(e,s)}if(n&&t>=3&&(o.error(`[${this.sessionKey}] API Error 500 persists after 3 retries`),this.channelSender)){const e=this.sessionKey.indexOf(":");if(e>0){const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if("cron"!==s){const e="API did not respond after 3 attempts. Please try again later.";this.channelSender(s,t,e).catch(()=>{})}}}return i}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),o.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,o.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){o.error(`[${this.sessionKey}] Failed to set model: ${e}`)}}close(){if(this.closed)return;this.closed=!0,this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.debounceResolve&&(this.debounceResolve(),this.debounceResolve=null),this.queue.close(),this.queryHandle&&this.queryHandle.close();const e=new Error("SessionAgent closed");for(const s of this.pendingResponses)s.reject(e);for(const s of this.collectBuffer)s.reject(e);for(const s of this.droppedResolvers)s.reject(e);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[],o.info(`[${this.sessionKey}] Closed`)}isActive(){return!this.closed&&!this.outputDone}getSessionId(){return this.currentSessionId}getModel(){return this.model}getSdkSlashCommands(){return this.sdkSlashCommands}setChannelSender(e){this.channelSender=e}setToolUseNotifier(e){this.toolUseNotifier=e}setTypingSetter(e){this.typingSetter=e}setTypingClearer(e){this.typingClearer=e}setTextBlockStreamer(e){this.textBlockStreamer=e}setUsageRecorder(e){this.usageRecorder=e}buildEnvForModel(e){const s=this.config.models.find(s=>s.id===e);if(!s?.proxy||"not-used"===s.proxy)return{env:{...process.env},proxied:!1,disableThinking:!1};const t={...process.env};return"direct"===s.proxy?(t.ANTHROPIC_BASE_URL=s.baseURL,t.ANTHROPIC_AUTH_TOKEN=s.apiKey,t.ANTHROPIC_API_KEY="",o.info(`[${this.sessionKey}] Direct env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!1}):(t.ANTHROPIC_BASE_URL=s.fastUrl||this.config.fastProxyUrl,t.ANTHROPIC_AUTH_TOKEN=s.fastProxyApiKey,t.ANTHROPIC_API_KEY="",delete t.ANTHROPIC_BETAS,delete t.CLAUDE_CODE_EXTRA_BODY,o.info(`[${this.sessionKey}] Proxy env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!0})}hasPendingPermission(){return null!==this.pendingPermission}resolvePermission(e){if(!this.pendingPermission)return;const s=this.pendingPermission;this.pendingPermission=null,e?(o.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(o.info(`[${this.sessionKey}] Permission denied: ${s.toolName}`),s.resolve({behavior:"deny",message:"User denied this action"}))}isBusy(){return this.pendingResponses.length>0}hasPendingQuestion(){return null!==this.pendingQuestion}resolveQuestion(e){if(!this.pendingQuestion)return;const s=this.pendingQuestion;this.pendingQuestion=null,o.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for AskUserQuestion, auto-approving`),{behavior:"allow",updatedInput:s};const e=this.sessionKey.indexOf(":");if(e<0)return{behavior:"allow",updatedInput:s};const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if(!t||!i||"cron"===t)return{behavior:"allow",updatedInput:s};const n=s?.questions;if(!Array.isArray(n)||0===n.length)return{behavior:"allow",updatedInput:s};const r={};for(const e of n){const n=e.question||"?",l=Array.isArray(e.options)?e.options:[],a=[];if(e.header&&a.push(`*${e.header}*`),a.push(n),l.some(e=>e.description)){a.push("");for(const e of l){const s=e.description?`: ${e.description}`:"";a.push(`• ${e.label}${s}`)}}const h=a.join("\n");if(this.typingClearer)try{await this.typingClearer(t,i)}catch{}try{if(l.length>0){const e=l.map(e=>({text:e.label||String(e),callbackData:`__ask:${e.label||String(e)}`}));await this.channelSender(t,i,h,e)}else await this.channelSender(t,i,h)}catch(e){return o.error(`[${this.sessionKey}] Failed to send AskUserQuestion: ${e}`),{behavior:"allow",updatedInput:s}}const u=55e3,c=await new Promise(e=>{const s=setTimeout(()=>{if(this.pendingQuestion){this.pendingQuestion=null;const s=l.length>0?l[0].label||String(l[0]):"No answer";o.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,i,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},u);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:n}});if(r[n]=c,this.typingSetter)try{await this.typingSetter(t,i)}catch{}}return o.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(r)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:r}}}if(this.autoApproveTools)return o.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for interactive permission, auto-approving: ${e}`),{behavior:"allow",updatedInput:s};const t=this.sessionKey.indexOf(":");if(t<0)return{behavior:"allow",updatedInput:s};const i=this.sessionKey.substring(0,t),n=this.sessionKey.substring(t+1);if(!i||!n||"cron"===i)return{behavior:"allow",updatedInput:s};const r=[`[Permission Request] Tool: ${e}`];if("Bash"===e&&s?.command)r.push(`Command: ${s.command}`),s.description&&r.push(`Description: ${s.description}`);else if("Write"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("Edit"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("ExitPlanMode"===e&&s?.plan){if(r.push(""),r.push(s.plan),Array.isArray(s.allowedPrompts)&&s.allowedPrompts.length>0){r.push(""),r.push("Requested permissions:");for(const e of s.allowedPrompts)r.push(` - [${e.tool}] ${e.prompt}`)}}else{const e=JSON.stringify(s);e.length<=300?r.push(`Input: ${e}`):r.push(`Input: ${e.slice(0,297)}...`)}r.push(""),r.push("Reply: approve to allow, deny to reject");const l=r.join("\n"),a=[{text:"Approve",callbackData:"__tool_perm:approve"},{text:"Deny",callbackData:"__tool_perm:deny"}];try{await this.channelSender(i,n,l,a)}catch(e){return o.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(i,n)}catch{}const h=12e4;return new Promise(t=>{const r=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,o.warn(`[${this.sessionKey}] Permission timeout for ${e}, auto-denying`),this.channelSender&&this.channelSender(i,n,`[Permission timeout] Tool ${e} denied after 120s`).catch(()=>{}),t({behavior:"deny",message:"Permission request timed out"}))},h);this.pendingPermission={resolve:t,toolName:e,input:s};const l=t;this.pendingPermission.resolve=e=>{clearTimeout(r),l(e)}})}async forwardAskUserQuestion(e){if(!this.channelSender)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(!t||!i||"cron"===t)return;const n=e?.questions;if(Array.isArray(n)){for(const e of n){const s=e.question||"?",n=Array.isArray(e.options)?e.options:[],r=[];if(e.header&&r.push(`*${e.header}*`),r.push(s),n.some(e=>e.description)){r.push("");for(const e of n){const s=e.description?`: ${e.description}`:"";r.push(`• ${e.label}${s}`)}}const l=r.join("\n");try{if(n.length>0){const e=n.map(e=>({text:e.label||String(e),callbackData:e.label||String(e)}));await this.channelSender(t,i,l,e)}else await this.channelSender(t,i,l)}catch(e){o.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,i)}catch(e){o.error(`[${this.sessionKey}] Failed to clear typing: ${e}`)}}}async notifyToolUse(e){if(!this.toolUseNotifier)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(t&&i&&"cron"!==t)try{await this.toolUseNotifier(t,i,e)}catch(e){o.error(`[${this.sessionKey}] Failed to notify tool use: ${e}`)}}async flushPendingTextBlock(){if(!this.textBlockStreamer||!this.pendingTextBlock)return;const e=this.sessionKey.indexOf(":");if(e<0)return;const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if(!s||!t||"cron"===s)return;const i=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0,this.streamedText+=i;try{await this.textBlockStreamer(s,t,i)}catch(e){o.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return o.warn(`[${this.sessionKey}] Queue cap reached (${this.queueCap}), rejecting message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.buildQueueMessage(e);return new Promise((e,t)=>{this.pendingResponses.push({resolve:e,reject:t}),this.queue.push(s),o.info(`[${this.sessionKey}] Message queued (pending=${this.pendingResponses.length})`)})}sendCollect(e){return this.pendingResponses.length>0?this.queueCap>0&&this.collectBuffer.length>=this.queueCap?this.applyDropPolicy(e):(this.lastCollectAt=Date.now(),o.info(`[${this.sessionKey}] Collecting message (buffer=${this.collectBuffer.length+1})`),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})):this.sendDirect(e)}async sendSteer(e){return this.pendingResponses.length>0&&(o.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return o.warn(`[${this.sessionKey}] Queue cap reached, rejecting new message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.collectBuffer.shift();return"summarize"===this.dropPolicy&&this.droppedSummaries.push(function(e,s){const t=e.replace(/\s+/g," ").trim();return t.length<=s?t:`${t.slice(0,s-1).trimEnd()}…`}(s.prompt.text,140)),this.droppedResolvers.push({resolve:s.resolve,reject:s.reject}),o.warn(`[${this.sessionKey}] Queue cap reached, dropped oldest message (policy=${this.dropPolicy}, dropped=${this.droppedResolvers.length})`),this.lastCollectAt=Date.now(),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})}async debounceThenFlush(){if(this.debounceMs<=0||this.closed)this.flushCollectBuffer();else{for(;!this.closed&&this.collectBuffer.length>0;){const e=Date.now()-this.lastCollectAt;if(e>=this.debounceMs)break;await new Promise(s=>{this.debounceResolve=s,this.debounceTimer=setTimeout(s,this.debounceMs-e)}),this.debounceTimer=null,this.debounceResolve=null}this.closed||this.flushCollectBuffer()}}flushCollectBuffer(){if(0===this.collectBuffer.length&&0===this.droppedResolvers.length)return;const e=this.collectBuffer.splice(0),s=e.map(e=>e.prompt),t=this.mergePrompts(s),i=this.buildQueueMessage(t),n=[...this.droppedResolvers.splice(0),...e.map(e=>({resolve:e.resolve,reject:e.reject}))];this.droppedSummaries=[],this.pendingResponses.push({resolve:e=>{for(const s of n)s.resolve(e)},reject:e=>{for(const s of n)s.reject(e)}}),this.queue.push(i),o.info(`[${this.sessionKey}] Flushed ${e.length} collected message(s) as one prompt`)}mergePrompts(e){const s=[],t=[];if(this.droppedSummaries.length>0){s.push(`[${this.droppedSummaries.length} earlier message(s) dropped due to queue cap]`);for(const e of this.droppedSummaries)s.push(`- ${e}`);s.push("")}if(1===e.length&&0===this.droppedSummaries.length)return e[0];if(e.length>0){s.push("[Queued messages while agent was busy]");for(let i=0;i<e.length;i++)e[i].text&&s.push(`${i+1}. ${e[i].text}`),t.push(...e[i].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){if(this.initialized)return;this.initialized=!0;const s=this.piProviderConfig?"pi":"claudecode";o.info(`[${this.sessionKey}] Starting agent: engine=${s}, model=${this.model}, mode=${this.queueMode}, debounce=${this.debounceMs}ms, cap=${this.queueCap||"unlimited"}, drop=${this.dropPolicy}, session=${this.currentSessionId||"new"}`),this.piProviderConfig?this.initPiEngine():(this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput())}async initPiEngine(){try{const e=await import("../pi-agent-provider/index.js"),s=await e.createToolRegistryFromOptions(this.opts);this.queryHandle=e.piQuery({prompt:this.queue,options:this.opts},this.piProviderConfig,s),this.processOutput(),o.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){o.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),o.warn(`[${this.sessionKey}] Falling back to Claude SDK (claudecode engine)`),this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput()}}buildQueueMessage(e){if(0===e.images.length)return o.debug(`[${this.sessionKey}] SDK request: text-only (${e.text.length} chars): ${this.config.verboseDebugLogs?e.text:e.text.slice(0,15)+"..."}`),{type:"user",message:{role:"user",content:e.text}};const s=[];for(const t of e.images)s.push({type:"image",source:{type:"base64",media_type:t.mimeType,data:t.base64}});return e.text&&s.push({type:"text",text:e.text}),o.debug(`[${this.sessionKey}] SDK request: ${s.length} block(s) [${s.map(e=>"image"===e.type?`image/${e.source.media_type}`:`text(${e.text?.length??0})`).join(", ")}]`),{type:"user",message:{role:"user",content:s}}}async processOutput(){if(this.queryHandle)try{for await(const e of this.queryHandle){if(this.closed)break;if(o.debug(`[${this.sessionKey}] SDK message: type=${e.type}, subtype=${e.subtype??"-"}, keys=${Object.keys(e).join(",")}`),"system"===e.type){const s=e,t=s.subtype;if("init"===t){const e=s.slash_commands;Array.isArray(e)&&(this.sdkSlashCommands=e.map(e=>e.replace(/^\//,"")))}if("compact_boundary"===t){const e=s.compact_metadata,t=["Context compacted."];e?.pre_tokens&&t.push(`Pre-compaction tokens: ${e.pre_tokens}.`),e?.trigger&&t.push(`Trigger: ${e.trigger}.`),this.currentResponse=t.join(" "),o.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const{type:e,...i}=s;this.currentResponse=JSON.stringify(i,null,2),o.info(`[${this.sessionKey}] System message (${t??"unknown"}): ${this.currentResponse.slice(0,200)}`)}}if("assistant"===e.type){const s=e.message.content,t=s.filter(e=>"text"===e.type).map(e=>e.text).join("");t&&(this.currentResponse=t,this.pendingTextBlock=t);const i=s.map(e=>e.type).join(", ");o.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${i}], text length=${t.length}: ${this.config.verboseDebugLogs?t:t.slice(0,15)+"..."}`);s.some(e=>"tool_use"===e.type)&&this.pendingTextBlock&&this.textBlockStreamer&&await this.flushPendingTextBlock();for(const e of s)if("tool_use"===e.type){const s=JSON.stringify(e.input);o.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&await this.notifyToolUse(e.name)}}if("tool_progress"===e.type){const s=e;o.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;o.debug(`[${this.sessionKey}] SDK result: subtype=${s.subtype}, stop_reason=${s.stop_reason??"null"}, session=${s.session_id??"n/a"}, result length=${s.result?.length??0}`),"session_id"in s&&(this.currentSessionId=s.session_id),this.usageRecorder&&(void 0!==s.total_cost_usd||s.modelUsage)&&this.usageRecorder(this.sessionKey,s.total_cost_usd,s.duration_ms,s.num_turns,s.modelUsage);const i=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&this.pendingResponses.length<=1&&(void 0!==s.total_cost_usd||s.usage)){const e=[];if(void 0!==s.total_cost_usd&&e.push(`Total cost: $${Number(s.total_cost_usd).toFixed(4)}`),void 0!==s.duration_ms&&e.push(`Duration: ${(s.duration_ms/1e3).toFixed(1)}s`),void 0!==s.num_turns&&e.push(`Turns: ${s.num_turns}`),s.modelUsage)for(const[t,i]of Object.entries(s.modelUsage)){const s=i,o=[` ${t}:`];s.inputTokens&&o.push(`input=${s.inputTokens}`),s.outputTokens&&o.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&o.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&o.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&o.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(o.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}if(!s.result&&!this.currentResponse&&this.pendingResponses.length<=1){const e=this.piProviderConfig;o.warn(`[${this.sessionKey}] Empty response on success: provider=${e?.provider??"sdk"}, modelId=${e?.modelId??"n/a"}, stop_reason=${i}. Check provider routing and API key.`)}"refusal"===i?(o.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"max_tokens"===i&&o.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",o.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",o.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];e.some(e=>e.includes("aborted"))?o.info(`[${this.sessionKey}] Request aborted (steer interrupt)`):o.error(`[${this.sessionKey}] SDK error: ${JSON.stringify(s)}`)}const n=this.pendingResponses.shift();if(n){const e=this.currentResponse||"";let s=e;this.streamedAny&&(s=this.pendingTextBlock||"",!s&&e&&e.length>this.streamedText.length&&(s=e.startsWith(this.streamedText)?e.slice(this.streamedText.length).replace(/^\n+/,""):e)),o.info(`[${this.sessionKey}] Response ready: session=${this.currentSessionId}, length=${s.length}${this.streamedAny?` (streamed, full=${e.length})`:""}`),n.resolve({response:s,fullResponse:this.streamedAny?e:void 0,sessionId:this.currentSessionId,sessionReset:!1,errorType:t,stopReason:i})}this.currentResponse="",this.pendingTextBlock="",this.streamedAny=!1,this.streamedText="","collect"===this.queueMode&&(this.collectBuffer.length>0||this.droppedResolvers.length>0)&&await this.debounceThenFlush()}}}catch(e){o.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();if(s)if(this.currentSessionId){const t=e instanceof Error?e.message:String(e);o.warn(`[${this.sessionKey}] Session error (${this.currentSessionId}): ${t}`),s.resolve({response:t,sessionId:"",sessionReset:!0})}else s.reject(e instanceof Error?e:new Error(String(e)));const t=new Error("SessionAgent terminated");for(const e of this.pendingResponses)e.reject(t);for(const e of this.collectBuffer)e.reject(t);for(const e of this.droppedResolvers)e.reject(t);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[]}finally{this.outputDone=!0}}}
1
+ import{query as e}from"@anthropic-ai/claude-agent-sdk";import{MessageQueue as s}from"./message-queue.js";import{resolveModelId as t}from"../config.js";import{createLogger as i}from"../utils/logger.js";const o=i("SessionAgent");export class SessionAgent{sessionKey;config;queue;queryHandle=null;pendingResponses=[];currentResponse="";currentSessionId;model;queueMode;closed=!1;piProviderConfig=null;outputDone=!1;initialized=!1;opts;collectBuffer=[];lastCollectAt=0;debounceMs;debounceTimer=null;debounceResolve=null;queueCap;dropPolicy;droppedResolvers=[];droppedSummaries=[];sdkSlashCommands=[];channelSender=null;toolUseNotifier=null;typingSetter=null;typingClearer=null;textBlockStreamer=null;pendingTextBlock="";streamedAny=!1;streamedText="";usageRecorder=null;autoApproveTools;pendingPermission=null;pendingQuestion=null;constructor(e,i,n,r,l,a,h,u,c,d,p,g,m,f,y,$,b,v,w,T,S,R,x){this.sessionKey=e,this.config=i,this.currentSessionId=l??"";const K=a??i.agent.model;this.model=K?t(i,K):"",this.queueMode=i.agent.queueMode,this.debounceMs=Math.max(0,i.agent.queueDebounceMs),this.queueCap=Math.max(0,i.agent.queueCap),this.dropPolicy=i.agent.queueDropPolicy,this.autoApproveTools=i.agent.autoApproveTools,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:p?{type:"preset",preset:"claude_code",append:n}:n,...i.agent.maxTurns>0?{maxTurns:i.agent.maxTurns}:{},cwd:i.agent.workspacePath,env:process.env,permissionMode:i.agent.permissionMode,allowDangerouslySkipPermissions:!1,...b?{sandbox:{enabled:!0,autoAllowBashIfSandboxed:!0,network:{allowLocalBinding:!0}}}:{},canUseTool:async(e,s)=>this.handleCanUseTool(e,s),hooks:{PreCompact:[{hooks:[async e=>{const s=e?.trigger??"auto";if(o.info(`[${this.sessionKey}] PreCompact hook fired (trigger=${s})`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e="auto"===s?"The conversation context is getting large — compacting memory to keep things running smoothly.":"Compacting conversation memory...";this.channelSender(t,i,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{o.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const P=i.agent.settingSources;"user"===P?this.opts.settingSources=["user"]:"project"===P?this.opts.settingSources=["project"]:"both"===P&&(this.opts.settingSources=["user","project"]);const _=i.agent.mainFallback;_&&(this.opts.fallbackModel=t(i,_)),i.agent.allowedTools.length>0&&(this.opts.allowedTools=i.agent.allowedTools),i.agent.disallowedTools.length>0&&(this.opts.disallowedTools=i.agent.disallowedTools);const k={};if(Object.keys(i.agent.mcpServers).length>0&&Object.assign(k,i.agent.mcpServers),h&&(k["node-tools"]=h),u&&(k["message-tools"]=u),c&&(k["server-tools"]=c),d&&(k["cron-tools"]=d),f&&(k["tts-tools"]=f),y&&(k["memory-tools"]=y),$&&(k["browser-tools"]=$),v&&(k["pico-tools"]=v),w&&(k["telegram-actions"]=w),T&&(k["a2ui-tools"]=T),S&&(k["dynamic-ui-tools"]=S),R&&(k["plasma-client-tools"]=R),x&&(k["concept-tools"]=x),Object.keys(k).length>0&&(this.opts.mcpServers=k,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(k)){const s=`mcp__${e}__*`;this.opts.allowedTools.includes(s)||this.opts.allowedTools.push(s)}if(l&&(this.opts.resume=l),!1===g&&(this.opts.allowedTools&&this.opts.allowedTools.length>0?this.opts.allowedTools=this.opts.allowedTools.filter(e=>"Task"!==e):(this.opts.disallowedTools||(this.opts.disallowedTools=[]),this.opts.disallowedTools.includes("Task")||this.opts.disallowedTools.push("Task"))),m){const e={};for(const s of i.agent.customSubAgents){if(!s.enabled)continue;const t=s.expandContext?r+"\n\n"+s.prompt:s.prompt;e[s.name]={description:s.description,prompt:t,tools:s.tools,..."inherit"!==s.model?{model:s.model}:{}}}Object.keys(e).length>0&&(this.opts.agents=e)}const A=i.agent.plugins.filter(e=>e.enabled);A.length>0&&(this.opts.options={...this.opts.options,plugins:A.map(e=>({type:"local",path:e.path}))});const C=this.buildEnvForModel(this.model);this.opts.env=C.env,C.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&o.info(`[${e}] Pi engine: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}resolvePiConfig(){const e=this.model,s=this.config.models?.find(s=>s.id===e),t=s?.name??"";let i;const n=this.config.agent.picoAgent;if(n?.enabled&&Array.isArray(n.modelRefs)&&(i=n.modelRefs.find(e=>e.split(":")[0]===t)),!i){const e=this.config.agent.engine;if(!e||"pi"!==e.type||!e.piModelRef)return null;i=e.piModelRef}const r=i.split(":");if(r.length<2)return o.warn(`[${this.currentSessionId}] Invalid piModelRef (missing ':'): ${i}`),null;const l=r[0].trim();let a,h;if(r.length>=3)a=r[1].trim(),h=r.slice(2).join(":").trim(),h.startsWith(a+":")&&(h=h.substring(a.length+1));else{const e=r[1].trim(),s=e.indexOf("/");s>0?(a=e.substring(0,s),h=e.substring(s+1)):(a="openrouter",h=e)}const u=this.config.models?.find(e=>e.name===l);let c,d;u?.baseURL&&u.baseURL.includes("openrouter.ai")&&"openrouter"!==a&&(o.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),o.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`);const p=n?.rollingMemoryModel;if(p){const e=p.split(":");if(e.length>=3)c=e[1].trim(),d=e.slice(2).join(":").trim();else if(2===e.length){const s=e[1].indexOf("/");s>0?(c=e[1].substring(0,s).trim(),d=e[1].substring(s+1).trim()):d=e[1].trim()}d&&o.info(`[${this.currentSessionId}] Summarization model resolved: ${c}/${d}`)}return{provider:a,modelId:h,apiKey:u?.apiKey||void 0,baseUrl:u?.baseURL||void 0,contextWindowTokens:u?.contextWindow||void 0,costInput:u?.costInput||void 0,costOutput:u?.costOutput||void 0,costCacheRead:u?.costCacheRead||void 0,costCacheWrite:u?.costCacheWrite||void 0,summarizationProvider:c,summarizationModelId:d}}static API_RETRY_RE=/API Error:\s*(?:500|502|503|529)|overloaded|internal server error/i;async send(e,s){const t=s??0;if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");let i;switch(this.ensureInitialized(),this.queueMode){case"collect":i=await this.sendCollect(e);break;case"steer":i=await this.sendSteer(e);break;default:i=await this.sendDirect(e)}const n=i.fullResponse??i.response,r=SessionAgent.API_RETRY_RE.test(n),l=this.config.agent.apiRetry,a=l.maxAttempts;if(r&&t<a){const s=t+1,i=Math.min(l.baseDelayMs*2**t,l.maxDelayMs);if(o.warn(`[${this.sessionKey}] Transient API error detected, retry ${s}/${a} in ${i}ms`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e=`API temporarily unavailable, retrying (attempt ${s}/${a})...`;this.channelSender(t,i,e).catch(()=>{})}}}return await new Promise(e=>setTimeout(e,i)),this.send(e,s)}if(r&&t>=a&&(o.error(`[${this.sessionKey}] API error persists after ${a} retries`),this.channelSender)){const e=this.sessionKey.indexOf(":");if(e>0){const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if("cron"!==s){const e=`API did not respond after ${a} attempts. Please try again later.`;this.channelSender(s,t,e).catch(()=>{})}}}return i}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),o.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,o.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){o.error(`[${this.sessionKey}] Failed to set model: ${e}`)}}close(){if(this.closed)return;this.closed=!0,this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.debounceResolve&&(this.debounceResolve(),this.debounceResolve=null),this.queue.close(),this.queryHandle&&this.queryHandle.close();const e=new Error("SessionAgent closed");for(const s of this.pendingResponses)s.reject(e);for(const s of this.collectBuffer)s.reject(e);for(const s of this.droppedResolvers)s.reject(e);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[],o.info(`[${this.sessionKey}] Closed`)}isActive(){return!this.closed&&!this.outputDone}getSessionId(){return this.currentSessionId}getModel(){return this.model}getSdkSlashCommands(){return this.sdkSlashCommands}setChannelSender(e){this.channelSender=e}setToolUseNotifier(e){this.toolUseNotifier=e}setTypingSetter(e){this.typingSetter=e}setTypingClearer(e){this.typingClearer=e}setTextBlockStreamer(e){this.textBlockStreamer=e}setUsageRecorder(e){this.usageRecorder=e}buildEnvForModel(e){const s=this.config.models.find(s=>s.id===e);if(!s?.proxy||"not-used"===s.proxy)return{env:{...process.env},proxied:!1,disableThinking:!1};const t={...process.env};return"direct"===s.proxy?(t.ANTHROPIC_BASE_URL=s.baseURL,t.ANTHROPIC_AUTH_TOKEN=s.apiKey,t.ANTHROPIC_API_KEY="",o.info(`[${this.sessionKey}] Direct env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!1}):(t.ANTHROPIC_BASE_URL=s.fastUrl||this.config.fastProxyUrl,t.ANTHROPIC_AUTH_TOKEN=s.fastProxyApiKey,t.ANTHROPIC_API_KEY="",delete t.ANTHROPIC_BETAS,delete t.CLAUDE_CODE_EXTRA_BODY,o.info(`[${this.sessionKey}] Proxy env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!0})}hasPendingPermission(){return null!==this.pendingPermission}resolvePermission(e){if(!this.pendingPermission)return;const s=this.pendingPermission;this.pendingPermission=null,e?(o.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(o.info(`[${this.sessionKey}] Permission denied: ${s.toolName}`),s.resolve({behavior:"deny",message:"User denied this action"}))}isBusy(){return this.pendingResponses.length>0}hasPendingQuestion(){return null!==this.pendingQuestion}resolveQuestion(e){if(!this.pendingQuestion)return;const s=this.pendingQuestion;this.pendingQuestion=null,o.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for AskUserQuestion, auto-approving`),{behavior:"allow",updatedInput:s};const e=this.sessionKey.indexOf(":");if(e<0)return{behavior:"allow",updatedInput:s};const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if(!t||!i||"cron"===t)return{behavior:"allow",updatedInput:s};const n=s?.questions;if(!Array.isArray(n)||0===n.length)return{behavior:"allow",updatedInput:s};const r={};for(const e of n){const n=e.question||"?",l=Array.isArray(e.options)?e.options:[],a=[];if(e.header&&a.push(`*${e.header}*`),a.push(n),l.some(e=>e.description)){a.push("");for(const e of l){const s=e.description?`: ${e.description}`:"";a.push(`• ${e.label}${s}`)}}const h=a.join("\n");if(this.typingClearer)try{await this.typingClearer(t,i)}catch{}try{if(l.length>0){const e=l.map(e=>({text:e.label||String(e),callbackData:`__ask:${e.label||String(e)}`}));await this.channelSender(t,i,h,e)}else await this.channelSender(t,i,h)}catch(e){return o.error(`[${this.sessionKey}] Failed to send AskUserQuestion: ${e}`),{behavior:"allow",updatedInput:s}}const u=55e3,c=await new Promise(e=>{const s=setTimeout(()=>{if(this.pendingQuestion){this.pendingQuestion=null;const s=l.length>0?l[0].label||String(l[0]):"No answer";o.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,i,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},u);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:n}});if(r[n]=c,this.typingSetter)try{await this.typingSetter(t,i)}catch{}}return o.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(r)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:r}}}if(this.autoApproveTools)return o.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for interactive permission, auto-approving: ${e}`),{behavior:"allow",updatedInput:s};const t=this.sessionKey.indexOf(":");if(t<0)return{behavior:"allow",updatedInput:s};const i=this.sessionKey.substring(0,t),n=this.sessionKey.substring(t+1);if(!i||!n||"cron"===i)return{behavior:"allow",updatedInput:s};const r=[`[Permission Request] Tool: ${e}`];if("Bash"===e&&s?.command)r.push(`Command: ${s.command}`),s.description&&r.push(`Description: ${s.description}`);else if("Write"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("Edit"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("ExitPlanMode"===e&&s?.plan){if(r.push(""),r.push(s.plan),Array.isArray(s.allowedPrompts)&&s.allowedPrompts.length>0){r.push(""),r.push("Requested permissions:");for(const e of s.allowedPrompts)r.push(` - [${e.tool}] ${e.prompt}`)}}else{const e=JSON.stringify(s);e.length<=300?r.push(`Input: ${e}`):r.push(`Input: ${e.slice(0,297)}...`)}r.push(""),r.push("Reply: approve to allow, deny to reject");const l=r.join("\n"),a=[{text:"Approve",callbackData:"__tool_perm:approve"},{text:"Deny",callbackData:"__tool_perm:deny"}];try{await this.channelSender(i,n,l,a)}catch(e){return o.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(i,n)}catch{}const h=12e4;return new Promise(t=>{const r=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,o.warn(`[${this.sessionKey}] Permission timeout for ${e}, auto-denying`),this.channelSender&&this.channelSender(i,n,`[Permission timeout] Tool ${e} denied after 120s`).catch(()=>{}),t({behavior:"deny",message:"Permission request timed out"}))},h);this.pendingPermission={resolve:t,toolName:e,input:s};const l=t;this.pendingPermission.resolve=e=>{clearTimeout(r),l(e)}})}async forwardAskUserQuestion(e){if(!this.channelSender)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(!t||!i||"cron"===t)return;const n=e?.questions;if(Array.isArray(n)){for(const e of n){const s=e.question||"?",n=Array.isArray(e.options)?e.options:[],r=[];if(e.header&&r.push(`*${e.header}*`),r.push(s),n.some(e=>e.description)){r.push("");for(const e of n){const s=e.description?`: ${e.description}`:"";r.push(`• ${e.label}${s}`)}}const l=r.join("\n");try{if(n.length>0){const e=n.map(e=>({text:e.label||String(e),callbackData:e.label||String(e)}));await this.channelSender(t,i,l,e)}else await this.channelSender(t,i,l)}catch(e){o.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,i)}catch(e){o.error(`[${this.sessionKey}] Failed to clear typing: ${e}`)}}}async notifyToolUse(e){if(!this.toolUseNotifier)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(t&&i&&"cron"!==t)try{await this.toolUseNotifier(t,i,e)}catch(e){o.error(`[${this.sessionKey}] Failed to notify tool use: ${e}`)}}async flushPendingTextBlock(){if(!this.textBlockStreamer||!this.pendingTextBlock)return;const e=this.sessionKey.indexOf(":");if(e<0)return;const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if(!s||!t||"cron"===s)return;const i=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0,this.streamedText+=i;try{await this.textBlockStreamer(s,t,i)}catch(e){o.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return o.warn(`[${this.sessionKey}] Queue cap reached (${this.queueCap}), rejecting message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.buildQueueMessage(e);return new Promise((e,t)=>{this.pendingResponses.push({resolve:e,reject:t}),this.queue.push(s),o.info(`[${this.sessionKey}] Message queued (pending=${this.pendingResponses.length})`)})}sendCollect(e){return this.pendingResponses.length>0?this.queueCap>0&&this.collectBuffer.length>=this.queueCap?this.applyDropPolicy(e):(this.lastCollectAt=Date.now(),o.info(`[${this.sessionKey}] Collecting message (buffer=${this.collectBuffer.length+1})`),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})):this.sendDirect(e)}async sendSteer(e){return this.pendingResponses.length>0&&(o.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return o.warn(`[${this.sessionKey}] Queue cap reached, rejecting new message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.collectBuffer.shift();return"summarize"===this.dropPolicy&&this.droppedSummaries.push(function(e,s){const t=e.replace(/\s+/g," ").trim();return t.length<=s?t:`${t.slice(0,s-1).trimEnd()}…`}(s.prompt.text,140)),this.droppedResolvers.push({resolve:s.resolve,reject:s.reject}),o.warn(`[${this.sessionKey}] Queue cap reached, dropped oldest message (policy=${this.dropPolicy}, dropped=${this.droppedResolvers.length})`),this.lastCollectAt=Date.now(),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})}async debounceThenFlush(){if(this.debounceMs<=0||this.closed)this.flushCollectBuffer();else{for(;!this.closed&&this.collectBuffer.length>0;){const e=Date.now()-this.lastCollectAt;if(e>=this.debounceMs)break;await new Promise(s=>{this.debounceResolve=s,this.debounceTimer=setTimeout(s,this.debounceMs-e)}),this.debounceTimer=null,this.debounceResolve=null}this.closed||this.flushCollectBuffer()}}flushCollectBuffer(){if(0===this.collectBuffer.length&&0===this.droppedResolvers.length)return;const e=this.collectBuffer.splice(0),s=e.map(e=>e.prompt),t=this.mergePrompts(s),i=this.buildQueueMessage(t),n=[...this.droppedResolvers.splice(0),...e.map(e=>({resolve:e.resolve,reject:e.reject}))];this.droppedSummaries=[],this.pendingResponses.push({resolve:e=>{for(const s of n)s.resolve(e)},reject:e=>{for(const s of n)s.reject(e)}}),this.queue.push(i),o.info(`[${this.sessionKey}] Flushed ${e.length} collected message(s) as one prompt`)}mergePrompts(e){const s=[],t=[];if(this.droppedSummaries.length>0){s.push(`[${this.droppedSummaries.length} earlier message(s) dropped due to queue cap]`);for(const e of this.droppedSummaries)s.push(`- ${e}`);s.push("")}if(1===e.length&&0===this.droppedSummaries.length)return e[0];if(e.length>0){s.push("[Queued messages while agent was busy]");for(let i=0;i<e.length;i++)e[i].text&&s.push(`${i+1}. ${e[i].text}`),t.push(...e[i].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){if(this.initialized)return;this.initialized=!0;const s=this.piProviderConfig?"pi":"claudecode";o.info(`[${this.sessionKey}] Starting agent: engine=${s}, model=${this.model}, mode=${this.queueMode}, debounce=${this.debounceMs}ms, cap=${this.queueCap||"unlimited"}, drop=${this.dropPolicy}, session=${this.currentSessionId||"new"}`),this.piProviderConfig?this.initPiEngine():(this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput())}async initPiEngine(){try{const e=await import("../pi-agent-provider/index.js"),s=await e.createToolRegistryFromOptions(this.opts);this.queryHandle=e.piQuery({prompt:this.queue,options:this.opts},this.piProviderConfig,s),this.processOutput(),o.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){o.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),o.warn(`[${this.sessionKey}] Falling back to Claude SDK (claudecode engine)`),this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput()}}buildQueueMessage(e){if(0===e.images.length)return o.debug(`[${this.sessionKey}] SDK request: text-only (${e.text.length} chars): ${this.config.verboseDebugLogs?e.text:e.text.slice(0,15)+"..."}`),{type:"user",message:{role:"user",content:e.text}};const s=[];for(const t of e.images)s.push({type:"image",source:{type:"base64",media_type:t.mimeType,data:t.base64}});return e.text&&s.push({type:"text",text:e.text}),o.debug(`[${this.sessionKey}] SDK request: ${s.length} block(s) [${s.map(e=>"image"===e.type?`image/${e.source.media_type}`:`text(${e.text?.length??0})`).join(", ")}]`),{type:"user",message:{role:"user",content:s}}}async processOutput(){if(this.queryHandle)try{for await(const e of this.queryHandle){if(this.closed)break;if(o.debug(`[${this.sessionKey}] SDK message: type=${e.type}, subtype=${e.subtype??"-"}, keys=${Object.keys(e).join(",")}`),"system"===e.type){const s=e,t=s.subtype;if("init"===t){const e=s.slash_commands;Array.isArray(e)&&(this.sdkSlashCommands=e.map(e=>e.replace(/^\//,"")))}if("compact_boundary"===t){const e=s.compact_metadata,t=["Context compacted."];e?.pre_tokens&&t.push(`Pre-compaction tokens: ${e.pre_tokens}.`),e?.trigger&&t.push(`Trigger: ${e.trigger}.`),this.currentResponse=t.join(" "),o.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const{type:e,...i}=s;this.currentResponse=JSON.stringify(i,null,2),o.info(`[${this.sessionKey}] System message (${t??"unknown"}): ${this.currentResponse.slice(0,200)}`)}}if("assistant"===e.type){const s=e.message.content,t=s.filter(e=>"text"===e.type).map(e=>e.text).join("");t&&(this.currentResponse=t,this.pendingTextBlock=t);const i=s.map(e=>e.type).join(", ");o.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${i}], text length=${t.length}: ${this.config.verboseDebugLogs?t:t.slice(0,15)+"..."}`);s.some(e=>"tool_use"===e.type)&&this.pendingTextBlock&&this.textBlockStreamer&&await this.flushPendingTextBlock();for(const e of s)if("tool_use"===e.type){const s=JSON.stringify(e.input);o.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&await this.notifyToolUse(e.name)}}if("tool_progress"===e.type){const s=e;o.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;o.debug(`[${this.sessionKey}] SDK result: subtype=${s.subtype}, stop_reason=${s.stop_reason??"null"}, session=${s.session_id??"n/a"}, result length=${s.result?.length??0}`),"session_id"in s&&(this.currentSessionId=s.session_id),this.usageRecorder&&(void 0!==s.total_cost_usd||s.modelUsage)&&this.usageRecorder(this.sessionKey,s.total_cost_usd,s.duration_ms,s.num_turns,s.modelUsage);const i=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&this.pendingResponses.length<=1&&(void 0!==s.total_cost_usd||s.usage)){const e=[];if(void 0!==s.total_cost_usd&&e.push(`Total cost: $${Number(s.total_cost_usd).toFixed(4)}`),void 0!==s.duration_ms&&e.push(`Duration: ${(s.duration_ms/1e3).toFixed(1)}s`),void 0!==s.num_turns&&e.push(`Turns: ${s.num_turns}`),s.modelUsage)for(const[t,i]of Object.entries(s.modelUsage)){const s=i,o=[` ${t}:`];s.inputTokens&&o.push(`input=${s.inputTokens}`),s.outputTokens&&o.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&o.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&o.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&o.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(o.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}if(!s.result&&!this.currentResponse&&this.pendingResponses.length<=1){const e=this.piProviderConfig;o.warn(`[${this.sessionKey}] Empty response on success: provider=${e?.provider??"sdk"}, modelId=${e?.modelId??"n/a"}, stop_reason=${i}. Check provider routing and API key.`)}"refusal"===i?(o.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"max_tokens"===i&&o.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",o.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",o.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];e.some(e=>e.includes("aborted"))?o.info(`[${this.sessionKey}] Request aborted (steer interrupt)`):o.error(`[${this.sessionKey}] SDK error: ${JSON.stringify(s)}`)}const n=this.pendingResponses.shift();if(n){const e=this.currentResponse||"";let s=e;this.streamedAny&&(s=this.pendingTextBlock||"",!s&&e&&e.length>this.streamedText.length&&(s=e.startsWith(this.streamedText)?e.slice(this.streamedText.length).replace(/^\n+/,""):e)),o.info(`[${this.sessionKey}] Response ready: session=${this.currentSessionId}, length=${s.length}${this.streamedAny?` (streamed, full=${e.length})`:""}`),n.resolve({response:s,fullResponse:this.streamedAny?e:void 0,sessionId:this.currentSessionId,sessionReset:!1,errorType:t,stopReason:i})}this.currentResponse="",this.pendingTextBlock="",this.streamedAny=!1,this.streamedText="","collect"===this.queueMode&&(this.collectBuffer.length>0||this.droppedResolvers.length>0)&&await this.debounceThenFlush()}}}catch(e){o.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();if(s)if(this.currentSessionId){const t=e instanceof Error?e.message:String(e);o.warn(`[${this.sessionKey}] Session error (${this.currentSessionId}): ${t}`),s.resolve({response:t,sessionId:"",sessionReset:!0})}else s.reject(e instanceof Error?e:new Error(String(e)));const t=new Error("SessionAgent terminated");for(const e of this.pendingResponses)e.reject(t);for(const e of this.collectBuffer)e.reject(t);for(const e of this.droppedResolvers)e.reject(t);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[]}finally{this.outputDone=!0}}}
package/dist/config.d.ts CHANGED
@@ -162,6 +162,12 @@ declare const ModelEntrySchema: z.ZodObject<{
162
162
  costCacheRead: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
163
163
  costCacheWrite: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
164
164
  }, z.core.$strip>;
165
+ declare const ApiRetryConfigSchema: z.ZodObject<{
166
+ maxAttempts: z.ZodDefault<z.ZodNumber>;
167
+ baseDelayMs: z.ZodDefault<z.ZodNumber>;
168
+ maxDelayMs: z.ZodDefault<z.ZodNumber>;
169
+ }, z.core.$strip>;
170
+ export type ApiRetryConfig = z.infer<typeof ApiRetryConfigSchema>;
165
171
  declare const AppConfigSchema: z.ZodObject<{
166
172
  gmabPath: z.ZodDefault<z.ZodOptional<z.ZodString>>;
167
173
  host: z.ZodDefault<z.ZodOptional<z.ZodString>>;
@@ -416,6 +422,11 @@ declare const AppConfigSchema: z.ZodObject<{
416
422
  inflightTyping: z.ZodDefault<z.ZodBoolean>;
417
423
  autoApproveTools: z.ZodDefault<z.ZodBoolean>;
418
424
  autoRenew: z.ZodDefault<z.ZodNumber>;
425
+ apiRetry: z.ZodDefault<z.ZodOptional<z.ZodObject<{
426
+ maxAttempts: z.ZodDefault<z.ZodNumber>;
427
+ baseDelayMs: z.ZodDefault<z.ZodNumber>;
428
+ maxDelayMs: z.ZodDefault<z.ZodNumber>;
429
+ }, z.core.$strip>>>;
419
430
  }, z.core.$strip>>>;
420
431
  cron: z.ZodDefault<z.ZodOptional<z.ZodObject<{
421
432
  enabled: z.ZodDefault<z.ZodBoolean>;
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- import{readFileSync as e,writeFileSync as t,existsSync as o,mkdirSync as a,renameSync as n,unlinkSync as l}from"node:fs";import{resolve as r,join as i}from"node:path";import{homedir as s}from"node:os";import{config as u}from"dotenv";import{parse as d,stringify as c}from"yaml";import{z as f}from"zod";import{BrowserConfigSchema as p}from"@hera-al/browser-server/config";import{createLogger as m}from"./utils/logger.js";const b=m("Config");function g(e){return"~"===e||e.startsWith("~/")?e.replace("~",s()):e}let h=g(process.env.GMAB_PATH??"~/gmab"),y=i(h,"data");export function getGmabPath(){return h}export function getDataDir(){return y}export function getNostromoKeyPath(){return i(y,".nostromo-key")}export function backupConfig(a){if(o(a))try{const r=e=>`${a}.backup${e}`;o(r(1))&&l(r(1));for(let e=2;e<=5;e++)o(r(e))&&n(r(e),r(e-1));t(r(5),e(a)),b.debug(`Config backup created: ${r(5)}`)}catch(e){b.warn(`Failed to create config backup: ${e}`)}}const v=f.record(f.string(),f.any()),x=f.object({reactions:f.boolean().default(!0),sendMessage:f.boolean().default(!0),editMessage:f.boolean().default(!0),deleteMessage:f.boolean().default(!0),sticker:f.boolean().default(!1),createForumTopic:f.boolean().default(!1)}),w=f.object({maxAttempts:f.number().default(3),baseDelayMs:f.number().default(1e3),maxDelayMs:f.number().default(3e4)}),M=f.enum(["off","dm","group","all","allowlist"]),k=f.object({botToken:f.string(),dmPolicy:f.enum(["open","token","allowlist"]).default("allowlist"),allowFrom:f.array(f.union([f.string(),f.number()])).default([]),name:f.string().optional(),reactionLevel:f.enum(["off","ack","minimal","extensive"]).default("ack"),reactionNotifications:f.enum(["off","own","all"]).default("off"),inlineButtonsScope:M.optional(),textChunkLimit:f.number().default(4e3),streamMode:f.enum(["off","partial","block"]).default("partial"),linkPreview:f.boolean().default(!0),actions:x.optional(),retry:w.optional(),timeoutSeconds:f.number().optional(),proxy:f.string().optional()}),P=f.object({enabled:f.boolean().default(!1),accounts:f.record(f.string(),k).default({})}),j=f.object({enabled:f.boolean().default(!1),accounts:v.default({})}),R=f.object({enabled:f.boolean().default(!0),port:f.number().default(3004)}),C=f.object({telegram:P.optional().default({enabled:!1,accounts:{}}),whatsapp:j.optional().default({enabled:!1,accounts:{}}),discord:j.optional().default({enabled:!1,accounts:{}}),slack:j.optional().default({enabled:!1,accounts:{}}),signal:j.optional().default({enabled:!1,accounts:{}}),msteams:j.optional().default({enabled:!1,accounts:{}}),googlechat:j.optional().default({enabled:!1,accounts:{}}),line:j.optional().default({enabled:!1,accounts:{}}),matrix:j.optional().default({enabled:!1,accounts:{}}),responses:R.optional().default({enabled:!0,port:3e3})}),S=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),T=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),A=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":S.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":T.optional().default({binaryPath:"whisper",model:"base"})}),I=f.object({voice:f.string().default("en-US-MichelleNeural"),lang:f.string().default("en-US"),outputFormat:f.string().default("audio-24khz-48kbitrate-mono-mp3")}),D=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),U=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),z=f.object({enabled:f.boolean().default(!1),provider:f.enum(["edge","openai","elevenlabs"]).default("openai"),maxTextLength:f.number().default(4096),timeoutMs:f.number().default(3e4),edge:I.optional().default({voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"}),openai:D.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:U.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),L=f.object({enabled:f.boolean().default(!1),embeddingModel:f.string().default("text-embedding-3-small"),embeddingDimensions:f.number().default(1536),modelRef:f.string().default(""),prefixQuery:f.string().default(""),prefixDocument:f.string().default(""),updateDebounceMs:f.number().default(3e3),embedIntervalMs:f.number().default(3e5),maxResults:f.number().default(6),maxSnippetChars:f.number().default(700),maxInjectedChars:f.number().default(4e3),rrfK:f.number().default(60)}),W={enabled:!1,embeddingModel:"text-embedding-3-small",embeddingDimensions:1536,modelRef:"",prefixQuery:"",prefixDocument:"",updateDebounceMs:3e3,embedIntervalMs:3e5,maxResults:6,maxSnippetChars:700,maxInjectedChars:4e3,rrfK:60},E=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:L.optional().default(W)}),F=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),K=f.object({id:f.string(),name:f.string(),types:f.array(f.enum(["internal","external","env-var"])).optional().default(["external"]),proxy:f.enum(["not-used","direct","proxied"]).optional().default("not-used"),fastUrl:f.string().optional().default(""),fastProxyApiKey:f.string().optional().default(""),apiKey:f.string().optional().default(""),baseURL:f.string().optional().default(""),useEnvVar:f.string().optional().default(""),contextWindow:f.number().optional().default(2e5),costInput:f.number().optional().default(0),costOutput:f.number().optional().default(0),costCacheRead:f.number().optional().default(0),costCacheWrite:f.number().optional().default(0)}),O=[{id:"claude-opus-4-6",name:"Claude Opus",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-sonnet-4-6",name:"Claude Sonnet",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0}],_=f.array(K).default(O),q=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),B=f.object({name:f.string(),description:f.string(),prompt:f.string(),model:f.enum(["sonnet","opus","haiku","inherit"]).default("inherit"),tools:f.array(f.string()).default(["Read","Write","Edit","Glob","Grep","WebSearch","WebFetch"]),expandContext:f.boolean().default(!1),enabled:f.boolean().default(!1)}),G=f.object({type:f.enum(["claudecode","pi"]).default("claudecode"),piModelRef:f.string().default("")}).passthrough(),X={type:"claudecode",piModelRef:""},$=f.object({enabled:f.boolean().default(!1),modelRefs:f.array(f.string()).default([]),rollingMemoryModel:f.string().default("")}),N={enabled:!1,modelRefs:[],rollingMemoryModel:""},V=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),engine:G.optional().default(X),picoAgent:$.optional().default(N),maxTurns:f.number().default(50),permissionMode:f.string().default("bypassPermissions"),sessionTTL:f.number().default(3600),queueMode:f.enum(["queue","collect","steer"]).default("steer"),queueDebounceMs:f.number().default(1500),queueCap:f.number().default(20),queueDropPolicy:f.enum(["old","new","summarize"]).default("summarize"),allowedTools:f.array(f.string()).default([]),disallowedTools:f.array(f.string()).default([]),mcpServers:f.record(f.string(),F).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array(B).default([]),plugins:f.array(q).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0)}),H={enabled:!1,provider:"openai",maxTextLength:4096,timeoutMs:3e4,edge:{voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"},openai:{modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"},elevenlabs:{modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"}},Q={enabled:!0,recallStrategy:"builtin-only",dir:"",search:W},Z={model:"claude-opus-4-6",mainFallback:"",engine:X,picoAgent:N,maxTurns:50,permissionMode:"bypassPermissions",sessionTTL:3600,queueMode:"steer",queueDebounceMs:1500,queueCap:20,queueDropPolicy:"summarize",allowedTools:[],disallowedTools:[],mcpServers:{},workspacePath:"./workspace",builtinCoderSkill:!1,settingSources:"project",customSubAgents:[],plugins:[],inflightTyping:!0,autoApproveTools:!0,autoRenew:0},J=f.object({enabled:f.boolean().default(!1),every:f.number().default(18e5),channel:f.string().default(""),chatId:f.string().default(""),message:f.string().default(""),ackMaxChars:f.number().default(300)}),Y={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},ee=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:J.optional().default(Y)}),te={enabled:!0,isolated:!0,broadcastEvents:!1,storePath:"",heartbeat:Y},oe=f.object({enabled:f.boolean().default(!0),port:f.number().default(3001),basePath:f.string().default("/nostromo"),configCheckInterval:f.number().default(5),autoRestart:f.boolean().default(!0)}),ae={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},ne=f.object({gmabPath:f.string().optional().default("~/gmab"),host:f.string().optional().default("127.0.0.1"),logLevel:f.enum(["debug","info","warn","error"]).optional().default("info"),verboseDebugLogs:f.boolean().optional().default(!0),timezone:f.string().optional().default(""),fastProxyUrl:f.string().optional().default("http://localhost:4181"),channels:C.optional().default({telegram:{enabled:!1,accounts:{}},whatsapp:{enabled:!1,accounts:{}},discord:{enabled:!1,accounts:{}},slack:{enabled:!1,accounts:{}},signal:{enabled:!1,accounts:{}},msteams:{enabled:!1,accounts:{}},googlechat:{enabled:!1,accounts:{}},line:{enabled:!1,accounts:{}},matrix:{enabled:!1,accounts:{}},responses:{enabled:!0,port:3004}}),models:_.optional().default(O),stt:A.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:z.optional().default(H),memory:E.optional().default(Q),agent:V.optional().default(Z),cron:ee.optional().default(te),nostromo:oe.optional().default(ae),browser:p.optional().default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,remoteCdpTimeoutMs:1500,profiles:{default:{cdpPort:9222,color:"#FF4500"}}})});function le(e){const t=function(e){const t=new Set,o=/\$\{([^}]+)\}/g;let a;for(;null!==(a=o.exec(e));)t.add(a[1]);return Array.from(t)}(e),o=t.filter(e=>!process.env[e]);o.length>0&&b.warn(`Missing environment variables referenced in config: ${o.join(", ")}. Add them to .env or export them before starting.`)}function re(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(re);if(null!==e&&"object"==typeof e){const t={};for(const[o,a]of Object.entries(e))t[o]=re(a);return t}return e}const ie={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:H,memory:Q,agent:{...Z,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:te,nostromo:ae};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),s=r(process.cwd(),".env");if(o(s)&&(u({path:s}),b.info(`Loaded .env from ${s}`)),!o(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...ie});t(l,e,"utf-8")}const f=e(l,"utf-8");le(f);const p=re(d(f)),m=ne.parse(p);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(m.gmabPath),y=i(h,"data"),a(y,{recursive:!0}),!m.timezone){m.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;try{const o=d(e(l,"utf-8"))??{};o.timezone=m.timezone,t(l,c(o),"utf-8"),b.info(`Timezone auto-detected and saved: ${m.timezone}`)}catch(e){}}return function(e){a(y,{recursive:!0});const t=process.env.WORKSPACE_PATH??e.agent.workspacePath;e.agent.workspacePath=r(g(t)),a(e.agent.workspacePath,{recursive:!0});const o=i(y,"cron");a(o,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):i(o,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:i(y,"core.db"),memoryDir:i(y,"memory"),cronStorePath:n}}(m)}export function loadRawConfig(t){const a=t??r(process.cwd(),"config.yaml");if(!o(a))return{};const n=e(a,"utf-8");return d(n)??{}}export function resolveModelEntry(e,t){if(!t)return;const o=e.models;if(!o?.length)return;const a=t.indexOf(":");if(a>=0){const e=t.substring(0,a),n=t.substring(a+1);return o.find(t=>t.name===e&&t.id===n)}return o.find(e=>e.name===t)??o.find(e=>e.id===t)}export function modelRefName(e){if(!e)return e;const t=e.indexOf(":");return t>=0?e.substring(0,t):e}export function resolveModelId(e,t){if(!t)return t;const o=resolveModelEntry(e,t);return o?o.id:t}
1
+ import{readFileSync as e,writeFileSync as t,existsSync as o,mkdirSync as a,renameSync as n,unlinkSync as l}from"node:fs";import{resolve as r,join as s}from"node:path";import{homedir as i}from"node:os";import{config as u}from"dotenv";import{parse as d,stringify as c}from"yaml";import{z as f}from"zod";import{BrowserConfigSchema as p}from"@hera-al/browser-server/config";import{createLogger as m}from"./utils/logger.js";const b=m("Config");function g(e){return"~"===e||e.startsWith("~/")?e.replace("~",i()):e}let h=g(process.env.GMAB_PATH??"~/gmab"),y=s(h,"data");export function getGmabPath(){return h}export function getDataDir(){return y}export function getNostromoKeyPath(){return s(y,".nostromo-key")}export function backupConfig(a){if(o(a))try{const r=e=>`${a}.backup${e}`;o(r(1))&&l(r(1));for(let e=2;e<=5;e++)o(r(e))&&n(r(e),r(e-1));t(r(5),e(a)),b.debug(`Config backup created: ${r(5)}`)}catch(e){b.warn(`Failed to create config backup: ${e}`)}}const v=f.record(f.string(),f.any()),x=f.object({reactions:f.boolean().default(!0),sendMessage:f.boolean().default(!0),editMessage:f.boolean().default(!0),deleteMessage:f.boolean().default(!0),sticker:f.boolean().default(!1),createForumTopic:f.boolean().default(!1)}),w=f.object({maxAttempts:f.number().default(3),baseDelayMs:f.number().default(1e3),maxDelayMs:f.number().default(3e4)}),M=f.enum(["off","dm","group","all","allowlist"]),k=f.object({botToken:f.string(),dmPolicy:f.enum(["open","token","allowlist"]).default("allowlist"),allowFrom:f.array(f.union([f.string(),f.number()])).default([]),name:f.string().optional(),reactionLevel:f.enum(["off","ack","minimal","extensive"]).default("ack"),reactionNotifications:f.enum(["off","own","all"]).default("off"),inlineButtonsScope:M.optional(),textChunkLimit:f.number().default(4e3),streamMode:f.enum(["off","partial","block"]).default("partial"),linkPreview:f.boolean().default(!0),actions:x.optional(),retry:w.optional(),timeoutSeconds:f.number().optional(),proxy:f.string().optional()}),P=f.object({enabled:f.boolean().default(!1),accounts:f.record(f.string(),k).default({})}),j=f.object({enabled:f.boolean().default(!1),accounts:v.default({})}),R=f.object({enabled:f.boolean().default(!0),port:f.number().default(3004)}),C=f.object({telegram:P.optional().default({enabled:!1,accounts:{}}),whatsapp:j.optional().default({enabled:!1,accounts:{}}),discord:j.optional().default({enabled:!1,accounts:{}}),slack:j.optional().default({enabled:!1,accounts:{}}),signal:j.optional().default({enabled:!1,accounts:{}}),msteams:j.optional().default({enabled:!1,accounts:{}}),googlechat:j.optional().default({enabled:!1,accounts:{}}),line:j.optional().default({enabled:!1,accounts:{}}),matrix:j.optional().default({enabled:!1,accounts:{}}),responses:R.optional().default({enabled:!0,port:3e3})}),S=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),T=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),A=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":S.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":T.optional().default({binaryPath:"whisper",model:"base"})}),D=f.object({voice:f.string().default("en-US-MichelleNeural"),lang:f.string().default("en-US"),outputFormat:f.string().default("audio-24khz-48kbitrate-mono-mp3")}),I=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),U=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),z=f.object({enabled:f.boolean().default(!1),provider:f.enum(["edge","openai","elevenlabs"]).default("openai"),maxTextLength:f.number().default(4096),timeoutMs:f.number().default(3e4),edge:D.optional().default({voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"}),openai:I.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:U.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),L=f.object({enabled:f.boolean().default(!1),embeddingModel:f.string().default("text-embedding-3-small"),embeddingDimensions:f.number().default(1536),modelRef:f.string().default(""),prefixQuery:f.string().default(""),prefixDocument:f.string().default(""),updateDebounceMs:f.number().default(3e3),embedIntervalMs:f.number().default(3e5),maxResults:f.number().default(6),maxSnippetChars:f.number().default(700),maxInjectedChars:f.number().default(4e3),rrfK:f.number().default(60)}),W={enabled:!1,embeddingModel:"text-embedding-3-small",embeddingDimensions:1536,modelRef:"",prefixQuery:"",prefixDocument:"",updateDebounceMs:3e3,embedIntervalMs:3e5,maxResults:6,maxSnippetChars:700,maxInjectedChars:4e3,rrfK:60},E=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:L.optional().default(W)}),F=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),K=f.object({id:f.string(),name:f.string(),types:f.array(f.enum(["internal","external","env-var"])).optional().default(["external"]),proxy:f.enum(["not-used","direct","proxied"]).optional().default("not-used"),fastUrl:f.string().optional().default(""),fastProxyApiKey:f.string().optional().default(""),apiKey:f.string().optional().default(""),baseURL:f.string().optional().default(""),useEnvVar:f.string().optional().default(""),contextWindow:f.number().optional().default(2e5),costInput:f.number().optional().default(0),costOutput:f.number().optional().default(0),costCacheRead:f.number().optional().default(0),costCacheWrite:f.number().optional().default(0)}),O=[{id:"claude-opus-4-6",name:"Claude Opus",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-sonnet-4-6",name:"Claude Sonnet",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0}],_=f.array(K).default(O),q=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),B=f.object({name:f.string(),description:f.string(),prompt:f.string(),model:f.enum(["sonnet","opus","haiku","inherit"]).default("inherit"),tools:f.array(f.string()).default(["Read","Write","Edit","Glob","Grep","WebSearch","WebFetch"]),expandContext:f.boolean().default(!1),enabled:f.boolean().default(!1)}),G=f.object({type:f.enum(["claudecode","pi"]).default("claudecode"),piModelRef:f.string().default("")}).passthrough(),X={type:"claudecode",piModelRef:""},$=f.object({enabled:f.boolean().default(!1),modelRefs:f.array(f.string()).default([]),rollingMemoryModel:f.string().default("")}),N={enabled:!1,modelRefs:[],rollingMemoryModel:""},V=f.object({maxAttempts:f.number().default(5),baseDelayMs:f.number().default(2e3),maxDelayMs:f.number().default(3e4)}),H={maxAttempts:5,baseDelayMs:2e3,maxDelayMs:3e4},Q=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),engine:G.optional().default(X),picoAgent:$.optional().default(N),maxTurns:f.number().default(50),permissionMode:f.string().default("bypassPermissions"),sessionTTL:f.number().default(3600),queueMode:f.enum(["queue","collect","steer"]).default("steer"),queueDebounceMs:f.number().default(1500),queueCap:f.number().default(20),queueDropPolicy:f.enum(["old","new","summarize"]).default("summarize"),allowedTools:f.array(f.string()).default([]),disallowedTools:f.array(f.string()).default([]),mcpServers:f.record(f.string(),F).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array(B).default([]),plugins:f.array(q).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0),apiRetry:V.optional().default(H)}),Z={enabled:!1,provider:"openai",maxTextLength:4096,timeoutMs:3e4,edge:{voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"},openai:{modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"},elevenlabs:{modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"}},J={enabled:!0,recallStrategy:"builtin-only",dir:"",search:W},Y={model:"claude-opus-4-6",mainFallback:"",engine:X,picoAgent:N,maxTurns:50,permissionMode:"bypassPermissions",sessionTTL:3600,queueMode:"steer",queueDebounceMs:1500,queueCap:20,queueDropPolicy:"summarize",allowedTools:[],disallowedTools:[],mcpServers:{},workspacePath:"./workspace",builtinCoderSkill:!1,settingSources:"project",customSubAgents:[],plugins:[],inflightTyping:!0,autoApproveTools:!0,autoRenew:0,apiRetry:H},ee=f.object({enabled:f.boolean().default(!1),every:f.number().default(18e5),channel:f.string().default(""),chatId:f.string().default(""),message:f.string().default(""),ackMaxChars:f.number().default(300)}),te={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},oe=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:ee.optional().default(te)}),ae={enabled:!0,isolated:!0,broadcastEvents:!1,storePath:"",heartbeat:te},ne=f.object({enabled:f.boolean().default(!0),port:f.number().default(3001),basePath:f.string().default("/nostromo"),configCheckInterval:f.number().default(5),autoRestart:f.boolean().default(!0)}),le={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},re=f.object({gmabPath:f.string().optional().default("~/gmab"),host:f.string().optional().default("127.0.0.1"),logLevel:f.enum(["debug","info","warn","error"]).optional().default("info"),verboseDebugLogs:f.boolean().optional().default(!0),timezone:f.string().optional().default(""),fastProxyUrl:f.string().optional().default("http://localhost:4181"),channels:C.optional().default({telegram:{enabled:!1,accounts:{}},whatsapp:{enabled:!1,accounts:{}},discord:{enabled:!1,accounts:{}},slack:{enabled:!1,accounts:{}},signal:{enabled:!1,accounts:{}},msteams:{enabled:!1,accounts:{}},googlechat:{enabled:!1,accounts:{}},line:{enabled:!1,accounts:{}},matrix:{enabled:!1,accounts:{}},responses:{enabled:!0,port:3004}}),models:_.optional().default(O),stt:A.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:z.optional().default(Z),memory:E.optional().default(J),agent:Q.optional().default(Y),cron:oe.optional().default(ae),nostromo:ne.optional().default(le),browser:p.optional().default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,remoteCdpTimeoutMs:1500,profiles:{default:{cdpPort:9222,color:"#FF4500"}}})});function se(e){const t=function(e){const t=new Set,o=/\$\{([^}]+)\}/g;let a;for(;null!==(a=o.exec(e));)t.add(a[1]);return Array.from(t)}(e),o=t.filter(e=>!process.env[e]);o.length>0&&b.warn(`Missing environment variables referenced in config: ${o.join(", ")}. Add them to .env or export them before starting.`)}function ie(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(ie);if(null!==e&&"object"==typeof e){const t={};for(const[o,a]of Object.entries(e))t[o]=ie(a);return t}return e}const ue={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:Z,memory:J,agent:{...Y,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:ae,nostromo:le};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),i=r(process.cwd(),".env");if(o(i)&&(u({path:i}),b.info(`Loaded .env from ${i}`)),!o(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...ue});t(l,e,"utf-8")}const f=e(l,"utf-8");se(f);const p=ie(d(f)),m=re.parse(p);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(m.gmabPath),y=s(h,"data"),a(y,{recursive:!0}),!m.timezone){m.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;try{const o=d(e(l,"utf-8"))??{};o.timezone=m.timezone,t(l,c(o),"utf-8"),b.info(`Timezone auto-detected and saved: ${m.timezone}`)}catch(e){}}return function(e){a(y,{recursive:!0});const t=process.env.WORKSPACE_PATH??e.agent.workspacePath;e.agent.workspacePath=r(g(t)),a(e.agent.workspacePath,{recursive:!0});const o=s(y,"cron");a(o,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):s(o,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:s(y,"core.db"),memoryDir:s(y,"memory"),cronStorePath:n}}(m)}export function loadRawConfig(t){const a=t??r(process.cwd(),"config.yaml");if(!o(a))return{};const n=e(a,"utf-8");return d(n)??{}}export function resolveModelEntry(e,t){if(!t)return;const o=e.models;if(!o?.length)return;const a=t.indexOf(":");if(a>=0){const e=t.substring(0,a),n=t.substring(a+1);return o.find(t=>t.name===e&&t.id===n)}return o.find(e=>e.name===t)??o.find(e=>e.id===t)}export function modelRefName(e){if(!e)return e;const t=e.indexOf(":");return t>=0?e.substring(0,t):e}export function resolveModelId(e,t){if(!t)return t;const o=resolveModelEntry(e,t);return o?o.id:t}
@@ -11,6 +11,7 @@ export declare class WebChatChannel implements ChannelAdapter {
11
11
  private onMessage;
12
12
  private typingIntervals;
13
13
  private inflightCount;
14
+ private pendingMessages;
14
15
  start(onMessage: MessageHandler): Promise<void>;
15
16
  registerConnection(chatId: string, ws: WebSocket): void;
16
17
  unregisterConnection(chatId: string): void;
@@ -38,6 +39,8 @@ export declare class WebChatChannel implements ChannelAdapter {
38
39
  private stopTypingInterval;
39
40
  private resendTypingIfActive;
40
41
  private sendWs;
42
+ private enqueuePending;
43
+ private flushPending;
41
44
  }
42
45
  interface RawAttachment {
43
46
  type: string;
@@ -1 +1 @@
1
- import{readFileSync as t}from"node:fs";import{basename as e,extname as n}from"node:path";import{parseMediaLines as s}from"../../utils/media-response.js";import{createLogger as i}from"../../utils/logger.js";const a=i("WebChat");export function buildWebChatId(t,e){return`${(t||"node").toLowerCase().replace(/[^a-z0-9_-]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,"")||"node"}-${e.slice(0,8)}`}export class WebChatChannel{name="webchat";connections=new Map;onMessage=null;typingIntervals=new Map;inflightCount=new Map;async start(t){this.onMessage=t,a.info("WebChat channel started (virtual — connections managed by Nostromo)")}registerConnection(t,e){const n=!this.connections.has(t);this.connections.set(t,e),n&&a.info(`WebChat connection registered: ${t}`)}unregisterConnection(t){this.connections.delete(t)&&a.info(`WebChat connection unregistered: ${t}`)}unregisterByWs(t){for(const[e,n]of this.connections)n===t&&(this.connections.delete(e),a.info(`WebChat connection unregistered: ${e}`))}async handleNodeChat(t,e,n){if(!this.onMessage)return void a.warn("WebChat: message received but no onMessage handler registered");const i=[];if(n.attachments&&Array.isArray(n.attachments))for(const t of n.attachments)i.push({type:t.type,mimeType:t.mimeType,fileName:t.fileName,duration:t.duration,caption:t.caption,getBuffer:()=>Promise.resolve(Buffer.from(t.data,"base64"))});const o={chatId:t,userId:e,channelName:"webchat",text:n.text,attachments:i,username:t};this.startTypingInterval(t);try{const e=await this.onMessage(o),{textParts:n,mediaEntries:i}=s(e);for(const e of i)try{await this.sendAudio(t,e.path,e.asVoice)}catch(e){a.error(`WebChat: failed to send audio to ${t}: ${e}`)}const r=n.join("\n").trim();r&&this.sendWs(t,{type:"chat_response",role:"assistant",text:r}),this.resendTypingIfActive(t)}catch(e){a.error(`WebChat: error handling message from ${t}: ${e}`),this.sendWs(t,{type:"chat_response",role:"assistant",text:"Error processing message."})}}async sendText(t,e){this.sendWs(t,{type:"chat_message",role:"assistant",text:e}),this.resendTypingIfActive(t)}async sendButtons(t,e,n){const s=n.flat();this.sendWs(t,{type:"chat_message",role:"assistant",text:e,buttons:s.map(t=>({text:t.text,callbackData:t.callbackData??t.text,...t.url?{url:t.url}:{}}))}),this.resendTypingIfActive(t)}async setTyping(t){this.typingIntervals.has(t)?this.sendWs(t,{type:"typing_indicator",typing:!0}):this.startTypingInterval(t)}async clearTyping(t){this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!1})}async releaseTyping(t){this.stopTypingInterval(t)}async sendAudio(s,i,o){try{const a=t(i),r=e(i),c=n(i).toLowerCase().replace(".",""),h={mp3:"audio/mpeg",ogg:"audio/ogg",opus:"audio/ogg",wav:"audio/wav",m4a:"audio/mp4",flac:"audio/flac"}[c]||"audio/mpeg";this.sendWs(s,{type:"chat_media",role:"assistant",mediaType:"audio",mimeType:h,fileName:r,data:a.toString("base64"),asVoice:o??!1}),this.resendTypingIfActive(s)}catch(t){a.error(`WebChat: failed to read audio file ${i}: ${t}`)}}async stop(){for(const[t,e]of this.typingIntervals)clearInterval(e),this.sendWs(t,{type:"typing_indicator",typing:!1});this.typingIntervals.clear(),this.inflightCount.clear(),this.onMessage=null,a.info("WebChat channel stopped")}startTypingInterval(t){const e=(this.inflightCount.get(t)??0)+1;if(this.inflightCount.set(t,e),e>1)return;const n=this.typingIntervals.get(t);n&&(clearInterval(n),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!0});const s=setInterval(()=>{(this.inflightCount.get(t)??0)>0?this.sendWs(t,{type:"typing_indicator",typing:!0}):(clearInterval(s),this.typingIntervals.delete(t),this.sendWs(t,{type:"typing_indicator",typing:!1}))},4e3);this.typingIntervals.set(t,s)}stopTypingInterval(t){const e=(this.inflightCount.get(t)??1)-1;if(e>0)this.inflightCount.set(t,e);else{this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!1})}}resendTypingIfActive(t){this.typingIntervals.has(t)&&this.sendWs(t,{type:"typing_indicator",typing:!0})}sendWs(t,e){const n=this.connections.get(t);if(n&&n.readyState===n.OPEN)try{n.send(JSON.stringify({...e,chatId:t}))}catch(e){a.error(`WebChat: failed to send to ${t}: ${e}`)}else a.warn(`WebChat: no active connection for chatId ${t}`)}}
1
+ import{readFileSync as t}from"node:fs";import{basename as e,extname as n}from"node:path";import{parseMediaLines as s}from"../../utils/media-response.js";import{createLogger as i}from"../../utils/logger.js";const a=i("WebChat");export function buildWebChatId(t,e){return`${(t||"node").toLowerCase().replace(/[^a-z0-9_-]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,"")||"node"}-${e.slice(0,8)}`}export class WebChatChannel{name="webchat";connections=new Map;onMessage=null;typingIntervals=new Map;inflightCount=new Map;pendingMessages=new Map;async start(t){this.onMessage=t,a.info("WebChat channel started (virtual — connections managed by Nostromo)")}registerConnection(t,e){const n=!this.connections.has(t);this.connections.set(t,e),n&&a.info(`WebChat connection registered: ${t}`),this.flushPending(t)}unregisterConnection(t){this.connections.delete(t)&&a.info(`WebChat connection unregistered: ${t}`)}unregisterByWs(t){for(const[e,n]of this.connections)n===t&&(this.connections.delete(e),a.info(`WebChat connection unregistered: ${e}`))}async handleNodeChat(t,e,n){if(!this.onMessage)return void a.warn("WebChat: message received but no onMessage handler registered");const i=[];if(n.attachments&&Array.isArray(n.attachments))for(const t of n.attachments)i.push({type:t.type,mimeType:t.mimeType,fileName:t.fileName,duration:t.duration,caption:t.caption,getBuffer:()=>Promise.resolve(Buffer.from(t.data,"base64"))});const o={chatId:t,userId:e,channelName:"webchat",text:n.text,attachments:i,username:t};this.startTypingInterval(t);try{const e=await this.onMessage(o),{textParts:n,mediaEntries:i}=s(e);for(const e of i)try{await this.sendAudio(t,e.path,e.asVoice)}catch(e){a.error(`WebChat: failed to send audio to ${t}: ${e}`)}const r=n.join("\n").trim();r&&this.sendWs(t,{type:"chat_response",role:"assistant",text:r}),this.resendTypingIfActive(t)}catch(e){a.error(`WebChat: error handling message from ${t}: ${e}`),this.sendWs(t,{type:"chat_response",role:"assistant",text:"Error processing message."})}}async sendText(t,e){this.sendWs(t,{type:"chat_message",role:"assistant",text:e}),this.resendTypingIfActive(t)}async sendButtons(t,e,n){const s=n.flat();this.sendWs(t,{type:"chat_message",role:"assistant",text:e,buttons:s.map(t=>({text:t.text,callbackData:t.callbackData??t.text,...t.url?{url:t.url}:{}}))}),this.resendTypingIfActive(t)}async setTyping(t){this.typingIntervals.has(t)?this.sendWs(t,{type:"typing_indicator",typing:!0},!1):this.startTypingInterval(t)}async clearTyping(t){this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!1},!1)}async releaseTyping(t){this.stopTypingInterval(t)}async sendAudio(s,i,o){try{const a=t(i),r=e(i),h=n(i).toLowerCase().replace(".",""),g={mp3:"audio/mpeg",ogg:"audio/ogg",opus:"audio/ogg",wav:"audio/wav",m4a:"audio/mp4",flac:"audio/flac"}[h]||"audio/mpeg";this.sendWs(s,{type:"chat_media",role:"assistant",mediaType:"audio",mimeType:g,fileName:r,data:a.toString("base64"),asVoice:o??!1}),this.resendTypingIfActive(s)}catch(t){a.error(`WebChat: failed to read audio file ${i}: ${t}`)}}async stop(){for(const[t,e]of this.typingIntervals)clearInterval(e),this.sendWs(t,{type:"typing_indicator",typing:!1},!1);this.typingIntervals.clear(),this.inflightCount.clear(),this.onMessage=null,a.info("WebChat channel stopped")}startTypingInterval(t){const e=(this.inflightCount.get(t)??0)+1;if(this.inflightCount.set(t,e),e>1)return;const n=this.typingIntervals.get(t);n&&(clearInterval(n),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!0},!1);const s=setInterval(()=>{(this.inflightCount.get(t)??0)>0?this.sendWs(t,{type:"typing_indicator",typing:!0},!1):(clearInterval(s),this.typingIntervals.delete(t),this.sendWs(t,{type:"typing_indicator",typing:!1},!1))},4e3);this.typingIntervals.set(t,s)}stopTypingInterval(t){const e=(this.inflightCount.get(t)??1)-1;if(e>0)this.inflightCount.set(t,e);else{this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sendWs(t,{type:"typing_indicator",typing:!1},!1)}}resendTypingIfActive(t){this.typingIntervals.has(t)&&this.sendWs(t,{type:"typing_indicator",typing:!0},!1)}sendWs(t,e,n=!0){const s=this.connections.get(t);if(s&&s.readyState===s.OPEN)try{s.send(JSON.stringify({...e,chatId:t}))}catch(e){a.error(`WebChat: failed to send to ${t}: ${e}`)}else n&&this.enqueuePending(t,e)}enqueuePending(t,e){let n=this.pendingMessages.get(t);for(n||(n=[],this.pendingMessages.set(t,n)),n.push(e);n.length>10;)n.shift();a.info(`WebChat: queued pending message for ${t} (${n.length}/10)`)}flushPending(t){const e=this.pendingMessages.get(t);if(e&&0!==e.length){a.info(`WebChat: flushing ${e.length} pending message(s) for ${t}`),this.pendingMessages.delete(t);for(const n of e)this.sendWs(t,n,!1)}}}
@@ -6,7 +6,7 @@ gmabPath: "~/gmab" # root data directory (expanded at run
6
6
  host: "127.0.0.1" # bind address for HTTP servers
7
7
  logLevel: "info" # debug | info | warn | error
8
8
  verboseDebugLogs: true # show full prompt/response text in debug logs (false = truncate to 15 chars)
9
- timezone: "" # auto-detected on first boot (e.g. Europe/Rome, America/New_York)
9
+ timezone: "Europe/Rome" # auto-detected on first boot (e.g. Europe/Rome, America/New_York)
10
10
  fastProxyUrl: "http://localhost:4181" # Anthropic Tool Name proxy URL (atn-proxy)
11
11
 
12
12
  channels:
@@ -23,20 +23,20 @@ channels:
23
23
  allowFrom: [] # telegram user IDs allowed (for allowlist policy)
24
24
 
25
25
  # ─── Advanced Features (optional) ───────────────────────────────────
26
- # name: "Main Bot" # optional display name for multi-account setups
26
+ name: "Main Bot" # optional display name for multi-account setups
27
27
 
28
- # reactionLevel: "ack" # agent reaction capability (default: "ack")
28
+ reactionLevel: "minimal" # agent reaction capability (default: "ack")
29
29
  # off - no reactions
30
30
  # ack - bot shows eye while processing
31
31
  # minimal - agent can react sparingly (~1 per 5-10 messages)
32
32
  # extensive - agent can react liberally when appropriate
33
33
 
34
- # reactionNotifications: "off" # user reaction notifications (default: "off")
34
+ reactionNotifications: "own" # user reaction notifications (default: "off")
35
35
  # off - ignore all reactions
36
36
  # own - notify when users react to bot messages
37
37
  # all - notify agent of all reactions
38
38
 
39
- # inlineButtonsScope: "allowlist" # where inline buttons are allowed (default: "allowlist")
39
+ inlineButtonsScope: "all" # where inline buttons are allowed (default: "allowlist")
40
40
  # off - buttons disabled everywhere
41
41
  # dm - buttons only in direct messages
42
42
  # group - buttons only in groups
@@ -49,20 +49,20 @@ channels:
49
49
  # partial - stream text before tool calls
50
50
  # block - stream block-by-block
51
51
 
52
- # linkPreview: true # show link previews in messages (default: true)
52
+ linkPreview: true # show link previews in messages (default: true)
53
53
 
54
- # actions: # enable/disable specific actions (default: all true except sticker)
55
- # reactions: true # allow message reactions
56
- # sendMessage: true # allow sending messages
57
- # editMessage: true # allow editing messages
58
- # deleteMessage: true # allow deleting messages
59
- # sticker: false # enable sticker cache & search (default: false)
60
- # createForumTopic: false # allow creating forum topics (default: false)
54
+ actions: # enable/disable specific actions (default: all true except sticker)
55
+ reactions: true # allow message reactions
56
+ sendMessage: true # allow sending messages
57
+ editMessage: true # allow editing messages
58
+ deleteMessage: true # allow deleting messages
59
+ sticker: false # enable sticker cache & search (default: false)
60
+ createForumTopic: false # allow creating forum topics (default: false)
61
61
 
62
- # retry: # retry policy for API calls (default: 3 attempts, 1s base delay)
63
- # maxAttempts: 3
64
- # baseDelayMs: 1000
65
- # maxDelayMs: 30000
62
+ retry: # retry policy for API calls (default: 3 attempts, 1s base delay)
63
+ maxAttempts: 3
64
+ baseDelayMs: 1000
65
+ maxDelayMs: 30000
66
66
 
67
67
  # timeoutSeconds: 60 # API timeout (optional)
68
68
  # proxy: "socks5://..." # SOCKS5 proxy for Telegram API (optional)
@@ -126,7 +126,7 @@ channels:
126
126
 
127
127
  responses:
128
128
  enabled: true
129
- port: 3004
129
+ port: 3002
130
130
 
131
131
  models: # model registry
132
132
  # ─── Internal models (Anthropic, via SDK) ──────────────────────
@@ -155,9 +155,9 @@ models: # model registry
155
155
  - id: embeddinggemma
156
156
  name: Embedding Gemma
157
157
  types: [external]
158
- apiKey: "${EMBEDDING_GEMMA}"
159
- baseURL: http://localhost:11434/api # local Ollama endpoint
160
- useEnvVar: EMBEDDING_GEMMA
158
+ apiKey: "sk-12345" # dummy key (Ollama doesn't require auth, but the field is required)
159
+ baseURL: http://host.docker.internal:11434/api # Ollama endpoint (from inside Docker)
160
+ useEnvVar: ""
161
161
  contextWindow: 8192
162
162
  costInput: 0
163
163
  costOutput: 0
@@ -281,13 +281,13 @@ memory:
281
281
  search:
282
282
  enabled: true # enable hybrid BM25 + semantic search over memory
283
283
  modelRef: "Embedding Gemma" # references a model in the models registry (for API key + base URL)
284
- embeddingModel: "text-embedding-3-small"
285
- prefixQuery: "" # prefix template for query embeddings (use {content} placeholder)
286
- prefixDocument: "" # prefix template for document embeddings (use {content} placeholder)
287
- embeddingDimensions: 1536 # 512, 768, or 1536 (native) — lower = faster, higher = more precise
284
+ embeddingModel: "embeddinggemma" # Ollama model (run: ollama pull gemma)
285
+ prefixQuery: "task: search result | query: {content}" # prefix template for query embeddings (use {content} placeholder)
286
+ prefixDocument: "title: none | text: {content}" # prefix template for document embeddings (use {content} placeholder)
287
+ embeddingDimensions: 768 # 512, 768, or 1536 (native) — lower = faster, higher = more precise
288
288
  updateDebounceMs: 3000 # debounce for fs.watch before re-indexing
289
289
  embedIntervalMs: 300000 # interval (ms) between embedding cycles (5 min)
290
- maxResults: 6 # default max results returned by memory_search
290
+ maxResults: 10 # default max results returned by memory_search
291
291
  maxSnippetChars: 700 # max chars per snippet in results
292
292
  maxInjectedChars: 4000 # max total chars injected into context from search results
293
293
  rrfK: 60 # RRF fusion constant (higher = smoother blending)
@@ -430,6 +430,10 @@ agent:
430
430
  inflightTyping: true # keep typing indicator active when multiple messages are in-flight
431
431
  autoApproveTools: true # auto-approve SDK tool permissions; when false, asks user via channel buttons
432
432
  autoRenew: 4 # auto-renew session after N consecutive errors (0 = disabled)
433
+ apiRetry: # retry policy for transient API errors (500/502/503/529/overloaded)
434
+ maxAttempts: 5 # total attempts before giving up (exponential backoff)
435
+ baseDelayMs: 2000 # initial delay between retries (doubles each attempt)
436
+ maxDelayMs: 30000 # maximum delay cap
433
437
 
434
438
  cron:
435
439
  enabled: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hera-al/server",
3
- "version": "1.6.14",
3
+ "version": "1.6.18",
4
4
  "private": false,
5
5
  "description": "Hera Artificial Life — Multi-channel AI agent gateway with autonomous capabilities",
6
6
  "license": "MIT",