u-foo 2.3.11 → 2.3.12

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.12",
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;
@@ -3,6 +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 { readJSON, writeJSON } = require("../bus/utils");
6
7
  const { runCliAgent } = require("./cliRunner");
7
8
  const { normalizeCliOutput } = require("./normalizeOutput");
8
9
  const { createActivityStatePublisher } = require("./activityStatePublisher");
@@ -15,6 +16,7 @@ const { listToolsForCallerTier, CALLER_TIERS } = require("../tools");
15
16
  const { redactToolCallPayload, redactSecrets } = require("../providerapi/redactor");
16
17
  const { buildCachedMemoryPrefix } = require("../memory");
17
18
  const { shouldForwardStreamToPublisher } = require("./publisherRouting");
19
+ const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnostics");
18
20
 
19
21
  function sleep(ms) {
20
22
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -493,14 +495,22 @@ function persistProviderSessionId(projectRoot, subscriber, providerSessionId) {
493
495
  try {
494
496
  const agentsFile = getUfooPaths(projectRoot).agentsFile;
495
497
  const parsed = fs.existsSync(agentsFile)
496
- ? JSON.parse(fs.readFileSync(agentsFile, "utf8"))
498
+ ? readJSON(agentsFile, null)
497
499
  : {};
500
+ if (!parsed) return false;
498
501
  if (!parsed.agents || typeof parsed.agents !== "object") return false;
499
- if (!parsed.agents[subscriber] || typeof parsed.agents[subscriber] !== "object") return false;
502
+ if (!parsed.agents[subscriber] || typeof parsed.agents[subscriber] !== "object") {
503
+ appendAgentRegistryDiagnostic(agentsFile, "provider_session_subscriber_missing", {
504
+ source: "agent.internalRunner.persistProviderSessionId",
505
+ subscriber,
506
+ known_ids: Object.keys(parsed.agents || {}).sort(),
507
+ });
508
+ return false;
509
+ }
500
510
  if (parsed.agents[subscriber].provider_session_id === id) return false;
501
511
  parsed.agents[subscriber].provider_session_id = id;
502
512
  parsed.agents[subscriber].provider_session_updated_at = new Date().toISOString();
503
- fs.writeFileSync(agentsFile, `${JSON.stringify(parsed, null, 2)}\n`);
513
+ writeJSON(agentsFile, parsed);
504
514
  return true;
505
515
  } catch {
506
516
  return false;
@@ -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()
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
  }
@@ -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
+ };
@@ -1,4 +1,5 @@
1
1
  const { getTimestamp, readJSON, writeJSON } = require("../bus/utils");
2
+ const { appendAgentRegistryDiagnostic, summarizeAgents } = require("./agentRegistryDiagnostics");
2
3
 
3
4
  const AGENTS_SCHEMA_VERSION = 1;
4
5
 
@@ -89,9 +90,23 @@ function normalizeAgentsData(data) {
89
90
  function loadAgentsData(filePath) {
90
91
  const data = readJSON(filePath, null);
91
92
  if (!data) {
93
+ appendAgentRegistryDiagnostic(filePath, "load_agents_empty", {
94
+ source: "ufoo.agentsStore.loadAgentsData",
95
+ reason: "missing_or_unreadable_registry",
96
+ });
92
97
  return normalizeAgentsData({});
93
98
  }
94
- return normalizeAgentsData(data);
99
+ const normalized = normalizeAgentsData(data);
100
+ const beforeSummary = summarizeAgents(data);
101
+ const afterSummary = summarizeAgents(normalized);
102
+ if (JSON.stringify(beforeSummary) !== JSON.stringify(afterSummary)) {
103
+ appendAgentRegistryDiagnostic(filePath, "load_agents_normalized", {
104
+ source: "ufoo.agentsStore.loadAgentsData",
105
+ before: beforeSummary,
106
+ after: afterSummary,
107
+ });
108
+ }
109
+ return normalized;
95
110
  }
96
111
 
97
112
  function parseTimestampMs(value) {
@@ -125,13 +140,27 @@ function mergeExternalActivityFields(targetMeta, diskMeta) {
125
140
  }
126
141
  }
127
142
 
128
- function saveAgentsData(filePath, data) {
143
+ function saveAgentsData(filePath, data, options = {}) {
144
+ const source = typeof options.source === "string" && options.source
145
+ ? options.source
146
+ : "ufoo.agentsStore.saveAgentsData";
129
147
  const normalized = normalizeAgentsData(data);
130
148
 
131
149
  // Merge externally-managed fields from disk to avoid daemon in-memory writes
132
150
  // overwriting fresher runner/notifier state updates.
133
151
  const disk = readJSON(filePath, null);
134
152
  if (disk && disk.agents && normalized.agents) {
153
+ const droppedIds = Object.keys(disk.agents)
154
+ .filter((id) => !Object.prototype.hasOwnProperty.call(normalized.agents, id))
155
+ .sort();
156
+ if (droppedIds.length > 0) {
157
+ appendAgentRegistryDiagnostic(filePath, "save_agents_dropping_disk_entries", {
158
+ source,
159
+ dropped_ids: droppedIds,
160
+ disk: summarizeAgents(disk),
161
+ next: summarizeAgents(normalized),
162
+ });
163
+ }
135
164
  for (const [id, diskMeta] of Object.entries(disk.agents)) {
136
165
  if (!diskMeta || typeof diskMeta !== "object") continue;
137
166
  const targetMeta = normalized.agents[id];
@@ -140,6 +169,13 @@ function saveAgentsData(filePath, data) {
140
169
  }
141
170
  }
142
171
 
172
+ const nextSummary = summarizeAgents(normalized);
173
+ if (nextSummary.count === 0 || options.trace === true) {
174
+ appendAgentRegistryDiagnostic(filePath, "save_agents_data", {
175
+ source,
176
+ next: nextSummary,
177
+ });
178
+ }
143
179
  writeJSON(filePath, normalized);
144
180
  }
145
181