u-foo 2.3.9 → 2.3.11

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.3.9",
3
+ "version": "2.3.11",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -45,6 +45,8 @@
45
45
  "bench:global-switch": "node scripts/global-chat-switch-benchmark.js"
46
46
  },
47
47
  "dependencies": {
48
+ "@anthropic-ai/claude-agent-sdk": "^0.2.138",
49
+ "@openai/codex-sdk": "^0.130.0",
48
50
  "blessed": "^0.1.81",
49
51
  "chalk": "^4.1.2",
50
52
  "commander": "^13.1.0",
@@ -3,9 +3,10 @@
3
3
  const {
4
4
  createClaudeEventState,
5
5
  normalizeClaudeEvent,
6
+ normalizeClaudeMessage,
7
+ normalizeClaudeUsage,
6
8
  } = require("./claudeEventTranslator");
7
9
  const { redactUfooEvent } = require("../providerapi/redactor");
8
- const { sendUpstreamRequest } = require("./upstreamTransport");
9
10
 
10
11
  const CACHE_CONTROL = Object.freeze({ type: "ephemeral" });
11
12
 
@@ -26,6 +27,25 @@ function resolveAnthropicSdk() {
26
27
  }
27
28
  }
28
29
 
30
+ async function resolveClaudeAgentSdk() {
31
+ try {
32
+ return await import("@anthropic-ai/claude-agent-sdk");
33
+ } catch (err) {
34
+ const error = new Error("Claude Agent SDK mode requires @anthropic-ai/claude-agent-sdk");
35
+ error.code = "CLAUDE_AGENT_SDK_UNAVAILABLE";
36
+ error.cause = err;
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ function resolveClaudeAgentQuery(sdk) {
42
+ const query = sdk && (sdk.query || (sdk.default && sdk.default.query));
43
+ if (typeof query !== "function") {
44
+ throw new Error("Claude Agent SDK module does not export query");
45
+ }
46
+ return query;
47
+ }
48
+
29
49
  async function defaultClaudeAuthProvider() {
30
50
  const apiKey = String(process.env.ANTHROPIC_API_KEY || "").trim();
31
51
  if (!apiKey) {
@@ -61,43 +81,12 @@ function defaultClaudeStreamFactory({ client, request }) {
61
81
  });
62
82
  }
63
83
 
64
- async function* defaultClaudeTransportStreamFactory({ request, auth = {}, model = "", attempt = 0 }) {
65
- const runtime = {
66
- provider: "claude",
67
- transport: "anthropic-messages",
68
- model: String(model || request.model || "").trim(),
69
- baseUrl: String(auth.baseUrl || process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com/v1").trim(),
70
- auth,
71
- credentialSource: auth.apiKey ? "thread-auth" : "thread-headers",
72
- };
73
- const result = await sendUpstreamRequest({
74
- runtime,
75
- request,
76
- timeoutMs: Number.isFinite(Number(request.timeout_ms)) ? Number(request.timeout_ms) : 120000,
84
+ function defaultClaudeAgentStreamFactory({ sdk, input, options }) {
85
+ const query = resolveClaudeAgentQuery(sdk);
86
+ return query({
87
+ prompt: String(input || ""),
88
+ options,
77
89
  });
78
- if (!result.ok) {
79
- const err = new Error(result.error || "Claude upstream request failed");
80
- err.code = "CLAUDE_UPSTREAM_FAILED";
81
- err.attempt = attempt;
82
- throw err;
83
- }
84
-
85
- yield {
86
- type: "message_start",
87
- message: {
88
- id: `msg-${Date.now().toString(36)}`,
89
- usage: result.usage || undefined,
90
- },
91
- };
92
- yield {
93
- type: "content_block_delta",
94
- index: 0,
95
- delta: {
96
- type: "text_delta",
97
- text: String(result.output || ""),
98
- },
99
- };
100
- yield { type: "message_stop" };
101
90
  }
102
91
 
103
92
  function normalizeToolDefinition(tool = {}) {
@@ -217,6 +206,74 @@ function createAssistantMessageFromState(state) {
217
206
  };
218
207
  }
219
208
 
209
+ function extraArgsToObject(extraArgs = []) {
210
+ const result = {};
211
+ if (!Array.isArray(extraArgs)) return result;
212
+ for (let i = 0; i < extraArgs.length; i += 1) {
213
+ const raw = String(extraArgs[i] || "");
214
+ if (!raw.startsWith("--")) continue;
215
+ const key = raw.replace(/^--+/, "");
216
+ if (!key) continue;
217
+ const next = i + 1 < extraArgs.length ? String(extraArgs[i + 1] || "") : "";
218
+ if (!next || next.startsWith("--")) {
219
+ result[key] = null;
220
+ continue;
221
+ }
222
+ result[key] = next;
223
+ i += 1;
224
+ }
225
+ return result;
226
+ }
227
+
228
+ function buildClaudeAgentOptions({
229
+ model = "",
230
+ cwd = "",
231
+ threadId = "",
232
+ extraArgs = [],
233
+ agentOptions = {},
234
+ opts = {},
235
+ } = {}) {
236
+ const options = {
237
+ ...agentOptions,
238
+ ...(opts && typeof opts.agentOptions === "object" ? opts.agentOptions : {}),
239
+ };
240
+ if (model && options.model === undefined) options.model = model;
241
+ if (cwd && options.cwd === undefined) options.cwd = cwd;
242
+ if (options.includePartialMessages === undefined) options.includePartialMessages = true;
243
+ if (options.extraArgs === undefined) {
244
+ const converted = extraArgsToObject(extraArgs);
245
+ if (Object.keys(converted).length > 0) options.extraArgs = converted;
246
+ }
247
+ if (threadId && !options.resume && !options.continue && !options.sessionId) {
248
+ options.resume = threadId;
249
+ }
250
+ if (opts && opts.abortController && options.abortController === undefined) {
251
+ options.abortController = opts.abortController;
252
+ }
253
+ if (opts && opts.signal && options.abortController === undefined) {
254
+ const controller = new AbortController();
255
+ opts.signal.addEventListener("abort", () => controller.abort(opts.signal.reason), { once: true });
256
+ options.abortController = controller;
257
+ }
258
+ return options;
259
+ }
260
+
261
+ function messageTextFromContent(content = []) {
262
+ if (!Array.isArray(content)) return "";
263
+ return content
264
+ .map((block) => (block && block.type === "text" ? String(block.text || "") : ""))
265
+ .join("");
266
+ }
267
+
268
+ function resultErrorMessage(message = {}) {
269
+ if (Array.isArray(message.errors) && message.errors.length > 0) {
270
+ return message.errors.map((item) => String(item || "")).filter(Boolean).join("\n");
271
+ }
272
+ if (typeof message.result === "string" && message.result) return message.result;
273
+ if (message.subtype) return String(message.subtype);
274
+ return "Claude Agent SDK query failed";
275
+ }
276
+
220
277
  function shouldRetryClaudeStream(err, attempt) {
221
278
  if (attempt >= 1) return false;
222
279
  const code = String((err && err.code) || "").trim().toUpperCase();
@@ -228,23 +285,132 @@ function shouldRetryClaudeStream(err, attempt) {
228
285
  class ClaudeApiThread {
229
286
  constructor({
230
287
  model = "",
288
+ cwd = "",
289
+ extraArgs = [],
231
290
  authProvider = defaultClaudeAuthProvider,
232
291
  clientFactory = defaultClaudeClientFactory,
233
- streamFactory = defaultClaudeStreamFactory,
292
+ streamFactory = defaultClaudeAgentStreamFactory,
234
293
  sdk,
294
+ agentOptions = {},
235
295
  maxTokens = 4096,
236
296
  } = {}) {
237
297
  this.id = "";
238
298
  this.model = model;
299
+ this.cwd = cwd;
300
+ this.extraArgs = Array.isArray(extraArgs) ? extraArgs.slice() : [];
239
301
  this.authProvider = authProvider;
240
302
  this.clientFactory = clientFactory;
241
303
  this.streamFactory = streamFactory;
242
304
  this.sdk = sdk;
305
+ this.agentOptions = { ...agentOptions };
243
306
  this.maxTokens = maxTokens;
244
307
  this.messages = [];
245
308
  }
246
309
 
247
310
  async *runStreamed(input, opts = {}) {
311
+ if (this.streamFactory === defaultClaudeAgentStreamFactory) {
312
+ yield* this.runAgentSdkStreamed(input, opts);
313
+ return;
314
+ }
315
+ yield* this.runMessagesStreamed(input, opts);
316
+ }
317
+
318
+ async *runAgentSdkStreamed(input, opts = {}) {
319
+ if (!this.sdk) {
320
+ this.sdk = await resolveClaudeAgentSdk();
321
+ }
322
+ const options = buildClaudeAgentOptions({
323
+ model: this.model,
324
+ cwd: this.cwd,
325
+ threadId: this.id,
326
+ extraArgs: this.extraArgs,
327
+ agentOptions: this.agentOptions,
328
+ opts,
329
+ });
330
+ const stream = await this.streamFactory({
331
+ sdk: this.sdk,
332
+ input,
333
+ options,
334
+ model: this.model,
335
+ cwd: this.cwd,
336
+ threadId: this.id,
337
+ opts,
338
+ });
339
+ const state = createClaudeEventState({ threadId: this.id });
340
+ let threadStarted = false;
341
+ let sawStreamEvents = false;
342
+ let sawText = false;
343
+ let sawTurnCompleted = false;
344
+
345
+ const emitThreadStarted = function* emitThreadStarted(self, sessionId = "") {
346
+ const nextId = String(sessionId || self.id || "").trim();
347
+ if (nextId) self.id = nextId;
348
+ if (!threadStarted && self.id) {
349
+ threadStarted = true;
350
+ yield redactUfooEvent({ type: "thread_started", threadId: self.id });
351
+ }
352
+ };
353
+
354
+ for await (const message of stream) {
355
+ if (!message || typeof message !== "object") continue;
356
+ const sessionId = String(message.session_id || message.sessionId || "").trim();
357
+ for (const event of emitThreadStarted(this, sessionId)) yield event;
358
+
359
+ if (message.type === "stream_event") {
360
+ sawStreamEvents = true;
361
+ const events = normalizeClaudeEvent(message.event || {}, state);
362
+ for (const event of events) {
363
+ if (!event || typeof event !== "object") continue;
364
+ if (event.type === "text_delta" && event.delta) sawText = true;
365
+ if (event.type === "turn_completed") sawTurnCompleted = true;
366
+ yield redactUfooEvent(event);
367
+ }
368
+ continue;
369
+ }
370
+
371
+ if (message.type === "assistant") {
372
+ if (sawStreamEvents) continue;
373
+ const events = normalizeClaudeMessage(message.message || {});
374
+ for (const event of events) {
375
+ if (!event || typeof event !== "object") continue;
376
+ if (event.type === "text_delta" && event.delta) sawText = true;
377
+ if (event.type === "turn_completed") sawTurnCompleted = true;
378
+ yield redactUfooEvent(event);
379
+ }
380
+ continue;
381
+ }
382
+
383
+ if (message.type === "result") {
384
+ if (message.is_error) {
385
+ yield redactUfooEvent({
386
+ type: "turn_failed",
387
+ turnId: state.turnId || message.uuid || "",
388
+ error: resultErrorMessage(message),
389
+ });
390
+ continue;
391
+ }
392
+ if (!sawText && typeof message.result === "string" && message.result) {
393
+ sawText = true;
394
+ yield redactUfooEvent({
395
+ type: "text_delta",
396
+ delta: message.result,
397
+ itemType: "text",
398
+ });
399
+ }
400
+ if (!sawTurnCompleted) {
401
+ sawTurnCompleted = true;
402
+ yield redactUfooEvent({
403
+ type: "turn_completed",
404
+ turnId: state.turnId || message.uuid || "",
405
+ usage: normalizeClaudeUsage(message.usage || null),
406
+ stopReason: String(message.stop_reason || ""),
407
+ });
408
+ }
409
+ }
410
+ }
411
+ }
412
+
413
+ async *runMessagesStreamed(input, opts = {}) {
248
414
  if (!this.id) this.id = createThreadId();
249
415
  const userMessage = normalizeMessageInput(input);
250
416
  const requestMessages = this.messages.slice();
@@ -332,12 +498,15 @@ module.exports = {
332
498
  ClaudeApiThread,
333
499
  ClaudeThreadProvider,
334
500
  createClaudeThreadProvider,
501
+ buildClaudeAgentOptions,
335
502
  defaultClaudeAuthProvider,
503
+ defaultClaudeAgentStreamFactory,
336
504
  defaultClaudeClientFactory,
337
505
  defaultClaudeStreamFactory,
338
- defaultClaudeTransportStreamFactory,
506
+ extraArgsToObject,
339
507
  normalizeMessageInput,
340
508
  normalizeToolDefinition,
509
+ resolveClaudeAgentSdk,
341
510
  resolveAnthropicSdk,
342
511
  withCacheControlOnLastBlock,
343
512
  };
@@ -1,80 +1,79 @@
1
1
  const { normalizeCodexEvent } = require("./codexEventTranslator");
2
2
  const { redactUfooEvent } = require("../providerapi/redactor");
3
- const { sendUpstreamPrompt } = require("./upstreamTransport");
4
3
 
5
- function resolveCodexSdk() {
4
+ async function resolveCodexSdk() {
6
5
  try {
7
- // Optional dependency during Phase 1a seam work.
8
- // eslint-disable-next-line global-require, import/no-extraneous-dependencies
9
- return require("@openai/codex-sdk");
6
+ return await import("@openai/codex-sdk");
10
7
  } catch (err) {
11
- const error = new Error("Codex SDK seam enabled but @openai/codex-sdk is not installed");
8
+ const error = new Error("Codex SDK mode requires @openai/codex-sdk");
12
9
  error.code = "CODEX_SDK_UNAVAILABLE";
13
10
  error.cause = err;
14
11
  throw error;
15
12
  }
16
13
  }
17
14
 
18
- function defaultCodexStreamFactory({
19
- sdk,
20
- model,
21
- cwd,
22
- extraArgs = [],
23
- threadId = "",
24
- input,
25
- opts = {},
26
- }) {
27
- if (!sdk || typeof sdk.runStreamed !== "function") {
28
- throw new Error("Codex SDK seam requires runStreamed support");
15
+ function resolveCodexConstructor(sdk) {
16
+ const Codex = sdk && (sdk.Codex || (sdk.default && sdk.default.Codex) || sdk.default);
17
+ if (typeof Codex !== "function") {
18
+ throw new Error("Codex SDK module does not export Codex");
29
19
  }
30
- const { history, ...sdkOpts } = opts;
31
- void history;
32
- return sdk.runStreamed({
33
- model,
34
- cwd,
35
- extraArgs,
36
- threadId,
37
- input,
38
- ...sdkOpts,
39
- });
20
+ return Codex;
40
21
  }
41
22
 
42
- function createThreadId() {
43
- return `codex-thread-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
23
+ function buildCodexOptions({ codexOptions = {} } = {}) {
24
+ return { ...codexOptions };
44
25
  }
45
26
 
46
- async function* defaultCodexTransportStreamFactory({
47
- model,
48
- cwd,
49
- threadId = "",
27
+ function buildThreadOptions({
28
+ model = "",
29
+ cwd = "",
30
+ threadOptions = {},
31
+ } = {}) {
32
+ const options = { ...threadOptions };
33
+ if (model && options.model === undefined) {
34
+ options.model = model;
35
+ }
36
+ if (cwd && options.workingDirectory === undefined) {
37
+ options.workingDirectory = cwd;
38
+ }
39
+ if (options.skipGitRepoCheck === undefined) {
40
+ options.skipGitRepoCheck = true;
41
+ }
42
+ if (options.sandboxMode === undefined) {
43
+ options.sandboxMode = "workspace-write";
44
+ }
45
+ return options;
46
+ }
47
+
48
+ function buildTurnOptions(opts = {}) {
49
+ const {
50
+ history,
51
+ tools,
52
+ timeoutMs,
53
+ ...turnOptions
54
+ } = opts || {};
55
+ void history;
56
+ void tools;
57
+ void timeoutMs;
58
+ return turnOptions;
59
+ }
60
+
61
+ async function* defaultCodexStreamFactory({
62
+ thread,
50
63
  input,
51
64
  opts = {},
52
65
  }) {
53
- const nextThreadId = String(threadId || "").trim() || createThreadId();
54
- const result = await sendUpstreamPrompt({
55
- projectRoot: cwd,
56
- provider: "codex",
57
- model,
58
- prompt: String(input || ""),
59
- messages: Array.isArray(opts.history) ? opts.history : [],
60
- timeoutMs: Number.isFinite(Number(opts.timeoutMs)) ? Number(opts.timeoutMs) : 120000,
61
- });
62
- if (!result.ok) {
63
- const err = new Error(result.error || "Codex upstream request failed");
64
- err.code = result.errorCode || "CODEX_UPSTREAM_FAILED";
65
- throw err;
66
- }
67
-
68
- yield { type: "thread.started", thread_id: nextThreadId };
69
- yield {
70
- type: "item.completed",
71
- item: { type: "message", text: String(result.output || "") },
72
- };
73
- yield {
74
- type: "turn.completed",
75
- turn_id: `turn-${Date.now().toString(36)}`,
76
- usage: result.usage || null,
77
- };
66
+ if (!thread || typeof thread.runStreamed !== "function") {
67
+ throw new Error("Codex SDK thread requires runStreamed support");
68
+ }
69
+ const streamed = await thread.runStreamed(String(input || ""), buildTurnOptions(opts));
70
+ const events = streamed && streamed.events ? streamed.events : streamed;
71
+ if (!events || typeof events[Symbol.asyncIterator] !== "function") {
72
+ throw new Error("Codex SDK runStreamed did not return an async event stream");
73
+ }
74
+ for await (const event of events) {
75
+ yield event;
76
+ }
78
77
  }
79
78
 
80
79
  class CodexSdkThread {
@@ -84,26 +83,67 @@ class CodexSdkThread {
84
83
  extraArgs = [],
85
84
  streamFactory = defaultCodexStreamFactory,
86
85
  sdk,
86
+ codexClient,
87
+ sdkThread,
88
+ threadId = "",
87
89
  tools = [],
90
+ codexOptions = {},
91
+ threadOptions = {},
88
92
  } = {}) {
89
- this.id = "";
93
+ this.id = String(threadId || "").trim();
90
94
  this.model = model;
91
95
  this.cwd = cwd;
92
96
  this.extraArgs = Array.isArray(extraArgs) ? extraArgs.slice() : [];
93
97
  this.streamFactory = streamFactory;
94
98
  this.sdk = sdk;
99
+ this.codexClient = codexClient || null;
100
+ this.sdkThread = sdkThread || null;
95
101
  this.tools = Array.isArray(tools) ? tools.slice() : [];
102
+ this.codexOptions = buildCodexOptions({ codexOptions });
103
+ this.threadOptions = { ...threadOptions };
96
104
  this.messages = [];
97
105
  }
98
106
 
107
+ async getCodexClient() {
108
+ if (this.codexClient) return this.codexClient;
109
+ if (!this.sdk) {
110
+ this.sdk = await resolveCodexSdk();
111
+ }
112
+ const Codex = resolveCodexConstructor(this.sdk);
113
+ this.codexClient = new Codex(this.codexOptions);
114
+ return this.codexClient;
115
+ }
116
+
117
+ async getSdkThread() {
118
+ if (this.sdkThread) return this.sdkThread;
119
+ const client = await this.getCodexClient();
120
+ const options = buildThreadOptions({
121
+ model: this.model,
122
+ cwd: this.cwd,
123
+ threadOptions: this.threadOptions,
124
+ });
125
+ this.sdkThread = this.id && typeof client.resumeThread === "function"
126
+ ? client.resumeThread(this.id, options)
127
+ : client.startThread(options);
128
+ if (this.sdkThread && this.sdkThread.id) {
129
+ this.id = this.sdkThread.id;
130
+ }
131
+ return this.sdkThread;
132
+ }
133
+
99
134
  async *runStreamed(input, opts = {}) {
100
135
  const mergedOpts = { ...opts };
101
136
  if (this.tools.length > 0 && !Array.isArray(mergedOpts.tools)) {
102
137
  mergedOpts.tools = this.tools.slice();
103
138
  }
104
139
  mergedOpts.history = this.messages.slice();
140
+ const sdkThread = this.streamFactory === defaultCodexStreamFactory
141
+ ? await this.getSdkThread()
142
+ : this.sdkThread;
105
143
  const stream = this.streamFactory({
106
144
  sdk: this.sdk,
145
+ thread: sdkThread,
146
+ codexClient: this.codexClient,
107
147
  model: this.model,
108
148
  cwd: this.cwd,
109
149
  extraArgs: this.extraArgs,
@@ -123,6 +163,9 @@ class CodexSdkThread {
123
163
  }
124
164
  yield redactUfooEvent(normalized);
125
165
  }
166
+ if (sdkThread && sdkThread.id) {
167
+ this.id = sdkThread.id;
168
+ }
126
169
  this.messages.push({ role: "user", content: String(input || "") });
127
170
  this.messages.push({ role: "assistant", content: outputText });
128
171
  }
@@ -139,14 +182,20 @@ class CodexThreadProvider {
139
182
  extraArgs = [],
140
183
  streamFactory = defaultCodexStreamFactory,
141
184
  sdk,
185
+ codexClient,
142
186
  tools = [],
187
+ codexOptions = {},
188
+ threadOptions = {},
143
189
  } = {}) {
144
190
  this.model = model;
145
191
  this.cwd = cwd;
146
192
  this.extraArgs = Array.isArray(extraArgs) ? extraArgs.slice() : [];
147
193
  this.streamFactory = streamFactory;
148
- this.sdk = sdk || (streamFactory === defaultCodexStreamFactory ? resolveCodexSdk() : null);
194
+ this.sdk = sdk || null;
195
+ this.codexClient = codexClient || null;
149
196
  this.tools = Array.isArray(tools) ? tools.slice() : [];
197
+ this.codexOptions = buildCodexOptions({ codexOptions });
198
+ this.threadOptions = { ...threadOptions };
150
199
  }
151
200
 
152
201
  startThread() {
@@ -156,14 +205,26 @@ class CodexThreadProvider {
156
205
  extraArgs: this.extraArgs,
157
206
  streamFactory: this.streamFactory,
158
207
  sdk: this.sdk,
208
+ codexClient: this.codexClient,
159
209
  tools: this.tools,
210
+ codexOptions: this.codexOptions,
211
+ threadOptions: this.threadOptions,
160
212
  });
161
213
  }
162
214
 
163
215
  resumeThread(threadId = "") {
164
- const thread = this.startThread();
165
- thread.id = String(threadId || "").trim();
166
- return thread;
216
+ return new CodexSdkThread({
217
+ model: this.model,
218
+ cwd: this.cwd,
219
+ extraArgs: this.extraArgs,
220
+ streamFactory: this.streamFactory,
221
+ sdk: this.sdk,
222
+ codexClient: this.codexClient,
223
+ threadId,
224
+ tools: this.tools,
225
+ codexOptions: this.codexOptions,
226
+ threadOptions: this.threadOptions,
227
+ });
167
228
  }
168
229
  }
169
230
 
@@ -175,7 +236,7 @@ module.exports = {
175
236
  CodexSdkThread,
176
237
  CodexThreadProvider,
177
238
  createCodexThreadProvider,
239
+ buildThreadOptions,
178
240
  defaultCodexStreamFactory,
179
- defaultCodexTransportStreamFactory,
180
241
  resolveCodexSdk,
181
242
  };
@@ -7,14 +7,8 @@ const { runCliAgent } = require("./cliRunner");
7
7
  const { normalizeCliOutput } = require("./normalizeOutput");
8
8
  const { createActivityStatePublisher } = require("./activityStatePublisher");
9
9
  const { loadConfig, normalizeCodexInternalThreadMode } = require("../config");
10
- const {
11
- createCodexThreadProvider,
12
- defaultCodexTransportStreamFactory,
13
- } = require("./codexThreadProvider");
14
- const {
15
- createClaudeThreadProvider,
16
- defaultClaudeTransportStreamFactory,
17
- } = require("./claudeThreadProvider");
10
+ const { createCodexThreadProvider } = require("./codexThreadProvider");
11
+ const { createClaudeThreadProvider } = require("./claudeThreadProvider");
18
12
  const { resolveClaudeUpstreamCredentials } = require("./credentials/claude");
19
13
  const { buildUpstreamAuthFromCredential } = require("./credentials");
20
14
  const { listToolsForCallerTier, CALLER_TIERS } = require("../tools");
@@ -131,6 +125,73 @@ function drainQueue(queueFile) {
131
125
  return content.split(/\r?\n/).filter(Boolean);
132
126
  }
133
127
 
128
+ function parseAgentViewRawInput(message) {
129
+ if (typeof message !== "string" || !message.trim()) return null;
130
+ try {
131
+ const parsed = JSON.parse(message);
132
+ if (parsed && parsed.raw === true && typeof parsed.data === "string") {
133
+ return parsed.data;
134
+ }
135
+ } catch {
136
+ // Not a raw agent-view envelope.
137
+ }
138
+ return null;
139
+ }
140
+
141
+ function createInteractiveInputSession({ write = () => {} } = {}) {
142
+ let buffer = "";
143
+
144
+ function writePrompt() {
145
+ write("> ");
146
+ }
147
+
148
+ function handleRaw(data) {
149
+ const submissions = [];
150
+ const text = String(data || "");
151
+ for (const char of text) {
152
+ if (char === "\r" || char === "\n") {
153
+ const submitted = buffer.trim();
154
+ buffer = "";
155
+ write("\r\n");
156
+ if (submitted) submissions.push(submitted);
157
+ else writePrompt();
158
+ continue;
159
+ }
160
+
161
+ if (char === "\u0003") {
162
+ buffer = "";
163
+ write("^C\r\n");
164
+ writePrompt();
165
+ continue;
166
+ }
167
+
168
+ if (char === "\u007f" || char === "\b") {
169
+ if (buffer.length > 0) {
170
+ buffer = buffer.slice(0, -1);
171
+ write("\b \b");
172
+ }
173
+ continue;
174
+ }
175
+
176
+ if (char >= " " && char !== "\u007f") {
177
+ buffer += char;
178
+ write(char);
179
+ }
180
+ }
181
+ return submissions;
182
+ }
183
+
184
+ return {
185
+ handleRaw,
186
+ writePrompt,
187
+ writeResponsePrompt: () => {
188
+ write("\r\n");
189
+ writePrompt();
190
+ },
191
+ getBuffer: () => buffer,
192
+ };
193
+ }
194
+
134
195
  async function handleEvent(
135
196
  projectRoot,
136
197
  agentType,
@@ -259,6 +320,9 @@ async function handleThreadedEvent({
259
320
  const plainReplyParts = [];
260
321
  for await (const event of threadRuntime.thread.runStreamed(prompt, {})) {
261
322
  if (!event || typeof event !== "object") continue;
323
+ if (typeof threadRuntime.syncProviderSessionId === "function") {
324
+ threadRuntime.syncProviderSessionId();
325
+ }
262
326
  if (event.type === "text_delta" && event.delta) {
263
327
  if (streamToPublisher) {
264
328
  emitStreamDelta(event.delta);
@@ -269,6 +333,9 @@ async function handleThreadedEvent({
269
333
  throw new Error(event.error || `thread turn failed for ${agentType}`);
270
334
  }
271
335
  }
336
+ if (typeof threadRuntime.syncProviderSessionId === "function") {
337
+ threadRuntime.syncProviderSessionId();
338
+ }
272
339
 
273
340
  if (streamToPublisher) {
274
341
  busSender.enqueue(
@@ -420,15 +487,47 @@ function buildClaudeAuthProvider(projectRoot) {
420
487
  };
421
488
  }
422
489
 
423
- function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], subscriber = "" }) {
490
+ function persistProviderSessionId(projectRoot, subscriber, providerSessionId) {
491
+ const id = String(providerSessionId || "").trim();
492
+ if (!projectRoot || !subscriber || !id) return false;
493
+ try {
494
+ const agentsFile = getUfooPaths(projectRoot).agentsFile;
495
+ const parsed = fs.existsSync(agentsFile)
496
+ ? JSON.parse(fs.readFileSync(agentsFile, "utf8"))
497
+ : {};
498
+ if (!parsed.agents || typeof parsed.agents !== "object") return false;
499
+ if (!parsed.agents[subscriber] || typeof parsed.agents[subscriber] !== "object") return false;
500
+ if (parsed.agents[subscriber].provider_session_id === id) return false;
501
+ parsed.agents[subscriber].provider_session_id = id;
502
+ parsed.agents[subscriber].provider_session_updated_at = new Date().toISOString();
503
+ fs.writeFileSync(agentsFile, `${JSON.stringify(parsed, null, 2)}\n`);
504
+ return true;
505
+ } catch {
506
+ return false;
507
+ }
508
+ }
509
+
510
+ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], subscriber = "", providerSessionId = "" }) {
424
511
  const disabledRuntime = {
425
512
  enabled: false,
426
513
  thread: null,
427
514
  toolRuntime: { enabled: false, mode: "disabled", tools: [] },
428
515
  close: async () => {},
429
516
  rebuildThread: async () => {},
517
+ syncProviderSessionId: () => false,
430
518
  };
431
519
 
520
+ const initialProviderSessionId = String(providerSessionId || "").trim();
521
+ let savedProviderSessionId = initialProviderSessionId;
522
+
523
+ function rememberProviderSessionId(thread) {
524
+ const id = String(thread && thread.id ? thread.id : "").trim();
525
+ if (!id || id === savedProviderSessionId) return false;
526
+ const changed = persistProviderSessionId(projectRoot, subscriber, id);
527
+ if (changed) savedProviderSessionId = id;
528
+ return changed;
529
+ }
530
+
432
531
  if (provider === "codex-cli") {
433
532
  if (getCodexThreadMode(projectRoot) !== "api") {
434
533
  return disabledRuntime;
@@ -444,9 +543,10 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
444
543
  cwd: projectRoot,
445
544
  extraArgs,
446
545
  tools: toolRuntime.tools,
447
- streamFactory: defaultCodexTransportStreamFactory,
448
546
  });
449
- let thread = providerInstance.startThread();
547
+ let thread = initialProviderSessionId
548
+ ? providerInstance.resumeThread(initialProviderSessionId)
549
+ : providerInstance.startThread();
450
550
 
451
551
  return {
452
552
  enabled: true,
@@ -454,6 +554,9 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
454
554
  get thread() {
455
555
  return thread;
456
556
  },
557
+ syncProviderSessionId() {
558
+ return rememberProviderSessionId(thread);
559
+ },
457
560
  async rebuildThread() {
458
561
  if (thread && typeof thread.close === "function") {
459
562
  await thread.close();
@@ -463,9 +566,10 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
463
566
  cwd: projectRoot,
464
567
  extraArgs,
465
568
  tools: toolRuntime.tools,
466
- streamFactory: defaultCodexTransportStreamFactory,
467
569
  });
468
- thread = providerInstance.startThread();
570
+ thread = savedProviderSessionId
571
+ ? providerInstance.resumeThread(savedProviderSessionId)
572
+ : providerInstance.startThread();
469
573
  },
470
574
  async close() {
471
575
  if (thread && typeof thread.close === "function") {
@@ -482,33 +586,40 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
482
586
  if (getClaudeThreadMode() !== "api") {
483
587
  return disabledRuntime;
484
588
  }
485
- if (typeof createClaudeThreadProvider !== "function" || typeof resolveClaudeUpstreamCredentials !== "function") {
589
+ if (typeof createClaudeThreadProvider !== "function") {
486
590
  return disabledRuntime;
487
591
  }
488
592
 
489
593
  try {
490
594
  let providerInstance = createClaudeThreadProvider({
491
595
  model,
492
- authProvider: buildClaudeAuthProvider(projectRoot),
493
- streamFactory: defaultClaudeTransportStreamFactory,
596
+ cwd: projectRoot,
597
+ extraArgs,
494
598
  });
495
- let thread = providerInstance.startThread();
599
+ let thread = initialProviderSessionId
600
+ ? providerInstance.resumeThread(initialProviderSessionId)
601
+ : providerInstance.startThread();
496
602
 
497
603
  return {
498
604
  enabled: true,
499
605
  get thread() {
500
606
  return thread;
501
607
  },
608
+ syncProviderSessionId() {
609
+ return rememberProviderSessionId(thread);
610
+ },
502
611
  async rebuildThread() {
503
612
  if (thread && typeof thread.close === "function") {
504
613
  await thread.close();
505
614
  }
506
615
  providerInstance = createClaudeThreadProvider({
507
616
  model,
508
- authProvider: buildClaudeAuthProvider(projectRoot),
509
- streamFactory: defaultClaudeTransportStreamFactory,
617
+ cwd: projectRoot,
618
+ extraArgs,
510
619
  });
511
- thread = providerInstance.startThread();
620
+ thread = savedProviderSessionId
621
+ ? providerInstance.resumeThread(savedProviderSessionId)
622
+ : providerInstance.startThread();
512
623
  },
513
624
  async close() {
514
625
  if (thread && typeof thread.close === "function") {
@@ -538,12 +649,14 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
538
649
  const provider = normalizedAgentType === "codex" ? "codex-cli" : "claude-cli";
539
650
  const model = process.env.UFOO_AGENT_MODEL || "";
540
651
  const busSender = createBusSender(projectRoot, subscriber);
652
+ const interactiveSessions = new Map();
541
653
  const threadRuntime = createThreadRuntime({
542
654
  projectRoot,
543
655
  provider,
544
656
  model,
545
657
  extraArgs,
546
658
  subscriber,
659
+ providerSessionId: process.env.UFOO_PROVIDER_SESSION_ID || "",
547
660
  });
548
661
 
549
662
  // Session state management for CLI continuity
@@ -586,6 +699,18 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
586
699
  activityPublisher.publish(state);
587
700
  }
588
701
 
702
+ function getInteractiveSession(publisher) {
703
+ const key = String(publisher || "unknown");
704
+ if (interactiveSessions.has(key)) return interactiveSessions.get(key);
705
+ const session = createInteractiveInputSession({
706
+ write: (delta) => {
707
+ busSender.enqueue(key, JSON.stringify({ stream: true, delta: String(delta || "") }));
708
+ },
709
+ });
710
+ interactiveSessions.set(key, session);
711
+ return session;
712
+ }
713
+
589
714
  setActivityState("ready");
590
715
 
591
716
  // 心跳更新函数
@@ -615,7 +740,6 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
615
740
  try {
616
741
  const lines = drainQueue(queueFile);
617
742
  if (lines.length > 0) {
618
- setActivityState("working");
619
743
  const events = [];
620
744
  for (const line of lines) {
621
745
  try {
@@ -625,7 +749,33 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
625
749
  }
626
750
  }
627
751
 
752
+ const runnableEvents = [];
628
753
  for (const evt of events) {
754
+ const rawInput = parseAgentViewRawInput(evt && evt.data ? evt.data.message : "");
755
+ if (rawInput === null) {
756
+ runnableEvents.push(evt);
757
+ continue;
758
+ }
759
+
760
+ const session = getInteractiveSession(evt.publisher || "unknown");
761
+ const submissions = session.handleRaw(rawInput);
762
+ for (const message of submissions) {
763
+ runnableEvents.push({
764
+ ...evt,
765
+ __agentViewRaw: true,
766
+ data: {
767
+ ...(evt.data || {}),
768
+ message,
769
+ },
770
+ });
771
+ }
772
+ }
773
+
774
+ if (runnableEvents.length > 0) {
775
+ setActivityState("working");
776
+ }
777
+
778
+ for (const evt of runnableEvents) {
629
779
  // eslint-disable-next-line no-await-in-loop
630
780
  await handleEvent(
631
781
  projectRoot,
@@ -640,6 +790,9 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
640
790
  extraArgs,
641
791
  threadRuntime
642
792
  );
793
+ if (evt.__agentViewRaw) {
794
+ getInteractiveSession(evt.publisher || "unknown").writeResponsePrompt();
795
+ }
643
796
  }
644
797
 
645
798
  // Persist CLI session state after processing (only if changed and for claude)
@@ -659,7 +812,10 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
659
812
  // 处理消息后更新心跳
660
813
  updateHeartbeat();
661
814
  lastHeartbeat = now;
662
- setActivityState("idle");
815
+ if (runnableEvents.length > 0) {
816
+ setActivityState("idle");
817
+ }
818
+ await busSender.flush();
663
819
  }
664
820
  } finally {
665
821
  processing = false;
@@ -684,4 +840,7 @@ module.exports = {
684
840
  getClaudeThreadMode,
685
841
  buildClaudeAuthProvider,
686
842
  shouldFallbackToLegacyThreadProvider,
843
+ parseAgentViewRawInput,
844
+ createInteractiveInputSession,
845
+ persistProviderSessionId,
687
846
  };
@@ -9,6 +9,7 @@ const { ActivityDetector } = require("./activityDetector");
9
9
  const { createActivityStatePublisher } = require("./activityStatePublisher");
10
10
  const {
11
11
  parseStreamEnvelope,
12
+ shouldAutoReplyFromPtyToPublisher,
12
13
  shouldForwardStreamToPublisher,
13
14
  } = require("./publisherRouting");
14
15
 
@@ -115,11 +116,6 @@ function computeInjectedSubmitDelayMs(agentType, text) {
115
116
  return delayMs;
116
117
  }
117
118
 
118
- function buildPrompt(text, marker) {
119
- if (!marker) return text;
120
- return `${text}\n\n请在完成后输出以下标记(单独一行):\n${marker}\n`;
121
- }
122
-
123
119
  function resolveCommand(agentType, extraArgs = []) {
124
120
  const normalizedAgent = String(agentType || "").trim().toLowerCase();
125
121
  const extra = Array.isArray(extraArgs) ? extraArgs : [];
@@ -171,9 +167,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
171
167
  UFOO_INTERNAL_PTY: "1",
172
168
  };
173
169
 
170
+ const idleMs = Number.parseInt(process.env.UFOO_INTERNAL_PTY_IDLE_MS || "", 10) || 30000;
174
171
  const eventBus = new EventBus(projectRoot);
175
172
  const activityDetector = new ActivityDetector(agentType, {
176
173
  mode: "internal-pty",
174
+ quietWindowMs: idleMs,
177
175
  });
178
176
  const agentsFilePath = getUfooPaths(projectRoot).agentsFile;
179
177
 
@@ -183,16 +181,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
183
181
  let ptyReady = false;
184
182
  let readyTimer = null;
185
183
  let currentPublisher = "";
186
- let currentMarker = "";
187
184
  let pendingOutput = [];
188
185
  let outputBuffer = "";
189
186
  let flushTimer = null;
190
187
  let idleTimer = null;
191
188
  let watchdogTimer = null;
192
- let suppressEcho = false;
193
- let echoMarker = "";
194
- let suppressTimer = null;
195
- let managedReplyBuffer = "";
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,
@@ -254,45 +244,23 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
254
244
  return result;
255
245
  }
256
246
 
257
- function appendManagedReply(chunk) {
258
- const text = String(chunk || "");
259
- if (!text) return;
260
- managedReplyBuffer += text;
261
- if (managedReplyBuffer.length > 40000) {
262
- managedReplyBuffer = managedReplyBuffer.slice(-40000);
263
- }
264
- }
265
-
266
- function takeManagedReply() {
267
- const reply = sanitizeChunk(managedReplyBuffer)
268
- .replace(/\n{3,}/g, "\n\n")
269
- .trim();
270
- managedReplyBuffer = "";
271
- return reply;
272
- }
273
-
274
247
  function completePublisherResponse(reason, fallbackNote = "") {
275
248
  if (!currentPublisher) return;
276
249
  if (flushTimer) {
277
250
  clearTimeout(flushTimer);
278
251
  flushTimer = null;
279
252
  }
253
+ if (!shouldAutoReplyFromPtyToPublisher(projectRoot, currentPublisher)) {
254
+ outputBuffer = "";
255
+ return;
256
+ }
280
257
  if (outputBuffer) {
281
258
  const remaining = outputBuffer;
282
259
  outputBuffer = "";
283
260
  deliverChunk(remaining);
284
261
  }
285
- if (canStreamToPublisher(currentPublisher)) {
286
- if (fallbackNote) enqueueSend(currentPublisher, fallbackNote);
287
- enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason }));
288
- return;
289
- }
290
- const reply = takeManagedReply();
291
- if (reply) {
292
- enqueueSend(currentPublisher, reply);
293
- } else if (fallbackNote) {
294
- enqueueSend(currentPublisher, fallbackNote);
295
- }
262
+ if (fallbackNote) enqueueSend(currentPublisher, fallbackNote);
263
+ enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason }));
296
264
  }
297
265
 
298
266
  // TTY view subscribers (same protocol as launcher inject.sock)
@@ -623,8 +591,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
623
591
  if (currentPublisher) {
624
592
  if (canStreamToPublisher(currentPublisher)) {
625
593
  enqueueSend(currentPublisher, payload);
626
- } else {
627
- appendManagedReply(cleaned);
628
594
  }
629
595
  } else {
630
596
  pendingOutput.push(payload);
@@ -684,8 +650,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
684
650
  detail: snap.detail,
685
651
  });
686
652
  // Quiet-window detector may classify IDLE sooner than stream fallback timer.
687
- // Release queue only when no explicit marker is being awaited.
688
- if (newState === "idle" && busy && !currentMarker && !suppressEcho) {
653
+ if (newState === "idle" && busy) {
689
654
  if (idleTimer) {
690
655
  clearTimeout(idleTimer);
691
656
  idleTimer = null;
@@ -699,7 +664,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
699
664
  }
700
665
  busy = false;
701
666
  currentPublisher = "";
702
- managedReplyBuffer = "";
703
667
  processQueue();
704
668
  }
705
669
  });
@@ -721,50 +685,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
721
685
  if (!clean) return;
722
686
  outputBuffer += clean;
723
687
  activityDetector.processOutput(clean);
724
- if (suppressEcho) {
725
- if (echoMarker && outputBuffer.includes(echoMarker)) {
726
- const idx = outputBuffer.indexOf(echoMarker);
727
- outputBuffer = outputBuffer.slice(idx + echoMarker.length);
728
- outputBuffer = outputBuffer.replace(/^\n+/, "");
729
- suppressEcho = false;
730
- currentMarker = echoMarker;
731
- echoMarker = "";
732
- if (suppressTimer) {
733
- clearTimeout(suppressTimer);
734
- suppressTimer = null;
735
- }
736
- } else {
737
- return;
738
- }
739
- }
740
- if (currentMarker) {
741
- const idx = outputBuffer.indexOf(currentMarker);
742
- if (idx !== -1) {
743
- const before = outputBuffer.slice(0, idx);
744
- outputBuffer = "";
745
- if (before) {
746
- deliverChunk(before);
747
- }
748
- if (currentPublisher) {
749
- completePublisherResponse("marker");
750
- }
751
- currentMarker = "";
752
- busy = false;
753
- activityDetector.markIdle();
754
- currentPublisher = "";
755
- managedReplyBuffer = "";
756
- if (watchdogTimer) {
757
- clearTimeout(watchdogTimer);
758
- watchdogTimer = null;
759
- }
760
- if (idleTimer) {
761
- clearTimeout(idleTimer);
762
- idleTimer = null;
763
- }
764
- processQueue();
765
- return;
766
- }
767
- }
768
688
  scheduleFlush();
769
689
  // Ready detection: during TUI startup, reset the quiet timer on each output.
770
690
  // Once output stops for READY_QUIET_MS, the TUI is considered initialized.
@@ -793,7 +713,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
793
713
  busy = false;
794
714
  activityDetector.markIdle();
795
715
  currentPublisher = "";
796
- managedReplyBuffer = "";
797
716
  processQueue();
798
717
  }, idleMs);
799
718
  }
@@ -832,8 +751,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
832
751
  busy = false;
833
752
  activityDetector.markIdle();
834
753
  currentPublisher = "";
835
- currentMarker = "";
836
- managedReplyBuffer = "";
837
754
 
838
755
  // If stop() was called, let the runner exit
839
756
  if (!running) return;
@@ -968,12 +885,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
968
885
  busy = true;
969
886
  activityDetector.markWorking();
970
887
  currentPublisher = next.publisher;
971
- currentMarker = next.marker || "";
972
- managedReplyBuffer = "";
973
- if (suppressTimer) {
974
- clearTimeout(suppressTimer);
975
- suppressTimer = null;
976
- }
977
888
  flushPending();
978
889
  if (next.text) {
979
890
  if (next.raw) {
@@ -981,30 +892,16 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
981
892
  } else {
982
893
  // Write text first, then send Enter separately.
983
894
  // Codex Ink TUI requires text and submit key as separate writes.
984
- // IMPORTANT: Defer marker detection until after Enter is sent,
985
- // because the prompt echo (TextInput display) contains the marker text.
986
- const prompt = buildPrompt(next.text, currentMarker);
987
- const savedMarker = currentMarker;
988
- suppressEcho = true;
989
- echoMarker = savedMarker;
990
- currentMarker = ""; // Disable marker detection during prompt echo & formatted display
991
- ptyProcess.write(prompt);
895
+ ptyProcess.write(next.text);
992
896
  setTimeout(() => {
993
897
  if (ptyProcess && ptyAlive) {
898
+ // Drop the local TUI input echo from any forwarded stream output.
994
899
  outputBuffer = "";
995
900
  const isClaude = agentType === "claude-code";
996
901
  if (isClaude) {
997
902
  // Claude Code: send CR directly without ESC.
998
903
  // ESC before CR is interpreted as Alt+Enter (newline).
999
904
  ptyProcess.write("\r");
1000
- suppressTimer = setTimeout(() => {
1001
- suppressTimer = null;
1002
- if (!suppressEcho) return;
1003
- suppressEcho = false;
1004
- echoMarker = "";
1005
- currentMarker = savedMarker;
1006
- outputBuffer = "";
1007
- }, 1500);
1008
905
  } else {
1009
906
  // Codex/others: ESC dismisses autocomplete, then CR submits.
1010
907
  ptyProcess.write("\x1b");
@@ -1012,14 +909,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
1012
909
  if (ptyProcess && ptyAlive) {
1013
910
  ptyProcess.write("\r");
1014
911
  }
1015
- suppressTimer = setTimeout(() => {
1016
- suppressTimer = null;
1017
- if (!suppressEcho) return;
1018
- suppressEcho = false;
1019
- echoMarker = "";
1020
- currentMarker = savedMarker;
1021
- outputBuffer = "";
1022
- }, 1500);
1023
912
  }, 100);
1024
913
  }
1025
914
  }
@@ -1030,17 +919,15 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
1030
919
  watchdogTimer = setTimeout(() => {
1031
920
  watchdogTimer = null;
1032
921
  if (!busy) return;
1033
- const timeoutNote = `[internal-pty] marker timeout; restarting PTY`;
922
+ const timeoutNote = `[internal-pty] task timeout; restarting PTY`;
1034
923
  if (currentPublisher) {
1035
924
  completePublisherResponse("timeout", timeoutNote);
1036
925
  }
1037
926
  logNote(timeoutNote);
1038
- restartPty("marker timeout");
1039
- currentMarker = "";
927
+ restartPty("task timeout");
1040
928
  busy = false;
1041
929
  activityDetector.markIdle();
1042
930
  currentPublisher = "";
1043
- managedReplyBuffer = "";
1044
931
  processQueue();
1045
932
  }, watchdogMs);
1046
933
  }
@@ -1088,11 +975,10 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
1088
975
  if (messageQueue.length >= maxQueue) {
1089
976
  messageQueue.shift();
1090
977
  }
1091
- const marker = raw ? "" : `__UFOO_DONE_${Date.now()}_${Math.random().toString(16).slice(2)}__`;
1092
978
  const publisher = typeof evt.publisher === "object" && evt.publisher
1093
979
  ? (evt.publisher.subscriber || evt.publisher.nickname || "unknown")
1094
980
  : (evt.publisher || "unknown");
1095
- messageQueue.push({ publisher, raw, text, marker });
981
+ messageQueue.push({ publisher, raw, text });
1096
982
  }
1097
983
  }
1098
984
  processQueue();
@@ -33,6 +33,10 @@ function shouldForwardStreamToPublisher(projectRoot, publisher) {
33
33
  return !isManagedAgentPublisher(projectRoot, id);
34
34
  }
35
35
 
36
+ function shouldAutoReplyFromPtyToPublisher(projectRoot, publisher) {
37
+ return shouldForwardStreamToPublisher(projectRoot, publisher);
38
+ }
39
+
36
40
  function parseStreamEnvelope(message) {
37
41
  if (typeof message !== "string" || !message.trim()) return null;
38
42
  try {
@@ -50,5 +54,6 @@ module.exports = {
50
54
  isManagedAgentPublisher,
51
55
  normalizePublisher,
52
56
  parseStreamEnvelope,
57
+ shouldAutoReplyFromPtyToPublisher,
53
58
  shouldForwardStreamToPublisher,
54
59
  };
@@ -126,7 +126,10 @@ function createAgentViewController(options = {}) {
126
126
 
127
127
  agentInputSuppressUntil = now() + 300;
128
128
  agentViewUsesBus = Boolean(options.useBus);
129
- if (!agentViewUsesBus) {
129
+ if (agentViewUsesBus) {
130
+ const label = getAgentLabel(agentId);
131
+ processStdout.write(`ufoo internal · ${label}\r\n\r\n> `);
132
+ } else {
130
133
  const sockPath = getInjectSockPath(agentId);
131
134
  connectAgentOutput(sockPath);
132
135
  connectAgentInput(sockPath);
@@ -57,6 +57,11 @@ function createDashboardKeyController(options = {}) {
57
57
  return Boolean(caps && caps.supportsSocketProtocol);
58
58
  }
59
59
 
60
+ function supportsInternalQueue(agentId) {
61
+ const caps = getAgentCapabilities(agentId);
62
+ return Boolean(caps && caps.supportsInternalQueueLoop);
63
+ }
64
+
60
65
  function withAgentInputFocus() {
61
66
  state.focusMode = "input";
62
67
  state.agentOutputSuppressed = false;
@@ -73,7 +78,7 @@ function createDashboardKeyController(options = {}) {
73
78
 
74
79
  function switchAgentView(agentId) {
75
80
  withAgentInputFocus();
76
- enterAgentView(agentId);
81
+ enterAgentView(agentId, { useBus: supportsInternalQueue(agentId) && !supportsSocket(agentId) });
77
82
  }
78
83
 
79
84
  function exitAgentDashboardToInput() {
@@ -154,7 +159,7 @@ function createDashboardKeyController(options = {}) {
154
159
  } else {
155
160
  withAgentInputFocus();
156
161
  state.selectedAgentIndex = nextIndex + 1;
157
- enterAgentView(nextAgent);
162
+ enterAgentView(nextAgent, { useBus: supportsInternalQueue(nextAgent) && !supportsSocket(nextAgent) });
158
163
  }
159
164
  } else {
160
165
  exitAgentView();
@@ -511,6 +516,16 @@ function createDashboardKeyController(options = {}) {
511
516
  enterAgentView(agentId);
512
517
  return true;
513
518
  }
519
+
520
+ if (supportsInternalQueue(agentId)) {
521
+ clearTargetAgent();
522
+ state.focusMode = "input";
523
+ state.dashboardView = "agents";
524
+ state.selectedAgentIndex = -1;
525
+ setScreenGrabKeys(false);
526
+ enterAgentView(agentId, { useBus: true });
527
+ return true;
528
+ }
514
529
  }
515
530
 
516
531
  exitDashboardMode(false);