u-foo 2.3.8 → 2.3.9

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.9",
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,10 @@ 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
+ shouldForwardStreamToPublisher,
13
+ } = require("./publisherRouting");
10
14
 
11
15
  function sleep(ms) {
12
16
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -64,11 +68,14 @@ function drainQueue(queueFile) {
64
68
  }
65
69
 
66
70
  function stripAnsi(text) {
67
- return text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
71
+ return text
72
+ .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
73
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
68
74
  }
69
75
 
70
76
  function parseInputMessage(message) {
71
77
  if (!message) return { raw: false, text: "" };
78
+ if (parseStreamEnvelope(message)) return null;
72
79
  try {
73
80
  const parsed = JSON.parse(message);
74
81
  if (parsed && typeof parsed === "object") {
@@ -185,6 +192,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
185
192
  let suppressEcho = false;
186
193
  let echoMarker = "";
187
194
  let suppressTimer = null;
195
+ let managedReplyBuffer = "";
188
196
  let ptyProcess = null;
189
197
  let restartCount = 0;
190
198
  let lastSpawnTime = 0;
@@ -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,55 @@ 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 appendManagedReply(chunk) {
258
+ const text = String(chunk || "");
259
+ if (!text) return;
260
+ managedReplyBuffer += text;
261
+ if (managedReplyBuffer.length > 40000) {
262
+ managedReplyBuffer = managedReplyBuffer.slice(-40000);
263
+ }
264
+ }
265
+
266
+ function takeManagedReply() {
267
+ const reply = sanitizeChunk(managedReplyBuffer)
268
+ .replace(/\n{3,}/g, "\n\n")
269
+ .trim();
270
+ managedReplyBuffer = "";
271
+ return reply;
272
+ }
273
+
274
+ function completePublisherResponse(reason, fallbackNote = "") {
275
+ if (!currentPublisher) return;
276
+ if (flushTimer) {
277
+ clearTimeout(flushTimer);
278
+ flushTimer = null;
279
+ }
280
+ if (outputBuffer) {
281
+ const remaining = outputBuffer;
282
+ outputBuffer = "";
283
+ deliverChunk(remaining);
284
+ }
285
+ if (canStreamToPublisher(currentPublisher)) {
286
+ if (fallbackNote) enqueueSend(currentPublisher, fallbackNote);
287
+ enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason }));
288
+ return;
289
+ }
290
+ const reply = takeManagedReply();
291
+ if (reply) {
292
+ enqueueSend(currentPublisher, reply);
293
+ } else if (fallbackNote) {
294
+ enqueueSend(currentPublisher, fallbackNote);
295
+ }
296
+ }
297
+
239
298
  // TTY view subscribers (same protocol as launcher inject.sock)
240
299
  const outputSubscribers = new Set();
241
300
  let term = null;
@@ -550,6 +609,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
550
609
  if (!currentPublisher || pendingOutput.length === 0) return;
551
610
  const chunks = pendingOutput;
552
611
  pendingOutput = [];
612
+ if (!canStreamToPublisher(currentPublisher)) return;
553
613
  for (const chunk of chunks) {
554
614
  enqueueSend(currentPublisher, chunk);
555
615
  }
@@ -561,7 +621,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
561
621
  if (!cleaned) return;
562
622
  const payload = JSON.stringify({ stream: true, delta: cleaned });
563
623
  if (currentPublisher) {
564
- enqueueSend(currentPublisher, payload);
624
+ if (canStreamToPublisher(currentPublisher)) {
625
+ enqueueSend(currentPublisher, payload);
626
+ } else {
627
+ appendManagedReply(cleaned);
628
+ }
565
629
  } else {
566
630
  pendingOutput.push(payload);
567
631
  if (pendingOutput.length > 50) pendingOutput.shift();
@@ -631,10 +695,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
631
695
  watchdogTimer = null;
632
696
  }
633
697
  if (currentPublisher) {
634
- enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "idle" }));
698
+ completePublisherResponse("idle");
635
699
  }
636
700
  busy = false;
637
701
  currentPublisher = "";
702
+ managedReplyBuffer = "";
638
703
  processQueue();
639
704
  }
640
705
  });
@@ -681,12 +746,13 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
681
746
  deliverChunk(before);
682
747
  }
