@pellux/goodvibes-sdk 0.38.0 → 1.0.0

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 (474) hide show
  1. package/dist/contracts/artifacts/operator-contract.json +3171 -929
  2. package/dist/daemon.d.ts +2 -0
  3. package/dist/daemon.d.ts.map +1 -1
  4. package/dist/daemon.js +4 -0
  5. package/dist/events/surfaces.d.ts +25 -3
  6. package/dist/events/surfaces.d.ts.map +1 -1
  7. package/dist/events/surfaces.js +22 -1
  8. package/dist/events/workflows.d.ts +7 -6
  9. package/dist/events/workflows.d.ts.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/platform/agents/archetypes.d.ts.map +1 -1
  13. package/dist/platform/agents/archetypes.js +7 -0
  14. package/dist/platform/agents/index.d.ts +1 -0
  15. package/dist/platform/agents/index.d.ts.map +1 -1
  16. package/dist/platform/agents/index.js +1 -0
  17. package/dist/platform/agents/orchestrator-runner.d.ts +13 -0
  18. package/dist/platform/agents/orchestrator-runner.d.ts.map +1 -1
  19. package/dist/platform/agents/orchestrator-runner.js +14 -0
  20. package/dist/platform/agents/orchestrator.d.ts +38 -6
  21. package/dist/platform/agents/orchestrator.d.ts.map +1 -1
  22. package/dist/platform/agents/orchestrator.js +66 -18
  23. package/dist/platform/agents/planner-decomposition-runner.d.ts +38 -0
  24. package/dist/platform/agents/planner-decomposition-runner.d.ts.map +1 -0
  25. package/dist/platform/agents/planner-decomposition-runner.js +113 -0
  26. package/dist/platform/agents/turn-knowledge-injection.d.ts +88 -3
  27. package/dist/platform/agents/turn-knowledge-injection.d.ts.map +1 -1
  28. package/dist/platform/agents/turn-knowledge-injection.js +147 -30
  29. package/dist/platform/agents/worktree.d.ts +126 -8
  30. package/dist/platform/agents/worktree.d.ts.map +1 -1
  31. package/dist/platform/agents/worktree.js +240 -19
  32. package/dist/platform/agents/wrfc-controller.d.ts +23 -0
  33. package/dist/platform/agents/wrfc-controller.d.ts.map +1 -1
  34. package/dist/platform/agents/wrfc-controller.js +158 -32
  35. package/dist/platform/agents/wrfc-gate-runtime.d.ts +9 -0
  36. package/dist/platform/agents/wrfc-gate-runtime.d.ts.map +1 -1
  37. package/dist/platform/agents/wrfc-gate-runtime.js +7 -3
  38. package/dist/platform/agents/wrfc-gates.d.ts +1 -1
  39. package/dist/platform/agents/wrfc-gates.d.ts.map +1 -1
  40. package/dist/platform/agents/wrfc-gates.js +7 -1
  41. package/dist/platform/agents/wrfc-types.d.ts +23 -3
  42. package/dist/platform/agents/wrfc-types.d.ts.map +1 -1
  43. package/dist/platform/automation/types.d.ts +9 -1
  44. package/dist/platform/automation/types.d.ts.map +1 -1
  45. package/dist/platform/calendar/calendar-api-shared.d.ts +25 -0
  46. package/dist/platform/calendar/calendar-api-shared.d.ts.map +1 -0
  47. package/dist/platform/calendar/calendar-api-shared.js +100 -0
  48. package/dist/platform/calendar/calendar-connector.d.ts +79 -0
  49. package/dist/platform/calendar/calendar-connector.d.ts.map +1 -0
  50. package/dist/platform/calendar/calendar-connector.js +155 -0
  51. package/dist/platform/calendar/google-calendar-api.d.ts +25 -0
  52. package/dist/platform/calendar/google-calendar-api.d.ts.map +1 -0
  53. package/dist/platform/calendar/google-calendar-api.js +89 -0
  54. package/dist/platform/calendar/http-fetch-adapter.d.ts +25 -0
  55. package/dist/platform/calendar/http-fetch-adapter.d.ts.map +1 -0
  56. package/dist/platform/calendar/http-fetch-adapter.js +25 -0
  57. package/dist/platform/calendar/ics-parser.d.ts +35 -0
  58. package/dist/platform/calendar/ics-parser.d.ts.map +1 -0
  59. package/dist/platform/calendar/ics-parser.js +236 -0
  60. package/dist/platform/calendar/index.d.ts +41 -0
  61. package/dist/platform/calendar/index.d.ts.map +1 -0
  62. package/dist/platform/calendar/index.js +49 -0
  63. package/dist/platform/calendar/merged-calendar-model.d.ts +67 -0
  64. package/dist/platform/calendar/merged-calendar-model.d.ts.map +1 -0
  65. package/dist/platform/calendar/merged-calendar-model.js +181 -0
  66. package/dist/platform/calendar/microsoft-graph-api.d.ts +25 -0
  67. package/dist/platform/calendar/microsoft-graph-api.d.ts.map +1 -0
  68. package/dist/platform/calendar/microsoft-graph-api.js +95 -0
  69. package/dist/platform/calendar/oauth-flow.d.ts +62 -0
  70. package/dist/platform/calendar/oauth-flow.d.ts.map +1 -0
  71. package/dist/platform/calendar/oauth-flow.js +305 -0
  72. package/dist/platform/calendar/oauth-providers.d.ts +43 -0
  73. package/dist/platform/calendar/oauth-providers.d.ts.map +1 -0
  74. package/dist/platform/calendar/oauth-providers.js +120 -0
  75. package/dist/platform/calendar/oauth-token-store.d.ts +72 -0
  76. package/dist/platform/calendar/oauth-token-store.d.ts.map +1 -0
  77. package/dist/platform/calendar/oauth-token-store.js +189 -0
  78. package/dist/platform/calendar/oauth-types.d.ts +274 -0
  79. package/dist/platform/calendar/oauth-types.d.ts.map +1 -0
  80. package/dist/platform/calendar/oauth-types.js +22 -0
  81. package/dist/platform/calendar/rrule.d.ts +39 -0
  82. package/dist/platform/calendar/rrule.d.ts.map +1 -0
  83. package/dist/platform/calendar/rrule.js +261 -0
  84. package/dist/platform/calendar/subscription-store.d.ts +118 -0
  85. package/dist/platform/calendar/subscription-store.d.ts.map +1 -0
  86. package/dist/platform/calendar/subscription-store.js +293 -0
  87. package/dist/platform/calendar/types.d.ts +170 -0
  88. package/dist/platform/calendar/types.d.ts.map +1 -0
  89. package/dist/platform/calendar/types.js +21 -0
  90. package/dist/platform/companion/companion-chat-attachments.d.ts +29 -0
  91. package/dist/platform/companion/companion-chat-attachments.d.ts.map +1 -0
  92. package/dist/platform/companion/companion-chat-attachments.js +121 -0
  93. package/dist/platform/companion/companion-chat-broker-bridge.d.ts +40 -0
  94. package/dist/platform/companion/companion-chat-broker-bridge.d.ts.map +1 -0
  95. package/dist/platform/companion/companion-chat-broker-bridge.js +14 -0
  96. package/dist/platform/companion/companion-chat-broker-sync.d.ts +38 -0
  97. package/dist/platform/companion/companion-chat-broker-sync.d.ts.map +1 -0
  98. package/dist/platform/companion/companion-chat-broker-sync.js +80 -0
  99. package/dist/platform/companion/companion-chat-gc.d.ts +63 -0
  100. package/dist/platform/companion/companion-chat-gc.d.ts.map +1 -0
  101. package/dist/platform/companion/companion-chat-gc.js +38 -0
  102. package/dist/platform/companion/companion-chat-manager.d.ts +67 -20
  103. package/dist/platform/companion/companion-chat-manager.d.ts.map +1 -1
  104. package/dist/platform/companion/companion-chat-manager.js +130 -144
  105. package/dist/platform/companion/companion-chat-persistence.d.ts +9 -1
  106. package/dist/platform/companion/companion-chat-persistence.d.ts.map +1 -1
  107. package/dist/platform/companion/companion-chat-persistence.js +10 -2
  108. package/dist/platform/companion/companion-chat-routes.d.ts +2 -1
  109. package/dist/platform/companion/companion-chat-routes.d.ts.map +1 -1
  110. package/dist/platform/companion/companion-chat-routes.js +37 -4
  111. package/dist/platform/config/credential-status.d.ts +28 -0
  112. package/dist/platform/config/credential-status.d.ts.map +1 -0
  113. package/dist/platform/config/credential-status.js +75 -0
  114. package/dist/platform/config/index.d.ts +25 -0
  115. package/dist/platform/config/index.d.ts.map +1 -1
  116. package/dist/platform/config/index.js +18 -0
  117. package/dist/platform/config/manager.d.ts +9 -0
  118. package/dist/platform/config/manager.d.ts.map +1 -1
  119. package/dist/platform/config/manager.js +21 -2
  120. package/dist/platform/config/migrations.d.ts +48 -0
  121. package/dist/platform/config/migrations.d.ts.map +1 -0
  122. package/dist/platform/config/migrations.js +54 -0
  123. package/dist/platform/config/schema-domain-core.d.ts +10 -1
  124. package/dist/platform/config/schema-domain-core.d.ts.map +1 -1
  125. package/dist/platform/config/schema-domain-core.js +46 -3
  126. package/dist/platform/config/schema-types.d.ts +13 -7
  127. package/dist/platform/config/schema-types.d.ts.map +1 -1
  128. package/dist/platform/config/schema.d.ts.map +1 -1
  129. package/dist/platform/config/schema.js +2 -0
  130. package/dist/platform/control-plane/approval-broker.d.ts +11 -0
  131. package/dist/platform/control-plane/approval-broker.d.ts.map +1 -1
  132. package/dist/platform/control-plane/approval-broker.js +17 -1
  133. package/dist/platform/control-plane/approval-hunk-apply.d.ts +77 -0
  134. package/dist/platform/control-plane/approval-hunk-apply.d.ts.map +1 -0
  135. package/dist/platform/control-plane/approval-hunk-apply.js +80 -0
  136. package/dist/platform/control-plane/gateway-disabled-response.d.ts +20 -0
  137. package/dist/platform/control-plane/gateway-disabled-response.d.ts.map +1 -0
  138. package/dist/platform/control-plane/gateway-disabled-response.js +24 -0
  139. package/dist/platform/control-plane/gateway-scope-enforcement.d.ts +68 -0
  140. package/dist/platform/control-plane/gateway-scope-enforcement.d.ts.map +1 -0
  141. package/dist/platform/control-plane/gateway-scope-enforcement.js +91 -0
  142. package/dist/platform/control-plane/gateway-utils.d.ts +17 -0
  143. package/dist/platform/control-plane/gateway-utils.d.ts.map +1 -1
  144. package/dist/platform/control-plane/gateway-utils.js +30 -0
  145. package/dist/platform/control-plane/gateway.d.ts +12 -2
  146. package/dist/platform/control-plane/gateway.d.ts.map +1 -1
  147. package/dist/platform/control-plane/gateway.js +27 -31
  148. package/dist/platform/control-plane/index.d.ts +6 -1
  149. package/dist/platform/control-plane/index.d.ts.map +1 -1
  150. package/dist/platform/control-plane/index.js +4 -0
  151. package/dist/platform/control-plane/invoke-input-validation.d.ts +56 -0
  152. package/dist/platform/control-plane/invoke-input-validation.d.ts.map +1 -0
  153. package/dist/platform/control-plane/invoke-input-validation.js +163 -0
  154. package/dist/platform/control-plane/method-catalog-admin.d.ts.map +1 -1
  155. package/dist/platform/control-plane/method-catalog-admin.js +12 -1
  156. package/dist/platform/control-plane/method-catalog-calendar.d.ts +16 -0
  157. package/dist/platform/control-plane/method-catalog-calendar.d.ts.map +1 -1
  158. package/dist/platform/control-plane/method-catalog-calendar.js +21 -0
  159. package/dist/platform/control-plane/method-catalog-channels.d.ts.map +1 -1
  160. package/dist/platform/control-plane/method-catalog-channels.js +32 -3
  161. package/dist/platform/control-plane/method-catalog-control-automation.d.ts.map +1 -1
  162. package/dist/platform/control-plane/method-catalog-control-automation.js +19 -34
  163. package/dist/platform/control-plane/method-catalog-control-companion.d.ts +15 -0
  164. package/dist/platform/control-plane/method-catalog-control-companion.d.ts.map +1 -0
  165. package/dist/platform/control-plane/method-catalog-control-companion.js +133 -0
  166. package/dist/platform/control-plane/method-catalog-control-core.d.ts.map +1 -1
  167. package/dist/platform/control-plane/method-catalog-control-core.js +104 -122
  168. package/dist/platform/control-plane/method-catalog-control.d.ts.map +1 -1
  169. package/dist/platform/control-plane/method-catalog-control.js +4 -0
  170. package/dist/platform/control-plane/method-catalog-email.d.ts +17 -0
  171. package/dist/platform/control-plane/method-catalog-email.d.ts.map +1 -1
  172. package/dist/platform/control-plane/method-catalog-email.js +21 -0
  173. package/dist/platform/control-plane/method-catalog-events.d.ts +28 -0
  174. package/dist/platform/control-plane/method-catalog-events.d.ts.map +1 -1
  175. package/dist/platform/control-plane/method-catalog-events.js +74 -1
  176. package/dist/platform/control-plane/method-catalog-fleet.d.ts +24 -0
  177. package/dist/platform/control-plane/method-catalog-fleet.d.ts.map +1 -0
  178. package/dist/platform/control-plane/method-catalog-fleet.js +65 -0
  179. package/dist/platform/control-plane/method-catalog-route-reconcile.d.ts +84 -0
  180. package/dist/platform/control-plane/method-catalog-route-reconcile.d.ts.map +1 -0
  181. package/dist/platform/control-plane/method-catalog-route-reconcile.js +103 -0
  182. package/dist/platform/control-plane/method-catalog-shared.d.ts +29 -0
  183. package/dist/platform/control-plane/method-catalog-shared.d.ts.map +1 -1
  184. package/dist/platform/control-plane/method-catalog.d.ts +9 -0
  185. package/dist/platform/control-plane/method-catalog.d.ts.map +1 -1
  186. package/dist/platform/control-plane/method-catalog.js +32 -3
  187. package/dist/platform/control-plane/operator-contract-schemas-admin.d.ts +8 -0
  188. package/dist/platform/control-plane/operator-contract-schemas-admin.d.ts.map +1 -1
  189. package/dist/platform/control-plane/operator-contract-schemas-admin.js +22 -1
  190. package/dist/platform/control-plane/operator-contract-schemas-fleet.d.ts +18 -0
  191. package/dist/platform/control-plane/operator-contract-schemas-fleet.d.ts.map +1 -0
  192. package/dist/platform/control-plane/operator-contract-schemas-fleet.js +182 -0
  193. package/dist/platform/control-plane/operator-contract-schemas-permissions.d.ts.map +1 -1
  194. package/dist/platform/control-plane/operator-contract-schemas-permissions.js +5 -0
  195. package/dist/platform/control-plane/operator-contract-schemas-runtime.d.ts +29 -0
  196. package/dist/platform/control-plane/operator-contract-schemas-runtime.d.ts.map +1 -1
  197. package/dist/platform/control-plane/operator-contract-schemas-runtime.js +57 -3
  198. package/dist/platform/control-plane/operator-contract-schemas.d.ts +1 -0
  199. package/dist/platform/control-plane/operator-contract-schemas.d.ts.map +1 -1
  200. package/dist/platform/control-plane/operator-contract-schemas.js +1 -0
  201. package/dist/platform/control-plane/operator-contract.d.ts.map +1 -1
  202. package/dist/platform/control-plane/operator-contract.js +8 -0
  203. package/dist/platform/control-plane/routes/checkpoints.d.ts +39 -0
  204. package/dist/platform/control-plane/routes/checkpoints.d.ts.map +1 -0
  205. package/dist/platform/control-plane/routes/checkpoints.js +166 -0
  206. package/dist/platform/control-plane/routes/fleet.d.ts +53 -0
  207. package/dist/platform/control-plane/routes/fleet.d.ts.map +1 -0
  208. package/dist/platform/control-plane/routes/fleet.js +153 -0
  209. package/dist/platform/control-plane/routes/gateway-verb-error.d.ts +30 -0
  210. package/dist/platform/control-plane/routes/gateway-verb-error.d.ts.map +1 -0
  211. package/dist/platform/control-plane/routes/gateway-verb-error.js +36 -0
  212. package/dist/platform/control-plane/routes/index.d.ts +0 -6
  213. package/dist/platform/control-plane/routes/index.d.ts.map +1 -1
  214. package/dist/platform/control-plane/routes/index.js +5 -5
  215. package/dist/platform/control-plane/routes/invocation-params.d.ts +23 -0
  216. package/dist/platform/control-plane/routes/invocation-params.d.ts.map +1 -0
  217. package/dist/platform/control-plane/routes/invocation-params.js +9 -0
  218. package/dist/platform/control-plane/routes/register-w3-s2.d.ts +26 -0
  219. package/dist/platform/control-plane/routes/register-w3-s2.d.ts.map +1 -0
  220. package/dist/platform/control-plane/routes/register-w3-s2.js +8 -0
  221. package/dist/platform/control-plane/routes/session-search.d.ts +37 -0
  222. package/dist/platform/control-plane/routes/session-search.d.ts.map +1 -0
  223. package/dist/platform/control-plane/routes/session-search.js +113 -0
  224. package/dist/platform/control-plane/session-broker-gc.d.ts +16 -0
  225. package/dist/platform/control-plane/session-broker-gc.d.ts.map +1 -1
  226. package/dist/platform/control-plane/session-broker-gc.js +23 -6
  227. package/dist/platform/control-plane/session-broker-helpers.d.ts +3 -3
  228. package/dist/platform/control-plane/session-broker-helpers.d.ts.map +1 -1
  229. package/dist/platform/control-plane/session-broker-inputs.d.ts +18 -0
  230. package/dist/platform/control-plane/session-broker-inputs.d.ts.map +1 -1
  231. package/dist/platform/control-plane/session-broker-inputs.js +34 -0
  232. package/dist/platform/control-plane/session-broker-sessions.d.ts +102 -3
  233. package/dist/platform/control-plane/session-broker-sessions.d.ts.map +1 -1
  234. package/dist/platform/control-plane/session-broker-sessions.js +179 -5
  235. package/dist/platform/control-plane/session-broker-state.d.ts +20 -1
  236. package/dist/platform/control-plane/session-broker-state.d.ts.map +1 -1
  237. package/dist/platform/control-plane/session-broker-state.js +67 -5
  238. package/dist/platform/control-plane/session-broker.d.ts +51 -38
  239. package/dist/platform/control-plane/session-broker.d.ts.map +1 -1
  240. package/dist/platform/control-plane/session-broker.js +183 -75
  241. package/dist/platform/control-plane/session-store-importer.d.ts +57 -0
  242. package/dist/platform/control-plane/session-store-importer.d.ts.map +1 -0
  243. package/dist/platform/control-plane/session-store-importer.js +227 -0
  244. package/dist/platform/control-plane/session-types.d.ts +142 -7
  245. package/dist/platform/control-plane/session-types.d.ts.map +1 -1
  246. package/dist/platform/core/execution-plan.d.ts +43 -1
  247. package/dist/platform/core/execution-plan.d.ts.map +1 -1
  248. package/dist/platform/core/execution-plan.js +46 -10
  249. package/dist/platform/core/index.d.ts +1 -0
  250. package/dist/platform/core/index.d.ts.map +1 -1
  251. package/dist/platform/core/index.js +1 -0
  252. package/dist/platform/core/orchestrator-runtime.d.ts +21 -1
  253. package/dist/platform/core/orchestrator-runtime.d.ts.map +1 -1
  254. package/dist/platform/core/orchestrator-tool-runtime.d.ts +7 -0
  255. package/dist/platform/core/orchestrator-tool-runtime.d.ts.map +1 -1
  256. package/dist/platform/core/orchestrator-tool-runtime.js +10 -0
  257. package/dist/platform/core/orchestrator-turn-loop.d.ts +8 -1
  258. package/dist/platform/core/orchestrator-turn-loop.d.ts.map +1 -1
  259. package/dist/platform/core/orchestrator-turn-loop.js +6 -0
  260. package/dist/platform/core/orchestrator.d.ts +6 -0
  261. package/dist/platform/core/orchestrator.d.ts.map +1 -1
  262. package/dist/platform/core/orchestrator.js +15 -0
  263. package/dist/platform/core/plan-decomposition.d.ts +196 -0
  264. package/dist/platform/core/plan-decomposition.d.ts.map +1 -0
  265. package/dist/platform/core/plan-decomposition.js +417 -0
  266. package/dist/platform/core/plan-proposal.d.ts +39 -0
  267. package/dist/platform/core/plan-proposal.d.ts.map +1 -1
  268. package/dist/platform/core/wrfc-routing.d.ts +1 -0
  269. package/dist/platform/core/wrfc-routing.d.ts.map +1 -1
  270. package/dist/platform/core/wrfc-routing.js +51 -1
  271. package/dist/platform/daemon/boot.d.ts +63 -0
  272. package/dist/platform/daemon/boot.d.ts.map +1 -0
  273. package/dist/platform/daemon/boot.js +60 -0
  274. package/dist/platform/daemon/cli.js +2 -1
  275. package/dist/platform/daemon/control-plane.d.ts.map +1 -1
  276. package/dist/platform/daemon/control-plane.js +50 -1
  277. package/dist/platform/daemon/facade-composition.d.ts.map +1 -1
  278. package/dist/platform/daemon/facade-composition.js +6 -0
  279. package/dist/platform/daemon/facade.d.ts +18 -6
  280. package/dist/platform/daemon/facade.d.ts.map +1 -1
  281. package/dist/platform/daemon/facade.js +50 -43
  282. package/dist/platform/daemon/http/router-route-contexts.d.ts +1 -0
  283. package/dist/platform/daemon/http/router-route-contexts.d.ts.map +1 -1
  284. package/dist/platform/daemon/http/router-route-contexts.js +1 -0
  285. package/dist/platform/daemon/http/router-session-broker-adapter.d.ts +13 -0
  286. package/dist/platform/daemon/http/router-session-broker-adapter.d.ts.map +1 -0
  287. package/dist/platform/daemon/http/router-session-broker-adapter.js +27 -0
  288. package/dist/platform/daemon/http/router.d.ts +1 -1
  289. package/dist/platform/daemon/http/router.d.ts.map +1 -1
  290. package/dist/platform/daemon/http/router.js +7 -22
  291. package/dist/platform/daemon/http/runtime-route-types.d.ts +42 -4
  292. package/dist/platform/daemon/http/runtime-route-types.d.ts.map +1 -1
  293. package/dist/platform/daemon/index.d.ts +2 -0
  294. package/dist/platform/daemon/index.d.ts.map +1 -1
  295. package/dist/platform/daemon/index.js +1 -0
  296. package/dist/platform/daemon/service-manager.d.ts +16 -0
  297. package/dist/platform/daemon/service-manager.d.ts.map +1 -1
  298. package/dist/platform/daemon/service-manager.js +79 -15
  299. package/dist/platform/intelligence/tree-sitter/service.d.ts.map +1 -1
  300. package/dist/platform/intelligence/tree-sitter/service.js +16 -2
  301. package/dist/platform/knowledge/browser-history/readers.d.ts.map +1 -1
  302. package/dist/platform/knowledge/browser-history/readers.js +15 -5
  303. package/dist/platform/knowledge/project-planning/service.d.ts +14 -1
  304. package/dist/platform/knowledge/project-planning/service.d.ts.map +1 -1
  305. package/dist/platform/knowledge/project-planning/service.js +77 -0
  306. package/dist/platform/knowledge/project-planning/types.d.ts +32 -0
  307. package/dist/platform/knowledge/project-planning/types.d.ts.map +1 -1
  308. package/dist/platform/orchestration/bookkeeping.d.ts +12 -0
  309. package/dist/platform/orchestration/bookkeeping.d.ts.map +1 -0
  310. package/dist/platform/orchestration/bookkeeping.js +91 -0
  311. package/dist/platform/orchestration/controller-compat.d.ts +14 -0
  312. package/dist/platform/orchestration/controller-compat.d.ts.map +1 -1
  313. package/dist/platform/orchestration/controller-compat.js +18 -4
  314. package/dist/platform/orchestration/engine.d.ts +26 -1
  315. package/dist/platform/orchestration/engine.d.ts.map +1 -1
  316. package/dist/platform/orchestration/engine.js +226 -21
  317. package/dist/platform/orchestration/index.d.ts +6 -4
  318. package/dist/platform/orchestration/index.d.ts.map +1 -1
  319. package/dist/platform/orchestration/index.js +3 -2
  320. package/dist/platform/orchestration/persistence.d.ts.map +1 -1
  321. package/dist/platform/orchestration/persistence.js +4 -1
  322. package/dist/platform/orchestration/phase-runner.d.ts +29 -2
  323. package/dist/platform/orchestration/phase-runner.d.ts.map +1 -1
  324. package/dist/platform/orchestration/phase-runner.js +98 -25
  325. package/dist/platform/orchestration/proposal-workstream.d.ts +56 -0
  326. package/dist/platform/orchestration/proposal-workstream.d.ts.map +1 -0
  327. package/dist/platform/orchestration/proposal-workstream.js +76 -0
  328. package/dist/platform/orchestration/scheduler.d.ts +19 -0
  329. package/dist/platform/orchestration/scheduler.d.ts.map +1 -1
  330. package/dist/platform/orchestration/scheduler.js +16 -0
  331. package/dist/platform/orchestration/types.d.ts +286 -1
  332. package/dist/platform/orchestration/types.d.ts.map +1 -1
  333. package/dist/platform/orchestration/types.js +32 -0
  334. package/dist/platform/orchestration/worktree-isolation.d.ts +33 -0
  335. package/dist/platform/orchestration/worktree-isolation.d.ts.map +1 -0
  336. package/dist/platform/orchestration/worktree-isolation.js +248 -0
  337. package/dist/platform/presentation/glyphs.d.ts +91 -0
  338. package/dist/platform/presentation/glyphs.d.ts.map +1 -0
  339. package/dist/platform/presentation/glyphs.js +93 -0
  340. package/dist/platform/presentation/index.d.ts +28 -0
  341. package/dist/platform/presentation/index.d.ts.map +1 -0
  342. package/dist/platform/presentation/index.js +27 -0
  343. package/dist/platform/presentation/thinking-phrases.d.ts +12 -0
  344. package/dist/platform/presentation/thinking-phrases.d.ts.map +1 -0
  345. package/dist/platform/presentation/thinking-phrases.js +32 -0
  346. package/dist/platform/presentation/tones.d.ts +121 -0
  347. package/dist/platform/presentation/tones.d.ts.map +1 -0
  348. package/dist/platform/presentation/tones.js +145 -0
  349. package/dist/platform/presentation/waiting-wording.d.ts +54 -0
  350. package/dist/platform/presentation/waiting-wording.d.ts.map +1 -0
  351. package/dist/platform/presentation/waiting-wording.js +51 -0
  352. package/dist/platform/providers/runtime-snapshot.d.ts.map +1 -1
  353. package/dist/platform/providers/runtime-snapshot.js +12 -2
  354. package/dist/platform/runtime/bootstrap-services.d.ts +62 -2
  355. package/dist/platform/runtime/bootstrap-services.d.ts.map +1 -1
  356. package/dist/platform/runtime/bootstrap-services.js +217 -50
  357. package/dist/platform/runtime/daemon-version-compat.d.ts +39 -0
  358. package/dist/platform/runtime/daemon-version-compat.d.ts.map +1 -0
  359. package/dist/platform/runtime/daemon-version-compat.js +75 -0
  360. package/dist/platform/runtime/detached-daemon-runtime.d.ts +26 -0
  361. package/dist/platform/runtime/detached-daemon-runtime.d.ts.map +1 -0
  362. package/dist/platform/runtime/detached-daemon-runtime.js +49 -0
  363. package/dist/platform/runtime/emitters/workflows.d.ts +1 -1
  364. package/dist/platform/runtime/emitters/workflows.d.ts.map +1 -1
  365. package/dist/platform/runtime/events/index.d.ts +13 -0
  366. package/dist/platform/runtime/events/index.d.ts.map +1 -1
  367. package/dist/platform/runtime/events/index.js +13 -0
  368. package/dist/platform/runtime/feature-flags/flags.d.ts.map +1 -1
  369. package/dist/platform/runtime/feature-flags/flags.js +39 -1
  370. package/dist/platform/runtime/fleet/adapters/orchestration.d.ts +40 -18
  371. package/dist/platform/runtime/fleet/adapters/orchestration.d.ts.map +1 -1
  372. package/dist/platform/runtime/fleet/adapters/orchestration.js +138 -34
  373. package/dist/platform/runtime/fleet/adapters/wrfc.d.ts +13 -0
  374. package/dist/platform/runtime/fleet/adapters/wrfc.d.ts.map +1 -1
  375. package/dist/platform/runtime/fleet/adapters/wrfc.js +48 -1
  376. package/dist/platform/runtime/fleet/registry.d.ts.map +1 -1
  377. package/dist/platform/runtime/fleet/registry.js +38 -4
  378. package/dist/platform/runtime/operator-client.d.ts +20 -2
  379. package/dist/platform/runtime/operator-client.d.ts.map +1 -1
  380. package/dist/platform/runtime/operator-client.js +11 -1
  381. package/dist/platform/runtime/provider-accounts/registry.d.ts.map +1 -1
  382. package/dist/platform/runtime/provider-accounts/registry.js +11 -1
  383. package/dist/platform/runtime/services.d.ts +9 -7
  384. package/dist/platform/runtime/services.d.ts.map +1 -1
  385. package/dist/platform/runtime/services.js +19 -4
  386. package/dist/platform/runtime/session-spine/client.d.ts +234 -0
  387. package/dist/platform/runtime/session-spine/client.d.ts.map +1 -0
  388. package/dist/platform/runtime/session-spine/client.js +449 -0
  389. package/dist/platform/runtime/session-spine/index.d.ts +12 -0
  390. package/dist/platform/runtime/session-spine/index.d.ts.map +1 -0
  391. package/dist/platform/runtime/session-spine/index.js +11 -0
  392. package/dist/platform/runtime/session-spine/union-cache.d.ts +225 -0
  393. package/dist/platform/runtime/session-spine/union-cache.d.ts.map +1 -0
  394. package/dist/platform/runtime/session-spine/union-cache.js +333 -0
  395. package/dist/platform/runtime/shell-command-ops.d.ts +3 -1
  396. package/dist/platform/runtime/shell-command-ops.d.ts.map +1 -1
  397. package/dist/platform/runtime/transports/daemon-http-client-validators.d.ts.map +1 -1
  398. package/dist/platform/runtime/transports/daemon-http-client-validators.js +22 -0
  399. package/dist/platform/runtime/transports/daemon-http-client.d.ts.map +1 -1
  400. package/dist/platform/runtime/transports/daemon-http-client.js +25 -2
  401. package/dist/platform/runtime/transports/http-types.d.ts +9 -2
  402. package/dist/platform/runtime/transports/http-types.d.ts.map +1 -1
  403. package/dist/platform/state/canonical-memory.d.ts +95 -0
  404. package/dist/platform/state/canonical-memory.d.ts.map +1 -0
  405. package/dist/platform/state/canonical-memory.js +155 -0
  406. package/dist/platform/state/code-index-reindex.d.ts +73 -0
  407. package/dist/platform/state/code-index-reindex.d.ts.map +1 -0
  408. package/dist/platform/state/code-index-reindex.js +156 -0
  409. package/dist/platform/state/code-index-store.d.ts.map +1 -1
  410. package/dist/platform/state/code-index-store.js +10 -2
  411. package/dist/platform/state/index.d.ts +8 -0
  412. package/dist/platform/state/index.d.ts.map +1 -1
  413. package/dist/platform/state/index.js +4 -0
  414. package/dist/platform/state/memory-recall-contract.d.ts +63 -0
  415. package/dist/platform/state/memory-recall-contract.d.ts.map +1 -0
  416. package/dist/platform/state/memory-recall-contract.js +94 -0
  417. package/dist/platform/state/memory-store.d.ts +2 -0
  418. package/dist/platform/state/memory-store.d.ts.map +1 -1
  419. package/dist/platform/state/memory-store.js +4 -0
  420. package/dist/platform/state/memory-vector-store.d.ts.map +1 -1
  421. package/dist/platform/state/memory-vector-store.js +13 -2
  422. package/dist/platform/state/sqlite-store.d.ts +2 -0
  423. package/dist/platform/state/sqlite-store.d.ts.map +1 -1
  424. package/dist/platform/state/sqlite-store.js +4 -0
  425. package/dist/platform/state/vibe-projection.d.ts +67 -0
  426. package/dist/platform/state/vibe-projection.d.ts.map +1 -0
  427. package/dist/platform/state/vibe-projection.js +115 -0
  428. package/dist/platform/tools/agent/index.d.ts.map +1 -1
  429. package/dist/platform/tools/agent/index.js +8 -0
  430. package/dist/platform/tools/agent/manager.d.ts +12 -0
  431. package/dist/platform/tools/agent/manager.d.ts.map +1 -1
  432. package/dist/platform/tools/agent/manager.js +6 -0
  433. package/dist/platform/tools/agent/schema.d.ts +26 -0
  434. package/dist/platform/tools/agent/schema.d.ts.map +1 -1
  435. package/dist/platform/tools/agent/schema.js +2 -2
  436. package/dist/platform/tools/agent/wrfc-batch-policy.d.ts +13 -0
  437. package/dist/platform/tools/agent/wrfc-batch-policy.d.ts.map +1 -1
  438. package/dist/platform/tools/agent/wrfc-batch-policy.js +60 -0
  439. package/dist/platform/tools/exec/runtime.d.ts +10 -0
  440. package/dist/platform/tools/exec/runtime.d.ts.map +1 -1
  441. package/dist/platform/tools/exec/runtime.js +22 -5
  442. package/dist/platform/tools/exec/schema.d.ts +1 -1
  443. package/dist/platform/tools/exec/schema.d.ts.map +1 -1
  444. package/dist/platform/tools/exec/schema.js +2 -1
  445. package/dist/platform/tools/index.d.ts +2 -0
  446. package/dist/platform/tools/index.d.ts.map +1 -1
  447. package/dist/platform/tools/index.js +2 -0
  448. package/dist/platform/tools/state/index.d.ts +9 -0
  449. package/dist/platform/tools/state/index.d.ts.map +1 -1
  450. package/dist/platform/tools/state/index.js +60 -3
  451. package/dist/platform/types/foundation-contract.d.ts +1 -1
  452. package/dist/platform/types/foundation-contract.d.ts.map +1 -1
  453. package/dist/platform/types/index.d.ts +1 -1
  454. package/dist/platform/types/index.d.ts.map +1 -1
  455. package/dist/platform/version.js +1 -1
  456. package/package.json +21 -9
  457. package/dist/platform/control-plane/routes/automation.d.ts +0 -3
  458. package/dist/platform/control-plane/routes/automation.d.ts.map +0 -1
  459. package/dist/platform/control-plane/routes/automation.js +0 -63
  460. package/dist/platform/control-plane/routes/context.d.ts +0 -2
  461. package/dist/platform/control-plane/routes/context.d.ts.map +0 -1
  462. package/dist/platform/control-plane/routes/context.js +0 -1
  463. package/dist/platform/control-plane/routes/operator.d.ts +0 -8
  464. package/dist/platform/control-plane/routes/operator.d.ts.map +0 -1
  465. package/dist/platform/control-plane/routes/operator.js +0 -415
  466. package/dist/platform/control-plane/routes/remote.d.ts +0 -3
  467. package/dist/platform/control-plane/routes/remote.d.ts.map +0 -1
  468. package/dist/platform/control-plane/routes/remote.js +0 -35
  469. package/dist/platform/control-plane/routes/sessions.d.ts +0 -3
  470. package/dist/platform/control-plane/routes/sessions.d.ts.map +0 -1
  471. package/dist/platform/control-plane/routes/sessions.js +0 -43
  472. package/dist/platform/control-plane/routes/tasks.d.ts +0 -3
  473. package/dist/platform/control-plane/routes/tasks.d.ts.map +0 -1
  474. package/dist/platform/control-plane/routes/tasks.js +0 -22
