u-foo 2.5.0 → 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/bin/uagy.js +42 -4
- package/bin/uclaude.js +42 -1
- package/bin/ucode.js +42 -1
- package/bin/ucodex.js +42 -1
- package/package.json +1 -1
- package/src/agents/internal/internalRunner.js +57 -78
- package/src/agents/launch/notifier.js +35 -111
- package/src/agents/launch/ptyRunner.js +28 -48
- package/src/agents/prompts/defaultBootstrap.js +16 -1
- package/src/app/cli/busCoreCommands.js +19 -1
- package/src/app/cli/run.js +1 -53
- package/src/code/agent.js +41 -140
- package/src/coordination/bus/deliveryQueue.js +308 -0
- package/src/coordination/bus/index.js +3 -27
- package/src/coordination/bus/message.js +35 -15
- package/src/coordination/bus/queue.js +23 -7
- package/src/runtime/daemon/deliveryScheduler.js +205 -0
- package/src/runtime/daemon/index.js +27 -38
- package/src/runtime/daemon/reportControlBus.js +20 -43
- package/src/coordination/bus/daemon.js +0 -535
|
@@ -21,7 +21,6 @@ const QueueManager = require("./queue");
|
|
|
21
21
|
const SubscriberManager = require("./subscriber");
|
|
22
22
|
const MessageManager = require("./message");
|
|
23
23
|
const NicknameManager = require("./nickname");
|
|
24
|
-
const BusDaemon = require("./daemon");
|
|
25
24
|
const Injector = require("./inject");
|
|
26
25
|
const { BusStore } = require("./store");
|
|
27
26
|
|
|
@@ -263,7 +262,7 @@ class EventBus {
|
|
|
263
262
|
}
|
|
264
263
|
}
|
|
265
264
|
|
|
266
|
-
logError("Not joined to bus.
|
|
265
|
+
logError("Not joined to bus. Launch through ufoo wrappers or register through MCP.");
|
|
267
266
|
return null;
|
|
268
267
|
}
|
|
269
268
|
|
|
@@ -764,10 +763,9 @@ class EventBus {
|
|
|
764
763
|
|
|
765
764
|
if (countAfter > countBefore) {
|
|
766
765
|
await sleep(50);
|
|
767
|
-
|
|
768
|
-
await daemon.injector.inject(target, options.command || "");
|
|
766
|
+
await this.injector.inject(target, options.command || "");
|
|
769
767
|
if (options.shake !== false) {
|
|
770
|
-
const tty =
|
|
768
|
+
const tty = this.injector.readTty(target);
|
|
771
769
|
if (tty) shakeTerminalByTty(tty, { skipFrontmost: true });
|
|
772
770
|
}
|
|
773
771
|
}
|
|
@@ -857,28 +855,6 @@ class EventBus {
|
|
|
857
855
|
}
|
|
858
856
|
}
|
|
859
857
|
|
|
860
|
-
/**
|
|
861
|
-
* Daemon 管理
|
|
862
|
-
*/
|
|
863
|
-
async daemon(action, options = {}) {
|
|
864
|
-
const interval = options.interval || 2000;
|
|
865
|
-
const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, interval, this.projectRoot);
|
|
866
|
-
|
|
867
|
-
switch (action) {
|
|
868
|
-
case "start":
|
|
869
|
-
await daemon.start(options.background || false);
|
|
870
|
-
break;
|
|
871
|
-
case "stop":
|
|
872
|
-
daemon.stop();
|
|
873
|
-
break;
|
|
874
|
-
case "status":
|
|
875
|
-
daemon.status();
|
|
876
|
-
break;
|
|
877
|
-
default:
|
|
878
|
-
throw new Error(`Unknown daemon action: ${action}`);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
858
|
/**
|
|
883
859
|
* 注入命令到订阅者终端
|
|
884
860
|
*/
|
|
@@ -13,6 +13,8 @@ const {
|
|
|
13
13
|
const NicknameManager = require("./nickname");
|
|
14
14
|
const { buildMessageData } = require("./messageMeta");
|
|
15
15
|
const { getUfooPaths } = require("../state/paths");
|
|
16
|
+
const { applyProjectNicknamePrefix } = require("../../runtime/daemon/nicknameScope");
|
|
17
|
+
const { QUEUE_TYPES, normalizeQueueEnvelope } = require("./deliveryQueue");
|
|
16
18
|
|
|
17
19
|
const SEQ_LOCK_TIMEOUT_MS = 5000;
|
|
18
20
|
const SEQ_LOCK_POLL_MS = 25;
|
|
@@ -222,7 +224,19 @@ class MessageManager {
|
|
|
222
224
|
return [byNickname];
|
|
223
225
|
}
|
|
224
226
|
|
|
225
|
-
// 3.
|
|
227
|
+
// 3. Try the project-scoped nickname form. Group prompts intentionally show
|
|
228
|
+
// short names, while older/runtime metadata may only contain scoped names.
|
|
229
|
+
if (this.projectRoot) {
|
|
230
|
+
const scopedTarget = applyProjectNicknamePrefix(this.projectRoot, target);
|
|
231
|
+
if (scopedTarget && scopedTarget !== target) {
|
|
232
|
+
const byScopedNickname = nicknameManager.resolveNickname(scopedTarget);
|
|
233
|
+
if (byScopedNickname) {
|
|
234
|
+
return [byScopedNickname];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 4. 尝试作为代理类型(匹配所有该类型的订阅者)
|
|
226
240
|
const isActive = (meta) => !meta || meta.status === "active";
|
|
227
241
|
|
|
228
242
|
const byType = Object.entries(subscribers)
|
|
@@ -233,7 +247,7 @@ class MessageManager {
|
|
|
233
247
|
return byType;
|
|
234
248
|
}
|
|
235
249
|
|
|
236
|
-
//
|
|
250
|
+
// 5. 通配符(所有活跃订阅者)
|
|
237
251
|
if (target === "*") {
|
|
238
252
|
return Object.entries(subscribers)
|
|
239
253
|
.filter(([, meta]) => isActive(meta))
|
|
@@ -257,7 +271,14 @@ class MessageManager {
|
|
|
257
271
|
if (meta && normalizedTarget === normalizeAgentTypeAlias(meta.agent_type)) return true;
|
|
258
272
|
|
|
259
273
|
// 昵称匹配
|
|
260
|
-
if (meta
|
|
274
|
+
if (meta) {
|
|
275
|
+
const candidates = [target];
|
|
276
|
+
if (this.projectRoot) {
|
|
277
|
+
const scopedTarget = applyProjectNicknamePrefix(this.projectRoot, target);
|
|
278
|
+
if (scopedTarget && scopedTarget !== target) candidates.push(scopedTarget);
|
|
279
|
+
}
|
|
280
|
+
if (candidates.includes(meta.nickname) || candidates.includes(meta.scoped_nickname)) return true;
|
|
281
|
+
}
|
|
261
282
|
|
|
262
283
|
// 通配符
|
|
263
284
|
if (target === "*") return true;
|
|
@@ -426,17 +447,22 @@ class MessageManager {
|
|
|
426
447
|
target,
|
|
427
448
|
data,
|
|
428
449
|
};
|
|
450
|
+
const queueEvent = normalizeQueueEnvelope(event, {
|
|
451
|
+
queueType: QUEUE_TYPES.AGENT_MESSAGE,
|
|
452
|
+
delivery: { mode: "inject", gate: "idle", max_inflight: 1 },
|
|
453
|
+
ack: { policy: "on_delivery" },
|
|
454
|
+
});
|
|
429
455
|
|
|
430
456
|
// 写入事件日志
|
|
431
457
|
const eventFile = path.join(this.eventsDir, `${date}.jsonl`);
|
|
432
|
-
appendJSONL(eventFile,
|
|
458
|
+
appendJSONL(eventFile, queueEvent);
|
|
433
459
|
|
|
434
460
|
// 为每个目标订阅者添加到待处理队列
|
|
435
461
|
for (const targetSubscriber of targets) {
|
|
436
462
|
// 检查订阅者的 offset,如果已经消费过这个 seq,不再添加
|
|
437
463
|
const offset = await this.queueManager.getOffset(targetSubscriber);
|
|
438
464
|
if (seq > offset) {
|
|
439
|
-
await this.queueManager.appendPending(targetSubscriber,
|
|
465
|
+
await this.queueManager.appendPending(targetSubscriber, queueEvent);
|
|
440
466
|
}
|
|
441
467
|
}
|
|
442
468
|
|
|
@@ -473,14 +499,15 @@ class MessageManager {
|
|
|
473
499
|
target,
|
|
474
500
|
data,
|
|
475
501
|
};
|
|
502
|
+
const queueEvent = normalizeQueueEnvelope(event);
|
|
476
503
|
|
|
477
504
|
const eventFile = path.join(this.eventsDir, `${date}.jsonl`);
|
|
478
|
-
appendJSONL(eventFile,
|
|
505
|
+
appendJSONL(eventFile, queueEvent);
|
|
479
506
|
|
|
480
507
|
for (const targetSubscriber of targets) {
|
|
481
508
|
const offset = await this.queueManager.getOffset(targetSubscriber);
|
|
482
509
|
if (seq > offset) {
|
|
483
|
-
await this.queueManager.appendPending(targetSubscriber,
|
|
510
|
+
await this.queueManager.appendPending(targetSubscriber, queueEvent);
|
|
484
511
|
}
|
|
485
512
|
}
|
|
486
513
|
|
|
@@ -499,14 +526,7 @@ class MessageManager {
|
|
|
499
526
|
* 确认消息(清空待处理队列)
|
|
500
527
|
*/
|
|
501
528
|
async ack(subscriber) {
|
|
502
|
-
|
|
503
|
-
const count = pending.length;
|
|
504
|
-
|
|
505
|
-
if (count > 0) {
|
|
506
|
-
await this.queueManager.clearPending(subscriber);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
return count;
|
|
529
|
+
return this.queueManager.ackPending(subscriber);
|
|
510
530
|
}
|
|
511
531
|
|
|
512
532
|
/**
|
|
@@ -3,10 +3,9 @@ const path = require("path");
|
|
|
3
3
|
const {
|
|
4
4
|
subscriberToSafeName,
|
|
5
5
|
ensureDir,
|
|
6
|
-
readJSONL,
|
|
7
|
-
appendJSONL,
|
|
8
6
|
truncateFile,
|
|
9
7
|
} = require("./utils");
|
|
8
|
+
const { DeliveryQueue, stripQueueEnvelope } = require("./deliveryQueue");
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* 队列管理器
|
|
@@ -49,6 +48,10 @@ class QueueManager {
|
|
|
49
48
|
return path.join(this.getQueueDir(subscriber), "pending.jsonl");
|
|
50
49
|
}
|
|
51
50
|
|
|
51
|
+
getDeliveryQueue(subscriber) {
|
|
52
|
+
return new DeliveryQueue(this.getPendingPath(subscriber));
|
|
53
|
+
}
|
|
54
|
+
|
|
52
55
|
/**
|
|
53
56
|
* 获取 tty 文件路径
|
|
54
57
|
*/
|
|
@@ -81,17 +84,15 @@ class QueueManager {
|
|
|
81
84
|
* 读取待处理消息
|
|
82
85
|
*/
|
|
83
86
|
async readPending(subscriber) {
|
|
84
|
-
|
|
85
|
-
return readJSONL(pendingPath);
|
|
87
|
+
return this.getDeliveryQueue(subscriber).readPending().map(stripQueueEnvelope);
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
/**
|
|
89
91
|
* 追加待处理消息
|
|
90
92
|
*/
|
|
91
93
|
async appendPending(subscriber, event) {
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
appendJSONL(pendingPath, event);
|
|
94
|
+
const deliveryQueue = this.getDeliveryQueue(subscriber);
|
|
95
|
+
deliveryQueue.append(event);
|
|
95
96
|
if (event && event.event === "wake") {
|
|
96
97
|
const wakePath = path.join(this.getQueueDir(subscriber), "wake");
|
|
97
98
|
fs.writeFileSync(wakePath, String(event.seq || Date.now()), "utf8");
|
|
@@ -106,6 +107,21 @@ class QueueManager {
|
|
|
106
107
|
truncateFile(pendingPath);
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* 确认待处理消息
|
|
112
|
+
*/
|
|
113
|
+
async ackPending(subscriber) {
|
|
114
|
+
const deliveryQueue = this.getDeliveryQueue(subscriber);
|
|
115
|
+
let count = 0;
|
|
116
|
+
while (true) {
|
|
117
|
+
const claim = deliveryQueue.claimNext();
|
|
118
|
+
if (!claim) break;
|
|
119
|
+
deliveryQueue.completeClaim(claim);
|
|
120
|
+
count += 1;
|
|
121
|
+
}
|
|
122
|
+
return count;
|
|
123
|
+
}
|
|
124
|
+
|
|
109
125
|
/**
|
|
110
126
|
* 检查是否有待处理消息
|
|
111
127
|
*/
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
3
|
+
const { DeliveryQueue } = require("../../coordination/bus/deliveryQueue");
|
|
4
|
+
const Injector = require("../../coordination/bus/inject");
|
|
5
|
+
const { buildPromptInjectionText } = require("../../coordination/bus/promptEnvelope");
|
|
6
|
+
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
7
|
+
const { normalizeQueueEnvelope } = require("../../coordination/bus/deliveryQueue");
|
|
8
|
+
|
|
9
|
+
function asState(value = "") {
|
|
10
|
+
return String(value || "").trim().toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isDeliverableActivityState(value = "") {
|
|
14
|
+
const state = asState(value);
|
|
15
|
+
return state === "idle" || state === "ready";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readAgentsFile(agentsFile) {
|
|
19
|
+
try {
|
|
20
|
+
if (!agentsFile || !fs.existsSync(agentsFile)) return { agents: {} };
|
|
21
|
+
const parsed = JSON.parse(fs.readFileSync(agentsFile, "utf8"));
|
|
22
|
+
if (!parsed || typeof parsed !== "object") return { agents: {} };
|
|
23
|
+
if (!parsed.agents || typeof parsed.agents !== "object") parsed.agents = {};
|
|
24
|
+
return parsed;
|
|
25
|
+
} catch {
|
|
26
|
+
return { agents: {} };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class DeliveryScheduler {
|
|
31
|
+
constructor(projectRoot, options = {}) {
|
|
32
|
+
this.projectRoot = projectRoot;
|
|
33
|
+
this.paths = getUfooPaths(projectRoot);
|
|
34
|
+
this.injector = options.injector || new Injector(this.paths.busDir, this.paths.agentsFile);
|
|
35
|
+
this.queueFactory = typeof options.queueFactory === "function"
|
|
36
|
+
? options.queueFactory
|
|
37
|
+
: (subscriber) => DeliveryQueue.forSubscriber(this.paths.busDir, subscriber);
|
|
38
|
+
this.buildInjectionText = typeof options.buildInjectionText === "function"
|
|
39
|
+
? options.buildInjectionText
|
|
40
|
+
: buildPromptInjectionText;
|
|
41
|
+
this.readAgents = typeof options.readAgents === "function"
|
|
42
|
+
? options.readAgents
|
|
43
|
+
: () => readAgentsFile(this.paths.agentsFile);
|
|
44
|
+
this.emitDelivery = typeof options.emitDelivery === "function"
|
|
45
|
+
? options.emitDelivery
|
|
46
|
+
: async () => {};
|
|
47
|
+
this.log = typeof options.log === "function" ? options.log : () => {};
|
|
48
|
+
this.intervalMs = Number.isFinite(options.intervalMs) && options.intervalMs > 0
|
|
49
|
+
? options.intervalMs
|
|
50
|
+
: 1000;
|
|
51
|
+
this.adapterRouter = options.adapterRouter || createTerminalAdapterRouter();
|
|
52
|
+
this.locks = new Set();
|
|
53
|
+
this.timer = null;
|
|
54
|
+
this.running = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getAgentMeta(subscriber) {
|
|
58
|
+
const data = this.readAgents() || { agents: {} };
|
|
59
|
+
const agents = data.agents && typeof data.agents === "object" ? data.agents : {};
|
|
60
|
+
return {
|
|
61
|
+
agents,
|
|
62
|
+
meta: agents[subscriber] || null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
shouldDeliver(subscriber) {
|
|
67
|
+
const { meta } = this.getAgentMeta(subscriber);
|
|
68
|
+
if (!meta || meta.status === "inactive") {
|
|
69
|
+
return { ok: false, reason: "missing_or_inactive" };
|
|
70
|
+
}
|
|
71
|
+
const launchMode = String(meta.launch_mode || "").trim();
|
|
72
|
+
const adapter = this.adapterRouter.getAdapter({ launchMode, agentId: subscriber, meta });
|
|
73
|
+
if (!adapter.capabilities.supportsNotifierInjector) {
|
|
74
|
+
return { ok: false, reason: launchMode ? "unsupported_launch_mode" : "missing_launch_mode" };
|
|
75
|
+
}
|
|
76
|
+
const activityState = asState(meta.activity_state);
|
|
77
|
+
if (!isDeliverableActivityState(activityState)) {
|
|
78
|
+
return { ok: false, reason: activityState || "unknown_activity_state" };
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, reason: "deliverable" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async deliverSubscriber(subscriber) {
|
|
84
|
+
if (!subscriber) return { ok: false, delivered: 0, reason: "missing_subscriber" };
|
|
85
|
+
if (this.locks.has(subscriber)) {
|
|
86
|
+
return { ok: true, delivered: 0, deferred: true, reason: "locked" };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.locks.add(subscriber);
|
|
90
|
+
try {
|
|
91
|
+
const gate = this.shouldDeliver(subscriber);
|
|
92
|
+
if (!gate.ok) {
|
|
93
|
+
return { ok: true, delivered: 0, deferred: true, reason: gate.reason };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const queue = this.queueFactory(subscriber);
|
|
97
|
+
const claim = queue.claimNext();
|
|
98
|
+
if (!claim) {
|
|
99
|
+
return { ok: true, delivered: 0, reason: "empty" };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const evt = claim.event;
|
|
103
|
+
const envelope = normalizeQueueEnvelope(evt || {});
|
|
104
|
+
const delivery = envelope.delivery || {};
|
|
105
|
+
if (delivery.mode !== "inject") {
|
|
106
|
+
queue.completeClaim(claim);
|
|
107
|
+
return { ok: true, delivered: 0, skipped: true, reason: "unsupported_delivery_mode" };
|
|
108
|
+
}
|
|
109
|
+
if (!envelope || envelope.event !== "message" || !envelope.data || typeof envelope.data.message !== "string") {
|
|
110
|
+
queue.completeClaim(claim);
|
|
111
|
+
return { ok: true, delivered: 0, skipped: true, reason: "unsupported_event" };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (delivery.gate === "idle") {
|
|
115
|
+
const secondGate = this.shouldDeliver(subscriber);
|
|
116
|
+
if (!secondGate.ok) {
|
|
117
|
+
queue.restoreClaim(claim);
|
|
118
|
+
return { ok: true, delivered: 0, deferred: true, reason: secondGate.reason };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const { agents } = this.getAgentMeta(subscriber);
|
|
123
|
+
const injectionText = this.buildInjectionText(envelope, subscriber, agents);
|
|
124
|
+
try {
|
|
125
|
+
await this.injector.inject(subscriber, injectionText);
|
|
126
|
+
queue.completeClaim(claim);
|
|
127
|
+
await this.emitDelivery({
|
|
128
|
+
subscriber,
|
|
129
|
+
event: envelope,
|
|
130
|
+
status: "ok",
|
|
131
|
+
});
|
|
132
|
+
return { ok: true, delivered: 1, event: envelope };
|
|
133
|
+
} catch (err) {
|
|
134
|
+
queue.restoreClaim(claim);
|
|
135
|
+
await this.emitDelivery({
|
|
136
|
+
subscriber,
|
|
137
|
+
event: envelope,
|
|
138
|
+
status: "error",
|
|
139
|
+
error: err && err.message ? err.message : String(err || "inject failed"),
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
delivered: 0,
|
|
144
|
+
reason: "inject_failed",
|
|
145
|
+
error: err && err.message ? err.message : String(err || "inject failed"),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
this.locks.delete(subscriber);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async deliverTargets(targets = []) {
|
|
154
|
+
const unique = Array.from(new Set((Array.isArray(targets) ? targets : [targets]).filter(Boolean)));
|
|
155
|
+
const results = [];
|
|
156
|
+
for (const subscriber of unique) {
|
|
157
|
+
// eslint-disable-next-line no-await-in-loop
|
|
158
|
+
results.push(await this.deliverSubscriber(subscriber));
|
|
159
|
+
}
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
listPendingSubscribers() {
|
|
164
|
+
const data = this.readAgents() || { agents: {} };
|
|
165
|
+
const agents = data.agents && typeof data.agents === "object" ? data.agents : {};
|
|
166
|
+
const subscribers = [];
|
|
167
|
+
for (const [subscriber, meta] of Object.entries(agents)) {
|
|
168
|
+
if (!meta || meta.status === "inactive") continue;
|
|
169
|
+
const queue = this.queueFactory(subscriber);
|
|
170
|
+
if (queue.readPending().length > 0) subscribers.push(subscriber);
|
|
171
|
+
}
|
|
172
|
+
return subscribers;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async tick() {
|
|
176
|
+
const subscribers = this.listPendingSubscribers();
|
|
177
|
+
return this.deliverTargets(subscribers);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
start() {
|
|
181
|
+
if (this.running) return;
|
|
182
|
+
this.running = true;
|
|
183
|
+
const run = () => {
|
|
184
|
+
this.tick().catch((err) => {
|
|
185
|
+
this.log(`delivery scheduler tick failed: ${err && err.message ? err.message : String(err)}`);
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
run();
|
|
189
|
+
this.timer = setInterval(run, this.intervalMs);
|
|
190
|
+
if (this.timer && typeof this.timer.unref === "function") this.timer.unref();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
stop() {
|
|
194
|
+
this.running = false;
|
|
195
|
+
if (this.timer) {
|
|
196
|
+
clearInterval(this.timer);
|
|
197
|
+
this.timer = null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
DeliveryScheduler,
|
|
204
|
+
isDeliverableActivityState,
|
|
205
|
+
};
|
|
@@ -8,6 +8,7 @@ const { buildStatus } = require("./status");
|
|
|
8
8
|
const EventBus = require("../../coordination/bus");
|
|
9
9
|
const { AgentProcessManager } = require("./agentProcessManager");
|
|
10
10
|
const NicknameManager = require("../../coordination/bus/nickname");
|
|
11
|
+
const { DeliveryQueue } = require("../../coordination/bus/deliveryQueue");
|
|
11
12
|
const { generateInstanceId, subscriberToSafeName } = require("../../coordination/bus/utils");
|
|
12
13
|
const { createDaemonIpcServer } = require("./ipcServer");
|
|
13
14
|
const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES, BUS_STATUS_PHASES } = require("../contracts/eventContract");
|
|
@@ -17,6 +18,7 @@ const { scheduleProviderSessionResolve, resolveSessionFromFile, persistProviderS
|
|
|
17
18
|
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
18
19
|
const { createDaemonCronController } = require("./cronOps");
|
|
19
20
|
const { createGroupOrchestrator } = require("./groupOrchestrator");
|
|
21
|
+
const { DeliveryScheduler } = require("./deliveryScheduler");
|
|
20
22
|
const { normalizeFormat, renderGroupDiagramFromTemplate, renderGroupDiagramFromRuntime } = require("../../orchestration/groups/diagram");
|
|
21
23
|
const { runPromptWithAssistant } = require("./promptLoop");
|
|
22
24
|
const { handlePromptRequest } = require("./promptRequest");
|
|
@@ -1064,46 +1066,23 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain, o
|
|
|
1064
1066
|
|
|
1065
1067
|
function pollQueue() {
|
|
1066
1068
|
if (!state.queueFile) return;
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
try {
|
|
1077
|
-
if (fs.existsSync(processingFile)) {
|
|
1078
|
-
fs.renameSync(processingFile, state.queueFile);
|
|
1079
|
-
}
|
|
1080
|
-
} catch {
|
|
1081
|
-
// ignore rollback errors
|
|
1082
|
-
}
|
|
1083
|
-
return;
|
|
1084
|
-
} finally {
|
|
1085
|
-
if (readOk) {
|
|
1086
|
-
try {
|
|
1087
|
-
if (fs.existsSync(processingFile)) {
|
|
1088
|
-
fs.rmSync(processingFile, { force: true });
|
|
1089
|
-
}
|
|
1090
|
-
} catch {
|
|
1091
|
-
// ignore cleanup errors
|
|
1092
|
-
}
|
|
1069
|
+
const queue = new DeliveryQueue(state.queueFile);
|
|
1070
|
+
queue.recover();
|
|
1071
|
+
while (true) {
|
|
1072
|
+
const claim = queue.claimNext();
|
|
1073
|
+
if (!claim) break;
|
|
1074
|
+
const evt = claim.event;
|
|
1075
|
+
if (!evt) {
|
|
1076
|
+
queue.completeClaim(claim);
|
|
1077
|
+
continue;
|
|
1093
1078
|
}
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
const lines = content.split(/\r?\n/).filter(Boolean);
|
|
1097
|
-
if (!lines.length) return;
|
|
1098
|
-
for (const line of lines) {
|
|
1099
|
-
let evt;
|
|
1100
1079
|
try {
|
|
1101
|
-
evt
|
|
1080
|
+
emitBusEvent(evt);
|
|
1081
|
+
queue.completeClaim(claim);
|
|
1102
1082
|
} catch {
|
|
1083
|
+
queue.restoreClaim(claim);
|
|
1103
1084
|
continue;
|
|
1104
1085
|
}
|
|
1105
|
-
if (!evt) continue;
|
|
1106
|
-
emitBusEvent(evt);
|
|
1107
1086
|
if (evt.publisher && state.pending.has(evt.publisher)) {
|
|
1108
1087
|
state.pending.delete(evt.publisher);
|
|
1109
1088
|
if (onStatus) {
|
|
@@ -1361,6 +1340,8 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1361
1340
|
log(`report bus event failed request=${meta.requestId || ""} error=${err.message || String(err)}`);
|
|
1362
1341
|
}
|
|
1363
1342
|
});
|
|
1343
|
+
const deliveryScheduler = new DeliveryScheduler(projectRoot, { log });
|
|
1344
|
+
deliveryScheduler.start();
|
|
1364
1345
|
|
|
1365
1346
|
handleIpcRequest = async (req, socket) => {
|
|
1366
1347
|
if (!req || typeof req !== "object") return;
|
|
@@ -1493,12 +1474,19 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1493
1474
|
try {
|
|
1494
1475
|
const publisher = busBridge.getSubscriber() || "ufoo-agent";
|
|
1495
1476
|
const eventBus = new EventBus(projectRoot);
|
|
1496
|
-
await eventBus.send(target, message, publisher, {
|
|
1477
|
+
const result = await eventBus.send(target, message, publisher, {
|
|
1497
1478
|
injectionMode: injection_mode,
|
|
1498
1479
|
source,
|
|
1499
1480
|
});
|
|
1500
|
-
|
|
1501
|
-
|
|
1481
|
+
const resolvedTargets = Array.isArray(result && result.targets) ? result.targets : [];
|
|
1482
|
+
for (const resolvedTarget of resolvedTargets) {
|
|
1483
|
+
busBridge.markPending(resolvedTarget);
|
|
1484
|
+
}
|
|
1485
|
+
if (resolvedTargets.length === 0) busBridge.markPending(target);
|
|
1486
|
+
deliveryScheduler.deliverTargets(resolvedTargets).catch((err) => {
|
|
1487
|
+
log(`delivery scheduler send trigger failed target=${target} error=${err.message || String(err)}`);
|
|
1488
|
+
});
|
|
1489
|
+
log(`bus_send target=${target} resolved=${resolvedTargets.join(",")} publisher=${publisher}`);
|
|
1502
1490
|
socket.write(
|
|
1503
1491
|
`${JSON.stringify({
|
|
1504
1492
|
type: IPC_RESPONSE_TYPES.BUS_SEND_OK,
|
|
@@ -2595,6 +2583,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2595
2583
|
processManager.cleanup();
|
|
2596
2584
|
|
|
2597
2585
|
ipcServer.stop();
|
|
2586
|
+
deliveryScheduler.stop();
|
|
2598
2587
|
busBridge.stop();
|
|
2599
2588
|
removeSocket(projectRoot);
|
|
2600
2589
|
removePidIfOwned(projectRoot);
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const fs = require("fs");
|
|
4
3
|
const path = require("path");
|
|
5
4
|
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
6
5
|
const {
|
|
7
|
-
appendJSONL,
|
|
8
6
|
ensureDir,
|
|
9
7
|
generateInstanceId,
|
|
10
8
|
} = require("../../coordination/bus/utils");
|
|
9
|
+
const {
|
|
10
|
+
DeliveryQueue,
|
|
11
|
+
QUEUE_TYPES,
|
|
12
|
+
normalizeQueueEnvelope,
|
|
13
|
+
} = require("../../coordination/bus/deliveryQueue");
|
|
11
14
|
|
|
12
15
|
const REPORT_CONTROL_TARGET = "ufoo-agent";
|
|
13
16
|
const REPORT_CONTROL_EVENT = "agent_report";
|
|
@@ -58,8 +61,12 @@ function buildReportControlEvent(report = {}, options = {}) {
|
|
|
58
61
|
async function enqueueAgentReport(projectRoot, report, options = {}) {
|
|
59
62
|
ensureReportControlQueue(projectRoot);
|
|
60
63
|
|
|
61
|
-
const event = buildReportControlEvent(report, options)
|
|
62
|
-
|
|
64
|
+
const event = normalizeQueueEnvelope(buildReportControlEvent(report, options), {
|
|
65
|
+
queueType: QUEUE_TYPES.REPORT,
|
|
66
|
+
delivery: { mode: "daemon_consume", gate: "none", max_inflight: 1 },
|
|
67
|
+
ack: { policy: "on_consume" },
|
|
68
|
+
});
|
|
69
|
+
new DeliveryQueue(getReportControlQueueFile(projectRoot)).append(event);
|
|
63
70
|
|
|
64
71
|
return {
|
|
65
72
|
queued: true,
|
|
@@ -72,46 +79,16 @@ async function enqueueAgentReport(projectRoot, report, options = {}) {
|
|
|
72
79
|
|
|
73
80
|
function takeReportControlEvents(projectRoot) {
|
|
74
81
|
const queueFile = getReportControlQueueFile(projectRoot);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
readOk = true;
|
|
84
|
-
} catch {
|
|
85
|
-
try {
|
|
86
|
-
if (fs.existsSync(processingFile)) {
|
|
87
|
-
fs.renameSync(processingFile, queueFile);
|
|
88
|
-
}
|
|
89
|
-
} catch {
|
|
90
|
-
// ignore rollback errors
|
|
91
|
-
}
|
|
92
|
-
return [];
|
|
93
|
-
} finally {
|
|
94
|
-
if (readOk) {
|
|
95
|
-
try {
|
|
96
|
-
if (fs.existsSync(processingFile)) {
|
|
97
|
-
fs.rmSync(processingFile, { force: true });
|
|
98
|
-
}
|
|
99
|
-
} catch {
|
|
100
|
-
// ignore cleanup errors
|
|
101
|
-
}
|
|
102
|
-
}
|
|
82
|
+
const queue = new DeliveryQueue(queueFile);
|
|
83
|
+
const events = [];
|
|
84
|
+
queue.recover();
|
|
85
|
+
while (true) {
|
|
86
|
+
const claim = queue.claimNext();
|
|
87
|
+
if (!claim) break;
|
|
88
|
+
events.push(claim.event);
|
|
89
|
+
queue.completeClaim(claim);
|
|
103
90
|
}
|
|
104
|
-
|
|
105
|
-
return content.split(/\r?\n/)
|
|
106
|
-
.filter(Boolean)
|
|
107
|
-
.map((line) => {
|
|
108
|
-
try {
|
|
109
|
-
return JSON.parse(line);
|
|
110
|
-
} catch {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
.filter(Boolean);
|
|
91
|
+
return events;
|
|
115
92
|
}
|
|
116
93
|
|
|
117
94
|
function isAgentReportControlEvent(evt) {
|