u-foo 2.3.11 → 2.3.13

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.
@@ -1,6 +1,7 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { getUfooPaths } = require("../ufoo/paths");
4
+ const { buildDefaultStartupBootstrapPrompt } = require("./defaultBootstrap");
4
5
 
5
6
  function readFileSafe(filePath = "") {
6
7
  if (!filePath) return "";
@@ -74,11 +75,27 @@ function buildBootstrapContent({
74
75
  return `${lines.join("\n")}\n`;
75
76
  }
76
77
 
78
+ function hasUfooProtocolPrompt(promptText = "") {
79
+ const text = String(promptText || "");
80
+ return text.includes("ufoo protocol:") && text.includes("ufoo ctx decisions -l");
81
+ }
82
+
83
+ function mergeDefaultUfooProtocolPrompt(projectRoot = "", promptText = "") {
84
+ const currentPrompt = String(promptText || "").trim();
85
+ if (hasUfooProtocolPrompt(currentPrompt)) return currentPrompt;
86
+ const defaultPrompt = buildDefaultStartupBootstrapPrompt({
87
+ agentType: "ufoo-code",
88
+ projectRoot,
89
+ }).trim();
90
+ return [defaultPrompt, currentPrompt].filter(Boolean).join("\n\n");
91
+ }
92
+
77
93
  function prepareUcodeBootstrap({
78
94
  projectRoot = process.cwd(),
79
95
  promptFile = "",
80
96
  promptText = "",
81
97
  targetFile = "",
98
+ includeDefaultProtocol = true,
82
99
  } = {}) {
83
100
  const resolvedProjectRoot = path.resolve(projectRoot);
84
101
  const resolvedPrompt = String(promptFile || "").trim();
@@ -86,11 +103,14 @@ function prepareUcodeBootstrap({
86
103
 
87
104
  const inlinePromptText = String(promptText || "").trim();
88
105
  const resolvedPromptText = inlinePromptText || readFileSafe(resolvedPrompt);
106
+ const finalPromptText = includeDefaultProtocol
107
+ ? mergeDefaultUfooProtocolPrompt(resolvedProjectRoot, resolvedPromptText)
108
+ : resolvedPromptText;
89
109
  const rules = resolveProjectRules(resolvedProjectRoot);
90
110
  const content = buildBootstrapContent({
91
111
  projectRoot: resolvedProjectRoot,
92
112
  promptFile: resolvedPrompt,
93
- promptText: resolvedPromptText,
113
+ promptText: finalPromptText,
94
114
  rules,
95
115
  });
96
116
 
@@ -107,6 +127,8 @@ function prepareUcodeBootstrap({
107
127
  }
108
128
 
109
129
  module.exports = {
130
+ hasUfooProtocolPrompt,
131
+ mergeDefaultUfooProtocolPrompt,
110
132
  readFileSafe,
111
133
  resolveProjectRules,
112
134
  defaultBootstrapPath,
@@ -1,7 +1,5 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
- const { runCliAgent } = require("./cliRunner");
4
- const { normalizeCliOutput } = require("./normalizeOutput");
5
3
  const { buildStatus } = require("../daemon/status");
6
4
  const { getUfooPaths } = require("../ufoo/paths");
7
5
  const { normalizeGateRouterResult } = require("../controller/gateRouter");
@@ -633,7 +631,6 @@ async function runUfooAgent({
633
631
  loopRuntime = null,
634
632
  controllerMode = null,
635
633
  }) {
636
- const state = loadSessionState(projectRoot);
637
634
  const mode = String(routingMode || (routingContext && routingContext.mode) || "").trim().toLowerCase();
638
635
  const resolvedControllerMode = String(
639
636
  controllerMode
@@ -660,7 +657,6 @@ async function runUfooAgent({
660
657
  let res;
661
658
 
662
659
  const useDirectProvider = shouldUseDirectProvider(provider);
663
- let usedDirectProvider = false;
664
660
 
665
661
  if (useDirectProvider) {
666
662
  res = await runNativeRouterCall({
@@ -671,47 +667,22 @@ async function runUfooAgent({
671
667
  model,
672
668
  });
673
669
  if (!res.ok) {
670
+ // eslint-disable-next-line no-console
671
+ console.error(`[ufoo-agent] native provider failed: ${res.error || "unknown error"}`);
674
672
  return { ok: false, error: res.error };
675
673
  } else {
676
- usedDirectProvider = true;
677
674
  res = { ok: true, output: res.output, sessionId: "", provider: res.provider, model: res.model };
678
675
  }
679
676
  }
680
677
 
681
678
  if (!useDirectProvider) {
682
- res = await runCliAgent({
683
- provider,
684
- model,
685
- prompt: fullPrompt,
686
- systemPrompt,
687
- sessionId: state.data?.sessionId,
688
- disableSession: provider === "claude-cli",
689
- cwd: projectRoot,
690
- });
691
-
692
- if (!res.ok) {
693
- const msg = (res.error || "").toLowerCase();
694
- if (msg.includes("session id") || msg.includes("session-id") || msg.includes("already in use")) {
695
- res = await runCliAgent({
696
- provider,
697
- model,
698
- prompt: fullPrompt,
699
- systemPrompt,
700
- sessionId: undefined,
701
- disableSession: provider === "claude-cli",
702
- cwd: projectRoot,
703
- });
704
- }
705
- }
706
-
707
- if (!res.ok) {
708
- return { ok: false, error: res.error };
709
- }
679
+ const error = `unsupported ufoo-agent provider "${provider || ""}"; cliRunner fallback has been removed`;
680
+ // eslint-disable-next-line no-console
681
+ console.error(`[ufoo-agent] ${error}`);
682
+ return { ok: false, error };
710
683
  }
711
684
 
712
- const rawText = usedDirectProvider
713
- ? String(res.output || "").trim()
714
- : normalizeCliOutput(res.output);
685
+ const rawText = String(res.output || "").trim();
715
686
  const text = stripMarkdownFence(rawText);
716
687
  let payload = null;
717
688
  try {
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,
package/src/bus/store.js CHANGED
@@ -5,6 +5,7 @@ const path = require("path");
5
5
  const { getTimestamp, ensureDir, safeNameToSubscriber, getTtyProcessInfo } = require("./utils");
6
6
  const { getUfooPaths } = require("../ufoo/paths");
7
7
  const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
8
+ const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnostics");
8
9
 
9
10
  function readQueueTty(queueDir) {
10
11
  try {
@@ -25,7 +26,7 @@ function buildUsedNicknameSet(agents = {}) {
25
26
  return set;
26
27
  }
27
28
 
28
- function recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now) {
29
+ function recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now, agentsFile) {
29
30
  if (!subscriber || data.agents[subscriber]) return false;
30
31
 
31
32
  if (subscriber === "ufoo-agent") {
@@ -45,6 +46,17 @@ function recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now) {
45
46
  };
46
47
  return true;
47
48
  }
49
+ appendAgentRegistryDiagnostic(
50
+ agentsFile,
51
+ "queue_entry_not_recovered",
52
+ {
53
+ source: "bus.store.recoverQueueEntry",
54
+ subscriber,
55
+ queue_dir: queueDir,
56
+ reason: "non_controller_queue_without_registry_entry",
57
+ used_nicknames: Array.from(usedNicknames || []).sort(),
58
+ }
59
+ );
48
60
  return false;
49
61
  }
50
62
 
@@ -112,20 +124,20 @@ class BusStore {
112
124
  if (!stat.isDirectory()) continue;
113
125
 
114
126
  const subscriber = safeNameToSubscriber(entry);
115
- recovered = recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now) || recovered;
127
+ recovered = recoverQueueEntry(data, subscriber, queueDir, usedNicknames, now, this.agentsFile) || recovered;
116
128
  }
117
129
 
118
130
  recovered = reconcileReservedControllerAliases(data, now) || recovered;
119
131
 
120
132
  if (recovered) {
121
- saveAgentsData(this.agentsFile, data);
133
+ saveAgentsData(this.agentsFile, data, { source: "bus.store.load.recoverQueueEntry", trace: true });
122
134
  }
123
135
  return data;
124
136
  }
125
137
 
126
138
  save(busData) {
127
139
  if (busData) {
128
- saveAgentsData(this.agentsFile, busData);
140
+ saveAgentsData(this.agentsFile, busData, { source: "bus.store.save" });
129
141
  }
130
142
  }
131
143
 
@@ -144,7 +156,7 @@ class BusStore {
144
156
  created_at: getTimestamp(),
145
157
  agents: {},
146
158
  };
147
- saveAgentsData(this.agentsFile, busData);
159
+ saveAgentsData(this.agentsFile, busData, { source: "bus.store.init", trace: true });
148
160
  }
149
161
  }
150
162
 
@@ -2,6 +2,7 @@ const fs = require("fs");
2
2
  const { getTimestamp, isAgentPidAlive, isMetaActive, isValidTty, getTtyProcessInfo } = require("./utils");
3
3
  const NicknameManager = require("./nickname");
4
4
  const { spawnSync } = require("child_process");
5
+ const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnostics");
5
6
 
6
7
  function detectTerminalAppFromEnv() {
7
8
  const termProgram = process.env.TERM_PROGRAM || "";
@@ -102,9 +103,14 @@ function hasProviderSession(meta) {
102
103
  * 订阅者管理
103
104
  */
104
105
  class SubscriberManager {
105
- constructor(busData, queueManager) {
106
+ constructor(busData, queueManager, options = {}) {
106
107
  this.busData = busData;
107
108
  this.queueManager = queueManager;
109
+ this.agentsFile = options.agentsFile || "";
110
+ }
111
+
112
+ logRegistry(event, payload = {}) {
113
+ appendAgentRegistryDiagnostic(this.agentsFile, event, payload);
108
114
  }
109
115
 
110
116
  cleanupSubscriberArtifacts(subscriber) {
@@ -152,6 +158,15 @@ class SubscriberManager {
152
158
  inheritedNickname = meta.nickname;
153
159
  }
154
160
  // Remove stale subscriber using same tty
161
+ this.logRegistry("cleanup_duplicate_tty", {
162
+ source: "bus.subscriber.cleanupDuplicateTty",
163
+ subscriber: id,
164
+ replacement: currentSubscriber,
165
+ tty: ttyPath,
166
+ same_agent_type: sameAgentType,
167
+ status: meta?.status || "",
168
+ nickname: meta?.nickname || "",
169
+ });
155
170
  delete this.busData.agents[id];
156
171
  try {
157
172
  const queueDir = this.queueManager.getQueueDir(id);
@@ -420,6 +435,16 @@ class SubscriberManager {
420
435
  const recoverable = hasProviderSession(meta);
421
436
  if (meta.status === "inactive") {
422
437
  if (!recoverable) {
438
+ this.logRegistry("cleanup_inactive_delete", {
439
+ source: "bus.subscriber.cleanupInactive",
440
+ subscriber: id,
441
+ reason: "internal_already_inactive_without_provider_session",
442
+ status: meta.status || "",
443
+ launch_mode: meta.launch_mode || "",
444
+ pid: meta.pid || 0,
445
+ tty: meta.tty || "",
446
+ last_seen: meta.last_seen || "",
447
+ });
423
448
  delete this.busData.agents[id];
424
449
  this.cleanupSubscriberArtifacts(id);
425
450
  }
@@ -427,11 +452,31 @@ class SubscriberManager {
427
452
  }
428
453
  if (!isMetaActive(meta)) {
429
454
  if (recoverable) {
455
+ this.logRegistry("cleanup_inactive_mark", {
456
+ source: "bus.subscriber.cleanupInactive",
457
+ subscriber: id,
458
+ reason: "internal_inactive_but_recoverable_provider_session",
459
+ status: meta.status || "",
460
+ launch_mode: meta.launch_mode || "",
461
+ pid: meta.pid || 0,
462
+ tty: meta.tty || "",
463
+ last_seen: meta.last_seen || "",
464
+ });
430
465
  meta.status = "inactive";
431
466
  meta.activity_state = "";
432
467
  meta.last_seen = getTimestamp();
433
468
  this.cleanupSubscriberArtifacts(id);
434
469
  } else {
470
+ this.logRegistry("cleanup_inactive_delete", {
471
+ source: "bus.subscriber.cleanupInactive",
472
+ subscriber: id,
473
+ reason: "internal_inactive_without_provider_session",
474
+ status: meta.status || "",
475
+ launch_mode: meta.launch_mode || "",
476
+ pid: meta.pid || 0,
477
+ tty: meta.tty || "",
478
+ last_seen: meta.last_seen || "",
479
+ });
435
480
  delete this.busData.agents[id];
436
481
  this.cleanupSubscriberArtifacts(id);
437
482
  }
@@ -439,6 +484,17 @@ class SubscriberManager {
439
484
  continue;
440
485
  }
441
486
  if (meta.status === "active" && !isMetaActive(meta)) {
487
+ this.logRegistry("cleanup_inactive_mark", {
488
+ source: "bus.subscriber.cleanupInactive",
489
+ subscriber: id,
490
+ reason: "active_meta_failed_liveness",
491
+ status: meta.status || "",
492
+ launch_mode: meta.launch_mode || "",
493
+ pid: meta.pid || 0,
494
+ tty: meta.tty || "",
495
+ tty_shell_pid: meta.tty_shell_pid || 0,
496
+ last_seen: meta.last_seen || "",
497
+ });
442
498
  meta.status = "inactive";
443
499
  meta.activity_state = "";
444
500
  meta.last_seen = getTimestamp();
package/src/bus/utils.js CHANGED
@@ -3,6 +3,7 @@ const fs = require("fs");
3
3
  const path = require("path");
4
4
  const { spawnSync } = require("child_process");
5
5
  const { redactSecrets } = require("../providerapi/redactor");
6
+ const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnostics");
6
7
 
7
8
  /**
8
9
  * 获取当前 UTC 时间戳(ISO 8601 格式)
@@ -200,6 +201,11 @@ function readJSON(filePath, defaultValue = null) {
200
201
  const content = fs.readFileSync(filePath, "utf8");
201
202
  return JSON.parse(content);
202
203
  } catch (err) {
204
+ appendAgentRegistryDiagnostic(filePath, "read_json_failed", {
205
+ source: "bus.utils.readJSON",
206
+ error: err && err.message ? err.message : String(err || "unknown"),
207
+ default_returned: defaultValue === null ? "null" : typeof defaultValue,
208
+ });
203
209
  return defaultValue;
204
210
  }
205
211
  }
@@ -442,7 +442,9 @@ function createDaemonMessageRouter(options = {}) {
442
442
  }
443
443
 
444
444
  function handleErrorMessage(msg) {
445
- resolveStatusLine(`{gray-fg}✗{/gray-fg} Error: ${msg.error}`);
445
+ const error = String(msg.error || "unknown error");
446
+ resolveStatusLine(`{gray-fg}✗{/gray-fg} Error: ${error}`);
447
+ logMessage("error", `{white-fg}✗{/white-fg} ${escapeBlessed(error)}`);
446
448
  renderScreen();
447
449
  return false;
448
450
  }
@@ -1,4 +1,4 @@
1
- const DEFAULT_MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal"];
1
+ const DEFAULT_MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal-pty", "internal"];
2
2
 
3
3
  function createDashboardKeyController(options = {}) {
4
4
  const {
@@ -1,6 +1,6 @@
1
1
  const { clampAgentWindowWithSelection } = require("./agentDirectory");
2
2
 
3
- const DEFAULT_MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal"];
3
+ const DEFAULT_MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal-pty", "internal"];
4
4
 
5
5
  function providerLabel(value) {
6
6
  if (value === "claude-cli") return "claude";
package/src/chat/index.js CHANGED
@@ -65,7 +65,7 @@ const {
65
65
  pruneTransientAgentStates,
66
66
  } = require("./transientAgentState");
67
67
 
68
- const MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal"];
68
+ const MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal-pty", "internal"];
69
69
 
70
70
  async function runChat(projectRoot, options = {}) {
71
71
  const globalMode = options && options.globalMode === true;
package/src/config.js CHANGED
@@ -29,6 +29,7 @@ const DEFAULT_UCODE_CONFIG = {
29
29
  function normalizeLaunchMode(value) {
30
30
  if (value === "auto") return "auto";
31
31
  if (value === "internal") return "internal";
32
+ if (value === "internal-pty") return "internal-pty";
32
33
  if (value === "tmux") return "tmux";
33
34
  if (value === "terminal") return "terminal";
34
35
  if (value === "host") return "host";
package/src/daemon/ops.js CHANGED
@@ -14,6 +14,7 @@ const {
14
14
  const {
15
15
  createSession: createHostSession,
16
16
  } = require("../terminal/adapters/hostAdapter");
17
+ const { resolveDefaultManualBootstrap } = require("../agent/defaultBootstrap");
17
18
 
18
19
  function normalizeLaunchAgent(agent = "") {
19
20
  const value = String(agent || "").trim().toLowerCase();
@@ -44,6 +45,43 @@ function toTmuxBinary(agent = "") {
44
45
  return "";
45
46
  }
46
47
 
48
+ function applyDefaultManagedBootstrap(projectRoot, normalizedAgent, args = [], extraEnv = {}) {
49
+ const agentType = toBusAgentType(normalizedAgent);
50
+ if (!agentType || agentType === "ufoo-code") {
51
+ return {
52
+ args: Array.isArray(args) ? args.slice() : [],
53
+ extraEnv: extraEnv && typeof extraEnv === "object" ? { ...extraEnv } : {},
54
+ applied: false,
55
+ };
56
+ }
57
+
58
+ const currentArgs = Array.isArray(args) ? args.slice() : [];
59
+ const currentExtraEnv = extraEnv && typeof extraEnv === "object" ? { ...extraEnv } : {};
60
+ const resolved = resolveDefaultManualBootstrap({
61
+ projectRoot,
62
+ agentType,
63
+ args: currentArgs,
64
+ env: {
65
+ ...process.env,
66
+ ...currentExtraEnv,
67
+ },
68
+ });
69
+
70
+ if (!resolved || resolved.mode === "skip") {
71
+ return { args: currentArgs, extraEnv: currentExtraEnv, applied: false };
72
+ }
73
+
74
+ return {
75
+ args: Array.isArray(resolved.args) ? resolved.args : currentArgs,
76
+ extraEnv: {
77
+ ...currentExtraEnv,
78
+ ...(resolved.env && typeof resolved.env === "object" ? resolved.env : {}),
79
+ UFOO_SKIP_DEFAULT_BOOTSTRAP: "1",
80
+ },
81
+ applied: true,
82
+ };
83
+ }
84
+
47
85
  function resolveUfooRunnerPath() {
48
86
  return path.resolve(__dirname, "../../bin/ufoo.js");
49
87
  }
@@ -116,7 +154,7 @@ function resolveHostLaunchContext(options = {}) {
116
154
 
117
155
  function resolveConfiguredLaunchMode(configuredMode = "", options = {}) {
118
156
  const mode = normalizeOptionalString(configuredMode);
119
- if (mode === "internal" || mode === "tmux" || mode === "terminal" || mode === "host") {
157
+ if (mode === "internal" || mode === "internal-pty" || mode === "tmux" || mode === "terminal" || mode === "host") {
120
158
  return mode;
121
159
  }
122
160
  const hostContext = resolveHostLaunchContext(options);
@@ -518,7 +556,9 @@ async function spawnManagedHostAgent(
518
556
  subscriberId = "";
519
557
  }
520
558
 
521
- const args = Array.isArray(extraArgs) ? extraArgs : [];
559
+ const hostBootstrap = applyDefaultManagedBootstrap(projectRoot, normalizedAgent, extraArgs, extraEnv);
560
+ const args = hostBootstrap.args;
561
+ const hostExtraEnv = hostBootstrap.extraEnv;
522
562
  const argText = args.length > 0 ? ` ${args.map(shellEscape).join(" ")}` : "";
523
563
 
524
564
  const titleCmd = buildTitleCmd(nickname);
@@ -538,8 +578,8 @@ async function spawnManagedHostAgent(
538
578
  env.UFOO_NICKNAME = nickname;
539
579
  }
540
580
  // Parse extraEnv string (e.g., "UFOO_UCODE_BOOTSTRAP_FILE=/path/to/file") and add to env
541
- if (extraEnv && typeof extraEnv === "object") {
542
- for (const [key, value] of Object.entries(extraEnv)) {
581
+ if (hostExtraEnv && typeof hostExtraEnv === "object") {
582
+ for (const [key, value] of Object.entries(hostExtraEnv)) {
543
583
  if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(String(key || ""))) {
544
584
  env[String(key)] = String(value ?? "");
545
585
  }
@@ -645,7 +685,9 @@ async function spawnInternalAgent(
645
685
  ? (count > 1 ? `${nickname}-${i + 1}` : nickname)
646
686
  : "";
647
687
  const providerSessionId = typeof options.providerSessionId === "string" ? options.providerSessionId.trim() : "";
648
- const usePty = process.env.UFOO_INTERNAL_PTY !== "0";
688
+ const usePty = typeof options.usePty === "boolean"
689
+ ? options.usePty
690
+ : process.env.UFOO_INTERNAL_PTY !== "0";
649
691
  const launchMode = usePty ? "internal-pty" : "internal";
650
692
 
651
693
  // 传递 launch_mode 和 parent PID 到 join
@@ -657,8 +699,9 @@ async function spawnInternalAgent(
657
699
  const finalNickname = joinResult.nickname || requestedNickname || "";
658
700
  bus.saveBusData();
659
701
 
702
+ const managedBootstrap = applyDefaultManagedBootstrap(projectRoot, normalizedAgent, extraArgs, extraEnv);
660
703
  const runnerCmd = usePty ? "agent-pty-runner" : "agent-runner";
661
- const args = Array.isArray(extraArgs) ? extraArgs : [];
704
+ const args = managedBootstrap.args;
662
705
  const child = spawn(process.execPath, [runner, runnerCmd, agent, ...args], {
663
706
  // 关键改动:不使用 detached,daemon 作为父进程
664
707
  detached: false,
@@ -666,7 +709,7 @@ async function spawnInternalAgent(
666
709
  cwd: projectRoot,
667
710
  env: {
668
711
  ...process.env,
669
- ...(extraEnv && typeof extraEnv === "object" ? extraEnv : {}),
712
+ ...(managedBootstrap.extraEnv && typeof managedBootstrap.extraEnv === "object" ? managedBootstrap.extraEnv : {}),
670
713
  UFOO_INTERNAL_AGENT: "1",
671
714
  UFOO_INTERNAL_PTY: usePty ? "1" : "0",
672
715
  UFOO_SUBSCRIBER_ID: subscriberId, // 直接传递 subscriber ID
@@ -888,7 +931,8 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
888
931
  throw new Error(`unsupported agent type: ${agent}`);
889
932
  }
890
933
 
891
- if (mode === "internal") {
934
+ if (mode === "internal" || mode === "internal-pty") {
935
+ const usePty = mode === "internal-pty";
892
936
  const result = await spawnInternalAgent(
893
937
  projectRoot,
894
938
  normalizedAgent,
@@ -896,9 +940,10 @@ async function launchAgent(projectRoot, agent, count = 1, nickname = "", process
896
940
  nickname,
897
941
  processManager,
898
942
  launchEnvObject,
899
- extraArgs
943
+ extraArgs,
944
+ { usePty }
900
945
  );
901
- return { mode: "internal", launchScope, subscriberIds: result.subscriberIds };
946
+ return { mode, launchScope, subscriberIds: result.subscriberIds };
902
947
  }
903
948
  if (mode === "tmux") {
904
949
  // Check if tmux is available
@@ -1129,7 +1174,7 @@ async function resumeAgents(projectRoot, target = "", processManager = null) {
1129
1174
  const args = buildResumeArgs(item.agent, sessionId);
1130
1175
  let reused = false;
1131
1176
  let resumedId = item.id;
1132
- if (mode === "internal") {
1177
+ if (mode === "internal" || mode === "internal-pty") {
1133
1178
  // Internal agents have no terminal/pane to reattach. Start a fresh
1134
1179
  // daemon-managed runner and replace the old recoverable registration.
1135
1180
  // The provider session is still reused via the normal provider args.
@@ -1142,7 +1187,7 @@ async function resumeAgents(projectRoot, target = "", processManager = null) {
1142
1187
  processManager,
1143
1188
  { UFOO_SKIP_SESSION_PROBE: "1" },
1144
1189
  args,
1145
- { replaceAgentId: item.id, providerSessionId: sessionId }
1190
+ { replaceAgentId: item.id, providerSessionId: sessionId, usePty: mode === "internal-pty" }
1146
1191
  );
1147
1192
  resumedId = launchResult.subscriberIds && launchResult.subscriberIds[0]
1148
1193
  ? launchResult.subscriberIds[0]
@@ -0,0 +1,91 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ function isAgentsFile(filePath) {
5
+ return path.basename(filePath || "") === "all-agents.json"
6
+ && path.basename(path.dirname(filePath || "")) === "agent";
7
+ }
8
+
9
+ function getRegistryLogPath(agentsFilePath) {
10
+ const ufooRoot = path.dirname(path.dirname(agentsFilePath));
11
+ return path.join(ufooRoot, "run", "agent-registry-diagnostics.log");
12
+ }
13
+
14
+ function summarizeFile(filePath) {
15
+ try {
16
+ const stat = fs.statSync(filePath);
17
+ return {
18
+ exists: true,
19
+ size: stat.size,
20
+ mtime: stat.mtime.toISOString(),
21
+ };
22
+ } catch (err) {
23
+ return {
24
+ exists: false,
25
+ error: err && err.code ? err.code : String(err || "unknown"),
26
+ };
27
+ }
28
+ }
29
+
30
+ function summarizeAgents(data) {
31
+ const agents = data && typeof data === "object" && data.agents && typeof data.agents === "object"
32
+ ? data.agents
33
+ : {};
34
+ const ids = Object.keys(agents).sort();
35
+ const statuses = {};
36
+ const nicknames = {};
37
+ for (const id of ids) {
38
+ const meta = agents[id] || {};
39
+ const status = typeof meta.status === "string" && meta.status ? meta.status : "unknown";
40
+ statuses[status] = (statuses[status] || 0) + 1;
41
+ if (typeof meta.nickname === "string" && meta.nickname) {
42
+ nicknames[id] = meta.nickname;
43
+ }
44
+ }
45
+ return {
46
+ count: ids.length,
47
+ ids,
48
+ statuses,
49
+ nicknames,
50
+ };
51
+ }
52
+
53
+ function safePayload(payload = {}) {
54
+ const out = {};
55
+ for (const [key, value] of Object.entries(payload || {})) {
56
+ if (/token|secret|password|credential|auth/i.test(key)) {
57
+ out[key] = "[REDACTED]";
58
+ } else {
59
+ out[key] = value;
60
+ }
61
+ }
62
+ return out;
63
+ }
64
+
65
+ function appendAgentRegistryDiagnostic(agentsFilePath, event, payload = {}) {
66
+ if (!agentsFilePath || !isAgentsFile(agentsFilePath)) return;
67
+ try {
68
+ const logPath = getRegistryLogPath(agentsFilePath);
69
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
70
+ const line = JSON.stringify({
71
+ ts: new Date().toISOString(),
72
+ pid: process.pid,
73
+ ppid: process.ppid,
74
+ event,
75
+ agents_file: agentsFilePath,
76
+ file: summarizeFile(agentsFilePath),
77
+ ...safePayload(payload),
78
+ });
79
+ fs.appendFileSync(logPath, `${line}\n`, "utf8");
80
+ } catch {
81
+ // Diagnostics must never affect agent liveness paths.
82
+ }
83
+ }
84
+
85
+ module.exports = {
86
+ appendAgentRegistryDiagnostic,
87
+ summarizeAgents,
88
+ summarizeFile,
89
+ isAgentsFile,
90
+ getRegistryLogPath,
91
+ };