metame-cli 1.5.19 → 1.5.21

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.
Files changed (40) hide show
  1. package/index.js +157 -80
  2. package/package.json +2 -2
  3. package/scripts/bin/bootstrap-worktree.sh +20 -0
  4. package/scripts/core/audit.js +190 -0
  5. package/scripts/core/handoff.js +780 -0
  6. package/scripts/core/handoff.test.js +1074 -0
  7. package/scripts/core/memory-model.js +183 -0
  8. package/scripts/core/memory-model.test.js +486 -0
  9. package/scripts/core/reactive-paths.js +44 -0
  10. package/scripts/core/reactive-paths.test.js +35 -0
  11. package/scripts/core/reactive-prompt.js +51 -0
  12. package/scripts/core/reactive-prompt.test.js +88 -0
  13. package/scripts/core/reactive-signal.js +40 -0
  14. package/scripts/core/reactive-signal.test.js +88 -0
  15. package/scripts/core/thread-chat-id.js +52 -0
  16. package/scripts/core/thread-chat-id.test.js +113 -0
  17. package/scripts/daemon-bridges.js +92 -38
  18. package/scripts/daemon-claude-engine.js +373 -444
  19. package/scripts/daemon-command-router.js +82 -8
  20. package/scripts/daemon-engine-runtime.js +7 -10
  21. package/scripts/daemon-reactive-lifecycle.js +100 -33
  22. package/scripts/daemon-session-commands.js +133 -43
  23. package/scripts/daemon-session-store.js +300 -82
  24. package/scripts/daemon-team-dispatch.js +16 -16
  25. package/scripts/daemon.js +21 -175
  26. package/scripts/deploy-manifest.js +90 -0
  27. package/scripts/docs/maintenance-manual.md +14 -11
  28. package/scripts/docs/pointer-map.md +13 -4
  29. package/scripts/feishu-adapter.js +31 -27
  30. package/scripts/hooks/intent-engine.js +6 -3
  31. package/scripts/hooks/intent-memory-recall.js +1 -0
  32. package/scripts/hooks/intent-perpetual.js +1 -1
  33. package/scripts/memory-extract.js +5 -97
  34. package/scripts/memory-gc.js +35 -90
  35. package/scripts/memory-migrate-v2.js +304 -0
  36. package/scripts/memory-nightly-reflect.js +40 -41
  37. package/scripts/memory.js +340 -859
  38. package/scripts/migrate-reactive-paths.js +122 -0
  39. package/scripts/signal-capture.js +4 -0
  40. package/scripts/sync-plugin.js +56 -0
package/scripts/daemon.js CHANGED
@@ -138,6 +138,7 @@ const {
138
138
  USAGE_RETENTION_DAYS_DEFAULT,
139
139
  normalizeUsageCategory,
140
140
  } = require('./usage-classifier');
141
+ const { createAudit } = require('./core/audit');
141
142
  const { createTaskBoard } = require('./task-board');
142
143
  const taskEnvelope = require('./daemon-task-envelope');
143
144
  const { createAdminCommandHandler } = require('./daemon-admin-commands');
@@ -196,38 +197,22 @@ function getActiveProviderEnv() {
196
197
  try { return providerMod.buildActiveEnv(); } catch { return {}; }
197
198
  }
198
199
 
