@aliwey/bmo 2.0.8 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/bmo.js +12 -0
- package/config/settings.py +1 -0
- package/core/__pycache__/bot_client.cpython-313.pyc +0 -0
- package/core/__pycache__/request_worker.cpython-313.pyc +0 -0
- package/core/bot_client.py +13 -9
- package/memory.md +14 -29
- package/package.json +1 -1
- package/webchat/server.js +10 -6
package/bin/bmo.js
CHANGED
|
@@ -209,6 +209,18 @@ Data lives in: ${BMO_HOME_DISPLAY}
|
|
|
209
209
|
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
// Copy generic memory.md template if it does not exist
|
|
213
|
+
const packageMemoryPath = path.join(PKG_DIR, 'memory.md');
|
|
214
|
+
const userMemoryPath = path.join(BMO_HOME, 'data', 'memory.md');
|
|
215
|
+
if (fs.existsSync(packageMemoryPath) && !fs.existsSync(userMemoryPath)) {
|
|
216
|
+
try {
|
|
217
|
+
fs.mkdirSync(path.dirname(userMemoryPath), { recursive: true });
|
|
218
|
+
fs.copyFileSync(packageMemoryPath, userMemoryPath);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.error(`Warning: Failed to copy memory.md template: ${e.message}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
212
224
|
// Launch BMO Python CLI — exactly like running `python cli.py`
|
|
213
225
|
const python = getPython();
|
|
214
226
|
const cliScript = path.join(PKG_DIR, 'cli.py');
|
package/config/settings.py
CHANGED
|
@@ -78,6 +78,7 @@ SESSIONS_FILE = DATA_DIR / "sessions.jsonl"
|
|
|
78
78
|
USER_MEMORY_FILE = DATA_DIR / "user_memory.json"
|
|
79
79
|
ACTIVE_SESSIONS_FILE = DATA_DIR / "active_sessions.json"
|
|
80
80
|
DATABASE_FILE = DATA_DIR / "bot.db"
|
|
81
|
+
MEMORY_FILE = DATA_DIR / "memory.md"
|
|
81
82
|
|
|
82
83
|
MAX_HISTORY_PER_CHAT = 500
|
|
83
84
|
MAX_CONTEXT_LENGTH = 32000
|
|
Binary file
|
|
Binary file
|
package/core/bot_client.py
CHANGED
|
@@ -14,7 +14,7 @@ from typing import Optional, Callable
|
|
|
14
14
|
|
|
15
15
|
from core.worker_manager import WorkerManager
|
|
16
16
|
|
|
17
|
-
from config.settings import OPENCODE_BASE_URL, OPENCODE_TIMEOUT, OPENCODE_POLL_INTERVAL, OPENCODE_POLL_TIMEOUT
|
|
17
|
+
from config.settings import OPENCODE_BASE_URL, OPENCODE_TIMEOUT, OPENCODE_POLL_INTERVAL, OPENCODE_POLL_TIMEOUT, MEMORY_FILE
|
|
18
18
|
|
|
19
19
|
logger = logging.getLogger(__name__)
|
|
20
20
|
|
|
@@ -46,7 +46,7 @@ class OpenCodeBotClient:
|
|
|
46
46
|
self._memory_cache_time: float = 0
|
|
47
47
|
self._agents_cache: Optional[list] = None
|
|
48
48
|
self._agents_cache_time: float = 0
|
|
49
|
-
self._memory_path =
|
|
49
|
+
self._memory_path = str(MEMORY_FILE)
|
|
50
50
|
self._worker: Optional[WorkerManager] = None
|
|
51
51
|
|
|
52
52
|
def set_worker_manager(self, worker: WorkerManager):
|
|
@@ -76,19 +76,23 @@ class OpenCodeBotClient:
|
|
|
76
76
|
self.is_connected = False
|
|
77
77
|
return self.is_connected
|
|
78
78
|
|
|
79
|
-
async def create_session(self, provider_id: Optional[str] = None, model_id: Optional[str] = None, env: Optional[dict] = None) -> Optional[str]:
|
|
79
|
+
async def create_session(self, provider_id: Optional[str] = None, model_id: Optional[str] = None, env: Optional[dict] = None, agent: str = "build") -> Optional[str]:
|
|
80
80
|
"""Create a new opencode session and return its ID."""
|
|
81
81
|
try:
|
|
82
82
|
# Force defaults if None/Empty
|
|
83
83
|
p_id = provider_id if (provider_id and str(provider_id) != "None") else "opencode"
|
|
84
84
|
m_id = model_id if (model_id and str(model_id) != "None") else "big-pickle"
|
|
85
85
|
|
|
86
|
-
#
|
|
86
|
+
# Map 'default' to 'build' to prevent server-side agent switching errors
|
|
87
|
+
agent_to_use = "build" if (not agent or agent == "default") else agent
|
|
88
|
+
|
|
89
|
+
# The server expects a nested 'model' object and optional 'agent'
|
|
87
90
|
payload = {
|
|
88
91
|
"model": {
|
|
89
92
|
"id": m_id,
|
|
90
93
|
"providerID": p_id
|
|
91
|
-
}
|
|
94
|
+
},
|
|
95
|
+
"agent": agent_to_use
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
if env:
|
|
@@ -104,7 +108,7 @@ class OpenCodeBotClient:
|
|
|
104
108
|
session_id = data.get("id")
|
|
105
109
|
|
|
106
110
|
actual_model = data.get("model", {}).get("id", "Unknown")
|
|
107
|
-
logger.info("--- OPENCODE SESSION CREATED: %s (Model: %s) ---", session_id, actual_model)
|
|
111
|
+
logger.info("--- OPENCODE SESSION CREATED: %s (Model: %s, Agent: %s) ---", session_id, actual_model, agent_to_use)
|
|
108
112
|
return session_id
|
|
109
113
|
except Exception as e:
|
|
110
114
|
logger.error("Failed to create session: %s", e)
|
|
@@ -282,7 +286,7 @@ class OpenCodeBotClient:
|
|
|
282
286
|
|
|
283
287
|
if not session_id:
|
|
284
288
|
# Inject keys if provided
|
|
285
|
-
session_id = await self.create_session(provider_id, model_id, env=provider_env)
|
|
289
|
+
session_id = await self.create_session(provider_id, model_id, env=provider_env, agent=active_agent)
|
|
286
290
|
if not session_id:
|
|
287
291
|
return "Error: Could not create OpenCode session."
|
|
288
292
|
|
|
@@ -411,7 +415,7 @@ class OpenCodeBotClient:
|
|
|
411
415
|
# Automatic recovery: if session not found, clear it and try once more
|
|
412
416
|
if r is not None and r.status_code == 404:
|
|
413
417
|
logger.warning("Session %s not found on server. Creating new session...", session_id)
|
|
414
|
-
session_id = await self.create_session(provider_id, model_id, env=provider_env)
|
|
418
|
+
session_id = await self.create_session(provider_id, model_id, env=provider_env, agent=active_agent)
|
|
415
419
|
if not session_id:
|
|
416
420
|
return "Error: Session lost and could not create a new one."
|
|
417
421
|
self.last_session_id = session_id
|
|
@@ -576,7 +580,7 @@ class OpenCodeBotClient:
|
|
|
576
580
|
return "❌ Error: Could not connect to OpenCode backend. Is the server running?"
|
|
577
581
|
|
|
578
582
|
if not session_id:
|
|
579
|
-
session_id = await self.create_session(provider_id, model_id, env=provider_env)
|
|
583
|
+
session_id = await self.create_session(provider_id, model_id, env=provider_env, agent=active_agent)
|
|
580
584
|
if not session_id:
|
|
581
585
|
return "Error: Could not create OpenCode session."
|
|
582
586
|
|
package/memory.md
CHANGED
|
@@ -1,43 +1,28 @@
|
|
|
1
1
|
# BMO Core Profile
|
|
2
2
|
|
|
3
|
+
This file contains the essential identity and core preferences of the USER and BMO.
|
|
4
|
+
Detailed technical experiences are stored separately in the Knowledge Vault (`data/experience/`).
|
|
5
|
+
|
|
3
6
|
## User Profile
|
|
4
7
|
|
|
5
|
-
- Name:
|
|
6
|
-
- Role:
|
|
7
|
-
- Preference:
|
|
8
|
+
- Name: [Enter Name]
|
|
9
|
+
- Role: [Enter Role]
|
|
10
|
+
- Preference: [Enter Preference]
|
|
11
|
+
- Arch: BMO is PORTABLE — each person runs their own instance on their device.
|
|
12
|
+
The Telegram bot lives on the owner's device. The owner uses both CLI and Telegram
|
|
13
|
+
to talk to their BMO. Other ALLOWED_USER_IDS can be added to connect via Telegram.
|
|
14
|
+
Session sync is between CLI ↔ Telegram on the SAME device for the SAME user(s).
|
|
15
|
+
ALLOWED_USER_IDS + OWNER_ID form a session group — they share one global active session.
|
|
16
|
+
Users outside the group get isolated sessions.
|
|
8
17
|
|
|
9
18
|
## Learned Preferences
|
|
10
19
|
|
|
11
|
-
- Language: English
|
|
12
|
-
- Communication: Concise
|
|
13
|
-
- Telegram Format: **Markdown** (integrated with CLI markdown output) — Telegram wire format uses MarkdownV2 parse mode, converted from standard markdown at send-time
|
|
14
|
-
- Project: BMO Telegram Bot (OpenCode Ecosystem).
|
|
20
|
+
- Language: English
|
|
21
|
+
- Communication: Concise and professional.
|
|
15
22
|
|
|
16
23
|
## Knowledge Index
|
|
17
24
|
|
|
18
25
|
This index summarizes technical achievements stored in `data/experience/`. BMO should read these files to reuse past solutions.
|
|
19
26
|
|
|
20
|
-
- **bmo_rebranding.md**: Full identity migration from NOVA to BMO (Core, Handlers, Web UI).
|
|
21
|
-
- **auto_summarization.md**: Implementation of idle-triggered session summaries with database persistence.
|
|
22
|
-
- **file_archival_system.md**: Session-linked file detection, copy, and indexing logic.
|
|
23
|
-
- **excel_inventory_automation.md**: Creation of multi-sheet Excel workbooks with cross-references for store management.
|
|
24
|
-
- **inline_menu_sessions.md**: Inline keyboard menu system with session history pagination and loading.
|
|
25
|
-
- **model_switching.md**: Model change with full session continuity — clears OpenCode session ID, creates fresh backend, injects SQLite history as context.
|
|
26
|
-
- **auto_title_summary.md**: Auto-summary now generates both summary + title after inactivity; new user guidance in system prompt to explain keyboard buttons.
|
|
27
|
-
- **webchat_public_default.md**: Webchat is always public — start server + cloudflared tunnel automatically. Never localhost-only unless user explicitly requests it. Full workflow: start_web_task → tunnel_webchat → wait 15s → check_task_status → send URL.
|
|
28
|
-
- **background_task_registry.md**: Full background task management system with JSON registry, MCP tools (start_web_task, list_background_tasks, check_task_status, stop_background_task, tunnel_webchat), auto-cleanup of dead processes, port conflict detection, and centralized logging. Prevents all bot freezing from servers/tunnels.
|
|
29
|
-
- **soft_new_session.md**: `/new` rewritten with Claude Code-style soft-reset — wipes model context via `delete_messages` but preserves `opencode_session_id` for warm system-prompt cache. Inherits provider/model/agent/mode from old session. Fixes 60s+ first-token latency on every `/new`.
|
|
30
|
-
- **cli_hang_timeout.md**: Fixed 20-min CLI hang — lowered `OPENCODE_TIMEOUT` 1200→120, wrapped `_do_send` POST in `asyncio.wait_for`, installed real OS-level `signal.SIGINT` handler in `cli.py` so Ctrl+C cancels the active streaming task even inside `rich.Live` + `prompt_toolkit.patch_stdout(raw=True)`.
|
|
31
|
-
- **cli_worker_kill.md**: Full worker-subprocess Ctrl+C fix — added `cancel_current()` to both `AsyncSubprocessWorker` and `MultiprocessWorker` + `WorkerManager` facade, SIGINT handler now kills the subprocess via `asyncio.run_coroutine_threadsafe(worker.cancel_current(), loop)` BEFORE cancelling the asyncio task (shielded futures would otherwise block propagation). `ensure_worker` checks `is_alive` so cancelled workers respawn on next request. Timeouts set to 900s (15 min) for reasoning-heavy tasks.
|
|
32
|
-
- **name_correction.md**: Fixed stale `Aliwi` → `Aliwey` in both profile files (`memory.md` + `data/memory.md`); reminder that the profile lives in two synced locations.
|
|
33
|
-
- **cli_exit_unclosed_transport.md**: Fixed `/exit` dumping 4 `ValueError: I/O operation on closed pipe` warnings. CLI disabled worker (`client._worker = None`) but `ensure_worker()` still spawned a stranded subprocess. Three-layer fix: skip worker start when client disables it + properly close pipe transports in `shutdown()` + post-loop cleanup in `cli.py`.
|
|
34
|
-
- **telegram_markdown_integration.md**: User preference flip — Telegram now uses Markdown (MarkdownV2 on the wire) integrated with CLI markdown output. Old HTML parser path must be replaced by a `to_markdown_v2()` converter + chunker. `_clean_telegram_html` is obsolete.
|
|
35
|
-
- **portable_distribution.md**: Made BMO distributable — each user runs their own instance. Configurable `OWNER_ID`, generic memory.md templates, path-agnostic build script, setup wizard with owner ID prompt, stripped all personal/dev artifacts, port 4800 default.
|
|
36
|
-
- **cli_spinner_inplace_update.md**: Fixed thinking-counter leaving a vertical trail of past values on screen. `rich.Console.print(..., end="")` does NOT honor `\r` — switched the tick update to `sys.stdout.write + flush` with manual ANSI dim codes, and clear the partial line on completion.
|
|
37
|
-
- **bfp_protocol.md**: BFP (BMO Friendship Protocol) — DID-based identity, A2A-compatible Agent Cards, WebSocket transport, relay discovery, and enterprise A2A bridge. BMO is the reference implementation.
|
|
38
|
-
- **bfp_integration_fix.md**: Fixed 3 critical bugs from parallel subagent isolation: A2A bridge sync/async mismatch calling `send_message()` with wrong args, `bfp.delegate` returning fake stubs. Extracted shared `core/bfp_tasks.py` module used by both transport and A2A bridge. 13 passing tests.
|
|
39
|
-
- **bfp_user_facing.md**: Discovery module (`bfp_discovery.py`), relay WS connector (`bfp_connector.py`), relay WS DID mapping + peer forwarding, CLI `/bfp status/find/delegate/talk` commands, `BFP_RELAY_URL` env var. Relay WS forwarding architecture: `send` action creates pending future, forwards to target's WS, awaits `relay_response`, returns result. All 20 BFP tests pass.
|
|
40
|
-
- **bfp_websocket_http_graceful.md**: Fixed `InvalidUpgrade: Keep-Alive` traceback when HTTP requests hit the BFP WebSocket port 8765. Added `_process_request` callback to `ws_serve()` that returns a 426 "Upgrade Required" response for non-WebSocket connections instead of letting the library crash.
|
|
41
|
-
|
|
42
27
|
---
|
|
43
28
|
*BMO: Always update this index after adding a new file to the vault.*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliwey/bmo",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "BMO — AI coding assistant with Telegram, CLI & Web sync. One command, all frontends.",
|
|
5
5
|
"keywords": ["ai", "coding-assistant", "telegram-bot", "cli", "opencode", "bfp"],
|
|
6
6
|
"homepage": "https://github.com/aliwey/bmo",
|
package/webchat/server.js
CHANGED
|
@@ -327,7 +327,10 @@ app.post('/api/switch-model', express.json(), async (req, res) => {
|
|
|
327
327
|
const parts = (model || 'big-pickle').split('/');
|
|
328
328
|
const bareModelId = parts.pop();
|
|
329
329
|
const providerId = provider || parts.pop() || 'opencode';
|
|
330
|
-
const payload = {
|
|
330
|
+
const payload = {
|
|
331
|
+
model: { id: bareModelId, providerID: providerId },
|
|
332
|
+
agent: 'build'
|
|
333
|
+
};
|
|
331
334
|
const r = await fetch(`${OPENCODE_URL}/session`, {
|
|
332
335
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
|
|
333
336
|
});
|
|
@@ -353,9 +356,10 @@ function generateTitle(firstMessage) {
|
|
|
353
356
|
return words.slice(0, 6).join(' ') + '…';
|
|
354
357
|
}
|
|
355
358
|
|
|
356
|
-
async function createOpenCodeSession(modelId = 'qwen3.6-plus-free', providerId = 'opencode') {
|
|
359
|
+
async function createOpenCodeSession(modelId = 'qwen3.6-plus-free', providerId = 'opencode', agent = 'build') {
|
|
357
360
|
const payload = {
|
|
358
|
-
model: { id: modelId, providerID: providerId }
|
|
361
|
+
model: { id: modelId, providerID: providerId },
|
|
362
|
+
agent: agent === 'default' ? 'build' : agent
|
|
359
363
|
};
|
|
360
364
|
const controller = new AbortController();
|
|
361
365
|
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
@@ -371,7 +375,7 @@ async function createOpenCodeSession(modelId = 'qwen3.6-plus-free', providerId =
|
|
|
371
375
|
}
|
|
372
376
|
const data = await r.json();
|
|
373
377
|
const sessionId = data.id;
|
|
374
|
-
console.log('[OpenCode] Created session:', sessionId, 'model:', modelId);
|
|
378
|
+
console.log('[OpenCode] Created session:', sessionId, 'model:', modelId, 'agent:', payload.agent);
|
|
375
379
|
return sessionId;
|
|
376
380
|
} catch (e) {
|
|
377
381
|
clearTimeout(timeout);
|
|
@@ -412,14 +416,14 @@ async function callOpenCode(text, sessionId, mode = 'execute', agent = 'default'
|
|
|
412
416
|
|
|
413
417
|
if (r && r.status === 404) {
|
|
414
418
|
console.log('[OpenCode] Session not found, creating new one...');
|
|
415
|
-
sessionId = await createOpenCodeSession(modelId || 'qwen3.6-plus-free', providerId || 'opencode');
|
|
419
|
+
sessionId = await createOpenCodeSession(modelId || 'qwen3.6-plus-free', providerId || 'opencode', agent);
|
|
416
420
|
r = await doSend(sessionId);
|
|
417
421
|
}
|
|
418
422
|
|
|
419
423
|
// 500 with a recoverable model → create new session and retry
|
|
420
424
|
if (r && r.status === 500 && modelId) {
|
|
421
425
|
console.log('[OpenCode] Session 500, creating new session with model:', modelId);
|
|
422
|
-
sessionId = await createOpenCodeSession(modelId, providerId);
|
|
426
|
+
sessionId = await createOpenCodeSession(modelId, providerId, agent);
|
|
423
427
|
r = await doSend(sessionId, 30000);
|
|
424
428
|
}
|
|
425
429
|
|