@aitne/daemon 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/dist/adapters/adapter-watchdog.d.ts +70 -0
  2. package/dist/adapters/adapter-watchdog.js +115 -0
  3. package/dist/adapters/discord.d.ts +17 -1
  4. package/dist/adapters/discord.js +33 -0
  5. package/dist/adapters/notification-manager.d.ts +27 -1
  6. package/dist/adapters/notification-manager.js +54 -39
  7. package/dist/adapters/slack-adapter.d.ts +26 -1
  8. package/dist/adapters/slack-adapter.js +41 -0
  9. package/dist/adapters/telegram-adapter.d.ts +18 -1
  10. package/dist/adapters/telegram-adapter.js +41 -2
  11. package/dist/adapters/types.d.ts +20 -0
  12. package/dist/adapters/whatsapp-adapter.d.ts +26 -7
  13. package/dist/adapters/whatsapp-adapter.js +74 -21
  14. package/dist/api/env-writer.js +8 -5
  15. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  16. package/dist/api/helpers/agent-errors-registry.js +5 -5
  17. package/dist/api/routes/agent.js +33 -12
  18. package/dist/api/routes/agents/index.js +75 -16
  19. package/dist/api/routes/agents/views.d.ts +37 -2
  20. package/dist/api/routes/agents/views.js +64 -2
  21. package/dist/api/routes/background-task.d.ts +22 -0
  22. package/dist/api/routes/background-task.js +338 -0
  23. package/dist/api/routes/browser-history.js +9 -1
  24. package/dist/api/routes/context/permissions.js +3 -2
  25. package/dist/api/routes/context/snapshots.js +0 -3
  26. package/dist/api/routes/context/write.js +3 -17
  27. package/dist/api/routes/dashboard/config.js +48 -12
  28. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  29. package/dist/api/routes/dashboard/notifications.js +9 -9
  30. package/dist/api/routes/integrations/crud-patch.js +5 -1
  31. package/dist/api/routes/integrations-reconcile.js +2 -2
  32. package/dist/api/routes/notion.d.ts +1 -1
  33. package/dist/api/routes/observations.js +7 -7
  34. package/dist/api/routes/obsidian.d.ts +1 -1
  35. package/dist/api/routes/receipts.js +5 -1
  36. package/dist/api/routes/setup-migrate.js +1 -1
  37. package/dist/api/routes/setup.js +1 -1
  38. package/dist/api/routes/task-flows.d.ts +1 -1
  39. package/dist/api/routes/task-flows.js +1 -1
  40. package/dist/api/routes/tuning.d.ts +29 -0
  41. package/dist/api/routes/tuning.js +304 -0
  42. package/dist/api/server.d.ts +44 -16
  43. package/dist/api/server.js +9 -0
  44. package/dist/bootstrap/adapters.d.ts +19 -0
  45. package/dist/bootstrap/adapters.js +61 -0
  46. package/dist/bootstrap/api.d.ts +5 -3
  47. package/dist/bootstrap/api.js +45 -13
  48. package/dist/bootstrap/catchup.d.ts +1 -1
  49. package/dist/bootstrap/catchup.js +11 -11
  50. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  51. package/dist/bootstrap/event-pipeline.js +245 -7
  52. package/dist/bootstrap/observers.js +9 -6
  53. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  54. package/dist/bootstrap/schedule-helpers.js +172 -19
  55. package/dist/config.js +26 -12
  56. package/dist/core/agent-core.d.ts +33 -1
  57. package/dist/core/agent-core.js +36 -1
  58. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  59. package/dist/core/agents/activity-scan-cadence.js +127 -0
  60. package/dist/core/agents/agent-route-override.d.ts +53 -0
  61. package/dist/core/agents/agent-route-override.js +69 -0
  62. package/dist/core/agents/builtin-registry.d.ts +51 -14
  63. package/dist/core/agents/builtin-registry.js +92 -15
  64. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  65. package/dist/core/agents/config-gate-reconcile.js +51 -0
  66. package/dist/core/agents/cron-substitute.d.ts +1 -1
  67. package/dist/core/agents/cron-substitute.js +1 -1
  68. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  69. package/dist/core/agents/custom-routine-migration.js +149 -0
  70. package/dist/core/agents/firing-blocked.d.ts +1 -1
  71. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  72. package/dist/core/agents/hourly-cadence.js +126 -0
  73. package/dist/core/agents/loader-boot.js +23 -0
  74. package/dist/core/agents/loader.d.ts +19 -0
  75. package/dist/core/agents/loader.js +34 -2
  76. package/dist/core/agents/override-merge.d.ts +1 -1
  77. package/dist/core/agents/override-merge.js +9 -1
  78. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  79. package/dist/core/agents/recurrence-convert.js +1 -1
  80. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  81. package/dist/core/alerts.js +6 -6
  82. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  83. package/dist/core/backends/auth-health-monitor.js +1 -1
  84. package/dist/core/backends/backend-router.d.ts +27 -1
  85. package/dist/core/backends/backend-router.js +165 -1
  86. package/dist/core/backends/claude-code-core.d.ts +71 -31
  87. package/dist/core/backends/claude-code-core.js +282 -54
  88. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  89. package/dist/core/backends/cli-quota-guards.js +40 -5
  90. package/dist/core/backends/codex-core.d.ts +6 -0
  91. package/dist/core/backends/codex-core.js +22 -6
  92. package/dist/core/backends/failure-spend.d.ts +58 -0
  93. package/dist/core/backends/failure-spend.js +137 -0
  94. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  95. package/dist/core/backends/gemini-cli-core.js +25 -6
  96. package/dist/core/backends/model-registry.d.ts +1 -1
  97. package/dist/core/backends/model-registry.js +4 -4
  98. package/dist/core/backends/opencode-core.d.ts +1 -1
  99. package/dist/core/backends/opencode-core.js +5 -5
  100. package/dist/core/backends/plan-presets.js +39 -15
  101. package/dist/core/bang-commands/commands-cost.js +3 -1
  102. package/dist/core/bang-commands/commands-report.js +4 -3
  103. package/dist/core/bang-commands/commands-research.js +4 -1
  104. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  105. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  106. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  107. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  108. package/dist/core/bang-commands/commands-task-control.js +147 -0
  109. package/dist/core/bang-commands/commands-wiki.js +5 -5
  110. package/dist/core/bang-commands/index.d.ts +2 -0
  111. package/dist/core/bang-commands/index.js +12 -0
  112. package/dist/core/bang-commands/registry.d.ts +12 -0
  113. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  114. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  115. package/dist/core/channel-timeline.d.ts +5 -1
  116. package/dist/core/channel-timeline.js +13 -0
  117. package/dist/core/context/index-reconciler.js +5 -2
  118. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  119. package/dist/core/context/policy-index-runner.js +25 -6
  120. package/dist/core/context-builder-calendar.js +10 -2
  121. package/dist/core/context-builder-conversation.d.ts +8 -1
  122. package/dist/core/context-builder-conversation.js +41 -7
  123. package/dist/core/context-builder-yesterday.js +4 -3
  124. package/dist/core/context-builder.d.ts +7 -2
  125. package/dist/core/context-builder.js +62 -20
  126. package/dist/core/context-file-serializer.d.ts +1 -1
  127. package/dist/core/context-file-serializer.js +1 -1
  128. package/dist/core/context-health.js +2 -2
  129. package/dist/core/context-paths.d.ts +1 -1
  130. package/dist/core/context-paths.js +1 -1
  131. package/dist/core/context-validation/prepare-write.js +1 -1
  132. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  133. package/dist/core/context-vault-aliases.d.ts +0 -13
  134. package/dist/core/context-vault-aliases.js +37 -0
  135. package/dist/core/custom-routines.d.ts +99 -0
  136. package/dist/core/custom-routines.js +187 -0
  137. package/dist/core/daemon-api-cli.js +49 -0
  138. package/dist/core/day-boundary.d.ts +46 -0
  139. package/dist/core/day-boundary.js +40 -0
  140. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  141. package/dist/core/dispatcher-activity-scan.js +775 -0
  142. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  143. package/dist/core/dispatcher-error-handling.js +38 -62
  144. package/dist/core/dispatcher-hourly-check.js +6 -1
  145. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  146. package/dist/core/dispatcher-message-handler.js +17 -0
  147. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  148. package/dist/core/dispatcher-morning-routine.js +13 -13
  149. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  150. package/dist/core/dispatcher-result-processor.js +167 -11
  151. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  152. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  153. package/dist/core/dispatcher-scheduled-tasks.d.ts +63 -1
  154. package/dist/core/dispatcher-scheduled-tasks.js +213 -6
  155. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  156. package/dist/core/dispatcher-task-delivery.js +555 -0
  157. package/dist/core/dispatcher-types.d.ts +48 -9
  158. package/dist/core/dispatcher-types.js +3 -3
  159. package/dist/core/dispatcher.d.ts +112 -31
  160. package/dist/core/dispatcher.js +284 -59
  161. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  162. package/dist/core/drift-effects.js +2 -2
  163. package/dist/core/feedback/consolidation-prep.js +17 -5
  164. package/dist/core/feedback/eviction-scorer.js +6 -2
  165. package/dist/core/feedback/lesson-format.js +9 -4
  166. package/dist/core/feedback/lesson-injection.d.ts +1 -1
  167. package/dist/core/feedback/lesson-injection.js +17 -2
  168. package/dist/core/feedback/lesson-store-overview.d.ts +8 -4
  169. package/dist/core/feedback/lesson-store-overview.js +8 -4
  170. package/dist/core/feedback/regeneralization-prep.js +29 -16
  171. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  172. package/dist/core/feedback/self-performance-prep.js +541 -0
  173. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  174. package/dist/core/feedback/tuning-actuator.js +432 -0
  175. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  176. package/dist/core/feedback/tuning-recommender.js +580 -0
  177. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  178. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  179. package/dist/core/health-monitor.d.ts +6 -0
  180. package/dist/core/health-monitor.js +1 -1
  181. package/dist/core/injection-policy.d.ts +4 -4
  182. package/dist/core/injection-policy.js +4 -4
  183. package/dist/core/integration-main-backend.js +4 -0
  184. package/dist/core/management-md.d.ts +2 -2
  185. package/dist/core/management-md.js +51 -13
  186. package/dist/core/morning/orchestrator.d.ts +2 -2
  187. package/dist/core/morning/orchestrator.js +2 -2
  188. package/dist/core/notification-gate.d.ts +64 -0
  189. package/dist/core/notification-gate.js +51 -0
  190. package/dist/core/notification-rate-limit.d.ts +40 -0
  191. package/dist/core/notification-rate-limit.js +50 -0
  192. package/dist/core/policy-files.d.ts +1 -1
  193. package/dist/core/policy-files.js +2 -2
  194. package/dist/core/pre-pass-freshness.d.ts +4 -4
  195. package/dist/core/retention.d.ts +5 -0
  196. package/dist/core/retention.js +20 -4
  197. package/dist/core/review-context.d.ts +1 -1
  198. package/dist/core/review-context.js +10 -5
  199. package/dist/core/roadmap-write-lock.d.ts +2 -1
  200. package/dist/core/roadmap-write-lock.js +15 -10
  201. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  202. package/dist/core/routine-acquisition-plan.js +78 -20
  203. package/dist/core/routine-fetch-window-retry.js +7 -4
  204. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  205. package/dist/core/routine-fetch-window-runner.js +264 -13
  206. package/dist/core/routine-windows.d.ts +2 -2
  207. package/dist/core/routine-windows.js +8 -5
  208. package/dist/core/scheduler.d.ts +175 -16
  209. package/dist/core/scheduler.js +559 -102
  210. package/dist/core/signal-detector.d.ts +12 -0
  211. package/dist/core/signal-detector.js +53 -9
  212. package/dist/core/skills-compiler-denied-tools.js +2 -2
  213. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  214. package/dist/core/skills-compiler-skill-index.js +2 -2
  215. package/dist/core/skills-compiler-variants.d.ts +1 -1
  216. package/dist/core/skills-compiler-variants.js +8 -0
  217. package/dist/core/skills-compiler.d.ts +29 -26
  218. package/dist/core/skills-compiler.js +117 -81
  219. package/dist/core/skills-manifest.d.ts +37 -0
  220. package/dist/core/skills-manifest.js +73 -2
  221. package/dist/core/sleep-inhibitor.d.ts +79 -0
  222. package/dist/core/sleep-inhibitor.js +132 -0
  223. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  224. package/dist/core/slim-system-prompt-loader.js +141 -0
  225. package/dist/core/spawn-gates.d.ts +126 -0
  226. package/dist/core/spawn-gates.js +180 -0
  227. package/dist/core/today-direct-writer.d.ts +2 -2
  228. package/dist/core/today-direct-writer.js +1 -1
  229. package/dist/core/today-write-lock.d.ts +4 -2
  230. package/dist/core/today-write-lock.js +30 -20
  231. package/dist/core/wake-detector.d.ts +55 -0
  232. package/dist/core/wake-detector.js +80 -0
  233. package/dist/core/wiki/compile-lock.d.ts +1 -1
  234. package/dist/core/wiki/compile-lock.js +1 -1
  235. package/dist/core/workdir.js +15 -6
  236. package/dist/db/activity-scan-signals.d.ts +77 -0
  237. package/dist/db/activity-scan-signals.js +378 -0
  238. package/dist/db/agents-store.d.ts +28 -0
  239. package/dist/db/agents-store.js +62 -0
  240. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  241. package/dist/db/background-task-clarifications-store.js +152 -0
  242. package/dist/db/background-task-store.d.ts +207 -0
  243. package/dist/db/background-task-store.js +380 -0
  244. package/dist/db/browser-history-store.d.ts +39 -6
  245. package/dist/db/browser-history-store.js +51 -7
  246. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  247. package/dist/db/browser-task-clarifications-store.js +35 -5
  248. package/dist/db/browser-task-store.d.ts +3 -0
  249. package/dist/db/browser-task-store.js +29 -4
  250. package/dist/db/deferred-dm.d.ts +86 -0
  251. package/dist/db/deferred-dm.js +199 -0
  252. package/dist/db/migrations.js +330 -0
  253. package/dist/db/observations.d.ts +2 -2
  254. package/dist/db/observations.js +3 -3
  255. package/dist/db/schema.js +217 -16
  256. package/dist/db/voice-transcripts-store.d.ts +1 -1
  257. package/dist/index.js +86 -29
  258. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  259. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  260. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  261. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  262. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  263. package/dist/observers/delegated-sync-worker.js +10 -10
  264. package/dist/observers/git-delegated-cron.d.ts +1 -1
  265. package/dist/observers/git-delegated-cron.js +2 -2
  266. package/dist/observers/github-poller-classifier.d.ts +3 -3
  267. package/dist/observers/github-poller-classifier.js +3 -3
  268. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  269. package/dist/observers/imminent-event-scheduler.js +1 -1
  270. package/dist/observers/mail-poller.d.ts +1 -0
  271. package/dist/observers/mail-poller.js +42 -3
  272. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  273. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  274. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  275. package/dist/observers/observation-summarizer/worker.js +4 -4
  276. package/dist/observers/obsidian-watcher.d.ts +1 -1
  277. package/dist/observers/obsidian-watcher.js +1 -1
  278. package/dist/safety/agent-write-tracker.d.ts +4 -4
  279. package/dist/safety/agent-write-tracker.js +4 -4
  280. package/dist/safety/audit.d.ts +43 -5
  281. package/dist/safety/audit.js +86 -18
  282. package/dist/safety/risk-classifier.d.ts +6 -0
  283. package/dist/safety/risk-classifier.js +75 -11
  284. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  285. package/dist/scheduler/activity-scan-gate.js +132 -0
  286. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  287. package/dist/services/background-task/background-task-budget.js +91 -0
  288. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  289. package/dist/services/background-task/background-task-driver.js +416 -0
  290. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  291. package/dist/services/background-task/background-task-runner.js +673 -0
  292. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  293. package/dist/services/background-task/background-task-tools.js +247 -0
  294. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  295. package/dist/services/background-task/background-task-transition-events.js +54 -0
  296. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  297. package/dist/services/browser-history/automation/egress-denylist.js +16 -6
  298. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  299. package/dist/services/browser-task/browser-task-runner.js +53 -8
  300. package/dist/services/observations-batch.d.ts +1 -1
  301. package/dist/services/observations-batch.js +2 -2
  302. package/dist/settings/runtime-settings.d.ts +38 -11
  303. package/dist/settings/runtime-settings.js +203 -40
  304. package/dist/settings/settings-store.js +11 -3
  305. package/package.json +4 -4
