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
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
|
-
|
|
887
|
-
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
let renamed = false;
|
|
887
|
+
const queue = new DeliveryQueue(target);
|
|
888
|
+
const drained = [];
|
|
889
|
+
const rawLines = [];
|
|
890
|
+
const claims = [];
|
|
891
891
|
try {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
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
|
-
|
|
1179
|
-
|
|
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
|
-
|
|
1204
|
-
|
|
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: ${
|
|
1207
|
-
|
|
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
|
-
|
|
1214
|
-
|
|
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.
|
|
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
|
*/
|