metame-cli 1.4.18 → 1.4.20

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
@@ -14,25 +14,31 @@
14
14
  <a href="./README.md">English</a> | <a href="./README中文版.md">中文</a>
15
15
  </p>
16
16
 
17
- > **Claude Code that knows you — and works from your phone.**
17
+ > **Your digital twin. Lives on your Mac.**
18
18
 
19
- MetaMe turns Claude Code into a persistent AI that remembers how you think, runs on your Mac 24/7, and takes commands from your phone via Telegram or Feishu.
19
+ MetaMe is an AI that lives on your machine remembers how you think, stays online 24/7, and takes commands from your phone via Telegram or Feishu. Not in the cloud. In your computer.
20
20
 
21
- One command. No cloud. Your machine, your data.
21
+ No cloud. Your machine, your data.
22
22
 
23
+ **macOS / Linux / Windows WSL — same command:**
24
+ ```bash
25
+ curl -fsSL https://raw.githubusercontent.com/Yaron9/MetaMe/main/install.sh | bash
26
+ ```
27
+
28
+ **Already have Node.js ≥ 18:**
23
29
  ```bash
24
30
  npm install -g metame-cli && metame
25
31
  ```
26
32
 
27
33
  ---
28
34
 
29
- > ### 🚀 v1.4.0Layered Memory Architecture
35
+ > ### 🚀 v1.4.19Multi-User ACL + Session Context Preview
30
36
  >
31
- > MetaMe now has a **three-layer memory system** that works completely in the background:
32
- > - **Long-term facts** extracted from every session, recalled semantically on demand
33
- > - **Session summary cache** when you resume after a 2h+ gap, MetaMe injects what you were last working on
34
- > - **Automatic session tagging** every conversation is indexed by topic, enabling future session routing
35
- > - **Unix Socket IPC** dispatch latency dropped from ~60s to <100ms
37
+ > - **Multi-user permission system**: role-based ACL (admin / member / stranger) share your bots with teammates without giving them full access. Manage users with `/user` commands.
38
+ > - **Session context preview**: `/resume` and `/sessions` now show the last message snippet so you know exactly what to pick up.
39
+ > - **Team Task protocol**: multi-agent task board for cross-agent collaboration. Agents can dispatch and track tasks across workspaces.
40
+ > - **Layered Memory Architecture**: three-layer memory (long-term facts, session summaries, session index) all automatic.
41
+ > - **Unix Socket IPC**: dispatch latency <100ms.
36
42
  >
37
43
  > Zero configuration. It just works.
38
44
 
@@ -151,7 +157,21 @@ Chain skills into multi-step workflows — research → write → publish — fu
151
157
  prompt: "Publish it"
152
158
  ```
153
159
 
154
- Task options: `interval` (every N seconds/minutes/hours/days), `at` (fixed local `HH:MM`), `days` (optional day filter), `require_idle` (defer when you're active, retry on next heartbeat tick), `precondition` (shell guard — skip if false, zero tokens), `notify` (push result to phone), `model`, `cwd`, `allowedTools`, `timeout`.
160
+ **Task options:**
161
+
162
+ | Option | Description |
163
+ |--------|-------------|
164
+ | `at` | Fixed-time trigger, e.g. `"09:30"` (local time) |
165
+ | `days` | Day filter, e.g. `"weekdays"`, `[mon, wed, fri]` |
166
+ | `interval` | Interval trigger, e.g. `"4h"`, `"30m"` |
167
+ | `require_idle` | Skip if you're active; retry on next heartbeat tick |
168
+ | `precondition` | Shell guard — skip task if command returns non-zero (zero tokens consumed) |
169
+ | `notify` | Push result to phone when done |
170
+ | `model` | Override model, e.g. `"sonnet"`, `"haiku"` |
171
+ | `cwd` | Working directory for the task |
172
+ | `timeout` | Max run time |
173
+
174
+ > **Scheduled tasks require system registration.** Run `metame daemon install-launchd` and tasks fire on schedule even with the screen locked or the lid closed — as long as the Mac is on.
155
175
 
156
176
  ### 5. Skills That Evolve Themselves
157
177
 
@@ -173,6 +193,13 @@ Task fails → skill-scout finds a skill → installs → retries → succeeds
173
193
 
174
194
  ## Quick Start
175
195
 
196
+ **macOS / Linux / Windows WSL:**
197
+ ```bash
198
+ curl -fsSL https://raw.githubusercontent.com/Yaron9/MetaMe/main/install.sh | bash
199
+ ```
200
+ > Same command everywhere. The script detects your OS and uses Homebrew (macOS) or apt/dnf/pacman (Linux/WSL) to install Node.js automatically.
201
+
202
+ **Already have Node.js ≥ 18:**
176
203
  ```bash