@@ -0,0 +1,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
+ }
@@ -4,7 +4,7 @@
4
4
  * is no subprocess to issue the curl call from. Two operations live here:
5
5
  *
6
6
  * 1. {@link appendAgentLogLine} — append a single `## Agent Log` bullet.
7
- * Used by the three-stage hourly_check gate (cost-reduction-structural
7
+ * Used by the three-stage activity_scan gate (cost-reduction-structural
8
8
  * §B) on the stage0_silent / stage2_log_only paths so a "no-op" cron
9
9
  * tick still leaves an audit trail without paying for an LLM session.
10
10
  * 2. {@link ensureTodaySkeleton} — seed the canonical empty skeleton when
@@ -35,7 +35,7 @@ export interface AppendAgentLogLineInput {
35
35
  /**
36
36
  * Bullet text to append, **without** the leading `- ` prefix and
37
37
  * without a trailing newline. The writer normalizes both.
38
- * Example: `"12:00 [hourly_check] Quiet — 0 obs"`.
38
+ * Example: `"12:00 [activity_scan] Quiet — 0 obs"`.
39
39
  */
40
40
  message: string;
41
41
  todayWriteLock: TodayWriteLockManager;
@@ -4,7 +4,7 @@
4
4
  * is no subprocess to issue the curl call from. Two operations live here:
5
5
  *
6
6
  * 1. {@link appendAgentLogLine} — append a single `## Agent Log` bullet.
7
- * Used by the three-stage hourly_check gate (cost-reduction-structural
7
+ * Used by the three-stage activity_scan gate (cost-reduction-structural
8
8
  * §B) on the stage0_silent / stage2_log_only paths so a "no-op" cron
9
9
  * tick still leaves an audit trail without paying for an LLM session.
10
10
  * 2. {@link ensureTodaySkeleton} — seed the canonical empty skeleton when
@@ -13,8 +13,9 @@ export interface TodayWriteLockManager {
13
13
  export declare class InMemoryTodayWriteLockManager implements TodayWriteLockManager {
14
14
  private readonly timeoutMs;
15
15
  private holder;
16
- private timer;
16
+ private expiresAtMs;
17
17
  constructor(timeoutMs: number);
18
+ private expireIfStale;
18
19
  acquire(): {
19
20
  ok: true;
20
21
  lockId: string;
@@ -38,8 +39,9 @@ export declare function getTodayWriteLockTimeoutMs(executeTimeoutMinutes: number
38
39
  export declare class MigrationLock {
39
40
  private readonly timeoutMs;
40
41
  private holder;
41
- private timer;
42
+ private expiresAtMs;
42
43
  constructor(timeoutMs: number);
44
+ private expireIfStale;
43
45
  acquire(): {
44
46
  ok: true;
45
47
  lockId: string;
@@ -4,44 +4,50 @@ const logger = createLogger("today-write-lock");
4
4
  export class InMemoryTodayWriteLockManager {
5
5
  timeoutMs;
6
6
  holder = null;
7
- timer = null;
7
+ expiresAtMs = 0;
8
8
  constructor(timeoutMs) {
9
9
  this.timeoutMs = timeoutMs;
10
10
  }
11
+ // Expiry is wall-clock (Date.now) checked lazily on access, not a
12
+ // setTimeout: Node timers run on the monotonic clock and don't advance
13
+ // while the machine sleeps, so a timer armed before sleep would hold the
14
+ // lock up to the whole sleep duration past its intended TTL.
15
+ expireIfStale() {
16
+ if (this.holder && Date.now() >= this.expiresAtMs) {
17
+ logger.warn({ lockId: this.holder }, "Today write lock expired by timeout");
18
+ this.holder = null;
19
+ }
20
+ }
11
21
  acquire() {
22
+ this.expireIfStale();
12
23
  if (this.holder) {
13
24
  logger.debug({ existingHolder: this.holder }, "Lock acquire rejected — already held");
14
25
  return { ok: false, holder: this.holder };
15
26
  }
16
27
  const lockId = randomUUID();
17
28
  this.holder = lockId;
18
- this.timer = setTimeout(() => {
19
- logger.warn({ lockId: this.holder }, "Today write lock expired by timeout");
20
- this.holder = null;
21
- this.timer = null;
22
- }, this.timeoutMs);
29
+ this.expiresAtMs = Date.now() + this.timeoutMs;
23
30
  logger.debug({ lockId }, "Today write lock acquired");
24
31
  return { ok: true, lockId };
25
32
  }
26
33
  release(lockId) {
34
+ this.expireIfStale();
27
35
  if (!this.holder || this.holder !== lockId) {
28
36
  return false;
29
37
  }
30
38
  this.holder = null;
31
- if (this.timer) {
32
- clearTimeout(this.timer);
33
- this.timer = null;
34
- }
35
39
  logger.debug({ lockId }, "Today write lock released");
36
40
  return true;
37
41
  }
38
42
  isHeldBy(lockId) {
43
+ this.expireIfStale();
39
44
  if (!this.holder) {
40
45
  return false;
41
46
  }
42
47
  return this.holder === lockId;
43
48
  }
44
49
  getHolder() {
50
+ this.expireIfStale();
45
51
  return this.holder;
46
52
  }
47
53
  }
@@ -62,41 +68,45 @@ export function getTodayWriteLockTimeoutMs(executeTimeoutMinutes) {
62
68
  export class MigrationLock {
63
69
  timeoutMs;
64
70
  holder = null;
65
- timer = null;
71
+ expiresAtMs = 0;
66
72
  constructor(timeoutMs) {
67
73
  this.timeoutMs = timeoutMs;
68
74
  }
75
+ // Wall-clock lazy expiry — see InMemoryTodayWriteLockManager for why
76
+ // this is not a setTimeout.
77
+ expireIfStale() {
78
+ if (this.holder && Date.now() >= this.expiresAtMs) {
79
+ logger.warn({ lockId: this.holder }, "Migration lock expired by timeout");
80
+ this.holder = null;
81
+ }
82
+ }
69
83
  acquire() {
84
+ this.expireIfStale();
70
85
  if (this.holder) {
71
86
  logger.debug({ existingHolder: this.holder }, "Migration lock rejected — already held");
72
87
  return { ok: false, holder: this.holder };
73
88
  }
74
89
  const lockId = randomUUID();
75
90
  this.holder = lockId;
76
- this.timer = setTimeout(() => {
77
- logger.warn({ lockId: this.holder }, "Migration lock expired by timeout");
78
- this.holder = null;
79
- this.timer = null;
80
- }, this.timeoutMs);
91
+ this.expiresAtMs = Date.now() + this.timeoutMs;
81
92
  logger.debug({ lockId }, "Migration lock acquired");
82
93
  return { ok: true, lockId };
83
94
  }
84
95
  release(lockId) {
96
+ this.expireIfStale();
85
97
  if (!this.holder || this.holder !== lockId) {
86
98
  return false;
87
99
  }
88
100
  this.holder = null;
89
- if (this.timer) {
90
- clearTimeout(this.timer);
91
- this.timer = null;
92
- }
93
101
  logger.debug({ lockId }, "Migration lock released");
94
102
  return true;
95
103
  }
96
104
  isHeld() {
105
+ this.expireIfStale();
97
106
  return this.holder !== null;
98
107
  }
99
108
  getHolder() {
109
+ this.expireIfStale();
100
110
  return this.holder;
101
111
  }
102
112
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Default tick cadence. One minute keeps detection latency low while the
3
+ * per-tick work (one Date.now() subtraction) is negligible.
4
+ */
5
+ export declare const WAKE_DETECTOR_INTERVAL_MS = 60000;
6
+ /**
7
+ * Minimum unexplained gap between ticks that counts as a sleep / suspend /
8
+ * forward clock jump. Five minutes is far above any plausible event-loop
9
+ * stall on a healthy daemon, and far below the shortest sleep that can
10
+ * swallow a cron tick worth catching up (the activity scan's default
11
+ * 60-minute cadence).
12
+ */
13
+ export declare const WAKE_GAP_THRESHOLD_MS: number;
14
+ export interface WakeDetectorOptions {
15
+ /**
16
+ * Invoked once per detected wake with the gap length. Errors (sync or
17
+ * async) are caught and logged — the detector keeps ticking.
18
+ */
19
+ onWake: (gapMs: number) => void | Promise<void>;
20
+ intervalMs?: number;
21
+ gapThresholdMs?: number;
22
+ /** Injectable wall clock for tests. */
23
+ now?: () => number;
24
+ }
25
+ /**
26
+ * Detects machine sleep / suspend / forward clock jumps from inside the
27
+ * process.
28
+ *
29
+ * node-cron (and every other timer in the daemon) runs on Node's timer
30
+ * wheel, which does not fire while the host is suspended — a cron tick
31
+ * scheduled inside a sleep window is silently lost, not replayed on wake.
32
+ * The boot-time catchup in `bootstrap/catchup.ts` covers daemon *restarts*,
33
+ * but a long sleep with the process still alive had no equivalent until
34
+ * this detector.
35
+ *
36
+ * Mechanism: a short `setInterval` notes the wall-clock time of each tick.
37
+ * Timers freeze during sleep, so the first tick after wake observes a
38
+ * wall-clock gap of roughly the sleep duration; anything above
39
+ * `gapThresholdMs` beyond the expected interval fires `onWake`. Backward
40
+ * clock jumps are ignored — there is nothing to catch up when time moves
41
+ * backward, and the next tick re-baselines automatically.
42
+ */
43
+ export declare class WakeDetector {
44
+ private readonly onWake;
45
+ private readonly intervalMs;
46
+ private readonly gapThresholdMs;
47
+ private readonly now;
48
+ private timer;
49
+ private lastTickMs;
50
+ constructor(options: WakeDetectorOptions);
51
+ start(): void;
52
+ stop(): void;
53
+ /** Test seam — exercises one tick without waiting on real timers. */
54
+ tick(): void;
55
+ }
@@ -0,0 +1,80 @@
1
+ import { createLogger } from "../logging.js";
2
+ const logger = createLogger("wake-detector");
3
+ /**
4
+ * Default tick cadence. One minute keeps detection latency low while the
5
+ * per-tick work (one Date.now() subtraction) is negligible.
6
+ */
7
+ export const WAKE_DETECTOR_INTERVAL_MS = 60_000;
8
+ /**
9
+ * Minimum unexplained gap between ticks that counts as a sleep / suspend /
10
+ * forward clock jump. Five minutes is far above any plausible event-loop
11
+ * stall on a healthy daemon, and far below the shortest sleep that can
12
+ * swallow a cron tick worth catching up (the activity scan's default
13
+ * 60-minute cadence).
14
+ */
15
+ export const WAKE_GAP_THRESHOLD_MS = 5 * 60_000;
16
+ /**
17
+ * Detects machine sleep / suspend / forward clock jumps from inside the
18
+ * process.
19
+ *
20
+ * node-cron (and every other timer in the daemon) runs on Node's timer
21
+ * wheel, which does not fire while the host is suspended — a cron tick
22
+ * scheduled inside a sleep window is silently lost, not replayed on wake.
23
+ * The boot-time catchup in `bootstrap/catchup.ts` covers daemon *restarts*,
24
+ * but a long sleep with the process still alive had no equivalent until
25
+ * this detector.
26
+ *
27
+ * Mechanism: a short `setInterval` notes the wall-clock time of each tick.
28
+ * Timers freeze during sleep, so the first tick after wake observes a
29
+ * wall-clock gap of roughly the sleep duration; anything above
30
+ * `gapThresholdMs` beyond the expected interval fires `onWake`. Backward
31
+ * clock jumps are ignored — there is nothing to catch up when time moves
32
+ * backward, and the next tick re-baselines automatically.
33
+ */
34
+ export class WakeDetector {
35
+ onWake;
36
+ intervalMs;
37
+ gapThresholdMs;
38
+ now;
39
+ timer = null;
40
+ lastTickMs = 0;
41
+ constructor(options) {
42
+ this.onWake = options.onWake;
43
+ this.intervalMs = options.intervalMs ?? WAKE_DETECTOR_INTERVAL_MS;
44
+ this.gapThresholdMs = options.gapThresholdMs ?? WAKE_GAP_THRESHOLD_MS;
45
+ this.now = options.now ?? Date.now;
46
+ }
47
+ start() {
48
+ if (this.timer)
49
+ return;
50
+ this.lastTickMs = this.now();
51
+ this.timer = setInterval(() => this.tick(), this.intervalMs);
52
+ this.timer.unref?.();
53
+ }
54
+ stop() {
55
+ if (!this.timer)
56
+ return;
57
+ clearInterval(this.timer);
58
+ this.timer = null;
59
+ }
60
+ /** Test seam — exercises one tick without waiting on real timers. */
61
+ tick() {
62
+ const current = this.now();
63
+ const gapMs = current - this.lastTickMs - this.intervalMs;
64
+ this.lastTickMs = current;
65
+ if (gapMs < this.gapThresholdMs)
66
+ return;
67
+ logger.warn({ gapMinutes: Math.round(gapMs / 60_000) }, "Wall-clock gap detected (machine sleep or clock jump) — running wake catch-up");
68
+ try {
69
+ const result = this.onWake(gapMs);
70
+ if (result && typeof result.then === "function") {
71
+ result.then(undefined, (err) => {
72
+ logger.error({ err }, "Wake catch-up handler failed");
73
+ });
74
+ }
75
+ }
76
+ catch (err) {
77
+ logger.error({ err }, "Wake catch-up handler threw");
78
+ }
79
+ }
80
+ }
@@ -12,7 +12,7 @@
12
12
  * time. The lock is held until the dispatcher releases it via
13
13
  * `releaseWikiCompileLock` in `executeDefault`'s `finally` block.
14
14
  * - The lock is purely in-process; that matches the dispatcher's
15
- * existing concurrency invariants (`hourlyCheckInProgress`,
15
+ * existing concurrency invariants (`activityScanInProgress`,
16
16
  * `morningRoutineActive` are also in-memory flags). A second
17
17
  * daemon process would not see the lock — but the daemon is
18
18
  * single-process by design.
@@ -12,7 +12,7 @@
12
12
  * time. The lock is held until the dispatcher releases it via
13
13
  * `releaseWikiCompileLock` in `executeDefault`'s `finally` block.
14
14
  * - The lock is purely in-process; that matches the dispatcher's
15
- * existing concurrency invariants (`hourlyCheckInProgress`,
15
+ * existing concurrency invariants (`activityScanInProgress`,
16
16
  * `morningRoutineActive` are also in-memory flags). A second
17
17
  * daemon process would not see the lock — but the daemon is
18
18
  * single-process by design.
@@ -25,7 +25,7 @@ import { createLogger } from "../logging.js";
25
25
  import { SkillsCompiler } from "./skills-compiler.js";
26
26
  import { EMPTY_MAIL_ACCOUNTS_MD, renderMailAccountsMd, } from "./skills-compiler-tree.js";
27
27
  import { refreshSkillIndexBlock } from "./skills-compiler-skill-index.js";
28
- import { getProfileForEvent, resolveSkillManifest, resolveSkillManifestForProcess, } from "./skills-manifest.js";
28
+ import { eventTypeAcceptsUserSkills, getProfileForEvent, resolveSkillManifest, resolveSkillManifestForProcess, } from "./skills-manifest.js";
29
29
  import { ensureDaemonApiCli } from "./daemon-api-cli.js";
30
30
  import { computeInstructionAssetStatus, readInstructionStampManifest, sessionInstructionAssetsStale, writeInstructionAssetStamp, } from "./release-assets.js";
31
31
  const logger = createLogger("workdir");
@@ -252,11 +252,15 @@ export function createSessionWorkdir(projectRoot, eventType, userSkillsDir, opti
252
252
  // turns, manifest now resolves to a different skill set).
253
253
  { processKey: options?.processKey ?? eventType, skillSlugs: deployed.skills });
254
254
  ensureDaemonApiCli(sessionDir);
255
- // User skills: every skill the user has authored, regardless of event type.
256
- // Uses the manifest-backed sync so the initial population is consistent
257
- // with the per-message sync the dispatcher runs on Opus events.
255
+ // User skills: every skill the user has authored, EXCEPT for
256
+ // narrow-persona keys (wiki.* / routine.research_*) which run a tight
257
+ // built-in manifest and would only be diluted by the owner's general
258
+ // skill library — see `eventTypeAcceptsUserSkills`. Uses the
259
+ // manifest-backed sync so the initial population is consistent with the
260
+ // per-message sync the dispatcher runs on Opus events.
258
261
  let userSync = null;
259
- if (userSkillsDir) {
262
+ if (userSkillsDir &&
263
+ eventTypeAcceptsUserSkills(options?.processKey ?? eventType)) {
260
264
  userSync = syncAllUserSkills(sessionDir, userSkillsDir);
261
265
  // docs/design/appendices/skills-unification.md Phase 1 §R4 — splice user-authored
262
266
  // slugs into the `<skill-index>` block AFTER they land on disk.
@@ -419,7 +423,12 @@ export function ensureSessionWorkdir(projectRoot, dataDir, dbSessionId, eventTyp
419
423
  // CONTEXT_VAULT_REDESIGN — falling back there would resurrect the
420
424
  // Obsidian-mode divergence bug v4 V11 fixed.
421
425
  const userSkillsDir = join(options?.contextDir ?? join(dataDir, "context"), "policies", "skills");
422
- const userSync = syncAllUserSkills(sessionDir, userSkillsDir);
426
+ // Narrow-persona keys (wiki.* / routine.research_*) skip the owner's
427
+ // user-skill library; their tight built-in manifest is the whole
428
+ // surface. See `eventTypeAcceptsUserSkills`.
429
+ const userSync = eventTypeAcceptsUserSkills(options?.processKey ?? eventType)
430
+ ? syncAllUserSkills(sessionDir, userSkillsDir)
431
+ : null;
423
432
  // docs/design/appendices/skills-unification.md Phase 1 §R4 — fold user-authored slugs
424
433
  // into the `<skill-index>` block now that they're on disk. Idempotent
425
434
  // and inexpensive (single instruction-file rewrite).
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Stage-0 signal compute for the three-stage activity_scan gate
3
+ * (cost-reduction-structural §B). Pure DB-read shape: every value is
4
+ * derived from existing tables (`observations`, `mail_messages_index`,
5
+ * `agent_schedule`, `agent_actions`) plus the optional today.md content
6
+ * passed in by the caller. No LLM call, no filesystem I/O of its own —
7
+ * the dispatcher injects the today.md snapshot (when available) so this
8
+ * module stays unit-testable from a synthetic Database handle.
9
+ *
10
+ * The shape mirrors the design doc 1:1 so the gate (Stage 1) can be
11
+ * a pure function over `ActivityScanSignals` + a config block.
12
+ */
13
+ import type Database from "better-sqlite3";
14
+ export interface ActivityScanSignals {
15
+ /** Post dedup-against-today user-actor pending observation count. */
16
+ pendingObsCount: number;
17
+ /**
18
+ * Maximum `novelty_score` across pending user observations whose
19
+ * `summary_status='done'`. `null` when no observation has a done
20
+ * summary yet — the gate treats null as a cautious mid-novelty default
21
+ * (see decideStage).
22
+ */
23
+ maxNoveltyScore: number | null;
24
+ /** Histogram of novelty levels across pending+done observations. */
25
+ noveltyDistribution: {
26
+ low: number;
27
+ mid: number;
28
+ high: number;
29
+ };
30
+ /** Count of unread mail rows whose sender is in the VIP list. */
31
+ vipMailUnreadCount: number;
32
+ /** True when at least one pending calendar observation exists. */
33
+ calendarHas24hChange: boolean;
34
+ /**
35
+ * True when at least two pending calendar observations carry
36
+ * overlapping `start`/`end` ISO timestamps in their JSON payload
37
+ * within the next 24 hours. Tolerant of missing fields — when the
38
+ * payload shape isn't recognizable, returns false (the LLM still
39
+ * sees the underlying observation in Stage 3 if escalated).
40
+ */
41
+ calendarHasConflict: boolean;
42
+ /**
43
+ * Number of `## Agent Plan` rows in today.md whose HH:MM is before
44
+ * `now` in the agent timezone. Best-effort regex parse — when
45
+ * today.md is missing or unparseable, returns 0.
46
+ */
47
+ agentPlanOverdueCount: number;
48
+ /** Count of `agent_schedule` rows due in the next 6 hours. */
49
+ scheduleApproachingCount: number;
50
+ /**
51
+ * Hours since the last activity_scan that escalated to Stage 3 (or
52
+ * before the gate was deployed, since the last activity_scan run at
53
+ * all). `Infinity` when no row has been recorded yet.
54
+ */
55
+ hoursSinceLastStage3Run: number;
56
+ }
57
+ export interface ComputeActivityScanSignalsOptions {
58
+ /** Owner-VIP mail addresses, lowercased exact-match. */
59
+ vipMailSenders?: readonly string[];
60
+ /** Look-ahead window for `scheduleApproachingCount`. Default 6h. */
61
+ scheduleHorizonHours?: number;
62
+ /** Look-ahead window for calendar conflict detection. Default 24h. */
63
+ calendarHorizonHours?: number;
64
+ /** today.md content snapshot (or null when missing/unreadable). */
65
+ todayMd?: string | null;
66
+ /** Wall-clock anchor — injectable for tests. Defaults to `new Date()`. */
67
+ now?: Date;
68
+ /**
69
+ * Agent timezone (IANA name, e.g. `Asia/Tokyo`) used to compare
70
+ * `## Agent Plan` HH:MM rows against `now`. When omitted, falls back
71
+ * to the JS engine's local timezone — fine in the common single-user
72
+ * deployment but wrong if the daemon runs in UTC and the operator
73
+ * pinned a different `config.timezone`.
74
+ */
75
+ agentTimezone?: string;
76
+ }
77
+ export declare function computeActivityScanSignals(db: Database.Database, options?: ComputeActivityScanSignalsOptions): ActivityScanSignals;