metame-cli 1.5.10 → 1.5.11
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 +49 -6
- package/index.js +202 -65
- package/package.json +6 -3
- package/scripts/daemon-admin-commands.js +34 -0
- package/scripts/daemon-bridges.js +18 -1
- package/scripts/daemon-claude-engine.js +41 -1
- package/scripts/daemon-default.yaml +3 -1
- package/scripts/daemon-ops-commands.js +25 -11
- package/scripts/daemon-reactive-lifecycle.js +355 -70
- package/scripts/daemon.js +79 -2
- package/scripts/distill.js +1 -1
- package/scripts/docs/maintenance-manual.md +55 -2
- package/scripts/docs/pointer-map.md +34 -0
- package/scripts/feishu-adapter.js +25 -0
- package/scripts/memory-extract.js +29 -1
- package/scripts/memory-nightly-reflect.js +104 -0
- package/scripts/signal-capture.js +3 -3
- package/scripts/skill-evolution.js +11 -2
package/scripts/daemon.js
CHANGED
|
@@ -48,6 +48,7 @@ const BRAIN_FILE = path.join(HOME, '.claude_profile.yaml');
|
|
|
48
48
|
const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
|
|
49
49
|
const DISPATCH_LOG = path.join(DISPATCH_DIR, 'dispatch-log.jsonl');
|
|
50
50
|
const { sleepSync, socketPath, needsSocketCleanup } = require('./platform');
|
|
51
|
+
const { handleReactiveOutput } = require('./daemon-reactive-lifecycle');
|
|
51
52
|
const SOCK_PATH = socketPath(METAME_DIR);
|
|
52
53
|
|
|
53
54
|
// Resolve claude binary path (daemon may not inherit user's full PATH)
|
|
@@ -286,12 +287,28 @@ function restoreConfig() {
|
|
|
286
287
|
if (!fs.existsSync(bak)) return false;
|
|
287
288
|
try {
|
|
288
289
|
const bakCfg = yaml.load(fs.readFileSync(bak, 'utf8')) || {};
|
|
289
|
-
// Preserve
|
|
290
|
-
//
|
|
290
|
+
// Preserve ALL user-critical fields from current config so /fix never
|
|
291
|
+
// loses secrets, chat IDs, or agent mappings
|
|
291
292
|
let curCfg = {};
|
|
292
293
|
try { curCfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {}; } catch { }
|
|
294
|
+
// Secret fields that must NEVER be reverted by a restore
|
|
295
|
+
const SECRET_FIELDS = ['app_id', 'app_secret', 'bot_token', 'operator_ids'];
|
|
293
296
|
for (const adapter of ['feishu', 'telegram']) {
|
|
294
297
|
if (curCfg[adapter] && bakCfg[adapter]) {
|
|
298
|
+
// Preserve secrets: current config always wins
|
|
299
|
+
for (const field of SECRET_FIELDS) {
|
|
300
|
+
if (curCfg[adapter][field] != null) {
|
|
301
|
+
bakCfg[adapter][field] = curCfg[adapter][field];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Preserve remote_dispatch secrets
|
|
305
|
+
if (curCfg[adapter].remote_dispatch && bakCfg[adapter].remote_dispatch) {
|
|
306
|
+
if (curCfg[adapter].remote_dispatch.secret) {
|
|
307
|
+
bakCfg[adapter].remote_dispatch.secret = curCfg[adapter].remote_dispatch.secret;
|
|
308
|
+
}
|
|
309
|
+
} else if (curCfg[adapter].remote_dispatch) {
|
|
310
|
+
bakCfg[adapter].remote_dispatch = curCfg[adapter].remote_dispatch;
|
|
311
|
+
}
|
|
295
312
|
const curIds = curCfg[adapter].allowed_chat_ids || [];
|
|
296
313
|
const bakIds = bakCfg[adapter].allowed_chat_ids || [];
|
|
297
314
|
// Union of both lists
|
|
@@ -301,8 +318,15 @@ function restoreConfig() {
|
|
|
301
318
|
bakCfg[adapter].chat_agent_map = Object.assign(
|
|
302
319
|
{}, bakCfg[adapter].chat_agent_map || {}, curCfg[adapter].chat_agent_map || {}
|
|
303
320
|
);
|
|
321
|
+
} else if (curCfg[adapter] && !bakCfg[adapter]) {
|
|
322
|
+
// Backup doesn't have this adapter at all — keep current entirely
|
|
323
|
+
bakCfg[adapter] = curCfg[adapter];
|
|
304
324
|
}
|
|
305
325
|
}
|
|
326
|
+
// Preserve projects (current takes precedence for each project key)
|
|
327
|
+
if (curCfg.projects) {
|
|
328
|
+
bakCfg.projects = Object.assign({}, bakCfg.projects || {}, curCfg.projects);
|
|
329
|
+
}
|
|
306
330
|
writeConfigSafe(bakCfg);
|
|
307
331
|
config = loadConfig(); // eslint-disable-line no-undef -- config is declared in main() closure
|
|
308
332
|
return true;
|
|
@@ -1083,6 +1107,37 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
1083
1107
|
chain: [], // reset chain for callbacks
|
|
1084
1108
|
}, config);
|
|
1085
1109
|
}
|
|
1110
|
+
|
|
1111
|
+
// ── Reactive lifecycle hook ──
|
|
1112
|
+
try {
|
|
1113
|
+
handleReactiveOutput(targetProject, outStr, loadConfig(), {
|
|
1114
|
+
log,
|
|
1115
|
+
loadState,
|
|
1116
|
+
saveState,
|
|
1117
|
+
checkBudget,
|
|
1118
|
+
handleDispatchItem: (item, cfg) => {
|
|
1119
|
+
dispatchTask(item.target, {
|
|
1120
|
+
from: item.from || '_reactive',
|
|
1121
|
+
type: 'reactive',
|
|
1122
|
+
priority: 'normal',
|
|
1123
|
+
new_session: !!item.new_session,
|
|
1124
|
+
payload: { title: 'reactive dispatch', prompt: item.prompt },
|
|
1125
|
+
}, cfg, null, null);
|
|
1126
|
+
},
|
|
1127
|
+
notifyUser: (msg) => {
|
|
1128
|
+
try {
|
|
1129
|
+
const cfg = loadConfig();
|
|
1130
|
+
if (cfg.feishu && cfg.feishu.enabled && cfg.feishu.admin_chat_id) {
|
|
1131
|
+
const { sendFeishuText } = require('./daemon-notify');
|
|
1132
|
+
sendFeishuText(cfg.feishu.admin_chat_id, msg, cfg);
|
|
1133
|
+
}
|
|
1134
|
+
} catch (e) { log('WARN', `Reactive notify failed: ${e.message}`); }
|
|
1135
|
+
},
|
|
1136
|
+
metameDir: path.join(os.homedir(), '.metame'),
|
|
1137
|
+
});
|
|
1138
|
+
} catch (e) {
|
|
1139
|
+
log('ERROR', `Reactive lifecycle error for ${targetProject}: ${e.message}`);
|
|
1140
|
+
}
|
|
1086
1141
|
};
|
|
1087
1142
|
// If streamOptions provided, use real bot so output appears in target's Feishu channel.
|
|
1088
1143
|
// Otherwise fall back to nullBot which captures output for replyFn.
|
|
@@ -1697,6 +1752,27 @@ function physiologicalHeartbeat(config) {
|
|
|
1697
1752
|
} catch (e) {
|
|
1698
1753
|
log('WARN', `Dispatch log rotation failed: ${e.message}`);
|
|
1699
1754
|
}
|
|
1755
|
+
|
|
1756
|
+
// 4. Reconcile perpetual projects — detect stale reactive loops
|
|
1757
|
+
try {
|
|
1758
|
+
const { reconcilePerpetualProjects } = require('./daemon-reactive-lifecycle');
|
|
1759
|
+
reconcilePerpetualProjects(config, {
|
|
1760
|
+
log,
|
|
1761
|
+
loadState,
|
|
1762
|
+
saveState,
|
|
1763
|
+
notifyUser: (msg) => {
|
|
1764
|
+
try {
|
|
1765
|
+
const cfg = loadConfig();
|
|
1766
|
+
if (cfg.feishu && cfg.feishu.enabled && cfg.feishu.admin_chat_id) {
|
|
1767
|
+
const { sendFeishuText } = require('./daemon-notify');
|
|
1768
|
+
sendFeishuText(cfg.feishu.admin_chat_id, msg, cfg);
|
|
1769
|
+
}
|
|
1770
|
+
} catch (e) { log('WARN', `Reconcile notify failed: ${e.message}`); }
|
|
1771
|
+
},
|
|
1772
|
+
});
|
|
1773
|
+
} catch (e) {
|
|
1774
|
+
log('WARN', `Reconcile check failed: ${e.message}`);
|
|
1775
|
+
}
|
|
1700
1776
|
}
|
|
1701
1777
|
|
|
1702
1778
|
// ── Timing constants ─────────────────────────────────────────────────────────
|
|
@@ -2315,6 +2391,7 @@ const { handleOpsCommand } = createOpsCommandHandler({
|
|
|
2315
2391
|
path,
|
|
2316
2392
|
spawn,
|
|
2317
2393
|
execSync,
|
|
2394
|
+
execFileSync,
|
|
2318
2395
|
log,
|
|
2319
2396
|
loadConfig,
|
|
2320
2397
|
loadState,
|
package/scripts/distill.js
CHANGED
|
@@ -221,7 +221,7 @@ ${promptInput}
|
|
|
221
221
|
|
|
222
222
|
fs.mkdirSync(POSTMORTEM_DIR, { recursive: true });
|
|
223
223
|
const day = new Date().toISOString().slice(0, 10);
|
|
224
|
-
const topicSlug = sanitizeSlug(skeleton.intent
|
|
224
|
+
const topicSlug = sanitizeSlug(title || skeleton.intent, `session-${String(skeleton.session_id || '').slice(0, 8)}`);
|
|
225
225
|
const filePath = path.join(POSTMORTEM_DIR, `${day}-${topicSlug}.md`);
|
|
226
226
|
const markdown = [
|
|
227
227
|
`# ${title}`,
|
|
@@ -354,7 +354,60 @@ Claude 看到 hook 注入:
|
|
|
354
354
|
- `/dispatch to windows:hunter <任务>`:手动跨设备派发
|
|
355
355
|
- `/dispatch to 猎手 <任务>`:按昵称解析,自动检测 `member.peer` 走远端
|
|
356
356
|
|
|
357
|
-
## 12.
|
|
357
|
+
## 12. 永续任务系统(Perpetual Task Engine)
|
|
358
|
+
|
|
359
|
+
### 概念
|
|
360
|
+
|
|
361
|
+
永续任务系统允许任何项目作为 reactive 永续循环运行。Agent 产出信号 → daemon 解析 → 门控检查 → 调度下一步。平台完全领域无关,科研、代码审计、文档维护等任何长期任务均可接入。
|
|
362
|
+
|
|
363
|
+
### 核心组件
|
|
364
|
+
|
|
365
|
+
| 组件 | 文件 | 职责 |
|
|
366
|
+
|------|------|------|
|
|
367
|
+
| Reactive Lifecycle | `daemon-reactive-lifecycle.js` | 信号解析、budget/depth gate、事件溯源、verifier 调用、state 生成 |
|
|
368
|
+
| Event Log | `~/.metame/events/<key>.jsonl` | 唯一 Source of Truth,daemon 独占写入 |
|
|
369
|
+
| Manifest | `<cwd>/perpetual.yaml` | 可选项目清单(completion_signal、脚本路径、约束) |
|
|
370
|
+
| Reconciliation | `reconcilePerpetualProjects()` | heartbeat 中零 token 停滞检测 |
|
|
371
|
+
| Status 命令 | `/status perpetual` | 查看所有永续项目的 phase/depth/mission/status |
|
|
372
|
+
|
|
373
|
+
### 接入一个新永续项目
|
|
374
|
+
|
|
375
|
+
1. 在 `daemon.yaml` 中注册项目,添加 `reactive: true`
|
|
376
|
+
2. 在项目目录创建 `CLAUDE.md`(定义 agent 行为)
|
|
377
|
+
3. 可选:创建 `scripts/verifier.js`(阶段门控)
|
|
378
|
+
4. 可选:创建 `perpetual.yaml`(覆盖默认约定)
|
|
379
|
+
5. 可选:创建 `scripts/archiver.js` + `scripts/mission-queue.js`(归档与任务队列)
|
|
380
|
+
|
|
381
|
+
不创建 perpetual.yaml 时,平台使用默认约定:
|
|
382
|
+
- Verifier: `scripts/verifier.js`
|
|
383
|
+
- Archiver: `scripts/archiver.js`
|
|
384
|
+
- Mission Queue: `scripts/mission-queue.js`
|
|
385
|
+
- 完成信号: `MISSION_COMPLETE`
|
|
386
|
+
|
|
387
|
+
### 事件溯源协议
|
|
388
|
+
|
|
389
|
+
所有状态变更记录在 `~/.metame/events/<projectKey>.jsonl`,一行一个 JSON 事件。`now/<key>.md` 和 `workspace/progress.tsv` 都是 event log 的投影(Projection),可随时从 event log 重建。
|
|
390
|
+
|
|
391
|
+
Event 类型:`MISSION_START` / `DISPATCH` / `MEMBER_COMPLETE` / `PHASE_GATE` / `DEPTH_LIMIT` / `BUDGET_LIMIT` / `MISSION_COMPLETE` / `ARCHIVE` / `STALE` / `INFRA_PAUSE`
|
|
392
|
+
|
|
393
|
+
### 设计契约
|
|
394
|
+
|
|
395
|
+
1. **Tolerant Reader**:`replayEventLog` 逐行解析,损坏行 WARN + skip,绝不 crash
|
|
396
|
+
2. **Error Semantic Isolation**:verifier L2b 区分 404(幻觉,打回 agent)和 50x(基建故障,挂起项目通知人类)
|
|
397
|
+
3. **State 由 daemon 生成**:agent 只读 `now/<key>.md`,不负责维护
|
|
398
|
+
|
|
399
|
+
### 故障排查
|
|
400
|
+
|
|
401
|
+
| 症状 | 检查 |
|
|
402
|
+
|------|------|
|
|
403
|
+
| 永续项目不启动 | `daemon.yaml` 中是否有 `reactive: true`? |
|
|
404
|
+
| 完成信号不触发 | 检查 `perpetual.yaml` 中的 `completion_signal` 是否与 CLAUDE.md 一致 |
|
|
405
|
+
| Verifier 不运行 | 检查 `scripts/verifier.js` 路径(或 manifest 中的自定义路径)是否存在 |
|
|
406
|
+
| 项目挂起 (infra_failure) | 外部 API 不可用,检查网络。非 agent 错误。 |
|
|
407
|
+
| Event log 损坏 | 重启后 replay 会跳过损坏行。`progress.tsv` 和 `now/<key>.md` 可从 event log 重建 |
|
|
408
|
+
| `/status perpetual` 无输出 | 确认项目配置了 `reactive: true` |
|
|
409
|
+
|
|
410
|
+
## 13. 私人配置保护(原 §12)
|
|
358
411
|
|
|
359
412
|
- `daemon.yaml` 是用户私人配置,包含 API keys、chat IDs、个人项目配置
|
|
360
413
|
- **绝不上传**到代码仓库,已加入 `.gitignore`
|
|
@@ -363,7 +416,7 @@ Claude 看到 hook 注入:
|
|
|
363
416
|
- 同样不应上传的文件:`MEMORY.md`、`SOUL.md`、`.env*`
|
|
364
417
|
- Agent 在执行任务时,**绝不能** `cp scripts/daemon.yaml ~/.metame/daemon.yaml`,这会覆盖用户私人配置
|
|
365
418
|
|
|
366
|
-
##
|
|
419
|
+
## 14. 变更后维护动作
|
|
367
420
|
|
|
368
421
|
1. `npm test`
|
|
369
422
|
2. `npm run sync:plugin`
|
|
@@ -130,6 +130,39 @@
|
|
|
130
130
|
- `scripts/memory.js`:`saveFactLabels()` 原子写入 API
|
|
131
131
|
- `scripts/memory-nightly-reflect.js`:`synthesized_insight` 回写、知识胶囊聚合与 `knowledge_capsule` 回写
|
|
132
132
|
|
|
133
|
+
## 永续任务系统(Perpetual Task Engine)
|
|
134
|
+
|
|
135
|
+
- Reactive 生命周期引擎:
|
|
136
|
+
- `scripts/daemon-reactive-lifecycle.js`
|
|
137
|
+
- 关键点:`handleReactiveOutput()` 通用任务链引擎(领域无关);
|
|
138
|
+
`parseReactiveSignals()` 信号解析(NEXT_DISPATCH + 可配完成信号);
|
|
139
|
+
`reconcilePerpetualProjects()` 停滞检测(零 token,heartbeat 驱动);
|
|
140
|
+
`replayEventLog()` 事件溯源状态重放;
|
|
141
|
+
`generateStateFile()` 从 event log 生成 `now/<key>.md`(投影);
|
|
142
|
+
`appendEvent()` 追加事件到 `~/.metame/events/<key>.jsonl`(唯一 SoT);
|
|
143
|
+
`loadProjectManifest()` 读取项目 `perpetual.yaml`(convention over config);
|
|
144
|
+
`resolveProjectScripts()` 按约定/manifest 发现 verifier/archiver/mission-queue 脚本
|
|
145
|
+
|
|
146
|
+
- daemon.js 接入点:
|
|
147
|
+
- `scripts/daemon.js` `outputHandler` 内 `handleReactiveOutput` 调用(try/catch 隔离)
|
|
148
|
+
- `scripts/daemon.js` `physiologicalHeartbeat` 内 `reconcilePerpetualProjects` 调用
|
|
149
|
+
|
|
150
|
+
- 可观测命令:
|
|
151
|
+
- `scripts/daemon-admin-commands.js`
|
|
152
|
+
- 关键点:`/status perpetual`(或 `/status reactive`)显示所有永续项目状态
|
|
153
|
+
|
|
154
|
+
- 设计文档:
|
|
155
|
+
- `docs/perpetual-task-system-design.md`(v4,含 4 份附录)
|
|
156
|
+
- `docs/perpetual-task-system-plan.md`(实施计划,Phase A-C)
|
|
157
|
+
|
|
158
|
+
- 项目清单协议:
|
|
159
|
+
- `<cwd>/perpetual.yaml` — 可选,声明 completion_signal / verifier / archiver / mission_queue 路径
|
|
160
|
+
- 不存在时使用默认约定:`scripts/verifier.js`、`scripts/archiver.js`、`scripts/mission-queue.js`、信号 `MISSION_COMPLETE`
|
|
161
|
+
|
|
162
|
+
- 事件日志:
|
|
163
|
+
- `~/.metame/events/<projectKey>.jsonl` — append-only,daemon 独占写入
|
|
164
|
+
- Event 类型:MISSION_START / DISPATCH / MEMBER_COMPLETE / PHASE_GATE / DEPTH_LIMIT / BUDGET_LIMIT / MISSION_COMPLETE / ARCHIVE / STALE / INFRA_PAUSE
|
|
165
|
+
|
|
133
166
|
## 运行时数据位置
|
|
134
167
|
|
|
135
168
|
- 画像:`~/.claude_profile.yaml`
|
|
@@ -141,6 +174,7 @@
|
|
|
141
174
|
- 复盘文档:`~/.metame/memory/postmortems/`
|
|
142
175
|
- Dispatch 队列:`~/.metame/dispatch/pending.jsonl`(本地 socket 降级)
|
|
143
176
|
- 远端 Dispatch 队列:`~/.metame/dispatch/remote-pending.jsonl`(跨设备中继)
|
|
177
|
+
- **永续任务事件日志**:`~/.metame/events/<projectKey>.jsonl`(唯一 SoT,append-only)
|
|
144
178
|
- 共享进度白板:`~/.metame/memory/now/shared.md`
|
|
145
179
|
- Agent 最新产出:`~/.metame/memory/agents/{key}_latest.md`
|
|
146
180
|
- Agent 收件箱:`~/.metame/memory/inbox/{key}/`(未读),`read/`(已归档)
|
|
@@ -91,6 +91,27 @@ function createBot(config) {
|
|
|
91
91
|
appSecret: app_secret,
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Validate credentials by attempting a lightweight API call.
|
|
96
|
+
* Returns { ok: true } or { ok: false, error: string }.
|
|
97
|
+
*/
|
|
98
|
+
async function validateCredentials() {
|
|
99
|
+
try {
|
|
100
|
+
await withTimeout(client.im.chat.list({ params: { page_size: 1 } }), 15000);
|
|
101
|
+
return { ok: true };
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const msg = err && err.message || String(err);
|
|
104
|
+
const isAuthError = /invalid|unauthorized|forbidden|token|credential|app_id|app_secret|permission|99991663|99991664|99991665/i.test(msg);
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: isAuthError
|
|
108
|
+
? `Feishu credential validation failed (app_id/app_secret may be incorrect): ${msg}`
|
|
109
|
+
: `Feishu API probe failed (network or config issue): ${msg}`,
|
|
110
|
+
isAuthError,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
94
115
|
// Private: send an interactive card JSON; returns { message_id } or null.
|
|
95
116
|
// All card functions funnel through here to avoid repeating the SDK call.
|
|
96
117
|
async function _sendInteractive(chatId, card) {
|
|
@@ -106,6 +127,7 @@ function createBot(config) {
|
|
|
106
127
|
let _editBrokenAt = 0; // timestamp when broken; auto-resets after 10min
|
|
107
128
|
|
|
108
129
|
return {
|
|
130
|
+
validateCredentials,
|
|
109
131
|
/**
|
|
110
132
|
* Send a plain text message
|
|
111
133
|
*/
|
|
@@ -526,6 +548,9 @@ function createBot(config) {
|
|
|
526
548
|
reconnect() {
|
|
527
549
|
_log('INFO', 'Feishu manual reconnect triggered');
|
|
528
550
|
reconnectDelay = 5000;
|
|
551
|
+
clearTimeout(reconnectTimer);
|
|
552
|
+
try { currentWs?.stop?.(); } catch { /* ignore */ }
|
|
553
|
+
currentWs = null;
|
|
529
554
|
connect();
|
|
530
555
|
},
|
|
531
556
|
isAlive() {
|
|
@@ -74,6 +74,7 @@ const SESSION_TAGS_FILE = path.join(os.homedir(), '.metame', 'session_tags.json'
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Persist session name and derived tags to ~/.metame/session_tags.json.
|
|
77
|
+
* Consumed by /sessions and /resume commands for friendly display titles.
|
|
77
78
|
* Merges into existing file — never overwrites existing entries.
|
|
78
79
|
*/
|
|
79
80
|
function saveSessionTag(sessionId, sessionName, facts) {
|
|
@@ -337,6 +338,19 @@ async function run() {
|
|
|
337
338
|
|
|
338
339
|
sessionAnalytics.markFactsExtracted(skeleton.session_id);
|
|
339
340
|
|
|
341
|
+
// Persist session summary to memory.db sessions table (makes sessions searchable)
|
|
342
|
+
try {
|
|
343
|
+
const keywords = facts.flatMap(f => Array.isArray(f.tags) ? f.tags : [])
|
|
344
|
+
.filter((v, i, a) => a.indexOf(v) === i).slice(0, 10).join(',');
|
|
345
|
+
memory.saveSession({
|
|
346
|
+
sessionId: skeleton.session_id,
|
|
347
|
+
project: skeleton.project || 'unknown',
|
|
348
|
+
scope: skeleton.project_id || null,
|
|
349
|
+
summary: `[${session_name}] ${facts.map(f => f.value).join(' | ').slice(0, 2000)}`,
|
|
350
|
+
keywords,
|
|
351
|
+
});
|
|
352
|
+
} catch { /* non-fatal — facts already saved, session is bonus */ }
|
|
353
|
+
|
|
340
354
|
// P2-A: persist session name + tags to session_tags.json
|
|
341
355
|
saveSessionTag(skeleton.session_id, session_name, facts);
|
|
342
356
|
|
|
@@ -391,12 +405,26 @@ async function run() {
|
|
|
391
405
|
const superMsg = superseded > 0 ? `, ${superseded} superseded` : '';
|
|
392
406
|
const labelMsg = labelsSaved > 0 ? `, ${labelsSaved} labels` : '';
|
|
393
407
|
console.log(`[memory-extract] Codex ${cs.session_id.slice(0, 8)} (${session_name}): ${saved} facts saved${superMsg}${labelMsg}`);
|
|
408
|
+
|
|
409
|
+
// Persist Codex session summary to memory.db sessions table
|
|
410
|
+
try {
|
|
411
|
+
const keywords = facts.flatMap(f => Array.isArray(f.tags) ? f.tags : [])
|
|
412
|
+
.filter((v, i, a) => a.indexOf(v) === i).slice(0, 10).join(',');
|
|
413
|
+
memory.saveSession({
|
|
414
|
+
sessionId: cs.session_id,
|
|
415
|
+
project: skeleton.project || 'unknown',
|
|
416
|
+
scope: skeleton.project_id || null,
|
|
417
|
+
summary: `[${session_name}] ${facts.map(f => f.value).join(' | ').slice(0, 2000)}`,
|
|
418
|
+
keywords,
|
|
419
|
+
});
|
|
420
|
+
} catch { /* non-fatal */ }
|
|
421
|
+
|
|
422
|
+
saveSessionTag(cs.session_id, session_name, facts);
|
|
394
423
|
} else {
|
|
395
424
|
console.log(`[memory-extract] Codex ${cs.session_id.slice(0, 8)} (${session_name}): no facts extracted`);
|
|
396
425
|
}
|
|
397
426
|
|
|
398
427
|
sessionAnalytics.markCodexFactsExtracted(cs.session_id);
|
|
399
|
-
saveSessionTag(cs.session_id, session_name, facts);
|
|
400
428
|
processed++;
|
|
401
429
|
} catch (e) {
|
|
402
430
|
console.log(`[memory-extract] Codex session error: ${e.message}`);
|
|
@@ -503,6 +503,109 @@ entity_prefix: ${group.prefix}
|
|
|
503
503
|
try { if (memory && typeof memory.close === 'function') memory.close(); } catch { /* non-fatal */ }
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
// ── Conflict Resolution ──────────────────────────────────────────────
|
|
507
|
+
// Query CONFLICT facts grouped by entity+relation, ask Haiku to pick winner.
|
|
508
|
+
// Loser is marked superseded_by winner; winner restored to OK.
|
|
509
|
+
let conflictsResolved = 0;
|
|
510
|
+
try {
|
|
511
|
+
const conflictGroups = db.prepare(`
|
|
512
|
+
SELECT entity, relation, COUNT(*) as cnt
|
|
513
|
+
FROM facts
|
|
514
|
+
WHERE conflict_status = 'CONFLICT' AND superseded_by IS NULL
|
|
515
|
+
GROUP BY entity, relation
|
|
516
|
+
HAVING cnt >= 2
|
|
517
|
+
ORDER BY cnt DESC
|
|
518
|
+
LIMIT 10
|
|
519
|
+
`).all();
|
|
520
|
+
|
|
521
|
+
if (conflictGroups.length > 0) {
|
|
522
|
+
console.log(`[NIGHTLY-REFLECT] Found ${conflictGroups.length} conflict group(s) to resolve.`);
|
|
523
|
+
|
|
524
|
+
// Collect all conflicting facts for these groups (batch to reduce queries)
|
|
525
|
+
const allConflicts = [];
|
|
526
|
+
for (const g of conflictGroups) {
|
|
527
|
+
const rows = db.prepare(`
|
|
528
|
+
SELECT id, entity, relation, value, confidence, created_at
|
|
529
|
+
FROM facts
|
|
530
|
+
WHERE entity = ? AND relation = ? AND conflict_status = 'CONFLICT' AND superseded_by IS NULL
|
|
531
|
+
ORDER BY created_at DESC
|
|
532
|
+
`).all(g.entity, g.relation);
|
|
533
|
+
if (rows.length >= 2) allConflicts.push({ entity: g.entity, relation: g.relation, facts: rows });
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (allConflicts.length > 0) {
|
|
537
|
+
// Limit to 5 groups to avoid truncating serialized JSON
|
|
538
|
+
const conflictInput = allConflicts.slice(0, 5).map(g => ({
|
|
539
|
+
entity: g.entity,
|
|
540
|
+
relation: g.relation,
|
|
541
|
+
candidates: g.facts.slice(0, 5).map(f => ({ id: f.id, value: f.value.slice(0, 150), confidence: f.confidence, created_at: f.created_at })),
|
|
542
|
+
}));
|
|
543
|
+
|
|
544
|
+
const resolvePrompt = `你是知识库冲突调解员。以下是同一 entity+relation 下的冲突事实,请选出每组最准确的一条保留。
|
|
545
|
+
|
|
546
|
+
冲突组(JSON):
|
|
547
|
+
${JSON.stringify(conflictInput, null, 2)}
|
|
548
|
+
|
|
549
|
+
输出 JSON 数组,每个元素对应一组冲突的裁决:
|
|
550
|
+
[
|
|
551
|
+
{
|
|
552
|
+
"entity": "...",
|
|
553
|
+
"relation": "...",
|
|
554
|
+
"winner_id": "保留的fact id",
|
|
555
|
+
"reason": "一句话理由"
|
|
556
|
+
}
|
|
557
|
+
]
|
|
558
|
+
|
|
559
|
+
规则:
|
|
560
|
+
- 优先选最新(created_at)且 confidence=high 的
|
|
561
|
+
- 如果旧条目更准确具体,选旧的
|
|
562
|
+
- 只输出 JSON 数组`;
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
const resolveRaw = await Promise.race([
|
|
566
|
+
callHaiku(resolvePrompt, distillEnv, 60000),
|
|
567
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 65000)),
|
|
568
|
+
]);
|
|
569
|
+
const verdicts = parseJsonFromLlm(resolveRaw);
|
|
570
|
+
if (Array.isArray(verdicts)) {
|
|
571
|
+
for (const v of verdicts) {
|
|
572
|
+
if (!v || !v.winner_id || !v.entity || !v.relation) continue;
|
|
573
|
+
// Validate winner exists in our conflict set
|
|
574
|
+
const group = allConflicts.find(g => g.entity === v.entity && g.relation === v.relation);
|
|
575
|
+
if (!group) continue;
|
|
576
|
+
const winnerExists = group.facts.some(f => f.id === v.winner_id);
|
|
577
|
+
if (!winnerExists) continue;
|
|
578
|
+
|
|
579
|
+
const loserIds = group.facts.filter(f => f.id !== v.winner_id).map(f => f.id);
|
|
580
|
+
if (loserIds.length === 0) continue;
|
|
581
|
+
|
|
582
|
+
// Mark losers as superseded
|
|
583
|
+
const placeholders = loserIds.map(() => '?').join(',');
|
|
584
|
+
db.prepare(
|
|
585
|
+
`UPDATE facts SET superseded_by = ?, conflict_status = 'OK', updated_at = datetime('now')
|
|
586
|
+
WHERE id IN (${placeholders})`
|
|
587
|
+
).run(v.winner_id, ...loserIds);
|
|
588
|
+
|
|
589
|
+
// Restore winner
|
|
590
|
+
db.prepare(
|
|
591
|
+
`UPDATE facts SET conflict_status = 'OK', updated_at = datetime('now') WHERE id = ?`
|
|
592
|
+
).run(v.winner_id);
|
|
593
|
+
|
|
594
|
+
conflictsResolved += loserIds.length;
|
|
595
|
+
}
|
|
596
|
+
if (conflictsResolved > 0) {
|
|
597
|
+
console.log(`[NIGHTLY-REFLECT] Resolved ${conflictsResolved} conflicting fact(s).`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} catch (e) {
|
|
601
|
+
console.log(`[NIGHTLY-REFLECT] Conflict resolution failed (non-fatal): ${e.message}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
} catch (e) {
|
|
606
|
+
console.log(`[NIGHTLY-REFLECT] Conflict query failed (non-fatal): ${e.message}`);
|
|
607
|
+
}
|
|
608
|
+
|
|
506
609
|
// Write audit log
|
|
507
610
|
writeReflectLog({
|
|
508
611
|
status: 'success',
|
|
@@ -510,6 +613,7 @@ entity_prefix: ${group.prefix}
|
|
|
510
613
|
decisions_written: decisions.length,
|
|
511
614
|
lessons_written: lessons.length,
|
|
512
615
|
synthesized_insights_saved: synthesizedSaved,
|
|
616
|
+
conflicts_resolved: conflictsResolved,
|
|
513
617
|
capsules_written: capsulesWritten,
|
|
514
618
|
capsule_facts_saved: capsuleFactsSaved,
|
|
515
619
|
decision_file: decisions.length > 0 ? decisionFile : null,
|
|
@@ -97,7 +97,7 @@ function withBufferLock(fn) {
|
|
|
97
97
|
acquired = true;
|
|
98
98
|
break;
|
|
99
99
|
} catch (e) {
|
|
100
|
-
if (e.code !== 'EEXIST')
|
|
100
|
+
if (e.code !== 'EEXIST') return false; // non-EEXIST errors (EMFILE, EACCES) → skip lock gracefully
|
|
101
101
|
try {
|
|
102
102
|
const age = Date.now() - fs.statSync(LOCK_FILE).mtimeMs;
|
|
103
103
|
if (age > LOCK_STALE_MS) {
|
|
@@ -159,12 +159,12 @@ process.stdin.on('end', () => {
|
|
|
159
159
|
// Skip empty or very short messages
|
|
160
160
|
// Chinese chars carry more info per char, so use weighted length
|
|
161
161
|
const weightedLen = [...prompt].reduce((sum, ch) => sum + (ch.charCodeAt(0) > 0x2e80 ? 3 : 1), 0);
|
|
162
|
-
if (weightedLen < 15) {
|
|
162
|
+
if (!isMeta && weightedLen < 15) {
|
|
163
163
|
process.exit(0);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// Hard cap to prevent giant prompt pastes from poisoning distill budget.
|
|
167
|
-
if (prompt.length > ABSOLUTE_MAX_CAPTURE_CHARS) {
|
|
167
|
+
if (!isMeta && prompt.length > ABSOLUTE_MAX_CAPTURE_CHARS) {
|
|
168
168
|
process.exit(0);
|
|
169
169
|
}
|
|
170
170
|
if (prompt.length > MAX_CAPTURE_CHARS) {
|
|
@@ -75,6 +75,7 @@ const DEFAULT_POLICY = {
|
|
|
75
75
|
min_evidence_for_gap: 3,
|
|
76
76
|
max_updates_per_analysis: 3,
|
|
77
77
|
max_gaps_per_analysis: 2,
|
|
78
|
+
max_signals_per_analysis: 30, // cap signals sent to Haiku per run
|
|
78
79
|
|
|
79
80
|
// Workflow discovery
|
|
80
81
|
workflow_discovery_interval: 2, // every N cold-path cycles
|
|
@@ -200,6 +201,7 @@ function sanitizePolicy(input) {
|
|
|
200
201
|
min_evidence_for_gap: clampInt(merged.min_evidence_for_gap, DEFAULT_POLICY.min_evidence_for_gap, 1, 20),
|
|
201
202
|
max_updates_per_analysis: clampInt(merged.max_updates_per_analysis, DEFAULT_POLICY.max_updates_per_analysis, 1, 20),
|
|
202
203
|
max_gaps_per_analysis: clampInt(merged.max_gaps_per_analysis, DEFAULT_POLICY.max_gaps_per_analysis, 1, 20),
|
|
204
|
+
max_signals_per_analysis: clampInt(merged.max_signals_per_analysis, DEFAULT_POLICY.max_signals_per_analysis, 5, 100),
|
|
203
205
|
workflow_discovery_interval: clampInt(merged.workflow_discovery_interval, DEFAULT_POLICY.workflow_discovery_interval, 1, 100),
|
|
204
206
|
min_signals_for_workflow: clampInt(merged.min_signals_for_workflow, DEFAULT_POLICY.min_signals_for_workflow, 1, 100),
|
|
205
207
|
workflow_proposal_threshold: clampInt(merged.workflow_proposal_threshold, DEFAULT_POLICY.workflow_proposal_threshold, 2, 50),
|
|
@@ -610,8 +612,15 @@ async function distillSkills() {
|
|
|
610
612
|
if (!content) return null;
|
|
611
613
|
|
|
612
614
|
const lines = content.split('\n');
|
|
613
|
-
const
|
|
614
|
-
if (
|
|
615
|
+
const allSignals = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
616
|
+
if (allSignals.length < policy.min_signals_for_distill) return null;
|
|
617
|
+
|
|
618
|
+
// Cap signals sent to Haiku to avoid prompt bloat / timeout.
|
|
619
|
+
// Keep most recent signals (higher relevance); overflow is still cleared.
|
|
620
|
+
const maxSignals = policy.max_signals_per_analysis || 30;
|
|
621
|
+
const signals = allSignals.length > maxSignals
|
|
622
|
+
? allSignals.slice(-maxSignals)
|
|
623
|
+
: allSignals;
|
|
615
624
|
|
|
616
625
|
// Get installed skills list
|
|
617
626
|
const installedSkills = listInstalledSkills();
|