u-foo 1.0.3 → 1.0.6

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 (91) hide show
  1. package/README.md +67 -8
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +117 -0
  4. package/SKILLS/uinit/SKILL.md +73 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucodex.js +13 -0
  8. package/bin/ufoo +9 -31
  9. package/bin/ufoo.js +13 -0
  10. package/modules/AGENTS.template.md +15 -7
  11. package/modules/bus/README.md +28 -23
  12. package/modules/bus/SKILLS/ubus/SKILL.md +18 -8
  13. package/modules/context/README.md +18 -40
  14. package/modules/context/SKILLS/uctx/SKILL.md +61 -1
  15. package/package.json +16 -4
  16. package/scripts/.archived/bash-to-js-migration/README.md +46 -0
  17. package/scripts/.archived/bash-to-js-migration/banner.sh +89 -0
  18. package/scripts/{bus-inject.sh → .archived/bash-to-js-migration/bus-inject.sh} +35 -3
  19. package/scripts/{bus.sh → .archived/bash-to-js-migration/bus.sh} +3 -1
  20. package/scripts/banner.sh +2 -89
  21. package/scripts/postinstall.js +59 -0
  22. package/src/agent/cliRunner.js +33 -5
  23. package/src/agent/internalRunner.js +78 -51
  24. package/src/agent/launcher.js +702 -0
  25. package/src/agent/notifier.js +200 -0
  26. package/src/agent/ptyRunner.js +377 -0
  27. package/src/agent/ptyWrapper.js +354 -0
  28. package/src/agent/readyDetector.js +159 -0
  29. package/src/agent/ufooAgent.js +37 -42
  30. package/src/bus/API_DESIGN.md +204 -0
  31. package/src/bus/activate.js +156 -0
  32. package/src/bus/daemon.js +308 -0
  33. package/src/bus/index.js +785 -0
  34. package/src/bus/inject.js +285 -0
  35. package/src/bus/message.js +302 -0
  36. package/src/bus/nickname.js +86 -0
  37. package/src/bus/queue.js +131 -0
  38. package/src/bus/shake.js +26 -0
  39. package/src/bus/subscriber.js +296 -0
  40. package/src/bus/utils.js +357 -0
  41. package/src/chat/index.js +1842 -249
  42. package/src/cli.js +658 -95
  43. package/src/config.js +9 -2
  44. package/src/context/decisions.js +314 -0
  45. package/src/context/doctor.js +183 -0
  46. package/src/context/index.js +38 -0
  47. package/src/daemon/index.js +749 -94
  48. package/src/daemon/ops.js +395 -51
  49. package/src/daemon/providerSessions.js +291 -0
  50. package/src/daemon/run.js +34 -1
  51. package/src/daemon/status.js +24 -7
  52. package/src/doctor/index.js +50 -0
  53. package/src/init/index.js +264 -0
  54. package/src/skills/index.js +159 -0
  55. package/src/status/index.js +252 -0
  56. package/src/terminal/detect.js +64 -0
  57. package/src/terminal/index.js +8 -0
  58. package/src/terminal/iterm2.js +126 -0
  59. package/src/ufoo/agentsStore.js +41 -0
  60. package/src/ufoo/paths.js +46 -0
  61. package/src/utils/banner.js +73 -0
  62. package/bin/uclaude +0 -65
  63. package/bin/ucodex +0 -65
  64. package/modules/bus/scripts/bus-alert.sh +0 -185
  65. package/modules/bus/scripts/bus-listen.sh +0 -117
  66. package/modules/context/ASSUMPTIONS.md +0 -7
  67. package/modules/context/CONSTRAINTS.md +0 -7
  68. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  69. package/modules/context/DECISION-PROTOCOL.md +0 -62
  70. package/modules/context/HANDOFF.md +0 -33
  71. package/modules/context/RULES.md +0 -15
  72. package/modules/context/SKILLS/README.md +0 -14
  73. package/modules/context/SYSTEM.md +0 -18
  74. package/modules/context/TEMPLATES/assumptions.md +0 -4
  75. package/modules/context/TEMPLATES/constraints.md +0 -4
  76. package/modules/context/TEMPLATES/decision.md +0 -16
  77. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  78. package/modules/context/TEMPLATES/system.md +0 -3
  79. package/modules/context/TEMPLATES/terminology.md +0 -4
  80. package/modules/context/TERMINOLOGY.md +0 -10
  81. /package/scripts/{bus-alert.sh → .archived/bash-to-js-migration/bus-alert.sh} +0 -0
  82. /package/scripts/{bus-autotrigger.sh → .archived/bash-to-js-migration/bus-autotrigger.sh} +0 -0
  83. /package/scripts/{bus-daemon.sh → .archived/bash-to-js-migration/bus-daemon.sh} +0 -0
  84. /package/scripts/{bus-listen.sh → .archived/bash-to-js-migration/bus-listen.sh} +0 -0
  85. /package/scripts/{context-decisions.sh → .archived/bash-to-js-migration/context-decisions.sh} +0 -0
  86. /package/scripts/{context-doctor.sh → .archived/bash-to-js-migration/context-doctor.sh} +0 -0
  87. /package/scripts/{context-lint.sh → .archived/bash-to-js-migration/context-lint.sh} +0 -0
  88. /package/scripts/{doctor.sh → .archived/bash-to-js-migration/doctor.sh} +0 -0
  89. /package/scripts/{init.sh → .archived/bash-to-js-migration/init.sh} +0 -0
  90. /package/scripts/{skills.sh → .archived/bash-to-js-migration/skills.sh} +0 -0
  91. /package/scripts/{status.sh → .archived/bash-to-js-migration/status.sh} +0 -0
