@aitne/daemon 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/dist/adapters/adapter-watchdog.d.ts +70 -0
  2. package/dist/adapters/adapter-watchdog.js +115 -0
  3. package/dist/adapters/discord.d.ts +17 -1
  4. package/dist/adapters/discord.js +33 -0
  5. package/dist/adapters/notification-manager.d.ts +27 -1
  6. package/dist/adapters/notification-manager.js +54 -39
  7. package/dist/adapters/slack-adapter.d.ts +26 -1
  8. package/dist/adapters/slack-adapter.js +41 -0
  9. package/dist/adapters/telegram-adapter.d.ts +18 -1
  10. package/dist/adapters/telegram-adapter.js +41 -2
  11. package/dist/adapters/types.d.ts +20 -0
  12. package/dist/adapters/whatsapp-adapter.d.ts +26 -7
  13. package/dist/adapters/whatsapp-adapter.js +74 -21
  14. package/dist/api/env-writer.js +8 -5
  15. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  16. package/dist/api/helpers/agent-errors-registry.js +5 -5
  17. package/dist/api/routes/agent.js +33 -12
  18. package/dist/api/routes/agents/index.js +75 -16
  19. package/dist/api/routes/agents/views.d.ts +37 -2
  20. package/dist/api/routes/agents/views.js +64 -2
  21. package/dist/api/routes/background-task.d.ts +22 -0
  22. package/dist/api/routes/background-task.js +338 -0
  23. package/dist/api/routes/browser-history.js +9 -1
  24. package/dist/api/routes/context/permissions.js +3 -2
  25. package/dist/api/routes/context/snapshots.js +0 -3
  26. package/dist/api/routes/context/write.js +3 -17
  27. package/dist/api/routes/dashboard/config.js +48 -12
  28. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  29. package/dist/api/routes/dashboard/notifications.js +9 -9
  30. package/dist/api/routes/integrations/crud-patch.js +5 -1
  31. package/dist/api/routes/integrations-reconcile.js +2 -2
  32. package/dist/api/routes/notion.d.ts +1 -1
  33. package/dist/api/routes/observations.js +7 -7
  34. package/dist/api/routes/obsidian.d.ts +1 -1
  35. package/dist/api/routes/receipts.js +5 -1
  36. package/dist/api/routes/setup-migrate.js +1 -1
  37. package/dist/api/routes/setup.js +1 -1
  38. package/dist/api/routes/task-flows.d.ts +1 -1
  39. package/dist/api/routes/task-flows.js +1 -1
  40. package/dist/api/routes/tuning.d.ts +29 -0
  41. package/dist/api/routes/tuning.js +304 -0
  42. package/dist/api/server.d.ts +44 -16
  43. package/dist/api/server.js +9 -0
  44. package/dist/bootstrap/adapters.d.ts +19 -0
  45. package/dist/bootstrap/adapters.js +61 -0
  46. package/dist/bootstrap/api.d.ts +5 -3
  47. package/dist/bootstrap/api.js +45 -13
  48. package/dist/bootstrap/catchup.d.ts +1 -1
  49. package/dist/bootstrap/catchup.js +11 -11
  50. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  51. package/dist/bootstrap/event-pipeline.js +245 -7
  52. package/dist/bootstrap/observers.js +9 -6
  53. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  54. package/dist/bootstrap/schedule-helpers.js +172 -19
  55. package/dist/config.js +26 -12
  56. package/dist/core/agent-core.d.ts +33 -1
  57. package/dist/core/agent-core.js +36 -1
  58. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  59. package/dist/core/agents/activity-scan-cadence.js +127 -0
  60. package/dist/core/agents/agent-route-override.d.ts +53 -0
  61. package/dist/core/agents/agent-route-override.js +69 -0
  62. package/dist/core/agents/builtin-registry.d.ts +51 -14
  63. package/dist/core/agents/builtin-registry.js +92 -15
  64. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  65. package/dist/core/agents/config-gate-reconcile.js +51 -0
  66. package/dist/core/agents/cron-substitute.d.ts +1 -1
  67. package/dist/core/agents/cron-substitute.js +1 -1
  68. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  69. package/dist/core/agents/custom-routine-migration.js +149 -0
  70. package/dist/core/agents/firing-blocked.d.ts +1 -1
  71. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  72. package/dist/core/agents/hourly-cadence.js +126 -0
  73. package/dist/core/agents/loader-boot.js +23 -0
  74. package/dist/core/agents/loader.d.ts +19 -0
  75. package/dist/core/agents/loader.js +34 -2
  76. package/dist/core/agents/override-merge.d.ts +1 -1
  77. package/dist/core/agents/override-merge.js +9 -1
  78. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  79. package/dist/core/agents/recurrence-convert.js +1 -1
  80. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  81. package/dist/core/alerts.js +6 -6
  82. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  83. package/dist/core/backends/auth-health-monitor.js +1 -1
  84. package/dist/core/backends/backend-router.d.ts +27 -1
  85. package/dist/core/backends/backend-router.js +165 -1
  86. package/dist/core/backends/claude-code-core.d.ts +71 -31
  87. package/dist/core/backends/claude-code-core.js +282 -54
  88. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  89. package/dist/core/backends/cli-quota-guards.js +40 -5
  90. package/dist/core/backends/codex-core.d.ts +6 -0
  91. package/dist/core/backends/codex-core.js +22 -6
  92. package/dist/core/backends/failure-spend.d.ts +58 -0
  93. package/dist/core/backends/failure-spend.js +137 -0
  94. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  95. package/dist/core/backends/gemini-cli-core.js +25 -6
  96. package/dist/core/backends/model-registry.d.ts +1 -1
  97. package/dist/core/backends/model-registry.js +4 -4
  98. package/dist/core/backends/opencode-core.d.ts +1 -1
  99. package/dist/core/backends/opencode-core.js +5 -5
  100. package/dist/core/backends/plan-presets.js +39 -15
  101. package/dist/core/bang-commands/commands-cost.js +3 -1
  102. package/dist/core/bang-commands/commands-report.js +4 -3
  103. package/dist/core/bang-commands/commands-research.js +4 -1
  104. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  105. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  106. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  107. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  108. package/dist/core/bang-commands/commands-task-control.js +147 -0
  109. package/dist/core/bang-commands/commands-wiki.js +5 -5
  110. package/dist/core/bang-commands/index.d.ts +2 -0
  111. package/dist/core/bang-commands/index.js +12 -0
  112. package/dist/core/bang-commands/registry.d.ts +12 -0
  113. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  114. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  115. package/dist/core/channel-timeline.d.ts +5 -1
  116. package/dist/core/channel-timeline.js +13 -0
  117. package/dist/core/context/index-reconciler.js +5 -2
  118. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  119. package/dist/core/context/policy-index-runner.js +25 -6
  120. package/dist/core/context-builder-calendar.js +10 -2
  121. package/dist/core/context-builder-conversation.d.ts +8 -1
  122. package/dist/core/context-builder-conversation.js +41 -7
  123. package/dist/core/context-builder-yesterday.js +4 -3
  124. package/dist/core/context-builder.d.ts +7 -2
  125. package/dist/core/context-builder.js +62 -20
  126. package/dist/core/context-file-serializer.d.ts +1 -1
  127. package/dist/core/context-file-serializer.js +1 -1
  128. package/dist/core/context-health.js +2 -2
  129. package/dist/core/context-paths.d.ts +1 -1
  130. package/dist/core/context-paths.js +1 -1
  131. package/dist/core/context-validation/prepare-write.js +1 -1
  132. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  133. package/dist/core/context-vault-aliases.d.ts +0 -13
  134. package/dist/core/context-vault-aliases.js +37 -0
  135. package/dist/core/custom-routines.d.ts +99 -0
  136. package/dist/core/custom-routines.js +187 -0
  137. package/dist/core/daemon-api-cli.js +49 -0
  138. package/dist/core/day-boundary.d.ts +46 -0
  139. package/dist/core/day-boundary.js +40 -0
  140. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  141. package/dist/core/dispatcher-activity-scan.js +775 -0
  142. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  143. package/dist/core/dispatcher-error-handling.js +38 -62
  144. package/dist/core/dispatcher-hourly-check.js +6 -1
  145. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  146. package/dist/core/dispatcher-message-handler.js +17 -0
  147. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  148. package/dist/core/dispatcher-morning-routine.js +13 -13
  149. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  150. package/dist/core/dispatcher-result-processor.js +167 -11
  151. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  152. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  153. package/dist/core/dispatcher-scheduled-tasks.d.ts +63 -1
  154. package/dist/core/dispatcher-scheduled-tasks.js +213 -6
  155. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  156. package/dist/core/dispatcher-task-delivery.js +555 -0
  157. package/dist/core/dispatcher-types.d.ts +48 -9
  158. package/dist/core/dispatcher-types.js +3 -3
  159. package/dist/core/dispatcher.d.ts +112 -31
  160. package/dist/core/dispatcher.js +284 -59
  161. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  162. package/dist/core/drift-effects.js +2 -2
  163. package/dist/core/feedback/consolidation-prep.js +17 -5
  164. package/dist/core/feedback/eviction-scorer.js +6 -2
  165. package/dist/core/feedback/lesson-format.js +9 -4
  166. package/dist/core/feedback/lesson-injection.d.ts +1 -1
  167. package/dist/core/feedback/lesson-injection.js +17 -2
  168. package/dist/core/feedback/lesson-store-overview.d.ts +8 -4
  169. package/dist/core/feedback/lesson-store-overview.js +8 -4
  170. package/dist/core/feedback/regeneralization-prep.js +29 -16
  171. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  172. package/dist/core/feedback/self-performance-prep.js +541 -0
  173. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  174. package/dist/core/feedback/tuning-actuator.js +432 -0
  175. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  176. package/dist/core/feedback/tuning-recommender.js +580 -0
  177. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  178. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  179. package/dist/core/health-monitor.d.ts +6 -0
  180. package/dist/core/health-monitor.js +1 -1
  181. package/dist/core/injection-policy.d.ts +4 -4
  182. package/dist/core/injection-policy.js +4 -4
  183. package/dist/core/integration-main-backend.js +4 -0
  184. package/dist/core/management-md.d.ts +2 -2
  185. package/dist/core/management-md.js +51 -13
  186. package/dist/core/morning/orchestrator.d.ts +2 -2
  187. package/dist/core/morning/orchestrator.js +2 -2
  188. package/dist/core/notification-gate.d.ts +64 -0
  189. package/dist/core/notification-gate.js +51 -0
  190. package/dist/core/notification-rate-limit.d.ts +40 -0
  191. package/dist/core/notification-rate-limit.js +50 -0
  192. package/dist/core/policy-files.d.ts +1 -1
  193. package/dist/core/policy-files.js +2 -2
  194. package/dist/core/pre-pass-freshness.d.ts +4 -4
  195. package/dist/core/retention.d.ts +5 -0
  196. package/dist/core/retention.js +20 -4
  197. package/dist/core/review-context.d.ts +1 -1
  198. package/dist/core/review-context.js +10 -5
  199. package/dist/core/roadmap-write-lock.d.ts +2 -1
  200. package/dist/core/roadmap-write-lock.js +15 -10
  201. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  202. package/dist/core/routine-acquisition-plan.js +78 -20
  203. package/dist/core/routine-fetch-window-retry.js +7 -4
  204. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  205. package/dist/core/routine-fetch-window-runner.js +264 -13
  206. package/dist/core/routine-windows.d.ts +2 -2
  207. package/dist/core/routine-windows.js +8 -5
  208. package/dist/core/scheduler.d.ts +175 -16
  209. package/dist/core/scheduler.js +559 -102
  210. package/dist/core/signal-detector.d.ts +12 -0
  211. package/dist/core/signal-detector.js +53 -9
  212. package/dist/core/skills-compiler-denied-tools.js +2 -2
  213. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  214. package/dist/core/skills-compiler-skill-index.js +2 -2
  215. package/dist/core/skills-compiler-variants.d.ts +1 -1
  216. package/dist/core/skills-compiler-variants.js +8 -0
  217. package/dist/core/skills-compiler.d.ts +29 -26
  218. package/dist/core/skills-compiler.js +117 -81
  219. package/dist/core/skills-manifest.d.ts +37 -0
  220. package/dist/core/skills-manifest.js +73 -2
  221. package/dist/core/sleep-inhibitor.d.ts +79 -0
  222. package/dist/core/sleep-inhibitor.js +132 -0
  223. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  224. package/dist/core/slim-system-prompt-loader.js +141 -0
  225. package/dist/core/spawn-gates.d.ts +126 -0
  226. package/dist/core/spawn-gates.js +180 -0
  227. package/dist/core/today-direct-writer.d.ts +2 -2
  228. package/dist/core/today-direct-writer.js +1 -1
  229. package/dist/core/today-write-lock.d.ts +4 -2
  230. package/dist/core/today-write-lock.js +30 -20
  231. package/dist/core/wake-detector.d.ts +55 -0
  232. package/dist/core/wake-detector.js +80 -0
  233. package/dist/core/wiki/compile-lock.d.ts +1 -1
  234. package/dist/core/wiki/compile-lock.js +1 -1
  235. package/dist/core/workdir.js +15 -6
  236. package/dist/db/activity-scan-signals.d.ts +77 -0
  237. package/dist/db/activity-scan-signals.js +378 -0
  238. package/dist/db/agents-store.d.ts +28 -0
  239. package/dist/db/agents-store.js +62 -0
  240. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  241. package/dist/db/background-task-clarifications-store.js +152 -0
  242. package/dist/db/background-task-store.d.ts +207 -0
  243. package/dist/db/background-task-store.js +380 -0
  244. package/dist/db/browser-history-store.d.ts +39 -6
  245. package/dist/db/browser-history-store.js +51 -7
  246. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  247. package/dist/db/browser-task-clarifications-store.js +35 -5
  248. package/dist/db/browser-task-store.d.ts +3 -0
  249. package/dist/db/browser-task-store.js +29 -4
  250. package/dist/db/deferred-dm.d.ts +86 -0
  251. package/dist/db/deferred-dm.js +199 -0
  252. package/dist/db/migrations.js +330 -0
  253. package/dist/db/observations.d.ts +2 -2
  254. package/dist/db/observations.js +3 -3
  255. package/dist/db/schema.js +217 -16
  256. package/dist/db/voice-transcripts-store.d.ts +1 -1
  257. package/dist/index.js +86 -29
  258. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  259. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  260. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  261. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  262. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  263. package/dist/observers/delegated-sync-worker.js +10 -10
  264. package/dist/observers/git-delegated-cron.d.ts +1 -1
  265. package/dist/observers/git-delegated-cron.js +2 -2
  266. package/dist/observers/github-poller-classifier.d.ts +3 -3
  267. package/dist/observers/github-poller-classifier.js +3 -3
  268. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  269. package/dist/observers/imminent-event-scheduler.js +1 -1
  270. package/dist/observers/mail-poller.d.ts +1 -0
  271. package/dist/observers/mail-poller.js +42 -3
  272. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  273. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  274. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  275. package/dist/observers/observation-summarizer/worker.js +4 -4
  276. package/dist/observers/obsidian-watcher.d.ts +1 -1
  277. package/dist/observers/obsidian-watcher.js +1 -1
  278. package/dist/safety/agent-write-tracker.d.ts +4 -4
  279. package/dist/safety/agent-write-tracker.js +4 -4
  280. package/dist/safety/audit.d.ts +43 -5
  281. package/dist/safety/audit.js +86 -18
  282. package/dist/safety/risk-classifier.d.ts +6 -0
  283. package/dist/safety/risk-classifier.js +75 -11
  284. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  285. package/dist/scheduler/activity-scan-gate.js +132 -0
  286. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  287. package/dist/services/background-task/background-task-budget.js +91 -0
  288. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  289. package/dist/services/background-task/background-task-driver.js +416 -0
  290. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  291. package/dist/services/background-task/background-task-runner.js +673 -0
  292. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  293. package/dist/services/background-task/background-task-tools.js +247 -0
  294. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  295. package/dist/services/background-task/background-task-transition-events.js +54 -0
  296. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  297. package/dist/services/browser-history/automation/egress-denylist.js +16 -6
  298. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  299. package/dist/services/browser-task/browser-task-runner.js +53 -8
  300. package/dist/services/observations-batch.d.ts +1 -1
  301. package/dist/services/observations-batch.js +2 -2
  302. package/dist/settings/runtime-settings.d.ts +38 -11
  303. package/dist/settings/runtime-settings.js +203 -40
  304. package/dist/settings/settings-store.js +11 -3
  305. package/package.json +4 -4
