u-foo 1.9.8 → 2.2.0

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.
Files changed (69) hide show
  1. package/package.json +2 -4
  2. package/src/agent/claudeEventTranslator.js +267 -0
  3. package/src/agent/claudeOauthTokenReader.js +52 -0
  4. package/src/agent/claudeThreadProvider.js +343 -0
  5. package/src/agent/cliRunner.js +4 -16
  6. package/src/agent/codexEventTranslator.js +78 -0
  7. package/src/agent/codexThreadProvider.js +181 -0
  8. package/src/agent/controllerToolExecutor.js +233 -0
  9. package/src/agent/credentials/claude.js +324 -0
  10. package/src/agent/credentials/codex.js +203 -0
  11. package/src/agent/credentials/index.js +106 -0
  12. package/src/agent/defaultBootstrap.js +128 -5
  13. package/src/agent/internalRunner.js +333 -2
  14. package/src/agent/loopObservability.js +190 -0
  15. package/src/agent/loopRuntime.js +457 -0
  16. package/src/agent/ufooAgent.js +178 -120
  17. package/src/agent/upstreamTransport.js +464 -0
  18. package/src/bus/utils.js +3 -2
  19. package/src/chat/dashboardView.js +51 -1
  20. package/src/chat/index.js +3 -1
  21. package/src/config.js +53 -17
  22. package/src/controller/flags.js +160 -0
  23. package/src/controller/gateRouter.js +201 -0
  24. package/src/controller/routerFastPath.js +22 -0
  25. package/src/controller/shadowGuard.js +280 -0
  26. package/src/daemon/index.js +2 -3
  27. package/src/daemon/promptLoop.js +33 -224
  28. package/src/daemon/promptRequest.js +360 -5
  29. package/src/daemon/status.js +2 -0
  30. package/src/history/inputTimeline.js +9 -4
  31. package/src/memory/index.js +24 -0
  32. package/src/providerapi/redactor.js +87 -0
  33. package/src/providerapi/shadowDiff.js +174 -0
  34. package/src/report/store.js +4 -3
  35. package/src/tools/handlers/ackBus.js +26 -0
  36. package/src/tools/handlers/common.js +64 -0
  37. package/src/tools/handlers/dispatchMessage.js +81 -0
  38. package/src/tools/handlers/listAgents.js +14 -0
  39. package/src/tools/handlers/readBusSummary.js +34 -0
  40. package/src/tools/handlers/readOpenDecisions.js +26 -0
  41. package/src/tools/handlers/readProjectRegistry.js +20 -0
  42. package/src/tools/handlers/readPromptHistory.js +123 -0
  43. package/src/tools/handlers/tier2.js +134 -0
  44. package/src/tools/index.js +55 -0
  45. package/src/tools/registry.js +69 -0
  46. package/src/tools/schemaFixtures.js +415 -0
  47. package/src/tools/tier0/listAgents.js +14 -0
  48. package/src/tools/tier0/readBusSummary.js +14 -0
  49. package/src/tools/tier0/readOpenDecisions.js +14 -0
  50. package/src/tools/tier0/readProjectRegistry.js +14 -0
  51. package/src/tools/tier0/readPromptHistory.js +14 -0
  52. package/src/tools/tier1/ackBus.js +14 -0
  53. package/src/tools/tier1/dispatchMessage.js +14 -0
  54. package/src/tools/tier1/routeAgent.js +14 -0
  55. package/src/tools/tier2/closeAgent.js +14 -0
  56. package/src/tools/tier2/launchAgent.js +14 -0
  57. package/src/tools/tier2/manageCron.js +14 -0
  58. package/src/tools/tier2/renameAgent.js +14 -0
  59. package/src/tools/types.js +75 -0
  60. package/src/tools/unimplemented.js +13 -0
  61. package/src/ufoo/paths.js +4 -0
  62. package/bin/ufoo-assistant-agent.js +0 -5
  63. package/bin/ufoo-engine.js +0 -25
  64. package/src/assistant/agent.js +0 -261
  65. package/src/assistant/bridge.js +0 -178
  66. package/src/assistant/constants.js +0 -15
  67. package/src/assistant/engine.js +0 -252
  68. package/src/assistant/stdio.js +0 -58
  69. package/src/assistant/ufooEngineCli.js +0 -312
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+
3
+ // §11.6 Phase 1 shadow diff metrics: model text BLEU ≥ 0.85 and
4
+ // tool-call sequence consistency ≥ 95%. This module provides the deterministic
5
+ // helpers the shadow harness feeds paired (legacy, api-backed) samples into.
6
+
7
+ const PHASE1_DEFAULT_BLEU_THRESHOLD = 0.85;
8
+ const PHASE1_DEFAULT_TOOLCALL_THRESHOLD = 0.95;
9
+
10
+ function tokenize(text) {
11
+ return String(text || "")
12
+ .toLowerCase()
13
+ .replace(/[^\p{L}\p{N}\s]/gu, " ")
14
+ .split(/\s+/)
15
+ .filter(Boolean);
16
+ }
17
+
18
+ function ngrams(tokens, n) {
19
+ if (!Array.isArray(tokens) || tokens.length < n) return [];
20
+ const out = [];
21
+ for (let i = 0; i <= tokens.length - n; i += 1) {
22
+ out.push(tokens.slice(i, i + n).join("\u0001"));
23
+ }
24
+ return out;
25
+ }
26
+
27
+ function countMap(list) {
28
+ const map = new Map();
29
+ for (const item of list) {
30
+ map.set(item, (map.get(item) || 0) + 1);
31
+ }
32
+ return map;
33
+ }
34
+
35
+ function clippedPrecision(candidateNgrams, referenceNgrams) {
36
+ if (candidateNgrams.length === 0) return 0;
37
+ const candidateCounts = countMap(candidateNgrams);
38
+ const referenceCounts = countMap(referenceNgrams);
39
+ let clipped = 0;
40
+ for (const [gram, count] of candidateCounts.entries()) {
41
+ const refCount = referenceCounts.get(gram) || 0;
42
+ clipped += Math.min(count, refCount);
43
+ }
44
+ return clipped / candidateNgrams.length;
45
+ }
46
+
47
+ function brevityPenalty(candidateLen, referenceLen) {
48
+ if (candidateLen === 0) return 0;
49
+ if (candidateLen >= referenceLen) return 1;
50
+ return Math.exp(1 - referenceLen / candidateLen);
51
+ }
52
+
53
+ function computeBleu(referenceText, candidateText, options = {}) {
54
+ const maxN = Number.isFinite(options.maxN) ? Math.max(1, Math.min(4, options.maxN)) : 4;
55
+ const weight = 1 / maxN;
56
+ const ref = tokenize(referenceText);
57
+ const cand = tokenize(candidateText);
58
+ if (cand.length === 0) return 0;
59
+ if (ref.length === 0) return 0;
60
+
61
+ let logSum = 0;
62
+ for (let n = 1; n <= maxN; n += 1) {
63
+ const precision = clippedPrecision(ngrams(cand, n), ngrams(ref, n));
64
+ if (precision <= 0) return 0;
65
+ logSum += weight * Math.log(precision);
66
+ }
67
+ const bp = brevityPenalty(cand.length, ref.length);
68
+ return bp * Math.exp(logSum);
69
+ }
70
+
71
+ function extractToolCallNames(events = []) {
72
+ return (Array.isArray(events) ? events : [])
73
+ .filter((event) => event && event.type === "tool_call" && event.name)
74
+ .map((event) => String(event.name).trim())
75
+ .filter(Boolean);
76
+ }
77
+
78
+ function computeToolCallSequenceConsistency(referenceSeq = [], candidateSeq = []) {
79
+ const ref = Array.isArray(referenceSeq) ? referenceSeq : [];
80
+ const cand = Array.isArray(candidateSeq) ? candidateSeq : [];
81
+ if (ref.length === 0 && cand.length === 0) return 1;
82
+
83
+ const m = ref.length;
84
+ const n = cand.length;
85
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
86
+ for (let i = 1; i <= m; i += 1) {
87
+ for (let j = 1; j <= n; j += 1) {
88
+ if (ref[i - 1] === cand[j - 1]) {
89
+ dp[i][j] = dp[i - 1][j - 1] + 1;
90
+ } else {
91
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
92
+ }
93
+ }
94
+ }
95
+ const lcs = dp[m][n];
96
+ const denom = Math.max(m, n);
97
+ if (denom === 0) return 1;
98
+ return lcs / denom;
99
+ }
100
+
101
+ function buildPhase1ShadowDiffSample({ legacy = {}, api = {} } = {}) {
102
+ const bleu = computeBleu(legacy.text || "", api.text || "");
103
+ const toolSeqConsistency = computeToolCallSequenceConsistency(
104
+ extractToolCallNames(legacy.events || legacy.toolCalls || []),
105
+ extractToolCallNames(api.events || api.toolCalls || [])
106
+ );
107
+ return {
108
+ bleu,
109
+ toolSeqConsistency,
110
+ };
111
+ }
112
+
113
+ function summarizePhase1ShadowDiff(samples = [], options = {}) {
114
+ const bleuThreshold = Number.isFinite(options.bleuThreshold)
115
+ ? options.bleuThreshold
116
+ : PHASE1_DEFAULT_BLEU_THRESHOLD;
117
+ const toolCallThreshold = Number.isFinite(options.toolCallThreshold)
118
+ ? options.toolCallThreshold
119
+ : PHASE1_DEFAULT_TOOLCALL_THRESHOLD;
120
+
121
+ const list = Array.isArray(samples) ? samples : [];
122
+ if (list.length === 0) {
123
+ return {
124
+ sampleCount: 0,
125
+ meanBleu: 0,
126
+ meanToolSeqConsistency: 0,
127
+ bleuPass: false,
128
+ toolCallPass: false,
129
+ overallPass: false,
130
+ bleuThreshold,
131
+ toolCallThreshold,
132
+ };
133
+ }
134
+
135
+ let totalBleu = 0;
136
+ let totalTool = 0;
137
+ let toolCallPassCount = 0;
138
+ for (const sample of list) {
139
+ totalBleu += Number(sample.bleu || 0);
140
+ const consistency = Number(sample.toolSeqConsistency || 0);
141
+ totalTool += consistency;
142
+ if (consistency >= toolCallThreshold) toolCallPassCount += 1;
143
+ }
144
+
145
+ const meanBleu = totalBleu / list.length;
146
+ const meanToolSeqConsistency = totalTool / list.length;
147
+ const bleuPass = meanBleu >= bleuThreshold;
148
+ const toolCallPassRate = toolCallPassCount / list.length;
149
+ const toolCallPass = toolCallPassRate >= toolCallThreshold;
150
+
151
+ return {
152
+ sampleCount: list.length,
153
+ meanBleu,
154
+ meanToolSeqConsistency,
155
+ toolCallPassRate,
156
+ bleuPass,
157
+ toolCallPass,
158
+ overallPass: bleuPass && toolCallPass,
159
+ bleuThreshold,
160
+ toolCallThreshold,
161
+ };
162
+ }
163
+
164
+ module.exports = {
165
+ PHASE1_DEFAULT_BLEU_THRESHOLD,
166
+ PHASE1_DEFAULT_TOOLCALL_THRESHOLD,
167
+ tokenize,
168
+ ngrams,
169
+ computeBleu,
170
+ extractToolCallNames,
171
+ computeToolCallSequenceConsistency,
172
+ buildPhase1ShadowDiffSample,
173
+ summarizePhase1ShadowDiff,
174
+ };
@@ -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 { redactSecrets } = require("../providerapi/redactor");
4
5
 
