@jungjaehoon/mama-os 0.20.1 → 0.22.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 (264) 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 +88 -3
  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 +24 -11
  14. package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
  15. package/dist/agent/gateway-tool-executor.js +349 -30
  16. package/dist/agent/gateway-tool-executor.js.map +1 -1
  17. package/dist/agent/gateway-tools.md +7 -2
  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/os-agent-capabilities.md +13 -12
  22. package/dist/agent/persistent-cli-adapter.d.ts +6 -0
  23. package/dist/agent/persistent-cli-adapter.d.ts.map +1 -1
  24. package/dist/agent/persistent-cli-adapter.js +21 -10
  25. package/dist/agent/persistent-cli-adapter.js.map +1 -1
  26. package/dist/agent/tool-registry.d.ts.map +1 -1
  27. package/dist/agent/tool-registry.js +33 -2
  28. package/dist/agent/tool-registry.js.map +1 -1
  29. package/dist/agent/types.d.ts +30 -2
  30. package/dist/agent/types.d.ts.map +1 -1
  31. package/dist/agent/types.js.map +1 -1
  32. package/dist/api/graph-api.d.ts.map +1 -1
  33. package/dist/api/graph-api.js +78 -5
  34. package/dist/api/graph-api.js.map +1 -1
  35. package/dist/api/index.d.ts +8 -1
  36. package/dist/api/index.d.ts.map +1 -1
  37. package/dist/api/index.js +18 -12
  38. package/dist/api/index.js.map +1 -1
  39. package/dist/api/operator-handler.d.ts +19 -0
  40. package/dist/api/operator-handler.d.ts.map +1 -0
  41. package/dist/api/operator-handler.js +106 -0
  42. package/dist/api/operator-handler.js.map +1 -0
  43. package/dist/api/operator-summary.d.ts +3 -0
  44. package/dist/api/operator-summary.d.ts.map +1 -0
  45. package/dist/api/operator-summary.js +72 -0
  46. package/dist/api/operator-summary.js.map +1 -0
  47. package/dist/api/operator-tasks-handler.d.ts +7 -0
  48. package/dist/api/operator-tasks-handler.d.ts.map +1 -0
  49. package/dist/api/operator-tasks-handler.js +165 -0
  50. package/dist/api/operator-tasks-handler.js.map +1 -0
  51. package/dist/api/report-handler.d.ts +7 -0
  52. package/dist/api/report-handler.d.ts.map +1 -1
  53. package/dist/api/report-handler.js +30 -0
  54. package/dist/api/report-handler.js.map +1 -1
  55. package/dist/api/report-persistence.d.ts +17 -0
  56. package/dist/api/report-persistence.d.ts.map +1 -0
  57. package/dist/api/report-persistence.js +91 -0
  58. package/dist/api/report-persistence.js.map +1 -0
  59. package/dist/cli/commands/start.d.ts.map +1 -1
  60. package/dist/cli/commands/start.js +402 -250
  61. package/dist/cli/commands/start.js.map +1 -1
  62. package/dist/cli/config/config-manager.d.ts.map +1 -1
  63. package/dist/cli/config/config-manager.js +93 -55
  64. package/dist/cli/config/config-manager.js.map +1 -1
  65. package/dist/cli/config/types.d.ts +17 -2
  66. package/dist/cli/config/types.d.ts.map +1 -1
  67. package/dist/cli/config/types.js +4 -0
  68. package/dist/cli/config/types.js.map +1 -1
  69. package/dist/cli/runtime/agent-loop-init.d.ts +3 -1
  70. package/dist/cli/runtime/agent-loop-init.d.ts.map +1 -1
  71. package/dist/cli/runtime/agent-loop-init.js +16 -7
  72. package/dist/cli/runtime/agent-loop-init.js.map +1 -1
  73. package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
  74. package/dist/cli/runtime/api-routes-init.js +503 -259
  75. package/dist/cli/runtime/api-routes-init.js.map +1 -1
  76. package/dist/cli/runtime/api-server-init.d.ts.map +1 -1
  77. package/dist/cli/runtime/api-server-init.js +12 -7
  78. package/dist/cli/runtime/api-server-init.js.map +1 -1
  79. package/dist/cli/runtime/connector-init.d.ts +19 -1
  80. package/dist/cli/runtime/connector-init.d.ts.map +1 -1
  81. package/dist/cli/runtime/connector-init.js +32 -2
  82. package/dist/cli/runtime/connector-init.js.map +1 -1
  83. package/dist/cli/runtime/gateway-init.d.ts.map +1 -1
  84. package/dist/cli/runtime/gateway-init.js +3 -1
  85. package/dist/cli/runtime/gateway-init.js.map +1 -1
  86. package/dist/cli/runtime/memory-agent-init.d.ts +2 -1
  87. package/dist/cli/runtime/memory-agent-init.d.ts.map +1 -1
  88. package/dist/cli/runtime/memory-agent-init.js +15 -3
  89. package/dist/cli/runtime/memory-agent-init.js.map +1 -1
  90. package/dist/cli/runtime/message-router-config.d.ts +4 -0
  91. package/dist/cli/runtime/message-router-config.d.ts.map +1 -0
  92. package/dist/cli/runtime/message-router-config.js +11 -0
  93. package/dist/cli/runtime/message-router-config.js.map +1 -0
  94. package/dist/cli/runtime/scheduler-init.d.ts.map +1 -1
  95. package/dist/cli/runtime/scheduler-init.js +12 -12
  96. package/dist/cli/runtime/scheduler-init.js.map +1 -1
  97. package/dist/connectors/framework/polling-scheduler.d.ts.map +1 -1
  98. package/dist/connectors/framework/polling-scheduler.js +53 -2
  99. package/dist/connectors/framework/polling-scheduler.js.map +1 -1
  100. package/dist/connectors/framework/types.d.ts +8 -0
  101. package/dist/connectors/framework/types.d.ts.map +1 -1
  102. package/dist/connectors/kagemusha/query-tools.d.ts.map +1 -1
  103. package/dist/connectors/kagemusha/query-tools.js +14 -3
  104. package/dist/connectors/kagemusha/query-tools.js.map +1 -1
  105. package/dist/connectors/slack/index.d.ts.map +1 -1
  106. package/dist/connectors/slack/index.js +3 -1
  107. package/dist/connectors/slack/index.js.map +1 -1
  108. package/dist/connectors/trello/index.d.ts.map +1 -1
  109. package/dist/connectors/trello/index.js +6 -2
  110. package/dist/connectors/trello/index.js.map +1 -1
  111. package/dist/db/agent-store.d.ts +5 -0
  112. package/dist/db/agent-store.d.ts.map +1 -1
  113. package/dist/db/agent-store.js +6 -3
  114. package/dist/db/agent-store.js.map +1 -1
  115. package/dist/db/migrations/agent-activity-tool-name.d.ts +8 -0
  116. package/dist/db/migrations/agent-activity-tool-name.d.ts.map +1 -0
  117. package/dist/db/migrations/agent-activity-tool-name.js +24 -0
  118. package/dist/db/migrations/agent-activity-tool-name.js.map +1 -0
  119. package/dist/db/migrations/wiki-artifacts.d.ts +3 -0
  120. package/dist/db/migrations/wiki-artifacts.d.ts.map +1 -0
  121. package/dist/db/migrations/wiki-artifacts.js +24 -0
  122. package/dist/db/migrations/wiki-artifacts.js.map +1 -0
  123. package/dist/envelope/run-guard.d.ts +10 -0
  124. package/dist/envelope/run-guard.d.ts.map +1 -0
  125. package/dist/envelope/run-guard.js +19 -0
  126. package/dist/envelope/run-guard.js.map +1 -0
  127. package/dist/gateways/message-router.d.ts.map +1 -1
  128. package/dist/gateways/message-router.js +15 -3
  129. package/dist/gateways/message-router.js.map +1 -1
  130. package/dist/gateways/types.d.ts +4 -0
  131. package/dist/gateways/types.d.ts.map +1 -1
  132. package/dist/multi-agent/agent-event-bus.d.ts +14 -0
  133. package/dist/multi-agent/agent-event-bus.d.ts.map +1 -1
  134. package/dist/multi-agent/agent-event-bus.js +34 -0
  135. package/dist/multi-agent/agent-event-bus.js.map +1 -1
  136. package/dist/multi-agent/conductor-persona.js +2 -2
  137. package/dist/multi-agent/dashboard-agent-persona.d.ts +6 -2
  138. package/dist/multi-agent/dashboard-agent-persona.d.ts.map +1 -1
  139. package/dist/multi-agent/dashboard-agent-persona.js +65 -32
  140. package/dist/multi-agent/dashboard-agent-persona.js.map +1 -1
  141. package/dist/multi-agent/memory-agent-persona.d.ts +1 -1
  142. package/dist/multi-agent/memory-agent-persona.d.ts.map +1 -1
  143. package/dist/multi-agent/memory-agent-persona.js +25 -4
  144. package/dist/multi-agent/memory-agent-persona.js.map +1 -1
  145. package/dist/multi-agent/runtime-process.d.ts +4 -2
  146. package/dist/multi-agent/runtime-process.d.ts.map +1 -1
  147. package/dist/multi-agent/runtime-process.js +7 -4
  148. package/dist/multi-agent/runtime-process.js.map +1 -1
  149. package/dist/multi-agent/types.d.ts +1 -1
  150. package/dist/multi-agent/wiki-agent-persona.d.ts +7 -1
  151. package/dist/multi-agent/wiki-agent-persona.d.ts.map +1 -1
  152. package/dist/multi-agent/wiki-agent-persona.js +82 -68
  153. package/dist/multi-agent/wiki-agent-persona.js.map +1 -1
  154. package/dist/operator/action-verifier.d.ts +50 -0
  155. package/dist/operator/action-verifier.d.ts.map +1 -0
  156. package/dist/operator/action-verifier.js +68 -0
  157. package/dist/operator/action-verifier.js.map +1 -0
  158. package/dist/operator/board-reconcile.d.ts +62 -0
  159. package/dist/operator/board-reconcile.d.ts.map +1 -0
  160. package/dist/operator/board-reconcile.js +160 -0
  161. package/dist/operator/board-reconcile.js.map +1 -0
  162. package/dist/operator/board-slot-instructions.d.ts +26 -0
  163. package/dist/operator/board-slot-instructions.d.ts.map +1 -0
  164. package/dist/operator/board-slot-instructions.js +76 -0
  165. package/dist/operator/board-slot-instructions.js.map +1 -0
  166. package/dist/operator/connector-delta-repo.d.ts +33 -0
  167. package/dist/operator/connector-delta-repo.d.ts.map +1 -0
  168. package/dist/operator/connector-delta-repo.js +0 -0
  169. package/dist/operator/connector-delta-repo.js.map +1 -0
  170. package/dist/operator/mama-memory-port.d.ts +13 -0
  171. package/dist/operator/mama-memory-port.d.ts.map +1 -0
  172. package/dist/operator/mama-memory-port.js +35 -0
  173. package/dist/operator/mama-memory-port.js.map +1 -0
  174. package/dist/operator/operator-interfaces.d.ts +136 -0
  175. package/dist/operator/operator-interfaces.d.ts.map +1 -0
  176. package/dist/operator/operator-interfaces.js +15 -0
  177. package/dist/operator/operator-interfaces.js.map +1 -0
  178. package/dist/operator/operator-trigger-loop.d.ts +119 -0
  179. package/dist/operator/operator-trigger-loop.d.ts.map +1 -0
  180. package/dist/operator/operator-trigger-loop.js +267 -0
  181. package/dist/operator/operator-trigger-loop.js.map +1 -0
  182. package/dist/operator/report-run.d.ts +78 -0
  183. package/dist/operator/report-run.d.ts.map +1 -0
  184. package/dist/operator/report-run.js +141 -0
  185. package/dist/operator/report-run.js.map +1 -0
  186. package/dist/operator/report-scheduler.d.ts +66 -0
  187. package/dist/operator/report-scheduler.d.ts.map +1 -0
  188. package/dist/operator/report-scheduler.js +135 -0
  189. package/dist/operator/report-scheduler.js.map +1 -0
  190. package/dist/operator/situation-report.d.ts +90 -0
  191. package/dist/operator/situation-report.d.ts.map +1 -0
  192. package/dist/operator/situation-report.js +221 -0
  193. package/dist/operator/situation-report.js.map +1 -0
  194. package/dist/operator/task-ledger.d.ts +96 -0
  195. package/dist/operator/task-ledger.d.ts.map +1 -0
  196. package/dist/operator/task-ledger.js +277 -0
  197. package/dist/operator/task-ledger.js.map +1 -0
  198. package/dist/operator/trigger-author.d.ts +45 -0
  199. package/dist/operator/trigger-author.d.ts.map +1 -0
  200. package/dist/operator/trigger-author.js +217 -0
  201. package/dist/operator/trigger-author.js.map +1 -0
  202. package/dist/operator/trigger-evolve.d.ts +31 -0
  203. package/dist/operator/trigger-evolve.d.ts.map +1 -0
  204. package/dist/operator/trigger-evolve.js +51 -0
  205. package/dist/operator/trigger-evolve.js.map +1 -0
  206. package/dist/operator/trigger-fire.d.ts +29 -0
  207. package/dist/operator/trigger-fire.d.ts.map +1 -0
  208. package/dist/operator/trigger-fire.js +41 -0
  209. package/dist/operator/trigger-fire.js.map +1 -0
  210. package/dist/operator/trigger-matcher.d.ts +19 -0
  211. package/dist/operator/trigger-matcher.d.ts.map +1 -0
  212. package/dist/operator/trigger-matcher.js +61 -0
  213. package/dist/operator/trigger-matcher.js.map +1 -0
  214. package/dist/operator/trigger-registry.d.ts +36 -0
  215. package/dist/operator/trigger-registry.d.ts.map +1 -0
  216. package/dist/operator/trigger-registry.js +135 -0
  217. package/dist/operator/trigger-registry.js.map +1 -0
  218. package/dist/operator/trigger-review.d.ts +23 -0
  219. package/dist/operator/trigger-review.d.ts.map +1 -0
  220. package/dist/operator/trigger-review.js +127 -0
  221. package/dist/operator/trigger-review.js.map +1 -0
  222. package/dist/operator/trigger-types.d.ts +84 -0
  223. package/dist/operator/trigger-types.d.ts.map +1 -0
  224. package/dist/operator/trigger-types.js +14 -0
  225. package/dist/operator/trigger-types.js.map +1 -0
  226. package/dist/scheduler/cron-worker.d.ts.map +1 -1
  227. package/dist/scheduler/cron-worker.js +3 -1
  228. package/dist/scheduler/cron-worker.js.map +1 -1
  229. package/dist/security/security-monitor.d.ts.map +1 -1
  230. package/dist/security/security-monitor.js +1 -0
  231. package/dist/security/security-monitor.js.map +1 -1
  232. package/dist/wiki/obsidian-writer.d.ts +1 -1
  233. package/dist/wiki/obsidian-writer.d.ts.map +1 -1
  234. package/dist/wiki/obsidian-writer.js +67 -27
  235. package/dist/wiki/obsidian-writer.js.map +1 -1
  236. package/dist/wiki/path-safety.d.ts +2 -0
  237. package/dist/wiki/path-safety.d.ts.map +1 -0
  238. package/dist/wiki/path-safety.js +40 -0
  239. package/dist/wiki/path-safety.js.map +1 -0
  240. package/dist/wiki/types.d.ts +4 -2
  241. package/dist/wiki/types.d.ts.map +1 -1
  242. package/dist/wiki/types.js +1 -1
  243. package/dist/wiki/types.js.map +1 -1
  244. package/dist/wiki-artifacts/normalization.d.ts +8 -0
  245. package/dist/wiki-artifacts/normalization.d.ts.map +1 -0
  246. package/dist/wiki-artifacts/normalization.js +41 -0
  247. package/dist/wiki-artifacts/normalization.js.map +1 -0
  248. package/dist/wiki-artifacts/types.d.ts +52 -0
  249. package/dist/wiki-artifacts/types.d.ts.map +1 -0
  250. package/dist/wiki-artifacts/types.js +3 -0
  251. package/dist/wiki-artifacts/types.js.map +1 -0
  252. package/dist/wiki-artifacts/wiki-publish-adapter.d.ts +14 -0
  253. package/dist/wiki-artifacts/wiki-publish-adapter.d.ts.map +1 -0
  254. package/dist/wiki-artifacts/wiki-publish-adapter.js +71 -0
  255. package/dist/wiki-artifacts/wiki-publish-adapter.js.map +1 -0
  256. package/package.json +5 -3
  257. package/public/ui/assets/index-CKOJQYXA.css +1 -0
  258. package/public/ui/assets/index-OSgiwL24.js +62 -0
  259. package/public/ui/index.html +14 -0
  260. package/public/ui/theme-init.js +20 -0
  261. package/public/viewer/js/utils/api.js +2 -2
  262. package/public/viewer/src/utils/api.ts +9 -2
  263. package/public/viewer/sw.js +1 -1
  264. package/public/viewer/viewer.html +17 -27
