u-foo 2.3.2 → 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.3.2",
3
+ "version": "2.3.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",
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { executeControllerTool } = require("./controllerToolExecutor");
4
4
  const { createLoopObserver } = require("./loopObservability");
5
+ const { finalizeRouterPayload } = require("../controller/routerFinalize");
5
6
 
6
7
  const DEFAULT_LOOP_OPTIONS = {
7
8
  enabled: false,
@@ -126,34 +127,23 @@ function buildLoopContinuationPrompt({
126
127
  async function finalizeLoopRun({
127
128
  projectRoot,
128
129
  payload,
130
+ prompt = "",
129
131
  processManager,
130
132
  dispatchMessages,
131
133
  handleOps,
132
134
  markPending,
133
135
  finalizeLocally = true,
134
136
  }) {
135
- if (finalizeLocally === false) {
136
- return { ok: true, payload, opsResults: [] };
137
- }
138
-
139
- const dispatch = Array.isArray(payload.dispatch) ? payload.dispatch : [];
140
- const ops = Array.isArray(payload.ops) ? payload.ops : [];
141
-
142
- for (const item of dispatch) {
143
- if (item && item.target && item.target !== "broadcast" && typeof markPending === "function") {
144
- markPending(item.target);
145
- }
146
- }
147
-
148
- if (typeof dispatchMessages === "function") {
149
- await dispatchMessages(projectRoot, dispatch);
150
- }
151
-
152
- const opsResults = typeof handleOps === "function"
153
- ? await handleOps(projectRoot, ops, processManager || null)
154
- : [];
155
-
156
- return { ok: true, payload, opsResults };
137
+ return finalizeRouterPayload({
138
+ projectRoot,
139
+ payload,
140
+ prompt,
141
+ processManager: processManager || null,
142
+ dispatchMessages,
143
+ handleOps,
144
+ markPending,
145
+ finalizeLocally,
146
+ });
157
147
  }
158
148
 
159
149
  function buildTerminalPayload(reason, lastPayload, rounds, toolCalls, toolErrors, totals = {}) {
@@ -248,6 +238,7 @@ async function runPromptWithControllerLoop({
248
238
  return finalizeLoopRun({
249
239
  projectRoot,
250
240
  payload,
241
+ prompt: currentPrompt,
251
242
  processManager,
252
243
  dispatchMessages,
253
244
  handleOps,
@@ -261,6 +252,7 @@ async function runPromptWithControllerLoop({
261
252
  return finalizeLoopRun({
262
253
  projectRoot,
263
254
  payload,
255
+ prompt: currentPrompt,
264
256
  processManager,
265
257
  dispatchMessages,
266
258
  handleOps,
@@ -357,6 +349,7 @@ async function runPromptWithControllerLoop({
357
349
  return finalizeLoopRun({
358
350
  projectRoot,
359
351
  payload: finalPayload,
352
+ prompt: currentPrompt,
360
353
  processManager,
361
354
  dispatchMessages,
362
355
  handleOps,
@@ -370,6 +363,7 @@ async function runPromptWithControllerLoop({
370
363
  return finalizeLoopRun({
371
364
  projectRoot,
372
365
  payload: finalPayload,
366
+ prompt: currentPrompt,
373
367
  processManager,
374
368
  dispatchMessages,
375
369
  handleOps,
@@ -430,6 +424,7 @@ async function runPromptWithControllerLoop({
430
424
  return finalizeLoopRun({
431
425
  projectRoot,
432
426
  payload: finalPayload,
427
+ prompt: currentPrompt,
433
428
  processManager,
434
429
  dispatchMessages,
435
430
  handleOps,
@@ -456,6 +451,7 @@ async function runPromptWithControllerLoop({
456
451
  return finalizeLoopRun({
457
452
  projectRoot,
458
453
  payload,
454
+ prompt: currentPrompt,
459
455
  processManager,
460
456
  dispatchMessages,
461
457
  handleOps,
@@ -9,6 +9,7 @@ const { normalizeProvider, sendUpstreamPrompt } = require("./upstreamTransport")
9
9
  const { normalizeAgentTypeAlias } = require("../bus/utils");
10
10
  const { buildCachedMemoryPrefix } = require("../memory");
11
11
  const { listProjectRuntimes, isGlobalControllerProjectRoot } = require("../projects");
12
+ const { assignMissingLaunchNicknames } = require("../controller/launchRouting");
12
13
  const {
13
14
  CONTROLLER_MODES,
14
15
  resolveControllerMode,
@@ -462,6 +463,8 @@ function buildSystemPrompt(context, options = {}) {
462
463
  "- When returning tool_call, set done=false and keep dispatch/ops empty for that round.",
463
464
  "- Use dispatch_message for direct bus delivery, ack_bus for controller queue acknowledgement, and launch_agent for bounded worker launches.",
464
465
  "- When you have enough information, omit tool_call and return the final reply/dispatch/ops with done=true.",
466
+ "- When launching a new coding agent for a user task, include a short task-specific nickname and include a dispatch to that launched nickname with the task.",
467
+ "- Do not emit launch-only ops for delegated work; a launched worker must receive a task in dispatch.",
465
468
  "- Do not emit assistant_call or ops.assistant_call; that legacy helper path has been removed.",
466
469
  `- Round budget: maxRounds=${loopRuntime.maxRounds || ""}, remainingToolCalls=${loopRuntime.remainingToolCalls || 0}.`,
467
470
  agentGuidance,
@@ -505,6 +508,8 @@ function buildSystemPrompt(context, options = {}) {
505
508
  "- For short-lived exploration, prefer a dispatch to an online agent or reply with a clarification.",
506
509
  "- Primary routing signal is semantic continuity from agent_prompt_history; prefer the agent that already handled similar prompts.",
507
510
  "- Launch a new coding agent when the request is a new topic without clear ownership in existing histories.",
511
+ "- When launching a new coding agent for a user task, include a short task-specific nickname and include a dispatch to that launched nickname with the task.",
512
+ "- Do not emit launch-only ops for delegated work; a launched worker must receive a task in dispatch.",
508
513
  "- dispatch.injection_mode defaults to immediate when omitted.",
509
514
  "- 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.",
510
515
  "- If the new request strongly continues the target agent's recent prompt history, keep injection_mode immediate even when that agent is busy.",
@@ -588,21 +593,6 @@ function buildRouteAgentSystemPrompt(context, options = {}) {
588
593
  ].join("\n");
589
594
  }
590
595
 
591
- function extractNickname(prompt) {
592
- if (!prompt) return "";
593
- const patterns = [
594
- /(?:叫|名为|叫做|取名|昵称)\s*([A-Za-z0-9_-]{1,32})/i,
595
- /(?:named|name)\s+([A-Za-z0-9_-]{1,32})/i,
596
- ];
597
- for (const re of patterns) {
598
- const match = prompt.match(re);
599
- if (match && match[1]) return match[1];
600
- }
601
- const quoted = prompt.match(/[“"']([^“"'\\]{1,32})[”"']/);
602
- if (quoted && quoted[1]) return quoted[1];
603
- return "";
604
- }
605
-
606
596
  function shouldUseDirectProvider(value = "") {
607
597
  const provider = normalizeProvider(value);
608
598
  return provider === "ucode" || provider === "codex" || provider === "claude";
@@ -733,15 +723,7 @@ async function runUfooAgent({
733
723
  payload = { reply: text, dispatch: [], ops: [] };
734
724
  }
735
725
 
736
- const fallbackNickname = extractNickname(prompt);
737
- if (fallbackNickname && payload && Array.isArray(payload.ops)) {
738
- for (const op of payload.ops) {
739
- if (op && (op.action === "launch" || op.action === "rename") && !op.nickname) {
740
- op.nickname = fallbackNickname;
741
- break;
742
- }
743
- }
744
- }
726
+ payload = assignMissingLaunchNicknames(payload, { prompt, context: bus });
745
727
 
746
728
  saveSessionState(projectRoot, {
747
729
  provider,
@@ -33,8 +33,8 @@ function computeAgentBar(options = {}) {
33
33
  let windowItems = Math.max(1, Math.min(maxAgentWindow, activeAgents.length));
34
34
  let start = agentListWindowStart;
35
35
  const ufooItem = focusMode === "dashboard" && selectedAgentIndex === 0
36
- ? "\x1b[90;7mucode\x1b[0m"
37
- : "\x1b[36mucode\x1b[0m";
36
+ ? "\x1b[90;7mufoo\x1b[0m"
37
+ : "\x1b[36mufoo\x1b[0m";
38
38
  const ufooLen = stripAnsi(ufooItem).length;
39
39
 
40
40
  const computeStart = (items) => {
@@ -78,7 +78,7 @@ function computeAgentBar(options = {}) {
78
78
  const prefix = indicator
79
79
  ? `${indicatorColor}${indicator}\x1b[0m`
80
80
  : "";
81
- const idx = s + i + 1; // +1 for ucode at index 0
81
+ const idx = s + i + 1; // +1 for ufoo chat at index 0
82
82
  if (focusMode === "dashboard" && idx === selectedAgentIndex) {
83
83
  return `${prefix}\x1b[90;7m${label}\x1b[0m`;
84
84
  }
@@ -76,35 +76,30 @@ function getWrapWidth(input, fallbackWidth) {
76
76
 
77
77
  /**
78
78
  * Simulate blessed's wrapping for a single logical line (no newlines).
79
- * Returns { rows, lastCol }.
79
+ * Returns the zero-based visual row and column at the end of the line.
80
80
  *
81
- * Blessed preprocesses double-width chars by inserting a \x03 marker after
82
- * each one, then wraps at character count = width. This means a double-width
83
- * char at col (width - 1) overflows by 1 cell — only the \x03 marker wraps
84
- * to the next line. We replicate this behavior so cursor math matches what
85
- * blessed actually renders.
81
+ * A cursor at exactly width cells is still at the end of the current visual
82
+ * row; it should not become a phantom row until another character or an
83
+ * explicit newline is processed.
86
84
  */
87
- function wrapLine(line, width, strWidth) {
85
+ function measureLineEnd(line, width, strWidth) {
88
86
  const chars = Array.from(String(line || ""));
87
+ let row = 0;
89
88
  let col = 0;
90
- let rows = 1;
91
89
  for (const ch of chars) {
92
90
  const w = safeStrWidth(strWidth, ch);
93
91
  if (w === 0) continue;
94
- if (col >= width) {
95
- rows += 1;
96
- col = col - width;
92
+ if (col > 0 && col + w > width) {
93
+ row += 1;
94
+ col = 0;
97
95
  }
98
96
  col += w;
99
97
  }
100
- if (col > width) {
101
- rows += 1;
102
- col = col - width;
103
- } else if (col === width) {
104
- rows += 1;
105
- col = 0;
106
- }
107
- return { rows, lastCol: col };
98
+ return { row, col };
99
+ }
100
+
101
+ function countLineRows(line, width, strWidth) {
102
+ return measureLineEnd(line, width, strWidth).row + 1;
108
103
  }
109
104
 
110
105
  function countLines(text, width, strWidth) {
@@ -114,8 +109,7 @@ function countLines(text, width, strWidth) {
114
109
  const lines = expanded.split("\n");
115
110
  let total = 0;
116
111
  for (const line of lines) {
117
- const { rows, lastCol } = wrapLine(line, width, strWidth);
118
- total += (lastCol === 0 && rows > 1) ? rows - 1 : rows;
112
+ total += countLineRows(line, width, strWidth);
119
113
  }
120
114
  return total;
121
115
  }
@@ -136,28 +130,10 @@ function getCursorRowCol(text, pos, width, strWidth) {
136
130
  for (let i = 0; i < lines.length; i += 1) {
137
131
  const line = lines[i] || "";
138
132
  if (i < lines.length - 1) {
139
- const { rows, lastCol } = wrapLine(line, width, strWidth);
140
- row += (lastCol === 0 && rows > 1) ? rows - 1 : rows;
133
+ row += countLineRows(line, width, strWidth);
141
134
  } else {
142
- const chars = Array.from(line);
143
- let col = 0;
144
- for (const ch of chars) {
145
- const w = safeStrWidth(strWidth, ch);
146
- if (w === 0) continue;
147
- if (col >= width) {
148
- row += 1;
149
- col = col - width;
150
- }
151
- col += w;
152
- }
153
- if (col > width) {
154
- row += 1;
155
- col = col - width;
156
- } else if (col === width) {
157
- row += 1;
158
- col = 0;
159
- }
160
- return { row, col };
135
+ const measured = measureLineEnd(line, width, strWidth);
136
+ return { row: row + measured.row, col: measured.col };
161
137
  }
162
138
  }
163
139
  return { row, col: 0 };
@@ -187,27 +163,20 @@ function getCursorPosForRowCol(text, targetRow, targetCol, width, strWidth) {
187
163
  lineOffset += ch.length;
188
164
  continue;
189
165
  }
190
- if (col >= width) {
166
+ if (col > 0 && col + w > width) {
167
+ if (row === targetRow && targetCol >= col) {
168
+ return expandedToOriginal(original, expPos + lineOffset);
169
+ }
191
170
  row += 1;
192
- col = col - width;
171
+ col = 0;
193
172
  }
194
173
  if (row === targetRow && col + w > targetCol) {
195
174
  return expandedToOriginal(original, expPos + lineOffset);
196
175
  }
197
176
  col += w;
198
177
  lineOffset += ch.length;
199
- if (col > width) {
200
- if (row === targetRow) {
201
- return expandedToOriginal(original, expPos + lineOffset);
202
- }
203
- row += 1;
204
- col = col - width;
205
- } else if (col === width) {
206
- if (row === targetRow) {
207
- return expandedToOriginal(original, expPos + lineOffset);
208
- }
209
- row += 1;
210
- col = 0;
178
+ if (row === targetRow && col === targetCol) {
179
+ return expandedToOriginal(original, expPos + lineOffset);
211
180
  }
212
181
  }
213
182
 
@@ -30,8 +30,10 @@ function createInputSubmitHandler(options = {}) {
30
30
  throw new Error("createInputSubmitHandler requires a mutable state object");
31
31
  }
32
32
 
33
- function userPrefix() {
34
- return "{white-fg}you{/white-fg} {gray-fg}·{/gray-fg}";
33
+ function userEcho(text, targetLabel = "") {
34
+ const body = escapeBlessed(text);
35
+ if (!targetLabel) return body;
36
+ return `{magenta-fg}@${escapeBlessed(targetLabel)}{/magenta-fg} ${body}`;
35
37
  }
36
38
 
37
39
  async function tryActivateTargetAgent(agentId) {
@@ -92,7 +94,7 @@ function createInputSubmitHandler(options = {}) {
92
94
  const label = getAgentLabel(state.targetAgent);
93
95
  logMessage(
94
96
  "user",
95
- `${userPrefix()} {magenta-fg}@${escapeBlessed(label)}{/magenta-fg} ${escapeBlessed(text)}`
97
+ userEcho(text, label)
96
98
  );
97
99
  renderScreen(); // Immediately render the user message
98
100
  markPendingDelivery(state.targetAgent);
@@ -126,7 +128,7 @@ function createInputSubmitHandler(options = {}) {
126
128
  const message = atTarget.message.trim();
127
129
  logMessage(
128
130
  "user",
129
- `${userPrefix()} {magenta-fg}@${escapeBlessed(atTarget.target)}{/magenta-fg} ${escapeBlessed(message)}`
131
+ userEcho(message, atTarget.target)
130
132
  );
131
133
  renderScreen(); // Immediately render the user message
132
134
  markPendingDelivery(resolvedTarget);
@@ -143,7 +145,7 @@ function createInputSubmitHandler(options = {}) {
143
145
 
144
146
  if (text.startsWith("/")) {
145
147
  if (shouldEchoCommandInChat(text)) {
146
- logMessage("user", `${userPrefix()} ${escapeBlessed(text)}`);
148
+ logMessage("user", userEcho(text));
147
149
  renderScreen(); // Render slash command immediately
148
150
  }
149
151
  try {
@@ -189,7 +191,7 @@ function createInputSubmitHandler(options = {}) {
189
191
  allow_relevance_queue: true,
190
192
  },
191
193
  });
192
- logMessage("user", `${userPrefix()} ${escapeBlessed(text)}`);
194
+ logMessage("user", userEcho(text));
193
195
  renderScreen(); // Render plain text message immediately
194
196
  }
195
197
 
package/src/code/agent.js CHANGED
@@ -1119,6 +1119,7 @@ async function runUbusCommand(state = {}, options = {}) {
1119
1119
  // eslint-disable-next-line no-await-in-loop
1120
1120
  nlResult = await runNl(message.task, state, {
1121
1121
  onProgress: progressReporter,
1122
+ signal: options.signal,
1122
1123
  });
1123
1124
  } catch (err) {
1124
1125
  sendErrors.push(`task from ${message.publisher} failed: ${err && err.message ? err.message : "task failed"}`);
@@ -1180,7 +1181,9 @@ async function runUbusCommand(state = {}, options = {}) {
1180
1181
  });
1181
1182
  }
1182
1183
 
1183
- const nlResult = await runNl(item.task, state);
1184
+ const nlResult = await runNl(item.task, state, {
1185
+ signal: options.signal,
1186
+ });
1184
1187
  const reply = String(formatNl(nlResult, false) || "").replace(/\s+/g, " ").trim() || "Done.";
1185
1188
  const sendRes = shell(`ufoo bus send ${shellQuote(item.publisher)} ${shellQuote(reply.slice(0, 2000))}`);
1186
1189
  if (!sendRes.ok) {
@@ -9,6 +9,8 @@ const { getBashToolDescription } = require("./prompts/toolDescriptions/bash");
9
9
  const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash"]);
10
10
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
11
11
  const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
12
+ const DEFAULT_MAX_NATIVE_TOOL_CALLS = 12;
13
+ const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 1;
12
14
 
13
15
  function nowMs() {
14
16
  return Date.now();
@@ -20,6 +22,36 @@ function normalizeTimeoutMs(value) {
20
22
  return Math.max(1000, Math.floor(parsed));
21
23
  }
22
24
 
25
+ function normalizePositiveInt(value, fallback) {
26
+ const parsed = Number.parseInt(String(value || ""), 10);
27
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
28
+ return Math.floor(parsed);
29
+ }
30
+
31
+ function resolveNativeToolBudget(env = process.env) {
32
+ return {
33
+ maxToolCalls: normalizePositiveInt(env.UFOO_UCODE_MAX_TOOL_CALLS, DEFAULT_MAX_NATIVE_TOOL_CALLS),
34
+ maxToolErrors: normalizePositiveInt(env.UFOO_UCODE_MAX_TOOL_ERRORS, DEFAULT_MAX_NATIVE_TOOL_ERRORS),
35
+ };
36
+ }
37
+
38
+ function enforceNativeToolBudget({
39
+ toolCallsExecuted = 0,
40
+ toolErrors = 0,
41
+ maxToolCalls = DEFAULT_MAX_NATIVE_TOOL_CALLS,
42
+ maxToolErrors = DEFAULT_MAX_NATIVE_TOOL_ERRORS,
43
+ lastTool = "",
44
+ lastError = "",
45
+ } = {}) {
46
+ if (toolCallsExecuted > maxToolCalls) {
47
+ throw new Error(`tool call budget exceeded (${maxToolCalls})`);
48
+ }
49
+ if (toolErrors >= maxToolErrors) {
50
+ const detail = [lastTool, lastError].filter(Boolean).join(": ");
51
+ throw new Error(`tool error budget exceeded (${maxToolErrors})${detail ? `: ${detail}` : ""}`);
52
+ }
53
+ }
54
+
23
55
  function createGuards({ signal = null, timeoutMs = 300000 } = {}) {
24
56
  const startedAt = nowMs();
25
57
  const budgetMs = normalizeTimeoutMs(timeoutMs);
@@ -907,6 +939,8 @@ async function runNativeLoopOpenAi({
907
939
  let aggregated = "";
908
940
  let streamed = false;
909
941
  let toolCallsExecuted = 0;
942
+ let toolErrors = 0;
943
+ const toolBudget = resolveNativeToolBudget();
910
944
 
911
945
  while (true) {
912
946
  guards.ensureActive();
@@ -991,6 +1025,17 @@ async function runNativeLoopOpenAi({
991
1025
  onToolEvent,
992
1026
  });
993
1027
  toolCallsExecuted += 1;
1028
+ if (!toolResult || toolResult.ok === false) {
1029
+ toolErrors += 1;
1030
+ }
1031
+ enforceNativeToolBudget({
1032
+ toolCallsExecuted,
1033
+ toolErrors,
1034
+ maxToolCalls: toolBudget.maxToolCalls,
1035
+ maxToolErrors: toolBudget.maxToolErrors,
1036
+ lastTool: toolCall.function.name,
1037
+ lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
1038
+ });
994
1039
  messages.push({
995
1040
  role: "tool",
996
1041
  tool_call_id: toolCall.id,
@@ -1034,6 +1079,8 @@ async function runNativeLoopAnthropic({
1034
1079
  let aggregated = "";
1035
1080
  let streamed = false;
1036
1081
  let toolCallsExecuted = 0;
1082
+ let toolErrors = 0;
1083
+ const toolBudget = resolveNativeToolBudget();
1037
1084
 
1038
1085
  while (true) {
1039
1086
  guards.ensureActive();
@@ -1109,6 +1156,17 @@ async function runNativeLoopAnthropic({
1109
1156
  onToolEvent,
1110
1157
  });
1111
1158
  toolCallsExecuted += 1;
1159
+ if (!toolResult || toolResult.ok === false) {
1160
+ toolErrors += 1;
1161
+ }
1162
+ enforceNativeToolBudget({
1163
+ toolCallsExecuted,
1164
+ toolErrors,
1165
+ maxToolCalls: toolBudget.maxToolCalls,
1166
+ maxToolErrors: toolBudget.maxToolErrors,
1167
+ lastTool: call.name,
1168
+ lastError: toolResult && toolResult.error ? String(toolResult.error) : "",
1169
+ });
1112
1170
  toolResults.push({
1113
1171
  type: "tool_result",
1114
1172
  tool_use_id: String(call.id || ""),
@@ -147,8 +147,9 @@ async function runDecomposedTask({
147
147
  }
148
148
  }
149
149
 
150
- // Stop on error for critical steps
151
- if (!stepResult.ok && (step.id === "identify" || step.id === "locate")) {
150
+ // Stop on any step failure. A failed tool/provider call means the
151
+ // current plan is no longer reliable, and continuing can trigger loops.
152
+ if (!stepResult.ok) {
152
153
  return {
153
154
  ok: false,
154
155
  error: `Failed at ${step.name}: ${stepResult.error}`,
@@ -266,4 +267,4 @@ module.exports = {
266
267
  runDecomposedTask,
267
268
  compileSummary,
268
269
  createBusProgressReporter,
269
- };
270
+ };
package/src/code/tui.js CHANGED
@@ -993,6 +993,7 @@ function runUcodeTui({
993
993
  const ubusResult = await runUbusCommand(state, {
994
994
  workspaceRoot,
995
995
  subscriberId: autoBusSubscriberId,
996
+ signal: abortController.signal,
996
997
  onMessageReceived: (msg) => {
997
998
  // Display the incoming message immediately
998
999
  const { extractAgentNickname } = require("./agent");
@@ -1254,59 +1255,63 @@ function runUcodeTui({
1254
1255
  });
1255
1256
  let streamState = null;
1256
1257
  let renderedToolLogCount = 0;
1257
- const nlResult = await runNaturalLanguageTask(result.task, state, {
1258
- signal: abortController.signal,
1259
- onDelta: (delta) => {
1260
- const text = escapeStripper.write(String(delta || ""));
1261
- if (!text) return;
1258
+ let nlResult = null;
1259
+ try {
1260
+ nlResult = await runNaturalLanguageTask(result.task, state, {
1261
+ signal: abortController.signal,
1262
+ onDelta: (delta) => {
1263
+ const text = escapeStripper.write(String(delta || ""));
1264
+ if (!text) return;
1265
+ if (!streamState) {
1266
+ streamState = createNlStreamState();
1267
+ }
1268
+ appendNlStreamDelta(streamState, text);
1269
+ },
1270
+ onToolLog: (entry) => {
1271
+ renderedToolLogCount += 1;
1272
+ logToolHint(entry);
1273
+ },
1274
+ });
1275
+ const tail = escapeStripper.flush();
1276
+ if (tail) {
1262
1277
  if (!streamState) {
1263
1278
  streamState = createNlStreamState();
1264
1279
  }
1265
- appendNlStreamDelta(streamState, text);
1266
- },
1267
- onToolLog: (entry) => {
1268
- renderedToolLogCount += 1;
1269
- logToolHint(entry);
1270
- },
1271
- });
1272
- const tail = escapeStripper.flush();
1273
- if (tail) {
1274
- if (!streamState) {
1275
- streamState = createNlStreamState();
1280
+ appendNlStreamDelta(streamState, tail);
1276
1281
  }
1277
- appendNlStreamDelta(streamState, tail);
1278
- }
1279
- pendingTask = null;
1280
- updateStatus("", "none");
1281
- let finalStreamInfo = { lastChar: "" };
1282
- if (streamState) {
1283
- finalStreamInfo = finalizeNlStream(streamState);
1284
- }
1285
- if (Array.isArray(nlResult && nlResult.logs) && nlResult.logs.length > renderedToolLogCount) {
1286
- for (const entry of nlResult.logs.slice(renderedToolLogCount)) {
1287
- logToolHint(entry);
1282
+ let finalStreamInfo = { lastChar: "" };
1283
+ if (streamState) {
1284
+ finalStreamInfo = finalizeNlStream(streamState);
1288
1285
  }
1289
- }
1290
- const streamed = Boolean(nlResult && nlResult.streamed);
1291
- const hasVisibleStreamText = Boolean(
1292
- streamState
1293
- && typeof streamState.full === "string"
1294
- && /[^\s]/.test(streamState.full)
1295
- );
1296
- const streamLastChar = nlResult && typeof nlResult.streamLastChar === "string"
1297
- ? nlResult.streamLastChar.slice(-1)
1298
- : finalStreamInfo.lastChar;
1299
- if (streamed && hasVisibleStreamText && streamLastChar !== "\n") {
1300
- logBox.log("");
1301
- screen.render();
1302
- }
1303
- const shouldSkipSummary = Boolean(streamed && nlResult && nlResult.ok && hasVisibleStreamText);
1304
- if (!shouldSkipSummary) {
1305
- logText(formatNlResult(nlResult, false));
1306
- }
1307
- const persisted = persistSessionState(state);
1308
- if (!persisted || persisted.ok === false) {
1309
- logText(`Error: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}`);
1286
+ if (Array.isArray(nlResult && nlResult.logs) && nlResult.logs.length > renderedToolLogCount) {
1287
+ for (const entry of nlResult.logs.slice(renderedToolLogCount)) {
1288
+ logToolHint(entry);
1289
+ }
1290
+ }
1291
+ const streamed = Boolean(nlResult && nlResult.streamed);
1292
+ const hasVisibleStreamText = Boolean(
1293
+ streamState
1294
+ && typeof streamState.full === "string"
1295
+ && /[^\s]/.test(streamState.full)
1296
+ );
1297
+ const streamLastChar = nlResult && typeof nlResult.streamLastChar === "string"
1298
+ ? nlResult.streamLastChar.slice(-1)
1299
+ : finalStreamInfo.lastChar;
1300
+ if (streamed && hasVisibleStreamText && streamLastChar !== "\n") {
1301
+ logBox.log("");
1302
+ screen.render();
1303
+ }
1304
+ const shouldSkipSummary = Boolean(streamed && nlResult && nlResult.ok && hasVisibleStreamText);
1305
+ if (!shouldSkipSummary) {
1306
+ logText(formatNlResult(nlResult, false));
1307
+ }
1308
+ const persisted = persistSessionState(state);
1309
+ if (!persisted || persisted.ok === false) {
1310
+ logText(`Error: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}`);
1311
+ }
1312
+ } finally {
1313
+ pendingTask = null;
1314
+ updateStatus("", "none");
1310
1315
  }
1311
1316
  }
1312
1317
  };
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+
3
+ function asTrimmedString(value) {
4
+ return typeof value === "string" ? value.trim() : "";
5
+ }
6
+
7
+ function normalizeNicknameSegment(value = "", fallback = "task") {
8
+ const normalized = asTrimmedString(value)
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9_-]+/g, "-")
11
+ .replace(/-+/g, "-")
12
+ .replace(/^-+|-+$/g, "");
13
+ return normalized || fallback;
14
+ }
15
+
16
+ function stripRoutingPromptMetadata(prompt = "") {
17
+ let text = String(prompt || "");
18
+ const markers = [
19
+ "\nRouting request metadata (JSON):",
20
+ "\nPrivate runtime reports for ufoo-agent (JSON):",
21
+ "\nController loop state (JSON):",
22
+ "\nController tool results so far (JSON):",
23
+ ];
24
+ for (const marker of markers) {
25
+ const index = text.indexOf(marker);
26
+ if (index >= 0) {
27
+ text = text.slice(0, index);
28
+ }
29
+ }
30
+ return text.trim();
31
+ }
32
+
33
+ function normalizeLaunchAgentForNickname(agent = "") {
34
+ const raw = asTrimmedString(agent).toLowerCase();
35
+ if (raw === "claude" || raw === "claude-code" || raw === "uclaude") return "claude";
36
+ if (raw === "codex" || raw === "ucodex") return "codex";
37
+ if (raw === "ufoo" || raw === "ucode" || raw === "ufoo-code") return "ucode";
38
+ return "";
39
+ }
40
+
41
+ function nicknameCapturePattern() {
42
+ return "(?:`([^`]{1,32})`|[\"'“”]([^\"'“”]{1,32})[\"'“”]?|([A-Za-z0-9_-]{1,32}))";
43
+ }
44
+
45
+ function pickNicknameCapture(match) {
46
+ if (!match) return "";
47
+ for (let i = 1; i < match.length; i += 1) {
48
+ const value = asTrimmedString(match[i]);
49
+ if (value) return normalizeNicknameSegment(value, "");
50
+ }
51
+ return "";
52
+ }
53
+
54
+ function extractRequestedLaunchNickname(prompt = "") {
55
+ const text = stripRoutingPromptMetadata(prompt);
56
+ if (!text) return "";
57
+ const value = nicknameCapturePattern();
58
+ const patterns = [
59
+ new RegExp(`(?:launch|start|create|spawn|open|new|启动|新建|创建|拉起)[\\s\\S]{0,80}(?:named|name|nickname|叫做|叫|名为|昵称|取名|为)\\s*${value}`, "i"),
60
+ new RegExp(`(?:named|name|nickname|叫做|叫|名为|昵称|取名)\\s*${value}[\\s\\S]{0,80}(?:agent|worker|codex|claude|ucode|ufoo|代理|智能体)`, "i"),
61
+ ];
62
+ for (const re of patterns) {
63
+ const nickname = pickNicknameCapture(text.match(re));
64
+ if (nickname) return nickname;
65
+ }
66
+ return "";
67
+ }
68
+
69
+ function collectKeywordTokens(text = "") {
70
+ const specs = [
71
+ [/处理|修复|解决|排查/g, "fix"],
72
+ [/路由|主路由/g, "route"],
73
+ [/启动|新启动|拉起/g, "launch"],
74
+ [/投递|派发|发送|分发/g, "dispatch"],
75
+ [/任务|工作/g, "task"],
76
+ [/测试|验证/g, "test"],
77
+ [/前端|界面/g, "frontend"],
78
+ [/后端|服务端/g, "backend"],
79
+ [/设计/g, "design"],
80
+ [/审查|评审/g, "review"],
81
+ [/调试/g, "debug"],
82
+ [/重构/g, "refactor"],
83
+ [/发布/g, "release"],
84
+ [/文档/g, "docs"],
85
+ [/性能/g, "perf"],
86
+ ];
87
+ const matches = [];
88
+ for (const [re, token] of specs) {
89
+ re.lastIndex = 0;
90
+ let match = re.exec(text);
91
+ while (match) {
92
+ matches.push({ index: match.index, token });
93
+ match = re.exec(text);
94
+ }
95
+ }
96
+ return matches
97
+ .sort((a, b) => a.index - b.index)
98
+ .map((item) => item.token);
99
+ }
100
+
101
+ const ENGLISH_STOPWORDS = new Set([
102
+ "the", "a", "an", "and", "or", "to", "for", "with", "from", "this", "that",
103
+ "please", "pls", "ufoo", "chat", "agent", "agents", "worker", "workers",
104
+ "nickname", "nick", "name", "named", "new", "current", "online", "source",
105
+ "metadata", "json", "request", "user", "feedback", "bug", "issue",
106
+ ]);
107
+
108
+ function collectEnglishTokens(text = "") {
109
+ const rawTokens = String(text || "").toLowerCase().match(/[a-z][a-z0-9_-]{1,24}/g) || [];
110
+ return rawTokens
111
+ .map((token) => normalizeNicknameSegment(token, ""))
112
+ .filter((token) => token && !ENGLISH_STOPWORDS.has(token));
113
+ }
114
+
115
+ function uniqueOrdered(values = []) {
116
+ const seen = new Set();
117
+ const result = [];
118
+ for (const value of values) {
119
+ const token = normalizeNicknameSegment(value, "");
120
+ if (!token || seen.has(token)) continue;
121
+ seen.add(token);
122
+ result.push(token);
123
+ }
124
+ return result;
125
+ }
126
+
127
+ function toExistingNicknameSet(existingNicknames = []) {
128
+ if (existingNicknames instanceof Set) return new Set(existingNicknames);
129
+ if (Array.isArray(existingNicknames)) return new Set(existingNicknames.map((item) => normalizeNicknameSegment(item, "")));
130
+ if (existingNicknames && typeof existingNicknames === "object") {
131
+ return new Set(Object.keys(existingNicknames).map((item) => normalizeNicknameSegment(item, "")));
132
+ }
133
+ return new Set();
134
+ }
135
+
136
+ function truncateNickname(value = "", maxLength = 32) {
137
+ const normalized = normalizeNicknameSegment(value, "task");
138
+ if (normalized.length <= maxLength) return normalized;
139
+ return normalized.slice(0, maxLength).replace(/-+$/g, "") || "task";
140
+ }
141
+
142
+ function makeUniqueNickname(base, existingNicknames = []) {
143
+ const existing = toExistingNicknameSet(existingNicknames);
144
+ const normalizedBase = truncateNickname(base);
145
+ if (!existing.has(normalizedBase)) return normalizedBase;
146
+ for (let i = 2; i < 100; i += 1) {
147
+ const suffix = `-${i}`;
148
+ const candidate = `${truncateNickname(normalizedBase, 32 - suffix.length)}${suffix}`;
149
+ if (!existing.has(candidate)) return candidate;
150
+ }
151
+ return `${truncateNickname(normalizedBase, 27)}-${Date.now().toString(36).slice(-4)}`;
152
+ }
153
+
154
+ function collectExistingNicknames(context = {}) {
155
+ const values = [];
156
+ if (context && typeof context === "object") {
157
+ if (context.nicknames && typeof context.nicknames === "object") {
158
+ values.push(...Object.keys(context.nicknames));
159
+ }
160
+ if (Array.isArray(context.agents)) {
161
+ for (const agent of context.agents) {
162
+ if (agent && agent.nickname) values.push(agent.nickname);
163
+ }
164
+ }
165
+ }
166
+ return values;
167
+ }
168
+
169
+ function buildLaunchNickname(prompt = "", agent = "", existingNicknames = []) {
170
+ const explicit = extractRequestedLaunchNickname(prompt);
171
+ if (explicit) return makeUniqueNickname(explicit, existingNicknames);
172
+
173
+ const corePrompt = stripRoutingPromptMetadata(prompt);
174
+ const agentPrefix = normalizeLaunchAgentForNickname(agent) || "agent";
175
+ const tokens = uniqueOrdered([
176
+ ...collectKeywordTokens(corePrompt),
177
+ ...collectEnglishTokens(corePrompt),
178
+ ]).filter((token) => token !== agentPrefix);
179
+ const stem = tokens.slice(0, 2).join("-") || "task";
180
+ return makeUniqueNickname(`${agentPrefix}-${stem}`, existingNicknames);
181
+ }
182
+
183
+ function assignMissingLaunchNicknames(payload = {}, options = {}) {
184
+ if (!payload || typeof payload !== "object" || !Array.isArray(payload.ops)) return payload;
185
+ const existing = new Set(collectExistingNicknames(options.context));
186
+ let changed = false;
187
+ const ops = payload.ops.map((op) => {
188
+ if (!op || op.action !== "launch" || op.nickname) return op;
189
+ const count = Number.parseInt(op.count || 1, 10);
190
+ if (Number.isFinite(count) && count > 1) return op;
191
+ const nickname = buildLaunchNickname(options.prompt || "", op.agent || "", existing);
192
+ existing.add(nickname);
193
+ changed = true;
194
+ return { ...op, nickname };
195
+ });
196
+ return changed ? { ...payload, ops } : payload;
197
+ }
198
+
199
+ module.exports = {
200
+ assignMissingLaunchNicknames,
201
+ buildLaunchNickname,
202
+ collectExistingNicknames,
203
+ extractRequestedLaunchNickname,
204
+ normalizeLaunchAgentForNickname,
205
+ stripRoutingPromptMetadata,
206
+ };
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+
3
+ const {
4
+ normalizeLaunchAgentForNickname,
5
+ stripRoutingPromptMetadata,
6
+ } = require("./launchRouting");
7
+
8
+ const LAUNCH_TARGET_PREFIX = "__ufoo_launch_";
9
+
10
+ function normalizePayload(payload) {
11
+ if (!payload || typeof payload !== "object") {
12
+ return { reply: "", dispatch: [], ops: [] };
13
+ }
14
+ return {
15
+ ...payload,
16
+ reply: typeof payload.reply === "string" ? payload.reply : "",
17
+ dispatch: Array.isArray(payload.dispatch) ? payload.dispatch : [],
18
+ ops: Array.isArray(payload.ops) ? payload.ops : [],
19
+ };
20
+ }
21
+
22
+ function isLaunchOp(op) {
23
+ return Boolean(op && op.action === "launch");
24
+ }
25
+
26
+ function hasDispatchMessage(item) {
27
+ return Boolean(item && String(item.target || "").trim() && String(item.message || "").trim());
28
+ }
29
+
30
+ function launchPlaceholder(index) {
31
+ return `${LAUNCH_TARGET_PREFIX}${index}`;
32
+ }
33
+
34
+ function parseLaunchPlaceholder(target = "") {
35
+ const raw = String(target || "").trim();
36
+ if (!raw.startsWith(LAUNCH_TARGET_PREFIX)) return -1;
37
+ const parsed = Number.parseInt(raw.slice(LAUNCH_TARGET_PREFIX.length), 10);
38
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : -1;
39
+ }
40
+
41
+ function isLaunchDispatchTarget(target = "", op = {}) {
42
+ const raw = String(target || "").trim();
43
+ if (!raw || raw.includes(":")) return false;
44
+ const normalized = raw.toLowerCase();
45
+ const nickname = String(op.nickname || "").trim();
46
+ if (nickname && raw === nickname) return true;
47
+ const placeholders = new Set([
48
+ "new",
49
+ "new-agent",
50
+ "new-worker",
51
+ "worker",
52
+ "agent",
53
+ "launched",
54
+ "launched-agent",
55
+ "launched-worker",
56
+ ]);
57
+ if (placeholders.has(normalized)) return true;
58
+ const launchAgent = normalizeLaunchAgentForNickname(op.agent || "");
59
+ return Boolean(launchAgent && normalizeLaunchAgentForNickname(raw) === launchAgent);
60
+ }
61
+
62
+ function resolveLaunchResultTarget(op = {}, result = {}) {
63
+ if (result && result.agent_id) return String(result.agent_id);
64
+ if (result && Array.isArray(result.subscriber_ids) && result.subscriber_ids[0]) {
65
+ return String(result.subscriber_ids[0]);
66
+ }
67
+ if (result && result.subscriber_id) return String(result.subscriber_id);
68
+ if (result && result.nickname) return String(result.nickname);
69
+ if (op && op.nickname) return String(op.nickname);
70
+ return "";
71
+ }
72
+
73
+ function resolveDispatchLaunchIndex(dispatchItem = {}, launchOps = []) {
74
+ const target = String(dispatchItem.target || "").trim();
75
+ const placeholderIndex = parseLaunchPlaceholder(target);
76
+ if (placeholderIndex >= 0) return placeholderIndex;
77
+
78
+ for (let i = 0; i < launchOps.length; i += 1) {
79
+ if (isLaunchDispatchTarget(target, launchOps[i])) return i;
80
+ }
81
+ return -1;
82
+ }
83
+
84
+ function prepareLaunchDispatchPayload(payload, prompt = "") {
85
+ const normalized = normalizePayload(payload);
86
+ const launchOps = [];
87
+ const otherOps = [];
88
+ for (const op of normalized.ops) {
89
+ if (isLaunchOp(op)) launchOps.push(op);
90
+ else if (op) otherOps.push(op);
91
+ }
92
+
93
+ if (launchOps.length === 0) {
94
+ return {
95
+ payload: normalized,
96
+ launchOps,
97
+ otherOps,
98
+ dispatch: normalized.dispatch.filter(hasDispatchMessage),
99
+ };
100
+ }
101
+
102
+ let dispatch = normalized.dispatch.filter(hasDispatchMessage).map((item) => ({ ...item }));
103
+ const taskMessage = stripRoutingPromptMetadata(prompt);
104
+ if (dispatch.length === 0 && !taskMessage) {
105
+ return {
106
+ payload: { ...normalized, dispatch: [], ops: otherOps },
107
+ launchOps: [],
108
+ otherOps,
109
+ dispatch: [],
110
+ droppedLaunches: launchOps,
111
+ };
112
+ }
113
+
114
+ if (dispatch.length === 0 && taskMessage) {
115
+ dispatch = launchOps.map((op, index) => ({
116
+ target: op.nickname || launchPlaceholder(index),
117
+ message: taskMessage,
118
+ injection_mode: "immediate",
119
+ source: "ufoo-agent",
120
+ }));
121
+ } else if (launchOps.length === 1) {
122
+ const fallbackTarget = launchOps[0].nickname || launchPlaceholder(0);
123
+ dispatch = dispatch.map((item) => (
124
+ isLaunchDispatchTarget(item.target, launchOps[0])
125
+ ? { ...item, target: fallbackTarget }
126
+ : item
127
+ ));
128
+ }
129
+
130
+ return {
131
+ payload: { ...normalized, dispatch, ops: [...launchOps, ...otherOps] },
132
+ launchOps,
133
+ otherOps,
134
+ dispatch,
135
+ };
136
+ }
137
+
138
+ function bindDispatchToLaunchResults(dispatch = [], launchOps = [], launchResults = []) {
139
+ const resultByIndex = launchResults.filter((item) => item && item.action === "launch");
140
+ return dispatch.map((item) => {
141
+ const index = resolveDispatchLaunchIndex(item, launchOps);
142
+ if (index < 0) return item;
143
+ const target = resolveLaunchResultTarget(launchOps[index], resultByIndex[index] || {});
144
+ return target ? { ...item, target } : item;
145
+ });
146
+ }
147
+
148
+ async function finalizeRouterPayload({
149
+ projectRoot,
150
+ payload,
151
+ prompt = "",
152
+ processManager,
153
+ dispatchMessages,
154
+ handleOps,
155
+ markPending = () => {},
156
+ finalizeLocally = true,
157
+ }) {
158
+ const prepared = prepareLaunchDispatchPayload(payload, prompt);
159
+ if (finalizeLocally === false) {
160
+ return {
161
+ ok: true,
162
+ payload: prepared.payload,
163
+ opsResults: [],
164
+ };
165
+ }
166
+
167
+ if (prepared.launchOps.length === 0) {
168
+ for (const item of prepared.dispatch) {
169
+ if (item && item.target && item.target !== "broadcast") {
170
+ markPending(item.target);
171
+ }
172
+ }
173
+ if (typeof dispatchMessages === "function") {
174
+ await dispatchMessages(projectRoot, prepared.dispatch);
175
+ }
176
+ const opsResults = typeof handleOps === "function" && prepared.otherOps.length > 0
177
+ ? await handleOps(projectRoot, prepared.otherOps, processManager)
178
+ : [];
179
+ return {
180
+ ok: true,
181
+ payload: { ...prepared.payload, dispatch: prepared.dispatch, ops: prepared.otherOps },
182
+ opsResults,
183
+ };
184
+ }
185
+
186
+ const launchResults = typeof handleOps === "function"
187
+ ? await handleOps(projectRoot, prepared.launchOps, processManager)
188
+ : [];
189
+ const boundDispatch = bindDispatchToLaunchResults(prepared.dispatch, prepared.launchOps, launchResults);
190
+
191
+ for (const item of boundDispatch) {
192
+ if (item && item.target && item.target !== "broadcast") {
193
+ markPending(item.target);
194
+ }
195
+ }
196
+ if (typeof dispatchMessages === "function") {
197
+ await dispatchMessages(projectRoot, boundDispatch);
198
+ }
199
+
200
+ const otherResults = typeof handleOps === "function" && prepared.otherOps.length > 0
201
+ ? await handleOps(projectRoot, prepared.otherOps, processManager)
202
+ : [];
203
+
204
+ return {
205
+ ok: true,
206
+ payload: { ...prepared.payload, dispatch: boundDispatch },
207
+ opsResults: [...launchResults, ...otherResults],
208
+ };
209
+ }
210
+
211
+ module.exports = {
212
+ bindDispatchToLaunchResults,
213
+ finalizeRouterPayload,
214
+ normalizePayload,
215
+ prepareLaunchDispatchPayload,
216
+ __private: {
217
+ isLaunchDispatchTarget,
218
+ launchPlaceholder,
219
+ resolveLaunchResultTarget,
220
+ stripRoutingPromptMetadata,
221
+ },
222
+ };
package/src/daemon/ops.js CHANGED
@@ -44,6 +44,10 @@ function toTmuxBinary(agent = "") {
44
44
  return "";
45
45
  }
46
46
 
47
+ function resolveUfooRunnerPath() {
48
+ return path.resolve(__dirname, "../../bin/ufoo.js");
49
+ }
50
+
47
51
  function normalizeLaunchScope(value, fallback = "inplace") {
48
52
  const raw = String(value || "").trim().toLowerCase();
49
53
  if (!raw) return fallback;
@@ -546,7 +550,7 @@ async function spawnManagedHostAgent(
546
550
  if (hasPreRegisteredSubscriber) {
547
551
  // Group mode: use ufoo launcher for activity_state monitoring
548
552
  // This enables ReadyDetector and bootstrap to work correctly
549
- const ufooRunner = path.join(projectRoot, "bin", "ufoo.js");
553
+ const ufooRunner = resolveUfooRunnerPath();
550
554
  const launchCmd = `${shellEscape(process.execPath)} ${shellEscape(ufooRunner)} agent-pty-runner ${shellEscape(normalizedAgent)}${argText}`.trim();
551
555
  runCmd = titleCmd
552
556
  ? `cd ${shellEscape(projectRoot)} && ${titleCmd} && ${launchCmd}`
@@ -591,7 +595,7 @@ async function spawnManagedHostAgent(
591
595
  }
592
596
 
593
597
  async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "", processManager = null, extraEnv = {}) {
594
- const runner = path.join(projectRoot, "bin", "ufoo.js");
598
+ const runner = resolveUfooRunnerPath();
595
599
  const logDir = getUfooPaths(projectRoot).runDir;
596
600
  fs.mkdirSync(logDir, { recursive: true });
597
601
 
@@ -1,3 +1,5 @@
1
+ const { finalizeRouterPayload } = require("../controller/routerFinalize");
2
+
1
3
  function normalizePayload(payload) {
2
4
  if (!payload || typeof payload !== "object") {
3
5
  return { reply: "", dispatch: [], ops: [] };
@@ -36,34 +38,23 @@ function stripAssistantCall(payload) {
36
38
  async function finalizePromptRun({
37
39
  projectRoot,
38
40
  payload,
41
+ prompt,
39
42
  processManager,
40
43
  dispatchMessages,
41
44
  handleOps,
42
45
  markPending,
43
46
  finalizeLocally = true,
44
47
  }) {
45
- if (finalizeLocally === false) {
46
- return {
47
- ok: true,
48
- payload,
49
- opsResults: [],
50
- };
51
- }
52
-
53
- for (const item of payload.dispatch || []) {
54
- if (item && item.target && item.target !== "broadcast") {
55
- markPending(item.target);
56
- }
57
- }
58
-
59
- await dispatchMessages(projectRoot, payload.dispatch || []);
60
- const opsResults = await handleOps(projectRoot, payload.ops || [], processManager);
61
-
62
- return {
63
- ok: true,
48
+ return finalizeRouterPayload({
49
+ projectRoot,
64
50
  payload,
65
- opsResults,
66
- };
51
+ prompt,
52
+ processManager,
53
+ dispatchMessages,
54
+ handleOps,
55
+ markPending,
56
+ finalizeLocally,
57
+ });
67
58
  }
68
59
 
69
60
  async function runPromptWithAssistant({
@@ -129,6 +120,7 @@ async function runPromptWithAssistant({
129
120
  return finalizePromptRun({
130
121
  projectRoot,
131
122
  payload: firstPayload,
123
+ prompt: prompt || "",
132
124
  processManager,
133
125
  dispatchMessages,
134
126
  handleOps,