u-foo 1.0.3 → 1.1.9

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 (179) hide show
  1. package/README.md +110 -11
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +132 -0
  4. package/SKILLS/uinit/SKILL.md +78 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucode-core.js +15 -0
  8. package/bin/ucode.js +125 -0
  9. package/bin/ucodex.js +13 -0
  10. package/bin/ufoo +9 -31
  11. package/bin/ufoo-assistant-agent.js +5 -0
  12. package/bin/ufoo-engine.js +25 -0
  13. package/bin/ufoo.js +17 -0
  14. package/modules/AGENTS.template.md +29 -11
  15. package/modules/bus/README.md +33 -25
  16. package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
  17. package/modules/context/README.md +18 -40
  18. package/modules/context/SKILLS/uctx/SKILL.md +63 -1
  19. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  20. package/package.json +25 -4
  21. package/scripts/import-pi-mono.js +124 -0
  22. package/scripts/postinstall.js +30 -0
  23. package/scripts/sync-claude-skills.sh +21 -0
  24. package/src/agent/cliRunner.js +554 -33
  25. package/src/agent/internalRunner.js +150 -56
  26. package/src/agent/launcher.js +754 -0
  27. package/src/agent/normalizeOutput.js +1 -1
  28. package/src/agent/notifier.js +340 -0
  29. package/src/agent/ptyRunner.js +847 -0
  30. package/src/agent/ptyWrapper.js +379 -0
  31. package/src/agent/readyDetector.js +175 -0
  32. package/src/agent/ucode.js +443 -0
  33. package/src/agent/ucodeBootstrap.js +113 -0
  34. package/src/agent/ucodeBuild.js +67 -0
  35. package/src/agent/ucodeDoctor.js +184 -0
  36. package/src/agent/ucodeRuntimeConfig.js +129 -0
  37. package/src/agent/ufooAgent.js +46 -42
  38. package/src/assistant/agent.js +260 -0
  39. package/src/assistant/bridge.js +172 -0
  40. package/src/assistant/engine.js +252 -0
  41. package/src/assistant/stdio.js +58 -0
  42. package/src/assistant/ufooEngineCli.js +306 -0
  43. package/src/bus/activate.js +172 -0
  44. package/src/bus/daemon.js +436 -0
  45. package/src/bus/index.js +842 -0
  46. package/src/bus/inject.js +315 -0
  47. package/src/bus/message.js +430 -0
  48. package/src/bus/nickname.js +88 -0
  49. package/src/bus/queue.js +136 -0
  50. package/src/bus/shake.js +26 -0
  51. package/src/bus/store.js +189 -0
  52. package/src/bus/subscriber.js +312 -0
  53. package/src/bus/utils.js +363 -0
  54. package/src/chat/agentBar.js +117 -0
  55. package/src/chat/agentDirectory.js +88 -0
  56. package/src/chat/agentSockets.js +225 -0
  57. package/src/chat/agentViewController.js +298 -0
  58. package/src/chat/chatLogController.js +115 -0
  59. package/src/chat/commandExecutor.js +700 -0
  60. package/src/chat/commands.js +132 -0
  61. package/src/chat/completionController.js +414 -0
  62. package/src/chat/cronScheduler.js +160 -0
  63. package/src/chat/daemonConnection.js +166 -0
  64. package/src/chat/daemonCoordinator.js +64 -0
  65. package/src/chat/daemonMessageRouter.js +257 -0
  66. package/src/chat/daemonReconnect.js +41 -0
  67. package/src/chat/daemonTransport.js +36 -0
  68. package/src/chat/daemonTransportDefaults.js +10 -0
  69. package/src/chat/dashboardKeyController.js +480 -0
  70. package/src/chat/dashboardView.js +154 -0
  71. package/src/chat/index.js +1011 -1392
  72. package/src/chat/inputHistoryController.js +105 -0
  73. package/src/chat/inputListenerController.js +304 -0
  74. package/src/chat/inputMath.js +104 -0
  75. package/src/chat/inputSubmitHandler.js +171 -0
  76. package/src/chat/layout.js +165 -0
  77. package/src/chat/pasteController.js +81 -0
  78. package/src/chat/rawKeyMap.js +42 -0
  79. package/src/chat/settingsController.js +132 -0
  80. package/src/chat/statusLineController.js +177 -0
  81. package/src/chat/streamTracker.js +138 -0
  82. package/src/chat/text.js +70 -0
  83. package/src/chat/transport.js +61 -0
  84. package/src/cli/busCoreCommands.js +59 -0
  85. package/src/cli/ctxCoreCommands.js +199 -0
  86. package/src/cli/onlineCoreCommands.js +379 -0
  87. package/src/cli.js +1162 -96
  88. package/src/code/README.md +29 -0
  89. package/src/code/UCODE_PROMPT.md +32 -0
  90. package/src/code/agent.js +1651 -0
  91. package/src/code/cli.js +158 -0
  92. package/src/code/config +0 -0
  93. package/src/code/dispatch.js +42 -0
  94. package/src/code/index.js +70 -0
  95. package/src/code/nativeRunner.js +1213 -0
  96. package/src/code/runtime.js +154 -0
  97. package/src/code/sessionStore.js +162 -0
  98. package/src/code/taskDecomposer.js +269 -0
  99. package/src/code/tools/bash.js +53 -0
  100. package/src/code/tools/common.js +42 -0
  101. package/src/code/tools/edit.js +70 -0
  102. package/src/code/tools/read.js +44 -0
  103. package/src/code/tools/write.js +35 -0
  104. package/src/code/tui.js +1580 -0
  105. package/src/config.js +56 -3
  106. package/src/context/decisions.js +324 -0
  107. package/src/context/doctor.js +183 -0
  108. package/src/context/index.js +55 -0
  109. package/src/context/sync.js +127 -0
  110. package/src/daemon/agentProcessManager.js +74 -0
  111. package/src/daemon/cronOps.js +241 -0
  112. package/src/daemon/index.js +998 -170
  113. package/src/daemon/ipcServer.js +99 -0
  114. package/src/daemon/ops.js +630 -48
  115. package/src/daemon/promptLoop.js +319 -0
  116. package/src/daemon/promptRequest.js +101 -0
  117. package/src/daemon/providerSessions.js +306 -0
  118. package/src/daemon/reporting.js +90 -0
  119. package/src/daemon/run.js +31 -1
  120. package/src/daemon/status.js +48 -8
  121. package/src/doctor/index.js +50 -0
  122. package/src/init/index.js +318 -0
  123. package/src/online/bridge.js +663 -0
  124. package/src/online/client.js +245 -0
  125. package/src/online/runner.js +253 -0
  126. package/src/online/server.js +992 -0
  127. package/src/online/tokens.js +103 -0
  128. package/src/report/store.js +331 -0
  129. package/src/shared/eventContract.js +35 -0
  130. package/src/shared/ptySocketContract.js +21 -0
  131. package/src/skills/index.js +159 -0
  132. package/src/status/index.js +285 -0
  133. package/src/terminal/adapterContract.js +87 -0
  134. package/src/terminal/adapterRouter.js +84 -0
  135. package/src/terminal/adapters/externalAdapter.js +14 -0
  136. package/src/terminal/adapters/internalAdapter.js +13 -0
  137. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  138. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  139. package/src/terminal/adapters/terminalAdapter.js +31 -0
  140. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  141. package/src/terminal/detect.js +64 -0
  142. package/src/terminal/index.js +8 -0
  143. package/src/terminal/iterm2.js +126 -0
  144. package/src/ufoo/agentsStore.js +107 -0
  145. package/src/ufoo/paths.js +46 -0
  146. package/src/utils/banner.js +76 -0
  147. package/bin/uclaude +0 -65
  148. package/bin/ucodex +0 -65
  149. package/modules/bus/scripts/bus-alert.sh +0 -185
  150. package/modules/bus/scripts/bus-listen.sh +0 -117
  151. package/modules/context/ASSUMPTIONS.md +0 -7
  152. package/modules/context/CONSTRAINTS.md +0 -7
  153. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  154. package/modules/context/DECISION-PROTOCOL.md +0 -62
  155. package/modules/context/HANDOFF.md +0 -33
  156. package/modules/context/RULES.md +0 -15
  157. package/modules/context/SKILLS/README.md +0 -14
  158. package/modules/context/SYSTEM.md +0 -18
  159. package/modules/context/TEMPLATES/assumptions.md +0 -4
  160. package/modules/context/TEMPLATES/constraints.md +0 -4
  161. package/modules/context/TEMPLATES/decision.md +0 -16
  162. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  163. package/modules/context/TEMPLATES/system.md +0 -3
  164. package/modules/context/TEMPLATES/terminology.md +0 -4
  165. package/modules/context/TERMINOLOGY.md +0 -10
  166. package/scripts/banner.sh +0 -89
  167. package/scripts/bus-alert.sh +0 -6
  168. package/scripts/bus-autotrigger.sh +0 -6
  169. package/scripts/bus-daemon.sh +0 -231
  170. package/scripts/bus-inject.sh +0 -144
  171. package/scripts/bus-listen.sh +0 -6
  172. package/scripts/bus.sh +0 -984
  173. package/scripts/context-decisions.sh +0 -167
  174. package/scripts/context-doctor.sh +0 -72
  175. package/scripts/context-lint.sh +0 -110
  176. package/scripts/doctor.sh +0 -22
  177. package/scripts/init.sh +0 -247
  178. package/scripts/skills.sh +0 -113
  179. package/scripts/status.sh +0 -125
