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.
- package/README.md +110 -11
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +132 -0
- package/SKILLS/uinit/SKILL.md +78 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +17 -0
- package/modules/AGENTS.template.md +29 -11
- package/modules/bus/README.md +33 -25
- package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +63 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +25 -4
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +30 -0
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +554 -33
- package/src/agent/internalRunner.js +150 -56
- package/src/agent/launcher.js +754 -0
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +340 -0
- package/src/agent/ptyRunner.js +847 -0
- package/src/agent/ptyWrapper.js +379 -0
- package/src/agent/readyDetector.js +175 -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 +46 -42
- 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 +172 -0
- package/src/bus/daemon.js +436 -0
- package/src/bus/index.js +842 -0
- package/src/bus/inject.js +315 -0
- package/src/bus/message.js +430 -0
- package/src/bus/nickname.js +88 -0
- package/src/bus/queue.js +136 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +312 -0
- package/src/bus/utils.js +363 -0
- 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 +1011 -1392
- 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 +1162 -96
- 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 +56 -3
- package/src/context/decisions.js +324 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +55 -0
- 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 +998 -170
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +630 -48
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +306 -0
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +31 -1
- package/src/daemon/status.js +48 -8
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +318 -0
- 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/skills/index.js +159 -0
- package/src/status/index.js +285 -0
- 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/terminal/detect.js +64 -0
- package/src/terminal/index.js +8 -0
- package/src/terminal/iterm2.js +126 -0
- package/src/ufoo/agentsStore.js +107 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +76 -0
- package/bin/uclaude +0 -65
- package/bin/ucodex +0 -65
- package/modules/bus/scripts/bus-alert.sh +0 -185
- package/modules/bus/scripts/bus-listen.sh +0 -117
- package/modules/context/ASSUMPTIONS.md +0 -7
- package/modules/context/CONSTRAINTS.md +0 -7
- package/modules/context/CONTEXT-STRUCTURE.md +0 -49
- package/modules/context/DECISION-PROTOCOL.md +0 -62
- package/modules/context/HANDOFF.md +0 -33
- package/modules/context/RULES.md +0 -15
- package/modules/context/SKILLS/README.md +0 -14
- package/modules/context/SYSTEM.md +0 -18
- package/modules/context/TEMPLATES/assumptions.md +0 -4
- package/modules/context/TEMPLATES/constraints.md +0 -4
- package/modules/context/TEMPLATES/decision.md +0 -16
- package/modules/context/TEMPLATES/project-context-readme.md +0 -6
- package/modules/context/TEMPLATES/system.md +0 -3
- package/modules/context/TEMPLATES/terminology.md +0 -4
- package/modules/context/TERMINOLOGY.md +0 -10
- package/scripts/banner.sh +0 -89
- package/scripts/bus-alert.sh +0 -6
- package/scripts/bus-autotrigger.sh +0 -6
- package/scripts/bus-daemon.sh +0 -231
- package/scripts/bus-inject.sh +0 -144
- package/scripts/bus-listen.sh +0 -6
- package/scripts/bus.sh +0 -984
- package/scripts/context-decisions.sh +0 -167
- package/scripts/context-doctor.sh +0 -72
- package/scripts/context-lint.sh +0 -110
- package/scripts/doctor.sh +0 -22
- package/scripts/init.sh +0 -247
- package/scripts/skills.sh +0 -113
- 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 };
|