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.
- package/package.json +1 -1
- package/src/agent/activityStateWriter.js +14 -3
- package/src/agent/defaultBootstrap.js +28 -3
- package/src/agent/internalRunner.js +139 -127
- package/src/agent/notifier.js +15 -4
- package/src/agent/ptyRunner.js +70 -8
- package/src/agent/ucodeBootstrap.js +23 -1
- package/src/agent/ufooAgent.js +7 -36
- package/src/bus/index.js +2 -1
- package/src/bus/store.js +17 -5
- package/src/bus/subscriber.js +57 -1
- package/src/bus/utils.js +6 -0
- package/src/chat/daemonMessageRouter.js +3 -1
- package/src/chat/dashboardKeyController.js +1 -1
- package/src/chat/dashboardView.js +1 -1
- package/src/chat/index.js +1 -1
- package/src/config.js +1 -0
- package/src/daemon/ops.js +57 -12
- package/src/ufoo/agentRegistryDiagnostics.js +91 -0
- package/src/ufoo/agentsStore.js +38 -2
- package/src/agent/cliRunner.js +0 -706
- package/src/agent/normalizeOutput.js +0 -41
|
@@ -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:
|
|
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,
|
package/src/agent/ufooAgent.js
CHANGED
|
@@ -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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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 =
|
|
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
|
|
package/src/bus/subscriber.js
CHANGED
|
@@ -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
|
-
|
|
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,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
|
|
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 (
|
|
542
|
-
for (const [key, value] of Object.entries(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
+
};
|