@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
@@ -11,28 +11,28 @@ function getLocalHourMinute(date, timeZone) {
11
11
  const minute = Number(parts.find((part) => part.type === "minute")?.value ?? "0");
12
12
  return { hour, minute };
13
13
  }
14
- function isHourlyCheckSlot(date, config) {
15
- if (!config.hourlyCheckEnabled)
14
+ function isActivityScanSlot(date, config) {
15
+ if (!config.activityScanEnabled)
16
16
  return false;
17
17
  const { hour, minute } = getLocalHourMinute(date, config.timezone || undefined);
18
18
  if (hour === config.dayBoundaryHour)
19
19
  return false;
20
- if (hour < config.hourlyCheckActiveStartHour || hour >= config.hourlyCheckActiveEndHour) {
20
+ if (hour < config.activityScanActiveStartHour || hour >= config.activityScanActiveEndHour) {
21
21
  return false;
22
22
  }
23
- return minute % config.hourlyCheckIntervalMinutes === 0;
23
+ return minute % config.activityScanIntervalMinutes === 0;
24
24
  }
25
- function getNextHourlyCheck(config) {
26
- if (!config.hourlyCheckEnabled) {
25
+ function getNextActivityScan(config) {
26
+ if (!config.activityScanEnabled) {
27
27
  return { active: false, nextRunAt: null };
28
28
  }
29
29
  const now = new Date();
30
- const active = isHourlyCheckSlot(now, config);
30
+ const active = isActivityScanSlot(now, config);
31
31
  const start = new Date(now.getTime() + 60_000);
32
32
  start.setSeconds(0, 0);
33
33
  for (let offset = 0; offset < 48 * 60; offset++) {
34
34
  const candidate = new Date(start.getTime() + offset * 60_000);
35
- if (isHourlyCheckSlot(candidate, config)) {
35
+ if (isActivityScanSlot(candidate, config)) {
36
36
  return { active, nextRunAt: candidate.toISOString() };
37
37
  }
38
38
  }
@@ -41,7 +41,7 @@ function getNextHourlyCheck(config) {
41
41
  export function registerNotificationsRoutes(app, deps) {
42
42
  const { db, config } = deps;
43
43
  app.get("/dashboard/next-check", (c) => {
44
- return c.json(getNextHourlyCheck(config));
44
+ return c.json(getNextActivityScan(config));
45
45
  });
46
46
  // STAGE-C-DM-FRESHNESS-PLAN §Task 4 — DM freshness aggregate. Powered
47
47
  // by `agent_actions.detail.dm_freshness.*` rows the DM dispatch path
@@ -368,6 +368,9 @@ export async function handleIntegrationPatch(c, deps) {
368
368
  const finalNativeSyncEnabled = parsed.data.nativeSyncEnabled === undefined
369
369
  ? previous.nativeSyncEnabled
370
370
  : parsed.data.nativeSyncEnabled;
371
+ const finalFetchTargets = parsed.data.fetchTargets === undefined
372
+ ? (previous.fetchTargets ?? [])
373
+ : parsed.data.fetchTargets;
371
374
  // §14.7 — synchronously consult the cached probe before committing a
372
375
  // mode flip to delegated/native. Per §14.7 the PATCH response path
373
376
  // intentionally never spawns a live probe ("no blocking subprocess");
@@ -470,6 +473,7 @@ export async function handleIntegrationPatch(c, deps) {
470
473
  ...(finalNativeSyncEnabled === false
471
474
  ? { nativeSyncEnabled: false }
472
475
  : {}),
476
+ fetchTargets: finalFetchTargets,
473
477
  deniedTools: finalDeniedTools,
474
478
  lastChangedAt: stamped,
475
479
  });
@@ -501,7 +505,7 @@ export async function handleIntegrationPatch(c, deps) {
501
505
  // re-evaluation — the predicate
502
506
  // (`hasActiveDelegatedSyncIntegration`) ignores `nativeSyncEnabled`
503
507
  // because the worker has no role in native mode (see appendix
504
- // §"Polling, observers, and the hourly-check threshold"). The
508
+ // §"Polling, observers, and the activity-scan threshold"). The
505
509
  // `nativeSyncEnabled` field is retained on the state row for
506
510
  // schema compatibility but toggling it is inert today.
507
511
  const syncChanged = (previous.delegatedSyncEnabled ?? true)
@@ -174,7 +174,7 @@ export function createIntegrationReconcileRoutes(deps) {
174
174
  * callers bypass the route and write any window key directly. Plan §6.0
175
175
  * defense layer 2.
176
176
  *
177
- * Calendar-only by design (post-Phase-5 review): the LLM hourly_check
177
+ * Calendar-only by design (post-Phase-5 review): the LLM activity_scan
178
178
  * Step 0b fetches the same `[now-15min, now+60min)` and `[now, now+24h)`
179
179
  * windows the daemon's `delegated-sync-worker` uses for `primary:imminent`
180
180
  * and `primary:24h`, so an LLM POST and a worker POST land in the same
@@ -188,7 +188,7 @@ export function createIntegrationReconcileRoutes(deps) {
188
188
  * `reconcile.ts:319-345`). The fix is to keep gmail/notion authoring
189
189
  * inside the daemon worker only; the LLM consumes drift signals via
190
190
  * `GET /api/observations`. The corresponding Step 0a / 0c blocks of
191
- * `routine.hourly_check.delegated.<backend>.md` were rewritten to
191
+ * `routine.activity_scan.delegated.<backend>.md` were rewritten to
192
192
  * forbid the POST and document the rationale; this allowlist is the
193
193
  * defense-in-depth backstop that catches a future overlay rewrite that
194
194
  * silently re-introduces the LLM call.
@@ -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
+ }