u-foo 1.0.6 → 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.
- package/README.md +44 -4
- package/SKILLS/ufoo/SKILL.md +17 -2
- package/SKILLS/uinit/SKILL.md +8 -3
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +4 -0
- package/modules/AGENTS.template.md +14 -4
- package/modules/bus/README.md +8 -5
- package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
- package/modules/context/SKILLS/uctx/SKILL.md +3 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +12 -3
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +20 -49
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +524 -31
- package/src/agent/internalRunner.js +76 -9
- package/src/agent/launcher.js +97 -45
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +144 -4
- package/src/agent/ptyRunner.js +480 -10
- package/src/agent/ptyWrapper.js +28 -3
- package/src/agent/readyDetector.js +16 -0
- package/src/agent/ucode.js +443 -0
- package/src/agent/ucodeBootstrap.js +113 -0
- package/src/agent/ucodeBuild.js +67 -0
- package/src/agent/ucodeDoctor.js +184 -0
- package/src/agent/ucodeRuntimeConfig.js +129 -0
- package/src/agent/ufooAgent.js +11 -2
- package/src/assistant/agent.js +260 -0
- package/src/assistant/bridge.js +172 -0
- package/src/assistant/engine.js +252 -0
- package/src/assistant/stdio.js +58 -0
- package/src/assistant/ufooEngineCli.js +306 -0
- package/src/bus/activate.js +27 -11
- package/src/bus/daemon.js +133 -5
- package/src/bus/index.js +137 -80
- package/src/bus/inject.js +47 -17
- package/src/bus/message.js +145 -17
- package/src/bus/nickname.js +3 -1
- package/src/bus/queue.js +6 -1
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +20 -4
- package/src/bus/utils.js +9 -3
- package/src/chat/agentBar.js +117 -0
- package/src/chat/agentDirectory.js +88 -0
- package/src/chat/agentSockets.js +225 -0
- package/src/chat/agentViewController.js +298 -0
- package/src/chat/chatLogController.js +115 -0
- package/src/chat/commandExecutor.js +700 -0
- package/src/chat/commands.js +132 -0
- package/src/chat/completionController.js +414 -0
- package/src/chat/cronScheduler.js +160 -0
- package/src/chat/daemonConnection.js +166 -0
- package/src/chat/daemonCoordinator.js +64 -0
- package/src/chat/daemonMessageRouter.js +257 -0
- package/src/chat/daemonReconnect.js +41 -0
- package/src/chat/daemonTransport.js +36 -0
- package/src/chat/daemonTransportDefaults.js +10 -0
- package/src/chat/dashboardKeyController.js +480 -0
- package/src/chat/dashboardView.js +154 -0
- package/src/chat/index.js +935 -2909
- package/src/chat/inputHistoryController.js +105 -0
- package/src/chat/inputListenerController.js +304 -0
- package/src/chat/inputMath.js +104 -0
- package/src/chat/inputSubmitHandler.js +171 -0
- package/src/chat/layout.js +165 -0
- package/src/chat/pasteController.js +81 -0
- package/src/chat/rawKeyMap.js +42 -0
- package/src/chat/settingsController.js +132 -0
- package/src/chat/statusLineController.js +177 -0
- package/src/chat/streamTracker.js +138 -0
- package/src/chat/text.js +70 -0
- package/src/chat/transport.js +61 -0
- package/src/cli/busCoreCommands.js +59 -0
- package/src/cli/ctxCoreCommands.js +199 -0
- package/src/cli/onlineCoreCommands.js +379 -0
- package/src/cli.js +741 -238
- package/src/code/README.md +29 -0
- package/src/code/UCODE_PROMPT.md +32 -0
- package/src/code/agent.js +1651 -0
- package/src/code/cli.js +158 -0
- package/src/code/config +0 -0
- package/src/code/dispatch.js +42 -0
- package/src/code/index.js +70 -0
- package/src/code/nativeRunner.js +1213 -0
- package/src/code/runtime.js +154 -0
- package/src/code/sessionStore.js +162 -0
- package/src/code/taskDecomposer.js +269 -0
- package/src/code/tools/bash.js +53 -0
- package/src/code/tools/common.js +42 -0
- package/src/code/tools/edit.js +70 -0
- package/src/code/tools/read.js +44 -0
- package/src/code/tools/write.js +35 -0
- package/src/code/tui.js +1580 -0
- package/src/config.js +47 -1
- package/src/context/decisions.js +12 -2
- package/src/context/index.js +18 -1
- package/src/context/sync.js +127 -0
- package/src/daemon/agentProcessManager.js +74 -0
- package/src/daemon/cronOps.js +241 -0
- package/src/daemon/index.js +661 -488
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +417 -179
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +32 -17
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +2 -5
- package/src/daemon/status.js +24 -1
- package/src/init/index.js +68 -14
- package/src/online/bridge.js +663 -0
- package/src/online/client.js +245 -0
- package/src/online/runner.js +253 -0
- package/src/online/server.js +992 -0
- package/src/online/tokens.js +103 -0
- package/src/report/store.js +331 -0
- package/src/shared/eventContract.js +35 -0
- package/src/shared/ptySocketContract.js +21 -0
- package/src/status/index.js +50 -17
- package/src/terminal/adapterContract.js +87 -0
- package/src/terminal/adapterRouter.js +84 -0
- package/src/terminal/adapters/externalAdapter.js +14 -0
- package/src/terminal/adapters/internalAdapter.js +13 -0
- package/src/terminal/adapters/internalPtyAdapter.js +42 -0
- package/src/terminal/adapters/internalQueueAdapter.js +37 -0
- package/src/terminal/adapters/terminalAdapter.js +31 -0
- package/src/terminal/adapters/tmuxAdapter.js +30 -0
- package/src/ufoo/agentsStore.js +69 -3
- package/src/utils/banner.js +5 -2
- package/scripts/.archived/bash-to-js-migration/README.md +0 -46
- package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
- package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
- package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
- package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
- package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
- package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
- package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
- package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
- package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
- package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
- package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
- package/scripts/banner.sh +0 -2
- package/src/bus/API_DESIGN.md +0 -204
package/src/agent/ptyRunner.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const net = require("net");
|
|
4
|
+
const { spawnSync } = require("child_process");
|
|
3
5
|
const EventBus = require("../bus");
|
|
6
|
+
const { PTY_SOCKET_MESSAGE_TYPES, PTY_SOCKET_SUBSCRIBE_MODES } = require("../shared/ptySocketContract");
|
|
4
7
|
const { runInternalRunner } = require("./internalRunner");
|
|
5
8
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
6
9
|
|
|
@@ -87,15 +90,19 @@ function buildPrompt(text, marker) {
|
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
function resolveCommand(agentType) {
|
|
93
|
+
const normalizedAgent = String(agentType || "").trim().toLowerCase();
|
|
90
94
|
const rawCmd = String(process.env.UFOO_PTY_CMD || "").trim();
|
|
91
95
|
if (rawCmd) {
|
|
92
96
|
const rawArgs = String(process.env.UFOO_PTY_ARGS || "").trim();
|
|
93
97
|
const args = rawArgs ? rawArgs.split(/\s+/).filter(Boolean) : [];
|
|
94
98
|
return { command: rawCmd, args };
|
|
95
99
|
}
|
|
96
|
-
if (
|
|
100
|
+
if (normalizedAgent === "claude" || normalizedAgent === "claude-code") {
|
|
97
101
|
return { command: "claude", args: [] };
|
|
98
102
|
}
|
|
103
|
+
if (normalizedAgent === "ufoo" || normalizedAgent === "ucode" || normalizedAgent === "ufoo-code") {
|
|
104
|
+
return { command: "ucode", args: [] };
|
|
105
|
+
}
|
|
99
106
|
return { command: "codex", args: ["--no-alt-screen", "--sandbox", "workspace-write"] };
|
|
100
107
|
}
|
|
101
108
|
|
|
@@ -107,11 +114,23 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
107
114
|
} catch {
|
|
108
115
|
throw new Error("node-pty not installed");
|
|
109
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
|
+
}
|
|
110
128
|
const { subscriber } = parseSubscriberId();
|
|
111
129
|
const queueDir = path.join(getUfooPaths(projectRoot).busQueuesDir, safeSubscriber(subscriber));
|
|
112
130
|
const queueFile = path.join(queueDir, "pending.jsonl");
|
|
113
131
|
const runDir = getUfooPaths(projectRoot).runDir;
|
|
114
132
|
const logFile = path.join(runDir, "pty-runner.log");
|
|
133
|
+
const injectSockPath = path.join(queueDir, "inject.sock");
|
|
115
134
|
|
|
116
135
|
const { command, args } = resolveCommand(agentType);
|
|
117
136
|
const env = {
|
|
@@ -124,6 +143,9 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
124
143
|
|
|
125
144
|
let running = true;
|
|
126
145
|
let busy = false;
|
|
146
|
+
let ptyAlive = false;
|
|
147
|
+
let ptyReady = false;
|
|
148
|
+
let readyTimer = null;
|
|
127
149
|
let currentPublisher = "";
|
|
128
150
|
let currentMarker = "";
|
|
129
151
|
let pendingOutput = [];
|
|
@@ -131,19 +153,291 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
131
153
|
let flushTimer = null;
|
|
132
154
|
let idleTimer = null;
|
|
133
155
|
let watchdogTimer = null;
|
|
156
|
+
let suppressEcho = false;
|
|
157
|
+
let echoMarker = "";
|
|
158
|
+
let suppressTimer = null;
|
|
134
159
|
let fallbackInProgress = false;
|
|
135
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
|
|
136
167
|
const messageQueue = [];
|
|
168
|
+
const injectServer = setupInjectServer();
|
|
169
|
+
initScreenBuffer(80, 24);
|
|
137
170
|
const maxChunk = 2000;
|
|
138
|
-
const idleMs =
|
|
171
|
+
const idleMs = 30000;
|
|
139
172
|
const watchdogMs = 120000;
|
|
140
173
|
const maxQueue = 200;
|
|
141
174
|
const watchdogAction = String(process.env.UFOO_PTY_WATCHDOG_ACTION || "restart").toLowerCase();
|
|
142
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
|
+
}
|
|
143
203
|
|
|
144
204
|
function enqueueSend(target, message) {
|
|
145
205
|
if (!target || !message) return;
|
|
146
|
-
sendQueue = sendQueue.then(() => eventBus.send(target, message, subscriber)).catch(() => {
|
|
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
|
+
}
|
|
147
441
|
}
|
|
148
442
|
|
|
149
443
|
function flushPending() {
|
|
@@ -157,7 +451,9 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
157
451
|
|
|
158
452
|
function deliverChunk(chunk) {
|
|
159
453
|
if (!chunk) return;
|
|
160
|
-
const
|
|
454
|
+
const cleaned = sanitizeChunk(chunk);
|
|
455
|
+
if (!cleaned) return;
|
|
456
|
+
const payload = JSON.stringify({ stream: true, delta: cleaned });
|
|
161
457
|
if (currentPublisher) {
|
|
162
458
|
enqueueSend(currentPublisher, payload);
|
|
163
459
|
} else {
|
|
@@ -197,9 +493,33 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
197
493
|
|
|
198
494
|
function attachPty(proc) {
|
|
199
495
|
proc.onData((data) => {
|
|
200
|
-
const
|
|
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");
|
|
201
505
|
if (!clean) return;
|
|
202
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
|
+
}
|
|
203
523
|
if (currentMarker) {
|
|
204
524
|
const idx = outputBuffer.indexOf(currentMarker);
|
|
205
525
|
if (idx !== -1) {
|
|
@@ -208,6 +528,9 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
208
528
|
if (before) {
|
|
209
529
|
deliverChunk(before);
|
|
210
530
|
}
|
|
531
|
+
if (currentPublisher) {
|
|
532
|
+
enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "marker" }));
|
|
533
|
+
}
|
|
211
534
|
currentMarker = "";
|
|
212
535
|
busy = false;
|
|
213
536
|
currentPublisher = "";
|
|
@@ -224,10 +547,29 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
224
547
|
}
|
|
225
548
|
}
|
|
226
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
|
+
}
|
|
227
566
|
if (busy) {
|
|
228
567
|
if (idleTimer) clearTimeout(idleTimer);
|
|
229
568
|
idleTimer = setTimeout(() => {
|
|
230
569
|
idleTimer = null;
|
|
570
|
+
if (currentPublisher) {
|
|
571
|
+
enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "idle" }));
|
|
572
|
+
}
|
|
231
573
|
busy = false;
|
|
232
574
|
currentPublisher = "";
|
|
233
575
|
processQueue();
|
|
@@ -236,6 +578,15 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
236
578
|
});
|
|
237
579
|
|
|
238
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
|
+
}
|
|
239
590
|
if (outputBuffer) {
|
|
240
591
|
flushOutput();
|
|
241
592
|
}
|
|
@@ -247,14 +598,56 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
247
598
|
clearTimeout(idleTimer);
|
|
248
599
|
idleTimer = null;
|
|
249
600
|
}
|
|
601
|
+
if (watchdogTimer) {
|
|
602
|
+
clearTimeout(watchdogTimer);
|
|
603
|
+
watchdogTimer = null;
|
|
604
|
+
}
|
|
250
605
|
const note = `[internal-pty] process exited code=${exitCode} signal=${signal || ""}`.trim();
|
|
251
606
|
if (currentPublisher) enqueueSend(currentPublisher, note);
|
|
252
607
|
logNote(note);
|
|
253
|
-
|
|
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
|
+
}
|
|
254
641
|
});
|
|
255
642
|
}
|
|
256
643
|
|
|
257
644
|
function spawnPtyProcess() {
|
|
645
|
+
lastSpawnTime = Date.now();
|
|
646
|
+
ptyReady = false;
|
|
647
|
+
if (readyTimer) {
|
|
648
|
+
clearTimeout(readyTimer);
|
|
649
|
+
readyTimer = null;
|
|
650
|
+
}
|
|
258
651
|
const proc = pty.spawn(command, args, {
|
|
259
652
|
name: "xterm-256color",
|
|
260
653
|
cols: 80,
|
|
@@ -262,6 +655,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
262
655
|
cwd: projectRoot,
|
|
263
656
|
env,
|
|
264
657
|
});
|
|
658
|
+
ptyAlive = true;
|
|
265
659
|
attachPty(proc);
|
|
266
660
|
return proc;
|
|
267
661
|
}
|
|
@@ -269,11 +663,16 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
269
663
|
function restartPty(reason) {
|
|
270
664
|
if (!running) return;
|
|
271
665
|
logNote(`Restarting PTY: ${reason}`);
|
|
666
|
+
ptyAlive = false;
|
|
667
|
+
ptyReady = false;
|
|
272
668
|
if (outputBuffer) {
|
|
273
669
|
flushOutput();
|
|
274
670
|
}
|
|
671
|
+
// Clear reference first so the old onExit handler skips (proc !== ptyProcess)
|
|
672
|
+
const oldPty = ptyProcess;
|
|
673
|
+
ptyProcess = null;
|
|
275
674
|
try {
|
|
276
|
-
if (
|
|
675
|
+
if (oldPty) oldPty.kill();
|
|
277
676
|
} catch {
|
|
278
677
|
// ignore
|
|
279
678
|
}
|
|
@@ -287,6 +686,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
287
686
|
if (outputBuffer) {
|
|
288
687
|
flushOutput();
|
|
289
688
|
}
|
|
689
|
+
cleanupInjectServer(injectServer);
|
|
290
690
|
try {
|
|
291
691
|
if (ptyProcess) ptyProcess.kill();
|
|
292
692
|
} catch {
|
|
@@ -299,6 +699,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
299
699
|
|
|
300
700
|
const stop = () => {
|
|
301
701
|
running = false;
|
|
702
|
+
cleanupInjectServer(injectServer);
|
|
302
703
|
try {
|
|
303
704
|
if (ptyProcess) ptyProcess.kill();
|
|
304
705
|
} catch {
|
|
@@ -308,22 +709,62 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
308
709
|
|
|
309
710
|
process.on("SIGTERM", stop);
|
|
310
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", () => {});
|
|
311
715
|
|
|
312
716
|
ptyProcess = spawnPtyProcess();
|
|
313
717
|
|
|
314
718
|
function processQueue() {
|
|
315
|
-
if (busy || messageQueue.length === 0 || !running) return;
|
|
719
|
+
if (busy || messageQueue.length === 0 || !running || !ptyAlive || !ptyReady) return;
|
|
316
720
|
const next = messageQueue.shift();
|
|
317
721
|
if (!next) return;
|
|
318
722
|
busy = true;
|
|
319
723
|
currentPublisher = next.publisher;
|
|
320
724
|
currentMarker = next.marker || "";
|
|
725
|
+
if (suppressTimer) {
|
|
726
|
+
clearTimeout(suppressTimer);
|
|
727
|
+
suppressTimer = null;
|
|
728
|
+
}
|
|
321
729
|
flushPending();
|
|
322
730
|
if (next.text) {
|
|
323
731
|
if (next.raw) {
|
|
324
732
|
ptyProcess.write(next.text);
|
|
325
733
|
} else {
|
|
326
|
-
|
|
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);
|
|
327
768
|
}
|
|
328
769
|
}
|
|
329
770
|
if (watchdogTimer) clearTimeout(watchdogTimer);
|
|
@@ -332,6 +773,9 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
332
773
|
if (!busy) return;
|
|
333
774
|
const timeoutNote = `[internal-pty] marker timeout; action=${watchdogAction}`;
|
|
334
775
|
if (currentPublisher) enqueueSend(currentPublisher, timeoutNote);
|
|
776
|
+
if (currentPublisher) {
|
|
777
|
+
enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason: "timeout" }));
|
|
778
|
+
}
|
|
335
779
|
logNote(timeoutNote);
|
|
336
780
|
if (watchdogAction === "fallback") {
|
|
337
781
|
void fallbackHeadless("marker timeout");
|
|
@@ -347,7 +791,30 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
347
791
|
}, watchdogMs);
|
|
348
792
|
}
|
|
349
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
|
+
|
|
350
810
|
while (running) {
|
|
811
|
+
// Periodic heartbeat
|
|
812
|
+
const now = Date.now();
|
|
813
|
+
if (now - lastHeartbeat > HEARTBEAT_INTERVAL) {
|
|
814
|
+
updateHeartbeat();
|
|
815
|
+
lastHeartbeat = now;
|
|
816
|
+
}
|
|
817
|
+
|
|
351
818
|
const lines = drainQueue(queueFile);
|
|
352
819
|
if (lines.length > 0) {
|
|
353
820
|
const events = [];
|
|
@@ -365,7 +832,10 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
365
832
|
messageQueue.shift();
|
|
366
833
|
}
|
|
367
834
|
const marker = raw ? "" : `__UFOO_DONE_${Date.now()}_${Math.random().toString(16).slice(2)}__`;
|
|
368
|
-
|
|
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 });
|
|
369
839
|
}
|
|
370
840
|
}
|
|
371
841
|
processQueue();
|