u-foo 2.3.10 → 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/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
  }
@@ -126,7 +126,10 @@ function createAgentViewController(options = {}) {
126
126
 
127
127
  agentInputSuppressUntil = now() + 300;
128
128
  agentViewUsesBus = Boolean(options.useBus);
129
- if (!agentViewUsesBus) {
129
+ if (agentViewUsesBus) {
130
+ const label = getAgentLabel(agentId);
131
+ processStdout.write(`ufoo internal · ${label}\r\n\r\n> `);
132
+ } else {
130
133
  const sockPath = getInjectSockPath(agentId);
131
134
  connectAgentOutput(sockPath);
132
135
  connectAgentInput(sockPath);
@@ -57,6 +57,11 @@ function createDashboardKeyController(options = {}) {
57
57
  return Boolean(caps && caps.supportsSocketProtocol);
58
58
  }
59
59
 
60
+ function supportsInternalQueue(agentId) {
61
+ const caps = getAgentCapabilities(agentId);
62
+ return Boolean(caps && caps.supportsInternalQueueLoop);
63
+ }
64
+
60
65
  function withAgentInputFocus() {
61
66
  state.focusMode = "input";
62
67
  state.agentOutputSuppressed = false;
@@ -73,7 +78,7 @@ function createDashboardKeyController(options = {}) {
73
78
 
74
79
  function switchAgentView(agentId) {
75
80
  withAgentInputFocus();
76
- enterAgentView(agentId);
81
+ enterAgentView(agentId, { useBus: supportsInternalQueue(agentId) && !supportsSocket(agentId) });
77
82
  }
78
83
 
79
84
  function exitAgentDashboardToInput() {
@@ -154,7 +159,7 @@ function createDashboardKeyController(options = {}) {
154
159
  } else {
155
160
  withAgentInputFocus();
156
161
  state.selectedAgentIndex = nextIndex + 1;
157
- enterAgentView(nextAgent);
162
+ enterAgentView(nextAgent, { useBus: supportsInternalQueue(nextAgent) && !supportsSocket(nextAgent) });
158
163
  }
159
164
  } else {
160
165
  exitAgentView();
@@ -511,6 +516,16 @@ function createDashboardKeyController(options = {}) {
511
516
  enterAgentView(agentId);
512
517
  return true;
513
518
  }
519
+
520
+ if (supportsInternalQueue(agentId)) {
521
+ clearTargetAgent();
522
+ state.focusMode = "input";
523
+ state.dashboardView = "agents";
524
+ state.selectedAgentIndex = -1;
525
+ setScreenGrabKeys(false);
526
+ enterAgentView(agentId, { useBus: true });
527
+ return true;
528
+ }
514
529
  }
515
530
 
516
531
  exitDashboardMode(false);
@@ -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