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
@@ -2,6 +2,7 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { getUfooPaths } = require("../ufoo/paths");
4
4
  const { spawnSync } = require("child_process");
5
+ const EventBus = require("../bus");
5
6
  const { runCliAgent } = require("./cliRunner");
6
7
  const { normalizeCliOutput } = require("./normalizeOutput");
7
8
 
@@ -37,6 +38,30 @@ function safeSubscriber(subscriber) {
37
38
  return subscriber.replace(/:/g, "_");
38
39
  }
39
40
 
41
+ function createBusSender(projectRoot, subscriber) {
42
+ const eventBus = new EventBus(projectRoot);
43
+ let sendQueue = Promise.resolve();
44
+
45
+ function enqueue(target, message) {
46
+ if (!target || !message) return;
47
+ sendQueue = sendQueue
48
+ .then(() => eventBus.send(target, message, subscriber))
49
+ .catch(() => {
50
+ // ignore per-message bus send errors to keep runner loop alive
51
+ });
52
+ }
53
+
54
+ async function flush() {
55
+ try {
56
+ await sendQueue;
57
+ } catch {
58
+ // ignore flush errors
59
+ }
60
+ }
61
+
62
+ return { enqueue, flush };
63
+ }
64
+
40
65
  function drainQueue(queueFile) {
41
66
  if (!fs.existsSync(queueFile)) return [];
42
67
  const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
@@ -70,11 +95,20 @@ function drainQueue(queueFile) {
70
95
  return content.split(/\r?\n/).filter(Boolean);
71
96
  }
72
97
 
73
- async function handleEvent(projectRoot, agentType, provider, model, subscriber, nickname, evt, cliSessionState) {
98
+ async function handleEvent(projectRoot, agentType, provider, model, subscriber, nickname, evt, cliSessionState, busSender) {
74
99
  if (!evt || !evt.data || !evt.data.message) return;
75
100
  const prompt = evt.data.message;
76
101
  const publisher = evt.publisher || "unknown";
77
102
  const sandbox = "workspace-write";
103
+ const streamState = { emitted: false, lastChar: "" };
104
+
105
+ const emitStreamDelta = (delta) => {
106
+ const text = String(delta || "");
107
+ if (!text) return;
108
+ streamState.emitted = true;
109
+ streamState.lastChar = text.slice(-1);
110
+ busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: text }));
111
+ };
78
112
 
79
113
  let res = await runCliAgent({
80
114
  provider,
@@ -83,6 +117,7 @@ async function handleEvent(projectRoot, agentType, provider, model, subscriber,
83
117
  sessionId: cliSessionState.cliSessionId,
84
118
  sandbox,
85
119
  cwd: projectRoot,
120
+ onStreamDelta: emitStreamDelta,
86
121
  });
87
122
 
88
123
  // Handle session errors with immediate retry (only for claude)
@@ -100,6 +135,7 @@ async function handleEvent(projectRoot, agentType, provider, model, subscriber,
100
135
  sessionId: null, // Let runCliAgent generate new session
101
136
  sandbox,
102
137
  cwd: projectRoot,
138
+ onStreamDelta: emitStreamDelta,
103
139
  });
104
140
  }
105
141
  }
@@ -117,13 +153,25 @@ async function handleEvent(projectRoot, agentType, provider, model, subscriber,
117
153
  reply = `[internal:${agentType}] error: ${res.error || "unknown error"}`;
118
154
  }
119
155
 
156
+ if (streamState.emitted) {
157
+ if (!res.ok) {
158
+ if (streamState.lastChar !== "\n") {
159
+ busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: "\n" }));
160
+ }
161
+ busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: reply }));
162
+ }
163
+ busSender.enqueue(
164
+ publisher,
165
+ JSON.stringify({ stream: true, done: true, reason: res.ok ? "complete" : "error" })
166
+ );
167
+ await busSender.flush();
168
+ return;
169
+ }
170
+
120
171
  if (!reply) return;
121
172
 
122
- spawnSync("ufoo", ["bus", "send", publisher, reply], {
123
- cwd: projectRoot,
124
- env: { ...process.env, AI_BUS_PUBLISHER: subscriber },
125
- stdio: "ignore",
126
- });
173
+ busSender.enqueue(publisher, reply);
174
+ await busSender.flush();
127
175
  }
128
176
 
