u-foo 1.9.8 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/package.json +2 -4
  2. package/src/agent/claudeEventTranslator.js +267 -0
  3. package/src/agent/claudeOauthTokenReader.js +52 -0
  4. package/src/agent/claudeThreadProvider.js +343 -0
  5. package/src/agent/cliRunner.js +4 -16
  6. package/src/agent/codexEventTranslator.js +78 -0
  7. package/src/agent/codexThreadProvider.js +181 -0
  8. package/src/agent/controllerToolExecutor.js +233 -0
  9. package/src/agent/credentials/claude.js +324 -0
  10. package/src/agent/credentials/codex.js +203 -0
  11. package/src/agent/credentials/index.js +106 -0
  12. package/src/agent/defaultBootstrap.js +128 -5
  13. package/src/agent/internalRunner.js +333 -2
  14. package/src/agent/loopObservability.js +190 -0
  15. package/src/agent/loopRuntime.js +457 -0
  16. package/src/agent/ufooAgent.js +178 -120
  17. package/src/agent/upstreamTransport.js +464 -0
  18. package/src/bus/utils.js +3 -2
  19. package/src/chat/dashboardView.js +51 -1
  20. package/src/chat/index.js +3 -1
  21. package/src/config.js +53 -17
  22. package/src/controller/flags.js +160 -0
  23. package/src/controller/gateRouter.js +201 -0
  24. package/src/controller/routerFastPath.js +22 -0
  25. package/src/controller/shadowGuard.js +280 -0
  26. package/src/daemon/index.js +2 -3
  27. package/src/daemon/promptLoop.js +33 -224
  28. package/src/daemon/promptRequest.js +360 -5
  29. package/src/daemon/status.js +2 -0
  30. package/src/history/inputTimeline.js +9 -4
  31. package/src/memory/index.js +24 -0
  32. package/src/providerapi/redactor.js +87 -0
  33. package/src/providerapi/shadowDiff.js +174 -0
  34. package/src/report/store.js +4 -3
  35. package/src/tools/handlers/ackBus.js +26 -0
  36. package/src/tools/handlers/common.js +64 -0
  37. package/src/tools/handlers/dispatchMessage.js +81 -0
  38. package/src/tools/handlers/listAgents.js +14 -0
  39. package/src/tools/handlers/readBusSummary.js +34 -0
  40. package/src/tools/handlers/readOpenDecisions.js +26 -0
  41. package/src/tools/handlers/readProjectRegistry.js +20 -0
  42. package/src/tools/handlers/readPromptHistory.js +123 -0
  43. package/src/tools/handlers/tier2.js +134 -0
  44. package/src/tools/index.js +55 -0
  45. package/src/tools/registry.js +69 -0
  46. package/src/tools/schemaFixtures.js +415 -0
  47. package/src/tools/tier0/listAgents.js +14 -0
  48. package/src/tools/tier0/readBusSummary.js +14 -0
  49. package/src/tools/tier0/readOpenDecisions.js +14 -0
  50. package/src/tools/tier0/readProjectRegistry.js +14 -0
  51. package/src/tools/tier0/readPromptHistory.js +14 -0
  52. package/src/tools/tier1/ackBus.js +14 -0
  53. package/src/tools/tier1/dispatchMessage.js +14 -0
  54. package/src/tools/tier1/routeAgent.js +14 -0
  55. package/src/tools/tier2/closeAgent.js +14 -0
  56. package/src/tools/tier2/launchAgent.js +14 -0
  57. package/src/tools/tier2/manageCron.js +14 -0
  58. package/src/tools/tier2/renameAgent.js +14 -0
  59. package/src/tools/types.js +75 -0
  60. package/src/tools/unimplemented.js +13 -0
  61. package/src/ufoo/paths.js +4 -0
  62. package/bin/ufoo-assistant-agent.js +0 -5
  63. package/bin/ufoo-engine.js +0 -25
  64. package/src/assistant/agent.js +0 -261
  65. package/src/assistant/bridge.js +0 -178
  66. package/src/assistant/constants.js +0 -15
  67. package/src/assistant/engine.js +0 -252
  68. package/src/assistant/stdio.js +0 -58
  69. package/src/assistant/ufooEngineCli.js +0 -312