199
- // ---------------------------------------------------------
200
- // LOGGING
201
- // ---------------------------------------------------------
202
- let _logMaxSize = 1048576; // cached, refreshed on config reload
203
- function refreshLogMaxSize(cfg) {
204
- _logMaxSize = (cfg && cfg.daemon && cfg.daemon.log_max_size) || 1048576;
205
- }
206
-
207
- function log(level, msg) {
208
- const ts = new Date().toISOString();
209
- const line = `[${ts}] [${level}] ${msg}\n`;
210
- try {
211
- // Rotate if over max size
212
- if (fs.existsSync(LOG_FILE)) {
213
- const stat = fs.statSync(LOG_FILE);
214
- if (stat.size > _logMaxSize) {
215
- const bakFile = LOG_FILE + '.bak';
216
- if (fs.existsSync(bakFile)) fs.unlinkSync(bakFile);
217
- fs.renameSync(LOG_FILE, bakFile);
218
- }
219
- }
220
- fs.appendFileSync(LOG_FILE, line, 'utf8');
221
- } catch {
222
- // Last resort
223
- process.stderr.write(line);
224
- }
225
- // When running as LaunchAgent (stdout redirected to file), mirror structured logs there too.
226
- // This unifies daemon.log and daemon-npm-stdout.log into one source of truth.
227
- if (!process.stdout.isTTY) {
228
- process.stdout.write(line);
229
- }
230
- }
200
+ const {
201
+ refreshLogMaxSize,
202
+ log,
203
+ loadState,
204
+ saveState,
205
+ ensureUsageShape,
206
+ ensureStateShape,
207
+ pruneDailyUsage,
208
+ } = createAudit({
209
+ fs,
210
+ logFile: LOG_FILE,
211
+ stateFile: STATE_FILE,
212
+ stdout: process.stdout,
213
+ stderr: process.stderr,
214
+ usageRetentionDaysDefault: USAGE_RETENTION_DAYS_DEFAULT,
215
+ });
231
216
 
232
217
  const {
233
218
  cpExtractTimestamp,
@@ -337,149 +322,6 @@ function restoreConfig() {
337
322
  }
338
323
  }
339
324
 