@@ -0,0 +1,261 @@
1
+ /**
2
+ * rrule.ts — a pure RRULE reader for an honest, deliberately-bounded subset.
3
+ *
4
+ * The honesty rule (see the decision record): an event's recurrence is either
5
+ * FULLY and correctly expanded, or NOT expanded at all with an explicit reason.
6
+ * We never emit occurrences from a rule we do not completely honor — a half-honored
7
+ * BYMONTHDAY would put events on wrong dates, which is worse than not expanding.
8
+ *
9
+ * Fully supported (expanded to real occurrences):
10
+ * - FREQ = DAILY | WEEKLY | MONTHLY | YEARLY
11
+ * - INTERVAL (default 1)
12
+ * - COUNT (occurrence cap)
13
+ * - UNTIL (inclusive end bound; DATE or date-time, 'Z' or floating)
14
+ * - BYDAY — ONLY for FREQ=WEEKLY with INTERVAL=1 and weekday-only tokens
15
+ * (MO,TU,WE,TH,FR,SA,SU with no leading ordinal). Under those
16
+ * conditions WKST does not change the matched set, so it is ignored
17
+ * safely. Any BYDAY outside that condition is treated as unsupported.
18
+ *
19
+ * Everything else present in the RRULE (BYMONTHDAY, BYMONTH, BYSETPOS, BYHOUR,
20
+ * BYWEEKNO, BYYEARDAY, ordinal BYDAY, BYDAY on non-weekly, INTERVAL>1 with BYDAY,
21
+ * or an unknown FREQ) marks the rule `unsupported` and names the offending part.
22
+ *
23
+ * PURE: no fs, no network, no process globals.
24
+ */
25
+ const WEEKDAY_TOKENS = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
26
+ const KNOWN_FREQ = new Set(['DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY']);
27
+ /** Parts we fully honor; anything else present makes the rule unsupported. */
28
+ const SUPPORTED_PARTS = new Set(['FREQ', 'INTERVAL', 'COUNT', 'UNTIL', 'WKST', 'BYDAY']);
29
+ function parseRRule(rule) {
30
+ const map = new Map();
31
+ for (const seg of rule.split(';')) {
32
+ const eq = seg.indexOf('=');
33
+ if (eq < 0)
34
+ continue;
35
+ const k = seg.slice(0, eq).trim().toUpperCase();
36
+ const v = seg.slice(eq + 1).trim();
37
+ if (k !== '')
38
+ map.set(k, v);
39
+ }
40
+ return { map };
41
+ }
42
+ /**
43
+ * Classify a raw RRULE into an honest RecurrenceInfo. Called by the parser at read
44
+ * time so the event carries its expansion verdict before any window is known.
45
+ */
46
+ export function describeRecurrence(rule) {
47
+ const { map } = parseRRule(rule);
48
+ const freq = (map.get('FREQ') ?? '').toUpperCase();
49
+ if (!KNOWN_FREQ.has(freq)) {
50
+ return { rule, expansion: 'unsupported', unsupportedReason: `RRULE FREQ '${freq || '(missing)'}' is not expanded by this build` };
51
+ }
52
+ for (const part of map.keys()) {
53
+ if (!SUPPORTED_PARTS.has(part)) {
54
+ return { rule, expansion: 'unsupported', unsupportedReason: `RRULE part ${part} is not expanded by this build` };
55
+ }
56
+ }
57
+ if (map.has('BYDAY')) {
58
+ const byDayVerdict = classifyByDay(freq, map);
59
+ if (byDayVerdict)
60
+ return { rule, expansion: 'unsupported', unsupportedReason: byDayVerdict };
61
+ }
62
+ return { rule, expansion: 'full' };
63
+ }
64
+ /** Return an unsupported-reason string if this BYDAY is outside the honored subset, else undefined. */
65
+ function classifyByDay(freq, map) {
66
+ if (freq !== 'WEEKLY') {
67
+ return `RRULE BYDAY on FREQ=${freq} is not expanded by this build (only weekly BYDAY is supported)`;
68
+ }
69
+ const interval = parseInterval(map);
70
+ if (interval !== 1) {
71
+ return 'RRULE BYDAY with INTERVAL>1 is not expanded by this build (week-boundary/WKST handling required)';
72
+ }
73
+ const tokens = (map.get('BYDAY') ?? '').split(',').map((t) => t.trim().toUpperCase());
74
+ for (const tok of tokens) {
75
+ if (!WEEKDAY_TOKENS.includes(tok)) {
76
+ return `RRULE BYDAY token '${tok}' with an ordinal is not expanded by this build`;
77
+ }
78
+ }
79
+ return undefined;
80
+ }
81
+ function parseInterval(map) {
82
+ const raw = map.get('INTERVAL');
83
+ if (!raw)
84
+ return 1;
85
+ const n = Number.parseInt(raw, 10);
86
+ return Number.isFinite(n) && n > 0 ? n : 1;
87
+ }
88
+ /** Parse an UNTIL value ('YYYYMMDD' or 'YYYYMMDDThhmmss[Z]') to a 'YYYY-MM-DD' date string. */
89
+ function parseUntilDate(raw) {
90
+ const m = /^(\d{4})(\d{2})(\d{2})/.exec(raw.trim());
91
+ if (!m)
92
+ return undefined;
93
+ return `${m[1]}-${m[2]}-${m[3]}`;
94
+ }
95
+ /** Extract 'YYYY-MM-DD' from an EventDateTime value. */
96
+ function datePart(value) {
97
+ return value.slice(0, 10);
98
+ }
99
+ /** Hard ceiling so a COUNT-less UNTIL-less rule cannot run away. */
100
+ const MAX_OCCURRENCES = 1000;
101
+ function toUtcDate(ymd) {
102
+ const [y, m, d] = ymd.split('-').map((s) => Number.parseInt(s, 10));
103
+ return Date.UTC(y ?? 1970, (m ?? 1) - 1, d ?? 1);
104
+ }
105
+ function fromUtcDate(ms) {
106
+ const d = new Date(ms);
107
+ const y = d.getUTCFullYear().toString().padStart(4, '0');
108
+ const m = (d.getUTCMonth() + 1).toString().padStart(2, '0');
109
+ const day = d.getUTCDate().toString().padStart(2, '0');
110
+ return `${y}-${m}-${day}`;
111
+ }
112
+ const DAY_MS = 86_400_000;
113
+ /** Build an occurrence start string that mirrors the seed's kind/zone. */
114
+ function occurrenceStart(event, ymd) {
115
+ const s = event.start;
116
+ if (s.kind === 'date')
117
+ return ymd;
118
+ const time = s.value.slice(10); // 'THH:mm:ss' or 'THH:mm:ssZ'
119
+ return `${ymd}${time}`;
120
+ }
121
+ /**
122
+ * Expand a (possibly recurring) event into concrete occurrences inside [window.from,
123
+ * window.to] inclusive. A non-recurring event yields at most its single seed. An
124
+ * event whose recurrence is `unsupported` yields ONLY its seed (if in-window),
125
+ * flagged `isSeed`, never a fabricated series — the caller reads
126
+ * `event.recurrence.expansion` to label it "recurrence not fully expanded".
127
+ */
128
+ export function expandEvent(event, window) {
129
+ const seedYmd = datePart(event.start.value);
130
+ const fromMs = toUtcDate(window.from);
131
+ const toMs = toUtcDate(window.to);
132
+ const seedMs = toUtcDate(seedYmd);
133
+ const inWindow = (ms) => ms >= fromMs && ms <= toMs;
134
+ const seedOccurrence = () => inWindow(seedMs) ? [{ event, start: occurrenceStart(event, seedYmd), isSeed: true }] : [];
135
+ if (!event.recurrence || event.recurrence.expansion !== 'full') {
136
+ return seedOccurrence();
137
+ }
138
+ const { map } = parseRRule(event.recurrence.rule);
139
+ const freq = (map.get('FREQ') ?? '').toUpperCase();
140
+ const interval = parseInterval(map);
141
+ const count = map.has('COUNT') ? Number.parseInt(map.get('COUNT') ?? '', 10) : undefined;
142
+ const untilYmd = map.has('UNTIL') ? parseUntilDate(map.get('UNTIL') ?? '') : undefined;
143
+ const untilMs = untilYmd ? toUtcDate(untilYmd) : undefined;
144
+ const byDay = map.has('BYDAY')
145
+ ? new Set((map.get('BYDAY') ?? '').split(',').map((t) => WEEKDAY_TOKENS.indexOf(t.trim().toUpperCase())))
146
+ : undefined;
147
+ const out = [];
148
+ let emitted = 0; // series position (counts every occurrence, in or out of window)
149
+ // `series` yields candidate UTC-midnight ms values in ascending series order.
150
+ // Weekly BYDAY walks day-by-day emitting matching weekdays; every other supported
151
+ // FREQ steps period-by-period from the seed. Both share one COUNT/UNTIL/window filter.
152
+ const emit = (ms) => {
153
+ // Return false to STOP the whole series (COUNT reached, past UNTIL, past window end).
154
+ if (count !== undefined && emitted >= count)
155
+ return false;
156
+ if (untilMs !== undefined && ms > untilMs)
157
+ return false;
158
+ if (ms > toMs)
159
+ return false;
160
+ emitted += 1;
161
+ if (ms >= fromMs) {
162
+ const ymd = fromUtcDate(ms);
163
+ out.push({ event, start: occurrenceStart(event, ymd), isSeed: ms === seedMs });
164
+ }
165
+ return true;
166
+ };
167
+ if (freq === 'WEEKLY' && byDay) {
168
+ let cursor = seedMs;
169
+ for (let i = 0; i < MAX_OCCURRENCES * 8; i++) {
170
+ if (byDay.has(new Date(cursor).getUTCDay())) {
171
+ if (!emit(cursor))
172
+ break;
173
+ }
174
+ else if ((untilMs !== undefined && cursor > untilMs) || cursor > toMs) {
175
+ break; // no more matches possible past UNTIL / window end
176
+ }
177
+ cursor += DAY_MS;
178
+ if (cursor - seedMs > DAY_MS * 366 * 10)
179
+ break; // 10-year hard ceiling
180
+ }
181
+ return out;
182
+ }
183
+ if (freq === 'MONTHLY' || freq === 'YEARLY') {
184
+ stepMonthlyOrYearly(freq, interval, seedMs, toMs, untilMs, emit);
185
+ return out;
186
+ }
187
+ const step = freqStepper(freq, interval);
188
+ let cursor = seedMs;
189
+ for (let i = 0; i < MAX_OCCURRENCES; i++) {
190
+ if (!emit(cursor))
191
+ break;
192
+ cursor = step(cursor);
193
+ }
194
+ return out;
195
+ }
196
+ /** A stepper that advances a UTC-midnight ms cursor by one FREQ*INTERVAL period. DAILY/WEEKLY
197
+ * only — MONTHLY/YEARLY need the anchor-day-preserving/skip-invalid logic in
198
+ * stepMonthlyOrYearly below, since naive Date.UTC(y, m+1, day) arithmetic normalizes an
199
+ * out-of-range day (e.g. day 31 in a 30-day month) into the NEXT month, permanently
200
+ * shifting the rest of the series (the bug this file used to have). */
201
+ function freqStepper(freq, interval) {
202
+ switch (freq) {
203
+ case 'DAILY':
204
+ return (ms) => ms + DAY_MS * interval;
205
+ case 'WEEKLY':
206
+ return (ms) => ms + DAY_MS * 7 * interval;
207
+ default:
208
+ return (ms) => ms + DAY_MS;
209
+ }
210
+ }
211
+ /**
212
+ * MONTHLY/YEARLY stepping, RFC 5545-correct: every candidate is measured from the
213
+ * ORIGINAL anchor day-of-month (`seedDay`), never from a previously normalized/drifted
214
+ * cursor. A candidate (year, month, seedDay) that is not a real calendar date — Feb 31,
215
+ * Feb 29 in a non-leap year, Apr 31, etc. — does not exist and is SKIPPED entirely: it is
216
+ * never passed to `emit`, so it never consumes COUNT and never appears in the output.
217
+ *
218
+ * Citation (RFC 5545 §3.3.10, "Recurrence Rule"): "Recurrence rules may generate
219
+ * recurrence instances with an invalid date (e.g., February 30) or nonexistent local time
220
+ * ... Such recurrence instances MUST be ignored and MUST NOT count towards the recurrence
221
+ * count specified by the COUNT rule part." This function and its COUNT bookkeeping (via
222
+ * the caller's `emit`) implement that rule directly: e.g. FREQ=MONTHLY anchored on the
223
+ * 31st walks Jan 31 -> Mar 31 -> May 31 -> ... (Feb/Apr/Jun skipped, uncounted); FREQ=YEARLY
224
+ * anchored on Feb 29 walks leap year -> leap year only.
225
+ */
226
+ function stepMonthlyOrYearly(freq, interval, seedMs, toMs, untilMs, emit) {
227
+ const seed = new Date(seedMs);
228
+ const seedYear = seed.getUTCFullYear();
229
+ const seedMonth = seed.getUTCMonth();
230
+ const seedDay = seed.getUTCDate();
231
+ // Generous candidate ceiling (not an occurrence ceiling — MAX_OCCURRENCES already
232
+ // bounds emitted occurrences via `emit`/COUNT): covers the worst case of a monthly
233
+ // anchor that only lands ~7 times a year (a 31st) needing many more candidate
234
+ // months than emitted occurrences, and a yearly Feb-29 anchor needing up to 4x the
235
+ // candidate years.
236
+ const maxCandidates = freq === 'MONTHLY' ? MAX_OCCURRENCES * 12 : MAX_OCCURRENCES * 4;
237
+ for (let k = 0; k < maxCandidates; k++) {
238
+ const year = freq === 'MONTHLY'
239
+ ? Math.floor((seedYear * 12 + seedMonth + k * interval) / 12)
240
+ : seedYear + k * interval;
241
+ const month = freq === 'MONTHLY'
242
+ ? (((seedYear * 12 + seedMonth + k * interval) % 12) + 12) % 12
243
+ : seedMonth;
244
+ // Bail out once even the FIRST of this candidate month is past every hard stop —
245
+ // further k only move further forward, so nothing later can help either. This
246
+ // check runs before the validity check so a run of skipped (invalid) candidates
247
+ // near the end of the window does not burn through maxCandidates.
248
+ const monthStartMs = Date.UTC(year, month, 1);
249
+ if (monthStartMs > toMs && (untilMs === undefined || monthStartMs > untilMs))
250
+ return;
251
+ const candidateMs = Date.UTC(year, month, seedDay);
252
+ const candidate = new Date(candidateMs);
253
+ const isRealDate = candidate.getUTCFullYear() === year
254
+ && candidate.getUTCMonth() === month
255
+ && candidate.getUTCDate() === seedDay;
256
+ if (!isRealDate)
257
+ continue; // invalid calendar date — skipped, does not consume COUNT
258
+ if (!emit(candidateMs))
259
+ return;
260
+ }
261
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * subscription-store.ts — named external-calendar feed subscriptions with honest,
3
+ * per-subscription status. The engine that agent-side `/calendar subscribe` and the
4
+ * connect wizard drive.
5
+ *
6
+ * Reaches the network ONLY through an injected `FeedFetcher`, and reads time ONLY
7
+ * through an injected `Clock`. That is the whole IO boundary: tests supply fake
8
+ * feeds and a fake clock, so no test ever touches a real URL, and refresh/staleness
9
+ * timing is deterministic. Persistence is the CALLER's job (the agent stores the
10
+ * feed URL via its secret manager and the rest of the metadata in its config);
11
+ * `snapshot()` / `restore()` move that metadata across restarts, events re-fetched.
12
+ *
13
+ * UX shape (per Mike's least-friction rule): `add({ url })` is paste-URL-and-done —
14
+ * it validates by fetching, auto-derives the subscription name from the feed's
15
+ * X-WR-CALNAME (falling back to the URL host) when the caller gives no name, and
16
+ * applies a sensible default refresh interval with no mandatory knobs. Every status
17
+ * it reports is honest: stale carries its age, unreachable/parse-error carry the
18
+ * stage and detail.
19
+ *
20
+ * PURE of ambient IO — no direct fs/network/process; all IO is injected.
21
+ */
22
+ import type { CalendarEvent, CalendarSubscription, Clock, FeedFetcher, RefreshReport, SubscriptionSnapshot } from './types.js';
23
+ /** Sensible refresh cadence for a read-only feed that rarely changes minute-to-minute. */
24
+ export declare const DEFAULT_REFRESH_INTERVAL_MS: number;
25
+ /** Never hammer a feed faster than this, even if a caller asks. */
26
+ export declare const MIN_REFRESH_INTERVAL_MS: number;
27
+ /** A feed refreshed less often than this is capped up to here. */
28
+ export declare const MAX_REFRESH_INTERVAL_MS: number;
29
+ export interface SubscriptionStoreOptions {
30
+ readonly fetcher: FeedFetcher;
31
+ readonly clock?: Clock;
32
+ readonly defaultRefreshIntervalMs?: number;
33
+ }
34
+ export interface AddSubscriptionInput {
35
+ readonly url: string;
36
+ /** Optional; when omitted, derived from the feed's X-WR-CALNAME or the URL host. */
37
+ readonly name?: string;
38
+ readonly refreshIntervalMs?: number;
39
+ }
40
+ /** The outcome of validate-by-fetch — what the wizard shows before saving. */
41
+ export type ValidationResult = {
42
+ readonly ok: true;
43
+ readonly calendarName?: string;
44
+ readonly eventCount: number;
45
+ readonly derivedName: string;
46
+ } | {
47
+ readonly ok: false;
48
+ /** Which stage failed, so failure wording can name it honestly. */
49
+ readonly stage: 'fetch' | 'parse';
50
+ readonly detail: string;
51
+ };
52
+ export type AddResult = {
53
+ readonly ok: true;
54
+ readonly subscription: CalendarSubscription;
55
+ readonly report: RefreshReport;
56
+ } | {
57
+ readonly ok: false;
58
+ readonly stage: 'fetch' | 'parse' | 'duplicate';
59
+ readonly detail: string;
60
+ };
61
+ /**
62
+ * Mask a feed URL for display. Google/Outlook "secret address" URLs grant read
63
+ * access, so a subscription's URL is secrets-adjacent — surfaces should show this,
64
+ * never the raw URL. Keeps the scheme+host and the last few chars, masks the middle.
65
+ */
66
+ export declare function maskFeedUrl(url: string): string;
67
+ export declare class SubscriptionStore {
68
+ private readonly fetcher;
69
+ private readonly clock;
70
+ private readonly defaultInterval;
71
+ private readonly records;
72
+ constructor(options: SubscriptionStoreOptions);
73
+ /**
74
+ * Fetch a feed WITHOUT saving it and report whether it is a usable calendar. Drives
75
+ * the wizard's validate-before-save step and the derived-name preview.
76
+ */
77
+ validateByFetch(url: string, requestedName?: string): Promise<ValidationResult>;
78
+ /**
79
+ * Paste-URL-and-done: validate by fetching, and on success register the subscription
80
+ * with an auto-derived name and default cadence, storing its events. Refuses (without
81
+ * saving) on a fetch/parse failure or a duplicate name, always with an honest reason.
82
+ */
83
+ add(input: AddSubscriptionInput): Promise<AddResult>;
84
+ /** Remove a subscription and drop its cached events. Returns whether it existed. */
85
+ remove(name: string): boolean;
86
+ list(): CalendarSubscription[];
87
+ get(name: string): CalendarSubscription | undefined;
88
+ has(name: string): boolean;
89
+ /** Events from the most recent successful parse of the named subscription. */
90
+ events(name: string): readonly CalendarEvent[];
91
+ /** All subscriptions' events, each tagged with its source subscription name. */
92
+ allEvents(): {
93
+ readonly name: string;
94
+ readonly events: readonly CalendarEvent[];
95
+ }[];
96
+ /**
97
+ * Refresh one subscription. Skips the network when not due unless `force`. Sends
98
+ * conditional-fetch validators (etag/last-modified) so an unchanged feed comes back
99
+ * 304 and keeps its events. Updates honest status either way.
100
+ */
101
+ refresh(name: string, opts?: {
102
+ force?: boolean;
103
+ }): Promise<RefreshReport>;
104
+ /** Apply a fetch result to a record, mutating status/events and returning the honest report. */
105
+ private applyFetch;
106
+ /** Refresh every subscription that is due (or never fetched). Used on boot + on demand. */
107
+ refreshDue(opts?: {
108
+ force?: boolean;
109
+ }): Promise<RefreshReport[]>;
110
+ /** Metadata snapshot for persistence; events are intentionally excluded (re-fetched on boot). */
111
+ snapshot(): SubscriptionSnapshot[];
112
+ /** Restore subscription metadata (from `snapshot()`); call `refreshDue()` afterward to load events. */
113
+ restore(snapshots: readonly SubscriptionSnapshot[]): void;
114
+ private deriveName;
115
+ private healthOf;
116
+ private toPublic;
117
+ }
118
+ //# sourceMappingURL=subscription-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription-store.d.ts","sourceRoot":"","sources":["../../../src/platform/calendar/subscription-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EACV,aAAa,EACb,oBAAoB,EACpB,KAAK,EACL,WAAW,EAEX,aAAa,EAEb,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAEpB,0FAA0F;AAC1F,eAAO,MAAM,2BAA2B,QAAiB,CAAC;AAC1D,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,QAAiB,CAAC;AACtD,kEAAkE;AAClE,eAAO,MAAM,uBAAuB,QAAsB,CAAC;AAK3D,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,wBAAwB,CAAC,EAAE,MAAM,CAAC;CAC5C;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,oFAAoF;IACpF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,8EAA8E;AAC9E,MAAM,MAAM,gBAAgB,GACxB;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AAEN,MAAM,MAAM,SAAS,GACjB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GAClG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAmCrG;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAS/C;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyC;gBAErD,OAAO,EAAE,wBAAwB;IAM7C;;;OAGG;IACG,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAsBrF;;;;OAIG;IACG,GAAG,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,SAAS,CAAC;IA8B1D,oFAAoF;IACpF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI7B,IAAI,IAAI,oBAAoB,EAAE;IAI9B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS;IAKnD,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,8EAA8E;IAC9E,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,aAAa,EAAE;IAI9C,gFAAgF;IAChF,SAAS,IAAI;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAA;KAAE,EAAE;IAInF;;;;OAIG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,aAAa,CAAC;IAkBnF,gGAAgG;IAChG,OAAO,CAAC,UAAU;IAkClB,2FAA2F;IACrF,UAAU,CAAC,IAAI,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAQ1E,iGAAiG;IACjG,QAAQ,IAAI,oBAAoB,EAAE;IAYlC,uGAAuG;IACvG,OAAO,CAAC,SAAS,EAAE,SAAS,oBAAoB,EAAE,GAAG,IAAI;IAgBzD,OAAO,CAAC,UAAU;IAYlB,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,QAAQ;CAqBjB"}
@@ -0,0 +1,293 @@
1
+ /**
2
+ * subscription-store.ts — named external-calendar feed subscriptions with honest,
3
+ * per-subscription status. The engine that agent-side `/calendar subscribe` and the
4
+ * connect wizard drive.
5
+ *
6
+ * Reaches the network ONLY through an injected `FeedFetcher`, and reads time ONLY
7
+ * through an injected `Clock`. That is the whole IO boundary: tests supply fake
8
+ * feeds and a fake clock, so no test ever touches a real URL, and refresh/staleness
9
+ * timing is deterministic. Persistence is the CALLER's job (the agent stores the
10
+ * feed URL via its secret manager and the rest of the metadata in its config);
11
+ * `snapshot()` / `restore()` move that metadata across restarts, events re-fetched.
12
+ *
13
+ * UX shape (per Mike's least-friction rule): `add({ url })` is paste-URL-and-done —
14
+ * it validates by fetching, auto-derives the subscription name from the feed's
15
+ * X-WR-CALNAME (falling back to the URL host) when the caller gives no name, and
16
+ * applies a sensible default refresh interval with no mandatory knobs. Every status
17
+ * it reports is honest: stale carries its age, unreachable/parse-error carry the
18
+ * stage and detail.
19
+ *
20
+ * PURE of ambient IO — no direct fs/network/process; all IO is injected.
21
+ */
22
+ import { parseIcs } from './ics-parser.js';
23
+ /** Sensible refresh cadence for a read-only feed that rarely changes minute-to-minute. */
24
+ export const DEFAULT_REFRESH_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
25
+ /** Never hammer a feed faster than this, even if a caller asks. */
26
+ export const MIN_REFRESH_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
27
+ /** A feed refreshed less often than this is capped up to here. */
28
+ export const MAX_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
29
+ /** Data is considered stale once it is older than this multiple of the interval. */
30
+ const STALE_MULTIPLE = 2;
31
+ /** Clamp a requested refresh interval into the safe [MIN, MAX] band. */
32
+ function clampInterval(requested, fallback) {
33
+ const v = requested ?? fallback;
34
+ if (!Number.isFinite(v) || v <= 0)
35
+ return fallback;
36
+ return Math.max(MIN_REFRESH_INTERVAL_MS, Math.min(MAX_REFRESH_INTERVAL_MS, v));
37
+ }
38
+ /** Best-effort host for name/derivation fallback; never throws. */
39
+ function urlHost(url) {
40
+ try {
41
+ return new URL(url).host || url;
42
+ }
43
+ catch {
44
+ return url;
45
+ }
46
+ }
47
+ /**
48
+ * Mask a feed URL for display. Google/Outlook "secret address" URLs grant read
49
+ * access, so a subscription's URL is secrets-adjacent — surfaces should show this,
50
+ * never the raw URL. Keeps the scheme+host and the last few chars, masks the middle.
51
+ */
52
+ export function maskFeedUrl(url) {
53
+ try {
54
+ const u = new URL(url);
55
+ const tail = u.pathname.length > 6 ? u.pathname.slice(-6) : u.pathname;
56
+ return `${u.protocol}//${u.host}/…${tail}`;
57
+ }
58
+ catch {
59
+ if (url.length <= 10)
60
+ return '…';
61
+ return `${url.slice(0, 4)}…${url.slice(-4)}`;
62
+ }
63
+ }
64
+ export class SubscriptionStore {
65
+ fetcher;
66
+ clock;
67
+ defaultInterval;
68
+ records = new Map();
69
+ constructor(options) {
70
+ this.fetcher = options.fetcher;
71
+ this.clock = options.clock ?? (() => Date.now());
72
+ this.defaultInterval = clampInterval(options.defaultRefreshIntervalMs, DEFAULT_REFRESH_INTERVAL_MS);
73
+ }
74
+ /**
75
+ * Fetch a feed WITHOUT saving it and report whether it is a usable calendar. Drives
76
+ * the wizard's validate-before-save step and the derived-name preview.
77
+ */
78
+ async validateByFetch(url, requestedName) {
79
+ const res = await this.fetcher({ url });
80
+ if (res.kind === 'error') {
81
+ return { ok: false, stage: 'fetch', detail: res.status ? `HTTP ${res.status}: ${res.message}` : res.message };
82
+ }
83
+ if (res.kind === 'not-modified') {
84
+ // A cold validate should never see 304 (no validators sent); treat as fetch failure.
85
+ return { ok: false, stage: 'fetch', detail: 'Server returned 304 Not Modified to an unconditional request — no calendar body to validate.' };
86
+ }
87
+ const parsed = parseIcs(res.body);
88
+ if (parsed.events.length === 0 && parsed.skipped.length === 0 && !parsed.calendarName) {
89
+ return { ok: false, stage: 'parse', detail: 'Fetched, but no VEVENTs or calendar name were found — this does not look like an iCalendar feed.' };
90
+ }
91
+ const derivedName = this.deriveName(requestedName, parsed.calendarName, url);
92
+ return {
93
+ ok: true,
94
+ ...(parsed.calendarName !== undefined ? { calendarName: parsed.calendarName } : {}),
95
+ eventCount: parsed.events.length,
96
+ derivedName,
97
+ };
98
+ }
99
+ /**
100
+ * Paste-URL-and-done: validate by fetching, and on success register the subscription
101
+ * with an auto-derived name and default cadence, storing its events. Refuses (without
102
+ * saving) on a fetch/parse failure or a duplicate name, always with an honest reason.
103
+ */
104
+ async add(input) {
105
+ // Single unconditional fetch — no separate validate round trip. We fetch once,
106
+ // derive the name from the body, then apply that SAME result into the record.
107
+ const now = this.clock();
108
+ const res = await this.fetcher({ url: input.url });
109
+ if (res.kind === 'error') {
110
+ return { ok: false, stage: 'fetch', detail: res.status ? `HTTP ${res.status}: ${res.message}` : res.message };
111
+ }
112
+ if (res.kind === 'not-modified') {
113
+ return { ok: false, stage: 'fetch', detail: 'Server returned 304 Not Modified to an unconditional request — no calendar body to add.' };
114
+ }
115
+ const parsed = parseIcs(res.body);
116
+ if (parsed.events.length === 0 && parsed.skipped.length === 0 && !parsed.calendarName) {
117
+ return { ok: false, stage: 'parse', detail: 'Fetched, but no VEVENTs or calendar name were found — this does not look like an iCalendar feed.' };
118
+ }
119
+ const name = this.deriveName(input.name, parsed.calendarName, input.url);
120
+ const record = {
121
+ name,
122
+ url: input.url,
123
+ refreshIntervalMs: clampInterval(input.refreshIntervalMs, this.defaultInterval),
124
+ lastResult: undefined,
125
+ events: [],
126
+ };
127
+ this.records.set(name, record);
128
+ record.lastFetchedAt = now;
129
+ const report = this.applyFetch(record, res, now);
130
+ return { ok: true, subscription: this.toPublic(record), report };
131
+ }
132
+ /** Remove a subscription and drop its cached events. Returns whether it existed. */
133
+ remove(name) {
134
+ return this.records.delete(name);
135
+ }
136
+ list() {
137
+ return [...this.records.values()].map((r) => this.toPublic(r));
138
+ }
139
+ get(name) {
140
+ const r = this.records.get(name);
141
+ return r ? this.toPublic(r) : undefined;
142
+ }
143
+ has(name) {
144
+ return this.records.has(name);
145
+ }
146
+ /** Events from the most recent successful parse of the named subscription. */
147
+ events(name) {
148
+ return this.records.get(name)?.events ?? [];
149
+ }
150
+ /** All subscriptions' events, each tagged with its source subscription name. */
151
+ allEvents() {
152
+ return [...this.records.values()].map((r) => ({ name: r.name, events: r.events }));
153
+ }
154
+ /**
155
+ * Refresh one subscription. Skips the network when not due unless `force`. Sends
156
+ * conditional-fetch validators (etag/last-modified) so an unchanged feed comes back
157
+ * 304 and keeps its events. Updates honest status either way.
158
+ */
159
+ async refresh(name, opts = {}) {
160
+ const r = this.records.get(name);
161
+ if (!r)
162
+ return { name, outcome: 'skipped', health: 'never-fetched', detail: `No subscription named '${name}'.` };
163
+ const now = this.clock();
164
+ if (!opts.force && r.lastFetchedAt !== undefined && now - r.lastFetchedAt < r.refreshIntervalMs) {
165
+ return { name, outcome: 'skipped', health: this.healthOf(r, now), ...(r.eventCount !== undefined ? { eventCount: r.eventCount } : {}), detail: 'Not due for refresh yet.' };
166
+ }
167
+ r.lastFetchedAt = now;
168
+ const res = await this.fetcher({
169
+ url: r.url,
170
+ ...(r.etag !== undefined ? { etag: r.etag } : {}),
171
+ ...(r.lastModified !== undefined ? { lastModified: r.lastModified } : {}),
172
+ });
173
+ return this.applyFetch(r, res, now);
174
+ }
175
+ /** Apply a fetch result to a record, mutating status/events and returning the honest report. */
176
+ applyFetch(r, res, now) {
177
+ const name = r.name;
178
+ if (res.kind === 'error') {
179
+ r.lastResult = 'unreachable';
180
+ r.lastDetail = res.status ? `HTTP ${res.status}: ${res.message}` : res.message;
181
+ return { name, outcome: 'unreachable', health: this.healthOf(r, now), ...(r.eventCount !== undefined ? { eventCount: r.eventCount } : {}), detail: r.lastDetail };
182
+ }
183
+ if (res.kind === 'not-modified') {
184
+ r.lastResult = 'not-modified';
185
+ r.lastSucceededAt = now;
186
+ if (res.etag !== undefined)
187
+ r.etag = res.etag;
188
+ if (res.lastModified !== undefined)
189
+ r.lastModified = res.lastModified;
190
+ r.lastDetail = undefined;
191
+ return { name, outcome: 'not-modified', health: this.healthOf(r, now), ...(r.eventCount !== undefined ? { eventCount: r.eventCount } : {}) };
192
+ }
193
+ const parsed = parseIcs(res.body);
194
+ if (parsed.events.length === 0 && parsed.skipped.length > 0) {
195
+ r.lastResult = 'parse-error';
196
+ r.lastDetail = `Fetched, but every VEVENT was unusable: ${parsed.skipped[0]?.message ?? 'no usable events'}`;
197
+ return { name, outcome: 'parse-error', health: this.healthOf(r, now), detail: r.lastDetail };
198
+ }
199
+ r.events = [...parsed.events];
200
+ r.eventCount = parsed.events.length;
201
+ r.lastResult = 'ok';
202
+ r.lastSucceededAt = now;
203
+ r.lastDetail = parsed.diagnostics.length > 0 ? `${parsed.diagnostics.length} note(s): ${parsed.diagnostics[0]?.message ?? ''}` : undefined;
204
+ if (res.etag !== undefined)
205
+ r.etag = res.etag;
206
+ if (res.lastModified !== undefined)
207
+ r.lastModified = res.lastModified;
208
+ return { name, outcome: 'updated', health: 'ok', eventCount: r.eventCount, ...(r.lastDetail !== undefined ? { detail: r.lastDetail } : {}) };
209
+ }
210
+ /** Refresh every subscription that is due (or never fetched). Used on boot + on demand. */
211
+ async refreshDue(opts = {}) {
212
+ const reports = [];
213
+ for (const name of this.records.keys()) {
214
+ reports.push(await this.refresh(name, opts));
215
+ }
216
+ return reports;
217
+ }
218
+ /** Metadata snapshot for persistence; events are intentionally excluded (re-fetched on boot). */
219
+ snapshot() {
220
+ return [...this.records.values()].map((r) => ({
221
+ name: r.name,
222
+ url: r.url,
223
+ refreshIntervalMs: r.refreshIntervalMs,
224
+ ...(r.lastFetchedAt !== undefined ? { lastFetchedAt: r.lastFetchedAt } : {}),
225
+ ...(r.lastSucceededAt !== undefined ? { lastSucceededAt: r.lastSucceededAt } : {}),
226
+ ...(r.etag !== undefined ? { etag: r.etag } : {}),
227
+ ...(r.lastModified !== undefined ? { lastModified: r.lastModified } : {}),
228
+ }));
229
+ }
230
+ /** Restore subscription metadata (from `snapshot()`); call `refreshDue()` afterward to load events. */
231
+ restore(snapshots) {
232
+ for (const s of snapshots) {
233
+ this.records.set(s.name, {
234
+ name: s.name,
235
+ url: s.url,
236
+ refreshIntervalMs: clampInterval(s.refreshIntervalMs, this.defaultInterval),
237
+ ...(s.lastFetchedAt !== undefined ? { lastFetchedAt: s.lastFetchedAt } : {}),
238
+ ...(s.lastSucceededAt !== undefined ? { lastSucceededAt: s.lastSucceededAt } : {}),
239
+ ...(s.etag !== undefined ? { etag: s.etag } : {}),
240
+ ...(s.lastModified !== undefined ? { lastModified: s.lastModified } : {}),
241
+ lastResult: undefined,
242
+ events: [],
243
+ });
244
+ }
245
+ }
246
+ deriveName(requested, calendarName, url) {
247
+ const candidate = (requested?.trim() || calendarName?.trim() || urlHost(url)).replace(/\s+/g, ' ').trim();
248
+ let name = candidate || urlHost(url);
249
+ // De-duplicate against existing names so an unnamed second Google feed still lands.
250
+ if (this.records.has(name)) {
251
+ let n = 2;
252
+ while (this.records.has(`${name} (${n})`))
253
+ n++;
254
+ name = `${name} (${n})`;
255
+ }
256
+ return name;
257
+ }
258
+ healthOf(r, now) {
259
+ if (r.lastResult === undefined)
260
+ return 'never-fetched';
261
+ if (r.lastResult === 'unreachable')
262
+ return 'unreachable';
263
+ if (r.lastResult === 'parse-error')
264
+ return 'parse-error';
265
+ if (r.lastSucceededAt === undefined)
266
+ return 'never-fetched';
267
+ const age = now - r.lastSucceededAt;
268
+ if (age > r.refreshIntervalMs * STALE_MULTIPLE)
269
+ return 'stale';
270
+ return 'ok';
271
+ }
272
+ toPublic(r) {
273
+ const now = this.clock();
274
+ const health = this.healthOf(r, now);
275
+ let detail = r.lastDetail;
276
+ if (health === 'stale' && r.lastSucceededAt !== undefined) {
277
+ const ageMin = Math.round((now - r.lastSucceededAt) / 60000);
278
+ detail = `Last updated ${ageMin} min ago (past the ${Math.round(r.refreshIntervalMs / 60000)} min refresh window).`;
279
+ }
280
+ return {
281
+ name: r.name,
282
+ url: r.url,
283
+ refreshIntervalMs: r.refreshIntervalMs,
284
+ ...(r.lastFetchedAt !== undefined ? { lastFetchedAt: r.lastFetchedAt } : {}),
285
+ ...(r.lastSucceededAt !== undefined ? { lastSucceededAt: r.lastSucceededAt } : {}),
286
+ ...(r.etag !== undefined ? { etag: r.etag } : {}),
287
+ ...(r.lastModified !== undefined ? { lastModified: r.lastModified } : {}),
288
+ health,
289
+ ...(detail !== undefined ? { detail } : {}),
290
+ ...(r.eventCount !== undefined ? { eventCount: r.eventCount } : {}),
291
+ };
292
+ }
293
+ }