u-foo 1.8.9 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,13 @@ const FALLBACK_LAUNCH_SUBCOMMANDS = [
4
4
  { cmd: "ucode", desc: "Launch ucode core agent" },
5
5
  ];
6
6
 
7
+ function sortSubcommandEntries(a, b) {
8
+ const aOrder = Number.isFinite(a && a.order) ? a.order : Number.POSITIVE_INFINITY;
9
+ const bOrder = Number.isFinite(b && b.order) ? b.order : Number.POSITIVE_INFINITY;
10
+ if (aOrder !== bOrder) return aOrder - bOrder;
11
+ return String(a && a.cmd ? a.cmd : "").localeCompare(String(b && b.cmd ? b.cmd : ""), "en", { sensitivity: "base" });
12
+ }
13
+
7
14
  function createCompletionController(options = {}) {
8
15
  const {
9
16
  input,
@@ -12,6 +19,7 @@ function createCompletionController(options = {}) {
12
19
  promptBox,
13
20
  commandRegistry = [],
14
21
  getGroupTemplateCandidates = () => [],
22
+ getSoloProfileCandidates = () => [],
15
23
  getMentionCandidates = () => [],
16
24
  normalizeCommandPrefix = () => {},
17
25
  truncateText = (text) => String(text || ""),
@@ -143,6 +151,7 @@ function createCompletionController(options = {}) {
143
151
  const mainCmd = parts[0];
144
152
  const isLaunch = mainCmd && mainCmd.toLowerCase() === "/launch";
145
153
  const isGroup = mainCmd && mainCmd.toLowerCase() === "/group";
154
+ const isSolo = mainCmd && mainCmd.toLowerCase() === "/solo";
146
155
  const wantsSubcommands = (parts.length > 1 || (endsWithSpace && parts.length === 1));
147
156
 
148
157
  if (isGroup) {
@@ -169,6 +178,28 @@ function createCompletionController(options = {}) {
169
178
  }
170
179
  }
171
180
 
181
+ if (isSolo) {
182
+ const soloSubcommand = String(parts[1] || "").trim().toLowerCase();
183
+ const wantsSoloRunArgs = soloSubcommand === "run" && (parts.length > 2 || endsWithSpace);
184
+ if (wantsSoloRunArgs) {
185
+ const profileFilter = String(parts[2] || "").trim().toLowerCase();
186
+ return (Array.isArray(getSoloProfileCandidates()) ? getSoloProfileCandidates() : [])
187
+ .map((item) => {
188
+ const profileId = String(item && item.cmd ? item.cmd : item && item.id ? item.id : "").trim();
189
+ if (!profileId) return null;
190
+ const desc = String(item && item.desc ? item.desc : item && item.summary ? item.summary : "").trim();
191
+ return {
192
+ cmd: profileId,
193
+ desc,
194
+ isArgumentSuggestion: true,
195
+ argumentPrefix: "/solo run",
196
+ };
197
+ })
198
+ .filter((item) => item && (!profileFilter || item.cmd.toLowerCase().startsWith(profileFilter)))
199
+ .sort((a, b) => a.cmd.localeCompare(b.cmd, "en", { sensitivity: "base" }));
200
+ }
201
+ }
202
+
172
203
  if ((wantsSubcommands || isLaunch) && mainCmd && mainCmd.startsWith("/")) {
173
204
  const subFilter = parts[1] || "";
174
205
  const mainCmdObj = commandRegistry.find((item) =>
@@ -188,12 +219,12 @@ function createCompletionController(options = {}) {
188
219
  if (isLaunch) {
189
220
  return subs
190
221
  .map((sub) => ({ ...sub, isSubcommand: true, parentCmd: mainCmd }))
191
- .sort((a, b) => a.cmd.localeCompare(b.cmd));
222
+ .sort(sortSubcommandEntries);
192
223
  }
193
224
  return subs
194
225
  .filter((sub) => sub.cmd.toLowerCase().startsWith(subFilter.toLowerCase()))
195
226
  .map((sub) => ({ ...sub, isSubcommand: true, parentCmd: mainCmd }))
196
- .sort((a, b) => a.cmd.localeCompare(b.cmd));
227
+ .sort(sortSubcommandEntries);
197
228
  }
198
229
  return [];
199
230
  }
@@ -340,7 +371,11 @@ function createCompletionController(options = {}) {
340
371
 
341
372
  if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
342
373
  show(input.value);
343
- } else if (selected.isSubcommand && selected.parentCmd === "/group" && selected.cmd === "run") {
374
+ } else if (
375
+ selected.isSubcommand
376
+ && ((selected.parentCmd === "/group" && selected.cmd === "run")
377
+ || (selected.parentCmd === "/solo" && selected.cmd === "run"))
378
+ ) {
344
379
  show(input.value);
345
380
  } else {
346
381
  hide();
@@ -384,7 +419,11 @@ function createCompletionController(options = {}) {
384
419
  applyPreview(nextPreview);
385
420
  if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
386
421
  show(input.value);
387
- } else if (selected.isSubcommand && selected.parentCmd === "/group" && selected.cmd === "run") {
422
+ } else if (
423
+ selected.isSubcommand
424
+ && ((selected.parentCmd === "/group" && selected.cmd === "run")
425
+ || (selected.parentCmd === "/solo" && selected.cmd === "run"))
426
+ ) {
388
427
  show(input.value);
389
428
  } else {
390
429
  hide();
@@ -1,3 +1,5 @@
1
+ const { restartLocks } = require("./daemonTransport");
2
+
1
3
  function resolveDaemonConnection(daemonConnection) {
2
4
  return typeof daemonConnection === "function" ? daemonConnection() : daemonConnection;
3
5
  }
@@ -14,11 +16,10 @@ function restartDaemonFlow(options = {}) {
14
16
 
15
17
  const statusMsg = resolveStatusLine || ((text) => logMessage("status", text));
16
18
 
17
- let restartInProgress = false;
18
-
19
19
  return async function restartDaemon() {
20
- if (restartInProgress) return;
21
- restartInProgress = true;
20
+ // Use global restart lock to prevent concurrent restart flows
21
+ if (restartLocks.get(projectRoot)) return;
22
+ restartLocks.set(projectRoot, true);
22
23
  statusMsg("{gray-fg}⚙{/gray-fg} Restarting daemon...");
23
24
  try {
24
25
  const connection = resolveDaemonConnection(daemonConnection);
@@ -34,7 +35,7 @@ function restartDaemonFlow(options = {}) {
34
35
  statusMsg("{gray-fg}✗{/gray-fg} Failed to reconnect to daemon");
35
36
  }
36
37
  } finally {
37
- restartInProgress = false;
38
+ restartLocks.delete(projectRoot);
38
39
  }
39
40
  };
40
41
  }
@@ -1,5 +1,8 @@
1
1
  const { DAEMON_TRANSPORT_DEFAULTS } = require("./daemonTransportDefaults");
2
2
 
3
+ // Global restart lock per project to prevent concurrent restart flows
4
+ const restartLocks = new Map();
5
+
3
6
  function createDaemonTransport(options = {}) {
4
7
  const {
5
8
  projectRoot,
@@ -34,7 +37,9 @@ function createDaemonTransport(options = {}) {
34
37
  );
35
38
  if (!client) {
36
39
  // Retry once with a fresh daemon start and longer wait.
37
- if (!isRunning(target.projectRoot)) {
40
+ // Check if a restart is already in progress via the explicit restart flow.
41
+ const isExplicitRestartInProgress = restartLocks.get(target.projectRoot);
42
+ if (!isExplicitRestartInProgress && !isRunning(target.projectRoot)) {
38
43
  startDaemon(target.projectRoot);
39
44
  await new Promise((resolve) => setTimeout(resolve, restartDelayMs));
40
45
  }
@@ -75,4 +80,5 @@ function createDaemonTransport(options = {}) {
75
80
 
76
81
  module.exports = {
77
82
  createDaemonTransport,
83
+ restartLocks,
78
84
  };
package/src/chat/index.js CHANGED
@@ -18,6 +18,7 @@ const { getUfooPaths } = require("../ufoo/paths");
18
18
  const { startDaemon, stopDaemon, connectWithRetry } = require("./transport");
19
19
  const { escapeBlessed, stripBlessedTags, truncateText } = require("./text");
20
20
  const { COMMAND_REGISTRY, parseCommand, parseAtTarget } = require("./commands");
21
+ const { buildPromptProfileCandidates } = require("../solo/commands");
21
22
  const inputMath = require("./inputMath");
22
23
  const { createStreamTracker } = require("./streamTracker");
23
24
  const agentDirectory = require("./agentDirectory");
@@ -43,15 +44,18 @@ const { createDaemonCoordinator } = require("./daemonCoordinator");
43
44
  const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
44
45
  const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
45
46
  const { createDaemonTransport } = require("./daemonTransport");
46
- const { listProjectRuntimes, resolveRuntimeDir } = require("../projects/registry");
47
- const { canonicalProjectRoot, buildProjectId } = require("../projects/projectId");
48
- const { loadTemplateRegistry } = require("../group/templates");
49
47
  const {
48
+ listProjectRuntimes,
49
+ resolveRuntimeDir,
50
+ canonicalProjectRoot,
51
+ buildProjectId,
50
52
  sortProjectRuntimes,
51
53
  parseTimestampMs,
52
54
  filterVisibleProjectRuntimes,
53
- } = require("./projectRuntimes");
54
- const { isGlobalControllerProjectRoot, resolveGlobalControllerProjectRoot } = require("../globalMode");
55
+ isGlobalControllerProjectRoot,
56
+ resolveGlobalControllerProjectRoot,
57
+ } = require("../projects");
58
+ const { loadTemplateRegistry } = require("../group/templates");
55
59
  const {
56
60
  DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
57
61
  setTransientAgentState: setTransientAgentStateValue,
@@ -559,6 +563,10 @@ async function runChat(projectRoot, options = {}) {
559
563
  source: item.source || "",
560
564
  }));
561
565
  },
566
+ getSoloProfileCandidates: () => {
567
+ const registry = loadPromptProfileRegistry(activeProjectRoot);
568
+ return buildPromptProfileCandidates(registry);
569
+ },
562
570
  getMentionCandidates: () => activeAgents.map((id) => ({
563
571
  id,
564
572
  label: getAgentLabel(id),
package/src/cli.js CHANGED
@@ -7,6 +7,9 @@ const { runBusCoreCommand } = require("./cli/busCoreCommands");
7
7
  const { runCtxCommand } = require("./cli/ctxCoreCommands");
8
8
  const { runOnlineCommand } = require("./cli/onlineCoreCommands");
9
9
  const { runGroupCoreCommand } = require("./cli/groupCoreCommands");
10
+ const { loadConfig } = require("./config");
11
+ const { loadPromptProfileRegistry } = require("./group/promptProfiles");
12
+ const { resolveSoloAgentType } = require("./solo/commands");
10
13
  const { listProjectRuntimes, getCurrentProjectRuntime } = require("./projects/registry");
11
14
  const { canonicalProjectRoot, buildProjectId } = require("./projects/projectId");
12
15
  const { getUfooPaths } = require("./ufoo/paths");
@@ -448,6 +451,68 @@ async function runCli(argv) {
448
451
  process.exitCode = 1;
449
452
  }
450
453
  });
454
+ program
455
+ .command("solo")
456
+ .description("Solo role agent operations")
457
+ .argument("<action>", "run|list")
458
+ .argument("[profile]", "Prompt profile id or alias")
459
+ .option("--agent <type>", "Agent type: codex|claude|ucode")
460
+ .option("--nickname <name>", "Optional nickname")
461
+ .option("--scope <scope>", "Launch scope: inplace|window", "inplace")
462
+ .option("--json", "Output role list as JSON")
463
+ .action(async (action, profile, opts) => {
464
+ try {
465
+ const projectRoot = process.cwd();
466
+ const subcommand = String(action || "").trim().toLowerCase();
467
+ if (subcommand === "list" || subcommand === "ls") {
468
+ const registry = loadPromptProfileRegistry(projectRoot);
469
+ if (opts.json) {
470
+ console.log(JSON.stringify({ profiles: registry.profiles || [] }, null, 2));
471
+ return;
472
+ }
473
+ const profiles = registry.profiles || [];
474
+ if (profiles.length === 0) {
475
+ console.log("No solo roles found.");
476
+ return;
477
+ }
478
+ console.log(`Available solo roles (${profiles.length}):`);
479
+ for (const p of profiles) {
480
+ const aliases = p.aliases && p.aliases.length > 0 ? ` (${p.aliases.join(", ")})` : "";
481
+ const source = p.source ? ` [${p.source}]` : "";
482
+ console.log(` ${p.id}${aliases}${source}`);
483
+ if (p.summary) console.log(` ${p.summary}`);
484
+ }
485
+ return;
486
+ }
487
+ if (subcommand !== "run") {
488
+ throw new Error(`Unknown solo action: ${subcommand}`);
489
+ }
490
+ const promptProfile = String(profile || "").trim();
491
+ if (!promptProfile) {
492
+ throw new Error("solo run requires <profile>");
493
+ }
494
+ await ensureDaemonRunning(projectRoot);
495
+ const config = loadConfig(projectRoot);
496
+ const agent = resolveSoloAgentType(config, opts.agent || "");
497
+ const scope = String(opts.scope || "inplace").trim().toLowerCase();
498
+ if (scope !== "inplace" && scope !== "window") {
499
+ throw new Error("scope must be inplace|window");
500
+ }
501
+ const resp = await sendDaemonRequest(projectRoot, {
502
+ type: "launch_agent",
503
+ agent: agent === "ucode" ? "ufoo" : agent,
504
+ nickname: String(opts.nickname || "").trim(),
505
+ prompt_profile: promptProfile,
506
+ count: 1,
507
+ launch_scope: scope,
508
+ ...collectHostLaunchRequestContext(),
509
+ });
510
+ console.log(resp?.data?.reply || `Launched ${agent} role ${promptProfile}`);
511
+ } catch (err) {
512
+ console.error(err.message || String(err));
513
+ process.exitCode = 1;
514
+ }
515
+ });
451
516
  program
452
517
  .command("role")
453
518
  .description("Assign a preset role to an existing agent")
@@ -1262,6 +1327,38 @@ async function runCli(argv) {
1262
1327
  }
1263
1328
  });
1264
1329
 
1330
+ const history = program.command("history").description("Agent input history timeline");
1331
+ history
1332
+ .command("build")
1333
+ .description("Build unified input timeline (incremental by default)")
1334
+ .option("--force", "Full rebuild (ignore watermark)")
1335
+ .action((opts) => {
1336
+ const { buildTimeline } = require("./history/inputTimeline");
1337
+ const result = buildTimeline(process.cwd(), { force: opts.force === true });
1338
+ console.log(`Timeline: ${result.count} total, ${result.newCount} new → ${result.file}`);
1339
+ });
1340
+ history
1341
+ .command("show")
1342
+ .description("Show recent timeline entries")
1343
+ .argument("[limit]", "Number of entries to show", "50")
1344
+ .action((limit) => {
1345
+ const { showTimeline } = require("./history/inputTimeline");
1346
+ showTimeline(process.cwd(), parseInt(limit, 10) || 50);
1347
+ });
1348
+ history
1349
+ .command("prompt")
1350
+ .description("Render timeline as injectable prompt context")
1351
+ .argument("[limit]", "Number of entries to include", "30")
1352
+ .action((limit) => {
1353
+ const { renderTimelineForPrompt } = require("./history/inputTimeline");
1354
+ const text = renderTimelineForPrompt(process.cwd(), parseInt(limit, 10) || 30);
1355
+ if (text) {
1356
+ console.log(text);
1357
+ } else {
1358
+ console.log("No timeline data. Run `ufoo history build` first.");
1359
+ }
1360
+ });
1361
+
1265
1362
  program.addHelpText(
1266
1363
  "after",
1267
1364
  `\nNotes:\n - If 'ufoo' isn't in PATH, run it via ${chalk.cyan(
@@ -1312,6 +1409,8 @@ async function runCli(argv) {
1312
1409
  console.log(" ufoo group status [groupId] [--json]");
1313
1410
  console.log(" ufoo group diagram <alias|groupId> [--ascii|--mermaid] [--json]");
1314
1411
  console.log(" ufoo group stop <groupId> [--json]");
1412
+ console.log(" ufoo solo list [--json]");
1413
+ console.log(" ufoo solo run <profile> [--agent <codex|claude|ucode>] [--nickname <name>] [--scope <inplace|window>]");
1315
1414
  console.log(" ufoo online server [--port 8787] [--host 127.0.0.1] [--token-file <path>]");
1316
1415
  console.log(" ufoo online token <subscriber> [--nickname <name>] [--server <url>] [--file <path>]");
1317
1416
  console.log(" ufoo online room create [--name <room>] --type public|private [--password <pwd>] [--created-by <name>] [--server <url>]");
@@ -1324,6 +1423,7 @@ async function runCli(argv) {
1324
1423
  console.log(" ufoo bus wake <target> [--reason <reason>] [--no-shake]");
1325
1424
  console.log(" ufoo bus <args...> (JS bus implementation)");
1326
1425
  console.log(" ufoo ctx <subcmd> ... (doctor|lint|decisions|sync)");
1426
+ console.log(" ufoo history <build|show|prompt> [limit]");
1327
1427
  console.log("");
1328
1428
  console.log("Notes:");
1329
1429
  console.log(" - For Codex notifications, use ufoo bus alert / ufoo bus listen");
@@ -1795,6 +1895,67 @@ async function runCli(argv) {
1795
1895
  })();
1796
1896
  return;
1797
1897
  }
1898
+ if (cmd === "solo") {
1899
+ const sub = String(rest[0] || "").trim().toLowerCase();
1900
+ (async () => {
1901
+ try {
1902
+ const projectRoot = process.cwd();
1903
+ if (sub === "list" || sub === "ls") {
1904
+ const outputJson = rest.includes("--json");
1905
+ const registry = loadPromptProfileRegistry(projectRoot);
1906
+ if (outputJson) {
1907
+ console.log(JSON.stringify({ profiles: registry.profiles || [] }, null, 2));
1908
+ return;
1909
+ }
1910
+ const profiles = registry.profiles || [];
1911
+ if (profiles.length === 0) {
1912
+ console.log("No solo roles found.");
1913
+ return;
1914
+ }
1915
+ console.log(`Available solo roles (${profiles.length}):`);
1916
+ for (const p of profiles) {
1917
+ const aliases = p.aliases && p.aliases.length > 0 ? ` (${p.aliases.join(", ")})` : "";
1918
+ const source = p.source ? ` [${p.source}]` : "";
1919
+ console.log(` ${p.id}${aliases}${source}`);
1920
+ if (p.summary) console.log(` ${p.summary}`);
1921
+ }
1922
+ return;
1923
+ }
1924
+ if (sub === "run") {
1925
+ const profile = String(rest[1] || "").trim();
1926
+ if (!profile) throw new Error("solo run requires <profile>");
1927
+ const agentIdx = rest.indexOf("--agent");
1928
+ const agentInput = agentIdx !== -1 ? String(rest[agentIdx + 1] || "").trim() : "";
1929
+ const nickIdx = rest.indexOf("--nickname");
1930
+ const nickname = nickIdx !== -1 ? String(rest[nickIdx + 1] || "").trim() : "";
1931
+ const scopeIdx = rest.indexOf("--scope");
1932
+ const scope = scopeIdx !== -1 ? String(rest[scopeIdx + 1] || "").trim().toLowerCase() : "inplace";
1933
+ if (scope !== "inplace" && scope !== "window") {
1934
+ throw new Error("scope must be inplace|window");
1935
+ }
1936
+ await ensureDaemonRunning(projectRoot);
1937
+ const config = loadConfig(projectRoot);
1938
+ const agent = resolveSoloAgentType(config, agentInput);
1939
+ const resp = await sendDaemonRequest(projectRoot, {
1940
+ type: "launch_agent",
1941
+ agent: agent === "ucode" ? "ufoo" : agent,
1942
+ nickname,
1943
+ prompt_profile: profile,
1944
+ count: 1,
1945
+ launch_scope: scope,
1946
+ ...collectHostLaunchRequestContext(),
1947
+ });
1948
+ console.log(resp?.data?.reply || `Launched ${agent} role ${profile}`);
1949
+ return;
1950
+ }
1951
+ throw new Error(`Unknown solo action: ${sub || "(empty)"}`);
1952
+ } catch (err) {
1953
+ console.error(err.message || String(err));
1954
+ process.exitCode = 1;
1955
+ }
1956
+ })();
1957
+ return;
1958
+ }
1798
1959
  if (cmd === "online") {
1799
1960
  const sub = rest[0] || "";
1800
1961
  if (!sub) {
@@ -1971,6 +2132,36 @@ async function runCli(argv) {
1971
2132
  return;
1972
2133
  }
1973
2134
 
2135
+ if (cmd === "history") {
2136
+ const sub = rest[0] || "show";
2137
+ const cwd = process.cwd();
2138
+ const { buildTimeline, showTimeline, renderTimelineForPrompt } = require("./history/inputTimeline");
2139
+
2140
+ if (sub === "build") {
2141
+ const force = rest.includes("--force");
2142
+ const result = buildTimeline(cwd, { force });
2143
+ console.log(`Timeline: ${result.count} total, ${result.newCount} new → ${result.file}`);
2144
+ return;
2145
+ }
2146
+ if (sub === "show") {
2147
+ const limit = parseInt(rest[1], 10) || 50;
2148
+ showTimeline(cwd, limit);
2149
+ return;
2150
+ }
2151
+ if (sub === "prompt") {
2152
+ const limit = parseInt(rest[1], 10) || 30;
2153
+ const text = renderTimelineForPrompt(cwd, limit);
2154
+ if (text) {
2155
+ console.log(text);
2156
+ } else {
2157
+ console.log("No timeline data. Run `ufoo history build` first.");
2158
+ }
2159
+ return;
2160
+ }
2161
+ console.log("Usage: ufoo history [build|show|prompt] [limit]");
2162
+ return;
2163
+ }
2164
+
1974
2165
  help();
1975
2166
  process.exitCode = 1;
1976
2167
  }
@@ -753,6 +753,7 @@ function createGroupOrchestrator(options = {}) {
753
753
  }
754
754
 
755
755
  const plan = buildLaunchPlan(validated.entry.data);
756
+ const templateDefaults = (validated.entry.data && validated.entry.data.defaults) || {};
756
757
  const groupId = generateGroupId(validated.entry.alias || alias, instance);
757
758
  const projectNicknamePrefix = buildProjectNicknamePrefix(projectRoot);
758
759
 
@@ -835,6 +836,12 @@ function createGroupOrchestrator(options = {}) {
835
836
  const item = compiled.executionPlan[i];
836
837
  const member = runtime.members[i];
837
838
  const extraEnv = {};
839
+ if (item.bootstrap_required) {
840
+ extraEnv.UFOO_SKIP_DEFAULT_BOOTSTRAP = "1";
841
+ }
842
+ if (Number.isInteger(templateDefaults.start_timeout_ms) && templateDefaults.start_timeout_ms > 0) {
843
+ extraEnv.UFOO_REGISTER_TIMEOUT_MS = String(templateDefaults.start_timeout_ms);
844
+ }
838
845
  let extraArgs = [];
839
846
  let bootstrapInjected = false;
840
847
 
@@ -12,7 +12,7 @@ const { generateInstanceId, subscriberToSafeName } = require("../bus/utils");
12
12
  const { createDaemonIpcServer } = require("./ipcServer");
13
13
  const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES, BUS_STATUS_PHASES } = require("../shared/eventContract");
14
14
  const { getUfooPaths } = require("../ufoo/paths");
15
- const { upsertProjectRuntime, markProjectStopped } = require("../projects/registry");
15
+ const { upsertProjectRuntime, markProjectStopped } = require("../projects");
16
16
  const { scheduleProviderSessionProbe, resolveSessionFromFile, persistProviderSession, loadProviderSessionCache } = require("./providerSessions");
17
17
  const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
18
18
  const { createDaemonCronController } = require("./cronOps");
@@ -22,7 +22,7 @@ const { runAssistantTask } = require("../assistant/bridge");
22
22
  const { runPromptWithAssistant } = require("./promptLoop");
23
23
  const { handlePromptRequest } = require("./promptRequest");
24
24
  const { recordAgentReport } = require("./reporting");
25
- const { isGlobalControllerProjectRoot } = require("../globalMode");
25
+ const { isGlobalControllerProjectRoot } = require("../projects");
26
26
  const {
27
27
  assignSoloRoleToExistingAgent,
28
28
  resolveSoloPromptProfile,
@@ -201,16 +201,16 @@ function looksLikeRunningDaemon(projectRoot, pid) {
201
201
  function isRunning(projectRoot) {
202
202
  const pid = readPid(projectRoot);
203
203
  if (!pid) return false;
204
- if (looksLikeRunningDaemon(projectRoot, pid)) {
205
- return true;
206
- }
204
+ return looksLikeRunningDaemon(projectRoot, pid);
205
+ }
206
+
207
+ function cleanupStaleState(projectRoot) {
207
208
  try {
208
209
  fs.unlinkSync(pidPath(projectRoot));
209
210
  } catch {
210
211
  // ignore
211
212
  }
212
213
  removeSocket(projectRoot);
213
- return false;
214
214
  }
215
215
 
216
216
  function removeSocket(projectRoot) {
@@ -1051,6 +1051,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1051
1051
  await init.init({ modules: "context,bus", project: root });
1052
1052
  }
1053
1053
  if (!isRunning(root)) {
1054
+ cleanupStaleState(root);
1054
1055
  const daemonBin = path.join(__dirname, "..", "..", "bin", "ufoo.js");
1055
1056
  const child = spawn(process.execPath, [daemonBin, "daemon", "--start"], {
1056
1057
  detached: true,
@@ -1384,6 +1385,12 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1384
1385
  }
1385
1386
  }
1386
1387
  }
1388
+ if (requestedProfile) {
1389
+ op.extra_env = {
1390
+ ...(op.extra_env && typeof op.extra_env === "object" ? op.extra_env : {}),
1391
+ UFOO_SKIP_DEFAULT_BOOTSTRAP: "1",
1392
+ };
1393
+ }
1387
1394
  try {
1388
1395
  const opsResults = await handleOps(projectRoot, [op], processManager);
1389
1396
  const launchResult = opsResults.find((r) => r.action === "launch");
@@ -2064,43 +2071,61 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
2064
2071
  }
2065
2072
  log(`agent_ready id=${subscriberId} pid=${agentPid || 0} - resolving session`);
2066
2073
 
2067
- // Try direct file read first if we have agentPid (fast path)
2068
2074
  const parsedAgentPid = Number.parseInt(agentPid, 10);
2069
- if (Number.isFinite(parsedAgentPid) && parsedAgentPid > 0) {
2070
- const agentType = subscriberId.split(":")[0] || "";
2071
- const resolved = resolveSessionFromFile(agentType, {
2072
- pid: parsedAgentPid,
2073
- cwd: projectRoot,
2074
- });
2075
- if (resolved && resolved.sessionId) {
2076
- log(`agent_ready session resolved from file for ${subscriberId}: ${resolved.sessionId}`);
2077
- persistProviderSession(projectRoot, subscriberId, resolved);
2078
- if (providerSessions) {
2079
- providerSessions.set(subscriberId, {
2080
- sessionId: resolved.sessionId,
2081
- source: resolved.source || "",
2082
- updated_at: new Date().toISOString(),
2083
- });
2075
+ const agentType = subscriberId.split(":")[0] || "";
2076
+
2077
+ // In _spawnDirect (host mode), AGENT_READY is sent immediately after
2078
+ // spawn() before Claude has written ~/.claude/sessions/<pid>.json.
2079
+ // Retry with short backoff to handle the race condition.
2080
+ const RETRY_DELAYS_MS = [100, 500, 1000, 2000, 3000];
2081
+
2082
+ const tryResolveSession = (attempt) => {
2083
+ if (Number.isFinite(parsedAgentPid) && parsedAgentPid > 0) {
2084
+ const resolved = resolveSessionFromFile(agentType, {
2085
+ pid: parsedAgentPid,
2086
+ cwd: projectRoot,
2087
+ });
2088
+ if (resolved && resolved.sessionId) {
2089
+ const attemptNote = attempt > 1 ? ` (attempt ${attempt})` : "";
2090
+ log(`agent_ready session resolved from file for ${subscriberId}: ${resolved.sessionId}${attemptNote}`);
2091
+ persistProviderSession(projectRoot, subscriberId, resolved);
2092
+ if (providerSessions) {
2093
+ providerSessions.set(subscriberId, {
2094
+ sessionId: resolved.sessionId,
2095
+ source: resolved.source || "",
2096
+ updated_at: new Date().toISOString(),
2097
+ });
2098
+ }
2099
+ // Cancel the scheduled probe to prevent redundant /ufoo injection
2100
+ const handle = probeHandles.get(subscriberId);
2101
+ if (handle && typeof handle.cancel === "function") {
2102
+ handle.cancel();
2103
+ }
2104
+ probeHandles.delete(subscriberId);
2105
+ return;
2084
2106
  }
2085
- // Cancel the scheduled probe to prevent /ufoo injection
2086
- const handle = probeHandles.get(subscriberId);
2087
- if (handle && typeof handle.cancel === "function") {
2088
- handle.cancel();
2107
+
2108
+ // Session file not ready — retry if attempts remain
2109
+ const delayMs = RETRY_DELAYS_MS[attempt - 1];
2110
+ if (delayMs !== undefined) {
2111
+ setTimeout(() => tryResolveSession(attempt + 1), delayMs);
2112
+ return;
2089
2113
  }
2090
- probeHandles.delete(subscriberId);
2091
- return;
2092
2114
  }
2093
- }
2094
2115
 
2095
- // Fallback: trigger scheduled probe
2096
- const probeHandle = probeHandles.get(subscriberId);
2097
- if (probeHandle && typeof probeHandle.triggerNow === "function") {
2098
- probeHandle.triggerNow().catch((err) => {
2099
- log(`agent_ready probe trigger failed for ${subscriberId}: ${err.message}`);
2100
- });
2101
- } else {
2102
- log(`agent_ready no probe handle found for ${subscriberId}`);
2103
- }
2116
+ // Exhausted retries or no pid — fall back to scheduled probe
2117
+ const probeHandle = probeHandles.get(subscriberId);
2118
+ if (probeHandle && typeof probeHandle.triggerNow === "function") {
2119
+ log(`agent_ready falling back to probe for ${subscriberId}`);
2120
+ probeHandle.triggerNow().catch((err) => {
2121
+ log(`agent_ready probe trigger failed for ${subscriberId}: ${err.message}`);
2122
+ });
2123
+ } else {
2124
+ log(`agent_ready no probe handle found for ${subscriberId}`);
2125
+ }
2126
+ };
2127
+
2128
+ tryResolveSession(1);
2104
2129
  return;
2105
2130
  }
2106
2131
  };
@@ -2323,4 +2348,4 @@ function stopDaemon(projectRoot) {
2323
2348
  return killed;
2324
2349
  }
2325
2350
 
2326
- module.exports = { startDaemon, stopDaemon, isRunning, socketPath };
2351
+ module.exports = { startDaemon, stopDaemon, isRunning, cleanupStaleState, socketPath };
@@ -5,7 +5,7 @@ const {
5
5
  listControllerInboxEntries,
6
6
  consumeControllerInboxEntries,
7
7
  } = require("../report/store");
8
- const { isGlobalControllerProjectRoot } = require("../globalMode");
8
+ const { isGlobalControllerProjectRoot } = require("../projects");
9
9
 
10
10
  function normalizeProjectRoute(route) {
11
11
  if (!route || typeof route !== "object") return null;
@@ -2,6 +2,15 @@
2
2
 
3
3
  const crypto = require("crypto");
4
4
 
5
+ const SHARED_UFOO_PROTOCOL = [
6
+ "ufoo protocol:",
7
+ "- At session start, sync shared context with `ufoo ctx decisions -l` and `ufoo ctx decisions -n 1`.",
8
+ "- Record a decision ONLY for important, plan-level knowledge: architectural choices, multi-option trade-off analysis, cross-agent coordination decisions, or plans that affect other agents. Do NOT record routine findings, simple bug fixes, or trivial observations. Use `ufoo ctx decisions new \"Title\"` BEFORE acting.",
9
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` for agent-to-agent handoffs.",
10
+ "- If you receive pending bus work, execute it immediately, reply to the sender, then `ufoo bus ack \"$UFOO_SUBSCRIBER_ID\"`.",
11
+ "- Use `ufoo report` for controller/runtime status updates, not as a substitute for direct handoffs.",
12
+ ].join("\n");
13
+
5
14
  const SHARED_GROUP_PREFIX = [
6
15
  "You are part of a ufoo multi-agent group.",
7
16
  "",
@@ -13,6 +22,8 @@ const SHARED_GROUP_PREFIX = [
13
22
  "- When reporting, separate facts, inferences, and recommendations.",
14
23
  "- Preserve continuity with the group's current task rather than restarting analysis from scratch.",
15
24
  "",
25
+ SHARED_UFOO_PROTOCOL,
26
+ "",
16
27
  "Coordination protocol:",
17
28
  "- Use direct handoff for worker-to-worker delivery.",
18
29
  "- Use private `ufoo report` updates for ufoo-agent control-plane reporting.",
@@ -28,6 +39,8 @@ const SOLO_AGENT_PREFIX = [
28
39
  "- Surface uncertainty explicitly.",
29
40
  "- Preserve continuity with the current task instead of restarting from scratch.",
30
41
  "- Use ufoo-agent for control-plane coordination, not as a substitute for doing your role.",
42
+ "",
43
+ SHARED_UFOO_PROTOCOL,
31
44
  ].join("\n");
32
45
 
33
46
  function asTrimmedString(value) {
@@ -145,6 +158,7 @@ function computeBootstrapFingerprint({
145
158
  }
146
159
 
147
160
  module.exports = {
161
+ SHARED_UFOO_PROTOCOL,
148
162
  SHARED_GROUP_PREFIX,
149
163
  SOLO_AGENT_PREFIX,
150
164
  buildGroupPromptMetadata,