177
204
  npm install -g metame-cli && metame
178
205
  ```
@@ -184,15 +211,43 @@ npm install -g metame-cli && metame
184
211
  | 1. Install & profile | `metame` | First run: cognitive interview → builds `~/.claude_profile.yaml` |
185
212
  | 2. Connect phone | Follow the setup wizard | Bot token + app credentials → `~/.metame/daemon.yaml` |
186
213
  | 3. Start daemon | `metame start` | Background daemon launches, bot goes online |
187
- | 4. Auto-start | `metame daemon install-launchd` | Survives reboot + crash recovery |
214
+ | 4. Register with system | macOS: `metame daemon install-launchd` · WSL/Linux: see below | Always-on, crash recovery |
215
+
216
+ > **What does system registration mean?**
217
+ > Once registered, MetaMe runs in the background automatically — screen locked, lid closed, woken from sleep — as long as the machine is on. Scheduled tasks fire on time. No terminal window needed.
218
+
219
+ **WSL2 / Linux — register with systemd:**
220
+
221
+ ```bash
222
+ cat > ~/.config/systemd/user/metame.service << 'EOF'
223
+ [Unit]
224
+ Description=MetaMe Daemon
225
+ After=network.target
226
+
227
+ [Service]
228
+ ExecStart=/usr/bin/env metame start
229
+ Restart=on-failure
230
+ RestartSec=5
231
+
232
+ [Install]
233
+ WantedBy=default.target
234
+ EOF
235
+
236
+ systemctl --user enable metame
237
+ systemctl --user start metame
238
+ ```
239
+
240
+ > WSL2 requires systemd enabled first: add `[boot]\nsystemd=true` to `/etc/wsl.conf`, then restart WSL.
241
+
242
+ > **WSL limitation:** `/mac` commands (macOS AppleScript/JXA automation) are not available.
188
243
 
189
244
  **Create your first Agent:**
190
245
 
191
- 1. Create a group chat in Telegram/Feishu, add your bot
192
- 2. Send `/agent bind <name>` in the group (e.g. `/agent bind personal`)
193
- 3. Pick a working directory from the buttons, or type a path directly — non-existent directories are created automatically → done
246
+ 1. In any existing group, say: `Create an agent, directory ~/xxx, responsible for xxx`
247
+ 2. Bot replies: Agent created — **send `/activate` in your new group to bind it**
248
+ 3. Create a new group, add the bot, send `/activate` binding complete
194
249
 
195
- > Want more Agents? Repeat: new group → add bot → `/agent bind <name>`. Each group = independent AI workspace.
250
+ > Want more Agents? Repeat: create in any group → new target group → `/activate`. Each group = independent AI workspace.
196
251
 
197
252
  ---
198
253
 
@@ -209,39 +264,60 @@ npm install -g metame-cli && metame
209
264
  | **Browser Automation** | Built-in Playwright MCP. Browser control out of the box for every user. |
210
265
  | **Provider Relay** | Route through any Anthropic-compatible API. Use GPT-4, DeepSeek, Gemini — zero config file mutation. |
211
266
  | **Metacognition** | Detects behavioral patterns (decision style, comfort zones, goal drift) and injects mirror observations. Zero extra API cost. |
267
+ | **Multi-User ACL** | Role-based permission system (admin / member / stranger). Share bots with teammates safely. Dynamic user management via `/user` commands with hot-reload config. |
268
+ | **Team Task** | Multi-agent task board for cross-agent collaboration. Agents can create, assign, and track tasks across workspaces. N-agent session scoping for parallel team workflows. |
212
269
  | **Emergency Tools** | `/doctor` diagnostics, `/mac` macOS control helpers, `/sh` raw shell, `/fix` config restore, `/undo` git-based rollback. |
213
270
 
214
271
  ## Defining Your Agents
215
272
 
216
- Agent configs live in `~/.metame/daemon.yaml` local only, never uploaded to npm or Git.
273
+ MetaMe's design philosophy: **one folder = one agent.**
217
274
 
218
- ### From your phone (recommended)
275
+ Give an agent a directory, drop a `CLAUDE.md` inside describing its role, and you're done. The folder is the agent — it can be a code project, a blog repo, any workspace you already have.
219
276
 
220
- The easiest way. Open any Telegram/Feishu group and use the `/agent` wizard:
277
+ ### Option 1: Just say it (recommended)
278
+
279
+ No commands needed. Tell the bot what you want in plain language. **The agent is created without binding to the current group** — send `/activate` in your new target group to complete the binding:
280
+
281
+ ```
282
+ You: Create an agent, directory ~/projects/assistant, responsible for writing and content
283
+ Bot: ✅ Agent「assistant」created
284
+ Dir: ~/projects/assistant
285
+ 📝 CLAUDE.md written
286
+
287
+ Next: send /activate in your new group to bind
288
+
289
+ ── In the new group ──
290
+
291
+ You: /activate
292
+ Bot: 🤖 assistant bound
293
+ Dir: ~/projects/assistant
294
+
295
+ You: Change this agent's role to: focused on Python backend development
296
+ Bot: ✅ Role definition updated in CLAUDE.md
297
+
298
+ You: List all agents
299
+ Bot: 📋 Agent list
300
+ 🤖 assistant ◀ current
301
+ Dir: ~/projects/assistant
302
+ ...
303
+ ```
304
+
305
+ Supported intents: create, bind (`/agent bind`), unbind, edit role, list — just say it naturally.
306
+
307
+ ### Option 2: Commands
308
+
309
+ Use `/agent` commands in any Telegram/Feishu group:
221
310
 
222
311
  | Command | What it does |
223
312
  |---------|-------------|
224
- | `/agent new` | Step-by-step wizard: pick a directory name the agent describe its role. MetaMe writes the role into `CLAUDE.md` automatically. You can also type a path directly in chat — if it doesn't exist, MetaMe creates it for you. |
225
- | `/agent bind <name> [dir]` | Quick bind: register this group as a named agent, optionally set working directory. |
313
+ | `/activate` | In a new group, sends this to auto-bind the most recently created pending agent. |
314
+ | `/agent bind <name> [dir]` | Manual bind: register this group as a named agent. Works anytime no need to recreate if agent already exists. |
226
315
  | `/agent list` | Show all configured agents. |
227
316
  | `/agent edit` | Update the current agent's role description (rewrites its `CLAUDE.md` section). |
317
+ | `/agent unbind` | Remove this group's agent binding. |
228
318
  | `/agent reset` | Remove the current agent's role section. |
229
319
 
230
- Example flow:
231
- ```
232
- You: /agent new
233
- Bot: Please select a working directory:
234
- 📁 ~/AGI 📁 ~/projects 📁 ~/Desktop
235
- You: ~/AGI/MyProject/NewDir
236
- Bot: ✅ 已新建目录:~/AGI/MyProject/NewDir
237
- What should we name this agent?
238
- You: 小美
239
- Bot: Describe 小美's role and responsibilities:
240
- You: Personal assistant. Manages my calendar, drafts messages, and tracks todos.
241
- Bot: ✅ Agent「小美」created. CLAUDE.md updated with role definition.
242
- ```
243
-
244
- You can tap a button to pick an existing directory, or type any path directly in chat. If the path doesn't exist, it's created automatically. All entry points (`/agent new` wizard and `/agent bind`) validate that the directory is real before saving.
320
+ > **Binding protection**: Each group can only be bound to one agent. Existing bindings cannot be overwritten without explicit `force:true`.
245
321
 
246
322
  ### From config file (for power users)
247
323
 
@@ -295,12 +371,22 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
295
371
  | `/undo <hash>` | Roll back to a specific git checkpoint |
296
372
  | `/list` | Browse & download project files |
297
373
  | `/model` | Switch model (sonnet/opus/haiku) |
298
- | `/agent bind <name> [dir]` | Register group as dedicated agent |
374
+ | `/activate` | Activate and bind the most recently created pending agent in a new group |
375
+ | `/agent bind <name> [dir]` | Manually register group as dedicated agent |
299
376
  | `/mac` | macOS control helper: permissions check/open + AppleScript/JXA execution |
300
377
  | `/sh <cmd>` | Raw shell — bypasses Claude |
301
378
  | `/memory` | Memory stats: fact count, session tags, DB size |
302
379
  | `/memory <keyword>` | Search long-term facts by keyword |
303
380
  | `/doctor` | Interactive diagnostics |
381
+ | `/user add <open_id>` | Add a user (admin only) |
382
+ | `/user role <open_id> <admin\|member>` | Set user role |
383
+ | `/user list` | List all configured users |
384
+ | `/user remove <open_id>` | Remove a user |
385
+ | `/sessions` | Browse recent sessions with last message preview |
386
+ | `/teamtask create <agent> <goal>` | Create a cross-agent collaboration task |
387
+ | `/teamtask` | List recent TeamTasks (last 10) |
388
+ | `/teamtask <task_id>` | View task detail |
389
+ | `/teamtask resume <task_id>` | Resume a task |
304
390
 
305
391
  ## How It Works
306
392
 
@@ -335,7 +421,7 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
335
421
  ## Security
336
422
 
337
423
  - All data stays on your machine. No cloud, no telemetry.
338
- - `allowed_chat_ids` whitelist — unauthorized users get a one-step `/agent bind` guide instead of silent rejection.
424
+ - `allowed_chat_ids` whitelist — new groups get a smart prompt: if a pending agent activation exists, they're guided to send `/activate`; otherwise they receive setup instructions.
339
425
  - `operator_ids` for shared groups — non-operators get read-only mode.
340
426
  - `~/.metame/` directory is mode 700.
341
427
  - Bot tokens stored locally, never transmitted.
@@ -346,7 +432,7 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
346
432
  |--------|-------|
347
433
  | Daemon memory (idle) | ~100 MB RSS — standard Node.js process baseline |
348
434
  | Daemon CPU (idle, between heartbeats) | ~0% — event-loop sleeping |
349
- | Cognitive profile injection | ~800 tokens/session (0.4% of 200k context) |
435
+ | Cognitive profile injection | ~600 tokens/session (0.3% of 200k context) |
350
436
  | Dispatch latency (Unix socket) | <100ms |
351
437
  | Memory consolidation (per session) | ~1,500–2,000 tokens input + ~50–300 tokens output (Haiku) |
352
438
  | Session summary (per session) | ~400–900 tokens input + ≤250 tokens output (Haiku) |
package/index.js CHANGED
@@ -30,7 +30,7 @@ if (!fs.existsSync(METAME_DIR)) {
30
30
  // Auto-deploy bundled scripts to ~/.metame/
31
31
  // IMPORTANT: daemon.yaml is USER CONFIG — never overwrite it. Only daemon-default.yaml (template) is synced.
32
32
  const scriptsDir = path.join(__dirname, 'scripts');
33
- const BUNDLED_BASE_SCRIPTS = ['signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'migrate-v2.js', 'daemon.js', 'telegram-adapter.js', 'feishu-adapter.js', 'daemon-default.yaml', 'providers.js', 'session-analytics.js', 'resolve-yaml.js', 'utils.js', 'skill-evolution.js', 'memory.js', 'memory-extract.js', 'memory-search.js', 'qmd-client.js', 'session-summarize.js', 'check-macos-control-capabilities.sh'];
33
+ const BUNDLED_BASE_SCRIPTS = ['signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'migrate-v2.js', 'daemon.js', 'telegram-adapter.js', 'feishu-adapter.js', 'daemon-default.yaml', 'providers.js', 'session-analytics.js', 'resolve-yaml.js', 'utils.js', 'skill-evolution.js', 'memory.js', 'memory-extract.js', 'memory-search.js', 'memory-gc.js', 'qmd-client.js', 'session-summarize.js', 'check-macos-control-capabilities.sh', 'usage-classifier.js', 'task-board.js', 'memory-nightly-reflect.js', 'memory-index.js'];
34
34
  const DAEMON_MODULE_SCRIPTS = (() => {
35
35
  try {
36
36
  return fs.readdirSync(scriptsDir).filter((f) => /^daemon-[\w-]+\.js$/.test(f));
@@ -78,6 +78,44 @@ if (scriptsUpdated) {
78
78
  console.log('📦 Scripts synced to ~/.metame/ — daemon will auto-restart when idle.');
79
79
  }
80
80
 
81
+ // ---------------------------------------------------------
82
+ // Deploy bundled skills to ~/.claude/skills/
83
+ // Only installs if not already present — never overwrites user customizations.
84
+ // ---------------------------------------------------------
85
+ const CLAUDE_SKILLS_DIR = path.join(HOME_DIR, '.claude', 'skills');
86
+ const bundledSkillsDir = path.join(__dirname, 'skills');
87
+ if (fs.existsSync(bundledSkillsDir)) {
88
+ try {
89
+ if (!fs.existsSync(CLAUDE_SKILLS_DIR)) {
90
+ fs.mkdirSync(CLAUDE_SKILLS_DIR, { recursive: true });
91
+ }
92
+ const skillsInstalled = [];
93
+ for (const skillName of fs.readdirSync(bundledSkillsDir)) {
94
+ const srcSkill = path.join(bundledSkillsDir, skillName);
95
+ const destSkill = path.join(CLAUDE_SKILLS_DIR, skillName);
96
+ if (!fs.statSync(srcSkill).isDirectory()) continue;
97
+ if (fs.existsSync(destSkill)) continue; // already installed, respect user's version
98
+ // Copy skill directory recursively
99
+ const copyDir = (src, dest) => {
100
+ fs.mkdirSync(dest, { recursive: true });
101
+ for (const entry of fs.readdirSync(src)) {
102
+ const s = path.join(src, entry);
103
+ const d = path.join(dest, entry);
104
+ if (fs.statSync(s).isDirectory()) copyDir(s, d);
105
+ else fs.copyFileSync(s, d);
106
+ }
107
+ };
108
+ copyDir(srcSkill, destSkill);
109
+ skillsInstalled.push(skillName);
110
+ }
111
+ if (skillsInstalled.length > 0) {
112
+ console.log(`🧠 Skills installed: ${skillsInstalled.join(', ')}`);
113
+ }
114
+ } catch {
115
+ // Non-fatal
116
+ }
117
+ }
118
+
81
119
  // Load daemon config for local launch flags
82
120
  let daemonCfg = {};
83
121
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.4.18",
3
+ "version": "1.4.20",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "scripts": {
14
14
  "test": "node --test scripts/*.test.js",
15
15
  "start": "node index.js",
16
- "sync:plugin": "cp scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-command-router.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/daemon-task-envelope.js scripts/task-board.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/usage-classifier.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-extract.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/skill-evolution.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo '✅ Plugin scripts synced'",
16
+ "sync:plugin": "cp scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-command-router.js scripts/daemon-user-acl.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/daemon-task-envelope.js scripts/task-board.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/usage-classifier.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-extract.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/skill-evolution.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo '✅ Plugin scripts synced'",
17
17
  "restart:daemon": "node index.js stop 2>/dev/null; sleep 1; node index.js start 2>/dev/null || echo '⚠️ Daemon not running or restart failed'",
18
18
  "precommit": "npm run sync:plugin && npm run restart:daemon"
19
19
  },
@@ -26,6 +26,10 @@ function createAdminCommandHandler(deps) {
26
26
  skillEvolution,
27
27
  taskBoard,
28
28
  taskEnvelope,
29
+ getActiveProcesses,
30
+ getMessageQueue,
31
+ loadState,
32
+ saveState,
29
33
  } = deps;
30
34
 
31
35
  function resolveProjectKey(targetName, projects) {
@@ -102,7 +106,13 @@ function createAdminCommandHandler(deps) {
102
106
  if (fs.existsSync(BRAIN_FILE)) {
103
107
  const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
104
108
  if (doc.identity) msg += `\nProfile: ${doc.identity.nickname || 'unknown'}`;
105
- if (doc.context && doc.context.focus) msg += `\nFocus: ${doc.context.focus}`;
109
+ const nowPath = require('path').join(require('os').homedir(), '.metame', 'memory', 'NOW.md');
110
+ try {
111
+ if (fs.existsSync(nowPath)) {
112
+ const nowContent = fs.readFileSync(nowPath, 'utf8').trim().split('\n')[0];
113
+ if (nowContent) msg += `\nNOW: ${nowContent.slice(0, 80)}`;
114
+ }
115
+ } catch { /* ignore */ }
106
116
  }
107
117
  } catch { /* ignore */ }
108
118
  await bot.sendMessage(chatId, msg);
@@ -639,6 +649,57 @@ function createAdminCommandHandler(deps) {
639
649
  return { handled: true, config };
640
650
  }
641
651
 
652
+ // /recover — kill all stuck tasks and reset message queues
653
+ if (text === '/recover') {
654
+ const activeProcesses = getActiveProcesses ? getActiveProcesses() : null;
655
+ const messageQueue = getMessageQueue ? getMessageQueue() : null;
656
+ if (!activeProcesses) {
657
+ await bot.sendMessage(chatId, '❌ 无法访问任务状态');
658
+ return { handled: true, config };
659
+ }
660
+ const stuckChatIds = [...activeProcesses.keys()];
661
+ let killed = 0;
662
+ for (const cid of stuckChatIds) {
663
+ const proc = activeProcesses.get(cid);
664
+ if (proc && proc.child) {
665
+ try { process.kill(-proc.child.pid, 'SIGTERM'); } catch { try { proc.child.kill('SIGTERM'); } catch { } }
666
+ killed++;
667
+ }
668
+ activeProcesses.delete(cid);
669
+ if (messageQueue && messageQueue.has(cid)) {
670
+ const q = messageQueue.get(cid);
671
+ if (q && q.timer) clearTimeout(q.timer);
672
+ messageQueue.delete(cid);
673
+ }
674
+ }
675
+ // Clear stale sessions (started: false = never completed first message, likely locked)
676
+ try {
677
+ const state = loadState();
678
+ let cleared = 0;
679
+ for (const [cid, sess] of Object.entries(state.sessions || {})) {
680
+ if (sess && !sess.started) {
681
+ delete state.sessions[cid];
682
+ cleared++;
683
+ }
684
+ }
685
+ if (cleared > 0) saveState(state);
686
+ } catch { /* non-critical */ }
687
+ // SIGKILL stragglers after 3s grace period
688
+ if (killed > 0) {
689
+ setTimeout(() => {
690
+ for (const cid of stuckChatIds) {
691
+ // proc references are stale but child.pid is still valid for cleanup
692
+ try { const proc = activeProcesses.get(cid); if (proc && proc.child) process.kill(-proc.child.pid, 'SIGKILL'); } catch { }
693
+ }
694
+ }, 3000);
695
+ }
696
+ const summary = killed > 0
697
+ ? `✅ 已重置 ${killed} 个卡住的任务,可重新发送消息。`
698
+ : '✅ 当前没有卡住的任务。';
699
+ await bot.sendMessage(chatId, summary);
700
+ return { handled: true, config };
701
+ }
702
+
642
703
  // /doctor — diagnostics; /fix — restore backup; /reset — reset model to sonnet
643
704
  if (text === '/fix') {
644
705
  if (restoreConfig()) {
@@ -696,15 +757,36 @@ function createAdminCommandHandler(deps) {
696
757
  const hasBak = fs.existsSync(bakFile);
697
758
  checks.push(hasBak ? '✅ 有备份' : '⚠️ 无备份');
698
759
 
760
+ // Check for stuck tasks (only flag tasks running > 10 minutes as suspicious)
761
+ const activeProcesses = getActiveProcesses ? getActiveProcesses() : null;
762
+ let hasStuck = false;
763
+ if (activeProcesses && activeProcesses.size > 0) {
764
+ const now = Date.now();
765
+ const stuckThreshold = 10 * 60 * 1000; // 10 minutes
766
+ const entries = [...activeProcesses.entries()];
767
+ const stuckEntries = entries.filter(([, proc]) => proc && proc.startedAt && (now - proc.startedAt) > stuckThreshold);
768
+ if (stuckEntries.length > 0) {
769
+ const stuckList = stuckEntries.map(([cid, proc]) => `${cid.slice(-8)}(${Math.round((now - proc.startedAt) / 60000)}min)`).join(', ');
770
+ checks.push(`⚠️ ${stuckEntries.length} 个任务疑似卡住 (${stuckList})`);
771
+ hasStuck = true;
772
+ issues++;
773
+ } else {
774
+ checks.push(`✅ ${entries.length} 个任务正常运行中`);
775
+ }
776
+ } else {
777
+ checks.push('✅ 无运行中任务');
778
+ }
779
+
699
780
  let msg = `🏥 诊断\n${checks.join('\n')}`;
700
781
  if (issues > 0) {
701
782
  if (bot.sendButtons) {
702
783
  const buttons = [];
703
- if (hasBak) buttons.push([{ text: '🔧 恢复备份', callback_data: '/fix' }]);
704
- buttons.push([{ text: '🔄 重置opus', callback_data: '/reset' }]);
784
+ if (hasStuck) buttons.push([{ text: '🔧 一键重置卡住任务', callback_data: '/recover' }]);
785
+ if (hasBak) buttons.push([{ text: '📦 恢复配置备份', callback_data: '/fix' }]);
786
+ buttons.push([{ text: '🔄 重置模型 opus', callback_data: '/reset' }]);
705
787
  await bot.sendButtons(chatId, msg, buttons);
706
788
  } else {
707
- msg += '\n/fix 恢复备份 /reset 重置opus';
789
+ msg += '\n/recover 重置卡住任务 /fix 恢复备份 /reset 重置opus';
708
790
  await bot.sendMessage(chatId, msg);
709
791
  }
710
792
  } else {