340
- let _cachedState = null;
341
-
342
- function ensureUsageShape(state) {
343
- if (!state.usage || typeof state.usage !== 'object') state.usage = {};
344
- if (!state.usage.categories || typeof state.usage.categories !== 'object') state.usage.categories = {};
345
- if (!state.usage.daily || typeof state.usage.daily !== 'object') state.usage.daily = {};
346
- const keepDays = Number(state.usage.retention_days);
347
- state.usage.retention_days = Number.isFinite(keepDays) && keepDays >= 7
348
- ? Math.floor(keepDays)
349
- : USAGE_RETENTION_DAYS_DEFAULT;
350
- }
351
-
352
- function ensureStateShape(state) {
353
- if (!state || typeof state !== 'object') return {
354
- pid: null,
355
- budget: { date: null, tokens_used: 0 },
356
- tasks: {},
357
- sessions: {},
358
- started_at: null,
359
- usage: { retention_days: USAGE_RETENTION_DAYS_DEFAULT, categories: {}, daily: {} },
360
- };
361
- if (!state.budget || typeof state.budget !== 'object') state.budget = { date: null, tokens_used: 0 };
362
- if (typeof state.budget.tokens_used !== 'number') state.budget.tokens_used = Number(state.budget.tokens_used) || 0;
363
- if (!Object.prototype.hasOwnProperty.call(state.budget, 'date')) state.budget.date = null;
364
- if (!state.tasks || typeof state.tasks !== 'object') state.tasks = {};
365
- if (!state.sessions || typeof state.sessions !== 'object') state.sessions = {};
366
- ensureUsageShape(state);
367
- return state;
368
- }
369
-
370
- function pruneDailyUsage(usage, todayIso) {
371
- const keepDays = usage.retention_days || USAGE_RETENTION_DAYS_DEFAULT;
372
- const cutoff = new Date(`${todayIso}T00:00:00.000Z`);
373
- cutoff.setUTCDate(cutoff.getUTCDate() - (keepDays - 1));
374
- const cutoffIso = cutoff.toISOString().slice(0, 10);
375
- for (const day of Object.keys(usage.daily || {})) {
376
- if (day < cutoffIso) delete usage.daily[day];
377
- }
378
- }
379
-
380
- function _readStateFromDisk() {
381
- try {
382
- const s = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
383
- return ensureStateShape(s);
384
- } catch {
385
- return ensureStateShape({
386
- pid: null,
387
- budget: { date: null, tokens_used: 0 },
388
- tasks: {},
389
- sessions: {},
390
- started_at: null,
391
- });
392
- }
393
- }
394
-
395
- function loadState() {
396
- if (!_cachedState) _cachedState = _readStateFromDisk();
397
- return _cachedState;
398
- }
399
-
400
- function saveState(state) {
401
- const next = ensureStateShape(state);
402
- if (_cachedState && _cachedState !== next) {
403
- const current = ensureStateShape(_cachedState);
404
-
405
- const currentBudgetDate = String(current.budget.date || '');
406
- const nextBudgetDate = String(next.budget.date || '');
407
- const currentBudgetTokens = Math.max(0, Math.floor(Number(current.budget.tokens_used) || 0));
408
- const nextBudgetTokens = Math.max(0, Math.floor(Number(next.budget.tokens_used) || 0));
409
- if (currentBudgetDate && (!nextBudgetDate || currentBudgetDate > nextBudgetDate)) {
410
- next.budget.date = currentBudgetDate;
411
- next.budget.tokens_used = currentBudgetTokens;
412
- } else if (currentBudgetDate && currentBudgetDate === nextBudgetDate) {
413
- next.budget.tokens_used = Math.max(currentBudgetTokens, nextBudgetTokens);
414
- }
415
-
416
- const currentKeepDays = Number(current.usage.retention_days) || USAGE_RETENTION_DAYS_DEFAULT;
417
- const nextKeepDays = Number(next.usage.retention_days) || USAGE_RETENTION_DAYS_DEFAULT;
418
- next.usage.retention_days = Math.max(currentKeepDays, nextKeepDays);
419
-
420
- for (const [category, curMeta] of Object.entries(current.usage.categories || {})) {
421
- if (!next.usage.categories[category] || typeof next.usage.categories[category] !== 'object') {
422
- next.usage.categories[category] = {};
423
- }
424
- const curTotal = Math.max(0, Math.floor(Number(curMeta && curMeta.total) || 0));
425
- const nextTotal = Math.max(0, Math.floor(Number(next.usage.categories[category].total) || 0));
426
- if (curTotal > nextTotal) next.usage.categories[category].total = curTotal;
427
-
428
- const curUpdated = String(curMeta && curMeta.updated_at || '');
429
- const nextUpdated = String(next.usage.categories[category].updated_at || '');
430
- if (curUpdated && curUpdated > nextUpdated) next.usage.categories[category].updated_at = curUpdated;
431
- }
432
-
433
- for (const [day, curDayUsageRaw] of Object.entries(current.usage.daily || {})) {
434
- const curDayUsage = (curDayUsageRaw && typeof curDayUsageRaw === 'object') ? curDayUsageRaw : {};
435
- if (!next.usage.daily[day] || typeof next.usage.daily[day] !== 'object') {
436
- next.usage.daily[day] = {};
437
- }
438
- const nextDayUsage = next.usage.daily[day];
439
- for (const [key, curValue] of Object.entries(curDayUsage)) {
440
- const curNum = Math.max(0, Math.floor(Number(curValue) || 0));
441
- const nextNum = Math.max(0, Math.floor(Number(nextDayUsage[key]) || 0));
442
- if (curNum > nextNum) nextDayUsage[key] = curNum;
443
- }
444
- const categorySum = Object.entries(nextDayUsage)
445
- .filter(([key]) => key !== 'total')
446
- .reduce((sum, [, value]) => sum + Math.max(0, Math.floor(Number(value) || 0)), 0);
447
- nextDayUsage.total = Math.max(Math.max(0, Math.floor(Number(nextDayUsage.total) || 0)), categorySum);
448
- }
449
-
450
- const currentUsageUpdated = String(current.usage.updated_at || '');
451
- const nextUsageUpdated = String(next.usage.updated_at || '');
452
- if (currentUsageUpdated && currentUsageUpdated > nextUsageUpdated) {
453
- next.usage.updated_at = currentUsageUpdated;
454
- }
455
-
456
- // Merge sessions: prevent concurrent agents from wiping each other's session data.
457
- // When a stale state object is saved (e.g. after a long spawnClaudeStreaming await),
458
- // preserve any sessions that were added/updated by other agents in the interim.
459
- if (current.sessions && typeof current.sessions === 'object') {
460
- if (!next.sessions || typeof next.sessions !== 'object') next.sessions = {};
461
- for (const [key, curSession] of Object.entries(current.sessions)) {
462
- if (!next.sessions[key]) {
463
- // Session exists in cache but not in incoming state → preserve it
464
- next.sessions[key] = curSession;
465
- } else {
466
- // Both have it → keep whichever has newer last_active
467
- const curActive = Number(curSession && curSession.last_active) || 0;
468
- const nextActive = Number(next.sessions[key] && next.sessions[key].last_active) || 0;
469
- if (curActive > nextActive) next.sessions[key] = curSession;
470
- }
471
- }
472
- }
473
- }
474
-
475
- _cachedState = next;
476
- try {
477
- fs.writeFileSync(STATE_FILE, JSON.stringify(next, null, 2), 'utf8');
478
- } catch (e) {
479
- log('ERROR', `Failed to save state: ${e.message}`);
480
- }
481
- }
482
-
483
325
  // ---------------------------------------------------------
