u-foo 2.3.8 → 2.3.10

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.8",
3
+ "version": "2.3.10",
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",
@@ -20,6 +20,7 @@ const { buildUpstreamAuthFromCredential } = require("./credentials");
20
20
  const { listToolsForCallerTier, CALLER_TIERS } = require("../tools");
21
21
  const { redactToolCallPayload, redactSecrets } = require("../providerapi/redactor");
22
22
  const { buildCachedMemoryPrefix } = require("../memory");
23
+ const { shouldForwardStreamToPublisher } = require("./publisherRouting");
23
24
 
24
25
  function sleep(ms) {
25
26
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -151,10 +152,12 @@ async function handleEvent(
151
152
  const publisher = evt.publisher || "unknown";
152
153
  const sandbox = "workspace-write";
153
154
  const streamState = { emitted: false, lastChar: "" };
155
+ const streamToPublisher = shouldForwardStreamToPublisher(projectRoot, publisher);
154
156
 
155
157
  const emitStreamDelta = (delta) => {
156
158
  const text = String(delta || "");
157
159
  if (!text) return;
160
+ if (!streamToPublisher) return;
158
161
  streamState.emitted = true;
159
162
  streamState.lastChar = text.slice(-1);
160
163
  busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: text }));
@@ -168,6 +171,7 @@ async function handleEvent(
168
171
  prompt,
169
172
  busSender,
170
173
  emitStreamDelta,
174
+ streamToPublisher,
171
175
  threadRuntime,
172
176
  });