129
177
  async function runInternalRunner({ projectRoot, agentType = "codex" }) {
@@ -133,8 +181,13 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
133
181
 
134
182
  const queueDir = path.join(getUfooPaths(projectRoot).busQueuesDir, safeSubscriber(subscriber));
135
183
  const queueFile = path.join(queueDir, "pending.jsonl");
136
- const provider = agentType === "codex" ? "codex-cli" : "claude-cli";
184
+ const normalizedAgentType = String(agentType || "").trim().toLowerCase();
185
+ if (normalizedAgentType === "ufoo" || normalizedAgentType === "ucode" || normalizedAgentType === "ufoo-code") {
186
+ throw new Error("ufoo core is not supported by headless internal runner; use internal-pty");
187
+ }
188
+ const provider = normalizedAgentType === "codex" ? "codex-cli" : "claude-cli";
137
189
  const model = process.env.UFOO_AGENT_MODEL || "";
190
+ const busSender = createBusSender(projectRoot, subscriber);
138
191
 
139
192
  // Session state management for CLI continuity
140
193
  // Use stable path based on nickname (if exists) or agent type, NOT subscriber ID
@@ -206,7 +259,17 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
206
259
 
207
260
  for (const evt of events) {
208
261
  // eslint-disable-next-line no-await-in-loop
209
- await handleEvent(projectRoot, parsedAgentType, provider, model, subscriber, nickname, evt, cliSessionState);
262
+ await handleEvent(
263
+ projectRoot,
264
+ parsedAgentType,
265
+ provider,
266
+ model,
267
+ subscriber,
268
+ nickname,
269
+ evt,
270
+ cliSessionState,
271
+ busSender
272
+ );
210
273
  }
211
274
 
212
275
  // Persist CLI session state after processing (only if changed and for claude)
@@ -236,4 +299,8 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
236
299
  }
237
300
  }
238
301
 
239
- module.exports = { runInternalRunner };
302
+ module.exports = {
303
+ runInternalRunner,
304
+ createBusSender,
305
+ handleEvent,
306
+ };
@@ -1,3 +1,5 @@
1
+ const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
2
+ const { PTY_SOCKET_MESSAGE_TYPES } = require("../shared/ptySocketContract");
1
3
  const { spawn, spawnSync } = require("child_process");
2
4
  const fs = require("fs");
3
5
  const net = require("net");
@@ -7,6 +9,7 @@ const { isAgentPidAlive } = require("../bus/utils");
7
9
  const { showBanner } = require("../utils/banner");
8
10
  const AgentNotifier = require("./notifier");
9
11
  const { getUfooPaths } = require("../ufoo/paths");
12
+ const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
10
13
  const PtyWrapper = require("./ptyWrapper");
11
14
  const ReadyDetector = require("./readyDetector");
12
15
 
@@ -31,6 +34,21 @@ async function connectWithRetry(sockPath, retries, delayMs) {
31
34
  return null;
32
35
  }
33
36
 