@@ -11,6 +11,8 @@
11
11
  import { existsSync, readFileSync } from "node:fs";
12
12
  import { formatSqliteDatetime, getAgentDayBoundsUtc, getAgentDayDateStr, getAgentDayProgressMinutes, nowInTimezone, parseSqliteUtcMs, } from "@aitne/shared";
13
13
  import { hasActionInWindow } from "../core/schedule-maintenance.js";
14
+ import { getAgentEnabled, getRuntimeWindow } from "../db/agents-store.js";
15
+ import { resolveActivityScanCadence } from "../core/agents/activity-scan-cadence.js";
14
16
  /**
15
17
  * Days of week on which the boot-time catchup will fire an unrun
16
18
  * `routine.weekly_review`. Friday is the canonical slot; Saturday and
@@ -64,11 +66,10 @@ export function getDueCatchupRoutines(db, config, agentDayStartUtc, agentDayEndU
64
66
  routines.push("weekly_review");
65
67
  }
66
68
  }
67
- // Monthly catchup is gated by the same kill switch as the scheduler
68
- // cron (see scheduler.ts comment block). Default OFF pre-release until
69
- // the Mirror+Prune redesign; operators opt in via
70
- // PA_MONTHLY_REVIEW_ENABLED or PATCH /api/config.
71
- if (config.monthlyReviewEnabled &&
69
+ // Monthly catchup is gated by the same switch as the scheduler cron:
70
+ // the monthly-review AGENT row's `enabled` (default OFF pre-release until
71
+ // the Mirror+Prune redesign; operators opt in from /agents/monthly-review).
72
+ if (getAgentEnabled(db, "monthly-review", false) &&
72
73
  tomorrowLocal.day === 1 &&
73
74
  !hasActionInWindow(db, "routine.monthly_review", agentDayStartUtc, agentDayEndUtc)) {
74
75
  routines.push("monthly_review");
@@ -77,11 +78,11 @@ export function getDueCatchupRoutines(db, config, agentDayStartUtc, agentDayEndU
77
78
  }
78
79
  /**
79
80
  * Decide whether the boot sequence should immediately fire one
80
- * catch-up `routine.hourly_check` (because the cron callback never ran
81
+ * catch-up `routine.activity_scan` (because the cron callback never ran
81
82
  * for the current slot — typically because the host was asleep / the
82
83
  * daemon was stopped during the slot window).
83
84
  *
84
- * Slot math mirrors `shouldFireHourlyTickAt` in `scheduler.ts` so the
85
+ * Slot math mirrors `shouldFireActivityScanTickAt` in `scheduler.ts` so the
85
86
  * catch-up always lands on the same slot the cron would have fired at.
86
87
  *
87
88
  * **Wrap-around active hours are NOT supported.** The active-hours
@@ -101,31 +102,35 @@ export function getDueCatchupRoutines(db, config, agentDayStartUtc, agentDayEndU
101
102
  * config-write time, or (b) split the window into two non-wrap
102
103
  * ranges in the same call site that consumes them.
103
104
  */
