u-foo 1.9.8 → 2.1.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 (68) 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/internalRunner.js +333 -2
  13. package/src/agent/loopObservability.js +190 -0
  14. package/src/agent/loopRuntime.js +457 -0
  15. package/src/agent/ufooAgent.js +178 -120
  16. package/src/agent/upstreamTransport.js +464 -0
  17. package/src/bus/utils.js +3 -2
  18. package/src/chat/dashboardView.js +51 -1
  19. package/src/chat/index.js +3 -1
  20. package/src/config.js +53 -17
  21. package/src/controller/flags.js +160 -0
  22. package/src/controller/gateRouter.js +201 -0
  23. package/src/controller/routerFastPath.js +22 -0
  24. package/src/controller/shadowGuard.js +280 -0
  25. package/src/daemon/index.js +2 -3
  26. package/src/daemon/promptLoop.js +33 -224
  27. package/src/daemon/promptRequest.js +360 -5
  28. package/src/daemon/status.js +2 -0
  29. package/src/history/inputTimeline.js +9 -4
  30. package/src/memory/index.js +24 -0
  31. package/src/providerapi/redactor.js +87 -0
  32. package/src/providerapi/shadowDiff.js +174 -0
  33. package/src/report/store.js +4 -3
  34. package/src/tools/handlers/ackBus.js +26 -0
  35. package/src/tools/handlers/common.js +64 -0
  36. package/src/tools/handlers/dispatchMessage.js +81 -0
  37. package/src/tools/handlers/listAgents.js +14 -0
  38. package/src/tools/handlers/readBusSummary.js +34 -0
  39. package/src/tools/handlers/readOpenDecisions.js +26 -0
  40. package/src/tools/handlers/readProjectRegistry.js +20 -0
  41. package/src/tools/handlers/readPromptHistory.js +123 -0
  42. package/src/tools/handlers/tier2.js +134 -0
  43. package/src/tools/index.js +55 -0
  44. package/src/tools/registry.js +69 -0
  45. package/src/tools/schemaFixtures.js +415 -0
  46. package/src/tools/tier0/listAgents.js +14 -0
  47. package/src/tools/tier0/readBusSummary.js +14 -0
  48. package/src/tools/tier0/readOpenDecisions.js +14 -0
  49. package/src/tools/tier0/readProjectRegistry.js +14 -0
  50. package/src/tools/tier0/readPromptHistory.js +14 -0
  51. package/src/tools/tier1/ackBus.js +14 -0
  52. package/src/tools/tier1/dispatchMessage.js +14 -0
  53. package/src/tools/tier1/routeAgent.js +14 -0
  54. package/src/tools/tier2/closeAgent.js +14 -0
  55. package/src/tools/tier2/launchAgent.js +14 -0
  56. package/src/tools/tier2/manageCron.js +14 -0
  57. package/src/tools/tier2/renameAgent.js +14 -0
  58. package/src/tools/types.js +75 -0
  59. package/src/tools/unimplemented.js +13 -0
  60. package/src/ufoo/paths.js +4 -0
  61. package/bin/ufoo-assistant-agent.js +0 -5
  62. package/bin/ufoo-engine.js +0 -25
  63. package/src/assistant/agent.js +0 -261
  64. package/src/assistant/bridge.js +0 -178
  65. package/src/assistant/constants.js +0 -15
  66. package/src/assistant/engine.js +0 -252
  67. package/src/assistant/stdio.js +0 -58
  68. package/src/assistant/ufooEngineCli.js +0 -312
@@ -6,6 +6,29 @@ const {
6
6
  consumeControllerInboxEntries,
7
7
  } = require("../report/store");
8
8
  const { isGlobalControllerProjectRoot } = require("../projects");
9
+ const {
10
+ resolveGateRouterConfig,
11
+ shouldUseGateRouter,
12
+ } = require("../controller/gateRouter");
13
+ const {
14
+ CONTROLLER_MODES,
15
+ applyControllerModeForMessage,
16
+ resolveControllerMode,
17
+ } = require("../controller/flags");
18
+ const {
19
+ resolveLoopRuntimeOptions,
20
+ runPromptWithControllerLoop,
21
+ } = require("../agent/loopRuntime");
22
+ const {
23
+ appendShadowDiff,
24
+ createLoopObserver,
25
+ } = require("../agent/loopObservability");
26
+ const {
27
+ DEFAULT_SHADOW_SAMPLING_RATE,
28
+ createShadowBudgetBreaker,
29
+ createShadowGuard,
30
+ shouldSampleShadow,
31
+ } = require("../controller/shadowGuard");
9
32
 
