@aitne/daemon 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/dist/adapters/adapter-watchdog.d.ts +70 -0
  2. package/dist/adapters/adapter-watchdog.js +115 -0
  3. package/dist/adapters/discord.d.ts +17 -1
  4. package/dist/adapters/discord.js +33 -0
  5. package/dist/adapters/notification-manager.d.ts +27 -1
  6. package/dist/adapters/notification-manager.js +54 -39
  7. package/dist/adapters/slack-adapter.d.ts +26 -1
  8. package/dist/adapters/slack-adapter.js +41 -0
  9. package/dist/adapters/telegram-adapter.d.ts +18 -1
  10. package/dist/adapters/telegram-adapter.js +41 -2
  11. package/dist/adapters/types.d.ts +20 -0
  12. package/dist/adapters/whatsapp-adapter.d.ts +26 -7
  13. package/dist/adapters/whatsapp-adapter.js +74 -21
  14. package/dist/api/env-writer.d.ts +1 -0
  15. package/dist/api/env-writer.js +17 -7
  16. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  17. package/dist/api/helpers/agent-errors-registry.js +5 -5
  18. package/dist/api/routes/agent-schedule.js +5 -1
  19. package/dist/api/routes/agent.js +33 -12
  20. package/dist/api/routes/agents/index.js +75 -16
  21. package/dist/api/routes/agents/views.d.ts +37 -2
  22. package/dist/api/routes/agents/views.js +64 -2
  23. package/dist/api/routes/apple-calendar.js +4 -1
  24. package/dist/api/routes/background-task.d.ts +22 -0
  25. package/dist/api/routes/background-task.js +338 -0
  26. package/dist/api/routes/browser-history.js +9 -1
  27. package/dist/api/routes/calendar.js +12 -2
  28. package/dist/api/routes/context/path-resolve.js +6 -1
  29. package/dist/api/routes/context/permissions.js +12 -2
  30. package/dist/api/routes/context/snapshots.js +0 -3
  31. package/dist/api/routes/context/write.js +3 -17
  32. package/dist/api/routes/dashboard/config.js +58 -12
  33. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  34. package/dist/api/routes/dashboard/notifications.js +9 -9
  35. package/dist/api/routes/dashboard/oauth-google.js +5 -3
  36. package/dist/api/routes/feedback.d.ts +3 -0
  37. package/dist/api/routes/feedback.js +349 -0
  38. package/dist/api/routes/git.js +10 -3
  39. package/dist/api/routes/github.js +5 -1
  40. package/dist/api/routes/integrations/crud-patch.js +5 -1
  41. package/dist/api/routes/integrations-reconcile.js +2 -2
  42. package/dist/api/routes/mcp.js +65 -13
  43. package/dist/api/routes/notion.d.ts +1 -1
  44. package/dist/api/routes/observations.js +7 -7
  45. package/dist/api/routes/obsidian.d.ts +1 -1
  46. package/dist/api/routes/receipts.js +5 -1
  47. package/dist/api/routes/setup-migrate.js +1 -1
  48. package/dist/api/routes/setup.js +1 -1
  49. package/dist/api/routes/task-flows.d.ts +1 -1
  50. package/dist/api/routes/task-flows.js +1 -1
  51. package/dist/api/routes/tuning.d.ts +29 -0
  52. package/dist/api/routes/tuning.js +304 -0
  53. package/dist/api/server.d.ts +44 -16
  54. package/dist/api/server.js +12 -0
  55. package/dist/bootstrap/adapters.d.ts +19 -0
  56. package/dist/bootstrap/adapters.js +61 -0
  57. package/dist/bootstrap/api.d.ts +5 -3
  58. package/dist/bootstrap/api.js +45 -13
  59. package/dist/bootstrap/catchup.d.ts +1 -1
  60. package/dist/bootstrap/catchup.js +11 -11
  61. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  62. package/dist/bootstrap/event-pipeline.js +246 -8
  63. package/dist/bootstrap/observers.js +9 -6
  64. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  65. package/dist/bootstrap/schedule-helpers.js +172 -19
  66. package/dist/config.js +32 -12
  67. package/dist/core/agent-core.d.ts +33 -1
  68. package/dist/core/agent-core.js +36 -1
  69. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  70. package/dist/core/agents/activity-scan-cadence.js +127 -0
  71. package/dist/core/agents/agent-route-override.d.ts +53 -0
  72. package/dist/core/agents/agent-route-override.js +69 -0
  73. package/dist/core/agents/builtin-registry.d.ts +51 -14
  74. package/dist/core/agents/builtin-registry.js +92 -15
  75. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  76. package/dist/core/agents/config-gate-reconcile.js +51 -0
  77. package/dist/core/agents/cron-substitute.d.ts +1 -1
  78. package/dist/core/agents/cron-substitute.js +1 -1
  79. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  80. package/dist/core/agents/custom-routine-migration.js +149 -0
  81. package/dist/core/agents/firing-blocked.d.ts +1 -1
  82. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  83. package/dist/core/agents/hourly-cadence.js +126 -0
  84. package/dist/core/agents/loader-boot.js +23 -0
  85. package/dist/core/agents/loader.d.ts +19 -0
  86. package/dist/core/agents/loader.js +34 -2
  87. package/dist/core/agents/override-merge.d.ts +1 -1
  88. package/dist/core/agents/override-merge.js +9 -1
  89. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  90. package/dist/core/agents/recurrence-convert.js +1 -1
  91. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  92. package/dist/core/alerts.js +6 -6
  93. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  94. package/dist/core/backends/auth-health-monitor.js +1 -1
  95. package/dist/core/backends/backend-router.d.ts +27 -1
  96. package/dist/core/backends/backend-router.js +165 -1
  97. package/dist/core/backends/claude-code-core.d.ts +71 -31
  98. package/dist/core/backends/claude-code-core.js +282 -54
  99. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  100. package/dist/core/backends/cli-quota-guards.js +40 -5
  101. package/dist/core/backends/codex-core.d.ts +6 -0
  102. package/dist/core/backends/codex-core.js +22 -6
  103. package/dist/core/backends/failure-spend.d.ts +58 -0
  104. package/dist/core/backends/failure-spend.js +137 -0
  105. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  106. package/dist/core/backends/gemini-cli-core.js +38 -6
  107. package/dist/core/backends/model-registry.d.ts +1 -1
  108. package/dist/core/backends/model-registry.js +4 -4
  109. package/dist/core/backends/opencode-core.d.ts +1 -1
  110. package/dist/core/backends/opencode-core.js +5 -5
  111. package/dist/core/backends/plan-presets.js +47 -18
  112. package/dist/core/bang-commands/commands-cost.js +3 -1
  113. package/dist/core/bang-commands/commands-report.js +4 -3
  114. package/dist/core/bang-commands/commands-research.js +4 -1
  115. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  116. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  117. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  118. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  119. package/dist/core/bang-commands/commands-task-control.js +147 -0
  120. package/dist/core/bang-commands/commands-wiki.js +5 -5
  121. package/dist/core/bang-commands/index.d.ts +2 -0
  122. package/dist/core/bang-commands/index.js +12 -0
  123. package/dist/core/bang-commands/registry.d.ts +12 -0
  124. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  125. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  126. package/dist/core/channel-timeline.d.ts +5 -1
  127. package/dist/core/channel-timeline.js +13 -0
  128. package/dist/core/context/index-reconciler.js +5 -2
  129. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  130. package/dist/core/context/policy-index-runner.js +25 -6
  131. package/dist/core/context-builder-calendar.js +10 -2
  132. package/dist/core/context-builder-conversation.d.ts +8 -1
  133. package/dist/core/context-builder-conversation.js +41 -7
  134. package/dist/core/context-builder-yesterday.js +4 -3
  135. package/dist/core/context-builder.d.ts +7 -2
  136. package/dist/core/context-builder.js +193 -5
  137. package/dist/core/context-file-serializer.d.ts +1 -1
  138. package/dist/core/context-file-serializer.js +1 -1
  139. package/dist/core/context-health.js +2 -2
  140. package/dist/core/context-paths.d.ts +11 -1
  141. package/dist/core/context-paths.js +17 -1
  142. package/dist/core/context-validation/prepare-write.js +1 -1
  143. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  144. package/dist/core/context-vault-aliases.d.ts +0 -13
  145. package/dist/core/context-vault-aliases.js +37 -0
  146. package/dist/core/custom-routines.d.ts +99 -0
  147. package/dist/core/custom-routines.js +187 -0
  148. package/dist/core/daemon-api-cli.js +50 -1
  149. package/dist/core/day-boundary.d.ts +46 -0
  150. package/dist/core/day-boundary.js +40 -0
  151. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  152. package/dist/core/dispatcher-activity-scan.js +775 -0
  153. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  154. package/dist/core/dispatcher-error-handling.js +38 -62
  155. package/dist/core/dispatcher-hourly-check.js +6 -1
  156. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  157. package/dist/core/dispatcher-message-handler.js +24 -0
  158. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  159. package/dist/core/dispatcher-morning-routine.js +13 -13
  160. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  161. package/dist/core/dispatcher-result-processor.js +167 -11
  162. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  163. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  164. package/dist/core/dispatcher-scheduled-tasks.d.ts +104 -1
  165. package/dist/core/dispatcher-scheduled-tasks.js +480 -8
  166. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  167. package/dist/core/dispatcher-task-delivery.js +555 -0
  168. package/dist/core/dispatcher-types.d.ts +48 -9
  169. package/dist/core/dispatcher-types.js +3 -3
  170. package/dist/core/dispatcher.d.ts +112 -31
  171. package/dist/core/dispatcher.js +297 -60
  172. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  173. package/dist/core/drift-effects.js +2 -2
  174. package/dist/core/feedback/consolidation-prep.d.ts +94 -0
  175. package/dist/core/feedback/consolidation-prep.js +254 -0
  176. package/dist/core/feedback/eviction-scorer.d.ts +81 -0
  177. package/dist/core/feedback/eviction-scorer.js +136 -0
  178. package/dist/core/feedback/lesson-format.d.ts +79 -0
  179. package/dist/core/feedback/lesson-format.js +199 -0
  180. package/dist/core/feedback/lesson-injection.d.ts +98 -0
  181. package/dist/core/feedback/lesson-injection.js +174 -0
  182. package/dist/core/feedback/lesson-merge.d.ts +51 -0
  183. package/dist/core/feedback/lesson-merge.js +88 -0
  184. package/dist/core/feedback/lesson-store-overview.d.ts +46 -0
  185. package/dist/core/feedback/lesson-store-overview.js +42 -0
  186. package/dist/core/feedback/promotion-gate.d.ts +69 -0
  187. package/dist/core/feedback/promotion-gate.js +117 -0
  188. package/dist/core/feedback/regeneralization-prep.d.ts +87 -0
  189. package/dist/core/feedback/regeneralization-prep.js +152 -0
  190. package/dist/core/feedback/scope-parser.d.ts +86 -0
  191. package/dist/core/feedback/scope-parser.js +141 -0
  192. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  193. package/dist/core/feedback/self-performance-prep.js +541 -0
  194. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  195. package/dist/core/feedback/tuning-actuator.js +432 -0
  196. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  197. package/dist/core/feedback/tuning-recommender.js +580 -0
  198. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  199. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  200. package/dist/core/health-monitor.d.ts +6 -0
  201. package/dist/core/health-monitor.js +1 -1
  202. package/dist/core/injection-policy.d.ts +83 -1
  203. package/dist/core/injection-policy.js +61 -3
  204. package/dist/core/integration-main-backend.js +4 -0
  205. package/dist/core/management-md.d.ts +2 -2
  206. package/dist/core/management-md.js +51 -13
  207. package/dist/core/morning/orchestrator.d.ts +2 -2
  208. package/dist/core/morning/orchestrator.js +2 -2
  209. package/dist/core/notification-gate.d.ts +64 -0
  210. package/dist/core/notification-gate.js +51 -0
  211. package/dist/core/notification-rate-limit.d.ts +40 -0
  212. package/dist/core/notification-rate-limit.js +50 -0
  213. package/dist/core/policy-files.d.ts +1 -1
  214. package/dist/core/policy-files.js +2 -2
  215. package/dist/core/pre-pass-freshness.d.ts +4 -4
  216. package/dist/core/retention.d.ts +5 -0
  217. package/dist/core/retention.js +20 -4
  218. package/dist/core/review-context.d.ts +1 -1
  219. package/dist/core/review-context.js +10 -5
  220. package/dist/core/roadmap-write-lock.d.ts +2 -1
  221. package/dist/core/roadmap-write-lock.js +15 -10
  222. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  223. package/dist/core/routine-acquisition-plan.js +78 -20
  224. package/dist/core/routine-fetch-window-retry.js +7 -4
  225. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  226. package/dist/core/routine-fetch-window-runner.js +264 -13
  227. package/dist/core/routine-windows.d.ts +2 -2
  228. package/dist/core/routine-windows.js +8 -5
  229. package/dist/core/scheduler.d.ts +175 -16
  230. package/dist/core/scheduler.js +559 -102
  231. package/dist/core/signal-detector.d.ts +51 -1
  232. package/dist/core/signal-detector.js +321 -24
  233. package/dist/core/skills-compiler-denied-tools.js +2 -2
  234. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  235. package/dist/core/skills-compiler-skill-index.js +2 -2
  236. package/dist/core/skills-compiler-variants.d.ts +1 -1
  237. package/dist/core/skills-compiler-variants.js +8 -0
  238. package/dist/core/skills-compiler.d.ts +29 -26
  239. package/dist/core/skills-compiler.js +117 -81
  240. package/dist/core/skills-manifest.d.ts +37 -0
  241. package/dist/core/skills-manifest.js +73 -2
  242. package/dist/core/sleep-inhibitor.d.ts +79 -0
  243. package/dist/core/sleep-inhibitor.js +132 -0
  244. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  245. package/dist/core/slim-system-prompt-loader.js +141 -0
  246. package/dist/core/spawn-gates.d.ts +126 -0
  247. package/dist/core/spawn-gates.js +180 -0
  248. package/dist/core/today-direct-writer.d.ts +60 -14
  249. package/dist/core/today-direct-writer.js +90 -13
  250. package/dist/core/today-write-lock.d.ts +4 -2
  251. package/dist/core/today-write-lock.js +30 -20
  252. package/dist/core/wake-detector.d.ts +55 -0
  253. package/dist/core/wake-detector.js +80 -0
  254. package/dist/core/wiki/compile-lock.d.ts +1 -1
  255. package/dist/core/wiki/compile-lock.js +1 -1
  256. package/dist/core/wiki/wiki-fts.js +13 -6
  257. package/dist/core/workdir.js +15 -6
  258. package/dist/db/activity-scan-signals.d.ts +77 -0
  259. package/dist/db/activity-scan-signals.js +378 -0
  260. package/dist/db/agents-store.d.ts +28 -0
  261. package/dist/db/agents-store.js +62 -0
  262. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  263. package/dist/db/background-task-clarifications-store.js +152 -0
  264. package/dist/db/background-task-store.d.ts +207 -0
  265. package/dist/db/background-task-store.js +380 -0
  266. package/dist/db/browser-history-store.d.ts +39 -6
  267. package/dist/db/browser-history-store.js +51 -7
  268. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  269. package/dist/db/browser-task-clarifications-store.js +35 -5
  270. package/dist/db/browser-task-store.d.ts +3 -0
  271. package/dist/db/browser-task-store.js +29 -4
  272. package/dist/db/deferred-dm.d.ts +86 -0
  273. package/dist/db/deferred-dm.js +199 -0
  274. package/dist/db/feedback-signals-store.d.ts +77 -0
  275. package/dist/db/feedback-signals-store.js +144 -0
  276. package/dist/db/migrations.js +380 -0
  277. package/dist/db/observations.d.ts +2 -2
  278. package/dist/db/observations.js +3 -3
  279. package/dist/db/schema.js +260 -22
  280. package/dist/db/voice-transcripts-store.d.ts +1 -1
  281. package/dist/index.js +86 -29
  282. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  283. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  284. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  285. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  286. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  287. package/dist/observers/delegated-sync-worker.js +10 -10
  288. package/dist/observers/git-delegated-cron.d.ts +1 -1
  289. package/dist/observers/git-delegated-cron.js +2 -2
  290. package/dist/observers/github-poller-classifier.d.ts +3 -3
  291. package/dist/observers/github-poller-classifier.js +3 -3
  292. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  293. package/dist/observers/imminent-event-scheduler.js +1 -1
  294. package/dist/observers/mail-poller.d.ts +1 -0
  295. package/dist/observers/mail-poller.js +42 -3
  296. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  297. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  298. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  299. package/dist/observers/observation-summarizer/worker.js +4 -4
  300. package/dist/observers/obsidian-watcher.d.ts +1 -1
  301. package/dist/observers/obsidian-watcher.js +1 -1
  302. package/dist/safety/agent-write-tracker.d.ts +4 -4
  303. package/dist/safety/agent-write-tracker.js +4 -4
  304. package/dist/safety/always-disallowed.d.ts +1 -1
  305. package/dist/safety/always-disallowed.js +39 -0
  306. package/dist/safety/audit.d.ts +43 -5
  307. package/dist/safety/audit.js +86 -18
  308. package/dist/safety/risk-classifier.d.ts +6 -0
  309. package/dist/safety/risk-classifier.js +97 -18
  310. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  311. package/dist/scheduler/activity-scan-gate.js +132 -0
  312. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  313. package/dist/services/background-task/background-task-budget.js +91 -0
  314. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  315. package/dist/services/background-task/background-task-driver.js +416 -0
  316. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  317. package/dist/services/background-task/background-task-runner.js +673 -0
  318. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  319. package/dist/services/background-task/background-task-tools.js +247 -0
  320. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  321. package/dist/services/background-task/background-task-transition-events.js +54 -0
  322. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  323. package/dist/services/browser-history/automation/egress-denylist.js +34 -8
  324. package/dist/services/browser-history/lifecycle/platform.js +44 -2
  325. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  326. package/dist/services/browser-task/browser-task-runner.js +53 -8
  327. package/dist/services/mcp/probe.js +30 -8
  328. package/dist/services/observations-batch.d.ts +1 -1
  329. package/dist/services/observations-batch.js +2 -2
  330. package/dist/settings/runtime-settings.d.ts +45 -12
  331. package/dist/settings/runtime-settings.js +215 -40
  332. package/dist/settings/settings-store.js +11 -3
  333. package/package.json +4 -4
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Single source of truth for the daemon's per-process **slim** system-prompt
3
+ * templates. A slim prompt replaces the verbose `preset: "claude_code"` system
4
+ * prompt (Claude SDK) / the wide profile + skill-index instruction file
5
+ * (Codex / Gemini CLI) with a tight, self-contained operational contract for
6
+ * one short, mechanical, lite-tier process key — shedding ~30 K tokens of
7
+ * built-in tool descriptions, the skills index, the memory-system docs, and
8
+ * tone/style guidance the key never uses.
9
+ *
10
+ * Two cost-reduction efforts share this registry:
11
+ * - `routine.fetch_window` — docs/design/appendices/fetch-window-cost-reduction.md
12
+ * Phase 1 / 1.5 (the original slim prompt).
13
+ * - `routine.research_cluster_update` — RESEARCH_CLUSTER_COST_FIX_PLAN.md F4
14
+ * (Phase 2): the nightly per-cluster journal-append session.
15
+ *
16
+ * Adding a slim key is a one-line entry in `SLIM_SYSTEM_PROMPT_LOADERS` below
17
+ * (plus the asset file under agent-assets/system-prompts/ and, for CLI parity,
18
+ * the skill set in skills-compiler.ts:`SLIM_CLI_SKILL_SETS`).
19
+ *
20
+ * The same template is consumed by two backend paths so the body stays
21
+ * byte-identical across backends:
22
+ * - Claude SDK — passed as `query()`'s `systemPrompt` string by
23
+ * `claude-code-core.ts:buildSystemPrompt`.
24
+ * - Codex / Gemini CLI — written verbatim as `AGENTS.md` / `GEMINI.md` by
25
+ * `skills-compiler.ts:materializeSlimCliSession`.
26
+ *
27
+ * Hoisting the loaders out of `claude-code-core.ts` lets the skills compiler
28
+ * import them without a cross-backend dependency, and keeps the disk read +
29
+ * cache amortised across both code paths (one read per template per daemon
30
+ * boot regardless of how many sessions of either backend run).
31
+ *
32
+ * Each template is immutable for the daemon's lifetime, so a single
33
+ * per-template module-level cache is sufficient. The test reset helpers let
34
+ * unit tests simulate a fresh boot without reaching into module internals.
35
+ */
36
+ import { type ProcessKey } from "@aitne/shared";
37
+ /**
38
+ * Load the slim `routine.fetch_window` system-prompt template, caching the
39
+ * result for the daemon's lifetime.
40
+ */
41
+ export declare function loadFetchWindowSystemPrompt(): string;
42
+ /**
43
+ * Load the slim `routine.research_cluster_update` system-prompt template
44
+ * (RESEARCH_CLUSTER_COST_FIX_PLAN.md F4), caching for the daemon's lifetime.
45
+ */
46
+ export declare function loadResearchClusterUpdateSystemPrompt(): string;
47
+ /**
48
+ * Registry of process keys whose session uses a slim system prompt instead of
49
+ * the wide preset / profile. Typed as `Partial<Record<ProcessKey, …>>` (not an
50
+ * inferred string-literal map) so a rename in `@aitne/shared/process-key.ts`
51
+ * lights up here rather than silently dead-branching every consumer.
52
+ *
53
+ * Both call sites (Claude SDK `buildSystemPrompt`, CLI `materializeSlimCliSession`)
54
+ * resolve membership through `loadSlimSystemPrompt` / `isSlimSystemPromptKey`,
55
+ * so adding a key here is the single edit that wires both backends.
56
+ */
57
+ export declare const SLIM_SYSTEM_PROMPT_LOADERS: Partial<Record<ProcessKey, () => string>>;
58
+ /** True when `processKey` has a slim system-prompt template registered. */
59
+ export declare function isSlimSystemPromptKey(processKey: ProcessKey | undefined): boolean;
60
+ /**
61
+ * Load the slim system-prompt body for `processKey`, or `null` when the key
62
+ * has no slim template (callers fall through to the wide preset / profile).
63
+ * The disk read is amortised by the per-template cache.
64
+ */
65
+ export declare function loadSlimSystemPrompt(processKey: ProcessKey | undefined): string | null;
66
+ /**
67
+ * Test-only — drop every cached slim prompt so a subsequent load re-reads from
68
+ * disk. Production never needs this; the templates are immutable per boot.
69
+ */
70
+ export declare function resetSlimSystemPromptsForTest(): void;
71
+ /**
72
+ * Back-compat alias retained so the existing fetch_window tests / call sites
73
+ * keep working after the Phase 2 generalization. Resets every slim cache (a
74
+ * superset of the prior fetch_window-only reset — harmless for tests). Prefer
75
+ * `resetSlimSystemPromptsForTest` in new code.
76
+ */
77
+ export declare function resetFetchWindowSystemPromptForTest(): void;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Single source of truth for the daemon's per-process **slim** system-prompt
3
+ * templates. A slim prompt replaces the verbose `preset: "claude_code"` system
4
+ * prompt (Claude SDK) / the wide profile + skill-index instruction file
5
+ * (Codex / Gemini CLI) with a tight, self-contained operational contract for
6
+ * one short, mechanical, lite-tier process key — shedding ~30 K tokens of
7
+ * built-in tool descriptions, the skills index, the memory-system docs, and
8
+ * tone/style guidance the key never uses.
9
+ *
10
+ * Two cost-reduction efforts share this registry:
11
+ * - `routine.fetch_window` — docs/design/appendices/fetch-window-cost-reduction.md
12
+ * Phase 1 / 1.5 (the original slim prompt).
13
+ * - `routine.research_cluster_update` — RESEARCH_CLUSTER_COST_FIX_PLAN.md F4
14
+ * (Phase 2): the nightly per-cluster journal-append session.
15
+ *
16
+ * Adding a slim key is a one-line entry in `SLIM_SYSTEM_PROMPT_LOADERS` below
17
+ * (plus the asset file under agent-assets/system-prompts/ and, for CLI parity,
18
+ * the skill set in skills-compiler.ts:`SLIM_CLI_SKILL_SETS`).
19
+ *
20
+ * The same template is consumed by two backend paths so the body stays
21
+ * byte-identical across backends:
22
+ * - Claude SDK — passed as `query()`'s `systemPrompt` string by
23
+ * `claude-code-core.ts:buildSystemPrompt`.
24
+ * - Codex / Gemini CLI — written verbatim as `AGENTS.md` / `GEMINI.md` by
25
+ * `skills-compiler.ts:materializeSlimCliSession`.
26
+ *
27
+ * Hoisting the loaders out of `claude-code-core.ts` lets the skills compiler
28
+ * import them without a cross-backend dependency, and keeps the disk read +
29
+ * cache amortised across both code paths (one read per template per daemon
30
+ * boot regardless of how many sessions of either backend run).
31
+ *
32
+ * Each template is immutable for the daemon's lifetime, so a single
33
+ * per-template module-level cache is sufficient. The test reset helpers let
34
+ * unit tests simulate a fresh boot without reaching into module internals.
35
+ */
36
+ import { existsSync, readFileSync } from "node:fs";
37
+ import { dirname, join } from "node:path";
38
+ import { fileURLToPath } from "node:url";
39
+ import { substituteBrandTokens } from "@aitne/shared";
40
+ /**
41
+ * Resolve + read a slim-prompt asset by file basename (without the `.md`
42
+ * extension), applying brand-token substitution.
43
+ *
44
+ * Path resolution follows the same shape as `prompts.ts:resolveTaskFlowsDir`:
45
+ * `__dirname` lives at `packages/daemon/src/core/` (or the matching
46
+ * `dist/core/` after build), so the repo's `agent-assets/` directory is four
47
+ * levels up. The cwd fallback only fires in unusual harness layouts.
48
+ *
49
+ * Brand tokens (`{APP_NAME}`) are substituted here so the slim template stays
50
+ * aligned with the wide path's substitution contract: the Claude SDK
51
+ * systemPrompt and the CLI AGENTS.md / GEMINI.md both see the substituted
52
+ * product name instead of a literal `{APP_NAME}` leaking through. APP_NAME is
53
+ * a compile-time constant, so the per-boot cache stays byte-stable.
54
+ */
55
+ function readSlimSystemPromptAsset(basename) {
56
+ const here = dirname(fileURLToPath(import.meta.url));
57
+ const candidates = [
58
+ join(here, "..", "..", "..", "..", "agent-assets", "system-prompts", `${basename}.md`),
59
+ // Defensive fallback — if the module is invoked from an unexpected layout
60
+ // we still find the asset by walking up from cwd. Last-resort only; the
61
+ // relative path above is the canonical resolution.
62
+ join(process.cwd(), "agent-assets", "system-prompts", `${basename}.md`),
63
+ ];
64
+ for (const path of candidates) {
65
+ if (existsSync(path)) {
66
+ return substituteBrandTokens(readFileSync(path, "utf-8"));
67
+ }
68
+ }
69
+ throw new Error(`slim system prompt "${basename}" not found. Looked in: ${candidates.join(", ")}`);
70
+ }
71
+ // Per-template module-level caches. Each template is byte-stable per boot, so
72
+ // a single cached string is sufficient; the reset helper drops them for tests.
73
+ let cachedFetchWindowSystemPrompt = null;
74
+ let cachedResearchClusterUpdateSystemPrompt = null;
75
+ /**
76
+ * Load the slim `routine.fetch_window` system-prompt template, caching the
77
+ * result for the daemon's lifetime.
78
+ */
79
+ export function loadFetchWindowSystemPrompt() {
80
+ if (cachedFetchWindowSystemPrompt !== null)
81
+ return cachedFetchWindowSystemPrompt;
82
+ cachedFetchWindowSystemPrompt = readSlimSystemPromptAsset("routine-fetch-window");
83
+ return cachedFetchWindowSystemPrompt;
84
+ }
85
+ /**
86
+ * Load the slim `routine.research_cluster_update` system-prompt template
87
+ * (RESEARCH_CLUSTER_COST_FIX_PLAN.md F4), caching for the daemon's lifetime.
88
+ */
89
+ export function loadResearchClusterUpdateSystemPrompt() {
90
+ if (cachedResearchClusterUpdateSystemPrompt !== null) {
91
+ return cachedResearchClusterUpdateSystemPrompt;
92
+ }
93
+ cachedResearchClusterUpdateSystemPrompt = readSlimSystemPromptAsset("routine-research-cluster-update");
94
+ return cachedResearchClusterUpdateSystemPrompt;
95
+ }
96
+ /**
97
+ * Registry of process keys whose session uses a slim system prompt instead of
98
+ * the wide preset / profile. Typed as `Partial<Record<ProcessKey, …>>` (not an
99
+ * inferred string-literal map) so a rename in `@aitne/shared/process-key.ts`
100
+ * lights up here rather than silently dead-branching every consumer.
101
+ *
102
+ * Both call sites (Claude SDK `buildSystemPrompt`, CLI `materializeSlimCliSession`)
103
+ * resolve membership through `loadSlimSystemPrompt` / `isSlimSystemPromptKey`,
104
+ * so adding a key here is the single edit that wires both backends.
105
+ */
106
+ export const SLIM_SYSTEM_PROMPT_LOADERS = {
107
+ "routine.fetch_window": loadFetchWindowSystemPrompt,
108
+ "routine.research_cluster_update": loadResearchClusterUpdateSystemPrompt,
109
+ };
110
+ /** True when `processKey` has a slim system-prompt template registered. */
111
+ export function isSlimSystemPromptKey(processKey) {
112
+ return processKey !== undefined && processKey in SLIM_SYSTEM_PROMPT_LOADERS;
113
+ }
114
+ /**
115
+ * Load the slim system-prompt body for `processKey`, or `null` when the key
116
+ * has no slim template (callers fall through to the wide preset / profile).
117
+ * The disk read is amortised by the per-template cache.
118
+ */
119
+ export function loadSlimSystemPrompt(processKey) {
120
+ if (processKey === undefined)
121
+ return null;
122
+ const loader = SLIM_SYSTEM_PROMPT_LOADERS[processKey];
123
+ return loader ? loader() : null;
124
+ }
125
+ /**
126
+ * Test-only — drop every cached slim prompt so a subsequent load re-reads from
127
+ * disk. Production never needs this; the templates are immutable per boot.
128
+ */
129
+ export function resetSlimSystemPromptsForTest() {
130
+ cachedFetchWindowSystemPrompt = null;
131
+ cachedResearchClusterUpdateSystemPrompt = null;
132
+ }
133
+ /**
134
+ * Back-compat alias retained so the existing fetch_window tests / call sites
135
+ * keep working after the Phase 2 generalization. Resets every slim cache (a
136
+ * superset of the prior fetch_window-only reset — harmless for tests). Prefer
137
+ * `resetSlimSystemPromptsForTest` in new code.
138
+ */
139
+ export function resetFetchWindowSystemPromptForTest() {
140
+ resetSlimSystemPromptsForTest();
141
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Pre-spawn gates for autonomous routine sessions —
3
+ * PREPASS_COST_REDUCTION_PLAN.md N2.
4
+ *
5
+ * Two cheap checks run before the daemon spawns an autonomous backend
6
+ * session (routine dispatch + pre-pass fan-out sub-sessions), so a
7
+ * session that would deterministically fail is skipped instead of
8
+ * billed/spawned:
9
+ *
10
+ * 1. **Offline gate** — DNS-resolve the *backend API host* (the daemon
11
+ * never talks to integration APIs in delegated/native mode, so the
12
+ * backend host is the only one that matters). Uses `dns.lookup`
13
+ * (getaddrinfo — the same resolver path that produces the observed
14
+ * ENOTFOUND failures), NOT `dns.resolve` (c-ares bypasses the OS
15
+ * resolver / hosts file and can disagree with what the session would
16
+ * see). Results are cached ~60s per host so a fan-out of N
17
+ * integrations costs one lookup.
18
+ * 2. **Auth gate** — consult the cached auth-health row
19
+ * (`readCachedAuthStatus`) and treat the backend as non-viable only
20
+ * when `shouldSkip` is true (expired/missing with a ≤10-min-fresh
21
+ * cache, or a recovery subprocess owning the row). The hourly
22
+ * `checkAll()` probe and the reactive `recordReactiveAuth*` writers
23
+ * refresh the cache independently of routine sessions, so a
24
+ * recovered backend un-skips within minutes.
25
+ *
26
+ * **A spawn is skipped only when EVERY candidate backend (main +
27
+ * fallback) is non-viable.** The BackendRouter already skips an
28
+ * auth-unhealthy main straight to its fallback, so gating on the main
29
+ * alone would suppress sessions the router could have completed —
30
+ * exactly the accuracy degradation the now-scope forbids.
31
+ *
32
+ * Skips never touch pre-pass freshness state, so the next tick retries.
33
+ * Every decision is fail-open: an unknown backend host, a gate-internal
34
+ * error, or a DB failure lets the spawn proceed — the gate exists to
35
+ * save doomed sessions, never to block live ones.
36
+ */
37
+ import type Database from "better-sqlite3";
38
+ import type { BackendId } from "@aitne/shared";
39
+ /**
40
+ * Backend → API host the SDK/CLI must reach for a session to be viable.
41
+ * Hosts chosen per the default auth path of each backend's runtime:
42
+ * Claude Code → Anthropic API; Codex CLI (ChatGPT-plan auth) →
43
+ * chatgpt.com; Gemini CLI (OAuth code-assist) → cloudcode-pa. The gate
44
+ * only needs a DNS answer for outage detection, so an API-key install
45
+ * resolving a sibling host of the same provider is equally conclusive.
46
+ * Backends without an entry (e.g. opencode, which routes to arbitrary
47
+ * providers) are fail-open: the offline gate passes them.
48
+ */
49
+ export declare const BACKEND_API_HOSTS: Partial<Record<BackendId, string>>;
50
+ export type SpawnGateSkipReason = "offline" | "auth_unhealthy";
51
+ /** Per-candidate diagnostics carried into the skip audit row. */
52
+ export interface SpawnGateBackendVerdict {
53
+ backendId: BackendId;
54
+ /** Host probed by the offline gate; null = no mapping (gate passed). */
55
+ host: string | null;
56
+ /** True when the host failed to resolve (within the cache TTL). */
57
+ offline: boolean;
58
+ /** Cached auth status string (`ok` / `expired` / `missing` / …). */
59
+ authStatus: string;
60
+ /** True when the cached auth row says a spawn is doomed. */
61
+ authShouldSkip: boolean;
62
+ /** Net verdict: this backend could run the session. */
63
+ viable: boolean;
64
+ }
65
+ export interface SpawnGateDecision {
66
+ skip: boolean;
67
+ /**
68
+ * Present iff `skip`. `offline` when every candidate was blocked by
69
+ * DNS; `auth_unhealthy` when at least one candidate resolved but had
70
+ * a confirmed-bad auth cache.
71
+ */
72
+ reason?: SpawnGateSkipReason;
73
+ backends: SpawnGateBackendVerdict[];
74
+ }
75
+ export interface AutonomousSpawnGateOptions {
76
+ /** Injected for tests; defaults to `node:dns/promises.lookup`. */
77
+ lookup?: (host: string) => Promise<unknown>;
78
+ /** Injected clock for tests; defaults to `Date.now`. */
79
+ now?: () => number;
80
+ /** DNS verdict cache TTL; defaults to 60s. */
81
+ dnsCacheTtlMs?: number;
82
+ /** Per-lookup deadline (fail-open past it); defaults to 2.5s. */
83
+ dnsLookupTimeoutMs?: number;
84
+ /**
85
+ * Freshness window forwarded to `readCachedAuthStatus`. Omit to use
86
+ * that module's 10-minute default; callers with a configured
87
+ * `authPreflightFreshnessMs` should thread it through so the gate and
88
+ * the router agree on what "recently confirmed bad" means.
89
+ */
90
+ authFreshnessMs?: number;
91
+ /** Host mapping override for tests. */
92
+ backendApiHosts?: Partial<Record<BackendId, string>>;
93
+ }
94
+ export declare class AutonomousSpawnGate {
95
+ private readonly db;
96
+ private readonly lookup;
97
+ private readonly now;
98
+ private readonly dnsCacheTtlMs;
99
+ private readonly dnsLookupTimeoutMs;
100
+ private readonly authFreshnessMs;
101
+ private readonly hosts;
102
+ private readonly dnsCache;
103
+ constructor(db: Database.Database, options?: AutonomousSpawnGateOptions);
104
+ /**
105
+ * Evaluate the gates for the candidate backends that could run the
106
+ * session (binding main first, then fallback). Returns `skip: false`
107
+ * for an empty candidate list (nothing to assert) and on any internal
108
+ * error (fail-open).
109
+ */
110
+ evaluate(candidates: readonly BackendId[]): Promise<SpawnGateDecision>;
111
+ private evaluateBackend;
112
+ private hostResolves;
113
+ /**
114
+ * One bounded lookup attempt. Three fail-OPEN (`true`) outcomes that
115
+ * deliberately do not count as "offline":
116
+ * - the resolver answered (any address);
117
+ * - `EAI_AGAIN` — the resolver said "try again", which is a transient
118
+ * resolver condition, not an outage verdict;
119
+ * - the deadline elapsed — no answer is not a negative answer.
120
+ * Only a definitive resolution failure (ENOTFOUND et al.) returns
121
+ * `false`. The verdict — including a fail-open one — is cached by the
122
+ * caller for the TTL so a hung resolver costs at most one deadline
123
+ * per host per minute.
124
+ */
125
+ private lookupWithDeadline;
126
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Pre-spawn gates for autonomous routine sessions —
3
+ * PREPASS_COST_REDUCTION_PLAN.md N2.
4
+ *
5
+ * Two cheap checks run before the daemon spawns an autonomous backend
6
+ * session (routine dispatch + pre-pass fan-out sub-sessions), so a
7
+ * session that would deterministically fail is skipped instead of
8
+ * billed/spawned:
9
+ *
10
+ * 1. **Offline gate** — DNS-resolve the *backend API host* (the daemon
11
+ * never talks to integration APIs in delegated/native mode, so the
12
+ * backend host is the only one that matters). Uses `dns.lookup`
13
+ * (getaddrinfo — the same resolver path that produces the observed
14
+ * ENOTFOUND failures), NOT `dns.resolve` (c-ares bypasses the OS
15
+ * resolver / hosts file and can disagree with what the session would
16
+ * see). Results are cached ~60s per host so a fan-out of N
17
+ * integrations costs one lookup.
18
+ * 2. **Auth gate** — consult the cached auth-health row
19
+ * (`readCachedAuthStatus`) and treat the backend as non-viable only
20
+ * when `shouldSkip` is true (expired/missing with a ≤10-min-fresh
21
+ * cache, or a recovery subprocess owning the row). The hourly
22
+ * `checkAll()` probe and the reactive `recordReactiveAuth*` writers
23
+ * refresh the cache independently of routine sessions, so a
24
+ * recovered backend un-skips within minutes.
25
+ *
26
+ * **A spawn is skipped only when EVERY candidate backend (main +
27
+ * fallback) is non-viable.** The BackendRouter already skips an
28
+ * auth-unhealthy main straight to its fallback, so gating on the main
29
+ * alone would suppress sessions the router could have completed —
30
+ * exactly the accuracy degradation the now-scope forbids.
31
+ *
32
+ * Skips never touch pre-pass freshness state, so the next tick retries.
33
+ * Every decision is fail-open: an unknown backend host, a gate-internal
34
+ * error, or a DB failure lets the spawn proceed — the gate exists to
35
+ * save doomed sessions, never to block live ones.
36
+ */
37
+ import { lookup as dnsLookup } from "node:dns/promises";
38
+ import { readCachedAuthStatus } from "./backends/auth-health-monitor.js";
39
+ import { createLogger } from "../logging.js";
40
+ const logger = createLogger("spawn-gates");
41
+ /**
42
+ * Backend → API host the SDK/CLI must reach for a session to be viable.
43
+ * Hosts chosen per the default auth path of each backend's runtime:
44
+ * Claude Code → Anthropic API; Codex CLI (ChatGPT-plan auth) →
45
+ * chatgpt.com; Gemini CLI (OAuth code-assist) → cloudcode-pa. The gate
46
+ * only needs a DNS answer for outage detection, so an API-key install
47
+ * resolving a sibling host of the same provider is equally conclusive.
48
+ * Backends without an entry (e.g. opencode, which routes to arbitrary
49
+ * providers) are fail-open: the offline gate passes them.
50
+ */
51
+ export const BACKEND_API_HOSTS = {
52
+ claude: "api.anthropic.com",
53
+ codex: "chatgpt.com",
54
+ gemini: "cloudcode-pa.googleapis.com",
55
+ };
56
+ /** Default TTL for cached per-host DNS verdicts (~60s per the N2 spec). */
57
+ const DEFAULT_DNS_CACHE_TTL_MS = 60 * 1000;
58
+ /**
59
+ * Deadline for a single `dns.lookup` call. getaddrinfo has no timeout
60
+ * of its own — a degraded resolver can block for the OS resolver
61
+ * timeout (5-30s), serially per candidate host, stalling the
62
+ * autonomous lane and every fan-out sub-session start. Past the
63
+ * deadline the gate fails OPEN (treats the host as resolvable): an
64
+ * answer we don't have is not an outage signal.
65
+ */
66
+ const DEFAULT_DNS_LOOKUP_TIMEOUT_MS = 2_500;
67
+ export class AutonomousSpawnGate {
68
+ db;
69
+ lookup;
70
+ now;
71
+ dnsCacheTtlMs;
72
+ dnsLookupTimeoutMs;
73
+ authFreshnessMs;
74
+ hosts;
75
+ dnsCache = new Map();
76
+ constructor(db, options = {}) {
77
+ this.db = db;
78
+ // `dnsLookup` is referenced directly (no wrapper arrow) so the
79
+ // default arm carries no never-invoked closure — tests cover the
80
+ // `??` branch by constructing without options, without doing real
81
+ // DNS.
82
+ this.lookup = options.lookup ?? dnsLookup;
83
+ this.now = options.now ?? (() => Date.now());
84
+ this.dnsCacheTtlMs = options.dnsCacheTtlMs ?? DEFAULT_DNS_CACHE_TTL_MS;
85
+ this.dnsLookupTimeoutMs =
86
+ options.dnsLookupTimeoutMs ?? DEFAULT_DNS_LOOKUP_TIMEOUT_MS;
87
+ this.authFreshnessMs = options.authFreshnessMs;
88
+ this.hosts = options.backendApiHosts ?? BACKEND_API_HOSTS;
89
+ }
90
+ /**
91
+ * Evaluate the gates for the candidate backends that could run the
92
+ * session (binding main first, then fallback). Returns `skip: false`
93
+ * for an empty candidate list (nothing to assert) and on any internal
94
+ * error (fail-open).
95
+ */
96
+ async evaluate(candidates) {
97
+ try {
98
+ if (candidates.length === 0) {
99
+ return { skip: false, backends: [] };
100
+ }
101
+ const backends = [];
102
+ for (const backendId of candidates) {
103
+ backends.push(await this.evaluateBackend(backendId));
104
+ }
105
+ if (backends.some((b) => b.viable)) {
106
+ return { skip: false, backends };
107
+ }
108
+ const reason = backends.every((b) => b.offline)
109
+ ? "offline"
110
+ : "auth_unhealthy";
111
+ return { skip: true, reason, backends };
112
+ }
113
+ catch (err) {
114
+ logger.warn({ err, candidates }, "Spawn-gate evaluation failed — failing open");
115
+ return { skip: false, backends: [] };
116
+ }
117
+ }
118
+ async evaluateBackend(backendId) {
119
+ const host = this.hosts[backendId] ?? null;
120
+ const offline = host === null ? false : !(await this.hostResolves(host));
121
+ // readCachedAuthStatus is fail-open by contract (returns
122
+ // `{status:"unknown", shouldSkip:false}` on any DB error), and
123
+ // `evaluate()`'s outer catch fails the whole gate open as the last
124
+ // line of defense — no per-call try/catch needed here.
125
+ const cached = this.authFreshnessMs === undefined
126
+ ? readCachedAuthStatus(this.db, backendId)
127
+ : readCachedAuthStatus(this.db, backendId, this.authFreshnessMs);
128
+ return {
129
+ backendId,
130
+ host,
131
+ offline,
132
+ authStatus: cached.status,
133
+ authShouldSkip: cached.shouldSkip,
134
+ viable: !offline && !cached.shouldSkip,
135
+ };
136
+ }
137
+ async hostResolves(host) {
138
+ const nowMs = this.now();
139
+ const cached = this.dnsCache.get(host);
140
+ if (cached && cached.expiresAtMs > nowMs) {
141
+ return cached.ok;
142
+ }
143
+ const ok = await this.lookupWithDeadline(host);
144
+ this.dnsCache.set(host, {
145
+ ok,
146
+ expiresAtMs: this.now() + this.dnsCacheTtlMs,
147
+ });
148
+ return ok;
149
+ }
150
+ /**
151
+ * One bounded lookup attempt. Three fail-OPEN (`true`) outcomes that
152
+ * deliberately do not count as "offline":
153
+ * - the resolver answered (any address);
154
+ * - `EAI_AGAIN` — the resolver said "try again", which is a transient
155
+ * resolver condition, not an outage verdict;
156
+ * - the deadline elapsed — no answer is not a negative answer.
157
+ * Only a definitive resolution failure (ENOTFOUND et al.) returns
158
+ * `false`. The verdict — including a fail-open one — is cached by the
159
+ * caller for the TTL so a hung resolver costs at most one deadline
160
+ * per host per minute.
161
+ */
162
+ async lookupWithDeadline(host) {
163
+ let timer;
164
+ const attempt = this.lookup(host).then(() => true, (err) => {
165
+ const code = typeof err === "object" && err !== null && "code" in err
166
+ ? err.code
167
+ : undefined;
168
+ return code === "EAI_AGAIN";
169
+ });
170
+ const deadline = new Promise((resolve) => {
171
+ timer = setTimeout(() => resolve(true), this.dnsLookupTimeoutMs);
172
+ });
173
+ try {
174
+ return await Promise.race([attempt, deadline]);
175
+ }
176
+ finally {
177
+ clearTimeout(timer);
178
+ }
179
+ }
180
+ }
@@ -1,9 +1,15 @@
1
1
  /**
2
- * Daemon-direct writer that appends a single bullet to today.md
3
- * `## Agent Log` without going through the agent at all. Used by the
4
- * three-stage hourly_check gate (cost-reduction-structural §B) on the
5
- * stage0_silent / stage2_log_only paths so a "no-op" cron tick still
6
- * leaves an audit trail in today.md without paying for an LLM session.
2
+ * Daemon-direct, lock-aware writes to today.md that run *before* (or
3
+ * instead of) an agent session bypassing `/api/context/*` because there
4
+ * is no subprocess to issue the curl call from. Two operations live here:
5
+ *
6
+ * 1. {@link appendAgentLogLine} append a single `## Agent Log` bullet.
7
+ * Used by the three-stage activity_scan gate (cost-reduction-structural
8
+ * §B) on the stage0_silent / stage2_log_only paths so a "no-op" cron
9
+ * tick still leaves an audit trail without paying for an LLM session.
10
+ * 2. {@link ensureTodaySkeleton} — seed the canonical empty skeleton when
11
+ * today.md is **absent**, so a section-only refresh routine has a valid
12
+ * PATCH target instead of 404-ing and budget-burning on full-file PUTs.
7
13
  *
8
14
  * Why this lives in the daemon (not /api/context/* via curl):
9
15
  * - These paths run *before* the agent is spawned. There is no
@@ -12,15 +18,16 @@
12
18
  * `context-staleness.ts`) — the same tier the PATCH route already
13
19
  * classifies for `## Agent Log` appends. Bypassing the route is
14
20
  * fine because we never touch the prompt-context-changed hook here.
15
- * - The today-write-lock invariant is preserved: we acquire it before
16
- * mutating the file, so morning_routine and direct writes never
17
- * interleave.
21
+ * - The today-write-lock invariant is preserved: both functions acquire
22
+ * it before mutating the file, so morning_routine and direct writes
23
+ * never interleave.
18
24
  *
19
- * Failure mode: when today.md is missing, malformed, or lacks the
20
- * `## Agent Log` heading, the writer logs a warning and returns false
21
- * rather than synthesizing the structure — that is the morning routine's
22
- * job. The gate caller treats false as "log not appended; consume
23
- * observations regardless".
25
+ * Synthesis boundary: `appendAgentLogLine` NEVER synthesizes structure
26
+ * a missing / malformed / heading-less file returns false and the gate
27
+ * caller proceeds. `ensureTodaySkeleton` synthesizes ONLY the empty
28
+ * skeleton, ONLY when the file is entirely absent, and never touches a
29
+ * present file. Neither populates today.md — full creation and repair stay
30
+ * the morning routine's job.
24
31
  */
25
32
  import type { TodayWriteLockManager } from "./today-write-lock.js";
26
33
  export interface AppendAgentLogLineInput {
@@ -28,7 +35,7 @@ export interface AppendAgentLogLineInput {
28
35
  /**
29
36
  * Bullet text to append, **without** the leading `- ` prefix and
30
37
  * without a trailing newline. The writer normalizes both.
31
- * Example: `"12:00 [hourly_check] Quiet — 0 obs"`.
38
+ * Example: `"12:00 [activity_scan] Quiet — 0 obs"`.
32
39
  */
33
40
  message: string;
34
41
  todayWriteLock: TodayWriteLockManager;
@@ -61,6 +68,45 @@ export interface AppendAgentLogLineResult {
61
68
  * writers cleanly.
62
69
  */
63
70
  export declare function appendAgentLogLine(input: AppendAgentLogLineInput): Promise<AppendAgentLogLineResult>;
71
+ export interface EnsureTodaySkeletonInput {
72
+ contextDir: string;
73
+ todayWriteLock: TodayWriteLockManager;
74
+ }
75
+ export interface EnsureTodaySkeletonResult {
76
+ seeded: boolean;
77
+ reason?: "already_present" | "lock_unavailable" | "io_error";
78
+ }
79
+ /**
80
+ * Guarantee a `today.md` working surface exists before a section-only
81
+ * refresh routine (`routine.today_refresh`) assumes it.
82
+ *
83
+ * `rotateDayFiles()` intentionally renames `today.md` → `yesterday.md` at
84
+ * the day boundary and relies on the morning routine to recreate the
85
+ * dated file. When the morning routine has not run yet — or failed (e.g.
86
+ * a quota/budget death with no fallback backend) — `today.md` is absent
87
+ * and the refresh task flow's `PATCH section=user_schedule` 404s. The
88
+ * agent then improvises full-file `PUT`s, which the strict
89
+ * `validateTodayContent` schema rejects line-by-line; on a single-backend
90
+ * binding with a tight per-turn budget that loop tips into
91
+ * `BackendQuotaError(max_budget_usd)` and the refresh dies without ever
92
+ * writing the file — the "Refresh Today does nothing" symptom.
93
+ *
94
+ * This deterministic pre-step removes that whole failure mode: when the
95
+ * file is **entirely absent** we seed the canonical empty skeleton so the
96
+ * agent's section PATCH always has a valid target. A file that already
97
+ * exists is left byte-untouched — a valid dated file OR the legacy
98
+ * `# Today` bridge stub both accept the section PATCH (the route's
99
+ * `allowLegacyToday` branch). We never repair a malformed-but-present
100
+ * file and never overwrite user content; full creation/repair stays the
101
+ * morning routine's job. The seeded skeleton is dateless (`# Today`), so
102
+ * it does NOT satisfy `hasCurrentAgentDayTodayMd()` and the pending
103
+ * morning-routine retry still fires and upgrades it.
104
+ *
105
+ * Lock-aware exactly like {@link appendAgentLogLine}: if the morning
106
+ * routine holds the today-write-lock (mid-creation) we skip and let it
107
+ * win — the refresh session then 409-defers on its own PATCH.
108
+ */
109
+ export declare function ensureTodaySkeleton(input: EnsureTodaySkeletonInput): Promise<EnsureTodaySkeletonResult>;
64
110
  /**
65
111
  * Splice a new bullet line into the `## Agent Log` section, immediately
66
112
  * before the next `## ` heading or end-of-file. Returns null when the