@jack200714/mafw 4.8.0 → 4.10.2

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 (42) hide show
  1. package/README.md +27 -3
  2. package/gateway/dist/core/manager/goal-snapshot.js +2 -2
  3. package/gateway/dist/core/manager/manager-session-runtime.js +59 -0
  4. package/gateway/dist/core/manager/milestone-push.js +25 -10
  5. package/gateway/dist/index.js +465 -440
  6. package/gateway/dist/media/media-plugin-loader.js +25 -1
  7. package/gateway/dist/media/resolve-prompt.js +20 -0
  8. package/gateway/dist/memory/gateway-db.js +23 -0
  9. package/gateway/dist/opencode-adapter.js +53 -4
  10. package/gateway/dist/plugins/package-context.js +24 -0
  11. package/gateway/dist/plugins/package-host.js +331 -0
  12. package/gateway/dist/plugins/package-types.js +2 -0
  13. package/gateway/dist/recall/gateway-db-migrate.js +5 -2
  14. package/gateway/dist/recall/redact.js +53 -0
  15. package/gateway/dist/recall/turn-pipeline.js +2 -0
  16. package/gateway/dist/routes/event-publish.js +44 -0
  17. package/gateway/dist/routes/plugins.js +4 -1
  18. package/gateway/dist/routes/registry.js +125 -0
  19. package/gateway/dist/routes/route-catalog.js +192 -0
  20. package/gateway/dist/routes/triage-dismiss.js +32 -0
  21. package/gateway/dist/routes/waitwhat-command.js +43 -0
  22. package/gateway/dist/routes/wave1-handlers.js +62 -0
  23. package/gateway/dist/routes/wave2-handlers.js +47 -0
  24. package/gateway/dist/runtime/contract.js +4 -1
  25. package/gateway/dist/runtime/event-broadcast.js +8 -0
  26. package/gateway/dist/runtime/loader.js +22 -1
  27. package/gateway/dist/runtime/normalize.js +13 -0
  28. package/gateway/dist/runtime/opencode-runtime.js +18 -2
  29. package/gateway/dist/runtime/pi/pi-approval-bridge.js +12 -2
  30. package/gateway/dist/runtime/pi/pi-approval-extension.js +11 -3
  31. package/gateway/dist/runtime/pi/pi-session.js +21 -3
  32. package/gateway/dist/runtime/plugins/pi-runtime.js +6 -3
  33. package/gateway/dist/runtime/serve-sidecar.js +4 -1
  34. package/gateway/dist/runtime/serve-supervisor.js +19 -20
  35. package/gateway/dist/runtime/validate.js +39 -0
  36. package/gateway/dist/skills/manager-identity.js +6 -1
  37. package/gateway/dist/usage/builtin-plugins/gateway.js +91 -36
  38. package/gateway/dist/usage/plugin-context.js +42 -2
  39. package/gateway/dist/usage/plugin-loader.js +32 -2
  40. package/gateway/package.json +2 -1
  41. package/package.json +3 -1
  42. package/packages/tui/dist/cli.js +114 -51
package/README.md CHANGED
@@ -19,8 +19,8 @@
19
19
 
20
20
  ## 这是什么
21
21
 
22
- MAFW 是一个以**谐波记忆(Harmonic Memory)**为核心的 AI Agent 工作平台。
23
- 它不再只是某个编辑器的插件,而是由四个部分组成的完整系统:
22
+ MAFW 是一个以**谐波记忆(Harmonic Memory)**为核心的 AI Agent 工作平台,
23
+ 由四个常驻部分组成(另可选项接入 opencode 编辑器):
24
24
 
25
25
  | 组件 | 形态 | 作用 |
26
26
  |---|---|---|
@@ -81,6 +81,30 @@ Gateway 与 Agent runtime 之间是能力自声明契约(Tier 0-2):内置
81
81
  external 托管模式可热切换,无需重启。契约能力面覆盖会话分支(fork / revert)、回合预算、
82
82
  结果信封(usage / finish)、三值审批回复(once / always / reject)、原生 question 通道等。
83
83
 
84
+ ### 🧩 统一插件包
85
+
86
+ `~/.mafw/plugins/` 下**一个插件包可同时贡献多种能力**——usage 配额、media 引擎、agent runtime、
87
+ UI 工具卡——不必按类型拆成多个文件丢进多个目录:
88
+
89
+ ```js
90
+ // ~/.mafw/plugins/my-vendor.js —— 一个文件,一个厂商全家桶
91
+ module.exports = {
92
+ name: "my-vendor",
93
+ usage: { name: "my-vendor", type: "api", plan: "Pro", async fetch(ctx) { /* 余额/限额 */ } },
94
+ media: { modalities: ["image"], engine: "pi", fixPayload(p) { /* 自定义 wire 格式 */ } },
95
+ runtime: { capabilities: { eventStream: true }, async createRuntime(ctx) { /* 接入自有 runtime */ } },
96
+ // 或 async activate(ctx) 拿统一 ctx 后返回贡献
97
+ };
98
+ ```
99
+
100
+ - **统一上下文**:`apiKey` / `fetch` / `pluginConfig` / `projectDir` / `gatewayPort` / `usage.modelStats` /
101
+ `emit`(向桌面/TUI/推送全通道广播自定义事件)——
102
+ 三类插件 ctx 的超集,配置收敛在 `plugins.<name>.config`
103
+ - **内置件同接口**:opencode runtime 与 media `pi` 引擎就是注册进同一体系的内置插件,
104
+ 用户同名文件/包可直接覆盖
105
+ - **优先级**:包 > legacy 目录文件 > 内置;旧 `usage-plugins/` 等四目录继续可用
106
+ - **热重载**:改动即生效(顶层 + 每包子目录双 watcher),插件中心(Desktop Config→Plugins)可见全部包状态
107
+
84
108
  ## 基准