10
33
  function normalizeProjectRoute(route) {
11
34
  if (!route || typeof route !== "object") return null;
@@ -63,6 +86,29 @@ function buildPromptWithPrivateReports(prompt = "", reports = [], requestMeta =
63
86
  return lines.join("\n");
64
87
  }
65
88
 
89
+ function normalizeMessageId(req = {}) {
90
+ return String(req.request_id || req.message_id || req.msg_id || req.id || "").trim();
91
+ }
92
+
93
+ function summarizeShadowPayload(payload = {}) {
94
+ if (!payload || typeof payload !== "object") {
95
+ return {
96
+ reply_present: false,
97
+ dispatch_count: 0,
98
+ ops_count: 0,
99
+ terminal_reason: "",
100
+ };
101
+ }
102
+ return {
103
+ reply_present: Boolean(payload.reply),
104
+ dispatch_count: Array.isArray(payload.dispatch) ? payload.dispatch.length : 0,
105
+ ops_count: Array.isArray(payload.ops) ? payload.ops.length : 0,
106
+ terminal_reason: payload.loop && typeof payload.loop === "object"
107
+ ? String(payload.loop.terminal_reason || "").trim()
108
+ : "",
109
+ };
110
+ }
111
+
66
112
  async function handlePromptRequest(options = {}) {
67
113
  const {
68
114
  projectRoot,
@@ -72,10 +118,12 @@ async function handlePromptRequest(options = {}) {
72
118
  model,
73
119
  processManager = null,
74
120
  runPromptWithAssistant,
121
+ runPromptWithControllerLoop: injectedLoopRunner = runPromptWithControllerLoop,
75
122
  runUfooAgent,
76
- runAssistantTask,
123
+ runUfooRouteAgent,
77
124
  dispatchMessages,
78
125
  handleOps,
126
+ ackBus,
79
127
  markPending = () => {},
80
128
  reportTaskStatus = () => {},
81
129
  forwardProjectPrompt = null,
@@ -84,8 +132,40 @@ async function handlePromptRequest(options = {}) {
84
132
 
85
133
  log(`prompt ${String(req.text || "").slice(0, 200)}`);
86
134
  const requestMeta = req.request_meta && typeof req.request_meta === "object" ? req.request_meta : {};
135
+ const messageId = normalizeMessageId(req);
136
+ const requestedControllerMode = String(
137
+ requestMeta.controller_mode || requestMeta.agent_execution_path || ""
138
+ ).trim();
139
+ let controllerMode = resolveControllerMode({
140
+ projectRoot,
141
+ requestedMode: requestedControllerMode,
142
+ });
87
143
  const isGlobalController = isGlobalControllerProjectRoot(projectRoot);
88
144
  const forcedProjectRoot = String(requestMeta.force_project_root || "").trim();
145
+ const resolvedLoopRuntime = resolveLoopRuntimeOptions();
146
+ if (controllerMode === CONTROLLER_MODES.LEGACY && resolvedLoopRuntime.enabled) {
147
+ controllerMode = CONTROLLER_MODES.LOOP;
148
+ }
149
+ const controllerObserver = createLoopObserver({
150
+ projectRoot,
151
+ enabled: true,
152
+ defaults: {
153
+ controller_mode: controllerMode,
154
+ },
155
+ });
156
+ const appliedControllerMode = applyControllerModeForMessage({
157
+ projectRoot,
158
+ nextMode: controllerMode,
159
+ messageId,
160
+ });
161
+ if (appliedControllerMode.transition) {
162
+ controllerObserver.emit("controller.flag.transition", appliedControllerMode.transition);
163
+ }
164
+ const loopRuntime = {
165
+ ...resolvedLoopRuntime,
166
+ enabled: controllerMode === CONTROLLER_MODES.LOOP,
167
+ };
168
+ const shadowEnabled = controllerMode === CONTROLLER_MODES.SHADOW;
89
169
 
90
170
  if (isGlobalController && forcedProjectRoot) {
91
171
  try {
@@ -131,26 +211,166 @@ async function handlePromptRequest(options = {}) {
131
211
  }
132
212
 
133
213
  const privateReports = listControllerInboxEntries(projectRoot, "ufoo-agent", { num: 100 });
134
- const promptText = buildPromptWithPrivateReports(req.text || "", privateReports, requestMeta);
135
214
  const useGlobalProjectRouter = isGlobalController;
215
+ const promptRunner = runPromptWithAssistant;
216
+ const ufooAgentOptions = useGlobalProjectRouter ? { routingMode: "global-router" } : { controllerMode };
217
+ let nextRequestMeta = requestMeta;
218
+ if (!Object.prototype.hasOwnProperty.call(nextRequestMeta, "agent_execution_path") && controllerMode !== CONTROLLER_MODES.LEGACY) {
219
+ nextRequestMeta = {
220
+ ...nextRequestMeta,
221
+ agent_execution_path: controllerMode,
222
+ };
223
+ }
224
+
225
+ const logGateRouterEvent = (event, details = {}) => {
226
+ controllerObserver.emit(event, details);
227
+ if (typeof log !== "function") return;
228
+ log(`event ${JSON.stringify({ event, ...details })}`);
229
+ };
230
+
231
+ const attachGateRouterMeta = (reason, detail = {}) => {
232
+ nextRequestMeta = {
233
+ ...nextRequestMeta,
234
+ gate_router: {
235
+ attempted: true,
236
+ reason,
237
+ ...detail,
238
+ },
239
+ };
240
+ };
241
+
242
+ controllerObserver.emit("controller.prompt_path_selected", {
243
+ applied_from_msg_id: messageId,
244
+ shadow_enabled: shadowEnabled,
245
+ loop_enabled: loopRuntime.enabled,
246
+ });
247
+
248
+ const gateRouterEligibility = shouldUseGateRouter({
249
+ projectRoot,
250
+ prompt: req.text || "",
251
+ requestMeta: nextRequestMeta,
252
+ });
253
+ const gateRouterConfig = gateRouterEligibility.enabled
254
+ ? resolveGateRouterConfig({
255
+ projectRoot,
256
+ requestMeta,
257
+ })
258
+ : null;
259
+
260
+ if (!useGlobalProjectRouter && gateRouterEligibility.enabled && typeof runUfooRouteAgent === "function") {
261
+ logGateRouterEvent("controller.gate_router_attempted", {
262
+ flag: gateRouterEligibility.executionPath,
263
+ intent_reason: gateRouterEligibility.intent.reason,
264
+ });
265
+
266
+ const routed = await runUfooRouteAgent({
267
+ projectRoot,
268
+ prompt: req.text || "",
269
+ provider: gateRouterConfig.provider,
270
+ model: gateRouterConfig.model,
271
+ timeoutMs: gateRouterConfig.timeoutMs,
272
+ });
273
+
274
+ if (!routed || routed.ok !== true) {
275
+ attachGateRouterMeta("provider_error", {
276
+ error: routed && routed.error ? routed.error : "route_agent_failed",
277
+ });
278
+ logGateRouterEvent("controller.gate_router_upgraded", {
279
+ reason: "provider_error",
280
+ fallback_used: "main_router",
281
+ });
282
+ } else {
283
+ const route = routed.route || {};
284
+ const canDispatch = route.decision === "direct_dispatch"
285
+ && route.target && route.target !== "unknown"
286
+ && route.confidence >= gateRouterConfig.confidenceThreshold;
287
+
288
+ if (!canDispatch) {
289
+ const upgradeReason = route.decision && route.decision !== "direct_dispatch"
290
+ ? route.decision
291
+ : "low_confidence";
292
+ attachGateRouterMeta(upgradeReason, {
293
+ decision: route.decision || "",
294
+ target: route.target || "unknown",
295
+ confidence: Number(route.confidence || 0),
296
+ route_reason: route.reason || "",
297
+ });
298
+ logGateRouterEvent("controller.gate_router_upgraded", {
299
+ reason: upgradeReason,
300
+ decision: route.decision || "",
301
+ target: route.target || "unknown",
302
+ confidence: Number(route.confidence || 0),
303
+ fallback_used: "main_router",
304
+ });
305
+ } else {
306
+ const payload = {
307
+ reply: "",
308
+ dispatch: [{
309
+ target: route.target,
310
+ message: route.message || req.text || "",
311
+ injection_mode: route.injection_mode || "immediate",
312
+ source: "ufoo-agent-gate-router",
313
+ }],
314
+ ops: [],
315
+ };
316
+ try {
317
+ markPending(route.target);
318
+ await dispatchMessages(projectRoot, payload.dispatch);
319
+ consumeControllerInboxEntries(projectRoot, "ufoo-agent", privateReports);
320
+ logGateRouterEvent("controller.gate_router_completed", {
321
+ target: route.target,
322
+ confidence: Number(route.confidence || 0),
323
+ provider: routed.meta && routed.meta.provider ? routed.meta.provider : "",
324
+ model: routed.meta && routed.meta.model ? routed.meta.model : "",
325
+ fallback_used: "none",
326
+ });
327
+ socket.write(
328
+ `${JSON.stringify({
329
+ type: IPC_RESPONSE_TYPES.RESPONSE,
330
+ data: payload,
331
+ opsResults: [],
332
+ })}\n`,
333
+ );
334
+ return true;
335
+ } catch (err) {
336
+ attachGateRouterMeta("dispatch_failed", {
337
+ target: route.target,
338
+ confidence: Number(route.confidence || 0),
339
+ route_reason: route.reason || "",
340
+ error: err && err.message ? err.message : String(err),
341
+ });
342
+ logGateRouterEvent("controller.gate_router_upgraded", {
343
+ reason: "dispatch_failed",
344
+ target: route.target,
345
+ confidence: Number(route.confidence || 0),
346
+ fallback_used: "main_router",
347
+ });
348
+ }
349
+ }
350
+ }
351
+ }
352
+
353
+ const promptText = buildPromptWithPrivateReports(req.text || "", privateReports, nextRequestMeta);
136
354
 
137
355
  try {
138
- const handled = await runPromptWithAssistant({
356
+ const handled = await promptRunner({
139
357
  projectRoot,
140
358
  prompt: promptText,
141
359
  provider,
142
360
  model,
143
361
  processManager,
144
362
  runUfooAgent,
145
- runAssistantTask,
363
+ runPromptWithControllerLoop: injectedLoopRunner,
146
364
  dispatchMessages,
147
365
  handleOps,
366
+ ackBus,
148
367
  markPending,
149
368
  reportTaskStatus,
150
369
  maxAssistantLoops: 2,
151
370
  log,
152
- ufooAgentOptions: useGlobalProjectRouter ? { routingMode: "global-router" } : {},
371
+ ufooAgentOptions,
153
372
  finalizeLocally: !useGlobalProjectRouter,
373
+ loopRuntime,
154
374
  });
155
375
 
156
376
  if (!handled.ok) {
@@ -164,6 +384,124 @@ async function handlePromptRequest(options = {}) {
164
384
  return false;
165
385
  }
166
386
 
387
+ let shadowResult = null;
388
+ if (shadowEnabled && !useGlobalProjectRouter && typeof injectedLoopRunner === "function") {
389
+ const shadowObserver = createLoopObserver({
390
+ projectRoot,
391
+ enabled: true,
392
+ defaults: {
393
+ controller_mode: controllerMode,
394
+ shadow_only: true,
395
+ },
396
+ });
397
+
398
+ const shadowSamplingRate = Number.isFinite(Number(requestMeta.shadow_sampling_rate))
399
+ ? Math.max(0, Math.min(1, Number(requestMeta.shadow_sampling_rate)))
400
+ : DEFAULT_SHADOW_SAMPLING_RATE;
401
+ const sampling = shouldSampleShadow({ messageId, samplingRate: shadowSamplingRate });
402
+ const budgetBreaker = createShadowBudgetBreaker({ projectRoot });
403
+ const budgetStatus = budgetBreaker.check();
404
+
405
+ if (!sampling.sampled) {
406
+ shadowObserver.emit("controller.shadow.skipped", {
407
+ applied_from_msg_id: messageId,
408
+ reason: "sampling_excluded",
409
+ sampling_rate: sampling.rate,
410
+ });
411
+ } else if (!budgetStatus.allowed) {
412
+ shadowObserver.emit("controller.shadow.skipped", {
413
+ applied_from_msg_id: messageId,
414
+ reason: budgetStatus.reason,
415
+ sampling_rate: sampling.rate,
416
+ });
417
+ } else {
418
+ const shadowGuard = createShadowGuard({ projectRoot });
419
+ const noOpExecutors = shadowGuard.buildNoOpExecutors();
420
+ const beforeSnapshot = shadowGuard.takeSnapshot();
421
+
422
+ shadowObserver.emit("controller.shadow.started", {
423
+ applied_from_msg_id: messageId,
424
+ primary_mode: CONTROLLER_MODES.LEGACY,
425
+ candidate_mode: CONTROLLER_MODES.LOOP,
426
+ sampling_rate: sampling.rate,
427
+ });
428
+
429
+ try {
430
+ shadowResult = await injectedLoopRunner({
431
+ projectRoot,
432
+ prompt: promptText,
433
+ provider,
434
+ model,
435
+ processManager,
436
+ runUfooAgent,
437
+ dispatchMessages: noOpExecutors.dispatchMessages,
438
+ handleOps: noOpExecutors.handleOps,
439
+ ackBus: noOpExecutors.ackBus,
440
+ markPending: noOpExecutors.markPending,
441
+ reportTaskStatus,
442
+ maxAssistantLoops: 2,
443
+ log,
444
+ ufooAgentOptions,
445
+ finalizeLocally: false,
446
+ loopRuntime: {
447
+ ...resolvedLoopRuntime,
448
+ enabled: true,
449
+ },
450
+ observer: shadowObserver,
451
+ observabilityDefaults: {
452
+ controller_mode: controllerMode,
453
+ shadow_only: true,
454
+ },
455
+ });
456
+ } catch (shadowErr) {
457
+ shadowResult = {
458
+ ok: false,
459
+ error: shadowErr && shadowErr.message ? shadowErr.message : String(shadowErr),
460
+ };
461
+ }
462
+
463
+ const assertion = shadowGuard.assertNoSideEffects(beforeSnapshot);
464
+ if (!assertion.ok) {
465
+ shadowObserver.emit("controller.shadow.violation", {
466
+ applied_from_msg_id: messageId,
467
+ violations: assertion.violations,
468
+ });
469
+ }
470
+
471
+ const loopSummary = shadowResult && shadowResult.payload && shadowResult.payload.loop
472
+ ? shadowResult.payload.loop
473
+ : null;
474
+ const totalInputTokens = loopSummary && typeof loopSummary.total_tokens === "number"
475
+ ? loopSummary.total_tokens
476
+ : 0;
477
+ budgetBreaker.record({ inputTokens: totalInputTokens });
478
+
479
+ const diffFile = appendShadowDiff(projectRoot, {
480
+ event: "controller.shadow.diff",
481
+ request_id: messageId,
482
+ primary_mode: CONTROLLER_MODES.LEGACY,
483
+ candidate_mode: CONTROLLER_MODES.LOOP,
484
+ sampling_rate: sampling.rate,
485
+ primary: summarizeShadowPayload(handled.payload),
486
+ shadow: shadowResult && shadowResult.ok
487
+ ? summarizeShadowPayload(shadowResult.payload)
488
+ : {
489
+ ok: false,
490
+ error: shadowResult && shadowResult.error ? String(shadowResult.error) : "shadow_run_failed",
491
+ },
492
+ side_effects_ok: assertion.ok,
493
+ side_effect_violations: assertion.violations,
494
+ });
495
+ shadowObserver.emit("controller.shadow.completed", {
496
+ applied_from_msg_id: messageId,
497
+ ok: shadowResult && shadowResult.ok === true,
498
+ diff_file: diffFile || "",
499
+ side_effects_ok: assertion.ok,
500
+ terminal_reason: loopSummary ? String(loopSummary.terminal_reason || "") : "",
501
+ });
502
+ }
503
+ }
504
+
167
505
  if (useGlobalProjectRouter) {
168
506
  const route = normalizeProjectRoute(handled.payload && handled.payload.project_route);
169
507
  if (route) {
@@ -216,6 +554,17 @@ async function handlePromptRequest(options = {}) {
216
554
  }
217
555
  const opsResults = handled.opsResults || [];
218
556
  log(`ok reply=${Boolean(payload.reply)} dispatch=${(payload.dispatch || []).length} ops=${(payload.ops || []).length}`);
557
+ controllerObserver.emit("controller.prompt_completed", {
558
+ applied_from_msg_id: messageId,
559
+ ok: true,
560
+ shadow_enabled: shadowEnabled,
561
+ shadow_ok: shadowResult ? shadowResult.ok === true : false,
562
+ dispatch_count: Array.isArray(payload.dispatch) ? payload.dispatch.length : 0,
563
+ ops_count: Array.isArray(payload.ops) ? payload.ops.length : 0,
564
+ terminal_reason: payload.loop && typeof payload.loop === "object"
565
+ ? String(payload.loop.terminal_reason || "").trim()
566
+ : "",
567
+ });
219
568
  socket.write(
220
569
  `${JSON.stringify({
221
570
  type: IPC_RESPONSE_TYPES.RESPONSE,
@@ -226,6 +575,12 @@ async function handlePromptRequest(options = {}) {
226
575
  return true;
227
576
  } catch (err) {
228
577
  log(`error ${err.message || String(err)}`);
578
+ controllerObserver.emit("controller.prompt_completed", {
579
+ applied_from_msg_id: messageId,
580
+ ok: false,
581
+ error: err.message || String(err),
582
+ shadow_enabled: shadowEnabled,
583
+ });
229
584
  socket.write(
230
585
  `${JSON.stringify({
231
586
  type: IPC_RESPONSE_TYPES.ERROR,
@@ -3,6 +3,7 @@ const path = require("path");
3
3
  const { getUfooPaths } = require("../ufoo/paths");
4
4
  const { isMetaActive } = require("../bus/utils");
5
5
  const { readReportSummary, countControllerInboxEntries } = require("../report/store");
6
+ const { readRecentLoopSummary } = require("../agent/loopObservability");
6
7
 
7
8
  function readBus(projectRoot) {
8
9
  const busPath = getUfooPaths(projectRoot).agentsFile;
@@ -180,6 +181,7 @@ function buildStatus(projectRoot, options = {}) {
180
181
  controller: {
181
182
  pending_total: controllerPendingTotal,
182
183
  },
184
+ loop: readRecentLoopSummary(projectRoot),
183
185
  cron: {
184
186
  count: cronTasks.length,
185
187
  tasks: cronTasks,
@@ -5,6 +5,7 @@ const os = require("os");
5
5
  const path = require("path");
6
6
  const { getUfooPaths } = require("../ufoo/paths");
7
7
  const { loadAgentsData } = require("../ufoo/agentsStore");
8
+ const { redactSecrets, redactString } = require("../providerapi/redactor");
8
9
 
9
10
  const HISTORY_DEBUG = process.env.UFOO_HISTORY_DEBUG === "1";
10
11
  const debugLog = (...args) => { if (HISTORY_DEBUG) console.error("[history]", ...args); };
@@ -446,10 +447,10 @@ function appendBusEntry(projectRoot, { seq, timestamp, publisher, target, messag
446
447
  fromId: publisher,
447
448
  to: nicknameMap.get(target) || target,
448
449
  toId: target,
449
- message,
450
+ message: redactString(message),
450
451
  };
451
452
 
452
- fs.appendFileSync(timelineFile, JSON.stringify(entry) + "\n", "utf8");
453
+ fs.appendFileSync(timelineFile, JSON.stringify(redactSecrets(entry)) + "\n", "utf8");
453
454
 
454
455
  if (seq) {
455
456
  const lock = acquireWatermarkLock(projectRoot);
@@ -504,10 +505,14 @@ function buildTimeline(projectRoot, { force = false } = {}) {
504
505
  const lock = acquireWatermarkLock(projectRoot);
505
506
  try {
506
507
  if (force) {
507
- const content = newEntries.map((e) => JSON.stringify(e)).join("\n") + (newEntries.length > 0 ? "\n" : "");
508
+ const content = newEntries.map((e) => JSON.stringify(redactSecrets(e))).join("\n") + (newEntries.length > 0 ? "\n" : "");
508
509
  fs.writeFileSync(timelineFile, content, "utf8");
509
510
  } else {
510
- fs.appendFileSync(timelineFile, newEntries.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf8");
511
+ fs.appendFileSync(
512
+ timelineFile,
513
+ newEntries.map((e) => JSON.stringify(redactSecrets(e))).join("\n") + "\n",
514
+ "utf8"
515
+ );
511
516
  }
512
517
 
513
518
  const prevCount = force ? 0 : (watermark.entryCount || 0);
@@ -0,0 +1,24 @@
1
+ const { ensureDir, appendJSONL, getTimestamp } = require("../bus/utils");
2
+ const { canonicalProjectRoot } = require("../projects/projectId");
3
+ const { getUfooPaths } = require("../ufoo/paths");
4
+
5
+ class MemoryManager {
6
+ constructor(projectRoot) {
7
+ // Phase 0 scaffolding only: this seam must stay dormant until loop/runtime
8
+ // wiring passes an explicit projectRoot into the memory tool path.
9
+ this.projectRoot = canonicalProjectRoot(projectRoot);
10
+ const paths = getUfooPaths(this.projectRoot);
11
+ this.memoryDir = paths.memoryDir;
12
+ this.memoryFile = paths.memoryFile;
13
+ ensureDir(this.memoryDir);
14
+ }
15
+
16
+ addEntry(entry) {
17
+ appendJSONL(this.memoryFile, {
18
+ timestamp: getTimestamp(),
19
+ ...entry,
20
+ });
21
+ }
22
+ }
23
+
24
+ module.exports = MemoryManager;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+
3
+ const REDACTED = "[REDACTED]";
4
+ const SENSITIVE_KEY_PATTERN = /(^|_|-)(authorization|accesstoken|access_token|refreshtoken|refresh_token|apikey|api_key|tokenhash|token_hash)$/i;
5
+ const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+\b/gi;
6
+
7
+ function isPlainObject(value) {
8
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+
11
+ function isSensitiveKey(key = "") {
12
+ const normalized = String(key || "").replace(/[\s-]+/g, "_");
13
+ if (!normalized) return false;
14
+ if (SENSITIVE_KEY_PATTERN.test(normalized)) return true;
15
+ return /^token$/i.test(normalized);
16
+ }
17
+
18
+ function redactString(value) {
19
+ return String(value || "").replace(BEARER_PATTERN, "Bearer [REDACTED]");
20
+ }
21
+
22
+ function redactSecrets(value, options = {}) {
23
+ const seen = options._seen || new WeakMap();
24
+
25
+ if (typeof value === "string") {
26
+ return redactString(value);
27
+ }
28
+ if (!value || typeof value !== "object") {
29
+ return value;
30
+ }
31
+ if (seen.has(value)) {
32
+ return seen.get(value);
33
+ }
34
+ if (Array.isArray(value)) {
35
+ const out = [];
36
+ seen.set(value, out);
37
+ for (const item of value) {
38
+ out.push(redactSecrets(item, { ...options, _seen: seen }));
39
+ }
40
+ return out;
41
+ }
42
+ if (!isPlainObject(value)) {
43
+ return value;
44
+ }
45
+
46
+ const out = {};
47
+ seen.set(value, out);
48
+ for (const [key, entryValue] of Object.entries(value)) {
49
+ if (isSensitiveKey(key)) {
50
+ out[key] = REDACTED;
51
+ continue;
52
+ }
53
+ out[key] = redactSecrets(entryValue, { ...options, _seen: seen });
54
+ }
55
+ return out;
56
+ }
57
+
58
+ function redactJsonLine(value) {
59
+ return JSON.stringify(redactSecrets(value));
60
+ }
61
+
62
+ function redactUfooEvent(event) {
63
+ if (!event || typeof event !== "object") return event;
64
+ return redactSecrets(event);
65
+ }
66
+
67
+ function redactToolCallPayload(payload = {}) {
68
+ const input = payload && typeof payload === "object" ? payload : {};
69
+ return {
70
+ name: typeof input.name === "string" ? input.name : String(input.name || ""),
71
+ args: redactSecrets(input.args || input.arguments || {}),
72
+ tool_call_id: typeof input.tool_call_id === "string"
73
+ ? input.tool_call_id
74
+ : (typeof input.toolCallId === "string" ? input.toolCallId : ""),
75
+ caller_tier: typeof input.caller_tier === "string" ? input.caller_tier : "",
76
+ };
77
+ }
78
+
79
+ module.exports = {
80
+ REDACTED,
81
+ redactSecrets,
82
+ redactJsonLine,
83
+ redactString,
84
+ isSensitiveKey,
85
+ redactUfooEvent,
86
+ redactToolCallPayload,
87
+ };