u-foo 1.0.6 → 1.2.0

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 (149) hide show
  1. package/README.md +247 -23
  2. package/SKILLS/ufoo/SKILL.md +17 -2
  3. package/SKILLS/uinit/SKILL.md +8 -3
  4. package/bin/ucode-core.js +15 -0
  5. package/bin/ucode.js +125 -0
  6. package/bin/ufoo-assistant-agent.js +5 -0
  7. package/bin/ufoo-engine.js +25 -0
  8. package/bin/ufoo.js +4 -0
  9. package/modules/AGENTS.template.md +14 -4
  10. package/modules/bus/README.md +8 -5
  11. package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
  12. package/modules/context/SKILLS/uctx/SKILL.md +3 -1
  13. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  14. package/package.json +12 -3
  15. package/scripts/import-pi-mono.js +124 -0
  16. package/scripts/postinstall.js +20 -49
  17. package/scripts/sync-claude-skills.sh +21 -0
  18. package/src/agent/cliRunner.js +524 -31
  19. package/src/agent/internalRunner.js +76 -9
  20. package/src/agent/launcher.js +97 -45
  21. package/src/agent/normalizeOutput.js +1 -1
  22. package/src/agent/notifier.js +144 -4
  23. package/src/agent/ptyRunner.js +480 -10
  24. package/src/agent/ptyWrapper.js +28 -3
  25. package/src/agent/readyDetector.js +16 -0
  26. package/src/agent/ucode.js +443 -0
  27. package/src/agent/ucodeBootstrap.js +113 -0
  28. package/src/agent/ucodeBuild.js +67 -0
  29. package/src/agent/ucodeDoctor.js +184 -0
  30. package/src/agent/ucodeRuntimeConfig.js +129 -0
  31. package/src/agent/ufooAgent.js +168 -28
  32. package/src/assistant/agent.js +260 -0
  33. package/src/assistant/bridge.js +172 -0
  34. package/src/assistant/engine.js +252 -0
  35. package/src/assistant/stdio.js +58 -0
  36. package/src/assistant/ufooEngineCli.js +306 -0
  37. package/src/bus/activate.js +27 -11
  38. package/src/bus/daemon.js +133 -5
  39. package/src/bus/index.js +137 -80
  40. package/src/bus/inject.js +47 -17
  41. package/src/bus/message.js +145 -17
  42. package/src/bus/nickname.js +3 -1
  43. package/src/bus/queue.js +6 -1
  44. package/src/bus/store.js +189 -0
  45. package/src/bus/subscriber.js +20 -4
  46. package/src/bus/utils.js +9 -3
  47. package/src/chat/agentBar.js +117 -0
  48. package/src/chat/agentDirectory.js +88 -0
  49. package/src/chat/agentSockets.js +225 -0
  50. package/src/chat/agentViewController.js +298 -0
  51. package/src/chat/chatLogController.js +115 -0
  52. package/src/chat/commandExecutor.js +700 -0
  53. package/src/chat/commands.js +132 -0
  54. package/src/chat/completionController.js +414 -0
  55. package/src/chat/cronScheduler.js +160 -0
  56. package/src/chat/daemonConnection.js +166 -0
  57. package/src/chat/daemonCoordinator.js +64 -0
  58. package/src/chat/daemonMessageRouter.js +257 -0
  59. package/src/chat/daemonReconnect.js +41 -0
  60. package/src/chat/daemonTransport.js +36 -0
  61. package/src/chat/daemonTransportDefaults.js +10 -0
  62. package/src/chat/dashboardKeyController.js +480 -0
  63. package/src/chat/dashboardView.js +157 -0
  64. package/src/chat/index.js +938 -2910
  65. package/src/chat/inputHistoryController.js +105 -0
  66. package/src/chat/inputListenerController.js +304 -0
  67. package/src/chat/inputMath.js +104 -0
  68. package/src/chat/inputSubmitHandler.js +171 -0
  69. package/src/chat/layout.js +165 -0
  70. package/src/chat/pasteController.js +81 -0
  71. package/src/chat/rawKeyMap.js +42 -0
  72. package/src/chat/settingsController.js +133 -0
  73. package/src/chat/statusLineController.js +177 -0
  74. package/src/chat/streamTracker.js +138 -0
  75. package/src/chat/text.js +70 -0
  76. package/src/chat/transport.js +61 -0
  77. package/src/cli/busCoreCommands.js +59 -0
  78. package/src/cli/ctxCoreCommands.js +199 -0
  79. package/src/cli/onlineCoreCommands.js +379 -0
  80. package/src/cli.js +741 -238
  81. package/src/code/README.md +29 -0
  82. package/src/code/UCODE_PROMPT.md +32 -0
  83. package/src/code/agent.js +1651 -0
  84. package/src/code/cli.js +158 -0
  85. package/src/code/config +0 -0
  86. package/src/code/dispatch.js +42 -0
  87. package/src/code/index.js +70 -0
  88. package/src/code/nativeRunner.js +1213 -0
  89. package/src/code/runtime.js +154 -0
  90. package/src/code/sessionStore.js +162 -0
  91. package/src/code/taskDecomposer.js +269 -0
  92. package/src/code/tools/bash.js +53 -0
  93. package/src/code/tools/common.js +42 -0
  94. package/src/code/tools/edit.js +70 -0
  95. package/src/code/tools/read.js +44 -0
  96. package/src/code/tools/write.js +35 -0
  97. package/src/code/tui.js +1587 -0
  98. package/src/config.js +50 -2
  99. package/src/context/decisions.js +12 -2
  100. package/src/context/index.js +18 -1
  101. package/src/context/sync.js +127 -0
  102. package/src/daemon/agentProcessManager.js +74 -0
  103. package/src/daemon/cronOps.js +241 -0
  104. package/src/daemon/index.js +662 -489
  105. package/src/daemon/ipcServer.js +99 -0
  106. package/src/daemon/ops.js +417 -179
  107. package/src/daemon/promptLoop.js +319 -0
  108. package/src/daemon/promptRequest.js +101 -0
  109. package/src/daemon/providerSessions.js +32 -17
  110. package/src/daemon/reporting.js +90 -0
  111. package/src/daemon/run.js +2 -5
  112. package/src/daemon/status.js +24 -1
  113. package/src/init/index.js +68 -14
  114. package/src/online/bridge.js +663 -0
  115. package/src/online/client.js +245 -0
  116. package/src/online/runner.js +253 -0
  117. package/src/online/server.js +992 -0
  118. package/src/online/tokens.js +103 -0
  119. package/src/report/store.js +331 -0
  120. package/src/shared/eventContract.js +35 -0
  121. package/src/shared/ptySocketContract.js +21 -0
  122. package/src/status/index.js +50 -17
  123. package/src/terminal/adapterContract.js +87 -0
  124. package/src/terminal/adapterRouter.js +84 -0
  125. package/src/terminal/adapters/externalAdapter.js +14 -0
  126. package/src/terminal/adapters/internalAdapter.js +13 -0
  127. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  128. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  129. package/src/terminal/adapters/terminalAdapter.js +31 -0
  130. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  131. package/src/ufoo/agentsStore.js +69 -3
  132. package/src/utils/banner.js +5 -2
  133. package/scripts/.archived/bash-to-js-migration/README.md +0 -46
  134. package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
  135. package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
  136. package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
  137. package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
  138. package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
  139. package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
  140. package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
  141. package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
  142. package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
  143. package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
  144. package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
  145. package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
  146. package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
  147. package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
  148. package/scripts/banner.sh +0 -2
  149. package/src/bus/API_DESIGN.md +0 -204
