u-foo 1.7.2 → 1.7.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": "1.7.2",
3
+ "version": "1.7.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",
@@ -7,6 +7,7 @@ const { shakeTerminalByTty } = require("../bus/shake");
7
7
  const { isITerm2 } = require("../terminal/detect");
8
8
  const iterm2 = require("../terminal/iterm2");
9
9
  const { createActivityStatePublisher } = require("./activityStatePublisher");
10
+ const { INJECTION_MODES, getInjectionModeFromEvent } = require("../bus/messageMeta");
10
11
 
11
12
  /**
12
13
  * Agent 消息通知监听器
@@ -232,11 +233,6 @@ class AgentNotifier {
232
233
  return 0;
233
234
  }
234
235
 
235
- const activityState = this.getCurrentActivityState();
236
- if (this.isBusyState(activityState)) {
237
- return 0;
238
- }
239
-
240
236
  // Back off on consecutive inject failures to avoid tight retry loop
241
237
  if (this.injectFailCount >= this.maxInjectRetries) {
242
238
  return 0;
@@ -251,6 +247,12 @@ class AgentNotifier {
251
247
  if (!evt || evt.event !== "message" || !evt.data || typeof evt.data.message !== "string") {
252
248
  continue;
253
249
  }
250
+ const injectionMode = getInjectionModeFromEvent(evt, INJECTION_MODES.IMMEDIATE);
251
+ const activityState = this.getCurrentActivityState();
252
+ if (injectionMode === INJECTION_MODES.QUEUED && this.isBusyState(activityState)) {
253
+ requeue.push(evt);
254
+ continue;
255
+ }
254
256
  if (consumedOne) {
255
257
  requeue.push(evt);
256
258
  continue;
@@ -274,7 +274,7 @@ function buildSystemPrompt(context) {
274
274
  "{",
275
275
  ' "reply": "string",',
276
276
  ` "assistant_call": {"kind":"explore|bash|mixed","task":"string","context":"optional","expect":"optional","provider":"codex|claude|ufoo (optional)","model":"optional","timeout_ms":${DEFAULT_ASSISTANT_TIMEOUT_MS}},`,
277
- ' "dispatch": [{"target":"broadcast|<agent-id>|<nickname>","message":"string"}],',
277
+ ' "dispatch": [{"target":"broadcast|<agent-id>|<nickname>","message":"string","injection_mode":"immediate|queued (optional)","source":"optional"}],',
278
278
  ' "ops": [{"action":"launch|close|rename|cron","agent":"codex|claude|ucode","count":1,"agent_id":"id","nickname":"optional","operation":"start|list|stop","every":"30m","interval_ms":1800000,"target":"agent-id|nickname|csv","targets":["agent-id"],"prompt":"message","id":"task-id|all"}],',
279
279
  ' "disambiguate": {"prompt":"string","candidates":[{"agent_id":"id","reason":"string"}]}',
280
280
  "}",
@@ -291,7 +291,10 @@ function buildSystemPrompt(context) {
291
291
  "- Prefer assistant_call over launching coding agents when the task is short-lived.",
292
292
  "- Primary routing signal is semantic continuity from agent_prompt_history; prefer the agent that already handled similar prompts.",
293
293
  "- Launch a new coding agent when the request is a new topic without clear ownership in existing histories.",
294
- "- If best-matching target agent is busy, keep routing to that same agent (queue semantics) instead of rerouting only by idle status.",
294
+ "- dispatch.injection_mode defaults to immediate when omitted.",
295
+ "- Use queued only when routing a chat-dialog request that is clearly a new unrelated task for an agent whose recent prompt history shows a different ongoing thread.",
296
+ "- If the new request strongly continues the target agent's recent prompt history, keep injection_mode immediate even when that agent is busy.",
297
+ "- Manual @agent sends in ufoo chat are handled outside this router and remain immediate; do not model them here.",
295
298
  "- Legacy compatibility: if model emits ops.assistant_call, daemon will still process it.",
296
299
  "- If no action needed, return reply with empty dispatch/ops.",
297
300
  agentGuidance,
package/src/bus/daemon.js CHANGED
@@ -1,10 +1,19 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
- const { readJSON, writeJSON, isPidAlive, isAgentPidAlive, ensureDir, safeNameToSubscriber, subscriberToSafeName } = require("./utils");
3
+ const { readJSON, writeJSON, isPidAlive, isAgentPidAlive, isMetaActive, ensureDir, safeNameToSubscriber, subscriberToSafeName } = require("./utils");
4
4
  const Injector = require("./inject");
5
5
  const QueueManager = require("./queue");
6
6
  const MessageManager = require("./message");
7
7
  const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
8
+ const { INJECTION_MODES, getInjectionModeFromEvent } = require("./messageMeta");
9
+
10
+ function isBusyActivityState(value = "") {
11
+ const state = String(value || "").trim().toLowerCase();
12
+ return state === "working"
13
+ || state === "running"
14
+ || state === "waiting_input"
15
+ || state === "blocked";
16
+ }
8
17
 
9
18
  /**
10
19
  * Bus Daemon - 监控消息并自动注入命令
@@ -25,6 +34,49 @@ class BusDaemon {
25
34
  this.queueManager = new QueueManager(busDir);
26
35
  this.injector = new Injector(busDir, agentsFile);
27
36
  this.adapterRouter = createTerminalAdapterRouter();
37
+ this.workingHoldMs = Number.parseInt(process.env.UFOO_ACTIVITY_WORKING_HOLD_MS || "", 10) || 5000;
38
+ this.lastWorkingAt = new Map();
39
+ }
40
+
41
+ setLegacyActivityState(subscriber, state) {
42
+ const busData = readJSON(this.agentsFile) || { agents: {} };
43
+ if (!busData.agents || !busData.agents[subscriber]) return;
44
+ busData.agents[subscriber].activity_state = state;
45
+ busData.agents[subscriber].activity_since = new Date().toISOString();
46
+ writeJSON(this.agentsFile, busData);
47
+ }
48
+
49
+ refreshLegacyActivityState(subscriber, meta, hasPending) {
50
+ const now = Date.now();
51
+ const current = String(meta?.activity_state || "").trim().toLowerCase();
52
+ const lastWorkingAt = this.lastWorkingAt.get(subscriber) || 0;
53
+ const holdActive = lastWorkingAt > 0 && (now - lastWorkingAt) < this.workingHoldMs;
54
+ const daemonManagedWorking = lastWorkingAt > 0;
55
+
56
+ if (holdActive) {
57
+ if (current !== "working") {
58
+ this.setLegacyActivityState(subscriber, "working");
59
+ }
60
+ return "working";
61
+ }
62
+
63
+ if (lastWorkingAt > 0) {
64
+ this.lastWorkingAt.delete(subscriber);
65
+ }
66
+
67
+ if (current === "starting") {
68
+ const next = hasPending ? "ready" : "idle";
69
+ this.setLegacyActivityState(subscriber, next);
70
+ return next;
71
+ }
72
+
73
+ if (current === "working" && daemonManagedWorking) {
74
+ const next = hasPending ? "ready" : "idle";
75
+ this.setLegacyActivityState(subscriber, next);
76
+ return next;
77
+ }
78
+
79
+ return current;
28
80
  }
29
81
 
30
82
  /**
@@ -257,15 +309,26 @@ class BusDaemon {
257
309
  continue;
258
310
  }
259
311
 
312
+ let currentActivityState = this.refreshLegacyActivityState(subscriber, meta, count > 0);
260
313
  const events = this.drainPending(pendingFile);
261
314
  const failed = [];
315
+ let deliveredCount = 0;
262
316
  for (const evt of events) {
263
317
  if (!evt || evt.event !== "message" || !evt.data || typeof evt.data.message !== "string") {
264
318
  continue;
265
319
  }
320
+ const injectionMode = getInjectionModeFromEvent(evt, INJECTION_MODES.IMMEDIATE);
321
+ if (injectionMode === INJECTION_MODES.QUEUED && isBusyActivityState(currentActivityState)) {
322
+ failed.push(evt);
323
+ continue;
324
+ }
266
325
  try {
267
326
  // eslint-disable-next-line no-await-in-loop
268
327
  await this.injector.inject(subscriber, String(evt.data.message));
328
+ deliveredCount += 1;
329
+ currentActivityState = "working";
330
+ this.lastWorkingAt.set(subscriber, Date.now());
331
+ this.setLegacyActivityState(subscriber, "working");
269
332
  } catch (err) {
270
333
  failed.push(evt);
271
334
  try {
@@ -312,7 +375,7 @@ class BusDaemon {
312
375
  // ignore requeue failures
313
376
  }
314
377
  }
315
- console.log(`[daemon] Delivered ${events.length} message(s) to ${subscriber}`);
378
+ console.log(`[daemon] Delivered ${deliveredCount} message(s) to ${subscriber}`);
316
379
  if (wakeActive) fs.rmSync(wakePath, { force: true });
317
380
  } catch (err) {
318
381
  console.error(`[daemon] Failed to inject: ${err.message}`);
@@ -405,12 +468,13 @@ class BusDaemon {
405
468
  continue;
406
469
  }
407
470
 
408
- // 检查 PID 是否仍然存活
409
- if (meta.pid && !isAgentPidAlive(meta.pid)) {
471
+ // 检查 agent 是否仍然存活(PID + TTY 交叉检查)
472
+ if (!isMetaActive(meta)) {
410
473
  const now = new Date().toISOString().split("T")[1].slice(0, 8);
411
- console.log(`[daemon] ${now} Agent ${subscriber} (pid=${meta.pid}) is dead, marking inactive`);
474
+ console.log(`[daemon] ${now} Agent ${subscriber} (pid=${meta.pid || 0}) is dead, marking inactive`);
412
475
 
413
476
  meta.status = "inactive";
477
+ meta.activity_state = "";
414
478
  changed = true;
415
479
 
416
480
  // 清理队列目录和 offset
package/src/bus/index.js CHANGED
@@ -306,7 +306,11 @@ class EventBus {
306
306
  const eventName = options.event || "message";
307
307
  const data = options.data || { message };
308
308
  const result = eventName === "message"
309
- ? await this.messageManager.send(target, message, publisher)
309
+ ? await this.messageManager.send(target, message, publisher, {
310
+ data,
311
+ injectionMode: options.injectionMode,
312
+ source: options.source,
313
+ })
310
314
  : await this.messageManager.emit(target, eventName, data, publisher);
311
315
  const silent = options.silent === true;
312
316
  if (!silent && eventName === "message") {
@@ -10,6 +10,7 @@ const {
10
10
  normalizeAgentTypeAlias,
11
11
  } = require("./utils");
12
12
  const NicknameManager = require("./nickname");
13
+ const { buildMessageData } = require("./messageMeta");
13
14
 
14
15
  const SEQ_LOCK_TIMEOUT_MS = 5000;
15
16
  const SEQ_LOCK_POLL_MS = 25;
@@ -241,7 +242,7 @@ class MessageManager {
241
242
  /**
242
243
  * 发送消息
243
244
  */
