@aitne/daemon 0.1.10 → 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 (305) 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.js +8 -5
  15. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  16. package/dist/api/helpers/agent-errors-registry.js +5 -5
  17. package/dist/api/routes/agent.js +33 -12
  18. package/dist/api/routes/agents/index.js +75 -16
  19. package/dist/api/routes/agents/views.d.ts +37 -2
  20. package/dist/api/routes/agents/views.js +64 -2
  21. package/dist/api/routes/background-task.d.ts +22 -0
  22. package/dist/api/routes/background-task.js +338 -0
  23. package/dist/api/routes/browser-history.js +9 -1
  24. package/dist/api/routes/context/permissions.js +3 -2
  25. package/dist/api/routes/context/snapshots.js +0 -3
  26. package/dist/api/routes/context/write.js +3 -17
  27. package/dist/api/routes/dashboard/config.js +48 -12
  28. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  29. package/dist/api/routes/dashboard/notifications.js +9 -9
  30. package/dist/api/routes/integrations/crud-patch.js +5 -1
  31. package/dist/api/routes/integrations-reconcile.js +2 -2
  32. package/dist/api/routes/notion.d.ts +1 -1
  33. package/dist/api/routes/observations.js +7 -7
  34. package/dist/api/routes/obsidian.d.ts +1 -1
  35. package/dist/api/routes/receipts.js +5 -1
  36. package/dist/api/routes/setup-migrate.js +1 -1
  37. package/dist/api/routes/setup.js +1 -1
  38. package/dist/api/routes/task-flows.d.ts +1 -1
  39. package/dist/api/routes/task-flows.js +1 -1
  40. package/dist/api/routes/tuning.d.ts +29 -0
  41. package/dist/api/routes/tuning.js +304 -0
  42. package/dist/api/server.d.ts +44 -16
  43. package/dist/api/server.js +9 -0
  44. package/dist/bootstrap/adapters.d.ts +19 -0
  45. package/dist/bootstrap/adapters.js +61 -0
  46. package/dist/bootstrap/api.d.ts +5 -3
  47. package/dist/bootstrap/api.js +45 -13
  48. package/dist/bootstrap/catchup.d.ts +1 -1
  49. package/dist/bootstrap/catchup.js +11 -11
  50. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  51. package/dist/bootstrap/event-pipeline.js +245 -7
  52. package/dist/bootstrap/observers.js +9 -6
  53. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  54. package/dist/bootstrap/schedule-helpers.js +172 -19
  55. package/dist/config.js +26 -12
  56. package/dist/core/agent-core.d.ts +33 -1
  57. package/dist/core/agent-core.js +36 -1
  58. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  59. package/dist/core/agents/activity-scan-cadence.js +127 -0
  60. package/dist/core/agents/agent-route-override.d.ts +53 -0
  61. package/dist/core/agents/agent-route-override.js +69 -0
  62. package/dist/core/agents/builtin-registry.d.ts +51 -14
  63. package/dist/core/agents/builtin-registry.js +92 -15
  64. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  65. package/dist/core/agents/config-gate-reconcile.js +51 -0
  66. package/dist/core/agents/cron-substitute.d.ts +1 -1
  67. package/dist/core/agents/cron-substitute.js +1 -1
  68. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  69. package/dist/core/agents/custom-routine-migration.js +149 -0
  70. package/dist/core/agents/firing-blocked.d.ts +1 -1
  71. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  72. package/dist/core/agents/hourly-cadence.js +126 -0
  73. package/dist/core/agents/loader-boot.js +23 -0
  74. package/dist/core/agents/loader.d.ts +19 -0
  75. package/dist/core/agents/loader.js +34 -2
  76. package/dist/core/agents/override-merge.d.ts +1 -1
  77. package/dist/core/agents/override-merge.js +9 -1
  78. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  79. package/dist/core/agents/recurrence-convert.js +1 -1
  80. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  81. package/dist/core/alerts.js +6 -6
  82. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  83. package/dist/core/backends/auth-health-monitor.js +1 -1
  84. package/dist/core/backends/backend-router.d.ts +27 -1
  85. package/dist/core/backends/backend-router.js +165 -1
  86. package/dist/core/backends/claude-code-core.d.ts +71 -31
  87. package/dist/core/backends/claude-code-core.js +282 -54
  88. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  89. package/dist/core/backends/cli-quota-guards.js +40 -5
  90. package/dist/core/backends/codex-core.d.ts +6 -0
  91. package/dist/core/backends/codex-core.js +22 -6
  92. package/dist/core/backends/failure-spend.d.ts +58 -0
  93. package/dist/core/backends/failure-spend.js +137 -0
  94. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  95. package/dist/core/backends/gemini-cli-core.js +25 -6
  96. package/dist/core/backends/model-registry.d.ts +1 -1
  97. package/dist/core/backends/model-registry.js +4 -4
  98. package/dist/core/backends/opencode-core.d.ts +1 -1
  99. package/dist/core/backends/opencode-core.js +5 -5
  100. package/dist/core/backends/plan-presets.js +39 -15
  101. package/dist/core/bang-commands/commands-cost.js +3 -1
  102. package/dist/core/bang-commands/commands-report.js +4 -3
  103. package/dist/core/bang-commands/commands-research.js +4 -1
  104. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  105. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  106. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  107. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  108. package/dist/core/bang-commands/commands-task-control.js +147 -0
  109. package/dist/core/bang-commands/commands-wiki.js +5 -5
  110. package/dist/core/bang-commands/index.d.ts +2 -0
  111. package/dist/core/bang-commands/index.js +12 -0
  112. package/dist/core/bang-commands/registry.d.ts +12 -0
  113. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  114. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  115. package/dist/core/channel-timeline.d.ts +5 -1
  116. package/dist/core/channel-timeline.js +13 -0
  117. package/dist/core/context/index-reconciler.js +5 -2
  118. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  119. package/dist/core/context/policy-index-runner.js +25 -6
  120. package/dist/core/context-builder-calendar.js +10 -2
  121. package/dist/core/context-builder-conversation.d.ts +8 -1
  122. package/dist/core/context-builder-conversation.js +41 -7
  123. package/dist/core/context-builder-yesterday.js +4 -3
  124. package/dist/core/context-builder.d.ts +7 -2
  125. package/dist/core/context-builder.js +62 -20
  126. package/dist/core/context-file-serializer.d.ts +1 -1
  127. package/dist/core/context-file-serializer.js +1 -1
  128. package/dist/core/context-health.js +2 -2
  129. package/dist/core/context-paths.d.ts +1 -1
  130. package/dist/core/context-paths.js +1 -1
  131. package/dist/core/context-validation/prepare-write.js +1 -1
  132. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  133. package/dist/core/context-vault-aliases.d.ts +0 -13
  134. package/dist/core/context-vault-aliases.js +37 -0
  135. package/dist/core/custom-routines.d.ts +99 -0
  136. package/dist/core/custom-routines.js +187 -0
  137. package/dist/core/daemon-api-cli.js +49 -0
  138. package/dist/core/day-boundary.d.ts +46 -0
  139. package/dist/core/day-boundary.js +40 -0
  140. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  141. package/dist/core/dispatcher-activity-scan.js +775 -0
  142. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  143. package/dist/core/dispatcher-error-handling.js +38 -62
  144. package/dist/core/dispatcher-hourly-check.js +6 -1
  145. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  146. package/dist/core/dispatcher-message-handler.js +17 -0
  147. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  148. package/dist/core/dispatcher-morning-routine.js +13 -13
  149. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  150. package/dist/core/dispatcher-result-processor.js +167 -11
  151. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  152. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  153. package/dist/core/dispatcher-scheduled-tasks.d.ts +63 -1
  154. package/dist/core/dispatcher-scheduled-tasks.js +213 -6
  155. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  156. package/dist/core/dispatcher-task-delivery.js +555 -0
  157. package/dist/core/dispatcher-types.d.ts +48 -9
  158. package/dist/core/dispatcher-types.js +3 -3
  159. package/dist/core/dispatcher.d.ts +112 -31
  160. package/dist/core/dispatcher.js +284 -59
  161. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  162. package/dist/core/drift-effects.js +2 -2
  163. package/dist/core/feedback/consolidation-prep.js +17 -5
  164. package/dist/core/feedback/eviction-scorer.js +6 -2
  165. package/dist/core/feedback/lesson-format.js +9 -4
  166. package/dist/core/feedback/lesson-injection.d.ts +1 -1
  167. package/dist/core/feedback/lesson-injection.js +17 -2
  168. package/dist/core/feedback/lesson-store-overview.d.ts +8 -4
  169. package/dist/core/feedback/lesson-store-overview.js +8 -4
  170. package/dist/core/feedback/regeneralization-prep.js +29 -16
  171. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  172. package/dist/core/feedback/self-performance-prep.js +541 -0
  173. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  174. package/dist/core/feedback/tuning-actuator.js +432 -0
  175. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  176. package/dist/core/feedback/tuning-recommender.js +580 -0
  177. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  178. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  179. package/dist/core/health-monitor.d.ts +6 -0
  180. package/dist/core/health-monitor.js +1 -1
  181. package/dist/core/injection-policy.d.ts +4 -4
  182. package/dist/core/injection-policy.js +4 -4
  183. package/dist/core/integration-main-backend.js +4 -0
  184. package/dist/core/management-md.d.ts +2 -2
  185. package/dist/core/management-md.js +51 -13
  186. package/dist/core/morning/orchestrator.d.ts +2 -2
  187. package/dist/core/morning/orchestrator.js +2 -2
  188. package/dist/core/notification-gate.d.ts +64 -0
  189. package/dist/core/notification-gate.js +51 -0
  190. package/dist/core/notification-rate-limit.d.ts +40 -0
  191. package/dist/core/notification-rate-limit.js +50 -0
  192. package/dist/core/policy-files.d.ts +1 -1
  193. package/dist/core/policy-files.js +2 -2
  194. package/dist/core/pre-pass-freshness.d.ts +4 -4
  195. package/dist/core/retention.d.ts +5 -0
  196. package/dist/core/retention.js +20 -4
  197. package/dist/core/review-context.d.ts +1 -1
  198. package/dist/core/review-context.js +10 -5
  199. package/dist/core/roadmap-write-lock.d.ts +2 -1
  200. package/dist/core/roadmap-write-lock.js +15 -10
  201. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  202. package/dist/core/routine-acquisition-plan.js +78 -20
  203. package/dist/core/routine-fetch-window-retry.js +7 -4
  204. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  205. package/dist/core/routine-fetch-window-runner.js +264 -13
  206. package/dist/core/routine-windows.d.ts +2 -2
  207. package/dist/core/routine-windows.js +8 -5
  208. package/dist/core/scheduler.d.ts +175 -16
  209. package/dist/core/scheduler.js +559 -102
  210. package/dist/core/signal-detector.d.ts +12 -0
  211. package/dist/core/signal-detector.js +53 -9
  212. package/dist/core/skills-compiler-denied-tools.js +2 -2
  213. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  214. package/dist/core/skills-compiler-skill-index.js +2 -2
  215. package/dist/core/skills-compiler-variants.d.ts +1 -1
  216. package/dist/core/skills-compiler-variants.js +8 -0
  217. package/dist/core/skills-compiler.d.ts +29 -26
  218. package/dist/core/skills-compiler.js +117 -81
  219. package/dist/core/skills-manifest.d.ts +37 -0
  220. package/dist/core/skills-manifest.js +73 -2
  221. package/dist/core/sleep-inhibitor.d.ts +79 -0
  222. package/dist/core/sleep-inhibitor.js +132 -0
  223. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  224. package/dist/core/slim-system-prompt-loader.js +141 -0
  225. package/dist/core/spawn-gates.d.ts +126 -0
  226. package/dist/core/spawn-gates.js +180 -0
  227. package/dist/core/today-direct-writer.d.ts +2 -2
  228. package/dist/core/today-direct-writer.js +1 -1
  229. package/dist/core/today-write-lock.d.ts +4 -2
  230. package/dist/core/today-write-lock.js +30 -20
  231. package/dist/core/wake-detector.d.ts +55 -0
  232. package/dist/core/wake-detector.js +80 -0
  233. package/dist/core/wiki/compile-lock.d.ts +1 -1
  234. package/dist/core/wiki/compile-lock.js +1 -1
  235. package/dist/core/workdir.js +15 -6
  236. package/dist/db/activity-scan-signals.d.ts +77 -0
  237. package/dist/db/activity-scan-signals.js +378 -0
  238. package/dist/db/agents-store.d.ts +28 -0
  239. package/dist/db/agents-store.js +62 -0
  240. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  241. package/dist/db/background-task-clarifications-store.js +152 -0
  242. package/dist/db/background-task-store.d.ts +207 -0
  243. package/dist/db/background-task-store.js +380 -0
  244. package/dist/db/browser-history-store.d.ts +39 -6
  245. package/dist/db/browser-history-store.js +51 -7
  246. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  247. package/dist/db/browser-task-clarifications-store.js +35 -5
  248. package/dist/db/browser-task-store.d.ts +3 -0
  249. package/dist/db/browser-task-store.js +29 -4
  250. package/dist/db/deferred-dm.d.ts +86 -0
  251. package/dist/db/deferred-dm.js +199 -0
  252. package/dist/db/migrations.js +330 -0
  253. package/dist/db/observations.d.ts +2 -2
  254. package/dist/db/observations.js +3 -3
  255. package/dist/db/schema.js +217 -16
  256. package/dist/db/voice-transcripts-store.d.ts +1 -1
  257. package/dist/index.js +86 -29
  258. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  259. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  260. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  261. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  262. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  263. package/dist/observers/delegated-sync-worker.js +10 -10
  264. package/dist/observers/git-delegated-cron.d.ts +1 -1
  265. package/dist/observers/git-delegated-cron.js +2 -2
  266. package/dist/observers/github-poller-classifier.d.ts +3 -3
  267. package/dist/observers/github-poller-classifier.js +3 -3
  268. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  269. package/dist/observers/imminent-event-scheduler.js +1 -1
  270. package/dist/observers/mail-poller.d.ts +1 -0
  271. package/dist/observers/mail-poller.js +42 -3
  272. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  273. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  274. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  275. package/dist/observers/observation-summarizer/worker.js +4 -4
  276. package/dist/observers/obsidian-watcher.d.ts +1 -1
  277. package/dist/observers/obsidian-watcher.js +1 -1
  278. package/dist/safety/agent-write-tracker.d.ts +4 -4
  279. package/dist/safety/agent-write-tracker.js +4 -4
  280. package/dist/safety/audit.d.ts +43 -5
  281. package/dist/safety/audit.js +86 -18
  282. package/dist/safety/risk-classifier.d.ts +6 -0
  283. package/dist/safety/risk-classifier.js +75 -11
  284. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  285. package/dist/scheduler/activity-scan-gate.js +132 -0
  286. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  287. package/dist/services/background-task/background-task-budget.js +91 -0
  288. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  289. package/dist/services/background-task/background-task-driver.js +416 -0
  290. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  291. package/dist/services/background-task/background-task-runner.js +673 -0
  292. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  293. package/dist/services/background-task/background-task-tools.js +247 -0
  294. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  295. package/dist/services/background-task/background-task-transition-events.js +54 -0
  296. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  297. package/dist/services/browser-history/automation/egress-denylist.js +16 -6
  298. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  299. package/dist/services/browser-task/browser-task-runner.js +53 -8
  300. package/dist/services/observations-batch.d.ts +1 -1
  301. package/dist/services/observations-batch.js +2 -2
  302. package/dist/settings/runtime-settings.d.ts +38 -11
  303. package/dist/settings/runtime-settings.js +203 -40
  304. package/dist/settings/settings-store.js +11 -3
  305. package/package.json +4 -4
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Self-Tuning Review Cycle — Actuate stage (SELF_TUNING_REVIEW_CYCLE_DESIGN.md
3
+ * §3.4, Phase 3).
4
+ *
5
+ * The daemon-side Actuate step ($0 — P1). Consumes `apply` verdicts the
6
+ * weekly session POSTed to `/api/tuning/verdicts` and applies them **per key
7
+ * namespace** (decision D5):
8
+ *
9
+ * - `config` knobs (R1/R3/R5) actuate through the injected
10
+ * `applyUpdates` seam — production binds it to the existing
11
+ * `applyConfigUpdates` chokepoint (`api/env-writer.ts`), which enforces
12
+ * the per-key bounds in `runtimeSettingsSchema` / `NUMERIC_RANGE` (P4 —
13
+ * bounds are enforced where they already live, never re-copied here).
14
+ * - `notification:<type>` keys (R2) never touch config — an apply verdict
15
+ * records the demotion as lesson guidance through the existing feedback
16
+ * loop (§3.2 v1 actuator is lesson-mediated; a real per-type knob is
17
+ * Phase 4).
18
+ * - `recurring_schedules:<id>` keys (R4) stay propose-only — an apply
19
+ * verdict DMs the owner the suggested `enabled=0` flip; a real flip
20
+ * needs its own audit/revert path outside `applyConfigUpdates`
21
+ * (Phase 4).
22
+ *
23
+ * Every applied config change writes the §3.4 ledger blob
24
+ * (`runtime_state.self_tuning:<key>` — the keys the Phase 1 Measure stage
25
+ * already reads and renders into `<tuning_ledger>`), an
26
+ * `agent_actions.action_type='self_tuning.applied'` audit row, and a
27
+ * one-line owner DM ("Reply `!revert tuning` to undo") — the
28
+ * Autonomous-plus-mandatory-DM pattern that replaced the abolished Notify
29
+ * tier. Reverts (manual `!revert tuning` or the auto-revert monitor) share
30
+ * {@link revertAppliedTuningChange} so the ledger stamp, audit row, and
31
+ * feedback signal are identical regardless of trigger; a reverted key gets
32
+ * the extended 28-day cool-down via the recommender's `isKeyInCooldown`.
33
+ *
34
+ * Pure-module conventions match `self-performance-prep.ts`: DB handle
35
+ * injected, `now` passed in, every side branch failure-isolated. Falls in
36
+ * the 100%-coverage set.
37
+ */
38
+ import type Database from "better-sqlite3";
39
+ import type { TuningActuator, TuningRecommendation, TuningRuleId } from "./tuning-recommender.js";
40
+ /** §3.4 — baseline/verify metric window length, same span as the Measure stage. */
41
+ export declare const TUNING_METRIC_WINDOW_DAYS = 7;
42
+ /**
43
+ * §3.4 ledger blob — `runtime_state.self_tuning:<key>`. Field names are
44
+ * load-bearing: `gatherLedger` (self-performance-prep.ts) reads `prev` /
45
+ * `applied_at` / `rule` / `baselineMetric` / `reverted_at` verbatim, and
46
+ * `isKeyInCooldown` (tuning-recommender.ts) keys the 14d/28d hysteresis off
47
+ * `applied_at` / `reverted_at`. The remaining fields are Phase 3 additions —
48
+ * unknown fields are ignored by the Phase 1/2 readers, so they are additive.
49
+ */
50
+ export interface TuningLedgerBlob {
51
+ prev: unknown;
52
+ applied_at: string;
53
+ rule: string;
54
+ /** What the apply touched — only `config` entries are revertable. */
55
+ actuator: TuningActuator;
56
+ proposed: unknown;
57
+ recommendation_id: string;
58
+ evidence: string;
59
+ /** Rule's target metric captured at apply time (D4); null when none. */
60
+ baselineMetric: unknown;
61
+ /** Stamped by the auto-revert monitor after a clean 7-day verify window. */
62
+ verified_at?: string;
63
+ verify_result?: string;
64
+ /** Present means the change regressed (or the owner undid it). */
65
+ reverted_at?: string;
66
+ revert_trigger?: "auto" | "bang_command";
67
+ revert_reason?: string;
68
+ }
69
+ export interface LedgerScanEntry {
70
+ /** Knob name / namespaced key — the runtime_state key minus the prefix. */
71
+ key: string;
72
+ blob: TuningLedgerBlob;
73
+ }
74
+ export declare function ledgerStateKey(key: string): string;
75
+ /**
76
+ * Scan the §3.4 ledger. A corrupt blob or one without a string `applied_at`
77
+ * is skipped — the actuator/monitor must never throw over a damaged ledger
78
+ * row (mirrors `gatherLedger`'s tolerance).
79
+ */
80
+ export declare function listLedgerEntries(db: Database.Database): LedgerScanEntry[];
81
+ /**
82
+ * The `!revert tuning` target: the most recently applied, not-yet-reverted
83
+ * `config` change. Lesson/schedule entries are hysteresis bookkeeping only —
84
+ * there is no machine state to restore. Already-verified entries remain
85
+ * revertable: passing the 7-day window means "no measured regression", not
86
+ * "the owner is forbidden from undoing it".
87
+ */
88
+ export declare function findLatestRevertableEntry(entries: ReadonlyArray<LedgerScanEntry>): LedgerScanEntry | null;
89
+ /** D4 — R1's target metric pair. */
90
+ export interface R1Metric {
91
+ /** Daily novelty≥2 observation arrivals (stale pre-pass suppresses these). */
92
+ noveltyGe2PerDay: number;
93
+ /** Share of audited gate ticks that took the cautious-escalate path. */
94
+ cautiousEscalateShare: number;
95
+ }
96
+ export declare function computeR1Metric(db: Database.Database, from: Date, to: Date): R1Metric;
97
+ /** D3 — R3's target metric: silenced ticks that carried real signal. */
98
+ export interface R3Metric {
99
+ stage0Ticks: number;
100
+ /** …of those, ticks whose audited snapshot had maxNoveltyScore ≥ 2. */
101
+ noveltyGe2: number;
102
+ }
103
+ export declare function computeR3Metric(db: Database.Database, from: Date, to: Date): R3Metric;
104
+ /**
105
+ * D3 (R5 arm) — the explicit-correction proxy: negative explicit /
106
+ * self_critique signals citing a lesson within the window. Excludes the
107
+ * self-tuning loop's own bookkeeping signals (verdict rejections and revert
108
+ * records mention lesson-byte knobs by name and would otherwise
109
+ * self-trigger).
110
+ */
111
+ export declare function countLessonRegressionSignals(db: Database.Database, from: Date, to: Date): number;
112
+ /**
113
+ * Capture the rule's pre-change baseline over the {@link
114
+ * TUNING_METRIC_WINDOW_DAYS} immediately before `now`. R5's verify metric is
115
+ * the correction proxy (no numeric baseline); unknown rules carry none.
116
+ */
117
+ export declare function captureBaselineMetric(db: Database.Database, rule: TuningRuleId | string, now: Date): unknown;
118
+ /**
119
+ * Best-effort `agent_actions` row. Audit failure must never fail an
120
+ * actuation that already happened — the ledger blob is the durable record.
121
+ */
122
+ export declare function auditSelfTuning(db: Database.Database, actionType: "self_tuning.applied" | "self_tuning.reverted" | "self_tuning.verified", trigger: "autonomous" | "user", result: "success" | "failed", detail: Record<string, unknown>): void;
123
+ export interface ActuatorDeps {
124
+ db: Database.Database;
125
+ /**
126
+ * Bound `applyConfigUpdates` (live config + settings store). The seam
127
+ * keeps env-writer I/O out of this module and lets tests assert the
128
+ * chokepoint contract (P4: bounds are enforced inside, not here).
129
+ */
130
+ applyUpdates: (updates: Record<string, unknown>) => Promise<{
131
+ updated: string[];
132
+ errors: Record<string, string>;
133
+ }>;
134
+ /** Live config read for the ledger's `prev` snapshot. */
135
+ getCurrentValue: (key: string) => unknown;
136
+ /** Owner DM sender. Absent in test harnesses; required for R4 applies. */
137
+ sendDm?: (message: string) => Promise<void>;
138
+ /** Mirrors `POST /api/feedback`'s kill switch for the R2 lesson signal. */
139
+ feedbackLearningEnabled?: boolean;
140
+ }
141
+ export interface AppliedChange {
142
+ id: string;
143
+ key: string;
144
+ rule: string;
145
+ mode: "config" | "lesson" | "dm_suggestion";
146
+ from?: unknown;
147
+ to?: unknown;
148
+ }
149
+ export interface ActuationFailure {
150
+ id: string;
151
+ key: string;
152
+ error: string;
153
+ }
154
+ export interface ActuationOutcome {
155
+ applied: AppliedChange[];
156
+ failures: ActuationFailure[];
157
+ }
158
+ /** §3.4 — the one-line owner DM for an applied config change. */
159
+ export declare function buildApplyDmMessage(rec: TuningRecommendation, prev: unknown): string;
160
+ /** D5 — R4 apply verdicts become an owner suggestion, never a flip. */
161
+ export declare function buildR4SuggestionDmMessage(rec: TuningRecommendation): string;
162
+ /**
163
+ * Actuate newly-recorded `apply` verdicts (D5 namespace dispatch). Each
164
+ * recommendation is processed in isolation: one failure (bounds rejection,
165
+ * missing DM path, thrown dependency) lands in `failures` and the rest
166
+ * proceed. Callers pass only verdicts recorded **this POST** — the route's
167
+ * per-id idempotency means a retried POST yields `duplicate` statuses and
168
+ * never reaches this function, so a change cannot double-apply (§3.4).
169
+ */
170
+ export declare function actuateApplyVerdicts(deps: ActuatorDeps, recommendations: ReadonlyArray<TuningRecommendation>, now: Date): Promise<ActuationOutcome>;
171
+ export interface RevertDeps {
172
+ db: Database.Database;
173
+ applyUpdates: (updates: Record<string, unknown>) => Promise<{
174
+ updated: string[];
175
+ errors: Record<string, string>;
176
+ }>;
177
+ feedbackLearningEnabled?: boolean;
178
+ }
179
+ export interface RevertOptions {
180
+ trigger: "auto" | "bang_command";
181
+ reason: string;
182
+ now: Date;
183
+ }
184
+ export type RevertResult = {
185
+ ok: true;
186
+ } | {
187
+ ok: false;
188
+ error: string;
189
+ };
190
+ /**
191
+ * Restore a config entry's `prev` value through the chokepoint, stamp
192
+ * `reverted_at` (which puts the key into the 28-day re-proposal cool-down,
193
+ * §3.4), audit `self_tuning.reverted`, and record the feedback signal that
194
+ * turns the failure into a lesson — `self_critique` for the monitor's
195
+ * measured regression, `explicit` correction when the owner typed
196
+ * `!revert tuning`.
197
+ */
198
+ export declare function revertAppliedTuningChange(deps: RevertDeps, entry: LedgerScanEntry, opts: RevertOptions): Promise<RevertResult>;
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Self-Tuning Review Cycle — Actuate stage (SELF_TUNING_REVIEW_CYCLE_DESIGN.md
3
+ * §3.4, Phase 3).
4
+ *
5
+ * The daemon-side Actuate step ($0 — P1). Consumes `apply` verdicts the
6
+ * weekly session POSTed to `/api/tuning/verdicts` and applies them **per key
7
+ * namespace** (decision D5):
8
+ *
9
+ * - `config` knobs (R1/R3/R5) actuate through the injected
10
+ * `applyUpdates` seam — production binds it to the existing
11
+ * `applyConfigUpdates` chokepoint (`api/env-writer.ts`), which enforces
12
+ * the per-key bounds in `runtimeSettingsSchema` / `NUMERIC_RANGE` (P4 —
13
+ * bounds are enforced where they already live, never re-copied here).
14
+ * - `notification:<type>` keys (R2) never touch config — an apply verdict
15
+ * records the demotion as lesson guidance through the existing feedback
16
+ * loop (§3.2 v1 actuator is lesson-mediated; a real per-type knob is
17
+ * Phase 4).
18
+ * - `recurring_schedules:<id>` keys (R4) stay propose-only — an apply
19
+ * verdict DMs the owner the suggested `enabled=0` flip; a real flip
20
+ * needs its own audit/revert path outside `applyConfigUpdates`
21
+ * (Phase 4).
22
+ *
23
+ * Every applied config change writes the §3.4 ledger blob
24
+ * (`runtime_state.self_tuning:<key>` — the keys the Phase 1 Measure stage
25
+ * already reads and renders into `<tuning_ledger>`), an
26
+ * `agent_actions.action_type='self_tuning.applied'` audit row, and a
27
+ * one-line owner DM ("Reply `!revert tuning` to undo") — the
28
+ * Autonomous-plus-mandatory-DM pattern that replaced the abolished Notify
29
+ * tier. Reverts (manual `!revert tuning` or the auto-revert monitor) share
30
+ * {@link revertAppliedTuningChange} so the ledger stamp, audit row, and
31
+ * feedback signal are identical regardless of trigger; a reverted key gets
32
+ * the extended 28-day cool-down via the recommender's `isKeyInCooldown`.
33
+ *
34
+ * Pure-module conventions match `self-performance-prep.ts`: DB handle
35
+ * injected, `now` passed in, every side branch failure-isolated. Falls in
36
+ * the 100%-coverage set.
37
+ */
38
+ import { formatSqliteDatetime } from "@aitne/shared";
39
+ import { ACTIVITY_SCAN_GATE_ACTION_TYPE, SELF_TUNING_LEDGER_PREFIX, } from "./self-performance-prep.js";
40
+ import { recordFeedbackSignal } from "../../db/feedback-signals-store.js";
41
+ import { readRuntimeState, writeRuntimeState } from "../../db/runtime-state.js";
42
+ import { createLogger } from "../../logging.js";
43
+ const logger = createLogger("tuning-actuator");
44
+ const DAY_MS = 24 * 60 * 60 * 1000;
45
+ /** §3.4 — baseline/verify metric window length, same span as the Measure stage. */
46
+ export const TUNING_METRIC_WINDOW_DAYS = 7;
47
+ export function ledgerStateKey(key) {
48
+ return `${SELF_TUNING_LEDGER_PREFIX}${key}`;
49
+ }
50
+ /** Tolerant JSON-object parse; anything malformed degrades to null. */
51
+ function parseJsonObject(raw) {
52
+ if (!raw)
53
+ return null;
54
+ try {
55
+ const parsed = JSON.parse(raw);
56
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
57
+ ? parsed
58
+ : null;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * Scan the §3.4 ledger. A corrupt blob or one without a string `applied_at`
66
+ * is skipped — the actuator/monitor must never throw over a damaged ledger
67
+ * row (mirrors `gatherLedger`'s tolerance).
68
+ */
69
+ export function listLedgerEntries(db) {
70
+ const rows = db
71
+ .prepare(`SELECT key, value_json FROM runtime_state
72
+ WHERE key LIKE ? ORDER BY key ASC`)
73
+ .all(`${SELF_TUNING_LEDGER_PREFIX}%`);
74
+ const entries = [];
75
+ for (const row of rows) {
76
+ const blob = parseJsonObject(row.value_json);
77
+ if (!blob || typeof blob.applied_at !== "string")
78
+ continue;
79
+ entries.push({
80
+ key: row.key.slice(SELF_TUNING_LEDGER_PREFIX.length),
81
+ blob: blob,
82
+ });
83
+ }
84
+ return entries;
85
+ }
86
+ /**
87
+ * The `!revert tuning` target: the most recently applied, not-yet-reverted
88
+ * `config` change. Lesson/schedule entries are hysteresis bookkeeping only —
89
+ * there is no machine state to restore. Already-verified entries remain
90
+ * revertable: passing the 7-day window means "no measured regression", not
91
+ * "the owner is forbidden from undoing it".
92
+ */
93
+ export function findLatestRevertableEntry(entries) {
94
+ const candidates = entries.filter((entry) => entry.blob.actuator === "config" &&
95
+ entry.blob.reverted_at === undefined &&
96
+ entry.blob.prev !== undefined);
97
+ if (candidates.length === 0)
98
+ return null;
99
+ return [...candidates].sort((a, b) => b.blob.applied_at.localeCompare(a.blob.applied_at))[0];
100
+ }
101
+ export function computeR1Metric(db, from, to) {
102
+ const fromUtc = formatSqliteDatetime(from);
103
+ const toUtc = formatSqliteDatetime(to);
104
+ const windowDays = Math.max((to.getTime() - from.getTime()) / DAY_MS, 1 / 24);
105
+ const arrivals = db
106
+ .prepare(`SELECT COUNT(*) AS n FROM observations
107
+ WHERE observed_at >= ? AND observed_at < ? AND novelty_score >= 2`)
108
+ .get(fromUtc, toUtc);
109
+ const gateRows = db
110
+ .prepare(`SELECT detail FROM agent_actions
111
+ WHERE action_type = ? AND started_at >= ? AND started_at < ?`)
112
+ .all(ACTIVITY_SCAN_GATE_ACTION_TYPE, fromUtc, toUtc);
113
+ let cautious = 0;
114
+ for (const row of gateRows) {
115
+ if (parseJsonObject(row.detail)?.cautious_escalate === true)
116
+ cautious += 1;
117
+ }
118
+ return {
119
+ noveltyGe2PerDay: arrivals.n / windowDays,
120
+ cautiousEscalateShare: gateRows.length > 0 ? cautious / gateRows.length : 0,
121
+ };
122
+ }
123
+ export function computeR3Metric(db, from, to) {
124
+ const rows = db
125
+ .prepare(`SELECT detail FROM agent_actions
126
+ WHERE action_type = ? AND started_at >= ? AND started_at < ?`)
127
+ .all(ACTIVITY_SCAN_GATE_ACTION_TYPE, formatSqliteDatetime(from), formatSqliteDatetime(to));
128
+ const metric = { stage0Ticks: 0, noveltyGe2: 0 };
129
+ for (const row of rows) {
130
+ const detail = parseJsonObject(row.detail);
131
+ if (detail?.stage_reached !== "stage0_silent")
132
+ continue;
133
+ metric.stage0Ticks += 1;
134
+ const snapshot = detail.signal_snapshot;
135
+ const novelty = typeof snapshot === "object" && snapshot !== null
136
+ ? snapshot.maxNoveltyScore
137
+ : null;
138
+ if (typeof novelty === "number" && novelty >= 2)
139
+ metric.noveltyGe2 += 1;
140
+ }
141
+ return metric;
142
+ }
143
+ /**
144
+ * D3 (R5 arm) — the explicit-correction proxy: negative explicit /
145
+ * self_critique signals citing a lesson within the window. Excludes the
146
+ * self-tuning loop's own bookkeeping signals (verdict rejections and revert
147
+ * records mention lesson-byte knobs by name and would otherwise
148
+ * self-trigger).
149
+ */
150
+ export function countLessonRegressionSignals(db, from, to) {
151
+ const row = db
152
+ .prepare(`SELECT COUNT(*) AS n FROM feedback_signals
153
+ WHERE created_at >= ? AND created_at < ?
154
+ AND source IN ('explicit', 'self_critique')
155
+ AND valence IN ('negative', 'correction')
156
+ AND summary LIKE '%lesson%'
157
+ AND summary NOT LIKE 'Tuning recommendation%'
158
+ AND summary NOT LIKE 'Self-tuning%'`)
159
+ .get(formatSqliteDatetime(from), formatSqliteDatetime(to));
160
+ return row.n;
161
+ }
162
+ /**
163
+ * Capture the rule's pre-change baseline over the {@link
164
+ * TUNING_METRIC_WINDOW_DAYS} immediately before `now`. R5's verify metric is
165
+ * the correction proxy (no numeric baseline); unknown rules carry none.
166
+ */
167
+ export function captureBaselineMetric(db, rule, now) {
168
+ const from = new Date(now.getTime() - TUNING_METRIC_WINDOW_DAYS * DAY_MS);
169
+ if (rule === "R1")
170
+ return computeR1Metric(db, from, now);
171
+ if (rule === "R3")
172
+ return computeR3Metric(db, from, now);
173
+ return null;
174
+ }
175
+ // ── Audit helper ────────────────────────────────────────────────────────────
176
+ /**
177
+ * Best-effort `agent_actions` row. Audit failure must never fail an
178
+ * actuation that already happened — the ledger blob is the durable record.
179
+ */
180
+ export function auditSelfTuning(db, actionType, trigger, result, detail) {
181
+ try {
182
+ db.prepare(`INSERT INTO agent_actions
183
+ (action_type, trigger, result, detail, started_at, completed_at)
184
+ VALUES (?, ?, ?, json(?), datetime('now'), datetime('now'))`).run(actionType, trigger, result, JSON.stringify(detail));
185
+ }
186
+ catch (err) {
187
+ logger.warn({ err, actionType }, "Failed to audit self-tuning action");
188
+ }
189
+ }
190
+ /** §3.4 — the one-line owner DM for an applied config change. */
191
+ export function buildApplyDmMessage(rec, prev) {
192
+ return (`Self-tuning (${rec.rule}): changed ${rec.key} ` +
193
+ `${String(prev)} → ${String(rec.proposedValue)} — ${rec.evidence}. ` +
194
+ "Reply `!revert tuning` to undo.");
195
+ }
196
+ /** D5 — R4 apply verdicts become an owner suggestion, never a flip. */
197
+ export function buildR4SuggestionDmMessage(rec) {
198
+ return (`Self-tuning suggestion (R4): ${rec.evidence} — consider disabling ` +
199
+ `${rec.key} from the dashboard schedules page. Schedules are never ` +
200
+ "disabled automatically.");
201
+ }
202
+ function writeLedgerEntry(db, rec, prev, baselineMetric, nowIso) {
203
+ const blob = {
204
+ prev,
205
+ applied_at: nowIso,
206
+ rule: rec.rule,
207
+ actuator: rec.actuator,
208
+ proposed: rec.proposedValue,
209
+ recommendation_id: rec.id,
210
+ evidence: rec.evidence,
211
+ baselineMetric,
212
+ };
213
+ writeRuntimeState(db, ledgerStateKey(rec.key), blob);
214
+ }
215
+ /**
216
+ * Actuate newly-recorded `apply` verdicts (D5 namespace dispatch). Each
217
+ * recommendation is processed in isolation: one failure (bounds rejection,
218
+ * missing DM path, thrown dependency) lands in `failures` and the rest
219
+ * proceed. Callers pass only verdicts recorded **this POST** — the route's
220
+ * per-id idempotency means a retried POST yields `duplicate` statuses and
221
+ * never reaches this function, so a change cannot double-apply (§3.4).
222
+ */
223
+ export async function actuateApplyVerdicts(deps, recommendations, now) {
224
+ const outcome = { applied: [], failures: [] };
225
+ const nowIso = now.toISOString();
226
+ for (const rec of recommendations) {
227
+ try {
228
+ if (rec.actuator === "config") {
229
+ const prev = deps.getCurrentValue(rec.key) ?? rec.currentValue;
230
+ const result = await deps.applyUpdates({ [rec.key]: rec.proposedValue });
231
+ const error = result.errors[rec.key];
232
+ if (error !== undefined || !result.updated.includes(rec.key)) {
233
+ const message = error ?? "Config chokepoint did not apply the key";
234
+ outcome.failures.push({ id: rec.id, key: rec.key, error: message });
235
+ auditSelfTuning(deps.db, "self_tuning.applied", "autonomous", "failed", {
236
+ recommendationId: rec.id,
237
+ rule: rec.rule,
238
+ key: rec.key,
239
+ proposed: rec.proposedValue,
240
+ error: message,
241
+ });
242
+ continue;
243
+ }
244
+ // Baseline capture is best-effort: a metric failure must not undo
245
+ // an apply that already landed — the monitor degrades to
246
+ // `no_baseline` for this entry.
247
+ let baselineMetric = null;
248
+ try {
249
+ baselineMetric = captureBaselineMetric(deps.db, rec.rule, now);
250
+ }
251
+ catch (err) {
252
+ logger.warn({ err, key: rec.key }, "Baseline metric capture failed");
253
+ }
254
+ writeLedgerEntry(deps.db, rec, prev, baselineMetric, nowIso);
255
+ auditSelfTuning(deps.db, "self_tuning.applied", "autonomous", "success", {
256
+ recommendationId: rec.id,
257
+ rule: rec.rule,
258
+ key: rec.key,
259
+ prev,
260
+ applied: rec.proposedValue,
261
+ evidence: rec.evidence,
262
+ });
263
+ // Mandatory owner DM (§3.4) — but a DM delivery failure cannot
264
+ // un-apply the change; the audit row + ledger remain the record.
265
+ if (deps.sendDm) {
266
+ try {
267
+ await deps.sendDm(buildApplyDmMessage(rec, prev));
268
+ }
269
+ catch (err) {
270
+ logger.warn({ err, key: rec.key }, "Self-tuning apply DM failed");
271
+ }
272
+ }
273
+ else {
274
+ logger.warn({ key: rec.key }, "Self-tuning change applied without DM path — owner not notified");
275
+ }
276
+ outcome.applied.push({
277
+ id: rec.id,
278
+ key: rec.key,
279
+ rule: rec.rule,
280
+ mode: "config",
281
+ from: prev,
282
+ to: rec.proposedValue,
283
+ });
284
+ continue;
285
+ }
286
+ if (rec.actuator === "lesson") {
287
+ // R2 — lesson-mediated (§3.2): the guidance flows through the
288
+ // existing feedback loop; no machine state changes. The ledger
289
+ // entry exists purely for the 14-day hysteresis so the same type
290
+ // is not re-proposed weekly.
291
+ if (deps.feedbackLearningEnabled !== false) {
292
+ recordFeedbackSignal(deps.db, {
293
+ source: "self_critique",
294
+ valence: "negative",
295
+ scopeType: "agent",
296
+ scopeRef: null,
297
+ actionKind: "agent_execution",
298
+ actionRef: rec.id,
299
+ agentId: null,
300
+ summary: `Demote ${rec.key}: ${rec.evidence} — batch into digests or ` +
301
+ "stay silent unless user-actionable (weekly apply verdict)",
302
+ evidence: {
303
+ kind: "do-less",
304
+ recommendationId: rec.id,
305
+ rule: rec.rule,
306
+ key: rec.key,
307
+ },
308
+ });
309
+ }
310
+ writeLedgerEntry(deps.db, rec, rec.currentValue, null, nowIso);
311
+ auditSelfTuning(deps.db, "self_tuning.applied", "autonomous", "success", {
312
+ recommendationId: rec.id,
313
+ rule: rec.rule,
314
+ key: rec.key,
315
+ mode: "lesson",
316
+ ...(deps.feedbackLearningEnabled === false
317
+ ? { note: "feedback_loop_disabled" }
318
+ : {}),
319
+ });
320
+ outcome.applied.push({
321
+ id: rec.id,
322
+ key: rec.key,
323
+ rule: rec.rule,
324
+ mode: "lesson",
325
+ });
326
+ continue;
327
+ }
328
+ // rec.actuator === "schedule" (R4) — the DM *is* the actuation
329
+ // (propose-only, D5). No DM path → the owner never saw the
330
+ // suggestion → report failure and leave the ledger unwritten so the
331
+ // rule re-proposes next cycle.
332
+ if (!deps.sendDm) {
333
+ outcome.failures.push({
334
+ id: rec.id,
335
+ key: rec.key,
336
+ error: "No DM path available for the R4 suggestion",
337
+ });
338
+ continue;
339
+ }
340
+ await deps.sendDm(buildR4SuggestionDmMessage(rec));
341
+ writeLedgerEntry(deps.db, rec, rec.currentValue, null, nowIso);
342
+ auditSelfTuning(deps.db, "self_tuning.applied", "autonomous", "success", {
343
+ recommendationId: rec.id,
344
+ rule: rec.rule,
345
+ key: rec.key,
346
+ mode: "dm_suggestion",
347
+ });
348
+ outcome.applied.push({
349
+ id: rec.id,
350
+ key: rec.key,
351
+ rule: rec.rule,
352
+ mode: "dm_suggestion",
353
+ });
354
+ }
355
+ catch (err) {
356
+ logger.warn({ err, id: rec.id }, "Tuning actuation failed");
357
+ outcome.failures.push({
358
+ id: rec.id,
359
+ key: rec.key,
360
+ error: err instanceof Error ? err.message : String(err),
361
+ });
362
+ }
363
+ }
364
+ return outcome;
365
+ }
366
+ /**
367
+ * Restore a config entry's `prev` value through the chokepoint, stamp
368
+ * `reverted_at` (which puts the key into the 28-day re-proposal cool-down,
369
+ * §3.4), audit `self_tuning.reverted`, and record the feedback signal that
370
+ * turns the failure into a lesson — `self_critique` for the monitor's
371
+ * measured regression, `explicit` correction when the owner typed
372
+ * `!revert tuning`.
373
+ */
374
+ export async function revertAppliedTuningChange(deps, entry, opts) {
375
+ const result = await deps.applyUpdates({ [entry.key]: entry.blob.prev });
376
+ const error = result.errors[entry.key];
377
+ if (error !== undefined || !result.updated.includes(entry.key)) {
378
+ const message = error ?? "Config chokepoint did not apply the revert";
379
+ auditSelfTuning(deps.db, "self_tuning.reverted", opts.trigger === "auto" ? "autonomous" : "user", "failed", {
380
+ key: entry.key,
381
+ rule: entry.blob.rule,
382
+ restored: entry.blob.prev,
383
+ trigger: opts.trigger,
384
+ reason: opts.reason,
385
+ error: message,
386
+ });
387
+ return { ok: false, error: message };
388
+ }
389
+ const nowIso = opts.now.toISOString();
390
+ // Re-read so a concurrent stamp (e.g. verified_at) is not clobbered;
391
+ // fall back to the scanned blob when the row vanished mid-flight.
392
+ const current = readRuntimeState(deps.db, ledgerStateKey(entry.key)) ??
393
+ entry.blob;
394
+ writeRuntimeState(deps.db, ledgerStateKey(entry.key), {
395
+ ...current,
396
+ reverted_at: nowIso,
397
+ revert_trigger: opts.trigger,
398
+ revert_reason: opts.reason,
399
+ });
400
+ auditSelfTuning(deps.db, "self_tuning.reverted", opts.trigger === "auto" ? "autonomous" : "user", "success", {
401
+ key: entry.key,
402
+ rule: entry.blob.rule,
403
+ restored: entry.blob.prev,
404
+ trigger: opts.trigger,
405
+ reason: opts.reason,
406
+ });
407
+ if (deps.feedbackLearningEnabled !== false) {
408
+ try {
409
+ recordFeedbackSignal(deps.db, {
410
+ source: opts.trigger === "auto" ? "self_critique" : "explicit",
411
+ valence: opts.trigger === "auto" ? "negative" : "correction",
412
+ scopeType: "agent",
413
+ scopeRef: null,
414
+ actionKind: "agent_execution",
415
+ actionRef: entry.blob.recommendation_id ?? entry.key,
416
+ agentId: null,
417
+ summary: `Self-tuning change ${entry.key} (${entry.blob.rule}) reverted ` +
418
+ `(${opts.trigger}): ${opts.reason}`,
419
+ evidence: {
420
+ kind: "revert",
421
+ rule: entry.blob.rule,
422
+ key: entry.key,
423
+ trigger: opts.trigger,
424
+ },
425
+ });
426
+ }
427
+ catch (err) {
428
+ logger.warn({ err, key: entry.key }, "Failed to record revert signal");
429
+ }
430
+ }
431
+ return { ok: true };
432
+ }