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.
@@ -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
- createCodexThreadProvider,
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 createThreadRuntime({ projectRoot, provider, model, extraArgs = [], subscriber = "" }) {
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 = providerInstance.startThread();
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 = providerInstance.startThread();
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" || typeof resolveClaudeUpstreamCredentials !== "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
- authProvider: buildClaudeAuthProvider(projectRoot),
493
- streamFactory: defaultClaudeTransportStreamFactory,
606
+ cwd: projectRoot,
607
+ extraArgs,
494
608
  });
495
- let thread = providerInstance.startThread();
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
- authProvider: buildClaudeAuthProvider(projectRoot),
509
- streamFactory: defaultClaudeTransportStreamFactory,
627
+ cwd: projectRoot,
628
+ extraArgs,
510
629
  });
511
- thread = providerInstance.startThread();
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
- setActivityState("idle");
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
  };
@@ -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 = JSON.parse(fs.readFileSync(this.agentsFile, "utf8"));
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 = JSON.parse(fs.readFileSync(this.agentsFile, "utf8"));
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
- fs.writeFileSync(this.agentsFile, JSON.stringify(data, null, 2));
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 = JSON.parse(fs.readFileSync(this.agentsFile, "utf8"));
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()
@@ -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
- // Release queue only when no explicit marker is being awaited.
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
- // IMPORTANT: Defer marker detection until after Enter is sent,
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] marker timeout; restarting 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("marker timeout");
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, marker });
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,