u-foo 1.7.3 → 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.3",
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
@@ -5,6 +5,15 @@ 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}`);
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
+ };
@@ -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
  }
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":
@@ -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(
@@ -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({