@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
@@ -5,7 +5,7 @@ import { composeSkillSet, getProfileForEvent, getProfileForProcess, resolveSkill
5
5
  import { applyCharacterBlockRewrite, buildCharacterBlock } from "./character-block.js";
6
6
  import { applyOutputLanguagePointerRewrite } from "./output-language-policy.js";
7
7
  import { createLogger } from "../logging.js";
8
- import { loadFetchWindowSystemPrompt } from "./fetch-window-prompt-loader.js";
8
+ import { loadSlimSystemPrompt } from "./slim-system-prompt-loader.js";
9
9
  import { substituteIntegrationRoutingTables } from "./management-md.js";
10
10
  import { loadCurationDeclaration, } from "./skill-curation/declarations.js";
11
11
  import { resolveBuiltinSkillDir } from "./skill-source-paths.js";
@@ -21,18 +21,43 @@ import { cliInstructionFileName, cliSkillsDirName, prependCodexReadSensitiveBann
21
21
  // spliceCurationAnchorsInSkill). Not exported — sibling modules carry
22
22
  // their own peer loggers; the test suite spies on those.
23
23
  const logger = createLogger("skills-compiler");
24
- // docs/design/appendices/fetch-window-cost-reduction.md Phase 1.5 single shared constant
25
- // for the pre-pass process key. Typed as `ProcessKey` (not the inferred
26
- // string literal) so a rename in `@aitne/shared/process-key.ts` lights up
27
- // here rather than silently dead-branching the slim materializer.
28
- const FETCH_WINDOW_PROCESS_KEY = "routine.fetch_window";
29
- // Single skill kept in the slim fetch_window CLI session: the
30
- // `observations` SKILL.md is the POST contract for
31
- // `/api/observations/batch` and the fetcher's only structural assertion.
32
- // The other skills the wide path inlines (`mail`, `notion`,
33
- // `external-services`, `attach`) are already restated by the integration
34
- // partial the runner inlines into the user prompt.
35
- const FETCH_WINDOW_SLIM_SKILL = "observations";
24
+ // Per-slim-key CLI skill bundle copied into the slim Codex / Gemini session
25
+ // (RESEARCH_CLUSTER_COST_FIX_PLAN.md F4 generalizes fetch-window-cost-reduction.md
26
+ // Phase 1.5). The slim materializer writes the slim system-prompt body — the
27
+ // SAME template the Claude SDK gets, via the shared `slim-system-prompt-loader`
28
+ // registry as AGENTS.md / GEMINI.md and copies ONLY these skill dirs; the
29
+ // wide manifest's other slugs are restated by the task-flow / user prompt, so
30
+ // inlining them would re-bloat the session.
31
+ //
32
+ // Keys MUST mirror `SLIM_SYSTEM_PROMPT_LOADERS` (drift-guarded by a unit test):
33
+ // a slim system prompt with no CLI skill set — or vice versa — is a wiring bug.
34
+ // Typed `Partial<Record<ProcessKey, …>>` so a key rename in
35
+ // `@aitne/shared/process-key.ts` lights up here rather than dead-branching.
36
+ // - routine.fetch_window — the `observations` SKILL.md is the
37
+ // `/api/observations/batch` POST contract and the fetcher's only
38
+ // structural assertion; the integration partial the runner inlines covers
39
+ // the rest (`mail` / `notion` / `external-services` / `attach` dropped).
40
+ // - routine.research_cluster_update — `browser-history` (the
41
+ // `/api/browser-history/*` curl surface) + `context` (the research-journal
42
+ // write path); the task-flow inlines the endpoints + journal template.
43
+ export const SLIM_CLI_SKILL_SETS = {
44
+ "routine.fetch_window": ["observations"],
45
+ "routine.research_cluster_update": ["browser-history", "context"],
46
+ };
47
+ /**
48
+ * The slim process key for this session, or null when it is not a slim key.
49
+ * Accepts the `processKey ?? eventType` union the CLI materializer threads
50
+ * (an event-type string that is not a registered ProcessKey simply misses the
51
+ * `in` check). Keyed off `SLIM_CLI_SKILL_SETS`, which the drift guard pins to
52
+ * `SLIM_SYSTEM_PROMPT_LOADERS`.
53
+ */
54
+ function slimCliKeyFor(processKeyOrEvent) {
55
+ if (processKeyOrEvent === undefined)
56
+ return null;
57
+ return processKeyOrEvent in SLIM_CLI_SKILL_SETS
58
+ ? processKeyOrEvent
59
+ : null;
60
+ }
36
61
  /**
37
62
  * Materializes backend-specific instruction files for session workdirs
38
63
  * by reading directly from the source tree (agent-assets/agent-profiles/ and
@@ -238,15 +263,17 @@ export class SkillsCompiler {
238
263
  else {
239
264
  this.materializeCliSession(params.sessionDir, profileName, skills, params.backendId, params.processKey ?? params.eventType, profileBodyOverride, params.wikiWorkspaceName);
240
265
  }
241
- // docs/design/appendices/fetch-window-cost-reduction.md Phase 1.5 the slim
242
- // CLI materializer drops every manifest skill except `observations`.
243
- // The Claude path is wide for fetch_window today (Phase 1 swaps only
244
- // the system prompt, not the workdir layout). Report what was
245
- // actually written so log lines / callers see truth, not the
246
- // manifest's pre-narrow list.
247
- const effectiveSkills = params.backendId !== "claude"
248
- && (params.processKey ?? params.eventType) === FETCH_WINDOW_PROCESS_KEY
249
- ? [FETCH_WINDOW_SLIM_SKILL]
266
+ // fetch-window-cost-reduction.md Phase 1.5 / RESEARCH_CLUSTER_COST_FIX_PLAN.md
267
+ // F4 — the slim CLI materializer drops every manifest skill except the
268
+ // key's `SLIM_CLI_SKILL_SETS` bundle. The Claude path stays wide today
269
+ // (Phase 1/F4 swap only the system prompt + settingSources, not the
270
+ // workdir layout). Report what was actually written so log lines /
271
+ // callers see truth, not the manifest's pre-narrow list.
272
+ const slimCliKey = params.backendId !== "claude"
273
+ ? slimCliKeyFor(params.processKey ?? params.eventType)
274
+ : null;
275
+ const effectiveSkills = slimCliKey
276
+ ? [...(SLIM_CLI_SKILL_SETS[slimCliKey] ?? [])]
250
277
  : skills;
251
278
  return { profile: profileName, skills: effectiveSkills };
252
279
  }
@@ -689,95 +716,104 @@ export class SkillsCompiler {
689
716
  }
690
717
  /**
691
718
  * docs/design/appendices/skills-unification.md Phase 1 item 15 — the slim path does NOT
692
- * emit a `<skill-index>` block or the skill-discovery preamble. The
693
- * fetch_window system prompt is a self-contained operational contract
694
- * (one-window-one-curl, no sub-tasks, exactly-one JSON-on-stdout) and
695
- * the only skill copied (`observations`) is referenced inline by the
696
- * runner-emitted user prompt. Adding the index would mis-signal the
697
- * fetcher to scan for skills before executing the acquisition plan.
719
+ * emit a `<skill-index>` block or the skill-discovery preamble. The slim
720
+ * system prompt is a self-contained operational contract and its copied
721
+ * skills are referenced inline by the dispatched user prompt / task-flow.
722
+ * Adding the index would mis-signal the agent to scan for skills before
723
+ * executing its mechanical task.
698
724
  *
699
- * docs/design/appendices/fetch-window-cost-reduction.md Phase 1.5 slim instruction-file
700
- * materializer for `routine.fetch_window` on Codex / Gemini CLI.
725
+ * fetch-window-cost-reduction.md Phase 1.5 / RESEARCH_CLUSTER_COST_FIX_PLAN.md
726
+ * F4 — slim instruction-file materializer for a `SLIM_CLI_SKILL_SETS` process
727
+ * key on Codex / Gemini CLI.
701
728
  *
702
- * Mirrors the Claude SDK's Phase 1 systemPrompt swap (the same
703
- * `agent-assets/system-prompts/routine-fetch-window.md` template is the
704
- * single source of truth): write the slim body verbatim as AGENTS.md /
705
- * GEMINI.md and copy only the `observations` skill — the
706
- * `/api/observations/batch` POST contract is the fetcher's sole
707
- * structural assertion. The integration partial inlined by the runner
708
- * (`routine-fetch-window-runner.ts:reassemblePrompt`) covers the
709
- * per-attempt call shape, so `mail` / `notion` / `external-services` /
710
- * `attach` skill bodies are deliberately omitted.
729
+ * Mirrors the Claude SDK's slim-systemPrompt swap (the same
730
+ * `agent-assets/system-prompts/<key>.md` template is the single source of
731
+ * truth, read through the shared `loadSlimSystemPrompt` registry): write the
732
+ * slim body verbatim as AGENTS.md / GEMINI.md and copy only the key's
733
+ * `SLIM_CLI_SKILL_SETS` bundle the wide manifest's other slugs are restated
734
+ * by the dispatched user prompt / task-flow, so their bodies are omitted.
711
735
  *
712
- * No safety preamble / character / behavioral-rules / daemon-API
713
- * sections — the slim template restates the only rules the fetcher
714
- * needs (localhost-only curl, no sub-tasks, no context writes, no
715
- * notify, JSON-on-stdout-and-exit). The destructive-action policy layer
716
- * (absolute-block list, Codex sandbox, Gemini admin TOML) still applies
717
- * unchanged at runtime.
736
+ * No safety preamble / character / behavioral-rules / daemon-API sections —
737
+ * the slim template restates the only rules the session needs. The
738
+ * destructive-action policy layer (absolute-block list, Codex sandbox,
739
+ * Gemini admin TOML) still applies unchanged at runtime.
718
740
  *
719
741
  * The `<mcp-servers>` section is appended downstream by
720
742
  * `services/mcp/session-materializer.ts:appendMcpSection` exactly as on
721
- * the wide path — Phase 3's allowlist filter, when it lands, will scope
722
- * that section without further changes here.
743
+ * the wide path.
723
744
  */
724
- materializeFetchWindowCliSession(sessionDir, backendId) {
745
+ materializeSlimCliSession(sessionDir, backendId, processKey) {
746
+ const skillSlugs = SLIM_CLI_SKILL_SETS[processKey];
747
+ // Defensive: the caller only diverts registered slim keys, but guard so a
748
+ // future divert bug fails loud rather than writing an empty session.
749
+ if (skillSlugs === undefined)
750
+ return;
751
+ const slim = loadSlimSystemPrompt(processKey);
752
+ if (slim === null)
753
+ return; // Unreachable for registered keys; satisfies the type.
725
754
  mkdirSync(sessionDir, { recursive: true });
726
- const slim = loadFetchWindowSystemPrompt();
727
755
  writeFileSync(join(sessionDir, cliInstructionFileName(backendId)), slim, "utf-8");
728
- // Copy ONLY the `observations` skill dir. The wide path's prune step
729
- // is replaced by an explicit single-slug list — `pruneStaleBuiltinSkillDirs`
756
+ // Copy ONLY the key's slim skill dirs. The wide path's prune step is
757
+ // replaced by the explicit slug list — `pruneStaleBuiltinSkillDirs`
730
758
  // removes any other built-in skill dir that a prior re-materialization
731
759
  // (e.g. a fallback-driven wide path on the same workdir) may have left.
732
760
  const cliSkillsRoot = cliSkillsDirName(backendId);
733
761
  if (cliSkillsRoot === null)
734
- return; // Claude-only — fetch_window slim runs on Claude SDK natively.
762
+ return; // Claude-only — slim keys run on Claude SDK natively.
735
763
  const skillsRoot = this.getSourceSkillsRoot();
736
764
  const destSkillsRoot = join(sessionDir, cliSkillsRoot, "skills");
737
765
  mkdirSync(destSkillsRoot, { recursive: true });
738
- pruneStaleBuiltinSkillDirs(destSkillsRoot, skillsRoot, [FETCH_WINDOW_SLIM_SKILL]);
739
- const src = resolveBuiltinSkillDir(skillsRoot, FETCH_WINDOW_SLIM_SKILL);
766
+ pruneStaleBuiltinSkillDirs(destSkillsRoot, skillsRoot, [...skillSlugs]);
767
+ for (const slug of skillSlugs) {
768
+ this.copySlimSkillDir(destSkillsRoot, skillsRoot, slug, backendId);
769
+ }
770
+ }
771
+ /**
772
+ * Copy one built-in skill dir into a slim CLI session, applying the same
773
+ * adaptation pipeline the wide CLI path runs (brand tokens, partial /
774
+ * reference includes, integration-mode filter, tool-deny prose, curation
775
+ * anchors). Frontmatter stays intact (skills-unification.md §R6). A
776
+ * no-op when the source SKILL.md is absent.
777
+ */
778
+ copySlimSkillDir(destSkillsRoot, skillsRoot, slug, backendId) {
779
+ const src = resolveBuiltinSkillDir(skillsRoot, slug);
740
780
  const skillMdPath = join(src, "SKILL.md");
741
781
  if (!existsSync(skillMdPath))
742
782
  return;
743
- const destDir = join(destSkillsRoot, FETCH_WINDOW_SLIM_SKILL);
783
+ const destDir = join(destSkillsRoot, slug);
744
784
  cpSync(src, destDir, { recursive: true });
745
785
  substituteBrandTokensInDir(destDir);
746
- // No `substituteWikiWorkspaceTokensInDir` — fetch_window never touches
747
- // wiki workspace state, and the resolver is a no-op for non-wiki
748
- // process keys anyway.
786
+ // No `substituteWikiWorkspaceTokensInDir` — no slim key touches wiki
787
+ // workspace state, and the resolver is a no-op for non-wiki keys anyway.
749
788
  let adapted = substituteBrandTokens(readFileSync(skillMdPath, "utf-8"));
750
- // observations/SKILL.md ships no `{{> base }}` or `{{> ref:* }}`
751
- // directives today, but run the resolvers anyway so a future curation
789
+ // Run the partial / reference resolvers even on bodies that ship no
790
+ // `{{> base }}` / `{{> ref:* }}` directives today, so a future curation
752
791
  // edit cannot silently drop content. Idempotent on plain content.
753
792
  adapted = substituteBrandTokens(renderPartialIncludes(adapted, join(src, "SKILL.base.md")));
754
793
  adapted = substituteBrandTokens(renderReferenceIncludes(adapted, src));
755
- // Mode-conditional filter — observations/SKILL.md carries
756
- // `<!-- mode:<predicate>:notion -->` markers (lines 273-345 today)
757
- // because the source / consume contract differs across `direct` /
758
- // `delegated-same` / `delegated-cross` / `native` / `disabled`. The
759
- // wide path applies this filter at `materializeCliSession`; the
760
- // slim path must match so the agent doesn't get every mode's prose
761
- // for every integration. Idempotent on bodies that lack markers.
794
+ // Mode-conditional filter — skills (e.g. observations/SKILL.md) carry
795
+ // `<!-- mode:<predicate>:<integration> -->` markers because the
796
+ // source / consume contract differs across `direct` / `delegated-same` /
797
+ // `delegated-cross` / `native` / `disabled`. The wide path applies this
798
+ // at `materializeCliSession`; the slim path must match so the agent
799
+ // doesn't get every mode's prose. Idempotent on bodies that lack markers.
762
800
  adapted = applyIntegrationModeFilter(adapted, this.integrations, backendId);
763
- // Tool-deny policy stays in force — even though the fetcher is fed a
764
- // narrow allowlist, the soft-enforcement prose lands in the body
765
- // BEFORE the CLI frontmatter strip so it's not lost.
766
- adapted = applyAllDeniedToolsForSkill(adapted, FETCH_WINDOW_SLIM_SKILL, backendId, this.integrations);
767
- // docs/design/appendices/skills-unification.md Phase 1 §R6 — frontmatter stays intact
768
- // across all backends. `adaptSkillForCli` is gone; the source body's
769
- // YAML preamble flows through verbatim.
801
+ // Tool-deny policy stays in force — even with a narrow allowlist, the
802
+ // soft-enforcement prose lands in the body BEFORE the CLI frontmatter
803
+ // strip so it's not lost.
804
+ adapted = applyAllDeniedToolsForSkill(adapted, slug, backendId, this.integrations);
770
805
  writeFileSync(join(destDir, "SKILL.md"), adapted, "utf-8");
771
- this.spliceCurationAnchorsInSkill(destDir, FETCH_WINDOW_SLIM_SKILL);
806
+ this.spliceCurationAnchorsInSkill(destDir, slug);
772
807
  }
773
808
  materializeCliSession(sessionDir, profileName, skillSlugs, backendId, processKey, profileBodyOverride, wikiWorkspaceName) {
774
- // docs/design/appendices/fetch-window-cost-reduction.md Phase 1.5 divert
775
- // `routine.fetch_window` to a slim materializer that mirrors the
776
- // Claude SDK's Phase 1 systemPrompt swap. Custom bang commands cannot
777
- // reach this branch (they bind to `messaging.custom_command`), so
809
+ // fetch-window-cost-reduction.md Phase 1.5 / RESEARCH_CLUSTER_COST_FIX_PLAN.md
810
+ // F4 divert a slim process key to the slim materializer that mirrors the
811
+ // Claude SDK's slim-systemPrompt swap. Custom bang commands cannot reach
812
+ // this branch (they bind to `messaging.custom_command`), so
778
813
  // `profileBodyOverride` is intentionally NOT forwarded.
779
- if (processKey === FETCH_WINDOW_PROCESS_KEY) {
780
- this.materializeFetchWindowCliSession(sessionDir, backendId);
814
+ const slimKey = slimCliKeyFor(processKey);
815
+ if (slimKey) {
816
+ this.materializeSlimCliSession(sessionDir, backendId, slimKey);
781
817
  return;
782
818
  }
783
819
  // docs/design/appendices/skills-unification.md Phase 1 — directory-based skill delivery.
@@ -106,6 +106,43 @@ export declare function resolveSkillManifestForProcess(processKey: ProcessKey, o
106
106
  db?: Database.Database | null;
107
107
  messageText?: string | null;
108
108
  }): string[];
109
+ /**
110
+ * Event-type / process-key families whose sessions do NOT receive the
111
+ * owner's user-authored skill library (`<contextDir>/policies/skills/`).
112
+ *
113
+ * User skills are general assistant extensions the owner writes for their
114
+ * conversational agent; `syncAllUserSkills` provisions them into a session
115
+ * workdir ON TOP of the process key's built-in manifest. Narrow-persona
116
+ * sessions run under dedicated profiles with tight, purpose-built manifests
117
+ * — dumping the full user-skill library into them is pure cold-start cost +
118
+ * attention dilution and never load-bearing (their work is fully covered by
119
+ * the built-in bundle, and the wiki/research skills are themselves
120
+ * built-ins, not user-authored).
121
+ *
122
+ * Excluded families:
123
+ * - `wiki.*` — the wiki-agent persona (ingest_url / compile /
124
+ * ask / lint / trace / connect).
125
+ * - `routine.research_*` — the browser-history research routines
126
+ * (cluster_update / offer_dm / dispatch /
127
+ * wiki_summary).
128
+ *
129
+ * Everything else (the conversational DM agent, DM-tone scheduled sessions,
130
+ * the daily/weekly/monthly routines, setup, knowledge import, git/github
131
+ * observers, scheduled tasks) keeps the prior "every user skill, every
132
+ * session" behaviour.
133
+ */
134
+ export declare const USER_SKILL_EXCLUDED_PREFIXES: readonly string[];
135
+ /**
136
+ * Whether a session for `eventTypeOrProcessKey` should be provisioned with
137
+ * the owner's user-authored skills. Keyed on the SAME string the skill
138
+ * manifest resolves against (`processKey ?? eventType`) so the gate and the
139
+ * built-in manifest agree on what a session is.
140
+ *
141
+ * Gates the `syncAllUserSkills` call at every materialisation chokepoint
142
+ * (`createSessionWorkdir`, `ensureSessionWorkdir`, and the backend-fallback
143
+ * re-materialiser). See `USER_SKILL_EXCLUDED_PREFIXES`.
144
+ */
145
+ export declare function eventTypeAcceptsUserSkills(eventTypeOrProcessKey: string): boolean;
109
146
  export declare function getProfileForEvent(eventType: string): string;
110
147
  export declare function getSkillsForEvent(eventType: string): string[];
111
148
  export declare function getProfileForProcess(processKey: ProcessKey): string;
@@ -127,7 +127,7 @@ export const EVENT_SKILL_SETS = {
127
127
  "roadmap",
128
128
  "management-policy",
129
129
  ],
130
- "routine.hourly_check": [
130
+ "routine.activity_scan": [
131
131
  "context",
132
132
  "today",
133
133
  "observations",
@@ -260,6 +260,14 @@ export const EVENT_SKILL_SETS = {
260
260
  // (no prior research offer to respond to) — that exclusion does
261
261
  // not generalise.
262
262
  "browser-task",
263
+ // BACKGROUND_TASK_RUNNER_DESIGN.md §5 / Phase 3 — DM-driven entry
264
+ // point to hand a long-running / open-ended task to the detached
265
+ // background runner. Loaded for the first DM too: the user's literal
266
+ // first message ("research X for me", "audit my repos") is exactly
267
+ // the spawn surface. The `background-task-reply` clarify skill is
268
+ // correctly absent here — no prior task can be parked on the very
269
+ // first DM of a session — exactly as `browser-history-respond` is.
270
+ "background-task",
263
271
  ],
264
272
  "message.received.dm": [
265
273
  "context",
@@ -305,6 +313,20 @@ export const EVENT_SKILL_SETS = {
305
313
  // the agent simply does not call its endpoints when the user's
306
314
  // request doesn't ask for a browser task.
307
315
  "browser-task",
316
+ // BACKGROUND_TASK_RUNNER_DESIGN.md §5 / Phase 3 — the generic
317
+ // long-running-task surface. `background-task` teaches the DM agent
318
+ // to compose a self-contained brief, set the notification policy,
319
+ // POST + ack + end the turn, and read `GET /:id` for follow-up
320
+ // detail. `background-task-reply` is the narrow clarify-relay (mirror
321
+ // of `browser-history-respond`) that translates the owner's answer to
322
+ // a parked task's question into `POST /:id/clarify`. Loaded for
323
+ // `message.received.dm` (which `message.dm` + `dashboard.chat`
324
+ // inherit via PROCESS_TO_EVENT_TYPE). The reply skill is NOT on
325
+ // `message.received.dm_first` (no task can be parked on a session's
326
+ // first DM). No conditional gate: both skill bodies no-op when their
327
+ // endpoints have nothing to act on, and the in-context cost is small.
328
+ "background-task",
329
+ "background-task-reply",
308
330
  ],
309
331
  // ── Task events ──
310
332
  "schedule.approaching": [
@@ -564,6 +586,13 @@ export const ALL_SKILLS = [
564
586
  // `browser-history-managed` skill that fronted the frozen workflow
565
587
  // registry was retired alongside the route + routines in Phase 6.
566
588
  "browser-task",
589
+ // BACKGROUND_TASK_RUNNER_DESIGN.md §5 / Phase 3 — generic detached
590
+ // long-task surface. `background-task` (spawn) loads on
591
+ // `message.received.dm` + `message.received.dm_first`;
592
+ // `background-task-reply` (clarify relay) loads on `message.received.dm`
593
+ // only. Both are DM-agent-exclusive, like browser-task.
594
+ "background-task",
595
+ "background-task-reply",
567
596
  // Scheduling split — `/schedule` is one-shot only; recurring tasks are
568
597
  // Agents. This skill teaches the DM agent to author a detailed recurring
569
598
  // Agent (`POST /api/agents`) when the user asks for an ongoing cadence.
@@ -579,7 +608,7 @@ const PROCESS_TO_EVENT_TYPE = {
579
608
  "routine.evening_review": "routine.evening_review",
580
609
  "routine.weekly_review": "routine.weekly_review",
581
610
  "routine.monthly_review": "routine.monthly_review",
582
- "routine.hourly_check": "routine.hourly_check",
611
+ "routine.activity_scan": "routine.activity_scan",
583
612
  "routine.roadmap_refresh": "routine.roadmap_refresh",
584
613
  "routine.today_refresh": "routine.today_refresh",
585
614
  "routine.user_profile_sweep": "routine.user_profile_sweep",
@@ -887,6 +916,48 @@ export function resolveSkillManifest(eventType, opts) {
887
916
  export function resolveSkillManifestForProcess(processKey, opts) {
888
917
  return resolveSkillManifest(PROCESS_TO_EVENT_TYPE[processKey] ?? processKey, opts);
889
918
  }
919
+ /**
920
+ * Event-type / process-key families whose sessions do NOT receive the
921
+ * owner's user-authored skill library (`<contextDir>/policies/skills/`).
922
+ *
923
+ * User skills are general assistant extensions the owner writes for their
924
+ * conversational agent; `syncAllUserSkills` provisions them into a session
925
+ * workdir ON TOP of the process key's built-in manifest. Narrow-persona
926
+ * sessions run under dedicated profiles with tight, purpose-built manifests
927
+ * — dumping the full user-skill library into them is pure cold-start cost +
928
+ * attention dilution and never load-bearing (their work is fully covered by
929
+ * the built-in bundle, and the wiki/research skills are themselves
930
+ * built-ins, not user-authored).
931
+ *
932
+ * Excluded families:
933
+ * - `wiki.*` — the wiki-agent persona (ingest_url / compile /
934
+ * ask / lint / trace / connect).
935
+ * - `routine.research_*` — the browser-history research routines
936
+ * (cluster_update / offer_dm / dispatch /
937
+ * wiki_summary).
938
+ *
939
+ * Everything else (the conversational DM agent, DM-tone scheduled sessions,
940
+ * the daily/weekly/monthly routines, setup, knowledge import, git/github
941
+ * observers, scheduled tasks) keeps the prior "every user skill, every
942
+ * session" behaviour.
943
+ */
944
+ export const USER_SKILL_EXCLUDED_PREFIXES = [
945
+ "wiki.",
946
+ "routine.research_",
947
+ ];
948
+ /**
949
+ * Whether a session for `eventTypeOrProcessKey` should be provisioned with
950
+ * the owner's user-authored skills. Keyed on the SAME string the skill
951
+ * manifest resolves against (`processKey ?? eventType`) so the gate and the
952
+ * built-in manifest agree on what a session is.
953
+ *
954
+ * Gates the `syncAllUserSkills` call at every materialisation chokepoint
955
+ * (`createSessionWorkdir`, `ensureSessionWorkdir`, and the backend-fallback
956
+ * re-materialiser). See `USER_SKILL_EXCLUDED_PREFIXES`.
957
+ */
958
+ export function eventTypeAcceptsUserSkills(eventTypeOrProcessKey) {
959
+ return !USER_SKILL_EXCLUDED_PREFIXES.some((prefix) => eventTypeOrProcessKey.startsWith(prefix));
960
+ }
890
961
  export function getProfileForEvent(eventType) {
891
962
  for (const rule of PROFILE_RULES) {
892
963
  if (rule.exact ? eventType === rule.prefix : eventType.startsWith(rule.prefix)) {
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Keep-awake posture while the daemon runs (`preventSleepMode` setting):
3
+ *
4
+ * - `"off"` — never inhibit sleep.
5
+ * - `"ac"` — inhibit system sleep only while on AC power
6
+ * (macOS `caffeinate -s`; battery drain stays impossible).
7
+ * - `"always"` — additionally inhibit idle sleep on battery
8
+ * (macOS `caffeinate -i -s`).
9
+ *
10
+ * Why this exists: every timer in the daemon (node-cron, WakeDetector,
11
+ * in-flight agent sessions) freezes while the host sleeps. A sleeping
12
+ * laptop turns the 04:00 day-boundary flow into hours of wake-catchup
13
+ * replays riding macOS maintenance DarkWakes — each replay re-fires
14
+ * day-boundary work in a 1–2 minute window before the machine re-sleeps,
15
+ * and cold prompt caches make those runs 10× the normal cost. Keeping
16
+ * the machine awake while plugged in removes that failure mode at the
17
+ * source; server installs (which never sleep) are unaffected.
18
+ *
19
+ * macOS only. `caffeinate` cannot override lid-close (clamshell) sleep —
20
+ * that needs root (`pmset disablesleep 1`), which the daemon must not
21
+ * touch. Windows/Linux hosts running this daemon are assumed to be
22
+ * servers or desktops with OS-level power management; the inhibitor is a
23
+ * no-op there and logs once at debug level.
24
+ */
25
+ export declare const PREVENT_SLEEP_MODES: readonly ["off", "ac", "always"];
26
+ export type PreventSleepMode = (typeof PREVENT_SLEEP_MODES)[number];
27
+ /** Respawns allowed after an unexpected `caffeinate` exit before giving up. */
28
+ export declare const SLEEP_INHIBITOR_MAX_RESPAWNS = 3;
29
+ /** Pause before a respawn so a persistently-dying binary cannot tight-loop. */
30
+ export declare const SLEEP_INHIBITOR_RESPAWN_DELAY_MS = 5000;
31
+ export interface SleepInhibitCommand {
32
+ command: string;
33
+ args: string[];
34
+ }
35
+ /**
36
+ * Pure command resolver — `null` means "do not inhibit" (mode off or
37
+ * unsupported platform). `-w <pid>` ties the power assertion to the
38
+ * daemon process itself, so the assertion is released even if the daemon
39
+ * is SIGKILLed and `stop()` never runs.
40
+ */
41
+ export declare function resolveSleepInhibitCommand(platform: NodeJS.Platform, mode: PreventSleepMode, pid: number): SleepInhibitCommand | null;
42
+ export interface SleepInhibitorChild {
43
+ on(event: "error", listener: (err: Error) => void): unknown;
44
+ on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
45
+ kill(signal?: NodeJS.Signals): boolean;
46
+ unref(): void;
47
+ pid?: number;
48
+ }
49
+ export type SleepInhibitorSpawn = (command: string, args: readonly string[]) => SleepInhibitorChild;
50
+ export interface SleepInhibitorOptions {
51
+ mode: PreventSleepMode;
52
+ /** Injectable for tests; defaults to the host platform. */
53
+ platform?: NodeJS.Platform;
54
+ /** Injectable for tests; defaults to the daemon's own pid. */
55
+ pid?: number;
56
+ spawnFn?: SleepInhibitorSpawn;
57
+ respawnDelayMs?: number;
58
+ }
59
+ /**
60
+ * Holds a `caffeinate` child for the daemon's lifetime. Crash-safe by
61
+ * construction (`-w pid` above); `stop()` exists for symmetric shutdown
62
+ * and to suppress the respawn path during graceful exit.
63
+ */
64
+ export declare class SleepInhibitor {
65
+ private readonly mode;
66
+ private readonly platform;
67
+ private readonly pid;
68
+ private readonly spawnFn;
69
+ private readonly respawnDelayMs;
70
+ private child;
71
+ private respawnTimer;
72
+ private respawns;
73
+ private started;
74
+ private stopped;
75
+ constructor(options: SleepInhibitorOptions);
76
+ start(): void;
77
+ stop(): void;
78
+ private spawnChild;
79
+ }
@@ -0,0 +1,132 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createLogger } from "../logging.js";
3
+ const logger = createLogger("sleep-inhibitor");
4
+ /**
5
+ * Keep-awake posture while the daemon runs (`preventSleepMode` setting):
6
+ *
7
+ * - `"off"` — never inhibit sleep.
8
+ * - `"ac"` — inhibit system sleep only while on AC power
9
+ * (macOS `caffeinate -s`; battery drain stays impossible).
10
+ * - `"always"` — additionally inhibit idle sleep on battery
11
+ * (macOS `caffeinate -i -s`).
12
+ *
13
+ * Why this exists: every timer in the daemon (node-cron, WakeDetector,
14
+ * in-flight agent sessions) freezes while the host sleeps. A sleeping
15
+ * laptop turns the 04:00 day-boundary flow into hours of wake-catchup
16
+ * replays riding macOS maintenance DarkWakes — each replay re-fires
17
+ * day-boundary work in a 1–2 minute window before the machine re-sleeps,
18
+ * and cold prompt caches make those runs 10× the normal cost. Keeping
19
+ * the machine awake while plugged in removes that failure mode at the
20
+ * source; server installs (which never sleep) are unaffected.
21
+ *
22
+ * macOS only. `caffeinate` cannot override lid-close (clamshell) sleep —
23
+ * that needs root (`pmset disablesleep 1`), which the daemon must not
24
+ * touch. Windows/Linux hosts running this daemon are assumed to be
25
+ * servers or desktops with OS-level power management; the inhibitor is a
26
+ * no-op there and logs once at debug level.
27
+ */
28
+ export const PREVENT_SLEEP_MODES = ["off", "ac", "always"];
29
+ /** Respawns allowed after an unexpected `caffeinate` exit before giving up. */
30
+ export const SLEEP_INHIBITOR_MAX_RESPAWNS = 3;
31
+ /** Pause before a respawn so a persistently-dying binary cannot tight-loop. */
32
+ export const SLEEP_INHIBITOR_RESPAWN_DELAY_MS = 5_000;
33
+ /**
34
+ * Pure command resolver — `null` means "do not inhibit" (mode off or
35
+ * unsupported platform). `-w <pid>` ties the power assertion to the
36
+ * daemon process itself, so the assertion is released even if the daemon
37
+ * is SIGKILLed and `stop()` never runs.
38
+ */
39
+ export function resolveSleepInhibitCommand(platform, mode, pid) {
40
+ if (mode === "off")
41
+ return null;
42
+ if (platform !== "darwin")
43
+ return null;
44
+ const flags = mode === "always" ? ["-i", "-s"] : ["-s"];
45
+ return { command: "caffeinate", args: [...flags, "-w", String(pid)] };
46
+ }
47
+ const defaultSpawn = (command, args) => spawn(command, args, { stdio: "ignore" });
48
+ /**
49
+ * Holds a `caffeinate` child for the daemon's lifetime. Crash-safe by
50
+ * construction (`-w pid` above); `stop()` exists for symmetric shutdown
51
+ * and to suppress the respawn path during graceful exit.
52
+ */
53
+ export class SleepInhibitor {
54
+ mode;
55
+ platform;
56
+ pid;
57
+ spawnFn;
58
+ respawnDelayMs;
59
+ child = null;
60
+ respawnTimer = null;
61
+ respawns = 0;
62
+ started = false;
63
+ stopped = false;
64
+ constructor(options) {
65
+ this.mode = options.mode;
66
+ this.platform = options.platform ?? process.platform;
67
+ this.pid = options.pid ?? process.pid;
68
+ this.spawnFn = options.spawnFn ?? defaultSpawn;
69
+ this.respawnDelayMs =
70
+ options.respawnDelayMs ?? SLEEP_INHIBITOR_RESPAWN_DELAY_MS;
71
+ }
72
+ start() {
73
+ if (this.started)
74
+ return;
75
+ this.started = true;
76
+ const cmd = resolveSleepInhibitCommand(this.platform, this.mode, this.pid);
77
+ if (!cmd) {
78
+ logger.debug({ mode: this.mode, platform: this.platform }, "Sleep inhibitor inactive (mode off or unsupported platform)");
79
+ return;
80
+ }
81
+ this.spawnChild(cmd);
82
+ }
83
+ stop() {
84
+ this.stopped = true;
85
+ if (this.respawnTimer) {
86
+ clearTimeout(this.respawnTimer);
87
+ this.respawnTimer = null;
88
+ }
89
+ // Kill but leave `this.child` set — the exit handler clears it and the
90
+ // `stopped` flag suppresses the respawn path.
91
+ this.child?.kill("SIGTERM");
92
+ }
93
+ spawnChild(cmd) {
94
+ let child;
95
+ try {
96
+ child = this.spawnFn(cmd.command, cmd.args);
97
+ }
98
+ catch (err) {
99
+ logger.warn({ err, command: cmd.command }, "Sleep inhibitor spawn failed — system sleep stays OS-managed");
100
+ return;
101
+ }
102
+ this.child = child;
103
+ // The inhibitor must never keep the event loop alive on its own.
104
+ child.unref();
105
+ child.on("error", (err) => {
106
+ // ENOENT and friends — the binary is missing or unrunnable, which a
107
+ // respawn cannot fix. Warn once and fall back to OS-managed sleep.
108
+ this.child = null;
109
+ logger.warn({ err, command: cmd.command }, "Sleep inhibitor process errored — system sleep stays OS-managed");
110
+ });
111
+ child.on("exit", (code, signal) => {
112
+ if (this.child !== child)
113
+ return; // killed by stop() or replaced
114
+ this.child = null;
115
+ if (this.stopped)
116
+ return;
117
+ if (this.respawns >= SLEEP_INHIBITOR_MAX_RESPAWNS) {
118
+ logger.error({ code, signal, respawns: this.respawns }, "Sleep inhibitor exited repeatedly — giving up; system sleep stays OS-managed");
119
+ return;
120
+ }
121
+ this.respawns += 1;
122
+ logger.warn({ code, signal, attempt: this.respawns }, "Sleep inhibitor exited unexpectedly — respawning");
123
+ this.respawnTimer = setTimeout(() => {
124
+ this.respawnTimer = null;
125
+ if (!this.stopped)
126
+ this.spawnChild(cmd);
127
+ }, this.respawnDelayMs);
128
+ this.respawnTimer.unref?.();
129
+ });
130
+ logger.info({ command: cmd.command, args: cmd.args, mode: this.mode }, "Sleep inhibitor active");
131
+ }
132
+ }