244
- async send(target, message, publisher = "unknown") {
245
+ async send(target, message, publisher = "unknown", options = {}) {
245
246
  const seq = await this.getNextSeq();
246
247
  const timestamp = getTimestamp();
247
248
  const date = getDate();
@@ -252,6 +253,8 @@ class MessageManager {
252
253
  throw new Error(`Target "${target}" not found`);
253
254
  }
254
255
 
256
+ const data = buildMessageData(message, options);
257
+
255
258
  // 构建事件
256
259
  const event = {
257
260
  seq,
@@ -260,7 +263,7 @@ class MessageManager {
260
263
  event: "message",
261
264
  publisher,
262
265
  target,
263
- data: { message },
266
+ data,
264
267
  };
265
268
 
266
269
  // 写入事件日志
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+
3
+ const INJECTION_MODES = {
4
+ IMMEDIATE: "immediate",
5
+ QUEUED: "queued",
6
+ };
7
+
8
+ function normalizeInjectionMode(value, fallback = INJECTION_MODES.IMMEDIATE) {
9
+ const raw = String(value || "").trim().toLowerCase();
10
+ if (raw === INJECTION_MODES.QUEUED) return INJECTION_MODES.QUEUED;
11
+ if (raw === INJECTION_MODES.IMMEDIATE) return INJECTION_MODES.IMMEDIATE;
12
+ return fallback;
13
+ }
14
+
15
+ function normalizeMessageSource(value) {
16
+ const raw = String(value || "").trim();
17
+ return raw || "";
18
+ }
19
+
20
+ function buildMessageData(message, options = {}) {
21
+ const base = options && typeof options.data === "object" && options.data
22
+ ? { ...options.data }
23
+ : {};
24
+ const data = { ...base, message };
25
+ data.injection_mode = normalizeInjectionMode(
26
+ options.injectionMode || data.injection_mode,
27
+ INJECTION_MODES.IMMEDIATE,
28
+ );
29
+ const source = normalizeMessageSource(options.source || data.source);
30
+ if (source) {
31
+ data.source = source;
32
+ } else {
33
+ delete data.source;
34
+ }
35
+ return data;
36
+ }
37
+
38
+ function getInjectionModeFromEvent(evt, fallback = INJECTION_MODES.IMMEDIATE) {
39
+ const data = evt && typeof evt.data === "object" && evt.data ? evt.data : {};
40
+ return normalizeInjectionMode(
41
+ data.injection_mode || evt?.injection_mode,
42
+ fallback,
43
+ );
44
+ }
45
+
46
+ module.exports = {
47
+ INJECTION_MODES,
48
+ normalizeInjectionMode,
49
+ normalizeMessageSource,
50
+ buildMessageData,
51
+ getInjectionModeFromEvent,
52
+ };
package/src/bus/utils.js CHANGED
@@ -310,23 +310,44 @@ function isMetaActive(meta) {
310
310
  // 2. PID 存活(最可靠)
311
311
  if (meta.pid && isAgentPidAlive(meta.pid)) return true;
312
312
 
313
- // 3. TTY 上有 agent 进程
313
+ // 3. PID 已记录但进程已死 确定离线
314
+ if (meta.pid) return false;
315
+
316
+ // 4. 无 PID(如 codex)— TTY 交叉校验
317
+ // 仅当 tty_shell_pid 也还活着时才信任 TTY 检查,
318
+ // 防止 TTY 上残留的僵尸进程导致误判存活
314
319
  if (meta.tty) {
315
320
  const ttyInfo = getTtyProcessInfo(meta.tty);
316
- if (ttyInfo && ttyInfo.hasAgent) return true;
321
+ if (ttyInfo && ttyInfo.hasAgent) {
322
+ // 如果记录了 tty_shell_pid,验证它还在
323
+ if (meta.tty_shell_pid) {
324
+ if (isPidAlive(meta.tty_shell_pid)) return true;
325
+ // shell pid 已死,TTY 上的进程是残留
326
+ return false;
327
+ }
328
+ // 无 tty_shell_pid,用 last_seen 超时兜底
329
+ if (meta.last_seen) {
330
+ const age = Date.now() - new Date(meta.last_seen).getTime();
331
+ return age <= HEARTBEAT_TIMEOUT_MS;
332
+ }
333
+ return true;
334
+ }
317
335
  }
318
336
 
319
- // 4. PID 存在但 dead(且 TTY 也没有 agent)→ 离线
320
- if (meta.pid) return false;
321
-
322
- // 5. 无 PID,用 last_seen 心跳超时兜底
337
+ // 5. PID TTY agent,用 last_seen 心跳超时兜底
323
338
  if (meta.status === "active" && meta.last_seen) {
324
339
  const age = Date.now() - new Date(meta.last_seen).getTime();
325
340
  return age <= HEARTBEAT_TIMEOUT_MS;
326
341
  }
327
342
 
328
- // 6. status=active 但无任何信息
329
- if (meta.status === "active") return true;
343
+ // 6. status=active 但无任何可靠信息 → 超时后判定离线
344
+ if (meta.status === "active") {
345
+ if (meta.joined_at) {
346
+ const age = Date.now() - new Date(meta.joined_at).getTime();
347
+ return age <= HEARTBEAT_TIMEOUT_MS;
348
+ }
349
+ return true;
350
+ }
330
351
 
331
352
  return false;
332
353
  }
@@ -241,13 +241,36 @@ function createCommandExecutor(options = {}) {
241
241
 
242
242
  try {
243
243
  if (subcommand === "send") {
244
- if (args.length < 3) {
245
- logMessage("error", "{white-fg}✗{/white-fg} Usage: /bus send <target> <message>");
244
+ let injectionMode = "immediate";
245
+ let index = 1;
246
+ while (index < args.length) {
247
+ const arg = args[index];
248
+ if (arg === "--queued") {
249
+ injectionMode = "queued";
250
+ index += 1;
251
+ continue;
252
+ }
253
+ if (arg === "--immediate") {
254
+ injectionMode = "immediate";
255
+ index += 1;
256
+ continue;
257
+ }
258
+ break;
259
+ }
260
+ const positionals = args.slice(index);
261
+ if (positionals.length < 2) {
262
+ logMessage("error", "{white-fg}✗{/white-fg} Usage: /bus send [--queued|--immediate] <target> <message>");
246
263
  return;
247
264
  }
248
- const target = args[1];
249
- const message = args.slice(2).join(" ");
250
- send({ type: IPC_REQUEST_TYPES.BUS_SEND, target, message });
265
+ const target = positionals[0];
266
+ const message = positionals.slice(1).join(" ");
267
+ send({
268
+ type: IPC_REQUEST_TYPES.BUS_SEND,
269
+ target,
270
+ message,
271
+ injection_mode: injectionMode,
272
+ source: "chat-command",
273
+ });
251
274
  logMessage("system", `{white-fg}✓{/white-fg} Message sent to ${target}`);
252
275
  return;
253
276
  }
@@ -839,6 +862,7 @@ function createCommandExecutor(options = {}) {
839
862
  alias,
840
863
  instance,
841
864
  dry_run: dryRun,
865
+ ...collectHostLaunchRequestContext(),
842
866
  });
843
867
  schedule(requestStatus, 1000);
844
868
  return;
package/src/chat/index.js CHANGED
@@ -316,6 +316,8 @@ async function runChat(projectRoot, options = {}) {
316
316
  type: IPC_REQUEST_TYPES.BUS_SEND,
317
317
  target,
318
318
  message: JSON.stringify({ raw: true, data }),
319
+ injection_mode: "immediate",
320
+ source: "chat-agent-view",
319
321
  });
320
322
  },
321
323
  });
@@ -90,7 +90,13 @@ function createInputSubmitHandler(options = {}) {
90
90
  );
91
91
  renderScreen(); // Immediately render the user message
92
92
  markPendingDelivery(state.targetAgent);
93
- send({ type: IPC_REQUEST_TYPES.BUS_SEND, target: state.targetAgent, message: text });
93
+ send({
94
+ type: IPC_REQUEST_TYPES.BUS_SEND,
95
+ target: state.targetAgent,
96
+ message: text,
97
+ injection_mode: "immediate",
98
+ source: "chat-direct",
99
+ });
94
100
  clearTargetAgent();
95
101
  focusInput();
96
102
  return;
@@ -120,7 +126,13 @@ function createInputSubmitHandler(options = {}) {
120
126
  );
121
127
  renderScreen(); // Immediately render the user message
122
128
  markPendingDelivery(resolvedTarget);
123
- send({ type: IPC_REQUEST_TYPES.BUS_SEND, target: resolvedTarget, message: atTarget.message });
129
+ send({
130
+ type: IPC_REQUEST_TYPES.BUS_SEND,
131
+ target: resolvedTarget,
132
+ message: atTarget.message,
133
+ injection_mode: "immediate",
134
+ source: "chat-direct",
135
+ });
124
136
  focusInput();
125
137
  return;
126
138
  }
