u-foo 2.2.3 → 2.2.4
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/package.json +1 -1
- package/src/bus/index.js +7 -4
- package/src/bus/message.js +2 -2
- package/src/bus/nickname.js +14 -3
- package/src/bus/subscriber.js +30 -10
- package/src/chat/agentDirectory.js +2 -2
- package/src/chat/commandExecutor.js +3 -1
- package/src/chat/daemonMessageRouter.js +2 -1
- package/src/chat/index.js +6 -3
- package/src/code/nativeRunner.js +2 -1
- package/src/daemon/index.js +35 -18
- package/src/daemon/nicknameScope.js +37 -0
- package/src/daemon/ops.js +22 -10
- package/src/daemon/reporting.js +2 -3
- package/src/daemon/soloBootstrap.js +13 -3
- package/src/daemon/status.js +5 -1
- package/src/status/index.js +2 -1
package/package.json
CHANGED
package/src/bus/index.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
sleep,
|
|
17
17
|
} = require("./utils");
|
|
18
18
|
const { shakeTerminalByTty } = require("./shake");
|
|
19
|
+
const { resolveDisplayNickname } = require("../daemon/nicknameScope");
|
|
19
20
|
const QueueManager = require("./queue");
|
|
20
21
|
const SubscriberManager = require("./subscriber");
|
|
21
22
|
const MessageManager = require("./message");
|
|
@@ -147,7 +148,8 @@ class EventBus {
|
|
|
147
148
|
if (!sessionId && !agentType && currentSubscriber && currentActive) {
|
|
148
149
|
this.subscriberManager.updateLastSeen(currentSubscriber);
|
|
149
150
|
this.saveBusData();
|
|
150
|
-
const
|
|
151
|
+
const currentDisplayNickname = resolveDisplayNickname(this.projectRoot, currentMeta);
|
|
152
|
+
const currentNickname = currentDisplayNickname ? ` (${currentDisplayNickname})` : "";
|
|
151
153
|
logInfo(`Already joined event bus: ${currentSubscriber}${currentNickname}`);
|
|
152
154
|
return currentSubscriber;
|
|
153
155
|
}
|
|
@@ -201,14 +203,15 @@ class EventBus {
|
|
|
201
203
|
/**
|
|
202
204
|
* 重命名订阅者
|
|
203
205
|
*/
|
|
204
|
-
async rename(subscriber, newNickname, publisher = null) {
|
|
206
|
+
async rename(subscriber, newNickname, publisher = null, options = {}) {
|
|
205
207
|
this.ensureBus();
|
|
206
208
|
this.loadBusData();
|
|
207
209
|
|
|
208
210
|
try {
|
|
209
211
|
const result = await this.subscriberManager.rename(
|
|
210
212
|
subscriber,
|
|
211
|
-
newNickname
|
|
213
|
+
newNickname,
|
|
214
|
+
options
|
|
212
215
|
);
|
|
213
216
|
this.saveBusData();
|
|
214
217
|
const pub = publisher || this.getDefaultPublisher() || "unknown";
|
|
@@ -468,7 +471,7 @@ class EventBus {
|
|
|
468
471
|
console.log(" (none)");
|
|
469
472
|
} else {
|
|
470
473
|
for (const sub of active) {
|
|
471
|
-
|
|
474
|
+
const nickname = sub.nickname ? ` (${sub.nickname})` : "";
|
|
472
475
|
console.log(` ${sub.id}${nickname}`);
|
|
473
476
|
}
|
|
474
477
|
}
|
package/src/bus/message.js
CHANGED
|
@@ -247,7 +247,7 @@ class MessageManager {
|
|
|
247
247
|
if (meta && normalizedTarget === normalizeAgentTypeAlias(meta.agent_type)) return true;
|
|
248
248
|
|
|
249
249
|
// 昵称匹配
|
|
250
|
-
if (meta && target === meta.nickname) return true;
|
|
250
|
+
if (meta && (target === meta.nickname || target === meta.scoped_nickname)) return true;
|
|
251
251
|
|
|
252
252
|
// 通配符
|
|
253
253
|
if (target === "*") return true;
|
|
@@ -423,7 +423,7 @@ class MessageManager {
|
|
|
423
423
|
})
|
|
424
424
|
.map(([id, meta]) => ({
|
|
425
425
|
id,
|
|
426
|
-
nickname: meta.nickname,
|
|
426
|
+
nickname: meta.nickname || meta.scoped_nickname || "",
|
|
427
427
|
agent_type: meta.agent_type,
|
|
428
428
|
last_seen: meta.last_seen,
|
|
429
429
|
}));
|
package/src/bus/nickname.js
CHANGED
|
@@ -14,7 +14,7 @@ class NicknameManager {
|
|
|
14
14
|
resolveNickname(nickname) {
|
|
15
15
|
const subscribers = this.busData.agents || {};
|
|
16
16
|
for (const [id, meta] of Object.entries(subscribers)) {
|
|
17
|
-
if (meta.nickname === nickname) {
|
|
17
|
+
if (meta.nickname === nickname || meta.scoped_nickname === nickname) {
|
|
18
18
|
return id;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -30,7 +30,10 @@ class NicknameManager {
|
|
|
30
30
|
nicknameExists(nickname, excludeSubscriber = null) {
|
|
31
31
|
const subscribers = this.busData.agents || {};
|
|
32
32
|
for (const [id, meta] of Object.entries(subscribers)) {
|
|
33
|
-
if (
|
|
33
|
+
if (
|
|
34
|
+
id !== excludeSubscriber
|
|
35
|
+
&& (meta.nickname === nickname || meta.scoped_nickname === nickname)
|
|
36
|
+
) {
|
|
34
37
|
return true;
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -71,10 +74,15 @@ class NicknameManager {
|
|
|
71
74
|
return meta?.nickname || null;
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
getScopedNickname(subscriber) {
|
|
78
|
+
const meta = this.busData.agents?.[subscriber];
|
|
79
|
+
return meta?.scoped_nickname || meta?.nickname || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
/**
|
|
75
83
|
* 设置订阅者的昵称
|
|
76
84
|
*/
|
|
77
|
-
setNickname(subscriber, nickname) {
|
|
85
|
+
setNickname(subscriber, nickname, scopedNickname = "") {
|
|
78
86
|
if (!this.busData.agents) {
|
|
79
87
|
this.busData.agents = {};
|
|
80
88
|
}
|
|
@@ -82,6 +90,9 @@ class NicknameManager {
|
|
|
82
90
|
this.busData.agents[subscriber] = {};
|
|
83
91
|
}
|
|
84
92
|
this.busData.agents[subscriber].nickname = nickname;
|
|
93
|
+
if (scopedNickname) {
|
|
94
|
+
this.busData.agents[subscriber].scoped_nickname = scopedNickname;
|
|
95
|
+
}
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
|
package/src/bus/subscriber.js
CHANGED
|
@@ -175,20 +175,29 @@ class SubscriberManager {
|
|
|
175
175
|
// 检查是否是重新加入(rejoin)
|
|
176
176
|
const existingMeta = this.busData.agents[subscriber];
|
|
177
177
|
let finalNickname = nickname;
|
|
178
|
+
let finalScopedNickname = typeof options.scopedNickname === "string"
|
|
179
|
+
? options.scopedNickname.trim()
|
|
180
|
+
: (typeof process.env.UFOO_SCOPED_NICKNAME === "string" ? process.env.UFOO_SCOPED_NICKNAME.trim() : "");
|
|
178
181
|
|
|
179
|
-
if (
|
|
180
|
-
// 重新加入,保留原昵称
|
|
181
|
-
finalNickname = existingMeta.nickname;
|
|
182
|
-
} else if (nickname) {
|
|
182
|
+
if (nickname) {
|
|
183
183
|
// 新昵称,检查冲突
|
|
184
|
-
|
|
184
|
+
const conflictTarget = finalScopedNickname || nickname;
|
|
185
|
+
if (nicknameManager.nicknameExists(conflictTarget, subscriber)) {
|
|
185
186
|
throw new Error(`Nickname "${nickname}" already exists`);
|
|
186
187
|
}
|
|
187
188
|
finalNickname = nickname;
|
|
189
|
+
} else if (existingMeta && existingMeta.nickname) {
|
|
190
|
+
// 重新加入,保留原昵称
|
|
191
|
+
finalNickname = existingMeta.nickname;
|
|
192
|
+
finalScopedNickname = existingMeta.scoped_nickname || finalScopedNickname || finalNickname;
|
|
188
193
|
} else {
|
|
189
194
|
// 自动生成昵称(并标记占用,避免并发重复)
|
|
190
195
|
finalNickname = nicknameManager.generateAutoNickname(agentType);
|
|
191
|
-
nicknameManager.setNickname(subscriber, finalNickname);
|
|
196
|
+
nicknameManager.setNickname(subscriber, finalNickname, finalScopedNickname);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!finalScopedNickname) {
|
|
200
|
+
finalScopedNickname = existingMeta?.scoped_nickname || finalNickname;
|
|
192
201
|
}
|
|
193
202
|
|
|
194
203
|
const explicitLaunchMode = typeof options.launchMode === "string"
|
|
@@ -221,6 +230,7 @@ class SubscriberManager {
|
|
|
221
230
|
const inheritedNickname = await this.cleanupDuplicateTty(subscriber, finalTty);
|
|
222
231
|
if (inheritedNickname && !nickname && !existingMeta) {
|
|
223
232
|
finalNickname = inheritedNickname;
|
|
233
|
+
if (!finalScopedNickname) finalScopedNickname = inheritedNickname;
|
|
224
234
|
}
|
|
225
235
|
|
|
226
236
|
// 更新订阅者信息(保留已有字段,如 provider_session_*)
|
|
@@ -252,6 +262,7 @@ class SubscriberManager {
|
|
|
252
262
|
...preserved,
|
|
253
263
|
agent_type: agentType,
|
|
254
264
|
nickname: finalNickname,
|
|
265
|
+
scoped_nickname: finalScopedNickname || finalNickname,
|
|
255
266
|
status: "active",
|
|
256
267
|
activity_state: "starting",
|
|
257
268
|
activity_since: getTimestamp(),
|
|
@@ -311,7 +322,11 @@ class SubscriberManager {
|
|
|
311
322
|
// 创建队列目录
|
|
312
323
|
this.queueManager.ensureQueueDir(subscriber);
|
|
313
324
|
|
|
314
|
-
return {
|
|
325
|
+
return {
|
|
326
|
+
subscriber,
|
|
327
|
+
nickname: finalNickname,
|
|
328
|
+
scopedNickname: this.busData.agents[subscriber].scoped_nickname || finalNickname,
|
|
329
|
+
};
|
|
315
330
|
}
|
|
316
331
|
|
|
317
332
|
/**
|
|
@@ -333,22 +348,27 @@ class SubscriberManager {
|
|
|
333
348
|
/**
|
|
334
349
|
* 重命名订阅者
|
|
335
350
|
*/
|
|
336
|
-
async rename(subscriber, newNickname) {
|
|
351
|
+
async rename(subscriber, newNickname, options = {}) {
|
|
337
352
|
if (!this.busData.agents || !this.busData.agents[subscriber]) {
|
|
338
353
|
throw new Error(`Subscriber "${subscriber}" not found`);
|
|
339
354
|
}
|
|
340
355
|
|
|
341
356
|
const nicknameManager = new NicknameManager(this.busData);
|
|
357
|
+
const scopedNickname = typeof options.scopedNickname === "string" && options.scopedNickname.trim()
|
|
358
|
+
? options.scopedNickname.trim()
|
|
359
|
+
: newNickname;
|
|
342
360
|
|
|
343
361
|
// 检查昵称冲突
|
|
344
|
-
if (nicknameManager.nicknameExists(
|
|
362
|
+
if (nicknameManager.nicknameExists(scopedNickname, subscriber)) {
|
|
345
363
|
throw new Error(`Nickname "${newNickname}" already exists`);
|
|
346
364
|
}
|
|
347
365
|
|
|
348
366
|
const oldNickname = this.busData.agents[subscriber].nickname;
|
|
367
|
+
const oldScopedNickname = this.busData.agents[subscriber].scoped_nickname || oldNickname;
|
|
349
368
|
this.busData.agents[subscriber].nickname = newNickname;
|
|
369
|
+
this.busData.agents[subscriber].scoped_nickname = scopedNickname;
|
|
350
370
|
|
|
351
|
-
return { subscriber, oldNickname, newNickname };
|
|
371
|
+
return { subscriber, oldNickname, newNickname, oldScopedNickname, newScopedNickname: scopedNickname };
|
|
352
372
|
}
|
|
353
373
|
|
|
354
374
|
/**
|
|
@@ -10,8 +10,8 @@ function buildAgentMaps(activeAgents = [], metaList = [], fallbackMap = null) {
|
|
|
10
10
|
|
|
11
11
|
for (const id of activeAgents) {
|
|
12
12
|
const meta = metaById.get(id);
|
|
13
|
-
const label = meta && meta.nickname
|
|
14
|
-
? meta.nickname
|
|
13
|
+
const label = meta && (meta.display_nickname || meta.nickname)
|
|
14
|
+
? (meta.display_nickname || meta.nickname)
|
|
15
15
|
: (fallbackMap && fallbackMap.get(id)) || id;
|
|
16
16
|
labelMap.set(id, label);
|
|
17
17
|
if (meta) {
|
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
normalizeControllerMode,
|
|
12
12
|
} = require("../config");
|
|
13
13
|
const { resolveTransport } = require("../code/nativeRunner");
|
|
14
|
+
const { resolveDisplayNickname } = require("../daemon/nicknameScope");
|
|
14
15
|
const { parseIntervalMs, formatIntervalMs } = require("./cronScheduler");
|
|
15
16
|
const { isGlobalControllerProjectRoot, resolveGlobalControllerUfooDir } = require("../projects");
|
|
16
17
|
const { loadPromptProfileRegistry } = require("../group/promptProfiles");
|
|
@@ -319,7 +320,8 @@ function createCommandExecutor(options = {}) {
|
|
|
319
320
|
} else {
|
|
320
321
|
logMessage("system", "{cyan-fg}Active agents:{/cyan-fg}");
|
|
321
322
|
for (const [id, meta] of subscribers) {
|
|
322
|
-
const
|
|
323
|
+
const displayNickname = meta ? resolveDisplayNickname(projectRoot, meta) : "";
|
|
324
|
+
const nickname = displayNickname ? ` (${displayNickname})` : "";
|
|
323
325
|
const status = meta && meta.status ? meta.status : "unknown";
|
|
324
326
|
logMessage("system", ` • ${id}${nickname} {white-fg}[${status}]{/white-fg}`);
|
|
325
327
|
}
|
|
@@ -229,7 +229,8 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
229
229
|
if (recoverableList.length > 0) {
|
|
230
230
|
logMessage("system", "{cyan-fg}Recoverable agents:{/cyan-fg}");
|
|
231
231
|
recoverableList.forEach((item) => {
|
|
232
|
-
const
|
|
232
|
+
const displayNickname = item.display_nickname || item.nickname || "";
|
|
233
|
+
const nickname = displayNickname ? ` (${displayNickname})` : "";
|
|
233
234
|
const meta = item.launchMode ? ` [${item.agent}/${item.launchMode}]` : ` [${item.agent}]`;
|
|
234
235
|
logMessage("system", ` • ${escapeBlessed(`${item.id}${nickname}${meta}`)}`);
|
|
235
236
|
});
|
package/src/chat/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const UfooInit = require("../init");
|
|
|
15
15
|
const AgentActivator = require("../bus/activate");
|
|
16
16
|
const { subscriberToSafeName } = require("../bus/utils");
|
|
17
17
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
18
|
+
const { resolveDisplayNickname } = require("../daemon/nicknameScope");
|
|
18
19
|
const { startDaemon, stopDaemon, connectWithRetry } = require("./transport");
|
|
19
20
|
const { escapeBlessed, stripBlessedTags, truncateText } = require("./text");
|
|
20
21
|
const { COMMAND_REGISTRY, parseCommand, parseAtTarget } = require("./commands");
|
|
@@ -915,7 +916,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
915
916
|
const busPath = getUfooPaths(activeProjectRoot).agentsFile;
|
|
916
917
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
917
918
|
for (const [id, meta] of Object.entries(bus.agents || {})) {
|
|
918
|
-
if (meta && meta.nickname === nickname) return id;
|
|
919
|
+
if (meta && (meta.nickname === nickname || meta.scoped_nickname === nickname)) return id;
|
|
919
920
|
}
|
|
920
921
|
} catch {
|
|
921
922
|
// ignore lookup errors
|
|
@@ -934,7 +935,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
934
935
|
const busPath = getUfooPaths(activeProjectRoot).agentsFile;
|
|
935
936
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
936
937
|
const meta = bus.agents && bus.agents[id];
|
|
937
|
-
if (meta
|
|
938
|
+
if (meta) return resolveDisplayNickname(activeProjectRoot, meta);
|
|
938
939
|
} catch {
|
|
939
940
|
// Keep original publisher ID
|
|
940
941
|
}
|
|
@@ -1275,7 +1276,9 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1275
1276
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
1276
1277
|
fallbackMap = new Map();
|
|
1277
1278
|
for (const [id, meta] of Object.entries(bus.agents || {})) {
|
|
1278
|
-
if (meta
|
|
1279
|
+
if (!meta) continue;
|
|
1280
|
+
const displayNickname = resolveDisplayNickname(activeProjectRoot, meta);
|
|
1281
|
+
if (displayNickname) fallbackMap.set(id, displayNickname);
|
|
1279
1282
|
}
|
|
1280
1283
|
} catch {
|
|
1281
1284
|
fallbackMap = null;
|
package/src/code/nativeRunner.js
CHANGED
|
@@ -486,6 +486,7 @@ async function runOpenAiLikeTurn({
|
|
|
486
486
|
} = {}) {
|
|
487
487
|
const payload = {
|
|
488
488
|
model,
|
|
489
|
+
max_tokens: 131072,
|
|
489
490
|
messages,
|
|
490
491
|
tools: buildCoreToolSpecs(),
|
|
491
492
|
tool_choice: "auto",
|
|
@@ -683,7 +684,7 @@ async function runAnthropicTurn({
|
|
|
683
684
|
} = {}) {
|
|
684
685
|
const payload = {
|
|
685
686
|
model,
|
|
686
|
-
max_tokens:
|
|
687
|
+
max_tokens: 131072,
|
|
687
688
|
messages,
|
|
688
689
|
tools: buildAnthropicToolSpecs(),
|
|
689
690
|
stream: true,
|
package/src/daemon/index.js
CHANGED
|
@@ -31,7 +31,11 @@ const {
|
|
|
31
31
|
buildSoloBootstrapFingerprint,
|
|
32
32
|
rollbackLaunchAfterRoleAssignmentFailure,
|
|
33
33
|
} = require("./soloBootstrap");
|
|
34
|
-
const {
|
|
34
|
+
const {
|
|
35
|
+
applyProjectNicknamePrefix,
|
|
36
|
+
resolveDisplayNickname,
|
|
37
|
+
resolveScopedNickname,
|
|
38
|
+
} = require("./nicknameScope");
|
|
35
39
|
|
|
36
40
|
let providerSessions = null;
|
|
37
41
|
let probeHandles = new Map();
|
|
@@ -60,7 +64,7 @@ function normalizeLaunchAgent(agent = "") {
|
|
|
60
64
|
return "";
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
async function renameSpawnedAgent(projectRoot, agentType, nickname, startIso) {
|
|
67
|
+
async function renameSpawnedAgent(projectRoot, agentType, nickname, startIso, scopedNickname = "") {
|
|
64
68
|
if (!nickname) return null;
|
|
65
69
|
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
66
70
|
const targetType = normalizeBusAgentType(agentType);
|
|
@@ -79,11 +83,11 @@ async function renameSpawnedAgent(projectRoot, agentType, nickname, startIso) {
|
|
|
79
83
|
await sleep(200);
|
|
80
84
|
continue;
|
|
81
85
|
}
|
|
82
|
-
let candidates = entries.filter(([, meta]) => !meta
|
|
86
|
+
let candidates = entries.filter(([, meta]) => !resolveDisplayNickname(projectRoot, meta));
|
|
83
87
|
if (candidates.length === 0) candidates = entries;
|
|
84
88
|
candidates.sort((a, b) => (a[1].joined_at || "").localeCompare(b[1].joined_at || ""));
|
|
85
89
|
const [agentId] = candidates[candidates.length - 1];
|
|
86
|
-
await eventBus.rename(agentId, nickname, "ufoo-agent");
|
|
90
|
+
await eventBus.rename(agentId, nickname, "ufoo-agent", { scopedNickname });
|
|
87
91
|
return { ok: true, agent_id: agentId, nickname };
|
|
88
92
|
} catch (err) {
|
|
89
93
|
lastError = err && err.message ? err.message : String(err || "rename failed");
|
|
@@ -382,13 +386,20 @@ async function waitForNewSubscriber(projectRoot, agentType, existing, timeoutMs
|
|
|
382
386
|
return null;
|
|
383
387
|
}
|
|
384
388
|
|
|
385
|
-
function checkAndCleanupNickname(projectRoot, nickname, { tty = "", agentType = "" } = {}) {
|
|
386
|
-
|
|
389
|
+
function checkAndCleanupNickname(projectRoot, nickname, { tty = "", agentType = "", scopedNickname = "" } = {}) {
|
|
390
|
+
const conflictNickname = scopedNickname || applyProjectNicknamePrefix(projectRoot, nickname, {
|
|
391
|
+
agentType,
|
|
392
|
+
force: true,
|
|
393
|
+
});
|
|
394
|
+
if (!conflictNickname) return { existing: null, cleaned: false };
|
|
387
395
|
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
388
396
|
try {
|
|
389
397
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
390
398
|
const entries = Object.entries(bus.agents || {})
|
|
391
|
-
.filter(([, meta]) =>
|
|
399
|
+
.filter(([, meta]) => {
|
|
400
|
+
const candidate = resolveScopedNickname(projectRoot, meta);
|
|
401
|
+
return meta && candidate === conflictNickname;
|
|
402
|
+
});
|
|
392
403
|
|
|
393
404
|
if (entries.length === 0) {
|
|
394
405
|
return { existing: null, cleaned: false };
|
|
@@ -431,7 +442,7 @@ function resolveSubscriberNickname(projectRoot, subscriberId) {
|
|
|
431
442
|
try {
|
|
432
443
|
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
433
444
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
434
|
-
return bus.agents?.[subscriberId]
|
|
445
|
+
return resolveDisplayNickname(projectRoot, bus.agents?.[subscriberId] || {});
|
|
435
446
|
} catch {
|
|
436
447
|
return "";
|
|
437
448
|
}
|
|
@@ -453,7 +464,8 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
453
464
|
continue;
|
|
454
465
|
}
|
|
455
466
|
const requestedNickname = String(op.nickname || "").trim();
|
|
456
|
-
const nickname =
|
|
467
|
+
const nickname = requestedNickname;
|
|
468
|
+
const scopedNickname = applyProjectNicknamePrefix(projectRoot, requestedNickname, { agentType: agent });
|
|
457
469
|
const startTime = new Date(Date.now() - 1000);
|
|
458
470
|
const startIso = startTime.toISOString();
|
|
459
471
|
if (nickname && count > 1) {
|
|
@@ -468,7 +480,7 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
468
480
|
}
|
|
469
481
|
try {
|
|
470
482
|
// Check for existing agent with same nickname
|
|
471
|
-
const { existing, cleaned } = checkAndCleanupNickname(projectRoot, nickname);
|
|
483
|
+
const { existing, cleaned } = checkAndCleanupNickname(projectRoot, nickname, { scopedNickname, agentType: agent });
|
|
472
484
|
if (existing) {
|
|
473
485
|
// Agent with this nickname already exists and is active
|
|
474
486
|
results.push({
|
|
@@ -486,6 +498,7 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
486
498
|
}
|
|
487
499
|
// eslint-disable-next-line no-await-in-loop
|
|
488
500
|
const launchResult = await launchAgent(projectRoot, agent, count, nickname, processManager, {
|
|
501
|
+
scopedNickname,
|
|
489
502
|
launchScope: op.launch_scope || "",
|
|
490
503
|
terminalApp: op.terminal_app || "",
|
|
491
504
|
tmuxLayoutContext:
|
|
@@ -555,7 +568,7 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
555
568
|
});
|
|
556
569
|
if (nickname) {
|
|
557
570
|
// eslint-disable-next-line no-await-in-loop
|
|
558
|
-
const renameResult = await renameSpawnedAgent(projectRoot, agent, nickname, startIso);
|
|
571
|
+
const renameResult = await renameSpawnedAgent(projectRoot, agent, nickname, startIso, scopedNickname);
|
|
559
572
|
if (renameResult) {
|
|
560
573
|
results.push({ action: "rename", ...renameResult });
|
|
561
574
|
}
|
|
@@ -615,10 +628,11 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
615
628
|
continue;
|
|
616
629
|
}
|
|
617
630
|
const targetMeta = eventBus.busData.agents[targetId] || {};
|
|
618
|
-
|
|
631
|
+
const scopedNickname = applyProjectNicknamePrefix(projectRoot, requestedNickname, {
|
|
619
632
|
agentType: targetMeta.agent_type || "",
|
|
620
633
|
});
|
|
621
|
-
|
|
634
|
+
nickname = requestedNickname;
|
|
635
|
+
const result = await eventBus.rename(targetId, nickname, "ufoo-agent", { scopedNickname });
|
|
622
636
|
results.push({
|
|
623
637
|
action: "rename",
|
|
624
638
|
ok: true,
|
|
@@ -1311,9 +1325,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1311
1325
|
const parsedCount = parseInt(count, 10);
|
|
1312
1326
|
const finalCount = Number.isFinite(parsedCount) && parsedCount > 0 ? parsedCount : 1;
|
|
1313
1327
|
const requestedProfile = String(prompt_profile || "").trim();
|
|
1314
|
-
const explicitNickname =
|
|
1315
|
-
agentType: normalizedAgent,
|
|
1316
|
-
});
|
|
1328
|
+
const explicitNickname = String(nickname || "").trim();
|
|
1317
1329
|
if (requestedProfile && finalCount > 1) {
|
|
1318
1330
|
socket.write(
|
|
1319
1331
|
`${JSON.stringify({
|
|
@@ -2027,23 +2039,28 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2027
2039
|
if (skipProbe) joinOptions.skipProbe = true;
|
|
2028
2040
|
|
|
2029
2041
|
let finalNickname = nickname || "";
|
|
2042
|
+
let scopedNickname = applyProjectNicknamePrefix(projectRoot, finalNickname, {
|
|
2043
|
+
agentType: normalizeBusAgentType(agentType),
|
|
2044
|
+
});
|
|
2030
2045
|
if (finalNickname) {
|
|
2031
2046
|
const nickCheck = checkAndCleanupNickname(projectRoot, finalNickname, {
|
|
2032
2047
|
tty: tty || "",
|
|
2033
2048
|
agentType: normalizeBusAgentType(agentType),
|
|
2049
|
+
scopedNickname,
|
|
2034
2050
|
});
|
|
2035
2051
|
if (nickCheck.existing) {
|
|
2036
2052
|
finalNickname = "";
|
|
2053
|
+
scopedNickname = "";
|
|
2037
2054
|
}
|
|
2038
2055
|
}
|
|
2039
2056
|
await eventBus.join(
|
|
2040
2057
|
sessionId,
|
|
2041
2058
|
normalizeBusAgentType(agentType),
|
|
2042
2059
|
finalNickname,
|
|
2043
|
-
joinOptions,
|
|
2060
|
+
{ ...joinOptions, scopedNickname },
|
|
2044
2061
|
);
|
|
2045
2062
|
if (finalNickname) {
|
|
2046
|
-
eventBus.rename(subscriberId, finalNickname, "ufoo-agent");
|
|
2063
|
+
eventBus.rename(subscriberId, finalNickname, "ufoo-agent", { scopedNickname });
|
|
2047
2064
|
}
|
|
2048
2065
|
eventBus.saveBusData();
|
|
2049
2066
|
const resolvedNickname = resolveSubscriberNickname(projectRoot, subscriberId) || finalNickname || "";
|
|
@@ -72,9 +72,46 @@ function applyProjectNicknamePrefix(projectRoot, nickname = "", options = {}) {
|
|
|
72
72
|
return `${projectPrefix}-${normalizedNickname}`;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function stripProjectNicknamePrefix(projectRoot, nickname = "") {
|
|
76
|
+
const normalizedNickname = normalizeNicknameSegment(nickname, "");
|
|
77
|
+
if (!normalizedNickname) return "";
|
|
78
|
+
const projectPrefix = buildProjectNicknamePrefix(projectRoot);
|
|
79
|
+
const scopedPrefix = `${projectPrefix}-`;
|
|
80
|
+
if (!normalizedNickname.startsWith(scopedPrefix)) {
|
|
81
|
+
return normalizedNickname;
|
|
82
|
+
}
|
|
83
|
+
const stripped = normalizedNickname.slice(scopedPrefix.length).replace(/^-+/, "");
|
|
84
|
+
return stripped || normalizedNickname;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveDisplayNickname(projectRoot, meta = {}, fallback = "") {
|
|
88
|
+
const explicit = asTrimmedString(meta.nickname);
|
|
89
|
+
if (explicit) {
|
|
90
|
+
return meta.scoped_nickname ? explicit : stripProjectNicknamePrefix(projectRoot, explicit);
|
|
91
|
+
}
|
|
92
|
+
const scoped = asTrimmedString(meta.scoped_nickname);
|
|
93
|
+
if (scoped) return stripProjectNicknamePrefix(projectRoot, scoped);
|
|
94
|
+
return asTrimmedString(fallback);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveScopedNickname(projectRoot, meta = {}, fallback = "") {
|
|
98
|
+
const scoped = asTrimmedString(meta.scoped_nickname);
|
|
99
|
+
if (scoped) return scoped;
|
|
100
|
+
const explicit = asTrimmedString(meta.nickname);
|
|
101
|
+
if (explicit) {
|
|
102
|
+
return meta.scoped_nickname ? scoped : applyProjectNicknamePrefix(projectRoot, explicit, { force: true });
|
|
103
|
+
}
|
|
104
|
+
const fallbackValue = asTrimmedString(fallback);
|
|
105
|
+
if (!fallbackValue) return "";
|
|
106
|
+
return applyProjectNicknamePrefix(projectRoot, fallbackValue, { force: true });
|
|
107
|
+
}
|
|
108
|
+
|
|
75
109
|
module.exports = {
|
|
76
110
|
normalizeNicknameSegment,
|
|
77
111
|
buildProjectNicknamePrefix,
|
|
78
112
|
isAutoGeneratedNickname,
|
|
79
113
|
applyProjectNicknamePrefix,
|
|
114
|
+
stripProjectNicknamePrefix,
|
|
115
|
+
resolveDisplayNickname,
|
|
116
|
+
resolveScopedNickname,
|
|
80
117
|
};
|
package/src/daemon/ops.js
CHANGED
|
@@ -7,7 +7,10 @@ const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
|
|
|
7
7
|
const { isAgentPidAlive, getTtyProcessInfo } = require("../bus/utils");
|
|
8
8
|
const { isITerm2 } = require("../terminal/detect");
|
|
9
9
|
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
10
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
applyProjectNicknamePrefix,
|
|
12
|
+
resolveDisplayNickname,
|
|
13
|
+
} = require("./nicknameScope");
|
|
11
14
|
const {
|
|
12
15
|
createSession: createHostSession,
|
|
13
16
|
} = require("../terminal/adapters/hostAdapter");
|
|
@@ -125,11 +128,15 @@ function resolveAgentId(projectRoot, agentId) {
|
|
|
125
128
|
try {
|
|
126
129
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
127
130
|
const entries = Object.entries(bus.agents || {});
|
|
128
|
-
const match = entries.find(([, meta]) =>
|
|
131
|
+
const match = entries.find(([, meta]) =>
|
|
132
|
+
meta?.nickname === agentId || meta?.scoped_nickname === agentId
|
|
133
|
+
);
|
|
129
134
|
if (match) return match[0];
|
|
130
135
|
const scopedNickname = applyProjectNicknamePrefix(projectRoot, agentId);
|
|
131
136
|
if (scopedNickname && scopedNickname !== agentId) {
|
|
132
|
-
const scopedMatch = entries.find(([, meta]) =>
|
|
137
|
+
const scopedMatch = entries.find(([, meta]) =>
|
|
138
|
+
meta?.nickname === scopedNickname || meta?.scoped_nickname === scopedNickname
|
|
139
|
+
);
|
|
133
140
|
if (scopedMatch) return scopedMatch[0];
|
|
134
141
|
}
|
|
135
142
|
const normalized = normalizeLaunchAgent(agentId);
|
|
@@ -848,7 +855,11 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
848
855
|
const launchScope = normalizeLaunchScope(options.launchScope, "inplace");
|
|
849
856
|
const terminalApp = normalizeTerminalAppPreference(options.terminalApp);
|
|
850
857
|
const extraEnvObject = options.extraEnv && typeof options.extraEnv === "object" ? options.extraEnv : {};
|
|
851
|
-
const
|
|
858
|
+
const scopedNickname = typeof options.scopedNickname === "string" ? options.scopedNickname.trim() : "";
|
|
859
|
+
const launchEnvObject = scopedNickname
|
|
860
|
+
? { ...extraEnvObject, UFOO_SCOPED_NICKNAME: scopedNickname }
|
|
861
|
+
: extraEnvObject;
|
|
862
|
+
const extraEnvPrefix = buildShellEnvPrefix(launchEnvObject);
|
|
852
863
|
const extraArgs = Array.isArray(options.extraArgs) ? options.extraArgs : [];
|
|
853
864
|
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
854
865
|
if (!normalizedAgent) {
|
|
@@ -862,7 +873,7 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
862
873
|
count,
|
|
863
874
|
nickname,
|
|
864
875
|
processManager,
|
|
865
|
-
|
|
876
|
+
launchEnvObject
|
|
866
877
|
);
|
|
867
878
|
return { mode: "internal", launchScope, subscriberIds: result.subscriberIds };
|
|
868
879
|
}
|
|
@@ -954,7 +965,7 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
954
965
|
nick,
|
|
955
966
|
processManager,
|
|
956
967
|
extraArgs,
|
|
957
|
-
|
|
968
|
+
launchEnvObject,
|
|
958
969
|
hostContext
|
|
959
970
|
);
|
|
960
971
|
if (result.subscriberId) subscriberIds.push(result.subscriberId);
|
|
@@ -1023,8 +1034,8 @@ function collectRecoverableAgents(projectRoot, target = "") {
|
|
|
1023
1034
|
} else {
|
|
1024
1035
|
targets = entries.filter(([id, meta]) =>
|
|
1025
1036
|
id === target
|
|
1026
|
-
|| (meta && meta.nickname === target)
|
|
1027
|
-
|| (scopedTarget && scopedTarget !== target && meta && meta.nickname === scopedTarget)
|
|
1037
|
+
|| (meta && (meta.nickname === target || meta.scoped_nickname === target))
|
|
1038
|
+
|| (scopedTarget && scopedTarget !== target && meta && (meta.nickname === scopedTarget || meta.scoped_nickname === scopedTarget))
|
|
1028
1039
|
);
|
|
1029
1040
|
}
|
|
1030
1041
|
}
|
|
@@ -1076,7 +1087,8 @@ function getRecoverableAgents(projectRoot, target = "") {
|
|
|
1076
1087
|
const { mode, recoverableEntries, skipped } = collectRecoverableAgents(projectRoot, target);
|
|
1077
1088
|
const recoverable = recoverableEntries.map((item) => ({
|
|
1078
1089
|
id: item.id,
|
|
1079
|
-
nickname: item.meta
|
|
1090
|
+
nickname: resolveDisplayNickname(projectRoot, item.meta),
|
|
1091
|
+
display_nickname: resolveDisplayNickname(projectRoot, item.meta),
|
|
1080
1092
|
agent: item.agent,
|
|
1081
1093
|
sessionId: item.meta.provider_session_id || "",
|
|
1082
1094
|
launchMode: item.meta.launch_mode || "",
|
|
@@ -1094,7 +1106,7 @@ async function resumeAgents(projectRoot, target = "", processManager = null) {
|
|
|
1094
1106
|
|
|
1095
1107
|
const resumed = [];
|
|
1096
1108
|
for (const item of recoverableEntries) {
|
|
1097
|
-
const nickname = item.meta
|
|
1109
|
+
const nickname = resolveDisplayNickname(projectRoot, item.meta);
|
|
1098
1110
|
const sessionId = item.meta.provider_session_id;
|
|
1099
1111
|
const reused = await tryReuseTerminal(projectRoot, item.id, item.meta, item.agent, sessionId);
|
|
1100
1112
|
if (!reused) {
|
package/src/daemon/reporting.js
CHANGED
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
appendControllerInboxEntry,
|
|
10
10
|
} = require("../report/store");
|
|
11
11
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
12
|
+
const { resolveDisplayNickname } = require("./nicknameScope");
|
|
12
13
|
|
|
13
14
|
function resolveAgentDisplayName(projectRoot, agentId) {
|
|
14
15
|
if (!agentId) return "unknown-agent";
|
|
@@ -16,9 +17,7 @@ function resolveAgentDisplayName(projectRoot, agentId) {
|
|
|
16
17
|
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
17
18
|
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
18
19
|
const meta = bus && bus.agents ? bus.agents[agentId] : null;
|
|
19
|
-
if (meta
|
|
20
|
-
return meta.nickname.trim();
|
|
21
|
-
}
|
|
20
|
+
if (meta) return resolveDisplayNickname(projectRoot, meta) || agentId;
|
|
22
21
|
} catch {
|
|
23
22
|
// ignore
|
|
24
23
|
}
|
|
@@ -5,7 +5,11 @@ const EventBus = require("../bus");
|
|
|
5
5
|
const { prepareUcodeBootstrap } = require("../agent/ucodeBootstrap");
|
|
6
6
|
const { isMetaActive } = require("../bus/utils");
|
|
7
7
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
applyProjectNicknamePrefix,
|
|
10
|
+
resolveDisplayNickname,
|
|
11
|
+
resolveScopedNickname,
|
|
12
|
+
} = require("./nicknameScope");
|
|
9
13
|
const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
|
|
10
14
|
const {
|
|
11
15
|
loadPromptProfileRegistry,
|
|
@@ -207,7 +211,11 @@ function resolveExistingAgent(projectRoot, target = "") {
|
|
|
207
211
|
for (const [subscriberId, meta] of Object.entries(agents)) {
|
|
208
212
|
if (
|
|
209
213
|
meta
|
|
210
|
-
&& (
|
|
214
|
+
&& (
|
|
215
|
+
meta.nickname === key
|
|
216
|
+
|| meta.scoped_nickname === key
|
|
217
|
+
|| (scopedKey && scopedKey !== key && (meta.nickname === scopedKey || meta.scoped_nickname === scopedKey))
|
|
218
|
+
)
|
|
211
219
|
&& isLiveAgentMeta(meta)
|
|
212
220
|
) {
|
|
213
221
|
return { subscriberId, meta };
|
|
@@ -222,7 +230,8 @@ function findOwningGroup(projectRoot, subscriberId = "") {
|
|
|
222
230
|
const liveMeta = getAgentRuntimeMeta(projectRoot, targetSubscriber);
|
|
223
231
|
if (!isLiveAgentMeta(liveMeta)) return null;
|
|
224
232
|
if (asTrimmedString(liveMeta.role_owner).toLowerCase() === "solo") return null;
|
|
225
|
-
const liveNickname =
|
|
233
|
+
const liveNickname = resolveDisplayNickname(projectRoot, liveMeta);
|
|
234
|
+
const liveScopedNickname = resolveScopedNickname(projectRoot, liveMeta);
|
|
226
235
|
const groupsDir = getUfooPaths(projectRoot).groupsDir;
|
|
227
236
|
if (!groupsDir) return null;
|
|
228
237
|
let files = [];
|
|
@@ -245,6 +254,7 @@ function findOwningGroup(projectRoot, subscriberId = "") {
|
|
|
245
254
|
!liveNickname
|
|
246
255
|
|| asTrimmedString(member.nickname) === liveNickname
|
|
247
256
|
|| asTrimmedString(member.runtime_nickname) === liveNickname
|
|
257
|
+
|| asTrimmedString(member.runtime_nickname) === liveScopedNickname
|
|
248
258
|
)
|
|
249
259
|
);
|
|
250
260
|
if (found) {
|
package/src/daemon/status.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
4
4
|
const { isMetaActive } = require("../bus/utils");
|
|
5
|
+
const { resolveDisplayNickname, resolveScopedNickname } = require("./nicknameScope");
|
|
5
6
|
const { readReportSummary, countControllerInboxEntries } = require("../report/store");
|
|
6
7
|
const { readRecentLoopSummary } = require("../agent/loopObservability");
|
|
7
8
|
|
|
@@ -146,7 +147,8 @@ function buildStatus(projectRoot, options = {}) {
|
|
|
146
147
|
: [];
|
|
147
148
|
const active = activeEntries.map(({ id }) => id);
|
|
148
149
|
const activeMeta = activeEntries.map(({ id, meta }) => {
|
|
149
|
-
const nickname = meta
|
|
150
|
+
const nickname = resolveDisplayNickname(projectRoot, meta);
|
|
151
|
+
const scoped_nickname = resolveScopedNickname(projectRoot, meta);
|
|
150
152
|
const display = nickname ? nickname : id;
|
|
151
153
|
const launch_mode = meta?.launch_mode || "unknown";
|
|
152
154
|
const tmux_pane = meta?.tmux_pane || "";
|
|
@@ -156,6 +158,8 @@ function buildStatus(projectRoot, options = {}) {
|
|
|
156
158
|
return {
|
|
157
159
|
id,
|
|
158
160
|
nickname,
|
|
161
|
+
scoped_nickname,
|
|
162
|
+
display_nickname: nickname,
|
|
159
163
|
display,
|
|
160
164
|
launch_mode,
|
|
161
165
|
tmux_pane,
|
package/src/status/index.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require("path");
|
|
|
3
3
|
const childProcess = require("child_process");
|
|
4
4
|
const { readJSON } = require("../bus/utils");
|
|
5
5
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
6
|
+
const { resolveDisplayNickname } = require("../daemon/nicknameScope");
|
|
6
7
|
|
|
7
8
|
function normalizeTty(ttyPath) {
|
|
8
9
|
if (!ttyPath) return "";
|
|
@@ -233,7 +234,7 @@ class StatusDisplay {
|
|
|
233
234
|
const busData = readJSON(agentsFile);
|
|
234
235
|
if (!busData || !busData.agents) return null;
|
|
235
236
|
const meta = busData.agents[subscriber];
|
|
236
|
-
return meta
|
|
237
|
+
return meta ? resolveDisplayNickname(this.projectRoot, meta) : null;
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
/**
|