85
109
 
86
110
  LongMemEval-S(session 粒度,谐波记忆管线):
@@ -106,7 +130,7 @@ LongMemEval-S(session 粒度,谐波记忆管线):
106
130
  ┌──────────┬──────┴──────┬───────────┐
107
131
  谐波记忆系统 Goal 编排引擎 自动化引擎 媒体/内核/用量
108
132
  OKF + 索引 manager/plan/ cron 规则 A2A · Jupyter
109
- 能量·合并 execute/review 衰减·压缩 provider 插件
133
+ 能量·合并 execute/review 衰减·压缩 统一插件包
110
134
  ```
111
135
 
112
136
  数据根目录 `~/.mafw/`:记忆 OKF 文件、统一数据库、日志、配置覆盖。
@@ -37,7 +37,7 @@ exports.collectActiveGoals = collectActiveGoals;
37
37
  exports.buildGoalSnapshot = buildGoalSnapshot;
38
38
  const fs = __importStar(require("fs"));
39
39
  const path = __importStar(require("path"));
40
- const TERMINAL_ACTIONS = new Set(['COMPLETED', 'FAILED']);
40
+ const TERMINAL_ACTIONS = new Set(['COMPLETED', 'FAILED', 'CANCELLED']);
41
41
  function collectActiveGoals(mafwDir) {
42
42
  const stateDir = path.join(mafwDir, 'state');
43
43
  if (!fs.existsSync(stateDir))
@@ -68,6 +68,6 @@ function buildGoalSnapshot(mafwDir, opts) {
68
68
  const goals = collectActiveGoals(mafwDir);
69
69
  if (goals.length === 0)
70
70
  return null;
71
- const lines = goals.slice(0, maxGoals).map(g => `- ${g.goalId}: ${g.title} [phase=${g.phase} round=${g.round}${g.pendingQuestions > 0 ? ` pendingQ=${g.pendingQuestions}` : ''}]`);
71
+ const lines = goals.slice(0, maxGoals).map(g => `- ${g.title} (${g.goalId}) [phase=${g.phase} round=${g.round}${g.pendingQuestions > 0 ? ` pendingQ=${g.pendingQuestions}` : ''}]`);
72
72
  return `<goal-snapshot>\n${lines.join('\n')}\n</goal-snapshot>`;
73
73
  }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ // Runtime ownership of manager-session kv entries — per-runtime SLOTS.
3
+ // The manager "topic" exists separately in each runtime's storage (opencode
4
+ // SQLite 'ses_*' vs pi SessionManager 'pi_*'), so the kv value keeps one slot
5
+ // per runtime and switching runtimes never drops a topic: switching back
6
+ // resumes the previous manager. Legacy v1 entries ({sessionId, createdAt})
7
+ // are migrated by session-id prefix on read/write. Unknown origin is kept
8
+ // conservatively (deletion is irreversible).
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.managerEntryRuntime = managerEntryRuntime;
11
+ exports.readManagerSlot = readManagerSlot;
12
+ exports.writeManagerSlot = writeManagerSlot;
13
+ /** Which runtime does this entry belong to? Explicit tag wins, then id-prefix inference, then null. */
14
+ function managerEntryRuntime(value) {
15
+ if (value?.runtime)
16
+ return value.runtime;
17
+ const sid = value?.sessionId || '';
18
+ if (sid.startsWith('pi_'))
19
+ return 'pi';
20
+ if (sid.startsWith('ses_'))
21
+ return 'opencode';
22
+ return null;
23
+ }
24
+ /**
25
+ * Read the slot for `currentRuntime`.
26
+ * v2 value → byRuntime[currentRuntime]; v1 legacy → its prefix-inferred slot
27
+ * when it matches, else null (the entry stays dormant, never deleted).
28
+ */
29
+ function readManagerSlot(value, currentRuntime) {
30
+ if (!value || typeof value !== 'object')
31
+ return null;
32
+ const v = value;
33
+ if (v.byRuntime && typeof v.byRuntime === 'object') {
34
+ return v.byRuntime[currentRuntime] ?? null;
35
+ }
36
+ // v1 legacy: belongs to exactly one runtime (prefix-inferred).
37
+ const owner = managerEntryRuntime(v);
38
+ return owner === currentRuntime ? { sessionId: v.sessionId, createdAt: v.createdAt } : null;
39
+ }
40
+ /**
41
+ * Write the slot for `currentRuntime`, preserving all other runtimes' slots.
42
+ * v1 legacy input is migrated into its prefix-inferred slot first.
43
+ */
44
+ function writeManagerSlot(existing, currentRuntime, slot) {
45
+ const byRuntime = {};
46
+ if (existing && typeof existing === 'object') {
47
+ const v = existing;
48
+ if (v.byRuntime && typeof v.byRuntime === 'object') {
49
+ Object.assign(byRuntime, v.byRuntime);
50
+ }
51
+ else {
52
+ const owner = managerEntryRuntime(v);
53
+ if (owner && v.sessionId)
54
+ byRuntime[owner] = { sessionId: v.sessionId, createdAt: v.createdAt };
55
+ }
56
+ }
57
+ byRuntime[currentRuntime] = slot;
58
+ return { byRuntime };
59
+ }
@@ -30,21 +30,19 @@ class MilestonePushNotifier {
30
30
  const key = milestoneDedupeKey(data.goalId, data.phase, state?.stateVersion ?? data.loop);
31
31
  if (this.deps.wasNotified(key))
32
32
  return;
33
- this.deps.markNotified(key);
34
- this.enqueue(data.projectDir, formatMilestoneLine(data.goalId, state?.title || data.goalId, data.phase, state?.reviewVerdict));
33
+ this.enqueue(data.projectDir, { key, line: formatMilestoneLine(data.goalId, state?.title || data.goalId, data.phase, state?.reviewVerdict) });
35
34
  }
36
35
  onArchived(goalId, projectDir, verdict) {
37
36
  const state = this.deps.readGoalState(goalId, projectDir);
38
37
  const key = milestoneDedupeKey(goalId, `ARCHIVED:${verdict}`, state?.stateVersion);
39
38
  if (this.deps.wasNotified(key))
40
39
  return;
41
- this.deps.markNotified(key);
42
- this.enqueue(projectDir, formatMilestoneLine(goalId, state?.title || goalId, `ARCHIVED(${verdict})`));
40
+ this.enqueue(projectDir, { key, line: formatMilestoneLine(goalId, state?.title || goalId, `ARCHIVED(${verdict})`) });
43
41
  }
44
42
  // Per-project coalescing: bursts of transitions collapse into one message.
45
- enqueue(projectDir, line) {
43
+ enqueue(projectDir, item) {
46
44
  const q = this.queues.get(projectDir) || [];
47
- q.push(line);
45
+ q.push(item);
48
46
  this.queues.set(projectDir, q);
49
47
  if (this.timers.has(projectDir))
50
48
  return;
@@ -52,21 +50,38 @@ class MilestonePushNotifier {
52
50
  }
53
51
  async flush(projectDir) {
54
52
  this.timers.delete(projectDir);
55
- const lines = this.queues.get(projectDir) || [];
53
+ const items = this.queues.get(projectDir) || [];
56
54
  this.queues.delete(projectDir);
57
- if (lines.length === 0)
55
+ if (items.length === 0)
58
56
  return;
59
57
  const session = this.deps.getManagerSession(projectDir);
60
- if (!session?.sessionId)
58
+ if (!session?.sessionId) {
59
+ // Manager slot not ready yet (e.g. right after a runtime switch): retry
60
+ // with a slower cadence instead of silently dropping the milestone
61
+ // (marking before delivery would lose it permanently).
62
+ this.requeue(projectDir, items);
61
63
  return;
62
- const text = `[MAFW GOAL 里程碑]\n${lines.join('\n')}\n(系统通知)`;
64
+ }
65
+ const text = `[MAFW GOAL 里程碑]\n${items.map((i) => i.line).join('\n')}\n(系统通知)`;
63
66
  try {
64
67
  await this.deps.promptNoReply(session.sessionId, text);
68
+ for (const item of items)
69
+ this.deps.markNotified(item.key);
65
70
  }
66
71
  catch (err) {
67
72
  logger_1.log.warn(`[MilestonePush] push failed (fail-open): ${err.message}`);
73
+ this.requeue(projectDir, items);
68
74
  }
69
75
  }
76
+ // Slower retry cadence than the coalescing flush — bounded noise while the
77
+ // manager session is being (re-)ensured on the active runtime.
78
+ requeue(projectDir, items) {
79
+ const q = (this.queues.get(projectDir) || []).concat(items);
80
+ this.queues.set(projectDir, q);
81
+ if (this.timers.has(projectDir))
82
+ return;
83
+ this.timers.set(projectDir, setTimeout(() => { void this.flush(projectDir); }, 60_000));
84
+ }
70
85
  dispose() {
71
86
  for (const t of this.timers.values())
72
87
  clearTimeout(t);