u-foo 1.7.3 → 1.7.5

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.5",
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,8 +274,8 @@ 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"}],',
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"}],',
277
+ ' "dispatch": [{"target":"broadcast|<agent-id>|<nickname>","message":"string","injection_mode":"immediate|queued (optional)","source":"optional"}],',
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,"at":"YYYY-MM-DD HH:mm","once_at_ms":1700000000000,"target":"agent-id|nickname|csv","targets":["agent-id"],"title":"optional short title","prompt":"message","id":"task-id|all"}],',
279
279
  ' "disambiguate": {"prompt":"string","candidates":[{"agent_id":"id","reason":"string"}]}',
280
280
  "}",
281
281
  "Rules:",
@@ -283,7 +283,7 @@ function buildSystemPrompt(context) {
283
283
  "- If multiple possible agents, use disambiguate with candidates and no dispatch.",
284
284
  "- If user specifies a nickname for a new agent, include ops.launch with nickname so daemon can rename.",
285
285
  "- If user requests rename, use ops.rename with agent_id and nickname (do NOT launch).",
286
- "- For scheduled follow-up (cron), use ops.cron with operation=start and include every+target(s)+prompt (or at for one-time).",
286
+ "- For scheduled follow-up (cron), use ops.cron with operation=start and include target(s)+prompt, plus optional title; use every/interval_ms for recurring or at/once_at_ms for one-time.",
287
287
  "- To check scheduled tasks, use ops.cron with operation=list.",
288
288
  "- To stop scheduled tasks, use ops.cron with operation=stop and id (or id=all).",
289
289
  "- Use top-level assistant_call for project exploration, temporary shell tasks, and quick execution support.",
@@ -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,
@@ -18,16 +18,11 @@ function resolveAssistantEngine({
18
18
  } = {}) {
19
19
  const config = loadConfig(projectRoot);
20
20
 
21
- const hasRequestedProvider = String(requestedProvider || "").trim().length > 0;
22
21
  const requested = normalizeAssistantEngine(requestedProvider);
23
- const configEngine = normalizeAssistantEngine(config.assistantEngine);
24
22
  const fallback = normalizeAssistantEngine(fallbackProvider) || "codex";
25
23
 
26
24
  let selected = requested;
27
- if (selected === "auto") {
28
- // Explicit assistant_call provider=auto should inherit current main agent provider.
29
- selected = hasRequestedProvider ? fallback : configEngine;
30
- }
25
+ // Omitted/auto assistant providers inherit the active ufoo-agent provider.
31
26
  if (selected === "auto") selected = fallback;
32
27
  if (selected === "auto") selected = "codex";
33
28
 
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
  }
@@ -665,6 +688,9 @@ function createCommandExecutor(options = {}) {
665
688
  const targetsRaw = String(
666
689
  kv.target || kv.targets || kv.agent || kv.agents || ""
667
690
  ).trim();
691
+ const title = String(
692
+ kv.title || kv.name || kv.label || ""
693
+ ).trim();
668
694
  const prompt = String(
669
695
  kv.prompt || kv.message || kv.msg || nonKvParts.join(" ") || ""
670
696
  ).trim();
@@ -672,7 +698,7 @@ function createCommandExecutor(options = {}) {
672
698
  if ((!intervalRaw && !atRaw) || !targetsRaw || !prompt) {
673
699
  logMessage(
674
700
  "error",
675
- "{white-fg}✗{/white-fg} Usage: /cron start every=<10s|5m> or at=\"YYYY-MM-DD HH:mm\" target=<agent1,agent2> prompt=\"...\""
701
+ "{white-fg}✗{/white-fg} Usage: /cron start every=<10s|5m> or at=\"YYYY-MM-DD HH:mm\" target=<agent1,agent2> [title=\"...\"] prompt=\"...\""
676
702
  );
677
703
  return;
678
704
  }
@@ -705,21 +731,18 @@ function createCommandExecutor(options = {}) {
705
731
  }
706
732
 
707
733
  if (typeof requestCron === "function") {
734
+ const request = {
735
+ operation: "start",
736
+ targets,
737
+ prompt,
738
+ };
739
+ if (title) request.title = title;
708
740
  if (atMs > 0) {
709
- requestCron({
710
- operation: "start",
711
- once_at_ms: atMs,
712
- targets,
713
- prompt,
714
- });
741
+ request.once_at_ms = atMs;
715
742
  } else {
716
- requestCron({
717
- operation: "start",
718
- interval_ms: intervalMs,
719
- targets,
720
- prompt,
721
- });
743
+ request.interval_ms = intervalMs;
722
744
  }
745
+ requestCron(request);
723
746
  schedule(requestStatus, 200);
724
747
  return;
725
748
  }
@@ -729,11 +752,13 @@ function createCommandExecutor(options = {}) {
729
752
  return;
730
753
  }
731
754
 
732
- const task = createCronTask({
755
+ const taskPayload = {
733
756
  intervalMs,
734
757
  targets,
735
758
  prompt,
736
- });
759
+ };
760
+ if (title) taskPayload.title = title;
761
+ const task = createCronTask(taskPayload);
737
762
  if (!task) {
738
763
  logMessage("error", "{white-fg}✗{/white-fg} Failed to create cron task");
739
764
  return;
@@ -741,7 +766,7 @@ function createCommandExecutor(options = {}) {
741
766
 
742
767
  logMessage(
743
768
  "system",
744
- `{white-fg}✓{/white-fg} Cron started ${task.id}: ${atMs > 0 ? `at ${formatCronAt(atMs)}` : `every ${formatIntervalMs(intervalMs)}`} -> ${targets.join(", ")}`
769
+ `{white-fg}✓{/white-fg} Cron started ${task.id}: ${task.label || `${atMs > 0 ? `at ${formatCronAt(atMs)}` : `every ${formatIntervalMs(intervalMs)}`} -> ${targets.join(", ")}`}`
745
770
  );
746
771
  }
747
772
 
@@ -30,7 +30,7 @@ const COMMAND_TREE = {
30
30
  "/cron": {
31
31
  desc: "Cron scheduler operations",
32
32
  children: {
33
- start: { desc: "Create cron task" },
33
+ start: { desc: "Create cron task (optional title)" },
34
34
  list: { desc: "List cron tasks" },
35
35
  stop: { desc: "Stop cron task by id or all" },
36
36
  },
@@ -29,13 +29,39 @@ function sanitizeSummaryText(value = "") {
29
29
  .trim();
30
30
  }
31
31
 
32
+ function truncateCronText(value = "", maxLength = 24) {
33
+ const text = String(value || "").trim();
34
+ if (!text) return "";
35
+ if (text.length <= maxLength) return text;
36
+ return `${text.slice(0, Math.max(1, maxLength - 3))}...`;
37
+ }
38
+
39
+ function resolveTaskTitle(task = {}) {
40
+ const explicitTitle = truncateCronText(
41
+ sanitizeSummaryText(task.title || "").replace(/:/g, "-"),
42
+ 24
43
+ );
44
+ if (explicitTitle) return explicitTitle;
45
+ const fallbackTitle = truncateCronText(
46
+ sanitizeSummaryText(task.prompt || "").replace(/:/g, "-"),
47
+ 24
48
+ );
49
+ return fallbackTitle || "(empty)";
50
+ }
51
+
52
+ function buildTaskLabel(task = {}) {
53
+ const targets = Array.isArray(task.targets) && task.targets.length > 0
54
+ ? task.targets.join("+")
55
+ : "unknown";
56
+ const title = resolveTaskTitle(task);
57
+ const interval = formatIntervalMs(task.intervalMs || 0);
58
+ return `${targets}:${title}:${interval}`;
59
+ }
60
+
32
61
  function summarizeTask(task = {}) {
33
62
  const id = String(task.id || "");
34
- const interval = formatIntervalMs(task.intervalMs || 0);
35
- const targets = Array.isArray(task.targets) ? task.targets.join("+") : "";
36
- const promptRaw = sanitizeSummaryText(task.prompt || "");
37
- const prompt = promptRaw.length > 24 ? `${promptRaw.slice(0, 24)}...` : promptRaw;
38
- return `${id}@${interval}->${targets}: ${prompt || "(empty)"}`;
63
+ const label = buildTaskLabel(task);
64
+ return id ? `${id} ${label}` : label;
39
65
  }
40
66
 
41
67
  function createCronScheduler(options = {}) {
@@ -58,7 +84,7 @@ function createCronScheduler(options = {}) {
58
84
  }
59
85
  }
60
86
 
61
- function addTask({ intervalMs = 0, targets = [], prompt = "" } = {}) {
87
+ function addTask({ intervalMs = 0, targets = [], prompt = "", title = "" } = {}) {
62
88
  const safeInterval = Number.parseInt(intervalMs, 10);
63
89
  const safeTargets = Array.isArray(targets)
64
90
  ? targets.map((item) => String(item || "").trim()).filter(Boolean)
@@ -74,6 +100,7 @@ function createCronScheduler(options = {}) {
74
100
  intervalMs: safeInterval,
75
101
  targets: Array.from(new Set(safeTargets)),
76
102
  prompt: safePrompt,
103
+ title: resolveTaskTitle({ title, prompt: safePrompt }),
77
104
  createdAt: nowFn(),
78
105
  lastRunAt: 0,
79
106
  tickCount: 0,
@@ -100,6 +127,7 @@ function createCronScheduler(options = {}) {
100
127
  notifyChange();
101
128
  return {
102
129
  ...task,
130
+ label: buildTaskLabel(task),
103
131
  summary: summarizeTask(task),
104
132
  };
105
133
  }
@@ -110,9 +138,11 @@ function createCronScheduler(options = {}) {
110
138
  intervalMs: task.intervalMs,
111
139
  targets: task.targets.slice(),
112
140
  prompt: task.prompt,
141
+ title: task.title,
113
142
  createdAt: task.createdAt,
114
143
  lastRunAt: task.lastRunAt,
115
144
  tickCount: task.tickCount,
145
+ label: buildTaskLabel(task),
116
146
  summary: summarizeTask(task),
117
147
  }));
118
148
  }
@@ -155,6 +185,7 @@ function createCronScheduler(options = {}) {
155
185
  module.exports = {
156
186
  parseIntervalMs,
157
187
  formatIntervalMs,
188
+ buildTaskLabel,
158
189
  summarizeTask,
159
190
  createCronScheduler,
160
191
  };
@@ -254,12 +254,12 @@ function createDaemonMessageRouter(options = {}) {
254
254
  if (task.mode === "once") {
255
255
  logMessage(
256
256
  "system",
257
- `{white-fg}✓{/white-fg} Cron scheduled ${escapeBlessed(task.id)} at ${escapeBlessed(task.onceAt || String(task.onceAtMs || ""))}`
257
+ `{white-fg}✓{/white-fg} Cron scheduled ${escapeBlessed(task.id)}: ${escapeBlessed(task.label || task.onceAt || String(task.onceAtMs || ""))}`
258
258
  );
259
259
  } else {
260
260
  logMessage(
261
261
  "system",
262
- `{white-fg}✓{/white-fg} Cron started ${escapeBlessed(task.id)}: every ${escapeBlessed(task.interval || String(task.intervalMs || ""))}`
262
+ `{white-fg}✓{/white-fg} Cron started ${escapeBlessed(task.id)}: ${escapeBlessed(task.label || task.interval || String(task.intervalMs || ""))}`
263
263
  );
264
264
  }
265
265
  } else if (operation === "stop") {
@@ -18,7 +18,6 @@ function createDashboardKeyController(options = {}) {
18
18
  exitDashboardMode = () => {},
19
19
  setLaunchMode = () => {},
20
20
  setAgentProvider = () => {},
21
- setAssistantEngine = () => {},
22
21
  setAutoResume = () => {},
23
22
  clampAgentWindow = () => {},
24
23
  clampAgentWindowWithSelection = () => {},
@@ -232,10 +231,7 @@ function createDashboardKeyController(options = {}) {
232
231
  }
233
232
 
234
233
  if (key.name === "down") {
235
- state.dashboardView = "assistant";
236
- const list = Array.isArray(state.assistantOptions) ? state.assistantOptions : [];
237
- const nextIndex = list.findIndex((opt) => opt.value === state.assistantEngine);
238
- state.selectedAssistantIndex = nextIndex >= 0 ? nextIndex : 0;
234
+ state.dashboardView = "cron";
239
235
  renderDashboardAndScreen();
240
236
  return true;
241
237
  }
@@ -261,67 +257,9 @@ function createDashboardKeyController(options = {}) {
261
257
  return true;
262
258
  }
263
259
 
264
- function handleAssistantKey(key) {
265
- const options = Array.isArray(state.assistantOptions) ? state.assistantOptions : [];
266
- if (options.length === 0) {
267
- if (key.name === "up") {
268
- state.dashboardView = "provider";
269
- renderDashboardAndScreen();
270
- return true;
271
- }
272
- if (key.name === "escape" || key.name === "enter" || key.name === "return") {
273
- exitDashboardMode(false);
274
- return true;
275
- }
276
- return true;
277
- }
278
-
279
- if (key.name === "left") {
280
- state.selectedAssistantIndex = state.selectedAssistantIndex <= 0
281
- ? options.length - 1
282
- : state.selectedAssistantIndex - 1;
283
- renderDashboardAndScreen();
284
- return true;
285
- }
286
-
287
- if (key.name === "right") {
288
- state.selectedAssistantIndex = state.selectedAssistantIndex >= options.length - 1
289
- ? 0
290
- : state.selectedAssistantIndex + 1;
291
- renderDashboardAndScreen();
292
- return true;
293
- }
294
-
295
- if (key.name === "up") {
296
- state.dashboardView = "provider";
297
- renderDashboardAndScreen();
298
- return true;
299
- }
300
-
301
- if (key.name === "down") {
302
- state.dashboardView = "cron";
303
- renderDashboardAndScreen();
304
- return true;
305
- }
306
-
307
- if (key.name === "enter" || key.name === "return") {
308
- const selected = options[state.selectedAssistantIndex];
309
- if (selected) setAssistantEngine(selected.value);
310
- exitDashboardMode(false);
311
- return true;
312
- }
313
-
314
- if (key.name === "escape") {
315
- exitDashboardMode(false);
316
- return true;
317
- }
318
-
319
- return true;
320
- }
321
-
322
260
  function handleCronKey(key) {
323
261
  if (key.name === "up") {
324
- state.dashboardView = "assistant";
262
+ state.dashboardView = "provider";
325
263
  renderDashboardAndScreen();
326
264
  return true;
327
265
  }
@@ -552,7 +490,6 @@ function createDashboardKeyController(options = {}) {
552
490
 
553
491
  if (state.dashboardView === "mode") return handleModeKey(key);
554
492
  if (state.dashboardView === "provider") return handleProviderKey(key);
555
- if (state.dashboardView === "assistant") return handleAssistantKey(key);
556
493
  if (state.dashboardView === "resume") return handleResumeKey(key);
557
494
  if (state.dashboardView === "cron") return handleCronKey(key);
558
495
 
@@ -8,14 +8,6 @@ function providerLabel(value) {
8
8
  return "codex";
9
9
  }
10
10
 
11
- function assistantLabel(value) {
12
- if (value === "codex") return "codex";
13
- if (value === "claude") return "claude";
14
- if (value === "ufoo") return "ucode";
15
- if (value === "ucode") return "ucode";
16
- return "auto";
17
- }
18
-
19
11
  function ensureAtPrefix(value) {
20
12
  const text = String(value || "").trim();
21
13
  if (!text) return text;
@@ -43,7 +35,6 @@ function buildSummaryLine(options = {}) {
43
35
  getAgentState = () => "",
44
36
  launchMode = "terminal",
45
37
  agentProvider = "codex-cli",
46
- assistantEngine = "auto",
47
38
  cronTasks = [],
48
39
  } = options;
49
40
  const agents = activeAgents.length > 0
@@ -55,7 +46,6 @@ function buildSummaryLine(options = {}) {
55
46
  return `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`
56
47
  + ` {gray-fg}Mode:{/gray-fg} {cyan-fg}${launchMode}{/cyan-fg}`
57
48
  + ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}`
58
- + ` {gray-fg}Assistant:{/gray-fg} {cyan-fg}${assistantLabel(assistantEngine)}{/cyan-fg}`
59
49
  + ` {gray-fg}Cron:{/gray-fg} {cyan-fg}${Array.isArray(cronTasks) ? cronTasks.length : 0}{/cyan-fg}`;
60
50
  }
61
51
 
@@ -134,11 +124,9 @@ function buildDashboardDetailLine(options = {}) {
134
124
  getAgentState = () => "",
135
125
  selectedModeIndex = 0,
136
126
  selectedProviderIndex = 0,
137
- selectedAssistantIndex = 0,
138
127
  selectedResumeIndex = 0,
139
128
  cronTasks = [],
140
129
  providerOptions = [],
141
- assistantOptions = [],
142
130
  resumeOptions = [],
143
131
  dashHints = {},
144
132
  modeOptions = DEFAULT_MODE_OPTIONS,
@@ -171,18 +159,6 @@ function buildDashboardDetailLine(options = {}) {
171
159
  return { content, windowStart };
172
160
  }
173
161
 
174
- if (dashboardView === "assistant") {
175
- const assistantParts = assistantOptions.map((opt, i) => {
176
- if (i === selectedAssistantIndex) {
177
- return `{inverse}${opt.label}{/inverse}`;
178
- }
179
- return `{cyan-fg}${opt.label}{/cyan-fg}`;
180
- });
181
- content += `{gray-fg}Assistant:{/gray-fg} ${assistantParts.join(" ")}`;
182
- content += ` {gray-fg}│ ${dashHints.assistant || ""}{/gray-fg}`;
183
- return { content, windowStart };
184
- }
185
-
186
162
  if (dashboardView === "resume") {
187
163
  const resumeParts = resumeOptions.map((opt, i) => {
188
164
  if (i === selectedResumeIndex) {
@@ -198,9 +174,9 @@ function buildDashboardDetailLine(options = {}) {
198
174
  if (dashboardView === "cron") {
199
175
  const items = Array.isArray(cronTasks) ? cronTasks : [];
200
176
  const summary = items.length > 0
201
- ? items.map((item) => item.summary || item.id || "").filter(Boolean).join(", ")
177
+ ? items.map((item) => item.label || item.summary || item.id || "").filter(Boolean).join(", ")
202
178
  : "none";
203
- content += `{gray-fg}Cron:{/gray-fg} {cyan-fg}${summary}{/cyan-fg}`;
179
+ content += `{gray-fg}Cron:{/gray-fg} {inverse}${summary}{/inverse}`;
204
180
  content += ` {gray-fg}│ ${dashHints.cron || ""}{/gray-fg}`;
205
181
  return { content, windowStart };
206
182
  }
@@ -259,14 +235,11 @@ function computeDashboardContent(options = {}) {
259
235
  getAgentState = () => "",
260
236
  launchMode = "terminal",
261
237
  agentProvider = "codex-cli",
262
- assistantEngine = "auto",
263
238
  selectedModeIndex = 0,
264
239
  selectedProviderIndex = 0,
265
- selectedAssistantIndex = 0,
266
240
  selectedResumeIndex = 0,
267
241
  cronTasks = [],
268
242
  providerOptions = [],
269
- assistantOptions = [],
270
243
  resumeOptions = [],
271
244
  dashHints = {},
272
245
  modeOptions = DEFAULT_MODE_OPTIONS,
@@ -297,7 +270,6 @@ function computeDashboardContent(options = {}) {
297
270
  getAgentState,
298
271
  launchMode,
299
272
  agentProvider,
300
- assistantEngine,
301
273
  cronTasks,
302
274
  });
303
275
  return {
@@ -317,11 +289,9 @@ function computeDashboardContent(options = {}) {
317
289
  getAgentState,
318
290
  selectedModeIndex,
319
291
  selectedProviderIndex,
320
- selectedAssistantIndex,
321
292
  selectedResumeIndex,
322
293
  cronTasks,
323
294
  providerOptions,
324
- assistantOptions,
325
295
  resumeOptions,
326
296
  dashHints,
327
297
  modeOptions,
@@ -344,11 +314,9 @@ function computeDashboardContent(options = {}) {
344
314
  getAgentState,
345
315
  selectedModeIndex,
346
316
  selectedProviderIndex,
347
- selectedAssistantIndex,
348
317
  selectedResumeIndex,
349
318
  cronTasks,
350
319
  providerOptions,
351
- assistantOptions,
352
320
  resumeOptions,
353
321
  dashHints,
354
322
  modeOptions,
@@ -362,7 +330,6 @@ function computeDashboardContent(options = {}) {
362
330
  getAgentState,
363
331
  launchMode,
364
332
  agentProvider,
365
- assistantEngine,
366
333
  cronTasks,
367
334
  });
368
335
 
@@ -372,5 +339,4 @@ function computeDashboardContent(options = {}) {
372
339
  module.exports = {
373
340
  computeDashboardContent,
374
341
  providerLabel,
375
- assistantLabel,
376
342
  };
package/src/chat/index.js CHANGED
@@ -9,7 +9,6 @@ const {
9
9
  saveConfig,
10
10
  normalizeLaunchMode,
11
11
  normalizeAgentProvider,
12
- normalizeAssistantEngine,
13
12
  } = require("../config");
14
13
  const { socketPath, isRunning } = require("../daemon");
15
14
  const UfooInit = require("../init");
@@ -104,7 +103,6 @@ async function runChat(projectRoot, options = {}) {
104
103
  const config = loadConfig(projectRoot);
105
104
  let launchMode = config.launchMode;
106
105
  let agentProvider = config.agentProvider;
107
- let assistantEngine = normalizeAssistantEngine(config.assistantEngine);
108
106
  let autoResume = config.autoResume !== false;
109
107
  let cronTasks = [];
110
108
 
@@ -316,6 +314,8 @@ async function runChat(projectRoot, options = {}) {
316
314
  type: IPC_REQUEST_TYPES.BUS_SEND,
317
315
  target,
318
316
  message: JSON.stringify({ raw: true, data }),
317
+ injection_mode: "immediate",
318
+ source: "chat-agent-view",
319
319
  });
320
320
  },
321
321
  });
@@ -673,7 +673,7 @@ async function runChat(projectRoot, options = {}) {
673
673
  let selectedAgentIndex = -1; // -1 = not in dashboard selection mode
674
674
  let targetAgent = null; // Selected agent for direct messaging
675
675
  let focusMode = "input"; // "input" or "dashboard"
676
- let dashboardView = "agents"; // "projects" | "agents" | "mode" | "provider" | "assistant" | "cron"
676
+ let dashboardView = "agents"; // "projects" | "agents" | "mode" | "provider" | "cron"
677
677
  let reportPendingTotal = 0;
678
678
  let selectedModeIndex = Math.max(0, MODE_OPTIONS.indexOf(launchMode));
679
679
  const providerOptions = [
@@ -682,16 +682,6 @@ async function runChat(projectRoot, options = {}) {
682
682
  { label: "ucode", value: "ucode" },
683
683
  ];
684
684
  let selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
685
- const assistantOptions = [
686
- { label: "auto", value: "auto" },
687
- { label: "codex", value: "codex" },
688
- { label: "claude", value: "claude" },
689
- { label: "ucode", value: "ufoo" },
690
- ];
691
- let selectedAssistantIndex = Math.max(
692
- 0,
693
- assistantOptions.findIndex((opt) => opt.value === assistantEngine)
694
- );
695
685
  const resumeOptions = [
696
686
  { label: "Resume previous session", value: true },
697
687
  { label: "Start new session", value: false },
@@ -702,8 +692,7 @@ async function runChat(projectRoot, options = {}) {
702
692
  agentsGlobal: "←/→ select · Enter · ↓ mode · ↑ projects",
703
693
  agentsEmpty: "↓ mode · ↑ back",
704
694
  mode: "←/→ select · Enter · ↓ provider · ↑ back",
705
- provider: "←/→ select · Enter · ↓ assistant · ↑ back",
706
- assistant: "←/→ select · Enter · ↓ cron · ↑ back",
695
+ provider: "←/→ select · Enter · ↓ cron · ↑ back",
707
696
  cron: "Ctrl+X close · ↑ back",
708
697
  resume: "",
709
698
  projects: "Use /project switch <index|path>",
@@ -1079,12 +1068,6 @@ async function runChat(projectRoot, options = {}) {
1079
1068
  }
1080
1069
  }
1081
1070
 
1082
- function setAssistantEngine(value) {
1083
- if (settingsController) {
1084
- settingsController.setAssistantEngine(value);
1085
- }
1086
- }
1087
-
1088
1071
  function setAutoResume(value) {
1089
1072
  if (settingsController) {
1090
1073
  settingsController.setAutoResume(value);
@@ -1101,7 +1084,6 @@ async function runChat(projectRoot, options = {}) {
1101
1084
  saveConfig,
1102
1085
  normalizeLaunchMode,
1103
1086
  normalizeAgentProvider,
1104
- normalizeAssistantEngine,
1105
1087
  fsModule: fs,
1106
1088
  getUfooPaths,
1107
1089
  logMessage,
@@ -1122,14 +1104,6 @@ async function runChat(projectRoot, options = {}) {
1122
1104
  setSelectedProviderIndex: (value) => {
1123
1105
  selectedProviderIndex = value;
1124
1106
  },
1125
- getAssistantEngine: () => assistantEngine,
1126
- setAssistantEngineState: (value) => {
1127
- assistantEngine = value;
1128
- },
1129
- setSelectedAssistantIndex: (value) => {
1130
- selectedAssistantIndex = value;
1131
- },
1132
- assistantOptions,
1133
1107
  providerOptions,
1134
1108
  modeOptions: MODE_OPTIONS,
1135
1109
  getAutoResume: () => autoResume,
@@ -1178,15 +1152,12 @@ async function runChat(projectRoot, options = {}) {
1178
1152
  },
1179
1153
  launchMode,
1180
1154
  agentProvider,
1181
- assistantEngine,
1182
1155
  autoResume,
1183
1156
  selectedModeIndex,
1184
1157
  selectedProviderIndex,
1185
- selectedAssistantIndex,
1186
1158
  selectedResumeIndex,
1187
1159
  cronTasks,
1188
1160
  providerOptions,
1189
- assistantOptions,
1190
1161
  resumeOptions,
1191
1162
  pendingReports: reportPendingTotal,
1192
1163
  dashHints: DASH_HINTS,
@@ -1335,10 +1306,6 @@ async function runChat(projectRoot, options = {}) {
1335
1306
  }
1336
1307
  selectedModeIndex = Math.max(0, MODE_OPTIONS.indexOf(launchMode));
1337
1308
  selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
1338
- selectedAssistantIndex = Math.max(
1339
- 0,
1340
- assistantOptions.findIndex((opt) => opt.value === assistantEngine)
1341
- );
1342
1309
  selectedResumeIndex = autoResume ? 0 : 1;
1343
1310
  // Immediately set @target when first agent is selected.
1344
1311
  if (!globalMode && selectedAgentIndex >= 0 && selectedAgentIndex < activeAgents.length) {
@@ -1366,15 +1333,12 @@ async function runChat(projectRoot, options = {}) {
1366
1333
  activeAgentMetaMap: { get: () => activeAgentMetaMap },
1367
1334
  selectedModeIndex: { get: () => selectedModeIndex, set: (value) => { selectedModeIndex = value; } },
1368
1335
  selectedProviderIndex: { get: () => selectedProviderIndex, set: (value) => { selectedProviderIndex = value; } },
1369
- selectedAssistantIndex: { get: () => selectedAssistantIndex, set: (value) => { selectedAssistantIndex = value; } },
1370
1336
  selectedResumeIndex: { get: () => selectedResumeIndex, set: (value) => { selectedResumeIndex = value; } },
1371
1337
  launchMode: { get: () => launchMode },
1372
1338
  agentProvider: { get: () => agentProvider },
1373
- assistantEngine: { get: () => assistantEngine },
1374
1339
  autoResume: { get: () => autoResume },
1375
1340
  cronTasks: { get: () => cronTasks },
1376
1341
  providerOptions: { get: () => providerOptions },
1377
- assistantOptions: { get: () => assistantOptions },
1378
1342
  resumeOptions: { get: () => resumeOptions },
1379
1343
  agentOutputSuppressed: {
1380
1344
  get: () => getAgentOutputSuppressed(),
@@ -1413,7 +1377,6 @@ async function runChat(projectRoot, options = {}) {
1413
1377
  exitDashboardMode,
1414
1378
  setLaunchMode,
1415
1379
  setAgentProvider,
1416
- setAssistantEngine,
1417
1380
  setAutoResume,
1418
1381
  clampAgentWindow,
1419
1382
  clampAgentWindowWithSelection,
@@ -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
  }
@@ -6,7 +6,6 @@ function createSettingsController(options = {}) {
6
6
  saveConfig = () => {},
7
7
  normalizeLaunchMode = (value) => value,
8
8
  normalizeAgentProvider = (value) => value,
9
- normalizeAssistantEngine = (value) => value,
10
9
  fsModule,
11
10
  getUfooPaths = () => ({ agentDir: "" }),
12
11
  logMessage = () => {},
@@ -19,10 +18,6 @@ function createSettingsController(options = {}) {
19
18
  getAgentProvider = () => "codex-cli",
20
19
  setAgentProviderState = () => {},
21
20
  setSelectedProviderIndex = () => {},
22
- getAssistantEngine = () => "auto",
23
- setAssistantEngineState = () => {},
24
- setSelectedAssistantIndex = () => {},
25
- assistantOptions = [],
26
21
  providerOptions = [],
27
22
  modeOptions = [],
28
23
  getAutoResume = () => true,
@@ -41,14 +36,6 @@ function createSettingsController(options = {}) {
41
36
  return value === "claude-cli" ? "claude" : "codex";
42
37
  }
43
38
 
44
- function assistantLabel(value) {
45
- const normalized = normalizeAssistantEngine(value);
46
- if (normalized === "codex") return "codex";
47
- if (normalized === "claude") return "claude";
48
- if (normalized === "ufoo") return "ufoo";
49
- return "auto";
50
- }
51
-
52
39
  function clearUfooAgentIdentity() {
53
40
  const agentDir = getUfooPaths(projectRoot).agentDir;
54
41
  const stateFile = path.join(agentDir, "ufoo-agent.json");
@@ -106,26 +93,11 @@ function createSettingsController(options = {}) {
106
93
  return true;
107
94
  }
108
95
 
109
- function setAssistantEngine(engine) {
110
- const next = normalizeAssistantEngine(engine);
111
- if (next === getAssistantEngine()) return false;
112
- setAssistantEngineState(next);
113
- const idx = assistantOptions.findIndex((opt) => opt && opt.value === next);
114
- setSelectedAssistantIndex(idx >= 0 ? idx : 0);
115
- saveConfig(projectRoot, { assistantEngine: next });
116
- logMessage("status", `{white-fg}⚙{/white-fg} assistant-engine: ${assistantLabel(next)}`);
117
- renderDashboard();
118
- renderScreen();
119
- return true;
120
- }
121
-
122
96
  return {
123
97
  providerLabel,
124
- assistantLabel,
125
98
  clearUfooAgentIdentity,
126
99
  setLaunchMode,
127
100
  setAgentProvider,
128
- setAssistantEngine,
129
101
  setAutoResume,
130
102
  };
131
103
  }
@@ -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":
@@ -128,18 +128,41 @@ function sanitizeSummaryText(value = "") {
128
128
  .trim();
129
129
  }
130
130
 
131
- function summarizeCronTask(task = {}) {
132
- const id = String(task.id || "");
133
- const targets = Array.isArray(task.targets) ? task.targets.join("+") : "";
134
- const promptRaw = sanitizeSummaryText(task.prompt || "");
135
- const prompt = promptRaw.length > 24 ? `${promptRaw.slice(0, 24)}...` : promptRaw;
131
+ function truncateCronText(value = "", maxLength = 24) {
132
+ const text = String(value || "").trim();
133
+ if (!text) return "";
134
+ if (text.length <= maxLength) return text;
135
+ return `${text.slice(0, Math.max(1, maxLength - 3))}...`;
136
+ }
136
137
 
137
- if (Number(task.onceAtMs) > 0) {
138
- return `${id}@once(${formatCronAtMs(task.onceAtMs)})->${targets}: ${prompt || "(empty)"}`;
139
- }
138
+ function normalizeCronTitle(value = "", prompt = "") {
139
+ const explicitTitle = truncateCronText(
140
+ sanitizeSummaryText(value || "").replace(/:/g, "-"),
141
+ 24
142
+ );
143
+ if (explicitTitle) return explicitTitle;
144
+ const fallbackTitle = truncateCronText(
145
+ sanitizeSummaryText(prompt || "").replace(/:/g, "-"),
146
+ 24
147
+ );
148
+ return fallbackTitle || "(empty)";
149
+ }
140
150
 
141
- const interval = formatIntervalMs(task.intervalMs || 0);
142
- return `${id}@${interval}->${targets}: ${prompt || "(empty)"}`;
151
+ function buildCronLabel(task = {}) {
152
+ const targets = Array.isArray(task.targets) && task.targets.length > 0
153
+ ? task.targets.join("+")
154
+ : "unknown";
155
+ const title = normalizeCronTitle(task.title || "", task.prompt || "");
156
+ const schedule = Number(task.onceAtMs) > 0
157
+ ? formatCronAtMs(task.onceAtMs)
158
+ : formatIntervalMs(task.intervalMs || 0);
159
+ return `${targets}:${title}:${schedule || "0s"}`;
160
+ }
161
+
162
+ function summarizeCronTask(task = {}) {
163
+ const id = String(task.id || "");
164
+ const label = buildCronLabel(task);
165
+ return id ? `${id} ${label}` : label;
143
166
  }
144
167
 
145
168
  function formatCronTask(task = {}) {
@@ -153,6 +176,8 @@ function formatCronTask(task = {}) {
153
176
  onceAt: onceAtMs > 0 ? formatCronAtMs(onceAtMs) : "",
154
177
  targets: Array.isArray(task.targets) ? task.targets.slice() : [],
155
178
  prompt: String(task.prompt || ""),
179
+ title: normalizeCronTitle(task.title || "", task.prompt || ""),
180
+ label: buildCronLabel(task),
156
181
  createdAt: Number(task.createdAt) || 0,
157
182
  lastRunAt: Number(task.lastRunAt) || 0,
158
183
  tickCount: Number(task.tickCount) || 0,
@@ -160,6 +185,10 @@ function formatCronTask(task = {}) {
160
185
  };
161
186
  }
162
187
 
188
+ function resolveCronTitle(op = {}) {
189
+ return String(op.title || op.name || op.label || "").trim();
190
+ }
191
+
163
192
  function createDaemonCronController(options = {}) {
164
193
  const {
165
194
  projectRoot = "",
@@ -198,6 +227,7 @@ function createDaemonCronController(options = {}) {
198
227
  onceAtMs: task.onceAtMs,
199
228
  targets: task.targets.slice(),
200
229
  prompt: task.prompt,
230
+ title: task.title,
201
231
  createdAt: task.createdAt,
202
232
  lastRunAt: task.lastRunAt,
203
233
  tickCount: task.tickCount,
@@ -275,13 +305,14 @@ function createDaemonCronController(options = {}) {
275
305
  }, task.intervalMs);
276
306
  }
277
307
 
278
- function addTask({ intervalMs = 0, onceAtMs = 0, targets = [], prompt = "" } = {}) {
308
+ function addTask({ intervalMs = 0, onceAtMs = 0, targets = [], prompt = "", title = "" } = {}) {
279
309
  const safeInterval = Number.parseInt(intervalMs, 10);
280
310
  const safeOnceAt = Number.parseInt(onceAtMs, 10);
281
311
  const safeTargets = Array.isArray(targets)
282
312
  ? targets.map((item) => String(item || "").trim()).filter(Boolean)
283
313
  : [];
284
314
  const safePrompt = String(prompt || "").trim();
315
+ const safeTitle = normalizeCronTitle(title, safePrompt);
285
316
 
286
317
  if (!safePrompt || safeTargets.length === 0) return null;
287
318
 
@@ -296,6 +327,7 @@ function createDaemonCronController(options = {}) {
296
327
  onceAtMs: useOnce ? safeOnceAt : 0,
297
328
  targets: Array.from(new Set(safeTargets)),
298
329
  prompt: safePrompt,
330
+ title: safeTitle,
299
331
  createdAt: nowFn(),
300
332
  lastRunAt: 0,
301
333
  tickCount: 0,
@@ -367,6 +399,7 @@ function createDaemonCronController(options = {}) {
367
399
  ? item.targets.map((v) => String(v || "").trim()).filter(Boolean)
368
400
  : [];
369
401
  const prompt = String(item && item.prompt ? item.prompt : "").trim();
402
+ const title = normalizeCronTitle(item && item.title ? item.title : "", prompt);
370
403
 
371
404
  if (!prompt || targets.length === 0) {
372
405
  changed = true;
@@ -389,6 +422,7 @@ function createDaemonCronController(options = {}) {
389
422
  onceAtMs: Number.isFinite(onceAtMs) ? Math.floor(onceAtMs) : 0,
390
423
  targets: Array.from(new Set(targets)),
391
424
  prompt,
425
+ title,
392
426
  createdAt: Number(item && item.createdAt) || now,
393
427
  lastRunAt: Number(item && item.lastRunAt) || 0,
394
428
  tickCount: Number(item && item.tickCount) || 0,
@@ -535,6 +569,7 @@ function createDaemonCronController(options = {}) {
535
569
  onceAtMs,
536
570
  targets,
537
571
  prompt,
572
+ title: resolveCronTitle(op),
538
573
  });
539
574
 
540
575
  if (!task) {
@@ -567,8 +602,10 @@ module.exports = {
567
602
  resolveCronOperation,
568
603
  resolveCronIntervalMs,
569
604
  resolveCronOnceAtMs,
605
+ resolveCronTitle,
570
606
  resolveCronPrompt,
571
607
  resolveCronTaskId,
572
608
  parseCronAtMs,
609
+ buildCronLabel,
573
610
  formatCronTask,
574
611
  };
@@ -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(
@@ -908,9 +915,9 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
908
915
  }
909
916
  } else if (result.operation === "start" && result.task) {
910
917
  if (result.task.mode === "once") {
911
- reply = `Cron scheduled ${result.task.id} at ${result.task.onceAt || result.task.onceAtMs}`;
918
+ reply = `Cron scheduled ${result.task.id}: ${result.task.label || result.task.onceAt || result.task.onceAtMs}`;
912
919
  } else {
913
- reply = `Cron started ${result.task.id}: every ${result.task.interval || result.task.intervalMs}`;
920
+ reply = `Cron started ${result.task.id}: ${result.task.label || result.task.interval || result.task.intervalMs}`;
914
921
  }
915
922
  } else {
916
923
  reply = "Cron updated";
@@ -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({
@@ -72,6 +72,8 @@ function normalizeCronTasks(raw = []) {
72
72
  onceAt: String(task && task.onceAt ? task.onceAt : ""),
73
73
  targets: Array.isArray(task && task.targets) ? task.targets.slice() : [],
74
74
  prompt: String(task && task.prompt ? task.prompt : ""),
75
+ title: String(task && task.title ? task.title : ""),
76
+ label: String(task && task.label ? task.label : ""),
75
77
  summary: String(task && task.summary ? task.summary : ""),
76
78
  createdAt: Number(task && task.createdAt ? task.createdAt : 0) || 0,
77
79
  lastRunAt: Number(task && task.lastRunAt ? task.lastRunAt : 0) || 0,