@jungjaehoon/mama-os 0.21.0 → 0.22.1

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 (151) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +12 -4
  3. package/dist/agent/agent-loop.d.ts.map +1 -1
  4. package/dist/agent/agent-loop.js +76 -11
  5. package/dist/agent/agent-loop.js.map +1 -1
  6. package/dist/agent/code-act/host-bridge.d.ts.map +1 -1
  7. package/dist/agent/code-act/host-bridge.js +74 -0
  8. package/dist/agent/code-act/host-bridge.js.map +1 -1
  9. package/dist/agent/codex-mcp-process.d.ts +1 -0
  10. package/dist/agent/codex-mcp-process.d.ts.map +1 -1
  11. package/dist/agent/codex-mcp-process.js +3 -2
  12. package/dist/agent/codex-mcp-process.js.map +1 -1
  13. package/dist/agent/gateway-tool-executor.d.ts +16 -1
  14. package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
  15. package/dist/agent/gateway-tool-executor.js +148 -2
  16. package/dist/agent/gateway-tool-executor.js.map +1 -1
  17. package/dist/agent/gateway-tools.md +6 -1
  18. package/dist/agent/model-runner.d.ts +8 -0
  19. package/dist/agent/model-runner.d.ts.map +1 -1
  20. package/dist/agent/model-runner.js.map +1 -1
  21. package/dist/agent/persistent-cli-adapter.d.ts +6 -0
  22. package/dist/agent/persistent-cli-adapter.d.ts.map +1 -1
  23. package/dist/agent/persistent-cli-adapter.js +21 -10
  24. package/dist/agent/persistent-cli-adapter.js.map +1 -1
  25. package/dist/agent/tool-registry.d.ts.map +1 -1
  26. package/dist/agent/tool-registry.js +32 -1
  27. package/dist/agent/tool-registry.js.map +1 -1
  28. package/dist/agent/types.d.ts +26 -2
  29. package/dist/agent/types.d.ts.map +1 -1
  30. package/dist/agent/types.js.map +1 -1
  31. package/dist/api/index.d.ts +1 -1
  32. package/dist/api/index.d.ts.map +1 -1
  33. package/dist/api/index.js +15 -13
  34. package/dist/api/index.js.map +1 -1
  35. package/dist/api/operator-handler.d.ts +2 -0
  36. package/dist/api/operator-handler.d.ts.map +1 -1
  37. package/dist/api/operator-handler.js +23 -5
  38. package/dist/api/operator-handler.js.map +1 -1
  39. package/dist/api/operator-summary.d.ts +3 -0
  40. package/dist/api/operator-summary.d.ts.map +1 -0
  41. package/dist/api/operator-summary.js +72 -0
  42. package/dist/api/operator-summary.js.map +1 -0
  43. package/dist/api/operator-tasks-handler.d.ts +7 -0
  44. package/dist/api/operator-tasks-handler.d.ts.map +1 -0
  45. package/dist/api/operator-tasks-handler.js +165 -0
  46. package/dist/api/operator-tasks-handler.js.map +1 -0
  47. package/dist/cli/commands/start.d.ts.map +1 -1
  48. package/dist/cli/commands/start.js +62 -12
  49. package/dist/cli/commands/start.js.map +1 -1
  50. package/dist/cli/runtime/agent-loop-init.d.ts +2 -1
  51. package/dist/cli/runtime/agent-loop-init.d.ts.map +1 -1
  52. package/dist/cli/runtime/agent-loop-init.js +16 -8
  53. package/dist/cli/runtime/agent-loop-init.js.map +1 -1
  54. package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
  55. package/dist/cli/runtime/api-routes-init.js +196 -158
  56. package/dist/cli/runtime/api-routes-init.js.map +1 -1
  57. package/dist/cli/runtime/gateway-init.d.ts.map +1 -1
  58. package/dist/cli/runtime/gateway-init.js +3 -1
  59. package/dist/cli/runtime/gateway-init.js.map +1 -1
  60. package/dist/cli/runtime/gateway-wiring.d.ts.map +1 -1
  61. package/dist/cli/runtime/gateway-wiring.js +6 -0
  62. package/dist/cli/runtime/gateway-wiring.js.map +1 -1
  63. package/dist/cli/runtime/memory-agent-init.d.ts +2 -1
  64. package/dist/cli/runtime/memory-agent-init.d.ts.map +1 -1
  65. package/dist/cli/runtime/memory-agent-init.js +15 -3
  66. package/dist/cli/runtime/memory-agent-init.js.map +1 -1
  67. package/dist/connectors/framework/types.d.ts +8 -0
  68. package/dist/connectors/framework/types.d.ts.map +1 -1
  69. package/dist/connectors/slack/index.d.ts.map +1 -1
  70. package/dist/connectors/slack/index.js +3 -1
  71. package/dist/connectors/slack/index.js.map +1 -1
  72. package/dist/connectors/trello/index.d.ts.map +1 -1
  73. package/dist/connectors/trello/index.js +6 -2
  74. package/dist/connectors/trello/index.js.map +1 -1
  75. package/dist/db/agent-store.d.ts +5 -0
  76. package/dist/db/agent-store.d.ts.map +1 -1
  77. package/dist/db/agent-store.js +6 -3
  78. package/dist/db/agent-store.js.map +1 -1
  79. package/dist/db/migrations/agent-activity-tool-name.d.ts +8 -0
  80. package/dist/db/migrations/agent-activity-tool-name.d.ts.map +1 -0
  81. package/dist/db/migrations/agent-activity-tool-name.js +24 -0
  82. package/dist/db/migrations/agent-activity-tool-name.js.map +1 -0
  83. package/dist/envelope/run-guard.d.ts +10 -0
  84. package/dist/envelope/run-guard.d.ts.map +1 -0
  85. package/dist/envelope/run-guard.js +19 -0
  86. package/dist/envelope/run-guard.js.map +1 -0
  87. package/dist/gateways/telegram.d.ts +1 -0
  88. package/dist/gateways/telegram.d.ts.map +1 -1
  89. package/dist/gateways/telegram.js +25 -1
  90. package/dist/gateways/telegram.js.map +1 -1
  91. package/dist/memory/history-extractor.d.ts.map +1 -1
  92. package/dist/memory/history-extractor.js +10 -5
  93. package/dist/memory/history-extractor.js.map +1 -1
  94. package/dist/multi-agent/agent-event-bus.d.ts +4 -0
  95. package/dist/multi-agent/agent-event-bus.d.ts.map +1 -1
  96. package/dist/multi-agent/agent-event-bus.js.map +1 -1
  97. package/dist/multi-agent/dashboard-agent-persona.d.ts.map +1 -1
  98. package/dist/multi-agent/dashboard-agent-persona.js +18 -5
  99. package/dist/multi-agent/dashboard-agent-persona.js.map +1 -1
  100. package/dist/multi-agent/runtime-process.d.ts +4 -2
  101. package/dist/multi-agent/runtime-process.d.ts.map +1 -1
  102. package/dist/multi-agent/runtime-process.js +7 -4
  103. package/dist/multi-agent/runtime-process.js.map +1 -1
  104. package/dist/observability/code-audit.d.ts +83 -0
  105. package/dist/observability/code-audit.d.ts.map +1 -0
  106. package/dist/observability/code-audit.js +334 -0
  107. package/dist/observability/code-audit.js.map +1 -0
  108. package/dist/operator/action-verifier.d.ts +50 -0
  109. package/dist/operator/action-verifier.d.ts.map +1 -0
  110. package/dist/operator/action-verifier.js +68 -0
  111. package/dist/operator/action-verifier.js.map +1 -0
  112. package/dist/operator/board-reconcile.d.ts +62 -0
  113. package/dist/operator/board-reconcile.d.ts.map +1 -0
  114. package/dist/operator/board-reconcile.js +160 -0
  115. package/dist/operator/board-reconcile.js.map +1 -0
  116. package/dist/operator/board-slot-instructions.d.ts +8 -0
  117. package/dist/operator/board-slot-instructions.d.ts.map +1 -1
  118. package/dist/operator/board-slot-instructions.js +25 -1
  119. package/dist/operator/board-slot-instructions.js.map +1 -1
  120. package/dist/operator/operator-trigger-loop.d.ts +15 -2
  121. package/dist/operator/operator-trigger-loop.d.ts.map +1 -1
  122. package/dist/operator/operator-trigger-loop.js +40 -1
  123. package/dist/operator/operator-trigger-loop.js.map +1 -1
  124. package/dist/operator/report-scheduler.d.ts +23 -0
  125. package/dist/operator/report-scheduler.d.ts.map +1 -1
  126. package/dist/operator/report-scheduler.js +56 -8
  127. package/dist/operator/report-scheduler.js.map +1 -1
  128. package/dist/operator/situation-report.d.ts +5 -1
  129. package/dist/operator/situation-report.d.ts.map +1 -1
  130. package/dist/operator/situation-report.js +14 -3
  131. package/dist/operator/situation-report.js.map +1 -1
  132. package/dist/operator/task-ledger.d.ts +96 -0
  133. package/dist/operator/task-ledger.d.ts.map +1 -0
  134. package/dist/operator/task-ledger.js +277 -0
  135. package/dist/operator/task-ledger.js.map +1 -0
  136. package/dist/security/security-monitor.d.ts +12 -0
  137. package/dist/security/security-monitor.d.ts.map +1 -1
  138. package/dist/security/security-monitor.js +51 -14
  139. package/dist/security/security-monitor.js.map +1 -1
  140. package/dist/utils/untrusted-content.d.ts +18 -0
  141. package/dist/utils/untrusted-content.d.ts.map +1 -0
  142. package/dist/utils/untrusted-content.js +34 -0
  143. package/dist/utils/untrusted-content.js.map +1 -0
  144. package/package.json +1 -1
  145. package/public/ui/assets/index-CKOJQYXA.css +1 -0
  146. package/public/ui/assets/index-OSgiwL24.js +62 -0
  147. package/public/ui/index.html +3 -2
  148. package/public/ui/theme-init.js +20 -0
  149. package/public/viewer/viewer.html +12 -28
  150. package/public/ui/assets/index-BDsaQNGd.js +0 -62
  151. package/public/ui/assets/index-C4URnm_X.css +0 -1
