@aitne/daemon 0.1.3 → 0.1.4

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 (249) hide show
  1. package/dist/adapters/whatsapp-adapter.d.ts.map +1 -1
  2. package/dist/adapters/whatsapp-adapter.js +0 -1
  3. package/dist/adapters/whatsapp-adapter.js.map +1 -1
  4. package/dist/api/integration-route-gate.d.ts +15 -11
  5. package/dist/api/integration-route-gate.d.ts.map +1 -1
  6. package/dist/api/integration-route-gate.js +60 -23
  7. package/dist/api/integration-route-gate.js.map +1 -1
  8. package/dist/api/json-body.d.ts +22 -7
  9. package/dist/api/json-body.d.ts.map +1 -1
  10. package/dist/api/json-body.js +27 -8
  11. package/dist/api/json-body.js.map +1 -1
  12. package/dist/api/routes/agent.d.ts.map +1 -1
  13. package/dist/api/routes/agent.js +18 -0
  14. package/dist/api/routes/agent.js.map +1 -1
  15. package/dist/api/routes/backends.d.ts.map +1 -1
  16. package/dist/api/routes/backends.js +96 -1
  17. package/dist/api/routes/backends.js.map +1 -1
  18. package/dist/api/routes/books.js +1 -1
  19. package/dist/api/routes/books.js.map +1 -1
  20. package/dist/api/routes/context.d.ts.map +1 -1
  21. package/dist/api/routes/context.js +13 -1
  22. package/dist/api/routes/context.js.map +1 -1
  23. package/dist/api/routes/dashboard.d.ts.map +1 -1
  24. package/dist/api/routes/dashboard.js +75 -5
  25. package/dist/api/routes/dashboard.js.map +1 -1
  26. package/dist/api/routes/github.d.ts.map +1 -1
  27. package/dist/api/routes/github.js +38 -5
  28. package/dist/api/routes/github.js.map +1 -1
  29. package/dist/api/routes/integrations.d.ts +35 -6
  30. package/dist/api/routes/integrations.d.ts.map +1 -1
  31. package/dist/api/routes/integrations.js +191 -16
  32. package/dist/api/routes/integrations.js.map +1 -1
  33. package/dist/api/routes/mail.d.ts.map +1 -1
  34. package/dist/api/routes/mail.js +112 -46
  35. package/dist/api/routes/mail.js.map +1 -1
  36. package/dist/api/routes/observations.d.ts.map +1 -1
  37. package/dist/api/routes/observations.js +161 -8
  38. package/dist/api/routes/observations.js.map +1 -1
  39. package/dist/api/routes/setup-migrate.d.ts +9 -1
  40. package/dist/api/routes/setup-migrate.d.ts.map +1 -1
  41. package/dist/api/routes/setup-migrate.js +4 -2
  42. package/dist/api/routes/setup-migrate.js.map +1 -1
  43. package/dist/api/routes/skills.d.ts.map +1 -1
  44. package/dist/api/routes/skills.js +39 -1
  45. package/dist/api/routes/skills.js.map +1 -1
  46. package/dist/api/routes/voice.d.ts.map +1 -1
  47. package/dist/api/routes/voice.js +62 -4
  48. package/dist/api/routes/voice.js.map +1 -1
  49. package/dist/bootstrap/adapters.d.ts +109 -0
  50. package/dist/bootstrap/adapters.d.ts.map +1 -0
  51. package/dist/bootstrap/adapters.js +237 -0
  52. package/dist/bootstrap/adapters.js.map +1 -0
  53. package/dist/bootstrap/catchup.d.ts +23 -0
  54. package/dist/bootstrap/catchup.d.ts.map +1 -0
  55. package/dist/bootstrap/catchup.js +124 -0
  56. package/dist/bootstrap/catchup.js.map +1 -0
  57. package/dist/bootstrap/schedule-helpers.d.ts +18 -0
  58. package/dist/bootstrap/schedule-helpers.d.ts.map +1 -0
  59. package/dist/bootstrap/schedule-helpers.js +96 -0
  60. package/dist/bootstrap/schedule-helpers.js.map +1 -0
  61. package/dist/bootstrap/services.d.ts +60 -0
  62. package/dist/bootstrap/services.d.ts.map +1 -0
  63. package/dist/bootstrap/services.js +209 -0
  64. package/dist/bootstrap/services.js.map +1 -0
  65. package/dist/core/backends/backend-router.d.ts +23 -0
  66. package/dist/core/backends/backend-router.d.ts.map +1 -1
  67. package/dist/core/backends/backend-router.js +48 -3
  68. package/dist/core/backends/backend-router.js.map +1 -1
  69. package/dist/core/backends/claude-auth.d.ts +70 -0
  70. package/dist/core/backends/claude-auth.d.ts.map +1 -0
  71. package/dist/core/backends/claude-auth.js +198 -0
  72. package/dist/core/backends/claude-auth.js.map +1 -0
  73. package/dist/core/backends/claude-code-core.d.ts +47 -119
  74. package/dist/core/backends/claude-code-core.d.ts.map +1 -1
  75. package/dist/core/backends/claude-code-core.js +112 -1565
  76. package/dist/core/backends/claude-code-core.js.map +1 -1
  77. package/dist/core/backends/claude-delegated.d.ts +86 -0
  78. package/dist/core/backends/claude-delegated.d.ts.map +1 -0
  79. package/dist/core/backends/claude-delegated.js +801 -0
  80. package/dist/core/backends/claude-delegated.js.map +1 -0
  81. package/dist/core/backends/claude-errors.d.ts +39 -0
  82. package/dist/core/backends/claude-errors.d.ts.map +1 -0
  83. package/dist/core/backends/claude-errors.js +71 -0
  84. package/dist/core/backends/claude-errors.js.map +1 -0
  85. package/dist/core/backends/claude-probe.d.ts +103 -0
  86. package/dist/core/backends/claude-probe.d.ts.map +1 -0
  87. package/dist/core/backends/claude-probe.js +336 -0
  88. package/dist/core/backends/claude-probe.js.map +1 -0
  89. package/dist/core/backends/claude-tool-collection.d.ts +135 -0
  90. package/dist/core/backends/claude-tool-collection.d.ts.map +1 -0
  91. package/dist/core/backends/claude-tool-collection.js +831 -0
  92. package/dist/core/backends/claude-tool-collection.js.map +1 -0
  93. package/dist/core/backends/gemini-cli-core.d.ts +21 -0
  94. package/dist/core/backends/gemini-cli-core.d.ts.map +1 -1
  95. package/dist/core/backends/gemini-cli-core.js +84 -6
  96. package/dist/core/backends/gemini-cli-core.js.map +1 -1
  97. package/dist/core/backends/prompt-utils.d.ts +1 -0
  98. package/dist/core/backends/prompt-utils.d.ts.map +1 -1
  99. package/dist/core/backends/prompt-utils.js +60 -3
  100. package/dist/core/backends/prompt-utils.js.map +1 -1
  101. package/dist/core/context-builder.d.ts +36 -12
  102. package/dist/core/context-builder.d.ts.map +1 -1
  103. package/dist/core/context-builder.js +179 -89
  104. package/dist/core/context-builder.js.map +1 -1
  105. package/dist/core/dispatcher-date-utils.d.ts +49 -0
  106. package/dist/core/dispatcher-date-utils.d.ts.map +1 -0
  107. package/dist/core/dispatcher-date-utils.js +132 -0
  108. package/dist/core/dispatcher-date-utils.js.map +1 -0
  109. package/dist/core/dispatcher-error-handling.d.ts +159 -0
  110. package/dist/core/dispatcher-error-handling.d.ts.map +1 -0
  111. package/dist/core/dispatcher-error-handling.js +393 -0
  112. package/dist/core/dispatcher-error-handling.js.map +1 -0
  113. package/dist/core/dispatcher-hourly-check.d.ts +150 -0
  114. package/dist/core/dispatcher-hourly-check.d.ts.map +1 -0
  115. package/dist/core/dispatcher-hourly-check.js +665 -0
  116. package/dist/core/dispatcher-hourly-check.js.map +1 -0
  117. package/dist/core/dispatcher-message-handler.d.ts +170 -0
  118. package/dist/core/dispatcher-message-handler.d.ts.map +1 -0
  119. package/dist/core/dispatcher-message-handler.js +1054 -0
  120. package/dist/core/dispatcher-message-handler.js.map +1 -0
  121. package/dist/core/dispatcher-morning-routine.d.ts +169 -0
  122. package/dist/core/dispatcher-morning-routine.d.ts.map +1 -0
  123. package/dist/core/dispatcher-morning-routine.js +434 -0
  124. package/dist/core/dispatcher-morning-routine.js.map +1 -0
  125. package/dist/core/dispatcher-prompt.d.ts +107 -0
  126. package/dist/core/dispatcher-prompt.d.ts.map +1 -0
  127. package/dist/core/dispatcher-prompt.js +227 -0
  128. package/dist/core/dispatcher-prompt.js.map +1 -0
  129. package/dist/core/dispatcher-repository-helpers.d.ts +39 -0
  130. package/dist/core/dispatcher-repository-helpers.d.ts.map +1 -0
  131. package/dist/core/dispatcher-repository-helpers.js +86 -0
  132. package/dist/core/dispatcher-repository-helpers.js.map +1 -0
  133. package/dist/core/dispatcher-result-processor.d.ts +145 -0
  134. package/dist/core/dispatcher-result-processor.d.ts.map +1 -0
  135. package/dist/core/dispatcher-result-processor.js +414 -0
  136. package/dist/core/dispatcher-result-processor.js.map +1 -0
  137. package/dist/core/dispatcher-scheduled-tasks.d.ts +406 -0
  138. package/dist/core/dispatcher-scheduled-tasks.d.ts.map +1 -0
  139. package/dist/core/dispatcher-scheduled-tasks.js +998 -0
  140. package/dist/core/dispatcher-scheduled-tasks.js.map +1 -0
  141. package/dist/core/dispatcher-types.d.ts +296 -0
  142. package/dist/core/dispatcher-types.d.ts.map +1 -0
  143. package/dist/core/dispatcher-types.js +106 -0
  144. package/dist/core/dispatcher-types.js.map +1 -0
  145. package/dist/core/dispatcher.d.ts +86 -610
  146. package/dist/core/dispatcher.d.ts.map +1 -1
  147. package/dist/core/dispatcher.js +293 -3542
  148. package/dist/core/dispatcher.js.map +1 -1
  149. package/dist/core/integration-health.d.ts +18 -10
  150. package/dist/core/integration-health.d.ts.map +1 -1
  151. package/dist/core/integration-health.js +31 -1
  152. package/dist/core/integration-health.js.map +1 -1
  153. package/dist/core/integration-lifecycle.d.ts +65 -0
  154. package/dist/core/integration-lifecycle.d.ts.map +1 -1
  155. package/dist/core/integration-lifecycle.js +167 -16
  156. package/dist/core/integration-lifecycle.js.map +1 -1
  157. package/dist/core/integration-main-backend.d.ts +40 -0
  158. package/dist/core/integration-main-backend.d.ts.map +1 -1
  159. package/dist/core/integration-main-backend.js +89 -2
  160. package/dist/core/integration-main-backend.js.map +1 -1
  161. package/dist/core/management-md.d.ts +51 -17
  162. package/dist/core/management-md.d.ts.map +1 -1
  163. package/dist/core/management-md.js +233 -56
  164. package/dist/core/management-md.js.map +1 -1
  165. package/dist/core/output-language-policy.d.ts +74 -0
  166. package/dist/core/output-language-policy.d.ts.map +1 -0
  167. package/dist/core/output-language-policy.js +194 -0
  168. package/dist/core/output-language-policy.js.map +1 -0
  169. package/dist/core/prompts.d.ts +1 -0
  170. package/dist/core/prompts.d.ts.map +1 -1
  171. package/dist/core/prompts.js +121 -3
  172. package/dist/core/prompts.js.map +1 -1
  173. package/dist/core/repository-management-docs.d.ts +24 -0
  174. package/dist/core/repository-management-docs.d.ts.map +1 -1
  175. package/dist/core/repository-management-docs.js +210 -26
  176. package/dist/core/repository-management-docs.js.map +1 -1
  177. package/dist/core/routine-acquisition-plan.d.ts +131 -0
  178. package/dist/core/routine-acquisition-plan.d.ts.map +1 -0
  179. package/dist/core/routine-acquisition-plan.js +268 -0
  180. package/dist/core/routine-acquisition-plan.js.map +1 -0
  181. package/dist/core/routine-fetch-window-runner.d.ts +201 -0
  182. package/dist/core/routine-fetch-window-runner.d.ts.map +1 -0
  183. package/dist/core/routine-fetch-window-runner.js +661 -0
  184. package/dist/core/routine-fetch-window-runner.js.map +1 -0
  185. package/dist/core/routine-windows.d.ts +156 -0
  186. package/dist/core/routine-windows.d.ts.map +1 -0
  187. package/dist/core/routine-windows.js +330 -0
  188. package/dist/core/routine-windows.js.map +1 -0
  189. package/dist/core/skills-compiler.d.ts +11 -0
  190. package/dist/core/skills-compiler.d.ts.map +1 -1
  191. package/dist/core/skills-compiler.js +102 -13
  192. package/dist/core/skills-compiler.js.map +1 -1
  193. package/dist/core/skills-manifest.d.ts.map +1 -1
  194. package/dist/core/skills-manifest.js +26 -0
  195. package/dist/core/skills-manifest.js.map +1 -1
  196. package/dist/core/system-reset.d.ts.map +1 -1
  197. package/dist/core/system-reset.js +25 -2
  198. package/dist/core/system-reset.js.map +1 -1
  199. package/dist/db/observations.d.ts +45 -2
  200. package/dist/db/observations.d.ts.map +1 -1
  201. package/dist/db/observations.js +112 -14
  202. package/dist/db/observations.js.map +1 -1
  203. package/dist/db/schema.d.ts.map +1 -1
  204. package/dist/db/schema.js +13 -25
  205. package/dist/db/schema.js.map +1 -1
  206. package/dist/index.js +83 -610
  207. package/dist/index.js.map +1 -1
  208. package/dist/observers/delegated-sync-worker.d.ts +45 -2
  209. package/dist/observers/delegated-sync-worker.d.ts.map +1 -1
  210. package/dist/observers/delegated-sync-worker.js +71 -21
  211. package/dist/observers/delegated-sync-worker.js.map +1 -1
  212. package/dist/observers/mail-poller.d.ts +12 -5
  213. package/dist/observers/mail-poller.d.ts.map +1 -1
  214. package/dist/observers/mail-poller.js +36 -14
  215. package/dist/observers/mail-poller.js.map +1 -1
  216. package/dist/observers/manager.d.ts +37 -5
  217. package/dist/observers/manager.d.ts.map +1 -1
  218. package/dist/observers/manager.js +28 -10
  219. package/dist/observers/manager.js.map +1 -1
  220. package/dist/services/delegated-backend-invoker.d.ts +1 -51
  221. package/dist/services/delegated-backend-invoker.d.ts.map +1 -1
  222. package/dist/services/delegated-backend-invoker.js +41 -480
  223. package/dist/services/delegated-backend-invoker.js.map +1 -1
  224. package/dist/services/delegated-invoker-audit.d.ts +94 -0
  225. package/dist/services/delegated-invoker-audit.d.ts.map +1 -0
  226. package/dist/services/delegated-invoker-audit.js +238 -0
  227. package/dist/services/delegated-invoker-audit.js.map +1 -0
  228. package/dist/services/delegated-invoker-cache-hits.d.ts +34 -0
  229. package/dist/services/delegated-invoker-cache-hits.d.ts.map +1 -0
  230. package/dist/services/delegated-invoker-cache-hits.js +104 -0
  231. package/dist/services/delegated-invoker-cache-hits.js.map +1 -0
  232. package/dist/services/delegated-invoker-janitors.d.ts +28 -0
  233. package/dist/services/delegated-invoker-janitors.d.ts.map +1 -0
  234. package/dist/services/delegated-invoker-janitors.js +104 -0
  235. package/dist/services/delegated-invoker-janitors.js.map +1 -0
  236. package/dist/services/delegated-invoker-utils.d.ts +42 -0
  237. package/dist/services/delegated-invoker-utils.d.ts.map +1 -0
  238. package/dist/services/delegated-invoker-utils.js +100 -0
  239. package/dist/services/delegated-invoker-utils.js.map +1 -0
  240. package/dist/services/delegated-task-runtime.d.ts +1 -1
  241. package/dist/services/delegated-task-runtime.js +1 -1
  242. package/dist/services/integrations/snapshot-partitions.d.ts +5 -0
  243. package/dist/services/integrations/snapshot-partitions.d.ts.map +1 -1
  244. package/dist/services/integrations/snapshot-partitions.js +12 -0
  245. package/dist/services/integrations/snapshot-partitions.js.map +1 -1
  246. package/dist/services/voice/transcriber-impl.d.ts.map +1 -1
  247. package/dist/services/voice/transcriber-impl.js +7 -8
  248. package/dist/services/voice/transcriber-impl.js.map +1 -1
  249. package/package.json +2 -2