683
748
  if (currentPublisher) {
684
- enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "marker" }));
749
+ completePublisherResponse("marker");
685
750
  }
686
751
  currentMarker = "";
687
752
  busy = false;
688
753
  activityDetector.markIdle();
689
754
  currentPublisher = "";
755
+ managedReplyBuffer = "";
690
756
  if (watchdogTimer) {
691
757
  clearTimeout(watchdogTimer);
692
758
  watchdogTimer = null;
@@ -722,11 +788,12 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
722
788
  idleTimer = setTimeout(() => {
723
789
  idleTimer = null;
724
790
  if (currentPublisher) {
725
- enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "idle" }));
791
+ completePublisherResponse("idle");
726
792
  }
727
793
  busy = false;
728
794
  activityDetector.markIdle();
729
795
  currentPublisher = "";
796
+ managedReplyBuffer = "";
730
797
  processQueue();
731
798
  }, idleMs);
732
799
  }
@@ -758,7 +825,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
758
825
  watchdogTimer = null;
759
826
  }
760
827
  const note = `[internal-pty] process exited code=${exitCode} signal=${signal || ""}`.trim();
761
- if (currentPublisher) enqueueSend(currentPublisher, note);
828
+ if (currentPublisher) completePublisherResponse("exit", note);
762
829
  logNote(note);
763
830
 
764
831
  // Reset busy state
@@ -766,6 +833,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
766
833
  activityDetector.markIdle();
767
834
  currentPublisher = "";
768
835
  currentMarker = "";
836
+ managedReplyBuffer = "";
769
837
 
770
838
  // If stop() was called, let the runner exit
771
839
  if (!running) return;
@@ -901,6 +969,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
901
969
  activityDetector.markWorking();
902
970
  currentPublisher = next.publisher;
903
971
  currentMarker = next.marker || "";
972
+ managedReplyBuffer = "";
904
973
  if (suppressTimer) {
905
974
  clearTimeout(suppressTimer);
906
975
  suppressTimer = null;
@@ -962,9 +1031,8 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
962
1031
  watchdogTimer = null;
963
1032
  if (!busy) return;
964
1033
  const timeoutNote = `[internal-pty] marker timeout; restarting PTY`;
965
- if (currentPublisher) enqueueSend(currentPublisher, timeoutNote);
966
1034
  if (currentPublisher) {
967
- enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "timeout" }));
1035
+ completePublisherResponse("timeout", timeoutNote);
968
1036
  }
969
1037
  logNote(timeoutNote);
970
1038
  restartPty("marker timeout");
@@ -972,6 +1040,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
972
1040
  busy = false;
973
1041
  activityDetector.markIdle();
974
1042
  currentPublisher = "";
1043
+ managedReplyBuffer = "";
975
1044
  processQueue();
976
1045
  }, watchdogMs);
977
1046
  }
@@ -1013,7 +1082,9 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
1013
1082
  }
1014
1083
  for (const evt of events) {
1015
1084
  if (!evt || !evt.data || typeof evt.data.message !== "string") continue;
1016
- const { raw, text } = parseInputMessage(evt.data.message);
1085
+ const input = parseInputMessage(evt.data.message);
1086
+ if (!input) continue;
1087
+ const { raw, text } = input;
1017
1088
  if (messageQueue.length >= maxQueue) {
1018
1089
  messageQueue.shift();
1019
1090
  }
@@ -1030,4 +1101,4 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
1030
1101
  }
1031
1102
  }
1032
1103
 
1033
- module.exports = { runPtyRunner };
1104
+ module.exports = { parseInputMessage, runPtyRunner };
@@ -0,0 +1,54 @@
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 parseStreamEnvelope(message) {
37
+ if (typeof message !== "string" || !message.trim()) return null;
38
+ try {
39
+ const parsed = JSON.parse(message);
40
+ if (parsed && typeof parsed === "object" && parsed.stream === true) {
41
+ return parsed;
42
+ }
43
+ } catch {
44
+ // Not JSON.
45
+ }
46
+ return null;
47
+ }
48
+
49
+ module.exports = {
50
+ isManagedAgentPublisher,
51
+ normalizePublisher,
52
+ parseStreamEnvelope,
53
+ shouldForwardStreamToPublisher,
54
+ };