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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
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 currentNickname = currentMeta.nickname ? ` (${currentMeta.nickname})` : "";
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
- const nickname = sub.nickname ? ` (${sub.nickname})` : "";
474
+ const nickname = sub.nickname ? ` (${sub.nickname})` : "";
472
475
  console.log(` ${sub.id}${nickname}`);
473
476
  }
474
477
  }
@@ -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
  }));
@@ -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 (id !== excludeSubscriber && meta.nickname === nickname) {
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
 
@@ -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 (existingMeta && existingMeta.nickname) {
180
- // 重新加入,保留原昵称
181
- finalNickname = existingMeta.nickname;
182
- } else if (nickname) {
182
+ if (nickname) {
183
183
  // 新昵称,检查冲突
184
- if (nicknameManager.nicknameExists(nickname, subscriber)) {
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 { subscriber, nickname: finalNickname };
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(newNickname, subscriber)) {
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 nickname = meta && meta.nickname ? ` (${meta.nickname})` : "";
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 nickname = item.nickname ? ` (${item.nickname})` : "";
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 && meta.nickname) return meta.nickname;
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 && meta.nickname) fallbackMap.set(id, meta.nickname);
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;
@@ -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: 4096,
687
+ max_tokens: 131072,
687
688
  messages,
688
689
  tools: buildAnthropicToolSpecs(),
689
690
  stream: true,
@@ -31,7 +31,11 @@ const {
31
31
  buildSoloBootstrapFingerprint,
32
32
  rollbackLaunchAfterRoleAssignmentFailure,
33
33
  } = require("./soloBootstrap");
34
- const { applyProjectNicknamePrefix } = require("./nicknameScope");
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.nickname);
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
- if (!nickname) return { existing: null, cleaned: false };
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]) => meta && meta.nickname === nickname);
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]?.nickname || "";
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 = applyProjectNicknamePrefix(projectRoot, requestedNickname, { agentType: agent });
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
- nickname = applyProjectNicknamePrefix(projectRoot, requestedNickname, {
631
+ const scopedNickname = applyProjectNicknamePrefix(projectRoot, requestedNickname, {
619
632
  agentType: targetMeta.agent_type || "",
620
633
  });
621
- const result = await eventBus.rename(targetId, nickname, "ufoo-agent");
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 = applyProjectNicknamePrefix(projectRoot, String(nickname || "").trim(), {
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 { applyProjectNicknamePrefix } = require("./nicknameScope");
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]) => meta?.nickname === agentId);
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]) => meta?.nickname === scopedNickname);
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 extraEnvPrefix = buildShellEnvPrefix(extraEnvObject);
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
- extraEnvObject
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
- extraEnvObject,
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.nickname || "",
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.nickname || "";
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) {
@@ -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 && typeof meta.nickname === "string" && meta.nickname.trim()) {
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 { applyProjectNicknamePrefix } = require("./nicknameScope");
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
- && (meta.nickname === key || (scopedKey && scopedKey !== key && meta.nickname === scopedKey))
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 = asTrimmedString(liveMeta.nickname);
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) {
@@ -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?.nickname || "";
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,
@@ -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 && meta.nickname ? meta.nickname : null;
237
+ return meta ? resolveDisplayNickname(this.projectRoot, meta) : null;
237
238
  }
238
239
 
239
240
  /**