@aitne/daemon 0.1.9 → 0.1.11

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 (333) hide show
  1. package/dist/adapters/adapter-watchdog.d.ts +70 -0
  2. package/dist/adapters/adapter-watchdog.js +115 -0
  3. package/dist/adapters/discord.d.ts +17 -1
  4. package/dist/adapters/discord.js +33 -0
  5. package/dist/adapters/notification-manager.d.ts +27 -1
  6. package/dist/adapters/notification-manager.js +54 -39
  7. package/dist/adapters/slack-adapter.d.ts +26 -1
  8. package/dist/adapters/slack-adapter.js +41 -0
  9. package/dist/adapters/telegram-adapter.d.ts +18 -1
  10. package/dist/adapters/telegram-adapter.js +41 -2
  11. package/dist/adapters/types.d.ts +20 -0
  12. package/dist/adapters/whatsapp-adapter.d.ts +26 -7
  13. package/dist/adapters/whatsapp-adapter.js +74 -21
  14. package/dist/api/env-writer.d.ts +1 -0
  15. package/dist/api/env-writer.js +17 -7
  16. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  17. package/dist/api/helpers/agent-errors-registry.js +5 -5
  18. package/dist/api/routes/agent-schedule.js +5 -1
  19. package/dist/api/routes/agent.js +33 -12
  20. package/dist/api/routes/agents/index.js +75 -16
  21. package/dist/api/routes/agents/views.d.ts +37 -2
  22. package/dist/api/routes/agents/views.js +64 -2
  23. package/dist/api/routes/apple-calendar.js +4 -1
  24. package/dist/api/routes/background-task.d.ts +22 -0
  25. package/dist/api/routes/background-task.js +338 -0
  26. package/dist/api/routes/browser-history.js +9 -1
  27. package/dist/api/routes/calendar.js +12 -2
  28. package/dist/api/routes/context/path-resolve.js +6 -1
  29. package/dist/api/routes/context/permissions.js +12 -2
  30. package/dist/api/routes/context/snapshots.js +0 -3
  31. package/dist/api/routes/context/write.js +3 -17
  32. package/dist/api/routes/dashboard/config.js +58 -12
  33. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  34. package/dist/api/routes/dashboard/notifications.js +9 -9
  35. package/dist/api/routes/dashboard/oauth-google.js +5 -3
  36. package/dist/api/routes/feedback.d.ts +3 -0
  37. package/dist/api/routes/feedback.js +349 -0
  38. package/dist/api/routes/git.js +10 -3
  39. package/dist/api/routes/github.js +5 -1
  40. package/dist/api/routes/integrations/crud-patch.js +5 -1
  41. package/dist/api/routes/integrations-reconcile.js +2 -2
  42. package/dist/api/routes/mcp.js +65 -13
  43. package/dist/api/routes/notion.d.ts +1 -1
  44. package/dist/api/routes/observations.js +7 -7
  45. package/dist/api/routes/obsidian.d.ts +1 -1
  46. package/dist/api/routes/receipts.js +5 -1
  47. package/dist/api/routes/setup-migrate.js +1 -1
  48. package/dist/api/routes/setup.js +1 -1
  49. package/dist/api/routes/task-flows.d.ts +1 -1
  50. package/dist/api/routes/task-flows.js +1 -1
  51. package/dist/api/routes/tuning.d.ts +29 -0
  52. package/dist/api/routes/tuning.js +304 -0
  53. package/dist/api/server.d.ts +44 -16
  54. package/dist/api/server.js +12 -0
  55. package/dist/bootstrap/adapters.d.ts +19 -0
  56. package/dist/bootstrap/adapters.js +61 -0
  57. package/dist/bootstrap/api.d.ts +5 -3
  58. package/dist/bootstrap/api.js +45 -13
  59. package/dist/bootstrap/catchup.d.ts +1 -1
  60. package/dist/bootstrap/catchup.js +11 -11
  61. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  62. package/dist/bootstrap/event-pipeline.js +246 -8
  63. package/dist/bootstrap/observers.js +9 -6
  64. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  65. package/dist/bootstrap/schedule-helpers.js +172 -19
  66. package/dist/config.js +32 -12
  67. package/dist/core/agent-core.d.ts +33 -1
  68. package/dist/core/agent-core.js +36 -1
  69. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  70. package/dist/core/agents/activity-scan-cadence.js +127 -0
  71. package/dist/core/agents/agent-route-override.d.ts +53 -0
  72. package/dist/core/agents/agent-route-override.js +69 -0
  73. package/dist/core/agents/builtin-registry.d.ts +51 -14
  74. package/dist/core/agents/builtin-registry.js +92 -15
  75. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  76. package/dist/core/agents/config-gate-reconcile.js +51 -0
  77. package/dist/core/agents/cron-substitute.d.ts +1 -1
  78. package/dist/core/agents/cron-substitute.js +1 -1
  79. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  80. package/dist/core/agents/custom-routine-migration.js +149 -0
  81. package/dist/core/agents/firing-blocked.d.ts +1 -1
  82. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  83. package/dist/core/agents/hourly-cadence.js +126 -0
  84. package/dist/core/agents/loader-boot.js +23 -0
  85. package/dist/core/agents/loader.d.ts +19 -0
  86. package/dist/core/agents/loader.js +34 -2
  87. package/dist/core/agents/override-merge.d.ts +1 -1
  88. package/dist/core/agents/override-merge.js +9 -1
  89. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  90. package/dist/core/agents/recurrence-convert.js +1 -1
  91. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  92. package/dist/core/alerts.js +6 -6
  93. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  94. package/dist/core/backends/auth-health-monitor.js +1 -1
  95. package/dist/core/backends/backend-router.d.ts +27 -1
  96. package/dist/core/backends/backend-router.js +165 -1
  97. package/dist/core/backends/claude-code-core.d.ts +71 -31
  98. package/dist/core/backends/claude-code-core.js +282 -54
  99. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  100. package/dist/core/backends/cli-quota-guards.js +40 -5
  101. package/dist/core/backends/codex-core.d.ts +6 -0
  102. package/dist/core/backends/codex-core.js +22 -6
  103. package/dist/core/backends/failure-spend.d.ts +58 -0
  104. package/dist/core/backends/failure-spend.js +137 -0
  105. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  106. package/dist/core/backends/gemini-cli-core.js +38 -6
  107. package/dist/core/backends/model-registry.d.ts +1 -1
  108. package/dist/core/backends/model-registry.js +4 -4
  109. package/dist/core/backends/opencode-core.d.ts +1 -1
  110. package/dist/core/backends/opencode-core.js +5 -5
  111. package/dist/core/backends/plan-presets.js +47 -18
  112. package/dist/core/bang-commands/commands-cost.js +3 -1
  113. package/dist/core/bang-commands/commands-report.js +4 -3
  114. package/dist/core/bang-commands/commands-research.js +4 -1
  115. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  116. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  117. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  118. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  119. package/dist/core/bang-commands/commands-task-control.js +147 -0
  120. package/dist/core/bang-commands/commands-wiki.js +5 -5
  121. package/dist/core/bang-commands/index.d.ts +2 -0
  122. package/dist/core/bang-commands/index.js +12 -0
  123. package/dist/core/bang-commands/registry.d.ts +12 -0
  124. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  125. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  126. package/dist/core/channel-timeline.d.ts +5 -1
  127. package/dist/core/channel-timeline.js +13 -0
  128. package/dist/core/context/index-reconciler.js +5 -2
  129. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  130. package/dist/core/context/policy-index-runner.js +25 -6
  131. package/dist/core/context-builder-calendar.js +10 -2
  132. package/dist/core/context-builder-conversation.d.ts +8 -1
  133. package/dist/core/context-builder-conversation.js +41 -7
  134. package/dist/core/context-builder-yesterday.js +4 -3
  135. package/dist/core/context-builder.d.ts +7 -2
  136. package/dist/core/context-builder.js +193 -5
  137. package/dist/core/context-file-serializer.d.ts +1 -1
  138. package/dist/core/context-file-serializer.js +1 -1
  139. package/dist/core/context-health.js +2 -2
  140. package/dist/core/context-paths.d.ts +11 -1
  141. package/dist/core/context-paths.js +17 -1
  142. package/dist/core/context-validation/prepare-write.js +1 -1
  143. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  144. package/dist/core/context-vault-aliases.d.ts +0 -13
  145. package/dist/core/context-vault-aliases.js +37 -0
  146. package/dist/core/custom-routines.d.ts +99 -0
  147. package/dist/core/custom-routines.js +187 -0
  148. package/dist/core/daemon-api-cli.js +50 -1
  149. package/dist/core/day-boundary.d.ts +46 -0
  150. package/dist/core/day-boundary.js +40 -0
  151. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  152. package/dist/core/dispatcher-activity-scan.js +775 -0
  153. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  154. package/dist/core/dispatcher-error-handling.js +38 -62
  155. package/dist/core/dispatcher-hourly-check.js +6 -1
  156. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  157. package/dist/core/dispatcher-message-handler.js +24 -0
  158. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  159. package/dist/core/dispatcher-morning-routine.js +13 -13
  160. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  161. package/dist/core/dispatcher-result-processor.js +167 -11
  162. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  163. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  164. package/dist/core/dispatcher-scheduled-tasks.d.ts +104 -1
  165. package/dist/core/dispatcher-scheduled-tasks.js +480 -8
  166. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  167. package/dist/core/dispatcher-task-delivery.js +555 -0
  168. package/dist/core/dispatcher-types.d.ts +48 -9
  169. package/dist/core/dispatcher-types.js +3 -3
  170. package/dist/core/dispatcher.d.ts +112 -31
  171. package/dist/core/dispatcher.js +297 -60
  172. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  173. package/dist/core/drift-effects.js +2 -2
  174. package/dist/core/feedback/consolidation-prep.d.ts +94 -0
  175. package/dist/core/feedback/consolidation-prep.js +254 -0
  176. package/dist/core/feedback/eviction-scorer.d.ts +81 -0
  177. package/dist/core/feedback/eviction-scorer.js +136 -0
  178. package/dist/core/feedback/lesson-format.d.ts +79 -0
  179. package/dist/core/feedback/lesson-format.js +199 -0
  180. package/dist/core/feedback/lesson-injection.d.ts +98 -0
  181. package/dist/core/feedback/lesson-injection.js +174 -0
  182. package/dist/core/feedback/lesson-merge.d.ts +51 -0
  183. package/dist/core/feedback/lesson-merge.js +88 -0
  184. package/dist/core/feedback/lesson-store-overview.d.ts +46 -0
  185. package/dist/core/feedback/lesson-store-overview.js +42 -0
  186. package/dist/core/feedback/promotion-gate.d.ts +69 -0
  187. package/dist/core/feedback/promotion-gate.js +117 -0
  188. package/dist/core/feedback/regeneralization-prep.d.ts +87 -0
  189. package/dist/core/feedback/regeneralization-prep.js +152 -0
  190. package/dist/core/feedback/scope-parser.d.ts +86 -0
  191. package/dist/core/feedback/scope-parser.js +141 -0
  192. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  193. package/dist/core/feedback/self-performance-prep.js +541 -0
  194. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  195. package/dist/core/feedback/tuning-actuator.js +432 -0
  196. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  197. package/dist/core/feedback/tuning-recommender.js +580 -0
  198. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  199. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  200. package/dist/core/health-monitor.d.ts +6 -0
  201. package/dist/core/health-monitor.js +1 -1
  202. package/dist/core/injection-policy.d.ts +83 -1
  203. package/dist/core/injection-policy.js +61 -3
  204. package/dist/core/integration-main-backend.js +4 -0
  205. package/dist/core/management-md.d.ts +2 -2
  206. package/dist/core/management-md.js +51 -13
  207. package/dist/core/morning/orchestrator.d.ts +2 -2
  208. package/dist/core/morning/orchestrator.js +2 -2
  209. package/dist/core/notification-gate.d.ts +64 -0
  210. package/dist/core/notification-gate.js +51 -0
  211. package/dist/core/notification-rate-limit.d.ts +40 -0
  212. package/dist/core/notification-rate-limit.js +50 -0
  213. package/dist/core/policy-files.d.ts +1 -1
  214. package/dist/core/policy-files.js +2 -2
  215. package/dist/core/pre-pass-freshness.d.ts +4 -4
  216. package/dist/core/retention.d.ts +5 -0
  217. package/dist/core/retention.js +20 -4
  218. package/dist/core/review-context.d.ts +1 -1
  219. package/dist/core/review-context.js +10 -5
  220. package/dist/core/roadmap-write-lock.d.ts +2 -1
  221. package/dist/core/roadmap-write-lock.js +15 -10
  222. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  223. package/dist/core/routine-acquisition-plan.js +78 -20
  224. package/dist/core/routine-fetch-window-retry.js +7 -4
  225. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  226. package/dist/core/routine-fetch-window-runner.js +264 -13
  227. package/dist/core/routine-windows.d.ts +2 -2
  228. package/dist/core/routine-windows.js +8 -5
  229. package/dist/core/scheduler.d.ts +175 -16
  230. package/dist/core/scheduler.js +559 -102
  231. package/dist/core/signal-detector.d.ts +51 -1
  232. package/dist/core/signal-detector.js +321 -24
  233. package/dist/core/skills-compiler-denied-tools.js +2 -2
  234. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  235. package/dist/core/skills-compiler-skill-index.js +2 -2
  236. package/dist/core/skills-compiler-variants.d.ts +1 -1
  237. package/dist/core/skills-compiler-variants.js +8 -0
  238. package/dist/core/skills-compiler.d.ts +29 -26
  239. package/dist/core/skills-compiler.js +117 -81
  240. package/dist/core/skills-manifest.d.ts +37 -0
  241. package/dist/core/skills-manifest.js +73 -2
  242. package/dist/core/sleep-inhibitor.d.ts +79 -0
  243. package/dist/core/sleep-inhibitor.js +132 -0
  244. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  245. package/dist/core/slim-system-prompt-loader.js +141 -0
  246. package/dist/core/spawn-gates.d.ts +126 -0
  247. package/dist/core/spawn-gates.js +180 -0
  248. package/dist/core/today-direct-writer.d.ts +60 -14
  249. package/dist/core/today-direct-writer.js +90 -13
  250. package/dist/core/today-write-lock.d.ts +4 -2
  251. package/dist/core/today-write-lock.js +30 -20
  252. package/dist/core/wake-detector.d.ts +55 -0
  253. package/dist/core/wake-detector.js +80 -0
  254. package/dist/core/wiki/compile-lock.d.ts +1 -1
  255. package/dist/core/wiki/compile-lock.js +1 -1
  256. package/dist/core/wiki/wiki-fts.js +13 -6
  257. package/dist/core/workdir.js +15 -6
  258. package/dist/db/activity-scan-signals.d.ts +77 -0
  259. package/dist/db/activity-scan-signals.js +378 -0
  260. package/dist/db/agents-store.d.ts +28 -0
  261. package/dist/db/agents-store.js +62 -0
  262. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  263. package/dist/db/background-task-clarifications-store.js +152 -0
  264. package/dist/db/background-task-store.d.ts +207 -0
  265. package/dist/db/background-task-store.js +380 -0
  266. package/dist/db/browser-history-store.d.ts +39 -6
  267. package/dist/db/browser-history-store.js +51 -7
  268. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  269. package/dist/db/browser-task-clarifications-store.js +35 -5
  270. package/dist/db/browser-task-store.d.ts +3 -0
  271. package/dist/db/browser-task-store.js +29 -4
  272. package/dist/db/deferred-dm.d.ts +86 -0
  273. package/dist/db/deferred-dm.js +199 -0
  274. package/dist/db/feedback-signals-store.d.ts +77 -0
  275. package/dist/db/feedback-signals-store.js +144 -0
  276. package/dist/db/migrations.js +380 -0
  277. package/dist/db/observations.d.ts +2 -2
  278. package/dist/db/observations.js +3 -3
  279. package/dist/db/schema.js +260 -22
  280. package/dist/db/voice-transcripts-store.d.ts +1 -1
  281. package/dist/index.js +86 -29
  282. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  283. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  284. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  285. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  286. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  287. package/dist/observers/delegated-sync-worker.js +10 -10
  288. package/dist/observers/git-delegated-cron.d.ts +1 -1
  289. package/dist/observers/git-delegated-cron.js +2 -2
  290. package/dist/observers/github-poller-classifier.d.ts +3 -3
  291. package/dist/observers/github-poller-classifier.js +3 -3
  292. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  293. package/dist/observers/imminent-event-scheduler.js +1 -1
  294. package/dist/observers/mail-poller.d.ts +1 -0
  295. package/dist/observers/mail-poller.js +42 -3
  296. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  297. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  298. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  299. package/dist/observers/observation-summarizer/worker.js +4 -4
  300. package/dist/observers/obsidian-watcher.d.ts +1 -1
  301. package/dist/observers/obsidian-watcher.js +1 -1
  302. package/dist/safety/agent-write-tracker.d.ts +4 -4
  303. package/dist/safety/agent-write-tracker.js +4 -4
  304. package/dist/safety/always-disallowed.d.ts +1 -1
  305. package/dist/safety/always-disallowed.js +39 -0
  306. package/dist/safety/audit.d.ts +43 -5
  307. package/dist/safety/audit.js +86 -18
  308. package/dist/safety/risk-classifier.d.ts +6 -0
  309. package/dist/safety/risk-classifier.js +97 -18
  310. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  311. package/dist/scheduler/activity-scan-gate.js +132 -0
  312. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  313. package/dist/services/background-task/background-task-budget.js +91 -0
  314. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  315. package/dist/services/background-task/background-task-driver.js +416 -0
  316. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  317. package/dist/services/background-task/background-task-runner.js +673 -0
  318. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  319. package/dist/services/background-task/background-task-tools.js +247 -0
  320. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  321. package/dist/services/background-task/background-task-transition-events.js +54 -0
  322. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  323. package/dist/services/browser-history/automation/egress-denylist.js +34 -8
  324. package/dist/services/browser-history/lifecycle/platform.js +44 -2
  325. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  326. package/dist/services/browser-task/browser-task-runner.js +53 -8
  327. package/dist/services/mcp/probe.js +30 -8
  328. package/dist/services/observations-batch.d.ts +1 -1
  329. package/dist/services/observations-batch.js +2 -2
  330. package/dist/settings/runtime-settings.d.ts +45 -12
  331. package/dist/settings/runtime-settings.js +215 -40
  332. package/dist/settings/settings-store.js +11 -3
  333. package/package.json +4 -4