@@ -0,0 +1,200 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const Injector = require("../bus/inject");
4
+ const { getUfooPaths } = require("../ufoo/paths");
5
+ const { shakeTerminalByTty } = require("../bus/shake");
6
+ const { isITerm2 } = require("../terminal/detect");
7
+ const iterm2 = require("../terminal/iterm2");
8
+
9
+ /**
10
+ * Agent 消息通知监听器
11
+ * 监控 pending.jsonl 队列文件,当有新消息时发出通知并自动触发
12
+ */
13
+ class AgentNotifier {
14
+ constructor(projectRoot, subscriber) {
15
+ this.projectRoot = projectRoot;
16
+ this.subscriber = subscriber;
17
+ this.interval = 2000; // 2秒轮询一次
18
+ this.lastCount = 0;
19
+ this.timer = null;
20
+ this.stopped = false;
21
+ this.autoTrigger = process.env.UFOO_AUTO_TRIGGER !== "0"; // 默认启用自动触发
22
+ this.lastNickname = "";
23
+
24
+ // 计算队列文件路径
25
+ const safeSub = subscriber.replace(/:/g, "_");
26
+ const paths = getUfooPaths(projectRoot);
27
+ this.queueFile = path.join(
28
+ paths.busQueuesDir,
29
+ safeSub,
30
+ "pending.jsonl"
31
+ );
32
+ this.agentsFile = paths.agentsFile;
33
+
34
+ // 初始化 injector
35
+ const busDir = paths.busDir;
36
+ this.injector = new Injector(busDir, paths.agentsFile);
37
+ }
38
+
39
+ /**
40
+ * 读取当前订阅者昵称
41
+ */
42
+ getNickname() {
43
+ try {
44
+ if (!this.agentsFile || !fs.existsSync(this.agentsFile)) return "";
45
+ const data = JSON.parse(fs.readFileSync(this.agentsFile, "utf8"));
46
+ const meta = data.agents && data.agents[this.subscriber];
47
+ return (meta && meta.nickname) ? String(meta.nickname) : "";
48
+ } catch {
49
+ return "";
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 设置终端标题为昵称
55
+ * iTerm2: 同时设置 badge 和 cwd
56
+ */
57
+ setTitle(nickname) {
58
+ if (!nickname) return;
59
+ if (!process.stdout || !process.stdout.isTTY) return;
60
+ process.stdout.write(`\x1b]0;${nickname}\x07`);
61
+ if (isITerm2()) {
62
+ iterm2.setBadge(nickname);
63
+ iterm2.setCwd(this.projectRoot);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * 检查昵称变化并更新标题
69
+ */
70
+ refreshTitle() {
71
+ const nickname = this.getNickname();
72
+ if (!nickname || nickname === this.lastNickname) return;
73
+ this.lastNickname = nickname;
74
+ this.setTitle(nickname);
75
+ }
76
+
77
+ /**
78
+ * 更新心跳时间戳(last_seen)
79
+ */
80
+ updateHeartbeat() {
81
+ try {
82
+ if (!this.agentsFile || !fs.existsSync(this.agentsFile)) return;
83
+ const data = JSON.parse(fs.readFileSync(this.agentsFile, "utf8"));
84
+ if (data.agents && data.agents[this.subscriber]) {
85
+ data.agents[this.subscriber].last_seen = new Date().toISOString();
86
+ fs.writeFileSync(this.agentsFile, JSON.stringify(data, null, 2));
87
+ }
88
+ } catch {
89
+ // 心跳更新失败时静默忽略
90
+ }
91
+ }
92
+
93
+ /**
94
+ * 获取当前队列中的消息数量
95
+ */
96
+ getMessageCount() {
97
+ try {
98
+ if (!fs.existsSync(this.queueFile)) return 0;
99
+ const content = fs.readFileSync(this.queueFile, "utf8");
100
+ if (!content.trim()) return 0;
101
+ return content.split("\n").filter((line) => line.trim()).length;
102
+ } catch {
103
+ return 0;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 发送终端通知
109
+ * iTerm2: 使用 OSC 9 原生通知
110
+ */
111
+ notify(newCount) {
112
+ if (isITerm2()) {
113
+ const nick = this.lastNickname || this.subscriber;
114
+ iterm2.notify(`${nick}: ${newCount} new message(s)`);
115
+ }
116
+ const tty = this.injector.readTty(this.subscriber);
117
+ if (tty) {
118
+ shakeTerminalByTty(tty);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * 自动触发终端输入
124
+ */
125
+ async autoTriggerInput() {
126
+ if (!this.autoTrigger) return;
127
+
128
+ try {
129
+ await this.injector.inject(this.subscriber);
130
+ } catch (err) {
131
+ // 自动触发失败时静默忽略,用户仍可手动输入
132
+ // console.error("[notifier] Auto-trigger failed:", err.message);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * 轮询检查队列
138
+ */
139
+ poll() {
140
+ if (this.stopped) return;
141
+
142
+ const currentCount = this.getMessageCount();
143
+
144
+ // 有新消息
145
+ if (currentCount > this.lastCount) {
146
+ const newCount = currentCount - this.lastCount;
147
+ this.notify(newCount);
148
+
149
+ // 自动触发终端输入(非阻塞)
150
+ this.autoTriggerInput().catch(() => {
151
+ // 忽略触发失败
152
+ });
153
+ }
154
+
155
+ this.lastCount = currentCount;
156
+ this.refreshTitle();
157
+ this.updateHeartbeat();
158
+ }
159
+
160
+ /**
161
+ * 启动监听
162
+ */
163
+ start() {
164
+ // 获取初始计数
165
+ this.lastCount = this.getMessageCount();
166
+ this.lastNickname = this.getNickname();
167
+ if (this.lastNickname) {
168
+ this.setTitle(this.lastNickname);
169
+ }
170
+
171
+ // 启动轮询
172
+ this.timer = setInterval(() => {
173
+ this.poll();
174
+ }, this.interval);
175
+
176
+ // 注册清理
177
+ process.on("exit", () => this.stop());
178
+ process.on("SIGINT", () => {
179
+ this.stop();
180
+ process.exit(0);
181
+ });
182
+ process.on("SIGTERM", () => {
183
+ this.stop();
184
+ process.exit(0);
185
+ });
186
+ }
187
+
188
+ /**
189
+ * 停止监听
190
+ */
191
+ stop() {
192
+ this.stopped = true;
193
+ if (this.timer) {
194
+ clearInterval(this.timer);
195
+ this.timer = null;
196
+ }
197
+ }
198
+ }
199
+
200
+ module.exports = AgentNotifier;
@@ -0,0 +1,377 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const EventBus = require("../bus");
4
+ const { runInternalRunner } = require("./internalRunner");
5
+ const { getUfooPaths } = require("../ufoo/paths");
6
+
7
+ function sleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+
11
+ function parseSubscriberId() {
12
+ if (process.env.UFOO_SUBSCRIBER_ID) {
13
+ const parts = process.env.UFOO_SUBSCRIBER_ID.split(":");
14
+ if (parts.length === 2) {
15
+ return {
16
+ subscriber: process.env.UFOO_SUBSCRIBER_ID,
17
+ agentType: parts[0],
18
+ sessionId: parts[1],
19
+ };
20
+ }
21
+ }
22
+ throw new Error("PTY runner requires UFOO_SUBSCRIBER_ID set by daemon");
23
+ }
24
+
25
+ function safeSubscriber(subscriber) {
26
+ return subscriber.replace(/:/g, "_");
27
+ }
28
+
29
+ function drainQueue(queueFile) {
30
+ if (!fs.existsSync(queueFile)) return [];
31
+ const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
32
+ let content = "";
33
+ let readOk = false;
34
+ try {
35
+ fs.renameSync(queueFile, processingFile);
36
+ content = fs.readFileSync(processingFile, "utf8");
37
+ readOk = true;
38
+ } catch {
39
+ try {
40
+ if (fs.existsSync(processingFile)) {
41
+ fs.renameSync(processingFile, queueFile);
42
+ }
43
+ } catch {
44
+ // ignore rollback errors
45
+ }
46
+ return [];
47
+ } finally {
48
+ if (readOk) {
49
+ try {
50
+ if (fs.existsSync(processingFile)) {
51
+ fs.rmSync(processingFile, { force: true });
52
+ }
53
+ } catch {
54
+ // ignore cleanup errors
55
+ }
56
+ }
57
+ }
58
+ if (!content.trim()) return [];
59
+ return content.split(/\r?\n/).filter(Boolean);
60
+ }
61
+
62
+ function stripAnsi(text) {
63
+ return text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
64
+ }
65
+
66
+ function parseInputMessage(message) {
67
+ if (!message) return { raw: false, text: "" };
68
+ try {
69
+ const parsed = JSON.parse(message);
70
+ if (parsed && typeof parsed === "object") {
71
+ if (parsed.raw && typeof parsed.data === "string") {
72
+ return { raw: true, text: parsed.data };
73
+ }
74
+ if (typeof parsed.text === "string") {
75
+ return { raw: false, text: parsed.text };
76
+ }
77
+ }
78
+ } catch {
79
+ // ignore json parse errors
80
+ }
81
+ return { raw: false, text: message };
82
+ }
83
+
84
+ function buildPrompt(text, marker) {
85
+ if (!marker) return text;
86
+ return `${text}\n\n请在完成后输出以下标记(单独一行):\n${marker}\n`;
87
+ }
88
+
89
+ function resolveCommand(agentType) {
90
+ const rawCmd = String(process.env.UFOO_PTY_CMD || "").trim();
91
+ if (rawCmd) {
92
+ const rawArgs = String(process.env.UFOO_PTY_ARGS || "").trim();
93
+ const args = rawArgs ? rawArgs.split(/\s+/).filter(Boolean) : [];
94
+ return { command: rawCmd, args };
95
+ }
96
+ if (agentType === "claude" || agentType === "claude-code") {
97
+ return { command: "claude", args: [] };
98
+ }
99
+ return { command: "codex", args: ["--no-alt-screen", "--sandbox", "workspace-write"] };
100
+ }
101
+
102
+ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
103
+ let pty;
104
+ try {
105
+ // eslint-disable-next-line global-require
106
+ pty = require("node-pty");
107
+ } catch {
108
+ throw new Error("node-pty not installed");
109
+ }
110
+ const { subscriber } = parseSubscriberId();
111
+ const queueDir = path.join(getUfooPaths(projectRoot).busQueuesDir, safeSubscriber(subscriber));
112
+ const queueFile = path.join(queueDir, "pending.jsonl");
113
+ const runDir = getUfooPaths(projectRoot).runDir;
114
+ const logFile = path.join(runDir, "pty-runner.log");
115
+
116
+ const { command, args } = resolveCommand(agentType);
117
+ const env = {
118
+ ...process.env,
119
+ UFOO_LAUNCH_MODE: "internal-pty",
120
+ UFOO_INTERNAL_PTY: "1",
121
+ };
122
+
123
+ const eventBus = new EventBus(projectRoot);
124
+
125
+ let running = true;
126
+ let busy = false;
127
+ let currentPublisher = "";
128
+ let currentMarker = "";
129
+ let pendingOutput = [];
130
+ let outputBuffer = "";
131
+ let flushTimer = null;
132
+ let idleTimer = null;
133
+ let watchdogTimer = null;
134
+ let fallbackInProgress = false;
135
+ let ptyProcess = null;
136
+ const messageQueue = [];
137
+ const maxChunk = 2000;
138
+ const idleMs = 2000;
139
+ const watchdogMs = 120000;
140
+ const maxQueue = 200;
141
+ const watchdogAction = String(process.env.UFOO_PTY_WATCHDOG_ACTION || "restart").toLowerCase();
142
+ let sendQueue = Promise.resolve();
143
+
144
+ function enqueueSend(target, message) {
145
+ if (!target || !message) return;
146
+ sendQueue = sendQueue.then(() => eventBus.send(target, message, subscriber)).catch(() => {});
147
+ }
148
+
149
+ function flushPending() {
150
+ if (!currentPublisher || pendingOutput.length === 0) return;
151
+ const chunks = pendingOutput;
152
+ pendingOutput = [];
153
+ for (const chunk of chunks) {
154
+ enqueueSend(currentPublisher, chunk);
155
+ }
156
+ }
157
+
158
+ function deliverChunk(chunk) {
159
+ if (!chunk) return;
160
+ const payload = JSON.stringify({ stream: true, delta: chunk });
161
+ if (currentPublisher) {
162
+ enqueueSend(currentPublisher, payload);
163
+ } else {
164
+ pendingOutput.push(payload);
165
+ if (pendingOutput.length > 50) pendingOutput.shift();
166
+ }
167
+ }
168
+
169
+ function flushOutput() {
170
+ if (!outputBuffer) return;
171
+ const chunk = outputBuffer.slice(0, maxChunk);
172
+ outputBuffer = outputBuffer.slice(chunk.length);
173
+ if (chunk) {
174
+ deliverChunk(chunk);
175
+ }
176
+ if (outputBuffer) {
177
+ scheduleFlush();
178
+ }
179
+ }
180
+
181
+ function scheduleFlush() {
182
+ if (flushTimer) return;
183
+ flushTimer = setTimeout(() => {
184
+ flushTimer = null;
185
+ flushOutput();
186
+ }, 120);
187
+ }
188
+
189
+ function logNote(note) {
190
+ try {
191
+ fs.mkdirSync(runDir, { recursive: true });
192
+ fs.appendFileSync(logFile, `[${new Date().toISOString()}] ${note}\n`);
193
+ } catch {
194
+ // ignore
195
+ }
196
+ }
197
+
198
+ function attachPty(proc) {
199
+ proc.onData((data) => {
200
+ const clean = stripAnsi(String(data || "")).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
201
+ if (!clean) return;
202
+ outputBuffer += clean;
203
+ if (currentMarker) {
204
+ const idx = outputBuffer.indexOf(currentMarker);
205
+ if (idx !== -1) {
206
+ const before = outputBuffer.slice(0, idx);
207
+ outputBuffer = "";
208
+ if (before) {
209
+ deliverChunk(before);
210
+ }
211
+ currentMarker = "";
212
+ busy = false;
213
+ currentPublisher = "";
214
+ if (watchdogTimer) {
215
+ clearTimeout(watchdogTimer);
216
+ watchdogTimer = null;
217
+ }
218
+ if (idleTimer) {
219
+ clearTimeout(idleTimer);
220
+ idleTimer = null;
221
+ }
222
+ processQueue();
223
+ return;
224
+ }
225
+ }
226
+ scheduleFlush();
227
+ if (busy) {
228
+ if (idleTimer) clearTimeout(idleTimer);
229
+ idleTimer = setTimeout(() => {
230
+ idleTimer = null;
231
+ busy = false;
232
+ currentPublisher = "";
233
+ processQueue();
234
+ }, idleMs);
235
+ }
236
+ });
237
+
238
+ proc.onExit(({ exitCode, signal }) => {
239
+ if (outputBuffer) {
240
+ flushOutput();
241
+ }
242
+ if (flushTimer) {
243
+ clearTimeout(flushTimer);
244
+ flushTimer = null;
245
+ }
246
+ if (idleTimer) {
247
+ clearTimeout(idleTimer);
248
+ idleTimer = null;
249
+ }
250
+ const note = `[internal-pty] process exited code=${exitCode} signal=${signal || ""}`.trim();
251
+ if (currentPublisher) enqueueSend(currentPublisher, note);
252
+ logNote(note);
253
+ running = false;
254
+ });
255
+ }
256
+
257
+ function spawnPtyProcess() {
258
+ const proc = pty.spawn(command, args, {
259
+ name: "xterm-256color",
260
+ cols: 80,
261
+ rows: 24,
262
+ cwd: projectRoot,
263
+ env,
264
+ });
265
+ attachPty(proc);
266
+ return proc;
267
+ }
268
+
269
+ function restartPty(reason) {
270
+ if (!running) return;
271
+ logNote(`Restarting PTY: ${reason}`);
272
+ if (outputBuffer) {
273
+ flushOutput();
274
+ }
275
+ try {
276
+ if (ptyProcess) ptyProcess.kill();
277
+ } catch {
278
+ // ignore
279
+ }
280
+ ptyProcess = spawnPtyProcess();
281
+ }
282
+
283
+ async function fallbackHeadless(reason) {
284
+ if (fallbackInProgress) return;
285
+ fallbackInProgress = true;
286
+ logNote(`Fallback to headless: ${reason}`);
287
+ if (outputBuffer) {
288
+ flushOutput();
289
+ }
290
+ try {
291
+ if (ptyProcess) ptyProcess.kill();
292
+ } catch {
293
+ // ignore
294
+ }
295
+ running = false;
296
+ await runInternalRunner({ projectRoot, agentType });
297
+ process.exit(0);
298
+ }
299
+
300
+ const stop = () => {
301
+ running = false;
302
+ try {
303
+ if (ptyProcess) ptyProcess.kill();
304
+ } catch {
305
+ // ignore
306
+ }
307
+ };
308
+
309
+ process.on("SIGTERM", stop);
310
+ process.on("SIGINT", stop);
311
+
312
+ ptyProcess = spawnPtyProcess();
313
+
314
+ function processQueue() {
315
+ if (busy || messageQueue.length === 0 || !running) return;
316
+ const next = messageQueue.shift();
317
+ if (!next) return;
318
+ busy = true;
319
+ currentPublisher = next.publisher;
320
+ currentMarker = next.marker || "";
321
+ flushPending();
322
+ if (next.text) {
323
+ if (next.raw) {
324
+ ptyProcess.write(next.text);
325
+ } else {
326
+ ptyProcess.write(`${buildPrompt(next.text, currentMarker)}\n`);
327
+ }
328
+ }
329
+ if (watchdogTimer) clearTimeout(watchdogTimer);
330
+ watchdogTimer = setTimeout(() => {
331
+ watchdogTimer = null;
332
+ if (!busy) return;
333
+ const timeoutNote = `[internal-pty] marker timeout; action=${watchdogAction}`;
334
+ if (currentPublisher) enqueueSend(currentPublisher, timeoutNote);
335
+ logNote(timeoutNote);
336
+ if (watchdogAction === "fallback") {
337
+ void fallbackHeadless("marker timeout");
338
+ return;
339
+ }
340
+ if (watchdogAction === "restart") {
341
+ restartPty("marker timeout");
342
+ }
343
+ currentMarker = "";
344
+ busy = false;
345
+ currentPublisher = "";
346
+ processQueue();
347
+ }, watchdogMs);
348
+ }
349
+
350
+ while (running) {
351
+ const lines = drainQueue(queueFile);
352
+ if (lines.length > 0) {
353
+ const events = [];
354
+ for (const line of lines) {
355
+ try {
356
+ events.push(JSON.parse(line));
357
+ } catch {
358
+ // ignore malformed line
359
+ }
360
+ }
361
+ for (const evt of events) {
362
+ if (!evt || !evt.data || typeof evt.data.message !== "string") continue;
363
+ const { raw, text } = parseInputMessage(evt.data.message);
364
+ if (messageQueue.length >= maxQueue) {
365
+ messageQueue.shift();
366
+ }
367
+ const marker = raw ? "" : `__UFOO_DONE_${Date.now()}_${Math.random().toString(16).slice(2)}__`;
368
+ messageQueue.push({ publisher: evt.publisher || "unknown", raw, text, marker });
369
+ }
370
+ }
371
+ processQueue();
372
+ // eslint-disable-next-line no-await-in-loop
373
+ await sleep(200);
374
+ }
375
+ }
376
+
377
+ module.exports = { runPtyRunner };