@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.
- 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 +465 -440
- package/gateway/dist/media/media-plugin-loader.js +25 -1
- package/gateway/dist/media/resolve-prompt.js +20 -0
- package/gateway/dist/memory/gateway-db.js +23 -0
- package/gateway/dist/opencode-adapter.js +53 -4
- 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 +4 -1
- package/gateway/dist/routes/registry.js +125 -0
- package/gateway/dist/routes/route-catalog.js +192 -0
- package/gateway/dist/routes/triage-dismiss.js +32 -0
- package/gateway/dist/routes/waitwhat-command.js +43 -0
- package/gateway/dist/routes/wave1-handlers.js +62 -0
- package/gateway/dist/routes/wave2-handlers.js +47 -0
- package/gateway/dist/runtime/contract.js +4 -1
- package/gateway/dist/runtime/event-broadcast.js +8 -0
- package/gateway/dist/runtime/loader.js +22 -1
- package/gateway/dist/runtime/normalize.js +13 -0
- package/gateway/dist/runtime/opencode-runtime.js +18 -2
- 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 +19 -20
- 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 +91 -36
- package/gateway/dist/usage/plugin-context.js +42 -2
- package/gateway/dist/usage/plugin-loader.js +32 -2
- package/gateway/package.json +2 -1
- package/package.json +3 -1
- package/packages/tui/dist/cli.js +114 -51
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");
|
|
@@ -94,7 +97,6 @@ const push_gateway_1 = require("./mobile/push-gateway");
|
|
|
94
97
|
const device_store_1 = require("./mobile/device-store");
|
|
95
98
|
const pairing_1 = require("./mobile/pairing");
|
|
96
99
|
const tray_1 = require("./tray");
|
|
97
|
-
const serve_sidecar_1 = require("./runtime/serve-sidecar");
|
|
98
100
|
const self_update_1 = require("./self-update");
|
|
99
101
|
const step_inject_1 = require("./recall/step-inject");
|
|
100
102
|
const inject_format_1 = require("./recall/inject-format");
|
|
@@ -103,24 +105,19 @@ const event_broadcast_1 = require("./runtime/event-broadcast");
|
|
|
103
105
|
const budget_guard_1 = require("./core/budget-guard");
|
|
104
106
|
const goal_budget_1 = require("./core/goal-budget");
|
|
105
107
|
const contract_1 = require("./runtime/contract");
|
|
108
|
+
const validate_1 = require("./runtime/validate");
|
|
106
109
|
const loader_1 = require("./runtime/loader");
|
|
107
110
|
const pi_runtime_1 = require("./runtime/plugins/pi-runtime");
|
|
108
111
|
const permission_1 = require("./routes/permission");
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const hub_1 = require("./plugins/hub");
|
|
112
|
-
const restart_agent_1 = require("./routes/restart-agent");
|
|
113
|
-
const session_mutations_1 = require("./routes/session-mutations");
|
|
112
|
+
const package_host_1 = require("./plugins/package-host");
|
|
113
|
+
const package_context_1 = require("./plugins/package-context");
|
|
114
114
|
const serve_supervisor_1 = require("./runtime/serve-supervisor");
|
|
115
115
|
const serve_for_runtime_1 = require("./runtime/serve-for-runtime");
|
|
116
|
-
const media_switch_1 = require("./routes/media-switch");
|
|
117
|
-
const usage_plugins_1 = require("./routes/usage-plugins");
|
|
118
116
|
const model_stats_1 = require("./usage/model-stats");
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const embedding_config_1 = require("./routes/embedding-config");
|
|
117
|
+
const registry_1 = require("./routes/registry");
|
|
118
|
+
const route_catalog_1 = require("./routes/route-catalog");
|
|
119
|
+
const wave1_handlers_1 = require("./routes/wave1-handlers");
|
|
120
|
+
const wave2_handlers_1 = require("./routes/wave2-handlers");
|
|
124
121
|
const manager_rotate_1 = require("./routes/manager-rotate");
|
|
125
122
|
function readBody(req) {
|
|
126
123
|
return new Promise((resolve, reject) => {
|
|
@@ -146,6 +143,8 @@ async function isPortHealthy(port) {
|
|
|
146
143
|
return false;
|
|
147
144
|
}
|
|
148
145
|
}
|
|
146
|
+
const manager_session_runtime_1 = require("./core/manager/manager-session-runtime");
|
|
147
|
+
const INTERNAL_SESSION_TTL_DAYS = 7;
|
|
149
148
|
class MafwScheduler {
|
|
150
149
|
serveInstance;
|
|
151
150
|
serveUrl;
|
|
@@ -175,6 +174,8 @@ class MafwScheduler {
|
|
|
175
174
|
lastActiveBySession = new Map();
|
|
176
175
|
lastWriteBySession = new Map();
|
|
177
176
|
tokenWriterSession = null;
|
|
177
|
+
/** 畸形事件 warn 限频(每 runtime 30s 一次) */
|
|
178
|
+
malformedEventWarnAt = new Map();
|
|
178
179
|
stopTokenWatcher = null;
|
|
179
180
|
// Path 1 step-injection state (mark-before-async + dedup + queue). All
|
|
180
181
|
// per-session entries expire via TtlMap.
|
|
@@ -217,6 +218,12 @@ class MafwScheduler {
|
|
|
217
218
|
mcpEndpoint;
|
|
218
219
|
mcpStreamableEndpoint;
|
|
219
220
|
opencodeClient = null;
|
|
221
|
+
/**
|
|
222
|
+
* P4 shadow registry + P5 Wave 1 dispatch:catalog 全量登记(130 条,贡献 spec),
|
|
223
|
+
* Wave 1 已 attach 的 22 条在请求链顶端优先 dispatch(见 startApiServer),
|
|
224
|
+
* 未 attach 的 shadow 条目自然落回 legacy 内联链。
|
|
225
|
+
*/
|
|
226
|
+
routeRegistry = new registry_1.RouteRegistry().register(...(0, route_catalog_1.buildRouteCatalog)());
|
|
220
227
|
runtimeCaps = (0, contract_1.minimalCapabilities)();
|
|
221
228
|
runtimeName = 'opencode';
|
|
222
229
|
runtimeLoader;
|
|
@@ -236,6 +243,8 @@ class MafwScheduler {
|
|
|
236
243
|
mediaPluginLoader;
|
|
237
244
|
mediaRuntimeExecutor;
|
|
238
245
|
mediaRuntimeExecutorRt;
|
|
246
|
+
pluginHost;
|
|
247
|
+
usageStatsProvider;
|
|
239
248
|
ttsService;
|
|
240
249
|
kernels;
|
|
241
250
|
automationEngine;
|
|
@@ -259,51 +268,34 @@ class MafwScheduler {
|
|
|
259
268
|
this.registryPath = config_1.config.paths.registryFile;
|
|
260
269
|
this.chatSessions = new chat_sessions_1.ChatSessionManager();
|
|
261
270
|
this.serveSupervisor = (0, serve_supervisor_1.createServeSupervisor)({
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
271
|
+
// 全部经 runtime 契约且 late-bound(runtime 可热切换):
|
|
272
|
+
// managed = runtime 是否持有启停原语;health = 契约 healthCheck;
|
|
273
|
+
// baseUrl/killServe = runtime 自报地址与清场原语;spawn 不传 host/port。
|
|
274
|
+
managed: () => !!this.opencodeClient?.agentProcess?.spawnServe,
|
|
275
|
+
health: async () => {
|
|
276
|
+
try {
|
|
277
|
+
return (await this.opencodeClient?.healthCheck?.()) ?? false;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
baseUrl: () => this.opencodeClient?.getBaseUrl?.() ?? this.serveUrl,
|
|
284
|
+
killServe: () => this.opencodeClient?.agentProcess?.killServe?.(),
|
|
267
285
|
spawn: async (opts) => {
|
|
268
286
|
const spawnServe = this.opencodeClient?.agentProcess?.spawnServe;
|
|
269
287
|
if (!spawnServe) {
|
|
270
288
|
throw new Error('active runtime does not own a server process (agentProcess.spawnServe missing)');
|
|
271
289
|
}
|
|
272
|
-
const sidecar = await spawnServe({
|
|
273
|
-
host: opts.host,
|
|
274
|
-
port: opts.port,
|
|
275
|
-
timeoutMs: opts.timeoutMs,
|
|
276
|
-
});
|
|
290
|
+
const sidecar = await spawnServe({ timeoutMs: opts.timeoutMs });
|
|
277
291
|
return { url: sidecar.url, close: () => sidecar.close() };
|
|
278
292
|
},
|
|
279
|
-
probe: async (url) => {
|
|
280
|
-
try {
|
|
281
|
-
const res = await fetch(`${url}/global/health`, { signal: AbortSignal.timeout(3000) });
|
|
282
|
-
return res.ok;
|
|
283
|
-
}
|
|
284
|
-
catch {
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
},
|
|
288
293
|
});
|
|
289
294
|
}
|
|
290
295
|
get serveRunning() {
|
|
291
296
|
return !!this.serveInstance;
|
|
292
297
|
}
|
|
293
298
|
/** Check actual serve health via TCP connection, not just object existence. */
|
|
294
|
-
async checkServeActualHealth() {
|
|
295
|
-
try {
|
|
296
|
-
const url = `${this.serveUrl}/global/health`;
|
|
297
|
-
const res = await fetch(url, {
|
|
298
|
-
signal: AbortSignal.timeout(2000),
|
|
299
|
-
headers: { 'User-Agent': 'MAFW-Gateway-HealthCheck' }
|
|
300
|
-
});
|
|
301
|
-
return res.ok;
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
return false;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
299
|
async start() {
|
|
308
300
|
logger_1.log.info('MAFW Scheduler v5.0 starting...');
|
|
309
301
|
// Single-instance guard: if a healthy gateway already owns apiPort, this
|
|
@@ -360,6 +352,16 @@ class MafwScheduler {
|
|
|
360
352
|
await this.runtimeLoader.init();
|
|
361
353
|
// 内置插件注册:pi-coding-agent runtime(进程内 SDK 嵌入)
|
|
362
354
|
this.runtimeLoader.registerBuiltin('pi', pi_runtime_1.createPiRuntime, pi_runtime_1.PI_CAPABILITIES, true);
|
|
355
|
+
// 2b. 统一插件包宿主(必须在 createRuntime 之前 init——包 runtime 贡献要先注册)
|
|
356
|
+
this.pluginHost = new package_host_1.PluginHost(process.env.MAFW_PLUGINS_DIR || config_1.config.resolvePath('plugins'), (name) => (0, package_context_1.createPluginPackageContext)(name, {
|
|
357
|
+
getCredentials: () => this.opencodeClient?.credentials ?? undefined,
|
|
358
|
+
usageStats: () => this.usageStatsProvider,
|
|
359
|
+
projectDir: this.projectDir,
|
|
360
|
+
gatewayPort: config_1.config.server.apiPort,
|
|
361
|
+
emit: (event) => this.broadcast(event),
|
|
362
|
+
}));
|
|
363
|
+
this.pluginHost.bindRuntime((entries) => this.runtimeLoader?.setPackageEntries(entries));
|
|
364
|
+
await this.pluginHost.init();
|
|
363
365
|
// 3. 创建 SDK 客户端(auth),用于健康检查和后续通信
|
|
364
366
|
const sdkConfig = {
|
|
365
367
|
baseUrl: this.serveUrl,
|
|
@@ -370,6 +372,12 @@ class MafwScheduler {
|
|
|
370
372
|
if (opencodePassword) {
|
|
371
373
|
sdkConfig.headers = { Authorization: 'Basic ' + Buffer.from(`opencode:${opencodePassword}`).toString('base64') };
|
|
372
374
|
}
|
|
375
|
+
// opencode 注册为正式内置插件(与用户插件同一接口;同名文件/包可覆盖)
|
|
376
|
+
const sdkSnapshot = { ...sdkConfig, headers: { ...sdkConfig.headers } };
|
|
377
|
+
this.runtimeLoader.registerBuiltin('opencode', async () => (await import('./runtime/opencode-runtime.js')).createOpencodeRuntime({
|
|
378
|
+
...sdkSnapshot,
|
|
379
|
+
headers: { ...sdkSnapshot.headers },
|
|
380
|
+
}), (0, contract_1.fullCapabilities)(), !!process.env.MAFW_SERVER_SERVE_URL);
|
|
373
381
|
const runtime = await this.createRuntime(sdkConfig);
|
|
374
382
|
this.opencodeClient = runtime;
|
|
375
383
|
this.runtimeCaps = runtime.capabilities;
|
|
@@ -404,7 +412,8 @@ class MafwScheduler {
|
|
|
404
412
|
}
|
|
405
413
|
else {
|
|
406
414
|
logger_1.log.info('OpenCode Serve not reachable, checking for stale process...');
|
|
407
|
-
|
|
415
|
+
// 清场原语归 runtime(serve 端口是实现细节;无原语的 runtime 无从清场)
|
|
416
|
+
this.opencodeClient?.agentProcess?.killServe?.();
|
|
408
417
|
try {
|
|
409
418
|
await this.startServe();
|
|
410
419
|
serveReady = !!this.serveInstance;
|
|
@@ -600,7 +609,7 @@ class MafwScheduler {
|
|
|
600
609
|
}
|
|
601
610
|
else {
|
|
602
611
|
try {
|
|
603
|
-
const ms = this.
|
|
612
|
+
const ms = this.readManagerSessionEntry(this.projectDir);
|
|
604
613
|
if (ms?.sessionId)
|
|
605
614
|
targets.add(ms.sessionId);
|
|
606
615
|
}
|
|
@@ -644,8 +653,10 @@ class MafwScheduler {
|
|
|
644
653
|
if (changed.includes('runtime')) {
|
|
645
654
|
const newPlugin = config_1.config.runtime?.plugin;
|
|
646
655
|
if (newPlugin !== prevPlugin && !this.switchingRuntime && !this.serveRecovering) {
|
|
647
|
-
|
|
656
|
+
this.switchingRuntime = true;
|
|
657
|
+
const prev = this.opencodeClient;
|
|
648
658
|
try {
|
|
659
|
+
logger_1.log.info(`[Scheduler] Runtime plugin changed: '${prevPlugin ?? 'builtin'}' → '${newPlugin ?? 'builtin'}'; hot-switching...`);
|
|
649
660
|
const sdkConfig = {
|
|
650
661
|
baseUrl: this.serveUrl,
|
|
651
662
|
directory: this.projectDir,
|
|
@@ -670,11 +681,28 @@ class MafwScheduler {
|
|
|
670
681
|
if (this.automationEngine)
|
|
671
682
|
this.automationEngine.setRuntimeClient(runtime);
|
|
672
683
|
await this.resubscribeEvents(`config hot-reload runtime plugin changed to '${newPlugin ?? 'builtin'}'`);
|
|
684
|
+
// Same desktop hint as the route path: hand-edited config.yaml
|
|
685
|
+
// switches must also refresh the renderer (menus/tabs/manager kv).
|
|
686
|
+
this.broadcast({ type: 'runtime_switched', runtime: runtime.name, previous: prevPlugin ?? null });
|
|
687
|
+
// Dispose the previous runtime AFTER the new one is fully wired
|
|
688
|
+
// (mirror of the route path) — without this, switching away from
|
|
689
|
+
// pi leaks its AgentSessions/ApprovalBridges/event stream.
|
|
690
|
+
if (prev && prev.dispose) {
|
|
691
|
+
try {
|
|
692
|
+
await prev.dispose();
|
|
693
|
+
}
|
|
694
|
+
catch (err) {
|
|
695
|
+
logger_1.log.warn(`[Scheduler] dispose previous runtime failed: ${err.message}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
673
698
|
logger_1.log.info(`[Scheduler] Runtime hot-switched to '${runtime.name}'`);
|
|
674
699
|
}
|
|
675
700
|
catch (err) {
|
|
676
701
|
logger_1.log.warn(`[Scheduler] Runtime hot-switch failed (non-fatal): ${err.message}`);
|
|
677
702
|
}
|
|
703
|
+
finally {
|
|
704
|
+
this.switchingRuntime = false;
|
|
705
|
+
}
|
|
678
706
|
}
|
|
679
707
|
}
|
|
680
708
|
}, 300);
|
|
@@ -849,6 +877,23 @@ class MafwScheduler {
|
|
|
849
877
|
}
|
|
850
878
|
handleOpencodeEvent(evt) {
|
|
851
879
|
const f = (0, normalize_1.normalizeOpencodeEvent)(evt);
|
|
880
|
+
// 畸形事件诊断(限频):runtime 插件发来的事件 type/properties 全空时
|
|
881
|
+
// 静默穿过会污染 trajectory 与桌面 SSE——这里给可定位诊断。
|
|
882
|
+
if ((0, normalize_1.isMalformedEvent)(evt)) {
|
|
883
|
+
const now = Date.now();
|
|
884
|
+
const last = this.malformedEventWarnAt.get(this.runtimeName) ?? 0;
|
|
885
|
+
if (now - last > 30_000) {
|
|
886
|
+
this.malformedEventWarnAt.set(this.runtimeName, now);
|
|
887
|
+
let summary;
|
|
888
|
+
try {
|
|
889
|
+
summary = JSON.stringify(evt)?.slice(0, 200) ?? '(unserializable)';
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
summary = '(unserializable)';
|
|
893
|
+
}
|
|
894
|
+
logger_1.log.warn(`[SSE] malformed event from runtime '${this.runtimeName}' (no type/properties) — dropped consumers may misbehave; payload: ${summary}`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
852
897
|
const { type, properties: props, sessionID } = f;
|
|
853
898
|
// Only memory-system sessions (index-scan / extract / reflect workers) are
|
|
854
899
|
// internal: their token-level deltas flooded the desktop renderer (per-delta
|
|
@@ -993,7 +1038,18 @@ class MafwScheduler {
|
|
|
993
1038
|
if (plugin) {
|
|
994
1039
|
try {
|
|
995
1040
|
const creds = await this.runtimeCredentialsForPlugin();
|
|
996
|
-
const rt = await plugin.createRuntime((0, loader_1.createRuntimePluginContext)(creds
|
|
1041
|
+
const rt = await plugin.createRuntime((0, loader_1.createRuntimePluginContext)(creds, {
|
|
1042
|
+
projectDir: this.projectDir,
|
|
1043
|
+
gatewayPort: config_1.config.server.apiPort,
|
|
1044
|
+
}));
|
|
1045
|
+
// 声明 vs 实现一致性校验:错位插件在切换/启动期给出字段级诊断并回退,
|
|
1046
|
+
// 而不是运行时深处才炸。
|
|
1047
|
+
const issues = (0, validate_1.validateRuntimeShape)(rt, pluginName);
|
|
1048
|
+
if (issues.length > 0) {
|
|
1049
|
+
for (const issue of issues)
|
|
1050
|
+
logger_1.log.error(`[Runtime] shape violation: ${issue}`);
|
|
1051
|
+
throw new Error(`runtime '${rt.name}' failed shape validation (${issues.length} issue(s)) — see logs`);
|
|
1052
|
+
}
|
|
997
1053
|
logger_1.log.info(`[Runtime] using plugin runtime '${rt.name}' (capabilities: ${JSON.stringify(rt.capabilities)})`);
|
|
998
1054
|
return rt;
|
|
999
1055
|
}
|
|
@@ -1005,6 +1061,11 @@ class MafwScheduler {
|
|
|
1005
1061
|
logger_1.log.warn(`[Runtime] plugin '${pluginName}' not found — falling back to opencode`);
|
|
1006
1062
|
}
|
|
1007
1063
|
}
|
|
1064
|
+
const builtin = this.runtimeLoader?.get('opencode');
|
|
1065
|
+
if (builtin) {
|
|
1066
|
+
return builtin.createRuntime((0, loader_1.createRuntimePluginContext)(undefined));
|
|
1067
|
+
}
|
|
1068
|
+
// 最终安全网:loader 未注册时直连(不应发生)
|
|
1008
1069
|
const { createOpencodeRuntime } = await import('./runtime/opencode-runtime.js');
|
|
1009
1070
|
return createOpencodeRuntime({
|
|
1010
1071
|
...sdkConfig,
|
|
@@ -1198,6 +1259,120 @@ class MafwScheduler {
|
|
|
1198
1259
|
const result = await rt.indexer.backfill(ids);
|
|
1199
1260
|
return { ...result, vectors: rt.vectors.size(), indexEntries: entries.length };
|
|
1200
1261
|
}
|
|
1262
|
+
/** P5 Wave 2:/api/runtime 路由 deps(自内联块上移,行为逐字节等价)。 */
|
|
1263
|
+
runtimeDeps() {
|
|
1264
|
+
return {
|
|
1265
|
+
loader: this.runtimeLoader,
|
|
1266
|
+
persist: (o) => config_1.config.persistOverrides(o),
|
|
1267
|
+
getCurrent: () => this.opencodeClient,
|
|
1268
|
+
runtimeName: () => this.runtimeName,
|
|
1269
|
+
runtimeCaps: () => this.runtimeCaps,
|
|
1270
|
+
envOverride: () => !!process.env.MAFW_RUNTIME_PLUGIN,
|
|
1271
|
+
createRuntime: async () => {
|
|
1272
|
+
const sdkConfig = {
|
|
1273
|
+
baseUrl: this.serveUrl,
|
|
1274
|
+
directory: this.projectDir,
|
|
1275
|
+
headers: {},
|
|
1276
|
+
};
|
|
1277
|
+
const opencodePassword = process.env.MAFW_OPENCODE_PASSWORD;
|
|
1278
|
+
if (opencodePassword) {
|
|
1279
|
+
sdkConfig.headers = { Authorization: 'Basic ' + Buffer.from(`opencode:${opencodePassword}`).toString('base64') };
|
|
1280
|
+
}
|
|
1281
|
+
return this.createRuntime(sdkConfig);
|
|
1282
|
+
},
|
|
1283
|
+
onSwitched: async (rt, prev) => {
|
|
1284
|
+
this.opencodeClient = rt;
|
|
1285
|
+
this.runtimeCaps = rt.capabilities;
|
|
1286
|
+
this.runtimeName = rt.name;
|
|
1287
|
+
// Switching onto a runtime that owns serve (builtin opencode) must
|
|
1288
|
+
// ensure the sidecar exists — the gateway may have started under an
|
|
1289
|
+
// external runtime (pi) that never spawned one. Must run AFTER the
|
|
1290
|
+
// assignment above: the supervisor's spawn closure reads
|
|
1291
|
+
// this.opencodeClient to find agentProcess.spawnServe.
|
|
1292
|
+
if (rt.agentProcess?.spawnServe) {
|
|
1293
|
+
await (0, serve_for_runtime_1.ensureServeForBuiltinRuntime)(this.serveSupervisor, { startWatchdog: () => this.startServeWatchdog() }, logger_1.log);
|
|
1294
|
+
}
|
|
1295
|
+
this.sdkSession.setClient(this.opencodeClient);
|
|
1296
|
+
if (this.trajectoryCollector)
|
|
1297
|
+
this.trajectoryCollector.setOpencodeClient(this.opencodeClient);
|
|
1298
|
+
if (this.automationEngine)
|
|
1299
|
+
this.automationEngine.setRuntimeClient(rt);
|
|
1300
|
+
await this.resubscribeEvents(`runtime switched to '${rt.name}'`);
|
|
1301
|
+
// Desktop hint: a runtime switch swaps the session storage backend
|
|
1302
|
+
// (opencode SQLite vs pi), so cached session lists are stale.
|
|
1303
|
+
this.broadcast({ type: 'runtime_switched', runtime: rt.name, previous: prev?.name ?? null });
|
|
1304
|
+
if (prev && prev.dispose) {
|
|
1305
|
+
try {
|
|
1306
|
+
await prev.dispose();
|
|
1307
|
+
}
|
|
1308
|
+
catch (err) {
|
|
1309
|
+
logger_1.log.warn(`[Runtime] dispose old runtime failed: ${err.message}`);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
},
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
/** P5 Wave 2:/api/runtime/restart-agent deps。 */
|
|
1316
|
+
restartAgentDeps() {
|
|
1317
|
+
return {
|
|
1318
|
+
capabilities: () => this.runtimeCaps,
|
|
1319
|
+
isRecovering: () => this.serveRecovering,
|
|
1320
|
+
isSwitching: () => this.switchingRuntime,
|
|
1321
|
+
begin: () => { this.serveRecovering = true; },
|
|
1322
|
+
end: () => { this.serveRecovering = false; },
|
|
1323
|
+
restartAgent: () => this.restartAgentOrchestrated(),
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
/** P5 Wave 2:/api/plugins* deps(Plugin Hub 四类型统一面)。 */
|
|
1327
|
+
pluginHubDeps() {
|
|
1328
|
+
return {
|
|
1329
|
+
hub: {
|
|
1330
|
+
dirs: {
|
|
1331
|
+
runtime: config_1.config.resolvePath('runtime-plugins'),
|
|
1332
|
+
media: config_1.config.resolvePath('media-plugins'),
|
|
1333
|
+
usage: config_1.config.resolvePath('usage-plugins'),
|
|
1334
|
+
ui: process.env.MAFW_UI_PLUGINS_DIR || path.join(os.homedir(), '.mafw', 'ui-plugins'),
|
|
1335
|
+
},
|
|
1336
|
+
builtinEntries: () => {
|
|
1337
|
+
const entries = [];
|
|
1338
|
+
const rt = (name) => ({ type: 'runtime', name, file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
|
|
1339
|
+
for (const name of this.runtimeLoader?.getBuiltinNames?.() ?? [])
|
|
1340
|
+
entries.push(rt(name));
|
|
1341
|
+
for (const name of this.mediaPluginLoader?.getBuiltinEngineNames?.() ?? []) {
|
|
1342
|
+
entries.push({ type: 'media', name, file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
|
|
1343
|
+
}
|
|
1344
|
+
const usageState = this.pluginLoader?.getState?.() ?? [];
|
|
1345
|
+
for (const s of usageState) {
|
|
1346
|
+
if (s.builtin && s.status === 'ok' && s.name) {
|
|
1347
|
+
entries.push({ type: 'usage', name: s.name, file: s.file, status: 'enabled', size: 0, mtime: '', pluginType: s.pluginType });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return entries;
|
|
1351
|
+
},
|
|
1352
|
+
getErrors: (type) => {
|
|
1353
|
+
const stateOf = (loader) => (loader && typeof loader.getState === 'function' ? loader.getState() : []);
|
|
1354
|
+
const source = type === 'runtime' ? this.runtimeLoader : type === 'media' ? this.mediaPluginLoader : type === 'usage' ? this.pluginLoader : null;
|
|
1355
|
+
const out = {};
|
|
1356
|
+
for (const p of stateOf(source)) {
|
|
1357
|
+
if (p && p.error)
|
|
1358
|
+
out[p.name || p.file] = p.error;
|
|
1359
|
+
}
|
|
1360
|
+
return out;
|
|
1361
|
+
},
|
|
1362
|
+
configDisabledUsage: () => new Set(Array.isArray(config_1.config.usage?.disabledPlugins) ? config_1.config.usage.disabledPlugins : []),
|
|
1363
|
+
reload: async (type) => {
|
|
1364
|
+
if (type === 'runtime')
|
|
1365
|
+
await this.runtimeLoader?.scan();
|
|
1366
|
+
else if (type === 'media')
|
|
1367
|
+
await this.mediaPluginLoader?.reload();
|
|
1368
|
+
else if (type === 'usage')
|
|
1369
|
+
await this.pluginLoader?.reload();
|
|
1370
|
+
// ui: desktop main fs.watch picks it up automatically
|
|
1371
|
+
},
|
|
1372
|
+
getPackages: () => this.pluginHost?.getState() ?? [],
|
|
1373
|
+
},
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1201
1376
|
embeddingConfigDeps() {
|
|
1202
1377
|
return {
|
|
1203
1378
|
currentConfig: () => {
|
|
@@ -1711,6 +1886,7 @@ class MafwScheduler {
|
|
|
1711
1886
|
}
|
|
1712
1887
|
this.pluginLoader?.stop();
|
|
1713
1888
|
this.mediaPluginLoader?.stop();
|
|
1889
|
+
this.pluginHost?.stop();
|
|
1714
1890
|
if (this.opencodeClient && typeof this.opencodeClient.dispose === 'function') {
|
|
1715
1891
|
void this.opencodeClient.dispose().catch((err) => {
|
|
1716
1892
|
logger_1.log.warn(`[Scheduler] runtime dispose error: ${err?.message ?? String(err)}`);
|
|
@@ -1724,32 +1900,6 @@ class MafwScheduler {
|
|
|
1724
1900
|
// }
|
|
1725
1901
|
logger_1.log.info('[Scheduler] Stopping...');
|
|
1726
1902
|
}
|
|
1727
|
-
// Proxy a native opencode request by trying every registered workspace.
|
|
1728
|
-
// Native reply/reject routes are workspace-scoped (WorkspaceRoutingMiddleware),
|
|
1729
|
-
// but the gateway's own projectDir is its cwd — the request may belong to any
|
|
1730
|
-
// registered project. GET list endpoints are cross-workspace and unaffected.
|
|
1731
|
-
async proxyNativeWorkspaces(path, method, body) {
|
|
1732
|
-
const dirs = new Set([this.projectDir || '.']);
|
|
1733
|
-
for (const key of this.registeredProjects.keys())
|
|
1734
|
-
dirs.add(key);
|
|
1735
|
-
let lastStatus = 502;
|
|
1736
|
-
for (const dir of dirs) {
|
|
1737
|
-
try {
|
|
1738
|
-
const r = await fetch(`${this.serveUrl}${path}?directory=${encodeURIComponent(dir)}`, {
|
|
1739
|
-
method,
|
|
1740
|
-
headers: { 'content-type': 'application/json', 'x-opencode-directory': encodeURIComponent(dir) },
|
|
1741
|
-
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
1742
|
-
});
|
|
1743
|
-
if (r.ok)
|
|
1744
|
-
return { ok: true, status: r.status };
|
|
1745
|
-
lastStatus = r.status;
|
|
1746
|
-
}
|
|
1747
|
-
catch (e) {
|
|
1748
|
-
logger_1.log.error(`[Native proxy] ${path} failed for ${dir}: ${e.message}`);
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
return { ok: false, status: lastStatus };
|
|
1752
|
-
}
|
|
1753
1903
|
// 鈹€鈹€ Services & Event Bus 鈹€鈹€
|
|
1754
1904
|
async initServices() {
|
|
1755
1905
|
const projectDir = this.projectDir;
|
|
@@ -1774,6 +1924,9 @@ class MafwScheduler {
|
|
|
1774
1924
|
getCredentials: () => this.opencodeClient?.credentials ?? undefined,
|
|
1775
1925
|
});
|
|
1776
1926
|
await this.mediaPluginLoader.init();
|
|
1927
|
+
this.pluginHost?.bindMedia((entries) => this.mediaPluginLoader?.setPackageEntries(entries));
|
|
1928
|
+
// 内置 pi 引擎登记(可被用户同名包/文件覆盖——resolveMediaPrompt 语义)
|
|
1929
|
+
this.mediaPluginLoader.registerBuiltinEngine('pi', ['image', 'video', 'audio']);
|
|
1777
1930
|
this.mediaService = new media_service_1.MediaService({
|
|
1778
1931
|
prompt: (0, pi_adapter_1.createPiPromptAdapter)({
|
|
1779
1932
|
getApiKey: (provider) => {
|
|
@@ -1783,31 +1936,24 @@ class MafwScheduler {
|
|
|
1783
1936
|
config: () => config_1.config.raw.media,
|
|
1784
1937
|
resolvePrompt: (kind, cfg) => {
|
|
1785
1938
|
const engineName = cfg[kind]?.engine ?? cfg.engine ?? 'pi';
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
return undefined;
|
|
1805
|
-
}
|
|
1806
|
-
if (!engine.modalities.includes(kind)) {
|
|
1807
|
-
logger_1.log.warn(`[MediaService] engine '${engineName}' does not support modality '${kind}', falling back to pi`);
|
|
1808
|
-
return undefined;
|
|
1809
|
-
}
|
|
1810
|
-
return engine.prompt;
|
|
1939
|
+
return (0, resolve_prompt_1.resolveMediaPrompt)(engineName, kind, {
|
|
1940
|
+
engines: this.mediaPluginLoader?.getEngines() ?? new Map(),
|
|
1941
|
+
builtinPi: () => {
|
|
1942
|
+
// pi runtime 激活时:图片走 AgentRuntime 会话(MediaRuntimeExecutor),
|
|
1943
|
+
// video/audio 由 executor 内部回退到 complete 路径
|
|
1944
|
+
if (this.opencodeClient?.name === 'pi') {
|
|
1945
|
+
// runtime 热切换后 opencodeClient 实例更换——旧 executor 持有 stale
|
|
1946
|
+
// runtime 引用,必须重建(dispose 尽力而为,不阻塞 prompt)。
|
|
1947
|
+
if (!this.mediaRuntimeExecutor || this.mediaRuntimeExecutorRt !== this.opencodeClient) {
|
|
1948
|
+
void this.mediaRuntimeExecutor?.dispose().catch(() => { });
|
|
1949
|
+
this.mediaRuntimeExecutor = (0, media_runtime_executor_1.createMediaRuntimeExecutor)(this.opencodeClient);
|
|
1950
|
+
this.mediaRuntimeExecutorRt = this.opencodeClient;
|
|
1951
|
+
}
|
|
1952
|
+
return this.mediaRuntimeExecutor.prompt;
|
|
1953
|
+
}
|
|
1954
|
+
return undefined;
|
|
1955
|
+
},
|
|
1956
|
+
});
|
|
1811
1957
|
},
|
|
1812
1958
|
});
|
|
1813
1959
|
// Determine workspace name: if projectDir resolves to a 'gateway' subdirectory,
|
|
@@ -1916,6 +2062,9 @@ class MafwScheduler {
|
|
|
1916
2062
|
const collector = new TrajectoryCollector(trajStore, this.getGatewayDb(), projectDir, () => config_1.config.trajectory.retentionDays);
|
|
1917
2063
|
collector.setRoleFor((sid) => this.internalSessionRoles.get(sid) ?? null);
|
|
1918
2064
|
this.trajectoryCollector = collector;
|
|
2065
|
+
const pruned = this.getGatewayDb().kvPruneOlderThan('internal-session', INTERNAL_SESSION_TTL_DAYS);
|
|
2066
|
+
if (pruned > 0)
|
|
2067
|
+
logger_1.log.info(`[Scheduler] pruned ${pruned} stale internal-session kv entries (> ${INTERNAL_SESSION_TTL_DAYS}d)`);
|
|
1919
2068
|
const restored = this.getGatewayDb().kvAll('internal-session');
|
|
1920
2069
|
for (const { key: sid, value } of restored) {
|
|
1921
2070
|
if (value?.role && !this.internalSessionRoles.has(sid)) {
|
|
@@ -1932,10 +2081,12 @@ class MafwScheduler {
|
|
|
1932
2081
|
const pluginsDir = path.join(os.homedir(), '.mafw', 'usage-plugins');
|
|
1933
2082
|
const builtinPluginsDir = path.join(__dirname, 'usage', 'builtin-plugins');
|
|
1934
2083
|
const disabledPlugins = Array.isArray(config_1.config.usage?.disabledPlugins) ? config_1.config.usage.disabledPlugins : [];
|
|
2084
|
+
const statsProvider = createUsageStatsProvider(trajStore);
|
|
2085
|
+
this.usageStatsProvider = statsProvider;
|
|
1935
2086
|
const pluginLoader = new PluginLoader(pluginsDir, [], {
|
|
1936
2087
|
builtinPluginsDir,
|
|
1937
2088
|
disabledPlugins,
|
|
1938
|
-
usageStats:
|
|
2089
|
+
usageStats: statsProvider,
|
|
1939
2090
|
// inline provider key 兜底(auth.json 无条目的自建 provider,如 gateway)——
|
|
1940
2091
|
// thunk 惰性求值,opencodeClient 此时尚未初始化也不影响。
|
|
1941
2092
|
resolveInlineApiKey: async (providerID) => {
|
|
@@ -1951,6 +2102,7 @@ class MafwScheduler {
|
|
|
1951
2102
|
});
|
|
1952
2103
|
await pluginLoader.init();
|
|
1953
2104
|
this.pluginLoader = pluginLoader;
|
|
2105
|
+
this.pluginHost?.bindUsage((entries) => pluginLoader.setPackageEntries(entries));
|
|
1954
2106
|
const { UsagePoller } = require('./usage/usage-poller');
|
|
1955
2107
|
this.usagePoller = new UsagePoller(trajStore, () => config_1.config.usage.limits, () => config_1.config.usage.budgets, pluginLoader);
|
|
1956
2108
|
logger_1.log.info('[Trajectory] store initialized');
|
|
@@ -2027,12 +2179,10 @@ class MafwScheduler {
|
|
|
2027
2179
|
return;
|
|
2028
2180
|
}
|
|
2029
2181
|
logger_1.log.info('Starting OpenCode Serve sidecar...');
|
|
2030
|
-
const port = config_1.config.server.servePort;
|
|
2031
|
-
const host = config_1.config.server.serveHost;
|
|
2032
2182
|
try {
|
|
2183
|
+
// host/port 归 runtime 自定(serve 端口是实现细节);sidecar 实际 URL
|
|
2184
|
+
// 由 runtime 吸收(getBaseUrl 跟随),gateway 只记 bookkeeping。
|
|
2033
2185
|
const sidecar = await spawnServe({
|
|
2034
|
-
host,
|
|
2035
|
-
port,
|
|
2036
2186
|
onOutput: (chunk) => logger_1.log.debug(`[Serve] ${chunk.trimEnd()}`),
|
|
2037
2187
|
onExit: (code) => this.handleServeExit(code),
|
|
2038
2188
|
});
|
|
@@ -2055,6 +2205,18 @@ class MafwScheduler {
|
|
|
2055
2205
|
handleServeExit(code) {
|
|
2056
2206
|
if (!this.serveOwned || this.serveRecovering)
|
|
2057
2207
|
return;
|
|
2208
|
+
// After a hot-switch to an in-process/external runtime, the (still owned)
|
|
2209
|
+
// opencode serve is no longer the active backend: its exit must NOT trigger
|
|
2210
|
+
// recovery — pi has no spawnServe, so the orchestrator would fall into the
|
|
2211
|
+
// supervisor's kill+spawn path and retry forever (killing whatever listens
|
|
2212
|
+
// on the serve port each cycle). A later switch back re-ensures via
|
|
2213
|
+
// ensureServeForBuiltinRuntime (probe → adopt or spawn).
|
|
2214
|
+
const rt = this.opencodeClient;
|
|
2215
|
+
if (rt?.external || !rt?.agentProcess?.spawnServe) {
|
|
2216
|
+
this.serveOwned = false;
|
|
2217
|
+
logger_1.log.info('[Scheduler] owned serve exited after runtime switch; recovery skipped (active runtime does not own serve)');
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2058
2220
|
this.serveInstance = undefined;
|
|
2059
2221
|
if (this.serveStableTimer) {
|
|
2060
2222
|
clearTimeout(this.serveStableTimer);
|
|
@@ -2282,18 +2444,14 @@ class MafwScheduler {
|
|
|
2282
2444
|
return projectID ? await this.sdkSession.listByProject(projectID) : await this.sdkSession.list();
|
|
2283
2445
|
}
|
|
2284
2446
|
async isServeHealthy() {
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
req.destroy();
|
|
2294
|
-
resolve(false);
|
|
2295
|
-
});
|
|
2296
|
-
});
|
|
2447
|
+
// 健康唯一真相源 = runtime 契约 healthCheck()(serve 型探测 serve,
|
|
2448
|
+
// 进程内 runtime 探测自身引擎);gateway 不自带 HTTP 探测。
|
|
2449
|
+
try {
|
|
2450
|
+
return (await this.opencodeClient?.healthCheck?.()) ?? false;
|
|
2451
|
+
}
|
|
2452
|
+
catch {
|
|
2453
|
+
return false;
|
|
2454
|
+
}
|
|
2297
2455
|
}
|
|
2298
2456
|
async waitForServeReady() {
|
|
2299
2457
|
const maxRetries = config_1.config.timeouts.serveReadyMaxRetries;
|
|
@@ -2310,9 +2468,45 @@ class MafwScheduler {
|
|
|
2310
2468
|
// 鈹€鈹€ 2. HTTP API 鈹€鈹€
|
|
2311
2469
|
async startApiServer() {
|
|
2312
2470
|
return new Promise((resolve) => {
|
|
2471
|
+
// P5 Wave 1: catalog shadow 登记 + 模块 handler 绑定(一次性,重复 attach 会抛错)。
|
|
2472
|
+
// adapter 显式桥接私有成员(结构化类型不认 private)。
|
|
2473
|
+
(0, wave1_handlers_1.attachWave1Handlers)(this.routeRegistry, {
|
|
2474
|
+
getGatewayDb: () => this.getGatewayDb(),
|
|
2475
|
+
opencodeClient: this.opencodeClient,
|
|
2476
|
+
automationEngine: this.automationEngine,
|
|
2477
|
+
ledger: this.ledger,
|
|
2478
|
+
rotateDeps: () => this.rotateDeps(),
|
|
2479
|
+
embeddingConfigDeps: () => this.embeddingConfigDeps(),
|
|
2480
|
+
modelConfigDeps: () => this.modelConfigDeps(),
|
|
2481
|
+
usagePluginsDeps: () => this.usagePluginsDeps(),
|
|
2482
|
+
pluginLoader: this.pluginLoader,
|
|
2483
|
+
mediaPluginLoader: this.mediaPluginLoader,
|
|
2484
|
+
broadcast: (e) => this.broadcast(e),
|
|
2485
|
+
runtimeCaps: this.runtimeCaps,
|
|
2486
|
+
});
|
|
2487
|
+
(0, wave2_handlers_1.attachWave2Handlers)(this.routeRegistry, {
|
|
2488
|
+
runtimeDeps: () => this.runtimeDeps(),
|
|
2489
|
+
restartAgentDeps: () => this.restartAgentDeps(),
|
|
2490
|
+
pluginHubDeps: () => this.pluginHubDeps(),
|
|
2491
|
+
runtimeSwitchBlocked: () => this.serveRecovering || this.switchingRuntime,
|
|
2492
|
+
beginRuntimeSwitch: () => { this.switchingRuntime = true; },
|
|
2493
|
+
endRuntimeSwitch: () => { this.switchingRuntime = false; },
|
|
2494
|
+
});
|
|
2313
2495
|
const server = http.createServer(async (req, res) => {
|
|
2314
2496
|
try {
|
|
2315
2497
|
res.setHeader('Content-Type', 'application/json');
|
|
2498
|
+
// P5 Wave 1: registry dispatch —— attach 过 handler 的路由在 legacy 链之前
|
|
2499
|
+
// 统一接管(shadow 条目无 handler 自然落回内联链)。挂在鉴权之后、一切
|
|
2500
|
+
// /api matcher 之前:goals.sessions 须先于 dashboard 兜底、embedding-config
|
|
2501
|
+
// 须先于 /api/memory/* 委托(AGENTS.md §6.5 顺序教训由 dispatch 位置一次性满足)。
|
|
2502
|
+
{
|
|
2503
|
+
const matched = this.routeRegistry.match(req.method, req.url || '');
|
|
2504
|
+
if (matched?.def.handler) {
|
|
2505
|
+
const handled = await matched.def.handler(req, res, matched.params);
|
|
2506
|
+
if (handled !== false)
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2316
2510
|
// CORS headers for SSE
|
|
2317
2511
|
res.setHeader("Access-Control-Allow-Origin", config_1.config.server.cors.origin);
|
|
2318
2512
|
res.setHeader("Access-Control-Allow-Methods", config_1.config.server.cors.methods);
|
|
@@ -3141,6 +3335,32 @@ class MafwScheduler {
|
|
|
3141
3335
|
res.end(JSON.stringify({ ok: true, text: answer }));
|
|
3142
3336
|
return;
|
|
3143
3337
|
}
|
|
3338
|
+
if (cmd === "waitwhat") {
|
|
3339
|
+
if (!sessionID) {
|
|
3340
|
+
res.writeHead(400);
|
|
3341
|
+
res.end(JSON.stringify({ ok: false, error: "sessionID required" }));
|
|
3342
|
+
return;
|
|
3343
|
+
}
|
|
3344
|
+
if (!this.opencodeClient) {
|
|
3345
|
+
res.writeHead(503);
|
|
3346
|
+
res.end(JSON.stringify({ ok: false, error: "LLM client not available" }));
|
|
3347
|
+
return;
|
|
3348
|
+
}
|
|
3349
|
+
const result = await (0, waitwhat_command_1.runWaitwhat)(String(sessionID), {
|
|
3350
|
+
listMessages: async (sid) => {
|
|
3351
|
+
const r = await this.opencodeClient.session.messages({ sessionID: sid, limit: 50 });
|
|
3352
|
+
return (r?.data || []);
|
|
3353
|
+
},
|
|
3354
|
+
promptAsync: async (sid, text) => {
|
|
3355
|
+
await this.opencodeClient.session.promptAsync({ sessionID: sid, parts: [{ type: "text", text }] });
|
|
3356
|
+
},
|
|
3357
|
+
});
|
|
3358
|
+
res.writeHead(result.ok ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
3359
|
+
res.end(JSON.stringify(result.ok
|
|
3360
|
+
? { ok: true, message: '重述请求已发送到当前会话' }
|
|
3361
|
+
: { ok: false, error: result.error }));
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3144
3364
|
if (cmd === "status") {
|
|
3145
3365
|
const statusPath = path.join(this.mafwDir, 'STATUS.md');
|
|
3146
3366
|
const text = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf-8') : 'No active Goals. Use /goal to create one.';
|
|
@@ -3254,16 +3474,8 @@ class MafwScheduler {
|
|
|
3254
3474
|
return;
|
|
3255
3475
|
}
|
|
3256
3476
|
// GET/POST /api/memory/embedding-config — memory embedding engine
|
|
3257
|
-
// settings
|
|
3258
|
-
//
|
|
3259
|
-
if (req.url?.match(/^\/api\/memory\/embedding-config(?:\?|$)/) && req.method === 'GET') {
|
|
3260
|
-
await (0, embedding_config_1.handleEmbeddingConfigGet)(req, res, this.embeddingConfigDeps());
|
|
3261
|
-
return;
|
|
3262
|
-
}
|
|
3263
|
-
if (req.url?.match(/^\/api\/memory\/embedding-config(?:\?|$)/) && req.method === 'POST') {
|
|
3264
|
-
await (0, embedding_config_1.handleEmbeddingConfigUpdate)(req, res, this.embeddingConfigDeps());
|
|
3265
|
-
return;
|
|
3266
|
-
}
|
|
3477
|
+
// settings(P5 Wave 1 起由 registry dispatch 接管,见 routes/wave1-handlers.ts;
|
|
3478
|
+
// 历史顺序约束"须高于 /api/memory/* dashboard 委托"由 dispatch 位置满足)
|
|
3267
3479
|
// GET /api/memory/stats — consolidation health + vector coverage (P2)
|
|
3268
3480
|
if (req.url?.match(/^\/api\/memory\/stats(?:\?|$)/) && req.method === 'GET') {
|
|
3269
3481
|
try {
|
|
@@ -3510,16 +3722,8 @@ class MafwScheduler {
|
|
|
3510
3722
|
res.end(JSON.stringify({ status: 'accepted' }));
|
|
3511
3723
|
return;
|
|
3512
3724
|
}
|
|
3513
|
-
// GET /api/goals/:id/sessions —
|
|
3514
|
-
//
|
|
3515
|
-
if (req.url?.match(/^\/api\/goals\/[^/]+\/sessions(?:\?|$)/) && req.method === 'GET') {
|
|
3516
|
-
const handled = await (0, goal_sessions_1.handleGoalSessions)(req, res, req.url, {
|
|
3517
|
-
listGoalSessions: (goalId) => this.getGatewayDb().listGoalSessions(goalId),
|
|
3518
|
-
getSession: (sessionID) => this.opencodeClient?.session.get({ sessionID }).catch(() => null),
|
|
3519
|
-
});
|
|
3520
|
-
if (handled)
|
|
3521
|
-
return;
|
|
3522
|
-
}
|
|
3725
|
+
// GET /api/goals/:id/sessions — P5 Wave 1 起由 registry dispatch 接管
|
|
3726
|
+
// (历史约束"须挂 Dashboard /api/goals* 兜底之前"由 dispatch 位置满足)。
|
|
3523
3727
|
// Dashboard API
|
|
3524
3728
|
if (req.url?.startsWith("/api/goals") || req.url?.startsWith("/api/stats") || req.url?.startsWith("/api/memory")) {
|
|
3525
3729
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -3633,8 +3837,8 @@ class MafwScheduler {
|
|
|
3633
3837
|
});
|
|
3634
3838
|
return;
|
|
3635
3839
|
}
|
|
3636
|
-
//
|
|
3637
|
-
if (req.url === '/control' && req.method === 'POST') {
|
|
3840
|
+
// 控制指令(/control 为 MCP handler 兼容路径;/api/goals/control 为 SDK goals.control 契约路径,两者等价)
|
|
3841
|
+
if ((req.url === '/control' || req.url === '/api/goals/control') && req.method === 'POST') {
|
|
3638
3842
|
let body = '';
|
|
3639
3843
|
req.on('data', chunk => body += chunk);
|
|
3640
3844
|
req.on('end', async () => {
|
|
@@ -3737,12 +3941,7 @@ class MafwScheduler {
|
|
|
3737
3941
|
}
|
|
3738
3942
|
return;
|
|
3739
3943
|
}
|
|
3740
|
-
// POST /api/manager/session/rotate —
|
|
3741
|
-
// (thin wiring → routes/manager-rotate.ts).
|
|
3742
|
-
if (req.method === 'POST' && req.url?.match(/^\/api\/manager\/session\/rotate(?:\?|$)/)) {
|
|
3743
|
-
await (0, manager_rotate_1.handleManagerRotate)(req, res, this.rotateDeps());
|
|
3744
|
-
return;
|
|
3745
|
-
}
|
|
3944
|
+
// POST /api/manager/session/rotate — P5 Wave 1 起由 registry dispatch 接管
|
|
3746
3945
|
// GET /api/manager/session — return manager session info (per-project,
|
|
3747
3946
|
// read from the gateway DB). ?projectDir= filters a single project.
|
|
3748
3947
|
if (req.url && req.url.startsWith('/api/manager/session') && req.method === 'GET') {
|
|
@@ -3750,8 +3949,14 @@ class MafwScheduler {
|
|
|
3750
3949
|
const filter = parsedUrl.searchParams.get('projectDir') || '';
|
|
3751
3950
|
try {
|
|
3752
3951
|
const all = this.getGatewayDb().kvAll('manager-session');
|
|
3952
|
+
// Per-runtime slots: surface only the ACTIVE runtime's manager
|
|
3953
|
+
// slot per project. Other runtimes' slots stay dormant (resumed
|
|
3954
|
+
// when switching back) — nothing is deleted here.
|
|
3955
|
+
const live = all
|
|
3956
|
+
.map((e) => ({ key: e.key, value: (0, manager_session_runtime_1.readManagerSlot)(e.value, this.runtimeName) }))
|
|
3957
|
+
.filter((e) => e.value !== null);
|
|
3753
3958
|
if (filter) {
|
|
3754
|
-
const found =
|
|
3959
|
+
const found = live.find((e) => e.key === filter);
|
|
3755
3960
|
if (!found) {
|
|
3756
3961
|
res.writeHead(404);
|
|
3757
3962
|
res.end(JSON.stringify({ error: 'No manager session for project' }));
|
|
@@ -3765,13 +3970,13 @@ class MafwScheduler {
|
|
|
3765
3970
|
}));
|
|
3766
3971
|
return;
|
|
3767
3972
|
}
|
|
3768
|
-
if (
|
|
3973
|
+
if (live.length === 0) {
|
|
3769
3974
|
res.writeHead(404);
|
|
3770
3975
|
res.end(JSON.stringify({ error: 'No manager session' }));
|
|
3771
3976
|
return;
|
|
3772
3977
|
}
|
|
3773
3978
|
res.writeHead(200);
|
|
3774
|
-
res.end(JSON.stringify(
|
|
3979
|
+
res.end(JSON.stringify(live.map((e) => ({
|
|
3775
3980
|
projectDir: e.key,
|
|
3776
3981
|
sessionId: e.value.sessionId,
|
|
3777
3982
|
createdAt: e.value.createdAt || null,
|
|
@@ -3982,17 +4187,15 @@ class MafwScheduler {
|
|
|
3982
4187
|
return;
|
|
3983
4188
|
}
|
|
3984
4189
|
// 鈹€鈹€ Question endpoints (AskCard 鈹€ proxy to native opencode Question API) 鈹€鈹€
|
|
3985
|
-
// GET /api/questions
|
|
4190
|
+
// GET /api/questions ── list pending questions(契约:session.question.list)
|
|
3986
4191
|
if (req.url?.match(/^\/api\/questions(?:\?|$)/) && req.method === 'GET') {
|
|
3987
4192
|
if (this.capGuardQuestion(res))
|
|
3988
4193
|
return;
|
|
3989
4194
|
try {
|
|
3990
|
-
const dir = new URL(req.url, this.serveUrl).searchParams.get('directory') || this.projectDir ||
|
|
3991
|
-
const
|
|
3992
|
-
|
|
3993
|
-
});
|
|
3994
|
-
const items = await r.json();
|
|
3995
|
-
res.end(JSON.stringify({ items }));
|
|
4195
|
+
const dir = new URL(req.url, this.serveUrl).searchParams.get('directory') || this.projectDir || undefined;
|
|
4196
|
+
const question = this.opencodeClient?.session?.question;
|
|
4197
|
+
const items = question ? await question.list(dir ? { directory: dir } : undefined) : [];
|
|
4198
|
+
res.end(JSON.stringify({ items: items ?? [] }));
|
|
3996
4199
|
}
|
|
3997
4200
|
catch (err) {
|
|
3998
4201
|
logger_1.log.error('[Question] list error:', err.message);
|
|
@@ -4000,20 +4203,34 @@ class MafwScheduler {
|
|
|
4000
4203
|
}
|
|
4001
4204
|
return;
|
|
4002
4205
|
}
|
|
4003
|
-
// POST /api/questions/{id}/reply
|
|
4206
|
+
// POST /api/questions/{id}/reply ── { answers: string[][] }(契约 + workspace 重试)
|
|
4004
4207
|
const qReplyMatch = req.url?.match(/^\/api\/questions\/([^/]+)\/reply(?:\?|$)/);
|
|
4005
4208
|
if (qReplyMatch && req.method === 'POST') {
|
|
4006
4209
|
if (this.capGuardQuestion(res))
|
|
4007
4210
|
return;
|
|
4211
|
+
const question = this.opencodeClient?.session?.question;
|
|
4212
|
+
if (!question) {
|
|
4213
|
+
res.writeHead(503);
|
|
4214
|
+
res.end(JSON.stringify({ status: 'error', error: 'question API not available on this runtime' }));
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4008
4217
|
try {
|
|
4009
4218
|
const body = JSON.parse(await readBody(req));
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4219
|
+
// workspace 路由:默认(SDK 自带 projectDir)→ 各注册项目逐试
|
|
4220
|
+
const dirs = [undefined, this.projectDir, ...this.registeredProjects.keys()];
|
|
4221
|
+
let lastErr = null;
|
|
4222
|
+
for (const dir of [...new Set(dirs)]) {
|
|
4223
|
+
try {
|
|
4224
|
+
await question.reply({ requestID: qReplyMatch[1], answers: body.answers, ...(dir ? { directory: dir } : {}) });
|
|
4225
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
4226
|
+
return;
|
|
4227
|
+
}
|
|
4228
|
+
catch (err) {
|
|
4229
|
+
lastErr = err;
|
|
4230
|
+
}
|
|
4015
4231
|
}
|
|
4016
|
-
res.
|
|
4232
|
+
res.writeHead(400);
|
|
4233
|
+
res.end(JSON.stringify({ status: 'error', error: lastErr?.message ?? 'question reply failed on all workspaces' }));
|
|
4017
4234
|
}
|
|
4018
4235
|
catch (err) {
|
|
4019
4236
|
logger_1.log.error('[Question] reply error:', err.message);
|
|
@@ -4022,19 +4239,32 @@ class MafwScheduler {
|
|
|
4022
4239
|
}
|
|
4023
4240
|
return;
|
|
4024
4241
|
}
|
|
4025
|
-
// POST /api/questions/{id}/reject
|
|
4242
|
+
// POST /api/questions/{id}/reject(契约 + workspace 重试)
|
|
4026
4243
|
const qRejectMatch = req.url?.match(/^\/api\/questions\/([^/]+)\/reject(?:\?|$)/);
|
|
4027
4244
|
if (qRejectMatch && req.method === 'POST') {
|
|
4028
4245
|
if (this.capGuardQuestion(res))
|
|
4029
4246
|
return;
|
|
4247
|
+
const question = this.opencodeClient?.session?.question;
|
|
4248
|
+
if (!question) {
|
|
4249
|
+
res.writeHead(503);
|
|
4250
|
+
res.end(JSON.stringify({ status: 'error', error: 'question API not available on this runtime' }));
|
|
4251
|
+
return;
|
|
4252
|
+
}
|
|
4030
4253
|
try {
|
|
4031
|
-
const
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4254
|
+
const dirs = [undefined, this.projectDir, ...this.registeredProjects.keys()];
|
|
4255
|
+
let lastErr = null;
|
|
4256
|
+
for (const dir of [...new Set(dirs)]) {
|
|
4257
|
+
try {
|
|
4258
|
+
await question.reject({ requestID: qRejectMatch[1], ...(dir ? { directory: dir } : {}) });
|
|
4259
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
4262
|
+
catch (err) {
|
|
4263
|
+
lastErr = err;
|
|
4264
|
+
}
|
|
4036
4265
|
}
|
|
4037
|
-
res.
|
|
4266
|
+
res.writeHead(400);
|
|
4267
|
+
res.end(JSON.stringify({ status: 'error', error: lastErr?.message ?? 'question reject failed on all workspaces' }));
|
|
4038
4268
|
}
|
|
4039
4269
|
catch (err) {
|
|
4040
4270
|
logger_1.log.error('[Question] reject error:', err.message);
|
|
@@ -4044,17 +4274,16 @@ class MafwScheduler {
|
|
|
4044
4274
|
return;
|
|
4045
4275
|
}
|
|
4046
4276
|
// 鈹€鈹€ Permission endpoints (PermissionCard 鈹€ proxy to native opencode Permission API) 鈹€鈹€
|
|
4047
|
-
// GET /api/permissions
|
|
4277
|
+
// GET /api/permissions ── list pending permission requests(契约:session.permissionList)
|
|
4048
4278
|
if (req.url?.match(/^\/api\/permissions(?:\?|$)/) && req.method === 'GET') {
|
|
4049
4279
|
if (this.capGuard(res, 'nativeApprovals'))
|
|
4050
4280
|
return;
|
|
4051
4281
|
try {
|
|
4052
|
-
const dir = new URL(req.url, this.serveUrl).searchParams.get('directory') || this.projectDir ||
|
|
4053
|
-
const
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
res.end(JSON.stringify({ items }));
|
|
4282
|
+
const dir = new URL(req.url, this.serveUrl).searchParams.get('directory') || this.projectDir || undefined;
|
|
4283
|
+
const items = this.opencodeClient?.session?.permissionList
|
|
4284
|
+
? await this.opencodeClient.session.permissionList(dir ? { directory: dir } : undefined)
|
|
4285
|
+
: [];
|
|
4286
|
+
res.end(JSON.stringify({ items: items ?? [] }));
|
|
4058
4287
|
}
|
|
4059
4288
|
catch (err) {
|
|
4060
4289
|
logger_1.log.error('[Permission] list error:', err.message);
|
|
@@ -4063,170 +4292,12 @@ class MafwScheduler {
|
|
|
4063
4292
|
return;
|
|
4064
4293
|
}
|
|
4065
4294
|
// 鈹€鈹€ Provider & Agents (composer model pill / @agent mention) 鈹€鈹€
|
|
4066
|
-
// ── /api/runtime routes
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
runtimeName: () => this.runtimeName,
|
|
4073
|
-
runtimeCaps: () => this.runtimeCaps,
|
|
4074
|
-
envOverride: () => !!process.env.MAFW_RUNTIME_PLUGIN,
|
|
4075
|
-
createRuntime: async () => {
|
|
4076
|
-
const sdkConfig = {
|
|
4077
|
-
baseUrl: this.serveUrl,
|
|
4078
|
-
directory: this.projectDir,
|
|
4079
|
-
headers: {},
|
|
4080
|
-
};
|
|
4081
|
-
const opencodePassword = process.env.MAFW_OPENCODE_PASSWORD;
|
|
4082
|
-
if (opencodePassword) {
|
|
4083
|
-
sdkConfig.headers = { Authorization: 'Basic ' + Buffer.from(`opencode:${opencodePassword}`).toString('base64') };
|
|
4084
|
-
}
|
|
4085
|
-
return this.createRuntime(sdkConfig);
|
|
4086
|
-
},
|
|
4087
|
-
onSwitched: async (rt, prev) => {
|
|
4088
|
-
this.opencodeClient = rt;
|
|
4089
|
-
this.runtimeCaps = rt.capabilities;
|
|
4090
|
-
this.runtimeName = rt.name;
|
|
4091
|
-
// Switching onto a runtime that owns serve (builtin opencode) must
|
|
4092
|
-
// ensure the sidecar exists — the gateway may have started under an
|
|
4093
|
-
// external runtime (pi) that never spawned one. Must run AFTER the
|
|
4094
|
-
// assignment above: the supervisor's spawn closure reads
|
|
4095
|
-
// this.opencodeClient to find agentProcess.spawnServe.
|
|
4096
|
-
if (rt.agentProcess?.spawnServe) {
|
|
4097
|
-
await (0, serve_for_runtime_1.ensureServeForBuiltinRuntime)(this.serveSupervisor, { startWatchdog: () => this.startServeWatchdog() }, logger_1.log);
|
|
4098
|
-
}
|
|
4099
|
-
this.sdkSession.setClient(this.opencodeClient);
|
|
4100
|
-
if (this.trajectoryCollector)
|
|
4101
|
-
this.trajectoryCollector.setOpencodeClient(this.opencodeClient);
|
|
4102
|
-
if (this.automationEngine)
|
|
4103
|
-
this.automationEngine.setRuntimeClient(rt);
|
|
4104
|
-
await this.resubscribeEvents(`runtime switched to '${rt.name}'`);
|
|
4105
|
-
// Desktop hint: a runtime switch swaps the session storage backend
|
|
4106
|
-
// (opencode SQLite vs pi), so cached session lists are stale.
|
|
4107
|
-
this.broadcast({ type: 'runtime_switched', runtime: rt.name, previous: prev?.name ?? null });
|
|
4108
|
-
if (prev && prev.dispose) {
|
|
4109
|
-
try {
|
|
4110
|
-
await prev.dispose();
|
|
4111
|
-
}
|
|
4112
|
-
catch (err) {
|
|
4113
|
-
logger_1.log.warn(`[Runtime] dispose old runtime failed: ${err.message}`);
|
|
4114
|
-
}
|
|
4115
|
-
}
|
|
4116
|
-
},
|
|
4117
|
-
};
|
|
4118
|
-
if (req.method === 'GET' && req.url?.match(/^\/api\/runtime(?:\?|$)/)) {
|
|
4119
|
-
await (0, runtime_switch_1.handleRuntimeGet)(req, res, runtimeDeps);
|
|
4120
|
-
return;
|
|
4121
|
-
}
|
|
4122
|
-
if (req.method === 'POST' && req.url?.match(/^\/api\/runtime\/switch(?:\?|$)/)) {
|
|
4123
|
-
if (this.serveRecovering || this.switchingRuntime) {
|
|
4124
|
-
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
4125
|
-
res.end(JSON.stringify({ error: 'Cannot switch runtime during agent restart' }));
|
|
4126
|
-
return;
|
|
4127
|
-
}
|
|
4128
|
-
this.switchingRuntime = true;
|
|
4129
|
-
try {
|
|
4130
|
-
await (0, runtime_switch_1.handleRuntimeSwitch)(req, res, runtimeDeps);
|
|
4131
|
-
}
|
|
4132
|
-
finally {
|
|
4133
|
-
this.switchingRuntime = false;
|
|
4134
|
-
}
|
|
4135
|
-
return;
|
|
4136
|
-
}
|
|
4137
|
-
if (req.method === 'POST' && req.url?.match(/^\/api\/runtime\/reload(?:\?|$)/)) {
|
|
4138
|
-
await (0, runtime_switch_1.handleRuntimeReload)(req, res, runtimeDeps);
|
|
4139
|
-
return;
|
|
4140
|
-
}
|
|
4141
|
-
if (req.method === 'POST' && req.url?.match(/^\/api\/runtime\/restart-agent(?:\?|$)/)) {
|
|
4142
|
-
await (0, restart_agent_1.handleRestartAgent)(req, res, {
|
|
4143
|
-
capabilities: () => this.runtimeCaps,
|
|
4144
|
-
isRecovering: () => this.serveRecovering,
|
|
4145
|
-
isSwitching: () => this.switchingRuntime,
|
|
4146
|
-
begin: () => { this.serveRecovering = true; },
|
|
4147
|
-
end: () => { this.serveRecovering = false; },
|
|
4148
|
-
restartAgent: () => this.restartAgentOrchestrated(),
|
|
4149
|
-
});
|
|
4150
|
-
return;
|
|
4151
|
-
}
|
|
4152
|
-
return;
|
|
4153
|
-
}
|
|
4154
|
-
// ── Plugin Hub (all four plugin types) ──
|
|
4155
|
-
const pluginHubDeps = {
|
|
4156
|
-
hub: {
|
|
4157
|
-
dirs: {
|
|
4158
|
-
runtime: config_1.config.resolvePath('runtime-plugins'),
|
|
4159
|
-
media: config_1.config.resolvePath('media-plugins'),
|
|
4160
|
-
usage: config_1.config.resolvePath('usage-plugins'),
|
|
4161
|
-
ui: process.env.MAFW_UI_PLUGINS_DIR || path.join(os.homedir(), '.mafw', 'ui-plugins'),
|
|
4162
|
-
},
|
|
4163
|
-
builtinEntries: () => {
|
|
4164
|
-
const entries = [];
|
|
4165
|
-
const rt = (name) => ({ type: 'runtime', name, file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
|
|
4166
|
-
entries.push(rt('opencode'));
|
|
4167
|
-
for (const name of this.runtimeLoader?.getBuiltinNames?.() ?? [])
|
|
4168
|
-
entries.push(rt(name));
|
|
4169
|
-
entries.push({ type: 'media', name: 'pi', file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
|
|
4170
|
-
const usageState = this.pluginLoader?.getState?.() ?? [];
|
|
4171
|
-
for (const s of usageState) {
|
|
4172
|
-
if (s.builtin && s.status === 'ok' && s.name) {
|
|
4173
|
-
entries.push({ type: 'usage', name: s.name, file: s.file, status: 'enabled', size: 0, mtime: '', pluginType: s.pluginType });
|
|
4174
|
-
}
|
|
4175
|
-
}
|
|
4176
|
-
return entries;
|
|
4177
|
-
},
|
|
4178
|
-
getErrors: (type) => {
|
|
4179
|
-
const stateOf = (loader) => (loader && typeof loader.getState === 'function' ? loader.getState() : []);
|
|
4180
|
-
const source = type === 'runtime' ? this.runtimeLoader : type === 'media' ? this.mediaPluginLoader : type === 'usage' ? this.pluginLoader : null;
|
|
4181
|
-
const out = {};
|
|
4182
|
-
for (const p of stateOf(source)) {
|
|
4183
|
-
if (p && p.error)
|
|
4184
|
-
out[p.name || p.file] = p.error;
|
|
4185
|
-
}
|
|
4186
|
-
return out;
|
|
4187
|
-
},
|
|
4188
|
-
configDisabledUsage: () => new Set(Array.isArray(config_1.config.usage?.disabledPlugins) ? config_1.config.usage.disabledPlugins : []),
|
|
4189
|
-
reload: async (type) => {
|
|
4190
|
-
if (type === 'runtime')
|
|
4191
|
-
await this.runtimeLoader?.scan();
|
|
4192
|
-
else if (type === 'media')
|
|
4193
|
-
await this.mediaPluginLoader?.reload();
|
|
4194
|
-
else if (type === 'usage')
|
|
4195
|
-
await this.pluginLoader?.reload();
|
|
4196
|
-
// ui: desktop main fs.watch picks it up automatically
|
|
4197
|
-
},
|
|
4198
|
-
},
|
|
4199
|
-
};
|
|
4200
|
-
try {
|
|
4201
|
-
const cleaned = (0, hub_1.cleanupExamples)(pluginHubDeps.hub);
|
|
4202
|
-
if (cleaned.removed.length)
|
|
4203
|
-
logger_1.log.info(`[PluginsHub] removed stale examples: ${cleaned.removed.length}`);
|
|
4204
|
-
if (cleaned.failed.length)
|
|
4205
|
-
logger_1.log.warn(`[PluginsHub] cleanupExamples failed: ${cleaned.failed.join(', ')}`);
|
|
4206
|
-
}
|
|
4207
|
-
catch (err) {
|
|
4208
|
-
logger_1.log.warn(`[PluginsHub] cleanupExamples error: ${err.message}`);
|
|
4209
|
-
}
|
|
4210
|
-
if (req.method === 'GET' && req.url?.match(/^\/api\/plugins(?:\?|$)/)) {
|
|
4211
|
-
await (0, plugins_1.handlePluginsList)(req, res, pluginHubDeps);
|
|
4212
|
-
return;
|
|
4213
|
-
}
|
|
4214
|
-
if (req.method === 'POST' && req.url?.match(/^\/api\/plugins\/install(?:\?|$)/)) {
|
|
4215
|
-
await (0, plugins_1.handlePluginsInstall)(req, res, pluginHubDeps);
|
|
4216
|
-
return;
|
|
4217
|
-
}
|
|
4218
|
-
if (req.method === 'POST' && req.url?.match(/^\/api\/plugins\/enable(?:\?|$)/)) {
|
|
4219
|
-
await (0, plugins_1.handlePluginsEnable)(req, res, pluginHubDeps);
|
|
4220
|
-
return;
|
|
4221
|
-
}
|
|
4222
|
-
if (req.method === 'POST' && req.url?.match(/^\/api\/plugins\/disable(?:\?|$)/)) {
|
|
4223
|
-
await (0, plugins_1.handlePluginsDisable)(req, res, pluginHubDeps);
|
|
4224
|
-
return;
|
|
4225
|
-
}
|
|
4226
|
-
if (req.method === 'POST' && req.url?.match(/^\/api\/plugins\/delete(?:\?|$)/)) {
|
|
4227
|
-
await (0, plugins_1.handlePluginsDelete)(req, res, pluginHubDeps);
|
|
4228
|
-
return;
|
|
4229
|
-
}
|
|
4295
|
+
// ── /api/runtime routes — P5 Wave 2 起由 registry dispatch 接管
|
|
4296
|
+
// (runtimeDeps/restartAgentDeps 构造上移至私有方法;内联块对未知
|
|
4297
|
+
// /api/runtime/* 悬空 return 吞请求的行为随之终结——现落回 legacy 链)
|
|
4298
|
+
// ── Plugin Hub (all four plugin types) — P5 Wave 2 起由 registry dispatch
|
|
4299
|
+
// 接管(pluginHubDeps 构造上移至私有方法;cleanupExamples 改为命中
|
|
4300
|
+
// /api/plugins 路由时才执行——内联时代每个到达此处的请求都跑)。
|
|
4230
4301
|
// GET /api/orchestration/outcomes — goal outcome query
|
|
4231
4302
|
if (req.url?.match(/^\/api\/orchestration\/outcomes(?:\?|$)/) && req.method === 'GET') {
|
|
4232
4303
|
const u = new URL(req.url, 'http://localhost');
|
|
@@ -4322,20 +4393,37 @@ class MafwScheduler {
|
|
|
4322
4393
|
}
|
|
4323
4394
|
return;
|
|
4324
4395
|
}
|
|
4325
|
-
// POST /api/permissions/{id}/reply
|
|
4396
|
+
// POST /api/permissions/{id}/reply ── { reply, message? }(契约:列表反查 sessionID → permissionReply)
|
|
4326
4397
|
const pReplyMatch = req.url?.match(/^\/api\/permissions\/([^/]+)\/reply(?:\?|$)/);
|
|
4327
4398
|
if (pReplyMatch && req.method === 'POST') {
|
|
4328
4399
|
if (this.capGuard(res, 'nativeApprovals'))
|
|
4329
4400
|
return;
|
|
4401
|
+
const runtime = this.opencodeClient;
|
|
4402
|
+
if (!runtime?.session?.permissionList || !runtime?.session?.permissionReply) {
|
|
4403
|
+
res.writeHead(503);
|
|
4404
|
+
res.end(JSON.stringify({ status: 'error', error: 'permission API not available on this runtime' }));
|
|
4405
|
+
return;
|
|
4406
|
+
}
|
|
4330
4407
|
try {
|
|
4331
4408
|
const body = JSON.parse(await readBody(req));
|
|
4332
|
-
const
|
|
4333
|
-
if (
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4409
|
+
const reply = body.reply;
|
|
4410
|
+
if (reply !== 'once' && reply !== 'always' && reply !== 'reject') {
|
|
4411
|
+
res.writeHead(400);
|
|
4412
|
+
res.end(JSON.stringify({ status: 'error', error: "reply must be 'once'|'always'|'reject'" }));
|
|
4413
|
+
return;
|
|
4414
|
+
}
|
|
4415
|
+
// 反查 sessionID:pending 列表项携带(跨 runtime 形状一致)
|
|
4416
|
+
const pending = await runtime.session.permissionList();
|
|
4417
|
+
const found = (Array.isArray(pending) ? pending : []).find((p) => p?.id === pReplyMatch[1]);
|
|
4418
|
+
if (!found?.sessionID) {
|
|
4419
|
+
res.writeHead(404);
|
|
4420
|
+
res.end(JSON.stringify({ status: 'error', error: 'permission request not found' }));
|
|
4421
|
+
return;
|
|
4422
|
+
}
|
|
4423
|
+
const ok = await runtime.session.permissionReply(found.sessionID, pReplyMatch[1], reply, body.message);
|
|
4424
|
+
if (!ok) {
|
|
4425
|
+
res.writeHead(404);
|
|
4426
|
+
res.end(JSON.stringify({ status: 'error', error: 'permission request not found' }));
|
|
4339
4427
|
return;
|
|
4340
4428
|
}
|
|
4341
4429
|
res.end(JSON.stringify({ status: 'ok' }));
|
|
@@ -4672,13 +4760,7 @@ class MafwScheduler {
|
|
|
4672
4760
|
}
|
|
4673
4761
|
return;
|
|
4674
4762
|
}
|
|
4675
|
-
// DELETE/PATCH /api/sessions/:id —
|
|
4676
|
-
if (await (0, session_mutations_1.handleSessionMutations)(req, res, {
|
|
4677
|
-
getCapabilities: () => this.runtimeCaps,
|
|
4678
|
-
getClient: () => (this.opencodeClient ?? null),
|
|
4679
|
-
})) {
|
|
4680
|
-
return;
|
|
4681
|
-
}
|
|
4763
|
+
// DELETE/PATCH /api/sessions/:id — P5 Wave 1 起由 registry dispatch 接管
|
|
4682
4764
|
// GET /api/sessions — list sessions (optional ?projectID=xxx)
|
|
4683
4765
|
if (req.url?.match(/^\/api\/sessions(?:\?|$)/) && req.method === 'GET') {
|
|
4684
4766
|
try {
|
|
@@ -4842,44 +4924,7 @@ class MafwScheduler {
|
|
|
4842
4924
|
}
|
|
4843
4925
|
return;
|
|
4844
4926
|
}
|
|
4845
|
-
//
|
|
4846
|
-
if (req.url?.match(/^\/api\/usage\/plugins(?:\?|$)/) && req.method === 'GET') {
|
|
4847
|
-
await (0, usage_plugins_1.handleUsagePluginsList)(req, res, this.usagePluginsDeps());
|
|
4848
|
-
return;
|
|
4849
|
-
}
|
|
4850
|
-
// POST /api/usage/plugins/create — template wizard or raw source
|
|
4851
|
-
if (req.url?.match(/^\/api\/usage\/plugins\/create$/) && req.method === 'POST') {
|
|
4852
|
-
await (0, usage_plugins_1.handleUsagePluginCreate)(req, res, this.usagePluginsDeps());
|
|
4853
|
-
return;
|
|
4854
|
-
}
|
|
4855
|
-
// GET/PUT /api/usage/plugins/:name/source — user plugin code editor
|
|
4856
|
-
const mSource = req.url?.match(/^\/api\/usage\/plugins\/([^/]+)\/source$/);
|
|
4857
|
-
if (mSource && req.method === 'GET') {
|
|
4858
|
-
await (0, usage_plugins_1.handleUsagePluginSourceGet)(req, res, this.usagePluginsDeps(), decodeURIComponent(mSource[1]));
|
|
4859
|
-
return;
|
|
4860
|
-
}
|
|
4861
|
-
if (mSource && req.method === 'PUT') {
|
|
4862
|
-
await (0, usage_plugins_1.handleUsagePluginSourcePut)(req, res, this.usagePluginsDeps(), decodeURIComponent(mSource[1]));
|
|
4863
|
-
return;
|
|
4864
|
-
}
|
|
4865
|
-
// POST /api/usage/plugins/:name/test — one-shot adapter fetch
|
|
4866
|
-
const mTest = req.url?.match(/^\/api\/usage\/plugins\/([^/]+)\/test$/);
|
|
4867
|
-
if (mTest && req.method === 'POST') {
|
|
4868
|
-
await (0, usage_plugins_1.handleUsagePluginTest)(req, res, this.usagePluginsDeps(), decodeURIComponent(mTest[1]));
|
|
4869
|
-
return;
|
|
4870
|
-
}
|
|
4871
|
-
// DELETE /api/usage/plugins/:name — remove user plugin file
|
|
4872
|
-
const mDel = req.url?.match(/^\/api\/usage\/plugins\/([^/]+)$/);
|
|
4873
|
-
if (mDel && req.method === 'DELETE') {
|
|
4874
|
-
await (0, usage_plugins_1.handleUsagePluginDelete)(req, res, this.usagePluginsDeps(), decodeURIComponent(mDel[1]));
|
|
4875
|
-
return;
|
|
4876
|
-
}
|
|
4877
|
-
// POST /api/usage/plugins/reload — manual reload
|
|
4878
|
-
if (req.url?.match(/^\/api\/usage\/plugins\/reload$/) && req.method === 'POST') {
|
|
4879
|
-
await this.pluginLoader?.reload();
|
|
4880
|
-
await (0, usage_plugins_1.handleUsagePluginsList)(req, res, this.usagePluginsDeps());
|
|
4881
|
-
return;
|
|
4882
|
-
}
|
|
4927
|
+
// /api/usage/plugins* — P5 Wave 1 起由 registry dispatch 接管(7 条)
|
|
4883
4928
|
// GET /api/media/plugins — media engine plugin state list
|
|
4884
4929
|
if (req.url?.match(/^\/api\/media\/plugins(?:\?|$)/) && req.method === 'GET') {
|
|
4885
4930
|
const plugins = (this.mediaPluginLoader?.getState() ?? []).map(s => ({
|
|
@@ -4907,43 +4952,10 @@ class MafwScheduler {
|
|
|
4907
4952
|
res.end(JSON.stringify({ ok: true, plugins }));
|
|
4908
4953
|
return;
|
|
4909
4954
|
}
|
|
4910
|
-
// POST /api/media/switch —
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
reloadPlugins: async () => { await this.mediaPluginLoader?.reload(); },
|
|
4915
|
-
availableEngines: () => (this.mediaPluginLoader?.getState().map(s => s.name).filter((n) => !!n) ?? []),
|
|
4916
|
-
currentMedia: () => ({
|
|
4917
|
-
engine: config_1.config.raw.media.engine,
|
|
4918
|
-
image: config_1.config.raw.media.image,
|
|
4919
|
-
video: config_1.config.raw.media.video,
|
|
4920
|
-
audio: config_1.config.raw.media.audio,
|
|
4921
|
-
}),
|
|
4922
|
-
});
|
|
4923
|
-
return;
|
|
4924
|
-
}
|
|
4925
|
-
// GET/POST /api/model-config — recall worker model + media models (hot-apply)
|
|
4926
|
-
if (req.url?.match(/^\/api\/model-config(?:\?|$)/) && req.method === 'GET') {
|
|
4927
|
-
await (0, model_config_1.handleModelConfigGet)(req, res, this.modelConfigDeps());
|
|
4928
|
-
return;
|
|
4929
|
-
}
|
|
4930
|
-
if (req.url?.match(/^\/api\/model-config(?:\?|$)/) && req.method === 'POST') {
|
|
4931
|
-
await (0, model_config_1.handleModelConfigUpdate)(req, res, this.modelConfigDeps());
|
|
4932
|
-
return;
|
|
4933
|
-
}
|
|
4934
|
-
// POST /api/sessions/:id/fork|revert|unrevert — session branch primitives
|
|
4935
|
-
// (capability-gated: sessionBranchApi; unrevert is opencode-only → 404 on pi)
|
|
4936
|
-
if (req.url?.match(/^\/api\/sessions\/[^/]+\/(fork|revert|unrevert)(?:\?|$)/) && req.method === 'POST') {
|
|
4937
|
-
const handled = await (0, session_branch_1.handleSessionBranch)(req, res, req.url, { getRuntime: () => this.opencodeClient ?? null });
|
|
4938
|
-
if (handled)
|
|
4939
|
-
return;
|
|
4940
|
-
}
|
|
4941
|
-
// POST /api/session/:id/summarize — 手动压缩会话(TUI /compact;runtime 薄代理)
|
|
4942
|
-
if (req.url?.match(/^\/api\/session\/[^/]+\/summarize(?:\?|$)/) && req.method === 'POST') {
|
|
4943
|
-
const handled = await (0, session_summarize_1.handleSessionSummarize)(req, res, req.url, { getRuntime: () => this.opencodeClient ?? null });
|
|
4944
|
-
if (handled)
|
|
4945
|
-
return;
|
|
4946
|
-
}
|
|
4955
|
+
// POST /api/media/switch — P5 Wave 1 起由 registry dispatch 接管
|
|
4956
|
+
// GET/POST /api/model-config — P5 Wave 1 起由 registry dispatch 接管
|
|
4957
|
+
// POST /api/sessions/:id/fork|revert|unrevert + /api/session/:id/summarize
|
|
4958
|
+
// — P5 Wave 1 起由 registry dispatch 接管
|
|
4947
4959
|
// GET /api/usage?sessionID=xxx&projectID=xxx — consolidated usage (summary + providers)
|
|
4948
4960
|
if (req.url?.match(/^\/api\/usage(?:\?|$)/) && req.method === 'GET') {
|
|
4949
4961
|
try {
|
|
@@ -5183,7 +5195,10 @@ class MafwScheduler {
|
|
|
5183
5195
|
session_id: sessionID,
|
|
5184
5196
|
turn_id: turnId,
|
|
5185
5197
|
source: source,
|
|
5186
|
-
|
|
5198
|
+
// Redact secrets once at capture so every downstream consumer
|
|
5199
|
+
// (turnCompress transcripts, worker prompts, archive) only
|
|
5200
|
+
// ever sees redacted content.
|
|
5201
|
+
content: (0, redact_1.redactSecrets)(content).slice(0, 100_000),
|
|
5187
5202
|
failure,
|
|
5188
5203
|
});
|
|
5189
5204
|
// Async scan prefetch: precompute semantic recall for this turn
|
|
@@ -5203,6 +5218,7 @@ class MafwScheduler {
|
|
|
5203
5218
|
});
|
|
5204
5219
|
return;
|
|
5205
5220
|
}
|
|
5221
|
+
// POST /api/events — P5 Wave 1 起由 registry dispatch 接管
|
|
5206
5222
|
// SSE 事件(→ Dashboard / Chat)
|
|
5207
5223
|
if (req.url && req.url.startsWith('/api/events') && req.method === 'GET') {
|
|
5208
5224
|
const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
@@ -5231,8 +5247,9 @@ class MafwScheduler {
|
|
|
5231
5247
|
res.end(JSON.stringify({ status: 'ok' }));
|
|
5232
5248
|
return;
|
|
5233
5249
|
}
|
|
5234
|
-
// Reverse proxy to
|
|
5235
|
-
|
|
5250
|
+
// Reverse proxy to the agent backend for non-MAFW routes —
|
|
5251
|
+
// target from the runtime contract (getBaseUrl), config 仅 bootstrap 兜底
|
|
5252
|
+
const serveUrl = this.opencodeClient?.getBaseUrl?.() ?? config_1.config.server.serveUrl;
|
|
5236
5253
|
try {
|
|
5237
5254
|
const proxyUrl = new URL(req.url || '/', serveUrl);
|
|
5238
5255
|
const proxyReq = http.request(proxyUrl, {
|
|
@@ -5289,7 +5306,7 @@ class MafwScheduler {
|
|
|
5289
5306
|
server.listen(this.apiPort, () => {
|
|
5290
5307
|
logger_1.log.info(`[Scheduler] HTTP API on port ${this.apiPort}`);
|
|
5291
5308
|
logger_1.log.info(`[Scheduler] - POST /register { projectDir, mafwDir }`);
|
|
5292
|
-
logger_1.log.info(`[Scheduler] - POST /control { action, goalId, ... }`);
|
|
5309
|
+
logger_1.log.info(`[Scheduler] - POST /control | /api/goals/control { action, goalId, ... }`);
|
|
5293
5310
|
logger_1.log.info(`[Scheduler] - GET /health`);
|
|
5294
5311
|
logger_1.log.info(`[Scheduler] - GET /mcp (MCP legacy SSE / StreamableHTTP 405)`);
|
|
5295
5312
|
logger_1.log.info(`[Scheduler] - POST /mcp (MCP StreamableHTTP stateless + legacy SSE messages)`);
|
|
@@ -6204,6 +6221,14 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6204
6221
|
});
|
|
6205
6222
|
return run;
|
|
6206
6223
|
}
|
|
6224
|
+
// Slot-based read of a manager-session kv entry: each runtime owns a slot
|
|
6225
|
+
// in the value (byRuntime), so switching runtimes never drops a topic —
|
|
6226
|
+
// switching back resumes the previous manager. Legacy v1 entries are
|
|
6227
|
+
// inferred by session id prefix and only surface under their own runtime.
|
|
6228
|
+
// See manager-session-runtime.ts.
|
|
6229
|
+
readManagerSessionEntry(projectDir) {
|
|
6230
|
+
return (0, manager_session_runtime_1.readManagerSlot)(this.getGatewayDb().kvGet('manager-session', projectDir), this.runtimeName);
|
|
6231
|
+
}
|
|
6207
6232
|
// Serialized rotate: joins any in-flight ensure/create for the same project
|
|
6208
6233
|
// so rotate and lazy-init never double-create (spec §4).
|
|
6209
6234
|
async runManagerExclusive(projectDir, fn) {
|
|
@@ -6220,7 +6245,7 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6220
6245
|
// The inflight map is shared with ensureManagerSession, whose joiners
|
|
6221
6246
|
// expect the promise to resolve to a session id string — map the rotate
|
|
6222
6247
|
// result onto the resulting kv entry.
|
|
6223
|
-
const mapped = run.then(() => this.
|
|
6248
|
+
const mapped = run.then(() => this.readManagerSessionEntry(projectDir)?.sessionId ?? '', () => '');
|
|
6224
6249
|
this.managerSessionInflight.set(projectDir, mapped);
|
|
6225
6250
|
try {
|
|
6226
6251
|
return await run;
|
|
@@ -6235,7 +6260,7 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6235
6260
|
throw new Error('opencodeClient not available');
|
|
6236
6261
|
// Manager identity lives in the gateway DB (kv_store), so it survives
|
|
6237
6262
|
// project-directory churn and never gets orphaned by directory moves.
|
|
6238
|
-
const existing = this.
|
|
6263
|
+
const existing = this.readManagerSessionEntry(projectDir);
|
|
6239
6264
|
if (existing?.sessionId) {
|
|
6240
6265
|
await this.sdkSession.registerExternal(existing.sessionId, projectDir, {
|
|
6241
6266
|
mafw: { role: 'manager', pinned: true, exemptFromTrim: true, exemptFromEvict: true, exemptFromArchive: true },
|
|
@@ -6251,7 +6276,7 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6251
6276
|
throw new Error('Failed to create manager session: no id returned');
|
|
6252
6277
|
}
|
|
6253
6278
|
const createdAt = new Date().toISOString();
|
|
6254
|
-
this.getGatewayDb().kvSet('manager-session', projectDir, { sessionId, createdAt });
|
|
6279
|
+
this.getGatewayDb().kvSet('manager-session', projectDir, (0, manager_session_runtime_1.writeManagerSlot)(this.getGatewayDb().kvGet('manager-session', projectDir), this.runtimeName, { sessionId, createdAt }));
|
|
6255
6280
|
this.registerInternalSession(sessionId, 'manager');
|
|
6256
6281
|
try {
|
|
6257
6282
|
await this.sdkSession.registerExternal(sessionId, projectDir, {
|
|
@@ -6283,7 +6308,7 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6283
6308
|
}
|
|
6284
6309
|
rotateDeps() {
|
|
6285
6310
|
return {
|
|
6286
|
-
getManagerSession: (pd) => this.
|
|
6311
|
+
getManagerSession: (pd) => this.readManagerSessionEntry(pd),
|
|
6287
6312
|
lock: (pd, fn) => this.runManagerExclusive(pd, fn),
|
|
6288
6313
|
ensure: (pd) => this.ensureManagerSession(pd, this.mafwDirFor(pd)),
|
|
6289
6314
|
downgrade: async (sid) => {
|
|
@@ -6330,7 +6355,7 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6330
6355
|
return undefined;
|
|
6331
6356
|
if (!this.milestonePush) {
|
|
6332
6357
|
this.milestonePush = new milestone_push_1.MilestonePushNotifier({
|
|
6333
|
-
getManagerSession: (pd) => this.
|
|
6358
|
+
getManagerSession: (pd) => this.readManagerSessionEntry(pd),
|
|
6334
6359
|
wasNotified: (key) => !!this.getGatewayDb().kvGet('milestone-notified', key),
|
|
6335
6360
|
markNotified: (key) => this.getGatewayDb().kvSet('milestone-notified', key, { at: new Date().toISOString() }),
|
|
6336
6361
|
readGoalState: (goalId, pd) => {
|
|
@@ -6359,7 +6384,7 @@ ${observations.map((o, i) => `[${i + 1}] ${o}`).join('\n')}`;
|
|
|
6359
6384
|
const sessionId = session.id;
|
|
6360
6385
|
if (!sessionId)
|
|
6361
6386
|
throw new Error('Failed to create manager session: no id returned');
|
|
6362
|
-
this.getGatewayDb().kvSet('manager-session', projectDir, { sessionId, createdAt: new Date().toISOString() });
|
|
6387
|
+
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() }));
|
|
6363
6388
|
this.registerInternalSession(sessionId, 'manager');
|
|
6364
6389
|
await this.sdkSession.registerExternal(sessionId, projectDir, {
|
|
6365
6390
|
mafw: { role: 'manager', pinned: true, exemptFromTrim: true, exemptFromEvict: true, exemptFromArchive: true },
|