@jack200714/mafw 4.5.2 → 4.10.1
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 +27 -3
- package/gateway/dist/core/manager/goal-snapshot.js +2 -2
- package/gateway/dist/core/manager/manager-session-runtime.js +59 -0
- package/gateway/dist/core/manager/milestone-push.js +25 -10
- package/gateway/dist/index.js +315 -139
- package/gateway/dist/media/media-plugin-loader.js +25 -13
- package/gateway/dist/media/resolve-prompt.js +20 -0
- package/gateway/dist/memory/gateway-db.js +23 -0
- package/gateway/dist/opencode-adapter.js +34 -0
- package/gateway/dist/plugins/hub.js +153 -19
- package/gateway/dist/plugins/package-context.js +24 -0
- package/gateway/dist/plugins/package-host.js +331 -0
- package/gateway/dist/plugins/package-types.js +2 -0
- package/gateway/dist/recall/gateway-db-migrate.js +5 -2
- package/gateway/dist/recall/redact.js +53 -0
- package/gateway/dist/recall/turn-pipeline.js +2 -0
- package/gateway/dist/routes/event-publish.js +44 -0
- package/gateway/dist/routes/plugins.js +19 -6
- package/gateway/dist/routes/waitwhat-command.js +43 -0
- package/gateway/dist/runtime/contract.js +4 -1
- package/gateway/dist/runtime/event-broadcast.js +41 -0
- package/gateway/dist/runtime/loader.js +45 -14
- package/gateway/dist/runtime/normalize.js +13 -0
- package/gateway/dist/runtime/pi/pi-approval-bridge.js +12 -2
- package/gateway/dist/runtime/pi/pi-approval-extension.js +11 -3
- package/gateway/dist/runtime/pi/pi-session.js +21 -3
- package/gateway/dist/runtime/plugins/pi-runtime.js +6 -3
- package/gateway/dist/runtime/serve-sidecar.js +4 -1
- package/gateway/dist/runtime/serve-supervisor.js +12 -0
- package/gateway/dist/runtime/validate.js +39 -0
- package/gateway/dist/skills/manager-identity.js +6 -1
- package/gateway/dist/usage/builtin-plugins/gateway.js +103 -26
- package/gateway/dist/usage/plugin-context.js +42 -2
- package/gateway/dist/usage/plugin-loader.js +32 -13
- package/gateway/package.json +2 -2
- package/package.json +3 -1
- package/packages/tui/dist/cli.js +28 -5
package/gateway/dist/index.js
CHANGED
|
@@ -63,6 +63,8 @@ const stale_verify_1 = require("./recall/stale-verify");
|
|
|
63
63
|
const session_worker_pool_1 = require("./recall/session-worker-pool");
|
|
64
64
|
const reflect_cursor_1 = require("./recall/reflect-cursor");
|
|
65
65
|
const index_scan_1 = require("./recall/index-scan");
|
|
66
|
+
const redact_1 = require("./recall/redact");
|
|
67
|
+
const waitwhat_command_1 = require("./routes/waitwhat-command");
|
|
66
68
|
const consolidation_service_1 = require("./memory/consolidation-service");
|
|
67
69
|
const auth_2 = require("./runtime/auth");
|
|
68
70
|
const harmonic_file_store_2 = require("./memory/harmonic-file-store");
|
|
@@ -72,6 +74,7 @@ const media_service_1 = require("./media/media-service");
|
|
|
72
74
|
const media_agent_1 = require("./media/media-agent");
|
|
73
75
|
const pi_adapter_1 = require("./media/pi-adapter");
|
|
74
76
|
const media_runtime_executor_1 = require("./media/media-runtime-executor");
|
|
77
|
+
const resolve_prompt_1 = require("./media/resolve-prompt");
|
|
75
78
|
const media_plugin_loader_1 = require("./media/media-plugin-loader");
|
|
76
79
|
const tts_service_1 = require("./media/tts-service");
|
|
77
80
|
const eval_endpoint_1 = require("./eval-endpoint");
|
|
@@ -99,14 +102,20 @@ const self_update_1 = require("./self-update");
|
|
|
99
102
|
const step_inject_1 = require("./recall/step-inject");
|
|
100
103
|
const inject_format_1 = require("./recall/inject-format");
|
|
101
104
|
const normalize_1 = require("./runtime/normalize");
|
|
105
|
+
const event_broadcast_1 = require("./runtime/event-broadcast");
|
|
102
106
|
const budget_guard_1 = require("./core/budget-guard");
|
|
103
107
|
const goal_budget_1 = require("./core/goal-budget");
|
|
104
108
|
const contract_1 = require("./runtime/contract");
|
|
109
|
+
const validate_1 = require("./runtime/validate");
|
|
105
110
|
const loader_1 = require("./runtime/loader");
|
|
106
111
|
const pi_runtime_1 = require("./runtime/plugins/pi-runtime");
|
|
107
112
|
const permission_1 = require("./routes/permission");
|
|
113
|
+
const event_publish_1 = require("./routes/event-publish");
|
|
108
114
|
const runtime_switch_1 = require("./routes/runtime-switch");
|
|
109
115
|
const plugins_1 = require("./routes/plugins");
|
|
116
|
+
const package_host_1 = require("./plugins/package-host");
|
|
117
|
+
const package_context_1 = require("./plugins/package-context");
|
|
118
|
+
const hub_1 = require("./plugins/hub");
|
|
110
119
|
const restart_agent_1 = require("./routes/restart-agent");
|
|
111
120
|
const session_mutations_1 = require("./routes/session-mutations");
|
|
112
121
|
const serve_supervisor_1 = require("./runtime/serve-supervisor");
|
|
@@ -144,6 +153,8 @@ async function isPortHealthy(port) {
|
|
|
144
153
|
return false;
|
|
145
154
|
}
|
|
146
155
|
}
|
|
156
|
+
const manager_session_runtime_1 = require("./core/manager/manager-session-runtime");
|
|
157
|
+
const INTERNAL_SESSION_TTL_DAYS = 7;
|
|
147
158
|
class MafwScheduler {
|
|
148
159
|
serveInstance;
|
|
149
160
|
serveUrl;
|
|
@@ -157,6 +168,10 @@ class MafwScheduler {
|
|
|
157
168
|
serveOwned = false;
|
|
158
169
|
serveExitStreak = 0;
|
|
159
170
|
serveRecovering = false;
|
|
171
|
+
// External runtimes (pi) do not own serve: the watchdog can only warn and
|
|
172
|
+
// reconnect the event stream. Flag keeps that warning a one-shot ERROR per
|
|
173
|
+
// outage instead of an unbounded WARN loop.
|
|
174
|
+
serveExternalDownNotified = false;
|
|
160
175
|
serveWatchdogTimer = null;
|
|
161
176
|
serveRetryTimer = null;
|
|
162
177
|
serveStableTimer = null;
|
|
@@ -169,6 +184,8 @@ class MafwScheduler {
|
|
|
169
184
|
lastActiveBySession = new Map();
|
|
170
185
|
lastWriteBySession = new Map();
|
|
171
186
|
tokenWriterSession = null;
|
|
187
|
+
/** 畸形事件 warn 限频(每 runtime 30s 一次) */
|
|
188
|
+
malformedEventWarnAt = new Map();
|
|
172
189
|
stopTokenWatcher = null;
|
|
173
190
|
// Path 1 step-injection state (mark-before-async + dedup + queue). All
|
|
174
191
|
// per-session entries expire via TtlMap.
|
|
@@ -206,8 +223,6 @@ class MafwScheduler {
|
|
|
206
223
|
registeredProjects = new Map();
|
|
207
224
|
registryPath;
|
|
208
225
|
registryWriteQueue = Promise.resolve();
|
|
209
|
-
configPath;
|
|
210
|
-
configWriteQueue = Promise.resolve();
|
|
211
226
|
running = true;
|
|
212
227
|
// private dashboard?: DashboardServer;
|
|
213
228
|
mcpEndpoint;
|
|
@@ -232,6 +247,8 @@ class MafwScheduler {
|
|
|
232
247
|
mediaPluginLoader;
|
|
233
248
|
mediaRuntimeExecutor;
|
|
234
249
|
mediaRuntimeExecutorRt;
|
|
250
|
+
pluginHost;
|
|
251
|
+
usageStatsProvider;
|
|
235
252
|
ttsService;
|
|
236
253
|
kernels;
|
|
237
254
|
automationEngine;
|
|
@@ -252,7 +269,6 @@ class MafwScheduler {
|
|
|
252
269
|
this.serveUrl = config_1.config.server.serveUrl;
|
|
253
270
|
this.apiPort = config_1.config.server.apiPort;
|
|
254
271
|
this.pollInterval = config_1.config.timeouts.backupPollInterval;
|
|
255
|
-
this.configPath = config_1.config.paths.globalConfig;
|
|
256
272
|
this.registryPath = config_1.config.paths.registryFile;
|
|
257
273
|
this.chatSessions = new chat_sessions_1.ChatSessionManager();
|
|
258
274
|
this.serveSupervisor = (0, serve_supervisor_1.createServeSupervisor)({
|
|
@@ -357,6 +373,16 @@ class MafwScheduler {
|
|
|
357
373
|
await this.runtimeLoader.init();
|
|
358
374
|
// 内置插件注册:pi-coding-agent runtime(进程内 SDK 嵌入)
|
|
359
375
|
this.runtimeLoader.registerBuiltin('pi', pi_runtime_1.createPiRuntime, pi_runtime_1.PI_CAPABILITIES, true);
|
|
376
|
+
// 2b. 统一插件包宿主(必须在 createRuntime 之前 init——包 runtime 贡献要先注册)
|
|
377
|
+
this.pluginHost = new package_host_1.PluginHost(process.env.MAFW_PLUGINS_DIR || config_1.config.resolvePath('plugins'), (name) => (0, package_context_1.createPluginPackageContext)(name, {
|
|
378
|
+
getCredentials: () => this.opencodeClient?.credentials ?? undefined,
|
|
379
|
+
usageStats: () => this.usageStatsProvider,
|
|
380
|
+
projectDir: this.projectDir,
|
|
381
|
+
gatewayPort: config_1.config.server.apiPort,
|
|
382
|
+
emit: (event) => this.broadcast(event),
|
|
383
|
+
}));
|
|
384
|
+
this.pluginHost.bindRuntime((entries) => this.runtimeLoader?.setPackageEntries(entries));
|
|
385
|
+
await this.pluginHost.init();
|
|
360
386
|
// 3. 创建 SDK 客户端(auth),用于健康检查和后续通信
|
|
361
387
|
const sdkConfig = {
|
|
362
388
|
baseUrl: this.serveUrl,
|
|
@@ -367,6 +393,12 @@ class MafwScheduler {
|
|
|
367
393
|
if (opencodePassword) {
|
|
368
394
|
sdkConfig.headers = { Authorization: 'Basic ' + Buffer.from(`opencode:${opencodePassword}`).toString('base64') };
|
|
369
395
|
}
|
|
396
|
+
// opencode 注册为正式内置插件(与用户插件同一接口;同名文件/包可覆盖)
|
|
397
|
+
const sdkSnapshot = { ...sdkConfig, headers: { ...sdkConfig.headers } };
|
|
398
|
+
this.runtimeLoader.registerBuiltin('opencode', async () => (await import('./runtime/opencode-runtime.js')).createOpencodeRuntime({
|
|
399
|
+
...sdkSnapshot,
|
|
400
|
+
headers: { ...sdkSnapshot.headers },
|
|
401
|
+
}), (0, contract_1.fullCapabilities)(), !!process.env.MAFW_SERVER_SERVE_URL);
|
|
370
402
|
const runtime = await this.createRuntime(sdkConfig);
|
|
371
403
|
this.opencodeClient = runtime;
|
|
372
404
|
this.runtimeCaps = runtime.capabilities;
|
|
@@ -423,8 +455,7 @@ class MafwScheduler {
|
|
|
423
455
|
// 4. Dashboard is now served via the API server on the same port
|
|
424
456
|
// this.dashboard = new DashboardServer(3001, this.projectDir, this);
|
|
425
457
|
// this.dashboard.start();
|
|
426
|
-
// 5.
|
|
427
|
-
await this.recoverConfig();
|
|
458
|
+
// 5. 恢复注册表(权威源:gateway DB kv_store + legacy 文件兜底)
|
|
428
459
|
await this.recoverRegistry();
|
|
429
460
|
// 5.0 Data-directory migration: move memory store + pipeline files from
|
|
430
461
|
// the previously-fixed gateway package .mafw (and any project-relative
|
|
@@ -598,7 +629,7 @@ class MafwScheduler {
|
|
|
598
629
|
}
|
|
599
630
|
else {
|
|
600
631
|
try {
|
|
601
|
-
const ms = this.
|
|
632
|
+
const ms = this.readManagerSessionEntry(this.projectDir);
|
|
602
633
|
if (ms?.sessionId)
|
|
603
634
|
targets.add(ms.sessionId);
|
|
604
635
|
}
|
|
@@ -642,8 +673,10 @@ class MafwScheduler {
|
|
|
642
673
|
if (changed.includes('runtime')) {
|
|
643
674
|
const newPlugin = config_1.config.runtime?.plugin;
|
|
644
675
|
if (newPlugin !== prevPlugin && !this.switchingRuntime && !this.serveRecovering) {
|
|
645
|
-
|
|
676
|
+
this.switchingRuntime = true;
|
|
677
|
+
const prev = this.opencodeClient;
|
|
646
678
|
try {
|
|
679
|
+
logger_1.log.info(`[Scheduler] Runtime plugin changed: '${prevPlugin ?? 'builtin'}' → '${newPlugin ?? 'builtin'}'; hot-switching...`);
|
|
647
680
|
const sdkConfig = {
|
|
648
681
|
baseUrl: this.serveUrl,
|
|
649
682
|
directory: this.projectDir,
|
|
@@ -668,11 +701,28 @@ class MafwScheduler {
|
|
|
668
701
|
if (this.automationEngine)
|
|
669
702
|
this.automationEngine.setRuntimeClient(runtime);
|
|
670
703
|
await this.resubscribeEvents(`config hot-reload runtime plugin changed to '${newPlugin ?? 'builtin'}'`);
|
|
704
|
+
// Same desktop hint as the route path: hand-edited config.yaml
|
|
705
|
+
// switches must also refresh the renderer (menus/tabs/manager kv).
|
|
706
|
+
this.broadcast({ type: 'runtime_switched', runtime: runtime.name, previous: prevPlugin ?? null });
|
|
707
|
+
// Dispose the previous runtime AFTER the new one is fully wired
|
|
708
|
+
// (mirror of the route path) — without this, switching away from
|
|
709
|
+
// pi leaks its AgentSessions/ApprovalBridges/event stream.
|
|
710
|
+
if (prev && prev.dispose) {
|
|
711
|
+
try {
|
|
712
|
+
await prev.dispose();
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
logger_1.log.warn(`[Scheduler] dispose previous runtime failed: ${err.message}`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
671
718
|
logger_1.log.info(`[Scheduler] Runtime hot-switched to '${runtime.name}'`);
|
|
672
719
|
}
|
|
673
720
|
catch (err) {
|
|
674
721
|
logger_1.log.warn(`[Scheduler] Runtime hot-switch failed (non-fatal): ${err.message}`);
|
|
675
722
|
}
|
|
723
|
+
finally {
|
|
724
|
+
this.switchingRuntime = false;
|
|
725
|
+
}
|
|
676
726
|
}
|
|
677
727
|
}
|
|
678
728
|
}, 300);
|
|
@@ -773,7 +823,8 @@ class MafwScheduler {
|
|
|
773
823
|
registeredAt: new Date().toISOString()
|
|
774
824
|
});
|
|
775
825
|
this.persistRegistry();
|
|
776
|
-
|
|
826
|
+
// 桌面 renderer 依此事件刷新 Rail 项目列表(否则只在 gateway ready 时拉一次)。
|
|
827
|
+
this.broadcast((0, event_broadcast_1.projectRegisteredEvent)(projectDir));
|
|
777
828
|
logger_1.log.info(`[Scheduler] Project registered via filesystem: ${projectDir}`);
|
|
778
829
|
}
|
|
779
830
|
catch {
|
|
@@ -846,6 +897,23 @@ class MafwScheduler {
|
|
|
846
897
|
}
|
|
847
898
|
handleOpencodeEvent(evt) {
|
|
848
899
|
const f = (0, normalize_1.normalizeOpencodeEvent)(evt);
|
|
900
|
+
// 畸形事件诊断(限频):runtime 插件发来的事件 type/properties 全空时
|
|
901
|
+
// 静默穿过会污染 trajectory 与桌面 SSE——这里给可定位诊断。
|
|
902
|
+
if ((0, normalize_1.isMalformedEvent)(evt)) {
|
|
903
|
+
const now = Date.now();
|
|
904
|
+
const last = this.malformedEventWarnAt.get(this.runtimeName) ?? 0;
|
|
905
|
+
if (now - last > 30_000) {
|
|
906
|
+
this.malformedEventWarnAt.set(this.runtimeName, now);
|
|
907
|
+
let summary;
|
|
908
|
+
try {
|
|
909
|
+
summary = JSON.stringify(evt)?.slice(0, 200) ?? '(unserializable)';
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
summary = '(unserializable)';
|
|
913
|
+
}
|
|
914
|
+
logger_1.log.warn(`[SSE] malformed event from runtime '${this.runtimeName}' (no type/properties) — dropped consumers may misbehave; payload: ${summary}`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
849
917
|
const { type, properties: props, sessionID } = f;
|
|
850
918
|
// Only memory-system sessions (index-scan / extract / reflect workers) are
|
|
851
919
|
// internal: their token-level deltas flooded the desktop renderer (per-delta
|
|
@@ -952,13 +1020,15 @@ class MafwScheduler {
|
|
|
952
1020
|
catch (err) {
|
|
953
1021
|
logger_1.log.warn(`[Trajectory] idle aggregation failed (non-fatal): ${err.message}`);
|
|
954
1022
|
}
|
|
955
|
-
this.broadcast(
|
|
1023
|
+
this.broadcast((0, event_broadcast_1.opencodeBroadcast)({ type: 'message.complete', sessionID }, memoryWorker));
|
|
956
1024
|
}
|
|
957
1025
|
else if (f.broadcast === 'error') {
|
|
958
|
-
this.broadcast(
|
|
1026
|
+
this.broadcast((0, event_broadcast_1.opencodeBroadcast)({ type: 'message.error', sessionID, error: props?.error instanceof Error ? props.error.message : String(props?.error ?? 'Unknown error') }, memoryWorker));
|
|
959
1027
|
}
|
|
960
1028
|
else {
|
|
961
|
-
|
|
1029
|
+
// directory 透传:session.created/updated/deleted 的消费方(桌面 Rail)
|
|
1030
|
+
// 据此定位所属项目做定向刷新(normalize 已从 GlobalEvent 信封提取)。
|
|
1031
|
+
this.broadcast((0, event_broadcast_1.opencodeBroadcast)({ type, properties: props, sessionID, directory: f.directory }, memoryWorker));
|
|
962
1032
|
}
|
|
963
1033
|
}
|
|
964
1034
|
/** 能力门:runtime 未声明该能力时以 503 显式拒绝(fail-open 的声明式降级)。 */
|
|
@@ -988,7 +1058,18 @@ class MafwScheduler {
|
|
|
988
1058
|
if (plugin) {
|
|
989
1059
|
try {
|
|
990
1060
|
const creds = await this.runtimeCredentialsForPlugin();
|
|
991
|
-
const rt = await plugin.createRuntime((0, loader_1.createRuntimePluginContext)(creds
|
|
1061
|
+
const rt = await plugin.createRuntime((0, loader_1.createRuntimePluginContext)(creds, {
|
|
1062
|
+
projectDir: this.projectDir,
|
|
1063
|
+
gatewayPort: config_1.config.server.apiPort,
|
|
1064
|
+
}));
|
|
1065
|
+
// 声明 vs 实现一致性校验:错位插件在切换/启动期给出字段级诊断并回退,
|
|
1066
|
+
// 而不是运行时深处才炸。
|
|
1067
|
+
const issues = (0, validate_1.validateRuntimeShape)(rt, pluginName);
|
|
1068
|
+
if (issues.length > 0) {
|
|
1069
|
+
for (const issue of issues)
|
|
1070
|
+
logger_1.log.error(`[Runtime] shape violation: ${issue}`);
|
|
1071
|
+
throw new Error(`runtime '${rt.name}' failed shape validation (${issues.length} issue(s)) — see logs`);
|
|
1072
|
+
}
|
|
992
1073
|
logger_1.log.info(`[Runtime] using plugin runtime '${rt.name}' (capabilities: ${JSON.stringify(rt.capabilities)})`);
|
|
993
1074
|
return rt;
|
|
994
1075
|
}
|
|
@@ -1000,6 +1081,11 @@ class MafwScheduler {
|
|
|
1000
1081
|
logger_1.log.warn(`[Runtime] plugin '${pluginName}' not found — falling back to opencode`);
|
|
1001
1082
|
}
|
|
1002
1083
|
}
|
|
1084
|
+
const builtin = this.runtimeLoader?.get('opencode');
|
|
1085
|
+
if (builtin) {
|
|
1086
|
+
return builtin.createRuntime((0, loader_1.createRuntimePluginContext)(undefined));
|
|
1087
|
+
}
|
|
1088
|
+
// 最终安全网:loader 未注册时直连(不应发生)
|
|
1003
1089
|
const { createOpencodeRuntime } = await import('./runtime/opencode-runtime.js');
|
|
1004
1090
|
return createOpencodeRuntime({
|
|
1005
1091
|
...sdkConfig,
|
|
@@ -1706,6 +1792,7 @@ class MafwScheduler {
|
|
|
1706
1792
|
}
|
|
1707
1793
|
this.pluginLoader?.stop();
|
|
1708
1794
|
this.mediaPluginLoader?.stop();
|
|
1795
|
+
this.pluginHost?.stop();
|
|
1709
1796
|
if (this.opencodeClient && typeof this.opencodeClient.dispose === 'function') {
|
|
1710
1797
|
void this.opencodeClient.dispose().catch((err) => {
|
|
1711
1798
|
logger_1.log.warn(`[Scheduler] runtime dispose error: ${err?.message ?? String(err)}`);
|
|
@@ -1719,32 +1806,6 @@ class MafwScheduler {
|
|
|
1719
1806
|
// }
|
|
1720
1807
|
logger_1.log.info('[Scheduler] Stopping...');
|
|
1721
1808
|
}
|
|
1722
|
-
// Proxy a native opencode request by trying every registered workspace.
|
|
1723
|
-
// Native reply/reject routes are workspace-scoped (WorkspaceRoutingMiddleware),
|
|
1724
|
-
// but the gateway's own projectDir is its cwd — the request may belong to any
|
|
1725
|
-
// registered project. GET list endpoints are cross-workspace and unaffected.
|
|
1726
|
-
async proxyNativeWorkspaces(path, method, body) {
|
|
1727
|
-
const dirs = new Set([this.projectDir || '.']);
|
|
1728
|
-
for (const key of this.registeredProjects.keys())
|
|
1729
|
-
dirs.add(key);
|
|
1730
|
-
let lastStatus = 502;
|
|
1731
|
-
for (const dir of dirs) {
|
|
1732
|
-
try {
|
|
1733
|
-
const r = await fetch(`${this.serveUrl}${path}?directory=${encodeURIComponent(dir)}`, {
|
|
1734
|
-
method,
|
|
1735
|
-
headers: { 'content-type': 'application/json', 'x-opencode-directory': encodeURIComponent(dir) },
|
|
1736
|
-
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
1737
|
-
});
|
|
1738
|
-
if (r.ok)
|
|
1739
|
-
return { ok: true, status: r.status };
|
|
1740
|
-
lastStatus = r.status;
|
|
1741
|
-
}
|
|
1742
|
-
catch (e) {
|
|
1743
|
-
logger_1.log.error(`[Native proxy] ${path} failed for ${dir}: ${e.message}`);
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
return { ok: false, status: lastStatus };
|
|
1747
|
-
}
|
|
1748
1809
|
// 鈹€鈹€ Services & Event Bus 鈹€鈹€
|
|
1749
1810
|
async initServices() {
|
|
1750
1811
|
const projectDir = this.projectDir;
|
|
@@ -1769,6 +1830,9 @@ class MafwScheduler {
|
|
|
1769
1830
|
getCredentials: () => this.opencodeClient?.credentials ?? undefined,
|
|
1770
1831
|
});
|
|
1771
1832
|
await this.mediaPluginLoader.init();
|
|
1833
|
+
this.pluginHost?.bindMedia((entries) => this.mediaPluginLoader?.setPackageEntries(entries));
|
|
1834
|
+
// 内置 pi 引擎登记(可被用户同名包/文件覆盖——resolveMediaPrompt 语义)
|
|
1835
|
+
this.mediaPluginLoader.registerBuiltinEngine('pi', ['image', 'video', 'audio']);
|
|
1772
1836
|
this.mediaService = new media_service_1.MediaService({
|
|
1773
1837
|
prompt: (0, pi_adapter_1.createPiPromptAdapter)({
|
|
1774
1838
|
getApiKey: (provider) => {
|
|
@@ -1778,31 +1842,24 @@ class MafwScheduler {
|
|
|
1778
1842
|
config: () => config_1.config.raw.media,
|
|
1779
1843
|
resolvePrompt: (kind, cfg) => {
|
|
1780
1844
|
const engineName = cfg[kind]?.engine ?? cfg.engine ?? 'pi';
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
return undefined;
|
|
1800
|
-
}
|
|
1801
|
-
if (!engine.modalities.includes(kind)) {
|
|
1802
|
-
logger_1.log.warn(`[MediaService] engine '${engineName}' does not support modality '${kind}', falling back to pi`);
|
|
1803
|
-
return undefined;
|
|
1804
|
-
}
|
|
1805
|
-
return engine.prompt;
|
|
1845
|
+
return (0, resolve_prompt_1.resolveMediaPrompt)(engineName, kind, {
|
|
1846
|
+
engines: this.mediaPluginLoader?.getEngines() ?? new Map(),
|
|
1847
|
+
builtinPi: () => {
|
|
1848
|
+
// pi runtime 激活时:图片走 AgentRuntime 会话(MediaRuntimeExecutor),
|
|
1849
|
+
// video/audio 由 executor 内部回退到 complete 路径
|
|
1850
|
+
if (this.opencodeClient?.name === 'pi') {
|
|
1851
|
+
// runtime 热切换后 opencodeClient 实例更换——旧 executor 持有 stale
|
|
1852
|
+
// runtime 引用,必须重建(dispose 尽力而为,不阻塞 prompt)。
|
|
1853
|
+
if (!this.mediaRuntimeExecutor || this.mediaRuntimeExecutorRt !== this.opencodeClient) {
|
|
1854
|
+
void this.mediaRuntimeExecutor?.dispose().catch(() => { });
|
|
1855
|
+
this.mediaRuntimeExecutor = (0, media_runtime_executor_1.createMediaRuntimeExecutor)(this.opencodeClient);
|
|
1856
|
+
this.mediaRuntimeExecutorRt = this.opencodeClient;
|
|
1857
|
+
}
|
|
1858
|
+
return this.mediaRuntimeExecutor.prompt;
|
|
1859
|
+
}
|
|
1860
|
+
return undefined;
|
|
1861
|
+
},
|
|
1862
|
+
});
|
|
1806
1863
|
},
|
|
1807
1864
|
});
|
|
1808
1865
|
// Determine workspace name: if projectDir resolves to a 'gateway' subdirectory,
|
|
@@ -1911,6 +1968,9 @@ class MafwScheduler {
|
|
|
1911
1968
|
const collector = new TrajectoryCollector(trajStore, this.getGatewayDb(), projectDir, () => config_1.config.trajectory.retentionDays);
|
|
1912
1969
|
collector.setRoleFor((sid) => this.internalSessionRoles.get(sid) ?? null);
|
|
1913
1970
|
this.trajectoryCollector = collector;
|
|
1971
|
+
const pruned = this.getGatewayDb().kvPruneOlderThan('internal-session', INTERNAL_SESSION_TTL_DAYS);
|
|
1972
|
+
if (pruned > 0)
|
|
1973
|
+
logger_1.log.info(`[Scheduler] pruned ${pruned} stale internal-session kv entries (> ${INTERNAL_SESSION_TTL_DAYS}d)`);
|
|
1914
1974
|
const restored = this.getGatewayDb().kvAll('internal-session');
|
|
1915
1975
|
for (const { key: sid, value } of restored) {
|
|
1916
1976
|
if (value?.role && !this.internalSessionRoles.has(sid)) {
|
|
@@ -1927,10 +1987,12 @@ class MafwScheduler {
|
|
|
1927
1987
|
const pluginsDir = path.join(os.homedir(), '.mafw', 'usage-plugins');
|
|
1928
1988
|
const builtinPluginsDir = path.join(__dirname, 'usage', 'builtin-plugins');
|
|
1929
1989
|
const disabledPlugins = Array.isArray(config_1.config.usage?.disabledPlugins) ? config_1.config.usage.disabledPlugins : [];
|
|
1990
|
+
const statsProvider = createUsageStatsProvider(trajStore);
|
|
1991
|
+
this.usageStatsProvider = statsProvider;
|
|
1930
1992
|
const pluginLoader = new PluginLoader(pluginsDir, [], {
|
|
1931
1993
|
builtinPluginsDir,
|
|
1932
1994
|
disabledPlugins,
|
|
1933
|
-
usageStats:
|
|
1995
|
+
usageStats: statsProvider,
|
|
1934
1996
|
// inline provider key 兜底(auth.json 无条目的自建 provider,如 gateway)——
|
|
1935
1997
|
// thunk 惰性求值,opencodeClient 此时尚未初始化也不影响。
|
|
1936
1998
|
resolveInlineApiKey: async (providerID) => {
|
|
@@ -1946,6 +2008,7 @@ class MafwScheduler {
|
|
|
1946
2008
|
});
|
|
1947
2009
|
await pluginLoader.init();
|
|
1948
2010
|
this.pluginLoader = pluginLoader;
|
|
2011
|
+
this.pluginHost?.bindUsage((entries) => pluginLoader.setPackageEntries(entries));
|
|
1949
2012
|
const { UsagePoller } = require('./usage/usage-poller');
|
|
1950
2013
|
this.usagePoller = new UsagePoller(trajStore, () => config_1.config.usage.limits, () => config_1.config.usage.budgets, pluginLoader);
|
|
1951
2014
|
logger_1.log.info('[Trajectory] store initialized');
|
|
@@ -2050,6 +2113,18 @@ class MafwScheduler {
|
|
|
2050
2113
|
handleServeExit(code) {
|
|
2051
2114
|
if (!this.serveOwned || this.serveRecovering)
|
|
2052
2115
|
return;
|
|
2116
|
+
// After a hot-switch to an in-process/external runtime, the (still owned)
|
|
2117
|
+
// opencode serve is no longer the active backend: its exit must NOT trigger
|
|
2118
|
+
// recovery — pi has no spawnServe, so the orchestrator would fall into the
|
|
2119
|
+
// supervisor's kill+spawn path and retry forever (killing whatever listens
|
|
2120
|
+
// on the serve port each cycle). A later switch back re-ensures via
|
|
2121
|
+
// ensureServeForBuiltinRuntime (probe → adopt or spawn).
|
|
2122
|
+
const rt = this.opencodeClient;
|
|
2123
|
+
if (rt?.external || !rt?.agentProcess?.spawnServe) {
|
|
2124
|
+
this.serveOwned = false;
|
|
2125
|
+
logger_1.log.info('[Scheduler] owned serve exited after runtime switch; recovery skipped (active runtime does not own serve)');
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2053
2128
|
this.serveInstance = undefined;
|
|
2054
2129
|
if (this.serveStableTimer) {
|
|
2055
2130
|
clearTimeout(this.serveStableTimer);
|
|
@@ -2122,6 +2197,10 @@ class MafwScheduler {
|
|
|
2122
2197
|
if (!this.running || this.serveRecovering)
|
|
2123
2198
|
return;
|
|
2124
2199
|
if (await this.isServeHealthy()) {
|
|
2200
|
+
if (failures > 0 || this.serveExternalDownNotified) {
|
|
2201
|
+
logger_1.log.info('[Scheduler] Serve back online');
|
|
2202
|
+
}
|
|
2203
|
+
this.serveExternalDownNotified = false;
|
|
2125
2204
|
failures = 0;
|
|
2126
2205
|
return;
|
|
2127
2206
|
}
|
|
@@ -2130,7 +2209,10 @@ class MafwScheduler {
|
|
|
2130
2209
|
if (failures >= this.serveWatchdogFailures) {
|
|
2131
2210
|
failures = 0;
|
|
2132
2211
|
if (this.opencodeClient?.external) {
|
|
2133
|
-
|
|
2212
|
+
if (!this.serveExternalDownNotified) {
|
|
2213
|
+
this.serveExternalDownNotified = true;
|
|
2214
|
+
logger_1.log.error('[Scheduler] External serve unreachable — the active runtime does not own the serve process, so it will NOT be respawned. Serve-dependent features (provider list, sessions, approvals) are degraded.');
|
|
2215
|
+
}
|
|
2134
2216
|
try {
|
|
2135
2217
|
await this.subscribeToEvents();
|
|
2136
2218
|
}
|
|
@@ -3129,6 +3211,32 @@ class MafwScheduler {
|
|
|
3129
3211
|
res.end(JSON.stringify({ ok: true, text: answer }));
|
|
3130
3212
|
return;
|
|
3131
3213
|
}
|
|
3214
|
+
if (cmd === "waitwhat") {
|
|
3215
|
+
if (!sessionID) {
|
|
3216
|
+
res.writeHead(400);
|
|
3217
|
+
res.end(JSON.stringify({ ok: false, error: "sessionID required" }));
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
if (!this.opencodeClient) {
|
|
3221
|
+
res.writeHead(503);
|
|
3222
|
+
res.end(JSON.stringify({ ok: false, error: "LLM client not available" }));
|
|
3223
|
+
return;
|
|
3224
|
+
}
|
|
3225
|
+
const result = await (0, waitwhat_command_1.runWaitwhat)(String(sessionID), {
|
|
3226
|
+
listMessages: async (sid) => {
|
|
3227
|
+
const r = await this.opencodeClient.session.messages({ sessionID: sid, limit: 50 });
|
|
3228
|
+
return (r?.data || []);
|
|
3229
|
+
},
|
|
3230
|
+
promptAsync: async (sid, text) => {
|
|
3231
|
+
await this.opencodeClient.session.promptAsync({ sessionID: sid, parts: [{ type: "text", text }] });
|
|
3232
|
+
},
|
|
3233
|
+
});
|
|
3234
|
+
res.writeHead(result.ok ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
3235
|
+
res.end(JSON.stringify(result.ok
|
|
3236
|
+
? { ok: true, message: '重述请求已发送到当前会话' }
|
|
3237
|
+
: { ok: false, error: result.error }));
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3132
3240
|
if (cmd === "status") {
|
|
3133
3241
|
const statusPath = path.join(this.mafwDir, 'STATUS.md');
|
|
3134
3242
|
const text = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf-8') : 'No active Goals. Use /goal to create one.';
|
|
@@ -3600,7 +3708,8 @@ class MafwScheduler {
|
|
|
3600
3708
|
});
|
|
3601
3709
|
// 持久化到磁盘(写队列防并发覆盖)
|
|
3602
3710
|
await this.persistRegistry();
|
|
3603
|
-
|
|
3711
|
+
// 桌面 renderer 依此事件刷新 Rail 项目列表(否则只在 gateway ready 时拉一次)。
|
|
3712
|
+
this.broadcast((0, event_broadcast_1.projectRegisteredEvent)(projectDir));
|
|
3604
3713
|
if (this.opencodeClient) {
|
|
3605
3714
|
try {
|
|
3606
3715
|
await this.ensureManagerSession(projectDir, mafwDir);
|
|
@@ -3737,8 +3846,14 @@ class MafwScheduler {
|
|
|
3737
3846
|
const filter = parsedUrl.searchParams.get('projectDir') || '';
|
|
3738
3847
|
try {
|
|
3739
3848
|
const all = this.getGatewayDb().kvAll('manager-session');
|
|
3849
|
+
// Per-runtime slots: surface only the ACTIVE runtime's manager
|
|
3850
|
+
// slot per project. Other runtimes' slots stay dormant (resumed
|
|
3851
|
+
// when switching back) — nothing is deleted here.
|
|
3852
|
+
const live = all
|
|
3853
|
+
.map((e) => ({ key: e.key, value: (0, manager_session_runtime_1.readManagerSlot)(e.value, this.runtimeName) }))
|
|
3854
|
+
.filter((e) => e.value !== null);
|
|
3740
3855
|
if (filter) {
|
|
3741
|
-
const found =
|
|
3856
|
+
const found = live.find((e) => e.key === filter);
|
|
3742
3857
|
if (!found) {
|
|
3743
3858
|
res.writeHead(404);
|
|
3744
3859
|
res.end(JSON.stringify({ error: 'No manager session for project' }));
|
|
@@ -3752,13 +3867,13 @@ class MafwScheduler {
|
|
|
3752
3867
|
}));
|
|
3753
3868
|
return;
|
|
3754
3869
|
}
|
|
3755
|
-
if (
|
|
3870
|
+
if (live.length === 0) {
|
|
3756
3871
|
res.writeHead(404);
|
|
3757
3872
|
res.end(JSON.stringify({ error: 'No manager session' }));
|
|
3758
3873
|
return;
|
|
3759
3874
|
}
|
|
3760
3875
|
res.writeHead(200);
|
|
3761
|
-
res.end(JSON.stringify(
|
|
3876
|
+
res.end(JSON.stringify(live.map((e) => ({
|
|
3762
3877
|
projectDir: e.key,
|
|
3763
3878
|
sessionId: e.value.sessionId,
|
|
3764
3879
|
createdAt: e.value.createdAt || null,
|
|
@@ -3969,17 +4084,15 @@ class MafwScheduler {
|
|
|
3969
4084
|
return;
|
|
3970
4085
|
}
|
|
3971
4086
|
// 鈹€鈹€ Question endpoints (AskCard 鈹€ proxy to native opencode Question API) 鈹€鈹€
|
|
3972
|
-
// GET /api/questions
|
|
4087
|
+
// GET /api/questions ── list pending questions(契约:session.question.list)
|
|
3973
4088
|
if (req.url?.match(/^\/api\/questions(?:\?|$)/) && req.method === 'GET') {
|
|
3974
4089
|
if (this.capGuardQuestion(res))
|
|
3975
4090
|
return;
|
|
3976
4091
|
try {
|
|
3977
|
-
const dir = new URL(req.url, this.serveUrl).searchParams.get('directory') || this.projectDir ||
|
|
3978
|
-
const
|
|
3979
|
-
|
|
3980
|
-
});
|
|
3981
|
-
const items = await r.json();
|
|
3982
|
-
res.end(JSON.stringify({ items }));
|
|
4092
|
+
const dir = new URL(req.url, this.serveUrl).searchParams.get('directory') || this.projectDir || undefined;
|
|
4093
|
+
const question = this.opencodeClient?.session?.question;
|
|
4094
|
+
const items = question ? await question.list(dir ? { directory: dir } : undefined) : [];
|
|
4095
|
+
res.end(JSON.stringify({ items: items ?? [] }));
|
|
3983
4096
|
}
|
|
3984
4097
|
catch (err) {
|
|
3985
4098
|
logger_1.log.error('[Question] list error:', err.message);
|
|
@@ -3987,20 +4100,34 @@ class MafwScheduler {
|
|
|
3987
4100
|
}
|
|
3988
4101
|
return;
|
|
3989
4102
|
}
|
|
3990
|
-
// POST /api/questions/{id}/reply
|
|
4103
|
+
// POST /api/questions/{id}/reply ── { answers: string[][] }(契约 + workspace 重试)
|
|
3991
4104
|
const qReplyMatch = req.url?.match(/^\/api\/questions\/([^/]+)\/reply(?:\?|$)/);
|
|
3992
4105
|
if (qReplyMatch && req.method === 'POST') {
|
|
3993
4106
|
if (this.capGuardQuestion(res))
|
|
3994
4107
|
return;
|
|
4108
|
+
const question = this.opencodeClient?.session?.question;
|
|
4109
|
+
if (!question) {
|
|
4110
|
+
res.writeHead(503);
|
|
4111
|
+
res.end(JSON.stringify({ status: 'error', error: 'question API not available on this runtime' }));
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
3995
4114
|
try {
|
|
3996
4115
|
const body = JSON.parse(await readBody(req));
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4116
|
+
// workspace 路由:默认(SDK 自带 projectDir)→ 各注册项目逐试
|
|
4117
|
+
const dirs = [undefined, this.projectDir, ...this.registeredProjects.keys()];
|
|
4118
|
+
let lastErr = null;
|
|
4119
|
+
for (const dir of [...new Set(dirs)]) {
|
|
4120
|
+
try {
|
|
4121
|
+
await question.reply({ requestID: qReplyMatch[1], answers: body.answers, ...(dir ? { directory: dir } : {}) });
|
|
4122
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
4123
|
+
return;
|
|
4124
|
+
}
|
|
4125
|
+
catch (err) {
|
|
4126
|
+
lastErr = err;
|
|
4127
|
+
}
|
|
4002
4128
|
}
|
|
4003
|
-
res.
|
|
4129
|
+
res.writeHead(400);
|
|
4130
|
+
res.end(JSON.stringify({ status: 'error', error: lastErr?.message ?? 'question reply failed on all workspaces' }));
|
|
4004
4131
|
}
|
|
4005
4132
|
catch (err) {
|
|
4006
4133
|
logger_1.log.error('[Question] reply error:', err.message);
|
|
@@ -4009,19 +4136,32 @@ class MafwScheduler {
|
|
|
4009
4136
|
}
|
|
4010
4137
|
return;
|
|
4011
4138
|
}
|
|
4012
|
-
// POST /api/questions/{id}/reject
|
|
4139
|
+
// POST /api/questions/{id}/reject(契约 + workspace 重试)
|
|
4013
4140
|
const qRejectMatch = req.url?.match(/^\/api\/questions\/([^/]+)\/reject(?:\?|$)/);
|
|
4014
4141
|
if (qRejectMatch && req.method === 'POST') {
|
|
4015
4142
|
if (this.capGuardQuestion(res))
|
|
4016
4143
|
return;
|
|
4144
|
+
const question = this.opencodeClient?.session?.question;
|
|
4145
|
+
if (!question) {
|
|
4146
|
+
res.writeHead(503);
|
|
4147
|
+
res.end(JSON.stringify({ status: 'error', error: 'question API not available on this runtime' }));
|
|
4148
|
+
return;
|
|
4149
|
+
}
|
|
4017
4150
|
try {
|
|
4018
|
-
const
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4151
|
+
const dirs = [undefined, this.projectDir, ...this.registeredProjects.keys()];
|
|
4152
|
+
let lastErr = null;
|
|
4153
|
+
for (const dir of [...new Set(dirs)]) {
|
|
4154
|
+
try {
|
|
4155
|
+
await question.reject({ requestID: qRejectMatch[1], ...(dir ? { directory: dir } : {}) });
|
|
4156
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
4157
|
+
return;
|
|
4158
|
+
}
|
|
4159
|
+
catch (err) {
|
|
4160
|
+
lastErr = err;
|
|
4161
|
+
}
|
|
4023
4162
|
}
|
|
4024
|
-
res.
|
|
4163
|
+
res.writeHead(400);
|
|
4164
|
+
res.end(JSON.stringify({ status: 'error', error: lastErr?.message ?? 'question reject failed on all workspaces' }));
|
|
4025
4165
|
}
|
|
4026
4166
|
catch (err) {
|
|
4027
4167
|
logger_1.log.error('[Question] reject error:', err.message);
|
|
@@ -4031,17 +4171,16 @@ class MafwScheduler {
|
|
|
4031
4171
|
return;
|
|
4032
4172
|
}
|
|
4033
4173
|
// 鈹€鈹€ Permission endpoints (PermissionCard 鈹€ proxy to native opencode Permission API) 鈹€鈹€
|
|
4034
|
-
// GET /api/permissions
|
|
4174
|
+
// GET /api/permissions ── list pending permission requests(契约:session.permissionList)
|
|
4035
4175
|
if (req.url?.match(/^\/api\/permissions(?:\?|$)/) && req.method === 'GET') {
|
|
4036
4176
|
if (this.capGuard(res, 'nativeApprovals'))
|
|
4037
4177
|
return;
|
|
4038
4178
|
try {
|
|
4039
|
-
const dir = new URL(req.url, this.serveUrl).searchParams.get('directory') || this.projectDir ||
|
|
4040
|
-
const
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
res.end(JSON.stringify({ items }));
|
|
4179
|
+
const dir = new URL(req.url, this.serveUrl).searchParams.get('directory') || this.projectDir || undefined;
|
|
4180
|
+
const items = this.opencodeClient?.session?.permissionList
|
|
4181
|
+
? await this.opencodeClient.session.permissionList(dir ? { directory: dir } : undefined)
|
|
4182
|
+
: [];
|
|
4183
|
+
res.end(JSON.stringify({ items: items ?? [] }));
|
|
4045
4184
|
}
|
|
4046
4185
|
catch (err) {
|
|
4047
4186
|
logger_1.log.error('[Permission] list error:', err.message);
|
|
@@ -4089,6 +4228,9 @@ class MafwScheduler {
|
|
|
4089
4228
|
if (this.automationEngine)
|
|
4090
4229
|
this.automationEngine.setRuntimeClient(rt);
|
|
4091
4230
|
await this.resubscribeEvents(`runtime switched to '${rt.name}'`);
|
|
4231
|
+
// Desktop hint: a runtime switch swaps the session storage backend
|
|
4232
|
+
// (opencode SQLite vs pi), so cached session lists are stale.
|
|
4233
|
+
this.broadcast({ type: 'runtime_switched', runtime: rt.name, previous: prev?.name ?? null });
|
|
4092
4234
|
if (prev && prev.dispose) {
|
|
4093
4235
|
try {
|
|
4094
4236
|
await prev.dispose();
|
|
@@ -4144,6 +4286,23 @@ class MafwScheduler {
|
|
|
4144
4286
|
usage: config_1.config.resolvePath('usage-plugins'),
|
|
4145
4287
|
ui: process.env.MAFW_UI_PLUGINS_DIR || path.join(os.homedir(), '.mafw', 'ui-plugins'),
|
|
4146
4288
|
},
|
|
4289
|
+
builtinEntries: () => {
|
|
4290
|
+
const entries = [];
|
|
4291
|
+
const rt = (name) => ({ type: 'runtime', name, file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
|
|
4292
|
+
// opencode 经 registerBuiltin 注册,getBuiltinNames 已含——不再手工 push(防重复)
|
|
4293
|
+
for (const name of this.runtimeLoader?.getBuiltinNames?.() ?? [])
|
|
4294
|
+
entries.push(rt(name));
|
|
4295
|
+
for (const name of this.mediaPluginLoader?.getBuiltinEngineNames?.() ?? []) {
|
|
4296
|
+
entries.push({ type: 'media', name, file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
|
|
4297
|
+
}
|
|
4298
|
+
const usageState = this.pluginLoader?.getState?.() ?? [];
|
|
4299
|
+
for (const s of usageState) {
|
|
4300
|
+
if (s.builtin && s.status === 'ok' && s.name) {
|
|
4301
|
+
entries.push({ type: 'usage', name: s.name, file: s.file, status: 'enabled', size: 0, mtime: '', pluginType: s.pluginType });
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
return entries;
|
|
4305
|
+
},
|
|
4147
4306
|
getErrors: (type) => {
|
|
4148
4307
|
const stateOf = (loader) => (loader && typeof loader.getState === 'function' ? loader.getState() : []);
|
|
4149
4308
|
const source = type === 'runtime' ? this.runtimeLoader : type === 'media' ? this.mediaPluginLoader : type === 'usage' ? this.pluginLoader : null;
|
|
@@ -4164,8 +4323,19 @@ class MafwScheduler {
|
|
|
4164
4323
|
await this.pluginLoader?.reload();
|
|
4165
4324
|
// ui: desktop main fs.watch picks it up automatically
|
|
4166
4325
|
},
|
|
4326
|
+
getPackages: () => this.pluginHost?.getState() ?? [],
|
|
4167
4327
|
},
|
|
4168
4328
|
};
|
|
4329
|
+
try {
|
|
4330
|
+
const cleaned = (0, hub_1.cleanupExamples)(pluginHubDeps.hub);
|
|
4331
|
+
if (cleaned.removed.length)
|
|
4332
|
+
logger_1.log.info(`[PluginsHub] removed stale examples: ${cleaned.removed.length}`);
|
|
4333
|
+
if (cleaned.failed.length)
|
|
4334
|
+
logger_1.log.warn(`[PluginsHub] cleanupExamples failed: ${cleaned.failed.join(', ')}`);
|
|
4335
|
+
}
|
|
4336
|
+
catch (err) {
|
|
4337
|
+
logger_1.log.warn(`[PluginsHub] cleanupExamples error: ${err.message}`);
|
|
4338
|
+
}
|
|
4169
4339
|
if (req.method === 'GET' && req.url?.match(/^\/api\/plugins(?:\?|$)/)) {
|
|
4170
4340
|
await (0, plugins_1.handlePluginsList)(req, res, pluginHubDeps);
|
|
4171
4341
|
return;
|
|
@@ -4281,20 +4451,37 @@ class MafwScheduler {
|
|
|
4281
4451
|
}
|
|
4282
4452
|
return;
|
|
4283
4453
|
}
|
|
4284
|
-
// POST /api/permissions/{id}/reply
|
|
4454
|
+
// POST /api/permissions/{id}/reply ── { reply, message? }(契约:列表反查 sessionID → permissionReply)
|
|
4285
4455
|
const pReplyMatch = req.url?.match(/^\/api\/permissions\/([^/]+)\/reply(?:\?|$)/);
|
|
4286
4456
|
if (pReplyMatch && req.method === 'POST') {
|
|
4287
4457
|
if (this.capGuard(res, 'nativeApprovals'))
|
|
4288
4458
|
return;
|
|
4459
|
+
const runtime = this.opencodeClient;
|
|
4460
|
+
if (!runtime?.session?.permissionList || !runtime?.session?.permissionReply) {
|
|
4461
|
+
res.writeHead(503);
|
|
4462
|
+
res.end(JSON.stringify({ status: 'error', error: 'permission API not available on this runtime' }));
|
|
4463
|
+
return;
|
|
4464
|
+
}
|
|
4289
4465
|
try {
|
|
4290
4466
|
const body = JSON.parse(await readBody(req));
|
|
4291
|
-
const
|
|
4292
|
-
if (
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4467
|
+
const reply = body.reply;
|
|
4468
|
+
if (reply !== 'once' && reply !== 'always' && reply !== 'reject') {
|
|
4469
|
+
res.writeHead(400);
|
|
4470
|
+
res.end(JSON.stringify({ status: 'error', error: "reply must be 'once'|'always'|'reject'" }));
|
|
4471
|
+
return;
|
|
4472
|
+
}
|
|
4473
|
+
// 反查 sessionID:pending 列表项携带(跨 runtime 形状一致)
|
|
4474
|
+
const pending = await runtime.session.permissionList();
|
|
4475
|
+
const found = (Array.isArray(pending) ? pending : []).find((p) => p?.id === pReplyMatch[1]);
|
|
4476
|
+
if (!found?.sessionID) {
|
|
4477
|
+
res.writeHead(404);
|
|
4478
|
+
res.end(JSON.stringify({ status: 'error', error: 'permission request not found' }));
|
|
4479
|
+
return;
|
|
4480
|
+
}
|
|
4481
|
+
const ok = await runtime.session.permissionReply(found.sessionID, pReplyMatch[1], reply, body.message);
|
|
4482
|
+
if (!ok) {
|
|
4483
|
+
res.writeHead(404);
|
|
4484
|
+
res.end(JSON.stringify({ status: 'error', error: 'permission request not found' }));
|
|
4298
4485
|
return;
|
|
4299
4486
|
}
|
|
4300
4487
|
res.end(JSON.stringify({ status: 'ok' }));
|
|
@@ -5142,7 +5329,10 @@ class MafwScheduler {
|
|
|
5142
5329
|
session_id: sessionID,
|
|
5143
5330
|
turn_id: turnId,
|
|
5144
5331
|
source: source,
|
|
5145
|
-
|
|
5332
|
+
// Redact secrets once at capture so every downstream consumer
|
|
5333
|
+
// (turnCompress transcripts, worker prompts, archive) only
|
|
5334
|
+
// ever sees redacted content.
|
|
5335
|
+
content: (0, redact_1.redactSecrets)(content).slice(0, 100_000),
|
|
5146
5336
|
failure,
|
|
5147
5337
|
});
|
|
5148
5338
|
// Async scan prefetch: precompute semantic recall for this turn
|
|
@@ -5162,6 +5352,11 @@ class MafwScheduler {
|
|
|
5162
5352
|
});
|
|
5163
5353
|
return;
|
|
5164
5354
|
}
|
|
5355
|
+
// POST /api/events —— 事件发布(插件/外部程序 → 全 UI 通道;契约见 routes/event-publish.ts)
|
|
5356
|
+
if (req.url && req.url.startsWith('/api/events') && req.method === 'POST') {
|
|
5357
|
+
await (0, event_publish_1.handleEventPublish)({ broadcast: (e) => this.broadcast(e) }, req, res);
|
|
5358
|
+
return;
|
|
5359
|
+
}
|
|
5165
5360
|
// SSE 事件(→ Dashboard / Chat)
|
|
5166
5361
|
if (req.url && req.url.startsWith('/api/events') && req.method === 'GET') {
|
|
5167
5362
|
const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
@@ -5433,33 +5628,6 @@ class MafwScheduler {
|
|
|
5433
5628
|
logger_1.log.info(`[Scheduler] Recovered ${filtered.length} registered projects`);
|
|
5434
5629
|
}
|
|
5435
5630
|
}
|
|
5436
|
-
async persistConfig() {
|
|
5437
|
-
this.configWriteQueue = this.configWriteQueue.then(async () => {
|
|
5438
|
-
const dir = path.dirname(this.configPath);
|
|
5439
|
-
if (!fs.existsSync(dir))
|
|
5440
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
5441
|
-
let config = {};
|
|
5442
|
-
if (fs.existsSync(this.configPath)) {
|
|
5443
|
-
config = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
|
|
5444
|
-
}
|
|
5445
|
-
config.projects = Object.fromEntries(this.registeredProjects);
|
|
5446
|
-
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
|
|
5447
|
-
});
|
|
5448
|
-
await this.configWriteQueue;
|
|
5449
|
-
}
|
|
5450
|
-
async recoverConfig() {
|
|
5451
|
-
if (fs.existsSync(this.configPath)) {
|
|
5452
|
-
try {
|
|
5453
|
-
const config = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
|
|
5454
|
-
const entries = Object.entries((config.projects || {}))
|
|
5455
|
-
.filter(([dir]) => !this.isUserDataDir(dir));
|
|
5456
|
-
this.registeredProjects = new Map(entries);
|
|
5457
|
-
}
|
|
5458
|
-
catch (err) {
|
|
5459
|
-
logger_1.log.error(`[Scheduler] Failed to recover config: ${err.message}`);
|
|
5460
|
-
}
|
|
5461
|
-
}
|
|
5462
|
-
}
|
|
5463
5631
|
// ── 4. 轮询(降级兜底 + autoresume) ──
|
|
5464
5632
|
startBackupPolling() {
|
|
5465
5633
|
const interval = config_1.config.timeouts.backupPollInterval;
|
|
@@ -6190,6 +6358,14 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6190
6358
|
});
|
|
6191
6359
|
return run;
|
|
6192
6360
|
}
|
|
6361
|
+
// Slot-based read of a manager-session kv entry: each runtime owns a slot
|
|
6362
|
+
// in the value (byRuntime), so switching runtimes never drops a topic —
|
|
6363
|
+
// switching back resumes the previous manager. Legacy v1 entries are
|
|
6364
|
+
// inferred by session id prefix and only surface under their own runtime.
|
|
6365
|
+
// See manager-session-runtime.ts.
|
|
6366
|
+
readManagerSessionEntry(projectDir) {
|
|
6367
|
+
return (0, manager_session_runtime_1.readManagerSlot)(this.getGatewayDb().kvGet('manager-session', projectDir), this.runtimeName);
|
|
6368
|
+
}
|
|
6193
6369
|
// Serialized rotate: joins any in-flight ensure/create for the same project
|
|
6194
6370
|
// so rotate and lazy-init never double-create (spec §4).
|
|
6195
6371
|
async runManagerExclusive(projectDir, fn) {
|
|
@@ -6206,7 +6382,7 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6206
6382
|
// The inflight map is shared with ensureManagerSession, whose joiners
|
|
6207
6383
|
// expect the promise to resolve to a session id string — map the rotate
|
|
6208
6384
|
// result onto the resulting kv entry.
|
|
6209
|
-
const mapped = run.then(() => this.
|
|
6385
|
+
const mapped = run.then(() => this.readManagerSessionEntry(projectDir)?.sessionId ?? '', () => '');
|
|
6210
6386
|
this.managerSessionInflight.set(projectDir, mapped);
|
|
6211
6387
|
try {
|
|
6212
6388
|
return await run;
|
|
@@ -6221,7 +6397,7 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6221
6397
|
throw new Error('opencodeClient not available');
|
|
6222
6398
|
// Manager identity lives in the gateway DB (kv_store), so it survives
|
|
6223
6399
|
// project-directory churn and never gets orphaned by directory moves.
|
|
6224
|
-
const existing = this.
|
|
6400
|
+
const existing = this.readManagerSessionEntry(projectDir);
|
|
6225
6401
|
if (existing?.sessionId) {
|
|
6226
6402
|
await this.sdkSession.registerExternal(existing.sessionId, projectDir, {
|
|
6227
6403
|
mafw: { role: 'manager', pinned: true, exemptFromTrim: true, exemptFromEvict: true, exemptFromArchive: true },
|
|
@@ -6237,7 +6413,7 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6237
6413
|
throw new Error('Failed to create manager session: no id returned');
|
|
6238
6414
|
}
|
|
6239
6415
|
const createdAt = new Date().toISOString();
|
|
6240
|
-
this.getGatewayDb().kvSet('manager-session', projectDir, { sessionId, createdAt });
|
|
6416
|
+
this.getGatewayDb().kvSet('manager-session', projectDir, (0, manager_session_runtime_1.writeManagerSlot)(this.getGatewayDb().kvGet('manager-session', projectDir), this.runtimeName, { sessionId, createdAt }));
|
|
6241
6417
|
this.registerInternalSession(sessionId, 'manager');
|
|
6242
6418
|
try {
|
|
6243
6419
|
await this.sdkSession.registerExternal(sessionId, projectDir, {
|
|
@@ -6269,7 +6445,7 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6269
6445
|
}
|
|
6270
6446
|
rotateDeps() {
|
|
6271
6447
|
return {
|
|
6272
|
-
getManagerSession: (pd) => this.
|
|
6448
|
+
getManagerSession: (pd) => this.readManagerSessionEntry(pd),
|
|
6273
6449
|
lock: (pd, fn) => this.runManagerExclusive(pd, fn),
|
|
6274
6450
|
ensure: (pd) => this.ensureManagerSession(pd, this.mafwDirFor(pd)),
|
|
6275
6451
|
downgrade: async (sid) => {
|
|
@@ -6316,7 +6492,7 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6316
6492
|
return undefined;
|
|
6317
6493
|
if (!this.milestonePush) {
|
|
6318
6494
|
this.milestonePush = new milestone_push_1.MilestonePushNotifier({
|
|
6319
|
-
getManagerSession: (pd) => this.
|
|
6495
|
+
getManagerSession: (pd) => this.readManagerSessionEntry(pd),
|
|
6320
6496
|
wasNotified: (key) => !!this.getGatewayDb().kvGet('milestone-notified', key),
|
|
6321
6497
|
markNotified: (key) => this.getGatewayDb().kvSet('milestone-notified', key, { at: new Date().toISOString() }),
|
|
6322
6498
|
readGoalState: (goalId, pd) => {
|
|
@@ -6345,7 +6521,7 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6345
6521
|
const sessionId = session.id;
|
|
6346
6522
|
if (!sessionId)
|
|
6347
6523
|
throw new Error('Failed to create manager session: no id returned');
|
|
6348
|
-
this.getGatewayDb().kvSet('manager-session', projectDir, { sessionId, createdAt: new Date().toISOString() });
|
|
6524
|
+
this.getGatewayDb().kvSet('manager-session', projectDir, (0, manager_session_runtime_1.writeManagerSlot)(this.getGatewayDb().kvGet('manager-session', projectDir), this.runtimeName, { sessionId, createdAt: new Date().toISOString() }));
|
|
6349
6525
|
this.registerInternalSession(sessionId, 'manager');
|
|
6350
6526
|
await this.sdkSession.registerExternal(sessionId, projectDir, {
|
|
6351
6527
|
mafw: { role: 'manager', pinned: true, exemptFromTrim: true, exemptFromEvict: true, exemptFromArchive: true },
|