u-foo 1.8.3 → 1.8.5
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/package.json +1 -1
- package/src/agent/launcher.js +21 -6
- package/src/agent/ptyRunner.js +120 -5
- package/src/bus/index.js +12 -6
- package/src/bus/message.js +16 -0
- package/src/bus/store.js +72 -25
- package/src/daemon/groupOrchestrator.js +43 -0
- package/src/daemon/index.js +8 -0
- package/src/daemon/ops.js +175 -24
- package/src/terminal/adapters/hostAdapter.js +2 -1
package/package.json
CHANGED
package/src/agent/launcher.js
CHANGED
|
@@ -177,6 +177,19 @@ function shouldShowLaunchBanner(agentType = "") {
|
|
|
177
177
|
return true;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
function computeInjectedSubmitDelayMs(agentType, text) {
|
|
181
|
+
const normalizedAgent = String(agentType || "").trim().toLowerCase();
|
|
182
|
+
const input = typeof text === "string" ? text : "";
|
|
183
|
+
let delayMs = normalizedAgent === "claude-code" ? 350 : 200;
|
|
184
|
+
if (input.includes("\n")) {
|
|
185
|
+
delayMs += normalizedAgent === "claude-code" ? 250 : 120;
|
|
186
|
+
}
|
|
187
|
+
if (input.length > 512) {
|
|
188
|
+
delayMs += Math.min(1200, Math.ceil(input.length / 512) * 90);
|
|
189
|
+
}
|
|
190
|
+
return delayMs;
|
|
191
|
+
}
|
|
192
|
+
|
|
180
193
|
async function resolveHostRegistrationData(launchMode) {
|
|
181
194
|
if (launchMode !== "host") {
|
|
182
195
|
return {
|
|
@@ -557,10 +570,10 @@ class AgentLauncher {
|
|
|
557
570
|
let shouldUsePty = false;
|
|
558
571
|
|
|
559
572
|
// 显式开关(优先级最高)
|
|
560
|
-
if (process.env.
|
|
561
|
-
shouldUsePty = false; // 强制回退spawn (CI/回滚)
|
|
562
|
-
} else if (process.env.UFOO_FORCE_PTY === "1") {
|
|
573
|
+
if (process.env.UFOO_FORCE_PTY === "1") {
|
|
563
574
|
shouldUsePty = true; // 强制使用PTY (测试/调试)
|
|
575
|
+
} else if (process.env.UFOO_DISABLE_PTY === "1") {
|
|
576
|
+
shouldUsePty = false; // 强制回退spawn (CI/回滚)
|
|
564
577
|
} else {
|
|
565
578
|
// 自动检测:Terminal/tmux模式 + 非internal
|
|
566
579
|
shouldUsePty =
|
|
@@ -778,13 +791,15 @@ class AgentLauncher {
|
|
|
778
791
|
// Claude Code (Ink TUI) interprets ESC+CR within ~100ms as
|
|
779
792
|
// Alt+Enter (newline) instead of two separate keys. Use a
|
|
780
793
|
// longer gap so the escape sequence parser times out.
|
|
781
|
-
|
|
794
|
+
const commandText = String(req.command);
|
|
795
|
+
const submitDelayMs = computeInjectedSubmitDelayMs(this.agentType, commandText);
|
|
796
|
+
wrapper.write(commandText);
|
|
782
797
|
if (normalizedAgentType === "claude-code") {
|
|
783
798
|
// Claude Code: send CR directly without ESC.
|
|
784
799
|
// ESC before CR is interpreted as Alt+Enter (newline).
|
|
785
800
|
setTimeout(() => {
|
|
786
801
|
wrapper.write("\r");
|
|
787
|
-
},
|
|
802
|
+
}, submitDelayMs);
|
|
788
803
|
} else {
|
|
789
804
|
// Codex/others: ESC dismisses autocomplete, then CR submits.
|
|
790
805
|
setTimeout(() => {
|
|
@@ -792,7 +807,7 @@ class AgentLauncher {
|
|
|
792
807
|
setTimeout(() => {
|
|
793
808
|
wrapper.write("\r");
|
|
794
809
|
}, 100);
|
|
795
|
-
},
|
|
810
|
+
}, submitDelayMs);
|
|
796
811
|
}
|
|
797
812
|
client.write(JSON.stringify({ ok: true }) + "\n");
|
|
798
813
|
if (wrapper.logger) {
|
package/src/agent/ptyRunner.js
CHANGED
|
@@ -85,6 +85,29 @@ function parseInputMessage(message) {
|
|
|
85
85
|
return { raw: false, text: message };
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function getOuterTerminalSize() {
|
|
89
|
+
const cols = Number.isInteger(process.stdout && process.stdout.columns) && process.stdout.columns > 0
|
|
90
|
+
? process.stdout.columns
|
|
91
|
+
: 80;
|
|
92
|
+
const rows = Number.isInteger(process.stdout && process.stdout.rows) && process.stdout.rows > 0
|
|
93
|
+
? process.stdout.rows
|
|
94
|
+
: 24;
|
|
95
|
+
return { cols, rows };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function computeInjectedSubmitDelayMs(agentType, text) {
|
|
99
|
+
const normalizedAgent = String(agentType || "").trim().toLowerCase();
|
|
100
|
+
const input = typeof text === "string" ? text : "";
|
|
101
|
+
let delayMs = normalizedAgent === "claude-code" ? 350 : 200;
|
|
102
|
+
if (input.includes("\n")) {
|
|
103
|
+
delayMs += normalizedAgent === "claude-code" ? 250 : 120;
|
|
104
|
+
}
|
|
105
|
+
if (input.length > 512) {
|
|
106
|
+
delayMs += Math.min(1200, Math.ceil(input.length / 512) * 90);
|
|
107
|
+
}
|
|
108
|
+
return delayMs;
|
|
109
|
+
}
|
|
110
|
+
|
|
88
111
|
function buildPrompt(text, marker) {
|
|
89
112
|
if (!marker) return text;
|
|
90
113
|
return `${text}\n\n请在完成后输出以下标记(单独一行):\n${marker}\n`;
|
|
@@ -223,6 +246,9 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
223
246
|
return 512 * 1024;
|
|
224
247
|
})();
|
|
225
248
|
let outputRingBuffer = "";
|
|
249
|
+
let outerInputHandler = null;
|
|
250
|
+
let outerResizeHandler = null;
|
|
251
|
+
let outerRawModeEnabled = false;
|
|
226
252
|
|
|
227
253
|
function initScreenBuffer(cols = 80, rows = 24) {
|
|
228
254
|
if (!Terminal || !SerializeAddon) return null;
|
|
@@ -302,6 +328,13 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
302
328
|
function broadcastOutput(data) {
|
|
303
329
|
const text = Buffer.from(data || "").toString("utf8");
|
|
304
330
|
if (!text) return;
|
|
331
|
+
if (process.stdout && process.stdout.isTTY && typeof process.stdout.write === "function") {
|
|
332
|
+
try {
|
|
333
|
+
process.stdout.write(text);
|
|
334
|
+
} catch {
|
|
335
|
+
// ignore outer terminal write failures
|
|
336
|
+
}
|
|
337
|
+
}
|
|
305
338
|
enqueueTermWrite(text);
|
|
306
339
|
outputRingBuffer += text;
|
|
307
340
|
if (outputRingBuffer.length > OUTPUT_RING_MAX) {
|
|
@@ -337,7 +370,9 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
337
370
|
if (req.type === "inject" && req.command) {
|
|
338
371
|
if (ptyProcess && ptyAlive) {
|
|
339
372
|
const isClaude = agentType === "claude-code";
|
|
340
|
-
|
|
373
|
+
const commandText = String(req.command);
|
|
374
|
+
const submitDelayMs = computeInjectedSubmitDelayMs(agentType, commandText);
|
|
375
|
+
ptyProcess.write(commandText);
|
|
341
376
|
if (isClaude) {
|
|
342
377
|
// Claude Code: send CR directly without ESC.
|
|
343
378
|
// ESC before CR is interpreted as Alt+Enter (newline).
|
|
@@ -345,7 +380,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
345
380
|
if (ptyProcess && ptyAlive) {
|
|
346
381
|
ptyProcess.write("\r");
|
|
347
382
|
}
|
|
348
|
-
},
|
|
383
|
+
}, submitDelayMs);
|
|
349
384
|
} else {
|
|
350
385
|
// Codex/others: ESC dismisses autocomplete, then CR submits.
|
|
351
386
|
setTimeout(() => {
|
|
@@ -356,7 +391,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
356
391
|
ptyProcess.write("\r");
|
|
357
392
|
}
|
|
358
393
|
}, 100);
|
|
359
|
-
},
|
|
394
|
+
}, submitDelayMs);
|
|
360
395
|
}
|
|
361
396
|
client.write(JSON.stringify({ ok: true }) + "\n");
|
|
362
397
|
} else {
|
|
@@ -443,6 +478,60 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
443
478
|
return server;
|
|
444
479
|
}
|
|
445
480
|
|
|
481
|
+
function syncOuterTerminalSize() {
|
|
482
|
+
const { cols, rows } = getOuterTerminalSize();
|
|
483
|
+
if (ptyProcess && ptyAlive && typeof ptyProcess.resize === "function") {
|
|
484
|
+
try {
|
|
485
|
+
ptyProcess.resize(cols, rows);
|
|
486
|
+
} catch {
|
|
487
|
+
// ignore outer resize failures
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (term && typeof term.resize === "function") {
|
|
491
|
+
try {
|
|
492
|
+
term.resize(cols, rows);
|
|
493
|
+
} catch {
|
|
494
|
+
// ignore local screen buffer resize failures
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function attachOuterTerminalBridge() {
|
|
500
|
+
if (process.stdin && typeof process.stdin.on === "function" && !outerInputHandler) {
|
|
501
|
+
outerInputHandler = (chunk) => {
|
|
502
|
+
if (!ptyProcess || !ptyAlive) return;
|
|
503
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
504
|
+
if (!text) return;
|
|
505
|
+
try {
|
|
506
|
+
ptyProcess.write(text);
|
|
507
|
+
} catch {
|
|
508
|
+
// ignore transient PTY bridge failures
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
process.stdin.on("data", outerInputHandler);
|
|
512
|
+
if (typeof process.stdin.resume === "function") {
|
|
513
|
+
process.stdin.resume();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (process.stdin && process.stdin.isTTY && typeof process.stdin.setRawMode === "function" && !outerRawModeEnabled) {
|
|
518
|
+
try {
|
|
519
|
+
process.stdin.setRawMode(true);
|
|
520
|
+
outerRawModeEnabled = true;
|
|
521
|
+
} catch {
|
|
522
|
+
// ignore raw mode failures on unsupported hosts
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (process.stdout && process.stdout.isTTY && typeof process.stdout.on === "function" && !outerResizeHandler) {
|
|
527
|
+
outerResizeHandler = () => {
|
|
528
|
+
syncOuterTerminalSize();
|
|
529
|
+
};
|
|
530
|
+
process.stdout.on("resize", outerResizeHandler);
|
|
531
|
+
syncOuterTerminalSize();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
446
535
|
function cleanupInjectServer(server) {
|
|
447
536
|
for (const sub of outputSubscribers) {
|
|
448
537
|
try { sub.destroy(); } catch { /* ignore */ }
|
|
@@ -728,10 +817,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
728
817
|
clearTimeout(readyTimer);
|
|
729
818
|
readyTimer = null;
|
|
730
819
|
}
|
|
820
|
+
const { cols, rows } = getOuterTerminalSize();
|
|
731
821
|
const proc = pty.spawn(command, args, {
|
|
732
822
|
name: "xterm-256color",
|
|
733
|
-
cols
|
|
734
|
-
rows
|
|
823
|
+
cols,
|
|
824
|
+
rows,
|
|
735
825
|
cwd: projectRoot,
|
|
736
826
|
env,
|
|
737
827
|
});
|
|
@@ -762,6 +852,30 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
762
852
|
const stop = () => {
|
|
763
853
|
running = false;
|
|
764
854
|
cleanupInjectServer(injectServer);
|
|
855
|
+
if (process.stdin && outerInputHandler) {
|
|
856
|
+
if (typeof process.stdin.off === "function") {
|
|
857
|
+
process.stdin.off("data", outerInputHandler);
|
|
858
|
+
} else if (typeof process.stdin.removeListener === "function") {
|
|
859
|
+
process.stdin.removeListener("data", outerInputHandler);
|
|
860
|
+
}
|
|
861
|
+
outerInputHandler = null;
|
|
862
|
+
}
|
|
863
|
+
if (process.stdout && outerResizeHandler) {
|
|
864
|
+
if (typeof process.stdout.off === "function") {
|
|
865
|
+
process.stdout.off("resize", outerResizeHandler);
|
|
866
|
+
} else if (typeof process.stdout.removeListener === "function") {
|
|
867
|
+
process.stdout.removeListener("resize", outerResizeHandler);
|
|
868
|
+
}
|
|
869
|
+
outerResizeHandler = null;
|
|
870
|
+
}
|
|
871
|
+
if (process.stdin && outerRawModeEnabled && typeof process.stdin.setRawMode === "function") {
|
|
872
|
+
try {
|
|
873
|
+
process.stdin.setRawMode(false);
|
|
874
|
+
} catch {
|
|
875
|
+
// ignore raw mode cleanup failures
|
|
876
|
+
}
|
|
877
|
+
outerRawModeEnabled = false;
|
|
878
|
+
}
|
|
765
879
|
try {
|
|
766
880
|
if (ptyProcess) ptyProcess.kill();
|
|
767
881
|
} catch {
|
|
@@ -776,6 +890,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
776
890
|
process.on("SIGHUP", () => {});
|
|
777
891
|
|
|
778
892
|
ptyProcess = spawnPtyProcess();
|
|
893
|
+
attachOuterTerminalBridge();
|
|
779
894
|
|
|
780
895
|
function processQueue() {
|
|
781
896
|
if (busy || messageQueue.length === 0 || !running || !ptyAlive || !ptyReady) return;
|
package/src/bus/index.js
CHANGED
|
@@ -285,13 +285,19 @@ class EventBus {
|
|
|
285
285
|
|
|
286
286
|
// 如果 publisher 不在 agents 列表中,自动注册它(懒加载模式)
|
|
287
287
|
if (publisher !== "unknown" && this.busData.agents && !this.busData.agents[publisher]) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
288
|
+
let subscriber;
|
|
289
|
+
if (publisher === "ufoo-agent") {
|
|
290
|
+
// Keep the reserved controller on its fixed identity even after metadata loss.
|
|
291
|
+
subscriber = (await this.subscriberManager.join("ufoo-agent", "ufoo-agent", "ufoo-agent")).subscriber;
|
|
292
|
+
} else {
|
|
293
|
+
// 解析 agent 信息
|
|
294
|
+
const parts = publisher.split(":");
|
|
295
|
+
const agentType = parts[0] || "unknown-agent";
|
|
296
|
+
const sessionId = parts[1] || require("./utils").generateInstanceId();
|
|
292
297
|
|
|
293
|
-
|
|
294
|
-
|
|
298
|
+
// 自动加入总线(静默模式,不输出日志)
|
|
299
|
+
subscriber = (await this.subscriberManager.join(sessionId, agentType, null)).subscriber;
|
|
300
|
+
}
|
|
295
301
|
this.saveBusData();
|
|
296
302
|
publisher = subscriber; // 使用规范化的 subscriber ID
|
|
297
303
|
}
|
package/src/bus/message.js
CHANGED
|
@@ -178,6 +178,7 @@ class MessageManager {
|
|
|
178
178
|
resolveTarget(target) {
|
|
179
179
|
const nicknameManager = new NicknameManager(this.busData);
|
|
180
180
|
const normalizedTarget = normalizeAgentTypeAlias(target);
|
|
181
|
+
const controllerTarget = target === "ufoo-agent";
|
|
181
182
|
|
|
182
183
|
// 0. Exact subscriber ID match (allows ids without ":" e.g. "ufoo-agent")
|
|
183
184
|
const subscribers = this.busData.agents || {};
|
|
@@ -185,6 +186,21 @@ class MessageManager {
|
|
|
185
186
|
return [target];
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
// Reserved controller ID should never fan out to legacy typed aliases.
|
|
190
|
+
if (controllerTarget) {
|
|
191
|
+
try {
|
|
192
|
+
const queueDir = this.queueManager && this.queueManager.getQueueDir
|
|
193
|
+
? this.queueManager.getQueueDir(target)
|
|
194
|
+
: path.join(this.busDir, "queues", target);
|
|
195
|
+
if (queueDir && fs.existsSync(queueDir)) {
|
|
196
|
+
return [target];
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// ignore queue lookup errors
|
|
200
|
+
}
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
|
|
188
204
|
// 1. 尝试作为订阅者 ID
|
|
189
205
|
if (target.includes(":")) {
|
|
190
206
|
return [target];
|
package/src/bus/store.js
CHANGED
|
@@ -47,6 +47,75 @@ function allocateRecoveredNickname(agentType, used) {
|
|
|
47
47
|
return nick;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now) {
|
|
51
|
+
if (!subscriber || data.agents[subscriber]) return false;
|
|
52
|
+
|
|
53
|
+
if (subscriber === "ufoo-agent") {
|
|
54
|
+
const tty = readQueueTty(queueDir);
|
|
55
|
+
const ttyInfo = tty ? getTtyProcessInfo(tty) : null;
|
|
56
|
+
data.agents[subscriber] = {
|
|
57
|
+
agent_type: "ufoo-agent",
|
|
58
|
+
nickname: "ufoo-agent",
|
|
59
|
+
status: "active",
|
|
60
|
+
joined_at: now,
|
|
61
|
+
last_seen: now,
|
|
62
|
+
pid: 0,
|
|
63
|
+
tty,
|
|
64
|
+
tty_shell_pid: ttyInfo && ttyInfo.shellPid ? ttyInfo.shellPid : 0,
|
|
65
|
+
tmux_pane: "",
|
|
66
|
+
launch_mode: "",
|
|
67
|
+
};
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const parts = subscriber.split(":");
|
|
72
|
+
if (parts.length !== 2) return false;
|
|
73
|
+
const [agentType, sessionId] = parts;
|
|
74
|
+
if (!agentType || !sessionId) return false;
|
|
75
|
+
if (!isRecoverableSessionId(sessionId)) return false;
|
|
76
|
+
|
|
77
|
+
const tty = readQueueTty(queueDir);
|
|
78
|
+
const ttyInfo = tty ? getTtyProcessInfo(tty) : null;
|
|
79
|
+
const activeByTty = Boolean(ttyInfo && ttyInfo.alive && ttyInfo.hasAgent);
|
|
80
|
+
const nickname = activeByTty ? allocateRecoveredNickname(agentType, usedNicknames) : "";
|
|
81
|
+
|
|
82
|
+
data.agents[subscriber] = {
|
|
83
|
+
agent_type: agentType,
|
|
84
|
+
nickname,
|
|
85
|
+
status: activeByTty ? "active" : "inactive",
|
|
86
|
+
joined_at: now,
|
|
87
|
+
last_seen: now,
|
|
88
|
+
pid: 0,
|
|
89
|
+
tty,
|
|
90
|
+
tty_shell_pid: ttyInfo && ttyInfo.shellPid ? ttyInfo.shellPid : 0,
|
|
91
|
+
tmux_pane: "",
|
|
92
|
+
launch_mode: "",
|
|
93
|
+
};
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function reconcileReservedControllerAliases(data, now) {
|
|
98
|
+
if (!data.agents || !data.agents["ufoo-agent"]) return false;
|
|
99
|
+
|
|
100
|
+
let changed = false;
|
|
101
|
+
for (const [id, meta] of Object.entries(data.agents)) {
|
|
102
|
+
if (!id.startsWith("ufoo-agent:")) continue;
|
|
103
|
+
if (!meta || meta.status !== "active") continue;
|
|
104
|
+
if (String(meta.agent_type || "").trim() !== "ufoo-agent") continue;
|
|
105
|
+
const hasRuntimeBinding = Boolean(
|
|
106
|
+
meta.tty
|
|
107
|
+
|| meta.tmux_pane
|
|
108
|
+
|| meta.host_inject_sock
|
|
109
|
+
|| meta.host_daemon_sock
|
|
110
|
+
);
|
|
111
|
+
if (hasRuntimeBinding) continue;
|
|
112
|
+
meta.status = "inactive";
|
|
113
|
+
meta.last_seen = now;
|
|
114
|
+
changed = true;
|
|
115
|
+
}
|
|
116
|
+
return changed;
|
|
117
|
+
}
|
|
118
|
+
|
|
50
119
|
class BusStore {
|
|
51
120
|
constructor(projectRoot) {
|
|
52
121
|
this.projectRoot = projectRoot;
|
|
@@ -89,33 +158,11 @@ class BusStore {
|
|
|
89
158
|
if (!stat.isDirectory()) continue;
|
|
90
159
|
|
|
91
160
|
const subscriber = safeNameToSubscriber(entry);
|
|
92
|
-
|
|
93
|
-
if (parts.length !== 2) continue;
|
|
94
|
-
const [agentType, sessionId] = parts;
|
|
95
|
-
if (!agentType || !sessionId) continue;
|
|
96
|
-
if (!isRecoverableSessionId(sessionId)) continue;
|
|
97
|
-
if (data.agents[subscriber]) continue;
|
|
98
|
-
|
|
99
|
-
const tty = readQueueTty(queueDir);
|
|
100
|
-
const ttyInfo = tty ? getTtyProcessInfo(tty) : null;
|
|
101
|
-
const activeByTty = Boolean(ttyInfo && ttyInfo.alive && ttyInfo.hasAgent);
|
|
102
|
-
const nickname = activeByTty ? allocateRecoveredNickname(agentType, usedNicknames) : "";
|
|
103
|
-
|
|
104
|
-
data.agents[subscriber] = {
|
|
105
|
-
agent_type: agentType,
|
|
106
|
-
nickname,
|
|
107
|
-
status: activeByTty ? "active" : "inactive",
|
|
108
|
-
joined_at: now,
|
|
109
|
-
last_seen: now,
|
|
110
|
-
pid: 0,
|
|
111
|
-
tty,
|
|
112
|
-
tty_shell_pid: ttyInfo && ttyInfo.shellPid ? ttyInfo.shellPid : 0,
|
|
113
|
-
tmux_pane: "",
|
|
114
|
-
launch_mode: "",
|
|
115
|
-
};
|
|
116
|
-
recovered = true;
|
|
161
|
+
recovered = recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now) || recovered;
|
|
117
162
|
}
|
|
118
163
|
|
|
164
|
+
recovered = reconcileReservedControllerAliases(data, now) || recovered;
|
|
165
|
+
|
|
119
166
|
if (recovered) {
|
|
120
167
|
saveAgentsData(this.agentsFile, data);
|
|
121
168
|
}
|
|
@@ -19,6 +19,32 @@ function asTrimmedString(value) {
|
|
|
19
19
|
return value.trim();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function resolveBootstrapInjectSettleMs(agentType = "", promptText = "", options = {}) {
|
|
23
|
+
const byAgent = options && typeof options.byAgent === "object" ? options.byAgent : {};
|
|
24
|
+
const normalizedAgent = asTrimmedString(agentType).toLowerCase();
|
|
25
|
+
const explicit = Number.isFinite(byAgent[normalizedAgent]) ? byAgent[normalizedAgent] : Number.NaN;
|
|
26
|
+
if (Number.isFinite(explicit) && explicit > 0) {
|
|
27
|
+
return explicit;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let delayMs = 0;
|
|
31
|
+
if (normalizedAgent === "claude") {
|
|
32
|
+
delayMs = 800;
|
|
33
|
+
} else if (normalizedAgent === "codex") {
|
|
34
|
+
delayMs = 250;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const text = typeof promptText === "string" ? promptText : "";
|
|
38
|
+
if (!text) return delayMs;
|
|
39
|
+
if (text.includes("\n")) {
|
|
40
|
+
delayMs += normalizedAgent === "claude" ? 200 : 100;
|
|
41
|
+
}
|
|
42
|
+
if (text.length > 1024) {
|
|
43
|
+
delayMs += Math.min(800, Math.ceil(text.length / 1024) * 120);
|
|
44
|
+
}
|
|
45
|
+
return delayMs;
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
function asStringArray(value) {
|
|
23
49
|
if (!Array.isArray(value)) return [];
|
|
24
50
|
return value.map((item) => asTrimmedString(item)).filter(Boolean);
|
|
@@ -608,6 +634,7 @@ function createGroupOrchestrator(options = {}) {
|
|
|
608
634
|
bootstrapRetryDelayMs = 250,
|
|
609
635
|
bootstrapProtectionMs = 3000,
|
|
610
636
|
bootstrapWorkingGraceMs = 10000,
|
|
637
|
+
bootstrapInjectSettleMsByAgent = {},
|
|
611
638
|
} = options;
|
|
612
639
|
|
|
613
640
|
if (!projectRoot) {
|
|
@@ -775,6 +802,7 @@ function createGroupOrchestrator(options = {}) {
|
|
|
775
802
|
|
|
776
803
|
const rollbackTargets = [];
|
|
777
804
|
const eventBus = new EventBus(projectRoot);
|
|
805
|
+
const tmuxLayoutContext = { mode: "group-right-column" };
|
|
778
806
|
|
|
779
807
|
for (let i = 0; i < compiled.executionPlan.length; i += 1) {
|
|
780
808
|
const item = compiled.executionPlan[i];
|
|
@@ -810,6 +838,8 @@ function createGroupOrchestrator(options = {}) {
|
|
|
810
838
|
agent: item.type,
|
|
811
839
|
count: 1,
|
|
812
840
|
nickname: item.nickname,
|
|
841
|
+
require_activity_monitor: true,
|
|
842
|
+
tmux_layout_context: tmuxLayoutContext,
|
|
813
843
|
...launchHostContext,
|
|
814
844
|
};
|
|
815
845
|
if (Object.keys(extraEnv).length > 0) {
|
|
@@ -924,6 +954,18 @@ function createGroupOrchestrator(options = {}) {
|
|
|
924
954
|
member.bootstrap_error
|
|
925
955
|
);
|
|
926
956
|
}
|
|
957
|
+
const settleDelayMs = resolveBootstrapInjectSettleMs(
|
|
958
|
+
item.type,
|
|
959
|
+
item.bootstrap_prompt,
|
|
960
|
+
{ byAgent: bootstrapInjectSettleMsByAgent }
|
|
961
|
+
);
|
|
962
|
+
if (settleDelayMs > 0) {
|
|
963
|
+
// Claude/Codex TUIs can render their prompt before the input handler
|
|
964
|
+
// is fully ready for a large bootstrap paste. Give the UI a short
|
|
965
|
+
// provider-specific settle window before injecting the group prompt.
|
|
966
|
+
// eslint-disable-next-line no-await-in-loop
|
|
967
|
+
await sleep(settleDelayMs);
|
|
968
|
+
}
|
|
927
969
|
// eslint-disable-next-line no-await-in-loop
|
|
928
970
|
const injected = await injectBootstrapPrompt(
|
|
929
971
|
eventBus,
|
|
@@ -1058,4 +1100,5 @@ module.exports = {
|
|
|
1058
1100
|
createGroupOrchestrator,
|
|
1059
1101
|
buildLaunchPlan,
|
|
1060
1102
|
normalizeGroupId,
|
|
1103
|
+
resolveBootstrapInjectSettleMs,
|
|
1061
1104
|
};
|
package/src/daemon/index.js
CHANGED
|
@@ -472,6 +472,12 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
472
472
|
const launchResult = await launchAgent(projectRoot, agent, count, nickname, processManager, {
|
|
473
473
|
launchScope: op.launch_scope || "",
|
|
474
474
|
terminalApp: op.terminal_app || "",
|
|
475
|
+
tmuxLayoutContext:
|
|
476
|
+
op.tmux_layout_context && typeof op.tmux_layout_context === "object"
|
|
477
|
+
? op.tmux_layout_context
|
|
478
|
+
: ((op.tmuxLayoutContext && typeof op.tmuxLayoutContext === "object")
|
|
479
|
+
? op.tmuxLayoutContext
|
|
480
|
+
: null),
|
|
475
481
|
extraEnv:
|
|
476
482
|
op.extra_env && typeof op.extra_env === "object"
|
|
477
483
|
? op.extra_env
|
|
@@ -486,6 +492,8 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
486
492
|
: ((op.hostCapabilities && typeof op.hostCapabilities === "object")
|
|
487
493
|
? op.hostCapabilities
|
|
488
494
|
: null),
|
|
495
|
+
requireActivityMonitor:
|
|
496
|
+
op.require_activity_monitor === true || op.requireActivityMonitor === true,
|
|
489
497
|
});
|
|
490
498
|
if (launchResult.mode === "internal" && launchResult.subscriberIds && launchResult.subscriberIds.length > 0) {
|
|
491
499
|
const probeAgentType = agent === "codex"
|
package/src/daemon/ops.js
CHANGED
|
@@ -102,6 +102,7 @@ function resolveHostLaunchContext(options = {}) {
|
|
|
102
102
|
options.hostCapabilities && typeof options.hostCapabilities === "object"
|
|
103
103
|
? { ...options.hostCapabilities }
|
|
104
104
|
: null,
|
|
105
|
+
requireActivityMonitor: options.requireActivityMonitor === true,
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
108
|
|
|
@@ -450,7 +451,7 @@ async function spawnManagedHostAgent(
|
|
|
450
451
|
nickname = "",
|
|
451
452
|
processManager = null,
|
|
452
453
|
extraArgs = [],
|
|
453
|
-
extraEnv =
|
|
454
|
+
extraEnv = {},
|
|
454
455
|
hostOptions = {}
|
|
455
456
|
) {
|
|
456
457
|
void processManager;
|
|
@@ -462,11 +463,11 @@ async function spawnManagedHostAgent(
|
|
|
462
463
|
}
|
|
463
464
|
|
|
464
465
|
const hostContext = resolveHostLaunchContext(hostOptions);
|
|
466
|
+
const requireActivityMonitor = hostContext.requireActivityMonitor === true;
|
|
465
467
|
if (!hostContext.hostDaemonSock) {
|
|
466
468
|
throw new Error("host launch requires UFOO_HOST_DAEMON_SOCK");
|
|
467
469
|
}
|
|
468
470
|
|
|
469
|
-
const existing = listSubscribers(projectRoot, agentType);
|
|
470
471
|
const createOptions = {};
|
|
471
472
|
if (hostOptions.groupId) {
|
|
472
473
|
createOptions.group_id = String(hostOptions.groupId).trim();
|
|
@@ -474,22 +475,87 @@ async function spawnManagedHostAgent(
|
|
|
474
475
|
createOptions.source_session_id = hostContext.hostSessionId;
|
|
475
476
|
}
|
|
476
477
|
|
|
478
|
+
// Pre-register subscriber on the bus so waitForNewSubscriber resolves immediately
|
|
479
|
+
const crypto = require("crypto");
|
|
480
|
+
const EventBus = require("../bus");
|
|
481
|
+
const existing = listSubscribers(projectRoot, agentType);
|
|
482
|
+
let subscriberId = "";
|
|
483
|
+
let preRegistrationError = null;
|
|
484
|
+
try {
|
|
485
|
+
const bus = new EventBus(projectRoot);
|
|
486
|
+
await bus.init();
|
|
487
|
+
if (bus.subscriberManager) {
|
|
488
|
+
const sessionToken = crypto.randomBytes(4).toString("hex");
|
|
489
|
+
subscriberId = `${agentType}:${sessionToken}`;
|
|
490
|
+
const defaultNickname = agentType === "ufoo-code" ? "ucode" : normalizedAgent;
|
|
491
|
+
const finalNickname = nickname || defaultNickname;
|
|
492
|
+
await bus.subscriberManager.join(sessionToken, agentType, finalNickname, {
|
|
493
|
+
launchMode: "host",
|
|
494
|
+
parentPid: process.pid,
|
|
495
|
+
});
|
|
496
|
+
bus.saveBusData();
|
|
497
|
+
}
|
|
498
|
+
} catch (err) {
|
|
499
|
+
preRegistrationError = err;
|
|
500
|
+
subscriberId = "";
|
|
501
|
+
}
|
|
502
|
+
|
|
477
503
|
const args = Array.isArray(extraArgs) ? extraArgs : [];
|
|
478
504
|
const argText = args.length > 0 ? ` ${args.map(shellEscape).join(" ")}` : "";
|
|
479
|
-
|
|
505
|
+
|
|
506
|
+
const titleCmd = buildTitleCmd(nickname);
|
|
507
|
+
const hasPreRegisteredSubscriber = !!subscriberId;
|
|
508
|
+
|
|
509
|
+
// Pass env vars to Horizon via the env parameter (Horizon will set them for the child process)
|
|
510
|
+
const env = {
|
|
511
|
+
UFOO_LAUNCH_MODE: "host",
|
|
512
|
+
};
|
|
513
|
+
if (requireActivityMonitor) {
|
|
514
|
+
env.UFOO_FORCE_PTY = "1";
|
|
515
|
+
}
|
|
516
|
+
if (subscriberId) {
|
|
517
|
+
env.UFOO_SUBSCRIBER_ID = subscriberId;
|
|
518
|
+
}
|
|
480
519
|
if (nickname) {
|
|
481
|
-
|
|
520
|
+
env.UFOO_NICKNAME = nickname;
|
|
482
521
|
}
|
|
483
|
-
|
|
484
|
-
|
|
522
|
+
// Parse extraEnv string (e.g., "UFOO_UCODE_BOOTSTRAP_FILE=/path/to/file") and add to env
|
|
523
|
+
if (extraEnv && typeof extraEnv === "object") {
|
|
524
|
+
for (const [key, value] of Object.entries(extraEnv)) {
|
|
525
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(String(key || ""))) {
|
|
526
|
+
env[String(key)] = String(value ?? "");
|
|
527
|
+
}
|
|
528
|
+
}
|
|
485
529
|
}
|
|
486
530
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
531
|
+
let runCmd;
|
|
532
|
+
if (hasPreRegisteredSubscriber) {
|
|
533
|
+
// Group mode: use ufoo launcher for activity_state monitoring
|
|
534
|
+
// This enables ReadyDetector and bootstrap to work correctly
|
|
535
|
+
const ufooRunner = path.join(projectRoot, "bin", "ufoo.js");
|
|
536
|
+
const launchCmd = `${shellEscape(process.execPath)} ${shellEscape(ufooRunner)} agent-pty-runner ${shellEscape(normalizedAgent)}${argText}`.trim();
|
|
537
|
+
runCmd = titleCmd
|
|
538
|
+
? `cd ${shellEscape(projectRoot)} && ${titleCmd} && ${launchCmd}`
|
|
539
|
+
: `cd ${shellEscape(projectRoot)} && ${launchCmd}`;
|
|
540
|
+
// Force PTY wrapper so ReadyDetector + ActivityDetector work for activity_state monitoring.
|
|
541
|
+
// Horizon sets UFOO_DISABLE_PTY=1 unconditionally; UFOO_FORCE_PTY=1 takes priority over it.
|
|
542
|
+
env.UFOO_FORCE_PTY = "1";
|
|
543
|
+
} else {
|
|
544
|
+
if (preRegistrationError) {
|
|
545
|
+
console.error(
|
|
546
|
+
`[host-launch] pre-registration failed for ${nickname || agentType}: ${preRegistrationError.message || String(preRegistrationError)}`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
// Fallback launch still goes through the regular agent launcher binary.
|
|
550
|
+
// For group/bootstrap-monitored flows we also force the PTY wrapper so
|
|
551
|
+
// activity_state can progress out of "starting" after self-registration.
|
|
552
|
+
const directCmd = `${binary}${argText}`;
|
|
553
|
+
runCmd = titleCmd
|
|
554
|
+
? `cd ${shellEscape(projectRoot)} && ${titleCmd} && ${directCmd}`
|
|
555
|
+
: `cd ${shellEscape(projectRoot)} && ${directCmd}`;
|
|
556
|
+
}
|
|
492
557
|
createOptions.command = runCmd;
|
|
558
|
+
createOptions.env = env;
|
|
493
559
|
|
|
494
560
|
const created = await createHostSession(hostContext.hostDaemonSock, createOptions);
|
|
495
561
|
const sessionId = normalizeOptionalString(created?.session_id);
|
|
@@ -498,8 +564,16 @@ async function spawnManagedHostAgent(
|
|
|
498
564
|
throw new Error("host create_session returned incomplete session info");
|
|
499
565
|
}
|
|
500
566
|
|
|
501
|
-
|
|
502
|
-
|
|
567
|
+
// If pre-registration succeeded we already have the subscriber ID;
|
|
568
|
+
// otherwise fall back to polling (slower but still works).
|
|
569
|
+
if (!subscriberId) {
|
|
570
|
+
subscriberId = await waitForNewSubscriber(projectRoot, agentType, existing, 20000);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Return format must match what launchAgent expects: { mode, launchScope, subscriberIds }
|
|
574
|
+
// subscriberIds is an array for consistency with other launch modes
|
|
575
|
+
const resultSubscriberId = subscriberId || null;
|
|
576
|
+
return { child: null, subscriberId: resultSubscriberId, subscriberIds: [resultSubscriberId].filter(Boolean), sessionId, injectSock };
|
|
503
577
|
}
|
|
504
578
|
|
|
505
579
|
async function spawnInternalAgent(projectRoot, agent, count = 1, nickname = "", processManager = null, extraEnv = {}) {
|
|
@@ -679,7 +753,49 @@ function resolveTmuxPaneTarget() {
|
|
|
679
753
|
return "";
|
|
680
754
|
}
|
|
681
755
|
|
|
682
|
-
function
|
|
756
|
+
function runTmuxCommand(tmuxArgs = [], failureMessage = "tmux command failed", captureStdout = false) {
|
|
757
|
+
return new Promise((resolve, reject) => {
|
|
758
|
+
const proc = spawn("tmux", tmuxArgs);
|
|
759
|
+
let stdout = "";
|
|
760
|
+
let stderr = "";
|
|
761
|
+
proc.stdout.on("data", (d) => {
|
|
762
|
+
stdout += d.toString("utf8");
|
|
763
|
+
});
|
|
764
|
+
proc.stderr.on("data", (d) => {
|
|
765
|
+
stderr += d.toString("utf8");
|
|
766
|
+
});
|
|
767
|
+
proc.on("error", (err) => reject(err));
|
|
768
|
+
proc.on("close", (code) => {
|
|
769
|
+
if (code === 0) {
|
|
770
|
+
resolve(captureStdout ? stdout.trim() : "");
|
|
771
|
+
} else {
|
|
772
|
+
reject(new Error(stderr || failureMessage));
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function applyTmuxLayout(layout = "", target = "") {
|
|
779
|
+
const normalizedLayout = String(layout || "").trim();
|
|
780
|
+
if (!normalizedLayout) return Promise.resolve("");
|
|
781
|
+
const tmuxArgs = ["select-layout"];
|
|
782
|
+
const normalizedTarget = String(target || "").trim();
|
|
783
|
+
if (normalizedTarget) {
|
|
784
|
+
tmuxArgs.push("-t", normalizedTarget);
|
|
785
|
+
}
|
|
786
|
+
tmuxArgs.push(normalizedLayout);
|
|
787
|
+
return runTmuxCommand(tmuxArgs, "tmux select-layout failed");
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function spawnTmuxPane(
|
|
791
|
+
projectRoot,
|
|
792
|
+
agent,
|
|
793
|
+
nickname = "",
|
|
794
|
+
extraArgs = [],
|
|
795
|
+
extraEnv = "",
|
|
796
|
+
target = "",
|
|
797
|
+
splitOptions = {}
|
|
798
|
+
) {
|
|
683
799
|
return new Promise((resolve, reject) => {
|
|
684
800
|
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
685
801
|
const binary = toTmuxBinary(normalizedAgent);
|
|
@@ -697,21 +813,25 @@ function spawnTmuxPane(projectRoot, agent, nickname = "", extraArgs = [], extraE
|
|
|
697
813
|
const runCmd = `cd ${shellEscape(projectRoot)} && ${setPaneEnv}${modeEnv}${nickEnv}${ttyEnv}${envPrefix}${binary}${argText}`;
|
|
698
814
|
|
|
699
815
|
const tmuxArgs = ["split-window", "-d"];
|
|
816
|
+
const orientation = String(splitOptions.orientation || "").trim().toLowerCase();
|
|
817
|
+
if (orientation === "horizontal") {
|
|
818
|
+
tmuxArgs.push("-h");
|
|
819
|
+
} else if (orientation === "vertical") {
|
|
820
|
+
tmuxArgs.push("-v");
|
|
821
|
+
}
|
|
822
|
+
const capturePaneId = splitOptions.capturePaneId === true;
|
|
823
|
+
if (capturePaneId) {
|
|
824
|
+
tmuxArgs.push("-P", "-F", "#{pane_id}");
|
|
825
|
+
}
|
|
700
826
|
const normalizedTarget = String(target || "").trim();
|
|
701
827
|
if (normalizedTarget) {
|
|
702
828
|
tmuxArgs.push("-t", normalizedTarget);
|
|
703
829
|
}
|
|
704
830
|
tmuxArgs.push(runCmd);
|
|
705
831
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
stderr += d.toString("utf8");
|
|
710
|
-
});
|
|
711
|
-
proc.on("close", (code) => {
|
|
712
|
-
if (code === 0) resolve();
|
|
713
|
-
else reject(new Error(stderr || "tmux split-window failed"));
|
|
714
|
-
});
|
|
832
|
+
runTmuxCommand(tmuxArgs, "tmux split-window failed", capturePaneId)
|
|
833
|
+
.then((paneId) => resolve({ paneId: capturePaneId ? paneId : "" }))
|
|
834
|
+
.catch(reject);
|
|
715
835
|
});
|
|
716
836
|
}
|
|
717
837
|
|
|
@@ -762,6 +882,12 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
762
882
|
}
|
|
763
883
|
const paneTarget = resolveTmuxPaneTarget();
|
|
764
884
|
const useSeparateWindow = launchScope === "window";
|
|
885
|
+
const tmuxLayoutContext = options.tmuxLayoutContext && typeof options.tmuxLayoutContext === "object"
|
|
886
|
+
? options.tmuxLayoutContext
|
|
887
|
+
: null;
|
|
888
|
+
const useGroupRightColumnLayout = !useSeparateWindow
|
|
889
|
+
&& tmuxLayoutContext
|
|
890
|
+
&& tmuxLayoutContext.mode === "group-right-column";
|
|
765
891
|
for (let i = 0; i < count; i += 1) {
|
|
766
892
|
// Use "ucode" as default nickname for ufoo/ucode agents
|
|
767
893
|
const defaultNick = normalizedAgent === "ufoo" ? "ucode" : normalizedAgent;
|
|
@@ -769,6 +895,31 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
769
895
|
if (useSeparateWindow) {
|
|
770
896
|
// eslint-disable-next-line no-await-in-loop
|
|
771
897
|
await spawnTmuxWindow(projectRoot, normalizedAgent, nick, [], extraEnvPrefix);
|
|
898
|
+
} else if (useGroupRightColumnLayout && paneTarget) {
|
|
899
|
+
const basePane = String(tmuxLayoutContext.basePane || paneTarget).trim() || paneTarget;
|
|
900
|
+
tmuxLayoutContext.basePane = basePane;
|
|
901
|
+
const rightColumnPane = String(tmuxLayoutContext.rightColumnPane || "").trim();
|
|
902
|
+
const splitTarget = rightColumnPane || basePane;
|
|
903
|
+
const splitOrientation = rightColumnPane ? "vertical" : "horizontal";
|
|
904
|
+
let splitResult;
|
|
905
|
+
try {
|
|
906
|
+
// eslint-disable-next-line no-await-in-loop
|
|
907
|
+
splitResult = await spawnTmuxPane(projectRoot, normalizedAgent, nick, [], extraEnvPrefix, splitTarget, {
|
|
908
|
+
orientation: splitOrientation,
|
|
909
|
+
capturePaneId: !rightColumnPane,
|
|
910
|
+
});
|
|
911
|
+
} catch {
|
|
912
|
+
// Fallback to new window when current pane target cannot be resolved.
|
|
913
|
+
// eslint-disable-next-line no-await-in-loop
|
|
914
|
+
await spawnTmuxWindow(projectRoot, normalizedAgent, nick, [], extraEnvPrefix);
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (!rightColumnPane && splitResult && splitResult.paneId) {
|
|
918
|
+
tmuxLayoutContext.rightColumnPane = splitResult.paneId;
|
|
919
|
+
}
|
|
920
|
+
// Keep the original chat pane on the left while stacking agents evenly on the right.
|
|
921
|
+
// eslint-disable-next-line no-await-in-loop
|
|
922
|
+
await applyTmuxLayout("main-vertical", basePane);
|
|
772
923
|
} else {
|
|
773
924
|
try {
|
|
774
925
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -795,7 +946,7 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
|
|
|
795
946
|
nick,
|
|
796
947
|
processManager,
|
|
797
948
|
[],
|
|
798
|
-
|
|
949
|
+
extraEnvObject,
|
|
799
950
|
hostContext
|
|
800
951
|
);
|
|
801
952
|
if (result.subscriberId) subscriberIds.push(result.subscriberId);
|
|
@@ -334,7 +334,7 @@ async function requestCloseSession(sockPath) {
|
|
|
334
334
|
/**
|
|
335
335
|
* Create a new terminal session via the daemon management socket.
|
|
336
336
|
* @param {string} [daemonSock] - Override daemon socket path (defaults to env)
|
|
337
|
-
* @param {object} [opts] - Options: { group_id, source_session_id, command }
|
|
337
|
+
* @param {object} [opts] - Options: { group_id, source_session_id, command, env }
|
|
338
338
|
* @returns {Promise<{session_id: string, inject_sock: string}>}
|
|
339
339
|
*/
|
|
340
340
|
async function createSession(daemonSock, opts = {}) {
|
|
@@ -343,6 +343,7 @@ async function createSession(daemonSock, opts = {}) {
|
|
|
343
343
|
if (opts.group_id) req.group_id = opts.group_id;
|
|
344
344
|
if (opts.source_session_id) req.source_session_id = opts.source_session_id;
|
|
345
345
|
if (opts.command) req.command = opts.command;
|
|
346
|
+
if (opts.env) req.env = opts.env;
|
|
346
347
|
return sendToSocket(sock, req);
|
|
347
348
|
}
|
|
348
349
|
|