@@ -1,8 +1,6 @@
1
- import { execFile } from "node:child_process";
2
1
  import { existsSync, readFileSync } from "node:fs";
3
2
  import { homedir } from "node:os";
4
3
  import { join } from "node:path";
5
- import { promisify } from "node:util";
6
4
  import { Hono } from "hono";
7
5
  import { z } from "zod";
8
6
  import { BACKEND_IDS } from "@aitne/shared";
@@ -13,6 +11,7 @@ import { deleteAllMcpSecrets, deleteMcpServer, disableAllMcpServers, DuplicateMc
13
11
  import { McpServerIdSchema, MCP_RISK_TIERS, MCP_TRANSPORTS, } from "../../services/mcp/types.js";
14
12
  import { probeMcpServer } from "../../services/mcp/probe.js";
15
13
  import { listMcpToolCalls } from "../../services/mcp/tool-audit.js";
14
+ import { runLineCommand } from "../../core/backends/cli-utils.js";
16
15
  const logger = createLogger("mcp-api");
17
16
  const BackendIdSchema = z.enum(BACKEND_IDS);
18
17
  const CreateInputSchema = z.object({
@@ -383,6 +382,24 @@ export function createMcpRoutes(deps) {
383
382
  composeIssue("mcp.not_found", { field: "id", received: id }),
384
383
  ]);
385
384
  }
385
+ // Validate keyName against the server's declared keys, mirroring the PUT
386
+ // handler. Without this guard DELETE accepted any raw `keyName` from the
387
+ // URL and removed the corresponding `mcp:<id>:<keyName>` blob — an
388
+ // asymmetry that let a caller target blobs the server never declared.
389
+ const keys = new Set([...server.envKeys, ...server.headerKeys]);
390
+ if (!keys.has(keyName)) {
391
+ return respondWithAgentError(c, 400, [
392
+ composeIssue("mcp.unknown_key", {
393
+ field: "keyName",
394
+ received: keyName,
395
+ expected: `one of ${[...keys].join(", ")}`,
396
+ }),
397
+ ], {
398
+ legacyFields: {
399
+ message: `keyName must be declared in envKeys/headerKeys: ${keyName}`,
400
+ },
401
+ });
402
+ }
386
403
  await deleteAllMcpSecrets(blobStore, id, [keyName]);
387
404
  return c.json({ status: "deleted" });
388
405
  });
