u-foo 3.0.21 → 3.0.22
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 +13 -7
- package/README.zh-CN.md +11 -7
- package/package.json +1 -1
- package/src/agents/launch/launcher.js +52 -29
- package/src/app/chat/commandExecutor.js +15 -0
- package/src/app/chat/daemonConnection.js +7 -2
- package/src/app/chat/daemonCoordinator.js +1 -0
- package/src/app/chat/daemonTransport.js +21 -5
- package/src/app/chat/projectCloseController.js +6 -1
- package/src/app/chat/transport.js +84 -8
- package/src/app/cli/run.js +44 -7
- package/src/config.js +48 -0
- package/src/runtime/contracts/eventContract.js +1 -0
- package/src/runtime/daemon/agentProcessManager.js +34 -14
- package/src/runtime/daemon/controlPlaneService.js +14 -5
- package/src/runtime/daemon/endpoint.js +79 -0
- package/src/runtime/daemon/globalDaemon.js +394 -0
- package/src/runtime/daemon/groupOrchestrator.js +6 -0
- package/src/runtime/daemon/index.js +272 -109
- package/src/runtime/daemon/mcpHttpServer.js +5 -0
- package/src/runtime/daemon/ops.js +48 -17
- package/src/runtime/daemon/processLifecycle.js +127 -0
- package/src/runtime/daemon/projectContext.js +46 -0
- package/src/runtime/daemon/projectRuntime.js +221 -0
- package/src/runtime/daemon/projectRuntimeGateway.js +72 -0
- package/src/runtime/daemon/projectRuntimeManager.js +268 -0
- package/src/runtime/daemon/run.js +66 -12
- package/src/runtime/projects/registry.js +26 -4
- package/src/runtime/projects/runtimes.js +1 -1
- package/src/ui/rustChatHost.js +46 -8
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
canonicalProjectRoot,
|
|
5
|
+
} = require("../projects");
|
|
6
|
+
const { createProjectContext } = require("./projectContext");
|
|
7
|
+
const {
|
|
8
|
+
createProjectRuntime,
|
|
9
|
+
RUNTIME_STATES,
|
|
10
|
+
} = require("./projectRuntime");
|
|
11
|
+
|
|
12
|
+
const DEFAULT_IDLE_GRACE_MS = 5 * 60 * 1000;
|
|
13
|
+
const DEFAULT_SWEEP_INTERVAL_MS = 30 * 1000;
|
|
14
|
+
|
|
15
|
+
class ProjectRuntimeManager {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.contextFactory = options.contextFactory || createProjectContext;
|
|
18
|
+
this.runtimeFactory = options.runtimeFactory || createProjectRuntime;
|
|
19
|
+
this.configureRuntime = typeof options.configureRuntime === "function"
|
|
20
|
+
? options.configureRuntime
|
|
21
|
+
: null;
|
|
22
|
+
this.authorizeProjectRoot = typeof options.authorizeProjectRoot === "function"
|
|
23
|
+
? options.authorizeProjectRoot
|
|
24
|
+
: (() => true);
|
|
25
|
+
this.idleGraceMs = Number.isFinite(options.idleGraceMs)
|
|
26
|
+
? Math.max(0, Number(options.idleGraceMs))
|
|
27
|
+
: DEFAULT_IDLE_GRACE_MS;
|
|
28
|
+
this.maxActiveRuntimes = Number.isFinite(options.maxActiveRuntimes)
|
|
29
|
+
? Math.max(1, Number(options.maxActiveRuntimes))
|
|
30
|
+
: 32;
|
|
31
|
+
this.maxConcurrentRequests = Number.isFinite(options.maxConcurrentRequests)
|
|
32
|
+
? Math.max(1, Number(options.maxConcurrentRequests))
|
|
33
|
+
: 256;
|
|
34
|
+
this.now = typeof options.now === "function" ? options.now : Date.now;
|
|
35
|
+
this.runtimes = new Map();
|
|
36
|
+
this.activationPromises = new Map();
|
|
37
|
+
this.activeRequestCount = 0;
|
|
38
|
+
this.disposed = false;
|
|
39
|
+
const sweepIntervalMs = Number.isFinite(options.sweepIntervalMs)
|
|
40
|
+
? Math.max(0, Number(options.sweepIntervalMs))
|
|
41
|
+
: DEFAULT_SWEEP_INTERVAL_MS;
|
|
42
|
+
this.sweepTimer = sweepIntervalMs > 0
|
|
43
|
+
? setInterval(() => {
|
|
44
|
+
void this.sweepIdle();
|
|
45
|
+
}, sweepIntervalMs)
|
|
46
|
+
: null;
|
|
47
|
+
if (this.sweepTimer && typeof this.sweepTimer.unref === "function") {
|
|
48
|
+
this.sweepTimer.unref();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
resolveProject(projectRoot, options = {}) {
|
|
53
|
+
const canonicalRoot = canonicalProjectRoot(projectRoot);
|
|
54
|
+
if (this.authorizeProjectRoot(canonicalRoot, options) !== true) {
|
|
55
|
+
const err = new Error(`project runtime access denied: ${canonicalRoot}`);
|
|
56
|
+
err.code = "PROJECT_RUNTIME_ACCESS_DENIED";
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
return canonicalRoot;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
createEntry(projectRoot, options = {}) {
|
|
63
|
+
const previousGeneration = Number(options.previousGeneration) || 0;
|
|
64
|
+
const context = this.contextFactory({
|
|
65
|
+
...options,
|
|
66
|
+
projectRoot,
|
|
67
|
+
runtimeGeneration: previousGeneration + 1,
|
|
68
|
+
});
|
|
69
|
+
const runtime = this.runtimeFactory(context, options.runtimeOptions || {});
|
|
70
|
+
if (this.configureRuntime) this.configureRuntime(runtime, context);
|
|
71
|
+
const entry = {
|
|
72
|
+
context,
|
|
73
|
+
runtime,
|
|
74
|
+
lastUsedAtMs: this.now(),
|
|
75
|
+
createdAt: new Date(this.now()).toISOString(),
|
|
76
|
+
};
|
|
77
|
+
this.runtimes.set(context.projectId, entry);
|
|
78
|
+
return entry;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
entryForRoot(projectRoot) {
|
|
82
|
+
const canonicalRoot = canonicalProjectRoot(projectRoot);
|
|
83
|
+
for (const entry of this.runtimes.values()) {
|
|
84
|
+
if (entry.context.projectRoot === canonicalRoot) return entry;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
activeRuntimeCount() {
|
|
90
|
+
let count = 0;
|
|
91
|
+
for (const entry of this.runtimes.values()) {
|
|
92
|
+
if (entry.runtime.state === RUNTIME_STATES.ACTIVE) count += 1;
|
|
93
|
+
}
|
|
94
|
+
return count;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async activate(projectRoot, options = {}) {
|
|
98
|
+
if (this.disposed) {
|
|
99
|
+
const err = new Error("project runtime manager is disposed");
|
|
100
|
+
err.code = "PROJECT_RUNTIME_MANAGER_DISPOSED";
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
const canonicalRoot = this.resolveProject(projectRoot, options);
|
|
104
|
+
let entry = this.entryForRoot(canonicalRoot);
|
|
105
|
+
if (entry && entry.runtime.state === RUNTIME_STATES.ACTIVE) {
|
|
106
|
+
entry.lastUsedAtMs = this.now();
|
|
107
|
+
return entry.runtime;
|
|
108
|
+
}
|
|
109
|
+
const projectId = entry
|
|
110
|
+
? entry.context.projectId
|
|
111
|
+
: this.contextFactory({
|
|
112
|
+
...options,
|
|
113
|
+
projectRoot: canonicalRoot,
|
|
114
|
+
runtimeGeneration: 1,
|
|
115
|
+
}).projectId;
|
|
116
|
+
if (this.activationPromises.has(projectId)) {
|
|
117
|
+
return this.activationPromises.get(projectId);
|
|
118
|
+
}
|
|
119
|
+
const activation = (async () => {
|
|
120
|
+
if (!entry) {
|
|
121
|
+
entry = this.createEntry(canonicalRoot, options);
|
|
122
|
+
} else if (
|
|
123
|
+
entry.runtime.state === RUNTIME_STATES.FAILED
|
|
124
|
+
|| entry.runtime.state === RUNTIME_STATES.DISPOSED
|
|
125
|
+
) {
|
|
126
|
+
entry = await this.recycle(canonicalRoot, options);
|
|
127
|
+
}
|
|
128
|
+
if (
|
|
129
|
+
entry.runtime.state !== RUNTIME_STATES.ACTIVE
|
|
130
|
+
&& this.activeRuntimeCount() >= this.maxActiveRuntimes
|
|
131
|
+
) {
|
|
132
|
+
await this.sweepIdle({ forcePressure: true });
|
|
133
|
+
}
|
|
134
|
+
if (
|
|
135
|
+
entry.runtime.state !== RUNTIME_STATES.ACTIVE
|
|
136
|
+
&& this.activeRuntimeCount() >= this.maxActiveRuntimes
|
|
137
|
+
) {
|
|
138
|
+
const err = new Error(
|
|
139
|
+
`active project runtime limit reached (${this.maxActiveRuntimes})`
|
|
140
|
+
);
|
|
141
|
+
err.code = "PROJECT_RUNTIME_LIMIT";
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
await entry.runtime.activate();
|
|
145
|
+
entry.lastUsedAtMs = this.now();
|
|
146
|
+
return entry.runtime;
|
|
147
|
+
})();
|
|
148
|
+
this.activationPromises.set(projectId, activation);
|
|
149
|
+
try {
|
|
150
|
+
return await activation;
|
|
151
|
+
} finally {
|
|
152
|
+
this.activationPromises.delete(projectId);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async call(projectRoot, operation, args = {}, requestContext = {}) {
|
|
157
|
+
if (this.activeRequestCount >= this.maxConcurrentRequests) {
|
|
158
|
+
const err = new Error(
|
|
159
|
+
`global project runtime request limit reached (${this.maxConcurrentRequests})`
|
|
160
|
+
);
|
|
161
|
+
err.code = "PROJECT_RUNTIME_REQUEST_LIMIT";
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
const runtime = await this.activate(projectRoot, requestContext);
|
|
165
|
+
const entry = this.runtimes.get(runtime.context.projectId);
|
|
166
|
+
this.activeRequestCount += 1;
|
|
167
|
+
if (entry) entry.lastUsedAtMs = this.now();
|
|
168
|
+
try {
|
|
169
|
+
return await runtime.call(operation, args, requestContext);
|
|
170
|
+
} finally {
|
|
171
|
+
this.activeRequestCount -= 1;
|
|
172
|
+
if (entry) entry.lastUsedAtMs = this.now();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
cancel(projectRoot, requestId) {
|
|
177
|
+
const entry = this.entryForRoot(projectRoot);
|
|
178
|
+
return entry ? entry.runtime.cancel(requestId) : false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async suspend(projectRoot) {
|
|
182
|
+
const entry = this.entryForRoot(projectRoot);
|
|
183
|
+
if (!entry) return false;
|
|
184
|
+
if (entry.runtime.state === RUNTIME_STATES.DORMANT) return true;
|
|
185
|
+
await entry.runtime.suspend();
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async recycle(projectRoot, options = {}) {
|
|
190
|
+
const canonicalRoot = this.resolveProject(projectRoot, options);
|
|
191
|
+
const existing = this.entryForRoot(canonicalRoot);
|
|
192
|
+
const generation = existing ? existing.context.runtimeGeneration : 0;
|
|
193
|
+
if (existing) {
|
|
194
|
+
existing.runtime.dispose();
|
|
195
|
+
this.runtimes.delete(existing.context.projectId);
|
|
196
|
+
}
|
|
197
|
+
return this.createEntry(canonicalRoot, {
|
|
198
|
+
...options,
|
|
199
|
+
previousGeneration: generation,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
remove(projectRoot) {
|
|
204
|
+
const entry = this.entryForRoot(projectRoot);
|
|
205
|
+
if (!entry) return false;
|
|
206
|
+
entry.runtime.dispose();
|
|
207
|
+
this.runtimes.delete(entry.context.projectId);
|
|
208
|
+
this.activationPromises.delete(entry.context.projectId);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async sweepIdle(options = {}) {
|
|
213
|
+
const nowMs = this.now();
|
|
214
|
+
const candidates = Array.from(this.runtimes.values())
|
|
215
|
+
.filter((entry) => entry.runtime.state === RUNTIME_STATES.ACTIVE)
|
|
216
|
+
.sort((a, b) => a.lastUsedAtMs - b.lastUsedAtMs);
|
|
217
|
+
let suspended = 0;
|
|
218
|
+
for (const entry of candidates) {
|
|
219
|
+
const idleForMs = nowMs - entry.lastUsedAtMs;
|
|
220
|
+
const underPressure = options.forcePressure === true
|
|
221
|
+
&& this.activeRuntimeCount() >= this.maxActiveRuntimes;
|
|
222
|
+
if (!underPressure && idleForMs < this.idleGraceMs) continue;
|
|
223
|
+
if (!entry.runtime.canSuspend()) continue;
|
|
224
|
+
try {
|
|
225
|
+
await entry.runtime.suspend();
|
|
226
|
+
suspended += 1;
|
|
227
|
+
} catch {
|
|
228
|
+
// A busy/failing runtime is reported by its own status and skipped.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return suspended;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
status() {
|
|
235
|
+
return {
|
|
236
|
+
disposed: this.disposed,
|
|
237
|
+
runtime_count: this.runtimes.size,
|
|
238
|
+
active_runtime_count: this.activeRuntimeCount(),
|
|
239
|
+
active_request_count: this.activeRequestCount,
|
|
240
|
+
max_active_runtimes: this.maxActiveRuntimes,
|
|
241
|
+
max_concurrent_requests: this.maxConcurrentRequests,
|
|
242
|
+
runtimes: Array.from(this.runtimes.values())
|
|
243
|
+
.map((entry) => ({
|
|
244
|
+
...entry.runtime.status(),
|
|
245
|
+
manager_last_used_at: new Date(entry.lastUsedAtMs).toISOString(),
|
|
246
|
+
}))
|
|
247
|
+
.sort((a, b) => a.project_root.localeCompare(b.project_root)),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
dispose() {
|
|
252
|
+
if (this.disposed) return;
|
|
253
|
+
this.disposed = true;
|
|
254
|
+
if (this.sweepTimer) clearInterval(this.sweepTimer);
|
|
255
|
+
this.sweepTimer = null;
|
|
256
|
+
for (const entry of this.runtimes.values()) {
|
|
257
|
+
entry.runtime.dispose();
|
|
258
|
+
}
|
|
259
|
+
this.runtimes.clear();
|
|
260
|
+
this.activationPromises.clear();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
DEFAULT_IDLE_GRACE_MS,
|
|
266
|
+
DEFAULT_SWEEP_INTERVAL_MS,
|
|
267
|
+
ProjectRuntimeManager,
|
|
268
|
+
};
|
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
|
+
const fs = require("fs");
|
|
2
3
|
const { startDaemon, stopDaemon, isRunning } = require("./index");
|
|
4
|
+
const { startGlobalDaemon } = require("./globalDaemon");
|
|
3
5
|
const { restartDaemonLifecycleSync } = require("./restart");
|
|
4
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
loadConfig,
|
|
8
|
+
defaultAgentModelForProvider,
|
|
9
|
+
normalizeDaemonTopology,
|
|
10
|
+
saveGlobalDaemonConfig,
|
|
11
|
+
} = require("../../config");
|
|
5
12
|
const { resolveNodeExecutable } = require("../process/nodeExecutable");
|
|
13
|
+
const {
|
|
14
|
+
isGlobalControllerProjectRoot,
|
|
15
|
+
resolveGlobalControllerProjectRoot,
|
|
16
|
+
} = require("../projects");
|
|
17
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
6
18
|
|
|
7
|
-
function spawnDaemonStart(projectRoot) {
|
|
19
|
+
function spawnDaemonStart(projectRoot, daemonTopology = "") {
|
|
8
20
|
const { spawn } = require("child_process");
|
|
9
21
|
const child = spawn(resolveNodeExecutable(), [path.join(__dirname, "..", "..", "..", "bin", "ufoo.js"), "daemon", "start"], {
|
|
10
22
|
detached: true,
|
|
11
23
|
stdio: "ignore",
|
|
12
|
-
env: {
|
|
24
|
+
env: {
|
|
25
|
+
...process.env,
|
|
26
|
+
UFOO_DAEMON_CHILD: "1",
|
|
27
|
+
...(daemonTopology ? { UFOO_DAEMON_TOPOLOGY: daemonTopology } : {}),
|
|
28
|
+
},
|
|
13
29
|
cwd: projectRoot,
|
|
14
30
|
});
|
|
15
31
|
child.unref();
|
|
@@ -22,8 +38,27 @@ function sleepSync(ms) {
|
|
|
22
38
|
|
|
23
39
|
function runDaemonCli(argv) {
|
|
24
40
|
const cmd = argv[1] || "start";
|
|
41
|
+
if (cmd === "topology" || cmd === "--topology") {
|
|
42
|
+
const requested = String(argv[2] || "").trim().toLowerCase();
|
|
43
|
+
if (!["project", "hybrid", "global"].includes(requested)) {
|
|
44
|
+
throw new Error("daemon topology requires project|hybrid|global");
|
|
45
|
+
}
|
|
46
|
+
const saved = saveGlobalDaemonConfig({ daemonTopology: requested });
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.log(saved.daemonTopology);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
25
51
|
const projectRoot = process.cwd();
|
|
26
52
|
const config = loadConfig(projectRoot);
|
|
53
|
+
const daemonTopology = normalizeDaemonTopology(
|
|
54
|
+
process.env.UFOO_DAEMON_TOPOLOGY || config.daemonTopology
|
|
55
|
+
);
|
|
56
|
+
const daemonRoot = daemonTopology === "project"
|
|
57
|
+
? projectRoot
|
|
58
|
+
: resolveGlobalControllerProjectRoot();
|
|
59
|
+
if (daemonTopology !== "project") {
|
|
60
|
+
fs.mkdirSync(getUfooPaths(daemonRoot).ufooDir, { recursive: true });
|
|
61
|
+
}
|
|
27
62
|
const envProvider = process.env.UFOO_AGENT_PROVIDER;
|
|
28
63
|
const provider = envProvider || config.agentProvider || "codex-cli";
|
|
29
64
|
const model =
|
|
@@ -31,32 +66,51 @@ function runDaemonCli(argv) {
|
|
|
31
66
|
|| (envProvider && envProvider !== config.agentProvider ? "" : config.agentModel)
|
|
32
67
|
|| defaultAgentModelForProvider(provider);
|
|
33
68
|
const resumeMode = process.env.UFOO_FORCE_RESUME === "1" ? "force" : "auto";
|
|
34
|
-
const
|
|
69
|
+
const useGlobalRuntimeHost =
|
|
70
|
+
isGlobalControllerProjectRoot(daemonRoot)
|
|
71
|
+
&& daemonTopology !== "project";
|
|
72
|
+
const startSelectedDaemon = (selectedResumeMode) => {
|
|
73
|
+
if (useGlobalRuntimeHost) {
|
|
74
|
+
return startGlobalDaemon({
|
|
75
|
+
controllerRoot: daemonRoot,
|
|
76
|
+
provider,
|
|
77
|
+
model,
|
|
78
|
+
resumeMode: selectedResumeMode,
|
|
79
|
+
topology: daemonTopology,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return startDaemon({
|
|
83
|
+
projectRoot: daemonRoot,
|
|
84
|
+
provider,
|
|
85
|
+
model,
|
|
86
|
+
resumeMode: selectedResumeMode,
|
|
87
|
+
});
|
|
88
|
+
};
|
|
35
89
|
|
|
36
90
|
if (cmd === "start" || cmd === "--start") {
|
|
37
|
-
if (isRunning(
|
|
91
|
+
if (isRunning(daemonRoot)) return;
|
|
38
92
|
if (!process.env.UFOO_DAEMON_CHILD) {
|
|
39
|
-
spawnDaemonStart(
|
|
93
|
+
spawnDaemonStart(daemonRoot, daemonTopology);
|
|
40
94
|
return;
|
|
41
95
|
}
|
|
42
|
-
|
|
96
|
+
startSelectedDaemon(resumeMode);
|
|
43
97
|
return;
|
|
44
98
|
}
|
|
45
99
|
if (cmd === "stop" || cmd === "--stop") {
|
|
46
|
-
if (!stopDaemon(
|
|
100
|
+
if (!stopDaemon(daemonRoot, { source: process.env.UFOO_DAEMON_STOP_SOURCE || `daemon-cli:${cmd} pid=${process.pid}` })) {
|
|
47
101
|
process.exitCode = 1;
|
|
48
102
|
}
|
|
49
103
|
return;
|
|
50
104
|
}
|
|
51
105
|
if (cmd === "restart" || cmd === "--restart") {
|
|
52
106
|
const result = restartDaemonLifecycleSync({
|
|
53
|
-
projectRoot,
|
|
107
|
+
projectRoot: daemonRoot,
|
|
54
108
|
isRunning,
|
|
55
109
|
stopDaemon,
|
|
56
110
|
startDaemon: () => {
|
|
57
|
-
if (!process.env.UFOO_DAEMON_CHILD) return spawnDaemonStart(
|
|
111
|
+
if (!process.env.UFOO_DAEMON_CHILD) return spawnDaemonStart(daemonRoot, daemonTopology);
|
|
58
112
|
// Manual restart does not auto-resume; crash-recovery is handled on next auto start with stale lock detection.
|
|
59
|
-
return
|
|
113
|
+
return startSelectedDaemon("none");
|
|
60
114
|
},
|
|
61
115
|
stopOptions: { source: process.env.UFOO_DAEMON_STOP_SOURCE || `daemon-cli:${cmd} pid=${process.pid}` },
|
|
62
116
|
sleepSync,
|
|
@@ -65,7 +119,7 @@ function runDaemonCli(argv) {
|
|
|
65
119
|
return;
|
|
66
120
|
}
|
|
67
121
|
if (cmd === "status" || cmd === "--status") {
|
|
68
|
-
const running = isRunning(
|
|
122
|
+
const running = isRunning(daemonRoot);
|
|
69
123
|
// eslint-disable-next-line no-console
|
|
70
124
|
console.log(running ? "running" : "stopped");
|
|
71
125
|
return;
|
|
@@ -119,7 +119,13 @@ function isSocketAlive(socketPath) {
|
|
|
119
119
|
|
|
120
120
|
function normalizeStatus(value, fallback = "running") {
|
|
121
121
|
const raw = String(value || "").trim().toLowerCase();
|
|
122
|
-
if (
|
|
122
|
+
if (
|
|
123
|
+
raw === "running"
|
|
124
|
+
|| raw === "dormant"
|
|
125
|
+
|| raw === "failed"
|
|
126
|
+
|| raw === "stale"
|
|
127
|
+
|| raw === "stopped"
|
|
128
|
+
) return raw;
|
|
123
129
|
return fallback;
|
|
124
130
|
}
|
|
125
131
|
|
|
@@ -192,6 +198,21 @@ function markProjectStopped(projectRoot, options = {}) {
|
|
|
192
198
|
}, options);
|
|
193
199
|
}
|
|
194
200
|
|
|
201
|
+
function markProjectDormant(projectRoot, options = {}) {
|
|
202
|
+
if (!projectRoot) return null;
|
|
203
|
+
const existing = readProjectRuntimeByRoot(projectRoot, options);
|
|
204
|
+
const paths = getUfooPaths(canonicalizeForRecord(projectRoot));
|
|
205
|
+
return upsertProjectRuntime({
|
|
206
|
+
projectRoot,
|
|
207
|
+
projectName: existing ? existing.project_name : path.basename(projectRoot),
|
|
208
|
+
daemonPid: existing ? existing.daemon_pid : process.pid,
|
|
209
|
+
socketPath: existing ? existing.socket_path : paths.ufooSock,
|
|
210
|
+
status: "dormant",
|
|
211
|
+
lastSeen: new Date().toISOString(),
|
|
212
|
+
lastSwitchAt: existing ? existing.last_switch_at : undefined,
|
|
213
|
+
}, options);
|
|
214
|
+
}
|
|
215
|
+
|
|
195
216
|
function validateProjectRuntime(entry = {}, options = {}) {
|
|
196
217
|
if (!entry || typeof entry !== "object") return null;
|
|
197
218
|
const staleTtlMs = Number.isFinite(options.staleTtlMs) ? options.staleTtlMs : DEFAULT_STALE_TTL_MS;
|
|
@@ -207,9 +228,9 @@ function validateProjectRuntime(entry = {}, options = {}) {
|
|
|
207
228
|
let status = normalizeStatus(entry.status, "running");
|
|
208
229
|
if (running) {
|
|
209
230
|
status = "running";
|
|
210
|
-
} else if (status === "stopped") {
|
|
211
|
-
// Respect explicit
|
|
212
|
-
|
|
231
|
+
} else if (status === "stopped" || status === "dormant" || status === "failed") {
|
|
232
|
+
// Respect explicit lifecycle state even when the compatibility project
|
|
233
|
+
// socket is intentionally absent.
|
|
213
234
|
} else if (ageMs === null || ageMs > staleTtlMs) {
|
|
214
235
|
status = "stale";
|
|
215
236
|
}
|
|
@@ -273,6 +294,7 @@ module.exports = {
|
|
|
273
294
|
runtimeFilePathByProjectRoot,
|
|
274
295
|
upsertProjectRuntime,
|
|
275
296
|
markProjectStopped,
|
|
297
|
+
markProjectDormant,
|
|
276
298
|
listProjectRuntimes,
|
|
277
299
|
getCurrentProjectRuntime,
|
|
278
300
|
validateProjectRuntime,
|
|
@@ -17,7 +17,7 @@ function filterVisibleProjectRuntimes(rows = []) {
|
|
|
17
17
|
const sourceRows = Array.isArray(rows) ? rows : [];
|
|
18
18
|
return sourceRows.filter((row) => {
|
|
19
19
|
const status = String((row && row.status) || "").trim().toLowerCase();
|
|
20
|
-
return status === "running";
|
|
20
|
+
return status === "running" || status === "dormant";
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
|
package/src/ui/rustChatHost.js
CHANGED
|
@@ -35,6 +35,10 @@ const {
|
|
|
35
35
|
} = require("../runtime/contracts/uiProtocol");
|
|
36
36
|
const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES } = require("../runtime/contracts/eventContract");
|
|
37
37
|
const { createDaemonMessageRouter } = require("../app/chat/daemonMessageRouter");
|
|
38
|
+
const {
|
|
39
|
+
resolveDaemonEndpoint,
|
|
40
|
+
routeDaemonRequest,
|
|
41
|
+
} = require("../runtime/daemon/endpoint");
|
|
38
42
|
|
|
39
43
|
function stripTags(value) {
|
|
40
44
|
return String(value || "").replace(/\{[^}]+\}/g, "");
|
|
@@ -145,19 +149,23 @@ async function runChatRust(projectRoot, options = {}) {
|
|
|
145
149
|
});
|
|
146
150
|
}
|
|
147
151
|
await ensureSubscriberId(projectRoot);
|
|
148
|
-
|
|
152
|
+
const initialDaemonEndpoint = resolveDaemonEndpoint(projectRoot);
|
|
153
|
+
const initialDaemonRoot = initialDaemonEndpoint.scope === "global"
|
|
154
|
+
? initialDaemonEndpoint.controllerRoot
|
|
155
|
+
: initialDaemonEndpoint.projectRoot;
|
|
156
|
+
if (!env.isRunning(initialDaemonRoot)) {
|
|
149
157
|
env.startDaemon(projectRoot);
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
const { socketPath } = require("../runtime/daemon");
|
|
153
160
|
const { connectWithRetry } = require("../app/chat/transport");
|
|
154
161
|
const { createDaemonTransport } = require("../app/chat/daemonTransport");
|
|
155
162
|
const { createDaemonConnection } = require("../app/chat/daemonConnection");
|
|
156
163
|
|
|
157
|
-
const sock = socketPath
|
|
164
|
+
const sock = initialDaemonEndpoint.socketPath;
|
|
158
165
|
const daemonTransport = createDaemonTransport({
|
|
159
166
|
projectRoot,
|
|
160
167
|
sockPath: sock,
|
|
168
|
+
daemonRoot: initialDaemonRoot,
|
|
161
169
|
isRunning: env.isRunning,
|
|
162
170
|
startDaemon: env.startDaemon,
|
|
163
171
|
connectWithRetry,
|
|
@@ -336,7 +344,16 @@ async function runChatRust(projectRoot, options = {}) {
|
|
|
336
344
|
multiSession.stop();
|
|
337
345
|
}
|
|
338
346
|
|
|
339
|
-
|
|
347
|
+
const targetEndpoint = resolveDaemonEndpoint(root);
|
|
348
|
+
const targetDaemonRoot = targetEndpoint.scope === "global"
|
|
349
|
+
? targetEndpoint.controllerRoot
|
|
350
|
+
: targetEndpoint.projectRoot;
|
|
351
|
+
if (
|
|
352
|
+
env.globalMode
|
|
353
|
+
&& targetEndpoint.scope === "project"
|
|
354
|
+
&& typeof env.isRunning === "function"
|
|
355
|
+
&& !env.isRunning(targetDaemonRoot)
|
|
356
|
+
) {
|
|
340
357
|
try {
|
|
341
358
|
const { markProjectStopped } = require("../runtime/projects");
|
|
342
359
|
markProjectStopped(root);
|
|
@@ -365,7 +382,9 @@ async function runChatRust(projectRoot, options = {}) {
|
|
|
365
382
|
if (daemonCoordinator && typeof daemonCoordinator.switchProject === "function") {
|
|
366
383
|
const res = await daemonCoordinator.switchProject({
|
|
367
384
|
projectRoot: root,
|
|
368
|
-
sockPath: socketPath
|
|
385
|
+
sockPath: targetEndpoint.socketPath,
|
|
386
|
+
daemonRoot: targetDaemonRoot,
|
|
387
|
+
transformRequest: (request) => routeDaemonRequest(targetEndpoint, request),
|
|
369
388
|
autoStart: options.autoStart === true,
|
|
370
389
|
});
|
|
371
390
|
if (!res || res.ok !== true) {
|
|
@@ -399,9 +418,14 @@ async function runChatRust(projectRoot, options = {}) {
|
|
|
399
418
|
return { ok: true, project_root: projectRoot, root: projectRoot };
|
|
400
419
|
}
|
|
401
420
|
if (daemonCoordinator && typeof daemonCoordinator.switchProject === "function") {
|
|
421
|
+
const controllerEndpoint = resolveDaemonEndpoint(projectRoot);
|
|
402
422
|
const res = await daemonCoordinator.switchProject({
|
|
403
423
|
projectRoot,
|
|
404
|
-
sockPath: socketPath
|
|
424
|
+
sockPath: controllerEndpoint.socketPath,
|
|
425
|
+
daemonRoot: controllerEndpoint.scope === "global"
|
|
426
|
+
? controllerEndpoint.controllerRoot
|
|
427
|
+
: controllerEndpoint.projectRoot,
|
|
428
|
+
transformRequest: (request) => routeDaemonRequest(controllerEndpoint, request),
|
|
405
429
|
});
|
|
406
430
|
if (!res || res.ok !== true) {
|
|
407
431
|
appendLocal("error", `Switch to global failed: ${(res && res.error) || "switch failed"}`);
|
|
@@ -1151,7 +1175,7 @@ async function runChatRust(projectRoot, options = {}) {
|
|
|
1151
1175
|
if (!root) return { ok: false, error: "missing project root" };
|
|
1152
1176
|
try {
|
|
1153
1177
|
const { createProjectCloseController } = require("../app/chat/projectCloseController");
|
|
1154
|
-
const { stopDaemon } = require("../app/chat/transport");
|
|
1178
|
+
const { requestDaemon, stopDaemon } = require("../app/chat/transport");
|
|
1155
1179
|
const { isRunning } = require("../runtime/daemon");
|
|
1156
1180
|
const projects = loadGlobalProjectRows(activeProjectRoot);
|
|
1157
1181
|
const index = projects.findIndex((row) => String(row.root || "") === root);
|
|
@@ -1166,8 +1190,21 @@ async function runChatRust(projectRoot, options = {}) {
|
|
|
1166
1190
|
})),
|
|
1167
1191
|
getActiveProjectRoot: () => activeProjectRoot,
|
|
1168
1192
|
resolveProjectRoot: (row) => String((row && (row.root || row.project_root)) || ""),
|
|
1169
|
-
isRunning
|
|
1193
|
+
isRunning: (targetRoot) => {
|
|
1194
|
+
const endpoint = resolveDaemonEndpoint(targetRoot);
|
|
1195
|
+
return endpoint.scope === "global" ? true : isRunning(targetRoot);
|
|
1196
|
+
},
|
|
1170
1197
|
stopDaemon,
|
|
1198
|
+
closeProject: async (targetRoot) => {
|
|
1199
|
+
const endpoint = resolveDaemonEndpoint(targetRoot);
|
|
1200
|
+
if (endpoint.scope === "global") {
|
|
1201
|
+
return requestDaemon(targetRoot, {
|
|
1202
|
+
type: IPC_REQUEST_TYPES.CLOSE_PROJECT_RUNTIME,
|
|
1203
|
+
terminate_agents: true,
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
return stopDaemon(targetRoot, { source: `project-close:${targetRoot}` });
|
|
1207
|
+
},
|
|
1171
1208
|
switchProject: async (fallbackRoot) => hostApi.switchToProjectRoot(fallbackRoot),
|
|
1172
1209
|
refreshProjects: () => publishProjects(),
|
|
1173
1210
|
logMessage: (kind, text) => {
|
|
@@ -1408,6 +1445,7 @@ async function runChatRust(projectRoot, options = {}) {
|
|
|
1408
1445
|
|
|
1409
1446
|
const daemonConnection = createDaemonConnection({
|
|
1410
1447
|
connectClient: daemonTransport.connectClient.bind(daemonTransport),
|
|
1448
|
+
transformRequest: (request) => routeDaemonRequest(initialDaemonEndpoint, request),
|
|
1411
1449
|
handleMessage: (msg) => {
|
|
1412
1450
|
if (typeof routedMessageHandler === "function" && routedMessageHandler(msg)) {
|
|
1413
1451
|
return;
|