484
326
  // PROFILE PREAMBLE (lightweight — only core fields for daemon)
485
327
  // ---------------------------------------------------------
@@ -1911,7 +1753,9 @@ const {
1911
1753
  findCodexSessionFile,
1912
1754
  clearSessionFileCache,
1913
1755
  truncateSessionToCheckpoint,
1756
+ stripThinkingSignatures,
1914
1757
  listRecentSessions,
1758
+ findAttachableSession,
1915
1759
  loadSessionTags,
1916
1760
  getSessionFileMtime,
1917
1761
  sessionLabel,
@@ -2157,6 +2001,7 @@ const { handleSessionCommand } = createSessionCommandHandler({
2157
2001
  getCachedFile,
2158
2002
  getSession,
2159
2003
  listRecentSessions,
2004
+ findAttachableSession,
2160
2005
  getSessionFileMtime,
2161
2006
  formatRelativeTime,
2162
2007
  sendDirListing,
@@ -2211,6 +2056,7 @@ const { spawnClaudeAsync, askClaude } = createClaudeEngine({
2211
2056
  getSessionName,
2212
2057
  writeSessionName,
2213
2058
  markSessionStarted,
2059
+ stripThinkingSignatures,
2214
2060
  gitCheckpoint,
2215
2061
  gitCheckpointAsync,
2216
2062
  recordTokens,
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ function collectFilesInDir(fs, path, srcDir, opts = {}) {
4
+ const excludedScripts = opts.excludedScripts || new Set();
5
+ const applyExclusions = opts.applyExclusions === true;
6
+ const files = [];
7
+ for (const entry of fs.readdirSync(srcDir)) {
8
+ const fullPath = path.join(srcDir, entry);
9
+ const stat = fs.statSync(fullPath);
10
+ if (!stat.isFile()) continue;
11
+ if (applyExclusions && excludedScripts.has(entry)) continue;
12
+ if (/\.test\.js$/.test(entry)) continue;
13
+ if (!/\.(js|yaml|sh)$/.test(entry)) continue;
14
+ files.push(entry);
15
+ }
16
+ return files;
17
+ }
18
+
19
+ function collectNestedGroups(fs, path, rootDir, destPrefix = '') {
20
+ const groups = [];
21
+ const entries = fs.readdirSync(rootDir);
22
+ const files = [];
23
+
24
+ for (const entry of entries) {
25
+ const fullPath = path.join(rootDir, entry);
26
+ const stat = fs.statSync(fullPath);
27
+ if (stat.isDirectory()) {
28
+ const childPrefix = destPrefix ? path.join(destPrefix, entry) : entry;
29
+ groups.push(...collectNestedGroups(fs, path, fullPath, childPrefix));
30
+ continue;
31
+ }
32
+ if (/\.test\.js$/.test(entry)) continue;
33
+ if (!/\.(js|yaml|sh)$/.test(entry)) continue;
34
+ files.push(entry);
35
+ }
36
+
37
+ groups.unshift({
38
+ srcDir: rootDir,
39
+ destSubdir: destPrefix,
40
+ fileList: files,
41
+ });
42
+ return groups;
43
+ }
44
+
45
+ function collectDeployGroups(fs, path, scriptsDir, opts = {}) {
46
+ const excludedScripts = opts.excludedScripts || new Set();
47
+ const includeNestedDirs = Array.isArray(opts.includeNestedDirs) ? opts.includeNestedDirs : [];
48
+
49
+ try {
50
+ fs.statSync(scriptsDir);
51
+ } catch {
52
+ return [];
53
+ }
54
+
55
+ const groups = [];
56
+ groups.push({
57
+ srcDir: scriptsDir,
58
+ destSubdir: '',
59
+ fileList: collectFilesInDir(fs, path, scriptsDir, { excludedScripts, applyExclusions: true }),
60
+ });
61
+
62
+ for (const dirName of includeNestedDirs) {
63
+ const srcDir = path.join(scriptsDir, dirName);
64
+ try {
65
+ if (!fs.existsSync(srcDir) || !fs.statSync(srcDir).isDirectory()) continue;
66
+ } catch {
67
+ continue;
68
+ }
69
+ groups.push(...collectNestedGroups(fs, path, srcDir, dirName));
70
+ }
71
+
72
+ return groups;
73
+ }
74
+
75
+ function collectSyntaxCheckFiles(path, deployGroups) {
76
+ const files = [];
77
+ for (const group of deployGroups || []) {
78
+ for (const file of group.fileList || []) {
79
+ if (!file.endsWith('.js')) continue;
80
+ files.push(path.join(group.srcDir, file));
81
+ }
82
+ }
83
+ return files;
84
+ }
85
+
86
+ module.exports = {
87
+ collectDeployGroups,
88
+ collectSyntaxCheckFiles,
89
+ collectNestedGroups,
90
+ };
@@ -43,18 +43,15 @@ feishu:
43
43
 
44
44
  ## 3. 会话与执行规则
45
45
 
46
- - 引擎 runtime 工厂:`scripts/daemon-engine-runtime.js`
47
- - 会话执行入口:`scripts/daemon-claude-engine.js`(Claude/Codex 共用)
48
- - Session 回写:`patchSessionSerialized()` 串行化,避免 thread.started 竞态覆盖
49
- - 同一个底层 session 不做自动“恢复摘要”注入;续聊直接依赖引擎原生上下文
50
- - 额外上下文只在显式链路进入时注入,例如 `/compact` 产物、`NOW.md`、memory facts / capsules、intent hints
46
+ - Runtime 工厂:`daemon-engine-runtime.js`
47
+ - 执行编排:`daemon-claude-engine.js`,streaming 纯逻辑委托 `core/handoff.js`(引擎中性),审计状态在 `core/audit.js`
48
+ - 架构纪律见 CLAUDE.md「代码架构纪律(Unix 哲学)」
51
49
 
52
50
  ### Codex 会话策略
53
51
 
54
52
  - 首轮:`codex exec --json -`
55
53
  - 续轮:`codex exec resume <thread_id> --json -`
56
54
  - `resume` 失败自动重试:同一 `chatId` 在 10 分钟内最多 1 次
57
- - 收到新 `thread_id` 时自动迁移 session id
58
55
 
59
56
  ## 4. 命令行为差异
60
57
 
@@ -152,6 +149,8 @@ feishu:
152
149
  ## 9. 双平台/双引擎维护矩阵
153
150
 
154
151
  ### 统一维护(改一处即可)
152
+ - **core/handoff.js**(引擎中性、平台中性的纯逻辑,通过参数接收平台/引擎差异)
153
+ - **core/audit.js**(纯状态管理,无平台差异)
155
154
  - agent-layer.js / daemon-agent-tools.js / daemon-agent-commands.js / daemon-user-acl.js
156
155
  - ENGINE_MODEL_CONFIG(daemon-engine-runtime.js 集中管理)
157
156
  - daemon-runtime-lifecycle.js 的语法检查和备份机制
@@ -427,9 +426,13 @@ Event 类型:`MISSION_START` / `DISPATCH` / `MEMBER_COMPLETE` / `PHASE_GATE` /
427
426
 
428
427
  ## 14. 变更后维护动作
429
428
 
430
- 1. `npm test`
431
- 2. `npm run sync:plugin`
432
- 3. 更新文档:
429
+ 1. 测试:
430
+ - `npm test`(全量)
431
+ - 改 `core/handoff.js` 时:`node --test scripts/core/handoff.test.js scripts/daemon-claude-engine.test.js`
432
+ - 改 `daemon.js` 审计相关时:`node --test scripts/daemon-audit.test.js`
433
+ 2. Lint:`npx eslint scripts/daemon*.js scripts/core/*.js`
434
+ 3. `npm run sync:plugin`
435
+ 4. 更新文档:
433
436
  - `scripts/docs/pointer-map.md`
434
- - `README.md`
435
- - `README中文版.md`
437
+ - `scripts/docs/maintenance-manual.md`
438
+ - `README.md` / `README中文版.md`
@@ -9,6 +9,7 @@
9
9
  - Daemon 主循环:`scripts/daemon.js`
10
10
  - 多引擎 runtime 适配层:`scripts/daemon-engine-runtime.js`
11
11
  - 会话执行引擎(Claude/Codex 共用入口):`scripts/daemon-claude-engine.js`
12
+ - **核心纯逻辑模块**:`scripts/core/handoff.js`(子进程生命周期)、`scripts/core/audit.js`(审计状态)
12
13
  - 管理命令:`scripts/daemon-admin-commands.js`
13
14
  - 命令路由:`scripts/daemon-command-router.js`
14
15
  - 执行命令(`/stop`、`/compact` 等):`scripts/daemon-exec-commands.js`
@@ -17,6 +18,7 @@
17
18
  - Provider/蒸馏模型配置:`scripts/providers.js`(`/provider`、`/distill-model`)
18
19
  - 跨平台基础设施:`scripts/platform.js`(`killProcessTree`、`socketPath`、`sleepSync`、`icon`)
19
20
  - 热重载安全机制:`scripts/daemon-runtime-lifecycle.js`(语法预检、last-good 备份、crash-loop 自愈)
21
+ - 打包工具:`scripts/deploy-manifest.js`(部署清单)、`scripts/sync-plugin.js`(plugin 镜像同步)
20
22
  - 维护手册:`scripts/docs/maintenance-manual.md`
21
23
 
22
24
  ## 多引擎(Claude/Codex)定位
@@ -27,8 +29,7 @@
27
29
 
28
30
  - 会话与引擎选择:
29
31
  - `scripts/daemon-claude-engine.js`
30
- - 关键点:`askClaude()` `project.engine`/session 选择 runtime;`patchSessionSerialized()` 串行回写 session
31
- - 说明:同一底层 session 续聊不再注入会话恢复摘要;额外上下文仅来自显式 compact / memory / intent 链路
32
+ - 关键点:`askClaude()` 路由+执行,streaming 纯逻辑委托 `core/handoff.js`;`patchSessionSerialized()` 串行回写避免竞态
32
33
  - Codex 规则:`exec`/`resume`、10 分钟窗口内一次自动重试、`thread_id` 迁移回写
33
34
 
34
35
  - Agent Soul 身份层(新):
@@ -36,7 +37,7 @@
36
37
  - 关键点:`ensureAgentLayer()` 创建 `~/.metame/agents/<id>/`(soul.md、memory-snapshot.md、agent.yaml);
37
38
  `createLinkOrMirror()` Windows 兼容(symlink → hardlink → copy 降级);
38
39
  `ensureClaudeMdSoulImport()` 在 CLAUDE.md 头部注入 `@SOUL.md`(Claude CLI 自动加载);
39
- Codex 引擎在每次新 session 时将 CLAUDE.md + SOUL.md 合并写入 AGENTS.md(见 daemon-claude-engine.js:957);
40
+ Codex 引擎在每次新 session 时将 CLAUDE.md + SOUL.md 合并写入 AGENTS.md
40
41
  `repairAgentLayer()` 懒迁移:老项目补建 soul 层,幂等安全
41
42
 
42
43
  - Agent 命令处理(新):
@@ -62,6 +63,14 @@
62
63
  - `scripts/daemon-admin-commands.js`
63
64
  - 关键点:`/engine` 切换默认引擎;`/doctor` 按默认引擎检查 CLI 可用性(Claude/Codex)并兼容自定义 provider 模型名
64
65
 
66
+ ## 核心模块层(scripts/core/)
67
+
68
+ 纯逻辑,无副作用,返回意图标志由调用方执行。
69
+
70
+ - `core/handoff.js`:子进程 spawn/kill、streaming 状态机、超时看门狗、结果构建。唯一消费者 `daemon-claude-engine.js`
71
+ - `core/audit.js`:审计状态。唯一消费者 `daemon.js`
72
+ - 测试:`core/handoff.test.js`、`daemon-audit.test.js`、`daemon-claude-engine.test.js`
73
+
65
74
  ## 团队 Dispatch 与跨设备通信定位
66
75
 
67
76
  - 共享 Dispatch 工具:
@@ -190,7 +199,7 @@
190
199
 
191
200
  1. 先看配置:`~/.metame/daemon.yaml` 与 `scripts/daemon-default.yaml`
192
201
  2. 再看命令入口:`scripts/daemon-admin-commands.js`、`scripts/daemon-command-router.js`、`scripts/daemon-exec-commands.js`
193
- 3. 再看执行链路:`scripts/daemon-engine-runtime.js` → `scripts/daemon-claude-engine.js` → `scripts/mentor-engine.js`
202
+ 3. 再看执行链路:`scripts/daemon-engine-runtime.js` → `scripts/daemon-claude-engine.js` → `scripts/core/handoff.js`(纯逻辑)→ `scripts/mentor-engine.js`
194
203
  4. 团队/跨设备:`scripts/daemon-team-dispatch.js` → `scripts/daemon-remote-dispatch.js` → `scripts/daemon-bridges.js`
195
204
  5. 最后看离线任务:`scripts/distill.js`、`scripts/memory-extract.js`、`scripts/memory-nightly-reflect.js`
196
205
 
@@ -112,15 +112,36 @@ function createBot(config) {
112
112
  }
113
113
  }
114
114
 
115
+ // ── Thread-aware send primitive ──────────────────────────────────────
116
+ // Detects composite "thread:chatId:threadId" IDs from daemon-bridges
117
+ // and routes to client.im.message.reply (stays inside the topic thread)
118
+ // instead of client.im.message.create.
119
+ const { parseThreadChatId } = require('./core/thread-chat-id');
120
+
121
+ async function _dispatchSend(chatId, msgType, content, timeout = 15000) {
122
+ const thread = parseThreadChatId(chatId);
123
+ let res;
124
+ if (thread) {
125
+ // Topic mode: reply inside the thread so the response stays in the topic
126
+ res = await withTimeout(client.im.message.reply({
127
+ path: { message_id: thread.threadId },
128
+ data: { msg_type: msgType, content },
129
+ }), timeout);
130
+ } else {
131
+ // Normal mode: create message in chat
132
+ res = await withTimeout(client.im.message.create({
133
+ params: { receive_id_type: 'chat_id' },
134
+ data: { receive_id: chatId, msg_type: msgType, content },
135
+ }), timeout);
136
+ }
137
+ const msgId = res?.data?.message_id;
138
+ return msgId ? { message_id: msgId } : null;
139
+ }
140
+
115
141
  // Private: send an interactive card JSON; returns { message_id } or null.
116
142
  // All card functions funnel through here to avoid repeating the SDK call.
117
143
  async function _sendInteractive(chatId, card) {
118
- const res = await withTimeout(client.im.message.create({
119
- params: { receive_id_type: 'chat_id' },
120
- data: { receive_id: chatId, msg_type: 'interactive', content: JSON.stringify(card) },
121
- }), 30000); // 30s: large card content can be slow; timeout must not fire after delivery
122
- const msgId = res?.data?.message_id;
123
- return msgId ? { message_id: msgId } : null;
144
+ return _dispatchSend(chatId, 'interactive', JSON.stringify(card), 30000);
124
145
  }
125
146
 
126
147
  let _editBroken = false; // closure var — safe against destructured calls
@@ -132,17 +153,7 @@ function createBot(config) {
132
153
  * Send a plain text message
133
154
  */
134
155
  async sendMessage(chatId, text) {
135
- const res = await withTimeout(client.im.message.create({
136
- params: { receive_id_type: 'chat_id' },
137
- data: {
138
- receive_id: chatId,
139
- msg_type: 'text',
140
- content: JSON.stringify({ text }),
141
- },
142
- }));
143
- // Return Telegram-compatible shape so daemon can edit it later
144
- const msgId = res?.data?.message_id;
145
- return msgId ? { message_id: msgId } : null;
156
+ return _dispatchSend(chatId, 'text', JSON.stringify({ text }));
146
157
  },
147
158
 
148
159
  async editMessage(chatId, messageId, text, header = null) {
@@ -345,16 +356,9 @@ function createBot(config) {
345
356
  throw new Error(`No file_key in response: ${JSON.stringify(uploadRes)}`);
346
357
  }
347
358
 
348
- // 2. Send file message
349
- const sendRes = await client.im.message.create({
350
- params: { receive_id_type: 'chat_id' },
351
- data: {
352
- receive_id: chatId,
353
- msg_type: 'file',
354
- content: JSON.stringify({ file_key: fileKey }),
355
- },
356
- });
357
- const msgId = sendRes?.data?.message_id;
359
+ // 2. Send file message (thread-aware)
360
+ const sendResult = await _dispatchSend(chatId, 'file', JSON.stringify({ file_key: fileKey }));
361
+ const msgId = sendResult?.message_id;
358
362
  if (caption) await this.sendMessage(chatId, caption);
359
363
  return msgId ? { message_id: msgId } : null;
360
364
  } catch (uploadErr) {
@@ -19,6 +19,10 @@
19
19
 
20
20
  'use strict';
21
21
 
22
+ // Global safety net: hooks must NEVER crash or exit non-zero
23
+ process.on('uncaughtException', () => process.exit(0));
24
+ process.on('unhandledRejection', () => process.exit(0));
25
+
22
26
  const fs = require('fs');
23
27
  const path = require('path');
24
28
  const os = require('os');
@@ -59,14 +63,13 @@ function run(data) {
59
63
  let intentBlock = '';
60
64
  try {
61
65
  intentBlock = buildIntentHintBlock(prompt, config, projectKey);
62
- } catch (e) {
63
- process.stderr.write(`[intent-engine] registry: ${e.message}\n`);
66
+ } catch {
64
67
  return exit();
65
68
  }
66
69
  if (!intentBlock) return exit();
67
70
 
68
71
  process.stdout.write(JSON.stringify({
69
- hookSpecificOutput: { additionalSystemPrompt: intentBlock },
72
+ hookSpecificOutput: { additionalContext: intentBlock },
70
73
  }));
71
74
  exit();
72
75
  }
@@ -31,5 +31,6 @@ module.exports = function detectMemoryRecall(prompt) {
31
31
  '- 搜索记忆: `node ~/.metame/memory-search.js "关键词1" "keyword2"`',
32
32
  '- 一次传 3-4 个关键词(中文+英文+函数名)',
33
33
  '- `--facts` 只搜事实,`--sessions` 只搜会话',
34
+ '- 统一召回: `require("./memory").assembleContext({ query, scope: { project, agent } })`',
34
35
  ].join('\n');
35
36
  };
@@ -70,7 +70,7 @@ const PERPETUAL_INTENTS = [
70
70
  {
71
71
  // User asks about event log or progress history
72
72
  pattern: /(事件|event).{0,5}(日志|log)|progress\.tsv|进度日志/i,
73
- hint: () => '[永续任务] 事件日志: `tail ~/.metame/events/<project>.jsonl`\n进度表: `cat workspace/progress.tsv`',
73
+ hint: () => '[永续任务] 事件日志: `tail ~/.metame/reactive/<project>/events.jsonl`\n进度表: `cat workspace/progress.tsv`',
74
74
  },
75
75
  ];
76
76