u-foo 2.5.1 → 2.5.3

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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/agents/internal/internalRunner.js +57 -78
  3. package/src/agents/launch/launcher.js +3 -0
  4. package/src/agents/launch/notifier.js +38 -112
  5. package/src/agents/launch/ptyRunner.js +59 -49
  6. package/src/agents/launch/ptyWrapper.js +32 -1
  7. package/src/app/cli/busCoreCommands.js +19 -1
  8. package/src/app/cli/run.js +1 -53
  9. package/src/code/agent.js +41 -140
  10. package/src/coordination/bus/deliveryQueue.js +308 -0
  11. package/src/coordination/bus/index.js +3 -27
  12. package/src/coordination/bus/message.js +35 -15
  13. package/src/coordination/bus/queue.js +23 -7
  14. package/src/runtime/daemon/deliveryScheduler.js +205 -0
  15. package/src/runtime/daemon/index.js +27 -38
  16. package/src/runtime/daemon/ops.js +1 -5
  17. package/src/runtime/daemon/reportControlBus.js +20 -43
  18. package/templates/groups/build-ultra.json +1 -1
  19. package/SKILLS/brandkit/SKILL.md +0 -798
  20. package/SKILLS/brutalist-skill/SKILL.md +0 -92
  21. package/SKILLS/gpt-tasteskill/SKILL.md +0 -74
  22. package/SKILLS/image-to-code-skill/SKILL.md +0 -1228
  23. package/SKILLS/imagegen-frontend-mobile/SKILL.md +0 -1465
  24. package/SKILLS/imagegen-frontend-web/SKILL.md +0 -987
  25. package/SKILLS/llms.txt +0 -13
  26. package/SKILLS/minimalist-skill/SKILL.md +0 -85
  27. package/SKILLS/output-skill/SKILL.md +0 -49
  28. package/SKILLS/redesign-skill/SKILL.md +0 -178
  29. package/SKILLS/soft-skill/SKILL.md +0 -98
  30. package/SKILLS/stitch-skill/DESIGN.md +0 -121
  31. package/SKILLS/stitch-skill/SKILL.md +0 -184
  32. package/SKILLS/taste-skill/SKILL.md +0 -1206
  33. package/SKILLS/taste-skill-v1/SKILL.md +0 -226
  34. package/src/coordination/bus/daemon.js +0 -535
@@ -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");
package/src/code/agent.js CHANGED
@@ -31,6 +31,7 @@ const {
31
31
  listUcodeSkills,
32
32
  showSkill,
33
33
  } = require("./skills");
34
+ const { DeliveryQueue } = require("../coordination/bus/deliveryQueue");
34
35
 
