@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,541 @@
1
+ /**
2
+ * Self-Tuning Review Cycle — Measure stage (SELF_TUNING_REVIEW_CYCLE_DESIGN.md
3
+ * §3.1, Phase 1).
4
+ *
5
+ * The daemon-side, deterministic Measure step ($0 — LLM tokens buy judgment
6
+ * only, P1). On the weekly-review dispatch it computes SQL aggregates over
7
+ * `agent_actions`, `notification_log`, and the `runtime_state` self-tuning
8
+ * ledger for a 7-day window plus a 7-day-prior baseline (trend column), and
9
+ * renders one compact `<self_performance>` block — hard-capped at
10
+ * {@link SELF_PERFORMANCE_MAX_BYTES} — so the weekly review's "Metrics (agent
11
+ * side)" section copies daemon-computed facts instead of paying Sonnet prices
12
+ * to re-count them.
13
+ *
14
+ * Two layers, mirroring `consolidation-prep.ts`:
15
+ * - {@link gatherSelfPerformanceData} — the single DB read (side-effect
16
+ * free): per-`action_type` run/cost/duration aggregates (`agent_actions`
17
+ * has no process_key column; `action_type` carries the routine identity),
18
+ * the `routine.fetch_window` empty-run rate per integration (from the
19
+ * fan-out audit rows' `detail.prePass` payload the runner persists), the
20
+ * `activity_scan.gate` stage distribution (from `buildGateAuditDetail`'s
21
+ * historical per-tick rows), per-notification-type `user_reaction`
22
+ * breakdowns (the first reader of the column `signal-detector.ts`
23
+ * populates), and the `runtime_state.self_tuning:*` ledger.
24
+ * - {@link buildSelfPerformanceBlock} — pure renderer. Deterministic
25
+ * byte-capped output: per-section row budgets shrink one row at a time
26
+ * (largest section first) until the block fits the cap, and clipped rows
27
+ * surface as `omitted="N"` so truncation is never silent.
28
+ *
29
+ * Phase 1 carries no actuator: nothing here writes config, schedules, or
30
+ * lessons. Later phases (Recommend / Judge / Actuate) consume the same data
31
+ * shape; {@link SELF_TUNING_LEDGER_PREFIX} is exported so the Phase 3
32
+ * actuator writes the ledger keys this module already reads.
33
+ */
34
+ import { formatSqliteDatetime } from "@aitne/shared";
35
+ import { extractMarkdownSection, parseLessonsSection, } from "./lesson-format.js";
36
+ /** Measurement window length; the baseline is the same span immediately prior. */
37
+ export const SELF_PERFORMANCE_WINDOW_DAYS = 7;
38
+ /** §3.1 — hard cap on the rendered `<self_performance>` block, in UTF-8 bytes. */
39
+ export const SELF_PERFORMANCE_MAX_BYTES = 1500;
40
+ /**
41
+ * §3.4 ledger key prefix. Phase 3's actuator writes
42
+ * `runtime_state.self_tuning:<key> = {prev, applied_at, baselineMetric, rule}`;
43
+ * Phase 1 already reads (and renders) whatever sits under the prefix so the
44
+ * weekly review sees applied changes the cycle they land.
45
+ */
46
+ export const SELF_TUNING_LEDGER_PREFIX = "self_tuning:";
47
+ /** Fan-out audit rows carry the fetcher event's type as `action_type`. */
48
+ export const FETCH_WINDOW_ACTION_TYPE = "routine.fetch_window";
49
+ /** Per-tick gate audit rows (`buildGateAuditDetail` payload in `detail`). */
50
+ export const ACTIVITY_SCAN_GATE_ACTION_TYPE = "activity_scan.gate";
51
+ const DAY_MS = 24 * 60 * 60 * 1000;
52
+ const SENT_STATUSES = new Set(["delivered", "batched"]);
53
+ const COMPLETED_PREPASS_STATUSES = new Set(["success", "partial"]);
54
+ const UNTYPED_NOTIFICATION = "(untyped)";
55
+ /** Tolerant JSON parse — a corrupt detail blob degrades to "no data", never a throw. */
56
+ function parseJson(raw) {
57
+ if (!raw)
58
+ return null;
59
+ try {
60
+ const parsed = JSON.parse(raw);
61
+ return typeof parsed === "object" && parsed !== null
62
+ ? parsed
63
+ : null;
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ /** Median over a pre-sorted array; averages the two middle values on even counts. */
70
+ function median(sortedAscending) {
71
+ const n = sortedAscending.length;
72
+ if (n === 0)
73
+ return null;
74
+ const mid = Math.floor(n / 2);
75
+ return n % 2 === 1
76
+ ? sortedAscending[mid]
77
+ : (sortedAscending[mid - 1] + sortedAscending[mid]) / 2;
78
+ }
79
+ function gatherActions(db, fromUtc, toUtc) {
80
+ const rows = db
81
+ .prepare(`SELECT action_type AS actionType, result,
82
+ cost_usd AS costUsd, duration_ms AS durationMs
83
+ FROM agent_actions
84
+ WHERE started_at >= ? AND started_at < ?`)
85
+ .all(fromUtc, toUtc);
86
+ const byType = new Map();
87
+ for (const row of rows) {
88
+ let agg = byType.get(row.actionType);
89
+ if (!agg) {
90
+ agg = {
91
+ actionType: row.actionType,
92
+ runs: 0,
93
+ success: 0,
94
+ partial: 0,
95
+ failed: 0,
96
+ skipped: 0,
97
+ costUsd: 0,
98
+ durations: [],
99
+ };
100
+ byType.set(row.actionType, agg);
101
+ }
102
+ agg.runs += 1;
103
+ // `in_progress` (transient) and NULL results count toward runs only.
104
+ if (row.result === "success")
105
+ agg.success += 1;
106
+ else if (row.result === "partial")
107
+ agg.partial += 1;
108
+ else if (row.result === "failed")
109
+ agg.failed += 1;
110
+ else if (row.result === "skipped")
111
+ agg.skipped += 1;
112
+ if (typeof row.costUsd === "number")
113
+ agg.costUsd += row.costUsd;
114
+ if (typeof row.durationMs === "number")
115
+ agg.durations.push(row.durationMs);
116
+ }
117
+ return [...byType.values()].map(({ durations, ...rest }) => {
118
+ const p50 = median([...durations].sort((a, b) => a - b));
119
+ return {
120
+ ...rest,
121
+ p50DurationMs: p50 === null ? null : Math.round(p50),
122
+ };
123
+ });
124
+ }
125
+ function gatherFetchWindow(db, fromUtc, toUtc) {
126
+ const rows = db
127
+ .prepare(`SELECT detail FROM agent_actions
128
+ WHERE action_type = ? AND started_at >= ? AND started_at < ?
129
+ AND detail IS NOT NULL`)
130
+ .all(FETCH_WINDOW_ACTION_TYPE, fromUtc, toUtc);
131
+ const byKey = new Map();
132
+ for (const row of rows) {
133
+ const prePass = parseJson(row.detail)?.prePass;
134
+ if (typeof prePass !== "object" || prePass === null)
135
+ continue;
136
+ const integrationKey = prePass.integrationKey;
137
+ const status = prePass.status;
138
+ if (typeof integrationKey !== "string")
139
+ continue;
140
+ if (typeof status !== "string" || !COMPLETED_PREPASS_STATUSES.has(status)) {
141
+ continue;
142
+ }
143
+ const fetched = typeof prePass.fetched === "number" ? prePass.fetched : 0;
144
+ const posted = typeof prePass.posted === "number" ? prePass.posted : 0;
145
+ let agg = byKey.get(integrationKey);
146
+ if (!agg) {
147
+ agg = { integrationKey, runs: 0, empty: 0 };
148
+ byKey.set(integrationKey, agg);
149
+ }
150
+ agg.runs += 1;
151
+ if (fetched === 0 && posted === 0)
152
+ agg.empty += 1;
153
+ }
154
+ return [...byKey.values()];
155
+ }
156
+ function gatherGate(db, fromUtc, toUtc) {
157
+ const rows = db
158
+ .prepare(`SELECT detail, result FROM agent_actions
159
+ WHERE action_type = ? AND started_at >= ? AND started_at < ?`)
160
+ .all(ACTIVITY_SCAN_GATE_ACTION_TYPE, fromUtc, toUtc);
161
+ const stats = {
162
+ ticks: rows.length,
163
+ stage0: 0,
164
+ stage2: 0,
165
+ stage3: 0,
166
+ stage3LowSignal: 0,
167
+ stage3LowSignalLowNovelty: 0,
168
+ };
169
+ for (const row of rows) {
170
+ const detail = parseJson(row.detail);
171
+ if (!detail)
172
+ continue; // corrupt/absent detail still counts as a tick
173
+ const stage = detail.stage_reached;
174
+ if (stage === "stage0_silent") {
175
+ stats.stage0 += 1;
176
+ continue;
177
+ }
178
+ // The writer never emits a bare "stage2": a Stage-2 tick settles to
179
+ // either the silent alias "stage2_log_only" or an escalated "stage3"
180
+ // (see HourlyGateStats.stage2). Accept both spellings defensively.
181
+ if (stage === "stage2" || stage === "stage2_log_only") {
182
+ stats.stage2 += 1;
183
+ continue;
184
+ }
185
+ if (stage !== "stage3")
186
+ continue;
187
+ // Min-observations-floor short-circuit: the gate verdict said stage3
188
+ // but no session ran (`resultOverride: "skipped"` in
189
+ // dispatcher-activity-scan.ts). Count as a tick only.
190
+ if (row.result === "skipped")
191
+ continue;
192
+ stats.stage3 += 1;
193
+ // R3 waste evidence — autonomous low-signal-fallback escalations only.
194
+ if (detail.gate_reason !== "low_signal_default" || detail.forced === true) {
195
+ continue;
196
+ }
197
+ stats.stage3LowSignal += 1;
198
+ const snapshot = detail.signal_snapshot;
199
+ const novelty = typeof snapshot === "object" && snapshot !== null
200
+ ? snapshot.maxNoveltyScore
201
+ : null;
202
+ // A null / missing novelty means nothing scored above the floor —
203
+ // that is the "≤ 1" waste case R3 measures, so it counts.
204
+ if (typeof novelty !== "number" || novelty <= 1) {
205
+ stats.stage3LowSignalLowNovelty += 1;
206
+ }
207
+ }
208
+ return stats;
209
+ }
210
+ function gatherNotifications(db, fromUtc, toUtc) {
211
+ const rows = db
212
+ .prepare(`SELECT notification_type AS notificationType,
213
+ user_reaction AS userReaction, status
214
+ FROM notification_log
215
+ WHERE created_at >= ? AND created_at < ?`)
216
+ .all(fromUtc, toUtc);
217
+ const byType = new Map();
218
+ for (const row of rows) {
219
+ if (row.status === null || !SENT_STATUSES.has(row.status))
220
+ continue;
221
+ const name = row.notificationType ?? UNTYPED_NOTIFICATION;
222
+ let agg = byType.get(name);
223
+ if (!agg) {
224
+ agg = {
225
+ notificationType: name,
226
+ sent: 0,
227
+ replied: 0,
228
+ acted: 0,
229
+ corrected: 0,
230
+ ignored: 0,
231
+ pending: 0,
232
+ };
233
+ byType.set(name, agg);
234
+ }
235
+ agg.sent += 1;
236
+ if (row.userReaction === "replied")
237
+ agg.replied += 1;
238
+ else if (row.userReaction === "acted")
239
+ agg.acted += 1;
240
+ else if (row.userReaction === "corrected")
241
+ agg.corrected += 1;
242
+ else if (row.userReaction === "ignored")
243
+ agg.ignored += 1;
244
+ }
245
+ for (const agg of byType.values()) {
246
+ // Unrecognised reaction values fall through to "no reaction yet".
247
+ agg.pending = Math.max(0, agg.sent - agg.replied - agg.acted - agg.corrected - agg.ignored);
248
+ }
249
+ return [...byType.values()];
250
+ }
251
+ function gatherLedger(db) {
252
+ const rows = db
253
+ .prepare(`SELECT key, value_json FROM runtime_state
254
+ WHERE key LIKE ? ORDER BY updated_at DESC, key ASC`)
255
+ .all(`${SELF_TUNING_LEDGER_PREFIX}%`);
256
+ const entries = [];
257
+ for (const row of rows) {
258
+ const value = parseJson(row.value_json);
259
+ if (!value)
260
+ continue; // a corrupt ledger blob never breaks the measure pass
261
+ entries.push({
262
+ key: row.key.slice(SELF_TUNING_LEDGER_PREFIX.length),
263
+ prev: value.prev,
264
+ appliedAt: typeof value.applied_at === "string" ? value.applied_at : null,
265
+ rule: typeof value.rule === "string" ? value.rule : null,
266
+ baselineMetric: value.baselineMetric,
267
+ ...(typeof value.reverted_at === "string"
268
+ ? { revertedAt: value.reverted_at }
269
+ : {}),
270
+ ...(typeof value.verify_result === "string"
271
+ ? { verifyResult: value.verify_result }
272
+ : {}),
273
+ });
274
+ }
275
+ return entries;
276
+ }
277
+ function gatherWindow(db, fromUtc, toUtc) {
278
+ return {
279
+ actions: gatherActions(db, fromUtc, toUtc),
280
+ fetchWindow: gatherFetchWindow(db, fromUtc, toUtc),
281
+ gate: gatherGate(db, fromUtc, toUtc),
282
+ notifications: gatherNotifications(db, fromUtc, toUtc),
283
+ };
284
+ }
285
+ /**
286
+ * The single DB read. Computes the current window `[now − windowDays, now)`
287
+ * and the baseline window `[now − 2·windowDays, now − windowDays)` over
288
+ * `agent_actions` / `notification_log` (both store SQLite UTC
289
+ * `YYYY-MM-DD HH:MM:SS` timestamps, so lexicographic comparison against
290
+ * `formatSqliteDatetime` cutoffs is exact), plus the self-tuning ledger.
291
+ */
292
+ export function gatherSelfPerformanceData(db, opts) {
293
+ const windowDays = opts.windowDays ?? SELF_PERFORMANCE_WINDOW_DAYS;
294
+ const end = formatSqliteDatetime(opts.now);
295
+ const mid = formatSqliteDatetime(new Date(opts.now.getTime() - windowDays * DAY_MS));
296
+ const start = formatSqliteDatetime(new Date(opts.now.getTime() - 2 * windowDays * DAY_MS));
297
+ return {
298
+ windowDays,
299
+ current: gatherWindow(db, mid, end),
300
+ baseline: gatherWindow(db, start, mid),
301
+ ledger: gatherLedger(db),
302
+ };
303
+ }
304
+ /**
305
+ * §3.5 — summarise one lesson store's byte pressure from its raw file
306
+ * contents. Pure (the caller does the FS read); a file with no `## Lessons`
307
+ * section degrades to an empty store, never a throw. `medianEv` carries the
308
+ * evidence signal R5's Phase 2 rule keys on (utilization > 90% with median
309
+ * evidence ≤ 1), so the measurement lands one phase ahead of the rule.
310
+ */
311
+ export function summarizeLessonStoreUtilization(scope, fileMd, capBytes) {
312
+ const sectionBody = extractMarkdownSection(fileMd, "Lessons");
313
+ const lessons = sectionBody ? parseLessonsSection(sectionBody) : [];
314
+ const bytes = sectionBody ? Buffer.byteLength(sectionBody, "utf-8") : 0;
315
+ const medianEv = median(lessons.map((lesson) => lesson.ev).sort((a, b) => a - b));
316
+ return { scope, bytes, capBytes, entries: lessons.length, medianEv };
317
+ }
318
+ // ── Renderer ────────────────────────────────────────────────────────────────
319
+ function xmlEscape(value) {
320
+ return value
321
+ .replace(/&/g, "&amp;")
322
+ .replace(/</g, "&lt;")
323
+ .replace(/>/g, "&gt;")
324
+ .replace(/"/g, "&quot;");
325
+ }
326
+ /** Compact USD: at most 4 decimal places, no trailing zeros. */
327
+ function usd(value) {
328
+ return String(Math.round(value * 10000) / 10000);
329
+ }
330
+ /** Integer percent; callers guarantee `denominator > 0`. */
331
+ function pct(numerator, denominator) {
332
+ return `${Math.round((100 * numerator) / denominator)}%`;
333
+ }
334
+ /** One-line inline value for a ledger `prev=` attribute. */
335
+ function inlineValue(value) {
336
+ // JSON.stringify returns runtime `undefined` for `undefined` input even
337
+ // though its declared type is `string` — normalise to "null".
338
+ const json = JSON.stringify(value);
339
+ const raw = typeof value === "string" ? value : json === undefined ? "null" : json;
340
+ return xmlEscape(raw.length > 60 ? `${raw.slice(0, 59)}…` : raw);
341
+ }
342
+ const INITIAL_BUDGET = {
343
+ actions: 8,
344
+ fetchWindow: 8,
345
+ notifications: 6,
346
+ lessonStores: 6,
347
+ ledger: 3,
348
+ };
349
+ /** Drop order when two sections render the same row count (ties only). */
350
+ const SHRINK_TIE_ORDER = [
351
+ "notifications",
352
+ "lessonStores",
353
+ "actions",
354
+ "fetchWindow",
355
+ "ledger",
356
+ ];
357
+ function findActionBaseline(baseline, actionType) {
358
+ return baseline.actions.find((entry) => entry.actionType === actionType);
359
+ }
360
+ function renderBlock(data, lessonStores, budget, generatedAt) {
361
+ const { current, baseline, ledger } = data;
362
+ const out = [];
363
+ out.push(`<self_performance generated_at="${xmlEscape(generatedAt)}" ` +
364
+ `window_days="${data.windowDays}" baseline="prior_${data.windowDays}d">`);
365
+ // Totals span ALL rows — they are what the weekly task-flow's
366
+ // "Metrics (agent side)" lines copy, so they must not depend on which
367
+ // per-type rows survived the byte budget below.
368
+ const sum = (window) => window.actions.reduce((acc, a) => ({
369
+ runs: acc.runs + a.runs,
370
+ failed: acc.failed + a.failed,
371
+ cost: acc.cost + a.costUsd,
372
+ }), { runs: 0, failed: 0, cost: 0 });
373
+ const cur = sum(current);
374
+ const prev = sum(baseline);
375
+ const notifSent = current.notifications.reduce((n, t) => n + t.sent, 0);
376
+ const notifIgnored = current.notifications.reduce((n, t) => n + t.ignored, 0);
377
+ const prevNotifSent = baseline.notifications.reduce((n, t) => n + t.sent, 0);
378
+ out.push(` <totals runs="${cur.runs}" failed="${cur.failed}" ` +
379
+ `cost_usd="${usd(cur.cost)}" prev_runs="${prev.runs}" ` +
380
+ `prev_failed="${prev.failed}" prev_cost_usd="${usd(prev.cost)}" ` +
381
+ `notif_sent="${notifSent}" notif_ignored="${notifIgnored}" ` +
382
+ `prev_notif_sent="${prevNotifSent}" />`);
383
+ const actions = [...current.actions].sort((a, b) => b.costUsd - a.costUsd ||
384
+ b.runs - a.runs ||
385
+ a.actionType.localeCompare(b.actionType));
386
+ const shownActions = actions.slice(0, budget.actions);
387
+ if (shownActions.length > 0) {
388
+ const omitted = actions.length - shownActions.length;
389
+ out.push(` <actions ranked="cost_desc"${omitted > 0 ? ` omitted="${omitted}"` : ""}>`);
390
+ for (const action of shownActions) {
391
+ const prevAction = findActionBaseline(baseline, action.actionType);
392
+ out.push(` <a t="${xmlEscape(action.actionType)}" runs="${action.runs}" ` +
393
+ `ok="${action.success}"` +
394
+ (action.partial > 0 ? ` part="${action.partial}"` : "") +
395
+ ` fail="${action.failed}" skip="${action.skipped}" ` +
396
+ `cost_usd="${usd(action.costUsd)}"` +
397
+ (action.p50DurationMs !== null
398
+ ? ` p50_ms="${action.p50DurationMs}"`
399
+ : "") +
400
+ ` prev_runs="${prevAction?.runs ?? 0}" ` +
401
+ `prev_cost_usd="${usd(prevAction?.costUsd ?? 0)}" />`);
402
+ }
403
+ out.push(" </actions>");
404
+ }
405
+ const integrations = [...current.fetchWindow].sort((a, b) => b.runs - a.runs || a.integrationKey.localeCompare(b.integrationKey));
406
+ const shownIntegrations = integrations.slice(0, budget.fetchWindow);
407
+ if (shownIntegrations.length > 0) {
408
+ const omitted = integrations.length - shownIntegrations.length;
409
+ out.push(` <fetch_window_empty note="empty = completed pre-pass run, nothing fetched/posted"` +
410
+ `${omitted > 0 ? ` omitted="${omitted}"` : ""}>`);
411
+ for (const integration of shownIntegrations) {
412
+ const prevIntegration = baseline.fetchWindow.find((entry) => entry.integrationKey === integration.integrationKey);
413
+ out.push(` <i k="${xmlEscape(integration.integrationKey)}" ` +
414
+ `runs="${integration.runs}" empty="${integration.empty}" ` +
415
+ `rate="${pct(integration.empty, integration.runs)}"` +
416
+ (prevIntegration && prevIntegration.runs > 0
417
+ ? ` prev_rate="${pct(prevIntegration.empty, prevIntegration.runs)}"`
418
+ : "") +
419
+ " />");
420
+ }
421
+ out.push(" </fetch_window_empty>");
422
+ }
423
+ if (current.gate.ticks > 0 || baseline.gate.ticks > 0) {
424
+ out.push(` <hourly_gate ticks="${current.gate.ticks}" ` +
425
+ `stage0="${current.gate.stage0}" stage2="${current.gate.stage2}" ` +
426
+ `stage3="${current.gate.stage3}" ` +
427
+ `stage3_low_signal="${current.gate.stage3LowSignal}" ` +
428
+ `stage3_low_signal_novelty_le1="${current.gate.stage3LowSignalLowNovelty}" ` +
429
+ `prev_ticks="${baseline.gate.ticks}" ` +
430
+ `prev_stage3="${baseline.gate.stage3}" />`);
431
+ }
432
+ const notifications = [...current.notifications].sort((a, b) => b.sent - a.sent || a.notificationType.localeCompare(b.notificationType));
433
+ const shownNotifications = notifications.slice(0, budget.notifications);
434
+ if (shownNotifications.length > 0) {
435
+ const omitted = notifications.length - shownNotifications.length;
436
+ out.push(` <notifications${omitted > 0 ? ` omitted="${omitted}"` : ""}>`);
437
+ for (const type of shownNotifications) {
438
+ out.push(` <n t="${xmlEscape(type.notificationType)}" sent="${type.sent}" ` +
439
+ `replied="${type.replied}" acted="${type.acted}" ` +
440
+ `corrected="${type.corrected}" ignored="${type.ignored}" ` +
441
+ `pending="${type.pending}" />`);
442
+ }
443
+ out.push(" </notifications>");
444
+ }
445
+ const stores = [...lessonStores].sort((a, b) => (b.capBytes > 0 ? b.bytes / b.capBytes : 0) -
446
+ (a.capBytes > 0 ? a.bytes / a.capBytes : 0) ||
447
+ a.scope.localeCompare(b.scope));
448
+ const shownStores = stores.slice(0, budget.lessonStores);
449
+ if (shownStores.length > 0) {
450
+ const omitted = stores.length - shownStores.length;
451
+ out.push(` <lesson_stores${omitted > 0 ? ` omitted="${omitted}"` : ""}>`);
452
+ for (const store of shownStores) {
453
+ out.push(` <s scope="${xmlEscape(store.scope)}" bytes="${store.bytes}" ` +
454
+ `cap="${store.capBytes}"` +
455
+ (store.capBytes > 0
456
+ ? ` util="${pct(store.bytes, store.capBytes)}"`
457
+ : "") +
458
+ ` entries="${store.entries}"` +
459
+ (store.medianEv !== null ? ` median_ev="${store.medianEv}"` : "") +
460
+ " />");
461
+ }
462
+ out.push(" </lesson_stores>");
463
+ }
464
+ const shownLedger = ledger.slice(0, budget.ledger);
465
+ if (shownLedger.length > 0) {
466
+ const omitted = ledger.length - shownLedger.length;
467
+ out.push(` <tuning_ledger${omitted > 0 ? ` omitted="${omitted}"` : ""}>`);
468
+ for (const entry of shownLedger) {
469
+ out.push(` <c key="${xmlEscape(entry.key)}" prev="${inlineValue(entry.prev)}"` +
470
+ (entry.appliedAt
471
+ ? ` applied_at="${xmlEscape(entry.appliedAt)}"`
472
+ : "") +
473
+ (entry.rule ? ` rule="${xmlEscape(entry.rule)}"` : "") +
474
+ (entry.baselineMetric !== undefined && entry.baselineMetric !== null
475
+ ? ` baseline="${inlineValue(entry.baselineMetric)}"`
476
+ : "") +
477
+ // §3.1 "measured effect" — the judge must see that a change was
478
+ // rolled back (key now in the 28d cool-down) or verified clean,
479
+ // not just that it was applied.
480
+ (entry.revertedAt
481
+ ? ` reverted_at="${xmlEscape(entry.revertedAt)}"`
482
+ : "") +
483
+ (entry.verifyResult
484
+ ? ` verified="${xmlEscape(entry.verifyResult)}"`
485
+ : "") +
486
+ " />");
487
+ }
488
+ out.push(" </tuning_ledger>");
489
+ }
490
+ out.push("</self_performance>");
491
+ return out.join("\n");
492
+ }
493
+ /**
494
+ * Compose the `<self_performance>` block. Returns `null` when there is no
495
+ * telemetry at all (fresh install) so the caller stamps nothing — no empty
496
+ * block in the prompt. The byte cap is a hard guarantee: row budgets shrink
497
+ * until the block fits; in the (synthetic) case where even the skeleton
498
+ * exceeds the cap, a minimal self-closing element is emitted instead.
499
+ */
500
+ export function buildSelfPerformanceBlock(data, opts) {
501
+ const hasData = data.current.actions.length > 0 ||
502
+ data.baseline.actions.length > 0 ||
503
+ data.current.notifications.length > 0 ||
504
+ data.baseline.notifications.length > 0 ||
505
+ data.current.gate.ticks > 0 ||
506
+ data.baseline.gate.ticks > 0 ||
507
+ data.ledger.length > 0;
508
+ if (!hasData)
509
+ return null;
510
+ const lessonStores = opts.lessonStores ?? [];
511
+ const maxBytes = opts.maxBytes ?? SELF_PERFORMANCE_MAX_BYTES;
512
+ const budget = { ...INITIAL_BUDGET };
513
+ const available = {
514
+ actions: data.current.actions.length,
515
+ fetchWindow: data.current.fetchWindow.length,
516
+ notifications: data.current.notifications.length,
517
+ lessonStores: lessonStores.length,
518
+ ledger: data.ledger.length,
519
+ };
520
+ const effectiveRows = (key) => Math.min(budget[key], available[key]);
521
+ let block = renderBlock(data, lessonStores, budget, opts.generatedAt);
522
+ while (Buffer.byteLength(block, "utf-8") > maxBytes) {
523
+ let target = null;
524
+ for (const key of SHRINK_TIE_ORDER) {
525
+ if (effectiveRows(key) === 0)
526
+ continue;
527
+ if (target === null || effectiveRows(key) > effectiveRows(target)) {
528
+ target = key;
529
+ }
530
+ }
531
+ if (target === null) {
532
+ // Even the row-free skeleton exceeds the cap (only reachable with a
533
+ // tiny custom maxBytes) — degrade to the minimal stub element.
534
+ return (`<self_performance generated_at="${xmlEscape(opts.generatedAt)}" ` +
535
+ `window_days="${data.windowDays}" overflow="true" />`);
536
+ }
537
+ budget[target] = effectiveRows(target) - 1;
538
+ block = renderBlock(data, lessonStores, budget, opts.generatedAt);
539
+ }
540
+ return block;
541
+ }