104
- export function shouldCatchUpHourlyCheck(db, config, now) {
105
- if (!config.hourlyCheckEnabled) {
105
+ export function shouldCatchUpActivityScan(db, config, now) {
106
+ // `agents.enabled` on the activity-scan row is the single on/off switch
107
+ // (AGENTS_HUB_REDESIGN_PLAN.md §2); cadence comes from the row's
108
+ // runtime_window with the legacy config keys as fallback.
109
+ if (!getAgentEnabled(db, "activity-scan", true)) {
106
110
  return false;
107
111
  }
112
+ const cadence = resolveActivityScanCadence(getRuntimeWindow(db, "activity-scan"), config);
108
113
  const tz = config.timezone || undefined;
109
114
  const local = nowInTimezone(tz, now);
110
- if (local.hours < config.hourlyCheckActiveStartHour ||
111
- local.hours >= config.hourlyCheckActiveEndHour ||
115
+ if (local.hours < cadence.activeStartHour ||
116
+ local.hours >= cadence.activeEndHour ||
112
117
  local.hours === config.dayBoundaryHour) {
113
118
  return false;
114
119
  }
115
- // Slot anchors to `activeStartHour`, mirroring shouldFireHourlyTickAt
120
+ // Slot anchors to `activeStartHour`, mirroring shouldFireActivityScanTickAt
116
121
  // in scheduler.ts so the catch-up function picks the same slot the
117
122
  // cron callback would have fired at. The earlier branch already
118
123
  // returned false when local.hours < activeStartHour, so the offset is
119
124
  // always non-negative here.
120
- const anchorMinutes = config.hourlyCheckActiveStartHour * 60;
125
+ const anchorMinutes = cadence.activeStartHour * 60;
121
126
  const offsetFromAnchor = local.hours * 60 + local.minutes - anchorMinutes;
122
- const slotOffsetFromAnchor = Math.floor(offsetFromAnchor / config.hourlyCheckIntervalMinutes) *
123
- config.hourlyCheckIntervalMinutes;
127
+ const slotOffsetFromAnchor = Math.floor(offsetFromAnchor / cadence.intervalMinutes) *
128
+ cadence.intervalMinutes;
124
129
  const slotMinutesSinceMidnight = anchorMinutes + slotOffsetFromAnchor;
125
130
  const dayStartUtc = getAgentDayBoundsUtc(tz, 0, now).start;
126
131
  const slotStartMs = parseSqliteUtcMs(dayStartUtc) + slotMinutesSinceMidnight * 60 * 1000;
127
132
  const slotStartUtc = formatSqliteDatetime(new Date(slotStartMs));
128
- return !hasActionInWindow(db, "routine.hourly_check", slotStartUtc, formatSqliteDatetime(now));
133
+ return !hasActionInWindow(db, "routine.activity_scan", slotStartUtc, formatSqliteDatetime(now));
129
134
  }
130
135
  export function getProgressMinutesForHour(hour, dayBoundaryHour) {
131
136
  const scheduledMinutes = hour * 60;
@@ -147,7 +152,7 @@ export function hasFreshAgentDayTodayMd(todayMdPath, timezone, dayBoundaryHour,
147
152
  }
148
153
  /**
149
154
  * Did `routine.morning_routine` complete successfully within the current
150
- * agent-day window? Used by the pre-routine gate that fronts hourly_check
155
+ * agent-day window? Used by the pre-routine gate that fronts activity_scan
151
156
  * and the review routines (evening / weekly / monthly) so they refuse to
152
157
  * run before the day has been properly opened.
153
158
  *
@@ -231,10 +236,10 @@ export function readMorningRoutineStallThresholdMinutes(db) {
231
236
  * without producing a successful `agent_actions` row. Returns the
232
237
  * offending row's metadata if stalled, null when the system is healthy.
233
238
  *
234
- * Pairs with `queueMorningRoutineWake` + the hourly-check pre-routine
239
+ * Pairs with `queueMorningRoutineWake` + the activity-scan pre-routine
235
240
  * gate. The dedup that keeps `queueMorningRoutineWake` from re-inserting
236
241
  * means a stuck wake row leaves the system in a silent freeze — the gate
237
- * skips `routine.hourly_check`, `routine.evening_review`, etc. forever
242
+ * skips `routine.activity_scan`, `routine.evening_review`, etc. forever
238
243
  * without surfacing to the user. This helper is the externally visible
239
244
  * signal the watchdog uses to break the silence.
240
245
  *
@@ -277,6 +282,154 @@ export function getStalledMorningRoutineWake(db, agentDayConfig, thresholdMinute
277
282
  ageMinutes,
278
283
  };
279
284
  }
285
+ /**
286
+ * Audit-row action types that count as "a morning-routine attempt started"
287
+ * for the missed-fire predicate below. The parent row
288
+ * (`routine.morning_routine`) is written when the pipeline finishes
289
+ * (success or failure); the Stage A row
290
+ * (`routine.morning_routine_today`) is written `in_progress` the moment
291
+ * the agent execution starts, so a hung Stage A is still visible as an
292
+ * attempt. The fetch-window pre-pass is deliberately NOT included — it
293
+ * also runs for hourly/scheduled flows, and counting it would let an
294
+ * unrelated pre-pass mask a dead morning pipeline.
295
+ */
296
+ export const MORNING_ROUTINE_ATTEMPT_ACTION_TYPES = [
297
+ "routine.morning_routine",
298
+ "routine.morning_routine_today",
299
+ ];
300
+ /**
301
+ * Grace period after the agent-day boundary before the missed-fire
302
+ * self-heal may conclude the 04:00 cron tick was swallowed. Wide enough
303
+ * that the normal cron → queueMorningRoutineWake → ScheduleWatcher claim
304
+ * → Stage A start chain has long since produced either a wake row or an
305
+ * attempt row; short enough that a swallowed tick costs at most ~25 min
306
+ * (grace + one self-heal interval) once the machine is awake.
307
+ */
308
+ export const MORNING_MISSED_FIRE_GRACE_MINUTES = 15;
309
+ /**
310
+ * Epoch ms of the most recent morning-routine attempt (any result,
311
+ * including `in_progress`) started within the current agent-day, or null
312
+ * when nothing has been attempted yet. Pure read.
313
+ */
314
+ export function getLatestMorningAttemptStartMs(db, agentDayConfig, now) {
315
+ const { start } = getAgentDayBoundsUtc(agentDayConfig.timezone, agentDayConfig.dayBoundaryHour, now);
316
+ const row = db
317
+ .prepare(`SELECT MAX(started_at) AS latest
318
+ FROM agent_actions
319
+ WHERE action_type IN (${MORNING_ROUTINE_ATTEMPT_ACTION_TYPES.map(() => "?").join(",")})
320
+ AND started_at >= ?`)
321
+ .get(...MORNING_ROUTINE_ATTEMPT_ACTION_TYPES, start);
322
+ if (!row.latest)
323
+ return null;
324
+ return parseSqliteUtcMs(row.latest);
325
+ }
326
+ /**
327
+ * Detect the hung-execution variant of the morning-routine silent stall:
328
+ * a wake row claimed to `running` at least `thresholdMinutes` ago with
329
+ * still no `routine.morning_routine` success for the current agent-day
330
+ * (machine slept mid-run and the backend stream died on the network
331
+ * change, the SDK call wedged, or the dispatch event was lost).
332
+ *
333
+ * `queueMorningRoutineWake`'s dedup treats a `running` row as "already
334
+ * in flight" and merges into it forever, so without recovery the day
335
+ * stays frozen until a daemon restart. The caller (scheduler self-heal
336
+ * tick) flips the returned row back to `pending` so the ScheduleWatcher
337
+ * re-claims it; the today-write-lock's wall-clock TTL guarantees a hung
338
+ * original cannot hold the lock against the re-run indefinitely.
339
+ *
340
+ * Staleness is measured from `task_context.claimedAt`, stamped by the
341
+ * ScheduleWatcher at claim time. That is the only signal that survives
342
+ * every confounder: `created_at` predates sleeps that delay the claim,
343
+ * `scheduled_for` is bumped by dedup merges, and attempt audit rows are
344
+ * windowed to the current agent-day (a run that started before the
345
+ * 04:00 boundary and hung across it has no attempt row "today").
346
+ * Invariants this leans on:
347
+ * - boot recovery (`recoverOrphanedRunningSchedules`) clears every
348
+ * `running` row at daemon start, so a live `running` row was always
349
+ * claimed by the current process — which always stamps;
350
+ * - `queueMorningRoutineWake`'s dedup-merge preserves unknown
351
+ * task_context keys, so a 04:00 cron merge cannot strip the stamp.
352
+ * A row without a readable stamp (stamp UPDATE failed, malformed JSON)
353
+ * is left to the alert-only watchdog rather than guessed at.
354
+ *
355
+ * Pure read — the caller owns the UPDATE.
356
+ */
357
+ export function getRecoverableStalledMorningWake(db, agentDayConfig, thresholdMinutes, now) {
358
+ const reference = now ?? new Date();
359
+ if (morningRoutineRanToday(db, agentDayConfig, reference)) {
360
+ return null;
361
+ }
362
+ const row = db
363
+ .prepare(`SELECT id, task_context
364
+ FROM agent_schedule
365
+ WHERE task_type = 'wake'
366
+ AND status = 'running'
367
+ AND json_extract(task_context, '$.routine') = 'morning_routine'
368
+ ORDER BY created_at ASC
369
+ LIMIT 1`)
370
+ .get();
371
+ if (!row)
372
+ return null;
373
+ let claimedAt;
374
+ try {
375
+ // The WHERE's json_extract guarantees task_context is non-null and
376
+ // valid to SQLite's JSON parser — but SQLite ≥3.42 accepts JSON5,
377
+ // which JSON.parse rejects, so the parse can still throw.
378
+ claimedAt = JSON.parse(row.task_context).claimedAt;
379
+ }
380
+ catch {
381
+ return null;
382
+ }
383
+ if (typeof claimedAt !== "string")
384
+ return null;
385
+ const claimedMs = parseSqliteUtcMs(claimedAt);
386
+ if (!Number.isFinite(claimedMs))
387
+ return null;
388
+ const claimedAgeMinutes = Math.floor((reference.getTime() - claimedMs) / 60_000);
389
+ if (claimedAgeMinutes < thresholdMinutes)
390
+ return null;
391
+ return { id: row.id, claimedAgeMinutes };
392
+ }
393
+ /**
394
+ * Detect the missed-fire variant of the morning-routine silent stall:
395
+ * the machine was asleep at the day-boundary minute, node-cron silently
396
+ * dropped the tick (it never replays missed firings), and nothing since
397
+ * has re-queued the routine. Observable signature, all three at once:
398
+ *
399
+ * 1. no morning-routine attempt has *started* this agent-day (any
400
+ * result — a failed/exhausted retry chain counts as attempted, so
401
+ * this self-heal never resurrects a chain that
402
+ * `scheduleMorningRetry` deliberately stopped after MAX_RETRIES);
403
+ * 2. no `pending`/`running` morning wake row exists (the cron, the
404
+ * wake catch-up, and the retry chain all leave one when they are
405
+ * mid-flight);
406
+ * 3. the agent-day is at least `graceMinutes` old, so a healthy cron
407
+ * fire has had ample time to produce 1 or 2.
408
+ *
409
+ * Complements the WakeDetector: gaps shorter than its 5-min threshold
410
+ * that straddle the boundary minute (lid closed 03:59–04:02), or a
411
+ * detector failure, still converge here within one self-heal interval.
412
+ *
413
+ * Pure read — the caller routes through `queueMorningRoutineWake`,
414
+ * whose DB-backed dedup makes double-queueing impossible.
415
+ */
416
+ export function shouldQueueMissedMorningFire(db, agentDayConfig, graceMinutes, now) {
417
+ const reference = now ?? new Date();
418
+ const progressMinutes = getAgentDayProgressMinutes(agentDayConfig.timezone, agentDayConfig.dayBoundaryHour, reference);
419
+ if (progressMinutes < graceMinutes)
420
+ return false;
421
+ const wakeRow = db
422
+ .prepare(`SELECT 1
423
+ FROM agent_schedule
424
+ WHERE task_type = 'wake'
425
+ AND status IN ('pending', 'running')
426
+ AND json_extract(task_context, '$.routine') = 'morning_routine'
427
+ LIMIT 1`)
428
+ .get();
429
+ if (wakeRow !== undefined)
430
+ return false;
431
+ return (getLatestMorningAttemptStartMs(db, agentDayConfig, reference) === null);
432
+ }
280
433
  // P22 — read the operator's chosen cadence for skill curation runs.
281
434
  // Mirrors the helper in `core/scheduler.ts` so the dispatcher hook here can
282
435
  // resolve cadence at runtime without crossing module boundaries.
package/dist/config.js CHANGED
@@ -99,8 +99,10 @@ export function loadDefaultRuntimeSettings() {
99
99
  // they know the value is now a no-op instead of silently dropping
100
100
  // their override. The runtime-settings Zod schema would otherwise
101
101
  // ignore the unknown key without any signal.
102
+ // Reads the LEGACY env name on purpose — the warning exists for operators
103
+ // who still carry the pre-redesign variable in their .env.
102
104
  if (env("HOURLY_CHECK_GATE_MODE") !== undefined) {
103
- console.warn("[config] PA_HOURLY_CHECK_GATE_MODE is set but no longer honoured — the hourly_check gate now has a single execution path (see HOURLY_CHECK_GATE_REDESIGN_PLAN.md Phase 4). Remove the env var to silence this warning.");
105
+ console.warn("[config] PA_HOURLY_CHECK_GATE_MODE is set but no longer honoured — the activity_scan gate now has a single execution path (see HOURLY_CHECK_GATE_REDESIGN_PLAN.md Phase 4). Remove the env var to silence this warning.");
104
106
  }
105
107
  const parsed = runtimeSettingsSchema.parse({
106
108
  slackOwnerUserId: env("SLACK_OWNER_USER_ID") ?? null,
@@ -133,6 +135,9 @@ export function loadDefaultRuntimeSettings() {
133
135
  proactiveForwardChannelTimelineEnabled: parseBooleanOrDefault(env("PROACTIVE_FORWARD_CHANNEL_TIMELINE_ENABLED"), true),
134
136
  proactiveForwardForceFreshSession: parseBooleanOrDefault(env("PROACTIVE_FORWARD_FORCE_FRESH_SESSION"), false),
135
137
  feedbackLearningEnabled: parseBooleanOrDefault(env("FEEDBACK_LEARNING_ENABLED"), true),
138
+ // SELF_TUNING_REVIEW_CYCLE_DESIGN.md §6 — Phase 3 actuation gate.
139
+ // Default false = shadow mode (recommend + verdict only, no actuation).
140
+ selfTuningEnabled: parseBooleanOrDefault(env("SELF_TUNING_ENABLED"), false),
136
141
  feedbackPromotionThreshold: parseNumberOrDefault(env("FEEDBACK_PROMOTION_THRESHOLD"), 2),
137
142
  feedbackLessonMaxBytesGlobal: parseNumberOrDefault(env("FEEDBACK_LESSON_MAX_BYTES_GLOBAL"), 8192),
138
143
  feedbackLessonMaxBytesPerAgent: parseNumberOrDefault(env("FEEDBACK_LESSON_MAX_BYTES_PER_AGENT"), 4096),
@@ -148,19 +153,26 @@ export function loadDefaultRuntimeSettings() {
148
153
  // at fire time so the flag takes effect on the next month-end without
149
154
  // restart.
150
155
  monthlyReviewEnabled: parseBooleanOrDefault(env("MONTHLY_REVIEW_ENABLED"), false),
151
- hourlyCheckEnabled: parseBooleanOrDefault(env("HOURLY_CHECK_ENABLED"), true),
152
- hourlyCheckIntervalMinutes: parseNumberOrDefault(env("HOURLY_CHECK_INTERVAL_MINUTES"), 60),
153
- hourlyCheckActiveStartHour: parseNumberOrDefault(env("HOURLY_CHECK_ACTIVE_START_HOUR"), 4),
154
- hourlyCheckActiveEndHour: parseNumberOrDefault(env("HOURLY_CHECK_ACTIVE_END_HOUR"), 24),
155
- hourlyCheckMinObservations: parseNumberOrDefault(env("HOURLY_CHECK_MIN_OBSERVATIONS"), 1),
156
- hourlyCheckStage2Enabled: parseBooleanOrDefault(env("HOURLY_CHECK_STAGE2_ENABLED"), false),
157
- hourlyCheckHeartbeatHours: parseNumberOrDefault(env("HOURLY_CHECK_HEARTBEAT_HOURS"), 4),
158
- hourlyCheckLowSignalPendingCeiling: parseNumberOrDefault(env("HOURLY_CHECK_LOW_SIGNAL_PENDING_CEILING"), 0),
159
- hourlyCheckPrePassFreshnessMinutes: parseNumberOrDefault(env("HOURLY_CHECK_PRE_PASS_FRESHNESS_MINUTES"), 30),
156
+ // The `?? env("HOURLY_CHECK_*")` fallbacks honour the pre-rename env
157
+ // names (the agent was "Hourly Check" until v0.1.11) — drop them after
158
+ // a deprecation window alongside LEGACY_RUNTIME_SETTING_KEY_ALIASES.
159
+ activityScanEnabled: parseBooleanOrDefault(env("ACTIVITY_SCAN_ENABLED") ?? env("HOURLY_CHECK_ENABLED"), true),
160
+ // Default 120 (every 2 hours) — see ACTIVITY_SCAN_CADENCE_DEFAULTS.
161
+ activityScanIntervalMinutes: parseNumberOrDefault(env("ACTIVITY_SCAN_INTERVAL_MINUTES") ?? env("HOURLY_CHECK_INTERVAL_MINUTES"), 120),
162
+ activityScanActiveStartHour: parseNumberOrDefault(env("ACTIVITY_SCAN_ACTIVE_START_HOUR") ?? env("HOURLY_CHECK_ACTIVE_START_HOUR"), 4),
163
+ activityScanActiveEndHour: parseNumberOrDefault(env("ACTIVITY_SCAN_ACTIVE_END_HOUR") ?? env("HOURLY_CHECK_ACTIVE_END_HOUR"), 24),
164
+ activityScanMinObservations: parseNumberOrDefault(env("ACTIVITY_SCAN_MIN_OBSERVATIONS") ?? env("HOURLY_CHECK_MIN_OBSERVATIONS"), 1),
165
+ activityScanStage2Enabled: parseBooleanOrDefault(env("ACTIVITY_SCAN_STAGE2_ENABLED") ?? env("HOURLY_CHECK_STAGE2_ENABLED"), false),
166
+ activityScanHeartbeatHours: parseNumberOrDefault(env("ACTIVITY_SCAN_HEARTBEAT_HOURS") ?? env("HOURLY_CHECK_HEARTBEAT_HOURS"), 4),
167
+ activityScanLowSignalPendingCeiling: parseNumberOrDefault(env("ACTIVITY_SCAN_LOW_SIGNAL_PENDING_CEILING")
168
+ ?? env("HOURLY_CHECK_LOW_SIGNAL_PENDING_CEILING"), 0),
169
+ activityScanPrePassFreshnessMinutes: parseNumberOrDefault(env("ACTIVITY_SCAN_PRE_PASS_FRESHNESS_MINUTES")
170
+ ?? env("HOURLY_CHECK_PRE_PASS_FRESHNESS_MINUTES"), 30),
160
171
  authProbeDisabled: parseBooleanOrDefault(env("AUTH_PROBE_DISABLED"), false),
161
172
  authPreflightFreshnessMs: parseNumberOrDefault(env("AUTH_PREFLIGHT_FRESHNESS_MS"), 600000),
162
173
  schedulePollIntervalSeconds: parseNumberOrDefault(env("SCHEDULE_POLL_INTERVAL_SECONDS"), 5),
163
174
  maxBriefingDelayMinutes: parseNumberOrDefault(env("MAX_BRIEFING_DELAY_MINUTES"), 30),
175
+ ownerActivityIdleThresholdMinutes: parseNumberOrDefault(env("OWNER_ACTIVITY_IDLE_THRESHOLD_MINUTES"), 5),
164
176
  maxNotificationsPerHour: parseNumberOrDefault(env("MAX_NOTIFICATIONS_PER_HOUR"), 3),
165
177
  maxNotificationsPerDay: parseNumberOrDefault(env("MAX_NOTIFICATIONS_PER_DAY"), 12),
166
178
  quietHoursStart: envOrDefault("QUIET_HOURS_START", "22:00"),
@@ -185,7 +197,7 @@ export function loadDefaultRuntimeSettings() {
185
197
  githubPollIntervalSeconds: parseNumberOrDefault(env("GITHUB_POLL_INTERVAL_SECONDS"), 1800),
186
198
  // Matches calendar/git poller cadence. Previously 60s, but 3 databases
187
199
  // × 1440 polls/day ≈ 4320 API calls/day was excessive when the only
188
- // downstream consumer is hourly_check. Override with
200
+ // downstream consumer is activity_scan. Override with
189
201
  // PA_NOTION_POLL_INTERVAL_SECONDS if you need closer to real-time
190
202
  // Notion sync (at the cost of API quota). If you raise this beyond
191
203
  // ~10 min, also raise `NOTION_WRITE_TTL_MS` in `api/routes/notion.ts`
@@ -202,7 +214,7 @@ export function loadDefaultRuntimeSettings() {
202
214
  mailIdleFallbackRecoveryMinutes: parseNumberOrDefault(env("MAIL_IDLE_FALLBACK_RECOVERY_MINUTES"), 60),
203
215
  mailMaxMessagesPerPoll: parseNumberOrDefault(env("MAIL_MAX_MESSAGES_PER_POLL"), 20),
204
216
  mailAuthFailureRetryHours: parseNumberOrDefault(env("MAIL_AUTH_FAILURE_RETRY_HOURS"), 6),
205
- hourlyObservationCharBudget: parseNumberOrDefault(env("HOURLY_OBSERVATION_CHAR_BUDGET"), 8000),
217
+ activityScanObservationCharBudget: parseNumberOrDefault(env("ACTIVITY_SCAN_OBSERVATION_CHAR_BUDGET") ?? env("HOURLY_OBSERVATION_CHAR_BUDGET"), 8000),
206
218
  prePassMaxAttemptsPerIntegration: parseNumberOrDefault(env("PRE_PASS_MAX_ATTEMPTS_PER_INTEGRATION"), 3),
207
219
  prePassBackoffMs: parseJsonOrDefault(env("PRE_PASS_BACKOFF_MS"), [1000, 2000, 4000]),
208
220
  prePassRetryEscalationTier: parseNullableStringEnv(env("PRE_PASS_RETRY_ESCALATION_TIER")),
@@ -243,6 +255,8 @@ export function loadDefaultRuntimeSettings() {
243
255
  // installs pre-seed them.
244
256
  primaryLanguage: envOrDefault("PRIMARY_LANGUAGE", "en"),
245
257
  vaultMode: env("VAULT_MODE") ?? "plain",
258
+ // Keep-awake posture (macOS caffeinate); see core/sleep-inhibitor.ts.
259
+ preventSleepMode: env("PREVENT_SLEEP_MODE") ?? "ac",
246
260
  });
247
261
  return normalizeRuntimeSettings(parsed);
248
262
  }
@@ -570,5 +570,37 @@ export declare class BackendDecisiveFailure extends Error {
570
570
  readonly backendId: BackendId;
571
571
  readonly kind: "quota" | "auth" | "max_turns" | "timeout" | "model_unavailable" | "policy_denied" | "other_non_retryable";
572
572
  readonly cause: unknown;
573
- constructor(backendId: BackendId, kind: "quota" | "auth" | "max_turns" | "timeout" | "model_unavailable" | "policy_denied" | "other_non_retryable", cause: unknown);
573
+ /**
574
+ * PREPASS_COST_REDUCTION_PLAN.md N1 — best-effort spend recovered
575
+ * from the failed run when the SDK/CLI surfaced usage before the
576
+ * terminal error (auth rejection mid-run, timeout, transport
577
+ * failure). Same shape as `BackendQuotaError.spend` so the
578
+ * dispatcher's post-hoc audit writer can record what the provider
579
+ * actually billed for a turn that produced no `AgentResult`.
580
+ * `null` when the failure happened before any usage was observed.
581
+ */
582
+ readonly spend: BackendQuotaSpend | null;
583
+ constructor(backendId: BackendId, kind: "quota" | "auth" | "max_turns" | "timeout" | "model_unavailable" | "policy_denied" | "other_non_retryable", cause: unknown,
584
+ /**
585
+ * PREPASS_COST_REDUCTION_PLAN.md N1 — best-effort spend recovered
586
+ * from the failed run when the SDK/CLI surfaced usage before the
587
+ * terminal error (auth rejection mid-run, timeout, transport
588
+ * failure). Same shape as `BackendQuotaError.spend` so the
589
+ * dispatcher's post-hoc audit writer can record what the provider
590
+ * actually billed for a turn that produced no `AgentResult`.
591
+ * `null` when the failure happened before any usage was observed.
592
+ */
593
+ spend?: BackendQuotaSpend | null);
574
594
  }
595
+ /**
596
+ * Recover the spend payload from a backend failover signal, regardless
597
+ * of which of the two error classes carries it. Handles the nested
598
+ * `BackendDecisiveFailure(kind="quota", cause=BackendQuotaError)` wrap
599
+ * the router produces, preferring the inner quota error's spend when
600
+ * both layers carry one. Returns `null` for non-backend errors.
601
+ *
602
+ * PREPASS_COST_REDUCTION_PLAN.md N1 — shared by the dispatcher's
603
+ * post-hoc audit writer and the pre-pass fan-out runner so both
604
+ * failure paths record the same figure for the same error.
605
+ */
606
+ export declare function extractBackendSpend(error: unknown): BackendQuotaSpend | null;
@@ -93,11 +93,46 @@ export class BackendDecisiveFailure extends Error {
93
93
  backendId;
94
94
  kind;
95
95
  cause;
96
- constructor(backendId, kind, cause) {
96
+ spend;
97
+ constructor(backendId, kind, cause,
98
+ /**
99
+ * PREPASS_COST_REDUCTION_PLAN.md N1 — best-effort spend recovered
100
+ * from the failed run when the SDK/CLI surfaced usage before the
101
+ * terminal error (auth rejection mid-run, timeout, transport
102
+ * failure). Same shape as `BackendQuotaError.spend` so the
103
+ * dispatcher's post-hoc audit writer can record what the provider
104
+ * actually billed for a turn that produced no `AgentResult`.
105
+ * `null` when the failure happened before any usage was observed.
106
+ */
107
+ spend = null) {
97
108
  super(`${backendId} decisive failure: ${kind}`);
98
109
  this.backendId = backendId;
99
110
  this.kind = kind;
100
111
  this.cause = cause;
112
+ this.spend = spend;
101
113
  this.name = "BackendDecisiveFailure";
102
114
  }
103
115
  }
116
+ /**
117
+ * Recover the spend payload from a backend failover signal, regardless
118
+ * of which of the two error classes carries it. Handles the nested
119
+ * `BackendDecisiveFailure(kind="quota", cause=BackendQuotaError)` wrap
120
+ * the router produces, preferring the inner quota error's spend when
121
+ * both layers carry one. Returns `null` for non-backend errors.
122
+ *
123
+ * PREPASS_COST_REDUCTION_PLAN.md N1 — shared by the dispatcher's
124
+ * post-hoc audit writer and the pre-pass fan-out runner so both
125
+ * failure paths record the same figure for the same error.
126
+ */
127
+ export function extractBackendSpend(error) {
128
+ if (error instanceof BackendQuotaError) {
129
+ return error.spend;
130
+ }
131
+ if (error instanceof BackendDecisiveFailure) {
132
+ if (error.cause instanceof BackendQuotaError && error.cause.spend) {
133
+ return error.cause.spend;
134
+ }
135
+ return error.spend;
136
+ }
137
+ return null;
138
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Activity-scan cadence resolution (AGENTS_HUB_REDESIGN_PLAN.md §2).
3
+ *
4
+ * The activity-scan Agent's firing window (interval + active hours) and its
5
+ * observation-threshold gate live on the **agent row** —
6
+ * `agents.metadata_json.runtime_window`, written by `PATCH /api/agents/
7
+ * activity-scan` (`schedule_window` body block) and preserved across loader
8
+ * re-runs / `npm i -g` by `loader.ts:nextMetadata`. The `activityScan*`
9
+ * config keys (`hourlyCheck*` before the v0.1.11 rename) are deprecated but
10
+ * still parsed; they act as the per-field fallback so a value an operator
11
+ * persisted pre-redesign keeps working until they touch the agent-level
12
+ * setting.
13
+ *
14
+ * Resolution order, per field: `runtime_window` override → legacy config key
15
+ * (which itself carries the shipped default). Pure module — callers fetch the
16
+ * stored override via `agents-store.ts:getRuntimeWindow` and supply the live
17
+ * config; this keeps the precedence logic in the 100%-coverage set.
18
+ */
19
+ /** Field bounds, shared by the PATCH validator and the sanitizer. */
20
+ export declare const RUNTIME_WINDOW_BOUNDS: {
21
+ readonly interval_minutes: {
22
+ readonly min: 5;
23
+ readonly max: 1440;
24
+ };
25
+ readonly active_start_hour: {
26
+ readonly min: 0;
27
+ readonly max: 23;
28
+ };
29
+ readonly active_end_hour: {
30
+ readonly min: 1;
31
+ readonly max: 24;
32
+ };
33
+ readonly min_observations: {
34
+ readonly min: 0;
35
+ readonly max: 1000;
36
+ };
37
+ };
38
+ export type RuntimeWindowField = keyof typeof RUNTIME_WINDOW_BOUNDS;
39
+ export declare const RUNTIME_WINDOW_FIELDS: readonly RuntimeWindowField[];
40
+ /**
41
+ * The persisted shape under `metadata_json.runtime_window`. Every field is
42
+ * optional — only operator-touched fields are stored, so an untouched field
43
+ * keeps tracking the config fallback.
44
+ */
45
+ export interface RuntimeWindowOverride {
46
+ interval_minutes?: number;
47
+ active_start_hour?: number;
48
+ active_end_hour?: number;
49
+ min_observations?: number;
50
+ }
51
+ /**
52
+ * The legacy config keys the resolver falls back to. Fields are optional at
53
+ * the type level so partially-stubbed test configs (and any pre-schema boot
54
+ * edge) resolve to the shipped defaults instead of producing a `NaN` cron.
55
+ */
56
+ export interface ActivityScanCadenceConfig {
57
+ activityScanIntervalMinutes?: number;
58
+ activityScanActiveStartHour?: number;
59
+ activityScanActiveEndHour?: number;
60
+ activityScanMinObservations?: number;
61
+ }
62
+ /** Shipped defaults — mirror `runtime-settings.ts` (`activityScan*` keys). */
63
+ export declare const ACTIVITY_SCAN_CADENCE_DEFAULTS: {
64
+ readonly intervalMinutes: 120;
65
+ readonly activeStartHour: 4;
66
+ readonly activeEndHour: 24;
67
+ readonly minObservations: 1;
68
+ };
69
+ /** Fully-resolved cadence every consumer (scheduler, gate, API) reads. */
70
+ export interface ResolvedActivityScanCadence {
71
+ intervalMinutes: number;
72
+ activeStartHour: number;
73
+ activeEndHour: number;
74
+ minObservations: number;
75
+ }
76
+ /** True when `value` is an integer within the field's bounds. */
77
+ export declare function isValidRuntimeWindowValue(field: RuntimeWindowField, value: unknown): value is number;
78
+ /**
79
+ * Sanitize a raw `metadata_json.runtime_window` blob (untrusted: hand-edited
80
+ * DBs, older daemons). Out-of-bounds / non-integer fields are dropped — the
81
+ * resolver then falls back to config for them rather than failing the boot.
82
+ */
83
+ export declare function parseRuntimeWindowOverride(value: unknown): RuntimeWindowOverride;
84
+ export declare function resolveActivityScanCadence(override: RuntimeWindowOverride | undefined, config: ActivityScanCadenceConfig): ResolvedActivityScanCadence;
85
+ export type RuntimeWindowMergeResult = {
86
+ ok: true;
87
+ value: RuntimeWindowOverride;
88
+ cadenceChanged: boolean;
89
+ } | {
90
+ ok: false;
91
+ field: string;
92
+ error: "invalid_field_value" | "invalid_window";
93
+ };
94
+ /**
95
+ * Merge a PATCH `schedule_window` block onto the stored override. Per-field
96
+ * type/bounds are validated; the cross-field window check (`end > start`) runs
97
+ * against the post-merge **resolved** values so a partial patch can't sneak an
98
+ * empty window past per-field validation. `null` resets a field back to the
99
+ * config fallback. `cadenceChanged` tells the route whether a cron rebuild
100
+ * (`reloadCrons`) is needed — `min_observations` is a fire-time gate and never
101
+ * requires one.
102
+ */
103
+ export declare function mergeRuntimeWindow(current: RuntimeWindowOverride, patch: Record<string, unknown>, config: ActivityScanCadenceConfig): RuntimeWindowMergeResult;