@@ -0,0 +1,58 @@
1
+ const { runAssistantAgentTask } = require("./agent");
2
+
3
+ function readStdin() {
4
+ return new Promise((resolve) => {
5
+ let data = "";
6
+ process.stdin.setEncoding("utf8");
7
+ process.stdin.on("data", (chunk) => {
8
+ data += chunk;
9
+ });
10
+ process.stdin.on("end", () => resolve(data));
11
+ process.stdin.resume();
12
+ });
13
+ }
14
+
15
+ async function runAssistantStdio() {
16
+ const startedAt = Date.now();
17
+ try {
18
+ const input = await readStdin();
19
+ const line = String(input || "")
20
+ .split(/\r?\n/)
21
+ .map((part) => part.trim())
22
+ .find(Boolean);
23
+
24
+ if (!line) {
25
+ process.stdout.write(
26
+ `${JSON.stringify({
27
+ ok: false,
28
+ summary: "",
29
+ artifacts: [],
30
+ logs: [],
31
+ error: "missing request payload",
32
+ metrics: { duration_ms: Date.now() - startedAt },
33
+ })}\n`
34
+ );
35
+ process.exitCode = 1;
36
+ return;
37
+ }
38
+
39
+ const payload = JSON.parse(line);
40
+ const result = await runAssistantAgentTask(payload);
41
+ process.stdout.write(`${JSON.stringify(result)}\n`);
42
+ process.exitCode = result.ok ? 0 : 1;
43
+ } catch (err) {
44
+ process.stdout.write(
45
+ `${JSON.stringify({
46
+ ok: false,
47
+ summary: "",
48
+ artifacts: [],
49
+ logs: [],
50
+ error: err && err.message ? err.message : "assistant stdio failed",
51
+ metrics: { duration_ms: Date.now() - startedAt },
52
+ })}\n`
53
+ );
54
+ process.exitCode = 1;
55
+ }
56
+ }
57
+
58
+ module.exports = { runAssistantStdio };
@@ -0,0 +1,306 @@
1
+ const { runCliAgent } = require("../agent/cliRunner");
2
+ const { normalizeCliOutput } = require("../agent/normalizeOutput");
3
+ const { loadConfig } = require("../config");
4
+
5
+ function normalizeProvider(value, fallback = "codex-cli") {
6
+ const raw = String(value || "").trim().toLowerCase();
7
+ if (!raw) return fallback;
8
+ if (raw === "codex" || raw === "codex-cli") return "codex-cli";
9
+ if (raw === "claude" || raw === "claude-cli") return "claude-cli";
10
+ return fallback;
11
+ }
12
+
13
+ function parseAssistantTaskArgs(argv = []) {
14
+ const options = {
15
+ assistantTask: false,
16
+ json: false,
17
+ cwd: "",
18
+ model: "",
19
+ sessionId: "",
20
+ provider: "",
21
+ kind: "mixed",
22
+ context: "",
23
+ expect: "",
24
+ task: "",
25
+ };
26
+
27
+ const args = Array.isArray(argv) ? argv.slice() : [];
28
+ const rest = [];
29
+ for (let i = 0; i < args.length; i += 1) {
30
+ const arg = args[i];
31
+ if (arg === "--assistant-task") {
32
+ options.assistantTask = true;
33
+ continue;
34
+ }
35
+ if (arg === "--json") {
36
+ options.json = true;
37
+ continue;
38
+ }
39
+ if (arg === "--cwd") {
40
+ options.cwd = args[++i] || "";
41
+ continue;
42
+ }
43
+ if (arg === "--model") {
44
+ options.model = args[++i] || "";
45
+ continue;
46
+ }
47
+ if (arg === "--session-id") {
48
+ options.sessionId = args[++i] || "";
49
+ continue;
50
+ }
51
+ if (arg === "--provider") {
52
+ options.provider = args[++i] || "";
53
+ continue;
54
+ }
55
+ if (arg === "--kind") {
56
+ options.kind = args[++i] || "mixed";
57
+ continue;
58
+ }
59
+ if (arg === "--context") {
60
+ options.context = args[++i] || "";
61
+ continue;
62
+ }
63
+ if (arg === "--expect") {
64
+ options.expect = args[++i] || "";
65
+ continue;
66
+ }
67
+ rest.push(arg);
68
+ }
69
+ options.task = rest.join(" ").trim();
70
+ return options;
71
+ }
72
+
73
+ function buildPrompt({ kind = "mixed", context = "", task = "", expect = "" } = {}) {
74
+ const lines = [];
75
+ lines.push(`Task kind: ${kind || "mixed"}`);
76
+ if (context) {
77
+ lines.push("Context:");
78
+ lines.push(context);
79
+ }
80
+ lines.push("Task:");
81
+ lines.push(task || "");
82
+ if (expect) {
83
+ lines.push("Expected result:");
84
+ lines.push(expect);
85
+ }
86
+ return lines.join("\n");
87
+ }
88
+
89
+ function buildSystemPrompt() {
90
+ return [
91
+ "You are ufoo-engine, a self-hosted assistant core.",
92
+ "Return ONLY valid JSON.",
93
+ "Schema:",
94
+ "{",
95
+ ' "ok": true|false,',
96
+ ' "summary": "string",',
97
+ ' "artifacts": ["string"],',
98
+ ' "logs": ["string"],',
99
+ ' "error": "string",',
100
+ ' "metrics": {"key":"value"}',
101
+ "}",
102
+ "Rules:",
103
+ "- summary should be concise and actionable.",
104
+ "- error must be non-empty only when ok=false.",
105
+ "- Do not output markdown wrappers.",
106
+ ].join("\n");
107
+ }
108
+
109
+ function normalizeEngineResult(parsed, fallbackError = "") {
110
+ if (!parsed || typeof parsed !== "object") {
111
+ const text = String(parsed || "").trim();
112
+ if (text) {
113
+ return {
114
+ ok: true,
115
+ summary: text,
116
+ artifacts: [],
117
+ logs: [],
118
+ error: "",
119
+ metrics: {},
120
+ };
121
+ }
122
+ return {
123
+ ok: false,
124
+ summary: "",
125
+ artifacts: [],
126
+ logs: [],
127
+ error: fallbackError || "ufoo-engine invalid response",
128
+ metrics: {},
129
+ };
130
+ }
131
+
132
+ return {
133
+ ok: parsed.ok !== false,
134
+ summary: typeof parsed.summary === "string" ? parsed.summary : "",
135
+ artifacts: Array.isArray(parsed.artifacts) ? parsed.artifacts : [],
136
+ logs: Array.isArray(parsed.logs) ? parsed.logs : [],
137
+ error: typeof parsed.error === "string" ? parsed.error : "",
138
+ metrics: parsed.metrics && typeof parsed.metrics === "object" ? parsed.metrics : {},
139
+ };
140
+ }
141
+
142
+ function isSessionError(errorText = "") {
143
+ const text = String(errorText || "").toLowerCase();
144
+ return text.includes("session id")
145
+ || text.includes("session-id")
146
+ || text.includes("already in use");
147
+ }
148
+
149
+ function parseStdinPayload(stdinText = "") {
150
+ const line = String(stdinText || "")
151
+ .split(/\r?\n/)
152
+ .map((part) => part.trim())
153
+ .find(Boolean);
154
+ if (!line) return null;
155
+ try {
156
+ return JSON.parse(line);
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ async function runEngineTask(taskInput, deps = {}) {
163
+ const {
164
+ runCliAgentImpl = runCliAgent,
165
+ normalizeCliOutputImpl = normalizeCliOutput,
166
+ loadConfigImpl = loadConfig,
167
+ env = process.env,
168
+ cwd = process.cwd(),
169
+ } = deps;
170
+
171
+ const projectRoot = taskInput.cwd || taskInput.projectRoot || cwd;
172
+ const config = loadConfigImpl(projectRoot);
173
+ const provider = normalizeProvider(
174
+ taskInput.provider || env.UFOO_UFOO_ENGINE_PROVIDER || config.agentProvider,
175
+ "codex-cli"
176
+ );
177
+ const model =
178
+ String(taskInput.model || "").trim()
179
+ || String(env.UFOO_UFOO_ENGINE_MODEL || "").trim()
180
+ || String(config.agentModel || "").trim()
181
+ || (provider === "claude-cli" ? "opus" : "");
182
+
183
+ const systemPrompt = buildSystemPrompt();
184
+ const prompt = buildPrompt({
185
+ kind: taskInput.kind,
186
+ context: taskInput.context,
187
+ task: taskInput.task,
188
+ expect: taskInput.expect,
189
+ });
190
+ const timeoutMs = Number.isFinite(taskInput.timeoutMs) ? taskInput.timeoutMs : 60000;
191
+
192
+ const runOnce = async (sessionId) => runCliAgentImpl({
193
+ provider,
194
+ model,
195
+ prompt,
196
+ systemPrompt,
197
+ sessionId: sessionId || undefined,
198
+ disableSession: false,
199
+ cwd: projectRoot,
200
+ timeoutMs,
201
+ sandbox: taskInput.kind === "explore" ? "read-only" : "workspace-write",
202
+ });
203
+
204
+ let cliRes = await runOnce(taskInput.sessionId || "");
205
+ if (!cliRes.ok && taskInput.sessionId && isSessionError(cliRes.error)) {
206
+ cliRes = await runOnce("");
207
+ }
208
+
209
+ if (!cliRes.ok) {
210
+ return {
211
+ ok: false,
212
+ summary: "",
213
+ artifacts: [],
214
+ logs: [],
215
+ error: cliRes.error || "ufoo-engine cli failed",
216
+ metrics: {},
217
+ session_id: "",
218
+ };
219
+ }
220
+
221
+ const normalized = normalizeCliOutputImpl(cliRes.output);
222
+ let parsed;
223
+ try {
224
+ parsed = JSON.parse(normalized);
225
+ } catch {
226
+ parsed = normalized;
227
+ }
228
+ const result = normalizeEngineResult(parsed);
229
+ return {
230
+ ...result,
231
+ session_id: cliRes.sessionId || "",
232
+ };
233
+ }
234
+
235
+ async function runUfooEngineCli({ argv = [], stdinText = "", deps = {} } = {}) {
236
+ const options = parseAssistantTaskArgs(argv);
237
+
238
+ let taskInput;
239
+ if (options.assistantTask) {
240
+ if (!options.task) {
241
+ const error = {
242
+ ok: false,
243
+ summary: "",
244
+ artifacts: [],
245
+ logs: [],
246
+ error: "missing task",
247
+ metrics: {},
248
+ };
249
+ return { exitCode: 1, output: `${JSON.stringify(error)}\n` };
250
+ }
251
+ taskInput = {
252
+ task: options.task,
253
+ kind: options.kind,
254
+ context: options.context,
255
+ expect: options.expect,
256
+ provider: options.provider,
257
+ model: options.model,
258
+ sessionId: options.sessionId,
259
+ cwd: options.cwd,
260
+ timeoutMs: 60000,
261
+ };
262
+ } else {
263
+ const payload = parseStdinPayload(stdinText);
264
+ if (!payload || typeof payload !== "object") {
265
+ const error = {
266
+ ok: false,
267
+ summary: "",
268
+ artifacts: [],
269
+ logs: [],
270
+ error: "missing request payload",
271
+ metrics: {},
272
+ };
273
+ return { exitCode: 1, output: `${JSON.stringify(error)}\n` };
274
+ }
275
+ taskInput = {
276
+ task: typeof payload.task === "string" ? payload.task : "",
277
+ kind: typeof payload.kind === "string" ? payload.kind : "mixed",
278
+ context: typeof payload.context === "string" ? payload.context : "",
279
+ expect: typeof payload.expect === "string" ? payload.expect : "",
280
+ provider: typeof payload.provider === "string" ? payload.provider : "",
281
+ model: typeof payload.model === "string" ? payload.model : "",
282
+ sessionId: typeof payload.session_id === "string" ? payload.session_id : "",
283
+ cwd: typeof payload.project_root === "string" ? payload.project_root : "",
284
+ timeoutMs: Number.isFinite(payload.timeout_ms) ? payload.timeout_ms : 60000,
285
+ };
286
+ }
287
+
288
+ const result = await runEngineTask(taskInput, deps);
289
+ const output = `${JSON.stringify(result)}\n`;
290
+ return {
291
+ exitCode: result.ok === false ? 1 : 0,
292
+ output,
293
+ };
294
+ }
295
+
296
+ module.exports = {
297
+ normalizeProvider,
298
+ parseAssistantTaskArgs,
299
+ buildPrompt,
300
+ buildSystemPrompt,
301
+ normalizeEngineResult,
302
+ parseStdinPayload,
303
+ runEngineTask,
304
+ runUfooEngineCli,
305
+ isSessionError,
306
+ };
@@ -1,6 +1,7 @@
1
1
  const fs = require("fs");
2
2
  const { getUfooPaths } = require("../ufoo/paths");
3
3
  const { spawn, spawnSync } = require("child_process");
4
+ const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
4
5
 
5
6
  /**
6
7
  * 激活指定 agent 的终端
@@ -135,21 +136,36 @@ end tell`;
135
136
  async activate(agentId) {
136
137
  const info = this.getAgentInfo(agentId);
137
138
 
138
- if (info.launch_mode === "internal" || info.launch_mode === "internal-pty") {
139
- throw new Error("Internal mode agents cannot be activated (no terminal window)");
140
- }
139
+ const activateTerminal = async () => {
140
+ if (info.tty) {
141
+ this.activateTerminalByTty(info.tty);
142
+ return;
143
+ }
144
+ throw new Error("Cannot activate: missing tty or tmux_pane for agent");
145
+ };
141
146
 
142
- if (info.launch_mode === "tmux" && info.tmux_pane) {
143
- await this.activateTmuxPane(info.tmux_pane);
144
- return;
145
- }
147
+ const activateTmux = async () => {
148
+ if (info.tmux_pane) {
149
+ await this.activateTmuxPane(info.tmux_pane);
150
+ return;
151
+ }
152
+ throw new Error("Cannot activate: missing tty or tmux_pane for agent");
153
+ };
146
154
 
147
- if (info.launch_mode === "terminal" && info.tty) {
148
- this.activateTerminalByTty(info.tty);
149
- return;
155
+ const adapterRouter = createTerminalAdapterRouter({
156
+ activateTerminal,
157
+ activateTmux,
158
+ });
159
+ const adapter = adapterRouter.getAdapter({ launchMode: info.launch_mode, agentId });
160
+
161
+ if (!adapter.capabilities.supportsActivate) {
162
+ if (adapter.capabilities.supportsInternalQueueLoop) {
163
+ throw new Error("Internal mode agents cannot be activated (no terminal window)");
164
+ }
165
+ throw new Error("Cannot activate: missing tty or tmux_pane for agent");
150
166
  }
151
167
 
152
- throw new Error("Cannot activate: missing tty or tmux_pane for agent");
168
+ await adapter.activate();
153
169
  }
154
170
  }
155
171
 
package/src/bus/daemon.js CHANGED
@@ -3,6 +3,8 @@ const path = require("path");
3
3
  const { readJSON, writeJSON, isPidAlive, isAgentPidAlive, ensureDir, safeNameToSubscriber, subscriberToSafeName } = require("./utils");
4
4
  const Injector = require("./inject");
5
5
  const QueueManager = require("./queue");
6
+ const MessageManager = require("./message");
7
+ const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
6
8
 
7
9
  /**
8
10
  * Bus Daemon - 监控消息并自动注入命令
@@ -22,6 +24,7 @@ class BusDaemon {
22
24
 
23
25
  this.queueManager = new QueueManager(busDir);
24
26
  this.injector = new Injector(busDir, agentsFile);
27
+ this.adapterRouter = createTerminalAdapterRouter();
25
28
  }
26
29
 
27
30
  /**
@@ -198,6 +201,8 @@ class BusDaemon {
198
201
  return;
199
202
  }
200
203
 
204
+ const busData = readJSON(this.agentsFile) || { agents: {} };
205
+ const messageManager = new MessageManager(this.busDir, busData, this.queueManager);
201
206
  const subscribers = fs.readdirSync(queuesDir);
202
207
 
203
208
  for (const safeName of subscribers) {
@@ -206,6 +211,19 @@ class BusDaemon {
206
211
  continue;
207
212
  }
208
213
 
214
+ const subscriber = safeNameToSubscriber(safeName);
215
+ const meta = busData.agents?.[subscriber];
216
+ const launchMode = meta?.launch_mode || "";
217
+ // Delivery ownership:
218
+ // - notifier/injector: terminal/tmux
219
+ // - internal queue loop: internal/internal-pty
220
+ // Bus daemon only handles legacy/unknown launch modes.
221
+ const adapter = this.adapterRouter.getAdapter({ launchMode, agentId: subscriber });
222
+ const { supportsNotifierInjector, supportsInternalQueueLoop } = adapter.capabilities;
223
+ if (launchMode && (supportsNotifierInjector || supportsInternalQueueLoop)) {
224
+ continue;
225
+ }
226
+
209
227
  // 获取当前消息数
210
228
  let count = 0;
211
229
  if (fs.statSync(pendingFile).size > 0) {
@@ -217,14 +235,85 @@ class BusDaemon {
217
235
  const lastCount = this.getLastCount(safeName);
218
236
 
219
237
  // 如果有新消息,注入命令
220
- if (count > lastCount) {
221
- const subscriber = safeNameToSubscriber(safeName);
238
+ const wakePath = path.join(queuesDir, safeName, "wake");
239
+ const wakeActive = fs.existsSync(wakePath);
240
+
241
+ if (count > 0 || wakeActive) {
222
242
  const now = new Date().toISOString().split("T")[1].slice(0, 8);
223
- console.log(`[daemon] ${now} New message for ${subscriber} (${lastCount} -> ${count})`);
243
+ const note = wakeActive && count <= lastCount ? " (wake)" : "";
244
+ console.log(`[daemon] ${now} New message for ${subscriber} (${lastCount} -> ${count})${note}`);
224
245
 
225
246
  try {
226
- await this.injector.inject(subscriber);
227
- console.log(`[daemon] Injected /bus into ${subscriber}`);
247
+ const agentType = String((meta && meta.agent_type) || "").trim().toLowerCase();
248
+ const isUfooCode = subscriber.startsWith("ufoo-code:")
249
+ || agentType === "ufoo-code"
250
+ || agentType === "ucode"
251
+ || agentType === "ufoo";
252
+ if (isUfooCode) {
253
+ // ufoo-code queue is consumed internally by ucode itself.
254
+ // Bus daemon should not inject any command/text into terminal.
255
+ if (wakeActive) fs.rmSync(wakePath, { force: true });
256
+ this.setLastCount(safeName, count);
257
+ continue;
258
+ }
259
+
260
+ const events = this.drainPending(pendingFile);
261
+ const failed = [];
262
+ for (const evt of events) {
263
+ if (!evt || evt.event !== "message" || !evt.data || typeof evt.data.message !== "string") {
264
+ continue;
265
+ }
266
+ try {
267
+ // eslint-disable-next-line no-await-in-loop
268
+ await this.injector.inject(subscriber, String(evt.data.message));
269
+ } catch (err) {
270
+ failed.push(evt);
271
+ try {
272
+ const pub = typeof evt.publisher === "object" && evt.publisher
273
+ ? (evt.publisher.subscriber || evt.publisher.nickname || "")
274
+ : (evt.publisher || "");
275
+ if (pub) {
276
+ // eslint-disable-next-line no-await-in-loop
277
+ await messageManager.emit(pub, "delivery", {
278
+ target: subscriber,
279
+ seq: evt.seq,
280
+ status: "error",
281
+ message: `delivery failed to ${meta?.nickname || subscriber}: ${err.message || "inject failed"}`,
282
+ }, subscriber, "status/delivery");
283
+ }
284
+ } catch {
285
+ // ignore delivery emit errors
286
+ }
287
+ continue;
288
+ }
289
+ try {
290
+ // Emit delivery status back to publisher (best-effort)
291
+ const pub = typeof evt.publisher === "object" && evt.publisher
292
+ ? (evt.publisher.subscriber || evt.publisher.nickname || "")
293
+ : (evt.publisher || "");
294
+ if (pub) {
295
+ // eslint-disable-next-line no-await-in-loop
296
+ await messageManager.emit(pub, "delivery", {
297
+ target: subscriber,
298
+ seq: evt.seq,
299
+ status: "ok",
300
+ message: `delivered to ${meta?.nickname || subscriber}`,
301
+ }, subscriber, "status/delivery");
302
+ }
303
+ } catch {
304
+ // ignore delivery emit errors
305
+ }
306
+ }
307
+ if (failed.length > 0) {
308
+ try {
309
+ const content = failed.map((e) => JSON.stringify(e)).join("\n") + "\n";
310
+ fs.appendFileSync(pendingFile, content, "utf8");
311
+ } catch {
312
+ // ignore requeue failures
313
+ }
314
+ }
315
+ console.log(`[daemon] Delivered ${events.length} message(s) to ${subscriber}`);
316
+ if (wakeActive) fs.rmSync(wakePath, { force: true });
228
317
  } catch (err) {
229
318
  console.error(`[daemon] Failed to inject: ${err.message}`);
230
319
  }
@@ -235,6 +324,45 @@ class BusDaemon {
235
324
  }
236
325
  }
237
326
 
327
+ drainPending(pendingFile) {
328
+ if (!fs.existsSync(pendingFile)) return [];
329
+ const processingFile = `${pendingFile}.processing.${process.pid}.${Date.now()}`;
330
+ let content = "";
331
+ let readOk = false;
332
+ try {
333
+ fs.renameSync(pendingFile, processingFile);
334
+ content = fs.readFileSync(processingFile, "utf8");
335
+ readOk = true;
336
+ } catch {
337
+ try {
338
+ if (fs.existsSync(processingFile)) {
339
+ fs.renameSync(processingFile, pendingFile);
340
+ }
341
+ } catch {
342
+ // ignore rollback errors
343
+ }
344
+ return [];
345
+ } finally {
346
+ if (readOk) {
347
+ try {
348
+ if (fs.existsSync(processingFile)) {
349
+ fs.rmSync(processingFile, { force: true });
350
+ }
351
+ } catch {
352
+ // ignore cleanup errors
353
+ }
354
+ }
355
+ }
356
+ if (!content.trim()) return [];
357
+ return content.split(/\r?\n/).filter(Boolean).map((line) => {
358
+ try {
359
+ return JSON.parse(line);
360
+ } catch {
361
+ return null;
362
+ }
363
+ }).filter(Boolean);
364
+ }
365
+
238
366
  /**
239
367
  * 获取上次的消息计数
240
368
  */