u-foo 2.5.1 → 2.5.2

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.5.1",
3
+ "version": "2.5.2",
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",
@@ -16,6 +16,7 @@ const { redactToolCallPayload, redactSecrets } = require("../../runtime/privacy/
16
16
  const { buildCachedMemoryPrefix } = require("../../coordination/memory");
17
17
  const { normalizePublisher, shouldForwardStreamToPublisher } = require("../launch/publisherRouting");
18
18
  const { appendAgentRegistryDiagnostic } = require("../../coordination/state/agentRegistryDiagnostics");
19
+ const { DeliveryQueue } = require("../../coordination/bus/deliveryQueue");
19
20
  const {
20
21
  buildDefaultStartupBootstrapPrompt,
21
22
  isValueForCodexOption,
@@ -216,37 +217,15 @@ function shouldStreamReplyToPublisher(projectRoot, publisher, evt = {}) {
216
217
  return shouldForwardStreamToPublisher(projectRoot, publisher);
217
218
  }
218
219
 
219
- function drainQueue(queueFile) {
220
- if (!fs.existsSync(queueFile)) return [];
221
- const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
222
- let content = "";
223
- let readOk = false;
224
- try {
225
- fs.renameSync(queueFile, processingFile);
226
- content = fs.readFileSync(processingFile, "utf8");
227
- readOk = true;
228
- } catch {
229
- try {
230
- if (fs.existsSync(processingFile)) {
231
- fs.renameSync(processingFile, queueFile);
232
- }
233
- } catch {
234
- // ignore rollback errors
235
- }
236
- return [];
237
- } finally {
238
- if (readOk) {
239
- try {
240
- if (fs.existsSync(processingFile)) {
241
- fs.rmSync(processingFile, { force: true });
242
- }
243
- } catch {
244
- // ignore cleanup errors
245
- }
246
- }
220
+ function claimQueuedEvents(queueFile) {
221
+ const queue = new DeliveryQueue(queueFile);
222
+ const claims = [];
223
+ while (true) {
224
+ const claim = queue.claimNext();
225
+ if (!claim) break;
226
+ claims.push({ ...claim, queue });
247
227
  }
248
- if (!content.trim()) return [];
249
- return content.split(/\r?\n/).filter(Boolean);
228
+ return claims;
250
229
  }
251
230
 
252
231
  function parseAgentViewRawInput(message) {
@@ -835,67 +814,67 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
835
814
  if (!processing) {
836
815
  processing = true;
837
816
  try {
838
- const lines = drainQueue(queueFile);
839
- if (lines.length > 0) {
840
- const events = [];
841
- for (const line of lines) {
842
- try {
843
- events.push(JSON.parse(line));
844
- } catch {
845
- // ignore malformed line
846
- }
847
- }
817
+ const claims = claimQueuedEvents(queueFile);
818
+ if (claims.length > 0) {
819
+ let handledAny = false;
848
820
 
849
- const runnableEvents = [];
850
- for (const evt of events) {
821
+ for (const claim of claims) {
822
+ const evt = claim.event;
823
+ const runnableEvents = [];
851
824
  const rawInput = parseAgentViewRawInput(evt && evt.data ? evt.data.message : "");
852
825
  if (rawInput === null) {
853
826
  runnableEvents.push(evt);
854
- continue;
827
+ } else {
828
+ const session = getInteractiveSession(evt.publisher || "unknown");
829
+ const submissions = session.handleRaw(rawInput);
830
+ for (const message of submissions) {
831
+ runnableEvents.push({
832
+ ...evt,
833
+ __agentViewRaw: true,
834
+ data: {
835
+ ...(evt.data || {}),
836
+ message,
837
+ },
838
+ });
839
+ }
855
840
  }
856
841
 
857
- const session = getInteractiveSession(evt.publisher || "unknown");
858
- const submissions = session.handleRaw(rawInput);
859
- for (const message of submissions) {
860
- runnableEvents.push({
861
- ...evt,
862
- __agentViewRaw: true,
863
- data: {
864
- ...(evt.data || {}),
865
- message,
866
- },
867
- });
842
+ if (runnableEvents.length > 0) {
843
+ activityTracker.notifyTurnStart("thinking");
868
844
  }
869
- }
870
-
871
- if (runnableEvents.length > 0) {
872
- activityTracker.notifyTurnStart("thinking");
873
- }
874
845
 
875
- for (const evt of runnableEvents) {
876
- // eslint-disable-next-line no-await-in-loop
877
- await handleEvent(
878
- projectRoot,
879
- parsedAgentType,
880
- provider,
881
- model,
882
- subscriber,
883
- nickname,
884
- evt,
885
- busSender,
886
- bootstrap.extraArgs,
887
- threadRuntime,
888
- bootstrap.promptText,
889
- activityTracker
890
- );
891
- if (evt.__agentViewRaw) {
892
- getInteractiveSession(evt.publisher || "unknown").writeResponsePrompt();
846
+ try {
847
+ for (const runnableEvent of runnableEvents) {
848
+ // eslint-disable-next-line no-await-in-loop
849
+ await handleEvent(
850
+ projectRoot,
851
+ parsedAgentType,
852
+ provider,
853
+ model,
854
+ subscriber,
855
+ nickname,
856
+ runnableEvent,
857
+ busSender,
858
+ bootstrap.extraArgs,
859
+ threadRuntime,
860
+ bootstrap.promptText,
861
+ activityTracker
862
+ );
863
+ if (runnableEvent.__agentViewRaw) {
864
+ getInteractiveSession(runnableEvent.publisher || "unknown").writeResponsePrompt();
865
+ }
866
+ }
867
+ claim.queue.completeClaim(claim);
868
+ if (runnableEvents.length > 0) handledAny = true;
869
+ } catch (err) {
870
+ claim.queue.restoreClaim(claim);
871
+ throw err;
893
872
  }
894
873
  }
895
874
  // 处理消息后更新心跳
896
875
  updateHeartbeat();
897
876
  lastHeartbeat = now;
898
- if (runnableEvents.length > 0) {
877
+ if (handledAny) {
899
878
  activityTracker.markIdle();
900
879
  }
901
880
  await busSender.flush();
@@ -9,8 +9,8 @@ const { shakeTerminalByTty } = require("../../coordination/bus/shake");
9
9
  const { isITerm2 } = require("../../runtime/terminal/detect");
10
10
  const iterm2 = require("../../runtime/terminal/iterm2");
11
11
  const { createActivityStatePublisher } = require("../activity/activityStatePublisher");
12
- const { INJECTION_MODES, getInjectionModeFromEvent } = require("../../coordination/bus/messageMeta");
13
12
  const { buildPromptInjectionText } = require("../../coordination/bus/promptEnvelope");
13
+ const { DeliveryQueue } = require("../../coordination/bus/deliveryQueue");
14
14
 
15
15
  /**
16
16
  * Agent 消息通知监听器
@@ -41,6 +41,7 @@ class AgentNotifier {
41
41
  safeSub,
42
42
  "pending.jsonl"
43
43
  );
44
+ this.deliveryQueue = new DeliveryQueue(this.queueFile);
44
45
  this.agentsFile = paths.agentsFile;
45
46
 
46
47
  // 初始化 injector
@@ -187,45 +188,6 @@ class AgentNotifier {
187
188
  }
188
189
  }
189
190
 
190
- drainPending() {
191
- if (!fs.existsSync(this.queueFile)) return [];
192
- const processingFile = `${this.queueFile}.processing.${process.pid}.${Date.now()}`;
193
- let content = "";
194
- let readOk = false;
195
- try {
196
- fs.renameSync(this.queueFile, processingFile);
197
- content = fs.readFileSync(processingFile, "utf8");
198
- readOk = true;
199
- } catch {
200
- try {
201
- if (fs.existsSync(processingFile)) {
202
- fs.renameSync(processingFile, this.queueFile);
203
- }
204
- } catch {
205
- // ignore rollback errors
206
- }
207
- return [];
208
- } finally {
209
- if (readOk) {
210
- try {
211
- if (fs.existsSync(processingFile)) {
212
- fs.rmSync(processingFile, { force: true });
213
- }
214
- } catch {
215
- // ignore cleanup errors
216
- }
217
- }
218
- }
219
- if (!content.trim()) return [];
220
- return content.split(/\r?\n/).filter(Boolean).map((line) => {
221
- try {
222
- return JSON.parse(line);
223
- } catch {
224
- return null;
225
- }
226
- }).filter(Boolean);
227
- }
228
-
229
191
  normalizePublisher(publisher) {
230
192
  if (!publisher) return "";
231
193
  if (typeof publisher === "string") return publisher;
@@ -272,57 +234,41 @@ class AgentNotifier {
272
234
  return 0;
273
235
  }
274
236
 
275
- const events = this.drainPending();
276
- if (events.length === 0) return 0;
277
- const requeue = [];
278
- let delivered = 0;
279
- let consumedOne = false;
280
- for (const evt of events) {
281
- if (!evt || evt.event !== "message" || !evt.data || typeof evt.data.message !== "string") {
282
- continue;
283
- }
284
- const injectionMode = getInjectionModeFromEvent(evt, INJECTION_MODES.IMMEDIATE);
285
- const activityState = this.getCurrentActivityState();
286
- if (injectionMode === INJECTION_MODES.QUEUED && this.isBusyState(activityState)) {
287
- requeue.push(evt);
288
- continue;
289
- }
290
- if (consumedOne) {
291
- requeue.push(evt);
292
- continue;
293
- }
294
- const message = buildPromptInjectionText(evt, this.subscriber, this.getAgentsMap());
295
- try {
296
- // Inject the prompt-facing text into the terminal/tmux agent
297
- // (Bus is the source of truth; inject is the delivery adapter.)
298
- // eslint-disable-next-line no-await-in-loop
299
- await this.injector.inject(this.subscriber, message);
300
- delivered += 1;
301
- consumedOne = true;
302
- this.injectFailCount = 0;
303
- this.updateActivityState("working");
304
- // eslint-disable-next-line no-await-in-loop
305
- await this.emitDelivery(evt, "ok");
306
- } catch (err) {
307
- consumedOne = true;
308
- this.injectFailCount += 1;
309
- requeue.push(evt);
310
- // eslint-disable-next-line no-await-in-loop
311
- await this.emitDelivery(evt, "error", err.message || "inject failed");
312
- }
237
+ if (this.isBusyState(this.getCurrentActivityState())) {
238
+ return 0;
313
239
  }
314
- if (requeue.length > 0) {
315
- try {
316
- const content = requeue.map((e) => JSON.stringify(e)).join("\n") + "\n";
317
- fs.appendFileSync(this.queueFile, content, "utf8");
318
- } catch {
319
- // ignore requeue failures
320
- }
240
+
241
+ const claim = this.deliveryQueue.claimNext();
242
+ if (!claim) return 0;
243
+
244
+ const evt = claim.event;
245
+ if (!evt || evt.event !== "message" || !evt.data || typeof evt.data.message !== "string") {
246
+ this.deliveryQueue.completeClaim(claim);
247
+ return 0;
248
+ }
249
+
250
+ const activityState = this.getCurrentActivityState();
251
+ if (this.isBusyState(activityState)) {
252
+ this.deliveryQueue.restoreClaim(claim);
253
+ return 0;
321
254
  }
322
- if (delivered > 0) {
255
+
256
+ const message = buildPromptInjectionText(evt, this.subscriber, this.getAgentsMap());
257
+ try {
258
+ // Inject the prompt-facing text into the terminal/tmux agent.
259
+ await this.injector.inject(this.subscriber, message);
260
+ this.deliveryQueue.completeClaim(claim);
261
+ this.injectFailCount = 0;
262
+ this.updateActivityState("working");
263
+ await this.emitDelivery(evt, "ok");
323
264
  this.lastWorkingAt = Date.now();
265
+ return 1;
266
+ } catch (err) {
267
+ this.injectFailCount += 1;
268
+ this.deliveryQueue.restoreClaim(claim);
269
+ await this.emitDelivery(evt, "error", err.message || "inject failed");
270
+ return 0;
324
271
  }
325
- return delivered;
326
272
  }
327
273
 
328
274
  /**
@@ -367,32 +313,10 @@ class AgentNotifier {
367
313
  if (currentCount > this.lastCount) {
368
314
  const newCount = currentCount - this.lastCount;
369
315
  this.notify(newCount);
370
-
371
- // 自动触发终端输入(非阻塞)
372
- this.autoTriggerInput().catch(() => {
373
- // 忽略触发失败
374
- });
375
316
  }
376
317
 
377
- // Ensure pending delivery happens even if count doesn't change
378
- if (this.autoTrigger && currentCount > 0) {
379
- if (this.isUfooCodeSubscriber()) {
380
- if (this.lastUbusWakeCount !== currentCount) {
381
- try {
382
- await this.autoTriggerInput();
383
- this.lastUbusWakeCount = currentCount;
384
- } catch {
385
- // ignore delivery errors
386
- }
387
- }
388
- } else {
389
- try {
390
- await this.deliverPending();
391
- } catch {
392
- // ignore delivery errors
393
- }
394
- }
395
- }
318
+ // Delivery is owned by the project daemon scheduler. The notifier keeps
319
+ // terminal notifications, title badges, heartbeat, and activity fallback.
396
320
  if (currentCount <= 0) {
397
321
  this.lastUbusWakeCount = -1;
398
322
  }
@@ -17,6 +17,7 @@ const {
17
17
  resolveDefaultManualBootstrap,
18
18
  } = require("../prompts/defaultBootstrap");
19
19
  const { buildPromptInjectionText } = require("../../coordination/bus/promptEnvelope");
20
+ const { DeliveryQueue } = require("../../coordination/bus/deliveryQueue");
20
21
 
21
22
  function sleep(ms) {
22
23
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -40,37 +41,15 @@ function safeSubscriber(subscriber) {
40
41
  return subscriber.replace(/:/g, "_");
41
42
  }
42
43
 
43
- function drainQueue(queueFile) {
44
- if (!fs.existsSync(queueFile)) return [];
45
- const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
46
- let content = "";
47
- let readOk = false;
48
- try {
49
- fs.renameSync(queueFile, processingFile);
50
- content = fs.readFileSync(processingFile, "utf8");
51
- readOk = true;
52
- } catch {
53
- try {
54
- if (fs.existsSync(processingFile)) {
55
- fs.renameSync(processingFile, queueFile);
56
- }
57
- } catch {
58
- // ignore rollback errors
59
- }
60
- return [];
61
- } finally {
62
- if (readOk) {
63
- try {
64
- if (fs.existsSync(processingFile)) {
65
- fs.rmSync(processingFile, { force: true });
66
- }
67
- } catch {
68
- // ignore cleanup errors
69
- }
70
- }
44
+ function claimQueuedEvents(queueFile) {
45
+ const queue = new DeliveryQueue(queueFile);
46
+ const claims = [];
47
+ while (true) {
48
+ const claim = queue.claimNext();
49
+ if (!claim) break;
50
+ claims.push({ ...claim, queue });
71
51
  }
72
- if (!content.trim()) return [];
73
- return content.split(/\r?\n/).filter(Boolean);
52
+ return claims;
74
53
  }
75
54
 
76
55
  function stripAnsi(text) {
@@ -1044,28 +1023,29 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
1044
1023
  lastHeartbeat = now;
1045
1024
  }
1046
1025
 
1047
- const lines = drainQueue(queueFile);
1048
- if (lines.length > 0) {
1026
+ const claims = claimQueuedEvents(queueFile);
1027
+ if (claims.length > 0) {
1049
1028
  const agents = readAgentsMap(agentsFilePath);
1050
- const events = [];
1051
- for (const line of lines) {
1029
+ for (const claim of claims) {
1030
+ const evt = claim.event;
1052
1031
  try {
1053
- events.push(JSON.parse(line));
1032
+ const input = buildPtyInputFromEvent(evt, subscriber, agents);
1033
+ if (!input) {
1034
+ claim.queue.completeClaim(claim);
1035
+ continue;
1036
+ }
1037
+ const { raw, text } = input;
1038
+ if (messageQueue.length >= maxQueue) {
1039
+ messageQueue.shift();
1040
+ }
1041
+ const publisher = typeof evt.publisher === "object" && evt.publisher
1042
+ ? (evt.publisher.subscriber || evt.publisher.nickname || "unknown")
1043
+ : (evt.publisher || "unknown");
1044
+ messageQueue.push({ publisher, raw, text });
1045
+ claim.queue.completeClaim(claim);
1054
1046
  } catch {
1055
- // ignore malformed line
1056
- }
1057
- }
1058
- for (const evt of events) {
1059
- const input = buildPtyInputFromEvent(evt, subscriber, agents);
1060
- if (!input) continue;
1061
- const { raw, text } = input;
1062
- if (messageQueue.length >= maxQueue) {
1063
- messageQueue.shift();
1047
+ claim.queue.restoreClaim(claim);
1064
1048
  }
1065
- const publisher = typeof evt.publisher === "object" && evt.publisher
1066
- ? (evt.publisher.subscriber || evt.publisher.nickname || "unknown")
1067
- : (evt.publisher || "unknown");
1068
- messageQueue.push({ publisher, raw, text });
1069
1049
  }
1070
1050
  }
1071
1051
  processQueue();
@@ -39,6 +39,18 @@ function parseSendArgs(cmdArgs = []) {
39
39
  };
40
40
  }
41
41
 
42
+ function resolvePollSubscriber(cmdArgs = [], env = process.env) {
43
+ const positionals = cmdArgs.filter((arg) => typeof arg === "string" && !arg.startsWith("--"));
44
+ const subscriber = String(positionals[0] || env.UFOO_SUBSCRIBER_ID || "").trim();
45
+ if (!subscriber) {
46
+ throw new Error("poll requires [subscriber] or UFOO_SUBSCRIBER_ID");
47
+ }
48
+ return {
49
+ subscriber,
50
+ autoAck: cmdArgs.includes("--ack") || cmdArgs.includes("--auto-ack"),
51
+ };
52
+ }
53
+
42
54
  async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
43
55
  switch (cmd) {
44
56
  case "init":
@@ -76,6 +88,12 @@ async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
76
88
  case "check":
77
89
  await eventBus.check(cmdArgs[0]);
78
90
  return {};
91
+ case "poll":
92
+ {
93
+ const parsed = resolvePollSubscriber(cmdArgs);
94
+ await eventBus.check(parsed.subscriber, parsed.autoAck);
95
+ }
96
+ return {};
79
97
  case "ack":
80
98
  await eventBus.ack(cmdArgs[0]);
81
99
  return {};
@@ -99,4 +117,4 @@ async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
99
117
  }
100
118
  }
101
119
 
102
- module.exports = { runBusCoreCommand };
120
+ module.exports = { runBusCoreCommand, resolvePollSubscriber };
@@ -1524,32 +1524,6 @@ async function runCli(argv) {
1524
1524
  process.exitCode = 1;
1525
1525
  });
1526
1526
  });
1527
- bus
1528
- .command("daemon")
1529
- .description("Start/stop daemon that auto-injects /bus into terminals")
1530
- .option("--interval <n>", "Poll interval in seconds", "2")
1531
- .option("--daemon", "Run in background")
1532
- .option("--stop", "Stop running daemon")
1533
- .option("--status", "Check daemon status")
1534
- .action((opts) => {
1535
- const EventBus = require("../../coordination/bus");
1536
- const eventBus = new EventBus(process.cwd());
1537
- (async () => {
1538
- try {
1539
- const interval = parseInt(opts.interval, 10) * 1000 || 2000;
1540
- if (opts.stop) {
1541
- await eventBus.daemon("stop");
1542
- } else if (opts.status) {
1543
- await eventBus.daemon("status");
1544
- } else {
1545
- await eventBus.daemon("start", { background: opts.daemon, interval });
1546
- }
1547
- } catch (err) {
1548
- console.error(err.message);
1549
- process.exitCode = 1;
1550
- }
1551
- })();
1552
- });
1553
1527
  bus
1554
1528
  .command("inject")
1555
1529
  .description("Inject /bus into a Terminal.app tab by subscriber ID")
@@ -1739,6 +1713,7 @@ async function runCli(argv) {
1739
1713
  console.log(" ufoo online send --nickname <name> --text <msg> [--channel <ch>] [--room <id>]");
1740
1714
  console.log(" ufoo online inbox <nickname> [--clear] [--unread]");
1741
1715
  console.log(" ufoo bus wake <target> [--reason <reason>] [--no-shake]");
1716
+ console.log(" ufoo bus poll [subscriber] [--ack]");
1742
1717
  console.log(" ufoo bus <args...> (JS bus implementation)");
1743
1718
  console.log(" ufoo ctx <subcmd> ... (doctor|lint|decisions|sync)");
1744
1719
  console.log(" ufoo history <build|show|prompt> [limit]");
@@ -2377,33 +2352,6 @@ async function runCli(argv) {
2377
2352
  });
2378
2353
  return;
2379
2354
  }
2380
- if (sub === "daemon") {
2381
- // 使用 JavaScript daemon
2382
- const EventBus = require("../../coordination/bus");
2383
- const eventBus = new EventBus(process.cwd());
2384
-
2385
- (async () => {
2386
- try {
2387
- const hasStop = rest.includes("--stop");
2388
- const hasStatus = rest.includes("--status");
2389
- const hasDaemon = rest.includes("--daemon");
2390
- const intervalIdx = rest.indexOf("--interval");
2391
- const interval = intervalIdx !== -1 ? parseInt(rest[intervalIdx + 1], 10) * 1000 : 2000;
2392
-
2393
- if (hasStop) {
2394
- await eventBus.daemon("stop");
2395
- } else if (hasStatus) {
2396
- await eventBus.daemon("status");
2397
- } else {
2398
- await eventBus.daemon("start", { background: hasDaemon, interval });
2399
- }
2400
- } catch (err) {
2401
- console.error(err.message);
2402
- process.exitCode = 1;
2403
- }
2404
- })();
2405
- return;
2406
- }
2407
2355
  if (sub === "inject") {
2408
2356
  // 使用 JavaScript inject
2409
2357
  const EventBus = require("../../coordination/bus");