@@ -433,10 +450,44 @@ export function createMcpRoutes(deps) {
433
450
  });
434
451
  }
435
452
  try {
436
- const { stdout, stderr } = await execFileAsync("gemini", args, {
437
- timeout: 120_000,
438
- maxBuffer: 1024 * 1024,
453
+ // Route through runLineCommand, not a bare execFile("gemini"): on
454
+ // Windows the npm-installed Gemini CLI is a `gemini.cmd` batch shim,
455
+ // which a shell:false spawn of the bare name cannot resolve (no PATHEXT)
456
+ // — so this dashboard install was 100% non-functional on Windows.
457
+ // runLineCommand's resolveWin32Invocation resolves the name via PATHEXT
458
+ // and launches the `.cmd` through an escaped cmd.exe wrapper (no
459
+ // shell:true, no metachar re-parse). The args are a static const, so
460
+ // there is no injection dimension regardless.
461
+ const result = await runLineCommand({
462
+ command: "gemini",
463
+ args: [...args],
464
+ cwd: homedir(),
465
+ timeoutMs: 120_000,
439
466
  });
467
+ const stdout = result.stdoutLines.join("\n");
468
+ const stderr = result.stderrLines.join("\n");
469
+ // Contract remap: execFile REJECTS on non-zero exit, but runLineCommand
470
+ // RESOLVES with exitCode !== 0 (it rejects only on a spawn-level error).
471
+ // Branch on exitCode/timedOut so an OAuth-required / version-mismatch
472
+ // failure still maps to the 502 install_failed path instead of being
473
+ // mis-reported as ok:true.
474
+ if (result.timedOut || (result.exitCode ?? 0) !== 0) {
475
+ const message = result.timedOut
476
+ ? "gemini install command timed out after 120s"
477
+ : stderr || stdout || `gemini exited with code ${result.exitCode}`;
478
+ logger.warn({ kind, args, exitCode: result.exitCode, timedOut: result.timedOut }, "gemini install command failed");
479
+ return respondWithAgentError(c, 502, [composeIssue("mcp.install_failed", { field: "gemini", received: message })], {
480
+ legacyFields: {
481
+ ok: false,
482
+ kind,
483
+ command: ["gemini", ...args].join(" "),
484
+ message,
485
+ stdout,
486
+ stderr,
487
+ exitCode: result.exitCode,
488
+ },
489
+ });
490
+ }
440
491
  logger.info({ kind, args, stdoutLen: stdout.length }, "gemini install command completed");
441
492
  return c.json({
442
493
  ok: true,
@@ -448,12 +499,14 @@ export function createMcpRoutes(deps) {
448
499
  });
449
500
  }
450
501
  catch (err) {
502
+ // Spawn-level failure only: runLineCommand rejects via child.once("error")
503
+ // with the raw spawn error (code:"ENOENT" when the bare/resolved name is
504
+ // unresolvable). On Windows, resolveWin32Invocation returns null for an
505
+ // unresolvable bare "gemini" so spawn still ENOENTs naturally — the 503
506
+ // gemini_cli_not_found path is preserved.
451
507
  const message = toSafeErrorMessage(err);
452
- // execFile rejects with the spawn error AND attaches stdout/stderr
453
- // / code on the rejection value. Surface them when present so the
454
- // dashboard can show OAuth-required / version-mismatch hints.
455
508
  const e = err;
456
- logger.warn({ kind, args, code: e.code, message }, "gemini install command failed");
509
+ logger.warn({ kind, args, code: e.code, message }, "gemini install command spawn failed");
457
510
  const code = e.code === "ENOENT" ? "mcp.gemini_cli_not_found" : "mcp.install_failed";
458
511
  const status = e.code === "ENOENT" ? 503 : 502;
459
512
  return respondWithAgentError(c, status, [composeIssue(code, { field: "gemini", received: message })], {
@@ -462,16 +515,15 @@ export function createMcpRoutes(deps) {
462
515
  kind,
463
516
  command: ["gemini", ...args].join(" "),
464
517
  message,
465
- stdout: e.stdout ?? "",
466
- stderr: e.stderr ?? "",
467
- exitCode: typeof e.code === "number" ? e.code : null,
518
+ stdout: "",
519
+ stderr: "",
520
+ exitCode: null,
468
521
  },
469
522
  });
470
523
  }
471
524
  });
472
525
  return app;
473
526
  }