5
6
  const REPORT_PHASES = {
6
7
  START: "start",
@@ -100,7 +101,7 @@ function normalizeReportInput(input = {}, options = {}) {
100
101
  function appendReport(projectRoot, entry) {
101
102
  ensureReportDir(projectRoot);
102
103
  const { reportsFile } = getReportPaths(projectRoot);
103
- fs.appendFileSync(reportsFile, `${JSON.stringify(entry)}\n`, "utf8");
104
+ fs.appendFileSync(reportsFile, `${JSON.stringify(redactSecrets(entry))}\n`, "utf8");
104
105
  }
105
106
 
106
107
  function parseJsonLines(file) {
@@ -151,7 +152,7 @@ function loadReportState(projectRoot) {
151
152
  function saveReportState(projectRoot, state) {
152
153
  ensureReportDir(projectRoot);
153
154
  const { stateFile } = getReportPaths(projectRoot);
154
- fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
155
+ fs.writeFileSync(stateFile, JSON.stringify(redactSecrets(state), null, 2));
155
156
  }
156
157
 
157
158
  function updateReportState(projectRoot, entry) {
@@ -205,7 +206,7 @@ function getControllerInboxFile(projectRoot, controllerId = "ufoo-agent") {
205
206
  function appendControllerInboxEntry(projectRoot, controllerId, entry) {
206
207
  const file = getControllerInboxFile(projectRoot, controllerId);
207
208
  fs.mkdirSync(path.dirname(file), { recursive: true });
208
- fs.appendFileSync(file, `${JSON.stringify(entry)}\n`, "utf8");
209
+ fs.appendFileSync(file, `${JSON.stringify(redactSecrets(entry))}\n`, "utf8");
209
210
  }
210
211
 
211
212
  function listControllerInboxEntries(projectRoot, controllerId = "ufoo-agent", options = {}) {
@@ -0,0 +1,26 @@
1
+ const { buildToolError, requireSubscriber, getEventBus } = require("./common");
2
+
3
+ async function ackBusHandler(ctx = {}, args = {}) {
4
+ const subscriber = requireSubscriber(ctx);
5
+ const requestedSubscriber = String(args.subscriber || subscriber).trim();
6
+
7
+ if (requestedSubscriber !== subscriber) {
8
+ throw buildToolError(
9
+ "forbidden_ack",
10
+ "ack_bus can only acknowledge the caller subscriber queue"
11
+ );
12
+ }
13
+
14
+ const eventBus = getEventBus(ctx);
15
+ const count = await eventBus.ack(subscriber);
16
+
17
+ return {
18
+ ok: true,
19
+ subscriber,
20
+ acknowledged: count,
21
+ };
22
+ }
23
+
24
+ module.exports = {
25
+ ackBusHandler,
26
+ };
@@ -0,0 +1,64 @@
1
+ const EventBus = require("../../bus");
2
+ const { CALLER_TIERS, normalizeCallerTier } = require("../types");
3
+
4
+ function extractAuditFields(ctx = {}) {
5
+ const audit = {};
6
+ const turnId = ctx.turn_id || ctx.turnId;
7
+ const toolCallId = ctx.tool_call_id || ctx.toolCallId;
8
+ if (turnId) audit.turn_id = String(turnId);
9
+ if (toolCallId) audit.tool_call_id = String(toolCallId);
10
+ const callerTier = normalizeCallerTier(ctx.caller_tier || ctx.callerTier);
11
+ if (callerTier) audit.caller_tier = callerTier;
12
+ return audit;
13
+ }
14
+
15
+ function buildToolError(code, message, extra = {}) {
16
+ const err = new Error(String(message || "tool execution failed"));
17
+ err.code = String(code || "tool_error");
18
+ Object.assign(err, extra);
19
+ return err;
20
+ }
21
+
22
+ function requireSubscriber(ctx = {}) {
23
+ const subscriber = String(ctx.subscriber || "").trim();
24
+ if (!subscriber) {
25
+ throw buildToolError("invalid_context", "tool requires subscriber context", extractAuditFields(ctx));
26
+ }
27
+ return subscriber;
28
+ }
29
+
30
+ function getEventBus(ctx = {}) {
31
+ if (ctx.eventBus) return ctx.eventBus;
32
+ return new EventBus(ctx.projectRoot);
33
+ }
34
+
35
+ function resolveCallerTier(ctx = {}) {
36
+ const raw = normalizeCallerTier(ctx.caller_tier || ctx.callerTier);
37
+ return raw || CALLER_TIERS.CONTROLLER;
38
+ }
39
+
40
+ function assertControllerTier(ctx = {}, toolName = "") {
41
+ const tier = resolveCallerTier(ctx);
42
+ if (tier !== CALLER_TIERS.CONTROLLER) {
43
+ const audit = extractAuditFields(ctx);
44
+ audit.caller_tier = tier;
45
+ throw buildToolError(
46
+ "forbidden_caller_tier",
47
+ `caller_tier "${tier}" is not allowed to invoke tool "${toolName}"`,
48
+ {
49
+ tool_name: toolName,
50
+ allowed_tiers: [CALLER_TIERS.CONTROLLER],
51
+ ...audit,
52
+ }
53
+ );
54
+ }
55
+ }
56
+
57
+ module.exports = {
58
+ buildToolError,
59
+ requireSubscriber,
60
+ getEventBus,
61
+ resolveCallerTier,
62
+ assertControllerTier,
63
+ extractAuditFields,
64
+ };
@@ -0,0 +1,81 @@
1
+ const { buildToolError, requireSubscriber, getEventBus } = require("./common");
2
+
3
+ function normalizeDispatchTarget(rawTarget = "") {
4
+ const target = String(rawTarget || "").trim();
5
+ if (!target) {
6
+ throw buildToolError("invalid_arguments", "dispatch_message requires target");
7
+ }
8
+ if (target === "*") return "broadcast";
9
+ return target;
10
+ }
11
+
12
+ function normalizeDispatchMode(args = {}) {
13
+ const raw = String(
14
+ args.mode || args.injection_mode || args.injectionMode || "immediate"
15
+ )
16
+ .trim()
17
+ .toLowerCase();
18
+ if (raw === "queued") return "queued";
19
+ if (raw === "immediate") return "immediate";
20
+ throw buildToolError(
21
+ "invalid_arguments",
22
+ "dispatch_message mode must be immediate or queued"
23
+ );
24
+ }
25
+
26
+ async function dispatchMessageHandler(ctx = {}, args = {}) {
27
+ const subscriber = requireSubscriber(ctx);
28
+ const target = normalizeDispatchTarget(args.target);
29
+ const message = String(args.message || "").trim();
30
+ const source = String(args.source || subscriber).trim();
31
+ const mode = normalizeDispatchMode(args);
32
+
33
+ if (!message) {
34
+ throw buildToolError("invalid_arguments", "dispatch_message requires message");
35
+ }
36
+ if (source !== subscriber) {
37
+ throw buildToolError(
38
+ "forbidden_source",
39
+ "dispatch_message source must match caller subscriber"
40
+ );
41
+ }
42
+
43
+ const eventBus = getEventBus(ctx);
44
+ let result;
45
+ try {
46
+ result = target === "broadcast"
47
+ ? await eventBus.broadcast(message, subscriber, {
48
+ injectionMode: mode,
49
+ source: subscriber,
50
+ silent: true,
51
+ })
52
+ : await eventBus.send(target, message, subscriber, {
53
+ injectionMode: mode,
54
+ source: subscriber,
55
+ silent: true,
56
+ });
57
+ } catch (err) {
58
+ if (err && /not found/i.test(String(err.message || ""))) {
59
+ throw buildToolError(
60
+ "invalid_target",
61
+ `dispatch_message target not found: ${target}`
62
+ );
63
+ }
64
+ throw err;
65
+ }
66
+
67
+ return {
68
+ ok: true,
69
+ target,
70
+ source: subscriber,
71
+ mode,
72
+ delivered: mode === "immediate" ? result.targets.length : 0,
73
+ queued: mode === "queued" ? result.targets.length : 0,
74
+ targets: result.targets,
75
+ seq: result.seq,
76
+ };
77
+ }
78
+
79
+ module.exports = {
80
+ dispatchMessageHandler,
81
+ };
@@ -0,0 +1,14 @@
1
+ const { buildStatus } = require("../../daemon/status");
2
+
3
+ function listAgentsHandler(ctx = {}) {
4
+ const status = buildStatus(ctx.projectRoot);
5
+ const agents = Array.isArray(status.active_meta) ? status.active_meta : [];
6
+ return {
7
+ count: agents.length,
8
+ agents,
9
+ };
10
+ }
11
+
12
+ module.exports = {
13
+ listAgentsHandler,
14
+ };
@@ -0,0 +1,34 @@
1
+ const { buildStatus } = require("../../daemon/status");
2
+
3
+ function readBusSummaryHandler(ctx = {}) {
4
+ const status = buildStatus(ctx.projectRoot);
5
+ const activeAgents = Array.isArray(status.active_meta) ? status.active_meta : [];
6
+ const busyCount = activeAgents.filter((item) => {
7
+ const state = String((item && item.activity_state) || "").trim().toLowerCase();
8
+ return state === "working"
9
+ || state === "starting"
10
+ || state === "running"
11
+ || state === "waiting_input"
12
+ || state === "blocked";
13
+ }).length;
14
+
15
+ return {
16
+ project_root: ctx.projectRoot,
17
+ summary: {
18
+ active_count: activeAgents.length,
19
+ busy_count: busyCount,
20
+ ready_count: Math.max(activeAgents.length - busyCount, 0),
21
+ unread_total: Number(status.unread && status.unread.total ? status.unread.total : 0) || 0,
22
+ decisions_open: Number(status.decisions && status.decisions.open ? status.decisions.open : 0) || 0,
23
+ reports_pending_total: Number(status.reports && status.reports.pending_total ? status.reports.pending_total : 0) || 0,
24
+ controller_pending_total: Number(status.controller && status.controller.pending_total ? status.controller.pending_total : 0) || 0,
25
+ cron_count: Number(status.cron && status.cron.count ? status.cron.count : 0) || 0,
26
+ groups_active: Number(status.groups && status.groups.active ? status.groups.active : 0) || 0,
27
+ },
28
+ active_agents: activeAgents,
29
+ };
30
+ }
31
+
32
+ module.exports = {
33
+ readBusSummaryHandler,
34
+ };
@@ -0,0 +1,26 @@
1
+ const DecisionsManager = require("../../context/decisions");
2
+
3
+ function readOpenDecisionsHandler(ctx = {}, args = {}) {
4
+ const limit = Number.isFinite(Number(args.limit)) && Number(args.limit) > 0
5
+ ? Math.floor(Number(args.limit))
6
+ : 20;
7
+ const manager = new DecisionsManager(ctx.projectRoot);
8
+ const decisions = manager.readDecisions()
9
+ .filter((item) => String(item.status || "open").trim().toLowerCase() === "open")
10
+ .slice(0, limit)
11
+ .map((item) => ({
12
+ file: item.file,
13
+ title: item.title,
14
+ status: item.status,
15
+ file_path: item.filePath,
16
+ }));
17
+
18
+ return {
19
+ count: decisions.length,
20
+ decisions,
21
+ };
22
+ }
23
+
24
+ module.exports = {
25
+ readOpenDecisionsHandler,
26
+ };
@@ -0,0 +1,20 @@
1
+ const { listProjectRuntimes } = require("../../projects/registry");
2
+
3
+ function readProjectRegistryHandler(_ctx = {}, args = {}) {
4
+ const validate = args.validate !== false;
5
+ const cleanupTmp = args.cleanup_tmp !== false;
6
+ const projects = listProjectRuntimes({
7
+ validate,
8
+ cleanupTmp,
9
+ runtimeDir: args.runtimeDir,
10
+ });
11
+
12
+ return {
13
+ count: projects.length,
14
+ projects,
15
+ };
16
+ }
17
+
18
+ module.exports = {
19
+ readProjectRegistryHandler,
20
+ };
@@ -0,0 +1,123 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const { buildStatus } = require("../../daemon/status");
5
+ const { getUfooPaths } = require("../../ufoo/paths");
6
+
7
+ function clipPromptText(value = "", maxChars = 240) {
8
+ const text = String(value || "").replace(/\s+/g, " ").trim();
9
+ if (!text) return "";
10
+ if (text.length <= maxChars) return text;
11
+ return `${text.slice(0, maxChars)}...[truncated]`;
12
+ }
13
+
14
+ function buildPromptHistory(projectRoot, activeAgents = [], args = {}) {
15
+ const perAgentLimit = Number.isFinite(Number(args.per_agent_limit)) && Number(args.per_agent_limit) > 0
16
+ ? Math.floor(Number(args.per_agent_limit))
17
+ : 6;
18
+ const maxFiles = Number.isFinite(Number(args.max_files)) && Number(args.max_files) > 0
19
+ ? Math.floor(Number(args.max_files))
20
+ : 3;
21
+ const target = String(args.target || "").trim();
22
+ const eventsDir = getUfooPaths(projectRoot).busEventsDir;
23
+ const activeIds = new Set(activeAgents.map((item) => String(item.id || "")).filter(Boolean));
24
+ const nicknames = {};
25
+ const rows = new Map();
26
+
27
+ for (const item of activeAgents) {
28
+ if (!item || !item.id) continue;
29
+ const id = String(item.id);
30
+ const nickname = String(item.nickname || "");
31
+ if (nickname) nicknames[nickname] = id;
32
+ rows.set(id, {
33
+ agent_id: id,
34
+ nickname,
35
+ samples: [],
36
+ sample_count: 0,
37
+ total_count: 0,
38
+ last_ts: "",
39
+ });
40
+ }
41
+
42
+ if (target) {
43
+ const resolved = activeIds.has(target) ? target : nicknames[target];
44
+ if (resolved) {
45
+ for (const id of Array.from(rows.keys())) {
46
+ if (id !== resolved) rows.delete(id);
47
+ }
48
+ }
49
+ }
50
+
51
+ let files = [];
52
+ try {
53
+ files = fs.readdirSync(eventsDir)
54
+ .filter((name) => name.endsWith(".jsonl"))
55
+ .sort()
56
+ .slice(-maxFiles)
57
+ .reverse();
58
+ } catch {
59
+ return { scanned_files: 0, matched_events: 0, per_agent: [] };
60
+ }
61
+
62
+ let matchedEvents = 0;
63
+ for (const file of files) {
64
+ let lines = [];
65
+ try {
66
+ lines = fs.readFileSync(path.join(eventsDir, file), "utf8")
67
+ .split(/\r?\n/)
68
+ .filter(Boolean)
69
+ .reverse();
70
+ } catch {
71
+ continue;
72
+ }
73
+
74
+ for (const line of lines) {
75
+ let event = null;
76
+ try {
77
+ event = JSON.parse(line);
78
+ } catch {
79
+ continue;
80
+ }
81
+ if (!event || event.event !== "message") continue;
82
+ const message = clipPromptText(event.data && event.data.message);
83
+ if (!message) continue;
84
+ const rawTarget = String(event.target || "").trim();
85
+ const agentId = rows.has(rawTarget) ? rawTarget : nicknames[rawTarget];
86
+ if (!agentId || !rows.has(agentId)) continue;
87
+
88
+ const row = rows.get(agentId);
89
+ matchedEvents += 1;
90
+ row.total_count += 1;
91
+ if (!row.last_ts) row.last_ts = String(event.timestamp || "");
92
+ if (row.samples.length < perAgentLimit) {
93
+ row.samples.push({
94
+ ts: String(event.timestamp || ""),
95
+ publisher: String(event.publisher || ""),
96
+ prompt: message,
97
+ });
98
+ row.sample_count = row.samples.length;
99
+ }
100
+ }
101
+ }
102
+
103
+ const perAgent = Array.from(rows.values())
104
+ .filter((row) => row.total_count > 0)
105
+ .sort((a, b) => String(b.last_ts || "").localeCompare(String(a.last_ts || "")));
106
+
107
+ return {
108
+ scanned_files: files.length,
109
+ matched_events: matchedEvents,
110
+ per_agent: perAgent,
111
+ };
112
+ }
113
+
114
+ function readPromptHistoryHandler(ctx = {}, args = {}) {
115
+ const status = buildStatus(ctx.projectRoot);
116
+ const activeAgents = Array.isArray(status.active_meta) ? status.active_meta : [];
117
+ return buildPromptHistory(ctx.projectRoot, activeAgents, args);
118
+ }
119
+
120
+ module.exports = {
121
+ buildPromptHistory,
122
+ readPromptHistoryHandler,
123
+ };