@@ -0,0 +1,847 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const net = require("net");
4
+ const { spawnSync } = require("child_process");
5
+ const EventBus = require("../bus");
6
+ const { PTY_SOCKET_MESSAGE_TYPES, PTY_SOCKET_SUBSCRIBE_MODES } = require("../shared/ptySocketContract");
7
+ const { runInternalRunner } = require("./internalRunner");
8
+ const { getUfooPaths } = require("../ufoo/paths");
9
+
10
+ function sleep(ms) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
13
+
14
+ function parseSubscriberId() {
15
+ if (process.env.UFOO_SUBSCRIBER_ID) {
16
+ const parts = process.env.UFOO_SUBSCRIBER_ID.split(":");
17
+ if (parts.length === 2) {
18
+ return {
19
+ subscriber: process.env.UFOO_SUBSCRIBER_ID,
20
+ agentType: parts[0],
21
+ sessionId: parts[1],
22
+ };
23
+ }
24
+ }
25
+ throw new Error("PTY runner requires UFOO_SUBSCRIBER_ID set by daemon");
26
+ }
27
+
28
+ function safeSubscriber(subscriber) {
29
+ return subscriber.replace(/:/g, "_");
30
+ }
31
+
32
+ function drainQueue(queueFile) {
33
+ if (!fs.existsSync(queueFile)) return [];
34
+ const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
35
+ let content = "";
36
+ let readOk = false;
37
+ try {
38
+ fs.renameSync(queueFile, processingFile);
39
+ content = fs.readFileSync(processingFile, "utf8");
40
+ readOk = true;
41
+ } catch {
42
+ try {
43
+ if (fs.existsSync(processingFile)) {
44
+ fs.renameSync(processingFile, queueFile);
45
+ }
46
+ } catch {
47
+ // ignore rollback errors
48
+ }
49
+ return [];
50
+ } finally {
51
+ if (readOk) {
52
+ try {
53
+ if (fs.existsSync(processingFile)) {
54
+ fs.rmSync(processingFile, { force: true });
55
+ }
56
+ } catch {
57
+ // ignore cleanup errors
58
+ }
59
+ }
60
+ }
61
+ if (!content.trim()) return [];
62
+ return content.split(/\r?\n/).filter(Boolean);
63
+ }
64
+
65
+ function stripAnsi(text) {
66
+ return text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
67
+ }
68
+
69
+ function parseInputMessage(message) {
70
+ if (!message) return { raw: false, text: "" };
71
+ try {
72
+ const parsed = JSON.parse(message);
73
+ if (parsed && typeof parsed === "object") {
74
+ if (parsed.raw && typeof parsed.data === "string") {
75
+ return { raw: true, text: parsed.data };
76
+ }
77
+ if (typeof parsed.text === "string") {
78
+ return { raw: false, text: parsed.text };
79
+ }
80
+ }
81
+ } catch {
82
+ // ignore json parse errors
83
+ }
84
+ return { raw: false, text: message };
85
+ }
86
+
87
+ function buildPrompt(text, marker) {
88
+ if (!marker) return text;
89
+ return `${text}\n\n请在完成后输出以下标记(单独一行):\n${marker}\n`;
90
+ }
91
+
92
+ function resolveCommand(agentType) {
93
+ const normalizedAgent = String(agentType || "").trim().toLowerCase();
94
+ const rawCmd = String(process.env.UFOO_PTY_CMD || "").trim();
95
+ if (rawCmd) {
96
+ const rawArgs = String(process.env.UFOO_PTY_ARGS || "").trim();
97
+ const args = rawArgs ? rawArgs.split(/\s+/).filter(Boolean) : [];
98
+ return { command: rawCmd, args };
99
+ }
100
+ if (normalizedAgent === "claude" || normalizedAgent === "claude-code") {
101
+ return { command: "claude", args: [] };
102
+ }
103
+ if (normalizedAgent === "ufoo" || normalizedAgent === "ucode" || normalizedAgent === "ufoo-code") {
104
+ return { command: "ucode", args: [] };
105
+ }
106
+ return { command: "codex", args: ["--no-alt-screen", "--sandbox", "workspace-write"] };
107
+ }
108
+
109
+ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
110
+ let pty;
111
+ try {
112
+ // eslint-disable-next-line global-require
113
+ pty = require("node-pty");
114
+ } catch {
115
+ throw new Error("node-pty not installed");
116
+ }
117
+ let Terminal = null;
118
+ let SerializeAddon = null;
119
+ try {
120
+ const xterm = await import("xterm-headless");
121
+ const serialize = await import("xterm-addon-serialize");
122
+ Terminal = xterm.Terminal || (xterm.default && xterm.default.Terminal);
123
+ SerializeAddon = serialize.SerializeAddon || (serialize.default && serialize.default.SerializeAddon);
124
+ } catch {
125
+ Terminal = null;
126
+ SerializeAddon = null;
127
+ }
128
+ const { subscriber } = parseSubscriberId();
129
+ const queueDir = path.join(getUfooPaths(projectRoot).busQueuesDir, safeSubscriber(subscriber));
130
+ const queueFile = path.join(queueDir, "pending.jsonl");
131
+ const runDir = getUfooPaths(projectRoot).runDir;
132
+ const logFile = path.join(runDir, "pty-runner.log");
133
+ const injectSockPath = path.join(queueDir, "inject.sock");
134
+
135
+ const { command, args } = resolveCommand(agentType);
136
+ const env = {
137
+ ...process.env,
138
+ UFOO_LAUNCH_MODE: "internal-pty",
139
+ UFOO_INTERNAL_PTY: "1",
140
+ };
141
+
142
+ const eventBus = new EventBus(projectRoot);
143
+
144
+ let running = true;
145
+ let busy = false;
146
+ let ptyAlive = false;
147
+ let ptyReady = false;
148
+ let readyTimer = null;
149
+ let currentPublisher = "";
150
+ let currentMarker = "";
151
+ let pendingOutput = [];
152
+ let outputBuffer = "";
153
+ let flushTimer = null;
154
+ let idleTimer = null;
155
+ let watchdogTimer = null;
156
+ let suppressEcho = false;
157
+ let echoMarker = "";
158
+ let suppressTimer = null;
159
+ let fallbackInProgress = false;
160
+ let ptyProcess = null;
161
+ let restartCount = 0;
162
+ let lastSpawnTime = 0;
163
+ const MAX_RESTARTS = 3;
164
+ const RESTART_STABLE_MS = 30000; // reset counter if process ran > 30s
165
+ const RESTART_DELAY_MS = 2000;
166
+ const READY_QUIET_MS = 3000; // TUI is "ready" after 3s of no output
167
+ const messageQueue = [];
168
+ const injectServer = setupInjectServer();
169
+ initScreenBuffer(80, 24);
170
+ const maxChunk = 2000;
171
+ const idleMs = 30000;
172
+ const watchdogMs = 120000;
173
+ const maxQueue = 200;
174
+ const watchdogAction = String(process.env.UFOO_PTY_WATCHDOG_ACTION || "restart").toLowerCase();
175
+ let sendQueue = Promise.resolve();
176
+ const DROP_LINE_PATTERNS = [
177
+ /__UFOO_DONE_/,
178
+ /请在完成后输出以下标记/,
179
+ /context left/i,
180
+ /esc to interrupt/i,
181
+ /for shortcuts/i,
182
+ /Preparing to run session start commands/i,
183
+ ];
184
+
185
+ function shouldDropLine(line) {
186
+ if (!line) return true;
187
+ const trimmed = line.trim();
188
+ if (!trimmed) return true;
189
+ if (/^[›❯>]$/.test(trimmed)) return true;
190
+ return DROP_LINE_PATTERNS.some((re) => re.test(trimmed));
191
+ }
192
+
193
+ function sanitizeChunk(chunk) {
194
+ if (!chunk) return "";
195
+ let text = String(chunk);
196
+ if (text.includes("\r")) {
197
+ const parts = text.split("\r");
198
+ text = parts[parts.length - 1];
199
+ }
200
+ const lines = text.split("\n").filter((line) => !shouldDropLine(line));
201
+ return lines.join("\n");
202
+ }
203
+
204
+ function enqueueSend(target, message) {
205
+ if (!target || !message) return;
206
+ sendQueue = sendQueue.then(() => eventBus.send(target, message, subscriber)).catch((err) => {
207
+ logNote(`[send-error] target=${target} err=${err.message || err}`);
208
+ });
209
+ }
210
+
211
+ // TTY view subscribers (same protocol as launcher inject.sock)
212
+ const outputSubscribers = new Set();
213
+ let term = null;
214
+ let serializeAddon = null;
215
+ let termWriteQueue = Promise.resolve();
216
+ const OUTPUT_RING_MAX = (() => {
217
+ const env = Number.parseInt(process.env.UFOO_INTERNAL_RING_MAX || "", 10);
218
+ if (Number.isFinite(env) && env > 0) return env;
219
+ return 512 * 1024;
220
+ })();
221
+ let outputRingBuffer = "";
222
+
223
+ function initScreenBuffer(cols = 80, rows = 24) {
224
+ if (!Terminal || !SerializeAddon) return null;
225
+ try {
226
+ const scrollbackEnv = Number.parseInt(process.env.UFOO_INTERNAL_SCROLLBACK || "", 10);
227
+ const scrollback = Number.isFinite(scrollbackEnv) && scrollbackEnv >= 0
228
+ ? scrollbackEnv
229
+ : 20000;
230
+ term = new Terminal({
231
+ cols,
232
+ rows,
233
+ scrollback,
234
+ allowProposedApi: true,
235
+ convertEol: true,
236
+ });
237
+ serializeAddon = new SerializeAddon();
238
+ term.loadAddon(serializeAddon);
239
+ } catch {
240
+ term = null;
241
+ serializeAddon = null;
242
+ }
243
+ return term;
244
+ }
245
+
246
+ function enqueueTermWrite(data) {
247
+ if (!term || !data) return;
248
+ termWriteQueue = termWriteQueue.then(() => new Promise((resolve) => {
249
+ term.write(data, resolve);
250
+ })).catch(() => {});
251
+ }
252
+
253
+ function serializeBuffer(buffer, scrollback) {
254
+ if (!term || !serializeAddon || !buffer) return "";
255
+ try {
256
+ if (typeof serializeAddon._serializeBuffer === "function") {
257
+ return serializeAddon._serializeBuffer(term, buffer, scrollback);
258
+ }
259
+ if (buffer === term.buffer.normal && typeof serializeAddon.serialize === "function") {
260
+ return serializeAddon.serialize({
261
+ scrollback,
262
+ excludeAltBuffer: true,
263
+ excludeModes: true,
264
+ });
265
+ }
266
+ return "";
267
+ } catch {
268
+ return "";
269
+ }
270
+ }
271
+
272
+ async function serializeSnapshot(mode = "full") {
273
+ if (!term || !serializeAddon) return null;
274
+ try {
275
+ await termWriteQueue;
276
+ const active = term.buffer.active;
277
+ const normal = term.buffer.normal;
278
+ const scrollback = term.options && Number.isFinite(term.options.scrollback)
279
+ ? term.options.scrollback
280
+ : undefined;
281
+
282
+ if (mode === "screen") {
283
+ const screen = serializeBuffer(active, 0);
284
+ return screen ? { data: screen } : null;
285
+ }
286
+
287
+ let data = serializeBuffer(normal, scrollback);
288
+ if (active && active !== normal) {
289
+ const alt = serializeBuffer(active, 0);
290
+ if (alt) data += `\x1b[H${alt}`;
291
+ }
292
+ return data ? { data } : null;
293
+ } catch {
294
+ return null;
295
+ }
296
+ }
297
+
298
+ function broadcastOutput(data) {
299
+ const text = Buffer.from(data || "").toString("utf8");
300
+ if (!text) return;
301
+ enqueueTermWrite(text);
302
+ outputRingBuffer += text;
303
+ if (outputRingBuffer.length > OUTPUT_RING_MAX) {
304
+ outputRingBuffer = outputRingBuffer.slice(-OUTPUT_RING_MAX);
305
+ }
306
+ if (outputSubscribers.size === 0) return;
307
+ const msg = JSON.stringify({ type: PTY_SOCKET_MESSAGE_TYPES.OUTPUT, data: text, encoding: "utf8" }) + "\n";
308
+ for (const sub of outputSubscribers) {
309
+ try {
310
+ sub.write(msg);
311
+ } catch {
312
+ outputSubscribers.delete(sub);
313
+ }
314
+ }
315
+ }
316
+
317
+ function setupInjectServer() {
318
+ const dir = path.dirname(injectSockPath);
319
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
320
+ if (fs.existsSync(injectSockPath)) {
321
+ try { fs.unlinkSync(injectSockPath); } catch { /* ignore */ }
322
+ }
323
+ const server = net.createServer((client) => {
324
+ let buffer = "";
325
+ client.on("data", (data) => {
326
+ buffer += data.toString("utf8");
327
+ const lines = buffer.split("\n");
328
+ buffer = lines.pop() || "";
329
+ for (const line of lines) {
330
+ if (!line.trim()) continue;
331
+ try {
332
+ const req = JSON.parse(line);
333
+ if (req.type === "inject" && req.command) {
334
+ if (ptyProcess && ptyAlive) {
335
+ ptyProcess.write(String(req.command));
336
+ setTimeout(() => {
337
+ if (!ptyProcess || !ptyAlive) return;
338
+ ptyProcess.write("\x1b");
339
+ setTimeout(() => {
340
+ if (ptyProcess && ptyAlive) {
341
+ ptyProcess.write("\r");
342
+ }
343
+ }, 100);
344
+ }, 200);
345
+ client.write(JSON.stringify({ ok: true }) + "\n");
346
+ } else {
347
+ client.write(JSON.stringify({ ok: false, error: "pty not ready" }) + "\n");
348
+ }
349
+ } else if (req.type === PTY_SOCKET_MESSAGE_TYPES.RAW && typeof req.data === "string") {
350
+ if (ptyProcess && ptyAlive) {
351
+ ptyProcess.write(req.data);
352
+ client.write(JSON.stringify({ ok: true }) + "\n");
353
+ } else {
354
+ client.write(JSON.stringify({ ok: false, error: "pty not ready" }) + "\n");
355
+ }
356
+ } else if (req.type === PTY_SOCKET_MESSAGE_TYPES.RESIZE && req.cols && req.rows) {
357
+ if (ptyProcess && ptyAlive && typeof ptyProcess.resize === "function") {
358
+ ptyProcess.resize(req.cols, req.rows);
359
+ }
360
+ if (term && typeof term.resize === "function") {
361
+ try { term.resize(req.cols, req.rows); } catch { /* ignore */ }
362
+ }
363
+ client.write(JSON.stringify({ ok: true }) + "\n");
364
+ } else if (req.type === PTY_SOCKET_MESSAGE_TYPES.SUBSCRIBE) {
365
+ outputSubscribers.add(client);
366
+ client.write(JSON.stringify({ type: PTY_SOCKET_MESSAGE_TYPES.SUBSCRIBED, ok: true }) + "\n");
367
+ const mode = req.mode === PTY_SOCKET_SUBSCRIBE_MODES.SCREEN
368
+ ? PTY_SOCKET_SUBSCRIBE_MODES.SCREEN
369
+ : PTY_SOCKET_SUBSCRIBE_MODES.FULL;
370
+ if (mode === PTY_SOCKET_SUBSCRIBE_MODES.FULL) {
371
+ if (outputRingBuffer.length > 0) {
372
+ try {
373
+ client.write(JSON.stringify({
374
+ type: PTY_SOCKET_MESSAGE_TYPES.REPLAY,
375
+ data: outputRingBuffer,
376
+ encoding: "utf8",
377
+ }) + "\n");
378
+ } catch {
379
+ // ignore replay send errors
380
+ }
381
+ } else {
382
+ serializeSnapshot(PTY_SOCKET_SUBSCRIBE_MODES.FULL).then((snapshot) => {
383
+ if (snapshot && snapshot.data) {
384
+ try {
385
+ client.write(JSON.stringify({
386
+ type: PTY_SOCKET_MESSAGE_TYPES.SNAPSHOT,
387
+ data: snapshot.data,
388
+ encoding: "utf8",
389
+ }) + "\n");
390
+ } catch {
391
+ // ignore snapshot send errors
392
+ }
393
+ }
394
+ }).catch(() => {});
395
+ }
396
+ } else {
397
+ serializeSnapshot(PTY_SOCKET_SUBSCRIBE_MODES.SCREEN).then((snapshot) => {
398
+ if (snapshot && snapshot.data) {
399
+ try {
400
+ client.write(JSON.stringify({
401
+ type: PTY_SOCKET_MESSAGE_TYPES.SNAPSHOT,
402
+ data: snapshot.data,
403
+ encoding: "utf8",
404
+ }) + "\n");
405
+ } catch {
406
+ // ignore snapshot send errors
407
+ }
408
+ }
409
+ }).catch(() => {});
410
+ }
411
+ } else {
412
+ client.write(JSON.stringify({ ok: false, error: "invalid request" }) + "\n");
413
+ }
414
+ } catch (err) {
415
+ client.write(JSON.stringify({ ok: false, error: err.message }) + "\n");
416
+ }
417
+ }
418
+ });
419
+ client.on("error", () => {
420
+ outputSubscribers.delete(client);
421
+ });
422
+ client.on("close", () => {
423
+ outputSubscribers.delete(client);
424
+ });
425
+ });
426
+ server.listen(injectSockPath);
427
+ return server;
428
+ }
429
+
430
+ function cleanupInjectServer(server) {
431
+ for (const sub of outputSubscribers) {
432
+ try { sub.destroy(); } catch { /* ignore */ }
433
+ }
434
+ outputSubscribers.clear();
435
+ try {
436
+ if (server) server.close();
437
+ if (fs.existsSync(injectSockPath)) fs.unlinkSync(injectSockPath);
438
+ } catch {
439
+ // ignore
440
+ }
441
+ }
442
+
443
+ function flushPending() {
444
+ if (!currentPublisher || pendingOutput.length === 0) return;
445
+ const chunks = pendingOutput;
446
+ pendingOutput = [];
447
+ for (const chunk of chunks) {
448
+ enqueueSend(currentPublisher, chunk);
449
+ }
450
+ }
451
+
452
+ function deliverChunk(chunk) {
453
+ if (!chunk) return;
454
+ const cleaned = sanitizeChunk(chunk);
455
+ if (!cleaned) return;
456
+ const payload = JSON.stringify({ stream: true, delta: cleaned });
457
+ if (currentPublisher) {
458
+ enqueueSend(currentPublisher, payload);
459
+ } else {
460
+ pendingOutput.push(payload);
461
+ if (pendingOutput.length > 50) pendingOutput.shift();
462
+ }
463
+ }
464
+
465
+ function flushOutput() {
466
+ if (!outputBuffer) return;
467
+ const chunk = outputBuffer.slice(0, maxChunk);
468
+ outputBuffer = outputBuffer.slice(chunk.length);
469
+ if (chunk) {
470
+ deliverChunk(chunk);
471
+ }
472
+ if (outputBuffer) {
473
+ scheduleFlush();
474
+ }
475
+ }
476
+
477
+ function scheduleFlush() {
478
+ if (flushTimer) return;
479
+ flushTimer = setTimeout(() => {
480
+ flushTimer = null;
481
+ flushOutput();
482
+ }, 120);
483
+ }
484
+
485
+ function logNote(note) {
486
+ try {
487
+ fs.mkdirSync(runDir, { recursive: true });
488
+ fs.appendFileSync(logFile, `[${new Date().toISOString()}] ${note}\n`);
489
+ } catch {
490
+ // ignore
491
+ }
492
+ }
493
+
494
+ function attachPty(proc) {
495
+ proc.onData((data) => {
496
+ const raw = String(data || "");
497
+ broadcastOutput(raw);
498
+ // Auto-respond to DSR (Device Status Report) cursor position query.
499
+ // Ink/codex sends \x1b[6n at startup; node-pty doesn't reply automatically,
500
+ // causing codex to crash with "cursor position could not be read".
501
+ if (raw.includes("\x1b[6n") || raw.includes("\x1b[?6n")) {
502
+ proc.write("\x1b[1;1R");
503
+ }
504
+ const clean = stripAnsi(raw).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
505
+ if (!clean) return;
506
+ outputBuffer += clean;
507
+ if (suppressEcho) {
508
+ if (echoMarker && outputBuffer.includes(echoMarker)) {
509
+ const idx = outputBuffer.indexOf(echoMarker);
510
+ outputBuffer = outputBuffer.slice(idx + echoMarker.length);
511
+ outputBuffer = outputBuffer.replace(/^\n+/, "");
512
+ suppressEcho = false;
513
+ currentMarker = echoMarker;
514
+ echoMarker = "";
515
+ if (suppressTimer) {
516
+ clearTimeout(suppressTimer);
517
+ suppressTimer = null;
518
+ }
519
+ } else {
520
+ return;
521
+ }
522
+ }
523
+ if (currentMarker) {
524
+ const idx = outputBuffer.indexOf(currentMarker);
525
+ if (idx !== -1) {
526
+ const before = outputBuffer.slice(0, idx);
527
+ outputBuffer = "";
528
+ if (before) {
529
+ deliverChunk(before);
530
+ }
531
+ if (currentPublisher) {
532
+ enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "marker" }));
533
+ }
534
+ currentMarker = "";
535
+ busy = false;
536
+ currentPublisher = "";
537
+ if (watchdogTimer) {
538
+ clearTimeout(watchdogTimer);
539
+ watchdogTimer = null;
540
+ }
541
+ if (idleTimer) {
542
+ clearTimeout(idleTimer);
543
+ idleTimer = null;
544
+ }
545
+ processQueue();
546
+ return;
547
+ }
548
+ }
549
+ scheduleFlush();
550
+ // Ready detection: during TUI startup, reset the quiet timer on each output.
551
+ // Once output stops for READY_QUIET_MS, the TUI is considered initialized.
552
+ if (!ptyReady && !busy) {
553
+ if (readyTimer) clearTimeout(readyTimer);
554
+ readyTimer = setTimeout(() => {
555
+ readyTimer = null;
556
+ if (!ptyReady) {
557
+ ptyReady = true;
558
+ // Discard TUI startup noise accumulated before ready
559
+ outputBuffer = "";
560
+ pendingOutput = [];
561
+ logNote("[internal-pty] TUI ready (output quiet for " + READY_QUIET_MS + "ms)");
562
+ processQueue();
563
+ }
564
+ }, READY_QUIET_MS);
565
+ }
566
+ if (busy) {
567
+ if (idleTimer) clearTimeout(idleTimer);
568
+ idleTimer = setTimeout(() => {
569
+ idleTimer = null;
570
+ if (currentPublisher) {
571
+ enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "idle" }));
572
+ }
573
+ busy = false;
574
+ currentPublisher = "";
575
+ processQueue();
576
+ }, idleMs);
577
+ }
578
+ });
579
+
580
+ proc.onExit(({ exitCode, signal }) => {
581
+ // Skip if this process has been replaced (e.g., by restartPty)
582
+ if (proc !== ptyProcess) return;
583
+
584
+ ptyAlive = false;
585
+ ptyReady = false;
586
+ if (readyTimer) {
587
+ clearTimeout(readyTimer);
588
+ readyTimer = null;
589
+ }
590
+ if (outputBuffer) {
591
+ flushOutput();
592
+ }
593
+ if (flushTimer) {
594
+ clearTimeout(flushTimer);
595
+ flushTimer = null;
596
+ }
597
+ if (idleTimer) {
598
+ clearTimeout(idleTimer);
599
+ idleTimer = null;
600
+ }
601
+ if (watchdogTimer) {
602
+ clearTimeout(watchdogTimer);
603
+ watchdogTimer = null;
604
+ }
605
+ const note = `[internal-pty] process exited code=${exitCode} signal=${signal || ""}`.trim();
606
+ if (currentPublisher) enqueueSend(currentPublisher, note);
607
+ logNote(note);
608
+
609
+ // Reset busy state
610
+ busy = false;
611
+ currentPublisher = "";
612
+ currentMarker = "";
613
+
614
+ // If stop() was called, let the runner exit
615
+ if (!running) return;
616
+
617
+ // Auto-restart with backoff
618
+ const elapsed = Date.now() - lastSpawnTime;
619
+ if (elapsed > RESTART_STABLE_MS) {
620
+ restartCount = 0; // Process was stable long enough, reset counter
621
+ }
622
+ restartCount++;
623
+
624
+ if (restartCount <= MAX_RESTARTS) {
625
+ const delay = Math.min(restartCount * RESTART_DELAY_MS, 10000);
626
+ logNote(`Auto-restarting PTY in ${delay}ms (attempt ${restartCount}/${MAX_RESTARTS})`);
627
+ setTimeout(() => {
628
+ if (!running) return;
629
+ try {
630
+ ptyProcess = spawnPtyProcess();
631
+ processQueue();
632
+ } catch (err) {
633
+ logNote(`Restart failed: ${err.message || err}`);
634
+ void fallbackHeadless(`restart failed: ${err.message || err}`);
635
+ }
636
+ }, delay);
637
+ } else {
638
+ logNote(`Max PTY restarts (${MAX_RESTARTS}) reached, falling back to headless runner`);
639
+ void fallbackHeadless("max PTY restarts exceeded");
640
+ }
641
+ });
642
+ }
643
+
644
+ function spawnPtyProcess() {
645
+ lastSpawnTime = Date.now();
646
+ ptyReady = false;
647
+ if (readyTimer) {
648
+ clearTimeout(readyTimer);
649
+ readyTimer = null;
650
+ }
651
+ const proc = pty.spawn(command, args, {
652
+ name: "xterm-256color",
653
+ cols: 80,
654
+ rows: 24,
655
+ cwd: projectRoot,
656
+ env,
657
+ });
658
+ ptyAlive = true;
659
+ attachPty(proc);
660
+ return proc;
661
+ }
662
+
663
+ function restartPty(reason) {
664
+ if (!running) return;
665
+ logNote(`Restarting PTY: ${reason}`);
666
+ ptyAlive = false;
667
+ ptyReady = false;
668
+ if (outputBuffer) {
669
+ flushOutput();
670
+ }
671
+ // Clear reference first so the old onExit handler skips (proc !== ptyProcess)
672
+ const oldPty = ptyProcess;
673
+ ptyProcess = null;
674
+ try {
675
+ if (oldPty) oldPty.kill();
676
+ } catch {
677
+ // ignore
678
+ }
679
+ ptyProcess = spawnPtyProcess();
680
+ }
681
+
682
+ async function fallbackHeadless(reason) {
683
+ if (fallbackInProgress) return;
684
+ fallbackInProgress = true;
685
+ logNote(`Fallback to headless: ${reason}`);
686
+ if (outputBuffer) {
687
+ flushOutput();
688
+ }
689
+ cleanupInjectServer(injectServer);
690
+ try {
691
+ if (ptyProcess) ptyProcess.kill();
692
+ } catch {
693
+ // ignore
694
+ }
695
+ running = false;
696
+ await runInternalRunner({ projectRoot, agentType });
697
+ process.exit(0);
698
+ }
699
+
700
+ const stop = () => {
701
+ running = false;
702
+ cleanupInjectServer(injectServer);
703
+ try {
704
+ if (ptyProcess) ptyProcess.kill();
705
+ } catch {
706
+ // ignore
707
+ }
708
+ };
709
+
710
+ process.on("SIGTERM", stop);
711
+ process.on("SIGINT", stop);
712
+ // Ignore SIGHUP so terminal closure doesn't kill the ptyRunner
713
+ // while the daemon is still alive.
714
+ process.on("SIGHUP", () => {});
715
+
716
+ ptyProcess = spawnPtyProcess();
717
+
718
+ function processQueue() {
719
+ if (busy || messageQueue.length === 0 || !running || !ptyAlive || !ptyReady) return;
720
+ const next = messageQueue.shift();
721
+ if (!next) return;
722
+ busy = true;
723
+ currentPublisher = next.publisher;
724
+ currentMarker = next.marker || "";
725
+ if (suppressTimer) {
726
+ clearTimeout(suppressTimer);
727
+ suppressTimer = null;
728
+ }
729
+ flushPending();
730
+ if (next.text) {
731
+ if (next.raw) {
732
+ ptyProcess.write(next.text);
733
+ } else {
734
+ // Write text first, then send Enter separately.
735
+ // Codex Ink TUI requires text and submit key as separate writes.
736
+ // IMPORTANT: Defer marker detection until after Enter is sent,
737
+ // because the prompt echo (TextInput display) contains the marker text.
738
+ const prompt = buildPrompt(next.text, currentMarker);
739
+ const savedMarker = currentMarker;
740
+ suppressEcho = true;
741
+ echoMarker = savedMarker;
742
+ currentMarker = ""; // Disable marker detection during prompt echo & formatted display
743
+ ptyProcess.write(prompt);
744
+ setTimeout(() => {
745
+ if (ptyProcess && ptyAlive) {
746
+ outputBuffer = "";
747
+ // Send ESC first to dismiss any auto-complete/suggestion overlay
748
+ // in Ink-based TUIs (Claude Code, Codex), then CR to submit.
749
+ // This matches the inject socket pattern in launcher.js.
750
+ ptyProcess.write("\x1b");
751
+ setTimeout(() => {
752
+ if (ptyProcess && ptyAlive) {
753
+ ptyProcess.write("\r");
754
+ }
755
+ // Fallback: if we never observe the marker in echoed output,
756
+ // stop suppressing after a short delay to avoid freezing output.
757
+ suppressTimer = setTimeout(() => {
758
+ suppressTimer = null;
759
+ if (!suppressEcho) return;
760
+ suppressEcho = false;
761
+ echoMarker = "";
762
+ currentMarker = savedMarker;
763
+ outputBuffer = "";
764
+ }, 1500);
765
+ }, 100);
766
+ }
767
+ }, 200);
768
+ }
769
+ }
770
+ if (watchdogTimer) clearTimeout(watchdogTimer);
771
+ watchdogTimer = setTimeout(() => {
772
+ watchdogTimer = null;
773
+ if (!busy) return;
774
+ const timeoutNote = `[internal-pty] marker timeout; action=${watchdogAction}`;
775
+ if (currentPublisher) enqueueSend(currentPublisher, timeoutNote);
776
+ if (currentPublisher) {
777
+ enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "timeout" }));
778
+ }
779
+ logNote(timeoutNote);
780
+ if (watchdogAction === "fallback") {
781
+ void fallbackHeadless("marker timeout");
782
+ return;
783
+ }
784
+ if (watchdogAction === "restart") {
785
+ restartPty("marker timeout");
786
+ }
787
+ currentMarker = "";
788
+ busy = false;
789
+ currentPublisher = "";
790
+ processQueue();
791
+ }, watchdogMs);
792
+ }
793
+
794
+ // Heartbeat to keep agent "online" in bus status
795
+ let lastHeartbeat = 0;
796
+ const HEARTBEAT_INTERVAL = 30000;
797
+ const updateHeartbeat = () => {
798
+ try {
799
+ spawnSync("ufoo", ["bus", "check", subscriber], {
800
+ cwd: projectRoot,
801
+ env: { ...process.env, UFOO_SUBSCRIBER_ID: subscriber },
802
+ stdio: "ignore",
803
+ timeout: 5000,
804
+ });
805
+ } catch {
806
+ // ignore heartbeat errors
807
+ }
808
+ };
809
+
810
+ while (running) {
811
+ // Periodic heartbeat
812
+ const now = Date.now();
813
+ if (now - lastHeartbeat > HEARTBEAT_INTERVAL) {
814
+ updateHeartbeat();
815
+ lastHeartbeat = now;
816
+ }
817
+
818
+ const lines = drainQueue(queueFile);
819
+ if (lines.length > 0) {
820
+ const events = [];
821
+ for (const line of lines) {
822
+ try {
823
+ events.push(JSON.parse(line));
824
+ } catch {
825
+ // ignore malformed line
826
+ }
827
+ }
828
+ for (const evt of events) {
829
+ if (!evt || !evt.data || typeof evt.data.message !== "string") continue;
830
+ const { raw, text } = parseInputMessage(evt.data.message);
831
+ if (messageQueue.length >= maxQueue) {
832
+ messageQueue.shift();
833
+ }
834
+ const marker = raw ? "" : `__UFOO_DONE_${Date.now()}_${Math.random().toString(16).slice(2)}__`;
835
+ const publisher = typeof evt.publisher === "object" && evt.publisher
836
+ ? (evt.publisher.subscriber || evt.publisher.nickname || "unknown")
837
+ : (evt.publisher || "unknown");
838
+ messageQueue.push({ publisher, raw, text, marker });
839
+ }
840
+ }
841
+ processQueue();
842
+ // eslint-disable-next-line no-await-in-loop
843
+ await sleep(200);
844
+ }
845
+ }
846
+
847
+ module.exports = { runPtyRunner };