474
- const execFileAsync = promisify(execFile);
475
527
  /**
476
528
  * Pre-spawn idempotency check. Returns true when the target install is
477
529
  * already present on disk, so the route can short-circuit without
@@ -11,7 +11,7 @@ export interface NotionRouteDependencies {
11
11
  /**
12
12
  * Optional shared write tracker. All write endpoints pre-mark the target
13
13
  * page (`notion:<pageId>`) so NotionPoller attributes the resulting
14
- * observation to `actor='agent'` and hourly_check's `?actor=user` filter
14
+ * observation to `actor='agent'` and activity_scan's `?actor=user` filter
15
15
  * excludes the echo. Without this the agent can observe its own writes
16
16
  * and loop.
17
17
  */
@@ -80,7 +80,7 @@ function resolveSinceParam(rawSince, rawAlias) {
80
80
  /**
81
81
  * INTEGRATION_NATIVE_MODE_DESIGN.md §11.3.1 — map an observation `source`
82
82
  * string to the integration key whose flip lock would gate writes against
83
- * it. The agent's hourly_check / native-mode skill always uses one of the
83
+ * it. The agent's activity_scan / native-mode skill always uses one of the
84
84
  * registry's integration keys verbatim as the `source` value (e.g.
85
85
  * `"gmail"`, `"google_calendar"`, `"notion"`). Sources outside the
86
86
  * registry (Obsidian, Git, messaging adapter, system) are never locked
@@ -102,7 +102,7 @@ const normalizeMailObservationPayload = normalizeMailObservationPayloadShared;
102
102
  * cost-reduction-structural §A "Failure modes" — the summary may be
103
103
  * outdated when the worker lags far behind the observation moment (e.g.
104
104
  * laptop-sleep backlog reclaimed at startup, where the summarizer has
105
- * caught up by the time hourly_check runs but the underlying source
105
+ * caught up by the time activity_scan runs but the underlying source
106
106
  * may have shifted in between). Surface a `summaryStale` flag the
107
107
  * consumer skill can branch on, rather than asking the LLM to do
108
108
  * timestamp arithmetic.
@@ -201,10 +201,10 @@ export function createObservationRoutes(deps) {
201
201
  /**
202
202
  * POST /observations — record an agent-originated observation.
203
203
  *
204
- * Used by `routine.hourly_check` to queue `roadmap_candidate` signals
204
+ * Used by `routine.activity_scan` to queue `roadmap_candidate` signals
205
205
  * (long-horizon intents too weak to write to roadmap.md directly;
206
206
  * ROADMAP-REDESIGN §3.4 RFC-C) AND by INTEGRATION_NATIVE_MODE_DESIGN.md
207
- * §8.3 native-mode hourly_check turns to persist the materialised mail
207
+ * §8.3 native-mode activity_scan turns to persist the materialised mail
208
208
  * thread / calendar event list the agent just fetched via the main
209
209
  * backend's MCP. The DB layer UPSERTs on `(source, ref)` where
210
210
  * `consumed_at IS NULL`, so re-posting the same candidate across hourly
@@ -233,7 +233,7 @@ export function createObservationRoutes(deps) {
233
233
  // Peek at the raw body BEFORE delegating to `readJsonBody` so we can
234
234
  // turn a query-string-shaped body ("limit=30", "actor=user&limit=20")
235
235
  // into a method-confusion hint. Production telemetry showed the
236
- // hourly_check agent sending `POST /api/observations` with body
236
+ // activity_scan agent sending `POST /api/observations` with body
237
237
  // `limit=30`, expecting it to fetch. Forwarding readJsonBody's
238
238
  // generic "Unexpected token 'l'" message gave the agent no signal
239
239
  // that the right call was `GET /api/observations?limit=30`.
@@ -554,7 +554,7 @@ export function createObservationRoutes(deps) {
554
554
  /**
555
555
  * Field-level validation contract for `POST /observations/consume`.
556
556
  *
557
- * Without per-field error envelopes, a single Stage-3 hourly_check can
557
+ * Without per-field error envelopes, a single Stage-3 activity_scan can
558
558
  * burn turns retrying this endpoint with shape variants
559
559
  * (`correlation_id` snake_case, stringified ids, the angle-bracket
560
560
  * placeholder copied verbatim, per-id paths, etc.). The legacy
@@ -686,7 +686,7 @@ export function createObservationRoutes(deps) {
686
686
  * Helpful 405 for `GET /api/observations/consume`. The bulk consume is
687
687
  * POST-only — without this handler the request 404s with no actionable
688
688
  * detail, and the agent's recovery loop produced 8x retries in one
689
- * routine.hourly_check session.
689
+ * routine.activity_scan session.
690
690
  */
691
691
  app.get("/observations/consume", (c) => c.json({
692
692
  error: "method_not_allowed",
@@ -6,7 +6,7 @@ export interface ObsidianRouteDependencies {
6
6
  /**
7
7
  * Optional shared tracker. When present, every write endpoint pre-marks
8
8
  * the target vault file so the obsidian-watcher attributes the resulting
9
- * chokidar event to `actor='agent'` (and hourly_check's `?actor=user`
9
+ * chokidar event to `actor='agent'` (and activity_scan's `?actor=user`
10
10
  * filter excludes it). Without this the agent can observe its own
11
11
  * Obsidian writes and loop.
12
12
  */
@@ -147,7 +147,11 @@ export function createReceiptRoutes(deps) {
147
147
  ]);
148
148
  }
149
149
  c.header("Content-Type", row.mime_type);
150
- c.header("Content-Disposition", `attachment; filename="${row.filename}"`);
150
+ // `row.filename` comes from email-attachment metadata (sender-controlled),
151
+ // so escape characters that could break out of the quoted header value or
152
+ // inject extra header lines — same discipline as the attachments route.
153
+ const safeFilename = row.filename.replace(/["\\\r\n]/g, "_");
154
+ c.header("Content-Disposition", `attachment; filename="${safeFilename}"`);
151
155
  return c.body(new Uint8Array(attachment.data));
152
156
  });
153
157
  /**
@@ -14,7 +14,7 @@ const logger = createLogger("setup-migrate");
14
14
  * in-flight cron tick to settle (plan §6.2 step 4). Configurable so
15
15
  * tests can override to 0; production default 1s is sufficient because
16
16
  * all known cron handlers either stop immediately (observer pollers)
17
- * or enqueue to the paused EventBus (schedule watcher / hourly check).
17
+ * or enqueue to the paused EventBus (schedule watcher / activity scan).
18
18
  *
19
19
  * Plan says "up to 10s" but that's a ceiling; shorter is fine when no
20
20
  * cron handler is known to block for that long.
@@ -296,7 +296,7 @@ export function createSetupRoutes(deps) {
296
296
  }
297
297
  // Engage the autonomous-work gate BEFORE enqueuing the greeting so any
298
298
  // concurrent cron tick / ScheduleWatcher poll observes the flag and
299
- // yields. Without this, a hourly_check firing in the same tick could
299
+ // yields. Without this, a activity_scan firing in the same tick could
300
300
  // still race the setup conversation and patch today.md, which would
301
301
  // mark the owner-DM session stale and orphan the setup mode.
302
302
  deps.onSetupStart?.(mode);
@@ -14,7 +14,7 @@
14
14
  * that disallows `/`, `..`, leading dots, and anything outside
15
15
  * `[A-Za-z0-9._-]`. Length is capped at 80 characters — the longest
16
16
  * bundled key today is ~45 characters
17
- * (`routine.hourly_check.delegated.gemini`), so 80 leaves comfortable
17
+ * (`routine.activity_scan.delegated.gemini`), so 80 leaves comfortable
18
18
  * headroom without giving an attacker a path-bomb surface.
19
19
  *
20
20
  * Risk tier: Approve. The task-flow body is dispatcher prose that the
@@ -14,7 +14,7 @@
14
14
  * that disallows `/`, `..`, leading dots, and anything outside
15
15
  * `[A-Za-z0-9._-]`. Length is capped at 80 characters — the longest
16
16
  * bundled key today is ~45 characters
17
- * (`routine.hourly_check.delegated.gemini`), so 80 leaves comfortable
17
+ * (`routine.activity_scan.delegated.gemini`), so 80 leaves comfortable
18
18
  * headroom without giving an attacker a path-bomb surface.
19
19
  *
20
20
  * Risk tier: Approve. The task-flow body is dispatcher prose that the
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Self-Tuning Review Cycle — verdict endpoint (SELF_TUNING_REVIEW_CYCLE_DESIGN.md
3
+ * §3.3 / §3.4, Phases 2–3).
4
+ *
5
+ * `POST /api/tuning/verdicts` is RiskTier.Autonomous by design — the
6
+ * abolished Notify tier's replacement pattern (Autonomous + mandatory owner
7
+ * DM on every applied change). Safety is carried by code, not tier (§3.4):
8
+ * - verdicts may only reference recommendation ids the daemon itself
9
+ * generated **this cycle** — no free-form key/value from the model;
10
+ * - ids are single-use and expire when the next weekly cycle overwrites
11
+ * the pending blob;
12
+ * - the handler is idempotent per id — a retried POST cannot double-apply
13
+ * (only verdicts *newly recorded by this POST* reach the actuator);
14
+ * - config writes go through the `applyConfigUpdates` chokepoint, which
15
+ * enforces the per-key bounds (P4).
16
+ *
17
+ * Verdicts are always recorded and audited
18
+ * (`agent_actions.action_type='self_tuning.verdict'`); rejection reasons
19
+ * become `self_critique` feedback signals (§3.3 — so repeated bad
20
+ * recommendations depress the rule via the existing lesson loop). While
21
+ * `selfTuningEnabled` is `false` (the shipped default — §7 shadow period)
22
+ * nothing is actuated regardless of verdict and every response carries
23
+ * `shadow: true` with an empty `applied` array. Once the owner flips the
24
+ * flag (the D1 sign-off), `apply` verdicts actuate per the D5 namespace
25
+ * semantics in `core/feedback/tuning-actuator.ts`.
26
+ */
27
+ import { Hono } from "hono";
28
+ import type { ApiDependencies } from "../server.js";
29
+ export declare function createTuningRoutes(deps: ApiDependencies): Hono;
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Self-Tuning Review Cycle — verdict endpoint (SELF_TUNING_REVIEW_CYCLE_DESIGN.md
3
+ * §3.3 / §3.4, Phases 2–3).
4
+ *
5
+ * `POST /api/tuning/verdicts` is RiskTier.Autonomous by design — the
6
+ * abolished Notify tier's replacement pattern (Autonomous + mandatory owner
7
+ * DM on every applied change). Safety is carried by code, not tier (§3.4):
8
+ * - verdicts may only reference recommendation ids the daemon itself
9
+ * generated **this cycle** — no free-form key/value from the model;
10
+ * - ids are single-use and expire when the next weekly cycle overwrites
11
+ * the pending blob;
12
+ * - the handler is idempotent per id — a retried POST cannot double-apply
13
+ * (only verdicts *newly recorded by this POST* reach the actuator);
14
+ * - config writes go through the `applyConfigUpdates` chokepoint, which
15
+ * enforces the per-key bounds (P4).
16
+ *
17
+ * Verdicts are always recorded and audited
18
+ * (`agent_actions.action_type='self_tuning.verdict'`); rejection reasons
19
+ * become `self_critique` feedback signals (§3.3 — so repeated bad
20
+ * recommendations depress the rule via the existing lesson loop). While
21
+ * `selfTuningEnabled` is `false` (the shipped default — §7 shadow period)
22
+ * nothing is actuated regardless of verdict and every response carries
23
+ * `shadow: true` with an empty `applied` array. Once the owner flips the
24
+ * flag (the D1 sign-off), `apply` verdicts actuate per the D5 namespace
25
+ * semantics in `core/feedback/tuning-actuator.ts`.
26
+ */
27
+ import { Hono } from "hono";
28
+ import { redactSensitiveString } from "@aitne/shared";
29
+ import { readJsonBody } from "../json-body.js";
30
+ import { applyConfigUpdates } from "../env-writer.js";
31
+ import { createSettingsStore } from "../../settings/settings-store.js";
32
+ import { recordFeedbackSignal } from "../../db/feedback-signals-store.js";
33
+ import { readRuntimeState, writeRuntimeState } from "../../db/runtime-state.js";
34
+ import { SELF_TUNING_NOTIFICATION_TYPE, TUNING_PENDING_CYCLE_STATE_KEY, applyVerdictsToCycle, } from "../../core/feedback/tuning-recommender.js";
35
+ import { actuateApplyVerdicts, } from "../../core/feedback/tuning-actuator.js";
36
+ import { createLogger } from "../../logging.js";
37
+ const logger = createLogger("tuning-api");
38
+ const MAX_REASON_CHARS = 280;
39
+ const VERDICTS = new Set(["apply", "reject", "defer"]);
40
+ function isRecord(value) {
41
+ return value !== null && typeof value === "object" && !Array.isArray(value);
42
+ }
43
+ function describeType(value) {
44
+ if (value === undefined)
45
+ return "missing";
46
+ if (value === null)
47
+ return "null";
48
+ if (Array.isArray(value))
49
+ return "array";
50
+ return typeof value;
51
+ }
52
+ function sanitizeReason(value) {
53
+ const flattened = value.replace(/[\u0000-\u001f\u007f]/g, " ");
54
+ const truncated = flattened.length <= MAX_REASON_CHARS
55
+ ? flattened
56
+ : flattened.slice(0, MAX_REASON_CHARS);
57
+ return redactSensitiveString(truncated).trim();
58
+ }
59
+ export function createTuningRoutes(deps) {
60
+ const app = new Hono();
61
+ const { db, config } = deps;
62
+ /**
63
+ * GET /tuning/pending — the current cycle's recommendations + recorded
64
+ * verdicts, for the owner's shadow-period validation and the dashboard.
65
+ * Autonomous: the blob holds knob names and telemetry counts only — no
66
+ * user prose, no secrets.
67
+ */
68
+ app.get("/tuning/pending", (c) => {
69
+ const cycle = readRuntimeState(db, TUNING_PENDING_CYCLE_STATE_KEY);
70
+ const live = config?.selfTuningEnabled === true;
71
+ return c.json({
72
+ cycle,
73
+ selfTuningEnabled: live,
74
+ shadow: !live,
75
+ });
76
+ });
77
+ app.post("/tuning/verdicts", async (c) => {
78
+ const parsedBody = await readJsonBody(c);
79
+ if (!parsedBody.ok)
80
+ return parsedBody.response;
81
+ const body = parsedBody.body;
82
+ if (!isRecord(body)) {
83
+ return c.json({
84
+ error: "validation_error",
85
+ message: "Body must be a JSON object",
86
+ expectedShape: '{"cycleId": string, "verdicts": [{"id": string, "verdict": "apply"|"reject"|"defer", "reason": string}]}',
87
+ }, 400);
88
+ }
89
+ const issues = [];
90
+ const cycleId = typeof body.cycleId === "string" ? body.cycleId : null;
91
+ if (!cycleId) {
92
+ issues.push({
93
+ field: "cycleId",
94
+ expected: "string (the cycle attribute of <tuning_recommendations>)",
95
+ got: describeType(body.cycleId),
96
+ });
97
+ }
98
+ if (!Array.isArray(body.verdicts) || body.verdicts.length === 0) {
99
+ issues.push({
100
+ field: "verdicts",
101
+ expected: "non-empty array",
102
+ got: describeType(body.verdicts),
103
+ });
104
+ }
105
+ const entries = [];
106
+ if (Array.isArray(body.verdicts)) {
107
+ body.verdicts.forEach((raw, index) => {
108
+ if (!isRecord(raw)) {
109
+ issues.push({
110
+ field: `verdicts[${index}]`,
111
+ expected: "object",
112
+ got: describeType(raw),
113
+ });
114
+ return;
115
+ }
116
+ const id = typeof raw.id === "string" ? raw.id : null;
117
+ const verdict = typeof raw.verdict === "string" ? raw.verdict : null;
118
+ const reason = typeof raw.reason === "string" ? sanitizeReason(raw.reason) : "";
119
+ if (!id) {
120
+ issues.push({
121
+ field: `verdicts[${index}].id`,
122
+ expected: "string recommendation id",
123
+ got: describeType(raw.id),
124
+ });
125
+ }
126
+ if (verdict === null || !VERDICTS.has(verdict)) {
127
+ issues.push({
128
+ field: `verdicts[${index}].verdict`,
129
+ expected: "'apply' | 'reject' | 'defer'",
130
+ got: verdict ?? describeType(raw.verdict),
131
+ });
132
+ }
133
+ if (reason.length === 0) {
134
+ issues.push({
135
+ field: `verdicts[${index}].reason`,
136
+ expected: "non-empty one-line string (max 280 chars)",
137
+ got: describeType(raw.reason),
138
+ });
139
+ }
140
+ if (id && verdict !== null && VERDICTS.has(verdict) && reason) {
141
+ entries.push({ id, verdict: verdict, reason });
142
+ }
143
+ });
144
+ }
145
+ if (issues.length > 0) {
146
+ return c.json({
147
+ error: "validation_error",
148
+ message: "Request body failed schema validation",
149
+ issues,
150
+ }, 400);
151
+ }
152
+ const cycle = readRuntimeState(db, TUNING_PENDING_CYCLE_STATE_KEY);
153
+ if (!cycle) {
154
+ return c.json({
155
+ error: "no_pending_cycle",
156
+ message: "No pending tuning cycle exists — recommendations are generated by the weekly review pre-step.",
157
+ }, 409);
158
+ }
159
+ if (cycle.cycleId !== cycleId) {
160
+ // §3.4 — single-use ids: the weekly pre-step overwrote the blob, so the
161
+ // referenced cycle's ids have expired. No replay.
162
+ return c.json({
163
+ error: "cycle_expired",
164
+ message: `Cycle '${cycleId}' is not the pending cycle — its ids have expired.`,
165
+ activeCycleId: cycle.cycleId,
166
+ }, 409);
167
+ }
168
+ const matched = entries.map((entry) => ({
169
+ entry,
170
+ rec: cycle.recommendations.find((rec) => rec.id === entry.id) ?? null,
171
+ }));
172
+ const known = matched.filter((m) => m.rec !== null);
173
+ const unknownIds = [
174
+ ...new Set(matched.filter((m) => m.rec === null).map((m) => m.entry.id)),
175
+ ];
176
+ if (unknownIds.length > 0) {
177
+ // §3.4 — verdicts may only reference daemon-generated ids from this
178
+ // cycle. Atomic reject: nothing is recorded on a partially-bad batch,
179
+ // so a corrected retry cannot double-record the valid half.
180
+ return c.json({
181
+ error: "unknown_recommendation_ids",
182
+ message: "Verdicts may only reference recommendation ids generated this cycle.",
183
+ unknownIds,
184
+ knownIds: cycle.recommendations.map((rec) => rec.id),
185
+ }, 400);
186
+ }
187
+ const { cycle: updated, results } = applyVerdictsToCycle(cycle, entries, new Date().toISOString());
188
+ writeRuntimeState(db, TUNING_PENDING_CYCLE_STATE_KEY, updated);
189
+ const recordedIds = new Set(results.filter((r) => r.status === "recorded").map((r) => r.id));
190
+ // §3.3 — rejection reasons become self_critique signals so the lesson
191
+ // loop learns which recommendations the judge keeps refusing. Recorded
192
+ // only (idempotency: a retried duplicate never double-posts), and only
193
+ // while the feedback loop is enabled — mirroring POST /api/feedback's
194
+ // kill-switch posture.
195
+ if (config?.feedbackLearningEnabled !== false) {
196
+ for (const { entry, rec } of known) {
197
+ if (entry.verdict !== "reject" || !recordedIds.has(entry.id))
198
+ continue;
199
+ try {
200
+ recordFeedbackSignal(db, {
201
+ source: "self_critique",
202
+ valence: "negative",
203
+ scopeType: "agent",
204
+ scopeRef: null,
205
+ actionKind: "agent_execution",
206
+ actionRef: entry.id,
207
+ agentId: null,
208
+ summary: sanitizeReason(`Tuning recommendation ${rec.rule} (${rec.key}) rejected: ${entry.reason}`),
209
+ evidence: {
210
+ kind: "do-less",
211
+ recommendationId: entry.id,
212
+ rule: rec.rule,
213
+ key: rec.key,
214
+ },
215
+ });
216
+ }
217
+ catch (err) {
218
+ logger.warn({ err, id: entry.id }, "Failed to record self_critique signal for rejected tuning recommendation");
219
+ }
220
+ }
221
+ }
222
+ const live = config?.selfTuningEnabled === true;
223
+ // Telemetry row — the record the owner reads to validate recommendation
224
+ // quality (§7). Failure here must not fail the verdict write: the blob
225
+ // is already persisted.
226
+ try {
227
+ db.prepare(`INSERT INTO agent_actions
228
+ (action_type, trigger, result, detail, started_at, completed_at)
229
+ VALUES ('self_tuning.verdict', 'autonomous', 'success', json(?), datetime('now'), datetime('now'))`).run(JSON.stringify({
230
+ cycleId: cycle.cycleId,
231
+ shadow: !live,
232
+ // `results` is index-aligned with `entries` — applyVerdictsToCycle
233
+ // emits exactly one result per entry, in order.
234
+ verdicts: entries.map((entry, index) => ({
235
+ id: entry.id,
236
+ verdict: entry.verdict,
237
+ reason: entry.reason,
238
+ status: results[index].status,
239
+ })),
240
+ }));
241
+ }
242
+ catch (err) {
243
+ logger.warn({ err }, "Failed to audit self_tuning.verdict");
244
+ }
245
+ // Phase 3 — Actuate (§3.4, D5 namespace semantics). Gated on the D1
246
+ // flag AND on per-id "recorded" status: duplicates/conflicts from a
247
+ // retried POST never reach the actuator, so a change cannot
248
+ // double-apply — including an `apply` recorded during the shadow period
249
+ // and re-POSTed after the flag flip. The actuator owns ledger writes,
250
+ // `self_tuning.applied` audit rows, and the mandatory owner DM; an
251
+ // actuation failure surfaces in `actuationFailures` without failing the
252
+ // verdict write (already persisted above).
253
+ let actuation = { applied: [], failures: [] };
254
+ if (live) {
255
+ const applyRecs = known
256
+ .filter(({ entry }) => entry.verdict === "apply" && recordedIds.has(entry.id))
257
+ .map(({ rec }) => rec);
258
+ if (applyRecs.length > 0) {
259
+ const settingsStore = createSettingsStore(db);
260
+ const agentConfig = config;
261
+ const sendNotification = deps.sendNotification;
262
+ actuation = await actuateApplyVerdicts({
263
+ db,
264
+ getCurrentValue: (key) => agentConfig[key],
265
+ applyUpdates: (updates) => applyConfigUpdates(agentConfig, settingsStore, updates, { db }),
266
+ ...(sendNotification
267
+ ? {
268
+ sendDm: async (message) => {
269
+ await sendNotification({
270
+ message,
271
+ notificationType: SELF_TUNING_NOTIFICATION_TYPE,
272
+ priority: "normal",
273
+ });
274
+ },
275
+ }
276
+ : {}),
277
+ feedbackLearningEnabled: config?.feedbackLearningEnabled,
278
+ }, applyRecs, new Date());
279
+ }
280
+ }
281
+ const counts = { recorded: 0, duplicate: 0, conflict: 0 };
282
+ for (const result of results)
283
+ counts[result.status] += 1;
284
+ logger.info({
285
+ cycleId: cycle.cycleId,
286
+ ...counts,
287
+ applied: actuation.applied.length,
288
+ actuationFailures: actuation.failures.length,
289
+ shadow: !live,
290
+ }, "Tuning verdicts recorded");
291
+ return c.json({
292
+ cycleId: cycle.cycleId,
293
+ results,
294
+ recorded: counts.recorded,
295
+ duplicates: counts.duplicate,
296
+ conflicts: counts.conflict,
297
+ shadow: !live,
298
+ applied: actuation.applied,
299
+ actuationFailures: actuation.failures,
300
+ selfTuningEnabled: live,
301
+ });
302
+ });
303
+ return app;
304
+ }