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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.3.11",
3
+ "version": "2.3.13",
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",
@@ -1,4 +1,6 @@
1
1
  const fs = require("fs");
2
+ const { readJSON, writeJSON } = require("../bus/utils");
3
+ const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnostics");
2
4
 
3
5
  /**
4
6
  * Centralized helper for writing activity_state to all-agents.json.
@@ -12,8 +14,17 @@ function writeActivityState(agentsFilePath, subscriber, state, options = {}) {
12
14
  const { since, force = false } = options;
13
15
  try {
14
16
  if (!agentsFilePath || !fs.existsSync(agentsFilePath)) return false;
15
- const data = JSON.parse(fs.readFileSync(agentsFilePath, "utf8"));
16
- if (!data.agents || !data.agents[subscriber]) return false;
17
+ const data = readJSON(agentsFilePath, null);
18
+ if (!data) return false;
19
+ if (!data.agents || !data.agents[subscriber]) {
20
+ appendAgentRegistryDiagnostic(agentsFilePath, "activity_state_subscriber_missing", {
21
+ source: "agent.activityStateWriter.writeActivityState",
22
+ subscriber,
23
+ state,
24
+ known_ids: Object.keys(data.agents || {}).sort(),
25
+ });
26
+ return false;
27
+ }
17
28
 
18
29
  const current = data.agents[subscriber].activity_state;
19
30
 
@@ -30,7 +41,7 @@ function writeActivityState(agentsFilePath, subscriber, state, options = {}) {
30
41
  data.agents[subscriber].activity_since = since
31
42
  ? new Date(since).toISOString()
32
43
  : new Date().toISOString();
33
- fs.writeFileSync(agentsFilePath, JSON.stringify(data, null, 2));
44
+ writeJSON(agentsFilePath, data);
34
45
  return true;
35
46
  } catch {
36
47
  return false;
@@ -23,7 +23,7 @@ function hasArg(args = [], names = []) {
23
23
  }
24
24
 
25
25
  function hasMetaCommandArgs(args = []) {
26
- return hasArg(args, ["-h", "--help", "-v", "--version"]);
26
+ return hasArg(args, ["-h", "--help", "-v", "--version", "resume"]);
27
27
  }
28
28
 
29
29
  function readOptionalFile(filePath) {
@@ -54,7 +54,7 @@ function buildDefaultStartupBootstrapPrompt({ agentType = "", projectRoot = "" }
54
54
  const normalizedAgent = asTrimmedString(agentType).toLowerCase();
55
55
  const displayAgent = normalizedAgent === "claude-code"
56
56
  ? "Claude"
57
- : (normalizedAgent === "codex" ? "Codex" : "agent");
57
+ : (normalizedAgent === "codex" ? "Codex" : (normalizedAgent === "ufoo-code" ? "ucode" : "agent"));
58
58
 
59
59
  const segments = [
60
60
  `Session bootstrap for ${displayAgent}.`,
@@ -102,6 +102,28 @@ function mergePromptSegments(...segments) {
102
102
  .join("\n\n");
103
103
  }
104
104
 
105
+ const CODEX_OPTIONS_WITH_VALUE = new Set([
106
+ "-m",
107
+ "--model",
108
+ "-c",
109
+ "--config",
110
+ "--profile",
111
+ "--sandbox",
112
+ "-s",
113
+ "--approval-mode",
114
+ "--ask-for-approval",
115
+ "--cd",
116
+ "--cwd",
117
+ "--color",
118
+ "--output-schema",
119
+ ]);
120
+
121
+ function isValueForCodexOption(args = [], index = -1) {
122
+ if (!Array.isArray(args) || index <= 0) return false;
123
+ const previous = asTrimmedString(args[index - 1]);
124
+ return CODEX_OPTIONS_WITH_VALUE.has(previous);
125
+ }
126
+
105
127
  function mergeClaudePromptArgs({
106
128
  projectRoot,
107
129
  agentType = "claude-code",
@@ -160,7 +182,9 @@ function mergeCodexPromptArgs({ args = [], bootstrapText = "" } = {}) {
160
182
  const lastIndex = currentArgs.length - 1;
161
183
  if (lastIndex < 0) return null;
162
184
  const lastItem = asTrimmedString(currentArgs[lastIndex]);
163
- const promptIndex = lastItem && !lastItem.startsWith("-") ? lastIndex : -1;
185
+ const promptIndex = lastItem && !lastItem.startsWith("-") && !isValueForCodexOption(currentArgs, lastIndex)
186
+ ? lastIndex
187
+ : -1;
164
188
 
165
189
  if (promptIndex < 0) return null;
166
190
 
@@ -262,6 +286,7 @@ function resolveDefaultManualBootstrap({
262
286
  module.exports = {
263
287
  hasArg,
264
288
  hasMetaCommandArgs,
289
+ isValueForCodexOption,
265
290
  buildDefaultStartupBootstrapPrompt,
266
291
  defaultBootstrapFile,
267
292
  prepareDefaultBootstrapFile,
@@ -3,8 +3,7 @@ const path = require("path");
3
3
  const { getUfooPaths } = require("../ufoo/paths");
4
4
  const { spawnSync } = require("child_process");
5
5
  const EventBus = require("../bus");
6
- const { runCliAgent } = require("./cliRunner");
7
- const { normalizeCliOutput } = require("./normalizeOutput");
6
+ const { readJSON, writeJSON } = require("../bus/utils");
8
7
  const { createActivityStatePublisher } = require("./activityStatePublisher");
9
8
  const { loadConfig, normalizeCodexInternalThreadMode } = require("../config");
10
9
  const { createCodexThreadProvider } = require("./codexThreadProvider");
@@ -15,6 +14,11 @@ const { listToolsForCallerTier, CALLER_TIERS } = require("../tools");
15
14
  const { redactToolCallPayload, redactSecrets } = require("../providerapi/redactor");
16
15
  const { buildCachedMemoryPrefix } = require("../memory");
17
16
  const { shouldForwardStreamToPublisher } = require("./publisherRouting");
17
+ const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnostics");
18
+ const {
19
+ buildDefaultStartupBootstrapPrompt,
20
+ isValueForCodexOption,
21
+ } = require("./defaultBootstrap");
18
22
 
19
23
  function sleep(ms) {
20
24
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -56,6 +60,98 @@ function safeSubscriber(subscriber) {
56
60
  return subscriber.replace(/:/g, "_");
57
61
  }
58
62
 
63
+ function readFileSafe(filePath = "") {
64
+ const target = String(filePath || "").trim();
65
+ if (!target) return "";
66
+ try {
67
+ return fs.readFileSync(target, "utf8");
68
+ } catch {
69
+ return "";
70
+ }
71
+ }
72
+
73
+ function hasUfooProtocolPrompt(promptText = "") {
74
+ const text = String(promptText || "");
75
+ return text.includes("ufoo protocol:") && text.includes("ufoo ctx decisions -l");
76
+ }
77
+
78
+ function hasPromptArg(args = []) {
79
+ if (!Array.isArray(args) || args.length === 0) return false;
80
+ const lastIndex = args.length - 1;
81
+ const lastItem = String(args[lastIndex] || "").trim();
82
+ if (!lastItem || lastItem.startsWith("-")) return false;
83
+ return !isValueForCodexOption(args, lastIndex);
84
+ }
85
+
86
+ function consumeClaudeAppendSystemPrompt(args = []) {
87
+ const nextArgs = [];
88
+ const promptSegments = [];
89
+ for (let index = 0; index < args.length; index += 1) {
90
+ const item = String(args[index] || "");
91
+ if (item === "--append-system-prompt") {
92
+ const filePath = String(args[index + 1] || "");
93
+ const content = readFileSafe(filePath);
94
+ if (content.trim()) promptSegments.push(content.trim());
95
+ index += 1;
96
+ continue;
97
+ }
98
+ if (item.startsWith("--append-system-prompt=")) {
99
+ const filePath = item.slice("--append-system-prompt=".length);
100
+ const content = readFileSafe(filePath);
101
+ if (content.trim()) promptSegments.push(content.trim());
102
+ continue;
103
+ }
104
+ nextArgs.push(item);
105
+ }
106
+ return {
107
+ args: nextArgs,
108
+ promptText: promptSegments.filter(Boolean).join("\n\n"),
109
+ };
110
+ }
111
+
112
+ function resolveInternalBootstrap({
113
+ projectRoot = process.cwd(),
114
+ agentType = "codex",
115
+ extraArgs = [],
116
+ env = process.env,
117
+ } = {}) {
118
+ const normalizedAgent = String(agentType || "").trim().toLowerCase();
119
+ const bootstrapAgentType = normalizedAgent === "claude" || normalizedAgent === "claude-code"
120
+ ? "claude-code"
121
+ : (normalizedAgent === "codex" ? "codex" : "");
122
+ const args = Array.isArray(extraArgs) ? extraArgs.slice() : [];
123
+ if (!bootstrapAgentType) {
124
+ return { promptText: "", extraArgs: args };
125
+ }
126
+
127
+ let promptText = "";
128
+ let nextArgs = args;
129
+
130
+ if (bootstrapAgentType === "claude-code") {
131
+ const consumed = consumeClaudeAppendSystemPrompt(args);
132
+ promptText = consumed.promptText;
133
+ nextArgs = consumed.args;
134
+ } else if (hasPromptArg(args)) {
135
+ promptText = String(args[args.length - 1] || "").trim();
136
+ nextArgs = args.slice(0, -1);
137
+ } else {
138
+ promptText = String(env.UFOO_STARTUP_BOOTSTRAP_TEXT || "").trim();
139
+ }
140
+
141
+ if (!hasUfooProtocolPrompt(promptText)) {
142
+ const defaultPrompt = buildDefaultStartupBootstrapPrompt({
143
+ agentType: bootstrapAgentType,
144
+ projectRoot,
145
+ }).trim();
146
+ promptText = [defaultPrompt, promptText.trim()].filter(Boolean).join("\n\n");
147
+ }
148
+
149
+ return {
150
+ promptText,
151
+ extraArgs: nextArgs,
152
+ };
153
+ }
154
+
59
155
  function buildMemoryPrefix(projectRoot, limit = 50) {
60
156
  try {
61
157
  return buildCachedMemoryPrefix(projectRoot, { limit }).prefix.trim();
@@ -88,10 +184,6 @@ function createBusSender(projectRoot, subscriber) {
88
184
  return { enqueue, flush };
89
185
  }
90
186
 
91
- function shouldFallbackToLegacyThreadProvider() {
92
- return false;
93
- }
94
-
95
187
  function drainQueue(queueFile) {
96
188
  if (!fs.existsSync(queueFile)) return [];
97
189
  const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
@@ -200,32 +292,29 @@ async function handleEvent(
200
292
  subscriber,
201
293
  nickname,
202
294
  evt,
203
- cliSessionState,
204
295
  busSender,
205
296
  extraArgs = [],
206
- threadRuntime = null
297
+ threadRuntime = null,
298
+ bootstrapText = ""
207
299
  ) {
208
300
  if (!evt || !evt.data || !evt.data.message) return;
209
301
  const memoryPrefix = buildMemoryPrefix(projectRoot);
210
- const prompt = memoryPrefix
211
- ? `${memoryPrefix}\n\n${evt.data.message}`
212
- : evt.data.message;
302
+ const prompt = [bootstrapText, memoryPrefix, evt.data.message]
303
+ .map((item) => String(item || "").trim())
304
+ .filter(Boolean)
305
+ .join("\n\n");
213
306
  const publisher = evt.publisher || "unknown";
214
- const sandbox = "workspace-write";
215
- const streamState = { emitted: false, lastChar: "" };
216
307
  const streamToPublisher = shouldForwardStreamToPublisher(projectRoot, publisher);
217
308
 
218
309
  const emitStreamDelta = (delta) => {
219
310
  const text = String(delta || "");
220
311
  if (!text) return;
221
312
  if (!streamToPublisher) return;
222
- streamState.emitted = true;
223
- streamState.lastChar = text.slice(-1);
224
313
  busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: text }));
225
314
  };
226
315
 
227
316
  if (threadRuntime && threadRuntime.enabled && threadRuntime.thread) {
228
- const threadedResult = await handleThreadedEvent({
317
+ await handleThreadedEvent({
229
318
  agentType,
230
319
  provider,
231
320
  publisher,
@@ -235,74 +324,18 @@ async function handleEvent(
235
324
  streamToPublisher,
236
325
  threadRuntime,
237
326
  });
238
- if (!threadedResult || !threadedResult.fallbackToLegacy) {
239
- return;
240
- }
241
- }
242
-
243
- let res = await runCliAgent({
244
- provider,
245
- model,
246
- prompt,
247
- sessionId: cliSessionState.cliSessionId,
248
- sandbox,
249
- cwd: projectRoot,
250
- extraArgs,
251
- onStreamDelta: emitStreamDelta,
252
- });
253
-
254
- // Handle session errors with immediate retry (only for claude)
255
- if (!res.ok && provider === "claude-cli") {
256
- const errMsg = (res.error || "").toLowerCase();
257
- if (errMsg.includes("session") || errMsg.includes("already in use")) {
258
- // Clear session and retry immediately with new session
259
- cliSessionState.cliSessionId = null;
260
- cliSessionState.needsSave = true;
261
-
262
- res = await runCliAgent({
263
- provider,
264
- model,
265
- prompt,
266
- sessionId: null, // Let runCliAgent generate new session
267
- sandbox,
268
- cwd: projectRoot,
269
- extraArgs,
270
- onStreamDelta: emitStreamDelta,
271
- });
272
- }
273
- }
274
-
275
- // Update CLI session ID for continuity (only for claude)
276
- if (res.ok && res.sessionId && provider === "claude-cli") {
277
- cliSessionState.cliSessionId = res.sessionId;
278
- cliSessionState.needsSave = true;
327
+ return;
279
328
  }
280
329
 
281
- let reply = "";
282
- if (res.ok) {
283
- reply = normalizeCliOutput(res.output) || "";
330
+ const errorText = `[internal:${agentType}] error: no thread runtime available for provider ${provider}; cliRunner fallback has been removed`;
331
+ // eslint-disable-next-line no-console
332
+ console.error(errorText);
333
+ if (streamToPublisher) {
334
+ busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: errorText }));
335
+ busSender.enqueue(publisher, JSON.stringify({ stream: true, done: true, reason: "error" }));
284
336
  } else {
285
- reply = `[internal:${agentType}] error: ${res.error || "unknown error"}`;
286
- }
287
-
288
- if (streamState.emitted) {
289
- if (!res.ok) {
290
- if (streamState.lastChar !== "\n") {
291
- busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: "\n" }));
292
- }
293
- busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: reply }));
294
- }
295
- busSender.enqueue(
296
- publisher,
297
- JSON.stringify({ stream: true, done: true, reason: res.ok ? "complete" : "error" })
298
- );
299
- await busSender.flush();
300
- return;
337
+ busSender.enqueue(publisher, errorText);
301
338
  }
302
-
303
- if (!reply) return;
304
-
305
- busSender.enqueue(publisher, reply);
306
339
  await busSender.flush();
307
340
  }
308
341
 
@@ -348,13 +381,12 @@ async function handleThreadedEvent({
348
381
  }
349
382
  await busSender.flush();
350
383
  } catch (err) {
351
- if (shouldFallbackToLegacyThreadProvider(err, provider)) {
352
- return { fallbackToLegacy: true };
353
- }
354
384
  if (threadRuntime && typeof threadRuntime.rebuildThread === "function") {
355
385
  await threadRuntime.rebuildThread();
356
386
  }
357
387
  const errorText = `[internal:${agentType}] error: ${err && err.message ? err.message : "unknown error"}`;
388
+ // eslint-disable-next-line no-console
389
+ console.error(errorText);
358
390
  if (streamToPublisher) {
359
391
  busSender.enqueue(
360
392
  publisher,
@@ -368,7 +400,6 @@ async function handleThreadedEvent({
368
400
  busSender.enqueue(publisher, errorText);
369
401
  }
370
402
  await busSender.flush();
371
- return { fallbackToLegacy: false };
372
403
  }
373
404
  }
374
405
 
@@ -471,8 +502,9 @@ function buildWorkerThreadToolRuntime({ projectRoot, subscriber, observer }) {
471
502
  function getClaudeThreadMode() {
472
503
  const envValue = process.env.UFOO_CLAUDE_INTERNAL_THREAD_MODE;
473
504
  const raw = String(envValue || "").trim().toLowerCase();
505
+ if (raw === "legacy" || raw === "off" || raw === "disabled" || raw === "0") return "legacy";
474
506
  if (raw === "api") return "api";
475
- return "legacy";
507
+ return "api";
476
508
  }
477
509
 
478
510
  function buildClaudeAuthProvider(projectRoot) {
@@ -493,14 +525,22 @@ function persistProviderSessionId(projectRoot, subscriber, providerSessionId) {
493
525
  try {
494
526
  const agentsFile = getUfooPaths(projectRoot).agentsFile;
495
527
  const parsed = fs.existsSync(agentsFile)
496
- ? JSON.parse(fs.readFileSync(agentsFile, "utf8"))
528
+ ? readJSON(agentsFile, null)
497
529
  : {};
530
+ if (!parsed) return false;
498
531
  if (!parsed.agents || typeof parsed.agents !== "object") return false;
499
- if (!parsed.agents[subscriber] || typeof parsed.agents[subscriber] !== "object") return false;
532
+ if (!parsed.agents[subscriber] || typeof parsed.agents[subscriber] !== "object") {
533
+ appendAgentRegistryDiagnostic(agentsFile, "provider_session_subscriber_missing", {
534
+ source: "agent.internalRunner.persistProviderSessionId",
535
+ subscriber,
536
+ known_ids: Object.keys(parsed.agents || {}).sort(),
537
+ });
538
+ return false;
539
+ }
500
540
  if (parsed.agents[subscriber].provider_session_id === id) return false;
501
541
  parsed.agents[subscriber].provider_session_id = id;
502
542
  parsed.agents[subscriber].provider_session_updated_at = new Date().toISOString();
503
- fs.writeFileSync(agentsFile, `${JSON.stringify(parsed, null, 2)}\n`);
543
+ writeJSON(agentsFile, parsed);
504
544
  return true;
505
545
  } catch {
506
546
  return false;
@@ -648,35 +688,23 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
648
688
  }
649
689
  const provider = normalizedAgentType === "codex" ? "codex-cli" : "claude-cli";
650
690
  const model = process.env.UFOO_AGENT_MODEL || "";
691
+ const bootstrap = resolveInternalBootstrap({
692
+ projectRoot,
693
+ agentType: normalizedAgentType,
694
+ extraArgs,
695
+ env: process.env,
696
+ });
651
697
  const busSender = createBusSender(projectRoot, subscriber);
652
698
  const interactiveSessions = new Map();
653
699
  const threadRuntime = createThreadRuntime({
654
700
  projectRoot,
655
701
  provider,
656
702
  model,
657
- extraArgs,
703
+ extraArgs: bootstrap.extraArgs,
658
704
  subscriber,
659
705
  providerSessionId: process.env.UFOO_PROVIDER_SESSION_ID || "",
660
706
  });
661
707
 
662
- // Session state management for CLI continuity
663
- // Use stable path based on nickname (if exists) or agent type, NOT subscriber ID
664
- const stableKey = nickname || `${agentType}-default`;
665
- const sessionDir = path.join(getUfooPaths(projectRoot).agentDir, "sessions");
666
- fs.mkdirSync(sessionDir, { recursive: true });
667
- const stateFile = path.join(sessionDir, `${stableKey}.json`);
668
-
669
- let cliSessionId = null;
670
- // Only load session for claude (codex doesn't support sessions)
671
- if (provider === "claude-cli") {
672
- try {
673
- const state = JSON.parse(fs.readFileSync(stateFile, "utf8"));
674
- cliSessionId = state.cliSessionId;
675
- } catch {
676
- // No previous session
677
- }
678
- }
679
-
680
708
  let running = true;
681
709
  let processing = false;
682
710
  let lastHeartbeat = 0;
@@ -689,7 +717,6 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
689
717
  process.on("SIGTERM", stop);
690
718
  process.on("SIGINT", stop);
691
719
 
692
- const cliSessionState = { cliSessionId, needsSave: false };
693
720
  const agentsFile = getUfooPaths(projectRoot).agentsFile;
694
721
  const activityPublisher = createActivityStatePublisher({
695
722
  agentsFile, subscriber, projectRoot,
@@ -785,30 +812,15 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
785
812
  subscriber,
786
813
  nickname,
787
814
  evt,
788
- cliSessionState,
789
815
  busSender,
790
- extraArgs,
791
- threadRuntime
816
+ bootstrap.extraArgs,
817
+ threadRuntime,
818
+ bootstrap.promptText
792
819
  );
793
820
  if (evt.__agentViewRaw) {
794
821
  getInteractiveSession(evt.publisher || "unknown").writeResponsePrompt();
795
822
  }
796
823
  }
797
-
798
- // Persist CLI session state after processing (only if changed and for claude)
799
- if (cliSessionState.needsSave && provider === "claude-cli") {
800
- try {
801
- fs.writeFileSync(stateFile, JSON.stringify({
802
- cliSessionId: cliSessionState.cliSessionId,
803
- nickname: nickname || "",
804
- updated_at: new Date().toISOString(),
805
- }));
806
- cliSessionState.needsSave = false;
807
- } catch {
808
- // ignore save errors
809
- }
810
- }
811
-
812
824
  // 处理消息后更新心跳
813
825
  updateHeartbeat();
814
826
  lastHeartbeat = now;
@@ -839,8 +851,8 @@ module.exports = {
839
851
  normalizeWorkerThreadToolMode,
840
852
  getClaudeThreadMode,
841
853
  buildClaudeAuthProvider,
842
- shouldFallbackToLegacyThreadProvider,
843
854
  parseAgentViewRawInput,
844
855
  createInteractiveInputSession,
856
+ resolveInternalBootstrap,
845
857
  persistProviderSessionId,
846
858
  };
@@ -1,8 +1,10 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const EventBus = require("../bus");
4
+ const { readJSON, writeJSON } = require("../bus/utils");
4
5
  const Injector = require("../bus/inject");
5
6
  const { getUfooPaths } = require("../ufoo/paths");
7
+ const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnostics");
6
8
  const { shakeTerminalByTty } = require("../bus/shake");
7
9
  const { isITerm2 } = require("../terminal/detect");
8
10
  const iterm2 = require("../terminal/iterm2");
@@ -62,7 +64,8 @@ class AgentNotifier {
62
64
  getNickname() {
63
65
  try {
64
66
  if (!this.agentsFile || !fs.existsSync(this.agentsFile)) return "";
65
- const data = JSON.parse(fs.readFileSync(this.agentsFile, "utf8"));
67
+ const data = readJSON(this.agentsFile, null);
68
+ if (!data) return "";
66
69
  const meta = data.agents && data.agents[this.subscriber];
67
70
  return (meta && meta.nickname) ? String(meta.nickname) : "";
68
71
  } catch {
@@ -109,11 +112,18 @@ class AgentNotifier {
109
112
  updateHeartbeat() {
110
113
  try {
111
114
  if (!this.agentsFile || !fs.existsSync(this.agentsFile)) return;
112
- const data = JSON.parse(fs.readFileSync(this.agentsFile, "utf8"));
115
+ const data = readJSON(this.agentsFile, null);
116
+ if (!data) return;
113
117
  if (data.agents && data.agents[this.subscriber]) {
114
118
  data.agents[this.subscriber].last_seen = new Date().toISOString();
115
- fs.writeFileSync(this.agentsFile, JSON.stringify(data, null, 2));
119
+ writeJSON(this.agentsFile, data);
120
+ return;
116
121
  }
122
+ appendAgentRegistryDiagnostic(this.agentsFile, "heartbeat_subscriber_missing", {
123
+ source: "agent.notifier.updateHeartbeat",
124
+ subscriber: this.subscriber,
125
+ known_ids: Object.keys(data.agents || {}).sort(),
126
+ });
117
127
  } catch {
118
128
  // 心跳更新失败时静默忽略
119
129
  }
@@ -132,7 +142,8 @@ class AgentNotifier {
132
142
  getCurrentActivityState() {
133
143
  try {
134
144
  if (!this.agentsFile || !fs.existsSync(this.agentsFile)) return "";
135
- const data = JSON.parse(fs.readFileSync(this.agentsFile, "utf8"));
145
+ const data = readJSON(this.agentsFile, null);
146
+ if (!data) return "";
136
147
  const meta = data.agents && data.agents[this.subscriber];
137
148
  return meta && typeof meta.activity_state === "string"
138
149
  ? String(meta.activity_state).trim().toLowerCase()
@@ -12,6 +12,10 @@ const {
12
12
  shouldAutoReplyFromPtyToPublisher,
13
13
  shouldForwardStreamToPublisher,
14
14
  } = require("./publisherRouting");
15
+ const {
16
+ isValueForCodexOption,
17
+ resolveDefaultManualBootstrap,
18
+ } = require("./defaultBootstrap");
15
19
 
16
20
  function sleep(ms) {
17
21
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -116,22 +120,70 @@ function computeInjectedSubmitDelayMs(agentType, text) {
116
120
  return delayMs;
117
121
  }
118
122
 
119
- function resolveCommand(agentType, extraArgs = []) {
123
+ function hasPromptArg(args = []) {
124
+ if (!Array.isArray(args) || args.length === 0) return false;
125
+ const lastIndex = args.length - 1;
126
+ const lastItem = String(args[lastIndex] || "").trim();
127
+ if (!lastItem || lastItem.startsWith("-")) return false;
128
+ return !isValueForCodexOption(args, lastIndex);
129
+ }
130
+
131
+ function appendStartupBootstrapArg(agentType, extraArgs = [], env = process.env) {
132
+ const normalizedAgent = String(agentType || "").trim().toLowerCase();
133
+ const args = Array.isArray(extraArgs) ? extraArgs.slice() : [];
134
+ if (normalizedAgent !== "codex") return args;
135
+ if (hasPromptArg(args)) return args;
136
+ const startupBootstrapText = String(env.UFOO_STARTUP_BOOTSTRAP_TEXT || "").trim();
137
+ if (!startupBootstrapText) return args;
138
+ return [...args, startupBootstrapText];
139
+ }
140
+
141
+ function resolvePtyBootstrapArgs(agentType, extraArgs = [], {
142
+ projectRoot = process.cwd(),
143
+ env = process.env,
144
+ } = {}) {
145
+ const normalizedAgent = String(agentType || "").trim().toLowerCase();
146
+ const args = Array.isArray(extraArgs) ? extraArgs.slice() : [];
147
+ if (normalizedAgent !== "codex" && normalizedAgent !== "claude" && normalizedAgent !== "claude-code") {
148
+ return { args, env: {} };
149
+ }
150
+
151
+ const bootstrapAgentType = normalizedAgent === "codex" ? "codex" : "claude-code";
152
+ const resolved = resolveDefaultManualBootstrap({
153
+ projectRoot,
154
+ agentType: bootstrapAgentType,
155
+ args,
156
+ env,
157
+ });
158
+ const resolvedArgs = resolved && resolved.mode !== "skip" && Array.isArray(resolved.args)
159
+ ? resolved.args
160
+ : args;
161
+ const resolvedEnv = resolved && resolved.mode !== "skip" && resolved.env && typeof resolved.env === "object"
162
+ ? resolved.env
163
+ : {};
164
+ return {
165
+ args: appendStartupBootstrapArg(normalizedAgent, resolvedArgs, { ...env, ...resolvedEnv }),
166
+ env: resolvedEnv,
167
+ };
168
+ }
169
+
170
+ function resolveCommand(agentType, extraArgs = [], options = {}) {
120
171
  const normalizedAgent = String(agentType || "").trim().toLowerCase();
121
- const extra = Array.isArray(extraArgs) ? extraArgs : [];
172
+ const bootstrap = resolvePtyBootstrapArgs(normalizedAgent, extraArgs, options);
173
+ const extra = bootstrap.args;
122
174
  const rawCmd = String(process.env.UFOO_PTY_CMD || "").trim();
123
175
  if (rawCmd) {
124
176
  const rawArgs = String(process.env.UFOO_PTY_ARGS || "").trim();
125
177
  const args = rawArgs ? rawArgs.split(/\s+/).filter(Boolean) : [];
126
- return { command: rawCmd, args: [...args, ...extra] };
178
+ return { command: rawCmd, args: [...args, ...extra], env: bootstrap.env };
127
179
  }
128
180
  if (normalizedAgent === "claude" || normalizedAgent === "claude-code") {
129
- return { command: "claude", args: [...extra] };
181
+ return { command: "claude", args: [...extra], env: bootstrap.env };
130
182
  }
131
183
  if (normalizedAgent === "ufoo" || normalizedAgent === "ucode" || normalizedAgent === "ufoo-code") {
132
- return { command: "ucode", args: [...extra] };
184
+ return { command: "ucode", args: [...extra], env: bootstrap.env };
133
185
  }
134
- return { command: "codex", args: ["--no-alt-screen", "--sandbox", "workspace-write", ...extra] };
186
+ return { command: "codex", args: ["--no-alt-screen", "--sandbox", "workspace-write", ...extra], env: bootstrap.env };
135
187
  }
136
188
 
137
189
  async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }) {
@@ -160,9 +212,13 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
160
212
  const logFile = path.join(runDir, "pty-runner.log");
161
213
  const injectSockPath = path.join(queueDir, "inject.sock");
162
214
 
163
- const { command, args } = resolveCommand(agentType, extraArgs);
215
+ const { command, args, env: commandEnv } = resolveCommand(agentType, extraArgs, {
216
+ projectRoot,
217
+ env: process.env,
218
+ });
164
219
  const env = {
165
220
  ...process.env,
221
+ ...(commandEnv && typeof commandEnv === "object" ? commandEnv : {}),
166
222
  UFOO_LAUNCH_MODE: "internal-pty",
167
223
  UFOO_INTERNAL_PTY: "1",
168
224
  };
@@ -987,4 +1043,10 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
987
1043
  }
988
1044
  }
989
1045
 
990
- module.exports = { parseInputMessage, runPtyRunner };
1046
+ module.exports = {
1047
+ appendStartupBootstrapArg,
1048
+ parseInputMessage,
1049
+ resolvePtyBootstrapArgs,
1050
+ resolveCommand,
1051
+ runPtyRunner,
1052
+ };