@@ -26,6 +26,16 @@ function hasMetaCommandArgs(args = []) {
26
26
  return hasArg(args, ["-h", "--help", "-v", "--version"]);
27
27
  }
28
28
 
29
+ function readOptionalFile(filePath) {
30
+ const target = asTrimmedString(filePath);
31
+ if (!target) return "";
32
+ try {
33
+ return fs.readFileSync(target, "utf8");
34
+ } catch {
35
+ return "";
36
+ }
37
+ }
38
+
29
39
  /**
30
40
  * Load the team activity timeline for prompt injection.
31
41
  * The daemon syncs manual inputs every ~30s; bus messages are appended in real-time.
@@ -67,6 +77,11 @@ function defaultBootstrapFile(projectRoot, agentType = "") {
67
77
  return path.join(getUfooPaths(projectRoot).agentDir, safeAgentType, "default-bootstrap.md");
68
78
  }
69
79
 
80
+ function mergedBootstrapFile(projectRoot, agentType = "") {
81
+ const safeAgentType = asTrimmedString(agentType).replace(/[^a-zA-Z0-9._-]/g, "-") || "agent";
82
+ return path.join(getUfooPaths(projectRoot).agentDir, safeAgentType, "merged-bootstrap.md");
83
+ }
84
+
70
85
  function prepareDefaultBootstrapFile({
71
86
  projectRoot,
72
87
  agentType = "",
@@ -80,6 +95,83 @@ function prepareDefaultBootstrapFile({
80
95
  return { ok: true, file };
81
96
  }
82
97
 
98
+ function mergePromptSegments(...segments) {
99
+ return segments
100
+ .map((item) => String(item || "").trim())
101
+ .filter(Boolean)
102
+ .join("\n\n");
103
+ }
104
+
105
+ function mergeClaudePromptArgs({
106
+ projectRoot,
107
+ agentType = "claude-code",
108
+ args = [],
109
+ bootstrapText = "",
110
+ } = {}) {
111
+ const currentArgs = Array.isArray(args) ? args.slice() : [];
112
+ for (let index = 0; index < currentArgs.length; index += 1) {
113
+ const item = asTrimmedString(currentArgs[index]);
114
+ if (!item) continue;
115
+
116
+ if (item === "--append-system-prompt") {
117
+ const existingFile = asTrimmedString(currentArgs[index + 1]);
118
+ const mergedText = mergePromptSegments(readOptionalFile(existingFile), bootstrapText);
119
+ const prepared = prepareDefaultBootstrapFile({
120
+ projectRoot,
121
+ agentType,
122
+ targetFile: mergedBootstrapFile(projectRoot, agentType),
123
+ promptText: mergedText,
124
+ });
125
+ currentArgs[index + 1] = prepared.file;
126
+ return { args: currentArgs, file: prepared.file, promptText: mergedText };
127
+ }
128
+
129
+ if (item.startsWith("--append-system-prompt=")) {
130
+ const existingFile = item.slice("--append-system-prompt=".length);
131
+ const mergedText = mergePromptSegments(readOptionalFile(existingFile), bootstrapText);
132
+ const prepared = prepareDefaultBootstrapFile({
133
+ projectRoot,
134
+ agentType,
135
+ targetFile: mergedBootstrapFile(projectRoot, agentType),
136
+ promptText: mergedText,
137
+ });
138
+ currentArgs[index] = `--append-system-prompt=${prepared.file}`;
139
+ return { args: currentArgs, file: prepared.file, promptText: mergedText };
140
+ }
141
+
142
+ if (item === "--system-prompt") {
143
+ const existingPrompt = String(currentArgs[index + 1] || "");
144
+ currentArgs[index + 1] = mergePromptSegments(existingPrompt, bootstrapText);
145
+ return { args: currentArgs, file: "", promptText: String(currentArgs[index + 1] || "") };
146
+ }
147
+
148
+ if (item.startsWith("--system-prompt=")) {
149
+ const existingPrompt = item.slice("--system-prompt=".length);
150
+ const mergedText = mergePromptSegments(existingPrompt, bootstrapText);
151
+ currentArgs[index] = `--system-prompt=${mergedText}`;
152
+ return { args: currentArgs, file: "", promptText: mergedText };
153
+ }
154
+ }
155
+ return null;
156
+ }
157
+
158
+ function mergeCodexPromptArgs({ args = [], bootstrapText = "" } = {}) {
159
+ const currentArgs = Array.isArray(args) ? args.slice() : [];
160
+ const lastIndex = currentArgs.length - 1;
161
+ if (lastIndex < 0) return null;
162
+ const lastItem = asTrimmedString(currentArgs[lastIndex]);
163
+ const promptIndex = lastItem && !lastItem.startsWith("-") ? lastIndex : -1;
164
+
165
+ if (promptIndex < 0) return null;
166
+
167
+ currentArgs[promptIndex] = mergePromptSegments(bootstrapText, currentArgs[promptIndex]);
168
+ return {
169
+ args: currentArgs,
170
+ promptText: String(currentArgs[promptIndex] || ""),
171
+ promptIndex,
172
+ };
173
+ }
174
+
83
175
  function resolveDefaultManualBootstrap({
84
176
  projectRoot,
85
177
  agentType = "",
@@ -98,10 +190,22 @@ function resolveDefaultManualBootstrap({
98
190
  }
99
191
 
100
192
  if (normalizedAgent === "claude-code") {
101
- if (hasArg(currentArgs, ["--append-system-prompt", "--system-prompt"])) {
102
- return { args: currentArgs, env: {}, mode: "skip" };
103
- }
104
193
  const promptText = buildDefaultStartupBootstrapPrompt({ agentType: normalizedAgent, projectRoot });
194
+ const merged = mergeClaudePromptArgs({
195
+ projectRoot,
196
+ agentType: normalizedAgent,
197
+ args: currentArgs,
198
+ bootstrapText: promptText,
199
+ });
200
+ if (merged) {
201
+ return {
202
+ args: merged.args,
203
+ env: {},
204
+ mode: "merged-system-prompt",
205
+ file: merged.file,
206
+ promptText: merged.promptText,
207
+ };
208
+ }
105
209
  const prepared = prepareDefaultBootstrapFile({
106
210
  projectRoot,
107
211
  agentType: normalizedAgent,
@@ -117,10 +221,29 @@ function resolveDefaultManualBootstrap({
117
221
  }
118
222
 
119
223
  if (normalizedAgent === "codex") {
224
+ const promptText = buildDefaultStartupBootstrapPrompt({ agentType: normalizedAgent, projectRoot });
225
+ const merged = mergeCodexPromptArgs({
226
+ args: currentArgs,
227
+ bootstrapText: promptText,
228
+ });
229
+ if (merged) {
230
+ return {
231
+ args: merged.args,
232
+ env: {},
233
+ mode: "initial-prompt-arg",
234
+ promptText: merged.promptText,
235
+ };
236
+ }
120
237
  if (currentArgs.length > 0) {
121
- return { args: currentArgs, env: {}, mode: "skip" };
238
+ return {
239
+ args: currentArgs,
240
+ env: {
241
+ UFOO_STARTUP_BOOTSTRAP_TEXT: promptText,
242
+ },
243
+ mode: "post-launch-inject",
244
+ promptText,
245
+ };
122
246
  }
123
- const promptText = buildDefaultStartupBootstrapPrompt({ agentType: normalizedAgent, projectRoot });
124
247
  return {
125
248
  args: currentArgs,
126
249
  env: {
@@ -6,11 +6,32 @@ const EventBus = require("../bus");
6
6
  const { runCliAgent } = require("./cliRunner");
7
7
  const { normalizeCliOutput } = require("./normalizeOutput");
8
8
  const { createActivityStatePublisher } = require("./activityStatePublisher");
9
+ const { loadConfig, normalizeCodexInternalThreadMode } = require("../config");
10
+ const {
11
+ createCodexThreadProvider,
12
+ defaultCodexTransportStreamFactory,
13
+ } = require("./codexThreadProvider");
14
+ const {
15
+ createClaudeThreadProvider,
16
+ defaultClaudeTransportStreamFactory,
17
+ } = require("./claudeThreadProvider");
18
+ const { resolveClaudeUpstreamCredentials } = require("./credentials/claude");
19
+ const { buildUpstreamAuthFromCredential } = require("./credentials");
20
+ const { listToolsForCallerTier, CALLER_TIERS } = require("../tools");
21
+ const { redactToolCallPayload, redactSecrets } = require("../providerapi/redactor");
9
22
 
10
23
  function sleep(ms) {
11
24
  return new Promise((resolve) => setTimeout(resolve, ms));
12
25
  }
13
26
 
27
+ function normalizeWorkerThreadToolMode(value = "") {
28
+ const raw = String(value || "").trim().toLowerCase();
29
+ if (raw === "worker-tier01" || raw === "tier01" || raw === "enabled" || raw === "1" || raw === "true") {
30
+ return "worker-tier01";
31
+ }
32
+ return "disabled";
33
+ }
34
+
14
35
  function buildEnv(agentType, sessionId, publisher, nickname) {
15
36
  const env = { ...process.env };
16
37
  env.AI_BUS_PUBLISHER = publisher || env.AI_BUS_PUBLISHER || "";
@@ -63,6 +84,18 @@ function createBusSender(projectRoot, subscriber) {
63
84
  return { enqueue, flush };
64
85
  }
65
86
 
87
+ function shouldFallbackToLegacyThreadProvider(err, provider) {
88
+ if (provider !== "claude-cli" || !err || typeof err !== "object") {
89
+ return false;
90
+ }
91
+ const code = String(err.code || "").trim().toUpperCase();
92
+ return (
93
+ code === "CLAUDE_AUTH_UNAVAILABLE"
94
+ || code === "CLAUDE_OAUTH_SCHEMA_UNSUPPORTED"
95
+ || code === "ANTHROPIC_SDK_UNAVAILABLE"
96
+ );
97
+ }
98
+
66
99
  function drainQueue(queueFile) {
67
100
  if (!fs.existsSync(queueFile)) return [];
68
101
  const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
@@ -106,7 +139,8 @@ async function handleEvent(
106
139
  evt,
107
140
  cliSessionState,
108
141
  busSender,
109
- extraArgs = []
142
+ extraArgs = [],
143
+ threadRuntime = null
110
144
  ) {
111
145
  if (!evt || !evt.data || !evt.data.message) return;
112
146
  const prompt = evt.data.message;
@@ -122,6 +156,21 @@ async function handleEvent(
122
156
  busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: text }));
123
157
  };
124
158
 
159
+ if (threadRuntime && threadRuntime.enabled && threadRuntime.thread) {
160
+ const threadedResult = await handleThreadedEvent({
161
+ agentType,
162
+ provider,
163
+ publisher,
164
+ prompt,
165
+ busSender,
166
+ emitStreamDelta,
167
+ threadRuntime,
168
+ });
169
+ if (!threadedResult || !threadedResult.fallbackToLegacy) {
170
+ return;
171
+ }
172
+ }
173
+
125
174
  let res = await runCliAgent({
126
175
  provider,
127
176
  model,
@@ -188,6 +237,270 @@ async function handleEvent(
188
237
  await busSender.flush();
189
238
  }
190
239
 
240
+ async function handleThreadedEvent({
241
+ agentType,
242
+ provider,
243
+ publisher,
244
+ prompt,
245
+ busSender,
246
+ emitStreamDelta,
247
+ threadRuntime,
248
+ }) {
249
+ try {
250
+ for await (const event of threadRuntime.thread.runStreamed(prompt, {})) {
251
+ if (!event || typeof event !== "object") continue;
252
+ if (event.type === "text_delta" && event.delta) {
253
+ emitStreamDelta(event.delta);
254
+ } else if (event.type === "turn_failed") {
255
+ throw new Error(event.error || `thread turn failed for ${agentType}`);
256
+ }
257
+ }
258
+
259
+ busSender.enqueue(
260
+ publisher,
261
+ JSON.stringify({ stream: true, done: true, reason: "complete" })
262
+ );
263
+ await busSender.flush();
264
+ } catch (err) {
265
+ if (shouldFallbackToLegacyThreadProvider(err, provider)) {
266
+ return { fallbackToLegacy: true };
267
+ }
268
+ if (threadRuntime && typeof threadRuntime.rebuildThread === "function") {
269
+ await threadRuntime.rebuildThread();
270
+ }
271
+ busSender.enqueue(
272
+ publisher,
273
+ JSON.stringify({
274
+ stream: true,
275
+ delta: `[internal:${agentType}] error: ${err && err.message ? err.message : "unknown error"}`,
276
+ })
277
+ );
278
+ busSender.enqueue(
279
+ publisher,
280
+ JSON.stringify({ stream: true, done: true, reason: "error" })
281
+ );
282
+ await busSender.flush();
283
+ return { fallbackToLegacy: false };
284
+ }
285
+ }
286
+
287
+ function getCodexThreadMode(projectRoot) {
288
+ const envValue = process.env.UFOO_CODEX_INTERNAL_THREAD_MODE;
289
+ if (typeof envValue === "string" && envValue.trim()) {
290
+ return normalizeCodexInternalThreadMode(envValue);
291
+ }
292
+ return loadConfig(projectRoot).codexInternalThreadMode;
293
+ }
294
+
295
+ function getWorkerThreadToolMode() {
296
+ return normalizeWorkerThreadToolMode(process.env.UFOO_CODEX_INTERNAL_THREAD_TOOLS);
297
+ }
298
+
299
+ function buildWorkerThreadToolRuntime({ projectRoot, subscriber, observer }) {
300
+ const mode = getWorkerThreadToolMode();
301
+ if (mode !== "worker-tier01") {
302
+ return {
303
+ enabled: false,
304
+ mode,
305
+ tools: [],
306
+ executeToolCall: null,
307
+ };
308
+ }
309
+
310
+ const eventBus = new EventBus(projectRoot);
311
+ const toolDefinitions = listToolsForCallerTier(CALLER_TIERS.WORKER);
312
+ const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool]));
313
+ const emitAudit = (phase, payload) => {
314
+ if (observer && typeof observer.onToolCall === "function") {
315
+ try { observer.onToolCall({ phase, payload }); } catch { /* ignore observer errors */ }
316
+ }
317
+ };
318
+
319
+ return {
320
+ enabled: toolDefinitions.length > 0,
321
+ mode,
322
+ tools: toolDefinitions.map((tool) => ({
323
+ name: tool.name,
324
+ description: tool.description,
325
+ input_schema: tool.input_schema,
326
+ })),
327
+ // Keep a shared-handler executor ready for a future continuation-capable SDK path.
328
+ // The current Codex seam injects tool descriptors only and does not execute live
329
+ // tool calls inside the SDK stream yet.
330
+ async executeToolCall(toolCall = {}) {
331
+ const name = String(toolCall.name || "").trim();
332
+ const definition = toolsByName.get(name);
333
+ const rawArgs = toolCall.arguments || toolCall.args || {};
334
+ // Slice 1 (§10.7 tool pre-call): build a redacted audit envelope before
335
+ // the handler receives args, so observability consumers never see raw secrets.
336
+ const redactedPayload = redactToolCallPayload({
337
+ name,
338
+ args: rawArgs,
339
+ tool_call_id: toolCall.tool_call_id || toolCall.toolCallId || "",
340
+ caller_tier: CALLER_TIERS.WORKER,
341
+ });
342
+ emitAudit("pre_call", redactedPayload);
343
+ if (!definition) {
344
+ const errorResult = {
345
+ ok: false,
346
+ error: {
347
+ code: "unsupported_tool",
348
+ message: `worker tool is unavailable: ${name}`,
349
+ },
350
+ };
351
+ emitAudit("post_call", { ...redactedPayload, result: errorResult });
352
+ return errorResult;
353
+ }
354
+
355
+ try {
356
+ const result = await definition.handler({
357
+ caller_tier: CALLER_TIERS.WORKER,
358
+ projectRoot,
359
+ subscriber,
360
+ eventBus,
361
+ }, rawArgs);
362
+ const safeResult = redactSecrets(result);
363
+ emitAudit("post_call", { ...redactedPayload, result: safeResult });
364
+ return safeResult;
365
+ } catch (err) {
366
+ const errorResult = {
367
+ ok: false,
368
+ error: {
369
+ code: err && err.code ? err.code : "tool_execution_failed",
370
+ message: err && err.message ? err.message : String(err || "tool execution failed"),
371
+ },
372
+ };
373
+ const safeErrorResult = redactSecrets(errorResult);
374
+ emitAudit("post_call", { ...redactedPayload, result: safeErrorResult });
375
+ return safeErrorResult;
376
+ }
377
+ },
378
+ };
379
+ }
380
+
381
+ function getClaudeThreadMode() {
382
+ const envValue = process.env.UFOO_CLAUDE_INTERNAL_THREAD_MODE;
383
+ const raw = String(envValue || "").trim().toLowerCase();
384
+ if (raw === "api") return "api";
385
+ return "legacy";
386
+ }
387
+
388
+ function buildClaudeAuthProvider(projectRoot) {
389
+ const config = loadConfig(projectRoot);
390
+ return async () => {
391
+ const credential = await resolveClaudeUpstreamCredentials({
392
+ profile: config.claudeOauthProfile,
393
+ tokenPath: config.claudeOauthTokenPath,
394
+ refreshWindowMs: Number(config.claudeOauthRefreshWindowSec || 300) * 1000,
395
+ });
396
+ return buildUpstreamAuthFromCredential(credential);
397
+ };
398
+ }
399
+
400
+ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], subscriber = "" }) {
401
+ const disabledRuntime = {
402
+ enabled: false,
403
+ thread: null,
404
+ toolRuntime: { enabled: false, mode: "disabled", tools: [] },
405
+ close: async () => {},
406
+ rebuildThread: async () => {},
407
+ };
408
+
409
+ if (provider === "codex-cli") {
410
+ if (getCodexThreadMode(projectRoot) !== "sdk") {
411
+ return disabledRuntime;
412
+ }
413
+
414
+ try {
415
+ const toolRuntime = buildWorkerThreadToolRuntime({
416
+ projectRoot,
417
+ subscriber,
418
+ });
419
+ let providerInstance = createCodexThreadProvider({
420
+ model,
421
+ cwd: projectRoot,
422
+ extraArgs,
423
+ tools: toolRuntime.tools,
424
+ streamFactory: defaultCodexTransportStreamFactory,
425
+ });
426
+ let thread = providerInstance.startThread();
427
+
428
+ return {
429
+ enabled: true,
430
+ toolRuntime,
431
+ get thread() {
432
+ return thread;
433
+ },
434
+ async rebuildThread() {
435
+ if (thread && typeof thread.close === "function") {
436
+ await thread.close();
437
+ }
438
+ providerInstance = createCodexThreadProvider({
439
+ model,
440
+ cwd: projectRoot,
441
+ extraArgs,
442
+ tools: toolRuntime.tools,
443
+ streamFactory: defaultCodexTransportStreamFactory,
444
+ });
445
+ thread = providerInstance.startThread();
446
+ },
447
+ async close() {
448
+ if (thread && typeof thread.close === "function") {
449
+ await thread.close();
450
+ }
451
+ },
452
+ };
453
+ } catch {
454
+ return disabledRuntime;
455
+ }
456
+ }
457
+
458
+ if (provider === "claude-cli") {
459
+ if (getClaudeThreadMode() !== "api") {
460
+ return disabledRuntime;
461
+ }
462
+ if (typeof createClaudeThreadProvider !== "function" || typeof resolveClaudeUpstreamCredentials !== "function") {
463
+ return disabledRuntime;
464
+ }
465
+
466
+ try {
467
+ let providerInstance = createClaudeThreadProvider({
468
+ model,
469
+ authProvider: buildClaudeAuthProvider(projectRoot),
470
+ streamFactory: defaultClaudeTransportStreamFactory,
471
+ });
472
+ let thread = providerInstance.startThread();
473
+
474
+ return {
475
+ enabled: true,
476
+ get thread() {
477
+ return thread;
478
+ },
479
+ async rebuildThread() {
480
+ if (thread && typeof thread.close === "function") {
481
+ await thread.close();
482
+ }
483
+ providerInstance = createClaudeThreadProvider({
484
+ model,
485
+ authProvider: buildClaudeAuthProvider(projectRoot),
486
+ streamFactory: defaultClaudeTransportStreamFactory,
487
+ });
488
+ thread = providerInstance.startThread();
489
+ },
490
+ async close() {
491
+ if (thread && typeof thread.close === "function") {
492
+ await thread.close();
493
+ }
494
+ },
495
+ };
496
+ } catch {
497
+ return disabledRuntime;
498
+ }
499
+ }
500
+
501
+ return disabledRuntime;
502
+ }
503
+
191
504
  async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs = [] }) {
192
505
  // Internal runner 必须由 daemon 启动,UFOO_SUBSCRIBER_ID 应该已经设置
193
506
  const { subscriber, agentType: parsedAgentType, sessionId } = parseSubscriberId();
@@ -202,6 +515,13 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
202
515
  const provider = normalizedAgentType === "codex" ? "codex-cli" : "claude-cli";
203
516
  const model = process.env.UFOO_AGENT_MODEL || "";
204
517
  const busSender = createBusSender(projectRoot, subscriber);
518
+ const threadRuntime = createThreadRuntime({
519
+ projectRoot,
520
+ provider,
521
+ model,
522
+ extraArgs,
523
+ subscriber,
524
+ });
205
525
 
206
526
  // Session state management for CLI continuity
207
527
  // Use stable path based on nickname (if exists) or agent type, NOT subscriber ID
@@ -294,7 +614,8 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
294
614
  evt,
295
615
  cliSessionState,
296
616
  busSender,
297
- extraArgs
617
+ extraArgs,
618
+ threadRuntime
298
619
  );
299
620
  }
300
621
 
@@ -324,10 +645,20 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
324
645
  // eslint-disable-next-line no-await-in-loop
325
646
  await sleep(1000);
326
647
  }
648
+
649
+ await threadRuntime.close();
327
650
  }
328
651
 
329
652
  module.exports = {
330
653
  runInternalRunner,
331
654
  createBusSender,
332
655
  handleEvent,
656
+ createThreadRuntime,
657
+ getCodexThreadMode,
658
+ getWorkerThreadToolMode,
659
+ buildWorkerThreadToolRuntime,
660
+ normalizeWorkerThreadToolMode,
661
+ getClaudeThreadMode,
662
+ buildClaudeAuthProvider,
663
+ shouldFallbackToLegacyThreadProvider,
333
664
  };