@@ -145,6 +157,11 @@ function createInputSubmitHandler(options = {}) {
145
157
  send({
146
158
  type: IPC_REQUEST_TYPES.PROMPT,
147
159
  text: `Use agent ${choice.agent_id} to handle: ${state.pending.original || "the request"}`,
160
+ request_meta: {
161
+ source: "chat-dialog",
162
+ dispatch_default_injection_mode: "immediate",
163
+ allow_relevance_queue: true,
164
+ },
148
165
  });
149
166
  state.pending = null;
150
167
  } else {
@@ -153,7 +170,15 @@ function createInputSubmitHandler(options = {}) {
153
170
  } else {
154
171
  state.pending = { original: text };
155
172
  queueStatusLine("ufoo-agent processing");
156
- send({ type: IPC_REQUEST_TYPES.PROMPT, text });
173
+ send({
174
+ type: IPC_REQUEST_TYPES.PROMPT,
175
+ text,
176
+ request_meta: {
177
+ source: "chat-dialog",
178
+ dispatch_default_injection_mode: "immediate",
179
+ allow_relevance_queue: true,
180
+ },
181
+ });
157
182
  logMessage("user", `{white-fg}→{/white-fg} ${escapeBlessed(text)}`);
158
183
  renderScreen(); // Render plain text message immediately
159
184
  }
@@ -1,5 +1,44 @@
1
1
  "use strict";
2
2
 
3
+ function parseSendArgs(cmdArgs = []) {
4
+ let injectionMode = "immediate";
5
+ let source = "";
6
+ let index = 0;
7
+
8
+ while (index < cmdArgs.length) {
9
+ const arg = cmdArgs[index];
10
+ if (arg === "--queued") {
11
+ injectionMode = "queued";
12
+ index += 1;
13
+ continue;
14
+ }
15
+ if (arg === "--immediate") {
16
+ injectionMode = "immediate";
17
+ index += 1;
18
+ continue;
19
+ }
20
+ if (arg === "--source") {
21
+ source = String(cmdArgs[index + 1] || "").trim();
22
+ index += 2;
23
+ continue;
24
+ }
25
+ break;
26
+ }
27
+
28
+ const positionals = cmdArgs.slice(index);
29
+
30
+ if (positionals.length < 2) {
31
+ throw new Error("send requires <target> <message>");
32
+ }
33
+
34
+ return {
35
+ target: positionals[0],
36
+ message: positionals.slice(1).join(" "),
37
+ injectionMode,
38
+ source,
39
+ };
40
+ }
41
+
3
42
  async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
4
43
  switch (cmd) {
5
44
  case "init":
@@ -15,7 +54,11 @@ async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
15
54
  case "send":
16
55
  {
17
56
  const publisher = await eventBus.ensureJoined();
18
- await eventBus.send(cmdArgs[0], cmdArgs[1], publisher);
57
+ const parsed = parseSendArgs(cmdArgs);
58
+ await eventBus.send(parsed.target, parsed.message, publisher, {
59
+ injectionMode: parsed.injectionMode,
60
+ source: parsed.source,
61
+ });
19
62
  }
20
63
  return {};
21
64
  case "broadcast":
package/src/cli.js CHANGED
@@ -791,6 +791,7 @@ async function runCli(argv) {
791
791
  alias,
792
792
  instance: opts.instance || "",
793
793
  dry_run: opts.dryRun === true,
794
+ ...collectHostLaunchRequestContext(),
794
795
  });
795
796
  if (opts.json) {
796
797
  console.log(JSON.stringify(resp?.data || {}, null, 2));
@@ -1684,6 +1685,7 @@ async function runCli(argv) {
1684
1685
  alias,
1685
1686
  instance,
1686
1687
  dry_run: dryRun,
1688
+ ...collectHostLaunchRequestContext(),
1687
1689
  });
1688
1690
  if (outputJson) {
1689
1691
  console.log(JSON.stringify(resp?.data || {}, null, 2));
@@ -190,6 +190,24 @@ function nowIso() {
190
190
  return new Date().toISOString();
191
191
  }
192
192
 
193
+ function buildLaunchHostContext(params = {}) {
194
+ const hostInjectSock = asTrimmedString(params.host_inject_sock || params.hostInjectSock);
195
+ const hostDaemonSock = asTrimmedString(params.host_daemon_sock || params.hostDaemonSock);
196
+ const hostName = asTrimmedString(params.host_name || params.hostName);
197
+ const hostSessionId = asTrimmedString(params.host_session_id || params.hostSessionId);
198
+ const context = {};
199
+ if (hostInjectSock) context.host_inject_sock = hostInjectSock;
200
+ if (hostDaemonSock) context.host_daemon_sock = hostDaemonSock;
201
+ if (hostName) context.host_name = hostName;
202
+ if (hostSessionId) context.host_session_id = hostSessionId;
203
+ if (params.host_capabilities && typeof params.host_capabilities === "object") {
204
+ context.host_capabilities = { ...params.host_capabilities };
205
+ } else if (params.hostCapabilities && typeof params.hostCapabilities === "object") {
206
+ context.host_capabilities = { ...params.hostCapabilities };
207
+ }
208
+ return context;
209
+ }
210
+
193
211
  function buildDefaultRuntime({
194
212
  groupId,
195
213
  instance,
@@ -309,6 +327,7 @@ function createGroupOrchestrator(options = {}) {
309
327
  const alias = asTrimmedString(params.alias);
310
328
  const instance = asTrimmedString(params.instance);
311
329
  const dryRun = params.dry_run === true || params.dryRun === true;
330
+ const launchHostContext = buildLaunchHostContext(params);
312
331
 
313
332
  if (!alias) {
314
333
  return { ok: false, error: "group run requires alias", status: "failed" };
@@ -377,6 +396,7 @@ function createGroupOrchestrator(options = {}) {
377
396
  agent: item.type,
378
397
  count: 1,
379
398
  nickname: item.nickname,
399
+ ...launchHostContext,
380
400
  };
381
401
 
382
402
  // eslint-disable-next-line no-await-in-loop
@@ -478,11 +478,15 @@ async function dispatchMessages(projectRoot, dispatch = []) {
478
478
  for (const item of dispatch) {
479
479
  if (!item || !item.target || !item.message) continue;
480
480
  const pub = item.publisher || defaultPublisher;
481
+ const sendOptions = {
482
+ injectionMode: item.injection_mode,
483
+ source: item.source,
484
+ };
481
485
  try {
482
486
  if (item.target === "broadcast") {
483
- await eventBus.broadcast(item.message, pub);
487
+ await eventBus.broadcast(item.message, pub, sendOptions);
484
488
  } else {
485
- await eventBus.send(item.target, item.message, pub);
489
+ await eventBus.send(item.target, item.message, pub, sendOptions);
486
490
  }
487
491
  } catch {
488
492
  // ignore dispatch failures
@@ -844,7 +848,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
844
848
  }
845
849
  if (req.type === IPC_REQUEST_TYPES.BUS_SEND) {
846
850
  // Direct bus send request from chat UI
847
- const { target, message } = req;
851
+ const { target, message, injection_mode, source } = req;
848
852
  if (!target || !message) {
849
853
  socket.write(
850
854
  `${JSON.stringify({
@@ -858,7 +862,10 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
858
862
  try {
859
863
  const publisher = busBridge.getSubscriber() || "ufoo-agent";
860
864
  const eventBus = new EventBus(projectRoot);
861
- await eventBus.send(target, message, publisher);
865
+ await eventBus.send(target, message, publisher, {
866
+ injectionMode: injection_mode,
867
+ source,
868
+ });
862
869
  busBridge.markPending(target);
863
870
  log(`bus_send target=${target} publisher=${publisher}`);
864
871
  socket.write(
@@ -1080,11 +1087,26 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1080
1087
  const alias = req.alias || req.template || "";
1081
1088
  const instance = req.instance || req.group_id || "";
1082
1089
  const dryRun = req.dry_run === true || req.dryRun === true;
1090
+ const hostInjectSock = req.host_inject_sock || req.hostInjectSock || "";
1091
+ const hostDaemonSock = req.host_daemon_sock || req.hostDaemonSock || "";
1092
+ const hostName = req.host_name || req.hostName || "";
1093
+ const hostSessionId = req.host_session_id || req.hostSessionId || "";
1094
+ const hostCapabilities =
1095
+ req.host_capabilities && typeof req.host_capabilities === "object"
1096
+ ? req.host_capabilities
1097
+ : ((req.hostCapabilities && typeof req.hostCapabilities === "object")
1098
+ ? req.hostCapabilities
1099
+ : null);
1083
1100
  try {
1084
1101
  const result = await daemonGroupOrchestrator.runGroup({
1085
1102
  alias,
1086
1103
  instance,
1087
1104
  dry_run: dryRun,
1105
+ host_inject_sock: hostInjectSock,
1106
+ host_daemon_sock: hostDaemonSock,
1107
+ host_name: hostName,
1108
+ host_session_id: hostSessionId,
1109
+ host_capabilities: hostCapabilities,
1088
1110
  });
1089
1111
  const ok = result && result.ok !== false;
1090
1112
  let reply = "";
@@ -6,13 +6,30 @@ const {
6
6
  consumeControllerInboxEntries,
7
7
  } = require("../report/store");
8
8
 
9
- function buildPromptWithPrivateReports(prompt = "", reports = []) {
9
+ function buildPromptWithPrivateReports(prompt = "", reports = [], requestMeta = {}) {
10
+ const meta = requestMeta && typeof requestMeta === "object" ? requestMeta : {};
11
+ const hasMeta = Object.keys(meta).length > 0;
10
12
  if (!Array.isArray(reports) || reports.length === 0) {
11
- return prompt;
13
+ if (!hasMeta) return prompt;
14
+ const lines = [];
15
+ lines.push(prompt || "");
16
+ lines.push("");
17
+ lines.push("Routing request metadata (JSON):");
18
+ lines.push(JSON.stringify(meta, null, 2));
19
+ lines.push("");
20
+ lines.push("Honor this metadata when choosing dispatch targets and injection_mode.");
21
+ return lines.join("\n");
12
22
  }
13
23
  const lines = [];
14
24
  lines.push(prompt || "");
15
25
  lines.push("");
26
+ if (hasMeta) {
27
+ lines.push("Routing request metadata (JSON):");
28
+ lines.push(JSON.stringify(meta, null, 2));
29
+ lines.push("");
30
+ lines.push("Honor this metadata when choosing dispatch targets and injection_mode.");
31
+ lines.push("");
32
+ }
16
33
  lines.push("Private runtime reports for ufoo-agent (JSON):");
17
34
  lines.push(JSON.stringify(reports, null, 2));
18
35
  lines.push("");
@@ -40,7 +57,7 @@ async function handlePromptRequest(options = {}) {
40
57
 
41
58
  log(`prompt ${String(req.text || "").slice(0, 200)}`);
42
59
  const privateReports = listControllerInboxEntries(projectRoot, "ufoo-agent", { num: 100 });
43
- const promptText = buildPromptWithPrivateReports(req.text || "", privateReports);
60
+ const promptText = buildPromptWithPrivateReports(req.text || "", privateReports, req.request_meta);
44
61
 
45
62
  try {
46
63
  const handled = await runPromptWithAssistant({