173
177
  if (!threadedResult || !threadedResult.fallbackToLegacy) {
@@ -248,22 +252,33 @@ async function handleThreadedEvent({
248
252
  prompt,
249
253
  busSender,
250
254
  emitStreamDelta,
255
+ streamToPublisher = true,
251
256
  threadRuntime,
252
257
  }) {
253
258
  try {
259
+ const plainReplyParts = [];
254
260
  for await (const event of threadRuntime.thread.runStreamed(prompt, {})) {
255
261
  if (!event || typeof event !== "object") continue;
256
262
  if (event.type === "text_delta" && event.delta) {
257
- emitStreamDelta(event.delta);
263
+ if (streamToPublisher) {
264
+ emitStreamDelta(event.delta);
265
+ } else {
266
+ plainReplyParts.push(String(event.delta));
267
+ }
258
268
  } else if (event.type === "turn_failed") {
259
269
  throw new Error(event.error || `thread turn failed for ${agentType}`);
260
270
  }
261
271
  }
262
272
 
263
- busSender.enqueue(
264
- publisher,
265
- JSON.stringify({ stream: true, done: true, reason: "complete" })
266
- );
273
+ if (streamToPublisher) {
274
+ busSender.enqueue(
275
+ publisher,
276
+ JSON.stringify({ stream: true, done: true, reason: "complete" })
277
+ );
278
+ } else {
279
+ const reply = plainReplyParts.join("").trim();
280
+ if (reply) busSender.enqueue(publisher, reply);
281
+ }
267
282
  await busSender.flush();
268
283
  } catch (err) {
269
284
  if (shouldFallbackToLegacyThreadProvider(err, provider)) {
@@ -272,17 +287,19 @@ async function handleThreadedEvent({
272
287
  if (threadRuntime && typeof threadRuntime.rebuildThread === "function") {
273
288
  await threadRuntime.rebuildThread();
274
289
  }
275
- busSender.enqueue(
276
- publisher,
277
- JSON.stringify({
278
- stream: true,
279
- delta: `[internal:${agentType}] error: ${err && err.message ? err.message : "unknown error"}`,
280
- })
281
- );
282
- busSender.enqueue(
283
- publisher,
284
- JSON.stringify({ stream: true, done: true, reason: "error" })
285
- );
290
+ const errorText = `[internal:${agentType}] error: ${err && err.message ? err.message : "unknown error"}`;
291
+ if (streamToPublisher) {
292
+ busSender.enqueue(
293
+ publisher,
294
+ JSON.stringify({ stream: true, delta: errorText })
295
+ );
296
+ busSender.enqueue(
297
+ publisher,
298
+ JSON.stringify({ stream: true, done: true, reason: "error" })
299
+ );
300
+ } else {
301
+ busSender.enqueue(publisher, errorText);
302
+ }
286
303
  await busSender.flush();
287
304
  return { fallbackToLegacy: false };
288
305
  }
@@ -7,6 +7,11 @@ const { PTY_SOCKET_MESSAGE_TYPES, PTY_SOCKET_SUBSCRIBE_MODES } = require("../sha
7
7
  const { getUfooPaths } = require("../ufoo/paths");
8
8
  const { ActivityDetector } = require("./activityDetector");
9
9
  const { createActivityStatePublisher } = require("./activityStatePublisher");
10
+ const {
11
+ parseStreamEnvelope,
12
+ shouldAutoReplyFromPtyToPublisher,
13
+ shouldForwardStreamToPublisher,
14
+ } = require("./publisherRouting");
10
15
 
11
16
  function sleep(ms) {
12
17
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -64,11 +69,14 @@ function drainQueue(queueFile) {
64
69
  }
65
70
 
66
71
  function stripAnsi(text) {
67
- return text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
72
+ return text
73
+ .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
74
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
68
75
  }
69
76
 
70
77
  function parseInputMessage(message) {
71
78
  if (!message) return { raw: false, text: "" };
79
+ if (parseStreamEnvelope(message)) return null;
72
80
  try {
73
81
  const parsed = JSON.parse(message);
74
82
  if (parsed && typeof parsed === "object") {
@@ -201,6 +209,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
201
209
  const watchdogMs = 120000;
202
210
  const maxQueue = 200;
203
211
  let sendQueue = Promise.resolve();
212
+ const streamPublisherCache = new Map();
204
213
  const DROP_LINE_PATTERNS = [
205
214
  /__UFOO_DONE_/,
206
215
  /请在完成后输出以下标记/,
@@ -208,6 +217,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
208
217
  /esc to interrupt/i,
209
218
  /for shortcuts/i,
210
219
  /Preparing to run session start commands/i,
220
+ /^[•\s]*(working|thinking|loading|reading|editing|running|checking)[•\s]*$/i,
211
221
  ];
212
222
 
213
223
  function shouldDropLine(line) {
@@ -236,6 +246,33 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
236
246
  });
237
247
  }
238
248
 
249
+ function canStreamToPublisher(target) {
250
+ if (!target) return false;
251
+ if (streamPublisherCache.has(target)) return streamPublisherCache.get(target);
252
+ const result = shouldForwardStreamToPublisher(projectRoot, target);
253
+ streamPublisherCache.set(target, result);
254
+ return result;
255
+ }
256
+
257
+ function completePublisherResponse(reason, fallbackNote = "") {
258
+ if (!currentPublisher) return;
259
+ if (flushTimer) {
260
+ clearTimeout(flushTimer);
261
+ flushTimer = null;
262
+ }
263
+ if (!shouldAutoReplyFromPtyToPublisher(projectRoot, currentPublisher)) {
264
+ outputBuffer = "";
265
+ return;
266
+ }
267
+ if (outputBuffer) {
268
+ const remaining = outputBuffer;
269
+ outputBuffer = "";
270
+ deliverChunk(remaining);
271
+ }
272
+ if (fallbackNote) enqueueSend(currentPublisher, fallbackNote);
273
+ enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason }));
274
+ }
275
+
239
276
  // TTY view subscribers (same protocol as launcher inject.sock)
240
277
  const outputSubscribers = new Set();
241
278
  let term = null;
@@ -550,6 +587,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
550
587
  if (!currentPublisher || pendingOutput.length === 0) return;
551
588
  const chunks = pendingOutput;
552
589
  pendingOutput = [];
590
+ if (!canStreamToPublisher(currentPublisher)) return;
553
591
  for (const chunk of chunks) {
554
592
  enqueueSend(currentPublisher, chunk);
555
593
  }
@@ -561,7 +599,9 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
561
599
  if (!cleaned) return;
562
600
  const payload = JSON.stringify({ stream: true, delta: cleaned });
563
601
  if (currentPublisher) {
564
- enqueueSend(currentPublisher, payload);
602
+ if (canStreamToPublisher(currentPublisher)) {
603
+ enqueueSend(currentPublisher, payload);
604
+ }
565
605
  } else {
566
606
  pendingOutput.push(payload);
567
607
  if (pendingOutput.length > 50) pendingOutput.shift();
@@ -631,7 +671,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
631
671
  watchdogTimer = null;
632
672
  }
633
673
  if (currentPublisher) {
634
- enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "idle" }));
674
+ completePublisherResponse("idle");
635
675
  }
636
676
  busy = false;
637
677
  currentPublisher = "";
@@ -681,7 +721,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
681
721
  deliverChunk(before);
682
722
  }
683
723
  if (currentPublisher) {
684
- enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "marker" }));
724
+ completePublisherResponse("marker");
685
725
  }
