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 +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/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/SKILLS/brandkit/SKILL.md +0 -798
- package/SKILLS/brutalist-skill/SKILL.md +0 -92
- package/SKILLS/gpt-tasteskill/SKILL.md +0 -74
- package/SKILLS/image-to-code-skill/SKILL.md +0 -1228
- package/SKILLS/imagegen-frontend-mobile/SKILL.md +0 -1465
- package/SKILLS/imagegen-frontend-web/SKILL.md +0 -987
- package/SKILLS/llms.txt +0 -13
- package/SKILLS/minimalist-skill/SKILL.md +0 -85
- package/SKILLS/output-skill/SKILL.md +0 -49
- package/SKILLS/redesign-skill/SKILL.md +0 -178
- package/SKILLS/soft-skill/SKILL.md +0 -98
- package/SKILLS/stitch-skill/DESIGN.md +0 -121
- package/SKILLS/stitch-skill/SKILL.md +0 -184
- package/SKILLS/taste-skill/SKILL.md +0 -1206
- package/SKILLS/taste-skill-v1/SKILL.md +0 -226
- package/src/coordination/bus/daemon.js +0 -535
|
@@ -1,535 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
const { readJSON, writeJSON, isPidAlive, isAgentPidAlive, isMetaActive, ensureDir, safeNameToSubscriber, subscriberToSafeName } = require("./utils");
|
|
4
|
-
const Injector = require("./inject");
|
|
5
|
-
const QueueManager = require("./queue");
|
|
6
|
-
const MessageManager = require("./message");
|
|
7
|
-
const { createTerminalAdapterRouter } = require("../../runtime/terminal/adapterRouter");
|
|
8
|
-
const {
|
|
9
|
-
INJECTION_MODES,
|
|
10
|
-
getInjectionModeFromEvent,
|
|
11
|
-
} = require("./messageMeta");
|
|
12
|
-
const {
|
|
13
|
-
buildPromptInjectionText,
|
|
14
|
-
shouldRenderPromptEnvelope,
|
|
15
|
-
} = require("./promptEnvelope");
|
|
16
|
-
|
|
17
|
-
function isBusyActivityState(value = "") {
|
|
18
|
-
const state = String(value || "").trim().toLowerCase();
|
|
19
|
-
return state === "working"
|
|
20
|
-
|| state === "running"
|
|
21
|
-
|| state === "waiting_input"
|
|
22
|
-
|| state === "blocked";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Bus Daemon - 监控消息并自动注入命令
|
|
27
|
-
*/
|
|
28
|
-
class BusDaemon {
|
|
29
|
-
constructor(busDir, agentsFile, daemonDir, interval = 2000, projectRoot = "") {
|
|
30
|
-
this.busDir = busDir;
|
|
31
|
-
this.agentsFile = agentsFile;
|
|
32
|
-
this.interval = interval;
|
|
33
|
-
this.daemonDir = daemonDir;
|
|
34
|
-
this.projectRoot = projectRoot || path.resolve(busDir, "..", "..");
|
|
35
|
-
this.pidFile = path.join(this.daemonDir, "daemon.pid");
|
|
36
|
-
this.logFile = path.join(this.daemonDir, "daemon.log");
|
|
37
|
-
this.countsDir = path.join(this.daemonDir, "counts", `${process.pid}`);
|
|
38
|
-
this.running = false;
|
|
39
|
-
this.cleanupCounter = 0;
|
|
40
|
-
this.cleanupInterval = 5; // 每 5 个周期清理一次
|
|
41
|
-
this.timelineSyncCounter = 0;
|
|
42
|
-
// 每 15 个周期同步一次 manual inputs (~15 × interval, default ~30s)
|
|
43
|
-
this.timelineSyncInterval = 15;
|
|
44
|
-
|
|
45
|
-
this.queueManager = new QueueManager(busDir);
|
|
46
|
-
this.injector = new Injector(busDir, agentsFile);
|
|
47
|
-
this.adapterRouter = createTerminalAdapterRouter();
|
|
48
|
-
this.workingHoldMs = Number.parseInt(process.env.UFOO_ACTIVITY_WORKING_HOLD_MS || "", 10) || 5000;
|
|
49
|
-
this.lastWorkingAt = new Map();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
setLegacyActivityState(subscriber, state) {
|
|
53
|
-
const busData = readJSON(this.agentsFile) || { agents: {} };
|
|
54
|
-
if (!busData.agents || !busData.agents[subscriber]) return;
|
|
55
|
-
busData.agents[subscriber].activity_state = state;
|
|
56
|
-
busData.agents[subscriber].activity_since = new Date().toISOString();
|
|
57
|
-
writeJSON(this.agentsFile, busData);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
refreshLegacyActivityState(subscriber, meta, hasPending) {
|
|
61
|
-
const now = Date.now();
|
|
62
|
-
const current = String(meta?.activity_state || "").trim().toLowerCase();
|
|
63
|
-
const lastWorkingAt = this.lastWorkingAt.get(subscriber) || 0;
|
|
64
|
-
const holdActive = lastWorkingAt > 0 && (now - lastWorkingAt) < this.workingHoldMs;
|
|
65
|
-
const daemonManagedWorking = lastWorkingAt > 0;
|
|
66
|
-
|
|
67
|
-
if (holdActive) {
|
|
68
|
-
if (current !== "working") {
|
|
69
|
-
this.setLegacyActivityState(subscriber, "working");
|
|
70
|
-
}
|
|
71
|
-
return "working";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (lastWorkingAt > 0) {
|
|
75
|
-
this.lastWorkingAt.delete(subscriber);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (current === "starting") {
|
|
79
|
-
const next = hasPending ? "ready" : "idle";
|
|
80
|
-
this.setLegacyActivityState(subscriber, next);
|
|
81
|
-
return next;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (current === "working" && daemonManagedWorking) {
|
|
85
|
-
const next = hasPending ? "ready" : "idle";
|
|
86
|
-
this.setLegacyActivityState(subscriber, next);
|
|
87
|
-
return next;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return current;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* 检查 daemon 是否正在运行
|
|
95
|
-
*/
|
|
96
|
-
isRunning() {
|
|
97
|
-
if (!fs.existsSync(this.pidFile)) {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const pid = parseInt(fs.readFileSync(this.pidFile, "utf8").trim(), 10);
|
|
102
|
-
return isPidAlive(pid);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 获取运行中的 daemon PID
|
|
107
|
-
*/
|
|
108
|
-
getRunningPid() {
|
|
109
|
-
if (!fs.existsSync(this.pidFile)) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const pid = parseInt(fs.readFileSync(this.pidFile, "utf8").trim(), 10);
|
|
114
|
-
return isPidAlive(pid) ? pid : null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* 启动 daemon
|
|
119
|
-
*/
|
|
120
|
-
async start(background = false) {
|
|
121
|
-
// 检查是否已经在运行
|
|
122
|
-
if (this.isRunning()) {
|
|
123
|
-
const pid = this.getRunningPid();
|
|
124
|
-
console.log(`[daemon] Already running (pid=${pid})`);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
ensureDir(this.daemonDir);
|
|
128
|
-
ensureDir(path.join(this.daemonDir, "counts"));
|
|
129
|
-
|
|
130
|
-
if (background) {
|
|
131
|
-
// 后台模式:spawn 独立进程
|
|
132
|
-
const { spawn } = require("child_process");
|
|
133
|
-
const logStream = fs.openSync(this.logFile, "a");
|
|
134
|
-
|
|
135
|
-
const child = spawn(
|
|
136
|
-
process.execPath,
|
|
137
|
-
[
|
|
138
|
-
path.join(__dirname, "..", "..", "..", "bin", "ufoo.js"),
|
|
139
|
-
"bus",
|
|
140
|
-
"daemon",
|
|
141
|
-
"--interval",
|
|
142
|
-
String(this.interval / 1000),
|
|
143
|
-
],
|
|
144
|
-
{
|
|
145
|
-
detached: true,
|
|
146
|
-
stdio: ["ignore", logStream, logStream],
|
|
147
|
-
cwd: process.cwd(),
|
|
148
|
-
}
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
child.unref();
|
|
152
|
-
|
|
153
|
-
// 等待 PID 文件创建
|
|
154
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
155
|
-
|
|
156
|
-
const pid = this.getRunningPid();
|
|
157
|
-
console.log(`[daemon] Started in background (pid=${pid}, log: ${this.logFile})`);
|
|
158
|
-
} else {
|
|
159
|
-
// 前台模式
|
|
160
|
-
this.run();
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* 停止 daemon
|
|
166
|
-
*/
|
|
167
|
-
stop() {
|
|
168
|
-
const pid = this.getRunningPid();
|
|
169
|
-
if (!pid) {
|
|
170
|
-
console.log("[daemon] Not running");
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
process.kill(pid, "SIGTERM");
|
|
176
|
-
console.log(`[daemon] Stopped (pid=${pid})`);
|
|
177
|
-
if (fs.existsSync(this.pidFile)) {
|
|
178
|
-
fs.unlinkSync(this.pidFile);
|
|
179
|
-
}
|
|
180
|
-
} catch (err) {
|
|
181
|
-
console.error(`[daemon] Failed to stop: ${err.message}`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* 显示 daemon 状态
|
|
187
|
-
*/
|
|
188
|
-
status() {
|
|
189
|
-
const pid = this.getRunningPid();
|
|
190
|
-
if (pid) {
|
|
191
|
-
console.log(`[daemon] Running (pid=${pid})`);
|
|
192
|
-
} else {
|
|
193
|
-
console.log("[daemon] Not running");
|
|
194
|
-
// 清理过时的 PID 文件
|
|
195
|
-
if (fs.existsSync(this.pidFile)) {
|
|
196
|
-
fs.unlinkSync(this.pidFile);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* 运行 daemon(前台)
|
|
203
|
-
*/
|
|
204
|
-
run() {
|
|
205
|
-
// 记录 PID
|
|
206
|
-
ensureDir(path.dirname(this.pidFile));
|
|
207
|
-
fs.writeFileSync(this.pidFile, `${process.pid}\n`, "utf8");
|
|
208
|
-
|
|
209
|
-
// 设置清理钩子
|
|
210
|
-
const cleanup = () => {
|
|
211
|
-
this.running = false;
|
|
212
|
-
if (fs.existsSync(this.pidFile)) {
|
|
213
|
-
fs.unlinkSync(this.pidFile);
|
|
214
|
-
}
|
|
215
|
-
if (fs.existsSync(this.countsDir)) {
|
|
216
|
-
fs.rmSync(this.countsDir, { recursive: true, force: true });
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
process.on("SIGTERM", cleanup);
|
|
221
|
-
process.on("SIGINT", cleanup);
|
|
222
|
-
process.on("exit", cleanup);
|
|
223
|
-
|
|
224
|
-
// 创建计数目录
|
|
225
|
-
ensureDir(this.countsDir);
|
|
226
|
-
|
|
227
|
-
console.log(`[daemon] Started (pid=${process.pid}, interval=${this.interval / 1000}s)`);
|
|
228
|
-
console.log(`[daemon] Watching: ${this.busDir}/queues/*/pending.jsonl`);
|
|
229
|
-
|
|
230
|
-
this.running = true;
|
|
231
|
-
this.watchLoop();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* 主监控循环
|
|
236
|
-
*/
|
|
237
|
-
async watchLoop() {
|
|
238
|
-
while (this.running) {
|
|
239
|
-
try {
|
|
240
|
-
// 定期清理死掉的 agent
|
|
241
|
-
this.cleanupCounter++;
|
|
242
|
-
if (this.cleanupCounter >= this.cleanupInterval) {
|
|
243
|
-
await this.cleanupDeadAgents();
|
|
244
|
-
this.cleanupCounter = 0;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// 定期同步 timeline(manual inputs from session files, ~15 × interval)
|
|
248
|
-
this.timelineSyncCounter++;
|
|
249
|
-
if (this.timelineSyncCounter >= this.timelineSyncInterval) {
|
|
250
|
-
this.syncTimeline();
|
|
251
|
-
this.timelineSyncCounter = 0;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// 检查所有订阅者的队列
|
|
255
|
-
await this.checkQueues();
|
|
256
|
-
} catch (err) {
|
|
257
|
-
console.error(`[daemon] Error: ${err.message}`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// 等待下一个周期
|
|
261
|
-
await new Promise((resolve) => setTimeout(resolve, this.interval));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* 增量同步 timeline — 捕获 manual inputs(bus 消息已在 send() 时实时追加)
|
|
267
|
-
*/
|
|
268
|
-
syncTimeline() {
|
|
269
|
-
try {
|
|
270
|
-
const { buildTimeline } = require("../history/inputTimeline");
|
|
271
|
-
buildTimeline(this.projectRoot);
|
|
272
|
-
} catch (err) {
|
|
273
|
-
if (process.env.UFOO_HISTORY_DEBUG === "1") {
|
|
274
|
-
console.error("[daemon][history] syncTimeline failed:", err.message);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* 检查所有队列
|
|
281
|
-
*/
|
|
282
|
-
async checkQueues() {
|
|
283
|
-
const queuesDir = path.join(this.busDir, "queues");
|
|
284
|
-
if (!fs.existsSync(queuesDir)) {
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const busData = readJSON(this.agentsFile) || { agents: {} };
|
|
289
|
-
const messageManager = new MessageManager(this.busDir, busData, this.queueManager);
|
|
290
|
-
const subscribers = fs.readdirSync(queuesDir);
|
|
291
|
-
|
|
292
|
-
for (const safeName of subscribers) {
|
|
293
|
-
const pendingFile = path.join(queuesDir, safeName, "pending.jsonl");
|
|
294
|
-
if (!fs.existsSync(pendingFile)) {
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const subscriber = safeNameToSubscriber(safeName);
|
|
299
|
-
const meta = busData.agents?.[subscriber];
|
|
300
|
-
const launchMode = meta?.launch_mode || "";
|
|
301
|
-
// Delivery ownership:
|
|
302
|
-
// - notifier/injector: terminal/tmux
|
|
303
|
-
// - internal queue loop: internal
|
|
304
|
-
// Bus daemon only handles legacy/unknown launch modes.
|
|
305
|
-
const adapter = this.adapterRouter.getAdapter({ launchMode, agentId: subscriber, meta });
|
|
306
|
-
const { supportsNotifierInjector, supportsInternalQueueLoop } = adapter.capabilities;
|
|
307
|
-
if (launchMode && (supportsNotifierInjector || supportsInternalQueueLoop)) {
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// 获取当前消息数
|
|
312
|
-
let count = 0;
|
|
313
|
-
if (fs.statSync(pendingFile).size > 0) {
|
|
314
|
-
const content = fs.readFileSync(pendingFile, "utf8").trim();
|
|
315
|
-
count = content ? content.split("\n").length : 0;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// 获取上次的消息数
|
|
319
|
-
const lastCount = this.getLastCount(safeName);
|
|
320
|
-
|
|
321
|
-
// 如果有新消息,注入命令
|
|
322
|
-
const wakePath = path.join(queuesDir, safeName, "wake");
|
|
323
|
-
const wakeActive = fs.existsSync(wakePath);
|
|
324
|
-
|
|
325
|
-
if (count > 0 || wakeActive) {
|
|
326
|
-
const now = new Date().toISOString().split("T")[1].slice(0, 8);
|
|
327
|
-
const note = wakeActive && count <= lastCount ? " (wake)" : "";
|
|
328
|
-
console.log(`[daemon] ${now} New message for ${subscriber} (${lastCount} -> ${count})${note}`);
|
|
329
|
-
|
|
330
|
-
try {
|
|
331
|
-
const agentType = String((meta && meta.agent_type) || "").trim().toLowerCase();
|
|
332
|
-
const isUfooCode = subscriber.startsWith("ufoo-code:")
|
|
333
|
-
|| agentType === "ufoo-code"
|
|
334
|
-
|| agentType === "ucode"
|
|
335
|
-
|| agentType === "ufoo";
|
|
336
|
-
if (isUfooCode) {
|
|
337
|
-
// ufoo-code queue is consumed internally by ucode itself.
|
|
338
|
-
// Bus daemon should not inject any command/text into terminal.
|
|
339
|
-
if (wakeActive) fs.rmSync(wakePath, { force: true });
|
|
340
|
-
this.setLastCount(safeName, count);
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
let currentActivityState = this.refreshLegacyActivityState(subscriber, meta, count > 0);
|
|
345
|
-
const events = this.drainPending(pendingFile);
|
|
346
|
-
const failed = [];
|
|
347
|
-
let deliveredCount = 0;
|
|
348
|
-
for (const evt of events) {
|
|
349
|
-
if (!evt || evt.event !== "message" || !evt.data || typeof evt.data.message !== "string") {
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
const injectionMode = getInjectionModeFromEvent(evt, INJECTION_MODES.IMMEDIATE);
|
|
353
|
-
if (injectionMode === INJECTION_MODES.QUEUED && isBusyActivityState(currentActivityState)) {
|
|
354
|
-
failed.push(evt);
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
try {
|
|
358
|
-
const injectionText = buildPromptInjectionText(evt, subscriber, busData.agents || {});
|
|
359
|
-
// eslint-disable-next-line no-await-in-loop
|
|
360
|
-
await this.injector.inject(subscriber, injectionText);
|
|
361
|
-
deliveredCount += 1;
|
|
362
|
-
currentActivityState = "working";
|
|
363
|
-
this.lastWorkingAt.set(subscriber, Date.now());
|
|
364
|
-
this.setLegacyActivityState(subscriber, "working");
|
|
365
|
-
} catch (err) {
|
|
366
|
-
failed.push(evt);
|
|
367
|
-
try {
|
|
368
|
-
const pub = typeof evt.publisher === "object" && evt.publisher
|
|
369
|
-
? (evt.publisher.subscriber || evt.publisher.nickname || "")
|
|
370
|
-
: (evt.publisher || "");
|
|
371
|
-
if (pub) {
|
|
372
|
-
// eslint-disable-next-line no-await-in-loop
|
|
373
|
-
await messageManager.emit(pub, "delivery", {
|
|
374
|
-
target: subscriber,
|
|
375
|
-
seq: evt.seq,
|
|
376
|
-
status: "error",
|
|
377
|
-
message: `delivery failed to ${meta?.nickname || subscriber}: ${err.message || "inject failed"}`,
|
|
378
|
-
}, subscriber, "status/delivery");
|
|
379
|
-
}
|
|
380
|
-
} catch {
|
|
381
|
-
// ignore delivery emit errors
|
|
382
|
-
}
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
try {
|
|
386
|
-
// Emit delivery status back to publisher (best-effort)
|
|
387
|
-
const pub = typeof evt.publisher === "object" && evt.publisher
|
|
388
|
-
? (evt.publisher.subscriber || evt.publisher.nickname || "")
|
|
389
|
-
: (evt.publisher || "");
|
|
390
|
-
if (pub) {
|
|
391
|
-
// eslint-disable-next-line no-await-in-loop
|
|
392
|
-
await messageManager.emit(pub, "delivery", {
|
|
393
|
-
target: subscriber,
|
|
394
|
-
seq: evt.seq,
|
|
395
|
-
status: "ok",
|
|
396
|
-
message: `delivered to ${meta?.nickname || subscriber}`,
|
|
397
|
-
}, subscriber, "status/delivery");
|
|
398
|
-
}
|
|
399
|
-
} catch {
|
|
400
|
-
// ignore delivery emit errors
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (failed.length > 0) {
|
|
404
|
-
try {
|
|
405
|
-
const content = failed.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
406
|
-
fs.appendFileSync(pendingFile, content, "utf8");
|
|
407
|
-
} catch {
|
|
408
|
-
// ignore requeue failures
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
console.log(`[daemon] Delivered ${deliveredCount} message(s) to ${subscriber}`);
|
|
412
|
-
if (wakeActive) fs.rmSync(wakePath, { force: true });
|
|
413
|
-
} catch (err) {
|
|
414
|
-
console.error(`[daemon] Failed to inject: ${err.message}`);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// 更新计数
|
|
419
|
-
this.setLastCount(safeName, count);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
drainPending(pendingFile) {
|
|
424
|
-
if (!fs.existsSync(pendingFile)) return [];
|
|
425
|
-
const processingFile = `${pendingFile}.processing.${process.pid}.${Date.now()}`;
|
|
426
|
-
let content = "";
|
|
427
|
-
let readOk = false;
|
|
428
|
-
try {
|
|
429
|
-
fs.renameSync(pendingFile, processingFile);
|
|
430
|
-
content = fs.readFileSync(processingFile, "utf8");
|
|
431
|
-
readOk = true;
|
|
432
|
-
} catch {
|
|
433
|
-
try {
|
|
434
|
-
if (fs.existsSync(processingFile)) {
|
|
435
|
-
fs.renameSync(processingFile, pendingFile);
|
|
436
|
-
}
|
|
437
|
-
} catch {
|
|
438
|
-
// ignore rollback errors
|
|
439
|
-
}
|
|
440
|
-
return [];
|
|
441
|
-
} finally {
|
|
442
|
-
if (readOk) {
|
|
443
|
-
try {
|
|
444
|
-
if (fs.existsSync(processingFile)) {
|
|
445
|
-
fs.rmSync(processingFile, { force: true });
|
|
446
|
-
}
|
|
447
|
-
} catch {
|
|
448
|
-
// ignore cleanup errors
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
if (!content.trim()) return [];
|
|
453
|
-
return content.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
454
|
-
try {
|
|
455
|
-
return JSON.parse(line);
|
|
456
|
-
} catch {
|
|
457
|
-
return null;
|
|
458
|
-
}
|
|
459
|
-
}).filter(Boolean);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* 获取上次的消息计数
|
|
464
|
-
*/
|
|
465
|
-
getLastCount(safeName) {
|
|
466
|
-
const countFile = path.join(this.countsDir, safeName);
|
|
467
|
-
if (!fs.existsSync(countFile)) {
|
|
468
|
-
return 0;
|
|
469
|
-
}
|
|
470
|
-
const content = fs.readFileSync(countFile, "utf8").trim();
|
|
471
|
-
return parseInt(content, 10) || 0;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* 设置消息计数
|
|
476
|
-
*/
|
|
477
|
-
setLastCount(safeName, count) {
|
|
478
|
-
const countFile = path.join(this.countsDir, safeName);
|
|
479
|
-
ensureDir(path.dirname(countFile));
|
|
480
|
-
fs.writeFileSync(countFile, `${count}\n`, "utf8");
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* 清理死掉的 agent
|
|
485
|
-
*/
|
|
486
|
-
async cleanupDeadAgents() {
|
|
487
|
-
const agentsFile = this.agentsFile;
|
|
488
|
-
if (!fs.existsSync(agentsFile)) {
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const busData = readJSON(agentsFile);
|
|
493
|
-
if (!busData || !busData.agents) {
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
let changed = false;
|
|
498
|
-
|
|
499
|
-
for (const [subscriber, meta] of Object.entries(busData.agents)) {
|
|
500
|
-
if (meta.status !== "active") {
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// 检查 agent 是否仍然存活(PID + TTY 交叉检查)
|
|
505
|
-
if (!isMetaActive(meta)) {
|
|
506
|
-
const now = new Date().toISOString().split("T")[1].slice(0, 8);
|
|
507
|
-
console.log(`[daemon] ${now} Agent ${subscriber} (pid=${meta.pid || 0}) is dead, marking inactive`);
|
|
508
|
-
|
|
509
|
-
meta.status = "inactive";
|
|
510
|
-
meta.activity_state = "";
|
|
511
|
-
changed = true;
|
|
512
|
-
|
|
513
|
-
// 清理队列目录和 offset
|
|
514
|
-
const safeName = subscriberToSafeName(subscriber);
|
|
515
|
-
const queueDir = path.join(this.busDir, "queues", safeName);
|
|
516
|
-
const offsetFile = path.join(this.busDir, "offsets", `${safeName}.offset`);
|
|
517
|
-
|
|
518
|
-
if (fs.existsSync(queueDir)) {
|
|
519
|
-
fs.rmSync(queueDir, { recursive: true, force: true });
|
|
520
|
-
}
|
|
521
|
-
if (fs.existsSync(offsetFile)) {
|
|
522
|
-
fs.unlinkSync(offsetFile);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (changed) {
|
|
528
|
-
writeJSON(agentsFile, busData);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
module.exports = BusDaemon;
|
|
534
|
-
module.exports.buildPromptInjectionText = buildPromptInjectionText;
|
|
535
|
-
module.exports.shouldRenderPromptEnvelope = shouldRenderPromptEnvelope;
|