@aitne/daemon 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/dist/adapters/adapter-watchdog.d.ts +70 -0
  2. package/dist/adapters/adapter-watchdog.js +115 -0
  3. package/dist/adapters/discord.d.ts +17 -1
  4. package/dist/adapters/discord.js +33 -0
  5. package/dist/adapters/notification-manager.d.ts +27 -1
  6. package/dist/adapters/notification-manager.js +54 -39
  7. package/dist/adapters/slack-adapter.d.ts +26 -1
  8. package/dist/adapters/slack-adapter.js +41 -0
  9. package/dist/adapters/telegram-adapter.d.ts +18 -1
  10. package/dist/adapters/telegram-adapter.js +41 -2
  11. package/dist/adapters/types.d.ts +20 -0
  12. package/dist/adapters/whatsapp-adapter.d.ts +26 -7
  13. package/dist/adapters/whatsapp-adapter.js +74 -21
  14. package/dist/api/env-writer.d.ts +1 -0
  15. package/dist/api/env-writer.js +17 -7
  16. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  17. package/dist/api/helpers/agent-errors-registry.js +5 -5
  18. package/dist/api/routes/agent-schedule.js +5 -1
  19. package/dist/api/routes/agent.js +33 -12
  20. package/dist/api/routes/agents/index.js +75 -16
  21. package/dist/api/routes/agents/views.d.ts +37 -2
  22. package/dist/api/routes/agents/views.js +64 -2
  23. package/dist/api/routes/apple-calendar.js +4 -1
  24. package/dist/api/routes/background-task.d.ts +22 -0
  25. package/dist/api/routes/background-task.js +338 -0
  26. package/dist/api/routes/browser-history.js +9 -1
  27. package/dist/api/routes/calendar.js +12 -2
  28. package/dist/api/routes/context/path-resolve.js +6 -1
  29. package/dist/api/routes/context/permissions.js +12 -2
  30. package/dist/api/routes/context/snapshots.js +0 -3
  31. package/dist/api/routes/context/write.js +3 -17
  32. package/dist/api/routes/dashboard/config.js +58 -12
  33. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  34. package/dist/api/routes/dashboard/notifications.js +9 -9
  35. package/dist/api/routes/dashboard/oauth-google.js +5 -3
  36. package/dist/api/routes/feedback.d.ts +3 -0
  37. package/dist/api/routes/feedback.js +349 -0
  38. package/dist/api/routes/git.js +10 -3
  39. package/dist/api/routes/github.js +5 -1
  40. package/dist/api/routes/integrations/crud-patch.js +5 -1
  41. package/dist/api/routes/integrations-reconcile.js +2 -2
  42. package/dist/api/routes/mcp.js +65 -13
  43. package/dist/api/routes/notion.d.ts +1 -1
  44. package/dist/api/routes/observations.js +7 -7
  45. package/dist/api/routes/obsidian.d.ts +1 -1
  46. package/dist/api/routes/receipts.js +5 -1
  47. package/dist/api/routes/setup-migrate.js +1 -1
  48. package/dist/api/routes/setup.js +1 -1
  49. package/dist/api/routes/task-flows.d.ts +1 -1
  50. package/dist/api/routes/task-flows.js +1 -1
  51. package/dist/api/routes/tuning.d.ts +29 -0
  52. package/dist/api/routes/tuning.js +304 -0
  53. package/dist/api/server.d.ts +44 -16
  54. package/dist/api/server.js +12 -0
  55. package/dist/bootstrap/adapters.d.ts +19 -0
  56. package/dist/bootstrap/adapters.js +61 -0
  57. package/dist/bootstrap/api.d.ts +5 -3
  58. package/dist/bootstrap/api.js +45 -13
  59. package/dist/bootstrap/catchup.d.ts +1 -1
  60. package/dist/bootstrap/catchup.js +11 -11
  61. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  62. package/dist/bootstrap/event-pipeline.js +246 -8
  63. package/dist/bootstrap/observers.js +9 -6
  64. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  65. package/dist/bootstrap/schedule-helpers.js +172 -19
  66. package/dist/config.js +32 -12
  67. package/dist/core/agent-core.d.ts +33 -1
  68. package/dist/core/agent-core.js +36 -1
  69. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  70. package/dist/core/agents/activity-scan-cadence.js +127 -0
  71. package/dist/core/agents/agent-route-override.d.ts +53 -0
  72. package/dist/core/agents/agent-route-override.js +69 -0
  73. package/dist/core/agents/builtin-registry.d.ts +51 -14
  74. package/dist/core/agents/builtin-registry.js +92 -15
  75. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  76. package/dist/core/agents/config-gate-reconcile.js +51 -0
  77. package/dist/core/agents/cron-substitute.d.ts +1 -1
  78. package/dist/core/agents/cron-substitute.js +1 -1
  79. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  80. package/dist/core/agents/custom-routine-migration.js +149 -0
  81. package/dist/core/agents/firing-blocked.d.ts +1 -1
  82. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  83. package/dist/core/agents/hourly-cadence.js +126 -0
  84. package/dist/core/agents/loader-boot.js +23 -0
  85. package/dist/core/agents/loader.d.ts +19 -0
  86. package/dist/core/agents/loader.js +34 -2
  87. package/dist/core/agents/override-merge.d.ts +1 -1
  88. package/dist/core/agents/override-merge.js +9 -1
  89. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  90. package/dist/core/agents/recurrence-convert.js +1 -1
  91. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  92. package/dist/core/alerts.js +6 -6
  93. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  94. package/dist/core/backends/auth-health-monitor.js +1 -1
  95. package/dist/core/backends/backend-router.d.ts +27 -1
  96. package/dist/core/backends/backend-router.js +165 -1
  97. package/dist/core/backends/claude-code-core.d.ts +71 -31
  98. package/dist/core/backends/claude-code-core.js +282 -54
  99. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  100. package/dist/core/backends/cli-quota-guards.js +40 -5
  101. package/dist/core/backends/codex-core.d.ts +6 -0
  102. package/dist/core/backends/codex-core.js +22 -6
  103. package/dist/core/backends/failure-spend.d.ts +58 -0
  104. package/dist/core/backends/failure-spend.js +137 -0
  105. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  106. package/dist/core/backends/gemini-cli-core.js +38 -6
  107. package/dist/core/backends/model-registry.d.ts +1 -1
  108. package/dist/core/backends/model-registry.js +4 -4
  109. package/dist/core/backends/opencode-core.d.ts +1 -1
  110. package/dist/core/backends/opencode-core.js +5 -5
  111. package/dist/core/backends/plan-presets.js +47 -18
  112. package/dist/core/bang-commands/commands-cost.js +3 -1
  113. package/dist/core/bang-commands/commands-report.js +4 -3
  114. package/dist/core/bang-commands/commands-research.js +4 -1
  115. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  116. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  117. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  118. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  119. package/dist/core/bang-commands/commands-task-control.js +147 -0
  120. package/dist/core/bang-commands/commands-wiki.js +5 -5
  121. package/dist/core/bang-commands/index.d.ts +2 -0
  122. package/dist/core/bang-commands/index.js +12 -0
  123. package/dist/core/bang-commands/registry.d.ts +12 -0
  124. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  125. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  126. package/dist/core/channel-timeline.d.ts +5 -1
  127. package/dist/core/channel-timeline.js +13 -0
  128. package/dist/core/context/index-reconciler.js +5 -2
  129. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  130. package/dist/core/context/policy-index-runner.js +25 -6
  131. package/dist/core/context-builder-calendar.js +10 -2
  132. package/dist/core/context-builder-conversation.d.ts +8 -1
  133. package/dist/core/context-builder-conversation.js +41 -7
  134. package/dist/core/context-builder-yesterday.js +4 -3
  135. package/dist/core/context-builder.d.ts +7 -2
  136. package/dist/core/context-builder.js +193 -5
  137. package/dist/core/context-file-serializer.d.ts +1 -1
  138. package/dist/core/context-file-serializer.js +1 -1
  139. package/dist/core/context-health.js +2 -2
  140. package/dist/core/context-paths.d.ts +11 -1
  141. package/dist/core/context-paths.js +17 -1
  142. package/dist/core/context-validation/prepare-write.js +1 -1
  143. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  144. package/dist/core/context-vault-aliases.d.ts +0 -13
  145. package/dist/core/context-vault-aliases.js +37 -0
  146. package/dist/core/custom-routines.d.ts +99 -0
  147. package/dist/core/custom-routines.js +187 -0
  148. package/dist/core/daemon-api-cli.js +50 -1
  149. package/dist/core/day-boundary.d.ts +46 -0
  150. package/dist/core/day-boundary.js +40 -0
  151. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  152. package/dist/core/dispatcher-activity-scan.js +775 -0
  153. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  154. package/dist/core/dispatcher-error-handling.js +38 -62
  155. package/dist/core/dispatcher-hourly-check.js +6 -1
  156. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  157. package/dist/core/dispatcher-message-handler.js +24 -0
  158. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  159. package/dist/core/dispatcher-morning-routine.js +13 -13
  160. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  161. package/dist/core/dispatcher-result-processor.js +167 -11
  162. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  163. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  164. package/dist/core/dispatcher-scheduled-tasks.d.ts +104 -1
  165. package/dist/core/dispatcher-scheduled-tasks.js +480 -8
  166. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  167. package/dist/core/dispatcher-task-delivery.js +555 -0
  168. package/dist/core/dispatcher-types.d.ts +48 -9
  169. package/dist/core/dispatcher-types.js +3 -3
  170. package/dist/core/dispatcher.d.ts +112 -31
  171. package/dist/core/dispatcher.js +297 -60
  172. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  173. package/dist/core/drift-effects.js +2 -2
  174. package/dist/core/feedback/consolidation-prep.d.ts +94 -0
  175. package/dist/core/feedback/consolidation-prep.js +254 -0
  176. package/dist/core/feedback/eviction-scorer.d.ts +81 -0
  177. package/dist/core/feedback/eviction-scorer.js +136 -0
  178. package/dist/core/feedback/lesson-format.d.ts +79 -0
  179. package/dist/core/feedback/lesson-format.js +199 -0
  180. package/dist/core/feedback/lesson-injection.d.ts +98 -0
  181. package/dist/core/feedback/lesson-injection.js +174 -0
  182. package/dist/core/feedback/lesson-merge.d.ts +51 -0
  183. package/dist/core/feedback/lesson-merge.js +88 -0
  184. package/dist/core/feedback/lesson-store-overview.d.ts +46 -0
  185. package/dist/core/feedback/lesson-store-overview.js +42 -0
  186. package/dist/core/feedback/promotion-gate.d.ts +69 -0
  187. package/dist/core/feedback/promotion-gate.js +117 -0
  188. package/dist/core/feedback/regeneralization-prep.d.ts +87 -0
  189. package/dist/core/feedback/regeneralization-prep.js +152 -0
  190. package/dist/core/feedback/scope-parser.d.ts +86 -0
  191. package/dist/core/feedback/scope-parser.js +141 -0
  192. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  193. package/dist/core/feedback/self-performance-prep.js +541 -0
  194. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  195. package/dist/core/feedback/tuning-actuator.js +432 -0
  196. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  197. package/dist/core/feedback/tuning-recommender.js +580 -0
  198. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  199. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  200. package/dist/core/health-monitor.d.ts +6 -0
  201. package/dist/core/health-monitor.js +1 -1
  202. package/dist/core/injection-policy.d.ts +83 -1
  203. package/dist/core/injection-policy.js +61 -3
  204. package/dist/core/integration-main-backend.js +4 -0
  205. package/dist/core/management-md.d.ts +2 -2
  206. package/dist/core/management-md.js +51 -13
  207. package/dist/core/morning/orchestrator.d.ts +2 -2
  208. package/dist/core/morning/orchestrator.js +2 -2
  209. package/dist/core/notification-gate.d.ts +64 -0
  210. package/dist/core/notification-gate.js +51 -0
  211. package/dist/core/notification-rate-limit.d.ts +40 -0
  212. package/dist/core/notification-rate-limit.js +50 -0
  213. package/dist/core/policy-files.d.ts +1 -1
  214. package/dist/core/policy-files.js +2 -2
  215. package/dist/core/pre-pass-freshness.d.ts +4 -4
  216. package/dist/core/retention.d.ts +5 -0
  217. package/dist/core/retention.js +20 -4
  218. package/dist/core/review-context.d.ts +1 -1
  219. package/dist/core/review-context.js +10 -5
  220. package/dist/core/roadmap-write-lock.d.ts +2 -1
  221. package/dist/core/roadmap-write-lock.js +15 -10
  222. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  223. package/dist/core/routine-acquisition-plan.js +78 -20
  224. package/dist/core/routine-fetch-window-retry.js +7 -4
  225. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  226. package/dist/core/routine-fetch-window-runner.js +264 -13
  227. package/dist/core/routine-windows.d.ts +2 -2
  228. package/dist/core/routine-windows.js +8 -5
  229. package/dist/core/scheduler.d.ts +175 -16
  230. package/dist/core/scheduler.js +559 -102
  231. package/dist/core/signal-detector.d.ts +51 -1
  232. package/dist/core/signal-detector.js +321 -24
  233. package/dist/core/skills-compiler-denied-tools.js +2 -2
  234. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  235. package/dist/core/skills-compiler-skill-index.js +2 -2
  236. package/dist/core/skills-compiler-variants.d.ts +1 -1
  237. package/dist/core/skills-compiler-variants.js +8 -0
  238. package/dist/core/skills-compiler.d.ts +29 -26
  239. package/dist/core/skills-compiler.js +117 -81
  240. package/dist/core/skills-manifest.d.ts +37 -0
  241. package/dist/core/skills-manifest.js +73 -2
  242. package/dist/core/sleep-inhibitor.d.ts +79 -0
  243. package/dist/core/sleep-inhibitor.js +132 -0
  244. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  245. package/dist/core/slim-system-prompt-loader.js +141 -0
  246. package/dist/core/spawn-gates.d.ts +126 -0
  247. package/dist/core/spawn-gates.js +180 -0
  248. package/dist/core/today-direct-writer.d.ts +60 -14
  249. package/dist/core/today-direct-writer.js +90 -13
  250. package/dist/core/today-write-lock.d.ts +4 -2
  251. package/dist/core/today-write-lock.js +30 -20
  252. package/dist/core/wake-detector.d.ts +55 -0
  253. package/dist/core/wake-detector.js +80 -0
  254. package/dist/core/wiki/compile-lock.d.ts +1 -1
  255. package/dist/core/wiki/compile-lock.js +1 -1
  256. package/dist/core/wiki/wiki-fts.js +13 -6
  257. package/dist/core/workdir.js +15 -6
  258. package/dist/db/activity-scan-signals.d.ts +77 -0
  259. package/dist/db/activity-scan-signals.js +378 -0
  260. package/dist/db/agents-store.d.ts +28 -0
  261. package/dist/db/agents-store.js +62 -0
  262. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  263. package/dist/db/background-task-clarifications-store.js +152 -0
  264. package/dist/db/background-task-store.d.ts +207 -0
  265. package/dist/db/background-task-store.js +380 -0
  266. package/dist/db/browser-history-store.d.ts +39 -6
  267. package/dist/db/browser-history-store.js +51 -7
  268. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  269. package/dist/db/browser-task-clarifications-store.js +35 -5
  270. package/dist/db/browser-task-store.d.ts +3 -0
  271. package/dist/db/browser-task-store.js +29 -4
  272. package/dist/db/deferred-dm.d.ts +86 -0
  273. package/dist/db/deferred-dm.js +199 -0
  274. package/dist/db/feedback-signals-store.d.ts +77 -0
  275. package/dist/db/feedback-signals-store.js +144 -0
  276. package/dist/db/migrations.js +380 -0
  277. package/dist/db/observations.d.ts +2 -2
  278. package/dist/db/observations.js +3 -3
  279. package/dist/db/schema.js +260 -22
  280. package/dist/db/voice-transcripts-store.d.ts +1 -1
  281. package/dist/index.js +86 -29
  282. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  283. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  284. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  285. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  286. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  287. package/dist/observers/delegated-sync-worker.js +10 -10
  288. package/dist/observers/git-delegated-cron.d.ts +1 -1
  289. package/dist/observers/git-delegated-cron.js +2 -2
  290. package/dist/observers/github-poller-classifier.d.ts +3 -3
  291. package/dist/observers/github-poller-classifier.js +3 -3
  292. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  293. package/dist/observers/imminent-event-scheduler.js +1 -1
  294. package/dist/observers/mail-poller.d.ts +1 -0
  295. package/dist/observers/mail-poller.js +42 -3
  296. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  297. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  298. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  299. package/dist/observers/observation-summarizer/worker.js +4 -4
  300. package/dist/observers/obsidian-watcher.d.ts +1 -1
  301. package/dist/observers/obsidian-watcher.js +1 -1
  302. package/dist/safety/agent-write-tracker.d.ts +4 -4
  303. package/dist/safety/agent-write-tracker.js +4 -4
  304. package/dist/safety/always-disallowed.d.ts +1 -1
  305. package/dist/safety/always-disallowed.js +39 -0
  306. package/dist/safety/audit.d.ts +43 -5
  307. package/dist/safety/audit.js +86 -18
  308. package/dist/safety/risk-classifier.d.ts +6 -0
  309. package/dist/safety/risk-classifier.js +97 -18
  310. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  311. package/dist/scheduler/activity-scan-gate.js +132 -0
  312. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  313. package/dist/services/background-task/background-task-budget.js +91 -0
  314. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  315. package/dist/services/background-task/background-task-driver.js +416 -0
  316. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  317. package/dist/services/background-task/background-task-runner.js +673 -0
  318. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  319. package/dist/services/background-task/background-task-tools.js +247 -0
  320. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  321. package/dist/services/background-task/background-task-transition-events.js +54 -0
  322. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  323. package/dist/services/browser-history/automation/egress-denylist.js +34 -8
  324. package/dist/services/browser-history/lifecycle/platform.js +44 -2
  325. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  326. package/dist/services/browser-task/browser-task-runner.js +53 -8
  327. package/dist/services/mcp/probe.js +30 -8
  328. package/dist/services/observations-batch.d.ts +1 -1
  329. package/dist/services/observations-batch.js +2 -2
  330. package/dist/settings/runtime-settings.d.ts +45 -12
  331. package/dist/settings/runtime-settings.js +215 -40
  332. package/dist/settings/settings-store.js +11 -3
  333. package/package.json +4 -4
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Feedback Learning Loop — mechanical merge / dedup (FEEDBACK_LEARNING_LOOP_DESIGN.md §4 step 3, §6).
3
+ *
4
+ * The *mechanical* half of "merge, don't append": group incoming signals by a
5
+ * normalised summary so identical reports collapse into one candidate, and
6
+ * collapse near-duplicate existing lessons (summing their `ev`) before the
7
+ * eviction scorer runs (§6: "Near-duplicates are merged … before eviction is
8
+ * even considered").
9
+ *
10
+ * The *semantic* half — judging whether a candidate matches an existing
11
+ * lesson's *intent* and phrasing the generalization — is the LLM's job (§4
12
+ * division of labour). This module never paraphrases; it only collapses exact
13
+ * normalised-text matches, which is safe to do deterministically.
14
+ */
15
+ /**
16
+ * Normalise a summary for mechanical equality: lowercase, strip punctuation,
17
+ * collapse whitespace. Two signals/lessons with the same normalised form are
18
+ * treated as the same candidate. Conservative — only *identical* phrasings
19
+ * collapse; anything semantic is left to the LLM.
20
+ */
21
+ export function normalizeSummary(summary) {
22
+ return summary
23
+ .toLowerCase()
24
+ .replace(/[^\p{L}\p{N}\s]/gu, " ")
25
+ .replace(/\s+/g, " ")
26
+ .trim();
27
+ }
28
+ /**
29
+ * Group signals by normalised summary, preserving first-seen order for both
30
+ * the groups and their members. Empty / whitespace-only summaries are kept
31
+ * under a stable empty key rather than dropped, so no signal id is lost from
32
+ * the consume set.
33
+ */
34
+ export function groupSignalsBySummary(signals) {
35
+ const order = [];
36
+ const byKey = new Map();
37
+ for (const signal of signals) {
38
+ const key = normalizeSummary(signal.summary);
39
+ let group = byKey.get(key);
40
+ if (!group) {
41
+ group = { key, summary: signal.summary, members: [] };
42
+ byKey.set(key, group);
43
+ order.push(key);
44
+ }
45
+ group.members.push(signal);
46
+ }
47
+ return order.map((key) => byKey.get(key));
48
+ }
49
+ const CONF_RANK = {
50
+ high: 3,
51
+ medium: 2,
52
+ low: 1,
53
+ };
54
+ /**
55
+ * Collapse near-duplicate lessons (identical normalised text) into one, summing
56
+ * `ev`, keeping the earliest `date`, the latest `last`, the max confidence, the
57
+ * strongest `kind` (constraint outranks a softer kind), and OR-ing
58
+ * `provisional` to `false` if any duplicate is active. Order is stable: the
59
+ * first occurrence's position is retained.
60
+ *
61
+ * Deterministic and lossless on `ev` — the summed evidence flows straight into
62
+ * the eviction score so a merged lesson is *harder* to evict, never easier.
63
+ */
64
+ export function dedupeLessons(lessons) {
65
+ const order = [];
66
+ const byKey = new Map();
67
+ for (const lesson of lessons) {
68
+ const key = normalizeSummary(lesson.text);
69
+ const existing = byKey.get(key);
70
+ if (!existing) {
71
+ byKey.set(key, { ...lesson });
72
+ order.push(key);
73
+ continue;
74
+ }
75
+ existing.ev += lesson.ev;
76
+ if (lesson.date < existing.date)
77
+ existing.date = lesson.date;
78
+ if (lesson.last > existing.last)
79
+ existing.last = lesson.last;
80
+ if (CONF_RANK[lesson.conf] > CONF_RANK[existing.conf]) {
81
+ existing.conf = lesson.conf;
82
+ }
83
+ if (lesson.kind === "constraint")
84
+ existing.kind = "constraint";
85
+ existing.provisional = existing.provisional && lesson.provisional;
86
+ }
87
+ return order.map((key) => byKey.get(key));
88
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Feedback Learning Loop — lesson-store overview (FEEDBACK_LEARNING_LOOP_DESIGN.md §9 Phase 5).
3
+ *
4
+ * The pure, deterministic half of the `GET /api/feedback/lessons` dashboard
5
+ * surface. The route (coverage-excluded — it does the FS enumeration) hands
6
+ * each lesson file's raw contents to {@link summarizeLessonStore}, which parses
7
+ * the `## Lessons` section and reports the cap-utilisation metrics the
8
+ * "view/edit lessons and tune caps/threshold" settings page renders:
9
+ * byte size vs. cap, entry count vs. cap, active vs. provisional split, and
10
+ * whether the store is currently over either cap.
11
+ *
12
+ * Mirrors the §4 division of labour: the byte/entry accounting is mechanical
13
+ * (here, 100% covered), while the route owns only the FS read + JSON assembly.
14
+ */
15
+ export interface LessonStoreSummary {
16
+ /** UTF-8 byte size of the on-disk `## Lessons` section (the cap unit, §6). */
17
+ bytes: number;
18
+ /** Per-scope byte cap. */
19
+ capBytes: number;
20
+ /** Total parsed lessons (active + provisional). */
21
+ entries: number;
22
+ /** Per-scope entry cap. */
23
+ maxEntries: number;
24
+ /** Injectable (promoted) lessons — the ones that actually reach a prompt. */
25
+ active: number;
26
+ /** Stored-but-not-yet-injected lessons awaiting corroboration (§4 step 4). */
27
+ provisional: number;
28
+ /** True when the file exceeds its byte cap or its entry cap. */
29
+ overCap: boolean;
30
+ }
31
+ /**
32
+ * Summarise one lesson store from its raw file contents. A file with no
33
+ * `## Lessons` section (or an empty one) reports zero entries — never throws,
34
+ * so a hand-edited or partially-written file degrades to "empty store" rather
35
+ * than breaking the overview. `bytes` measures the on-disk `## Lessons`
36
+ * section body — the §6 cap unit (`lessonsSectionByteLength` in
37
+ * lesson-format.ts) the eviction scorer and the nightly worksheet's
38
+ * `over_cap` enforce. Measuring the whole file here previously reported a
39
+ * permanently-stuck `overCap: true` in the band where the section fit the
40
+ * cap but frontmatter + heading overhead pushed the file past it — a state
41
+ * no enforcement actor would ever clear.
42
+ */
43
+ export declare function summarizeLessonStore(fileMd: string, caps: {
44
+ capBytes: number;
45
+ maxEntries: number;
46
+ }): LessonStoreSummary;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Feedback Learning Loop — lesson-store overview (FEEDBACK_LEARNING_LOOP_DESIGN.md §9 Phase 5).
3
+ *
4
+ * The pure, deterministic half of the `GET /api/feedback/lessons` dashboard
5
+ * surface. The route (coverage-excluded — it does the FS enumeration) hands
6
+ * each lesson file's raw contents to {@link summarizeLessonStore}, which parses
7
+ * the `## Lessons` section and reports the cap-utilisation metrics the
8
+ * "view/edit lessons and tune caps/threshold" settings page renders:
9
+ * byte size vs. cap, entry count vs. cap, active vs. provisional split, and
10
+ * whether the store is currently over either cap.
11
+ *
12
+ * Mirrors the §4 division of labour: the byte/entry accounting is mechanical
13
+ * (here, 100% covered), while the route owns only the FS read + JSON assembly.
14
+ */
15
+ import { extractMarkdownSection, parseLessonsSection, } from "./lesson-format.js";
16
+ /**
17
+ * Summarise one lesson store from its raw file contents. A file with no
18
+ * `## Lessons` section (or an empty one) reports zero entries — never throws,
19
+ * so a hand-edited or partially-written file degrades to "empty store" rather
20
+ * than breaking the overview. `bytes` measures the on-disk `## Lessons`
21
+ * section body — the §6 cap unit (`lessonsSectionByteLength` in
22
+ * lesson-format.ts) the eviction scorer and the nightly worksheet's
23
+ * `over_cap` enforce. Measuring the whole file here previously reported a
24
+ * permanently-stuck `overCap: true` in the band where the section fit the
25
+ * cap but frontmatter + heading overhead pushed the file past it — a state
26
+ * no enforcement actor would ever clear.
27
+ */
28
+ export function summarizeLessonStore(fileMd, caps) {
29
+ const sectionBody = extractMarkdownSection(fileMd, "Lessons");
30
+ const lessons = sectionBody ? parseLessonsSection(sectionBody) : [];
31
+ const provisional = lessons.filter((lesson) => lesson.provisional).length;
32
+ const bytes = sectionBody ? Buffer.byteLength(sectionBody, "utf-8") : 0;
33
+ return {
34
+ bytes,
35
+ capBytes: caps.capBytes,
36
+ entries: lessons.length,
37
+ maxEntries: caps.maxEntries,
38
+ active: lessons.length - provisional,
39
+ provisional,
40
+ overCap: bytes > caps.capBytes || lessons.length > caps.maxEntries,
41
+ };
42
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Feedback Learning Loop — promotion gate (FEEDBACK_LEARNING_LOOP_DESIGN.md §4 step 4).
3
+ *
4
+ * Decides whether a candidate lesson (one or more corroborating signals) is
5
+ * *active / injectable* or stays *provisional*. This is the deterministic
6
+ * "globally optimized, not single-shot" gate (requirement #4): the LLM groups
7
+ * signals by intent (semantic), but the promote/hold decision is pure code so
8
+ * the model never decides the threshold.
9
+ *
10
+ * Two hard rules kill the §3.5.1 sign-inversion failure mode at the gate:
11
+ * 1. `ignored` carries `valence='neutral'` and weight 0.25 — silence is weak
12
+ * corroboration, never disapproval, and can never *flip a lesson negative*.
13
+ * 2. `ignored` is **non-initiating**: a candidate made *only* of `ignored`
14
+ * signals never promotes, regardless of weighted sum. An ignore can
15
+ * strengthen an explicit/corrected lesson; it can never start one.
16
+ */
17
+ import type { FeedbackSignalSource, FeedbackSignalValence } from "../../db/feedback-signals-store.js";
18
+ /** Minimal signal shape the gate scores — a projection of `feedback_signals`. */
19
+ export interface GateSignal {
20
+ source: FeedbackSignalSource;
21
+ valence: FeedbackSignalValence | null;
22
+ }
23
+ /**
24
+ * Per-signal evidence weight (§3.5.1 / §4 step 4):
25
+ * explicit | corrected = 1.0 · self_critique | replied | acted = 0.5 · ignored = 0.25
26
+ *
27
+ * Derived from `(source, valence)` only — `valence` already encodes the
28
+ * behavioral reaction (corrected→correction, ignored→neutral,
29
+ * replied/acted→positive), so the gate needs no `evidence_json` lookup.
30
+ * Checked in priority order: an authoritative directive (explicit source or a
31
+ * correction) outranks everything; only the *behavioral* `ignored` reaction
32
+ * (see {@link isIgnoredSignal}) drops to 0.25 — a neutral valence on an
33
+ * `explicit`/`self_critique` row is a deliberate signal, not silence, and keeps
34
+ * the 0.5 (or 1.0, for explicit) authoritative weight.
35
+ */
36
+ export declare function signalWeight(signal: GateSignal): number;
37
+ /**
38
+ * `ignored` is the §3.5.1 behavioral notification-elapsed reaction: a
39
+ * `behavioral` signal carrying `valence='neutral'`. It drives the
40
+ * non-initiating / never-negative rule. A *neutral* valence on an `explicit`
41
+ * or `self_critique` row is NOT an ignore — it is an authoritative/deliberate
42
+ * signal that merely lacks a positive/negative tilt — so it must not inherit
43
+ * the 0.25 weight or the non-initiating treatment. Scoping the check to
44
+ * `behavioral` keeps this consistent with {@link signalWeight}, which already
45
+ * treats an explicit-neutral row as authoritative (1.0).
46
+ */
47
+ export declare function isIgnoredSignal(signal: GateSignal): boolean;
48
+ /** An authoritative owner directive — promotes on first occurrence. */
49
+ export declare function isExplicitDirective(signal: GateSignal): boolean;
50
+ /** Weighted evidence sum across a candidate's contributing signals. */
51
+ export declare function computeWeightedEvidence(signals: ReadonlyArray<GateSignal>): number;
52
+ export type PromotionReason = "explicit-directive" | "evidence-threshold" | "below-threshold" | "ignored-non-initiating" | "no-signals";
53
+ export interface PromotionVerdict {
54
+ /** Active & injectable when true; provisional otherwise. */
55
+ promotable: boolean;
56
+ /** Stored-but-excluded-from-injection marker (`<!-- provisional -->`). */
57
+ provisional: boolean;
58
+ /** `high` if any explicit/corrected; `medium` at threshold; else `low`. */
59
+ conf: "high" | "medium" | "low";
60
+ weightedEv: number;
61
+ reason: PromotionReason;
62
+ }
63
+ /**
64
+ * Evaluate the promotion gate for a candidate's contributing signals.
65
+ *
66
+ * @param threshold weighted-evidence bar for behavioral/self_critique
67
+ * (`feedbackPromotionThreshold`, default 2).
68
+ */
69
+ export declare function evaluatePromotion(signals: ReadonlyArray<GateSignal>, threshold: number): PromotionVerdict;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Feedback Learning Loop — promotion gate (FEEDBACK_LEARNING_LOOP_DESIGN.md §4 step 4).
3
+ *
4
+ * Decides whether a candidate lesson (one or more corroborating signals) is
5
+ * *active / injectable* or stays *provisional*. This is the deterministic
6
+ * "globally optimized, not single-shot" gate (requirement #4): the LLM groups
7
+ * signals by intent (semantic), but the promote/hold decision is pure code so
8
+ * the model never decides the threshold.
9
+ *
10
+ * Two hard rules kill the §3.5.1 sign-inversion failure mode at the gate:
11
+ * 1. `ignored` carries `valence='neutral'` and weight 0.25 — silence is weak
12
+ * corroboration, never disapproval, and can never *flip a lesson negative*.
13
+ * 2. `ignored` is **non-initiating**: a candidate made *only* of `ignored`
14
+ * signals never promotes, regardless of weighted sum. An ignore can
15
+ * strengthen an explicit/corrected lesson; it can never start one.
16
+ */
17
+ /**
18
+ * Per-signal evidence weight (§3.5.1 / §4 step 4):
19
+ * explicit | corrected = 1.0 · self_critique | replied | acted = 0.5 · ignored = 0.25
20
+ *
21
+ * Derived from `(source, valence)` only — `valence` already encodes the
22
+ * behavioral reaction (corrected→correction, ignored→neutral,
23
+ * replied/acted→positive), so the gate needs no `evidence_json` lookup.
24
+ * Checked in priority order: an authoritative directive (explicit source or a
25
+ * correction) outranks everything; only the *behavioral* `ignored` reaction
26
+ * (see {@link isIgnoredSignal}) drops to 0.25 — a neutral valence on an
27
+ * `explicit`/`self_critique` row is a deliberate signal, not silence, and keeps
28
+ * the 0.5 (or 1.0, for explicit) authoritative weight.
29
+ */
30
+ export function signalWeight(signal) {
31
+ if (signal.source === "explicit")
32
+ return 1.0;
33
+ if (signal.valence === "correction")
34
+ return 1.0;
35
+ if (isIgnoredSignal(signal))
36
+ return 0.25;
37
+ return 0.5;
38
+ }
39
+ /**
40
+ * `ignored` is the §3.5.1 behavioral notification-elapsed reaction: a
41
+ * `behavioral` signal carrying `valence='neutral'`. It drives the
42
+ * non-initiating / never-negative rule. A *neutral* valence on an `explicit`
43
+ * or `self_critique` row is NOT an ignore — it is an authoritative/deliberate
44
+ * signal that merely lacks a positive/negative tilt — so it must not inherit
45
+ * the 0.25 weight or the non-initiating treatment. Scoping the check to
46
+ * `behavioral` keeps this consistent with {@link signalWeight}, which already
47
+ * treats an explicit-neutral row as authoritative (1.0).
48
+ */
49
+ export function isIgnoredSignal(signal) {
50
+ return signal.source === "behavioral" && signal.valence === "neutral";
51
+ }
52
+ /** An authoritative owner directive — promotes on first occurrence. */
53
+ export function isExplicitDirective(signal) {
54
+ return signal.source === "explicit" || signal.valence === "correction";
55
+ }
56
+ /** Weighted evidence sum across a candidate's contributing signals. */
57
+ export function computeWeightedEvidence(signals) {
58
+ return signals.reduce((sum, signal) => sum + signalWeight(signal), 0);
59
+ }
60
+ /**
61
+ * Evaluate the promotion gate for a candidate's contributing signals.
62
+ *
63
+ * @param threshold weighted-evidence bar for behavioral/self_critique
64
+ * (`feedbackPromotionThreshold`, default 2).
65
+ */
66
+ export function evaluatePromotion(signals, threshold) {
67
+ const weightedEv = computeWeightedEvidence(signals);
68
+ if (signals.length === 0) {
69
+ return {
70
+ promotable: false,
71
+ provisional: true,
72
+ conf: "low",
73
+ weightedEv,
74
+ reason: "no-signals",
75
+ };
76
+ }
77
+ // Rule 2: an ignored-only candidate is non-initiating — never promotes,
78
+ // regardless of weighted sum (two coincidental busy-morning ignores cannot
79
+ // teach "stop notifying about X").
80
+ if (signals.every(isIgnoredSignal)) {
81
+ return {
82
+ promotable: false,
83
+ provisional: true,
84
+ conf: "low",
85
+ weightedEv,
86
+ reason: "ignored-non-initiating",
87
+ };
88
+ }
89
+ // An explicit owner directive (or any correction) is authoritative →
90
+ // promote on first occurrence with high confidence.
91
+ if (signals.some(isExplicitDirective)) {
92
+ return {
93
+ promotable: true,
94
+ provisional: false,
95
+ conf: "high",
96
+ weightedEv,
97
+ reason: "explicit-directive",
98
+ };
99
+ }
100
+ // Behavioral / self_critique corroboration: promote at the weighted bar.
101
+ if (weightedEv >= threshold) {
102
+ return {
103
+ promotable: true,
104
+ provisional: false,
105
+ conf: "medium",
106
+ weightedEv,
107
+ reason: "evidence-threshold",
108
+ };
109
+ }
110
+ return {
111
+ promotable: false,
112
+ provisional: true,
113
+ conf: "low",
114
+ weightedEv,
115
+ reason: "below-threshold",
116
+ };
117
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Feedback Learning Loop — monthly re-generalization pre-step
3
+ * (FEEDBACK_LEARNING_LOOP_DESIGN.md §4 "Monthly re-generalization", Phase 5).
4
+ *
5
+ * The deterministic, daemon-side half of the *monthly* collapse. Where the
6
+ * nightly evening-review pre-step (`consolidation-prep.ts`) folds unconsumed
7
+ * *signals* into lessons, the monthly pass re-reads the *already-consolidated*
8
+ * lesson stores and surfaces them so the LLM can collapse several specific
9
+ * lessons that share a theme into one higher-level principle — e.g. three
10
+ * "shorter mail summary" / "shorter standup" / "shorter report" lessons → one
11
+ * `agent`-scope lesson "Default to terse, bulleted output." This is the engine
12
+ * that turns accumulated specifics into a small set of meaningful generalizations.
13
+ *
14
+ * Two layers, mirroring `consolidation-prep.ts`:
15
+ * - The dispatcher (coverage-excluded, FS-heavy) enumerates the lesson files
16
+ * on disk — the global `policies/agent-lessons.md` plus every per-agent
17
+ * `policies/agents/<slug>/lessons.md` — and reads their contents.
18
+ * - `buildRegeneralizationWorksheet(scopes, …)` — this pure markdown/XML
19
+ * composer turns those contents into a `<feedback_regeneralization>` block.
20
+ * Every output byte is a deterministic function of its inputs, so it stays
21
+ * I/O-free and 100% coverable.
22
+ *
23
+ * Unlike the evening worksheet, this pass carries **no signals and no consume
24
+ * ids** — it neither promotes nor consumes; it only ranks the existing lessons
25
+ * (lowest-scored first, the same eviction order Step 4 already uses) and flags
26
+ * staleness / over-cap so the LLM's collapse honours the same caps. A scope is
27
+ * surfaced only when it holds at least {@link MIN_LESSONS_FOR_REGENERALIZATION}
28
+ * *active* lessons — you need two to collapse one — and the whole block is
29
+ * omitted when no scope qualifies, so a sparse vault adds nothing to the
30
+ * monthly prompt.
31
+ *
32
+ * **Promotion-neutral by construction.** Only *active* (non-provisional)
33
+ * lessons are surfaced for collapse. Provisional lessons are awaiting
34
+ * corroboration and are owned exclusively by the nightly evening pass — the
35
+ * single promotion authority (`promotion-gate.ts`). Offering them here would
36
+ * let the LLM merge two provisional lessons into one active lesson, summing
37
+ * their `ev` past the threshold and bypassing the gate's
38
+ * `ignored`-only-never-promotes guard (§3.5.1) — the exact sign-inversion the
39
+ * gate exists to kill. They stay in the file untouched; the task-flow tells the
40
+ * LLM to preserve any provisional lesson byte-for-byte.
41
+ */
42
+ import { type CanonicalScope } from "./scope-parser.js";
43
+ /** A scope needs at least this many *active* lessons before a collapse is possible. */
44
+ export declare const MIN_LESSONS_FOR_REGENERALIZATION = 2;
45
+ export interface RegeneralizationScopeInput {
46
+ /** `agent` (global) or `agent_slug` (per-agent) — the user scope is handled
47
+ * by the existing nightly user-profile consolidation, not re-generalised. */
48
+ scope: CanonicalScope;
49
+ /** Canonical store path (`policies/agent-lessons.md` / `policies/agents/<slug>/lessons.md`). */
50
+ storeFile: string;
51
+ /** Current lessons-store file contents. */
52
+ existingFileMd: string;
53
+ /** Byte/entry caps for the scope (§6). */
54
+ caps: {
55
+ capBytes: number;
56
+ maxEntries: number;
57
+ };
58
+ }
59
+ export interface BuildRegeneralizationOptions {
60
+ nowIso: string;
61
+ recencyHalfLifeDays?: number;
62
+ /**
63
+ * Staleness horizon in days (`feedbackLessonStaleDays`, §4 step 7). A lesson
64
+ * whose `last=` predates `now − staleDays` and is not a `constraint` is
65
+ * flagged `stale="true"` so the LLM can drop it while collapsing. Omitted ⇒
66
+ * nothing is flagged stale.
67
+ */
68
+ staleDays?: number;
69
+ }
70
+ export interface RegeneralizationResult {
71
+ /** `<feedback_regeneralization>…</…>` block for verbatim injection. */
72
+ block: string;
73
+ /** Number of scopes surfaced (each with ≥ MIN_LESSONS_FOR_REGENERALIZATION active lessons). */
74
+ scopeCount: number;
75
+ /** Total *active* lessons surfaced across all scopes (provisional excluded). */
76
+ lessonCount: number;
77
+ }
78
+ /**
79
+ * Compose the `<feedback_regeneralization>` block. Returns `null` when no scope
80
+ * holds at least {@link MIN_LESSONS_FOR_REGENERALIZATION} *active*
81
+ * (non-provisional) lessons — there is nothing to collapse, so the caller
82
+ * stamps nothing (no empty block in the prompt). Provisional lessons are
83
+ * excluded from the collapse set (see module header) but still counted in each
84
+ * scope's `current_entries` / `over_cap` so the cap status stays whole-file
85
+ * truthful. Scopes are emitted in input order.
86
+ */
87
+ export declare function buildRegeneralizationWorksheet(scopes: ReadonlyArray<RegeneralizationScopeInput>, opts: BuildRegeneralizationOptions): RegeneralizationResult | null;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Feedback Learning Loop — monthly re-generalization pre-step
3
+ * (FEEDBACK_LEARNING_LOOP_DESIGN.md §4 "Monthly re-generalization", Phase 5).
4
+ *
5
+ * The deterministic, daemon-side half of the *monthly* collapse. Where the
6
+ * nightly evening-review pre-step (`consolidation-prep.ts`) folds unconsumed
7
+ * *signals* into lessons, the monthly pass re-reads the *already-consolidated*
8
+ * lesson stores and surfaces them so the LLM can collapse several specific
9
+ * lessons that share a theme into one higher-level principle — e.g. three
10
+ * "shorter mail summary" / "shorter standup" / "shorter report" lessons → one
11
+ * `agent`-scope lesson "Default to terse, bulleted output." This is the engine
12
+ * that turns accumulated specifics into a small set of meaningful generalizations.
13
+ *
14
+ * Two layers, mirroring `consolidation-prep.ts`:
15
+ * - The dispatcher (coverage-excluded, FS-heavy) enumerates the lesson files
16
+ * on disk — the global `policies/agent-lessons.md` plus every per-agent
17
+ * `policies/agents/<slug>/lessons.md` — and reads their contents.
18
+ * - `buildRegeneralizationWorksheet(scopes, …)` — this pure markdown/XML
19
+ * composer turns those contents into a `<feedback_regeneralization>` block.
20
+ * Every output byte is a deterministic function of its inputs, so it stays
21
+ * I/O-free and 100% coverable.
22
+ *
23
+ * Unlike the evening worksheet, this pass carries **no signals and no consume
24
+ * ids** — it neither promotes nor consumes; it only ranks the existing lessons
25
+ * (lowest-scored first, the same eviction order Step 4 already uses) and flags
26
+ * staleness / over-cap so the LLM's collapse honours the same caps. A scope is
27
+ * surfaced only when it holds at least {@link MIN_LESSONS_FOR_REGENERALIZATION}
28
+ * *active* lessons — you need two to collapse one — and the whole block is
29
+ * omitted when no scope qualifies, so a sparse vault adds nothing to the
30
+ * monthly prompt.
31
+ *
32
+ * **Promotion-neutral by construction.** Only *active* (non-provisional)
33
+ * lessons are surfaced for collapse. Provisional lessons are awaiting
34
+ * corroboration and are owned exclusively by the nightly evening pass — the
35
+ * single promotion authority (`promotion-gate.ts`). Offering them here would
36
+ * let the LLM merge two provisional lessons into one active lesson, summing
37
+ * their `ev` past the threshold and bypassing the gate's
38
+ * `ignored`-only-never-promotes guard (§3.5.1) — the exact sign-inversion the
39
+ * gate exists to kill. They stay in the file untouched; the task-flow tells the
40
+ * LLM to preserve any provisional lesson byte-for-byte.
41
+ */
42
+ import { extractMarkdownSection, parseLessonsSection, } from "./lesson-format.js";
43
+ import { scoreLesson, isLessonStale, DEFAULT_RECENCY_HALFLIFE_DAYS, } from "./eviction-scorer.js";
44
+ import { formatScope, scopeSectionSlug } from "./scope-parser.js";
45
+ /** A scope needs at least this many *active* lessons before a collapse is possible. */
46
+ export const MIN_LESSONS_FOR_REGENERALIZATION = 2;
47
+ function xmlEscape(value) {
48
+ return value
49
+ .replace(/&/g, "&amp;")
50
+ .replace(/</g, "&lt;")
51
+ .replace(/>/g, "&gt;")
52
+ .replace(/"/g, "&quot;");
53
+ }
54
+ function round2(value) {
55
+ return (Math.round(value * 100) / 100).toFixed(2);
56
+ }
57
+ /** Collapse a one-line excerpt of a lesson for an XML text node.
58
+ * The clip strips a trailing lone high surrogate so cutting through an
59
+ * astral char (emoji) can't leave a U+FFFD in the worksheet. */
60
+ function inline(text, max = 300) {
61
+ const flat = text.replace(/\s+/g, " ").trim();
62
+ const clipped = flat.length > max
63
+ ? `${flat.slice(0, max - 1).replace(/[\uD800-\uDBFF]$/, "")}…`
64
+ : flat;
65
+ return xmlEscape(clipped);
66
+ }
67
+ function renderScope(input, sectionBody, activeLessons, totalEntries, opts, out) {
68
+ const label = formatScope(input.scope);
69
+ const section = scopeSectionSlug(input.scope);
70
+ const halfLife = opts.recencyHalfLifeDays ?? DEFAULT_RECENCY_HALFLIFE_DAYS;
71
+ // `current_bytes` / `current_entries` describe the on-disk `## Lessons`
72
+ // SECTION (active + provisional) — the §6 cap unit
73
+ // (`lessonsSectionByteLength` in lesson-format.ts), the same unit the
74
+ // nightly worksheet's `over_cap` and the eviction engine measure. The
75
+ // whole-file measure used previously disagreed with the nightly pass in a
76
+ // narrow band (frontmatter + `# heading` overhead), making the two
77
+ // worksheets contradict each other on the same store. `over_cap` covers
78
+ // the full entry set, not just the collapsible active subset — the LLM's
79
+ // Step-12 eviction targets the disk cap. The caller extracted
80
+ // `sectionBody` once during eligibility (a scope is only eligible when
81
+ // its `## Lessons` section parsed), so it is measured here verbatim.
82
+ const currentBytes = Buffer.byteLength(sectionBody, "utf-8");
83
+ const overCap = currentBytes > input.caps.capBytes || totalEntries > input.caps.maxEntries;
84
+ const provisionalHeld = totalEntries - activeLessons.length;
85
+ // Ascending score → rank 1 = lowest score = drop-first, the same convention
86
+ // the evening worksheet uses so the LLM reads both with one mental model.
87
+ const ranked = [...activeLessons].sort((a, b) => scoreLesson(a, opts.nowIso, undefined, halfLife) -
88
+ scoreLesson(b, opts.nowIso, undefined, halfLife));
89
+ out.push(` <scope label="${xmlEscape(label)}" store="${xmlEscape(input.storeFile)}" ` +
90
+ `section="${xmlEscape(section)}" ` +
91
+ `cap_bytes="${input.caps.capBytes}" max_entries="${input.caps.maxEntries}" ` +
92
+ `current_bytes="${currentBytes}" current_entries="${totalEntries}" ` +
93
+ `provisional_held="${provisionalHeld}" over_cap="${overCap}">`);
94
+ out.push(` <lessons note="active (non-provisional) lessons only, ranked by eviction ` +
95
+ `score (rank 1 = lowest, drop-first); cluster lessons that share a theme ` +
96
+ `and collapse each cluster into ONE higher-level principle; drop any lesson ` +
97
+ `marked stale=&quot;true&quot; unless it joins a cluster; never collapse ` +
98
+ `across a contradiction; preserve any provisional lesson in the file ` +
99
+ `byte-for-byte — they await corroboration and are not yours to collapse or promote">`);
100
+ ranked.forEach((lesson, idx) => {
101
+ out.push(` <lesson rank="${idx + 1}" score="${round2(scoreLesson(lesson, opts.nowIso, undefined, halfLife))}" ev="${lesson.ev}" kind="${lesson.kind}" last="${lesson.last}" ` +
102
+ `provisional="${lesson.provisional}" ` +
103
+ `stale="${isLessonStale(lesson, opts.nowIso, opts.staleDays)}">` +
104
+ `${inline(lesson.text)}</lesson>`);
105
+ });
106
+ out.push(" </lessons>");
107
+ out.push(" </scope>");
108
+ }
109
+ /**
110
+ * Compose the `<feedback_regeneralization>` block. Returns `null` when no scope
111
+ * holds at least {@link MIN_LESSONS_FOR_REGENERALIZATION} *active*
112
+ * (non-provisional) lessons — there is nothing to collapse, so the caller
113
+ * stamps nothing (no empty block in the prompt). Provisional lessons are
114
+ * excluded from the collapse set (see module header) but still counted in each
115
+ * scope's `current_entries` / `over_cap` so the cap status stays whole-file
116
+ * truthful. Scopes are emitted in input order.
117
+ */
118
+ export function buildRegeneralizationWorksheet(scopes, opts) {
119
+ const eligible = [];
120
+ for (const input of scopes) {
121
+ // Single extraction per scope — `renderScope` reuses this body for its
122
+ // `current_bytes` measure instead of re-extracting (the old double
123
+ // extraction left renderScope with an unreachable missing-section arm).
124
+ const sectionBody = extractMarkdownSection(input.existingFileMd, "Lessons");
125
+ if (!sectionBody)
126
+ continue;
127
+ const allLessons = parseLessonsSection(sectionBody);
128
+ // Collapse the ACTIVE set only — provisional lessons are owned by the
129
+ // evening promotion gate (see module header); merging them here would
130
+ // bypass the `ignored`-only-never-promotes guard.
131
+ const active = allLessons.filter((lesson) => !lesson.provisional);
132
+ if (active.length >= MIN_LESSONS_FOR_REGENERALIZATION) {
133
+ eligible.push({ input, sectionBody, active, totalEntries: allLessons.length });
134
+ }
135
+ }
136
+ if (eligible.length === 0)
137
+ return null;
138
+ const out = [];
139
+ out.push(`<feedback_regeneralization generated_at="${xmlEscape(opts.nowIso)}" ` +
140
+ `scopes="${eligible.length}">`);
141
+ let lessonCount = 0;
142
+ for (const { input, sectionBody, active, totalEntries } of eligible) {
143
+ renderScope(input, sectionBody, active, totalEntries, opts, out);
144
+ lessonCount += active.length;
145
+ }
146
+ out.push("</feedback_regeneralization>");
147
+ return {
148
+ block: out.join("\n"),
149
+ scopeCount: eligible.length,
150
+ lessonCount,
151
+ };
152
+ }