@@ -0,0 +1,661 @@
1
+ /**
2
+ * `RoutineFetchWindowRunner` — Phase 4 / D1 pre-pass orchestration.
3
+ *
4
+ * ROUTINE_DATA_ACQUISITION_DESIGN.md §6.1.1 / Phase 4 D1 — every routine
5
+ * dispatcher (morning_routine, today_refresh, hourly_check, evening /
6
+ * weekly / monthly review) calls this runner immediately before
7
+ * dispatching the parent session. The runner:
8
+ *
9
+ * 1. Reads the per-routine plan from `ROUTINE_WINDOWS` and the current
10
+ * integration state, fans out per-account where applicable, resolves
11
+ * each row's predicate (`direct` / `delegated-same` / `delegated-cross`
12
+ * / `native` / skip) via `buildAcquisitionPlan`, and renders an
13
+ * `<acquisition-plan>` block.
14
+ * 2. Synthesises a `routine.fetch_window` RoutineEvent, embeds the plan
15
+ * block in `event.data.acquisitionPlanBlock` so ContextBuilder folds
16
+ * it into the fetcher's prompt context, and routes the event
17
+ * synchronously through the standard `IAgentRouter.execute` /
18
+ * `PromptAssembler.assemble` pipeline. The session inherits the
19
+ * `routine.fetch_window` ProcessKey binding (lite tier per F1 / §6.9)
20
+ * and the dedicated `routine-fetch-window` agent profile.
21
+ * 3. Parses the agent's single-JSON-line output into a structured
22
+ * `FetchReport`, renders a `<fetch_report>` block, and logs one
23
+ * `agent_actions` row via the dispatcher's audit logger.
24
+ * 4. Returns the rendered block + parsed report so the caller can graft
25
+ * the block into the **parent** routine event's
26
+ * `event.data.fetchReportBlock`. ContextBuilder injects that block
27
+ * verbatim into the parent session's prompt (mirrors the
28
+ * `<gate_decision>` pattern used by hourly_check Stage 3).
29
+ *
30
+ * Failure-mode contract (Phase 4 D1 + design §11 R5 / OQ4):
31
+ *
32
+ * - **No applicable rows** (routine has no windows in `ROUTINE_WINDOWS`,
33
+ * every integration is disabled, every account list is empty) — the
34
+ * runner returns an empty `<fetch_report>` with `status="skipped"` and
35
+ * `fetched=posted=duplicates=0`. The parent routine still runs; the
36
+ * block is informational only.
37
+ * - **Pre-pass session errors** (binding resolve fails, agent throws,
38
+ * JSON parse fails) — the runner logs the failure and returns a
39
+ * `<fetch_report status="failed">` with one `pre-pass-failed` error.
40
+ * Pre-pass cost gains are forfeit for this tick; the parent routine
41
+ * continues with whatever observations the rest of the plan produced.
42
+ * Throwing here would otherwise propagate up and abort the parent
43
+ * routine — the opposite of P3 ("Lite for Fetch, Medium for Decide").
44
+ * - **Partial success** — the report's `errors` array carries per-row
45
+ * failures (`no-surface`, `fetch-failed`, `budget-exhausted`). The
46
+ * block surfaces them so the parent prompt can decide whether to
47
+ * treat its observations view as complete.
48
+ */
49
+ import { EventPriority, INTEGRATION_KEYS, createEvent, getAgentDayDateStr, getIntegrationDescriptor, isRoutineEvent, } from "@aitne/shared";
50
+ import { readIntegrations } from "../db/integrations-store.js";
51
+ import { ROUTINE_WINDOWS, routineHasWindows, } from "./routine-windows.js";
52
+ import { buildAcquisitionPlan, buildAcquisitionTimestamps, } from "./routine-acquisition-plan.js";
53
+ import { createLogger } from "../logging.js";
54
+ const logger = createLogger("routine-fetch-window-runner");
55
+ // ── Module helpers ────────────────────────────────────────────────────────
56
+ /** The ProcessKey + event type the pre-pass session always runs under. */
57
+ const FETCH_WINDOW_PROCESS_KEY = "routine.fetch_window";
58
+ const FETCH_WINDOW_EVENT_TYPE = "routine.fetch_window";
59
+ /**
60
+ * Daemon REST surfaces the pre-pass partials may target. Curl prefixes
61
+ * are constructed with the configured `apiPort` at dispatch time so a
62
+ * non-default port survives the clamp. Everything OTHER than these
63
+ * prefixes is denied — the pre-pass cannot reach `/api/notify`,
64
+ * `/api/context/*`, `/api/agent/*`, etc., even though Bash(curl *) is
65
+ * the project default. This is the daemon-side enforcement that backs
66
+ * the agent profile's "no notify, no context writes" guardrails (P3:
67
+ * Lite for Fetch — the pre-pass has zero business making decisions).
68
+ *
69
+ * `jq *` stays allowed because direct-mode partials pipe curl output
70
+ * through jq for compact projection before posting to /api/observations.
71
+ *
72
+ * The clamp is Claude-only — Codex/Gemini have no per-spawn allowedTools
73
+ * surface today (CLAUDE.md acknowledges the gap). The
74
+ * `process_backend_config` envelope (`max_turns=20`, `max_budget_usd=0.20`)
75
+ * remains the floor on those backends.
76
+ */
77
+ function buildPrePassDaemonRestPatterns(apiPort) {
78
+ const root = `http://localhost:${apiPort}/api`;
79
+ return [
80
+ // Observations — the only write surface the pre-pass touches.
81
+ // Catches both POST /api/observations and GET /api/observations*.
82
+ `Bash(curl ${root}/observations*)`,
83
+ // Direct-mode mail / calendar / notion reads.
84
+ `Bash(curl ${root}/mail/*)`,
85
+ `Bash(curl ${root}/calendar/*)`,
86
+ `Bash(curl ${root}/notion/*)`,
87
+ // delegated-cross proxy. Only Gmail / Google Calendar / Notion
88
+ // expose this; user-managed Outlook has no proxy and the runner
89
+ // collapses cross-backend bindings to delegated-same per
90
+ // routine-acquisition-plan.ts:resolveFetchMode.
91
+ `Bash(curl ${root}/integrations/*)`,
92
+ // Compact-projection helper used by the partials.
93
+ "Bash(jq *)",
94
+ ];
95
+ }
96
+ /**
97
+ * Project the active integrations into the per-backend MCP tool names
98
+ * the pre-pass needs. Includes both delegated-same bindings (where the
99
+ * connector is registered through the daemon's Claude SDK) AND native
100
+ * bindings (where the connector is loaded by the user but the same
101
+ * descriptor declares which capability tool names exist). User-managed
102
+ * descriptors (`backendConnectors[backend]` undefined for Outlook) are
103
+ * skipped — those rows surface as `no-surface` errors per the partial
104
+ * contract, which is the documented behaviour.
105
+ *
106
+ * Cross-backend delegated bindings contribute zero MCP tools because
107
+ * the partial reaches them through `/api/integrations/<key>/exec`, not
108
+ * via the session backend's MCP namespace.
109
+ */
110
+ function collectIntegrationToolsForBackend(integrations, backend) {
111
+ const out = new Set();
112
+ for (const key of INTEGRATION_KEYS) {
113
+ const state = integrations[key];
114
+ if (!state)
115
+ continue;
116
+ let active = false;
117
+ if (state.mode === "delegated" && state.delegatedBackend === backend) {
118
+ active = true;
119
+ }
120
+ else if (state.mode === "native" && state.nativeBackend === backend) {
121
+ active = true;
122
+ }
123
+ if (!active)
124
+ continue;
125
+ const connector = getIntegrationDescriptor(key).backendConnectors[backend];
126
+ if (!connector)
127
+ continue; // user-managed (no descriptor connector for this backend)
128
+ for (const toolNames of Object.values(connector.capabilityTools)) {
129
+ for (const toolName of toolNames) {
130
+ out.add(connector.toolNamespace + toolName);
131
+ }
132
+ }
133
+ }
134
+ return Array.from(out);
135
+ }
136
+ /**
137
+ * Compose the per-execute `allowedToolsOverride` for the pre-pass. The
138
+ * override REPLACES the SDK's default allowlist (no union per
139
+ * claude-code-core.ts:437) so the list must be exhaustive for every
140
+ * surface the partials use under any (integration, mode) cell. Mode
141
+ * coverage:
142
+ *
143
+ * - `direct`: daemon REST → curl prefix.
144
+ * - `delegated-same`: session backend MCP → integration tool name.
145
+ * - `delegated-cross`: daemon delegation proxy → curl prefix
146
+ * (`/api/integrations/<key>/exec`).
147
+ * - `native` (descriptor-bound): session backend MCP → integration
148
+ * tool name.
149
+ * - `native` (user-managed) / no-surface: nothing in the override —
150
+ * the partial records `no-surface` and the runner's report carries
151
+ * the gap forward to the parent routine.
152
+ *
153
+ * Exported for unit testing — the runner consumes it via
154
+ * `composePrePassAllowedTools` at dispatch time.
155
+ */
156
+ export function composePrePassAllowedTools(apiPort, integrations, sessionBackend) {
157
+ return [
158
+ ...buildPrePassDaemonRestPatterns(apiPort),
159
+ ...collectIntegrationToolsForBackend(integrations, sessionBackend),
160
+ ];
161
+ }
162
+ /**
163
+ * Map a `MailAccount.kind` onto the integration key the registry uses
164
+ * for routing. Today: `gmail` → `gmail`, `outlook` → `outlook_mail`.
165
+ * Yahoo / iCloud / IMAP accounts are not tied to a routed integration
166
+ * and therefore do not participate in the pre-pass fan-out today.
167
+ */
168
+ function mailAccountIntegrationKey(account) {
169
+ switch (account.kind) {
170
+ case "gmail":
171
+ return "gmail";
172
+ case "outlook":
173
+ return "outlook_mail";
174
+ default:
175
+ return null;
176
+ }
177
+ }
178
+ /**
179
+ * Derive the canonical `RoutineWindowKey` from a routine event. The
180
+ * caller's intent is conveyed by `event.type` (always `routine.<name>`),
181
+ * with `RoutineEvent.routine` carrying the same suffix without the
182
+ * `routine.` prefix. Returns null for routines outside the catalog so
183
+ * the caller can short-circuit before touching plan assembly.
184
+ */
185
+ export function routineWindowKeyFromEvent(event) {
186
+ if (!isRoutineEvent(event))
187
+ return null;
188
+ const candidate = `routine.${event.routine}`;
189
+ return (ROUTINE_WINDOWS[candidate] !== undefined
190
+ ? candidate
191
+ : null);
192
+ }
193
+ /**
194
+ * Walk `text` and return every balanced `{...}` slice (top-level objects
195
+ * only; nested braces are honoured). Strings are tracked so brace
196
+ * characters inside `"..."` literals don't unbalance the scan. Used by
197
+ * `parseFetchWindowOutput` to pick the LAST top-level object on stdout —
198
+ * agents occasionally emit a think-aloud line carrying a JSON snippet
199
+ * before the verdict, and the fetcher's contract is "the last
200
+ * top-level JSON object wins."
201
+ *
202
+ * Exported for direct unit testing; the runner consumes it via
203
+ * `parseFetchWindowOutput`.
204
+ */
205
+ export function extractBalancedJsonObjects(text) {
206
+ const out = [];
207
+ let depth = 0;
208
+ let start = -1;
209
+ let inString = false;
210
+ let escape = false;
211
+ for (let i = 0; i < text.length; i++) {
212
+ const ch = text[i];
213
+ if (inString) {
214
+ if (escape) {
215
+ escape = false;
216
+ }
217
+ else if (ch === "\\") {
218
+ escape = true;
219
+ }
220
+ else if (ch === '"') {
221
+ inString = false;
222
+ }
223
+ continue;
224
+ }
225
+ if (ch === '"') {
226
+ inString = true;
227
+ continue;
228
+ }
229
+ if (ch === "{") {
230
+ if (depth === 0)
231
+ start = i;
232
+ depth++;
233
+ }
234
+ else if (ch === "}") {
235
+ if (depth > 0) {
236
+ depth--;
237
+ if (depth === 0 && start >= 0) {
238
+ out.push(text.slice(start, i + 1));
239
+ start = -1;
240
+ }
241
+ }
242
+ }
243
+ }
244
+ return out;
245
+ }
246
+ /** Strict-JSON parse of the fetcher's single-line output. */
247
+ export function parseFetchWindowOutput(output) {
248
+ const trimmed = (output ?? "").trim();
249
+ if (!trimmed)
250
+ return { parseError: "empty-output" };
251
+ // Tolerate code fences without making them mandatory — mirrors
252
+ // `parseStage2Verdict` in dispatcher-types.ts.
253
+ const stripped = trimmed
254
+ .replace(/^```(?:json)?\s*/i, "")
255
+ .replace(/```\s*$/i, "")
256
+ .trim();
257
+ const candidates = extractBalancedJsonObjects(stripped);
258
+ if (candidates.length === 0)
259
+ return { parseError: "no-json-object" };
260
+ const objText = candidates[candidates.length - 1];
261
+ let parsed;
262
+ try {
263
+ parsed = JSON.parse(objText);
264
+ }
265
+ catch (err) {
266
+ return { parseError: `invalid-json: ${err.message}` };
267
+ }
268
+ if (!parsed || typeof parsed !== "object") {
269
+ return { parseError: "not-an-object" };
270
+ }
271
+ const obj = parsed;
272
+ const fetched = typeof obj.fetched === "number" ? obj.fetched : 0;
273
+ const posted = typeof obj.posted === "number" ? obj.posted : 0;
274
+ const duplicates = typeof obj.duplicates === "number" ? obj.duplicates : 0;
275
+ const errors = Array.isArray(obj.errors)
276
+ ? obj.errors
277
+ .filter((row) => typeof row === "object" && row !== null)
278
+ .map((row) => ({ ...row }))
279
+ : [];
280
+ const status = errors.length > 0 ? "partial" : "success";
281
+ return {
282
+ status,
283
+ fetched,
284
+ posted,
285
+ duplicates,
286
+ errors,
287
+ skipped: false,
288
+ };
289
+ }
290
+ function xmlEscape(value) {
291
+ return value
292
+ .replace(/&/g, "&amp;")
293
+ .replace(/"/g, "&quot;")
294
+ .replace(/</g, "&lt;")
295
+ .replace(/>/g, "&gt;");
296
+ }
297
+ /**
298
+ * Render the `<fetch_report>` XML block injected into the parent
299
+ * routine's prompt. Keep the schema narrow — every additional attribute
300
+ * costs prompt tokens on the cache-warm parent session.
301
+ *
302
+ * `meta.routine` accepts any string so the no-routine-key skip path can
303
+ * render the parent event's actual type (e.g. `routine.skill_curation`)
304
+ * instead of borrowing a catalog entry as a placeholder. The renderer
305
+ * strips the `routine.` prefix verbatim — callers may pass either the
306
+ * fully-qualified ProcessKey or a bare suffix.
307
+ */
308
+ export function renderFetchReportBlock(report, meta) {
309
+ const routineAttr = meta.routine.replace(/^routine\./, "");
310
+ const lines = [
311
+ `<fetch_report routine="${xmlEscape(routineAttr)}" agent_day="${xmlEscape(meta.agentDay)}" status="${xmlEscape(report.status)}" fetched="${report.fetched}" posted="${report.posted}" duplicates="${report.duplicates}">`,
312
+ ];
313
+ if (report.failureReason) {
314
+ lines.push(` <failure>${xmlEscape(report.failureReason)}</failure>`);
315
+ }
316
+ for (const err of report.errors) {
317
+ const type = typeof err.type === "string" ? err.type : "unknown";
318
+ // Compact, attribute-shaped serialisation: every string-typed key
319
+ // becomes an XML attribute; nested objects are collapsed to JSON
320
+ // text content so the block stays parseable both as XML and as a
321
+ // line-by-line scan target.
322
+ const attrEntries = Object.entries(err).filter(([k, v]) => k !== "type" && (typeof v === "string" || typeof v === "number"));
323
+ const attrs = attrEntries
324
+ .map(([k, v]) => `${xmlEscape(k)}="${xmlEscape(String(v))}"`)
325
+ .join(" ");
326
+ const nested = Object.entries(err).filter(([k, v]) => k !== "type"
327
+ && typeof v !== "string"
328
+ && typeof v !== "number");
329
+ if (nested.length > 0) {
330
+ lines.push(` <error type="${xmlEscape(type)}"${attrs ? " " + attrs : ""}>${xmlEscape(JSON.stringify(Object.fromEntries(nested)))}</error>`);
331
+ }
332
+ else {
333
+ lines.push(` <error type="${xmlEscape(type)}"${attrs ? " " + attrs : ""} />`);
334
+ }
335
+ }
336
+ lines.push("</fetch_report>");
337
+ return lines.join("\n");
338
+ }
339
+ // ── Runner ────────────────────────────────────────────────────────────────
340
+ export class RoutineFetchWindowRunner {
341
+ db;
342
+ config;
343
+ contextBuilder;
344
+ agentRouter;
345
+ audit;
346
+ prompt;
347
+ getActiveMailAccounts;
348
+ constructor(deps) {
349
+ this.db = deps.db;
350
+ this.config = deps.config;
351
+ this.contextBuilder = deps.contextBuilder;
352
+ this.agentRouter = deps.agentRouter;
353
+ this.audit = deps.audit;
354
+ this.prompt = deps.prompt;
355
+ this.getActiveMailAccounts = deps.getActiveMailAccounts;
356
+ }
357
+ /**
358
+ * Execute the pre-pass for `parentEvent`. Returns the fetch report
359
+ * and rendered `<fetch_report>` block; callers graft the block into
360
+ * the parent event's `event.data.fetchReportBlock` so ContextBuilder
361
+ * injects it into the parent prompt.
362
+ *
363
+ * `routineKey` overrides the auto-derived window key — used by
364
+ * morning_routine to opt into `routine.morning_routine_initial`'s
365
+ * plan when `yesterday.md` is absent (their plans currently coincide,
366
+ * but the seam exists for future divergence).
367
+ */
368
+ async run(parentEvent, routineKey) {
369
+ const key = routineKey ?? routineWindowKeyFromEvent(parentEvent);
370
+ const agentDay = getAgentDayDateStr(this.config.timezone || undefined, this.config.dayBoundaryHour);
371
+ if (!key) {
372
+ const report = {
373
+ status: "skipped",
374
+ fetched: 0,
375
+ posted: 0,
376
+ duplicates: 0,
377
+ errors: [],
378
+ skipped: true,
379
+ failureReason: "no-routine-window-key",
380
+ };
381
+ // Surface the parent's actual event type rather than borrowing a
382
+ // catalog entry — the report attribution would otherwise lie about
383
+ // which routine the pre-pass was attempted for, hiding the
384
+ // misroute behind a plausible-looking placeholder.
385
+ const block = renderFetchReportBlock(report, {
386
+ routine: parentEvent.type,
387
+ agentDay,
388
+ });
389
+ return { report, block };
390
+ }
391
+ if (!routineHasWindows(key)) {
392
+ const report = {
393
+ status: "skipped",
394
+ fetched: 0,
395
+ posted: 0,
396
+ duplicates: 0,
397
+ errors: [],
398
+ skipped: true,
399
+ };
400
+ const block = renderFetchReportBlock(report, { routine: key, agentDay });
401
+ return { report, block };
402
+ }
403
+ // Build the acquisition plan from the current integration state +
404
+ // active accounts. The dispatcher's session backend is the resolved
405
+ // main backend for the routine.fetch_window ProcessKey — resolve via
406
+ // the agent router so the per-(integration, mode) predicate
407
+ // (`delegated-same` vs `delegated-cross`) is correct for the actual
408
+ // run.
409
+ //
410
+ // `integrationsSnapshot` and `sessionBackend` are hoisted above the
411
+ // try block so the `allowedToolsOverride` composer below can re-use
412
+ // the same snapshot the plan was built from. Recomputing the
413
+ // snapshot at execute time would risk a TOCTOU drift if the
414
+ // operator flips an integration mid-pre-pass — the plan would have
415
+ // a row the override couldn't reach, manifesting as a silent MCP
416
+ // permission denial.
417
+ let fetcherEvent;
418
+ let sessionBackend;
419
+ let integrationsSnapshot;
420
+ try {
421
+ // Resolve binding off a placeholder event so we know the session
422
+ // backend before we synthesise the real one (the plan attribute
423
+ // depends on it). The placeholder borrows correlationId so audit
424
+ // rows correlate back to the parent.
425
+ const placeholder = {
426
+ ...createEvent({
427
+ type: FETCH_WINDOW_EVENT_TYPE,
428
+ source: parentEvent.source,
429
+ priority: EventPriority.NORMAL,
430
+ correlationId: parentEvent.correlationId,
431
+ }),
432
+ routine: "fetch_window",
433
+ };
434
+ const preBinding = this.agentRouter.resolveBinding(placeholder, {
435
+ processKey: FETCH_WINDOW_PROCESS_KEY,
436
+ });
437
+ sessionBackend = preBinding.main.backendId;
438
+ integrationsSnapshot = readIntegrations(this.db);
439
+ const accounts = this.collectAccounts(integrationsSnapshot);
440
+ const timestamps = buildAcquisitionTimestamps(new Date(), this.config.timezone || undefined, this.config.dayBoundaryHour);
441
+ const planBlock = buildAcquisitionPlan({
442
+ routine: key,
443
+ agentDay,
444
+ integrations: integrationsSnapshot,
445
+ sessionBackend,
446
+ accounts,
447
+ timestamps,
448
+ });
449
+ fetcherEvent = {
450
+ ...placeholder,
451
+ data: {
452
+ ...placeholder.data,
453
+ acquisitionPlanBlock: planBlock,
454
+ parentRoutine: key,
455
+ parentCorrelationId: parentEvent.correlationId,
456
+ },
457
+ };
458
+ }
459
+ catch (err) {
460
+ return this.fail(key, agentDay, parentEvent, "plan-assembly-failed", err);
461
+ }
462
+ // The acquisition plan can resolve to zero `<fetch>` rows when every
463
+ // integration the routine touches is disabled / cross-backend-bound
464
+ // on a connector-less integration / etc. Treat that as a skip — we
465
+ // don't pay the Haiku cold-start to confirm the agent has nothing
466
+ // to do.
467
+ if (!fetcherPlanHasFetches(fetcherEvent.data.acquisitionPlanBlock)) {
468
+ const report = {
469
+ status: "skipped",
470
+ fetched: 0,
471
+ posted: 0,
472
+ duplicates: 0,
473
+ errors: [],
474
+ skipped: true,
475
+ fetcherCorrelationId: fetcherEvent.correlationId,
476
+ };
477
+ const block = renderFetchReportBlock(report, { routine: key, agentDay });
478
+ logger.debug({
479
+ routine: key,
480
+ correlationId: fetcherEvent.correlationId,
481
+ parentCorrelationId: parentEvent.correlationId,
482
+ }, "Routine fetch-window pre-pass skipped — acquisition plan empty");
483
+ return { report, block };
484
+ }
485
+ // Execute the fetcher session through the standard router pipeline.
486
+ let context;
487
+ try {
488
+ context = await this.contextBuilder.build(fetcherEvent);
489
+ }
490
+ catch (err) {
491
+ return this.fail(key, agentDay, parentEvent, "context-build-failed", err, {
492
+ fetcherCorrelationId: fetcherEvent.correlationId,
493
+ });
494
+ }
495
+ let binding;
496
+ try {
497
+ binding = this.agentRouter.resolveBinding(fetcherEvent, {
498
+ processKey: FETCH_WINDOW_PROCESS_KEY,
499
+ });
500
+ }
501
+ catch (err) {
502
+ return this.fail(key, agentDay, parentEvent, "binding-resolve-failed", err, {
503
+ fetcherCorrelationId: fetcherEvent.correlationId,
504
+ });
505
+ }
506
+ const reassemblePrompt = (bid) => this.prompt.assemble(FETCH_WINDOW_EVENT_TYPE, FETCH_WINDOW_PROCESS_KEY, bid);
507
+ const prompt = reassemblePrompt(binding.main.backendId);
508
+ // Defense-in-depth tool clamp. Replaces the SDK's default
509
+ // allowlist with a narrow set covering only the surfaces the
510
+ // partials use. Backs the agent profile's "no notify, no context
511
+ // writes" guardrails with a daemon-enforced floor so a misbehaving
512
+ // Haiku turn cannot reach `/api/notify`, `/api/context/*`, or any
513
+ // tool outside the documented partial contract. Codex / Gemini
514
+ // ignore per-spawn `allowedToolsOverride` (CLAUDE.md §"Per-spawn
515
+ // tools surface gap"), but the `process_backend_config` envelope
516
+ // (max_turns / max_budget_usd) still applies.
517
+ const allowedToolsOverride = composePrePassAllowedTools(this.config.apiPort, integrationsSnapshot, binding.main.backendId);
518
+ let result;
519
+ try {
520
+ result = await this.agentRouter.execute({
521
+ prompt,
522
+ context,
523
+ event: fetcherEvent,
524
+ processKey: FETCH_WINDOW_PROCESS_KEY,
525
+ preResolvedBinding: binding,
526
+ reassemblePrompt,
527
+ allowedToolsOverride,
528
+ });
529
+ }
530
+ catch (err) {
531
+ return this.fail(key, agentDay, parentEvent, "agent-execute-failed", err, {
532
+ fetcherCorrelationId: fetcherEvent.correlationId,
533
+ });
534
+ }
535
+ // Audit row for the fetcher session itself, distinct from the
536
+ // parent routine's audit row written by the result processor.
537
+ try {
538
+ this.audit.logAction({
539
+ event: fetcherEvent,
540
+ model: result.model,
541
+ costUsd: result.costUsd,
542
+ usage: result.usage,
543
+ modelUsage: result.modelUsage,
544
+ durationMs: result.durationMs,
545
+ numTurns: result.numTurns,
546
+ trigger: "autonomous",
547
+ ...(result.backendId ? { backend: result.backendId } : {}),
548
+ ...(result.costSource ? { costSource: result.costSource } : {}),
549
+ contextUpdated: result.contextUpdated,
550
+ ...(typeof result.advisorCallCount === "number"
551
+ ? { advisorCallCount: result.advisorCallCount }
552
+ : {}),
553
+ });
554
+ }
555
+ catch (err) {
556
+ logger.warn({ err, routine: key, correlationId: fetcherEvent.correlationId }, "Failed to log routine.fetch_window agent_actions row");
557
+ }
558
+ const parsed = parseFetchWindowOutput(result.output);
559
+ if ("parseError" in parsed) {
560
+ const report = {
561
+ status: "failed",
562
+ fetched: 0,
563
+ posted: 0,
564
+ duplicates: 0,
565
+ errors: [{ type: "pre-pass-parse-failed", reason: parsed.parseError }],
566
+ skipped: false,
567
+ failureReason: parsed.parseError,
568
+ fetcherCorrelationId: fetcherEvent.correlationId,
569
+ };
570
+ const block = renderFetchReportBlock(report, { routine: key, agentDay });
571
+ logger.warn({
572
+ routine: key,
573
+ reason: parsed.parseError,
574
+ correlationId: fetcherEvent.correlationId,
575
+ parentCorrelationId: parentEvent.correlationId,
576
+ }, "Routine fetch-window pre-pass output unparsable — parent routine will see <fetch_report status='failed'>");
577
+ return { report, block };
578
+ }
579
+ const report = {
580
+ ...parsed,
581
+ fetcherCorrelationId: fetcherEvent.correlationId,
582
+ };
583
+ const block = renderFetchReportBlock(report, { routine: key, agentDay });
584
+ logger.info({
585
+ routine: key,
586
+ status: report.status,
587
+ fetched: report.fetched,
588
+ posted: report.posted,
589
+ duplicates: report.duplicates,
590
+ errorCount: report.errors.length,
591
+ correlationId: fetcherEvent.correlationId,
592
+ parentCorrelationId: parentEvent.correlationId,
593
+ }, "Routine fetch-window pre-pass completed");
594
+ return { report, block };
595
+ }
596
+ /**
597
+ * Helper for the failure paths — renders a `<fetch_report status="failed">`
598
+ * block and logs the underlying error. Never throws so the caller can
599
+ * always continue with the parent routine dispatch.
600
+ */
601
+ fail(routine, agentDay, parentEvent, kind, err, extra = {}) {
602
+ const message = err instanceof Error ? err.message : String(err);
603
+ const report = {
604
+ status: "failed",
605
+ fetched: 0,
606
+ posted: 0,
607
+ duplicates: 0,
608
+ errors: [{ type: "pre-pass-failed", kind, message }],
609
+ skipped: false,
610
+ failureReason: `${kind}: ${message}`,
611
+ ...(extra.fetcherCorrelationId
612
+ ? { fetcherCorrelationId: extra.fetcherCorrelationId }
613
+ : {}),
614
+ };
615
+ const block = renderFetchReportBlock(report, { routine, agentDay });
616
+ logger.warn({
617
+ routine,
618
+ kind,
619
+ err,
620
+ parentCorrelationId: parentEvent.correlationId,
621
+ }, "Routine fetch-window pre-pass failed — parent routine will see <fetch_report status='failed'>");
622
+ return { report, block };
623
+ }
624
+ /**
625
+ * Translate the mail registry's active-account list into the
626
+ * `AcquisitionAccount[]` shape `buildAcquisitionPlan` expects. Only
627
+ * accounts whose integration is currently non-disabled survive — a
628
+ * disabled gmail integration with five accounts produces zero rows,
629
+ * matching the partial's `<!-- mode:disabled:gmail -->` no-op.
630
+ */
631
+ collectAccounts(integrations) {
632
+ const rows = [];
633
+ for (const account of this.getActiveMailAccounts()) {
634
+ const integrationKey = mailAccountIntegrationKey(account);
635
+ if (integrationKey === null)
636
+ continue;
637
+ const state = integrations[integrationKey];
638
+ if (!state || state.mode === "disabled")
639
+ continue;
640
+ rows.push({
641
+ integration: integrationKey,
642
+ accountId: account.id,
643
+ label: account.email,
644
+ });
645
+ }
646
+ return rows;
647
+ }
648
+ }
649
+ // ── Internal helpers ──────────────────────────────────────────────────────
650
+ /**
651
+ * Return true when the rendered plan carries at least one `<fetch ...>`
652
+ * element. The plan is well-formed even when empty (the dispatcher
653
+ * always emits the wrapper) — we still want to short-circuit so the
654
+ * pre-pass cold-start never fires for a routine with nothing to do.
655
+ */
656
+ function fetcherPlanHasFetches(plan) {
657
+ if (typeof plan !== "string")
658
+ return false;
659
+ return /<fetch\s/i.test(plan);
660
+ }
661
+ //# sourceMappingURL=routine-fetch-window-runner.js.map