@@ -53,8 +53,11 @@ const session_service_js_1 = require("../../validation/session-service.js");
53
53
  const agent_store_js_1 = require("../../db/agent-store.js");
54
54
  const memory_agent_dashboard_js_1 = require("../../memory/memory-agent-dashboard.js");
55
55
  const upload_handler_js_1 = require("../../api/upload-handler.js");
56
+ const operator_tasks_handler_js_1 = require("../../api/operator-tasks-handler.js");
56
57
  const auth_middleware_js_1 = require("../../api/auth-middleware.js");
57
58
  const utilities_js_1 = require("./utilities.js");
59
+ const code_audit_js_1 = require("../../observability/code-audit.js");
60
+ const security_monitor_js_1 = require("../../security/security-monitor.js");
58
61
  const debugLogger = __importStar(require("@jungjaehoon/mama-core/debug-logger"));
59
62
  const { DebugLogger } = debugLogger;
60
63
  const routesLogger = new DebugLogger('api-routes');
@@ -200,6 +203,9 @@ async function registerApiRoutes(params) {
200
203
  }
201
204
  // Wire EventBus to tool executor for agent_notices tool
202
205
  toolExecutor.setAgentEventBus(eventBus);
206
+ (0, operator_tasks_handler_js_1.registerOperatorTaskRoutes)(apiServer.app, {
207
+ getTaskLedger: () => toolExecutor.getTaskLedger(),
208
+ });
203
209
  const hasEnabledAgentConfig = (agentId) => {
204
210
  const agentConfig = config.multi_agent?.agents?.[agentId];
205
211
  return Boolean(agentConfig && agentConfig.enabled !== false);
@@ -262,7 +268,9 @@ async function registerApiRoutes(params) {
262
268
  ensureDashboardPersona();
263
269
  routesLogger.debug('[Dashboard Agent] Persona ensured at ~/.mama/personas/dashboard.md');
264
270
  // Dashboard cron: 30-min interval via AgentProcessManager
265
- const dashboardPrompt = `You are triggered on a schedule. Before writing anything, determine if an update is needed:
271
+ // Built PER RUN: the tracker's D-day arithmetic needs today's date, and a
272
+ // module-const prompt would freeze it (#134 lesson).
273
+ const buildDashboardPrompt = () => `You are triggered on a schedule. Today is ${new Date().toISOString().slice(0, 10)}. Before writing anything, determine if an update is needed:
266
274
 
267
275
  1. Use agent_notices({limit: 50}) to find the most recent dashboard-agent publish/task_complete notice. Treat that as the last briefing boundary.
268
276
  2. Use context_compile first to find recent substantive decisions (limit 20, max_tool_calls 2, strictness "balanced").
@@ -270,13 +278,13 @@ async function registerApiRoutes(params) {
270
278
  Do not include dashboard_briefing, wiki_compilation, system-audit, or audit-log labels in the context_compile task text; filter those operational summaries after the packet returns.
271
279
  If context_compile is unavailable because there is no active worker envelope, fall back to mama_search once (limit 20).
272
280
  3. If NO substantive decisions or agent alerts exist since the last dashboard publish → respond "NO_UPDATE" and stop. Do NOT call report_publish.
273
- 4. If new substantive information exists -> analyze it and publish ALL FOUR board slots (briefing, action_required, decisions, pipeline) in a SINGLE report_publish call, using the board HTML vocabulary from your persona.
281
+ 4. If new substantive information exists -> analyze it and publish ALL FOUR board slots (briefing, action_required, decisions, pipeline) in a SINGLE report_publish call, using the board HTML vocabulary from your persona. The pipeline slot is the item tracker projected from task_list (see your persona); compute D-day from today's date above.
274
282
  5. Do NOT call mama_save for the briefing; report_publish and agent_activity are the durable operational record.
275
283
 
276
284
  This saves resources. Only publish when there is genuinely new information to report.`;
277
285
  // Owner-initiated refresh skips the NO_UPDATE delta gate: an explicit
278
286
  // request means "rebuild the board now", not "tell me nothing changed".
279
- const forcedDashboardPrompt = dashboardPrompt.replace(/3\. If NO substantive decisions[^\n]*\n/, '3. The owner explicitly requested a fresh board: do NOT reply NO_UPDATE. Rebuild and publish even if nothing changed since the last publish.\n');
287
+ const buildForcedDashboardPrompt = () => buildDashboardPrompt().replace(/3\. If NO substantive decisions[^\n]*\n/, '3. The owner explicitly requested a fresh board: do NOT reply NO_UPDATE. Rebuild and publish even if nothing changed since the last publish.\n');
280
288
  const doDashboardRun = async (opts) => {
281
289
  const pm = toolExecutor.getAgentProcessManager();
282
290
  if (!pm) {
@@ -287,7 +295,7 @@ This saves resources. Only publish when there is genuinely new information to re
287
295
  // console.log on run outcomes: the DebugLogger only surfaces warn/error in the
288
296
  // daemon log, and a silent outcome reads as a hang from the outside.
289
297
  console.log(`[Dashboard Agent] run started${opts?.force ? ' (owner-forced, delta gate bypassed)' : ''}`);
290
- const { noUpdate } = await executeValidatedRun('dashboard-agent', opts?.force ? forcedDashboardPrompt : dashboardPrompt);
298
+ const { noUpdate } = await executeValidatedRun('dashboard-agent', opts?.force ? buildForcedDashboardPrompt() : buildDashboardPrompt());
291
299
  console.log(noUpdate
292
300
  ? '[Dashboard Agent] no changes detected, publish skipped'
293
301
  : '[Dashboard Agent] board published');
@@ -296,16 +304,20 @@ This saves resources. Only publish when there is genuinely new information to re
296
304
  routesLogger.error('[Dashboard Agent] Error:', err instanceof Error ? err.message : err);
297
305
  }
298
306
  };
299
- // Serialize ALL dashboard runs (boot, interval, manual) on one chain -- the shared
300
- // agent process rejects concurrent requests, so unserialized triggers raced into
301
- // 'Process is busy' errors (same pattern as CronWorker's executionQueue).
302
- let dashboardRunChain = Promise.resolve();
303
- const runDashboardAgent = (opts) => {
304
- // Trailing catch keeps one failed link from poisoning every future run:
305
- // a rejected chain would silently skip all subsequent .then() links.
306
- dashboardRunChain = dashboardRunChain.then(() => doDashboardRun(opts)).catch(() => { });
307
- return dashboardRunChain;
307
+ // ONE board-writer queue: dashboard cron, manual refresh, AND reconcile runs
308
+ // all serialize here -- the shared agent process rejects concurrent requests
309
+ // ('Process is busy' class). Per-job rejection propagates to the caller
310
+ // while the chain itself survives (M8 review: a caller must be able to
311
+ // distinguish its own job's failure).
312
+ let boardWriterChain = Promise.resolve();
313
+ const boardWriterQueue = {
314
+ push(job) {
315
+ const jobPromise = boardWriterChain.then(job);
316
+ boardWriterChain = jobPromise.catch(() => { });
317
+ return jobPromise;
318
+ },
308
319
  };
320
+ const runDashboardAgent = (opts) => boardWriterQueue.push(() => doDashboardRun(opts)).catch(() => { });
309
321
  // First run after 10s (let connectors poll first), then every 30 min
310
322
  setTimeout(runDashboardAgent, 10_000);
311
323
  setInterval(runDashboardAgent, 30 * 60 * 1000);
@@ -314,6 +326,112 @@ This saves resources. Only publish when there is genuinely new information to re
314
326
  runDashboardAgent({ force: true }).catch(() => { });
315
327
  res.json({ ok: true, message: 'Dashboard agent triggered (forced refresh)' });
316
328
  });
329
+ // -- M8 board reconcile leg (freshness layer; default OFF, opt-in like
330
+ // MAMA_TRIGGER_LOOP). The trigger loop emits operator:channel-delta after
331
+ // committing its cursor; the 30-min cron above remains the repair pass.
332
+ if (process.env.MAMA_BOARD_RECONCILE === '1') {
333
+ const { buildReconcilePrompt, ReconcileScheduler } = await import('../../operator/board-reconcile.js');
334
+ const { captureSnapshot, verifyAfterRun, OBLIGATED_TOOLS } = await import('../../operator/action-verifier.js');
335
+ const kagemushaContext = (process.env.MAMA_RECONCILE_TASK_CONTEXT ?? 'native') === 'kagemusha';
336
+ // Verifier deps (M8 Phase 2): run-bound signals only -- trace rows from
337
+ // the dashboard agent's gateway_tool_call activity, notes from the
338
+ // operator ledger. Observe, never block.
339
+ const reconcileLedger = toolExecutor.getTaskLedger();
340
+ // gateway_tool_call rows now populate normalized_tool_name at the logging
341
+ // site; the input_summary fallback covers rows written before that fix.
342
+ const traceToolList = OBLIGATED_TOOLS.map((t) => `'${t}'`).join(',');
343
+ const verifierDeps = {
344
+ getSlots: () => apiServer.reportStore.getAllSorted(),
345
+ getLedgerHash: () => reconcileLedger?.payloadHash() ?? '',
346
+ getScopedNoteMaxId: (scope) => reconcileLedger?.maxNoUpdateId(scope) ?? 0,
347
+ getTraceMaxId: () => {
348
+ if (!sessionsDb)
349
+ return 0;
350
+ const row = sessionsDb
351
+ .prepare(`SELECT MAX(id) AS max_id FROM agent_activity
352
+ WHERE type = 'gateway_tool_call' AND agent_id = 'dashboard-agent'`)
353
+ .get();
354
+ return row.max_id ?? 0;
355
+ },
356
+ countObligatedTraceRowsSince: (maxId) => {
357
+ if (!sessionsDb)
358
+ return 0;
359
+ const row = sessionsDb
360
+ .prepare(`SELECT COUNT(*) AS n FROM agent_activity
361
+ WHERE type = 'gateway_tool_call' AND agent_id = 'dashboard-agent'
362
+ AND id > ? AND (normalized_tool_name IN (${traceToolList}) OR input_summary IN (${traceToolList}))`)
363
+ .get(maxId);
364
+ return row.n;
365
+ },
366
+ };
367
+ const reconcileScheduler = new ReconcileScheduler({
368
+ debounceMs: Number(process.env.MAMA_RECONCILE_DEBOUNCE_MS) || undefined,
369
+ maxWaitMs: Number(process.env.MAMA_RECONCILE_MAX_WAIT_MS) || undefined,
370
+ globalMaxPerHour: Number(process.env.MAMA_RECONCILE_MAX_PER_HOUR) || undefined,
371
+ log: (line) => console.log(line),
372
+ run: (channelKey, deltaLines) => boardWriterQueue.push(async () => {
373
+ const scope = `reconcile:${channelKey}`;
374
+ const before = captureSnapshot(verifierDeps, scope);
375
+ const prompt = buildReconcilePrompt({
376
+ channelKey,
377
+ deltaLines,
378
+ todayIso: new Date().toISOString().slice(0, 10),
379
+ kagemushaContext,
380
+ });
381
+ await executeValidatedRun('dashboard-agent', prompt, {
382
+ requestTimeout: 300_000,
383
+ });
384
+ const verdict = verifyAfterRun(verifierDeps, before, scope);
385
+ const outcome = verdict.verified ? 'reconcile_verified' : 'reconcile_unverified';
386
+ console.log(`[reconcile] ${outcome} channel=${channelKey}${verdict.effects.length > 0 ? ` (${verdict.effects.join('; ')})` : ''}`);
387
+ try {
388
+ if (sessionsDb) {
389
+ (0, agent_store_js_1.logActivity)(sessionsDb, {
390
+ agent_id: 'dashboard-agent',
391
+ agent_version: 0,
392
+ type: outcome,
393
+ input_summary: `reconcile ${channelKey}`,
394
+ output_summary: verdict.effects.join('; '),
395
+ execution_status: 'completed',
396
+ trigger_reason: 'reconcile',
397
+ });
398
+ }
399
+ }
400
+ catch {
401
+ /* telemetry only */
402
+ }
403
+ if (!verdict.verified) {
404
+ // Loud, never blocking (observability over restriction).
405
+ eventBus.emit({
406
+ type: 'agent:action',
407
+ agent: 'Dashboard Agent',
408
+ action: 'reconcile_unverified',
409
+ target: channelKey,
410
+ });
411
+ }
412
+ }),
413
+ });
414
+ eventBus.on('operator:channel-delta', (event) => {
415
+ if (event.type === 'operator:channel-delta') {
416
+ reconcileScheduler.enqueue(event.channelKey, event.lines);
417
+ }
418
+ });
419
+ // Manual reconcile: lines from the body, or the caller must supply them
420
+ // (no silent alternate data path -- M8 review #17).
421
+ apiServer.app.post('/api/operator/reconcile', auth_middleware_js_1.requireAuth, (req, res) => {
422
+ const { channelKey, lines } = (req.body ?? {});
423
+ if (!channelKey || !Array.isArray(lines) || lines.length === 0) {
424
+ res.status(400).json({
425
+ ok: false,
426
+ error: 'channelKey and non-empty lines[] are required',
427
+ });
428
+ return;
429
+ }
430
+ reconcileScheduler.enqueue(channelKey, lines);
431
+ res.json({ ok: true, message: 'Reconcile queued (async; runs after debounce)' });
432
+ });
433
+ console.log('[reconcile] Board reconcile leg enabled (MAMA_BOARD_RECONCILE=1)');
434
+ }
317
435
  }
318
436
  else {
319
437
  routesLogger.debug('[Dashboard Agent] Skipped; dashboard-agent is not configured');
@@ -503,164 +621,84 @@ This saves resources. Only compile when there is genuinely new information to do
503
621
  });
504
622
  console.log(`[Memory Promotion] Ready: every ${PROMOTION_INTERVAL_MS / 3_600_000}h, POST /api/memory/promote`);
505
623
  }
506
- // ── Conductor Audit hourly system health check ──────────────────────
624
+ // -- System Audit: hourly deterministic code checks ---------------------
625
+ // Owner decision 2026-04-22 (mama_conductor_audit_code_based_read_only),
626
+ // landed 2026-07-17: the audit is fact collection and recording executed by
627
+ // CODE - no LLM invocation, no auto-fix, no broad filesystem access. The
628
+ // prior hourly LLM audit violated that decision and the 2026-05-14
629
+ // no-shell-autofix rule, and lost its tools entirely to the persona
630
+ // lockdown (--tools ""). The 24h alert-dedup contract (owner verdict
631
+ // 2026-07-11) is preserved inside runCodeAudit; MAJOR findings flow through
632
+ // dispatchSecurityAlertDirect to the configured security alert sender.
507
633
  const AUDIT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
508
634
  const AUDIT_INITIAL_DELAY_MS = 5 * 60 * 1000; // 5 min after startup
509
- // Owner verdict 2026-07-11: the hourly audit re-sent the same MAJOR alert every
510
- // run because each audit was stateless. agent_notices does not carry audit
511
- // findings (review feedback on #134), so dedup state lives in a JSON file the
512
- // conductor reads/writes itself, and the current time is interpolated per run
513
- // so the 24h comparison is grounded.
514
- const buildAuditPrompt = (nowIso) => 'Perform a system audit. Read ~/.mama/skills/audit-checklist.md and execute each step. ' +
515
- 'Classify findings as MINOR or MAJOR. ' +
516
- 'For MINOR items: execute the auto-fix command immediately (Bash curl). Do NOT just report — fix it. ' +
517
- 'For MAJOR items: report to human via channel alert, subject to the dedup rule below. ' +
518
- `Alert dedup (MANDATORY): the current time is ${nowIso}. Read ~/.mama/state/audit-findings.json ` +
519
- '(it may not exist on the first run). Re-alert a MAJOR finding only if it is NEW (no entry ' +
520
- 'with the same id), has ESCALATED, or its last_alerted_at is older than 24 hours compared to ' +
521
- 'the current time; otherwise stay silent about it. Never alert MINOR findings to the owner. ' +
522
- 'After the audit, Write ~/.mama/state/audit-findings.json with an array of every current ' +
523
- 'finding: {id (stable kebab-case slug), severity, first_seen, last_seen, last_alerted_at}. ' +
524
- 'Carry forward first_seen and last_alerted_at from the previous file; set last_alerted_at to ' +
525
- 'the current time only for findings you actually alerted this run. ' +
526
- // Owner verdict 2026-07-10: hourly-audit self-notes are memory noise -- the two
527
- // "durable policy" saves it produced were unscoped near-duplicates polluting
528
- // global recall. The audit record lives in validation sessions + agent_activity;
529
- // MAJOR findings reach the owner via the channel alert. No memory writes here.
530
- 'Do NOT call mama_save under any circumstances: the validation session and agent_activity ' +
531
- 'are the durable audit record, and repeated audits must not accumulate policy notes in memory.';
532
- const runConductorAudit = async () => {
533
- let auditSession = null;
534
- let auditVersion = 0;
535
- const auditStart = Date.now();
635
+ const doSystemAudit = async () => {
536
636
  try {
537
- routesLogger.debug('[Conductor Audit] Starting hourly audit...');
538
- try {
539
- const ver = sessionsDb ? (0, agent_store_js_1.getLatestVersion)(sessionsDb, 'conductor') : null;
540
- auditVersion = ver?.version ?? 0;
541
- auditSession =
542
- validationService?.startSession('conductor', auditVersion, 'audit', {
543
- goal: 'hourly system audit',
544
- }) ?? null;
545
- }
546
- catch (bootstrapErr) {
547
- routesLogger.warn('[Conductor Audit] Failed to initialize audit telemetry:', bootstrapErr);
548
- auditVersion = 0;
549
- auditSession = null;
550
- }
551
- if (sessionsDb) {
552
- try {
553
- const startRow = (0, agent_store_js_1.logActivity)(sessionsDb, {
554
- agent_id: 'conductor',
555
- agent_version: auditVersion,
556
- type: 'audit_start',
557
- input_summary: 'Hourly system audit',
558
- run_id: auditSession?.id,
559
- execution_status: 'started',
560
- trigger_reason: 'audit',
637
+ const report = await (0, code_audit_js_1.runCodeAudit)({
638
+ config: {
639
+ telegram: config.telegram,
640
+ multi_agent: config.multi_agent,
641
+ },
642
+ // Evaluated at audit time: gateway-wiring registers the sender only
643
+ // for ACTIVE gateways, so a configured target on a dead gateway must
644
+ // not report a false PASS.
645
+ securityAlertConfigured: (0, security_monitor_js_1.hasSecurityAlertSender)(),
646
+ // Direct dispatch, NOT recordSecurityEvent: audit findings are
647
+ // self-generated and must not fabricate incident/denylist/RDAP
648
+ // artifacts for a pseudo client. dispatchSecurityAlertDirect awaits
649
+ // delivery and throws on failure, so runCodeAudit keeps
650
+ // last_alerted_at null and retries on the next run (fail loud).
651
+ alert: async (finding, reason) => {
652
+ await (0, security_monitor_js_1.dispatchSecurityAlertDirect)({
653
+ type: 'system_audit_finding',
654
+ severity: 'critical',
655
+ message: `[Audit/${reason}] ${finding.summary}`,
656
+ // path carries the finding id so the alert-cooldown fingerprint
657
+ // distinguishes findings instead of collapsing them all into one.
658
+ path: finding.id,
659
+ details: { findingId: finding.id, detail: finding.detail ?? null },
561
660
  });
562
- if (auditSession) {
563
- try {
564
- validationService?.recordRun(auditSession.id, { activityId: startRow.id });
565
- }
566
- catch (telemetryErr) {
567
- routesLogger.warn('[Conductor Audit] Failed to link startup activity to validation session:', telemetryErr);
568
- }
569
- }
570
- }
571
- catch (telemetryErr) {
572
- routesLogger.warn('[Conductor Audit] Failed to write startup telemetry:', telemetryErr);
573
- }
574
- }
575
- const auditChannelId = `conductor-audit-${Date.now()}`;
576
- await messageRouter.process({
577
- source: 'system',
578
- channelId: auditChannelId,
579
- userId: 'system',
580
- text: buildAuditPrompt(new Date().toISOString()),
661
+ },
581
662
  });
582
- const auditDuration = Date.now() - auditStart;
583
- try {
584
- if (sessionsDb) {
585
- const activityRow = (0, agent_store_js_1.logActivity)(sessionsDb, {
586
- agent_id: 'conductor',
587
- agent_version: auditVersion,
588
- type: 'audit_complete',
589
- input_summary: 'Hourly system audit',
590
- duration_ms: auditDuration,
591
- run_id: auditSession?.id,
592
- execution_status: 'completed',
593
- trigger_reason: 'audit',
594
- });
595
- if (auditSession) {
596
- validationService?.recordRun(auditSession.id, { activityId: activityRow.id });
597
- }
598
- }
599
- }
600
- catch (telemetryErr) {
601
- routesLogger.warn('[Conductor Audit] Failed to write completion telemetry:', telemetryErr);
602
- }
603
- try {
604
- if (auditSession && validationService) {
605
- validationService.finalizeSession(auditSession.id, {
606
- execution_status: 'completed',
607
- metrics: { duration_ms: auditDuration },
608
- });
609
- }
610
- }
611
- catch (telemetryErr) {
612
- routesLogger.warn('[Conductor Audit] Failed to finalize validation session:', telemetryErr);
613
- }
614
- routesLogger.debug('[Conductor Audit] Audit complete');
663
+ const majors = report.findings.filter((f) => f.severity === 'MAJOR').length;
664
+ const minors = report.findings.filter((f) => f.severity === 'MINOR').length;
665
+ routesLogger.info(`[System Audit] ${report.pass_items.length} pass, ${majors} MAJOR, ${minors} MINOR, ` +
666
+ `alerted=[${report.alerted.join(',')}] in ${report.duration_ms}ms`);
667
+ if (report.alert_delivery_failures.length > 0) {
668
+ routesLogger.error('[System Audit] Alert delivery failures:', report.alert_delivery_failures.join('; '));
669
+ }
670
+ return report;
615
671
  }
616
672
  catch (err) {
617
- const auditDuration = Date.now() - auditStart;
618
- try {
619
- if (sessionsDb) {
620
- const failRow = (0, agent_store_js_1.logActivity)(sessionsDb, {
621
- agent_id: 'conductor',
622
- agent_version: auditVersion,
623
- type: 'audit_failed',
624
- input_summary: 'Hourly system audit failed',
625
- error_message: err instanceof Error ? err.message : String(err),
626
- duration_ms: auditDuration,
627
- run_id: auditSession?.id,
628
- execution_status: 'failed',
629
- trigger_reason: 'audit',
630
- });
631
- if (auditSession) {
632
- validationService?.recordRun(auditSession.id, { activityId: failRow.id });
633
- }
634
- }
635
- }
636
- catch (telemetryErr) {
637
- routesLogger.warn('[Conductor Audit] Failed to write failure telemetry:', telemetryErr);
638
- }
639
- try {
640
- if (auditSession && validationService) {
641
- validationService.finalizeSession(auditSession.id, {
642
- execution_status: 'failed',
643
- error_message: err instanceof Error ? err.message : String(err),
644
- metrics: { duration_ms: auditDuration },
645
- });
646
- }
647
- }
648
- catch (telemetryErr) {
649
- routesLogger.warn('[Conductor Audit] Failed to finalize failed session:', telemetryErr);
650
- }
651
- routesLogger.error('[Conductor Audit] Failed:', err instanceof Error ? err.message : String(err));
673
+ routesLogger.error('[System Audit] Failed:', err instanceof Error ? err.message : String(err));
674
+ return null;
652
675
  }
653
676
  };
677
+ // Serialize scheduled and manual runs (same pattern as the memory-promotion
678
+ // chain above): overlapping audits would read the same prior state, alert
679
+ // twice, and race the audit-findings.json write. doSystemAudit never
680
+ // rejects, so chaining on the previous run is sufficient.
681
+ let auditRunChain = Promise.resolve(null);
682
+ const runSystemAudit = () => {
683
+ const next = auditRunChain.then(doSystemAudit);
684
+ auditRunChain = next;
685
+ return next;
686
+ };
654
687
  setTimeout(() => {
655
- runConductorAudit();
656
- setInterval(runConductorAudit, AUDIT_INTERVAL_MS);
688
+ void runSystemAudit();
689
+ setInterval(() => void runSystemAudit(), AUDIT_INTERVAL_MS);
657
690
  }, AUDIT_INITIAL_DELAY_MS);
658
- // Manual trigger
691
+ // Manual trigger returns the full report (read-only, no LLM, fast)
659
692
  apiServer.app.post('/api/conductor/audit', auth_middleware_js_1.requireAuth, async (_req, res) => {
660
- runConductorAudit().catch(() => { });
661
- res.json({ ok: true, message: 'Conductor audit triggered' });
693
+ const report = await runSystemAudit();
694
+ if (report) {
695
+ res.json({ ok: true, mode: 'code', report });
696
+ }
697
+ else {
698
+ res.status(500).json({ ok: false, error: 'audit failed - see daemon.log' });
699
+ }
662
700
  });
663
- routesLogger.info('[Conductor Audit] Ready runs every 60 min, POST /api/conductor/audit for manual trigger');
701
+ routesLogger.info('[System Audit] Ready - deterministic code checks every 60 min, POST /api/conductor/audit for manual run');
664
702
  // ── Memory Agent stats API ────────────────────────────────────────────
665
703
  apiServer.app.get('/api/memory-agent/stats', auth_middleware_js_1.requireAuth, (_req, res) => {
666
704
  const stats = messageRouter.getMemoryAgentStats();