@@ -53,6 +53,7 @@ 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");
58
59
  const debugLogger = __importStar(require("@jungjaehoon/mama-core/debug-logger"));
@@ -200,46 +201,46 @@ async function registerApiRoutes(params) {
200
201
  }
201
202
  // Wire EventBus to tool executor for agent_notices tool
202
203
  toolExecutor.setAgentEventBus(eventBus);
203
- // ── Report Slots ──────────────────────────────────────────────────────
204
- {
205
- const { broadcastReportUpdate } = await import('../../api/report-handler.js');
206
- // Manual refresh endpoint (kept for compatibility)
207
- apiServer.app.post('/api/report/refresh', auth_middleware_js_1.requireAuth, (_req, res) => {
208
- res.json({ ok: true, message: 'Viewer now renders data directly from Intelligence API' });
209
- });
210
- // Wire report_publish tool to OS agent only briefing slot
211
- toolExecutor.setReportPublisher((slots) => {
212
- for (const [slotId, html] of Object.entries(slots)) {
213
- if (slotId !== 'briefing')
214
- continue; // only accept briefing slot
215
- apiServer.reportStore.update(slotId, html, 0);
216
- }
217
- broadcastReportUpdate(apiServer.reportSseClients, {
218
- slots: apiServer.reportStore.getAllSorted(),
219
- });
220
- routesLogger.debug(`[Report] Agent published briefing slot`);
221
- eventBus.emit({
222
- type: 'agent:action',
223
- agent: 'dashboard-agent',
224
- action: 'publish',
225
- target: 'briefing',
226
- });
227
- });
228
- // ── Conductor Persona (section injection — non-destructive) ────────
229
- const { ensureConductorPersona } = await import('../../multi-agent/conductor-persona.js');
230
- ensureConductorPersona();
231
- // ── Dashboard Agent ─────────────────────────────────────────────────
232
- const { ensureDashboardPersona } = await import('../../multi-agent/dashboard-agent-persona.js');
233
- ensureDashboardPersona();
234
- routesLogger.debug('[Dashboard Agent] Persona ensured at ~/.mama/personas/dashboard.md');
235
- // Merge code-act MCP server into mama-mcp-config.json
236
- // Makes code_act available to all CLI processes (AgentProcessManager agents)
204
+ (0, operator_tasks_handler_js_1.registerOperatorTaskRoutes)(apiServer.app, {
205
+ getTaskLedger: () => toolExecutor.getTaskLedger(),
206
+ });
207
+ const hasEnabledAgentConfig = (agentId) => {
208
+ const agentConfig = config.multi_agent?.agents?.[agentId];
209
+ return Boolean(agentConfig && agentConfig.enabled !== false);
210
+ };
211
+ // ── Report Slots + legacy dashboard/wiki fanout ───────────────────────
212
+ const dashboardAgentConfigured = hasEnabledAgentConfig('dashboard-agent');
213
+ const wikiAgentConfigured = hasEnabledAgentConfig('wiki-agent');
214
+ // Manual refresh endpoint (kept for compatibility)
215
+ apiServer.app.post('/api/report/refresh', auth_middleware_js_1.requireAuth, (_req, res) => {
216
+ res.json({ ok: true, message: 'Viewer now renders data directly from Intelligence API' });
217
+ });
218
+ // ── Conductor Persona (section injection — non-destructive) ────────
219
+ const { ensureConductorPersona } = await import('../../multi-agent/conductor-persona.js');
220
+ ensureConductorPersona();
221
+ if (dashboardAgentConfigured || wikiAgentConfigured) {
222
+ // Merge code-act MCP server into mama-mcp-config.json.
223
+ // Makes code_act available to configured legacy self-paced agents.
237
224
  const codeActServerPath = node_path_1.default.join(__dirname, '../../mcp/code-act-server.js');
238
225
  try {
239
226
  const mamaMcpConfigPath = node_path_1.default.join((0, node_os_1.homedir)(), '.mama', 'mama-mcp-config.json');
240
- const existing = (0, node_fs_1.existsSync)(mamaMcpConfigPath)
241
- ? JSON.parse((0, node_fs_1.readFileSync)(mamaMcpConfigPath, 'utf-8'))
242
- : { mcpServers: {} };
227
+ let existing = { mcpServers: {} };
228
+ if ((0, node_fs_1.existsSync)(mamaMcpConfigPath)) {
229
+ try {
230
+ const parsedConfig = JSON.parse((0, node_fs_1.readFileSync)(mamaMcpConfigPath, 'utf-8'));
231
+ if (parsedConfig && typeof parsedConfig === 'object' && !Array.isArray(parsedConfig)) {
232
+ existing = parsedConfig;
233
+ }
234
+ }
235
+ catch (parseErr) {
236
+ routesLogger.warn('[api-routes-init] Invalid MCP config JSON; recreating code-act entry:', parseErr);
237
+ }
238
+ }
239
+ if (!existing.mcpServers ||
240
+ typeof existing.mcpServers !== 'object' ||
241
+ Array.isArray(existing.mcpServers)) {
242
+ existing.mcpServers = {};
243
+ }
243
244
  existing.mcpServers['code-act'] = {
244
245
  command: 'node',
245
246
  args: [codeActServerPath],
@@ -251,8 +252,23 @@ async function registerApiRoutes(params) {
251
252
  catch (err) {
252
253
  routesLogger.warn('[api-routes-init] Failed to merge code-act into MCP config:', err);
253
254
  }
255
+ }
256
+ // report_publish is a core surface (feeds the /ui operator board), not a
257
+ // multi-agent feature: wire it unconditionally. All slot ids are accepted;
258
+ // size/count caps and loud logging live in createReportPublisher.
259
+ {
260
+ const { createReportPublisher } = await import('../../api/report-handler.js');
261
+ toolExecutor.setReportPublisher(createReportPublisher(apiServer.reportStore, apiServer.reportSseClients));
262
+ }
263
+ if (dashboardAgentConfigured) {
264
+ // ── Dashboard Agent ───────────────────────────────────────────────
265
+ const { ensureDashboardPersona } = await import('../../multi-agent/dashboard-agent-persona.js');
266
+ ensureDashboardPersona();
267
+ routesLogger.debug('[Dashboard Agent] Persona ensured at ~/.mama/personas/dashboard.md');
254
268
  // Dashboard cron: 30-min interval via AgentProcessManager
255
- const dashboardPrompt = `You are triggered on a schedule. Before writing anything, determine if an update is needed:
269
+ // Built PER RUN: the tracker's D-day arithmetic needs today's date, and a
270
+ // module-const prompt would freeze it (#134 lesson).
271
+ 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:
256
272
 
257
273
  1. Use agent_notices({limit: 50}) to find the most recent dashboard-agent publish/task_complete notice. Treat that as the last briefing boundary.
258
274
  2. Use context_compile first to find recent substantive decisions (limit 20, max_tool_calls 2, strictness "balanced").
@@ -260,279 +276,507 @@ async function registerApiRoutes(params) {
260
276
  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.
261
277
  If context_compile is unavailable because there is no active worker envelope, fall back to mama_search once (limit 20).
262
278
  3. If NO substantive decisions or agent alerts exist since the last dashboard publish → respond "NO_UPDATE" and stop. Do NOT call report_publish.
263
- 4. If new substantive information exists analyze it, write a new briefing, and publish via report_publish in the "briefing" slot.
279
+ 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.
264
280
  5. Do NOT call mama_save for the briefing; report_publish and agent_activity are the durable operational record.
265
281
 
266
282
  This saves resources. Only publish when there is genuinely new information to report.`;
267
- const runDashboardAgent = async () => {
283
+ // Owner-initiated refresh skips the NO_UPDATE delta gate: an explicit
284
+ // request means "rebuild the board now", not "tell me nothing changed".
285
+ 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');
286
+ const doDashboardRun = async (opts) => {
268
287
  const pm = toolExecutor.getAgentProcessManager();
269
288
  if (!pm) {
270
289
  routesLogger.warn('[Dashboard Agent] AgentProcessManager not available yet');
271
290
  return;
272
291
  }
273
292
  try {
274
- routesLogger.debug('[Dashboard Agent] Checking for updates...');
275
- const { noUpdate } = await executeValidatedRun('dashboard-agent', dashboardPrompt);
276
- if (noUpdate) {
277
- routesLogger.debug('[Dashboard Agent] No changes detected, skipped');
278
- }
279
- else {
280
- routesLogger.debug('[Dashboard Agent] Briefing published');
281
- }
293
+ // console.log on run outcomes: the DebugLogger only surfaces warn/error in the
294
+ // daemon log, and a silent outcome reads as a hang from the outside.
295
+ console.log(`[Dashboard Agent] run started${opts?.force ? ' (owner-forced, delta gate bypassed)' : ''}`);
296
+ const { noUpdate } = await executeValidatedRun('dashboard-agent', opts?.force ? buildForcedDashboardPrompt() : buildDashboardPrompt());
297
+ console.log(noUpdate
298
+ ? '[Dashboard Agent] no changes detected, publish skipped'
299
+ : '[Dashboard Agent] board published');
282
300
  }
283
301
  catch (err) {
284
302
  routesLogger.error('[Dashboard Agent] Error:', err instanceof Error ? err.message : err);
285
303
  }
286
304
  };
305
+ // ONE board-writer queue: dashboard cron, manual refresh, AND reconcile runs
306
+ // all serialize here -- the shared agent process rejects concurrent requests
307
+ // ('Process is busy' class). Per-job rejection propagates to the caller
308
+ // while the chain itself survives (M8 review: a caller must be able to
309
+ // distinguish its own job's failure).
310
+ let boardWriterChain = Promise.resolve();
311
+ const boardWriterQueue = {
312
+ push(job) {
313
+ const jobPromise = boardWriterChain.then(job);
314
+ boardWriterChain = jobPromise.catch(() => { });
315
+ return jobPromise;
316
+ },
317
+ };
318
+ const runDashboardAgent = (opts) => boardWriterQueue.push(() => doDashboardRun(opts)).catch(() => { });
287
319
  // First run after 10s (let connectors poll first), then every 30 min
288
320
  setTimeout(runDashboardAgent, 10_000);
289
321
  setInterval(runDashboardAgent, 30 * 60 * 1000);
290
- // Manual trigger
322
+ // Manual trigger (owner-forced: bypasses the delta gate)
291
323
  apiServer.app.post('/api/report/agent-refresh', auth_middleware_js_1.requireAuth, async (_req, res) => {
292
- runDashboardAgent().catch(() => { });
293
- res.json({ ok: true, message: 'Dashboard agent triggered' });
324
+ runDashboardAgent({ force: true }).catch(() => { });
325
+ res.json({ ok: true, message: 'Dashboard agent triggered (forced refresh)' });
294
326
  });
295
- // ── Wiki Agent ──────────────────────────────────────────────────────
296
- const wikiConfig = config.wiki;
297
- if (wikiConfig?.enabled && wikiConfig.vaultPath) {
298
- const { ensureWikiPersona } = await import('../../multi-agent/wiki-agent-persona.js');
299
- const { ObsidianWriter } = await import('../../wiki/obsidian-writer.js');
300
- ensureWikiPersona();
301
- const obsWriter = new ObsidianWriter(wikiConfig.vaultPath, wikiConfig.wikiDir || 'wiki');
302
- obsWriter.ensureDirectories();
303
- routesLogger.debug(`[Wiki Agent] Persona ensured, vault: ${obsWriter.getWikiPath()}`);
304
- // Wire Obsidian vault path for CLI tool
305
- const fullWikiPath = obsWriter.getWikiPath();
306
- toolExecutor.setObsidianVaultPath(fullWikiPath);
307
- routesLogger.debug(`[Wiki Agent] Obsidian CLI vault: ${fullWikiPath}`);
308
- // Ensure Obsidian is running for CLI access (macOS only)
327
+ // -- M8 board reconcile leg (freshness layer; default OFF, opt-in like
328
+ // MAMA_TRIGGER_LOOP). The trigger loop emits operator:channel-delta after
329
+ // committing its cursor; the 30-min cron above remains the repair pass.
330
+ if (process.env.MAMA_BOARD_RECONCILE === '1') {
331
+ const { buildReconcilePrompt, ReconcileScheduler } = await import('../../operator/board-reconcile.js');
332
+ const { captureSnapshot, verifyAfterRun, OBLIGATED_TOOLS } = await import('../../operator/action-verifier.js');
333
+ const kagemushaContext = (process.env.MAMA_RECONCILE_TASK_CONTEXT ?? 'native') === 'kagemusha';
334
+ // Verifier deps (M8 Phase 2): run-bound signals only -- trace rows from
335
+ // the dashboard agent's gateway_tool_call activity, notes from the
336
+ // operator ledger. Observe, never block.
337
+ const reconcileLedger = toolExecutor.getTaskLedger();
338
+ // gateway_tool_call rows now populate normalized_tool_name at the logging
339
+ // site; the input_summary fallback covers rows written before that fix.
340
+ const traceToolList = OBLIGATED_TOOLS.map((t) => `'${t}'`).join(',');
341
+ const verifierDeps = {
342
+ getSlots: () => apiServer.reportStore.getAllSorted(),
343
+ getLedgerHash: () => reconcileLedger?.payloadHash() ?? '',
344
+ getScopedNoteMaxId: (scope) => reconcileLedger?.maxNoUpdateId(scope) ?? 0,
345
+ getTraceMaxId: () => {
346
+ if (!sessionsDb)
347
+ return 0;
348
+ const row = sessionsDb
349
+ .prepare(`SELECT MAX(id) AS max_id FROM agent_activity
350
+ WHERE type = 'gateway_tool_call' AND agent_id = 'dashboard-agent'`)
351
+ .get();
352
+ return row.max_id ?? 0;
353
+ },
354
+ countObligatedTraceRowsSince: (maxId) => {
355
+ if (!sessionsDb)
356
+ return 0;
357
+ const row = sessionsDb
358
+ .prepare(`SELECT COUNT(*) AS n FROM agent_activity
359
+ WHERE type = 'gateway_tool_call' AND agent_id = 'dashboard-agent'
360
+ AND id > ? AND (normalized_tool_name IN (${traceToolList}) OR input_summary IN (${traceToolList}))`)
361
+ .get(maxId);
362
+ return row.n;
363
+ },
364
+ };
365
+ const reconcileScheduler = new ReconcileScheduler({
366
+ debounceMs: Number(process.env.MAMA_RECONCILE_DEBOUNCE_MS) || undefined,
367
+ maxWaitMs: Number(process.env.MAMA_RECONCILE_MAX_WAIT_MS) || undefined,
368
+ globalMaxPerHour: Number(process.env.MAMA_RECONCILE_MAX_PER_HOUR) || undefined,
369
+ log: (line) => console.log(line),
370
+ run: (channelKey, deltaLines) => boardWriterQueue.push(async () => {
371
+ const scope = `reconcile:${channelKey}`;
372
+ const before = captureSnapshot(verifierDeps, scope);
373
+ const prompt = buildReconcilePrompt({
374
+ channelKey,
375
+ deltaLines,
376
+ todayIso: new Date().toISOString().slice(0, 10),
377
+ kagemushaContext,
378
+ });
379
+ await executeValidatedRun('dashboard-agent', prompt, {
380
+ requestTimeout: 300_000,
381
+ });
382
+ const verdict = verifyAfterRun(verifierDeps, before, scope);
383
+ const outcome = verdict.verified ? 'reconcile_verified' : 'reconcile_unverified';
384
+ console.log(`[reconcile] ${outcome} channel=${channelKey}${verdict.effects.length > 0 ? ` (${verdict.effects.join('; ')})` : ''}`);
385
+ try {
386
+ if (sessionsDb) {
387
+ (0, agent_store_js_1.logActivity)(sessionsDb, {
388
+ agent_id: 'dashboard-agent',
389
+ agent_version: 0,
390
+ type: outcome,
391
+ input_summary: `reconcile ${channelKey}`,
392
+ output_summary: verdict.effects.join('; '),
393
+ execution_status: 'completed',
394
+ trigger_reason: 'reconcile',
395
+ });
396
+ }
397
+ }
398
+ catch {
399
+ /* telemetry only */
400
+ }
401
+ if (!verdict.verified) {
402
+ // Loud, never blocking (observability over restriction).
403
+ eventBus.emit({
404
+ type: 'agent:action',
405
+ agent: 'Dashboard Agent',
406
+ action: 'reconcile_unverified',
407
+ target: channelKey,
408
+ });
409
+ }
410
+ }),
411
+ });
412
+ eventBus.on('operator:channel-delta', (event) => {
413
+ if (event.type === 'operator:channel-delta') {
414
+ reconcileScheduler.enqueue(event.channelKey, event.lines);
415
+ }
416
+ });
417
+ // Manual reconcile: lines from the body, or the caller must supply them
418
+ // (no silent alternate data path -- M8 review #17).
419
+ apiServer.app.post('/api/operator/reconcile', auth_middleware_js_1.requireAuth, (req, res) => {
420
+ const { channelKey, lines } = (req.body ?? {});
421
+ if (!channelKey || !Array.isArray(lines) || lines.length === 0) {
422
+ res.status(400).json({
423
+ ok: false,
424
+ error: 'channelKey and non-empty lines[] are required',
425
+ });
426
+ return;
427
+ }
428
+ reconcileScheduler.enqueue(channelKey, lines);
429
+ res.json({ ok: true, message: 'Reconcile queued (async; runs after debounce)' });
430
+ });
431
+ console.log('[reconcile] Board reconcile leg enabled (MAMA_BOARD_RECONCILE=1)');
432
+ }
433
+ }
434
+ else {
435
+ routesLogger.debug('[Dashboard Agent] Skipped; dashboard-agent is not configured');
436
+ }
437
+ // ── Wiki Agent ──────────────────────────────────────────────────────
438
+ const wikiConfig = config.wiki;
439
+ if (wikiAgentConfigured && wikiConfig?.enabled && wikiConfig.vaultPath) {
440
+ const { ensureWikiPersona } = await import('../../multi-agent/wiki-agent-persona.js');
441
+ const { ObsidianWriter } = await import('../../wiki/obsidian-writer.js');
442
+ ensureWikiPersona();
443
+ const obsWriter = new ObsidianWriter(wikiConfig.vaultPath, wikiConfig.wikiDir || 'wiki');
444
+ obsWriter.ensureDirectories();
445
+ routesLogger.debug(`[Wiki Agent] Persona ensured, vault: ${obsWriter.getWikiPath()}`);
446
+ // Wire Obsidian vault path for CLI tool. The wiki directory itself is what
447
+ // gets registered as an Obsidian vault (agent-facing paths like daily/... are
448
+ // relative to it), so the CLI vault name is the wiki path's basename.
449
+ const fullWikiPath = obsWriter.getWikiPath();
450
+ const obsidianVaultName = node_path_1.default.basename(fullWikiPath);
451
+ toolExecutor.setObsidianVaultPath(fullWikiPath, obsidianVaultName);
452
+ routesLogger.debug(`[Wiki Agent] Obsidian CLI vault: ${fullWikiPath} (vault=${obsidianVaultName})`);
453
+ // Ensure Obsidian is running with the wiki vault open (macOS only).
454
+ // NOTE: obsidian://open?vault= only opens vaults ALREADY registered in
455
+ // Obsidian; registering the wiki directory as a vault is a one-time manual
456
+ // setup step. If it is not registered, the CLI reports unavailable and the
457
+ // agent falls back to wiki_publish (direct file writes still work).
458
+ if (process.platform === 'darwin') {
309
459
  try {
310
- const { execSync: execSyncChild } = await import('child_process');
311
- execSyncChild('pgrep -x Obsidian || open -a Obsidian', {
312
- timeout: 5000,
313
- stdio: 'ignore',
460
+ const { execFile: execFileChild } = await import('child_process');
461
+ execFileChild('open', [`obsidian://open?vault=${encodeURIComponent(obsidianVaultName)}`], { timeout: 5000 }, () => {
462
+ /* non-fatal: CLI will return error, agent falls back to wiki_publish */
314
463
  });
315
464
  }
316
465
  catch {
317
- /* non-fatal: CLI will return error, agent falls back to wiki_publish */
466
+ /* non-fatal */
318
467
  }
319
- // Wire wiki_publish tool to shared gateway executor (used by code-act path)
320
- toolExecutor.setWikiPublisher((pages) => {
321
- for (const page of pages) {
322
- obsWriter.writePage(page);
323
- }
324
- if (pages.length > 0) {
325
- obsWriter.updateIndex(pages);
326
- obsWriter.appendLog('compile', `Published ${pages.length} pages`);
327
- }
328
- routesLogger.debug(`[Wiki Agent] Published ${pages.length} pages to vault`);
329
- eventBus.emit({
330
- type: 'wiki:compiled',
331
- pages: pages.map((p) => p.path || ''),
332
- });
468
+ }
469
+ // Wire wiki_publish tool to shared gateway executor (used by code-act path)
470
+ toolExecutor.setWikiPublisher((pages) => {
471
+ for (const page of pages) {
472
+ obsWriter.writePage(page);
473
+ }
474
+ if (pages.length > 0) {
475
+ obsWriter.updateIndex(pages);
476
+ obsWriter.appendLog('compile', `Published ${pages.length} pages`);
477
+ }
478
+ routesLogger.debug(`[Wiki Agent] Published ${pages.length} pages to vault`);
479
+ eventBus.emit({
480
+ type: 'wiki:compiled',
481
+ pages: pages.map((p) => p.path || ''),
333
482
  });
334
- // Wiki trigger via executeValidatedRun
335
- const runWikiAgent = async () => {
336
- if (!toolExecutor.getAgentProcessManager()) {
337
- routesLogger.warn('[Wiki Agent] AgentProcessManager not available yet');
338
- return;
339
- }
340
- try {
341
- routesLogger.debug('[Wiki Agent] Checking for updates...');
342
- const wikiPrompt = `You are triggered on a schedule. Before writing anything, determine if an update is needed:
483
+ });
484
+ // Wiki trigger via executeValidatedRun
485
+ const doWikiRun = async () => {
486
+ if (!toolExecutor.getAgentProcessManager()) {
487
+ routesLogger.warn('[Wiki Agent] AgentProcessManager not available yet');
488
+ return;
489
+ }
490
+ try {
491
+ routesLogger.debug('[Wiki Agent] Checking for updates...');
492
+ const wikiPrompt = `You are triggered on a schedule. Before writing anything, determine if an update is needed:
343
493
 
344
494
  1. Use agent_notices({limit: 100}) to find the most recent wiki-agent compiled/publish/task_complete notice. Treat that as the last compilation boundary.
345
- 2. Use context_compile first to find recent substantive decisions (limit 30, max_tool_calls 3, strictness "balanced").
495
+ 2. NOVELTY CHECK by recency, not semantics: call mama_search({limit: 30}) with NO query -- that returns the newest decisions in creation order regardless of language or wording (semantic/lexical search misses cross-language items, which caused missed compilations). Compare created_at against the boundary.
496
+ 3. If NO substantive decisions are newer than the boundary -> respond "NO_UPDATE" and stop. Do NOT call obsidian or wiki_publish.
497
+ 4. If new items exist, use context_compile first to gather supporting context (limit 30, max_tool_calls 3, strictness "balanced").
346
498
  Use this exact task text for context_compile: "recent substantive project decisions, task progress, agent alerts, and major changes".
347
499
  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.
348
- If context_compile is unavailable because there is no active worker envelope, fall back to mama_search once (limit 30).
349
- 3. If NO substantive decisions exist since the last wiki compilation respond "NO_UPDATE" and stop. Do NOT call obsidian or wiki_publish.
350
- 4. If new substantive decisions exist use obsidian("search") to check existing pages, then update or create pages as needed. Clean up duplicates.
351
- 5. Do NOT call mama_save for the compilation; Obsidian/wiki files plus agent_activity are the durable operational record.
500
+ If the packet misses some of the new items from step 2 (it often will for cross-language content), write from the step-2 items directly -- the recency list is authoritative for WHAT is new; the packet only enriches.
501
+ If context_compile is unavailable because there is no active worker envelope, fall back to ONE queried mama_search for enrichment and continue from the step-2 recency list.
502
+ 5. Then follow your persona: APPEND today's daily note (daily/YYYY-MM-DD.md, create with the section skeleton on first write of the day), then promote qualifying lesson candidates into lessons/ pages (search before create; update, never duplicate). Do NOT write per-task status pages.
503
+ 6. Do NOT call mama_save for the compilation; Obsidian/wiki files plus agent_activity are the durable operational record.
352
504
 
353
505
  This saves resources. Only compile when there is genuinely new information to document.`;
354
- const { noUpdate } = await executeValidatedRun('wiki-agent', wikiPrompt, {
355
- requestTimeout: 600_000,
506
+ const { noUpdate } = await executeValidatedRun('wiki-agent', wikiPrompt, {
507
+ requestTimeout: 600_000,
508
+ });
509
+ if (noUpdate) {
510
+ routesLogger.debug('[Wiki Agent] No changes detected, skipped');
511
+ }
512
+ else {
513
+ routesLogger.debug('[Wiki Agent] Compilation complete');
514
+ }
515
+ }
516
+ catch (err) {
517
+ routesLogger.error('[Wiki Agent] Error:', err instanceof Error ? err.message : err);
518
+ }
519
+ };
520
+ // Serialize ALL wiki runs (boot, event-driven, manual) on one chain -- the
521
+ // shared agent process rejects concurrent requests, so the boot run and a
522
+ // manual trigger raced into 'Process is busy' (same pattern as the
523
+ // dashboard agent's runDashboardAgent chain above).
524
+ let wikiRunChain = Promise.resolve();
525
+ const runWikiAgent = () => {
526
+ wikiRunChain = wikiRunChain.then(doWikiRun).catch(() => { });
527
+ return wikiRunChain;
528
+ };
529
+ // Event-driven: compile when extraction completes. Trailing-edge debounce so
530
+ // bursts of extraction:completed events coalesce into one wiki compile run.
531
+ eventBus.onDebounced('extraction:completed', () => runWikiAgent(), 30_000);
532
+ // Promotion feeds the wiki: freshly promoted decisions are exactly the
533
+ // material daily notes and lessons compile from.
534
+ eventBus.onDebounced('memory:promoted', () => runWikiAgent(), 30_000);
535
+ // Emit agent:action notices when wiki pages are compiled
536
+ eventBus.on('wiki:compiled', (event) => {
537
+ if (event.type === 'wiki:compiled') {
538
+ for (const page of event.pages) {
539
+ eventBus.emit({
540
+ type: 'agent:action',
541
+ agent: 'Wiki Agent',
542
+ action: 'compiled',
543
+ target: page,
356
544
  });
357
- if (noUpdate) {
358
- routesLogger.debug('[Wiki Agent] No changes detected, skipped');
359
- }
360
- else {
361
- routesLogger.debug('[Wiki Agent] Compilation complete');
362
- }
363
545
  }
364
- catch (err) {
365
- routesLogger.error('[Wiki Agent] Error:', err instanceof Error ? err.message : err);
546
+ }
547
+ });
548
+ // Manual trigger API
549
+ apiServer.app.post('/api/wiki/compile', auth_middleware_js_1.requireAuth, async (_req, res) => {
550
+ runWikiAgent().catch(() => { });
551
+ res.json({ ok: true, message: 'Wiki compilation triggered' });
552
+ });
553
+ // First run after 15s (let connectors and dashboard agent go first)
554
+ setTimeout(runWikiAgent, 15_000);
555
+ routesLogger.info('[Wiki Agent] Ready — triggers: extraction:completed event, POST /api/wiki/compile');
556
+ }
557
+ // -- Memory Promotion: scheduled observation->decision curation --
558
+ // Owner directive 2026-07-11: the decision layer starved after the 07-03/04
559
+ // backfill because nothing periodically judges connector-ingested business
560
+ // data (the M0 kill switch removed direct LLM extraction, and the memory
561
+ // agent only audits owner conversation turns). This pass has the memory
562
+ // agent promote DURABLE judgments only -- task states stay on the board.
563
+ const memoryAgentConfigured = hasEnabledAgentConfig('memory');
564
+ if (memoryAgentConfigured) {
565
+ const PROMOTION_INTERVAL_MS = Math.max(1, Number(process.env.MAMA_MEMORY_PROMOTION_HOURS) || 6) * 60 * 60 * 1000;
566
+ const PROMOTION_INITIAL_DELAY_MS = 10 * 60 * 1000; // let connectors poll first
567
+ const buildPromotionPrompt = (nowIso) => 'PROMOTION RUN. You are curating durable business memory from recent data. ' +
568
+ `The current time is ${nowIso}.\n` +
569
+ '1. agent_notices({limit: 100}): find your latest promotion notice (action "promoted" ' +
570
+ 'or "no_update") and treat it as the boundary; default to the last 24h when absent.\n' +
571
+ '2. kagemusha_entities({activeOnly: true}) to find the rooms active since the boundary, ' +
572
+ 'then kagemusha_messages({channelId, since: <boundary ISO>}) on the busiest 3-4 rooms.\n' +
573
+ '3. For each candidate judgment, mama_search first to find the existing topic; reuse it ' +
574
+ 'so the evolution chain stays intact.\n' +
575
+ '4. Promote at most 5 durable judgments per run via mama_save, following the PROMOTION RUN ' +
576
+ 'rules in your persona (pricing/scope agreements, standing client preferences, process ' +
577
+ 'rules, recurring risk patterns; NEVER task lifecycle states, greetings, or logistics). ' +
578
+ 'Include scopes (the source channel, and the project when identifiable) and event_date.\n' +
579
+ '5. Finish with exactly PROMOTED <n> or NO_UPDATE.';
580
+ const doPromotionRun = async () => {
581
+ if (!toolExecutor.getAgentProcessManager()) {
582
+ routesLogger.warn('[Memory Promotion] AgentProcessManager not available yet');
583
+ return;
584
+ }
585
+ try {
586
+ const { response, noUpdate } = await executeValidatedRun('memory', buildPromotionPrompt(new Date().toISOString()), { requestTimeout: 600_000 });
587
+ const promotedMatch = response?.match(/PROMOTED\s+(\d+)/);
588
+ const saved = promotedMatch ? Number(promotedMatch[1]) : 0;
589
+ eventBus.emit({
590
+ type: 'agent:action',
591
+ agent: 'Memory Agent',
592
+ action: noUpdate || saved === 0 ? 'no_update' : 'promoted',
593
+ target: `promotion run: ${saved} saved`,
594
+ });
595
+ if (saved > 0) {
596
+ eventBus.emit({ type: 'memory:promoted', saved });
597
+ console.log(`[Memory Promotion] Promoted ${saved} durable judgments`);
366
598
  }
367
- };
368
- // Event-driven: compile when extraction completes (debounced)
369
- eventBus.on('extraction:completed', () => runWikiAgent());
370
- // Emit agent:action notices when wiki pages are compiled
371
- eventBus.on('wiki:compiled', (event) => {
372
- if (event.type === 'wiki:compiled') {
373
- for (const page of event.pages) {
374
- eventBus.emit({
375
- type: 'agent:action',
376
- agent: 'Wiki Agent',
377
- action: 'compiled',
378
- target: page,
379
- });
380
- }
599
+ else {
600
+ routesLogger.debug('[Memory Promotion] Nothing qualified for promotion');
381
601
  }
382
- });
383
- // Manual trigger API
384
- apiServer.app.post('/api/wiki/compile', auth_middleware_js_1.requireAuth, async (_req, res) => {
385
- runWikiAgent().catch(() => { });
386
- res.json({ ok: true, message: 'Wiki compilation triggered' });
387
- });
388
- // First run after 15s (let connectors and dashboard agent go first)
389
- setTimeout(runWikiAgent, 15_000);
390
- routesLogger.info('[Wiki Agent] Ready triggers: extraction:completed event, POST /api/wiki/compile');
391
- }
602
+ }
603
+ catch (err) {
604
+ routesLogger.error('[Memory Promotion] Error:', err instanceof Error ? err.message : err);
605
+ }
606
+ };
607
+ // Serialize all promotion runs on one chain (same 'Process is busy' class
608
+ // as the dashboard and wiki agents).
609
+ let promotionRunChain = Promise.resolve();
610
+ const runMemoryPromotion = () => {
611
+ promotionRunChain = promotionRunChain.then(doPromotionRun).catch(() => { });
612
+ return promotionRunChain;
613
+ };
614
+ setTimeout(runMemoryPromotion, PROMOTION_INITIAL_DELAY_MS);
615
+ setInterval(runMemoryPromotion, PROMOTION_INTERVAL_MS);
616
+ apiServer.app.post('/api/memory/promote', auth_middleware_js_1.requireAuth, (_req, res) => {
617
+ runMemoryPromotion().catch(() => { });
618
+ res.json({ ok: true, message: 'Memory promotion run triggered' });
619
+ });
620
+ console.log(`[Memory Promotion] Ready: every ${PROMOTION_INTERVAL_MS / 3_600_000}h, POST /api/memory/promote`);
392
621
  }
393
622
  // ── Conductor Audit — hourly system health check ──────────────────────
394
- {
395
- const AUDIT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
396
- const AUDIT_INITIAL_DELAY_MS = 5 * 60 * 1000; // 5 min after startup
397
- const auditPrompt = 'Perform a system audit. Read ~/.mama/skills/audit-checklist.md and execute each step. ' +
398
- 'Classify findings as MINOR or MAJOR. ' +
399
- 'For MINOR items: execute the auto-fix command immediately (Bash curl). Do NOT just report — fix it. ' +
400
- 'For MAJOR items: report to human via channel alert. ' +
401
- 'Do NOT save raw audit results or audit logs as mama_save decisions. ' +
402
- 'Only call mama_save when the audit produces a durable policy/remediation decision or reusable lesson; otherwise rely on validation sessions and agent_activity for the audit record.';
403
- const runConductorAudit = async () => {
404
- let auditSession = null;
405
- let auditVersion = 0;
406
- const auditStart = Date.now();
623
+ const AUDIT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
624
+ const AUDIT_INITIAL_DELAY_MS = 5 * 60 * 1000; // 5 min after startup
625
+ // Owner verdict 2026-07-11: the hourly audit re-sent the same MAJOR alert every
626
+ // run because each audit was stateless. agent_notices does not carry audit
627
+ // findings (review feedback on #134), so dedup state lives in a JSON file the
628
+ // conductor reads/writes itself, and the current time is interpolated per run
629
+ // so the 24h comparison is grounded.
630
+ const buildAuditPrompt = (nowIso) => 'Perform a system audit. Read ~/.mama/skills/audit-checklist.md and execute each step. ' +
631
+ 'Classify findings as MINOR or MAJOR. ' +
632
+ 'For MINOR items: execute the auto-fix command immediately (Bash curl). Do NOT just report — fix it. ' +
633
+ 'For MAJOR items: report to human via channel alert, subject to the dedup rule below. ' +
634
+ `Alert dedup (MANDATORY): the current time is ${nowIso}. Read ~/.mama/state/audit-findings.json ` +
635
+ '(it may not exist on the first run). Re-alert a MAJOR finding only if it is NEW (no entry ' +
636
+ 'with the same id), has ESCALATED, or its last_alerted_at is older than 24 hours compared to ' +
637
+ 'the current time; otherwise stay silent about it. Never alert MINOR findings to the owner. ' +
638
+ 'After the audit, Write ~/.mama/state/audit-findings.json with an array of every current ' +
639
+ 'finding: {id (stable kebab-case slug), severity, first_seen, last_seen, last_alerted_at}. ' +
640
+ 'Carry forward first_seen and last_alerted_at from the previous file; set last_alerted_at to ' +
641
+ 'the current time only for findings you actually alerted this run. ' +
642
+ // Owner verdict 2026-07-10: hourly-audit self-notes are memory noise -- the two
643
+ // "durable policy" saves it produced were unscoped near-duplicates polluting
644
+ // global recall. The audit record lives in validation sessions + agent_activity;
645
+ // MAJOR findings reach the owner via the channel alert. No memory writes here.
646
+ 'Do NOT call mama_save under any circumstances: the validation session and agent_activity ' +
647
+ 'are the durable audit record, and repeated audits must not accumulate policy notes in memory.';
648
+ const runConductorAudit = async () => {
649
+ let auditSession = null;
650
+ let auditVersion = 0;
651
+ const auditStart = Date.now();
652
+ try {
653
+ routesLogger.debug('[Conductor Audit] Starting hourly audit...');
407
654
  try {
408
- routesLogger.debug('[Conductor Audit] Starting hourly audit...');
655
+ const ver = sessionsDb ? (0, agent_store_js_1.getLatestVersion)(sessionsDb, 'conductor') : null;
656
+ auditVersion = ver?.version ?? 0;
657
+ auditSession =
658
+ validationService?.startSession('conductor', auditVersion, 'audit', {
659
+ goal: 'hourly system audit',
660
+ }) ?? null;
661
+ }
662
+ catch (bootstrapErr) {
663
+ routesLogger.warn('[Conductor Audit] Failed to initialize audit telemetry:', bootstrapErr);
664
+ auditVersion = 0;
665
+ auditSession = null;
666
+ }
667
+ if (sessionsDb) {
409
668
  try {
410
- const ver = sessionsDb ? (0, agent_store_js_1.getLatestVersion)(sessionsDb, 'conductor') : null;
411
- auditVersion = ver?.version ?? 0;
412
- auditSession =
413
- validationService?.startSession('conductor', auditVersion, 'audit', {
414
- goal: 'hourly system audit',
415
- }) ?? null;
416
- }
417
- catch (bootstrapErr) {
418
- routesLogger.warn('[Conductor Audit] Failed to initialize audit telemetry:', bootstrapErr);
419
- auditVersion = 0;
420
- auditSession = null;
421
- }
422
- if (sessionsDb) {
423
- try {
424
- const startRow = (0, agent_store_js_1.logActivity)(sessionsDb, {
425
- agent_id: 'conductor',
426
- agent_version: auditVersion,
427
- type: 'audit_start',
428
- input_summary: 'Hourly system audit',
429
- run_id: auditSession?.id,
430
- execution_status: 'started',
431
- trigger_reason: 'audit',
432
- });
433
- if (auditSession) {
434
- try {
435
- validationService?.recordRun(auditSession.id, { activityId: startRow.id });
436
- }
437
- catch (telemetryErr) {
438
- routesLogger.warn('[Conductor Audit] Failed to link startup activity to validation session:', telemetryErr);
439
- }
669
+ const startRow = (0, agent_store_js_1.logActivity)(sessionsDb, {
670
+ agent_id: 'conductor',
671
+ agent_version: auditVersion,
672
+ type: 'audit_start',
673
+ input_summary: 'Hourly system audit',
674
+ run_id: auditSession?.id,
675
+ execution_status: 'started',
676
+ trigger_reason: 'audit',
677
+ });
678
+ if (auditSession) {
679
+ try {
680
+ validationService?.recordRun(auditSession.id, { activityId: startRow.id });
440
681
  }
441
- }
442
- catch (telemetryErr) {
443
- routesLogger.warn('[Conductor Audit] Failed to write startup telemetry:', telemetryErr);
444
- }
445
- }
446
- const auditChannelId = `conductor-audit-${Date.now()}`;
447
- await messageRouter.process({
448
- source: 'system',
449
- channelId: auditChannelId,
450
- userId: 'system',
451
- text: auditPrompt,
452
- });
453
- const auditDuration = Date.now() - auditStart;
454
- try {
455
- if (sessionsDb) {
456
- const activityRow = (0, agent_store_js_1.logActivity)(sessionsDb, {
457
- agent_id: 'conductor',
458
- agent_version: auditVersion,
459
- type: 'audit_complete',
460
- input_summary: 'Hourly system audit',
461
- duration_ms: auditDuration,
462
- run_id: auditSession?.id,
463
- execution_status: 'completed',
464
- trigger_reason: 'audit',
465
- });
466
- if (auditSession) {
467
- validationService?.recordRun(auditSession.id, { activityId: activityRow.id });
682
+ catch (telemetryErr) {
683
+ routesLogger.warn('[Conductor Audit] Failed to link startup activity to validation session:', telemetryErr);
468
684
  }
469
685
  }
470
686
  }
471
687
  catch (telemetryErr) {
472
- routesLogger.warn('[Conductor Audit] Failed to write completion telemetry:', telemetryErr);
473
- }
474
- try {
475
- if (auditSession && validationService) {
476
- validationService.finalizeSession(auditSession.id, {
477
- execution_status: 'completed',
478
- metrics: { duration_ms: auditDuration },
479
- });
480
- }
481
- }
482
- catch (telemetryErr) {
483
- routesLogger.warn('[Conductor Audit] Failed to finalize validation session:', telemetryErr);
688
+ routesLogger.warn('[Conductor Audit] Failed to write startup telemetry:', telemetryErr);
484
689
  }
485
- routesLogger.debug('[Conductor Audit] Audit complete');
486
690
  }
487
- catch (err) {
488
- const auditDuration = Date.now() - auditStart;
489
- try {
490
- if (sessionsDb) {
491
- const failRow = (0, agent_store_js_1.logActivity)(sessionsDb, {
492
- agent_id: 'conductor',
493
- agent_version: auditVersion,
494
- type: 'audit_failed',
495
- input_summary: 'Hourly system audit failed',
496
- error_message: err instanceof Error ? err.message : String(err),
497
- duration_ms: auditDuration,
498
- run_id: auditSession?.id,
499
- execution_status: 'failed',
500
- trigger_reason: 'audit',
501
- });
502
- if (auditSession) {
503
- validationService?.recordRun(auditSession.id, { activityId: failRow.id });
504
- }
691
+ const auditChannelId = `conductor-audit-${Date.now()}`;
692
+ await messageRouter.process({
693
+ source: 'system',
694
+ channelId: auditChannelId,
695
+ userId: 'system',
696
+ text: buildAuditPrompt(new Date().toISOString()),
697
+ });
698
+ const auditDuration = Date.now() - auditStart;
699
+ try {
700
+ if (sessionsDb) {
701
+ const activityRow = (0, agent_store_js_1.logActivity)(sessionsDb, {
702
+ agent_id: 'conductor',
703
+ agent_version: auditVersion,
704
+ type: 'audit_complete',
705
+ input_summary: 'Hourly system audit',
706
+ duration_ms: auditDuration,
707
+ run_id: auditSession?.id,
708
+ execution_status: 'completed',
709
+ trigger_reason: 'audit',
710
+ });
711
+ if (auditSession) {
712
+ validationService?.recordRun(auditSession.id, { activityId: activityRow.id });
505
713
  }
506
714
  }
507
- catch (telemetryErr) {
508
- routesLogger.warn('[Conductor Audit] Failed to write failure telemetry:', telemetryErr);
715
+ }
716
+ catch (telemetryErr) {
717
+ routesLogger.warn('[Conductor Audit] Failed to write completion telemetry:', telemetryErr);
718
+ }
719
+ try {
720
+ if (auditSession && validationService) {
721
+ validationService.finalizeSession(auditSession.id, {
722
+ execution_status: 'completed',
723
+ metrics: { duration_ms: auditDuration },
724
+ });
509
725
  }
510
- try {
511
- if (auditSession && validationService) {
512
- validationService.finalizeSession(auditSession.id, {
513
- execution_status: 'failed',
514
- error_message: err instanceof Error ? err.message : String(err),
515
- metrics: { duration_ms: auditDuration },
516
- });
726
+ }
727
+ catch (telemetryErr) {
728
+ routesLogger.warn('[Conductor Audit] Failed to finalize validation session:', telemetryErr);
729
+ }
730
+ routesLogger.debug('[Conductor Audit] Audit complete');
731
+ }
732
+ catch (err) {
733
+ const auditDuration = Date.now() - auditStart;
734
+ try {
735
+ if (sessionsDb) {
736
+ const failRow = (0, agent_store_js_1.logActivity)(sessionsDb, {
737
+ agent_id: 'conductor',
738
+ agent_version: auditVersion,
739
+ type: 'audit_failed',
740
+ input_summary: 'Hourly system audit failed',
741
+ error_message: err instanceof Error ? err.message : String(err),
742
+ duration_ms: auditDuration,
743
+ run_id: auditSession?.id,
744
+ execution_status: 'failed',
745
+ trigger_reason: 'audit',
746
+ });
747
+ if (auditSession) {
748
+ validationService?.recordRun(auditSession.id, { activityId: failRow.id });
517
749
  }
518
750
  }
519
- catch (telemetryErr) {
520
- routesLogger.warn('[Conductor Audit] Failed to finalize failed session:', telemetryErr);
751
+ }
752
+ catch (telemetryErr) {
753
+ routesLogger.warn('[Conductor Audit] Failed to write failure telemetry:', telemetryErr);
754
+ }
755
+ try {
756
+ if (auditSession && validationService) {
757
+ validationService.finalizeSession(auditSession.id, {
758
+ execution_status: 'failed',
759
+ error_message: err instanceof Error ? err.message : String(err),
760
+ metrics: { duration_ms: auditDuration },
761
+ });
521
762
  }
522
- routesLogger.error('[Conductor Audit] Failed:', err instanceof Error ? err.message : String(err));
523
763
  }
524
- };
525
- setTimeout(() => {
526
- runConductorAudit();
527
- setInterval(runConductorAudit, AUDIT_INTERVAL_MS);
528
- }, AUDIT_INITIAL_DELAY_MS);
529
- // Manual trigger
530
- apiServer.app.post('/api/conductor/audit', auth_middleware_js_1.requireAuth, async (_req, res) => {
531
- runConductorAudit().catch(() => { });
532
- res.json({ ok: true, message: 'Conductor audit triggered' });
533
- });
534
- routesLogger.info('[Conductor Audit] Ready — runs every 60 min, POST /api/conductor/audit for manual trigger');
535
- }
764
+ catch (telemetryErr) {
765
+ routesLogger.warn('[Conductor Audit] Failed to finalize failed session:', telemetryErr);
766
+ }
767
+ routesLogger.error('[Conductor Audit] Failed:', err instanceof Error ? err.message : String(err));
768
+ }
769
+ };
770
+ setTimeout(() => {
771
+ runConductorAudit();
772
+ setInterval(runConductorAudit, AUDIT_INTERVAL_MS);
773
+ }, AUDIT_INITIAL_DELAY_MS);
774
+ // Manual trigger
775
+ apiServer.app.post('/api/conductor/audit', auth_middleware_js_1.requireAuth, async (_req, res) => {
776
+ runConductorAudit().catch(() => { });
777
+ res.json({ ok: true, message: 'Conductor audit triggered' });
778
+ });
779
+ routesLogger.info('[Conductor Audit] Ready — runs every 60 min, POST /api/conductor/audit for manual trigger');
536
780
  // ── Memory Agent stats API ────────────────────────────────────────────
537
781
  apiServer.app.get('/api/memory-agent/stats', auth_middleware_js_1.requireAuth, (_req, res) => {
538
782
  const stats = messageRouter.getMemoryAgentStats();
@@ -1187,7 +1431,7 @@ Keep the report under 2000 characters as it will be sent to Discord.`;
1187
1431
  res.sendFile(node_path_1.default.join(publicDir, 'setup.html'));
1188
1432
  });
1189
1433
  apiServer.app.use(express_1.default.static(publicDir, {
1190
- setHeaders: (res) => {
1434
+ setHeaders: (res, _filePath) => {
1191
1435
  res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
1192
1436
  res.setHeader('Pragma', 'no-cache');
1193
1437
  res.setHeader('Expires', '0');