u-foo 2.3.10 → 2.3.12
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 +3 -1
- package/src/agent/activityStateWriter.js +14 -3
- package/src/agent/claudeThreadProvider.js +208 -39
- package/src/agent/codexThreadProvider.js +125 -64
- package/src/agent/internalRunner.js +191 -22
- package/src/agent/notifier.js +15 -4
- package/src/agent/ptyRunner.js +8 -92
- package/src/bus/index.js +2 -1
- package/src/bus/store.js +17 -5
- package/src/bus/subscriber.js +57 -1
- package/src/bus/utils.js +6 -0
- package/src/chat/agentViewController.js +4 -1
- package/src/chat/dashboardKeyController.js +17 -2
- package/src/ufoo/agentRegistryDiagnostics.js +91 -0
- package/src/ufoo/agentsStore.js +38 -2
|
@@ -3,24 +3,20 @@ const path = require("path");
|
|
|
3
3
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
4
4
|
const { spawnSync } = require("child_process");
|
|
5
5
|
const EventBus = require("../bus");
|
|
6
|
+
const { readJSON, writeJSON } = require("../bus/utils");
|
|
6
7
|
const { runCliAgent } = require("./cliRunner");
|
|
7
8
|
const { normalizeCliOutput } = require("./normalizeOutput");
|
|
8
9
|
const { createActivityStatePublisher } = require("./activityStatePublisher");
|
|
9
10
|
const { loadConfig, normalizeCodexInternalThreadMode } = require("../config");
|
|
10
|
-
const {
|
|
11
|
-
|
|
12
|
-
defaultCodexTransportStreamFactory,
|
|
13
|
-
} = require("./codexThreadProvider");
|
|
14
|
-
const {
|
|
15
|
-
createClaudeThreadProvider,
|
|
16
|
-
defaultClaudeTransportStreamFactory,
|
|
17
|
-
} = require("./claudeThreadProvider");
|
|
11
|
+
const { createCodexThreadProvider } = require("./codexThreadProvider");
|
|
12
|
+
const { createClaudeThreadProvider } = require("./claudeThreadProvider");
|
|
18
13
|
const { resolveClaudeUpstreamCredentials } = require("./credentials/claude");
|
|
19
14
|
const { buildUpstreamAuthFromCredential } = require("./credentials");
|
|
20
15
|
const { listToolsForCallerTier, CALLER_TIERS } = require("../tools");
|
|
21
16
|
const { redactToolCallPayload, redactSecrets } = require("../providerapi/redactor");
|
|
22
17
|
const { buildCachedMemoryPrefix } = require("../memory");
|
|
23
18
|
const { shouldForwardStreamToPublisher } = require("./publisherRouting");
|
|
19
|
+
const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnostics");
|
|
24
20
|
|
|
25
21
|
function sleep(ms) {
|
|
26
22
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -131,6 +127,73 @@ function drainQueue(queueFile) {
|
|
|
131
127
|
return content.split(/\r?\n/).filter(Boolean);
|
|
132
128
|
}
|
|
133
129
|
|
|
130
|
+
function parseAgentViewRawInput(message) {
|
|
131
|
+
if (typeof message !== "string" || !message.trim()) return null;
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(message);
|
|
134
|
+
if (parsed && parsed.raw === true && typeof parsed.data === "string") {
|
|
135
|
+
return parsed.data;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Not a raw agent-view envelope.
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createInteractiveInputSession({ write = () => {} } = {}) {
|
|
144
|
+
let buffer = "";
|
|
145
|
+
|
|
146
|
+
function writePrompt() {
|
|
147
|
+
write("> ");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function handleRaw(data) {
|
|
151
|
+
const submissions = [];
|
|
152
|
+
const text = String(data || "");
|
|
153
|
+
for (const char of text) {
|
|
154
|
+
if (char === "\r" || char === "\n") {
|
|
155
|
+
const submitted = buffer.trim();
|
|
156
|
+
buffer = "";
|
|
157
|
+
write("\r\n");
|
|
158
|
+
if (submitted) submissions.push(submitted);
|
|
159
|
+
else writePrompt();
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (char === "\u0003") {
|
|
164
|
+
buffer = "";
|
|
165
|
+
write("^C\r\n");
|
|
166
|
+
writePrompt();
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (char === "\u007f" || char === "\b") {
|
|
171
|
+
if (buffer.length > 0) {
|
|
172
|
+
buffer = buffer.slice(0, -1);
|
|
173
|
+
write("\b \b");
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (char >= " " && char !== "\u007f") {
|
|
179
|
+
buffer += char;
|
|
180
|
+
write(char);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return submissions;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
handleRaw,
|
|
188
|
+
writePrompt,
|
|
189
|
+
writeResponsePrompt: () => {
|
|
190
|
+
write("\r\n");
|
|
191
|
+
writePrompt();
|
|
192
|
+
},
|
|
193
|
+
getBuffer: () => buffer,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
134
197
|
async function handleEvent(
|
|
135
198
|
projectRoot,
|
|
136
199
|
agentType,
|
|
@@ -259,6 +322,9 @@ async function handleThreadedEvent({
|
|
|
259
322
|
const plainReplyParts = [];
|
|
260
323
|
for await (const event of threadRuntime.thread.runStreamed(prompt, {})) {
|
|
261
324
|
if (!event || typeof event !== "object") continue;
|
|
325
|
+
if (typeof threadRuntime.syncProviderSessionId === "function") {
|
|
326
|
+
threadRuntime.syncProviderSessionId();
|
|
327
|
+
}
|
|
262
328
|
if (event.type === "text_delta" && event.delta) {
|
|
263
329
|
if (streamToPublisher) {
|
|
264
330
|
emitStreamDelta(event.delta);
|
|
@@ -269,6 +335,9 @@ async function handleThreadedEvent({
|
|
|
269
335
|
throw new Error(event.error || `thread turn failed for ${agentType}`);
|
|
270
336
|
}
|
|
271
337
|
}
|
|
338
|
+
if (typeof threadRuntime.syncProviderSessionId === "function") {
|
|
339
|
+
threadRuntime.syncProviderSessionId();
|
|
340
|
+
}
|
|
272
341
|
|
|
273
342
|
if (streamToPublisher) {
|
|
274
343
|
busSender.enqueue(
|
|
@@ -420,15 +489,55 @@ function buildClaudeAuthProvider(projectRoot) {
|
|
|
420
489
|
};
|
|
421
490
|
}
|
|
422
491
|
|
|
423
|
-
function
|
|
492
|
+
function persistProviderSessionId(projectRoot, subscriber, providerSessionId) {
|
|
493
|
+
const id = String(providerSessionId || "").trim();
|
|
494
|
+
if (!projectRoot || !subscriber || !id) return false;
|
|
495
|
+
try {
|
|
496
|
+
const agentsFile = getUfooPaths(projectRoot).agentsFile;
|
|
497
|
+
const parsed = fs.existsSync(agentsFile)
|
|
498
|
+
? readJSON(agentsFile, null)
|
|
499
|
+
: {};
|
|
500
|
+
if (!parsed) return false;
|
|
501
|
+
if (!parsed.agents || typeof parsed.agents !== "object") return false;
|
|
502
|
+
if (!parsed.agents[subscriber] || typeof parsed.agents[subscriber] !== "object") {
|
|
503
|
+
appendAgentRegistryDiagnostic(agentsFile, "provider_session_subscriber_missing", {
|
|
504
|
+
source: "agent.internalRunner.persistProviderSessionId",
|
|
505
|
+
subscriber,
|
|
506
|
+
known_ids: Object.keys(parsed.agents || {}).sort(),
|
|
507
|
+
});
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
if (parsed.agents[subscriber].provider_session_id === id) return false;
|
|
511
|
+
parsed.agents[subscriber].provider_session_id = id;
|
|
512
|
+
parsed.agents[subscriber].provider_session_updated_at = new Date().toISOString();
|
|
513
|
+
writeJSON(agentsFile, parsed);
|
|
514
|
+
return true;
|
|
515
|
+
} catch {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], subscriber = "", providerSessionId = "" }) {
|
|
424
521
|
const disabledRuntime = {
|
|
425
522
|
enabled: false,
|
|
426
523
|
thread: null,
|
|
427
524
|
toolRuntime: { enabled: false, mode: "disabled", tools: [] },
|
|
428
525
|
close: async () => {},
|
|
429
526
|
rebuildThread: async () => {},
|
|
527
|
+
syncProviderSessionId: () => false,
|
|
430
528
|
};
|
|
431
529
|
|
|
530
|
+
const initialProviderSessionId = String(providerSessionId || "").trim();
|
|
531
|
+
let savedProviderSessionId = initialProviderSessionId;
|
|
532
|
+
|
|
533
|
+
function rememberProviderSessionId(thread) {
|
|
534
|
+
const id = String(thread && thread.id ? thread.id : "").trim();
|
|
535
|
+
if (!id || id === savedProviderSessionId) return false;
|
|
536
|
+
const changed = persistProviderSessionId(projectRoot, subscriber, id);
|
|
537
|
+
if (changed) savedProviderSessionId = id;
|
|
538
|
+
return changed;
|
|
539
|
+
}
|
|
540
|
+
|
|
432
541
|
if (provider === "codex-cli") {
|
|
433
542
|
if (getCodexThreadMode(projectRoot) !== "api") {
|
|
434
543
|
return disabledRuntime;
|
|
@@ -444,9 +553,10 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
|
|
|
444
553
|
cwd: projectRoot,
|
|
445
554
|
extraArgs,
|
|
446
555
|
tools: toolRuntime.tools,
|
|
447
|
-
streamFactory: defaultCodexTransportStreamFactory,
|
|
448
556
|
});
|
|
449
|
-
let thread =
|
|
557
|
+
let thread = initialProviderSessionId
|
|
558
|
+
? providerInstance.resumeThread(initialProviderSessionId)
|
|
559
|
+
: providerInstance.startThread();
|
|
450
560
|
|
|
451
561
|
return {
|
|
452
562
|
enabled: true,
|
|
@@ -454,6 +564,9 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
|
|
|
454
564
|
get thread() {
|
|
455
565
|
return thread;
|
|
456
566
|
},
|
|
567
|
+
syncProviderSessionId() {
|
|
568
|
+
return rememberProviderSessionId(thread);
|
|
569
|
+
},
|
|
457
570
|
async rebuildThread() {
|
|
458
571
|
if (thread && typeof thread.close === "function") {
|
|
459
572
|
await thread.close();
|
|
@@ -463,9 +576,10 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
|
|
|
463
576
|
cwd: projectRoot,
|
|
464
577
|
extraArgs,
|
|
465
578
|
tools: toolRuntime.tools,
|
|
466
|
-
streamFactory: defaultCodexTransportStreamFactory,
|
|
467
579
|
});
|
|
468
|
-
thread =
|
|
580
|
+
thread = savedProviderSessionId
|
|
581
|
+
? providerInstance.resumeThread(savedProviderSessionId)
|
|
582
|
+
: providerInstance.startThread();
|
|
469
583
|
},
|
|
470
584
|
async close() {
|
|
471
585
|
if (thread && typeof thread.close === "function") {
|
|
@@ -482,33 +596,40 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
|
|
|
482
596
|
if (getClaudeThreadMode() !== "api") {
|
|
483
597
|
return disabledRuntime;
|
|
484
598
|
}
|
|
485
|
-
if (typeof createClaudeThreadProvider !== "function"
|
|
599
|
+
if (typeof createClaudeThreadProvider !== "function") {
|
|
486
600
|
return disabledRuntime;
|
|
487
601
|
}
|
|
488
602
|
|
|
489
603
|
try {
|
|
490
604
|
let providerInstance = createClaudeThreadProvider({
|
|
491
605
|
model,
|
|
492
|
-
|
|
493
|
-
|
|
606
|
+
cwd: projectRoot,
|
|
607
|
+
extraArgs,
|
|
494
608
|
});
|
|
495
|
-
let thread =
|
|
609
|
+
let thread = initialProviderSessionId
|
|
610
|
+
? providerInstance.resumeThread(initialProviderSessionId)
|
|
611
|
+
: providerInstance.startThread();
|
|
496
612
|
|
|
497
613
|
return {
|
|
498
614
|
enabled: true,
|
|
499
615
|
get thread() {
|
|
500
616
|
return thread;
|
|
501
617
|
},
|
|
618
|
+
syncProviderSessionId() {
|
|
619
|
+
return rememberProviderSessionId(thread);
|
|
620
|
+
},
|
|
502
621
|
async rebuildThread() {
|
|
503
622
|
if (thread && typeof thread.close === "function") {
|
|
504
623
|
await thread.close();
|
|
505
624
|
}
|
|
506
625
|
providerInstance = createClaudeThreadProvider({
|
|
507
626
|
model,
|
|
508
|
-
|
|
509
|
-
|
|
627
|
+
cwd: projectRoot,
|
|
628
|
+
extraArgs,
|
|
510
629
|
});
|
|
511
|
-
thread =
|
|
630
|
+
thread = savedProviderSessionId
|
|
631
|
+
? providerInstance.resumeThread(savedProviderSessionId)
|
|
632
|
+
: providerInstance.startThread();
|
|
512
633
|
},
|
|
513
634
|
async close() {
|
|
514
635
|
if (thread && typeof thread.close === "function") {
|
|
@@ -538,12 +659,14 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
538
659
|
const provider = normalizedAgentType === "codex" ? "codex-cli" : "claude-cli";
|
|
539
660
|
const model = process.env.UFOO_AGENT_MODEL || "";
|
|
540
661
|
const busSender = createBusSender(projectRoot, subscriber);
|
|
662
|
+
const interactiveSessions = new Map();
|
|
541
663
|
const threadRuntime = createThreadRuntime({
|
|
542
664
|
projectRoot,
|
|
543
665
|
provider,
|
|
544
666
|
model,
|
|
545
667
|
extraArgs,
|
|
546
668
|
subscriber,
|
|
669
|
+
providerSessionId: process.env.UFOO_PROVIDER_SESSION_ID || "",
|
|
547
670
|
});
|
|
548
671
|
|
|
549
672
|
// Session state management for CLI continuity
|
|
@@ -586,6 +709,18 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
586
709
|
activityPublisher.publish(state);
|
|
587
710
|
}
|
|
588
711
|
|
|
712
|
+
function getInteractiveSession(publisher) {
|
|
713
|
+
const key = String(publisher || "unknown");
|
|
714
|
+
if (interactiveSessions.has(key)) return interactiveSessions.get(key);
|
|
715
|
+
const session = createInteractiveInputSession({
|
|
716
|
+
write: (delta) => {
|
|
717
|
+
busSender.enqueue(key, JSON.stringify({ stream: true, delta: String(delta || "") }));
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
interactiveSessions.set(key, session);
|
|
721
|
+
return session;
|
|
722
|
+
}
|
|
723
|
+
|
|
589
724
|
setActivityState("ready");
|
|
590
725
|
|
|
591
726
|
// 心跳更新函数
|
|
@@ -615,7 +750,6 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
615
750
|
try {
|
|
616
751
|
const lines = drainQueue(queueFile);
|
|
617
752
|
if (lines.length > 0) {
|
|
618
|
-
setActivityState("working");
|
|
619
753
|
const events = [];
|
|
620
754
|
for (const line of lines) {
|
|
621
755
|
try {
|
|
@@ -625,7 +759,33 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
625
759
|
}
|
|
626
760
|
}
|
|
627
761
|
|
|
762
|
+
const runnableEvents = [];
|
|
628
763
|
for (const evt of events) {
|
|
764
|
+
const rawInput = parseAgentViewRawInput(evt && evt.data ? evt.data.message : "");
|
|
765
|
+
if (rawInput === null) {
|
|
766
|
+
runnableEvents.push(evt);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const session = getInteractiveSession(evt.publisher || "unknown");
|
|
771
|
+
const submissions = session.handleRaw(rawInput);
|
|
772
|
+
for (const message of submissions) {
|
|
773
|
+
runnableEvents.push({
|
|
774
|
+
...evt,
|
|
775
|
+
__agentViewRaw: true,
|
|
776
|
+
data: {
|
|
777
|
+
...(evt.data || {}),
|
|
778
|
+
message,
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (runnableEvents.length > 0) {
|
|
785
|
+
setActivityState("working");
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
for (const evt of runnableEvents) {
|
|
629
789
|
// eslint-disable-next-line no-await-in-loop
|
|
630
790
|
await handleEvent(
|
|
631
791
|
projectRoot,
|
|
@@ -640,6 +800,9 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
640
800
|
extraArgs,
|
|
641
801
|
threadRuntime
|
|
642
802
|
);
|
|
803
|
+
if (evt.__agentViewRaw) {
|
|
804
|
+
getInteractiveSession(evt.publisher || "unknown").writeResponsePrompt();
|
|
805
|
+
}
|
|
643
806
|
}
|
|
644
807
|
|
|
645
808
|
// Persist CLI session state after processing (only if changed and for claude)
|
|
@@ -659,7 +822,10 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
659
822
|
// 处理消息后更新心跳
|
|
660
823
|
updateHeartbeat();
|
|
661
824
|
lastHeartbeat = now;
|
|
662
|
-
|
|
825
|
+
if (runnableEvents.length > 0) {
|
|
826
|
+
setActivityState("idle");
|
|
827
|
+
}
|
|
828
|
+
await busSender.flush();
|
|
663
829
|
}
|
|
664
830
|
} finally {
|
|
665
831
|
processing = false;
|
|
@@ -684,4 +850,7 @@ module.exports = {
|
|
|
684
850
|
getClaudeThreadMode,
|
|
685
851
|
buildClaudeAuthProvider,
|
|
686
852
|
shouldFallbackToLegacyThreadProvider,
|
|
853
|
+
parseAgentViewRawInput,
|
|
854
|
+
createInteractiveInputSession,
|
|
855
|
+
persistProviderSessionId,
|
|
687
856
|
};
|
package/src/agent/notifier.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const EventBus = require("../bus");
|
|
4
|
+
const { readJSON, writeJSON } = require("../bus/utils");
|
|
4
5
|
const Injector = require("../bus/inject");
|
|
5
6
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
7
|
+
const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnostics");
|
|
6
8
|
const { shakeTerminalByTty } = require("../bus/shake");
|
|
7
9
|
const { isITerm2 } = require("../terminal/detect");
|
|
8
10
|
const iterm2 = require("../terminal/iterm2");
|
|
@@ -62,7 +64,8 @@ class AgentNotifier {
|
|
|
62
64
|
getNickname() {
|
|
63
65
|
try {
|
|
64
66
|
if (!this.agentsFile || !fs.existsSync(this.agentsFile)) return "";
|
|
65
|
-
const data =
|
|
67
|
+
const data = readJSON(this.agentsFile, null);
|
|
68
|
+
if (!data) return "";
|
|
66
69
|
const meta = data.agents && data.agents[this.subscriber];
|
|
67
70
|
return (meta && meta.nickname) ? String(meta.nickname) : "";
|
|
68
71
|
} catch {
|
|
@@ -109,11 +112,18 @@ class AgentNotifier {
|
|
|
109
112
|
updateHeartbeat() {
|
|
110
113
|
try {
|
|
111
114
|
if (!this.agentsFile || !fs.existsSync(this.agentsFile)) return;
|
|
112
|
-
const data =
|
|
115
|
+
const data = readJSON(this.agentsFile, null);
|
|
116
|
+
if (!data) return;
|
|
113
117
|
if (data.agents && data.agents[this.subscriber]) {
|
|
114
118
|
data.agents[this.subscriber].last_seen = new Date().toISOString();
|
|
115
|
-
|
|
119
|
+
writeJSON(this.agentsFile, data);
|
|
120
|
+
return;
|
|
116
121
|
}
|
|
122
|
+
appendAgentRegistryDiagnostic(this.agentsFile, "heartbeat_subscriber_missing", {
|
|
123
|
+
source: "agent.notifier.updateHeartbeat",
|
|
124
|
+
subscriber: this.subscriber,
|
|
125
|
+
known_ids: Object.keys(data.agents || {}).sort(),
|
|
126
|
+
});
|
|
117
127
|
} catch {
|
|
118
128
|
// 心跳更新失败时静默忽略
|
|
119
129
|
}
|
|
@@ -132,7 +142,8 @@ class AgentNotifier {
|
|
|
132
142
|
getCurrentActivityState() {
|
|
133
143
|
try {
|
|
134
144
|
if (!this.agentsFile || !fs.existsSync(this.agentsFile)) return "";
|
|
135
|
-
const data =
|
|
145
|
+
const data = readJSON(this.agentsFile, null);
|
|
146
|
+
if (!data) return "";
|
|
136
147
|
const meta = data.agents && data.agents[this.subscriber];
|
|
137
148
|
return meta && typeof meta.activity_state === "string"
|
|
138
149
|
? String(meta.activity_state).trim().toLowerCase()
|
package/src/agent/ptyRunner.js
CHANGED
|
@@ -116,11 +116,6 @@ function computeInjectedSubmitDelayMs(agentType, text) {
|
|
|
116
116
|
return delayMs;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
function buildPrompt(text, marker) {
|
|
120
|
-
if (!marker) return text;
|
|
121
|
-
return `${text}\n\n请在完成后输出以下标记(单独一行):\n${marker}\n`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
119
|
function resolveCommand(agentType, extraArgs = []) {
|
|
125
120
|
const normalizedAgent = String(agentType || "").trim().toLowerCase();
|
|
126
121
|
const extra = Array.isArray(extraArgs) ? extraArgs : [];
|
|
@@ -172,9 +167,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
172
167
|
UFOO_INTERNAL_PTY: "1",
|
|
173
168
|
};
|
|
174
169
|
|
|
170
|
+
const idleMs = Number.parseInt(process.env.UFOO_INTERNAL_PTY_IDLE_MS || "", 10) || 30000;
|
|
175
171
|
const eventBus = new EventBus(projectRoot);
|
|
176
172
|
const activityDetector = new ActivityDetector(agentType, {
|
|
177
173
|
mode: "internal-pty",
|
|
174
|
+
quietWindowMs: idleMs,
|
|
178
175
|
});
|
|
179
176
|
const agentsFilePath = getUfooPaths(projectRoot).agentsFile;
|
|
180
177
|
|
|
@@ -184,15 +181,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
184
181
|
let ptyReady = false;
|
|
185
182
|
let readyTimer = null;
|
|
186
183
|
let currentPublisher = "";
|
|
187
|
-
let currentMarker = "";
|
|
188
184
|
let pendingOutput = [];
|
|
189
185
|
let outputBuffer = "";
|
|
190
186
|
let flushTimer = null;
|
|
191
187
|
let idleTimer = null;
|
|
192
188
|
let watchdogTimer = null;
|
|
193
|
-
let suppressEcho = false;
|
|
194
|
-
let echoMarker = "";
|
|
195
|
-
let suppressTimer = null;
|
|
196
189
|
let ptyProcess = null;
|
|
197
190
|
let restartCount = 0;
|
|
198
191
|
let lastSpawnTime = 0;
|
|
@@ -205,14 +198,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
205
198
|
const injectServer = setupInjectServer();
|
|
206
199
|
initScreenBuffer(80, 24);
|
|
207
200
|
const maxChunk = 2000;
|
|
208
|
-
const idleMs = 30000;
|
|
209
201
|
const watchdogMs = 120000;
|
|
210
202
|
const maxQueue = 200;
|
|
211
203
|
let sendQueue = Promise.resolve();
|
|
212
204
|
const streamPublisherCache = new Map();
|
|
213
205
|
const DROP_LINE_PATTERNS = [
|
|
214
|
-
/__UFOO_DONE_/,
|
|
215
|
-
/请在完成后输出以下标记/,
|
|
216
206
|
/context left/i,
|
|
217
207
|
/esc to interrupt/i,
|
|
218
208
|
/for shortcuts/i,
|
|
@@ -660,8 +650,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
660
650
|
detail: snap.detail,
|
|
661
651
|
});
|
|
662
652
|
// Quiet-window detector may classify IDLE sooner than stream fallback timer.
|
|
663
|
-
|
|
664
|
-
if (newState === "idle" && busy && !currentMarker && !suppressEcho) {
|
|
653
|
+
if (newState === "idle" && busy) {
|
|
665
654
|
if (idleTimer) {
|
|
666
655
|
clearTimeout(idleTimer);
|
|
667
656
|
idleTimer = null;
|
|
@@ -696,49 +685,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
696
685
|
if (!clean) return;
|
|
697
686
|
outputBuffer += clean;
|
|
698
687
|
activityDetector.processOutput(clean);
|
|
699
|
-
if (suppressEcho) {
|
|
700
|
-
if (echoMarker && outputBuffer.includes(echoMarker)) {
|
|
701
|
-
const idx = outputBuffer.indexOf(echoMarker);
|
|
702
|
-
outputBuffer = outputBuffer.slice(idx + echoMarker.length);
|
|
703
|
-
outputBuffer = outputBuffer.replace(/^\n+/, "");
|
|
704
|
-
suppressEcho = false;
|
|
705
|
-
currentMarker = echoMarker;
|
|
706
|
-
echoMarker = "";
|
|
707
|
-
if (suppressTimer) {
|
|
708
|
-
clearTimeout(suppressTimer);
|
|
709
|
-
suppressTimer = null;
|
|
710
|
-
}
|
|
711
|
-
} else {
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
if (currentMarker) {
|
|
716
|
-
const idx = outputBuffer.indexOf(currentMarker);
|
|
717
|
-
if (idx !== -1) {
|
|
718
|
-
const before = outputBuffer.slice(0, idx);
|
|
719
|
-
outputBuffer = "";
|
|
720
|
-
if (before) {
|
|
721
|
-
deliverChunk(before);
|
|
722
|
-
}
|
|
723
|
-
if (currentPublisher) {
|
|
724
|
-
completePublisherResponse("marker");
|
|
725
|
-
}
|
|
726
|
-
currentMarker = "";
|
|
727
|
-
busy = false;
|
|
728
|
-
activityDetector.markIdle();
|
|
729
|
-
currentPublisher = "";
|
|
730
|
-
if (watchdogTimer) {
|
|
731
|
-
clearTimeout(watchdogTimer);
|
|
732
|
-
watchdogTimer = null;
|
|
733
|
-
}
|
|
734
|
-
if (idleTimer) {
|
|
735
|
-
clearTimeout(idleTimer);
|
|
736
|
-
idleTimer = null;
|
|
737
|
-
}
|
|
738
|
-
processQueue();
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
688
|
scheduleFlush();
|
|
743
689
|
// Ready detection: during TUI startup, reset the quiet timer on each output.
|
|
744
690
|
// Once output stops for READY_QUIET_MS, the TUI is considered initialized.
|
|
@@ -805,7 +751,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
805
751
|
busy = false;
|
|
806
752
|
activityDetector.markIdle();
|
|
807
753
|
currentPublisher = "";
|
|
808
|
-
currentMarker = "";
|
|
809
754
|
|
|
810
755
|
// If stop() was called, let the runner exit
|
|
811
756
|
if (!running) return;
|
|
@@ -940,11 +885,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
940
885
|
busy = true;
|
|
941
886
|
activityDetector.markWorking();
|
|
942
887
|
currentPublisher = next.publisher;
|
|
943
|
-
currentMarker = next.marker || "";
|
|
944
|
-
if (suppressTimer) {
|
|
945
|
-
clearTimeout(suppressTimer);
|
|
946
|
-
suppressTimer = null;
|
|
947
|
-
}
|
|
948
888
|
flushPending();
|
|
949
889
|
if (next.text) {
|
|
950
890
|
if (next.raw) {
|
|
@@ -952,30 +892,16 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
952
892
|
} else {
|
|
953
893
|
// Write text first, then send Enter separately.
|
|
954
894
|
// Codex Ink TUI requires text and submit key as separate writes.
|
|
955
|
-
|
|
956
|
-
// because the prompt echo (TextInput display) contains the marker text.
|
|
957
|
-
const prompt = buildPrompt(next.text, currentMarker);
|
|
958
|
-
const savedMarker = currentMarker;
|
|
959
|
-
suppressEcho = true;
|
|
960
|
-
echoMarker = savedMarker;
|
|
961
|
-
currentMarker = ""; // Disable marker detection during prompt echo & formatted display
|
|
962
|
-
ptyProcess.write(prompt);
|
|
895
|
+
ptyProcess.write(next.text);
|
|
963
896
|
setTimeout(() => {
|
|
964
897
|
if (ptyProcess && ptyAlive) {
|
|
898
|
+
// Drop the local TUI input echo from any forwarded stream output.
|
|
965
899
|
outputBuffer = "";
|
|
966
900
|
const isClaude = agentType === "claude-code";
|
|
967
901
|
if (isClaude) {
|
|
968
902
|
// Claude Code: send CR directly without ESC.
|
|
969
903
|
// ESC before CR is interpreted as Alt+Enter (newline).
|
|
970
904
|
ptyProcess.write("\r");
|
|
971
|
-
suppressTimer = setTimeout(() => {
|
|
972
|
-
suppressTimer = null;
|
|
973
|
-
if (!suppressEcho) return;
|
|
974
|
-
suppressEcho = false;
|
|
975
|
-
echoMarker = "";
|
|
976
|
-
currentMarker = savedMarker;
|
|
977
|
-
outputBuffer = "";
|
|
978
|
-
}, 1500);
|
|
979
905
|
} else {
|
|
980
906
|
// Codex/others: ESC dismisses autocomplete, then CR submits.
|
|
981
907
|
ptyProcess.write("\x1b");
|
|
@@ -983,14 +909,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
983
909
|
if (ptyProcess && ptyAlive) {
|
|
984
910
|
ptyProcess.write("\r");
|
|
985
911
|
}
|
|
986
|
-
suppressTimer = setTimeout(() => {
|
|
987
|
-
suppressTimer = null;
|
|
988
|
-
if (!suppressEcho) return;
|
|
989
|
-
suppressEcho = false;
|
|
990
|
-
echoMarker = "";
|
|
991
|
-
currentMarker = savedMarker;
|
|
992
|
-
outputBuffer = "";
|
|
993
|
-
}, 1500);
|
|
994
912
|
}, 100);
|
|
995
913
|
}
|
|
996
914
|
}
|
|
@@ -1001,13 +919,12 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
1001
919
|
watchdogTimer = setTimeout(() => {
|
|
1002
920
|
watchdogTimer = null;
|
|
1003
921
|
if (!busy) return;
|
|
1004
|
-
const timeoutNote = `[internal-pty]
|
|
922
|
+
const timeoutNote = `[internal-pty] task timeout; restarting PTY`;
|
|
1005
923
|
if (currentPublisher) {
|
|
1006
924
|
completePublisherResponse("timeout", timeoutNote);
|
|
1007
925
|
}
|
|
1008
926
|
logNote(timeoutNote);
|
|
1009
|
-
restartPty("
|
|
1010
|
-
currentMarker = "";
|
|
927
|
+
restartPty("task timeout");
|
|
1011
928
|
busy = false;
|
|
1012
929
|
activityDetector.markIdle();
|
|
1013
930
|
currentPublisher = "";
|
|
@@ -1058,11 +975,10 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
1058
975
|
if (messageQueue.length >= maxQueue) {
|
|
1059
976
|
messageQueue.shift();
|
|
1060
977
|
}
|
|
1061
|
-
const marker = raw ? "" : `__UFOO_DONE_${Date.now()}_${Math.random().toString(16).slice(2)}__`;
|
|
1062
978
|
const publisher = typeof evt.publisher === "object" && evt.publisher
|
|
1063
979
|
? (evt.publisher.subscriber || evt.publisher.nickname || "unknown")
|
|
1064
980
|
: (evt.publisher || "unknown");
|
|
1065
|
-
messageQueue.push({ publisher, raw, text
|
|
981
|
+
messageQueue.push({ publisher, raw, text });
|
|
1066
982
|
}
|
|
1067
983
|
}
|
|
1068
984
|
processQueue();
|
package/src/bus/index.js
CHANGED
|
@@ -60,7 +60,8 @@ class EventBus {
|
|
|
60
60
|
this.queueManager = new QueueManager(this.busDir);
|
|
61
61
|
this.subscriberManager = new SubscriberManager(
|
|
62
62
|
this.busData,
|
|
63
|
-
this.queueManager
|
|
63
|
+
this.queueManager,
|
|
64
|
+
{ agentsFile: this.agentsFile }
|
|
64
65
|
);
|
|
65
66
|
this.messageManager = new MessageManager(
|
|
66
67
|
this.busDir,
|