37
+ async function probeDaemonSocket(sockPath) {
38
+ try {
39
+ const client = await connectSocket(sockPath);
40
+ try {
41
+ client.end();
42
+ client.destroy();
43
+ } catch {
44
+ // ignore cleanup errors
45
+ }
46
+ return { ok: true, code: "" };
47
+ } catch (err) {
48
+ return { ok: false, code: err && err.code ? err.code : "" };
49
+ }
50
+ }
51
+
34
52
  function normalizeTty(ttyPath) {
35
53
  if (!ttyPath) return "";
36
54
  const trimmed = String(ttyPath).trim();
@@ -136,6 +154,12 @@ function resolveLaunchMode() {
136
154
  return "terminal";
137
155
  }
138
156
 
157
+ function shouldShowLaunchBanner(agentType = "") {
158
+ if (process.env.UFOO_SUPPRESS_LAUNCHER_BANNER === "1") return false;
159
+ void agentType;
160
+ return true;
161
+ }
162
+
139
163
  /**
140
164
  * Agent 启动器
141
165
  * 统一处理 agent 启动流程:初始化、daemon 注册、banner、命令执行
@@ -209,19 +233,34 @@ class AgentLauncher {
209
233
  * 确保 daemon 正在运行
210
234
  */
211
235
  async ensureDaemon() {
212
- const pidFile = getUfooPaths(this.cwd).ufooDaemonPid;
236
+ const paths = getUfooPaths(this.cwd);
237
+ const pidFile = paths.ufooDaemonPid;
238
+ const sockFile = paths.ufooSock;
239
+
240
+ const existingProbe = await probeDaemonSocket(sockFile);
241
+ if (existingProbe.ok) return "running";
242
+ if (existingProbe.code === "EPERM" && fs.existsSync(pidFile)) {
243
+ // Sandbox may deny socket connect probes; keep prior behavior.
244
+ return "running";
245
+ }
213
246
 
214
- if (fs.existsSync(pidFile)) {
215
- const pidStr = fs.readFileSync(pidFile, "utf8").trim();
216
- if (pidStr) {
217
- const pid = parseInt(pidStr, 10);
218
- try {
219
- process.kill(pid, 0); // Check if alive
220
- return "running";
221
- } catch {
222
- // Dead, start new
223
- }
224
- }
247
+ // Stale daemon runtime markers can block restart with false-positive "running".
248
+ // Clean local runtime markers only when socket probe failed.
249
+ try {
250
+ if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
251
+ } catch {
252
+ // ignore
253
+ }
254
+ try {
255
+ if (fs.existsSync(sockFile)) fs.unlinkSync(sockFile);
256
+ } catch {
257
+ // ignore
258
+ }
259
+ try {
260
+ const lockFile = path.join(paths.runDir, "daemon.lock");
261
+ if (fs.existsSync(lockFile)) fs.unlinkSync(lockFile);
262
+ } catch {
263
+ // ignore
225
264
  }
226
265
 
227
266
  // Start daemon using correct command
@@ -231,23 +270,23 @@ class AgentLauncher {
231
270
  detached: true,
232
271
  });
233
272
 
234
- // Wait for daemon socket to be ready
235
- const sockFile = getUfooPaths(this.cwd).ufooSock;
273
+ // Wait for daemon socket to be ready and reachable
274
+ let lastProbeCode = "";
236
275
  for (let i = 0; i < 30; i++) {
237
- if (fs.existsSync(sockFile)) {
238
- try {
239
- const stat = fs.statSync(sockFile);
240
- if (stat.isSocket()) {
241
- break;
242
- }
243
- } catch {
244
- // Continue waiting
245
- }
276
+ const probe = await probeDaemonSocket(sockFile);
277
+ if (probe.ok) {
278
+ return "started";
279
+ }
280
+ if (probe.code === "EPERM" && fs.existsSync(pidFile)) {
281
+ return "started";
246
282
  }
283
+ lastProbeCode = probe.code || lastProbeCode;
247
284
  await new Promise((resolve) => setTimeout(resolve, 100));
248
285
  }
249
286
 
250
- return "started";
287
+ throw new Error(
288
+ `Failed to start ufoo daemon${lastProbeCode ? ` (${lastProbeCode})` : ""}`
289
+ );
251
290
  }
252
291
 
253
292
  /**
@@ -265,15 +304,17 @@ class AgentLauncher {
265
304
  const tmuxPane = process.env.TMUX_PANE || "";
266
305
  const launchMode = resolveLaunchMode();
267
306
 
268
- // 只在 terminal/tmux 模式下查找旧 session(可见终端才需要恢复)
307
+ // 只在支持 session reuse 的模式下查找旧 session(可见终端才需要恢复)
269
308
  // internal 模式由 daemon 管理,不需要自动恢复
270
- const shouldReuse = launchMode === "terminal" || launchMode === "tmux";
309
+ const adapterRouter = createTerminalAdapterRouter();
310
+ const adapter = adapterRouter.getAdapter({ launchMode, agentId: "" });
311
+ const shouldReuse = adapter.capabilities.supportsSessionReuse;
271
312
  const previousSession = shouldReuse
272
313
  ? findPreviousSession(this.cwd, this.agentType, tty, tmuxPane)
273
314
  : null;
274
315
 
275
316
  const req = {
276
- type: "register_agent",
317
+ type: IPC_REQUEST_TYPES.REGISTER_AGENT,
277
318
  agentType: this.agentType,
278
319
  nickname: nickname || (previousSession?.nickname) || "",
279
320
  parentPid: process.pid,
@@ -417,18 +458,24 @@ class AgentLauncher {
417
458
  // 4. 更新环境变量(供子进程/后续使用)
418
459
  if (subscriberId) process.env.UFOO_SUBSCRIBER_ID = subscriberId;
419
460
  if (finalNickname) process.env.UFOO_NICKNAME = finalNickname;
461
+ process.env.UFOO_AGENT_TYPE = this.agentType;
462
+
463
+ // 5. 显示 banner(ucode 自带 TUI banner,这里避免重复)
464
+ if (shouldShowLaunchBanner(this.agentType)) {
465
+ showBanner({
466
+ agentType: this.agentType,
467
+ sessionId,
468
+ nickname: finalNickname,
469
+ daemonStatus,
470
+ });
471
+ }
420
472
 
421
- // 5. 显示 banner
422
- showBanner({
423
- agentType: this.agentType,
424
- sessionId,
425
- nickname: finalNickname,
426
- daemonStatus,
427
- });
428
-
429
- // 6. 启动消息通知监听器
430
- const notifier = new AgentNotifier(this.cwd, subscriberId);
431
- notifier.start();
473
+ // 6. 启动消息通知监听器(ufoo-code 改为内部 bus 轮询消费)
474
+ const shouldStartNotifier = String(this.agentType || "").trim().toLowerCase() !== "ufoo-code";
475
+ if (shouldStartNotifier) {
476
+ const notifier = new AgentNotifier(this.cwd, subscriberId);
477
+ notifier.start();
478
+ }
432
479
 
433
480
  // 7. 启动命令(PTY wrapper或直接spawn)
434
481
 
@@ -480,7 +527,7 @@ class AgentLauncher {
480
527
  const daemonSock = await connectWithRetry(daemonSockPath, 3, 100);
481
528
  if (daemonSock) {
482
529
  daemonSock.write(`${JSON.stringify({
483
- type: "agent_ready",
530
+ type: IPC_REQUEST_TYPES.AGENT_READY,
484
531
  subscriberId,
485
532
  })}\n`);
486
533
  daemonSock.end();
@@ -571,7 +618,7 @@ class AgentLauncher {
571
618
  }
572
619
  // Forward to all output subscribers
573
620
  if (outputSubscribers.size > 0) {
574
- const msg = JSON.stringify({ type: "output", data: text, encoding: "utf8" }) + "\n";
621
+ const msg = JSON.stringify({ type: PTY_SOCKET_MESSAGE_TYPES.OUTPUT, data: text, encoding: "utf8" }) + "\n";
575
622
  for (const sub of outputSubscribers) {
576
623
  try {
577
624
  sub.write(msg);
@@ -595,6 +642,11 @@ class AgentLauncher {
595
642
  try {
596
643
  const req = JSON.parse(line);
597
644
  if (req.type === "inject" && req.command) {
645
+ const normalizedAgentType = String(this.agentType || "").trim().toLowerCase();
646
+ if (normalizedAgentType === "ufoo-code" || normalizedAgentType === "ucode" || normalizedAgentType === "ufoo") {
647
+ client.write(JSON.stringify({ ok: false, error: "inject disabled for ufoo-code (internal bus loop)" }) + "\n");
648
+ continue;
649
+ }
598
650
  // 注入命令到PTY(带延迟确保输入完成)
599
651
  wrapper.write(req.command);
600
652
  setTimeout(() => {
@@ -613,23 +665,23 @@ class AgentLauncher {
613
665
  };
614
666
  wrapper.logger.write(JSON.stringify(logEntry) + "\n");
615
667
  }
616
- } else if (req.type === "raw" && req.data) {
668
+ } else if (req.type === PTY_SOCKET_MESSAGE_TYPES.RAW && req.data) {
617
669
  // Raw PTY write (no Enter appended) - for TTY view passthrough
618
670
  wrapper.write(req.data);
619
671
  client.write(JSON.stringify({ ok: true }) + "\n");
620
- } else if (req.type === "resize" && req.cols && req.rows) {
672
+ } else if (req.type === PTY_SOCKET_MESSAGE_TYPES.RESIZE && req.cols && req.rows) {
621
673
  // Resize PTY - for TTY view viewport adjustment
622
674
  if (wrapper.pty && !wrapper.pty._closed) {
623
675
  wrapper.pty.resize(req.cols, req.rows);
624
676
  }
625
677
  client.write(JSON.stringify({ ok: true }) + "\n");
626
- } else if (req.type === "subscribe") {
678
+ } else if (req.type === PTY_SOCKET_MESSAGE_TYPES.SUBSCRIBE) {
627
679
  // Subscribe to PTY output stream for TTY view
628
680
  outputSubscribers.add(client);
629
- client.write(JSON.stringify({ type: "subscribed", ok: true }) + "\n");
681
+ client.write(JSON.stringify({ type: PTY_SOCKET_MESSAGE_TYPES.SUBSCRIBED, ok: true }) + "\n");
630
682
  // Replay from in-memory ring buffer
631
683
  if (outputRingBuffer.length > 0) {
632
- client.write(JSON.stringify({ type: "replay", data: outputRingBuffer, encoding: "utf8" }) + "\n");
684
+ client.write(JSON.stringify({ type: PTY_SOCKET_MESSAGE_TYPES.REPLAY, data: outputRingBuffer, encoding: "utf8" }) + "\n");
633
685
  }
634
686
  } else {
635
687
  client.write(JSON.stringify({ ok: false, error: "invalid request" }) + "\n");
@@ -3,7 +3,7 @@ function extractTextFromObject(obj) {
3
3
  if (obj.structured_output && typeof obj.structured_output === "object") {
4
4
  return JSON.stringify(obj.structured_output);
5
5
  }
6
- const candidates = ["output", "text", "message", "content", "output_text", "result"];
6
+ const candidates = ["reply", "output", "text", "message", "content", "output_text", "result"];
7
7
  for (const key of candidates) {
8
8
  const val = obj[key];
9
9
  if (typeof val === "string") return val;
@@ -1,5 +1,6 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
+ const EventBus = require("../bus");
3
4
  const Injector = require("../bus/inject");
4
5
  const { getUfooPaths } = require("../ufoo/paths");
5
6
  const { shakeTerminalByTty } = require("../bus/shake");
@@ -20,6 +21,7 @@ class AgentNotifier {
20
21
  this.stopped = false;
21
22
  this.autoTrigger = process.env.UFOO_AUTO_TRIGGER !== "0"; // 默认启用自动触发
22
23
  this.lastNickname = "";
24
+ this.lastUbusWakeCount = -1;
23
25
 
24
26
  // 计算队列文件路径
25
27
  const safeSub = subscriber.replace(/:/g, "_");
@@ -34,6 +36,11 @@ class AgentNotifier {
34
36
  // 初始化 injector
35
37
  const busDir = paths.busDir;
36
38
  this.injector = new Injector(busDir, paths.agentsFile);
39
+ this.eventBus = new EventBus(projectRoot);
40
+ }
41
+
42
+ isUfooCodeSubscriber() {
43
+ return String(this.subscriber || "").startsWith("ufoo-code:");
37
44
  }
38
45
 
39
46
  /**
@@ -104,6 +111,116 @@ class AgentNotifier {
104
111
  }
105
112
  }
106
113
 
114
+ drainPending() {
115
+ if (!fs.existsSync(this.queueFile)) return [];
116
+ const processingFile = `${this.queueFile}.processing.${process.pid}.${Date.now()}`;
117
+ let content = "";
118
+ let readOk = false;
119
+ try {
120
+ fs.renameSync(this.queueFile, processingFile);
121
+ content = fs.readFileSync(processingFile, "utf8");
122
+ readOk = true;
123
+ } catch {
124
+ try {
125
+ if (fs.existsSync(processingFile)) {
126
+ fs.renameSync(processingFile, this.queueFile);
127
+ }
128
+ } catch {
129
+ // ignore rollback errors
130
+ }
131
+ return [];
132
+ } finally {
133
+ if (readOk) {
134
+ try {
135
+ if (fs.existsSync(processingFile)) {
136
+ fs.rmSync(processingFile, { force: true });
137
+ }
138
+ } catch {
139
+ // ignore cleanup errors
140
+ }
141
+ }
142
+ }
143
+ if (!content.trim()) return [];
144
+ return content.split(/\r?\n/).filter(Boolean).map((line) => {
145
+ try {
146
+ return JSON.parse(line);
147
+ } catch {
148
+ return null;
149
+ }
150
+ }).filter(Boolean);
151
+ }
152
+
153
+ normalizePublisher(publisher) {
154
+ if (!publisher) return "";
155
+ if (typeof publisher === "string") return publisher;
156
+ if (typeof publisher === "object") {
157
+ return publisher.subscriber || publisher.nickname || "";
158
+ }
159
+ return String(publisher);
160
+ }
161
+
162
+ async emitDelivery(evt, status, errorMessage = "") {
163
+ const publisher = this.normalizePublisher(evt.publisher);
164
+ if (!publisher) return;
165
+ const data = {
166
+ target: this.subscriber,
167
+ seq: evt.seq,
168
+ status,
169
+ };
170
+ if (errorMessage) data.error = errorMessage;
171
+ // Provide a human-readable message for chat UI
172
+ if (status === "ok") {
173
+ data.message = `delivered to ${this.lastNickname || this.subscriber}`;
174
+ } else {
175
+ data.message = `delivery failed to ${this.lastNickname || this.subscriber}: ${errorMessage || "unknown error"}`;
176
+ }
177
+ try {
178
+ await this.eventBus.send(publisher, "", this.subscriber, { event: "delivery", data });
179
+ } catch {
180
+ // ignore delivery emit failures
181
+ }
182
+ }
183
+
184
+ async deliverPending() {
185
+ if (this.isUfooCodeSubscriber()) {
186
+ // ufoo-code consumes bus queue internally; notifier must not inject text/commands.
187
+ return 0;
188
+ }
189
+
190
+ const events = this.drainPending();
191
+ if (events.length === 0) return 0;
192
+ const failed = [];
193
+ let delivered = 0;
194
+ for (const evt of events) {
195
+ if (!evt || evt.event !== "message" || !evt.data || typeof evt.data.message !== "string") {
196
+ continue;
197
+ }
198
+ const message = String(evt.data.message);
199
+ try {
200
+ // Inject the actual message text into the terminal/tmux agent
201
+ // (Bus is the source of truth; inject is the delivery adapter.)
202
+ // eslint-disable-next-line no-await-in-loop
203
+ await this.injector.inject(this.subscriber, message);
204
+ delivered += 1;
205
+ // eslint-disable-next-line no-await-in-loop
206
+ await this.emitDelivery(evt, "ok");
207
+ } catch (err) {
208
+ failed.push(evt);
209
+ // eslint-disable-next-line no-await-in-loop
210
+ await this.emitDelivery(evt, "error", err.message || "inject failed");
211
+ }
212
+ }
213
+ if (failed.length > 0) {
214
+ try {
215
+ const content = failed.map((e) => JSON.stringify(e)).join("\n") + "\n";
216
+ fs.appendFileSync(this.queueFile, content, "utf8");
217
+ } catch {
218
+ // ignore requeue failures
219
+ }
220
+ }
221
+ return delivered;
222
+ }
223
+
107
224
  /**
108
225
  * 发送终端通知
109
226
  * iTerm2: 使用 OSC 9 原生通知
@@ -126,7 +243,7 @@ class AgentNotifier {
126
243
  if (!this.autoTrigger) return;
127
244
 
128
245
  try {
129
- await this.injector.inject(this.subscriber);
246
+ await this.deliverPending();
130
247
  } catch (err) {
131
248
  // 自动触发失败时静默忽略,用户仍可手动输入
132
249
  // console.error("[notifier] Auto-trigger failed:", err.message);
@@ -136,7 +253,7 @@ class AgentNotifier {
136
253
  /**
137
254
  * 轮询检查队列
138
255
  */
139
- poll() {
256
+ async poll() {
140
257
  if (this.stopped) return;
141
258
 
142
259
  const currentCount = this.getMessageCount();
@@ -152,7 +269,30 @@ class AgentNotifier {
152
269
  });
153
270
  }
154
271
 
155
- this.lastCount = currentCount;
272
+ // Ensure pending delivery happens even if count doesn't change
273
+ if (this.autoTrigger && currentCount > 0) {
274
+ if (this.isUfooCodeSubscriber()) {
275
+ if (this.lastUbusWakeCount !== currentCount) {
276
+ try {
277
+ await this.autoTriggerInput();
278
+ this.lastUbusWakeCount = currentCount;
279
+ } catch {
280
+ // ignore delivery errors
281
+ }
282
+ }
283
+ } else {
284
+ try {
285
+ await this.deliverPending();
286
+ } catch {
287
+ // ignore delivery errors
288
+ }
289
+ }
290
+ }
291
+ if (currentCount <= 0) {
292
+ this.lastUbusWakeCount = -1;
293
+ }
294
+
295
+ this.lastCount = this.getMessageCount();
156
296
  this.refreshTitle();
157
297
  this.updateHeartbeat();
158
298
  }
@@ -170,7 +310,7 @@ class AgentNotifier {
170
310
 
171
311
  // 启动轮询
172
312
  this.timer = setInterval(() => {
173
- this.poll();
313
+ this.poll().catch(() => {});
174
314
  }, this.interval);
175
315
 
176
316
  // 注册清理