35
36
  function printPrompt() {
36
37
  process.stdout.write("> ");
@@ -883,110 +884,31 @@ function getPendingBusCount(workspaceRoot = process.cwd(), subscriberId = "") {
883
884
  function drainJsonlFile(filePath = "") {
884
885
  const target = String(filePath || "").trim();
885
886
  if (!target) return { drained: [], rawLines: [], error: "" };
886
- if (!fs.existsSync(target)) return { drained: [], rawLines: [], error: "" };
887
-
888
- const processingFile = `${target}.processing.${process.pid}.${Date.now()}`;
889
- let content = "";
890
- let renamed = false;
887
+ const queue = new DeliveryQueue(target);
888
+ const drained = [];
889
+ const rawLines = [];
890
+ const claims = [];
891
891
  try {
892
- fs.renameSync(target, processingFile);
893
- renamed = true;
894
- content = String(fs.readFileSync(processingFile, "utf8") || "");
895
- } catch (err) {
896
- // Restore on failure.
897
- try {
898
- if (renamed && fs.existsSync(processingFile)) {
899
- fs.renameSync(processingFile, target);
900
- }
901
- } catch {
902
- // ignore
892
+ queue.recover();
893
+ while (true) {
894
+ const claim = queue.claimNext();
895
+ if (!claim) break;
896
+ claims.push(claim);
897
+ drained.push(claim.event);
898
+ rawLines.push(JSON.stringify(claim.event));
899
+ queue.completeClaim(claim);
903
900
  }
901
+ } catch (err) {
902
+ for (const claim of claims) queue.restoreClaim(claim);
904
903
  return { drained: [], rawLines: [], error: err && err.message ? err.message : "drain failed" };
905
904
  }
906
-
907
- const rawLines = content.split(/\r?\n/).filter((line) => line.trim());
908
- const drained = rawLines.map((line) => {
909
- try {
910
- return JSON.parse(line);
911
- } catch {
912
- return null;
913
- }
914
- }).filter(Boolean);
915
-
916
- // Keep processing file around for potential requeue decisions by caller.
917
- return { drained, rawLines, error: "", processingFile };
918
- }
919
-
920
- function requeueJsonlLines(filePath = "", lines = []) {
921
- const target = String(filePath || "").trim();
922
- const list = Array.isArray(lines) ? lines.filter((l) => String(l || "").trim()) : [];
923
- if (!target || list.length === 0) return;
924
- try {
925
- fs.mkdirSync(path.dirname(target), { recursive: true });
926
- fs.appendFileSync(target, `${list.join("\n")}\n`, "utf8");
927
- } catch {
928
- // ignore requeue errors
929
- }
930
- }
931
-
932
- function cleanupProcessingFile(filePath = "") {
933
- const target = String(filePath || "").trim();
934
- if (!target) return;
935
- try {
936
- if (fs.existsSync(target)) fs.rmSync(target, { force: true });
937
- } catch {
938
- // ignore
939
- }
940
- }
941
-
942
- function recoverStaleProcessingFiles(pendingFilePath = "", options = {}) {
943
- const pendingFile = String(pendingFilePath || "").trim();
944
- if (!pendingFile) return 0;
945
- const maxAgeMs = Number.isFinite(options.maxAgeMs) ? options.maxAgeMs : 30000;
946
- const dir = path.dirname(pendingFile);
947
- const base = path.basename(pendingFile);
948
- const prefix = `${base}.processing.`;
949
- const now = Date.now();
950
- let recovered = 0;
951
-
952
- try {
953
- if (!fs.existsSync(dir)) return 0;
954
- const names = fs.readdirSync(dir);
955
- for (const name of names) {
956
- if (!name || !name.startsWith(prefix)) continue;
957
- const fullPath = path.join(dir, name);
958
- let stat = null;
959
- try {
960
- stat = fs.statSync(fullPath);
961
- } catch {
962
- continue;
963
- }
964
- if (!stat || !stat.isFile()) continue;
965
- const pidMatch = name.match(/\.processing\.(\d+)\./);
966
- const pid = pidMatch ? parseInt(pidMatch[1], 10) : NaN;
967
- const pidDead = Number.isFinite(pid) && pid > 0 && !isPidAlive(pid);
968
- const tooOld = Number.isFinite(maxAgeMs) && maxAgeMs > 0 && (now - stat.mtimeMs >= maxAgeMs);
969
- if (!pidDead && !tooOld) continue;
970
-
971
- let content = "";
972
- try {
973
- content = String(fs.readFileSync(fullPath, "utf8") || "");
974
- } catch {
975
- content = "";
976
- }
977
-
978
- const lines = content.split(/\r?\n/).filter((line) => String(line || "").trim());
979
- if (lines.length > 0) {
980
- requeueJsonlLines(pendingFile, lines);
981
- }
982
- cleanupProcessingFile(fullPath);
983
- recovered += 1;
984
- }
985
- } catch {
986
- return recovered;
987
- }
988
-
989
- return recovered;
905
+ return {
906
+ drained,
907
+ rawLines,
908
+ error: "",
909
+ claims,
910
+ processingFile: claims[0] ? claims[0].processingFile : "",
911
+ };
990
912
  }
991
913
 
992
914
  function extractTaskFromBusEvent(evt) {
@@ -1144,39 +1066,22 @@ async function runUbusCommand(state = {}, options = {}) {
1144
1066
 
1145
1067
  // Prefer consuming pending.jsonl directly (stable, ANSI/wrapping-proof).
1146
1068
  const pendingFile = resolvePendingQueueFile(runtimeWorkspace, subscriberId);
1147
- // Recover any stale processing files from prior crashes so they don't "black hole" messages.
1148
- recoverStaleProcessingFiles(pendingFile, { maxAgeMs: 30000 });
1069
+ const queue = pendingFile ? new DeliveryQueue(pendingFile) : null;
1070
+ if (queue) queue.recover();
1149
1071
  const hasPendingFile = Boolean(pendingFile && fs.existsSync(pendingFile));
1150
- const drainedRes = hasPendingFile ? drainJsonlFile(pendingFile) : { drained: [], rawLines: [], error: "", processingFile: "" };
1151
- if (drainedRes && drainedRes.error) {
1152
- return {
1153
- ok: false,
1154
- summary: "",
1155
- error: drainedRes.error,
1156
- handled: 0,
1157
- subscriberId,
1158
- };
1159
- }
1160
- const rawLines = Array.isArray(drainedRes.rawLines) ? drainedRes.rawLines : [];
1161
- const messages = rawLines
1162
- .map((rawLine) => {
1163
- try {
1164
- const evt = JSON.parse(rawLine);
1165
- const msg = extractTaskFromBusEvent(evt);
1166
- if (!msg) return null;
1167
- return { ...msg, rawLine };
1168
- } catch {
1169
- return null;
1170
- }
1171
- })
1172
- .filter(Boolean);
1173
1072
  let handled = 0;
1174
1073
  const sendErrors = [];
1175
- const failedRawLines = [];
1176
1074
  const messageExchanges = [];
1177
1075
 
1178
- try {
1179
- for (const message of messages) {
1076
+ if (queue && hasPendingFile) {
1077
+ while (fs.existsSync(pendingFile)) {
1078
+ const claim = queue.claimNext();
1079
+ if (!claim) break;
1080
+ const message = extractTaskFromBusEvent(claim.event);
1081
+ if (!message) {
1082
+ queue.completeClaim(claim);
1083
+ continue;
1084
+ }
1180
1085
  let nlResult;
1181
1086
 
1182
1087
  // Notify that we received the message (for immediate display)
@@ -1200,32 +1105,28 @@ async function runUbusCommand(state = {}, options = {}) {
1200
1105
  signal: options.signal,
1201
1106
  });
1202
1107
  } catch (err) {
1203
- sendErrors.push(`task from ${message.publisher} failed: ${err && err.message ? err.message : "task failed"}`);
1204
- failedRawLines.push(message.rawLine);
1108
+ const errorMessage = err && err.message ? err.message : "task failed";
1109
+ sendErrors.push(`task from ${message.publisher} failed: ${errorMessage}`);
1110
+ queue.restoreClaim(claim);
1205
1111
  // Send error notification
1206
- shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote(`❌ Error: ${err.message}`)}`);
1207
- continue;
1112
+ shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote(`❌ Error: ${errorMessage}`)}`);
1113
+ break;
1208
1114
  }
1209
1115
  const reply = String(formatNl(nlResult, false) || "").replace(/\s+/g, " ").trim() || "Done.";
1210
1116
  const sendRes = shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote(reply.slice(0, 2000))}`);
1211
1117
  if (!sendRes.ok) {
1212
1118
  sendErrors.push(`reply to ${message.publisher} failed: ${sendRes.error || "send failed"}`);
1213
- failedRawLines.push(message.rawLine);
1214
- continue;
1119
+ queue.restoreClaim(claim);
1120
+ break;
1215
1121
  }
1216
1122
  handled += 1;
1123
+ queue.completeClaim(claim);
1217
1124
  messageExchanges.push({
1218
1125
  from: message.publisher,
1219
1126
  task: message.task,
1220
1127
  reply,
1221
1128
  });
1222
1129
  }
1223
- } finally {
1224
- // If we drained the pending file but had failures, requeue only failed lines.
1225
- if (failedRawLines.length > 0) {
1226
- requeueJsonlLines(pendingFile, failedRawLines);
1227
- }
1228
- cleanupProcessingFile(drainedRes.processingFile);
1229
1130
  }
1230
1131
 
1231
1132
  // Fallback: if there is no pending file, fall back to CLI `bus check` parsing.
@@ -0,0 +1,308 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const {
4
+ ensureDir,
5
+ appendJSONL,
6
+ generateInstanceId,
7
+ readJSONL,
8
+ writeFileAtomic,
9
+ subscriberToSafeName,
10
+ isPidAlive,
11
+ } = require("./utils");
12
+
13
+ const PROCESSING_STALE_MS = 30000;
14
+
15
+ function positiveSeq(event) {
16
+ const seq = Number(event && event.seq);
17
+ return Number.isFinite(seq) && seq > 0 ? seq : 0;
18
+ }
19
+
20
+ function eventToJsonl(events = []) {
21
+ const lines = events
22
+ .filter(Boolean)
23
+ .map((event) => JSON.stringify(event));
24
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
25
+ }
26
+
27
+ function readJsonlLoose(filePath) {
28
+ if (!filePath || !fs.existsSync(filePath)) return [];
29
+ const content = fs.readFileSync(filePath, "utf8");
30
+ if (!content.trim()) return [];
31
+ return content.split(/\r?\n/)
32
+ .filter(Boolean)
33
+ .map((line) => {
34
+ try {
35
+ return JSON.parse(line);
36
+ } catch {
37
+ return null;
38
+ }
39
+ })
40
+ .filter(Boolean);
41
+ }
42
+
43
+ const QUEUE_TYPES = {
44
+ AGENT_MESSAGE: "agent_message",
45
+ DAEMON_CONTROL: "daemon_control",
46
+ REPORT: "report",
47
+ WAKE: "wake",
48
+ DELIVERY_STATUS: "delivery_status",
49
+ EVENT: "event",
50
+ };
51
+
52
+ function inferQueueType(event = {}) {
53
+ if (event.queue_type) return String(event.queue_type);
54
+ if (event.event === "message") return QUEUE_TYPES.AGENT_MESSAGE;
55
+ if (event.event === "wake") return QUEUE_TYPES.WAKE;
56
+ if (event.event === "delivery") return QUEUE_TYPES.DELIVERY_STATUS;
57
+ if (event.type === "report/control" || event.event === "controller_report") return QUEUE_TYPES.REPORT;
58
+ if (String(event.target || "") === "ufoo-agent") return QUEUE_TYPES.DAEMON_CONTROL;
59
+ return QUEUE_TYPES.EVENT;
60
+ }
61
+
62
+ function defaultDeliveryForType(queueType) {
63
+ if (queueType === QUEUE_TYPES.AGENT_MESSAGE) {
64
+ return { mode: "inject", gate: "idle", max_inflight: 1 };
65
+ }
66
+ if (queueType === QUEUE_TYPES.WAKE) {
67
+ return { mode: "notify_only", gate: "none", max_inflight: 1 };
68
+ }
69
+ if (queueType === QUEUE_TYPES.DAEMON_CONTROL || queueType === QUEUE_TYPES.REPORT || queueType === QUEUE_TYPES.DELIVERY_STATUS) {
70
+ return { mode: "daemon_consume", gate: "none", max_inflight: 1 };
71
+ }
72
+ return { mode: "self_consume", gate: "none", max_inflight: 1 };
73
+ }
74
+
75
+ function defaultAckForType(queueType) {
76
+ if (queueType === QUEUE_TYPES.AGENT_MESSAGE) return { policy: "on_delivery" };
77
+ if (queueType === QUEUE_TYPES.WAKE) return { policy: "fire_and_forget" };
78
+ return { policy: "on_consume" };
79
+ }
80
+
81
+ function normalizeQueueEnvelope(event = {}, overrides = {}) {
82
+ const queueType = overrides.queueType || overrides.queue_type || inferQueueType(event);
83
+ const existingDelivery = event.delivery && typeof event.delivery === "object" ? event.delivery : {};
84
+ const overrideDelivery = overrides.delivery && typeof overrides.delivery === "object" ? overrides.delivery : {};
85
+ const existingAck = event.ack && typeof event.ack === "object" ? event.ack : {};
86
+ const overrideAck = overrides.ack && typeof overrides.ack === "object" ? overrides.ack : {};
87
+ return {
88
+ ...event,
89
+ queue_type: queueType,
90
+ delivery: {
91
+ ...defaultDeliveryForType(queueType),
92
+ ...existingDelivery,
93
+ ...overrideDelivery,
94
+ },
95
+ ack: {
96
+ ...defaultAckForType(queueType),
97
+ ...existingAck,
98
+ ...overrideAck,
99
+ },
100
+ };
101
+ }
102
+
103
+ function stripQueueEnvelope(event = {}) {
104
+ if (!event || typeof event !== "object") return event;
105
+ const stripped = { ...event };
106
+ delete stripped.queue_type;
107
+ delete stripped.delivery;
108
+ delete stripped.ack;
109
+ return stripped;
110
+ }
111
+
112
+ class DeliveryQueue {
113
+ constructor(pendingFile) {
114
+ this.pendingFile = pendingFile;
115
+ }
116
+
117
+ static forSubscriber(busDir, subscriber) {
118
+ return new DeliveryQueue(path.join(
119
+ busDir,
120
+ "queues",
121
+ subscriberToSafeName(subscriber),
122
+ "pending.jsonl"
123
+ ));
124
+ }
125
+
126
+ queueDir() {
127
+ return path.dirname(this.pendingFile);
128
+ }
129
+
130
+ ensureQueueDir() {
131
+ ensureDir(this.queueDir());
132
+ }
133
+
134
+ processingPatternPrefix() {
135
+ return `${path.basename(this.pendingFile)}.processing.`;
136
+ }
137
+
138
+ processingFiles() {
139
+ const dir = this.queueDir();
140
+ if (!fs.existsSync(dir)) return [];
141
+ const prefix = this.processingPatternPrefix();
142
+ return fs.readdirSync(dir)
143
+ .filter((name) => name.startsWith(prefix))
144
+ .sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }))
145
+ .map((name) => path.join(dir, name));
146
+ }
147
+
148
+ processingFileInfo(filePath) {
149
+ const name = path.basename(filePath || "");
150
+ const escapedBase = path.basename(this.pendingFile).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
151
+ const match = name.match(new RegExp(`^${escapedBase}\\.processing\\.(\\d+)(?:\\.(\\d+))?`));
152
+ const pid = match ? parseInt(match[1], 10) : NaN;
153
+ const timestamp = match && match[2] ? parseInt(match[2], 10) : NaN;
154
+ return {
155
+ pid: Number.isFinite(pid) && pid > 0 ? pid : 0,
156
+ timestamp: Number.isFinite(timestamp) && timestamp > 0 ? timestamp : 0,
157
+ };
158
+ }
159
+
160
+ isRecoverableProcessingFile(filePath, options = {}) {
161
+ const maxAgeMs = Number.isFinite(options.maxAgeMs) ? options.maxAgeMs : PROCESSING_STALE_MS;
162
+ const info = this.processingFileInfo(filePath);
163
+ if (info.pid > 0) return !isPidAlive(info.pid);
164
+ if (!Number.isFinite(maxAgeMs) || maxAgeMs <= 0) return false;
165
+
166
+ const now = Date.now();
167
+ if (info.timestamp > 0 && now - info.timestamp >= maxAgeMs) return true;
168
+
169
+ try {
170
+ const stat = fs.statSync(filePath);
171
+ return Boolean(stat && stat.isFile() && now - stat.mtimeMs >= maxAgeMs);
172
+ } catch {
173
+ return false;
174
+ }
175
+ }
176
+
177
+ readPending() {
178
+ this.recover();
179
+ return readJSONL(this.pendingFile);
180
+ }
181
+
182
+ readPendingRaw() {
183
+ return readJSONL(this.pendingFile);
184
+ }
185
+
186
+ append(event) {
187
+ if (!event) return;
188
+ this.ensureQueueDir();
189
+ appendJSONL(this.pendingFile, normalizeQueueEnvelope(event));
190
+ }
191
+
192
+ writePending(events = []) {
193
+ this.ensureQueueDir();
194
+ const items = Array.isArray(events) ? events.filter(Boolean) : [];
195
+ if (items.length === 0) {
196
+ try {
197
+ fs.rmSync(this.pendingFile, { force: true });
198
+ } catch {
199
+ // ignore cleanup errors
200
+ }
201
+ return;
202
+ }
203
+ writeFileAtomic(this.pendingFile, eventToJsonl(items));
204
+ }
205
+
206
+ mergeAndSort(events = []) {
207
+ const sequenced = new Map();
208
+ const unsequenced = [];
209
+ for (const event of events) {
210
+ if (!event) continue;
211
+ const seq = positiveSeq(event);
212
+ if (seq > 0) {
213
+ if (!sequenced.has(seq)) sequenced.set(seq, event);
214
+ } else {
215
+ unsequenced.push(event);
216
+ }
217
+ }
218
+ return [
219
+ ...Array.from(sequenced.entries())
220
+ .sort(([a], [b]) => a - b)
221
+ .map(([, event]) => event),
222
+ ...unsequenced,
223
+ ];
224
+ }
225
+
226
+ recover(options = {}) {
227
+ const files = this.processingFiles()
228
+ .filter((file) => this.isRecoverableProcessingFile(file, options));
229
+ if (files.length === 0) return { recovered: 0, files: [] };
230
+
231
+ const all = [...this.readPendingRaw()];
232
+ for (const file of files) {
233
+ all.push(...readJsonlLoose(file));
234
+ }
235
+
236
+ const merged = this.mergeAndSort(all);
237
+ this.writePending(merged);
238
+
239
+ for (const file of files) {
240
+ try {
241
+ fs.rmSync(file, { force: true });
242
+ } catch {
243
+ // ignore cleanup errors
244
+ }
245
+ }
246
+
247
+ return { recovered: merged.length, files };
248
+ }
249
+
250
+ claimNext() {
251
+ this.recover();
252
+ const pending = this.readPendingRaw();
253
+ if (pending.length === 0) return null;
254
+
255
+ const event = pending[0];
256
+ const remaining = pending.slice(1);
257
+ const processingFile = `${this.pendingFile}.processing.${process.pid}.${Date.now()}.${generateInstanceId()}`;
258
+ this.ensureQueueDir();
259
+ writeFileAtomic(processingFile, eventToJsonl([event]));
260
+ this.writePending(remaining);
261
+
262
+ return {
263
+ event,
264
+ processingFile,
265
+ pendingFile: this.pendingFile,
266
+ };
267
+ }
268
+
269
+ completeClaim(claim) {
270
+ const processingFile = claim && claim.processingFile;
271
+ if (!processingFile) return false;
272
+ try {
273
+ if (fs.existsSync(processingFile)) {
274
+ fs.rmSync(processingFile, { force: true });
275
+ return true;
276
+ }
277
+ } catch {
278
+ return false;
279
+ }
280
+ return false;
281
+ }
282
+
283
+ restoreClaim(claim) {
284
+ const event = claim && claim.event;
285
+ const processingFile = claim && claim.processingFile;
286
+ if (!event) return false;
287
+
288
+ const merged = this.mergeAndSort([event, ...this.readPendingRaw()]);
289
+ this.writePending(merged);
290
+ if (processingFile) {
291
+ try {
292
+ fs.rmSync(processingFile, { force: true });
293
+ } catch {
294
+ // ignore cleanup errors
295
+ }
296
+ }
297
+ return true;
298
+ }
299
+ }
300
+
301
+ module.exports = {
302
+ DeliveryQueue,
303
+ QUEUE_TYPES,
304
+ normalizeQueueEnvelope,
305
+ stripQueueEnvelope,
306
+ inferQueueType,
307
+ positiveSeq,
308
+ };
@@ -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. Please run: ufoo bus join");
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
- const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, 2000, this.projectRoot);
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 = daemon.injector.readTty(target);
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
  */