686
726
  currentMarker = "";
687
727
  busy = false;
@@ -722,7 +762,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
722
762
  idleTimer = setTimeout(() => {
723
763
  idleTimer = null;
724
764
  if (currentPublisher) {
725
- enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "idle" }));
765
+ completePublisherResponse("idle");
726
766
  }
727
767
  busy = false;
728
768
  activityDetector.markIdle();
@@ -758,7 +798,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
758
798
  watchdogTimer = null;
759
799
  }
760
800
  const note = `[internal-pty] process exited code=${exitCode} signal=${signal || ""}`.trim();
761
- if (currentPublisher) enqueueSend(currentPublisher, note);
801
+ if (currentPublisher) completePublisherResponse("exit", note);
762
802
  logNote(note);
763
803
 
764
804
  // Reset busy state
@@ -962,9 +1002,8 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
962
1002
  watchdogTimer = null;
963
1003
  if (!busy) return;
964
1004
  const timeoutNote = `[internal-pty] marker timeout; restarting PTY`;
965
- if (currentPublisher) enqueueSend(currentPublisher, timeoutNote);
966
1005
  if (currentPublisher) {
967
- enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "timeout" }));
1006
+ completePublisherResponse("timeout", timeoutNote);
968
1007
  }
969
1008
  logNote(timeoutNote);
970
1009
  restartPty("marker timeout");
@@ -1013,7 +1052,9 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
1013
1052
  }
1014
1053
  for (const evt of events) {
1015
1054
  if (!evt || !evt.data || typeof evt.data.message !== "string") continue;
1016
- const { raw, text } = parseInputMessage(evt.data.message);
1055
+ const input = parseInputMessage(evt.data.message);
1056
+ if (!input) continue;
1057
+ const { raw, text } = input;
1017
1058
  if (messageQueue.length >= maxQueue) {
1018
1059
  messageQueue.shift();
1019
1060
  }
@@ -1030,4 +1071,4 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
1030
1071
  }
1031
1072
  }
1032
1073
 
1033
- module.exports = { runPtyRunner };
1074
+ module.exports = { parseInputMessage, runPtyRunner };
@@ -0,0 +1,59 @@
1
+ const fs = require("fs");
2
+ const { getUfooPaths } = require("../ufoo/paths");
3
+
4
+ function normalizePublisher(publisher) {
5
+ if (typeof publisher === "string") return publisher.trim();
6
+ if (publisher && typeof publisher === "object") {
7
+ return String(publisher.subscriber || publisher.id || publisher.nickname || "").trim();
8
+ }
9
+ return String(publisher || "").trim();
10
+ }
11
+
12
+ function readAgents(projectRoot) {
13
+ try {
14
+ const parsed = JSON.parse(fs.readFileSync(getUfooPaths(projectRoot).agentsFile, "utf8"));
15
+ return parsed && typeof parsed === "object" && parsed.agents && typeof parsed.agents === "object"
16
+ ? parsed.agents
17
+ : {};
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+
23
+ function isManagedAgentPublisher(projectRoot, publisher) {
24
+ const id = normalizePublisher(publisher);
25
+ if (!id) return false;
26
+ const agents = readAgents(projectRoot);
27
+ return Boolean(agents[id]);
28
+ }
29
+
30
+ function shouldForwardStreamToPublisher(projectRoot, publisher) {
31
+ const id = normalizePublisher(publisher);
32
+ if (!id) return false;
33
+ return !isManagedAgentPublisher(projectRoot, id);
34
+ }
35
+
36
+ function shouldAutoReplyFromPtyToPublisher(projectRoot, publisher) {
37
+ return shouldForwardStreamToPublisher(projectRoot, publisher);
38
+ }
39
+
40
+ function parseStreamEnvelope(message) {
41
+ if (typeof message !== "string" || !message.trim()) return null;
42
+ try {
43
+ const parsed = JSON.parse(message);
44
+ if (parsed && typeof parsed === "object" && parsed.stream === true) {
45
+ return parsed;
46
+ }
47
+ } catch {
48
+ // Not JSON.
49
+ }
50
+ return null;
51
+ }
52
+
53
+ module.exports = {
54
+ isManagedAgentPublisher,
55
+ normalizePublisher,
56
+ parseStreamEnvelope,
57
+ shouldAutoReplyFromPtyToPublisher,
58
+ shouldForwardStreamToPublisher,
59
+ };