@murphai/murph 1.3.1 → 1.3.2

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 (779) hide show
  1. package/CHANGELOG.md +808 -0
  2. package/config.schema.json +91 -40
  3. package/dist/bin.js +6 -4
  4. package/dist/bin.js.map +1 -1
  5. package/dist/cli-entry.d.ts +8 -1
  6. package/dist/cli-entry.d.ts.map +1 -1
  7. package/dist/cli-entry.js +79 -3
  8. package/dist/cli-entry.js.map +1 -1
  9. package/dist/commands/audit-command-helpers.d.ts.map +1 -1
  10. package/dist/commands/audit-command-helpers.js +7 -5
  11. package/dist/commands/audit-command-helpers.js.map +1 -1
  12. package/dist/commands/batch.d.ts +5 -5
  13. package/dist/commands/batch.d.ts.map +1 -1
  14. package/dist/commands/batch.js +2 -21
  15. package/dist/commands/batch.js.map +1 -1
  16. package/dist/commands/commons.d.ts.map +1 -1
  17. package/dist/commands/commons.js +2 -4
  18. package/dist/commands/commons.js.map +1 -1
  19. package/dist/commands/exercise.d.ts.map +1 -1
  20. package/dist/commands/exercise.js +16 -3
  21. package/dist/commands/exercise.js.map +1 -1
  22. package/dist/commands/experiment-progress-card-image.d.ts.map +1 -1
  23. package/dist/commands/experiment-progress-card-image.js +63 -12
  24. package/dist/commands/experiment-progress-card-image.js.map +1 -1
  25. package/dist/commands/experiment.d.ts.map +1 -1
  26. package/dist/commands/experiment.js +45 -5
  27. package/dist/commands/experiment.js.map +1 -1
  28. package/dist/commands/export-intake-read-helpers.d.ts.map +1 -1
  29. package/dist/commands/export-intake-read-helpers.js +2 -3
  30. package/dist/commands/export-intake-read-helpers.js.map +1 -1
  31. package/dist/commands/food.d.ts.map +1 -1
  32. package/dist/commands/food.js +1 -7
  33. package/dist/commands/food.js.map +1 -1
  34. package/dist/commands/habitat.d.ts.map +1 -1
  35. package/dist/commands/habitat.js +120 -9
  36. package/dist/commands/habitat.js.map +1 -1
  37. package/dist/commands/health-blood-test-save.d.ts.map +1 -1
  38. package/dist/commands/health-blood-test-save.js +42 -8
  39. package/dist/commands/health-blood-test-save.js.map +1 -1
  40. package/dist/commands/meal.d.ts.map +1 -1
  41. package/dist/commands/meal.js +4 -1
  42. package/dist/commands/meal.js.map +1 -1
  43. package/dist/commands/measurement.d.ts.map +1 -1
  44. package/dist/commands/measurement.js +3 -0
  45. package/dist/commands/measurement.js.map +1 -1
  46. package/dist/commands/murph-age.d.ts +9 -1
  47. package/dist/commands/murph-age.d.ts.map +1 -1
  48. package/dist/commands/murph-age.js +132 -7
  49. package/dist/commands/murph-age.js.map +1 -1
  50. package/dist/commands/public-validation-issue.d.ts +7 -0
  51. package/dist/commands/public-validation-issue.d.ts.map +1 -0
  52. package/dist/commands/public-validation-issue.js +7 -0
  53. package/dist/commands/public-validation-issue.js.map +1 -0
  54. package/dist/commands/scheduled-log.d.ts +12 -0
  55. package/dist/commands/scheduled-log.d.ts.map +1 -1
  56. package/dist/commands/scheduled-log.js +456 -106
  57. package/dist/commands/scheduled-log.js.map +1 -1
  58. package/dist/commands/wearables.d.ts +286 -0
  59. package/dist/commands/wearables.d.ts.map +1 -1
  60. package/dist/commands/wearables.js +33 -1
  61. package/dist/commands/wearables.js.map +1 -1
  62. package/dist/commands/workout-live.d.ts.map +1 -1
  63. package/dist/commands/workout-live.js +123 -42
  64. package/dist/commands/workout-live.js.map +1 -1
  65. package/dist/commands/workout-typed-options.d.ts +38 -0
  66. package/dist/commands/workout-typed-options.d.ts.map +1 -0
  67. package/dist/commands/workout-typed-options.js +90 -0
  68. package/dist/commands/workout-typed-options.js.map +1 -0
  69. package/dist/commands/workout.d.ts.map +1 -1
  70. package/dist/commands/workout.js +80 -73
  71. package/dist/commands/workout.js.map +1 -1
  72. package/dist/hosted-data-api-labels.d.ts.map +1 -1
  73. package/dist/hosted-data-api-labels.js +161 -30
  74. package/dist/hosted-data-api-labels.js.map +1 -1
  75. package/dist/incur-error-bridge.d.ts.map +1 -1
  76. package/dist/incur-error-bridge.js +7 -15
  77. package/dist/incur-error-bridge.js.map +1 -1
  78. package/dist/incur.generated.d.ts +22 -13
  79. package/dist/incur.generated.d.ts.map +1 -1
  80. package/dist/mapbox-address.d.ts.map +1 -1
  81. package/dist/mapbox-address.js +3 -4
  82. package/dist/mapbox-address.js.map +1 -1
  83. package/dist/mapbox-route-client.d.ts +2 -2
  84. package/dist/mapbox-route-client.d.ts.map +1 -1
  85. package/dist/mapbox-route-client.js +49 -12
  86. package/dist/mapbox-route-client.js.map +1 -1
  87. package/dist/mapbox-route-directions.d.ts.map +1 -1
  88. package/dist/mapbox-route-directions.js +0 -1
  89. package/dist/mapbox-route-directions.js.map +1 -1
  90. package/dist/mapbox-route-elevation.js +0 -1
  91. package/dist/mapbox-route-elevation.js.map +1 -1
  92. package/dist/mapbox-route-points.js +0 -2
  93. package/dist/mapbox-route-points.js.map +1 -1
  94. package/dist/mapbox-route.d.ts.map +1 -1
  95. package/dist/mapbox-route.js +1 -1
  96. package/dist/mapbox-route.js.map +1 -1
  97. package/dist/vault-cli-command-manifest.d.ts +304 -6
  98. package/dist/vault-cli-command-manifest.d.ts.map +1 -1
  99. package/dist/vault-cli-skill-hash.generated.d.ts +1 -1
  100. package/dist/vault-cli-skill-hash.generated.js +1 -1
  101. package/node_modules/@murphai/assistant-cli/dist/assistant/ui/ink.d.ts.map +1 -1
  102. package/node_modules/@murphai/assistant-cli/dist/assistant/ui/ink.js +6 -2
  103. package/node_modules/@murphai/assistant-cli/dist/assistant/ui/ink.js.map +1 -1
  104. package/node_modules/@murphai/assistant-engine/README.md +4 -3
  105. package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.d.ts +15 -1
  106. package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.d.ts.map +1 -1
  107. package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.js +121 -24
  108. package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-controller.js.map +1 -1
  109. package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-journal.js +2 -2
  110. package/node_modules/@murphai/assistant-engine/dist/assistant/active-turn-input-journal.js.map +1 -1
  111. package/node_modules/@murphai/assistant-engine/dist/assistant/ask-continuation.d.ts.map +1 -1
  112. package/node_modules/@murphai/assistant-engine/dist/assistant/ask-continuation.js +1 -0
  113. package/node_modules/@murphai/assistant-engine/dist/assistant/ask-continuation.js.map +1 -1
  114. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/reply.d.ts.map +1 -1
  115. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/reply.js +55 -7
  116. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/reply.js.map +1 -1
  117. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/run-loop.d.ts.map +1 -1
  118. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/run-loop.js +9 -1
  119. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/run-loop.js.map +1 -1
  120. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/shared.d.ts +1 -0
  121. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/shared.d.ts.map +1 -1
  122. package/node_modules/@murphai/assistant-engine/dist/assistant/automation/shared.js.map +1 -1
  123. package/node_modules/@murphai/assistant-engine/dist/assistant/cli-surface-contract.generated.json +1 -1
  124. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.d.ts +1 -1
  125. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.d.ts.map +1 -1
  126. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.js +7 -7
  127. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-base-instructions.js.map +1 -1
  128. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.d.ts +1 -1
  129. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.d.ts.map +1 -1
  130. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.js +123 -22
  131. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn/planning.js.map +1 -1
  132. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn-runner.d.ts.map +1 -1
  133. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn-runner.js +48 -19
  134. package/node_modules/@murphai/assistant-engine/dist/assistant/codex-turn-runner.js.map +1 -1
  135. package/node_modules/@murphai/assistant-engine/dist/assistant/context-snapshot.js +3 -7
  136. package/node_modules/@murphai/assistant-engine/dist/assistant/context-snapshot.js.map +1 -1
  137. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.d.ts +5 -0
  138. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.d.ts.map +1 -1
  139. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.js +24 -10
  140. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/execution.js.map +1 -1
  141. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.d.ts +2 -0
  142. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.d.ts.map +1 -1
  143. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.js +12 -0
  144. package/node_modules/@murphai/assistant-engine/dist/assistant/cron/schedule.js.map +1 -1
  145. package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.d.ts +5 -0
  146. package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.d.ts.map +1 -1
  147. package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.js +9 -10
  148. package/node_modules/@murphai/assistant-engine/dist/assistant/delivery-service.js.map +1 -1
  149. package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.d.ts +41 -9
  150. package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.d.ts.map +1 -1
  151. package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.js +9 -0
  152. package/node_modules/@murphai/assistant-engine/dist/assistant/execution-context.js.map +1 -1
  153. package/node_modules/@murphai/assistant-engine/dist/assistant/experiment-support-automations.d.ts.map +1 -1
  154. package/node_modules/@murphai/assistant-engine/dist/assistant/experiment-support-automations.js +2 -0
  155. package/node_modules/@murphai/assistant-engine/dist/assistant/experiment-support-automations.js.map +1 -1
  156. package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.d.ts +3 -0
  157. package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.d.ts.map +1 -1
  158. package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.js +8 -0
  159. package/node_modules/@murphai/assistant-engine/dist/assistant/hosted-tool-context.js.map +1 -1
  160. package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.d.ts +3 -0
  161. package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.d.ts.map +1 -1
  162. package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.js +26 -1
  163. package/node_modules/@murphai/assistant-engine/dist/assistant/issue-reporting.js.map +1 -1
  164. package/node_modules/@murphai/assistant-engine/dist/assistant/local-service.d.ts.map +1 -1
  165. package/node_modules/@murphai/assistant-engine/dist/assistant/local-service.js +687 -370
  166. package/node_modules/@murphai/assistant-engine/dist/assistant/local-service.js.map +1 -1
  167. package/node_modules/@murphai/assistant-engine/dist/assistant/managed-automations.d.ts.map +1 -1
  168. package/node_modules/@murphai/assistant-engine/dist/assistant/managed-automations.js +54 -18
  169. package/node_modules/@murphai/assistant-engine/dist/assistant/managed-automations.js.map +1 -1
  170. package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.d.ts +2 -3
  171. package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.d.ts.map +1 -1
  172. package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.js +10 -9
  173. package/node_modules/@murphai/assistant-engine/dist/assistant/model-behavior.js.map +1 -1
  174. package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.d.ts +1 -1
  175. package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.d.ts.map +1 -1
  176. package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.js +146 -18
  177. package/node_modules/@murphai/assistant-engine/dist/assistant/notification-turn.js.map +1 -1
  178. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.d.ts +25 -0
  179. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.d.ts.map +1 -0
  180. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.js +72 -0
  181. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-followup-seed.js.map +1 -0
  182. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.d.ts +4 -0
  183. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.d.ts.map +1 -1
  184. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.js +14 -0
  185. package/node_modules/@murphai/assistant-engine/dist/assistant/onboarding-state.js.map +1 -1
  186. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.d.ts +1 -1
  187. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.d.ts.map +1 -1
  188. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.js +8 -5
  189. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox/store.js.map +1 -1
  190. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.d.ts +4 -0
  191. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.d.ts.map +1 -1
  192. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.js +8 -0
  193. package/node_modules/@murphai/assistant-engine/dist/assistant/outbox.js.map +1 -1
  194. package/node_modules/@murphai/assistant-engine/dist/assistant/private-completion-continuity.js +1 -0
  195. package/node_modules/@murphai/assistant-engine/dist/assistant/private-completion-continuity.js.map +1 -1
  196. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/codex-cli.d.ts.map +1 -1
  197. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/codex-cli.js +25 -2
  198. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/codex-cli.js.map +1 -1
  199. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.d.ts +11 -3
  200. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.d.ts.map +1 -1
  201. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.js +67 -176
  202. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/helpers.js.map +1 -1
  203. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/types.d.ts +17 -0
  204. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/types.d.ts.map +1 -1
  205. package/node_modules/@murphai/assistant-engine/dist/assistant/providers/types.js.map +1 -1
  206. package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.d.ts +1 -1
  207. package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.d.ts.map +1 -1
  208. package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.js +10 -0
  209. package/node_modules/@murphai/assistant-engine/dist/assistant/reply-delivery-context.js.map +1 -1
  210. package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.d.ts +19 -4
  211. package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.d.ts.map +1 -1
  212. package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.js +76 -40
  213. package/node_modules/@murphai/assistant-engine/dist/assistant/runtime-residue.js.map +1 -1
  214. package/node_modules/@murphai/assistant-engine/dist/assistant/service-contracts.d.ts +3 -1
  215. package/node_modules/@murphai/assistant-engine/dist/assistant/service-contracts.d.ts.map +1 -1
  216. package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.d.ts +4 -2
  217. package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.d.ts.map +1 -1
  218. package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.js +8 -7
  219. package/node_modules/@murphai/assistant-engine/dist/assistant/service-usage.js.map +1 -1
  220. package/node_modules/@murphai/assistant-engine/dist/assistant/shared.d.ts +1 -1
  221. package/node_modules/@murphai/assistant-engine/dist/assistant/shared.d.ts.map +1 -1
  222. package/node_modules/@murphai/assistant-engine/dist/assistant/shared.js +1 -1
  223. package/node_modules/@murphai/assistant-engine/dist/assistant/shared.js.map +1 -1
  224. package/node_modules/@murphai/assistant-engine/dist/assistant/store/types.d.ts +1 -0
  225. package/node_modules/@murphai/assistant-engine/dist/assistant/store/types.d.ts.map +1 -1
  226. package/node_modules/@murphai/assistant-engine/dist/assistant/store.d.ts.map +1 -1
  227. package/node_modules/@murphai/assistant-engine/dist/assistant/store.js +6 -0
  228. package/node_modules/@murphai/assistant-engine/dist/assistant/store.js.map +1 -1
  229. package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.d.ts +3 -0
  230. package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.d.ts.map +1 -1
  231. package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.js +57 -29
  232. package/node_modules/@murphai/assistant-engine/dist/assistant/system-prompt.js.map +1 -1
  233. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.d.ts +1 -0
  234. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.d.ts.map +1 -1
  235. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.js +17 -3
  236. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-digest.js.map +1 -1
  237. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.d.ts +4 -7
  238. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.d.ts.map +1 -1
  239. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.js +21 -6
  240. package/node_modules/@murphai/assistant-engine/dist/assistant/tool-validation-feedback.js.map +1 -1
  241. package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.d.ts +1 -0
  242. package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.d.ts.map +1 -1
  243. package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.js +6 -0
  244. package/node_modules/@murphai/assistant-engine/dist/assistant/turn-finalizer.js.map +1 -1
  245. package/node_modules/@murphai/assistant-engine/dist/assistant/turn-input.d.ts +1 -1
  246. package/node_modules/@murphai/assistant-engine/dist/assistant/turn-input.d.ts.map +1 -1
  247. package/node_modules/@murphai/assistant-engine/dist/assistant/usage-attribution.d.ts +1 -1
  248. package/node_modules/@murphai/assistant-engine/dist/assistant/usage-attribution.d.ts.map +1 -1
  249. package/node_modules/@murphai/assistant-engine/dist/assistant/usage-attribution.js.map +1 -1
  250. package/node_modules/@murphai/assistant-engine/dist/assistant-ask.js +8 -0
  251. package/node_modules/@murphai/assistant-engine/dist/assistant-ask.js.map +1 -1
  252. package/node_modules/@murphai/assistant-engine/dist/assistant-cli-access.d.ts.map +1 -1
  253. package/node_modules/@murphai/assistant-engine/dist/assistant-cli-access.js +1 -0
  254. package/node_modules/@murphai/assistant-engine/dist/assistant-cli-access.js.map +1 -1
  255. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.d.ts +49 -0
  256. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.d.ts.map +1 -0
  257. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.js +387 -0
  258. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/analyze-video-tool.js.map +1 -0
  259. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.d.ts +1 -1
  260. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.d.ts.map +1 -1
  261. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.js +12 -13
  262. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-protocol.js.map +1 -1
  263. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.d.ts +1 -0
  264. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.d.ts.map +1 -1
  265. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.js +16 -5
  266. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-requests.js.map +1 -1
  267. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.d.ts +1 -1
  268. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.d.ts.map +1 -1
  269. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.js +2 -1
  270. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/app-server-rpc.js.map +1 -1
  271. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.d.ts +4 -0
  272. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.d.ts.map +1 -1
  273. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.js +26 -13
  274. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/command-family.js.map +1 -1
  275. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.d.ts +1024 -1091
  276. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.d.ts.map +1 -1
  277. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.js +354 -330
  278. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tool-catalog.js.map +1 -1
  279. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.d.ts +50 -0
  280. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.d.ts.map +1 -0
  281. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.js +79 -0
  282. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/analyze-video.js.map +1 -0
  283. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.d.ts +4 -0
  284. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.d.ts.map +1 -0
  285. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.js +153 -0
  286. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation-model-input-schema.js.map +1 -0
  287. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.d.ts +4 -1
  288. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.d.ts.map +1 -1
  289. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.js +29 -35
  290. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/automation.js.map +1 -1
  291. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.d.ts +4 -1
  292. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.d.ts.map +1 -1
  293. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.js +43 -9
  294. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/device.js.map +1 -1
  295. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/dynamic-tool-wrapper.d.ts +6 -0
  296. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/dynamic-tool-wrapper.d.ts.map +1 -1
  297. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/dynamic-tool-wrapper.js.map +1 -1
  298. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.d.ts +60 -0
  299. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.d.ts.map +1 -0
  300. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.js +91 -0
  301. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/member-memory.js.map +1 -0
  302. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.d.ts +41 -1
  303. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.d.ts.map +1 -1
  304. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.js +85 -9
  305. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools/physical-notes.js.map +1 -1
  306. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.d.ts +25 -2
  307. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.d.ts.map +1 -1
  308. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.js +280 -89
  309. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/dynamic-tools.js.map +1 -1
  310. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/failures.js +1 -1
  311. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/response-card-validation-feedback.d.ts.map +1 -1
  312. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/response-card-validation-feedback.js +1 -3
  313. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/response-card-validation-feedback.js.map +1 -1
  314. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.d.ts +19 -0
  315. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.d.ts.map +1 -0
  316. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.js +203 -0
  317. package/node_modules/@murphai/assistant-engine/dist/assistant-codex/workout-delivery-context.js.map +1 -0
  318. package/node_modules/@murphai/assistant-engine/dist/assistant-codex.d.ts +9 -1
  319. package/node_modules/@murphai/assistant-engine/dist/assistant-codex.d.ts.map +1 -1
  320. package/node_modules/@murphai/assistant-engine/dist/assistant-codex.js +220 -130
  321. package/node_modules/@murphai/assistant-engine/dist/assistant-codex.js.map +1 -1
  322. package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.d.ts +1 -1
  323. package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.d.ts.map +1 -1
  324. package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.js +1 -1
  325. package/node_modules/@murphai/assistant-engine/dist/assistant-runtime-residue.js.map +1 -1
  326. package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.d.ts +1 -1
  327. package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.d.ts.map +1 -1
  328. package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.js +1 -1
  329. package/node_modules/@murphai/assistant-engine/dist/assistant-skill-assets.js.map +1 -1
  330. package/node_modules/@murphai/assistant-engine/dist/index.d.ts +2 -1
  331. package/node_modules/@murphai/assistant-engine/dist/index.d.ts.map +1 -1
  332. package/node_modules/@murphai/assistant-engine/dist/index.js +1 -0
  333. package/node_modules/@murphai/assistant-engine/dist/index.js.map +1 -1
  334. package/node_modules/@murphai/assistant-engine/skills/automatic-meal-capture/SKILL.md +111 -68
  335. package/node_modules/@murphai/assistant-engine/skills/behavior-followthrough/SKILL.md +0 -2
  336. package/node_modules/@murphai/assistant-engine/skills/daily-activity/SKILL.md +11 -4
  337. package/node_modules/@murphai/assistant-engine/skills/food-journal/SKILL.md +83 -22
  338. package/node_modules/@murphai/assistant-engine/skills/hosted-low-usage/SKILL.md +3 -3
  339. package/node_modules/@murphai/assistant-engine/skills/murph-family/SKILL.md +1 -1
  340. package/node_modules/@murphai/assistant-engine/skills/nutrition-strategy/SKILL.md +17 -11
  341. package/node_modules/@murphai/assistant-engine/skills/nutrition-strategy/references/daily-nutrition-card-goals.md +62 -34
  342. package/node_modules/@murphai/assistant-engine/skills/physical-notes/SKILL.md +47 -0
  343. package/node_modules/@murphai/assistant-engine/skills/shared/exercise-catalog-runtime.md +6 -0
  344. package/node_modules/@murphai/assistant-engine/skills/signup-link/SKILL.md +1 -1
  345. package/node_modules/@murphai/assistant-engine/skills/strength-training/SKILL.md +12 -5
  346. package/node_modules/@murphai/assistant-engine/skills/tracked-table/SKILL.md +156 -66
  347. package/node_modules/@murphai/core/dist/automation.d.ts.map +1 -1
  348. package/node_modules/@murphai/core/dist/automation.js +17 -9
  349. package/node_modules/@murphai/core/dist/automation.js.map +1 -1
  350. package/node_modules/@murphai/core/dist/bank/habitat.d.ts +2 -0
  351. package/node_modules/@murphai/core/dist/bank/habitat.d.ts.map +1 -1
  352. package/node_modules/@murphai/core/dist/bank/habitat.js +27 -0
  353. package/node_modules/@murphai/core/dist/bank/habitat.js.map +1 -1
  354. package/node_modules/@murphai/core/dist/bank/regimens.js +1 -1
  355. package/node_modules/@murphai/core/dist/bank/regimens.js.map +1 -1
  356. package/node_modules/@murphai/core/dist/domains/events/ledger.d.ts +21 -0
  357. package/node_modules/@murphai/core/dist/domains/events/ledger.d.ts.map +1 -1
  358. package/node_modules/@murphai/core/dist/domains/events/ledger.js +102 -8
  359. package/node_modules/@murphai/core/dist/domains/events/ledger.js.map +1 -1
  360. package/node_modules/@murphai/core/dist/domains/events.d.ts +2 -2
  361. package/node_modules/@murphai/core/dist/domains/events.d.ts.map +1 -1
  362. package/node_modules/@murphai/core/dist/domains/events.js +1 -1
  363. package/node_modules/@murphai/core/dist/domains/events.js.map +1 -1
  364. package/node_modules/@murphai/core/dist/history/api.d.ts.map +1 -1
  365. package/node_modules/@murphai/core/dist/history/api.js +18 -3
  366. package/node_modules/@murphai/core/dist/history/api.js.map +1 -1
  367. package/node_modules/@murphai/core/dist/index.d.ts +4 -2
  368. package/node_modules/@murphai/core/dist/index.d.ts.map +1 -1
  369. package/node_modules/@murphai/core/dist/index.js +3 -1
  370. package/node_modules/@murphai/core/dist/index.js.map +1 -1
  371. package/node_modules/@murphai/core/dist/mutations.d.ts +25 -2
  372. package/node_modules/@murphai/core/dist/mutations.d.ts.map +1 -1
  373. package/node_modules/@murphai/core/dist/mutations.js +1588 -78
  374. package/node_modules/@murphai/core/dist/mutations.js.map +1 -1
  375. package/node_modules/@murphai/core/dist/operations/index.d.ts +1 -1
  376. package/node_modules/@murphai/core/dist/operations/index.d.ts.map +1 -1
  377. package/node_modules/@murphai/core/dist/operations/index.js +1 -1
  378. package/node_modules/@murphai/core/dist/operations/index.js.map +1 -1
  379. package/node_modules/@murphai/core/dist/operations/write-batch.d.ts +2 -0
  380. package/node_modules/@murphai/core/dist/operations/write-batch.d.ts.map +1 -1
  381. package/node_modules/@murphai/core/dist/operations/write-batch.js +7 -0
  382. package/node_modules/@murphai/core/dist/operations/write-batch.js.map +1 -1
  383. package/node_modules/@murphai/core/dist/protocols.d.ts.map +1 -1
  384. package/node_modules/@murphai/core/dist/protocols.js +52 -9
  385. package/node_modules/@murphai/core/dist/protocols.js.map +1 -1
  386. package/node_modules/@murphai/core/dist/public-mutations.d.ts +1 -1
  387. package/node_modules/@murphai/core/dist/public-mutations.d.ts.map +1 -1
  388. package/node_modules/@murphai/core/dist/public-mutations.js +2 -2
  389. package/node_modules/@murphai/core/dist/public-mutations.js.map +1 -1
  390. package/node_modules/@murphai/core/dist/scheduled-logs.d.ts +3 -0
  391. package/node_modules/@murphai/core/dist/scheduled-logs.d.ts.map +1 -1
  392. package/node_modules/@murphai/core/dist/scheduled-logs.js +65 -29
  393. package/node_modules/@murphai/core/dist/scheduled-logs.js.map +1 -1
  394. package/node_modules/@murphai/device-syncd/README.md +140 -46
  395. package/node_modules/@murphai/device-syncd/dist/client.d.ts +14 -0
  396. package/node_modules/@murphai/device-syncd/dist/client.d.ts.map +1 -1
  397. package/node_modules/@murphai/device-syncd/dist/client.js.map +1 -1
  398. package/node_modules/@murphai/device-syncd/dist/config/connect-routes.d.ts +4 -1
  399. package/node_modules/@murphai/device-syncd/dist/config/connect-routes.d.ts.map +1 -1
  400. package/node_modules/@murphai/device-syncd/dist/config/connect-routes.js +24 -2
  401. package/node_modules/@murphai/device-syncd/dist/config/connect-routes.js.map +1 -1
  402. package/node_modules/@murphai/device-syncd/dist/config/junction-connect-sources.js +2 -2
  403. package/node_modules/@murphai/device-syncd/dist/config/junction-connect-sources.js.map +1 -1
  404. package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.d.ts +53 -0
  405. package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.d.ts.map +1 -1
  406. package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.js +13 -0
  407. package/node_modules/@murphai/device-syncd/dist/config/provider-manifests.js.map +1 -1
  408. package/node_modules/@murphai/device-syncd/dist/connect-config.d.ts +1 -1
  409. package/node_modules/@murphai/device-syncd/dist/connect-config.d.ts.map +1 -1
  410. package/node_modules/@murphai/device-syncd/dist/connect-config.js +1 -1
  411. package/node_modules/@murphai/device-syncd/dist/connect-config.js.map +1 -1
  412. package/node_modules/@murphai/device-syncd/dist/fitbit-migration.d.ts +57 -0
  413. package/node_modules/@murphai/device-syncd/dist/fitbit-migration.d.ts.map +1 -0
  414. package/node_modules/@murphai/device-syncd/dist/fitbit-migration.js +166 -0
  415. package/node_modules/@murphai/device-syncd/dist/fitbit-migration.js.map +1 -0
  416. package/node_modules/@murphai/device-syncd/dist/hosted-runtime.d.ts +43 -0
  417. package/node_modules/@murphai/device-syncd/dist/hosted-runtime.d.ts.map +1 -1
  418. package/node_modules/@murphai/device-syncd/dist/hosted-runtime.js +129 -5
  419. package/node_modules/@murphai/device-syncd/dist/hosted-runtime.js.map +1 -1
  420. package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.d.ts +3 -0
  421. package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.d.ts.map +1 -1
  422. package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.js +36 -16
  423. package/node_modules/@murphai/device-syncd/dist/junction-historical-backfill-progress.js.map +1 -1
  424. package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.d.ts +17 -2
  425. package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.d.ts.map +1 -1
  426. package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.js +99 -40
  427. package/node_modules/@murphai/device-syncd/dist/junction-inline-authority.js.map +1 -1
  428. package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.d.ts +17 -0
  429. package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.d.ts.map +1 -0
  430. package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.js +45 -0
  431. package/node_modules/@murphai/device-syncd/dist/junction-source-reconnect.js.map +1 -0
  432. package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.d.ts +8 -1
  433. package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.d.ts.map +1 -1
  434. package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.js +12 -1
  435. package/node_modules/@murphai/device-syncd/dist/junction-timeseries-progress.js.map +1 -1
  436. package/node_modules/@murphai/device-syncd/dist/providers/junction-client.d.ts +7 -0
  437. package/node_modules/@murphai/device-syncd/dist/providers/junction-client.d.ts.map +1 -1
  438. package/node_modules/@murphai/device-syncd/dist/providers/junction-client.js +285 -46
  439. package/node_modules/@murphai/device-syncd/dist/providers/junction-client.js.map +1 -1
  440. package/node_modules/@murphai/device-syncd/dist/providers/junction.d.ts +9 -0
  441. package/node_modules/@murphai/device-syncd/dist/providers/junction.d.ts.map +1 -1
  442. package/node_modules/@murphai/device-syncd/dist/providers/junction.js +2045 -367
  443. package/node_modules/@murphai/device-syncd/dist/providers/junction.js.map +1 -1
  444. package/node_modules/@murphai/device-syncd/dist/public-account.d.ts +2 -0
  445. package/node_modules/@murphai/device-syncd/dist/public-account.d.ts.map +1 -1
  446. package/node_modules/@murphai/device-syncd/dist/public-account.js +96 -0
  447. package/node_modules/@murphai/device-syncd/dist/public-account.js.map +1 -1
  448. package/node_modules/@murphai/device-syncd/dist/public-ingress.d.ts.map +1 -1
  449. package/node_modules/@murphai/device-syncd/dist/public-ingress.js +6 -4
  450. package/node_modules/@murphai/device-syncd/dist/public-ingress.js.map +1 -1
  451. package/node_modules/@murphai/device-syncd/dist/service.d.ts +6 -2
  452. package/node_modules/@murphai/device-syncd/dist/service.d.ts.map +1 -1
  453. package/node_modules/@murphai/device-syncd/dist/service.js +385 -44
  454. package/node_modules/@murphai/device-syncd/dist/service.js.map +1 -1
  455. package/node_modules/@murphai/device-syncd/dist/source-staleness.d.ts +15 -0
  456. package/node_modules/@murphai/device-syncd/dist/source-staleness.d.ts.map +1 -1
  457. package/node_modules/@murphai/device-syncd/dist/source-staleness.js +25 -1
  458. package/node_modules/@murphai/device-syncd/dist/source-staleness.js.map +1 -1
  459. package/node_modules/@murphai/device-syncd/dist/store/jobs.d.ts +10 -1
  460. package/node_modules/@murphai/device-syncd/dist/store/jobs.d.ts.map +1 -1
  461. package/node_modules/@murphai/device-syncd/dist/store/jobs.js +95 -1
  462. package/node_modules/@murphai/device-syncd/dist/store/jobs.js.map +1 -1
  463. package/node_modules/@murphai/device-syncd/dist/store/schema.d.ts +1 -1
  464. package/node_modules/@murphai/device-syncd/dist/store/schema.d.ts.map +1 -1
  465. package/node_modules/@murphai/device-syncd/dist/store/schema.js +25 -3
  466. package/node_modules/@murphai/device-syncd/dist/store/schema.js.map +1 -1
  467. package/node_modules/@murphai/device-syncd/dist/store/sources.d.ts +24 -2
  468. package/node_modules/@murphai/device-syncd/dist/store/sources.d.ts.map +1 -1
  469. package/node_modules/@murphai/device-syncd/dist/store/sources.js +223 -14
  470. package/node_modules/@murphai/device-syncd/dist/store/sources.js.map +1 -1
  471. package/node_modules/@murphai/device-syncd/dist/store.d.ts +13 -3
  472. package/node_modules/@murphai/device-syncd/dist/store.d.ts.map +1 -1
  473. package/node_modules/@murphai/device-syncd/dist/store.js +104 -22
  474. package/node_modules/@murphai/device-syncd/dist/store.js.map +1 -1
  475. package/node_modules/@murphai/device-syncd/dist/types.d.ts +96 -9
  476. package/node_modules/@murphai/device-syncd/dist/types.d.ts.map +1 -1
  477. package/node_modules/@murphai/device-syncd/dist/types.js +4 -0
  478. package/node_modules/@murphai/device-syncd/dist/types.js.map +1 -1
  479. package/node_modules/@murphai/device-syncd/package.json +8 -0
  480. package/node_modules/@murphai/health-metrics/dist/definitions/activity.d.ts.map +1 -1
  481. package/node_modules/@murphai/health-metrics/dist/definitions/activity.js +66 -0
  482. package/node_modules/@murphai/health-metrics/dist/definitions/activity.js.map +1 -1
  483. package/node_modules/@murphai/health-metrics/dist/definitions/body.js +2 -2
  484. package/node_modules/@murphai/health-metrics/dist/definitions/body.js.map +1 -1
  485. package/node_modules/@murphai/health-metrics/dist/normalize.d.ts.map +1 -1
  486. package/node_modules/@murphai/health-metrics/dist/normalize.js +3 -0
  487. package/node_modules/@murphai/health-metrics/dist/normalize.js.map +1 -1
  488. package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.d.ts +3 -1
  489. package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.d.ts.map +1 -1
  490. package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.js +5 -1
  491. package/node_modules/@murphai/health-metrics/dist/wearable-metric-catalog.js.map +1 -1
  492. package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.d.ts +79 -0
  493. package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.d.ts.map +1 -1
  494. package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.js +19 -0
  495. package/node_modules/@murphai/health-metrics/dist/wearable-provider-catalog.js.map +1 -1
  496. package/node_modules/@murphai/importers/dist/core-port.d.ts +15 -1
  497. package/node_modules/@murphai/importers/dist/core-port.d.ts.map +1 -1
  498. package/node_modules/@murphai/importers/dist/core-port.js.map +1 -1
  499. package/node_modules/@murphai/importers/dist/create-importers.d.ts +9 -1
  500. package/node_modules/@murphai/importers/dist/create-importers.d.ts.map +1 -1
  501. package/node_modules/@murphai/importers/dist/create-importers.js +12 -2
  502. package/node_modules/@murphai/importers/dist/create-importers.js.map +1 -1
  503. package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.d.ts +17 -3
  504. package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.d.ts.map +1 -1
  505. package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.js +102 -8
  506. package/node_modules/@murphai/importers/dist/device-providers/import-device-provider-snapshot.js.map +1 -1
  507. package/node_modules/@murphai/importers/dist/device-providers/index.d.ts +5 -4
  508. package/node_modules/@murphai/importers/dist/device-providers/index.d.ts.map +1 -1
  509. package/node_modules/@murphai/importers/dist/device-providers/index.js +4 -3
  510. package/node_modules/@murphai/importers/dist/device-providers/index.js.map +1 -1
  511. package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.d.ts +11 -1
  512. package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.d.ts.map +1 -1
  513. package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.js +420 -27
  514. package/node_modules/@murphai/importers/dist/device-providers/junction-bounded-features.js.map +1 -1
  515. package/node_modules/@murphai/importers/dist/device-providers/junction-origin.d.ts +1 -0
  516. package/node_modules/@murphai/importers/dist/device-providers/junction-origin.d.ts.map +1 -1
  517. package/node_modules/@murphai/importers/dist/device-providers/junction-origin.js +7 -2
  518. package/node_modules/@murphai/importers/dist/device-providers/junction-origin.js.map +1 -1
  519. package/node_modules/@murphai/importers/dist/device-providers/junction-resources.d.ts +4 -1
  520. package/node_modules/@murphai/importers/dist/device-providers/junction-resources.d.ts.map +1 -1
  521. package/node_modules/@murphai/importers/dist/device-providers/junction-resources.js +42 -1
  522. package/node_modules/@murphai/importers/dist/device-providers/junction-resources.js.map +1 -1
  523. package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.d.ts +79 -0
  524. package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.d.ts.map +1 -0
  525. package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.js +356 -0
  526. package/node_modules/@murphai/importers/dist/device-providers/junction-timeseries-features.js.map +1 -0
  527. package/node_modules/@murphai/importers/dist/device-providers/junction.d.ts +76 -3
  528. package/node_modules/@murphai/importers/dist/device-providers/junction.d.ts.map +1 -1
  529. package/node_modules/@murphai/importers/dist/device-providers/junction.js +1676 -347
  530. package/node_modules/@murphai/importers/dist/device-providers/junction.js.map +1 -1
  531. package/node_modules/@murphai/importers/dist/device-providers/shared-normalization.d.ts.map +1 -1
  532. package/node_modules/@murphai/importers/dist/device-providers/shared-normalization.js +1 -0
  533. package/node_modules/@murphai/importers/dist/device-providers/shared-normalization.js.map +1 -1
  534. package/node_modules/@murphai/importers/dist/device-providers/types.d.ts +9 -1
  535. package/node_modules/@murphai/importers/dist/device-providers/types.d.ts.map +1 -1
  536. package/node_modules/@murphai/importers/dist/index.d.ts +2 -2
  537. package/node_modules/@murphai/importers/dist/index.d.ts.map +1 -1
  538. package/node_modules/@murphai/importers/dist/index.js +1 -1
  539. package/node_modules/@murphai/importers/dist/index.js.map +1 -1
  540. package/node_modules/@murphai/inboxd/README.md +1 -1
  541. package/node_modules/@murphai/inboxd/dist/indexing/retention.d.ts +6 -0
  542. package/node_modules/@murphai/inboxd/dist/indexing/retention.d.ts.map +1 -1
  543. package/node_modules/@murphai/inboxd/dist/indexing/retention.js +64 -18
  544. package/node_modules/@murphai/inboxd/dist/indexing/retention.js.map +1 -1
  545. package/node_modules/@murphai/inboxd/dist/retention.d.ts +1 -1
  546. package/node_modules/@murphai/inboxd/dist/retention.d.ts.map +1 -1
  547. package/node_modules/@murphai/inboxd/dist/retention.js +1 -1
  548. package/node_modules/@murphai/inboxd/dist/retention.js.map +1 -1
  549. package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.d.ts +1 -0
  550. package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.d.ts.map +1 -1
  551. package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.js +5 -0
  552. package/node_modules/@murphai/operator-config/dist/assistant-cli-contracts.js.map +1 -1
  553. package/node_modules/@murphai/operator-config/dist/assistant-response-cards.d.ts +175 -152
  554. package/node_modules/@murphai/operator-config/dist/assistant-response-cards.d.ts.map +1 -1
  555. package/node_modules/@murphai/operator-config/dist/assistant-response-cards.js +62 -41
  556. package/node_modules/@murphai/operator-config/dist/assistant-response-cards.js.map +1 -1
  557. package/node_modules/@murphai/operator-config/dist/device-sync-client.d.ts.map +1 -1
  558. package/node_modules/@murphai/operator-config/dist/device-sync-client.js +1 -5
  559. package/node_modules/@murphai/operator-config/dist/device-sync-client.js.map +1 -1
  560. package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.d.ts +66 -0
  561. package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.d.ts.map +1 -1
  562. package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.js +58 -0
  563. package/node_modules/@murphai/operator-config/dist/vault-cli-contracts.js.map +1 -1
  564. package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.d.ts +24 -0
  565. package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.d.ts.map +1 -0
  566. package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.js +219 -0
  567. package/node_modules/@murphai/operator-config/dist/vault-cli-error-projection.js.map +1 -0
  568. package/node_modules/@murphai/operator-config/dist/vault-cli-errors.d.ts.map +1 -1
  569. package/node_modules/@murphai/operator-config/dist/vault-cli-errors.js.map +1 -1
  570. package/node_modules/@murphai/operator-config/dist/workout-action-binding.d.ts +9 -0
  571. package/node_modules/@murphai/operator-config/dist/workout-action-binding.d.ts.map +1 -1
  572. package/node_modules/@murphai/operator-config/dist/workout-action-binding.js +48 -5
  573. package/node_modules/@murphai/operator-config/dist/workout-action-binding.js.map +1 -1
  574. package/node_modules/@murphai/operator-config/package.json +5 -0
  575. package/node_modules/@murphai/query/README.md +20 -5
  576. package/node_modules/@murphai/query/dist/browser-replica/build.d.ts +1 -1
  577. package/node_modules/@murphai/query/dist/browser-replica/build.d.ts.map +1 -1
  578. package/node_modules/@murphai/query/dist/browser-replica/build.js +25 -16
  579. package/node_modules/@murphai/query/dist/browser-replica/build.js.map +1 -1
  580. package/node_modules/@murphai/query/dist/browser-replica/json.d.ts +6 -0
  581. package/node_modules/@murphai/query/dist/browser-replica/json.d.ts.map +1 -0
  582. package/node_modules/@murphai/query/dist/browser-replica/json.js +103 -0
  583. package/node_modules/@murphai/query/dist/browser-replica/json.js.map +1 -0
  584. package/node_modules/@murphai/query/dist/browser-replica/shared.d.ts +1 -0
  585. package/node_modules/@murphai/query/dist/browser-replica/shared.d.ts.map +1 -1
  586. package/node_modules/@murphai/query/dist/browser-replica/source.d.ts +13 -0
  587. package/node_modules/@murphai/query/dist/browser-replica/source.d.ts.map +1 -0
  588. package/node_modules/@murphai/query/dist/browser-replica/source.js +45 -0
  589. package/node_modules/@murphai/query/dist/browser-replica/source.js.map +1 -0
  590. package/node_modules/@murphai/query/dist/browser-replica-server.d.ts +2 -0
  591. package/node_modules/@murphai/query/dist/browser-replica-server.d.ts.map +1 -1
  592. package/node_modules/@murphai/query/dist/browser-replica-server.js +2 -0
  593. package/node_modules/@murphai/query/dist/browser-replica-server.js.map +1 -1
  594. package/node_modules/@murphai/query/dist/health/blood-tests.d.ts.map +1 -1
  595. package/node_modules/@murphai/query/dist/health/blood-tests.js +3 -2
  596. package/node_modules/@murphai/query/dist/health/blood-tests.js.map +1 -1
  597. package/node_modules/@murphai/query/dist/health/canonical-collector.d.ts +11 -1
  598. package/node_modules/@murphai/query/dist/health/canonical-collector.d.ts.map +1 -1
  599. package/node_modules/@murphai/query/dist/health/canonical-collector.js +39 -16
  600. package/node_modules/@murphai/query/dist/health/canonical-collector.js.map +1 -1
  601. package/node_modules/@murphai/query/dist/health/entity-slices.d.ts +9 -3
  602. package/node_modules/@murphai/query/dist/health/entity-slices.d.ts.map +1 -1
  603. package/node_modules/@murphai/query/dist/health/entity-slices.js +6 -6
  604. package/node_modules/@murphai/query/dist/health/entity-slices.js.map +1 -1
  605. package/node_modules/@murphai/query/dist/health/immunizations.d.ts.map +1 -1
  606. package/node_modules/@murphai/query/dist/health/immunizations.js +14 -11
  607. package/node_modules/@murphai/query/dist/health/immunizations.js.map +1 -1
  608. package/node_modules/@murphai/query/dist/health/loaders.d.ts +21 -7
  609. package/node_modules/@murphai/query/dist/health/loaders.d.ts.map +1 -1
  610. package/node_modules/@murphai/query/dist/health/loaders.js +53 -15
  611. package/node_modules/@murphai/query/dist/health/loaders.js.map +1 -1
  612. package/node_modules/@murphai/query/dist/health/registries.d.ts +1 -0
  613. package/node_modules/@murphai/query/dist/health/registries.d.ts.map +1 -1
  614. package/node_modules/@murphai/query/dist/health/registries.js.map +1 -1
  615. package/node_modules/@murphai/query/dist/index.d.ts +4 -3
  616. package/node_modules/@murphai/query/dist/index.d.ts.map +1 -1
  617. package/node_modules/@murphai/query/dist/index.js +4 -3
  618. package/node_modules/@murphai/query/dist/index.js.map +1 -1
  619. package/node_modules/@murphai/query/dist/meal-nutrition.d.ts +1 -1
  620. package/node_modules/@murphai/query/dist/meal-nutrition.d.ts.map +1 -1
  621. package/node_modules/@murphai/query/dist/meal-nutrition.js +10 -3
  622. package/node_modules/@murphai/query/dist/meal-nutrition.js.map +1 -1
  623. package/node_modules/@murphai/query/dist/metrics/projection.d.ts.map +1 -1
  624. package/node_modules/@murphai/query/dist/metrics/projection.js +19 -6
  625. package/node_modules/@murphai/query/dist/metrics/projection.js.map +1 -1
  626. package/node_modules/@murphai/query/dist/murph-age.d.ts +5 -1
  627. package/node_modules/@murphai/query/dist/murph-age.d.ts.map +1 -1
  628. package/node_modules/@murphai/query/dist/murph-age.js +22 -8
  629. package/node_modules/@murphai/query/dist/murph-age.js.map +1 -1
  630. package/node_modules/@murphai/query/dist/projection/entity-store.js +4 -4
  631. package/node_modules/@murphai/query/dist/projection/entity-store.js.map +1 -1
  632. package/node_modules/@murphai/query/dist/projection/schema.d.ts +1 -1
  633. package/node_modules/@murphai/query/dist/projection/schema.d.ts.map +1 -1
  634. package/node_modules/@murphai/query/dist/projection/schema.js +3 -1
  635. package/node_modules/@murphai/query/dist/projection/schema.js.map +1 -1
  636. package/node_modules/@murphai/query/dist/projection/wearable-summary-compose.js +4 -0
  637. package/node_modules/@murphai/query/dist/projection/wearable-summary-compose.js.map +1 -1
  638. package/node_modules/@murphai/query/dist/projection/wearable-summary-projector.js +4 -0
  639. package/node_modules/@murphai/query/dist/projection/wearable-summary-projector.js.map +1 -1
  640. package/node_modules/@murphai/query/dist/query-visibility.d.ts +5 -0
  641. package/node_modules/@murphai/query/dist/query-visibility.d.ts.map +1 -1
  642. package/node_modules/@murphai/query/dist/query-visibility.js +11 -5
  643. package/node_modules/@murphai/query/dist/query-visibility.js.map +1 -1
  644. package/node_modules/@murphai/query/dist/read-model.d.ts +8 -0
  645. package/node_modules/@murphai/query/dist/read-model.d.ts.map +1 -1
  646. package/node_modules/@murphai/query/dist/read-model.js +11 -1
  647. package/node_modules/@murphai/query/dist/read-model.js.map +1 -1
  648. package/node_modules/@murphai/query/dist/scheduled-logs.d.ts.map +1 -1
  649. package/node_modules/@murphai/query/dist/scheduled-logs.js +55 -79
  650. package/node_modules/@murphai/query/dist/scheduled-logs.js.map +1 -1
  651. package/node_modules/@murphai/query/dist/vault-source.d.ts +22 -4
  652. package/node_modules/@murphai/query/dist/vault-source.d.ts.map +1 -1
  653. package/node_modules/@murphai/query/dist/vault-source.js +188 -59
  654. package/node_modules/@murphai/query/dist/vault-source.js.map +1 -1
  655. package/node_modules/@murphai/query/dist/wearables/candidates.d.ts.map +1 -1
  656. package/node_modules/@murphai/query/dist/wearables/candidates.js +26 -15
  657. package/node_modules/@murphai/query/dist/wearables/candidates.js.map +1 -1
  658. package/node_modules/@murphai/query/dist/wearables/public-output.d.ts.map +1 -1
  659. package/node_modules/@murphai/query/dist/wearables/public-output.js +6 -0
  660. package/node_modules/@murphai/query/dist/wearables/public-output.js.map +1 -1
  661. package/node_modules/@murphai/query/dist/wearables/source-health.d.ts.map +1 -1
  662. package/node_modules/@murphai/query/dist/wearables/source-health.js +4 -0
  663. package/node_modules/@murphai/query/dist/wearables/source-health.js.map +1 -1
  664. package/node_modules/@murphai/query/dist/wearables/types.d.ts +41 -4
  665. package/node_modules/@murphai/query/dist/wearables/types.d.ts.map +1 -1
  666. package/node_modules/@murphai/query/dist/wearables/types.js +2 -0
  667. package/node_modules/@murphai/query/dist/wearables/types.js.map +1 -1
  668. package/node_modules/@murphai/query/dist/wearables/workout-features.d.ts +5 -0
  669. package/node_modules/@murphai/query/dist/wearables/workout-features.d.ts.map +1 -0
  670. package/node_modules/@murphai/query/dist/wearables/workout-features.js +179 -0
  671. package/node_modules/@murphai/query/dist/wearables/workout-features.js.map +1 -0
  672. package/node_modules/@murphai/query/dist/wearables.d.ts.map +1 -1
  673. package/node_modules/@murphai/query/dist/wearables.js +23 -6
  674. package/node_modules/@murphai/query/dist/wearables.js.map +1 -1
  675. package/node_modules/@murphai/query/package.json +5 -0
  676. package/node_modules/@murphai/runtime-state/README.md +1 -1
  677. package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.d.ts +4 -0
  678. package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.d.ts.map +1 -1
  679. package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.js +16 -3
  680. package/node_modules/@murphai/runtime-state/dist/assistant-runtime-issues.js.map +1 -1
  681. package/node_modules/@murphai/runtime-state/dist/hosted-bundles.d.ts +1 -0
  682. package/node_modules/@murphai/runtime-state/dist/hosted-bundles.d.ts.map +1 -1
  683. package/node_modules/@murphai/runtime-state/dist/hosted-bundles.js +31 -0
  684. package/node_modules/@murphai/runtime-state/dist/hosted-bundles.js.map +1 -1
  685. package/node_modules/@murphai/runtime-state/dist/hosted-email.d.ts +5 -0
  686. package/node_modules/@murphai/runtime-state/dist/hosted-email.d.ts.map +1 -1
  687. package/node_modules/@murphai/runtime-state/dist/hosted-email.js +10 -0
  688. package/node_modules/@murphai/runtime-state/dist/hosted-email.js.map +1 -1
  689. package/node_modules/@murphai/setup-cli/dist/incur-error-bridge.d.ts.map +1 -1
  690. package/node_modules/@murphai/setup-cli/dist/incur-error-bridge.js +6 -15
  691. package/node_modules/@murphai/setup-cli/dist/incur-error-bridge.js.map +1 -1
  692. package/node_modules/@murphai/vault-usecases/README.md +9 -0
  693. package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.d.ts +6 -0
  694. package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.d.ts.map +1 -1
  695. package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.js +49 -1
  696. package/node_modules/@murphai/vault-usecases/dist/commands/query-record-command-helpers.js.map +1 -1
  697. package/node_modules/@murphai/vault-usecases/dist/usecases/capture.d.ts.map +1 -1
  698. package/node_modules/@murphai/vault-usecases/dist/usecases/capture.js +22 -12
  699. package/node_modules/@murphai/vault-usecases/dist/usecases/capture.js.map +1 -1
  700. package/node_modules/@murphai/vault-usecases/dist/usecases/clinical-imports.d.ts.map +1 -1
  701. package/node_modules/@murphai/vault-usecases/dist/usecases/clinical-imports.js +1 -3
  702. package/node_modules/@murphai/vault-usecases/dist/usecases/clinical-imports.js.map +1 -1
  703. package/node_modules/@murphai/vault-usecases/dist/usecases/document-meal-read.d.ts.map +1 -1
  704. package/node_modules/@murphai/vault-usecases/dist/usecases/document-meal-read.js +24 -18
  705. package/node_modules/@murphai/vault-usecases/dist/usecases/document-meal-read.js.map +1 -1
  706. package/node_modules/@murphai/vault-usecases/dist/usecases/encounter.d.ts.map +1 -1
  707. package/node_modules/@murphai/vault-usecases/dist/usecases/encounter.js +1 -3
  708. package/node_modules/@murphai/vault-usecases/dist/usecases/encounter.js.map +1 -1
  709. package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.d.ts +8 -0
  710. package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.d.ts.map +1 -1
  711. package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.js +56 -5
  712. package/node_modules/@murphai/vault-usecases/dist/usecases/event-record-mutations.js.map +1 -1
  713. package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.d.ts +20 -0
  714. package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.d.ts.map +1 -0
  715. package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.js +72 -0
  716. package/node_modules/@murphai/vault-usecases/dist/usecases/exact-event-record.js.map +1 -0
  717. package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.d.ts +1 -1
  718. package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.d.ts.map +1 -1
  719. package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.js +42 -9
  720. package/node_modules/@murphai/vault-usecases/dist/usecases/experiment-journal-vault.js.map +1 -1
  721. package/node_modules/@murphai/vault-usecases/dist/usecases/explicit-health-family-services.d.ts.map +1 -1
  722. package/node_modules/@murphai/vault-usecases/dist/usecases/explicit-health-family-services.js +158 -24
  723. package/node_modules/@murphai/vault-usecases/dist/usecases/explicit-health-family-services.js.map +1 -1
  724. package/node_modules/@murphai/vault-usecases/dist/usecases/food.d.ts.map +1 -1
  725. package/node_modules/@murphai/vault-usecases/dist/usecases/food.js +98 -5
  726. package/node_modules/@murphai/vault-usecases/dist/usecases/food.js.map +1 -1
  727. package/node_modules/@murphai/vault-usecases/dist/usecases/integrated-services.d.ts.map +1 -1
  728. package/node_modules/@murphai/vault-usecases/dist/usecases/integrated-services.js +20 -4
  729. package/node_modules/@murphai/vault-usecases/dist/usecases/integrated-services.js.map +1 -1
  730. package/node_modules/@murphai/vault-usecases/dist/usecases/intervention-experiment-link.d.ts.map +1 -1
  731. package/node_modules/@murphai/vault-usecases/dist/usecases/intervention-experiment-link.js +49 -24
  732. package/node_modules/@murphai/vault-usecases/dist/usecases/intervention-experiment-link.js.map +1 -1
  733. package/node_modules/@murphai/vault-usecases/dist/usecases/measurement-read.d.ts.map +1 -1
  734. package/node_modules/@murphai/vault-usecases/dist/usecases/measurement-read.js +13 -11
  735. package/node_modules/@murphai/vault-usecases/dist/usecases/measurement-read.js.map +1 -1
  736. package/node_modules/@murphai/vault-usecases/dist/usecases/provider-event.d.ts.map +1 -1
  737. package/node_modules/@murphai/vault-usecases/dist/usecases/provider-event.js +18 -11
  738. package/node_modules/@murphai/vault-usecases/dist/usecases/provider-event.js.map +1 -1
  739. package/node_modules/@murphai/vault-usecases/dist/usecases/recipe.d.ts.map +1 -1
  740. package/node_modules/@murphai/vault-usecases/dist/usecases/recipe.js +43 -1
  741. package/node_modules/@murphai/vault-usecases/dist/usecases/recipe.js.map +1 -1
  742. package/node_modules/@murphai/vault-usecases/dist/usecases/shared.d.ts.map +1 -1
  743. package/node_modules/@murphai/vault-usecases/dist/usecases/shared.js +1 -3
  744. package/node_modules/@murphai/vault-usecases/dist/usecases/shared.js.map +1 -1
  745. package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.d.ts +2 -0
  746. package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.d.ts.map +1 -1
  747. package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.js +24 -1
  748. package/node_modules/@murphai/vault-usecases/dist/usecases/vault-usecase-helpers.js.map +1 -1
  749. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-format.d.ts +2 -0
  750. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-format.d.ts.map +1 -1
  751. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.d.ts +36 -4
  752. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.d.ts.map +1 -1
  753. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.js +48 -12
  754. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-model.js.map +1 -1
  755. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.d.ts +14 -14
  756. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.d.ts.map +1 -1
  757. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.js +84 -82
  758. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live-state.js.map +1 -1
  759. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.d.ts +16 -4
  760. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.d.ts.map +1 -1
  761. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.js +316 -79
  762. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-live.js.map +1 -1
  763. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-read.d.ts.map +1 -1
  764. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-read.js +19 -17
  765. package/node_modules/@murphai/vault-usecases/dist/usecases/workout-read.js.map +1 -1
  766. package/node_modules/@murphai/vault-usecases/dist/usecases/workout.d.ts +20 -7
  767. package/node_modules/@murphai/vault-usecases/dist/usecases/workout.d.ts.map +1 -1
  768. package/node_modules/@murphai/vault-usecases/dist/usecases/workout.js +56 -31
  769. package/node_modules/@murphai/vault-usecases/dist/usecases/workout.js.map +1 -1
  770. package/node_modules/incur/dist/Cli.js +7 -32
  771. package/node_modules/incur/dist/Errors.d.ts +17 -0
  772. package/node_modules/incur/dist/Errors.js +23 -0
  773. package/node_modules/incur/dist/internal/command.d.ts +2 -0
  774. package/node_modules/incur/dist/internal/command.js +2 -6
  775. package/node_modules/incur/src/Cli.ts +11 -32
  776. package/node_modules/incur/src/Errors.ts +36 -0
  777. package/node_modules/incur/src/internal/command.ts +4 -6
  778. package/package.json +4 -4
  779. package/node_modules/@murphai/assistant-engine/skills/nutrition-strategy/references/daily-nutrition-card-safety.md +0 -211
@@ -1,23 +1,25 @@
1
1
  import { createHmac, createHash, timingSafeEqual } from "node:crypto";
2
- import { COMPANION_HRV_RMSSD_RESOURCE, JUNCTION_ALLOWED_SUMMARY_RESOURCES, JUNCTION_ALLOWED_TIMESERIES_RESOURCES, JUNCTION_DEFAULT_SUMMARY_RESOURCES, JUNCTION_DEFAULT_TIMESERIES_RESOURCES, JUNCTION_KNOWN_TIMESERIES_RESOURCES, normalizeJunctionResourceName, resolveJunctionTimeseriesResourcePolicy, usesJunctionTimeseriesIntervalStartOwnership, parseCompanionHrvRmssdAdmissionId, parseSerializedCompanionHrvRmssdObservation, serializeCompanionHrvRmssdObservation, } from "@murphai/contracts";
3
- import { JUNCTION_ECG_VOLTAGE_FEATURE_SCHEMA, JUNCTION_WORKOUT_STREAM_FEATURE_SCHEMA, buildJunctionBoundedFeatureIdentity, buildJunctionDailyTimeseriesAggregateResourceId, canNormalizeJunctionSleepCycleRecordToCompactStages, classifyJunctionSummaryNormalizationEvidence, countAcceptedJunctionDailyTimeseriesProviderRecords, identifyJunctionBloodPressureProviderRecords, JUNCTION_DENSE_FIDELITY_RESOURCES, reduceJunctionElectrocardiogramVoltageRecords, reduceJunctionWorkoutStreamPayload, resolveJunctionBoundedFeatureRecords, resolveJunctionWeightProviderRecordIdentity, selectJunctionWorkoutStreamCandidates, } from "@murphai/importers/device-providers/junction";
2
+ import { performance } from "node:perf_hooks";
3
+ import { COMPANION_HRV_RMSSD_RESOURCE, JUNCTION_ALLOWED_SUMMARY_RESOURCES, JUNCTION_ALLOWED_TIMESERIES_RESOURCES, JUNCTION_DEFAULT_SUMMARY_RESOURCES, JUNCTION_DEFAULT_TIMESERIES_RESOURCES, JUNCTION_KNOWN_TIMESERIES_RESOURCES, normalizeJunctionResourceName, resolveJunctionTimeseriesResourcePolicy, usesJunctionTimeseriesIntervalStartOwnership, parseCompanionHrvRmssdAdmissionId, parseSerializedCompanionHrvRmssdObservation, serializeCompanionHrvRmssdObservation, toLocalDayKey, } from "@murphai/contracts";
4
+ import { JUNCTION_ECG_VOLTAGE_FEATURE_SCHEMA, JUNCTION_WORKOUT_STREAM_FEATURE_SCHEMA, JunctionWorkoutStreamTimestampCardinalityError, buildJunctionBoundedFeatureIdentity, buildJunctionDailyTimeseriesAggregateResourceId, canNormalizeJunctionSleepCycleRecordToCompactStages, classifyJunctionSummaryNormalizationEvidence, countAcceptedJunctionDailyTimeseriesProviderRecords, identifyJunctionBloodPressureProviderRecords, normalizeJunctionCanonicalBodyTimestamp, JUNCTION_DENSE_FIDELITY_RESOURCES, parseJunctionCompleteDayTimestamp, reduceJunctionElectrocardiogramVoltageRecords, reduceJunctionWorkoutStreamPayload, resolveJunctionBoundedFeatureRecords, resolveJunctionWeightProviderRecordIdentity, selectJunctionWorkoutStreamCandidates, } from "@murphai/importers/device-providers/junction";
4
5
  import { normalizeJunctionSourceProviderSlug, resolveJunctionOrigin, } from "@murphai/importers/device-providers/junction-origin";
5
- import { isJunctionRawDirectIdentityContainerKey, isJunctionRawDirectIdentityKey, } from "@murphai/importers/device-providers/junction-resources";
6
+ import { isJunctionRawDirectIdentityContainerKey, isJunctionRawDirectIdentityKey, maxJunctionCanonicalCoverageBoundary, normalizeJunctionCanonicalCoverageBoundary, } from "@murphai/importers/device-providers/junction-resources";
6
7
  import { JUNCTION_DEVICE_PROVIDER_DESCRIPTOR } from "@murphai/importers/device-providers/provider-descriptors";
7
8
  import { deviceSyncError, isDeviceSyncError } from "../errors.js";
8
9
  import { JunctionTimeseriesProgressError } from "../junction-timeseries-progress.js";
9
10
  import { isHostedRuntimeIdShapedDiagnosticToken, sanitizeHostedRuntimeDiagnosticText, } from "../hosted-runtime.js";
10
- import { isJunctionCredentialIndependentInlineImportJob, resolveDeviceSyncJunctionInlineSourceProviderSlug, } from "../junction-inline-authority.js";
11
- import { addJunctionExtendedTimeseriesHistoryBackfillCoverage, addJunctionHistoricalBackfillEvidence, canRepresentJunctionExtendedTimeseriesHistoryBackfillCoverage, canCurrentRuntimeMutateJunctionExtendedTimeseriesHistoryBackfillCoverage, canCurrentRuntimeMutateJunctionHistoricalBackfillProgress, encodeJunctionHistoricalBackfillStatus, hasJunctionExtendedTimeseriesHistoryBackfillCoverage, hasJunctionHistoricalBackfillEvidence, JUNCTION_HISTORICAL_BACKFILL_COVERAGE_VERSION, JUNCTION_HISTORICAL_BACKFILL_METADATA_KEYS, readJunctionHistoricalBackfillEvidence, readJunctionHistoricalBackfillStatus, } from "../junction-historical-backfill-progress.js";
11
+ import { areJunctionProviderSlugsDataEquivalent, classifyDeviceSyncJunctionInlineSourceProviderSlug, isJunctionInlineImportJob, resolveDeviceSyncJunctionInlineSourceProviderSlug, } from "../junction-inline-authority.js";
12
+ import { addJunctionExtendedTimeseriesHistoryBackfillCoverage, addJunctionHistoricalBackfillEvidence, canRepresentJunctionExtendedTimeseriesHistoryBackfillCoverage, canCurrentRuntimeMutateJunctionExtendedTimeseriesHistoryBackfillCoverage, canCurrentRuntimeMutateJunctionHistoricalBackfillProgress, encodeJunctionHistoricalBackfillStatus, hasJunctionExtendedTimeseriesHistoryBackfillCoverage, hasJunctionHistoricalBackfillEvidence, JUNCTION_HISTORICAL_BACKFILL_COVERAGE_VERSION, JUNCTION_HISTORICAL_BACKFILL_METADATA_KEYS, readJunctionHistoricalBackfillEvidence, readJunctionHistoricalBackfillStatus, resolveJunctionExtendedTimeseriesHistoryBackfillVersion, } from "../junction-historical-backfill-progress.js";
12
13
  import { DEVICE_SYNC_METADATA_MAX_STRING_LENGTH } from "../metadata.js";
13
- import { DEVICE_SYNC_HISTORICAL_DATA_RECONNECT_REQUIRED_ERROR_CODE, DEVICE_SYNC_SOURCE_USER_DISCONNECTED_ERROR_CODE, resolveDeviceSyncSourceState, isDeviceSyncSourceAdmitted, isDeviceSyncSourceDisconnectFenced, isJunctionHistoricalResetProviderSlug, requiresHistoricalResetDeviceSyncSource, } from "../public-account.js";
14
+ import { buildDeviceSyncSourceCanonicalCoverageBoundaryKey, buildDeviceSyncSourceCanonicalCoverageFinalizedAtKey, DEVICE_SYNC_SOURCE_HISTORICAL_BACKFILL_COMPLETED_AT_KEY, DEVICE_SYNC_SOURCE_PROVIDER_DISCONNECTED_ERROR_CODE, isGoogleHealthFitbitMigrationLegacyTerminal, isGoogleHealthFitbitMigrationLegacyCoverageReady, isDeviceSyncSourceResourceAvailabilityMetadataKey, readDeviceSyncSourceCanonicalCoverageBoundary, readDeviceSyncSourceCanonicalCoverageFinalizedAt, } from "../fitbit-migration.js";
15
+ import { DEVICE_SYNC_HISTORICAL_DATA_RECONNECT_REQUIRED_ERROR_CODE, DEVICE_SYNC_SOURCE_USER_DISCONNECTED_ERROR_CODE, isDeviceSyncSourceAdmitted, isDeviceSyncSourceDisconnectFenced, isJunctionHistoricalResetProviderSlug, resolveDeviceSyncSourceState, requiresHistoricalResetDeviceSyncSource, } from "../public-account.js";
14
16
  import { assertValidJunctionClientUserIdSecret, buildJunctionDeviceSyncRuntimeDescriptor, normalizeJunctionClientUserIdNamespace, normalizeJunctionDeviceSyncRuntimeConfig, } from "../configured-provider-runtime-descriptors.js";
15
17
  import { addMilliseconds, normalizeString, sha256Text, subtractDays, } from "../shared.js";
16
18
  import { JUNCTION_COMPANION_HRV_OBSERVATION_INVALID_CODE, JUNCTION_COMPANION_HRV_SOURCE_PROVIDER, JUNCTION_COMPANION_HEALTH_METADATA_EVENT_TYPE, JUNCTION_COMPANION_HEALTH_METADATA_MAX_BATCH_BYTES, JUNCTION_COMPANION_HEALTH_METADATA_RESOURCE, JUNCTION_COMPANION_HEALTH_METADATA_SOURCE_PROVIDER, JUNCTION_COMPANION_HEALTH_METADATA_SOURCE_TYPE, JUNCTION_CALENDAR_REFRESH_EMPTY_IDENTITY_INVALID_CODE, JUNCTION_CALENDAR_REFRESH_JOB_INVALID_CODE, JunctionCompanionHealthMetadataParseError, parseJunctionCompanionHealthMetadataBatch, } from "../junction-resources.js";
17
19
  import { JunctionClient, JUNCTION_MAX_USER_PROVIDERS, } from "./junction-client.js";
18
- import { areJunctionDeviceConnectProviderSlugsEquivalent, resolveJunctionDeviceConnectRouteByProviderSlug, } from "../config/connect-routes.js";
20
+ import { canonicalizeJunctionProviderSlug, JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG, JUNCTION_GOOGLE_HEALTH_PROVIDER_SLUG, resolveJunctionDeviceConnectRouteByProviderSlug, } from "../config/connect-routes.js";
19
21
  import { buildJunctionProviderSourceInstanceKey, JUNCTION_DEFAULT_PROVIDER_FILTER, normalizeJunctionProviderFilter, } from "../config/junction-connect-sources.js";
20
- import { classifyDeviceSyncWebhookAcceptanceMode } from "../types.js";
22
+ import { classifyDeviceSyncWebhookAcceptanceMode, JUNCTION_TEMPORAL_AUTHORITY_DEDUPE_PREFIX, } from "../types.js";
21
23
  import { evaluatePushPrimarySourceStaleness } from "../source-staleness.js";
22
24
  import { JUNCTION_PUSH_SOURCE_RECOVERY_JOB_KIND, JUNCTION_PUSH_SOURCE_RECOVERY_METADATA_KEYS, buildJunctionPushSourceRecoveryMetadataPatch, readJunctionPushSourceRecoveryState, resolveJunctionPushSourceRecoveryAttempts, resolveJunctionPushSourceRecoveryStatus, selectDueJunctionPushSourceRecovery, } from "../junction-push-source-recovery.js";
23
25
  export { JUNCTION_DEVICE_PROVIDER_DESCRIPTOR };
@@ -39,7 +41,7 @@ const JUNCTION_WEBHOOK_ROOT_FIELDS = Object.freeze({
39
41
  const JUNCTION_SDK_ISO_8601_DATE_PATTERN = /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
40
42
  const JUNCTION_PROFILE_SUMMARY_RESOURCE = "profile";
41
43
  const JUNCTION_PROFILE_SUMMARY_CHECKED_AT_METADATA_KEY = "junctionProfileSummaryCheckedAt";
42
- const JUNCTION_PROFILE_SUMMARY_NORMALIZATION_REVISION = 1;
44
+ const JUNCTION_PROFILE_SUMMARY_NORMALIZATION_REVISION = 2;
43
45
  const JUNCTION_PROFILE_SUMMARY_NORMALIZATION_REVISION_METADATA_KEY = "junctionProfileSummaryNormalizationRevision";
44
46
  // `profile` is deliberately excluded: it is a current-state snapshot, so
45
47
  // counting it as completion evidence would mark every backfill useful and
@@ -88,7 +90,7 @@ const JUNCTION_KNOWN_WEBHOOK_RESOURCE_NAMES = new Set([
88
90
  ]);
89
91
  const DEFAULT_SUMMARY_BACKFILL_DAYS = JUNCTION_DEVICE_PROVIDER_DESCRIPTOR.sync.windows.backfillDays;
90
92
  const DEFAULT_TIMESERIES_BACKFILL_DAYS = 14;
91
- const JUNCTION_NOTE_HISTORY_BACKFILL_VERSION = 2;
93
+ const JUNCTION_EXTENDED_TIMESERIES_BACKFILL_DAYS = 180;
92
94
  const JUNCTION_BOUNDED_TIMESERIES_BACKFILL_POLICY = Object.freeze({
93
95
  history: "bounded",
94
96
  });
@@ -96,7 +98,6 @@ const JUNCTION_SPARSE_DAILY_TIMESERIES_BACKFILL_POLICY = Object.freeze({
96
98
  anchor: "current_day",
97
99
  completion: "daily_aggregate",
98
100
  history: "extended",
99
- version: 1,
100
101
  });
101
102
  // This is the history-depth decision for every admitted resource. Only the
102
103
  // sparse resources represented by the package-owned source/resource matrix
@@ -109,7 +110,6 @@ const JUNCTION_TIMESERIES_BACKFILL_POLICIES = Object.freeze({
109
110
  anchor: "source_first_seen",
110
111
  completion: "exact_records",
111
112
  history: "extended",
112
- version: 1,
113
113
  },
114
114
  body_temperature: JUNCTION_SPARSE_DAILY_TIMESERIES_BACKFILL_POLICY,
115
115
  body_temperature_delta: JUNCTION_SPARSE_DAILY_TIMESERIES_BACKFILL_POLICY,
@@ -125,7 +125,6 @@ const JUNCTION_TIMESERIES_BACKFILL_POLICIES = Object.freeze({
125
125
  anchor: "current_day",
126
126
  completion: "fetch_complete",
127
127
  history: "extended",
128
- version: JUNCTION_NOTE_HISTORY_BACKFILL_VERSION,
129
128
  },
130
129
  respiratory_rate: JUNCTION_BOUNDED_TIMESERIES_BACKFILL_POLICY,
131
130
  sleep_breathing_disturbance: JUNCTION_SPARSE_DAILY_TIMESERIES_BACKFILL_POLICY,
@@ -137,7 +136,6 @@ const JUNCTION_TIMESERIES_BACKFILL_POLICIES = Object.freeze({
137
136
  anchor: "current_day",
138
137
  completion: "exact_records",
139
138
  history: "extended",
140
- version: 1,
141
139
  },
142
140
  body_mass_index: JUNCTION_BOUNDED_TIMESERIES_BACKFILL_POLICY,
143
141
  carbohydrates: JUNCTION_BOUNDED_TIMESERIES_BACKFILL_POLICY,
@@ -171,7 +169,23 @@ function resolveJunctionExtendedTimeseriesBackfillPolicy(resource) {
171
169
  return null;
172
170
  }
173
171
  const policy = JUNCTION_TIMESERIES_BACKFILL_POLICIES[resource];
174
- return policy.history === "extended" ? policy : null;
172
+ if (policy.history !== "extended") {
173
+ return null;
174
+ }
175
+ const version = resolveJunctionExtendedTimeseriesHistoryBackfillVersion(resource);
176
+ if (version === null) {
177
+ throw new TypeError("Junction extended-history policy requires a coverage generation.");
178
+ }
179
+ return { ...policy, version };
180
+ }
181
+ function requiresJunctionHistoricalPullReadiness(policy) {
182
+ return policy?.anchor === "current_day"
183
+ && (policy.completion === "daily_aggregate" || policy.completion === "exact_records");
184
+ }
185
+ function doesJunctionImportReceiptResolveDeliveredRows(resource) {
186
+ return isJunctionBodyTimeseriesResource(resource)
187
+ || resource === "carbohydrates"
188
+ || resource === "insulin_injection";
175
189
  }
176
190
  const JUNCTION_DENSE_FIDELITY_RESOURCE_SET = new Set(JUNCTION_DENSE_FIDELITY_RESOURCES);
177
191
  const JUNCTION_CALENDAR_DAY_AGGREGATE_RESOURCE_SET = new Set([
@@ -225,6 +239,13 @@ const JUNCTION_FULL_JOB_INVENTORY_COLLECTION_WORK_LIMIT = Object.freeze({
225
239
  maxPages: 1,
226
240
  requestTimeoutMs: 8_000,
227
241
  });
242
+ const JUNCTION_TEMPORAL_AUTHORITY_LAG_MS = TIMESERIES_CHUNK_MS;
243
+ const JUNCTION_TEMPORAL_AUTHORITY_RESOURCES = new Set([
244
+ "blood_oxygen",
245
+ "stress_level",
246
+ ]);
247
+ const JUNCTION_TEMPORAL_AUTHORITY_JOB_VERSION = 1;
248
+ const JUNCTION_MAX_TEMPORAL_RECONCILE_DAYS = 14;
228
249
  const JUNCTION_MAX_DIAGNOSTIC_TIMESERIES_PROBE_DAYS = 14;
229
250
  const JUNCTION_DIAGNOSTIC_SHAPE_KEY_LIMIT = 24;
230
251
  const JUNCTION_DIAGNOSTIC_RESOURCE_NAME_LIMIT = 64;
@@ -234,20 +255,45 @@ const EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS = Object.freeze([
234
255
  6 * 60 * 60_000,
235
256
  24 * 60 * 60_000,
236
257
  ]);
258
+ const JUNCTION_WORKOUT_STREAM_EMPTY_REPLAY_DELAY_MS = 24 * 60 * 60_000;
237
259
  const JUNCTION_HISTORICAL_UNRESOLVED_PROVIDER_RECORD_IDENTITIES_VERSION = 1;
238
260
  const JUNCTION_BLOOD_PRESSURE_PROVIDER_RECORD_IDENTITY_PATTERN = /^blood-pressure-[0-9a-f]{16}$/u;
239
261
  const JUNCTION_SCHEDULED_RECONCILE_PRIORITY = 40;
240
262
  const JUNCTION_HISTORICAL_BACKFILL_PRIORITY = 30;
241
263
  const JUNCTION_HISTORICAL_BACKFILL_RETRY_PRIORITY = 50;
264
+ const JUNCTION_REOPENED_HISTORY_PRIORITY_CYCLE_SLOTS = 4;
265
+ /**
266
+ * Keeps the reopened and ordinary ordinals independent from the 3:1 cadence so
267
+ * every member of either stable pool remains reachable.
268
+ */
269
+ export function selectJunctionScheduledHistoryCandidate(input) {
270
+ if (input.reopenedCandidates.length === 0) {
271
+ return input.ordinaryCandidates.length === 0
272
+ ? undefined
273
+ : input.ordinaryCandidates[input.scheduleSlot % input.ordinaryCandidates.length];
274
+ }
275
+ if (input.ordinaryCandidates.length === 0) {
276
+ return input.reopenedCandidates[input.scheduleSlot % input.reopenedCandidates.length];
277
+ }
278
+ const priorityCycle = Math.floor(input.scheduleSlot / JUNCTION_REOPENED_HISTORY_PRIORITY_CYCLE_SLOTS);
279
+ const priorityCycleOffset = input.scheduleSlot % JUNCTION_REOPENED_HISTORY_PRIORITY_CYCLE_SLOTS;
280
+ if (priorityCycleOffset === JUNCTION_REOPENED_HISTORY_PRIORITY_CYCLE_SLOTS - 1) {
281
+ return input.ordinaryCandidates[priorityCycle % input.ordinaryCandidates.length];
282
+ }
283
+ const reopenedOrdinal = priorityCycle
284
+ * (JUNCTION_REOPENED_HISTORY_PRIORITY_CYCLE_SLOTS - 1)
285
+ + priorityCycleOffset;
286
+ return input.reopenedCandidates[reopenedOrdinal % input.reopenedCandidates.length];
287
+ }
242
288
  export function createJunctionDeviceSyncProvider(config) {
243
289
  const runtimeConfig = normalizeJunctionDeviceSyncRuntimeConfig(config);
244
290
  const client = new JunctionClient(toClientConfig(config));
245
291
  const { clientUserIdNamespace, providerFilter, reconcileIntervalMs, summaryResources, timeseriesResources, } = runtimeConfig;
246
292
  const summaryBackfillDays = config.summaryBackfillDays ?? DEFAULT_SUMMARY_BACKFILL_DAYS;
247
293
  const timeseriesBackfillDays = config.timeseriesBackfillDays ?? DEFAULT_TIMESERIES_BACKFILL_DAYS;
248
- const extendedTimeseriesBackfillDays = config.timeseriesBackfillDays ?? summaryBackfillDays;
249
294
  const extendedBackfillTimeseriesResources = timeseriesResources.filter((resource) => resolveJunctionExtendedTimeseriesBackfillPolicy(resource) !== null);
250
295
  const reconcileDays = config.reconcileDays ?? DEFAULT_RECONCILE_DAYS;
296
+ const temporalReconcileDays = normalizeJunctionTemporalReconcileDays(reconcileDays);
251
297
  const pushSourceRecoveryEnabled = config.pushSourceRecoveryEnabled === true;
252
298
  const webhookTimestampToleranceMs = config.webhookTimestampToleranceMs ?? DEFAULT_WEBHOOK_TIMESTAMP_TOLERANCE_MS;
253
299
  async function beginConnection(context) {
@@ -356,6 +402,14 @@ export function createJunctionDeviceSyncProvider(config) {
356
402
  });
357
403
  }
358
404
  const sourceConnectionWork = buildSourceConnectionWork({
405
+ ...(context.sourceProviderSlug
406
+ ? {
407
+ historicalProofAuthorization: {
408
+ firstSeenAt: context.now,
409
+ sourceProviderSlug: context.sourceProviderSlug,
410
+ },
411
+ }
412
+ : {}),
359
413
  now: context.now,
360
414
  sourceProviderSlug: context.sourceProviderSlug,
361
415
  });
@@ -413,10 +467,16 @@ export function createJunctionDeviceSyncProvider(config) {
413
467
  });
414
468
  }
415
469
  }
416
- async function revokeSourceAccess(account, sourceProviderSlug) {
470
+ async function revokeSourceAccess(account, sourceProviderSlug, options) {
417
471
  const userId = normalizeString(account.externalAccountId);
418
- const targetProviderSlug = normalizeProviderSlug(sourceProviderSlug);
419
- if (!userId || !targetProviderSlug) {
472
+ const targetProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
473
+ const requiredActiveProviderSlug = options?.requiredActiveSourceProviderSlug === undefined
474
+ ? null
475
+ : canonicalizeJunctionProviderSlug(options.requiredActiveSourceProviderSlug);
476
+ if (!userId
477
+ || !targetProviderSlug
478
+ || (options?.requiredActiveSourceProviderSlug !== undefined
479
+ && !requiredActiveProviderSlug)) {
420
480
  throw deviceSyncError({
421
481
  code: "JUNCTION_SOURCE_DEREGISTER_INPUT_INVALID",
422
482
  message: "Junction source cleanup requires a stored user and provider slug.",
@@ -425,20 +485,34 @@ export function createJunctionDeviceSyncProvider(config) {
425
485
  });
426
486
  }
427
487
  const providers = await client.listUserProviders(userId);
428
- const targetIsRegistered = providers.some((provider) => mapJunctionSourceStatus(provider.status) !== "disconnected"
429
- && (normalizeProviderSlug(provider.origin.sourceProviderSlug)
430
- ?? normalizeProviderSlug(provider.slug)) === targetProviderSlug);
431
- if (!targetIsRegistered) {
488
+ const targetStatuses = providers
489
+ .filter((provider) => (canonicalizeJunctionProviderSlug(provider.origin.sourceProviderSlug)
490
+ ?? canonicalizeJunctionProviderSlug(provider.slug)) === targetProviderSlug)
491
+ .map((provider) => mapJunctionSourceStatus(provider.status));
492
+ if (targetStatuses.length === 0
493
+ || targetStatuses.every((status) => status === "disconnected")) {
432
494
  return;
433
495
  }
496
+ if (requiredActiveProviderSlug
497
+ && (!targetStatuses.includes("connected")
498
+ || !providers.some((provider) => mapJunctionSourceStatus(provider.status) === "connected"
499
+ && (canonicalizeJunctionProviderSlug(provider.origin.sourceProviderSlug)
500
+ ?? canonicalizeJunctionProviderSlug(provider.slug)) === requiredActiveProviderSlug))) {
501
+ throw deviceSyncError({
502
+ code: "JUNCTION_REQUIRED_SOURCE_NOT_ACTIVE",
503
+ message: "Junction source cleanup requires an active successor source.",
504
+ retryable: true,
505
+ httpStatus: 409,
506
+ });
507
+ }
434
508
  await client.deregisterProvider({
435
509
  providerSlug: targetProviderSlug,
436
510
  userId,
437
511
  });
438
512
  }
439
- async function isSourceAccessActive(account, sourceProviderSlug) {
513
+ async function isSourceAccessActive(account, sourceProviderSlug, options) {
440
514
  const userId = normalizeString(account.externalAccountId);
441
- const targetProviderSlug = normalizeProviderSlug(sourceProviderSlug);
515
+ const targetProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
442
516
  if (!userId || !targetProviderSlug) {
443
517
  throw deviceSyncError({
444
518
  code: "JUNCTION_SOURCE_STATUS_INPUT_INVALID",
@@ -447,13 +521,33 @@ export function createJunctionDeviceSyncProvider(config) {
447
521
  httpStatus: 409,
448
522
  });
449
523
  }
450
- return (await client.listUserProviders(userId)).some((provider) => mapJunctionSourceStatus(provider.status) === "connected"
451
- && (normalizeProviderSlug(provider.origin.sourceProviderSlug)
452
- ?? normalizeProviderSlug(provider.slug)) === targetProviderSlug);
524
+ const providerStatuses = (await client.listUserProviders(userId))
525
+ .filter((provider) => (canonicalizeJunctionProviderSlug(provider.origin.sourceProviderSlug)
526
+ ?? canonicalizeJunctionProviderSlug(provider.slug)) === targetProviderSlug)
527
+ .map((provider) => provider.status.trim().toLowerCase());
528
+ // Connection authority is the provider row's literal status. The broader
529
+ // projection mapper also accepts lifecycle/resource aliases such as `available`.
530
+ if (providerStatuses.length > 0
531
+ && providerStatuses.every((status) => status === "connected")) {
532
+ return true;
533
+ }
534
+ const statuses = providerStatuses.map(mapJunctionSourceStatus);
535
+ if (statuses.length === 0 || statuses.every((status) => status === "disconnected")) {
536
+ return false;
537
+ }
538
+ if (options?.requireDefinitive) {
539
+ throw deviceSyncError({
540
+ code: "JUNCTION_SOURCE_STATUS_AMBIGUOUS",
541
+ message: "Junction source status is not definitive yet. Retry shortly.",
542
+ retryable: true,
543
+ httpStatus: 503,
544
+ });
545
+ }
546
+ return false;
453
547
  }
454
- function createScheduledJobs(account, now) {
548
+ function createScheduledJobs(account, now, context) {
455
549
  const scheduledHistoricalBackfillJobs = buildScheduledHistoricalBackfillJobs(account, now);
456
- const scheduledExtendedTimeseriesBackfillJobs = buildScheduledExtendedTimeseriesBackfillJobs(account, now);
550
+ const scheduledExtendedTimeseriesBackfillJobs = buildScheduledExtendedTimeseriesBackfillJobs(account, now, context);
457
551
  const nextReconcileAt = resolveJunctionNextReconcileAt(account, now, addMilliseconds(now, reconcileIntervalMs));
458
552
  return {
459
553
  jobs: [
@@ -470,8 +564,8 @@ export function createJunctionDeviceSyncProvider(config) {
470
564
  nextReconcileAt,
471
565
  };
472
566
  }
473
- function buildScheduledExtendedTimeseriesBackfillJobs(account, now) {
474
- return extendedBackfillTimeseriesResources.flatMap((resource) => {
567
+ function buildScheduledExtendedTimeseriesBackfillJobs(account, now, context) {
568
+ const candidates = extendedBackfillTimeseriesResources.flatMap((resource) => {
475
569
  const policy = resolveJunctionExtendedTimeseriesBackfillPolicy(resource);
476
570
  if (!policy) {
477
571
  return [];
@@ -479,11 +573,11 @@ export function createJunctionDeviceSyncProvider(config) {
479
573
  if (!canCurrentRuntimeMutateJunctionExtendedTimeseriesHistoryBackfillCoverage(account.metadata, resource, policy.version)) {
480
574
  return [];
481
575
  }
482
- const scheduledSources = new Map();
576
+ const scheduledSources = [];
483
577
  for (const sourceState of resolveJunctionAccountSourceStates(account.sources ?? [])) {
484
578
  const identitySource = sourceState.identitySource;
485
579
  const lifecycleSource = sourceState.lifecycleSource;
486
- const sourceProviderSlug = normalizeProviderSlug(identitySource.sourceProviderSlug);
580
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(identitySource.sourceProviderSlug);
487
581
  if (!sourceProviderSlug
488
582
  || !isDeviceSyncSourceAdmitted([lifecycleSource], lifecycleSource.sourceProviderSlug)
489
583
  || !isJunctionResourceAvailableInSummary(lifecycleSource.resourceAvailabilitySummary, resource)
@@ -491,33 +585,62 @@ export function createJunctionDeviceSyncProvider(config) {
491
585
  || !canRepresentJunctionExtendedTimeseriesHistoryBackfillCoverage(account.metadata, sourceProviderSlug, resource, policy.version)) {
492
586
  continue;
493
587
  }
494
- const sourceFirstSeenAt = identitySource.firstSeenAt;
495
- if (!sourceFirstSeenAt) {
588
+ const firstSeenAt = identitySource.firstSeenAt;
589
+ if (!firstSeenAt) {
496
590
  throw junctionSourceStateUnavailableError();
497
591
  }
498
- const existingFirstSeenAt = scheduledSources.get(sourceProviderSlug);
499
- if (!existingFirstSeenAt || Date.parse(sourceFirstSeenAt) < Date.parse(existingFirstSeenAt)) {
500
- scheduledSources.set(sourceProviderSlug, sourceFirstSeenAt);
501
- }
592
+ scheduledSources.push({
593
+ firstSeenAt,
594
+ lifecycleEpoch: readJunctionSourceLifecycleEpoch(lifecycleSource),
595
+ sourceProviderSlug,
596
+ });
502
597
  }
503
- return [...scheduledSources.entries()].map(([sourceProviderSlug, firstSeenAt]) => {
598
+ return scheduledSources.map((source) => {
504
599
  // Exact blood-pressure history is anchored to source admission. Other
505
600
  // sparse resources catch up the current extended-history window once.
506
601
  const window = buildExtendedTimeseriesBackfillWindow({
507
- anchorAt: policy.anchor === "source_first_seen" ? firstSeenAt : now,
508
- days: extendedTimeseriesBackfillDays,
602
+ anchorAt: policy.anchor === "source_first_seen" ? source.firstSeenAt : now,
603
+ days: JUNCTION_EXTENDED_TIMESERIES_BACKFILL_DAYS,
509
604
  });
510
- return buildExtendedTimeseriesBackfillJob({
605
+ return {
606
+ anchor: policy.anchor,
511
607
  availableAt: now,
512
- ...(resource === "note" ? { historicalBackfillVersion: policy.version } : {}),
608
+ historicalBackfillVersion: policy.version,
513
609
  historicalWindowStart: window.windowStart,
514
610
  resource,
515
- sourceProviderSlug,
611
+ sourceLifecycleEpoch: source.lifecycleEpoch,
612
+ sourceProviderSlug: source.sourceProviderSlug,
516
613
  windowEnd: window.windowEnd,
517
614
  windowStart: window.windowStart,
518
- });
615
+ };
519
616
  });
520
617
  });
618
+ if (candidates.length === 0) {
619
+ return [];
620
+ }
621
+ candidates.sort((left, right) => left.resource.localeCompare(right.resource)
622
+ || left.sourceProviderSlug.localeCompare(right.sourceProviderSlug));
623
+ const sourceFirstCandidates = candidates.filter((candidate) => candidate.anchor === "source_first_seen");
624
+ const scheduleTimeCandidates = candidates.filter((candidate) => candidate.anchor === "current_day");
625
+ const scheduleSlot = Math.floor(Date.parse(now) / reconcileIntervalMs);
626
+ const scheduleTimeJobs = scheduleTimeCandidates.map((candidate) => [candidate, buildExtendedTimeseriesBackfillJob(candidate)]);
627
+ const activeDedupeKeys = context?.findActiveDedupeKeys(scheduleTimeJobs.map(([, job]) => job.dedupeKey)) ?? new Set();
628
+ const inactiveJobs = scheduleTimeJobs.filter(([, job]) => !activeDedupeKeys.has(job.dedupeKey));
629
+ const reopenedInactiveJobs = context
630
+ ? inactiveJobs.filter(([candidate]) => candidate.sourceLifecycleEpoch > 1)
631
+ : [];
632
+ const ordinaryInactiveJobs = context
633
+ ? inactiveJobs.filter(([candidate]) => candidate.sourceLifecycleEpoch === 1)
634
+ : inactiveJobs;
635
+ const scheduledJob = selectJunctionScheduledHistoryCandidate({
636
+ ordinaryCandidates: ordinaryInactiveJobs,
637
+ reopenedCandidates: reopenedInactiveJobs,
638
+ scheduleSlot,
639
+ })?.[1];
640
+ return [
641
+ ...sourceFirstCandidates.map((candidate) => buildExtendedTimeseriesBackfillJob(candidate)),
642
+ ...(scheduledJob ? [scheduledJob] : []),
643
+ ];
521
644
  }
522
645
  /**
523
646
  * A dead push carrier cannot recover on its own and no pull can rediscover
@@ -640,11 +763,11 @@ export function createJunctionDeviceSyncProvider(config) {
640
763
  }
641
764
  async function loadJunctionHistoricalPullSnapshot(context) {
642
765
  try {
643
- return await client.introspectHistoricalPull({
766
+ return await measureJunctionProviderRequest(context, "inventory", () => client.introspectHistoricalPull({
644
767
  signal: context.signal ?? null,
645
768
  userId: context.account.externalAccountId,
646
769
  userLimit: 1,
647
- });
770
+ }));
648
771
  }
649
772
  catch (error) {
650
773
  if (context.signal?.aborted) {
@@ -664,7 +787,7 @@ export function createJunctionDeviceSyncProvider(config) {
664
787
  * cannot succeed.
665
788
  */
666
789
  async function executePushSourceRecoveryJob(context, job) {
667
- const sourceProviderSlug = normalizeProviderSlug(job.payload.sourceProviderSlug);
790
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(job.payload.sourceProviderSlug);
668
791
  const silentSinceAt = normalizeString(job.payload.silentSinceAt);
669
792
  if (!sourceProviderSlug || !silentSinceAt) {
670
793
  return {};
@@ -715,10 +838,10 @@ export function createJunctionDeviceSyncProvider(config) {
715
838
  let endpointUnavailable = false;
716
839
  let failureCode = null;
717
840
  try {
718
- ({ endpointUnavailable } = await client.bulkTriggerHistoricalPull({
841
+ ({ endpointUnavailable } = await measureJunctionProviderRequest(context, "inventory", () => client.bulkTriggerHistoricalPull({
719
842
  sourceProviderSlug,
720
843
  userIds: [context.account.externalAccountId],
721
- }));
844
+ })));
722
845
  }
723
846
  catch (error) {
724
847
  failureCode = isDeviceSyncError(error)
@@ -748,25 +871,36 @@ export function createJunctionDeviceSyncProvider(config) {
748
871
  }
749
872
  async function executeJob(context, job) {
750
873
  const skippedOptionalResources = [];
874
+ if (job.kind === JUNCTION_PUSH_SOURCE_RECOVERY_JOB_KIND) {
875
+ return executePushSourceRecoveryJob(context, job);
876
+ }
877
+ const historicalProofAuthorization = readJunctionHistoricalProofAuthorization(job);
878
+ if (historicalProofAuthorization
879
+ && !await isJunctionHistoricalProofAuthorizationCurrent(context, historicalProofAuthorization)) {
880
+ return {
881
+ nextReconcileAt: resolveJunctionNextReconcileAt(context.account, context.now, addMilliseconds(context.now, reconcileIntervalMs)),
882
+ };
883
+ }
751
884
  const completedWorkoutStreamIdentities = readJunctionWorkoutStreamCompletedIdentities(job);
752
885
  if (job.kind === "resource") {
753
886
  return executeResourceJob(context, job, skippedOptionalResources, completedWorkoutStreamIdentities);
754
887
  }
755
- if (job.kind === JUNCTION_PUSH_SOURCE_RECOVERY_JOB_KIND) {
756
- return executePushSourceRecoveryJob(context, job);
757
- }
758
888
  if (isFullJobTimeseriesContinuation(job)) {
759
889
  return executeFullJobTimeseriesContinuation(context, job, skippedOptionalResources, completedWorkoutStreamIdentities);
760
890
  }
761
891
  const window = resolveJobWindow(job, context.now, job.kind === "backfill" ? summaryBackfillDays : reconcileDays);
892
+ const sourceProviderSlug = normalizeProviderSlug(job.payload.sourceProviderSlug);
762
893
  const isConnectHistoricalBackfill = isConnectHistoricalBackfillWindow(context.account, window);
763
- const sourceProviders = await client.listUserProviders(context.account.externalAccountId, {
894
+ const listedSourceProviders = await measureJunctionProviderRequest(context, "inventory", () => client.listUserProviders(context.account.externalAccountId, {
764
895
  ...(job.kind === "reconcile" && context.shouldYield
765
896
  ? { collectionWorkLimit: JUNCTION_FULL_JOB_INVENTORY_COLLECTION_WORK_LIMIT }
766
897
  : {}),
767
898
  signal: context.signal ?? null,
768
- });
769
- await projectJunctionSources(context, sourceProviders);
899
+ }));
900
+ await projectJunctionSources(context, listedSourceProviders);
901
+ const sourceProviders = sourceProviderSlug
902
+ ? listedSourceProviders.filter((provider) => areJunctionProviderSlugsDataEquivalent(provider.origin.sourceProviderSlug ?? provider.slug, sourceProviderSlug))
903
+ : listedSourceProviders;
770
904
  const isCurrentSummaryReconcile = job.kind === "reconcile" && isCurrentScheduledClosedWindow(window, context.now, reconcileDays);
771
905
  const summaryWindow = isCurrentSummaryReconcile
772
906
  ? resolveCurrentSummaryWindow(context.now, reconcileDays)
@@ -791,9 +925,21 @@ export function createJunctionDeviceSyncProvider(config) {
791
925
  }
792
926
  }
793
927
  const summaryPhaseComplete = job.kind === "reconcile" && job.payload.summaryPhaseComplete === true;
794
- const summaries = summaryPhaseComplete
795
- ? {}
796
- : await fetchSummarySnapshots(context, summaryWindow.windowStart, summaryWindow.windowEnd, skippedOptionalResources, { dateQueryFormat: summaryDateQueryFormat });
928
+ const summaryFetchResult = summaryPhaseComplete
929
+ ? { providerRecordsSeen: false, snapshots: {} }
930
+ : await fetchSummarySnapshots(context, summaryWindow.windowStart, summaryWindow.windowEnd, skippedOptionalResources, {
931
+ dateQueryFormat: summaryDateQueryFormat,
932
+ sourceProviderSlug,
933
+ });
934
+ const { providerRecordsSeen: historicalSummaryHasFetchedRecords, snapshots: summaries, } = summaryFetchResult;
935
+ const sourceScopedHistoricalSummaryHasRecords = job.kind === "backfill" && sourceProviderSlug
936
+ ? await hasJunctionSourceScopedAdmittedSnapshotRecords({
937
+ context,
938
+ snapshots: summaries,
939
+ sourceProviderSlug,
940
+ sourceProviders,
941
+ })
942
+ : false;
797
943
  const profileSummaryResult = summaryPhaseComplete
798
944
  ? { checked: false, records: [] }
799
945
  : await fetchProfileSummaryOnce(context, skippedOptionalResources);
@@ -818,23 +964,27 @@ export function createJunctionDeviceSyncProvider(config) {
818
964
  windowEnd: summaryWindow.windowEnd,
819
965
  windowStart: summaryWindow.windowStart,
820
966
  });
821
- const historicalSummaryHasRecords = hasJunctionHistoricalBackfillSummaryRecords(summaryNormalizationEvidence);
967
+ const historicalSummaryHasRecords = hasJunctionHistoricalBackfillSummaryRecords(sourceProviderSlug
968
+ ? summaryNormalizationEvidence.filter((evidence) => areJunctionProviderSlugsDataEquivalent(evidence.sourceProviderSlug, sourceProviderSlug))
969
+ : summaryNormalizationEvidence);
822
970
  const baseTimeseriesWindowStart = job.kind === "backfill"
823
971
  ? maxIsoTimestamp(window.windowStart, subtractDays(window.windowEnd, timeseriesBackfillDays))
824
972
  : window.windowStart;
825
973
  if (job.kind !== "backfill" || summaryHasFetchedRecords) {
826
- await context.importSnapshot({
827
- provider: "junction",
828
- accountId: buildJunctionImportAccountId(context.account.externalAccountId),
829
- connectionId: context.account.id,
974
+ await commitPreparedJunctionCanonicalImport(context, preparedSummaryImport, {
830
975
  importedAt: summaryWindow.windowEnd,
831
976
  windowStart: summaryWindow.windowStart,
832
977
  windowEnd: summaryWindow.windowEnd,
833
- connections: importConnections,
834
978
  summaries: importSummaries,
835
979
  timeseries: {},
836
- });
837
- }
980
+ }, context.now);
981
+ }
982
+ const historicalProviderRecordsSeen = sourceProviderSlug !== null
983
+ && (job.payload.historicalProviderRecordsSeen === true
984
+ || historicalSummaryHasFetchedRecords);
985
+ const historicalRecordsSeen = sourceProviderSlug !== null
986
+ && (job.payload.historicalRecordsSeen === true
987
+ || sourceScopedHistoricalSummaryHasRecords);
838
988
  // Current records are durable before the optional historical-status probe.
839
989
  // An unavailable introspection endpoint must not hold fresh ingestion hostage.
840
990
  const historicalPullSnapshot = isConnectHistoricalBackfill && !context.shouldYield?.()
@@ -851,7 +1001,7 @@ export function createJunctionDeviceSyncProvider(config) {
851
1001
  const dailyTimeseriesResources = timeseriesResources.filter((resource) => JUNCTION_CALENDAR_DAY_AGGREGATE_RESOURCE_SET.has(resource));
852
1002
  if (dailyTimeseriesResources.length > 0
853
1003
  && hourlyFidelityWindow) {
854
- const timeseriesImport = await importTimeseriesDailyAggregateSnapshots(context, sourceProviders, hourlyFidelityWindow.windowStart, hourlyFidelityWindow.windowEnd, skippedOptionalResources, dailyTimeseriesResources);
1004
+ const timeseriesImport = await importTimeseriesDailyAggregateSnapshots(context, sourceProviders, hourlyFidelityWindow.windowStart, hourlyFidelityWindow.windowEnd, skippedOptionalResources, dailyTimeseriesResources, sourceProviderSlug);
855
1005
  if (timeseriesImport.yieldedAt) {
856
1006
  return withJunctionSkippedResourceMetadata(context, withJunctionMetadataPatch(buildYieldedJunctionJobResult({
857
1007
  context,
@@ -873,7 +1023,15 @@ export function createJunctionDeviceSyncProvider(config) {
873
1023
  windowEnd: window.windowEnd,
874
1024
  })
875
1025
  : buildNonConnectHistoricalBackfillFollowUp({
876
- hasRecords: historicalSummaryHasRecords,
1026
+ hasRecords: sourceProviderSlug
1027
+ ? historicalRecordsSeen
1028
+ : historicalSummaryHasRecords,
1029
+ ...(sourceProviderSlug
1030
+ ? {
1031
+ historicalProviderRecordsSeen,
1032
+ historicalRecordsSeen,
1033
+ }
1034
+ : {}),
877
1035
  job,
878
1036
  now: context.now,
879
1037
  windowStart: window.windowStart,
@@ -911,32 +1069,128 @@ export function createJunctionDeviceSyncProvider(config) {
911
1069
  await markJunctionHistoricalReconnectRequired(context, historicalSummaryCoverage.reconnectProviderSlugs);
912
1070
  }
913
1071
  }
1072
+ const temporalResources = context.vaultTimeZone
1073
+ ? timeseriesResources.filter((resource) => JUNCTION_TEMPORAL_AUTHORITY_RESOURCES.has(resource))
1074
+ : [];
1075
+ const temporalRecoveryWindows = context.vaultTimeZone
1076
+ ? buildLatestAuthoritativeDailyWindows({
1077
+ horizonDays: temporalReconcileDays,
1078
+ now: context.now,
1079
+ timeZone: context.vaultTimeZone,
1080
+ windowEnd: window.windowEnd,
1081
+ })
1082
+ : [];
1083
+ const newestTemporalWindow = temporalRecoveryWindows[0] ?? null;
1084
+ const deferredNewestTemporalResources = new Set();
1085
+ if (newestTemporalWindow && context.vaultTimeZone) {
1086
+ for (const [resourceIndex, resource] of temporalResources.entries()) {
1087
+ if (context.shouldYield?.()) {
1088
+ for (const remainingResource of temporalResources.slice(resourceIndex)) {
1089
+ deferredNewestTemporalResources.add(remainingResource);
1090
+ }
1091
+ break;
1092
+ }
1093
+ const skippedResourceCountBeforeFetch = skippedOptionalResources.length;
1094
+ try {
1095
+ await importJunctionTimeseriesResourceSnapshot({
1096
+ authorizedLocalDay: {
1097
+ dayKey: newestTemporalWindow.dayKey,
1098
+ timeZone: context.vaultTimeZone,
1099
+ },
1100
+ context,
1101
+ dateQueryFormat: "datetime",
1102
+ resource,
1103
+ skippedOptionalResources,
1104
+ sourceProviders,
1105
+ windowEnd: newestTemporalWindow.windowEnd,
1106
+ windowStart: newestTemporalWindow.windowStart,
1107
+ });
1108
+ }
1109
+ catch (error) {
1110
+ if (isRetryableDeviceSyncFailure(error)
1111
+ || isJunctionJobSignalAbort(error, context.signal)) {
1112
+ deferredNewestTemporalResources.add(resource);
1113
+ if (isJunctionJobSignalAbort(error, context.signal)) {
1114
+ for (const remainingResource of temporalResources.slice(resourceIndex + 1)) {
1115
+ deferredNewestTemporalResources.add(remainingResource);
1116
+ }
1117
+ break;
1118
+ }
1119
+ continue;
1120
+ }
1121
+ throw error;
1122
+ }
1123
+ if (skippedOptionalResources.length > skippedResourceCountBeforeFetch) {
1124
+ deferredNewestTemporalResources.add(resource);
1125
+ }
1126
+ }
1127
+ }
1128
+ const temporalContinuationJobs = context.vaultTimeZone
1129
+ ? [
1130
+ ...(newestTemporalWindow && deferredNewestTemporalResources.size > 0
1131
+ ? buildJunctionTemporalAuthorityJobs({
1132
+ now: context.now,
1133
+ resources: [...deferredNewestTemporalResources],
1134
+ timeZone: context.vaultTimeZone,
1135
+ windows: [newestTemporalWindow],
1136
+ })
1137
+ : []),
1138
+ ...buildJunctionTemporalAuthorityJobs({
1139
+ now: context.now,
1140
+ resources: temporalResources,
1141
+ timeZone: context.vaultTimeZone,
1142
+ windows: temporalRecoveryWindows.slice(1),
1143
+ }),
1144
+ ]
1145
+ : [];
914
1146
  const nextReconcileAt = backfillFollowUp.nextReconcileAt ?? resolveJunctionNextReconcileAt(context.account, context.now, addMilliseconds(context.now, reconcileIntervalMs));
915
1147
  const shouldScheduleTimeseries = timeseriesResources.length > 0
916
1148
  && (job.kind === "backfill"
917
1149
  || shouldImportClosedTimeseriesForReconcile(context.account.lastSyncCompletedAt, window.windowEnd));
918
- const timeseriesContinuation = shouldScheduleTimeseries
1150
+ const historicalProofStillCurrent = !historicalProofAuthorization
1151
+ || await isJunctionHistoricalProofAuthorizationCurrent(context, historicalProofAuthorization);
1152
+ const timeseriesContinuation = shouldScheduleTimeseries && historicalProofStillCurrent
919
1153
  ? buildFullJobTimeseriesContinuationJob({
920
1154
  deferredEmptyBackfillAttempts: readDeferredEmptyBackfillAttempts(backfillFollowUp),
1155
+ historicalProviderRecordsSeen,
1156
+ historicalRecordsSeen,
921
1157
  job,
922
1158
  timeseriesCursor: baseTimeseriesWindowStart,
923
1159
  timeseriesResourceCursor: timeseriesResources[0] ?? null,
924
1160
  timeseriesWindowHours: 24,
925
1161
  window,
926
1162
  workoutStreamCursor: null,
1163
+ workoutStreamEmptySeen: false,
927
1164
  })
928
1165
  : null;
929
- if (timeseriesContinuation) {
1166
+ if (timeseriesContinuation || temporalContinuationJobs.length > 0) {
930
1167
  return withJunctionSkippedResourceMetadata(context, withJunctionMetadataPatch({
931
1168
  ...(backfillFollowUp.metadataPatch
932
1169
  ? { metadataPatch: backfillFollowUp.metadataPatch }
933
1170
  : {}),
934
1171
  nextReconcileAt,
935
- scheduledJobs: [{ ...timeseriesContinuation, availableAt: context.now }],
1172
+ scheduledJobs: [
1173
+ ...temporalContinuationJobs,
1174
+ ...(timeseriesContinuation
1175
+ ? [{ ...timeseriesContinuation, availableAt: context.now }]
1176
+ : []),
1177
+ ],
936
1178
  }, profileMetadataPatch), skippedOptionalResources);
937
1179
  }
1180
+ if (job.kind === "backfill"
1181
+ && historicalProofStillCurrent
1182
+ && !backfillFollowUp.scheduledJobs?.length) {
1183
+ await completeSourceScopedHistoricalBackfill({
1184
+ context,
1185
+ historicalProviderRecordsSeen,
1186
+ historicalRecordsSeen,
1187
+ job,
1188
+ sourceProviderSlug,
1189
+ sourceProviders,
1190
+ });
1191
+ }
938
1192
  return withJunctionSkippedResourceMetadata(context, withJunctionMetadataPatch({
939
- ...backfillFollowUp,
1193
+ ...(historicalProofStillCurrent ? backfillFollowUp : {}),
940
1194
  nextReconcileAt,
941
1195
  }, profileMetadataPatch), skippedOptionalResources);
942
1196
  }
@@ -949,7 +1203,7 @@ export function createJunctionDeviceSyncProvider(config) {
949
1203
  : [];
950
1204
  const existingByInstanceKey = new Map(existingSources.map((source) => [source.sourceInstanceKey, source]));
951
1205
  const recoveryProviderSlugs = new Set(pendingProviderSlugs
952
- .map(normalizeProviderSlug)
1206
+ .map(canonicalizeJunctionProviderSlug)
953
1207
  .filter((providerSlug) => providerSlug !== null));
954
1208
  for (const providerSlug of recoveryProviderSlugs) {
955
1209
  if (!isJunctionHistoricalResetProviderSlug(providerSlug)) {
@@ -962,7 +1216,7 @@ export function createJunctionDeviceSyncProvider(config) {
962
1216
  if (!projectedSourceInstanceKey) {
963
1217
  continue;
964
1218
  }
965
- const existing = existingByInstanceKey.get(projectedSourceInstanceKey) ?? existingSources.find((source) => normalizeProviderSlug(source.sourceProviderSlug) === providerSlug);
1219
+ const existing = existingByInstanceKey.get(projectedSourceInstanceKey) ?? existingSources.find((source) => canonicalizeJunctionProviderSlug(source.sourceProviderSlug) === providerSlug);
966
1220
  if (existing?.status === "error"
967
1221
  && existing.lastErrorCode === DEVICE_SYNC_HISTORICAL_DATA_RECONNECT_REQUIRED_ERROR_CODE) {
968
1222
  continue;
@@ -989,7 +1243,7 @@ export function createJunctionDeviceSyncProvider(config) {
989
1243
  if (!webhookDataJson) {
990
1244
  return null;
991
1245
  }
992
- if (!isJunctionCredentialIndependentInlineImportJob({
1246
+ if (!isJunctionInlineImportJob({
993
1247
  kind: job.kind,
994
1248
  payload: job.payload,
995
1249
  })) {
@@ -1345,7 +1599,7 @@ export function createJunctionDeviceSyncProvider(config) {
1345
1599
  const calendarRefreshDay = readJunctionSparseCalendarRefreshDay(job);
1346
1600
  const resource = normalizeJunctionResourceName(job.payload.resource);
1347
1601
  const resourceCategory = normalizeString(job.payload.resourceCategory);
1348
- const sourceProviderSlug = normalizeProviderSlug(job.payload.sourceProviderSlug);
1602
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(job.payload.sourceProviderSlug);
1349
1603
  if (!calendarRefreshDay
1350
1604
  && sourceProviderSlug
1351
1605
  && !isJunctionSourceAdmittedForImport(context.account.sources ?? [], sourceProviderSlug)
@@ -1360,9 +1614,9 @@ export function createJunctionDeviceSyncProvider(config) {
1360
1614
  if (listedSourceProviders) {
1361
1615
  return listedSourceProviders;
1362
1616
  }
1363
- const sourceProviders = await client.listUserProviders(context.account.externalAccountId, {
1617
+ const sourceProviders = await measureJunctionProviderRequest(context, "inventory", () => client.listUserProviders(context.account.externalAccountId, {
1364
1618
  signal: context.signal ?? null,
1365
- });
1619
+ }));
1366
1620
  listedSourceProviders = sourceProviders;
1367
1621
  return sourceProviders;
1368
1622
  };
@@ -1436,6 +1690,63 @@ export function createJunctionDeviceSyncProvider(config) {
1436
1690
  }, skippedOptionalResources);
1437
1691
  }
1438
1692
  const summaries = {};
1693
+ const temporalAuthorityJob = readJunctionTemporalAuthorityJob(job);
1694
+ if (temporalAuthorityJob) {
1695
+ if (context.shouldYield?.()) {
1696
+ throw deviceSyncError({
1697
+ code: "JUNCTION_TEMPORAL_AUTHORITY_JOB_YIELDED",
1698
+ message: "Junction temporal catch-up yielded before collection.",
1699
+ retryable: true,
1700
+ });
1701
+ }
1702
+ if (!context.vaultTimeZone) {
1703
+ throw deviceSyncError({
1704
+ code: "JUNCTION_TEMPORAL_AUTHORITY_TIMEZONE_UNAVAILABLE",
1705
+ message: "Junction temporal catch-up requires the vault timezone.",
1706
+ retryable: true,
1707
+ });
1708
+ }
1709
+ if (context.vaultTimeZone !== temporalAuthorityJob.timeZone) {
1710
+ return {};
1711
+ }
1712
+ const expectedWindow = resolveVaultLocalDayWindow(temporalAuthorityJob.dayKey, temporalAuthorityJob.timeZone);
1713
+ if (!expectedWindow
1714
+ || expectedWindow.windowStart !== window.windowStart
1715
+ || expectedWindow.windowEnd !== window.windowEnd
1716
+ || Date.parse(expectedWindow.windowEnd) + JUNCTION_TEMPORAL_AUTHORITY_LAG_MS
1717
+ > Date.parse(context.now)) {
1718
+ throw deviceSyncError({
1719
+ code: "JUNCTION_TEMPORAL_AUTHORITY_JOB_INVALID",
1720
+ message: "Junction temporal catch-up job did not describe an eligible complete local day.",
1721
+ retryable: false,
1722
+ });
1723
+ }
1724
+ const skippedResourceCountBeforeFetch = skippedOptionalResources.length;
1725
+ const sourceProviders = await loadAndProjectSourceProviders();
1726
+ await importJunctionTimeseriesResourceSnapshot({
1727
+ authorizedLocalDay: {
1728
+ dayKey: temporalAuthorityJob.dayKey,
1729
+ timeZone: temporalAuthorityJob.timeZone,
1730
+ },
1731
+ context,
1732
+ dateQueryFormat: "datetime",
1733
+ resource: temporalAuthorityJob.resource,
1734
+ skippedOptionalResources,
1735
+ sourceProviders,
1736
+ windowEnd: expectedWindow.windowEnd,
1737
+ windowStart: expectedWindow.windowStart,
1738
+ });
1739
+ if (skippedOptionalResources.length > skippedResourceCountBeforeFetch) {
1740
+ throw deviceSyncError({
1741
+ code: "JUNCTION_TEMPORAL_AUTHORITY_RESOURCE_UNAVAILABLE",
1742
+ message: "Junction temporal catch-up resource was unavailable.",
1743
+ retryable: true,
1744
+ });
1745
+ }
1746
+ return {
1747
+ nextReconcileAt: clampWebhookJobNextReconcileAt(context),
1748
+ };
1749
+ }
1439
1750
  if (resource) {
1440
1751
  const directInput = readJunctionDirectResourceJobInput(job, window);
1441
1752
  if (directInput) {
@@ -1522,6 +1833,13 @@ export function createJunctionDeviceSyncProvider(config) {
1522
1833
  retryable: false,
1523
1834
  });
1524
1835
  }
1836
+ const sourceLifecycleEpoch = extendedHistoricalPolicy
1837
+ ? readSafeInteger(job.payload.sourceLifecycleEpoch)
1838
+ : null;
1839
+ if (extendedHistoricalPolicy
1840
+ && (sourceLifecycleEpoch === null || sourceLifecycleEpoch < 1)) {
1841
+ return {};
1842
+ }
1525
1843
  const historicalWindowStart = toIsoTimestampIfValid(normalizeString(job.payload.historicalWindowStart))
1526
1844
  ?? window.windowStart;
1527
1845
  const dailyAggregateRetryAttempts = readHistoricalBackfillJobEmptyAttempts(job);
@@ -1529,12 +1847,20 @@ export function createJunctionDeviceSyncProvider(config) {
1529
1847
  let sourceIdentityAuthority;
1530
1848
  let currentSourceAdmission = "admitted";
1531
1849
  try {
1850
+ if (extendedHistoricalBackfill
1851
+ && sourceProviderSlug
1852
+ && sourceLifecycleEpoch !== null) {
1853
+ currentSourceAdmission = await resolveJunctionCurrentSourceAdmission(context, sourceProviderSlug, sourceLifecycleEpoch ?? undefined);
1854
+ if (currentSourceAdmission === "fenced") {
1855
+ return {};
1856
+ }
1857
+ }
1532
1858
  sourceProviders = await loadAndProjectSourceProviders();
1533
1859
  if (extendedHistoricalBackfill && sourceProviderSlug) {
1534
1860
  sourceIdentityAuthority = context.listConnectionSources
1535
1861
  ? await context.listConnectionSources()
1536
1862
  : context.account.sources ?? [];
1537
- currentSourceAdmission = resolveJunctionCurrentSourceAdmissionFromSources(sourceIdentityAuthority, sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only");
1863
+ currentSourceAdmission = resolveJunctionCurrentSourceAdmissionFromSources(sourceIdentityAuthority, sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only", sourceLifecycleEpoch ?? undefined);
1538
1864
  }
1539
1865
  }
1540
1866
  catch (error) {
@@ -1608,7 +1934,7 @@ export function createJunctionDeviceSyncProvider(config) {
1608
1934
  window,
1609
1935
  });
1610
1936
  }
1611
- if (extendedHistoricalPolicy?.completion === "daily_aggregate"
1937
+ if (requiresJunctionHistoricalPullReadiness(extendedHistoricalPolicy)
1612
1938
  && window.windowStart === historicalWindowStart) {
1613
1939
  const historicalPullReadiness = resolveJunctionHistoricalPullReadiness({
1614
1940
  resource: effectiveResource,
@@ -1643,12 +1969,8 @@ export function createJunctionDeviceSyncProvider(config) {
1643
1969
  const retryDelayMs = EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS.at(-1) ?? 0;
1644
1970
  return {
1645
1971
  nextReconcileAt: clampWebhookJobNextReconcileAt(context),
1646
- scheduledJobs: [buildExtendedTimeseriesBackfillJob({
1972
+ scheduledJobs: [buildExtendedTimeseriesBackfillFollowUp(job, {
1647
1973
  availableAt: addMilliseconds(context.now, retryDelayMs),
1648
- dedupeKey: job.dedupeKey,
1649
- historicalWindowStart,
1650
- resource: effectiveResource,
1651
- sourceProviderSlug,
1652
1974
  windowEnd: window.windowEnd,
1653
1975
  windowStart: window.windowStart,
1654
1976
  })],
@@ -1674,7 +1996,7 @@ export function createJunctionDeviceSyncProvider(config) {
1674
1996
  if (timeseriesPolicy?.historyWindow === "dense_timeseries"
1675
1997
  && (timeseriesPolicy.enabledByDefault === false
1676
1998
  || JUNCTION_CLOSED_DAY_TIMESERIES_RESOURCES.has(effectiveResource))) {
1677
- const dailyImport = await importTimeseriesDailySnapshots(context, sourceProviders, window.windowStart, window.windowEnd, skippedOptionalResources, effectiveResource, sourceProviderSlug, completedWorkoutStreamIdentities);
1999
+ const dailyImport = await importTimeseriesDailySnapshots(context, sourceProviders, window.windowStart, window.windowEnd, skippedOptionalResources, effectiveResource, sourceProviderSlug, completedWorkoutStreamIdentities, job.payload.workoutStreamEmptySeen === true);
1678
2000
  const result = withJunctionSkippedResourceMetadata(context, dailyImport.yieldedAt
1679
2001
  ? buildYieldedJunctionJobResult({
1680
2002
  context,
@@ -1684,9 +2006,41 @@ export function createJunctionDeviceSyncProvider(config) {
1684
2006
  workoutStreamCursor: effectiveResource === "workout_stream"
1685
2007
  ? dailyImport.workoutStreamCursor
1686
2008
  : undefined,
2009
+ workoutStreamEmptySeen: dailyImport.workoutStreamEmptySeen,
2010
+ })
2011
+ : { nextReconcileAt: clampWebhookJobNextReconcileAt(context) }, skippedOptionalResources);
2012
+ return withJunctionWorkoutStreamEmptyReplay({
2013
+ job,
2014
+ now: context.now,
2015
+ replayWindow: dailyImport.emptyWorkoutStreamReplayWindow,
2016
+ result,
2017
+ sourceProviderSlug,
2018
+ });
2019
+ }
2020
+ if (!extendedHistoricalBackfill
2021
+ && timeseriesPolicy?.maxCanonicalRecordsPerWindow !== undefined) {
2022
+ const [boundedWindow] = buildPreciseTimeseriesWindows(window.windowStart, window.windowEnd);
2023
+ if (!boundedWindow) {
2024
+ return withJunctionSkippedResourceMetadata(context, { nextReconcileAt: clampWebhookJobNextReconcileAt(context) }, skippedOptionalResources);
2025
+ }
2026
+ await importJunctionTimeseriesResourceSnapshot({
2027
+ context,
2028
+ dateQueryFormat: "datetime",
2029
+ resource: effectiveResource,
2030
+ skippedOptionalResources,
2031
+ sourceProviderSlug,
2032
+ sourceProviders,
2033
+ windowEnd: boundedWindow.windowEnd,
2034
+ windowStart: boundedWindow.windowStart,
2035
+ });
2036
+ return withJunctionSkippedResourceMetadata(context, Date.parse(boundedWindow.windowEnd) < Date.parse(window.windowEnd)
2037
+ ? buildYieldedJunctionJobResult({
2038
+ context,
2039
+ job,
2040
+ windowEnd: window.windowEnd,
2041
+ windowStart: boundedWindow.windowEnd,
1687
2042
  })
1688
2043
  : { nextReconcileAt: clampWebhookJobNextReconcileAt(context) }, skippedOptionalResources);
1689
- return result;
1690
2044
  }
1691
2045
  const timeseriesImport = await importTimeseriesPreciseSnapshots(context, sourceProviders, window.windowStart, window.windowEnd, skippedOptionalResources, [effectiveResource], sourceProviderSlug, {
1692
2046
  dateQueryFormat: extendedHistoricalPolicy?.completion === "daily_aggregate"
@@ -1695,6 +2049,9 @@ export function createJunctionDeviceSyncProvider(config) {
1695
2049
  historicalProviderRecordsSeen: job.payload.historicalProviderRecordsSeen === true,
1696
2050
  preservePartialRetryableFailure: extendedHistoricalBackfill,
1697
2051
  ...(sourceIdentityAuthority ? { sourceIdentityAuthority } : {}),
2052
+ sourceLifecycleEpoch: extendedHistoricalBackfill
2053
+ ? sourceLifecycleEpoch ?? undefined
2054
+ : undefined,
1698
2055
  sourceStatusRequirement: extendedHistoricalBackfill
1699
2056
  ? "connected"
1700
2057
  : undefined,
@@ -1732,7 +2089,7 @@ export function createJunctionDeviceSyncProvider(config) {
1732
2089
  const historicalRecordsSeen = extendedHistoricalBackfill
1733
2090
  ? job.payload.historicalRecordsSeen === true
1734
2091
  || timeseriesImport.canonicalEventCount > 0
1735
- || (effectiveResource === "weight"
2092
+ || (doesJunctionImportReceiptResolveDeliveredRows(effectiveResource)
1736
2093
  && timeseriesImport.providerRecordsExamined)
1737
2094
  : undefined;
1738
2095
  const dailyAggregateNeedsRetry = extendedHistoricalPolicy?.completion === "daily_aggregate"
@@ -1744,14 +2101,12 @@ export function createJunctionDeviceSyncProvider(config) {
1744
2101
  const retryDelayMs = EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS[retryAttempts - 1] ?? 0;
1745
2102
  return finalizePreciseImportResult(withJunctionSkippedResourceMetadata(context, {
1746
2103
  nextReconcileAt: clampWebhookJobNextReconcileAt(context),
1747
- scheduledJobs: [buildExtendedTimeseriesBackfillJob({
2104
+ scheduledJobs: [buildExtendedTimeseriesBackfillFollowUp(job, {
1748
2105
  availableAt: addMilliseconds(context.now, retryDelayMs),
1749
- dedupeKey: job.dedupeKey,
1750
- emptyBackfillAttempts: retryAttempts,
1751
- historicalRecordsSeen,
1752
- historicalWindowStart,
1753
- resource: effectiveResource,
1754
- sourceProviderSlug,
2106
+ payload: {
2107
+ emptyBackfillAttempts: retryAttempts,
2108
+ historicalRecordsSeen,
2109
+ },
1755
2110
  windowEnd: window.windowEnd,
1756
2111
  windowStart: window.windowStart,
1757
2112
  })],
@@ -1759,7 +2114,11 @@ export function createJunctionDeviceSyncProvider(config) {
1759
2114
  }
1760
2115
  const historicalUnresolvedProviderRecords = extendedHistoricalBackfill
1761
2116
  && extendedHistoricalPolicy?.completion !== "daily_aggregate"
1762
- ? resolveJunctionHistoricalUnresolvedProviderRecords(job, timeseriesImport)
2117
+ ? isJunctionBodyTimeseriesResource(effectiveResource)
2118
+ && timeseriesImport.fetchComplete
2119
+ && timeseriesImport.unresolvedProviderRecordCount === 0
2120
+ ? { identities: [], withoutStableIdentity: false }
2121
+ : resolveJunctionHistoricalUnresolvedProviderRecords(job, timeseriesImport)
1763
2122
  : undefined;
1764
2123
  const historicalUnresolvedProviderRecordCount = historicalUnresolvedProviderRecords === undefined
1765
2124
  ? undefined
@@ -1789,7 +2148,7 @@ export function createJunctionDeviceSyncProvider(config) {
1789
2148
  const result = withJunctionSkippedResourceMetadata(context, {
1790
2149
  nextReconcileAt: clampWebhookJobNextReconcileAt(context),
1791
2150
  }, skippedOptionalResources);
1792
- const historicalPullReadiness = extendedHistoricalPolicy?.completion === "daily_aggregate"
2151
+ const historicalPullReadiness = requiresJunctionHistoricalPullReadiness(extendedHistoricalPolicy)
1793
2152
  && timeseriesImport.fetchComplete
1794
2153
  ? resolveJunctionHistoricalPullReadiness({
1795
2154
  resource: effectiveResource,
@@ -1797,6 +2156,13 @@ export function createJunctionDeviceSyncProvider(config) {
1797
2156
  sourceProviderSlug,
1798
2157
  })
1799
2158
  : undefined;
2159
+ if (extendedHistoricalBackfill
2160
+ && extendedHistoricalPolicy?.anchor === "current_day"
2161
+ && sourceProviderSlug
2162
+ && sourceLifecycleEpoch !== null
2163
+ && await resolveJunctionCurrentSourceAdmission(context, sourceProviderSlug, sourceLifecycleEpoch) !== "admitted") {
2164
+ return {};
2165
+ }
1800
2166
  return finalizePreciseImportResult(withJunctionHistoricalCoverageVerification(context, job, window, withJunctionExtendedTimeseriesBackfillFollowUp({
1801
2167
  context,
1802
2168
  historicalPullReadiness,
@@ -1820,17 +2186,13 @@ export function createJunctionDeviceSyncProvider(config) {
1820
2186
  }
1821
2187
  const sourceProviders = await loadAndProjectSourceProviders();
1822
2188
  const preparedImport = await prepareJunctionImportSnapshot(context, summaries, sourceProviders);
1823
- await context.importSnapshot({
1824
- provider: "junction",
1825
- accountId: buildJunctionImportAccountId(context.account.externalAccountId),
1826
- connectionId: context.account.id,
2189
+ await commitPreparedJunctionCanonicalImport(context, preparedImport, {
1827
2190
  importedAt: context.now,
1828
2191
  windowStart: window.windowStart,
1829
2192
  windowEnd: window.windowEnd,
1830
- connections: preparedImport.connections,
1831
2193
  summaries: preparedImport.snapshots,
1832
2194
  timeseries: {},
1833
- });
2195
+ }, context.now);
1834
2196
  return withJunctionHistoricalCoverageVerification(context, job, window, withJunctionSkippedResourceMetadata(context, {
1835
2197
  nextReconcileAt: clampWebhookJobNextReconcileAt(context),
1836
2198
  }, skippedOptionalResources));
@@ -1881,14 +2243,14 @@ export function createJunctionDeviceSyncProvider(config) {
1881
2243
  if (!policy) {
1882
2244
  return input.result;
1883
2245
  }
1884
- const sourceProviderSlug = normalizeProviderSlug(input.job.payload.sourceProviderSlug);
2246
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.job.payload.sourceProviderSlug);
1885
2247
  const historicalWindowStart = toIsoTimestampIfValid(normalizeString(input.job.payload.historicalWindowStart))
1886
2248
  ?? input.window.windowStart;
1887
2249
  const recordsSeen = input.job.payload.historicalRecordsSeen === true
1888
2250
  || input.importResult.canonicalEventCount > 0
1889
- || (input.resource === "weight"
2251
+ || (doesJunctionImportReceiptResolveDeliveredRows(input.resource)
1890
2252
  && input.importResult.providerRecordsExamined);
1891
- const unresolvedProviderRecords = input.resource === "weight"
2253
+ const unresolvedProviderRecords = isJunctionBodyTimeseriesResource(input.resource)
1892
2254
  && input.importResult.fetchComplete
1893
2255
  && input.importResult.unresolvedProviderRecordCount === 0
1894
2256
  ? { identities: [], withoutStableIdentity: false }
@@ -1896,22 +2258,20 @@ export function createJunctionDeviceSyncProvider(config) {
1896
2258
  const unresolvedProviderRecordCount = countJunctionHistoricalUnresolvedProviderRecords(unresolvedProviderRecords);
1897
2259
  const unresolvedProviderRecordIdentitiesJson = encodeJunctionHistoricalUnresolvedProviderRecords(unresolvedProviderRecords);
1898
2260
  const unresolvedProviderRecordsSeen = unresolvedProviderRecordCount > 0;
1899
- if (policy.completion === "daily_aggregate" && input.importResult.fetchComplete) {
2261
+ if (requiresJunctionHistoricalPullReadiness(policy) && input.importResult.fetchComplete) {
1900
2262
  const historicalPullReadiness = input.historicalPullReadiness ?? "unavailable";
1901
2263
  const currentReconcileWindowStart = floorUtcDayTimestamp(subtractDays(floorUtcDayTimestamp(input.context.now), reconcileDays));
1902
2264
  if (historicalPullReadiness === "pending"
1903
- || (historicalPullReadiness === "unavailable" && !recordsSeen)) {
2265
+ || (policy.completion === "daily_aggregate"
2266
+ && historicalPullReadiness === "unavailable"
2267
+ && !recordsSeen)) {
1904
2268
  const retryDelayMs = EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS.at(-1) ?? 0;
1905
2269
  return {
1906
2270
  ...input.result,
1907
2271
  scheduledJobs: [
1908
2272
  ...(input.result.scheduledJobs ?? []),
1909
- buildExtendedTimeseriesBackfillJob({
2273
+ buildExtendedTimeseriesBackfillFollowUp(input.job, {
1910
2274
  availableAt: addMilliseconds(input.context.now, retryDelayMs),
1911
- dedupeKey: input.job.dedupeKey,
1912
- historicalWindowStart,
1913
- resource: input.resource,
1914
- sourceProviderSlug,
1915
2275
  windowEnd: maxIsoTimestamp(input.window.windowEnd, currentReconcileWindowStart),
1916
2276
  windowStart: historicalWindowStart,
1917
2277
  }),
@@ -1923,13 +2283,9 @@ export function createJunctionDeviceSyncProvider(config) {
1923
2283
  ...input.result,
1924
2284
  scheduledJobs: [
1925
2285
  ...(input.result.scheduledJobs ?? []),
1926
- buildExtendedTimeseriesBackfillJob({
2286
+ buildExtendedTimeseriesBackfillFollowUp(input.job, {
1927
2287
  availableAt: input.context.now,
1928
- dedupeKey: input.job.dedupeKey,
1929
- historicalRecordsSeen: recordsSeen,
1930
- historicalWindowStart,
1931
- resource: input.resource,
1932
- sourceProviderSlug,
2288
+ payload: { historicalRecordsSeen: recordsSeen },
1933
2289
  windowEnd: currentReconcileWindowStart,
1934
2290
  windowStart: input.window.windowEnd,
1935
2291
  }),
@@ -1939,7 +2295,10 @@ export function createJunctionDeviceSyncProvider(config) {
1939
2295
  if (historicalPullReadiness === "terminal_failure") {
1940
2296
  return input.result;
1941
2297
  }
1942
- return withJunctionMetadataPatch(input.result, buildJunctionExtendedTimeseriesBackfillCompletionMetadataPatch(input.context, input.job, input.resource, sourceProviderSlug));
2298
+ if (policy.completion === "daily_aggregate"
2299
+ || historicalPullReadiness !== "unavailable") {
2300
+ return withJunctionMetadataPatch(input.result, buildJunctionExtendedTimeseriesBackfillCompletionMetadataPatch(input.context, input.job, input.resource, sourceProviderSlug));
2301
+ }
1943
2302
  }
1944
2303
  // A complete note scan can close its one-time source coverage even when
1945
2304
  // no action-like tags exist. Neutral wearable-tag notes, including a
@@ -1958,7 +2317,8 @@ export function createJunctionDeviceSyncProvider(config) {
1958
2317
  let retryDelayMs = EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS[emptyBackfillAttempts - 1]
1959
2318
  ?? null;
1960
2319
  if (retryDelayMs === null
1961
- && (!input.importResult.fetchComplete || unresolvedProviderRecordsSeen)) {
2320
+ && (!input.importResult.fetchComplete
2321
+ || unresolvedProviderRecords.identities.length > 0)) {
1962
2322
  emptyBackfillAttempts = EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS.length;
1963
2323
  retryDelayMs =
1964
2324
  EMPTY_HISTORICAL_BACKFILL_RETRY_DELAYS_MS[emptyBackfillAttempts - 1]
@@ -1971,22 +2331,15 @@ export function createJunctionDeviceSyncProvider(config) {
1971
2331
  ...input.result,
1972
2332
  scheduledJobs: [
1973
2333
  ...(input.result.scheduledJobs ?? []),
1974
- buildExtendedTimeseriesBackfillJob({
2334
+ buildExtendedTimeseriesBackfillFollowUp(input.job, {
1975
2335
  availableAt: addMilliseconds(input.context.now, retryDelayMs),
1976
- dedupeKey: input.job.dedupeKey,
1977
- emptyBackfillAttempts,
1978
- ...(input.resource === "note"
1979
- ? {
1980
- historicalBackfillVersion: readJunctionNoteHistoryBackfillVersion(input.job.payload),
1981
- }
1982
- : {}),
1983
- historicalProviderRecordsSeen: unresolvedProviderRecordsSeen,
1984
- historicalRecordsSeen: recordsSeen,
1985
- historicalUnresolvedProviderRecordIdentitiesJson: unresolvedProviderRecordIdentitiesJson,
1986
- historicalUnresolvedProviderRecordCount: unresolvedProviderRecordCount,
1987
- historicalWindowStart,
1988
- resource: input.resource,
1989
- sourceProviderSlug,
2336
+ payload: {
2337
+ emptyBackfillAttempts,
2338
+ historicalProviderRecordsSeen: unresolvedProviderRecordsSeen,
2339
+ historicalRecordsSeen: recordsSeen,
2340
+ historicalUnresolvedProviderRecordIdentitiesJson: unresolvedProviderRecordIdentitiesJson,
2341
+ historicalUnresolvedProviderRecordCount: unresolvedProviderRecordCount || undefined,
2342
+ },
1990
2343
  windowEnd: input.window.windowEnd,
1991
2344
  windowStart: input.importResult.fetchComplete
1992
2345
  ? historicalWindowStart
@@ -2006,9 +2359,7 @@ export function createJunctionDeviceSyncProvider(config) {
2006
2359
  if (!canCurrentRuntimeMutateJunctionExtendedTimeseriesHistoryBackfillCoverage(context.account.metadata, resource, policy.version)) {
2007
2360
  return {};
2008
2361
  }
2009
- const admittedVersion = resource === "note"
2010
- ? readJunctionNoteHistoryBackfillVersion(job.payload)
2011
- : policy.version;
2362
+ const admittedVersion = readJunctionHistoricalBackfillVersion(job.payload);
2012
2363
  if (admittedVersion !== policy.version) {
2013
2364
  // A semantic generation can only certify its own coverage. Older jobs
2014
2365
  // may still import facts without closing the current generation's bit.
@@ -2070,7 +2421,7 @@ export function createJunctionDeviceSyncProvider(config) {
2070
2421
  const acceptedCanonicalDelivery = importResult.durableDeliveryAccepted
2071
2422
  && providerSlug !== null
2072
2423
  && importResult.normalizationEvidence.some((entry) => entry.resource === directInput.resource
2073
- && canonicalizeJunctionHistoricalProviderSlug(entry.sourceProviderSlug) === providerSlug);
2424
+ && canonicalizeJunctionProviderSlug(entry.sourceProviderSlug) === providerSlug);
2074
2425
  if (!canCurrentRuntimeMutateJunctionHistoricalBackfillProgress(context.account.metadata)
2075
2426
  || !directHistoricalWindow
2076
2427
  || !acceptedCanonicalDelivery
@@ -2124,19 +2475,29 @@ export function createJunctionDeviceSyncProvider(config) {
2124
2475
  const data = readPlainObject(verified.payload[JUNCTION_WEBHOOK_ROOT_FIELDS.data]);
2125
2476
  const externalAccountSelection = requireJunctionWebhookUserIdSelection(verified.payload, data);
2126
2477
  const resource = inferJunctionWebhookResource(eventType, data);
2127
- const sourceProviderSlug = extractJunctionWebhookSourceProviderSlug(data);
2478
+ const envelopeSourceProviderSlug = extractJunctionWebhookSourceProviderSlug(data);
2128
2479
  const objectId = extractJunctionWebhookObjectId(data);
2129
- const eventOccurredAt = extractJunctionWebhookOccurredAt(data);
2480
+ const eventOccurredAt = extractJunctionWebhookOccurredAt(data, resource);
2130
2481
  const occurredAt = eventOccurredAt ?? context.now;
2131
2482
  const window = buildJunctionWebhookWindow(data, occurredAt, context.now, resource);
2483
+ const historicalPullCompleted = isJunctionHistoricalDataEvent(eventType)
2484
+ && data !== null
2485
+ && isJunctionHistoricalPullCompletedWebhookData(data, externalAccountSelection.userId);
2486
+ const dataSourceProviderSlug = resolveJunctionWebhookDataSourceProviderSlug({
2487
+ data,
2488
+ envelopeSourceProviderSlug,
2489
+ eventType,
2490
+ historicalPullCompleted,
2491
+ });
2132
2492
  const webhookDataJsons = buildJunctionWebhookDataJobJsons({
2133
2493
  data,
2134
2494
  eventType,
2135
2495
  externalAccountId: externalAccountSelection.userId,
2136
2496
  resource,
2137
2497
  summaryResources,
2138
- sourceProviderSlug,
2498
+ sourceProviderSlug: dataSourceProviderSlug,
2139
2499
  });
2500
+ const sourceProviderSlug = dataSourceProviderSlug ?? envelopeSourceProviderSlug;
2140
2501
  const jobs = buildJunctionWebhookJobs({
2141
2502
  eventType,
2142
2503
  objectId,
@@ -2160,18 +2521,14 @@ export function createJunctionDeviceSyncProvider(config) {
2160
2521
  // A historical-pull completion is a data-less notification, so accepting
2161
2522
  // its fetch job proves nothing arrived. Treating it as delivery would
2162
2523
  // refresh the arrival signal and hide the very stall this detects.
2163
- dataSourceProviderSlug: isJunctionDataEvent(eventType)
2164
- && !(isJunctionHistoricalDataEvent(eventType)
2165
- && data !== null
2166
- && isJunctionHistoricalPullCompletedWebhookData(data, externalAccountSelection.userId))
2167
- ? sourceProviderSlug
2168
- : null,
2524
+ dataSourceProviderSlug,
2169
2525
  jobs,
2170
2526
  unknownAccountAction: "accept",
2171
2527
  };
2172
2528
  }
2173
2529
  async function fetchSummarySnapshots(context, windowStart, windowEnd, skippedOptionalResources, options = {}) {
2174
2530
  const snapshots = {};
2531
+ let providerRecordsSeen = false;
2175
2532
  for (const resource of summaryResources) {
2176
2533
  if (isJunctionProfileSummaryResource(resource)) {
2177
2534
  continue;
@@ -2179,6 +2536,7 @@ export function createJunctionDeviceSyncProvider(config) {
2179
2536
  const request = {
2180
2537
  resource,
2181
2538
  signal: context.signal ?? null,
2539
+ sourceProviderSlug: options.sourceProviderSlug,
2182
2540
  userId: context.account.externalAccountId,
2183
2541
  windowStart,
2184
2542
  windowEnd,
@@ -2186,9 +2544,11 @@ export function createJunctionDeviceSyncProvider(config) {
2186
2544
  if (options.dateQueryFormat) {
2187
2545
  request.dateQueryFormat = options.dateQueryFormat;
2188
2546
  }
2189
- snapshots[resource] = await fetchOptionalJunctionResourceRecords(context, "summary", resource, skippedOptionalResources, () => client.listSummary(request));
2547
+ const records = await fetchOptionalJunctionResourceRecords(context, "summary", resource, skippedOptionalResources, () => client.listSummary(request));
2548
+ providerRecordsSeen ||= records.length > 0;
2549
+ snapshots[resource] = scopeJunctionRecordsToSourceProvider(records, options.sourceProviderSlug);
2190
2550
  }
2191
- return snapshots;
2551
+ return { providerRecordsSeen, snapshots };
2192
2552
  }
2193
2553
  async function executeBoundedSummaryReconcile(input) {
2194
2554
  const profileAlreadyChecked = hasCheckedJunctionProfileSummary(input.context.account.metadata);
@@ -2239,17 +2599,13 @@ export function createJunctionDeviceSyncProvider(config) {
2239
2599
  }
2240
2600
  }
2241
2601
  const preparedSummaryImport = await prepareJunctionImportSnapshot(input.context, summaries, input.sourceProviders);
2242
- await input.context.importSnapshot({
2243
- provider: "junction",
2244
- accountId: buildJunctionImportAccountId(input.context.account.externalAccountId),
2245
- connectionId: input.context.account.id,
2602
+ await commitPreparedJunctionCanonicalImport(input.context, preparedSummaryImport, {
2246
2603
  importedAt: input.summaryWindow.windowEnd,
2247
2604
  windowStart: input.summaryWindow.windowStart,
2248
2605
  windowEnd: input.summaryWindow.windowEnd,
2249
- connections: preparedSummaryImport.connections,
2250
2606
  summaries: preparedSummaryImport.snapshots,
2251
2607
  timeseries: {},
2252
- });
2608
+ }, input.context.now);
2253
2609
  const nextResource = eligibleUnits[cursorIndex >= 0 ? cursorIndex + 1 : 1]?.[0] ?? null;
2254
2610
  const sourceProviderSlug = normalizeProviderSlug(input.job.payload.sourceProviderSlug);
2255
2611
  const continuation = buildExactWindowJob({
@@ -2287,12 +2643,14 @@ export function createJunctionDeviceSyncProvider(config) {
2287
2643
  async function fetchTimeseriesSnapshots(context, windowStart, windowEnd, skippedOptionalResources, resources = timeseriesResources, sourceProviderSlug, options = {}) {
2288
2644
  const snapshots = {};
2289
2645
  for (const resource of resources) {
2290
- snapshots[resource] = await fetchTimeseriesResourceInChunks(context, resource, windowStart, windowEnd, skippedOptionalResources, sourceProviderSlug, options);
2646
+ const fetched = await fetchTimeseriesResourceInChunks(context, resource, windowStart, windowEnd, skippedOptionalResources, sourceProviderSlug, options);
2647
+ snapshots[resource] = fetched.records;
2291
2648
  }
2292
2649
  return snapshots;
2293
2650
  }
2294
2651
  async function fetchTimeseriesResourceInChunks(context, resource, windowStart, windowEnd, skippedOptionalResources, sourceProviderSlug, options = {}) {
2295
2652
  const records = [];
2653
+ let providerRecordsSeen = false;
2296
2654
  let chunkStart = Date.parse(windowStart);
2297
2655
  const end = Date.parse(windowEnd);
2298
2656
  let optionalFailureLogged = false;
@@ -2317,8 +2675,9 @@ export function createJunctionDeviceSyncProvider(config) {
2317
2675
  if (options.requireStructurallyCompleteCollection) {
2318
2676
  request.requireStructurallyCompleteCollection = true;
2319
2677
  }
2320
- const chunkRecords = await fetchJunctionTimeseriesWindow(client, request);
2321
- records.push(...filterJunctionTimeseriesRecordsToWindow(resource, chunkRecords, chunkWindowStart, chunkWindowEnd, options.dateQueryFormat));
2678
+ const chunkRecords = await measureJunctionProviderRequest(context, "resource", () => fetchJunctionTimeseriesWindow(client, request));
2679
+ providerRecordsSeen ||= chunkRecords.length > 0;
2680
+ records.push(...filterJunctionTimeseriesRecordsToWindow(resource, chunkRecords, chunkWindowStart, chunkWindowEnd, options.dateQueryFormat, options.authorizedLocalDay, context.vaultTimeZone));
2322
2681
  }
2323
2682
  catch (error) {
2324
2683
  const failure = classifyOptionalJunctionResourceFailure(error, "timeseries", resource, context.account.externalAccountId);
@@ -2339,9 +2698,15 @@ export function createJunctionDeviceSyncProvider(config) {
2339
2698
  chunkStart = chunkEnd;
2340
2699
  }
2341
2700
  if (resource === "electrocardiogram_voltage" || resource === "workout_stream") {
2342
- return resolveJunctionBoundedFeatureRecords(resource, records);
2701
+ return {
2702
+ providerRecordsSeen,
2703
+ records: resolveJunctionBoundedFeatureRecords(resource, records),
2704
+ };
2343
2705
  }
2344
- return dedupeJunctionTimeseriesRecords(resource, records);
2706
+ return {
2707
+ providerRecordsSeen,
2708
+ records: dedupeJunctionTimeseriesRecords(resource, records),
2709
+ };
2345
2710
  }
2346
2711
  async function importTimeseriesPreciseSnapshots(context, sourceProviders, windowStart, windowEnd, skippedOptionalResources, resources, sourceProviderSlug, options = {}) {
2347
2712
  const accumulatedTimeseries = {};
@@ -2370,9 +2735,8 @@ export function createJunctionDeviceSyncProvider(config) {
2370
2735
  const skippedResourceCountBeforeFetch = skippedOptionalResources.length;
2371
2736
  let timeseries;
2372
2737
  try {
2373
- timeseries = {
2374
- [resource]: await fetchTimeseriesResourceInChunks(context, resource, window.windowStart, window.windowEnd, skippedOptionalResources, sourceProviderSlug, { dateQueryFormat: options.dateQueryFormat ?? "datetime" }),
2375
- };
2738
+ const fetched = await fetchTimeseriesResourceInChunks(context, resource, window.windowStart, window.windowEnd, skippedOptionalResources, sourceProviderSlug, { dateQueryFormat: options.dateQueryFormat ?? "datetime" });
2739
+ timeseries = { [resource]: fetched.records };
2376
2740
  }
2377
2741
  catch (error) {
2378
2742
  if (options.preservePartialRetryableFailure === true
@@ -2410,7 +2774,8 @@ export function createJunctionDeviceSyncProvider(config) {
2410
2774
  let unresolvedProviderRecordCount = providerRecordCount;
2411
2775
  let unresolvedProviderRecordsWithoutStableIdentity = providerRecordCount > 0;
2412
2776
  const requiresBloodPressureRecordResolution = resources.includes("blood_pressure");
2413
- const successfulImportTerminatesWeightRows = resources.length === 1 && resources[0] === "weight";
2777
+ const successfulImportTerminatesDeliveredRows = resources.length === 1
2778
+ && doesJunctionImportReceiptResolveDeliveredRows(resources[0] ?? "");
2414
2779
  if (executionWindowStart && executionWindowEnd) {
2415
2780
  const identifyFetchedProviderRecords = (sourceIdentities) => requiresBloodPressureRecordResolution && providerRecordCount > 0
2416
2781
  ? identifyJunctionBloodPressureProviderRecords({
@@ -2442,7 +2807,7 @@ export function createJunctionDeviceSyncProvider(config) {
2442
2807
  const importSourceIdentities = resolveJunctionAccountSourceIdentities(currentSources);
2443
2808
  if (sourceProviderSlug
2444
2809
  && options.sourceStatusRequirement === "connected") {
2445
- postFetchSourceAdmission = resolveJunctionCurrentSourceAdmissionFromSources(currentSources, sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only");
2810
+ postFetchSourceAdmission = resolveJunctionCurrentSourceAdmissionFromSources(currentSources, sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only", options.sourceLifecycleEpoch);
2446
2811
  if (postFetchSourceAdmission !== "admitted") {
2447
2812
  return {
2448
2813
  acceptedProviderRecordCount: 0,
@@ -2477,18 +2842,14 @@ export function createJunctionDeviceSyncProvider(config) {
2477
2842
  windowStart: executionWindowStart,
2478
2843
  });
2479
2844
  if (hasJunctionSnapshotRecords(preparedImport.snapshots)) {
2480
- const receipt = await context.importSnapshot({
2481
- provider: "junction",
2482
- accountId: buildJunctionImportAccountId(context.account.externalAccountId),
2483
- connectionId: context.account.id,
2845
+ const receipt = await commitPreparedJunctionCanonicalImport(context, preparedImport, {
2484
2846
  importedAt: executionWindowEnd,
2485
2847
  windowStart: executionWindowStart,
2486
2848
  windowEnd: executionWindowEnd,
2487
2849
  timeseriesWindowKind: "precise",
2488
- connections: preparedImport.connections,
2489
2850
  summaries: {},
2490
2851
  timeseries: preparedImport.snapshots,
2491
- });
2852
+ }, context.now);
2492
2853
  providerRecordsExamined = true;
2493
2854
  canonicalEventCount = readProviderSnapshotCanonicalEventCount(receipt);
2494
2855
  canonicalEventDayKeys = readProviderSnapshotCanonicalEventDayKeys(receipt);
@@ -2510,11 +2871,11 @@ export function createJunctionDeviceSyncProvider(config) {
2510
2871
  unresolvedProviderRecordIdentities.length
2511
2872
  + (unresolvedProviderRecordsWithoutStableIdentity ? 1 : 0);
2512
2873
  }
2513
- else if (successfulImportTerminatesWeightRows) {
2514
- // A resolved import receipt means the weight normalizer examined
2515
- // every delivered row. Valid readings became canonical events and
2516
- // deterministic validation rejects are terminal; delivery errors
2517
- // throw before this branch and retain the retry obligation.
2874
+ else if (successfulImportTerminatesDeliveredRows) {
2875
+ // A resolved exact-record import receipt means the canonical owner
2876
+ // examined every delivered row. Valid readings became canonical
2877
+ // events and deterministic validation rejects are terminal;
2878
+ // delivery errors throw before this branch and remain retryable.
2518
2879
  unresolvedProviderRecordCount = 0;
2519
2880
  unresolvedProviderRecordsWithoutStableIdentity = false;
2520
2881
  }
@@ -2524,6 +2885,10 @@ export function createJunctionDeviceSyncProvider(config) {
2524
2885
  unresolvedProviderRecordsWithoutStableIdentity = unresolvedProviderRecordCount > 0;
2525
2886
  }
2526
2887
  }
2888
+ if (sourceProviderSlug
2889
+ && options.sourceStatusRequirement === "connected") {
2890
+ postFetchSourceAdmission = await resolveJunctionCurrentSourceAdmission(context, sourceProviderSlug, options.sourceLifecycleEpoch);
2891
+ }
2527
2892
  }
2528
2893
  catch (error) {
2529
2894
  if (options.preservePartialRetryableFailure === true
@@ -2637,10 +3002,7 @@ export function createJunctionDeviceSyncProvider(config) {
2637
3002
  : [],
2638
3003
  });
2639
3004
  if (hasJunctionSnapshotRecords(preparedImport.snapshots)) {
2640
- const receipt = await context.importSnapshot({
2641
- provider: "junction",
2642
- accountId: buildJunctionImportAccountId(context.account.externalAccountId),
2643
- connectionId: context.account.id,
3005
+ const receipt = await commitPreparedJunctionCanonicalImport(context, preparedImport, {
2644
3006
  importedAt: window.windowEnd,
2645
3007
  windowStart: window.windowStart,
2646
3008
  windowEnd: window.windowEnd,
@@ -2649,16 +3011,17 @@ export function createJunctionDeviceSyncProvider(config) {
2649
3011
  ? {
2650
3012
  strictSparseCalendarRepair: {
2651
3013
  dayKey: window.windowStart.slice(0, 10),
2652
- resource: windowResources[0],
2653
3014
  ...emptySparseCalendarSource,
3015
+ // Calendar-refresh admission already proved this member of
3016
+ // the fixed sparse resource set before collection began.
3017
+ resource: windowResources[0],
2654
3018
  sourceProviderSlug: normalizeJunctionSourceProviderSlug(emptySparseCalendarSource.sourceProviderSlug) ?? emptySparseCalendarSource.sourceProviderSlug,
2655
3019
  },
2656
3020
  }
2657
3021
  : {}),
2658
- connections: preparedImport.connections,
2659
3022
  summaries: {},
2660
3023
  timeseries: preparedImport.snapshots,
2661
- });
3024
+ }, context.now);
2662
3025
  for (const resourceId of readProviderSnapshotCanonicalEventExternalRefResourceIds(receipt) ?? []) {
2663
3026
  appliedDailyAggregateResourceIds.add(resourceId);
2664
3027
  }
@@ -2669,7 +3032,7 @@ export function createJunctionDeviceSyncProvider(config) {
2669
3032
  yieldedAt: null,
2670
3033
  };
2671
3034
  }
2672
- async function importTimeseriesDailySnapshots(context, sourceProviders, windowStart, windowEnd, skippedOptionalResources, resource, sourceProviderSlug, completedWorkoutStreamIdentities = new Set()) {
3035
+ async function importTimeseriesDailySnapshots(context, sourceProviders, windowStart, windowEnd, skippedOptionalResources, resource, sourceProviderSlug, completedWorkoutStreamIdentities = new Set(), workoutStreamEmptySeen = false) {
2673
3036
  let resumeWorkoutStreamIdentities = new Set(completedWorkoutStreamIdentities);
2674
3037
  let madeProgress = false;
2675
3038
  for (const window of buildClosedDailyWindows(windowStart, windowEnd)) {
@@ -2680,6 +3043,7 @@ export function createJunctionDeviceSyncProvider(config) {
2680
3043
  if (madeProgress) {
2681
3044
  return {
2682
3045
  workoutStreamCursor,
3046
+ workoutStreamEmptySeen,
2683
3047
  yieldedAt: window.windowStart,
2684
3048
  };
2685
3049
  }
@@ -2695,20 +3059,37 @@ export function createJunctionDeviceSyncProvider(config) {
2695
3059
  sourceProviders,
2696
3060
  windowEnd: window.windowEnd,
2697
3061
  windowStart: window.windowStart,
3062
+ workoutStreamEmptySeen,
2698
3063
  });
2699
3064
  workoutStreamCursor = workoutImport.workoutStreamCursor;
3065
+ workoutStreamEmptySeen = workoutImport.emptyTimestampArraySeen;
2700
3066
  madeProgress ||= workoutImport.madeProgress;
2701
3067
  if (workoutImport.yieldedAt) {
2702
3068
  return {
2703
3069
  workoutStreamCursor,
3070
+ workoutStreamEmptySeen,
2704
3071
  yieldedAt: workoutImport.yieldedAt,
2705
3072
  };
2706
3073
  }
3074
+ if (workoutStreamEmptySeen) {
3075
+ return {
3076
+ emptyWorkoutStreamReplayWindow: {
3077
+ windowEnd: window.windowEnd,
3078
+ windowStart: window.windowStart,
3079
+ },
3080
+ workoutStreamCursor: null,
3081
+ workoutStreamEmptySeen: false,
3082
+ yieldedAt: window.windowEnd,
3083
+ };
3084
+ }
2707
3085
  madeProgress = true;
2708
3086
  }
2709
3087
  else {
2710
3088
  try {
2711
3089
  await importJunctionTimeseriesResourceSnapshot({
3090
+ collectionWorkLimit: resource === "electrocardiogram_voltage"
3091
+ ? JUNCTION_FULL_JOB_TIMESERIES_COLLECTION_WORK_LIMIT
3092
+ : undefined,
2712
3093
  context,
2713
3094
  dateQueryFormat: "date",
2714
3095
  resource,
@@ -2724,6 +3105,7 @@ export function createJunctionDeviceSyncProvider(config) {
2724
3105
  if (madeProgress) {
2725
3106
  return {
2726
3107
  workoutStreamCursor,
3108
+ workoutStreamEmptySeen: false,
2727
3109
  yieldedAt: window.windowStart,
2728
3110
  };
2729
3111
  }
@@ -2741,6 +3123,7 @@ export function createJunctionDeviceSyncProvider(config) {
2741
3123
  }
2742
3124
  return {
2743
3125
  workoutStreamCursor: null,
3126
+ workoutStreamEmptySeen: false,
2744
3127
  yieldedAt: null,
2745
3128
  };
2746
3129
  }
@@ -2766,15 +3149,33 @@ export function createJunctionDeviceSyncProvider(config) {
2766
3149
  : persistedTimeseriesWindowHours;
2767
3150
  const resource = resourceCursor.resource;
2768
3151
  const policy = resolveJunctionTimeseriesResourcePolicy(resource);
3152
+ const sourceProviderSlug = normalizeProviderSlug(job.payload.sourceProviderSlug);
3153
+ let historicalProviderRecordsSeen = sourceProviderSlug !== null
3154
+ && job.payload.historicalProviderRecordsSeen === true;
3155
+ let historicalRecordsSeen = sourceProviderSlug !== null
3156
+ && job.payload.historicalRecordsSeen === true;
2769
3157
  if ((timeseriesWindowHours === 1
2770
3158
  && policy?.normalizationMode !== "hourly_or_session_feature")
2771
3159
  || (!resourceCursor.restartFromWindowStart
2772
3160
  && resource !== "workout_stream"
2773
- && job.payload.workoutStreamCursor !== undefined)) {
3161
+ && (job.payload.workoutStreamCursor !== undefined
3162
+ || job.payload.workoutStreamEmptySeen !== undefined))) {
2774
3163
  throw invalidJunctionTimeseriesResourceProgress();
2775
3164
  }
3165
+ const listedSourceProviders = sourceProviderSlug || resource === "workout_stream"
3166
+ ? await measureJunctionProviderRequest(context, "inventory", () => client.listUserProviders(context.account.externalAccountId, {
3167
+ ...(!sourceProviderSlug && resource === "workout_stream"
3168
+ ? { collectionWorkLimit: JUNCTION_FULL_JOB_INVENTORY_COLLECTION_WORK_LIMIT }
3169
+ : {}),
3170
+ signal: context.signal ?? null,
3171
+ }))
3172
+ : [];
3173
+ const sourceProviders = sourceProviderSlug
3174
+ ? listedSourceProviders.filter((provider) => areJunctionProviderSlugsDataEquivalent(provider.origin.sourceProviderSlug ?? provider.slug, sourceProviderSlug))
3175
+ : listedSourceProviders;
2776
3176
  const executionWindowEnd = new Date(Math.min(Date.parse(timeseriesCursor) + timeseriesWindowHours * TIMESERIES_HOUR_MS, Date.parse(window.windowEnd))).toISOString();
2777
3177
  let workoutStreamCursor = null;
3178
+ let workoutStreamReplayWindow;
2778
3179
  if (resource === "workout_stream") {
2779
3180
  try {
2780
3181
  const workoutImport = await importJunctionWorkoutStreamWindow({
@@ -2783,41 +3184,69 @@ export function createJunctionDeviceSyncProvider(config) {
2783
3184
  completedIdentities: completedWorkoutStreamIdentities,
2784
3185
  context,
2785
3186
  skippedOptionalResources,
2786
- sourceProviders: [],
3187
+ sourceProviderSlug,
3188
+ sourceProviders,
2787
3189
  windowEnd: executionWindowEnd,
2788
3190
  windowStart: timeseriesCursor,
3191
+ workoutStreamEmptySeen: job.payload.workoutStreamEmptySeen === true,
2789
3192
  });
3193
+ historicalProviderRecordsSeen ||=
3194
+ sourceProviderSlug !== null
3195
+ && workoutImport.historicalProviderRecordsSeen === true;
3196
+ historicalRecordsSeen ||=
3197
+ sourceProviderSlug !== null
3198
+ && workoutImport.historicalRecordsSeen === true;
2790
3199
  workoutStreamCursor = workoutImport.workoutStreamCursor;
2791
3200
  if (workoutImport.yieldedAt) {
2792
3201
  return buildFullJobTimeseriesContinuationResult({
2793
3202
  context,
3203
+ historicalProviderRecordsSeen,
3204
+ historicalRecordsSeen,
2794
3205
  job,
2795
3206
  skippedOptionalResources,
3207
+ sourceProviders,
2796
3208
  continuation: {
2797
3209
  timeseriesCursor,
2798
3210
  timeseriesResourceCursor: resource,
2799
3211
  timeseriesWindowHours,
2800
3212
  workoutStreamCursor,
3213
+ workoutStreamEmptySeen: workoutImport.emptyTimestampArraySeen,
2801
3214
  },
2802
3215
  window,
2803
3216
  });
2804
3217
  }
3218
+ workoutStreamReplayWindow = workoutImport.emptyTimestampArraySeen
3219
+ ? {
3220
+ windowEnd: executionWindowEnd,
3221
+ windowStart: timeseriesCursor,
3222
+ }
3223
+ : undefined;
2805
3224
  }
2806
3225
  catch (error) {
2807
3226
  if (error instanceof JunctionTimeseriesProgressError) {
3227
+ historicalProviderRecordsSeen ||=
3228
+ sourceProviderSlug !== null
3229
+ && error.historicalProviderRecordsSeen;
3230
+ historicalRecordsSeen ||=
3231
+ sourceProviderSlug !== null
3232
+ && error.historicalRecordsSeen;
2808
3233
  if (!error.workoutStreamCursor
2809
3234
  || error.workoutStreamCursor === job.payload.workoutStreamCursor) {
2810
3235
  throw error.failure;
2811
3236
  }
2812
3237
  return buildFullJobTimeseriesContinuationResult({
2813
3238
  context,
3239
+ historicalProviderRecordsSeen,
3240
+ historicalRecordsSeen,
2814
3241
  job,
2815
3242
  skippedOptionalResources,
3243
+ sourceProviders,
2816
3244
  continuation: {
2817
3245
  timeseriesCursor,
2818
3246
  timeseriesResourceCursor: resource,
2819
3247
  timeseriesWindowHours,
2820
3248
  workoutStreamCursor: error.workoutStreamCursor,
3249
+ workoutStreamEmptySeen: error.workoutStreamEmptySeen,
2821
3250
  },
2822
3251
  window,
2823
3252
  });
@@ -2838,16 +3267,23 @@ export function createJunctionDeviceSyncProvider(config) {
2838
3267
  windowStart: timeseriesCursor,
2839
3268
  };
2840
3269
  if (Date.parse(importWindow.windowStart) < Date.parse(importWindow.windowEnd)) {
2841
- await importJunctionTimeseriesResourceSnapshot({
3270
+ const timeseriesImport = await importJunctionTimeseriesResourceSnapshot({
2842
3271
  collectionWorkLimit: JUNCTION_FULL_JOB_TIMESERIES_COLLECTION_WORK_LIMIT,
2843
3272
  context,
2844
3273
  dateQueryFormat: timeseriesWindowHours === 1 ? "datetime" : "date",
2845
3274
  resource,
2846
3275
  skippedOptionalResources,
2847
- sourceProviders: [],
3276
+ sourceProviderSlug,
3277
+ sourceProviders,
2848
3278
  windowEnd: importWindow.windowEnd,
2849
3279
  windowStart: importWindow.windowStart,
2850
3280
  });
3281
+ historicalProviderRecordsSeen ||=
3282
+ sourceProviderSlug !== null
3283
+ && timeseriesImport.historicalProviderRecordsSeen === true;
3284
+ historicalRecordsSeen ||=
3285
+ sourceProviderSlug !== null
3286
+ && timeseriesImport.historicalRecordsSeen === true;
2851
3287
  }
2852
3288
  }
2853
3289
  catch (error) {
@@ -2856,13 +3292,17 @@ export function createJunctionDeviceSyncProvider(config) {
2856
3292
  && policy?.normalizationMode === "hourly_or_session_feature") {
2857
3293
  return buildFullJobTimeseriesContinuationResult({
2858
3294
  context,
3295
+ historicalProviderRecordsSeen,
3296
+ historicalRecordsSeen,
2859
3297
  job,
2860
3298
  skippedOptionalResources,
3299
+ sourceProviders,
2861
3300
  continuation: {
2862
3301
  timeseriesCursor,
2863
3302
  timeseriesResourceCursor: resource,
2864
3303
  timeseriesWindowHours: 1,
2865
3304
  workoutStreamCursor: null,
3305
+ workoutStreamEmptySeen: false,
2866
3306
  },
2867
3307
  window,
2868
3308
  });
@@ -2872,8 +3312,12 @@ export function createJunctionDeviceSyncProvider(config) {
2872
3312
  }
2873
3313
  return buildFullJobTimeseriesContinuationResult({
2874
3314
  context,
3315
+ historicalProviderRecordsSeen,
3316
+ historicalRecordsSeen,
2875
3317
  job,
3318
+ replayWindow: workoutStreamReplayWindow,
2876
3319
  skippedOptionalResources,
3320
+ sourceProviders,
2877
3321
  continuation: resolveNextFullJobTimeseriesContinuation({
2878
3322
  baseTimeseriesWindowStart,
2879
3323
  executionWindowEnd,
@@ -2887,38 +3331,110 @@ export function createJunctionDeviceSyncProvider(config) {
2887
3331
  }
2888
3332
  async function importJunctionTimeseriesResourceSnapshot(input) {
2889
3333
  const calendarDayAggregate = JUNCTION_CALENDAR_DAY_AGGREGATE_RESOURCE_SET.has(input.resource);
2890
- if (calendarDayAggregate) {
3334
+ // Temporal-authority fetches carry complete vault-local-day authority with
3335
+ // their own conservative closure lag; the provider calendar-day closure
3336
+ // gate owns only ordinary calendar-day imports.
3337
+ if (calendarDayAggregate && !input.authorizedLocalDay) {
2891
3338
  const windowEndMs = Date.parse(input.windowEnd);
2892
3339
  const globallyClosedProviderDayEnd = resolveGloballyClosedProviderDayEnd(input.windowEnd, input.context.now);
2893
3340
  if (!Number.isFinite(windowEndMs)
2894
3341
  || !Number.isFinite(globallyClosedProviderDayEnd)
2895
3342
  || windowEndMs > globallyClosedProviderDayEnd) {
2896
- return;
3343
+ return { yieldedAt: null };
2897
3344
  }
2898
3345
  }
2899
- const records = await fetchTimeseriesResourceInChunks(input.context, input.resource, input.windowStart, input.windowEnd, input.skippedOptionalResources, input.sourceProviderSlug, {
3346
+ const skippedResourceCountBeforeFetch = input.skippedOptionalResources.length;
3347
+ const sourceLifecycleFence = resolveJunctionTimeseriesResourcePolicy(input.resource)
3348
+ ?.maxCanonicalRecordsPerWindow !== undefined
3349
+ && input.context.listConnectionSources
3350
+ ? captureJunctionSourceLifecycleFence(input.context.account.sources ?? [])
3351
+ : null;
3352
+ const fetched = await fetchTimeseriesResourceInChunks(input.context, input.resource, input.windowStart, input.windowEnd, input.skippedOptionalResources, input.sourceProviderSlug, {
3353
+ ...(input.authorizedLocalDay
3354
+ ? {
3355
+ authorizedLocalDay: input.authorizedLocalDay,
3356
+ // A lossy successful response must never be certified as an
3357
+ // authoritative empty or partial complete-day replacement.
3358
+ requireStructurallyCompleteCollection: true,
3359
+ }
3360
+ : {}),
2900
3361
  collectionWorkLimit: input.collectionWorkLimit,
2901
3362
  dateQueryFormat: input.dateQueryFormat,
2902
3363
  });
2903
- if (records.length === 0) {
2904
- return;
3364
+ const { providerRecordsSeen } = fetched;
3365
+ const records = scopeJunctionRecordsToSourceProvider(fetched.records, input.sourceProviderSlug);
3366
+ if (input.skippedOptionalResources.length > skippedResourceCountBeforeFetch) {
3367
+ return {
3368
+ historicalProviderRecordsSeen: providerRecordsSeen,
3369
+ historicalRecordsSeen: false,
3370
+ yieldedAt: null,
3371
+ };
2905
3372
  }
2906
- const preparedImport = await prepareJunctionImportSnapshot(input.context, { [input.resource]: records }, input.sourceProviders, { projectAccountSourceIdentities: calendarDayAggregate });
2907
- if (!hasJunctionSnapshotRecords(preparedImport.snapshots)) {
2908
- return;
3373
+ const currentSources = sourceLifecycleFence && input.context.listConnectionSources
3374
+ ? await input.context.listConnectionSources()
3375
+ : null;
3376
+ if (sourceLifecycleFence
3377
+ && (!currentSources
3378
+ || !isJunctionSourceLifecycleFenceCurrent(sourceLifecycleFence, currentSources))) {
3379
+ throw junctionTimeseriesSourceLifecycleSuperseded();
2909
3380
  }
2910
- await input.context.importSnapshot({
2911
- provider: "junction",
2912
- accountId: buildJunctionImportAccountId(input.context.account.externalAccountId),
2913
- connectionId: input.context.account.id,
3381
+ if (records.length === 0 && !input.authorizedLocalDay) {
3382
+ return {
3383
+ historicalProviderRecordsSeen: providerRecordsSeen,
3384
+ historicalRecordsSeen: false,
3385
+ yieldedAt: null,
3386
+ };
3387
+ }
3388
+ const sourceScopedHistoricalRecordsSeen = input.sourceProviderSlug
3389
+ ? await hasJunctionSourceScopedAdmittedSnapshotRecords({
3390
+ context: input.context,
3391
+ ...(currentSources ? { listedSources: currentSources } : {}),
3392
+ options: { projectAccountSourceIdentities: calendarDayAggregate },
3393
+ snapshots: { [input.resource]: records },
3394
+ sourceProviderSlug: input.sourceProviderSlug,
3395
+ sourceProviders: input.sourceProviders,
3396
+ })
3397
+ : false;
3398
+ const preparedImport = currentSources
3399
+ ? prepareJunctionImportSnapshotForSources({ [input.resource]: records }, input.sourceProviders, currentSources, calendarDayAggregate
3400
+ ? { sourceIdentities: resolveJunctionAccountSourceIdentities(currentSources) }
3401
+ : {}, {
3402
+ allowUnlistedSources: input.context.connectionSourceAdmissionMode !== "listed_only",
3403
+ sourceStatusRequirement: "not_disconnected",
3404
+ })
3405
+ : await prepareJunctionImportSnapshot(input.context, { [input.resource]: records }, input.sourceProviders, { projectAccountSourceIdentities: calendarDayAggregate });
3406
+ if (!hasJunctionSnapshotRecords(preparedImport.snapshots) && !input.authorizedLocalDay) {
3407
+ return {
3408
+ historicalProviderRecordsSeen: providerRecordsSeen,
3409
+ historicalRecordsSeen: sourceScopedHistoricalRecordsSeen,
3410
+ yieldedAt: null,
3411
+ };
3412
+ }
3413
+ await commitPreparedJunctionCanonicalImport(input.context, preparedImport, {
2914
3414
  importedAt: input.windowEnd,
2915
3415
  windowStart: input.windowStart,
2916
3416
  windowEnd: input.windowEnd,
2917
- ...(calendarDayAggregate ? { timeseriesWindowKind: "calendar_day" } : {}),
2918
- connections: preparedImport.connections,
3417
+ ...(calendarDayAggregate && !input.authorizedLocalDay
3418
+ ? { timeseriesWindowKind: "calendar_day" }
3419
+ : {}),
2919
3420
  summaries: {},
2920
3421
  timeseries: preparedImport.snapshots,
2921
- });
3422
+ }, input.context.now, input.authorizedLocalDay
3423
+ ? {
3424
+ completeSourceDay: {
3425
+ connectionId: buildJunctionImportAccountId(input.context.account.externalAccountId),
3426
+ dayKey: input.authorizedLocalDay.dayKey,
3427
+ resources: [input.resource],
3428
+ revisionAt: input.context.now,
3429
+ timeZone: input.authorizedLocalDay.timeZone,
3430
+ },
3431
+ }
3432
+ : undefined);
3433
+ return {
3434
+ historicalProviderRecordsSeen: providerRecordsSeen,
3435
+ historicalRecordsSeen: sourceScopedHistoricalRecordsSeen,
3436
+ yieldedAt: null,
3437
+ };
2922
3438
  }
2923
3439
  async function importJunctionWorkoutStreamWindow(input) {
2924
3440
  const policy = resolveJunctionTimeseriesResourcePolicy("workout_stream");
@@ -2928,11 +3444,17 @@ export function createJunctionDeviceSyncProvider(config) {
2928
3444
  throw new TypeError("Junction workout_stream policy did not define bounded limits.");
2929
3445
  }
2930
3446
  let completedIdentities = new Set(input.completedIdentities);
3447
+ let historicalProviderRecordsSeen = false;
3448
+ let historicalRecordsSeen = false;
2931
3449
  let madeProgress = false;
3450
+ let workoutStreamEmptySeen = input.workoutStreamEmptySeen === true;
2932
3451
  const carryTerminalProgressOrThrow = (error) => {
2933
3452
  if (isJunctionJobSignalAbort(error, input.context.signal)) {
2934
3453
  if (input.allowImmediateYield || madeProgress) {
2935
3454
  return {
3455
+ emptyTimestampArraySeen: workoutStreamEmptySeen,
3456
+ historicalProviderRecordsSeen,
3457
+ historicalRecordsSeen,
2936
3458
  madeProgress,
2937
3459
  workoutStreamCursor: encodeJunctionWorkoutStreamCompletedIdentities(completedIdentities),
2938
3460
  yieldedAt: input.windowStart,
@@ -2941,13 +3463,40 @@ export function createJunctionDeviceSyncProvider(config) {
2941
3463
  throw error;
2942
3464
  }
2943
3465
  if (isRetryableDeviceSyncFailure(error)) {
2944
- throw new JunctionTimeseriesProgressError(error, input.windowStart, encodeJunctionWorkoutStreamCompletedIdentities(completedIdentities));
3466
+ throw new JunctionTimeseriesProgressError(error, input.windowStart, encodeJunctionWorkoutStreamCompletedIdentities(completedIdentities), {
3467
+ historicalProviderRecordsSeen,
3468
+ historicalRecordsSeen,
3469
+ workoutStreamEmptySeen,
3470
+ });
2945
3471
  }
2946
3472
  throw error;
2947
3473
  };
2948
- let candidates;
3474
+ let eligibleSourceProviderSlugs;
2949
3475
  try {
2950
- candidates = await listJunctionWorkoutStreamCandidates(client, {
3476
+ eligibleSourceProviderSlugs = await resolveJunctionWorkoutStreamEligibleSources(input.context, input.sourceProviders);
3477
+ }
3478
+ catch (error) {
3479
+ return carryTerminalProgressOrThrow(error);
3480
+ }
3481
+ const sourceScopeProvided = input.sourceProviderSlug !== undefined
3482
+ && input.sourceProviderSlug !== null;
3483
+ const scopedSourceProviderSlug = canonicalizeJunctionProviderSlug(input.sourceProviderSlug);
3484
+ if (eligibleSourceProviderSlugs.size === 0
3485
+ || (sourceScopeProvided
3486
+ && (!scopedSourceProviderSlug
3487
+ || !eligibleSourceProviderSlugs.has(scopedSourceProviderSlug)))) {
3488
+ return {
3489
+ emptyTimestampArraySeen: workoutStreamEmptySeen,
3490
+ historicalProviderRecordsSeen,
3491
+ historicalRecordsSeen,
3492
+ madeProgress,
3493
+ workoutStreamCursor: null,
3494
+ yieldedAt: null,
3495
+ };
3496
+ }
3497
+ let candidateListing;
3498
+ try {
3499
+ candidateListing = await listJunctionWorkoutStreamCandidates(client, {
2951
3500
  collectionWorkLimit: input.collectionWorkLimit,
2952
3501
  resource: "workout_stream",
2953
3502
  signal: input.context.signal ?? null,
@@ -2955,11 +3504,13 @@ export function createJunctionDeviceSyncProvider(config) {
2955
3504
  userId: input.context.account.externalAccountId,
2956
3505
  windowEnd: input.windowEnd,
2957
3506
  windowStart: input.windowStart,
2958
- });
3507
+ }, true, eligibleSourceProviderSlugs);
2959
3508
  }
2960
3509
  catch (error) {
2961
3510
  return carryTerminalProgressOrThrow(error);
2962
3511
  }
3512
+ const { candidates } = candidateListing;
3513
+ historicalProviderRecordsSeen = candidateListing.providerRecordsSeen;
2963
3514
  const candidateIdentities = new Set(candidates.map((candidate) => candidate.identity));
2964
3515
  completedIdentities = new Set([...completedIdentities].filter((identity) => candidateIdentities.has(identity)));
2965
3516
  for (const [candidateIndex, candidate] of candidates.entries()) {
@@ -2969,6 +3520,9 @@ export function createJunctionDeviceSyncProvider(config) {
2969
3520
  if (input.context.shouldYield?.()) {
2970
3521
  if (input.allowImmediateYield || madeProgress) {
2971
3522
  return {
3523
+ emptyTimestampArraySeen: workoutStreamEmptySeen,
3524
+ historicalProviderRecordsSeen,
3525
+ historicalRecordsSeen,
2972
3526
  madeProgress,
2973
3527
  workoutStreamCursor: encodeJunctionWorkoutStreamCompletedIdentities(completedIdentities),
2974
3528
  yieldedAt: input.windowStart,
@@ -2976,12 +3530,12 @@ export function createJunctionDeviceSyncProvider(config) {
2976
3530
  }
2977
3531
  input.context.throwIfAborted?.();
2978
3532
  }
2979
- let feature;
3533
+ let fetchedFeature;
2980
3534
  try {
2981
- feature = await fetchJunctionWorkoutStreamFeature(client, candidate, maxSamples, input.context.signal ?? null, input.collectionWorkLimit);
3535
+ fetchedFeature = await fetchJunctionWorkoutStreamFeature(client, candidate, maxSamples, input.context.signal ?? null, input.collectionWorkLimit);
2982
3536
  }
2983
3537
  catch (error) {
2984
- const failure = classifyOptionalJunctionResourceFailure(error, "timeseries", "workout_stream", input.context.account.externalAccountId);
3538
+ const failure = classifyOptionalJunctionWorkoutStreamCandidateFailure(error, input.context.account.externalAccountId);
2985
3539
  if (!failure) {
2986
3540
  return carryTerminalProgressOrThrow(addJunctionWorkoutStreamCandidateFailureContext(error, {
2987
3541
  aliasSource: candidate.workoutIdAliasSource,
@@ -2999,8 +3553,21 @@ export function createJunctionDeviceSyncProvider(config) {
2999
3553
  madeProgress = true;
3000
3554
  continue;
3001
3555
  }
3556
+ const feature = fetchedFeature.feature;
3557
+ if (fetchedFeature.emptyTimestampArray) {
3558
+ input.context.logger.warn?.("Deferring Junction workout with an empty stream.", {
3559
+ errorCode: "JUNCTION_WORKOUT_STREAM_EMPTY",
3560
+ provider: "junction",
3561
+ resource: "workout_stream",
3562
+ resourceCategory: "timeseries",
3563
+ });
3564
+ completedIdentities.add(candidate.identity);
3565
+ madeProgress = true;
3566
+ workoutStreamEmptySeen = true;
3567
+ continue;
3568
+ }
3002
3569
  if (feature === undefined) {
3003
- input.context.logger.warn?.("Skipping Junction workout with unaligned metric cardinality.", {
3570
+ input.context.logger.warn?.("Skipping Junction workout without an importable stream feature.", {
3004
3571
  errorCode: "JUNCTION_WORKOUT_STREAM_CARDINALITY_MISMATCH",
3005
3572
  provider: "junction",
3006
3573
  resource: "workout_stream",
@@ -3011,19 +3578,26 @@ export function createJunctionDeviceSyncProvider(config) {
3011
3578
  continue;
3012
3579
  }
3013
3580
  try {
3014
- const preparedImport = await prepareJunctionImportSnapshot(input.context, { workout_stream: [feature] }, input.sourceProviders);
3581
+ const sourceScopedFeature = input.sourceProviderSlug
3582
+ ? withJunctionSourceProviderFallback(feature, input.sourceProviderSlug)
3583
+ : feature;
3584
+ historicalRecordsSeen ||= input.sourceProviderSlug
3585
+ ? await hasJunctionSourceScopedAdmittedSnapshotRecords({
3586
+ context: input.context,
3587
+ snapshots: { workout_stream: [sourceScopedFeature] },
3588
+ sourceProviderSlug: input.sourceProviderSlug,
3589
+ sourceProviders: input.sourceProviders,
3590
+ })
3591
+ : false;
3592
+ const preparedImport = await prepareJunctionImportSnapshot(input.context, { workout_stream: [sourceScopedFeature] }, input.sourceProviders);
3015
3593
  if (hasJunctionSnapshotRecords(preparedImport.snapshots)) {
3016
- await input.context.importSnapshot({
3017
- provider: "junction",
3018
- accountId: buildJunctionImportAccountId(input.context.account.externalAccountId),
3019
- connectionId: input.context.account.id,
3594
+ await commitPreparedJunctionCanonicalImport(input.context, preparedImport, {
3020
3595
  importedAt: input.windowEnd,
3021
3596
  windowStart: input.windowStart,
3022
3597
  windowEnd: input.windowEnd,
3023
- connections: preparedImport.connections,
3024
3598
  summaries: {},
3025
3599
  timeseries: preparedImport.snapshots,
3026
- });
3600
+ }, input.context.now);
3027
3601
  }
3028
3602
  }
3029
3603
  catch (error) {
@@ -3033,6 +3607,9 @@ export function createJunctionDeviceSyncProvider(config) {
3033
3607
  madeProgress = true;
3034
3608
  }
3035
3609
  return {
3610
+ emptyTimestampArraySeen: workoutStreamEmptySeen,
3611
+ historicalProviderRecordsSeen,
3612
+ historicalRecordsSeen,
3036
3613
  madeProgress,
3037
3614
  workoutStreamCursor: encodeJunctionWorkoutStreamCompletedIdentities(completedIdentities),
3038
3615
  yieldedAt: null,
@@ -3058,14 +3635,10 @@ export function createJunctionDeviceSyncProvider(config) {
3058
3635
  normalizationEvidence,
3059
3636
  };
3060
3637
  }
3061
- const receipt = await context.importSnapshot({
3062
- provider: "junction",
3063
- accountId: buildJunctionImportAccountId(context.account.externalAccountId),
3064
- connectionId: context.account.id,
3638
+ const receipt = await commitPreparedJunctionCanonicalImport(context, preparedImport, {
3065
3639
  importedAt: context.now,
3066
3640
  windowStart,
3067
3641
  windowEnd,
3068
- connections,
3069
3642
  summaries,
3070
3643
  timeseries: {},
3071
3644
  });
@@ -3100,7 +3673,7 @@ export function createJunctionDeviceSyncProvider(config) {
3100
3673
  }
3101
3674
  async function fetchOptionalJunctionResourceRecords(context, resourceCategory, resource, skippedOptionalResources, load) {
3102
3675
  try {
3103
- return await load();
3676
+ return await measureJunctionProviderRequest(context, "resource", load);
3104
3677
  }
3105
3678
  catch (error) {
3106
3679
  const failure = classifyOptionalJunctionResourceFailure(error, resourceCategory, resource, context.account.externalAccountId);
@@ -3116,6 +3689,15 @@ export function createJunctionDeviceSyncProvider(config) {
3116
3689
  return [];
3117
3690
  }
3118
3691
  }
3692
+ async function measureJunctionProviderRequest(context, category, load) {
3693
+ const startedAt = performance.now();
3694
+ try {
3695
+ return await load();
3696
+ }
3697
+ finally {
3698
+ context.recordProviderRequestTiming?.(category, Math.max(0, performance.now() - startedAt));
3699
+ }
3700
+ }
3119
3701
  function logSkippedOptionalJunctionResource(context, resourceCategory, resource, failure) {
3120
3702
  context.logger.warn?.("Skipping unavailable Junction resource response.", {
3121
3703
  errorCode: "JUNCTION_API_REQUEST_FAILED",
@@ -3161,8 +3743,26 @@ export function createJunctionDeviceSyncProvider(config) {
3161
3743
  if (retryDelayMs === null) {
3162
3744
  return null;
3163
3745
  }
3746
+ const sourceProviderSlug = normalizeProviderSlug(input.job.payload.sourceProviderSlug);
3747
+ const historicalProofAuthorization = readJunctionHistoricalProofAuthorization(input.job);
3164
3748
  const retryJob = buildExactWindowJob({
3165
3749
  kind: "backfill",
3750
+ payload: {
3751
+ emptyBackfillAttempts,
3752
+ ...(sourceProviderSlug ? { sourceProviderSlug } : {}),
3753
+ ...(historicalProofAuthorization
3754
+ ? {
3755
+ historicalProofFirstSeenAt: historicalProofAuthorization.firstSeenAt,
3756
+ historicalProofSourceProviderSlug: historicalProofAuthorization.sourceProviderSlug,
3757
+ }
3758
+ : {}),
3759
+ ...(input.historicalProviderRecordsSeen
3760
+ ? { historicalProviderRecordsSeen: true }
3761
+ : {}),
3762
+ ...(input.historicalRecordsSeen
3763
+ ? { historicalRecordsSeen: true }
3764
+ : {}),
3765
+ },
3166
3766
  priority: Math.max(input.job.priority, JUNCTION_HISTORICAL_BACKFILL_RETRY_PRIORITY),
3167
3767
  windowEnd: input.window.windowEnd,
3168
3768
  windowStart: input.window.windowStart,
@@ -3170,33 +3770,67 @@ export function createJunctionDeviceSyncProvider(config) {
3170
3770
  return {
3171
3771
  ...retryJob,
3172
3772
  availableAt: addMilliseconds(input.now, retryDelayMs),
3173
- payload: {
3174
- ...(retryJob.payload ?? {}),
3175
- emptyBackfillAttempts,
3176
- },
3177
3773
  };
3178
3774
  }
3179
- function buildFullJobTimeseriesContinuationResult(input) {
3180
- const continuationJob = input.continuation
3775
+ async function completeSourceScopedHistoricalBackfill(input) {
3776
+ const historicalProofAuthorization = readJunctionHistoricalProofAuthorization(input.job);
3777
+ if (!input.sourceProviderSlug
3778
+ || !historicalProofAuthorization
3779
+ || !await isJunctionHistoricalProofAuthorizationCurrent(input.context, historicalProofAuthorization)
3780
+ || (input.historicalProviderRecordsSeen && !input.historicalRecordsSeen)
3781
+ || !input.sourceProviders.some((provider) => areJunctionProviderSlugsDataEquivalent(provider.origin.sourceProviderSlug ?? provider.slug, input.sourceProviderSlug))) {
3782
+ return;
3783
+ }
3784
+ await projectJunctionSources(input.context, input.sourceProviders, {
3785
+ historicalBackfillCompletedProviderSlug: input.sourceProviderSlug,
3786
+ });
3787
+ }
3788
+ async function buildFullJobTimeseriesContinuationResult(input) {
3789
+ const historicalProofAuthorization = readJunctionHistoricalProofAuthorization(input.job);
3790
+ const historicalProofStillCurrent = !historicalProofAuthorization
3791
+ || await isJunctionHistoricalProofAuthorizationCurrent(input.context, historicalProofAuthorization);
3792
+ const continuationJob = input.continuation && historicalProofStillCurrent
3181
3793
  ? buildFullJobTimeseriesContinuationJob({
3182
3794
  deferredEmptyBackfillAttempts: readHistoricalBackfillJobEmptyAttempts(input.job),
3795
+ historicalProviderRecordsSeen: input.historicalProviderRecordsSeen,
3796
+ historicalRecordsSeen: input.historicalRecordsSeen,
3183
3797
  job: input.job,
3184
3798
  ...input.continuation,
3185
3799
  window: input.window,
3186
3800
  })
3187
3801
  : null;
3188
- const scheduledJob = continuationJob
3189
- ? { ...continuationJob, availableAt: input.context.now }
3190
- : buildDeferredNonConnectHistoricalBackfillRetry({
3191
- account: input.context.account,
3802
+ const scheduledJob = !historicalProofStillCurrent
3803
+ ? null
3804
+ : continuationJob
3805
+ ? { ...continuationJob, availableAt: input.context.now }
3806
+ : buildDeferredNonConnectHistoricalBackfillRetry({
3807
+ account: input.context.account,
3808
+ historicalProviderRecordsSeen: input.historicalProviderRecordsSeen,
3809
+ historicalRecordsSeen: input.historicalRecordsSeen,
3810
+ job: input.job,
3811
+ now: input.context.now,
3812
+ window: input.window,
3813
+ });
3814
+ if (!scheduledJob && input.job.kind === "backfill") {
3815
+ await completeSourceScopedHistoricalBackfill({
3816
+ context: input.context,
3817
+ historicalProviderRecordsSeen: input.historicalProviderRecordsSeen,
3818
+ historicalRecordsSeen: input.historicalRecordsSeen,
3192
3819
  job: input.job,
3193
- now: input.context.now,
3194
- window: input.window,
3820
+ sourceProviderSlug: normalizeProviderSlug(input.job.payload.sourceProviderSlug),
3821
+ sourceProviders: input.sourceProviders,
3195
3822
  });
3196
- return withJunctionSkippedResourceMetadata(input.context, {
3197
- ...(scheduledJob ? { scheduledJobs: [scheduledJob] } : {}),
3198
- nextReconcileAt: resolveJunctionNextReconcileAt(input.context.account, input.context.now, addMilliseconds(input.context.now, reconcileIntervalMs)),
3199
- }, input.skippedOptionalResources);
3823
+ }
3824
+ return withJunctionWorkoutStreamEmptyReplay({
3825
+ job: input.job,
3826
+ now: input.context.now,
3827
+ replayWindow: input.replayWindow,
3828
+ result: withJunctionSkippedResourceMetadata(input.context, {
3829
+ ...(scheduledJob ? { scheduledJobs: [scheduledJob] } : {}),
3830
+ nextReconcileAt: resolveJunctionNextReconcileAt(input.context.account, input.context.now, addMilliseconds(input.context.now, reconcileIntervalMs)),
3831
+ }, input.skippedOptionalResources),
3832
+ sourceProviderSlug: normalizeProviderSlug(input.job.payload.sourceProviderSlug),
3833
+ });
3200
3834
  }
3201
3835
  function buildFullJobTimeseriesContinuationJob(input) {
3202
3836
  if (input.job.kind !== "backfill" && input.job.kind !== "reconcile") {
@@ -3212,7 +3846,12 @@ export function createJunctionDeviceSyncProvider(config) {
3212
3846
  })) {
3213
3847
  return null;
3214
3848
  }
3215
- const sourceProviderSlug = normalizeProviderSlug(input.job.payload.sourceProviderSlug);
3849
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.job.payload.sourceProviderSlug);
3850
+ const historicalProofAuthorization = readJunctionHistoricalProofAuthorization(input.job);
3851
+ const historicalProviderRecordsSeen = input.historicalProviderRecordsSeen === true
3852
+ || input.job.payload.historicalProviderRecordsSeen === true;
3853
+ const historicalRecordsSeen = input.historicalRecordsSeen === true
3854
+ || input.job.payload.historicalRecordsSeen === true;
3216
3855
  return buildExactWindowJob({
3217
3856
  kind: input.job.kind,
3218
3857
  payload: {
@@ -3220,12 +3859,27 @@ export function createJunctionDeviceSyncProvider(config) {
3220
3859
  ? { emptyBackfillAttempts: input.deferredEmptyBackfillAttempts }
3221
3860
  : {}),
3222
3861
  ...(sourceProviderSlug ? { sourceProviderSlug } : {}),
3862
+ ...(historicalProofAuthorization
3863
+ ? {
3864
+ historicalProofFirstSeenAt: historicalProofAuthorization.firstSeenAt,
3865
+ historicalProofSourceProviderSlug: historicalProofAuthorization.sourceProviderSlug,
3866
+ }
3867
+ : {}),
3868
+ ...(input.job.kind === "backfill" && historicalProviderRecordsSeen
3869
+ ? { historicalProviderRecordsSeen: true }
3870
+ : {}),
3871
+ ...(input.job.kind === "backfill" && historicalRecordsSeen
3872
+ ? { historicalRecordsSeen: true }
3873
+ : {}),
3223
3874
  timeseriesCursor: input.timeseriesCursor,
3224
3875
  timeseriesResourceCursor,
3225
3876
  ...(input.timeseriesWindowHours === 1 ? { timeseriesWindowHours: 1 } : {}),
3226
3877
  ...(input.workoutStreamCursor
3227
3878
  ? { workoutStreamCursor: input.workoutStreamCursor }
3228
3879
  : {}),
3880
+ ...(input.workoutStreamEmptySeen
3881
+ ? { workoutStreamEmptySeen: true }
3882
+ : {}),
3229
3883
  },
3230
3884
  priority: input.job.priority,
3231
3885
  windowEnd: input.window.windowEnd,
@@ -3236,7 +3890,7 @@ export function createJunctionDeviceSyncProvider(config) {
3236
3890
  if (Date.parse(input.windowStart) >= Date.parse(input.windowEnd)) {
3237
3891
  return null;
3238
3892
  }
3239
- const sourceProviderSlug = normalizeProviderSlug(input.job.payload.sourceProviderSlug);
3893
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.job.payload.sourceProviderSlug);
3240
3894
  if (input.job.kind !== "resource") {
3241
3895
  return null;
3242
3896
  }
@@ -3264,6 +3918,9 @@ export function createJunctionDeviceSyncProvider(config) {
3264
3918
  windowEnd: input.windowEnd,
3265
3919
  windowStart: input.windowStart,
3266
3920
  workoutStreamCursor: input.workoutStreamCursor || undefined,
3921
+ workoutStreamEmptySeen: input.workoutStreamEmptySeen === true
3922
+ ? true
3923
+ : undefined,
3267
3924
  });
3268
3925
  return {
3269
3926
  kind: "resource",
@@ -3300,31 +3957,41 @@ export function createJunctionDeviceSyncProvider(config) {
3300
3957
  && normalizeString(account.metadata[JUNCTION_HISTORICAL_BACKFILL_METADATA_KEYS.windowStart]) === connectWindow.windowStart
3301
3958
  && normalizeString(account.metadata[JUNCTION_HISTORICAL_BACKFILL_METADATA_KEYS.windowEnd]) === connectWindow.windowEnd;
3302
3959
  }
3303
- function buildInitialJobs(now, sourceProviderSlug) {
3304
- const normalizedSourceProviderSlug = normalizeProviderSlug(sourceProviderSlug);
3305
- const payload = normalizedSourceProviderSlug
3306
- ? { sourceProviderSlug: normalizedSourceProviderSlug }
3960
+ function buildInitialJobs(now, sourceProviderSlug, historicalProofAuthorization) {
3961
+ const normalizedSourceProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
3962
+ const sourcePayload = normalizedSourceProviderSlug
3963
+ ? {
3964
+ sourceProviderSlug: normalizedSourceProviderSlug,
3965
+ }
3307
3966
  : undefined;
3308
3967
  return [
3309
3968
  buildWindowJob({
3310
3969
  kind: "backfill",
3311
3970
  now,
3312
- payload,
3971
+ payload: sourcePayload && historicalProofAuthorization
3972
+ ? {
3973
+ ...sourcePayload,
3974
+ historicalProofFirstSeenAt: historicalProofAuthorization.firstSeenAt,
3975
+ historicalProofSourceProviderSlug: historicalProofAuthorization.sourceProviderSlug,
3976
+ }
3977
+ : sourcePayload,
3313
3978
  windowStart: subtractDays(now, summaryBackfillDays),
3314
3979
  priority: JUNCTION_HISTORICAL_BACKFILL_PRIORITY,
3315
3980
  }),
3316
3981
  buildWindowJob({
3317
3982
  kind: "reconcile",
3318
3983
  now,
3319
- payload,
3984
+ payload: sourcePayload,
3320
3985
  windowStart: subtractDays(now, reconcileDays),
3321
3986
  priority: JUNCTION_SCHEDULED_RECONCILE_PRIORITY,
3322
3987
  }),
3323
3988
  ];
3324
3989
  }
3325
3990
  function buildSourceConnectionWork(input) {
3991
+ const sourceProviderSlug = normalizeProviderSlug(input.sourceProviderSlug);
3992
+ const historicalProofAuthorization = normalizeJunctionHistoricalProofAuthorization(input.historicalProofAuthorization);
3326
3993
  return {
3327
- initialJobs: buildInitialJobs(input.now, input.sourceProviderSlug),
3994
+ initialJobs: buildInitialJobs(input.now, sourceProviderSlug, historicalProofAuthorization),
3328
3995
  nextReconcileAt: addMilliseconds(input.now, reconcileIntervalMs),
3329
3996
  };
3330
3997
  }
@@ -3370,32 +4037,207 @@ async function fetchJunctionTimeseriesWindow(junctionClient, input) {
3370
4037
  if (!maxSamples) {
3371
4038
  throw new TypeError("Junction workout_stream policy did not define bounded limits.");
3372
4039
  }
3373
- const candidates = await listJunctionWorkoutStreamCandidates(junctionClient, input);
4040
+ const { candidates } = await listJunctionWorkoutStreamCandidates(junctionClient, input);
3374
4041
  const features = [];
3375
4042
  for (const candidate of candidates) {
3376
- const feature = await fetchJunctionWorkoutStreamFeature(junctionClient, candidate, maxSamples, input.signal ?? null);
4043
+ const { feature } = await fetchJunctionWorkoutStreamFeature(junctionClient, candidate, maxSamples, input.signal ?? null);
3377
4044
  if (feature !== undefined) {
3378
4045
  features.push(feature);
3379
4046
  }
3380
4047
  }
3381
4048
  return features;
3382
4049
  }
4050
+ if (policy.normalizationMode === "ecg_recording_feature") {
4051
+ if (!policy.maxRecordsPerWindow || !policy.maxSamplesPerWindow) {
4052
+ throw new TypeError("Junction ECG voltage policy did not define bounded limits.");
4053
+ }
4054
+ return fetchJunctionElectrocardiogramVoltageFeatures(junctionClient, input, {
4055
+ maxRecordings: policy.maxRecordsPerWindow,
4056
+ maxSamples: policy.maxSamplesPerWindow,
4057
+ });
4058
+ }
3383
4059
  const records = await junctionClient.listTimeseries({
3384
4060
  ...input,
3385
4061
  maxRecords: input.maxRecords ?? policy.maxSamplesPerWindow,
3386
4062
  });
3387
- if (policy.normalizationMode !== "ecg_recording_feature") {
3388
- return records;
4063
+ return records;
4064
+ }
4065
+ async function fetchJunctionElectrocardiogramVoltageFeatures(junctionClient, input, limits) {
4066
+ const summaries = await junctionClient.listSummary({
4067
+ collectionWorkLimit: input.collectionWorkLimit,
4068
+ maxRecords: limits.maxRecordings + 1,
4069
+ resource: "electrocardiogram",
4070
+ signal: input.signal ?? null,
4071
+ sourceProviderSlug: input.sourceProviderSlug,
4072
+ userId: input.userId,
4073
+ windowEnd: input.windowEnd,
4074
+ windowStart: input.windowStart,
4075
+ });
4076
+ const candidates = selectJunctionElectrocardiogramRecordingCandidates(summaries, {
4077
+ maxRecordings: limits.maxRecordings,
4078
+ requestedSourceProviderSlug: input.sourceProviderSlug,
4079
+ windowEnd: input.windowEnd,
4080
+ windowStart: input.windowStart,
4081
+ });
4082
+ const expectedSampleCount = candidates.reduce((total, candidate) => total + candidate.voltageSampleCount, 0);
4083
+ if (expectedSampleCount > limits.maxSamples) {
4084
+ throw junctionElectrocardiogramBindingError("sample_limit_exceeded", {
4085
+ expectedSampleCount,
4086
+ maxSampleCount: limits.maxSamples,
4087
+ });
4088
+ }
4089
+ const features = [];
4090
+ for (const candidate of candidates) {
4091
+ if (candidate.voltageSampleCount === 0) {
4092
+ continue;
4093
+ }
4094
+ const samples = await junctionClient.listElectrocardiogramVoltage({
4095
+ collectionWorkLimit: input.collectionWorkLimit,
4096
+ maxRecords: candidate.voltageSampleCount + 1,
4097
+ recordingId: candidate.recordingId,
4098
+ signal: input.signal ?? null,
4099
+ sourceInstanceId: candidate.sourceInstanceId,
4100
+ sourceProviderSlug: candidate.sourceProviderSlug,
4101
+ sourceType: candidate.sourceType,
4102
+ userId: input.userId,
4103
+ windowEnd: candidate.sessionEnd,
4104
+ windowStart: candidate.sessionStart,
4105
+ });
4106
+ let candidateFeatures;
4107
+ try {
4108
+ candidateFeatures = reduceJunctionElectrocardiogramVoltageRecords(samples, { maxRecordings: 1, maxSamples: limits.maxSamples });
4109
+ }
4110
+ catch (error) {
4111
+ if (isDeviceSyncError(error)) {
4112
+ throw error;
4113
+ }
4114
+ throw junctionElectrocardiogramBindingError("sample_normalization_invalid");
4115
+ }
4116
+ const feature = candidateFeatures[0];
4117
+ if (!feature || candidateFeatures.length !== 1) {
4118
+ throw junctionElectrocardiogramBindingError("feature_cardinality_mismatch", {
4119
+ actualRecordingCount: candidateFeatures.length,
4120
+ expectedRecordingCount: 1,
4121
+ });
4122
+ }
4123
+ const actualSampleCount = readJunctionElectrocardiogramSampleCount(feature);
4124
+ if (actualSampleCount !== candidate.voltageSampleCount) {
4125
+ throw junctionElectrocardiogramBindingError("sample_count_mismatch", {
4126
+ actualSampleCount: actualSampleCount ?? 0,
4127
+ expectedSampleCount: candidate.voltageSampleCount,
4128
+ });
4129
+ }
4130
+ features.push(feature);
4131
+ }
4132
+ return features;
4133
+ }
4134
+ function selectJunctionElectrocardiogramRecordingCandidates(summaries, input) {
4135
+ const windowStartMs = Date.parse(input.windowStart);
4136
+ const windowEndMs = Date.parse(input.windowEnd);
4137
+ if (!Number.isFinite(windowStartMs)
4138
+ || !Number.isFinite(windowEndMs)
4139
+ || windowEndMs <= windowStartMs) {
4140
+ throw new TypeError("Junction ECG collection window was invalid.");
4141
+ }
4142
+ const selected = new Map();
4143
+ for (const value of summaries) {
4144
+ const summary = readPlainObject(value);
4145
+ const recordingId = normalizeString(summary?.id);
4146
+ const sessionStart = readJunctionElectrocardiogramTimestamp(summary, ["sessionStart", "session_start"]);
4147
+ const sessionEnd = readJunctionElectrocardiogramTimestamp(summary, ["sessionEnd", "session_end"]);
4148
+ const voltageSampleCount = readJunctionElectrocardiogramSampleCount(summary);
4149
+ if (!summary || !recordingId || !sessionStart || !sessionEnd || voltageSampleCount === null) {
4150
+ throw junctionElectrocardiogramBindingError("summary_identity_incomplete");
4151
+ }
4152
+ const sessionStartMs = Date.parse(sessionStart);
4153
+ const sessionEndMs = Date.parse(sessionEnd);
4154
+ if (sessionEndMs < sessionStartMs) {
4155
+ throw junctionElectrocardiogramBindingError("summary_window_invalid");
4156
+ }
4157
+ if (sessionStartMs < windowStartMs || sessionStartMs >= windowEndMs) {
4158
+ continue;
4159
+ }
4160
+ const unresolvedOrigin = resolveJunctionOrigin(summary);
4161
+ const canonicalSourceProviderSlug = canonicalizeJunctionProviderSlug(unresolvedOrigin.sourceProviderSlug);
4162
+ const origin = resolveJunctionOrigin({
4163
+ ...summary,
4164
+ sourceProviderSlug: canonicalSourceProviderSlug
4165
+ ?? unresolvedOrigin.sourceProviderSlug,
4166
+ });
4167
+ const sourceProviderSlug = normalizeJunctionSourceProviderSlug(origin.sourceProviderSlug);
4168
+ if (!sourceProviderSlug
4169
+ || (input.requestedSourceProviderSlug
4170
+ && !areJunctionProviderSlugsDataEquivalent(sourceProviderSlug, input.requestedSourceProviderSlug))) {
4171
+ throw junctionElectrocardiogramBindingError("summary_source_inconsistent");
4172
+ }
4173
+ const candidate = {
4174
+ recordingId,
4175
+ sessionEnd,
4176
+ sessionStart,
4177
+ sourceInstanceId: normalizeString(origin.sourceInstanceId),
4178
+ sourceProviderSlug,
4179
+ sourceType: normalizeString(origin.sourceType),
4180
+ voltageSampleCount,
4181
+ };
4182
+ const existing = selected.get(recordingId);
4183
+ if (existing && JSON.stringify(existing) !== JSON.stringify(candidate)) {
4184
+ throw junctionElectrocardiogramBindingError("summary_identity_conflict");
4185
+ }
4186
+ selected.set(recordingId, candidate);
4187
+ }
4188
+ if (selected.size > input.maxRecordings) {
4189
+ throw junctionElectrocardiogramBindingError("recording_limit_exceeded", {
4190
+ actualRecordingCount: selected.size,
4191
+ maxRecordingCount: input.maxRecordings,
4192
+ });
3389
4193
  }
3390
- if (!policy.maxRecordsPerWindow || !policy.maxSamplesPerWindow) {
3391
- throw new TypeError("Junction ECG voltage policy did not define bounded limits.");
4194
+ const candidates = [...selected.values()].sort((left, right) => left.sessionStart.localeCompare(right.sessionStart)
4195
+ || left.recordingId.localeCompare(right.recordingId));
4196
+ const latestEndBySource = new Map();
4197
+ for (const candidate of candidates) {
4198
+ const sourceIdentity = JSON.stringify([
4199
+ candidate.sourceProviderSlug,
4200
+ candidate.sourceType ?? null,
4201
+ candidate.sourceInstanceId ?? null,
4202
+ ]);
4203
+ const latestEnd = latestEndBySource.get(sourceIdentity);
4204
+ if (latestEnd !== undefined && Date.parse(candidate.sessionStart) < latestEnd) {
4205
+ throw junctionElectrocardiogramBindingError("summary_windows_ambiguous");
4206
+ }
4207
+ latestEndBySource.set(sourceIdentity, Date.parse(candidate.sessionEnd));
3392
4208
  }
3393
- return reduceJunctionElectrocardiogramVoltageRecords(records, {
3394
- maxRecordings: policy.maxRecordsPerWindow,
3395
- maxSamples: policy.maxSamplesPerWindow,
4209
+ return candidates;
4210
+ }
4211
+ function readJunctionElectrocardiogramTimestamp(summary, keys) {
4212
+ for (const key of keys) {
4213
+ const value = summary?.[key];
4214
+ const timestamp = value instanceof Date
4215
+ ? value.getTime()
4216
+ : Date.parse(normalizeString(value) ?? "");
4217
+ if (Number.isFinite(timestamp)) {
4218
+ return new Date(timestamp).toISOString();
4219
+ }
4220
+ }
4221
+ return null;
4222
+ }
4223
+ function readJunctionElectrocardiogramSampleCount(summary) {
4224
+ const value = summary?.voltageSampleCount ?? summary?.voltage_sample_count;
4225
+ return typeof value === "number"
4226
+ && Number.isSafeInteger(value)
4227
+ && value >= 0
4228
+ ? value
4229
+ : null;
4230
+ }
4231
+ function junctionElectrocardiogramBindingError(reason, details = {}) {
4232
+ return deviceSyncError({
4233
+ code: "JUNCTION_ECG_RECORDING_BINDING_INCOMPLETE",
4234
+ details: { reason, ...details },
4235
+ message: "Junction ECG summary and voltage response were inconsistent.",
4236
+ retryable: true,
4237
+ httpStatus: 502,
3396
4238
  });
3397
4239
  }
3398
- async function listJunctionWorkoutStreamCandidates(junctionClient, input) {
4240
+ async function listJunctionWorkoutStreamCandidates(junctionClient, input, strictSourceScope = false, eligibleSourceProviderSlugs) {
3399
4241
  const maxWorkouts = resolveJunctionTimeseriesResourcePolicy("workout_stream")
3400
4242
  ?.maxRecordsPerWindow;
3401
4243
  if (!maxWorkouts) {
@@ -3409,10 +4251,25 @@ async function listJunctionWorkoutStreamCandidates(junctionClient, input) {
3409
4251
  userId: input.userId,
3410
4252
  windowStart: input.windowStart,
3411
4253
  windowEnd: input.windowEnd,
3412
- maxRecords: maxWorkouts + 1,
3413
4254
  });
3414
- const summaries = indexRecords.map((record) => withJunctionSourceProviderFallback(record, input.sourceProviderSlug));
3415
- return selectJunctionWorkoutStreamCandidates(summaries, maxWorkouts);
4255
+ const summaries = strictSourceScope
4256
+ ? scopeJunctionRecordsToSourceProvider(indexRecords, input.sourceProviderSlug)
4257
+ : indexRecords.map((record) => withJunctionSourceProviderFallback(record, input.sourceProviderSlug));
4258
+ const eligibleSummaries = eligibleSourceProviderSlugs === undefined
4259
+ ? summaries
4260
+ : summaries.filter((summary) => {
4261
+ const summaryRecord = readPlainObject(summary);
4262
+ if (!summaryRecord) {
4263
+ return false;
4264
+ }
4265
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(resolveJunctionOrigin(summaryRecord).sourceProviderSlug);
4266
+ return sourceProviderSlug !== null
4267
+ && eligibleSourceProviderSlugs.has(sourceProviderSlug);
4268
+ });
4269
+ return {
4270
+ candidates: selectJunctionWorkoutStreamCandidates(eligibleSummaries, maxWorkouts),
4271
+ providerRecordsSeen: indexRecords.length > 0,
4272
+ };
3416
4273
  }
3417
4274
  async function fetchJunctionWorkoutStreamFeature(junctionClient, candidate, maxSamples, signal, collectionWorkLimit) {
3418
4275
  const stream = await junctionClient.getWorkoutStream({
@@ -3420,11 +4277,26 @@ async function fetchJunctionWorkoutStreamFeature(junctionClient, candidate, maxS
3420
4277
  signal,
3421
4278
  workoutId: candidate.workoutId,
3422
4279
  });
3423
- return reduceJunctionWorkoutStreamPayload({
3424
- maxSamples,
3425
- stream,
3426
- summary: candidate.summary,
3427
- });
4280
+ try {
4281
+ return {
4282
+ emptyTimestampArray: false,
4283
+ feature: reduceJunctionWorkoutStreamPayload({
4284
+ maxSamples,
4285
+ stream,
4286
+ summary: candidate.summary,
4287
+ }),
4288
+ };
4289
+ }
4290
+ catch (error) {
4291
+ if (error instanceof JunctionWorkoutStreamTimestampCardinalityError
4292
+ && error.diagnostic.kind === "empty") {
4293
+ return {
4294
+ emptyTimestampArray: true,
4295
+ feature: undefined,
4296
+ };
4297
+ }
4298
+ throw error;
4299
+ }
3428
4300
  }
3429
4301
  function withJunctionSourceProviderFallback(value, sourceProviderSlug) {
3430
4302
  const record = readPlainObject(value);
@@ -3434,6 +4306,24 @@ function withJunctionSourceProviderFallback(value, sourceProviderSlug) {
3434
4306
  }
3435
4307
  return { ...record, sourceProviderSlug: normalizedSource };
3436
4308
  }
4309
+ function scopeJunctionRecordsToSourceProvider(records, sourceProviderSlug) {
4310
+ const normalizedSource = normalizeProviderSlug(sourceProviderSlug);
4311
+ if (!normalizedSource) {
4312
+ return [...records];
4313
+ }
4314
+ return records.flatMap((record) => {
4315
+ const scopedRecord = withJunctionSourceProviderFallback(record, normalizedSource);
4316
+ const scopedRecordObject = readPlainObject(scopedRecord);
4317
+ if (!scopedRecordObject) {
4318
+ return [];
4319
+ }
4320
+ const recordSourceProviderSlug = normalizeProviderSlug(resolveJunctionOrigin(scopedRecordObject).sourceProviderSlug);
4321
+ return recordSourceProviderSlug
4322
+ && areJunctionProviderSlugsDataEquivalent(recordSourceProviderSlug, normalizedSource)
4323
+ ? [scopedRecord]
4324
+ : [];
4325
+ });
4326
+ }
3437
4327
  function withJunctionSkippedResourceMetadata(context, result, skippedOptionalResources) {
3438
4328
  if (skippedOptionalResources.length === 0) {
3439
4329
  return result;
@@ -3558,6 +4448,24 @@ function classifyOptionalJunctionResourceFailure(error, resourceCategory, resour
3558
4448
  ...(responseDetail ? { responseDetail } : {}),
3559
4449
  };
3560
4450
  }
4451
+ function classifyOptionalJunctionWorkoutStreamCandidateFailure(error, accountExternalId) {
4452
+ const existing = classifyOptionalJunctionResourceFailure(error, "timeseries", "workout_stream", accountExternalId);
4453
+ if (existing) {
4454
+ return existing;
4455
+ }
4456
+ if (!isDeviceSyncError(error)
4457
+ || error.code !== "JUNCTION_API_REQUEST_FAILED"
4458
+ || error.details?.status !== 400) {
4459
+ return null;
4460
+ }
4461
+ const reason = classifyClearOptionalJunctionResourceFailureReason({
4462
+ responseErrorCode: readJunctionDiagnosticString(error.details.responseErrorCode),
4463
+ responseErrorDescription: readJunctionDiagnosticString(error.details.responseErrorDescription),
4464
+ });
4465
+ return reason === "unsupported"
4466
+ ? { reason, responseStatus: 400 }
4467
+ : null;
4468
+ }
3561
4469
  function isRetryableDeviceSyncFailure(error) {
3562
4470
  return isDeviceSyncError(error) && error.retryable;
3563
4471
  }
@@ -4486,13 +5394,6 @@ function resolveJunctionProviderRouteSlug(value) {
4486
5394
  return resolveJunctionDeviceConnectRouteByProviderSlug(normalized)?.route.sourceProviderSlug
4487
5395
  ?? normalized;
4488
5396
  }
4489
- function areJunctionProviderSlugsRouteEquivalent(left, right) {
4490
- const normalizedLeft = normalizeProviderSlug(left);
4491
- const normalizedRight = normalizeProviderSlug(right);
4492
- return normalizedLeft !== null
4493
- && normalizedRight !== null
4494
- && areJunctionDeviceConnectProviderSlugsEquivalent(normalizedLeft, normalizedRight);
4495
- }
4496
5397
  function resolveJunctionAccountSourceIdentities(sources) {
4497
5398
  const identities = [];
4498
5399
  for (const source of sources) {
@@ -4507,7 +5408,7 @@ function resolveJunctionAccountSourceIdentities(sources) {
4507
5408
  function resolveJunctionAccountSourceStates(sources) {
4508
5409
  const states = [];
4509
5410
  for (const source of sources) {
4510
- if (states.some((state) => areJunctionProviderSlugsRouteEquivalent(state.identitySource.sourceProviderSlug, source.sourceProviderSlug))) {
5411
+ if (states.some((state) => areJunctionProviderSlugsDataEquivalent(state.identitySource.sourceProviderSlug, source.sourceProviderSlug))) {
4511
5412
  continue;
4512
5413
  }
4513
5414
  const state = resolveJunctionAccountSourceState(sources, source.sourceProviderSlug);
@@ -4520,7 +5421,7 @@ function resolveJunctionAccountSourceStates(sources) {
4520
5421
  function mergeJunctionAccountSourceIdentities(accountSourceIdentities, requestedSourceIdentities) {
4521
5422
  return [
4522
5423
  ...accountSourceIdentities,
4523
- ...requestedSourceIdentities.filter((requestedIdentity) => !accountSourceIdentities.some((accountIdentity) => areJunctionProviderSlugsRouteEquivalent(requestedIdentity.sourceProviderSlug, accountIdentity.sourceProviderSlug))),
5424
+ ...requestedSourceIdentities.filter((requestedIdentity) => !accountSourceIdentities.some((accountIdentity) => areJunctionProviderSlugsDataEquivalent(requestedIdentity.sourceProviderSlug, accountIdentity.sourceProviderSlug))),
4524
5425
  ];
4525
5426
  }
4526
5427
  function resolveJunctionAccountSourceIdentity(sources, sourceProviderSlug, allowUnlistedSource = false) {
@@ -4559,7 +5460,7 @@ function findJunctionAccountSource(sources, sourceProviderSlug) {
4559
5460
  ?.identitySource;
4560
5461
  }
4561
5462
  function findJunctionAccountSources(sources, sourceProviderSlug) {
4562
- return sources.filter((source) => areJunctionProviderSlugsRouteEquivalent(source.sourceProviderSlug, sourceProviderSlug));
5463
+ return sources.filter((source) => areJunctionProviderSlugsDataEquivalent(source.sourceProviderSlug, sourceProviderSlug));
4563
5464
  }
4564
5465
  function resolveJunctionAccountSourceState(sources, sourceProviderSlug) {
4565
5466
  const [first, ...rest] = findJunctionAccountSources(sources, sourceProviderSlug);
@@ -4574,7 +5475,7 @@ function stripUndefined(value) {
4574
5475
  function sanitizeJunctionImportConnections(providers, sourceIdentities = []) {
4575
5476
  return providers.map((provider) => {
4576
5477
  const sourceProviderSlug = provider.origin.sourceProviderSlug ?? provider.slug;
4577
- const projectedSourceIdentity = sourceIdentities.find((identity) => areJunctionProviderSlugsRouteEquivalent(sourceProviderSlug, identity.sourceProviderSlug));
5478
+ const projectedSourceIdentity = sourceIdentities.find((identity) => areJunctionProviderSlugsDataEquivalent(sourceProviderSlug, identity.sourceProviderSlug));
4578
5479
  return stripUndefined({
4579
5480
  sourceProviderSlug: projectedSourceIdentity?.sourceProviderSlug ?? sourceProviderSlug,
4580
5481
  sourceInstanceId: projectedSourceIdentity?.sourceInstanceId ?? provider.origin.sourceInstanceId,
@@ -4593,11 +5494,40 @@ async function prepareJunctionImportSnapshot(context, snapshots, providers, opti
4593
5494
  sourceStatusRequirement,
4594
5495
  });
4595
5496
  }
5497
+ async function hasJunctionSourceScopedAdmittedSnapshotRecords(input) {
5498
+ const listedSources = input.listedSources
5499
+ ?? (input.context.listConnectionSources
5500
+ ? await input.context.listConnectionSources({
5501
+ sourceProviderSlug: input.sourceProviderSlug,
5502
+ })
5503
+ : input.context.account.sources ?? []);
5504
+ const sourceAuthorities = listedSources.filter((source) => areJunctionProviderSlugsDataEquivalent(source.sourceProviderSlug, input.sourceProviderSlug));
5505
+ const sourceProviders = input.sourceProviders.filter((provider) => areJunctionProviderSlugsDataEquivalent(provider.origin.sourceProviderSlug ?? provider.slug, input.sourceProviderSlug));
5506
+ if (sourceAuthorities.length === 0 || sourceProviders.length === 0) {
5507
+ return false;
5508
+ }
5509
+ const options = input.options ?? {};
5510
+ const sourceIdentities = options.projectAccountSourceIdentities
5511
+ ? mergeJunctionAccountSourceIdentities(resolveJunctionAccountSourceIdentities(sourceAuthorities), options.sourceIdentities ?? [])
5512
+ : options.sourceIdentities;
5513
+ const prepared = prepareJunctionImportSnapshotForSources(input.snapshots, sourceProviders, sourceAuthorities, {
5514
+ ...options,
5515
+ ...(sourceIdentities ? { sourceIdentities } : {}),
5516
+ }, {
5517
+ allowUnlistedSources: false,
5518
+ sourceStatusRequirement: "not_disconnected",
5519
+ });
5520
+ // This is evidence that the source-scoped historical pull produced an
5521
+ // admitted record. Canonical import still evaluates every account source,
5522
+ // preserving the Fitbit writer fence until cutover is actually ready.
5523
+ return hasJunctionSnapshotRecords(prepared.snapshots);
5524
+ }
4596
5525
  function prepareJunctionImportSnapshotForSources(snapshots, providers, currentSources, options = {}, admission = {}) {
4597
5526
  const allowUnlistedSources = admission.allowUnlistedSources ?? true;
4598
5527
  const sourceStatusRequirement = admission.sourceStatusRequirement ?? "not_disconnected";
4599
5528
  const sourceProviders = providers.filter((provider) => isJunctionSourceAdmittedForImport(currentSources, provider.origin.sourceProviderSlug ?? provider.slug, allowUnlistedSources, sourceStatusRequirement));
4600
5529
  return {
5530
+ canonicalCoverageFence: buildJunctionGoogleHealthCanonicalCoverageFence(currentSources),
4601
5531
  connections: sanitizeJunctionImportConnections(sourceProviders, options.sourceIdentities),
4602
5532
  sourceProviders,
4603
5533
  snapshots: sanitizeJunctionImportSnapshots(filterJunctionImportSnapshots(snapshots, providers, currentSources, allowUnlistedSources, sourceStatusRequirement), providers, options),
@@ -4614,15 +5544,133 @@ function filterJunctionImportSnapshots(snapshots, providers, sources, allowUnlis
4614
5544
  records.filter((record) => isJunctionImportRecordAdmitted(record, sourceReferences, sources, hasPendingSourceAdmission, allowUnlistedSources, sourceStatusRequirement)),
4615
5545
  ]));
4616
5546
  }
5547
+ async function commitPreparedJunctionCanonicalImport(context, preparedImport, snapshot, providerPulledAt, importOptions) {
5548
+ const receipt = await context.importSnapshot({
5549
+ provider: "junction",
5550
+ accountId: buildJunctionImportAccountId(context.account.externalAccountId),
5551
+ connectionId: context.account.id,
5552
+ ...snapshot,
5553
+ connections: preparedImport.connections,
5554
+ canonicalCoverageFence: preparedImport.canonicalCoverageFence,
5555
+ ...(providerPulledAt
5556
+ ? { canonicalCoverageProviderPulledAt: providerPulledAt }
5557
+ : {}),
5558
+ }, importOptions);
5559
+ await recordAcceptedJunctionFitbitCoverage(context, receipt);
5560
+ return receipt;
5561
+ }
5562
+ async function recordAcceptedJunctionFitbitCoverage(context, receipt) {
5563
+ const canonicalCoverage = readProviderSnapshotJunctionCanonicalCoverage(receipt);
5564
+ if (!readProviderSnapshotDurableDeliveryAccepted(receipt)
5565
+ || canonicalCoverage.length === 0
5566
+ || !context.upsertConnectionSource) {
5567
+ return;
5568
+ }
5569
+ const coverageByResource = new Map();
5570
+ for (const evidence of canonicalCoverage) {
5571
+ if (normalizeProviderSlug(evidence.sourceProviderSlug)
5572
+ !== JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG
5573
+ || !buildDeviceSyncSourceCanonicalCoverageBoundaryKey(evidence.resource)) {
5574
+ continue;
5575
+ }
5576
+ const existing = coverageByResource.get(evidence.resource);
5577
+ const coverageBoundary = existing
5578
+ ? maxJunctionCanonicalCoverageBoundary(evidence.resource, existing.coverageBoundary, evidence.coverageBoundary)
5579
+ : evidence.coverageBoundary;
5580
+ coverageByResource.set(evidence.resource, {
5581
+ coverageBoundary,
5582
+ coverageFinalizedAt: coverageBoundary === evidence.coverageBoundary
5583
+ ? evidence.coverageFinalizedAt ?? existing?.coverageFinalizedAt
5584
+ : existing?.coverageFinalizedAt,
5585
+ });
5586
+ }
5587
+ if (coverageByResource.size === 0) {
5588
+ return;
5589
+ }
5590
+ const sources = context.listConnectionSources
5591
+ ? await context.listConnectionSources({
5592
+ sourceProviderSlug: JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG,
5593
+ })
5594
+ : context.account.sources ?? [];
5595
+ const legacy = sources.find((source) => normalizeProviderSlug(source.sourceProviderSlug)
5596
+ === JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG);
5597
+ if (!legacy || legacy.status === "disconnected") {
5598
+ return;
5599
+ }
5600
+ const resourceAvailabilitySummary = {
5601
+ ...(legacy.resourceAvailabilitySummary ?? {}),
5602
+ };
5603
+ let changed = false;
5604
+ for (const [resource, coverage] of coverageByResource) {
5605
+ const key = buildDeviceSyncSourceCanonicalCoverageBoundaryKey(resource);
5606
+ if (!key) {
5607
+ continue;
5608
+ }
5609
+ const existingCoverage = readDeviceSyncSourceCanonicalCoverageBoundary(resourceAvailabilitySummary, resource);
5610
+ const coverageAdvanced = !existingCoverage
5611
+ || maxJunctionCanonicalCoverageBoundary(resource, existingCoverage, coverage.coverageBoundary) !== existingCoverage;
5612
+ if (coverageAdvanced) {
5613
+ resourceAvailabilitySummary[key] = coverage.coverageBoundary;
5614
+ changed = true;
5615
+ }
5616
+ const finalizedAtKey = buildDeviceSyncSourceCanonicalCoverageFinalizedAtKey(resource);
5617
+ if (!finalizedAtKey) {
5618
+ continue;
5619
+ }
5620
+ const existingFinalizedAt = readDeviceSyncSourceCanonicalCoverageFinalizedAt(resourceAvailabilitySummary, resource);
5621
+ if (coverageAdvanced) {
5622
+ resourceAvailabilitySummary[finalizedAtKey] = coverage.coverageFinalizedAt ?? null;
5623
+ continue;
5624
+ }
5625
+ if (existingCoverage !== coverage.coverageBoundary
5626
+ || !coverage.coverageFinalizedAt
5627
+ || existingFinalizedAt) {
5628
+ continue;
5629
+ }
5630
+ resourceAvailabilitySummary[finalizedAtKey] = coverage.coverageFinalizedAt;
5631
+ changed = true;
5632
+ }
5633
+ if (!changed) {
5634
+ return;
5635
+ }
5636
+ const sourceInstanceKey = legacy.sourceInstanceKey
5637
+ ?? buildJunctionProviderSourceInstanceKey({
5638
+ connectionId: context.account.id,
5639
+ sourceProviderSlug: JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG,
5640
+ });
5641
+ if (!sourceInstanceKey) {
5642
+ return;
5643
+ }
5644
+ await context.upsertConnectionSource({
5645
+ sourceInstanceKey,
5646
+ sourceProviderSlug: legacy.sourceProviderSlug,
5647
+ ...(legacy.displayName === undefined
5648
+ ? {}
5649
+ : { displayName: legacy.displayName }),
5650
+ status: legacy.status,
5651
+ resourceAvailabilitySummary,
5652
+ ...(legacy.lastErrorCode === undefined
5653
+ ? {}
5654
+ : { lastErrorCode: legacy.lastErrorCode }),
5655
+ ...(legacy.lastErrorMessage === undefined
5656
+ ? {}
5657
+ : { lastErrorMessage: legacy.lastErrorMessage }),
5658
+ ...(legacy.firstSeenAt ? { firstSeenAt: legacy.firstSeenAt } : {}),
5659
+ lastSeenAt: context.now,
5660
+ });
5661
+ }
4617
5662
  function isJunctionImportRecordAdmitted(value, sourceReferences, sources, hasPendingSourceAdmission, allowUnlistedSources, sourceStatusRequirement) {
4618
5663
  const record = readPlainObject(value);
4619
5664
  if (!record) {
4620
5665
  return true;
4621
5666
  }
4622
5667
  const fallback = readJunctionSourceReference(record, sourceReferences);
4623
- const sourceProviderSlug = normalizeProviderSlug(resolveJunctionOrigin(record, fallback).sourceProviderSlug);
5668
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(resolveJunctionOrigin(record, fallback).sourceProviderSlug);
4624
5669
  if (sourceProviderSlug) {
4625
- return isJunctionSourceAdmittedForImport(sources, sourceProviderSlug, allowUnlistedSources, sourceStatusRequirement);
5670
+ if (!isJunctionSourceAdmittedForImport(sources, sourceProviderSlug, allowUnlistedSources, sourceStatusRequirement)) {
5671
+ return false;
5672
+ }
5673
+ return true;
4626
5674
  }
4627
5675
  return !hasPendingSourceAdmission || !hasJunctionSourceReferenceIdentity(record);
4628
5676
  }
@@ -4678,7 +5726,7 @@ function sanitizeJunctionImportSnapshotValue(value, sourceReferences, options =
4678
5726
  (unresolvedPreservedSourceReference ? undefined : origin.sourceInstanceId);
4679
5727
  const sourceProviderSlug = normalizeProviderSlug(origin.sourceProviderSlug);
4680
5728
  const projectedSourceIdentity = options.sourceIdentities
4681
- ?.find((identity) => areJunctionProviderSlugsRouteEquivalent(sourceProviderSlug, identity.sourceProviderSlug));
5729
+ ?.find((identity) => areJunctionProviderSlugsDataEquivalent(sourceProviderSlug, identity.sourceProviderSlug));
4682
5730
  return stripUndefined({
4683
5731
  ...sanitized,
4684
5732
  sourceProviderSlug: projectedSourceIdentity?.sourceProviderSlug
@@ -4791,24 +5839,28 @@ function dedupeJunctionTimeseriesRecords(resource, records) {
4791
5839
  providerRowKey: buildJunctionTimeseriesProviderRowKey(resource, record),
4792
5840
  record,
4793
5841
  }));
4794
- const contentKeysByProviderRow = new Map();
5842
+ const contentKeysByStableIdentity = new Map();
4795
5843
  for (const candidate of candidates) {
4796
- if (!candidate.providerRowKey || !candidate.contentKey) {
5844
+ const stableIdentity = candidate.providerRowKey
5845
+ ?? (isJunctionBodyTimeseriesResource(resource) ? candidate.key : null);
5846
+ if (!stableIdentity || !candidate.contentKey) {
4797
5847
  continue;
4798
5848
  }
4799
- const contentKeys = contentKeysByProviderRow.get(candidate.providerRowKey)
5849
+ const contentKeys = contentKeysByStableIdentity.get(stableIdentity)
4800
5850
  ?? new Set();
4801
5851
  contentKeys.add(candidate.contentKey);
4802
- contentKeysByProviderRow.set(candidate.providerRowKey, contentKeys);
5852
+ contentKeysByStableIdentity.set(stableIdentity, contentKeys);
4803
5853
  }
4804
- const conflictingProviderRows = new Set([...contentKeysByProviderRow.entries()]
5854
+ const conflictingStableIdentities = new Set([...contentKeysByStableIdentity.entries()]
4805
5855
  .filter(([, contentKeys]) => contentKeys.size > 1)
4806
- .map(([providerRowKey]) => providerRowKey));
5856
+ .map(([stableIdentity]) => stableIdentity));
4807
5857
  const seen = new Set();
4808
5858
  const deduped = [];
4809
5859
  for (const candidate of candidates) {
4810
- if (candidate.providerRowKey
4811
- && conflictingProviderRows.has(candidate.providerRowKey)) {
5860
+ const stableIdentity = candidate.providerRowKey
5861
+ ?? (isJunctionBodyTimeseriesResource(resource) ? candidate.key : null);
5862
+ if (stableIdentity
5863
+ && conflictingStableIdentities.has(stableIdentity)) {
4812
5864
  continue;
4813
5865
  }
4814
5866
  if (candidate.key && seen.has(candidate.key)) {
@@ -4828,7 +5880,7 @@ function dedupeJunctionTimeseriesSnapshotRecords(snapshot) {
4828
5880
  }
4829
5881
  return deduped;
4830
5882
  }
4831
- function filterJunctionTimeseriesRecordsToWindow(resource, records, windowStart, windowEnd, dateQueryFormat) {
5883
+ function filterJunctionTimeseriesRecordsToWindow(resource, records, windowStart, windowEnd, dateQueryFormat, authorizedLocalDay, vaultTimeZone) {
4832
5884
  const startMs = Date.parse(windowStart);
4833
5885
  const endMs = Date.parse(windowEnd);
4834
5886
  if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) {
@@ -4841,9 +5893,41 @@ function filterJunctionTimeseriesRecordsToWindow(resource, records, windowStart,
4841
5893
  if (!entry) {
4842
5894
  return true;
4843
5895
  }
4844
- const rawTimestamp = resolveJunctionTimeseriesRecordRawTimestamp(entry, preferIntervalStart);
5896
+ const rawTimestamp = isJunctionBodyTimeseriesResource(resource)
5897
+ ? resolveJunctionTimeseriesRecordTimestampForResource(resource, entry)
5898
+ : resolveJunctionTimeseriesRecordRawTimestamp(entry, preferIntervalStart);
4845
5899
  if (!rawTimestamp) {
4846
- return true;
5900
+ return !isJunctionBodyTimeseriesResource(resource);
5901
+ }
5902
+ if (authorizedLocalDay) {
5903
+ // Drop only rows the strict complete-day parse proves belong to a
5904
+ // different valid calendar day, and only when the raw shape and the
5905
+ // record's declared semantics agree on absoluteness. Anything the
5906
+ // parse rejects — and any raw-versus-semantics disagreement, which the
5907
+ // importer must see to fail the day closed — stays in the collection
5908
+ // so a contradictory row can never be laundered into a partial or
5909
+ // empty replacement by being discarded as adjacent-day overlap.
5910
+ const parsed = parseJunctionCompleteDayTimestamp(rawTimestamp);
5911
+ if (!parsed) {
5912
+ return true;
5913
+ }
5914
+ if ((parsed.kind === "absolute")
5915
+ !== hasAbsoluteJunctionTimeseriesTimestamp(rawTimestamp, entry)) {
5916
+ return true;
5917
+ }
5918
+ if (parsed.kind === "absolute") {
5919
+ return toLocalDayKey(parsed.instant, authorizedLocalDay.timeZone)
5920
+ === authorizedLocalDay.dayKey;
5921
+ }
5922
+ return parsed.dayKey === authorizedLocalDay.dayKey;
5923
+ }
5924
+ if (!hasAbsoluteJunctionTimeseriesTimestamp(rawTimestamp, entry) && vaultTimeZone) {
5925
+ const rawDayKey = /^\d{4}-\d{2}-\d{2}/u.exec(rawTimestamp)?.[0];
5926
+ const windowStartDayKey = toLocalDayKey(windowStart, vaultTimeZone);
5927
+ const windowEndDayKey = toLocalDayKey(new Date(endMs - 1).toISOString(), vaultTimeZone);
5928
+ return Boolean(rawDayKey
5929
+ && rawDayKey >= windowStartDayKey
5930
+ && rawDayKey <= windowEndDayKey);
4847
5931
  }
4848
5932
  if (dateQueryFormat === "date"
4849
5933
  && policy?.normalizationMode === "daily_aggregate") {
@@ -4886,7 +5970,7 @@ function buildJunctionTimeseriesRecordKey(resource, record) {
4886
5970
  }
4887
5971
  const origin = resolveJunctionOrigin(entry);
4888
5972
  const sourceProviderSlug = normalizeProviderSlug(origin.sourceProviderSlug);
4889
- const timestamp = resolveJunctionTimeseriesRecordTimestamp(entry);
5973
+ const timestamp = resolveJunctionTimeseriesRecordTimestampForResource(resource, entry);
4890
5974
  if (!sourceProviderSlug || !timestamp) {
4891
5975
  return null;
4892
5976
  }
@@ -4936,7 +6020,7 @@ function buildJunctionTimeseriesRecordContentKey(resource, record) {
4936
6020
  }
4937
6021
  return JSON.stringify([
4938
6022
  "junction-timeseries-content",
4939
- resolveJunctionTimeseriesRecordTimestamp(entry) ?? "",
6023
+ resolveJunctionTimeseriesRecordTimestampForResource(resource, entry) ?? "",
4940
6024
  normalizeString(entry.recordedAt ?? entry.recorded_at ?? entry.updatedAt ?? entry.updated_at)
4941
6025
  ?? "",
4942
6026
  String(entry.timeZoneOffsetMinutes
@@ -4988,10 +6072,50 @@ function readJunctionTimeseriesProviderRowId(resource, entry) {
4988
6072
  // Sparse records without stable provider row ids retain their semantic tuple
4989
6073
  // identity so same-time rows remain distinct when their canonical bodies differ.
4990
6074
  function junctionTimeseriesRecordValueIdentity(resource, entry, includeProviderRowId = true) {
6075
+ if (JUNCTION_TEMPORAL_AUTHORITY_RESOURCES.has(resource)) {
6076
+ const providerRowId = [
6077
+ "id",
6078
+ "resourceId",
6079
+ "resource_id",
6080
+ "externalId",
6081
+ "external_id",
6082
+ "providerId",
6083
+ "provider_id",
6084
+ "recordId",
6085
+ "record_id",
6086
+ "sampleId",
6087
+ "sample_id",
6088
+ ].map((key) => normalizeString(entry[key])).find(Boolean) ?? "";
6089
+ const value = resource === "blood_oxygen"
6090
+ ? firstJunctionTimeseriesIdentityValue(entry, [
6091
+ "value",
6092
+ "spo2",
6093
+ "spO2",
6094
+ "bloodOxygen",
6095
+ "blood_oxygen",
6096
+ "oxygenSaturation",
6097
+ "oxygen_saturation",
6098
+ ])
6099
+ : firstJunctionTimeseriesIdentityValue(entry, [
6100
+ "value",
6101
+ "stressLevel",
6102
+ "stress_level",
6103
+ "averageStressLevel",
6104
+ "average_stress_level",
6105
+ "stress.average",
6106
+ "stressLevelValue",
6107
+ "stress_level_value",
6108
+ "score",
6109
+ ]);
6110
+ return [providerRowId, value];
6111
+ }
4991
6112
  if (!junctionTimeseriesRecordNeedsSemanticIdentity(resource)) {
4992
6113
  return [];
4993
6114
  }
4994
6115
  const providerRowId = readJunctionTimeseriesProviderRowId(resource, entry) ?? "";
6116
+ if (isJunctionBodyTimeseriesResource(resource) && includeProviderRowId) {
6117
+ return providerRowId ? [providerRowId] : [];
6118
+ }
4995
6119
  if (resource === "weight") {
4996
6120
  if (includeProviderRowId && providerRowId) {
4997
6121
  return [providerRowId];
@@ -5121,12 +6245,63 @@ function junctionTimeseriesRecordValueIdentity(resource, entry, includeProviderR
5121
6245
  String(entry.delivery_mode ?? entry.deliveryMode ?? ""),
5122
6246
  ];
5123
6247
  }
6248
+ function firstJunctionTimeseriesIdentityValue(entry, paths) {
6249
+ for (const path of paths) {
6250
+ let value = entry;
6251
+ for (const segment of path.split(".")) {
6252
+ value = readPlainObject(value)?.[segment];
6253
+ }
6254
+ const numericValue = typeof value === "number"
6255
+ ? value
6256
+ : Number(normalizeString(value));
6257
+ if (Number.isFinite(numericValue)) {
6258
+ return String(numericValue);
6259
+ }
6260
+ }
6261
+ return "";
6262
+ }
5124
6263
  function resolveJunctionTimeseriesRecordTimestamp(record, preferIntervalStart = false) {
5125
6264
  const rawTimestamp = resolveJunctionTimeseriesRecordRawTimestamp(record, preferIntervalStart);
5126
6265
  return rawTimestamp
5127
6266
  ? toIsoTimestampIfValid(rawTimestamp) ?? rawTimestamp
5128
6267
  : null;
5129
6268
  }
6269
+ const JUNCTION_INSTANT_BODY_TIMESERIES_RESOURCES = new Set([
6270
+ "fat",
6271
+ "weight",
6272
+ ]);
6273
+ const JUNCTION_INTERVAL_BODY_TIMESERIES_RESOURCES = new Set([
6274
+ "body_mass_index",
6275
+ "lean_body_mass",
6276
+ "waist_circumference",
6277
+ ]);
6278
+ function isJunctionBodyTimeseriesResource(resource) {
6279
+ return resource !== null
6280
+ && resource !== undefined
6281
+ && (JUNCTION_INSTANT_BODY_TIMESERIES_RESOURCES.has(resource)
6282
+ || JUNCTION_INTERVAL_BODY_TIMESERIES_RESOURCES.has(resource));
6283
+ }
6284
+ function resolveJunctionTimeseriesRecordTimestampForResource(resource, record) {
6285
+ if (JUNCTION_TEMPORAL_AUTHORITY_RESOURCES.has(resource)) {
6286
+ const rawTimestamp = resolveJunctionTimeseriesRecordRawTimestamp(record);
6287
+ if (!rawTimestamp || !hasAbsoluteJunctionTimeseriesTimestamp(rawTimestamp, record)) {
6288
+ return rawTimestamp;
6289
+ }
6290
+ return toIsoTimestampIfValid(rawTimestamp) ?? rawTimestamp;
6291
+ }
6292
+ if (JUNCTION_INSTANT_BODY_TIMESERIES_RESOURCES.has(resource)) {
6293
+ return normalizeJunctionCanonicalBodyTimestamp(record.timestamp) ?? null;
6294
+ }
6295
+ if (JUNCTION_INTERVAL_BODY_TIMESERIES_RESOURCES.has(resource)) {
6296
+ const start = normalizeJunctionCanonicalBodyTimestamp(record.start);
6297
+ const end = normalizeJunctionCanonicalBodyTimestamp(record.end);
6298
+ if (!start || !end || Date.parse(end) <= Date.parse(start)) {
6299
+ return null;
6300
+ }
6301
+ return start;
6302
+ }
6303
+ return resolveJunctionTimeseriesRecordTimestamp(record);
6304
+ }
5130
6305
  function resolveJunctionTimeseriesRecordRawTimestamp(record, preferIntervalStart = false) {
5131
6306
  const intervalKeys = preferIntervalStart
5132
6307
  ? ["sessionStart", "session_start", "timeStart", "time_start", "start", "startAt", "start_at", "sessionEnd", "session_end", "timeEnd", "time_end", "end", "endAt", "end_at"]
@@ -5159,6 +6334,16 @@ function isJunctionFloatingCalendarTimestamp(value) {
5159
6334
  }
5160
6335
  return /^\d{4}-\d{2}-\d{2}(?:$|[ t]\d{2}:\d{2})/iu.test(normalized);
5161
6336
  }
6337
+ function hasAbsoluteJunctionTimeseriesTimestamp(timestamp, record) {
6338
+ const explicitSemantics = normalizeString(record.timestampSemantics ?? record.timestamp_semantics);
6339
+ if (explicitSemantics === "floating") {
6340
+ return false;
6341
+ }
6342
+ if (explicitSemantics === "utc" || explicitSemantics === "offset") {
6343
+ return true;
6344
+ }
6345
+ return /(?:z|[+-]\d{2}:?\d{2})$/iu.test(timestamp.trim());
6346
+ }
5162
6347
  function buildJunctionRedirectUrl(callbackUrl, state) {
5163
6348
  const url = new URL(callbackUrl);
5164
6349
  url.searchParams.set("murph_state", state);
@@ -5318,6 +6503,7 @@ function resolveNextFullJobTimeseriesContinuation(input) {
5318
6503
  timeseriesResourceCursor: input.resource,
5319
6504
  timeseriesWindowHours: input.timeseriesWindowHours,
5320
6505
  workoutStreamCursor: null,
6506
+ workoutStreamEmptySeen: false,
5321
6507
  };
5322
6508
  }
5323
6509
  const nextResource = input.resources[input.resources.indexOf(input.resource) + 1] ?? null;
@@ -5327,6 +6513,7 @@ function resolveNextFullJobTimeseriesContinuation(input) {
5327
6513
  timeseriesResourceCursor: nextResource,
5328
6514
  timeseriesWindowHours: 24,
5329
6515
  workoutStreamCursor: null,
6516
+ workoutStreamEmptySeen: false,
5330
6517
  }
5331
6518
  : null;
5332
6519
  }
@@ -5339,12 +6526,6 @@ function readDeferredEmptyBackfillAttempts(followUp) {
5339
6526
  }
5340
6527
  return 0;
5341
6528
  }
5342
- function readBackfillTimeseriesCursor(job, ownerWindow) {
5343
- const cursor = toIsoTimestampIfValid(job.payload.timeseriesCursor);
5344
- return cursor && isTimestampInHalfOpenWindow(cursor, ownerWindow)
5345
- ? cursor
5346
- : null;
5347
- }
5348
6529
  function isTimestampInHalfOpenWindow(timestamp, window) {
5349
6530
  const timestampMs = Date.parse(timestamp);
5350
6531
  const windowStartMs = Date.parse(window.windowStart);
@@ -5382,6 +6563,96 @@ function shouldImportClosedTimeseriesForReconcile(lastSyncCompletedAt, windowEnd
5382
6563
  || !Number.isFinite(windowEndMs)
5383
6564
  || lastCompletedClosedDayMs < windowEndMs;
5384
6565
  }
6566
+ function normalizeJunctionTemporalReconcileDays(value) {
6567
+ if (!Number.isFinite(value)) {
6568
+ return DEFAULT_RECONCILE_DAYS;
6569
+ }
6570
+ return Math.max(1, Math.min(Math.floor(value), JUNCTION_MAX_TEMPORAL_RECONCILE_DAYS));
6571
+ }
6572
+ function buildLatestAuthoritativeDailyWindows(input) {
6573
+ const nowMs = Date.parse(input.now);
6574
+ const windowEndMs = Date.parse(input.windowEnd);
6575
+ if (!Number.isFinite(nowMs) || !Number.isFinite(windowEndMs)) {
6576
+ return [];
6577
+ }
6578
+ const latestAuthoritativeDayKey = latestAuthoritativeVaultDayKey(input.now, input.timeZone);
6579
+ if (!latestAuthoritativeDayKey) {
6580
+ return [];
6581
+ }
6582
+ let latestWindowBoundDayKey = toLocalDayKey(new Date(windowEndMs - 1).toISOString(), input.timeZone);
6583
+ for (let offset = 0; offset < 4; offset += 1) {
6584
+ const candidate = resolveVaultLocalDayWindow(latestWindowBoundDayKey, input.timeZone);
6585
+ if (candidate && Date.parse(candidate.windowEnd) <= windowEndMs) {
6586
+ break;
6587
+ }
6588
+ latestWindowBoundDayKey = addIsoDateDays(latestWindowBoundDayKey, -1);
6589
+ }
6590
+ const newestDayKey = latestAuthoritativeDayKey < latestWindowBoundDayKey
6591
+ ? latestAuthoritativeDayKey
6592
+ : latestWindowBoundDayKey;
6593
+ const newestWindow = resolveVaultLocalDayWindow(newestDayKey, input.timeZone);
6594
+ if (!newestWindow || Date.parse(newestWindow.windowEnd) > windowEndMs) {
6595
+ return [];
6596
+ }
6597
+ const windows = [];
6598
+ for (let offset = 0; offset < input.horizonDays; offset += 1) {
6599
+ const dayKey = addIsoDateDays(newestDayKey, -offset);
6600
+ const window = resolveVaultLocalDayWindow(dayKey, input.timeZone);
6601
+ if (!window) {
6602
+ continue;
6603
+ }
6604
+ windows.push({
6605
+ dayKey,
6606
+ ...window,
6607
+ });
6608
+ }
6609
+ return windows;
6610
+ }
6611
+ function buildJunctionTemporalAuthorityJobs(input) {
6612
+ const temporalResources = input.resources.filter((resource) => JUNCTION_TEMPORAL_AUTHORITY_RESOURCES.has(resource));
6613
+ return input.windows.flatMap((window, windowIndex) => temporalResources.map((resource) => {
6614
+ const payload = {
6615
+ resource,
6616
+ resourceCategory: "timeseries",
6617
+ temporalAuthorityTimeZone: input.timeZone,
6618
+ windowEnd: window.windowEnd,
6619
+ windowStart: window.windowStart,
6620
+ };
6621
+ return {
6622
+ // The store orders equal-priority work by availability. Give each older
6623
+ // day a stable millisecond tier so restarts drain the newest missing day
6624
+ // first without outranking webhook work or introducing another cursor.
6625
+ availableAt: addMilliseconds(input.now, windowIndex),
6626
+ dedupeKey: `${JUNCTION_TEMPORAL_AUTHORITY_DEDUPE_PREFIX}v${JUNCTION_TEMPORAL_AUTHORITY_JOB_VERSION}:${sha256Text(JSON.stringify([input.timeZone, resource, window.dayKey]))}`,
6627
+ kind: "resource",
6628
+ payload,
6629
+ priority: JUNCTION_HISTORICAL_BACKFILL_PRIORITY,
6630
+ };
6631
+ }));
6632
+ }
6633
+ function readJunctionTemporalAuthorityJob(job) {
6634
+ if (job.kind !== "resource") {
6635
+ return null;
6636
+ }
6637
+ // The retained authority timezone is the only persisted authority state;
6638
+ // the day key derives from the stored exact window so hosted wake hints
6639
+ // cannot restore a temporal child with partial authority. Legacy payloads
6640
+ // may still carry temporalAuthorityDayKey; it is ignored.
6641
+ const timeZone = normalizeString(job.payload.temporalAuthorityTimeZone);
6642
+ const resource = normalizeJunctionResourceName(job.payload.resource);
6643
+ const windowStart = normalizeString(job.payload.windowStart);
6644
+ if (!timeZone
6645
+ || !resource
6646
+ || !JUNCTION_TEMPORAL_AUTHORITY_RESOURCES.has(resource)
6647
+ || !windowStart) {
6648
+ return null;
6649
+ }
6650
+ const dayKey = toLocalDayKey(windowStart, timeZone);
6651
+ if (!dayKey || !/^\d{4}-\d{2}-\d{2}$/u.test(dayKey)) {
6652
+ return null;
6653
+ }
6654
+ return { dayKey, resource, timeZone };
6655
+ }
5385
6656
  function resolveLatestGloballyClosedProviderDayWindow(windowStart, windowEnd, asOf) {
5386
6657
  const flooredWindowStartMs = Date.parse(floorUtcDayTimestamp(windowStart));
5387
6658
  const globallyClosedEndMs = resolveGloballyClosedProviderDayEnd(windowEnd, asOf);
@@ -5414,14 +6685,76 @@ function buildClosedDailyWindows(windowStart, windowEnd) {
5414
6685
  if (chunkEndMs <= chunkStartMs) {
5415
6686
  break;
5416
6687
  }
6688
+ const dailyWindowStart = new Date(chunkStartMs).toISOString();
5417
6689
  windows.push({
5418
- windowStart: new Date(chunkStartMs).toISOString(),
6690
+ dayKey: dailyWindowStart.slice(0, 10),
6691
+ windowStart: dailyWindowStart,
5419
6692
  windowEnd: new Date(chunkEndMs).toISOString(),
5420
6693
  });
5421
6694
  chunkStartMs = chunkEndMs;
5422
6695
  }
5423
6696
  return windows;
5424
6697
  }
6698
+ function latestAuthoritativeVaultDayKey(at, timeZone) {
6699
+ const atMs = Date.parse(at);
6700
+ if (!Number.isFinite(atMs)) {
6701
+ return null;
6702
+ }
6703
+ let candidateDayKey = toLocalDayKey(at, timeZone);
6704
+ for (let offset = 0; offset < 4; offset += 1) {
6705
+ const candidate = resolveVaultLocalDayWindow(candidateDayKey, timeZone);
6706
+ if (candidate
6707
+ && Date.parse(candidate.windowEnd) + JUNCTION_TEMPORAL_AUTHORITY_LAG_MS <= atMs) {
6708
+ return candidateDayKey;
6709
+ }
6710
+ candidateDayKey = addIsoDateDays(candidateDayKey, -1);
6711
+ }
6712
+ return null;
6713
+ }
6714
+ function resolveVaultLocalDayWindow(dayKey, timeZone) {
6715
+ const startMs = findVaultLocalDayStartMs(dayKey, timeZone);
6716
+ const endMs = findVaultLocalDayStartMs(addIsoDateDays(dayKey, 1), timeZone);
6717
+ return startMs === null || endMs === null || startMs >= endMs
6718
+ ? null
6719
+ : {
6720
+ windowStart: new Date(startMs).toISOString(),
6721
+ windowEnd: new Date(endMs).toISOString(),
6722
+ };
6723
+ }
6724
+ function findVaultLocalDayStartMs(dayKey, timeZone) {
6725
+ const anchorMs = Date.parse(`${dayKey}T00:00:00.000Z`);
6726
+ if (!Number.isFinite(anchorMs)) {
6727
+ return null;
6728
+ }
6729
+ const searchStartMs = anchorMs - 2 * TIMESERIES_CHUNK_MS;
6730
+ const searchEndMs = anchorMs + 2 * TIMESERIES_CHUNK_MS;
6731
+ let matchingMs = null;
6732
+ for (let candidateMs = searchStartMs; candidateMs <= searchEndMs; candidateMs += 60 * 60_000) {
6733
+ if (toLocalDayKey(new Date(candidateMs).toISOString(), timeZone) === dayKey) {
6734
+ matchingMs = candidateMs;
6735
+ break;
6736
+ }
6737
+ }
6738
+ if (matchingMs === null) {
6739
+ return null;
6740
+ }
6741
+ let lowerMs = matchingMs - 60 * 60_000;
6742
+ let upperMs = matchingMs;
6743
+ while (upperMs - lowerMs > 1) {
6744
+ const middleMs = Math.floor((lowerMs + upperMs) / 2);
6745
+ if (toLocalDayKey(new Date(middleMs).toISOString(), timeZone) < dayKey) {
6746
+ lowerMs = middleMs;
6747
+ }
6748
+ else {
6749
+ upperMs = middleMs;
6750
+ }
6751
+ }
6752
+ return upperMs;
6753
+ }
6754
+ function addIsoDateDays(dayKey, days) {
6755
+ const value = Date.parse(`${dayKey}T00:00:00.000Z`);
6756
+ return new Date(value + days * TIMESERIES_CHUNK_MS).toISOString().slice(0, 10);
6757
+ }
5425
6758
  function resolveGloballyClosedProviderDayEnd(windowEnd, asOf) {
5426
6759
  const requestedClosedEndMs = Date.parse(floorUtcDayTimestamp(windowEnd));
5427
6760
  const asOfMs = Date.parse(asOf);
@@ -5480,13 +6813,11 @@ function hasJunctionHistoricalBackfillSummaryRecords(evidence) {
5480
6813
  return evidence.some(({ resource }) => isJunctionHistoricalBackfillCompletionSummaryResource(resource));
5481
6814
  }
5482
6815
  function resolveJunctionHistoricalPullReadiness(input) {
5483
- const sourceProviderSlug = canonicalizeJunctionHistoricalProviderSlug(input.sourceProviderSlug)
5484
- ?? normalizeProviderSlug(input.sourceProviderSlug);
6816
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.sourceProviderSlug);
5485
6817
  if (!sourceProviderSlug || !input.snapshot?.matchedUser) {
5486
6818
  return "unavailable";
5487
6819
  }
5488
- const source = input.snapshot.sources.find((entry) => (canonicalizeJunctionHistoricalProviderSlug(entry.sourceProviderSlug)
5489
- ?? normalizeProviderSlug(entry.sourceProviderSlug)) === sourceProviderSlug);
6820
+ const source = input.snapshot.sources.find((entry) => canonicalizeJunctionProviderSlug(entry.sourceProviderSlug) === sourceProviderSlug);
5490
6821
  if (!source) {
5491
6822
  return "unavailable";
5492
6823
  }
@@ -5586,7 +6917,7 @@ function evaluateJunctionHistoricalBackfillCoverage(summaryEvidence, sourceProvi
5586
6917
  continue;
5587
6918
  }
5588
6919
  const coveredBySummary = summaryEvidence.some((entry) => entry.resource === resource
5589
- && canonicalizeJunctionHistoricalProviderSlug(entry.sourceProviderSlug) === providerSlug);
6920
+ && canonicalizeJunctionProviderSlug(entry.sourceProviderSlug) === providerSlug);
5590
6921
  const coveredByPush = hasJunctionHistoricalBackfillEvidence(historicalPushEvidence, providerSlug, resource, window.windowStart, window.windowEnd);
5591
6922
  if (!coveredBySummary && !coveredByPush) {
5592
6923
  hasPendingResource = true;
@@ -5610,16 +6941,8 @@ function evaluateJunctionHistoricalBackfillCoverage(summaryEvidence, sourceProvi
5610
6941
  .sort((left, right) => left.localeCompare(right)),
5611
6942
  };
5612
6943
  }
5613
- function canonicalizeJunctionHistoricalProviderSlug(value) {
5614
- const providerSlug = normalizeProviderSlug(value);
5615
- if (!providerSlug) {
5616
- return null;
5617
- }
5618
- return resolveJunctionDeviceConnectRouteByProviderSlug(providerSlug)?.route.sourceProviderSlug
5619
- ?? null;
5620
- }
5621
6944
  function resolveJunctionHistoricalCoverageProviderSlug(value, providerFilter) {
5622
- const providerSlug = canonicalizeJunctionHistoricalProviderSlug(value);
6945
+ const providerSlug = canonicalizeJunctionProviderSlug(value);
5623
6946
  if (!providerSlug) {
5624
6947
  return null;
5625
6948
  }
@@ -5639,7 +6962,7 @@ function readJunctionDirectHistoricalEvidenceWindow(input, connectWindow, eviden
5639
6962
  if (!providerSlug
5640
6963
  || !isJunctionHistoricalBackfillRequiredSummaryResource(input.resource)
5641
6964
  || !evidence.some((entry) => entry.resource === input.resource
5642
- && canonicalizeJunctionHistoricalProviderSlug(entry.sourceProviderSlug) === providerSlug)) {
6965
+ && canonicalizeJunctionProviderSlug(entry.sourceProviderSlug) === providerSlug)) {
5643
6966
  return null;
5644
6967
  }
5645
6968
  return connectWindow;
@@ -5666,11 +6989,23 @@ function isJunctionResourceAvailableInSummary(availabilitySummary, resource) {
5666
6989
  return Object.entries(availabilitySummary).some(([candidate, availability]) => normalizeJunctionResourceName(candidate) === normalizedResource
5667
6990
  && isJunctionResourceAdvertisedAvailable(availability));
5668
6991
  }
6992
+ async function resolveJunctionWorkoutStreamEligibleSources(context, providers) {
6993
+ if (!context.listConnectionSources) {
6994
+ return new Set();
6995
+ }
6996
+ const sources = await context.listConnectionSources();
6997
+ return new Set(projectJunctionSourcesByProviderSlug(context.account.id, providers).flatMap((source) => isJunctionSourceAdmittedForImport(sources, source.sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only")
6998
+ && source.status === "connected"
6999
+ && isJunctionResourceAvailableInSummary(source.resourceAvailabilitySummary, "workout_stream")
7000
+ ? [source.sourceProviderSlug]
7001
+ : []));
7002
+ }
5669
7003
  function isJunctionSourceResourceCurrentlyAvailable(input) {
5670
- if (!input.sourceProviderSlug) {
7004
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.sourceProviderSlug);
7005
+ if (!sourceProviderSlug) {
5671
7006
  return false;
5672
7007
  }
5673
- return projectJunctionSourcesByProviderSlug(input.connectionId, input.providers).some((source) => source.sourceProviderSlug === input.sourceProviderSlug
7008
+ return projectJunctionSourcesByProviderSlug(input.connectionId, input.providers).some((source) => source.sourceProviderSlug === sourceProviderSlug
5674
7009
  && source.status === "connected"
5675
7010
  && isJunctionResourceAvailableInSummary(source.resourceAvailabilitySummary, input.resource));
5676
7011
  }
@@ -5760,8 +7095,16 @@ function buildNonConnectHistoricalBackfillFollowUp(input) {
5760
7095
  return {};
5761
7096
  }
5762
7097
  const retryAt = addMilliseconds(input.now, retryDelayMs);
7098
+ const sourceProviderSlug = normalizeProviderSlug(input.job.payload.sourceProviderSlug);
5763
7099
  const retryJob = buildExactWindowJob({
5764
7100
  kind: "backfill",
7101
+ payload: stripUndefined({
7102
+ ...(sourceProviderSlug ? { sourceProviderSlug } : {}),
7103
+ ...(input.historicalProviderRecordsSeen
7104
+ ? { historicalProviderRecordsSeen: true }
7105
+ : {}),
7106
+ ...(input.historicalRecordsSeen ? { historicalRecordsSeen: true } : {}),
7107
+ }),
5765
7108
  priority: Math.max(input.job.priority, JUNCTION_HISTORICAL_BACKFILL_RETRY_PRIORITY),
5766
7109
  windowStart: input.windowStart,
5767
7110
  windowEnd: input.windowEnd,
@@ -5952,7 +7295,7 @@ function resolveJunctionHistoricalUnresolvedProviderRecords(job, importResult) {
5952
7295
  }
5953
7296
  return {
5954
7297
  identities: [...identities].sort(),
5955
- withoutStableIdentity: carried.withoutStableIdentity
7298
+ withoutStableIdentity: (!importResult.fetchComplete && carried.withoutStableIdentity)
5956
7299
  || importResult.unresolvedProviderRecordsWithoutStableIdentity,
5957
7300
  };
5958
7301
  }
@@ -5972,6 +7315,32 @@ function buildConnectHistoricalBackfillWindow(account, summaryBackfillDays) {
5972
7315
  windowEnd: floorUtcDayTimestamp(account.connectedAt),
5973
7316
  };
5974
7317
  }
7318
+ function normalizeJunctionHistoricalProofAuthorization(input) {
7319
+ const firstSeenAt = normalizeString(input?.firstSeenAt);
7320
+ const sourceProviderSlug = normalizeProviderSlug(input?.sourceProviderSlug);
7321
+ if (!firstSeenAt
7322
+ || !Number.isFinite(Date.parse(firstSeenAt))
7323
+ || !sourceProviderSlug) {
7324
+ return null;
7325
+ }
7326
+ return { firstSeenAt, sourceProviderSlug };
7327
+ }
7328
+ function readJunctionHistoricalProofAuthorization(job) {
7329
+ if (job.kind !== "backfill") {
7330
+ return null;
7331
+ }
7332
+ return normalizeJunctionHistoricalProofAuthorization({
7333
+ firstSeenAt: normalizeString(job.payload.historicalProofFirstSeenAt) ?? "",
7334
+ sourceProviderSlug: normalizeProviderSlug(job.payload.historicalProofSourceProviderSlug) ?? "",
7335
+ });
7336
+ }
7337
+ async function isJunctionHistoricalProofAuthorizationCurrent(context, authorization) {
7338
+ const sources = context.listConnectionSources
7339
+ ? await context.listConnectionSources()
7340
+ : context.account.sources ?? [];
7341
+ return sources.some((source) => source.firstSeenAt === authorization.firstSeenAt
7342
+ && areJunctionProviderSlugsDataEquivalent(source.sourceProviderSlug, authorization.sourceProviderSlug));
7343
+ }
5975
7344
  function buildWindowJob(input) {
5976
7345
  const windowEnd = floorUtcDayTimestamp(input.now);
5977
7346
  const windowStart = floorUtcDayTimestamp(input.windowStart);
@@ -5986,13 +7355,26 @@ function buildWindowJob(input) {
5986
7355
  }
5987
7356
  function buildExactWindowJob(input) {
5988
7357
  const windowPayload = input.payload;
5989
- const sourceProviderSlug = normalizeString(windowPayload?.sourceProviderSlug);
7358
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(windowPayload?.sourceProviderSlug);
7359
+ const historicalProofAuthorization = normalizeJunctionHistoricalProofAuthorization(windowPayload?.historicalProofFirstSeenAt
7360
+ && windowPayload.historicalProofSourceProviderSlug
7361
+ ? {
7362
+ firstSeenAt: windowPayload.historicalProofFirstSeenAt,
7363
+ sourceProviderSlug: windowPayload.historicalProofSourceProviderSlug,
7364
+ }
7365
+ : null);
5990
7366
  const dedupeIdentity = [
5991
7367
  "junction",
5992
7368
  input.kind,
5993
7369
  input.windowStart,
5994
7370
  input.windowEnd,
5995
7371
  ...(sourceProviderSlug ? [sourceProviderSlug] : []),
7372
+ ...(historicalProofAuthorization
7373
+ ? [
7374
+ historicalProofAuthorization.sourceProviderSlug,
7375
+ historicalProofAuthorization.firstSeenAt,
7376
+ ]
7377
+ : []),
5996
7378
  ];
5997
7379
  return {
5998
7380
  kind: input.kind,
@@ -6006,6 +7388,50 @@ function buildExactWindowJob(input) {
6006
7388
  dedupeKey: sha256Text(JSON.stringify(dedupeIdentity)),
6007
7389
  };
6008
7390
  }
7391
+ function withJunctionWorkoutStreamEmptyReplay(input) {
7392
+ if (!input.replayWindow
7393
+ || (input.job.kind === "resource"
7394
+ && input.job.payload.workoutStreamEmptyReplay === true)) {
7395
+ return input.result;
7396
+ }
7397
+ const replayJob = buildJunctionWorkoutStreamEmptyReplayJob({
7398
+ availableAt: addMilliseconds(input.now, JUNCTION_WORKOUT_STREAM_EMPTY_REPLAY_DELAY_MS),
7399
+ sourceProviderSlug: input.sourceProviderSlug,
7400
+ windowEnd: input.replayWindow.windowEnd,
7401
+ windowStart: input.replayWindow.windowStart,
7402
+ });
7403
+ return {
7404
+ ...input.result,
7405
+ scheduledJobs: [
7406
+ ...(input.result.scheduledJobs ?? []),
7407
+ replayJob,
7408
+ ],
7409
+ };
7410
+ }
7411
+ function buildJunctionWorkoutStreamEmptyReplayJob(input) {
7412
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.sourceProviderSlug);
7413
+ const payload = {
7414
+ resource: "workout_stream",
7415
+ resourceCategory: "timeseries",
7416
+ ...(sourceProviderSlug ? { sourceProviderSlug } : {}),
7417
+ windowEnd: input.windowEnd,
7418
+ windowStart: input.windowStart,
7419
+ workoutStreamEmptyReplay: true,
7420
+ };
7421
+ return {
7422
+ availableAt: input.availableAt,
7423
+ dedupeKey: sha256Text(JSON.stringify([
7424
+ "junction",
7425
+ "workout-stream-empty-replay",
7426
+ input.windowStart,
7427
+ input.windowEnd,
7428
+ sourceProviderSlug,
7429
+ ])),
7430
+ kind: "resource",
7431
+ payload,
7432
+ priority: JUNCTION_HISTORICAL_BACKFILL_RETRY_PRIORITY,
7433
+ };
7434
+ }
6009
7435
  function buildJunctionSparseCalendarRefreshJobs(input) {
6010
7436
  const targets = [...new Map(input.targets.map((target) => [
6011
7437
  JSON.stringify([
@@ -6116,7 +7542,7 @@ function filterJunctionSparseCalendarRecordsToSource(records, source) {
6116
7542
  if (!recordSourceProviderSlug) {
6117
7543
  throw incompleteJunctionSparseCalendarCollectionError();
6118
7544
  }
6119
- if (!areJunctionProviderSlugsRouteEquivalent(recordSourceProviderSlug, sourceProviderSlug)) {
7545
+ if (!areJunctionProviderSlugsDataEquivalent(recordSourceProviderSlug, sourceProviderSlug)) {
6120
7546
  return [];
6121
7547
  }
6122
7548
  const recordSourceType = normalizeString(origin.sourceType);
@@ -6183,28 +7609,59 @@ function buildJunctionExtendedTimeseriesBackfillDedupeKey(payload) {
6183
7609
  || !policy) {
6184
7610
  return null;
6185
7611
  }
6186
- if (!["blood_pressure", "weight"].includes(resource)) {
6187
- const coverageVersion = resource === "note"
6188
- ? readJunctionNoteHistoryBackfillVersion(payload)
6189
- : policy.version;
7612
+ const sourceLifecycleEpoch = readSafeInteger(payload.sourceLifecycleEpoch);
7613
+ if (sourceLifecycleEpoch === null
7614
+ || sourceLifecycleEpoch < 1) {
7615
+ return null;
7616
+ }
7617
+ const coverageVersion = readJunctionHistoricalBackfillVersion(payload);
7618
+ if (policy.completion !== "exact_records") {
6190
7619
  return sha256Text(JSON.stringify([
6191
7620
  "junction",
6192
7621
  "extended-timeseries-backfill",
6193
- normalizeProviderSlug(payload.sourceProviderSlug),
7622
+ canonicalizeJunctionProviderSlug(payload.sourceProviderSlug),
6194
7623
  resource,
7624
+ sourceLifecycleEpoch,
6195
7625
  coverageVersion,
6196
7626
  ]));
6197
7627
  }
6198
7628
  return sha256Text(JSON.stringify([
6199
7629
  "junction",
6200
7630
  "extended-timeseries-backfill",
6201
- normalizeProviderSlug(payload.sourceProviderSlug),
7631
+ canonicalizeJunctionProviderSlug(payload.sourceProviderSlug),
6202
7632
  resource,
7633
+ sourceLifecycleEpoch,
7634
+ coverageVersion,
6203
7635
  historicalWindowStart,
6204
7636
  windowEnd,
6205
7637
  ]));
6206
7638
  }
7639
+ function buildExtendedTimeseriesBackfillFollowUp(job, input) {
7640
+ const payload = stripUndefined({
7641
+ ...job.payload,
7642
+ ...input.payload,
7643
+ ...(input.payload?.historicalProviderRecordsSeen === false
7644
+ ? { historicalProviderRecordsSeen: undefined }
7645
+ : {}),
7646
+ ...(input.payload?.historicalRecordsSeen === false
7647
+ ? { historicalRecordsSeen: undefined }
7648
+ : {}),
7649
+ sourceProviderSlug: canonicalizeJunctionProviderSlug(input.payload?.sourceProviderSlug ?? job.payload.sourceProviderSlug) ?? undefined,
7650
+ windowEnd: input.windowEnd,
7651
+ windowStart: input.windowStart,
7652
+ });
7653
+ return {
7654
+ kind: "resource",
7655
+ payload,
7656
+ availableAt: input.availableAt,
7657
+ priority: job.priority,
7658
+ dedupeKey: job.dedupeKey
7659
+ ?? buildJunctionExtendedTimeseriesBackfillDedupeKey(payload)
7660
+ ?? undefined,
7661
+ };
7662
+ }
6207
7663
  function buildExtendedTimeseriesBackfillJob(input) {
7664
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.sourceProviderSlug);
6208
7665
  const payload = {
6209
7666
  ...(input.emptyBackfillAttempts && input.emptyBackfillAttempts > 0
6210
7667
  ? { emptyBackfillAttempts: input.emptyBackfillAttempts }
@@ -6233,8 +7690,11 @@ function buildExtendedTimeseriesBackfillJob(input) {
6233
7690
  historicalWindowStart: input.historicalWindowStart,
6234
7691
  resource: input.resource,
6235
7692
  resourceCategory: "timeseries",
6236
- ...(input.sourceProviderSlug
6237
- ? { sourceProviderSlug: input.sourceProviderSlug }
7693
+ ...(input.sourceLifecycleEpoch === undefined
7694
+ ? {}
7695
+ : { sourceLifecycleEpoch: input.sourceLifecycleEpoch }),
7696
+ ...(sourceProviderSlug
7697
+ ? { sourceProviderSlug }
6238
7698
  : {}),
6239
7699
  windowEnd: input.windowEnd,
6240
7700
  windowStart: input.windowStart,
@@ -6252,13 +7712,14 @@ function buildExtendedTimeseriesBackfillJob(input) {
6252
7712
  dedupeKey,
6253
7713
  };
6254
7714
  }
6255
- function readJunctionNoteHistoryBackfillVersion(payload) {
7715
+ function readJunctionHistoricalBackfillVersion(payload) {
6256
7716
  const version = payload.historicalBackfillVersion;
6257
7717
  return typeof version === "number" && Number.isSafeInteger(version) && version >= 1
6258
7718
  ? version
6259
7719
  : 1;
6260
7720
  }
6261
7721
  function buildJunctionWebhookJobs(input) {
7722
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(input.sourceProviderSlug);
6262
7723
  if (isJunctionProviderConnectionEvent(input.eventType)) {
6263
7724
  const backfillWindowStart = subtractDays(input.window.windowEnd, input.summaryBackfillDays);
6264
7725
  return [
@@ -6294,7 +7755,7 @@ function buildJunctionWebhookJobs(input) {
6294
7755
  occurredAt: input.occurredAt,
6295
7756
  resource: input.resource?.name ?? "",
6296
7757
  resourceCategory: input.resource?.category ?? "",
6297
- sourceProviderSlug: input.sourceProviderSlug ?? "",
7758
+ sourceProviderSlug: sourceProviderSlug ?? "",
6298
7759
  ...(webhookDataJson ? { webhookDataJson } : {}),
6299
7760
  windowStart: input.window.windowStart,
6300
7761
  windowEnd: input.window.windowEnd,
@@ -6304,7 +7765,7 @@ function buildJunctionWebhookJobs(input) {
6304
7765
  ? sha256Text(JSON.stringify([
6305
7766
  "junction-webhook",
6306
7767
  "resource-data",
6307
- input.sourceProviderSlug,
7768
+ sourceProviderSlug,
6308
7769
  input.resource?.category,
6309
7770
  input.resource?.name,
6310
7771
  input.objectId ?? "",
@@ -6315,7 +7776,7 @@ function buildJunctionWebhookJobs(input) {
6315
7776
  : sha256Text(JSON.stringify([
6316
7777
  "junction-webhook",
6317
7778
  "resource",
6318
- input.sourceProviderSlug,
7779
+ sourceProviderSlug,
6319
7780
  input.resource?.category,
6320
7781
  input.resource?.name,
6321
7782
  input.window.windowStart,
@@ -6437,10 +7898,32 @@ function buildJunctionWebhookDataJobJsons(input) {
6437
7898
  }
6438
7899
  const withSource = stripUndefined({
6439
7900
  ...record,
6440
- sourceProviderSlug: normalizeProviderSlug(record.sourceProviderSlug) ?? input.sourceProviderSlug ?? undefined,
7901
+ sourceProviderSlug: input.sourceProviderSlug ?? undefined,
6441
7902
  });
6442
7903
  return [serializeJunctionWebhookDataJobRecord(withSource)];
6443
7904
  }
7905
+ function resolveJunctionWebhookDataSourceProviderSlug(input) {
7906
+ if (!input.data
7907
+ || !isJunctionDataEvent(input.eventType)
7908
+ || input.historicalPullCompleted) {
7909
+ return null;
7910
+ }
7911
+ const classification = classifyDeviceSyncJunctionInlineSourceProviderSlug(input.data);
7912
+ if (classification.status === "ambiguous") {
7913
+ throw junctionWebhookSourceNotReadyError();
7914
+ }
7915
+ return classification.status === "resolved"
7916
+ ? classification.sourceProviderSlug
7917
+ : input.envelopeSourceProviderSlug;
7918
+ }
7919
+ function junctionWebhookSourceNotReadyError() {
7920
+ return deviceSyncError({
7921
+ code: "WEBHOOK_SOURCE_NOT_READY",
7922
+ message: "Junction webhook data source provenance was ambiguous. Retry later.",
7923
+ retryable: true,
7924
+ httpStatus: 503,
7925
+ });
7926
+ }
6444
7927
  function serializeJunctionWebhookDataJobRecord(record) {
6445
7928
  return JSON.stringify(record);
6446
7929
  }
@@ -6619,7 +8102,7 @@ function readJunctionWebhookResourceFromEventType(eventType) {
6619
8102
  return null;
6620
8103
  }
6621
8104
  function extractJunctionWebhookSourceProviderSlug(data) {
6622
- return (normalizeProviderSlug(resolveJunctionOrigin(data ?? undefined).sourceProviderSlug)
8105
+ return canonicalizeJunctionProviderSlug(resolveJunctionOrigin(data ?? undefined).sourceProviderSlug
6623
8106
  ?? extractJunctionWebhookSourceProviderSlugFallback(data));
6624
8107
  }
6625
8108
  function extractJunctionWebhookSourceProviderSlugFallback(data) {
@@ -6675,7 +8158,10 @@ function extractJunctionWebhookRootTimestamp(data) {
6675
8158
  }
6676
8159
  return null;
6677
8160
  }
6678
- function extractJunctionWebhookOccurredAt(data) {
8161
+ function extractJunctionWebhookOccurredAt(data, resource) {
8162
+ if (isJunctionBodyTimeseriesResource(resource?.name)) {
8163
+ return readJunctionWebhookDataTimestampRange(data, resource?.name)?.firstTimestamp ?? null;
8164
+ }
6679
8165
  const rootTimestamp = extractJunctionWebhookRootTimestamp(data);
6680
8166
  if (rootTimestamp) {
6681
8167
  return rootTimestamp;
@@ -6684,16 +8170,21 @@ function extractJunctionWebhookOccurredAt(data) {
6684
8170
  if (nestedTimestamp) {
6685
8171
  return nestedTimestamp;
6686
8172
  }
6687
- return readJunctionWebhookDataTimestampRange(data)?.firstTimestamp ?? null;
8173
+ return readJunctionWebhookDataTimestampRange(data, resource?.name)?.firstTimestamp ?? null;
6688
8174
  }
6689
8175
  function buildJunctionWebhookWindow(data, occurredAt, now, resource) {
8176
+ const bodyTimeseriesResource = isJunctionBodyTimeseriesResource(resource?.name);
6690
8177
  const explicitStart = toJunctionWebhookWindowBoundaryTimestampIfValid(data?.window_start, "start")
6691
8178
  ?? toJunctionWebhookWindowBoundaryTimestampIfValid(data?.start_date, "start")
6692
- ?? toJunctionWebhookWindowBoundaryTimestampIfValid(data?.start, "start")
8179
+ ?? (bodyTimeseriesResource
8180
+ ? null
8181
+ : toJunctionWebhookWindowBoundaryTimestampIfValid(data?.start, "start"))
6693
8182
  ?? toJunctionWebhookWindowBoundaryTimestampIfValid(data?.from, "start");
6694
8183
  const explicitEnd = toJunctionWebhookWindowBoundaryTimestampIfValid(data?.window_end, "end")
6695
8184
  ?? toJunctionWebhookWindowBoundaryTimestampIfValid(data?.end_date, "end")
6696
- ?? toJunctionWebhookWindowBoundaryTimestampIfValid(data?.end, "end")
8185
+ ?? (bodyTimeseriesResource
8186
+ ? null
8187
+ : toJunctionWebhookWindowBoundaryTimestampIfValid(data?.end, "end"))
6697
8188
  ?? toJunctionWebhookWindowBoundaryTimestampIfValid(data?.to, "end");
6698
8189
  if (explicitStart && explicitEnd) {
6699
8190
  return {
@@ -6701,7 +8192,7 @@ function buildJunctionWebhookWindow(data, occurredAt, now, resource) {
6701
8192
  windowEnd: minIsoTimestamp(explicitEnd, now),
6702
8193
  };
6703
8194
  }
6704
- const dataTimestampRange = readJunctionWebhookDataTimestampRange(data);
8195
+ const dataTimestampRange = readJunctionWebhookDataTimestampRange(data, resource?.name);
6705
8196
  if (dataTimestampRange) {
6706
8197
  const windowStart = floorUtcDayTimestamp(dataTimestampRange.firstTimestamp);
6707
8198
  const windowEnd = minIsoTimestamp(addMilliseconds(floorUtcDayTimestamp(dataTimestampRange.lastTimestamp), TIMESERIES_CHUNK_MS), now);
@@ -6743,11 +8234,13 @@ function toJunctionWebhookWindowBoundaryTimestampIfValid(value, boundary) {
6743
8234
  }
6744
8235
  return toIsoTimestampIfValid(normalized);
6745
8236
  }
6746
- function readJunctionWebhookDataTimestampRange(data) {
8237
+ function readJunctionWebhookDataTimestampRange(data, resource) {
6747
8238
  const record = data ? readPlainObject(data) : null;
6748
8239
  const timestamps = record
6749
- ? expandJunctionWebhookTimeseriesDataRecords(record).slice(1).flatMap((entry) => {
6750
- const timestamp = toIsoTimestampIfValid(resolveJunctionTimeseriesRecordTimestamp(entry));
8240
+ ? expandJunctionWebhookTimeseriesDataRecords(record)
8241
+ .slice(isJunctionBodyTimeseriesResource(resource) ? 0 : 1)
8242
+ .flatMap((entry) => {
8243
+ const timestamp = resolveJunctionTimeseriesRecordTimestampForResource(resource ?? "", entry);
6751
8244
  return timestamp ? [timestamp] : [];
6752
8245
  })
6753
8246
  : [];
@@ -7064,7 +8557,9 @@ async function projectJunctionSources(context, providers, options = {}) {
7064
8557
  && historicalState.status !== "complete"));
7065
8558
  const preserveHistoricalReconnectProviderSlugs = options.preserveHistoricalReconnectProviderSlugs === undefined
7066
8559
  ? null
7067
- : new Set(options.preserveHistoricalReconnectProviderSlugs);
8560
+ : new Set(options.preserveHistoricalReconnectProviderSlugs
8561
+ .map(canonicalizeJunctionProviderSlug)
8562
+ .filter((providerSlug) => providerSlug !== null));
7068
8563
  const projectedSources = projectJunctionSourcesByProviderSlug(context.account.id, providers);
7069
8564
  if (projectedSources.length > JUNCTION_MAX_USER_PROVIDERS) {
7070
8565
  throw deviceSyncError({
@@ -7099,6 +8594,10 @@ async function projectJunctionSources(context, providers, options = {}) {
7099
8594
  && existing.lastErrorCode === DEVICE_SYNC_HISTORICAL_DATA_RECONNECT_REQUIRED_ERROR_CODE
7100
8595
  : requiresHistoricalResetDeviceSyncSource(existing));
7101
8596
  const historicalReconnectError = keepHistoricalReconnect ? existing : null;
8597
+ const historicalBackfillCompletedAt = source.sourceProviderSlug === options.historicalBackfillCompletedProviderSlug
8598
+ ? context.now
8599
+ : existing?.resourceAvailabilitySummary?.[DEVICE_SYNC_SOURCE_HISTORICAL_BACKFILL_COMPLETED_AT_KEY];
8600
+ const existingSourceMetadata = Object.fromEntries(Object.entries(existing?.resourceAvailabilitySummary ?? {}).filter(([key]) => isDeviceSyncSourceResourceAvailabilityMetadataKey(key)));
7102
8601
  const persistedSource = await context.upsertConnectionSource({
7103
8602
  sourceInstanceKey: accountSourceIdentity?.sourceInstanceKey ?? source.sourceInstanceKey,
7104
8603
  sourceProviderSlug: accountSourceIdentity?.sourceProviderSlug ?? source.sourceProviderSlug,
@@ -7107,7 +8606,15 @@ async function projectJunctionSources(context, providers, options = {}) {
7107
8606
  status: keepHistoricalReconnect
7108
8607
  ? "error"
7109
8608
  : source.status,
7110
- resourceAvailabilitySummary: source.resourceAvailabilitySummary,
8609
+ resourceAvailabilitySummary: {
8610
+ ...source.resourceAvailabilitySummary,
8611
+ ...existingSourceMetadata,
8612
+ ...(typeof historicalBackfillCompletedAt === "string"
8613
+ ? {
8614
+ [DEVICE_SYNC_SOURCE_HISTORICAL_BACKFILL_COMPLETED_AT_KEY]: historicalBackfillCompletedAt,
8615
+ }
8616
+ : {}),
8617
+ },
7111
8618
  // Only assert error fields when this projection saw an errored entry;
7112
8619
  // omitting the keys lets the store preserve existing detail while the
7113
8620
  // status stays "error" and auto-clear it once the status recovers.
@@ -7130,11 +8637,138 @@ async function projectJunctionSources(context, providers, options = {}) {
7130
8637
  }
7131
8638
  }
7132
8639
  }
8640
+ function buildJunctionGoogleHealthCanonicalCoverageFence(sources) {
8641
+ const legacySources = sources.filter((source) => normalizeProviderSlug(source.sourceProviderSlug) === JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG);
8642
+ if (legacySources.length === 0
8643
+ || legacySources.some((source) => !isGoogleHealthFitbitMigrationLegacyTerminal(source))) {
8644
+ return undefined;
8645
+ }
8646
+ const coverageBoundaryByResource = {};
8647
+ for (const resource of [
8648
+ ...JUNCTION_ALLOWED_SUMMARY_RESOURCES,
8649
+ ...JUNCTION_ALLOWED_TIMESERIES_RESOURCES,
8650
+ ]) {
8651
+ const coverageKey = buildDeviceSyncSourceCanonicalCoverageBoundaryKey(resource);
8652
+ if (!coverageKey) {
8653
+ continue;
8654
+ }
8655
+ const summariesWithCoverage = legacySources.flatMap((source) => {
8656
+ const summary = source.resourceAvailabilitySummary;
8657
+ return summary && coverageKey in summary ? [summary] : [];
8658
+ });
8659
+ if (summariesWithCoverage.length === 0) {
8660
+ continue;
8661
+ }
8662
+ const coverageValues = summariesWithCoverage.map((summary) => readDeviceSyncSourceCanonicalCoverageBoundary(summary, resource));
8663
+ if (coverageValues.some((coverage) => coverage === null)) {
8664
+ coverageBoundaryByResource[resource] = null;
8665
+ continue;
8666
+ }
8667
+ const validCoverage = coverageValues.filter((coverage) => coverage !== null);
8668
+ coverageBoundaryByResource[resource] = validCoverage.reduce((latest, coverage) => maxJunctionCanonicalCoverageBoundary(resource, latest, coverage));
8669
+ }
8670
+ return Object.keys(coverageBoundaryByResource).length > 0
8671
+ ? {
8672
+ coverageBoundaryByResource,
8673
+ sourceProviderSlug: JUNCTION_GOOGLE_HEALTH_PROVIDER_SLUG,
8674
+ }
8675
+ : undefined;
8676
+ }
8677
+ function readJunctionSourceLifecycleEpoch(source) {
8678
+ const lifecycleEpoch = source.lifecycleEpoch;
8679
+ return typeof lifecycleEpoch === "number" && Number.isSafeInteger(lifecycleEpoch) && lifecycleEpoch >= 1
8680
+ ? lifecycleEpoch
8681
+ : 1;
8682
+ }
8683
+ function captureJunctionSourceLifecycleFence(sources) {
8684
+ return new Map([...buildJunctionCurrentLifecycleSourceMap(sources)]
8685
+ .map(([sourceProviderSlug, sources]) => [
8686
+ sourceProviderSlug,
8687
+ readJunctionSourceLifecycleEpoch(sources[0]),
8688
+ ]));
8689
+ }
8690
+ function isJunctionSourceLifecycleFenceCurrent(expected, currentSources) {
8691
+ const current = captureJunctionSourceLifecycleFence(currentSources);
8692
+ return current.size === expected.size
8693
+ && [...expected].every(([sourceProviderSlug, lifecycleEpoch]) => current.get(sourceProviderSlug) === lifecycleEpoch);
8694
+ }
8695
+ function junctionTimeseriesSourceLifecycleSuperseded() {
8696
+ return deviceSyncError({
8697
+ code: "JUNCTION_TIMESERIES_SOURCE_LIFECYCLE_SUPERSEDED",
8698
+ message: "Junction source authorization changed during collection. Retry shortly.",
8699
+ retryable: true,
8700
+ httpStatus: 503,
8701
+ });
8702
+ }
8703
+ function buildJunctionCurrentLifecycleSourceMap(sources) {
8704
+ const currentByProviderSlug = new Map();
8705
+ for (const source of sources) {
8706
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(source.sourceProviderSlug);
8707
+ if (!sourceProviderSlug) {
8708
+ continue;
8709
+ }
8710
+ const lifecycleEpoch = readJunctionSourceLifecycleEpoch(source);
8711
+ const current = currentByProviderSlug.get(sourceProviderSlug);
8712
+ const currentLifecycleEpoch = current?.[0]
8713
+ ? readJunctionSourceLifecycleEpoch(current[0])
8714
+ : 0;
8715
+ if (!current || lifecycleEpoch > currentLifecycleEpoch) {
8716
+ currentByProviderSlug.set(sourceProviderSlug, [source]);
8717
+ }
8718
+ else if (lifecycleEpoch === currentLifecycleEpoch) {
8719
+ current.push(source);
8720
+ }
8721
+ }
8722
+ return currentByProviderSlug;
8723
+ }
8724
+ function selectCurrentJunctionLifecycleSources(sources, sourceProviderSlug) {
8725
+ const canonicalSourceProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
8726
+ if (!canonicalSourceProviderSlug) {
8727
+ return [];
8728
+ }
8729
+ return buildJunctionCurrentLifecycleSourceMap(sources).get(canonicalSourceProviderSlug) ?? [];
8730
+ }
8731
+ function isJunctionLifecycleSourceGroupAdmitted(sources, sourceStatusRequirement) {
8732
+ if (sources.length === 0 || sources.some(isDeviceSyncSourceDisconnectFenced)) {
8733
+ return false;
8734
+ }
8735
+ const status = sources
8736
+ .map((source) => source.status)
8737
+ .reduce(mergeJunctionSourceStatus);
8738
+ return sourceStatusRequirement === "connected"
8739
+ ? status === "connected"
8740
+ : status !== "disconnected";
8741
+ }
7133
8742
  function isJunctionSourceAdmittedForImport(sources, sourceProviderSlug, allowUnlistedSources = true, sourceStatusRequirement = "not_disconnected") {
7134
- const normalizedSourceProviderSlug = normalizeProviderSlug(sourceProviderSlug);
8743
+ const normalizedSourceProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
7135
8744
  if (!normalizedSourceProviderSlug) {
7136
8745
  return true;
7137
8746
  }
8747
+ // Junction does not deduplicate equivalent Fitbit and Google Health records.
8748
+ // During migration, keep legacy Fitbit as the sole canonical writer until its
8749
+ // access is explicitly terminal through local cutover or provider-confirmed absence.
8750
+ // A disconnect-in-progress fence blocks both sides so a failed revoke cannot
8751
+ // leave a brief dual-writer interval before the legacy row is restored.
8752
+ if (normalizedSourceProviderSlug === JUNCTION_GOOGLE_HEALTH_PROVIDER_SLUG) {
8753
+ const legacySources = sources.filter((source) => normalizeProviderSlug(source.sourceProviderSlug)
8754
+ === JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG);
8755
+ if (legacySources.length > 0) {
8756
+ const canonicalFence = buildJunctionGoogleHealthCanonicalCoverageFence(sources);
8757
+ const successor = sources.find((source) => normalizeProviderSlug(source.sourceProviderSlug)
8758
+ === JUNCTION_GOOGLE_HEALTH_PROVIDER_SLUG);
8759
+ if (legacySources.some((source) => !isGoogleHealthFitbitMigrationLegacyTerminal(source))
8760
+ || !successor
8761
+ || legacySources.some((source) => !isGoogleHealthFitbitMigrationLegacyCoverageReady({
8762
+ legacyAccessTerminal: true,
8763
+ legacySummary: source.resourceAvailabilitySummary,
8764
+ successorSummary: successor.resourceAvailabilitySummary,
8765
+ }))
8766
+ || (canonicalFence
8767
+ && Object.values(canonicalFence.coverageBoundaryByResource).some((boundary) => boundary === null))) {
8768
+ return false;
8769
+ }
8770
+ }
8771
+ }
7138
8772
  const accountSourceIdentity = resolveJunctionAccountSourceIdentity(sources, normalizedSourceProviderSlug, allowUnlistedSources);
7139
8773
  if (!accountSourceIdentity) {
7140
8774
  return false;
@@ -7150,7 +8784,7 @@ function isJunctionSourceAdmittedForImport(sources, sourceProviderSlug, allowUnl
7150
8784
  && !isDeviceSyncSourceDisconnectFenced(lifecycleSource);
7151
8785
  }
7152
8786
  function isJunctionSourceProjectionFenced(sources, sourceProviderSlug) {
7153
- const normalizedSourceProviderSlug = normalizeProviderSlug(sourceProviderSlug);
8787
+ const normalizedSourceProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
7154
8788
  if (!normalizedSourceProviderSlug) {
7155
8789
  return false;
7156
8790
  }
@@ -7163,28 +8797,36 @@ function isJunctionSourceProjectionFenced(sources, sourceProviderSlug) {
7163
8797
  && isDeviceSyncSourceDisconnectFenced(sourceState.lifecycleSource);
7164
8798
  }
7165
8799
  function hasJunctionSourceListing(sources, sourceProviderSlug) {
7166
- const normalizedSourceProviderSlug = normalizeProviderSlug(sourceProviderSlug);
8800
+ const normalizedSourceProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
7167
8801
  if (!normalizedSourceProviderSlug) {
7168
8802
  return true;
7169
8803
  }
7170
- return sources.some((source) => areJunctionProviderSlugsRouteEquivalent(source.sourceProviderSlug, normalizedSourceProviderSlug));
8804
+ return sources.some((source) => areJunctionProviderSlugsDataEquivalent(source.sourceProviderSlug, normalizedSourceProviderSlug));
7171
8805
  }
7172
- async function resolveJunctionCurrentSourceAdmission(context, sourceProviderSlug) {
8806
+ async function resolveJunctionCurrentSourceAdmission(context, sourceProviderSlug, expectedLifecycleEpoch) {
7173
8807
  const sources = context.listConnectionSources
7174
- ? await context.listConnectionSources()
8808
+ ? await context.listConnectionSources({ sourceProviderSlug })
7175
8809
  : context.account.sources ?? [];
7176
- return resolveJunctionCurrentSourceAdmissionFromSources(sources, sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only");
8810
+ return resolveJunctionCurrentSourceAdmissionFromSources(sources, sourceProviderSlug, context.connectionSourceAdmissionMode !== "listed_only", expectedLifecycleEpoch);
7177
8811
  }
7178
- function resolveJunctionCurrentSourceAdmissionFromSources(sources, sourceProviderSlug, allowUnlistedSources) {
8812
+ function resolveJunctionCurrentSourceAdmissionFromSources(sources, sourceProviderSlug, allowUnlistedSources, expectedLifecycleEpoch) {
7179
8813
  if (isJunctionSourceProjectionFenced(sources, sourceProviderSlug)) {
7180
8814
  return "fenced";
7181
8815
  }
8816
+ const sourceState = resolveJunctionAccountSourceState(sources, sourceProviderSlug);
8817
+ if (expectedLifecycleEpoch !== undefined
8818
+ && (sourceState === null
8819
+ ? !allowUnlistedSources
8820
+ : readJunctionSourceLifecycleEpoch(sourceState.lifecycleSource)
8821
+ !== expectedLifecycleEpoch)) {
8822
+ return "fenced";
8823
+ }
7182
8824
  return isJunctionSourceAdmittedForImport(sources, sourceProviderSlug, allowUnlistedSources, "connected")
7183
8825
  ? "admitted"
7184
8826
  : "pending";
7185
8827
  }
7186
8828
  async function isJunctionCompanionSourceCurrentlyAdmitted(context, sourceProviderSlug) {
7187
- const normalizedSourceProviderSlug = normalizeProviderSlug(sourceProviderSlug);
8829
+ const normalizedSourceProviderSlug = canonicalizeJunctionProviderSlug(sourceProviderSlug);
7188
8830
  if (!context.listConnectionSources) {
7189
8831
  throw junctionCompanionSourceStateUnavailableError();
7190
8832
  }
@@ -7203,7 +8845,7 @@ async function isJunctionCompanionSourceCurrentlyAdmitted(context, sourceProvide
7203
8845
  }
7204
8846
  throw junctionCompanionSourceStateUnavailableError(error);
7205
8847
  }
7206
- if (isDeviceSyncSourceAdmitted(sources, normalizedSourceProviderSlug)) {
8848
+ if (isJunctionSourceAdmittedForImport(sources, normalizedSourceProviderSlug, true, "connected")) {
7207
8849
  return true;
7208
8850
  }
7209
8851
  if (sources.some((source) => source.lastErrorCode === DEVICE_SYNC_SOURCE_USER_DISCONNECTED_ERROR_CODE)) {
@@ -7249,7 +8891,8 @@ function projectJunctionSourcesByProviderSlug(connectionId, providers) {
7249
8891
  sourceProviderSlug: provider.origin.sourceProviderSlug,
7250
8892
  sourceInstanceId: provider.origin.sourceInstanceId,
7251
8893
  });
7252
- const sourceProviderSlug = normalizeProviderSlug(origin.sourceProviderSlug) ?? normalizeProviderSlug(provider.slug);
8894
+ const sourceProviderSlug = canonicalizeJunctionProviderSlug(origin.sourceProviderSlug)
8895
+ ?? canonicalizeJunctionProviderSlug(provider.slug);
7253
8896
  if (!sourceProviderSlug) {
7254
8897
  continue;
7255
8898
  }
@@ -7264,7 +8907,10 @@ function projectJunctionSourcesByProviderSlug(connectionId, providers) {
7264
8907
  const status = mapJunctionSourceStatus(provider.status);
7265
8908
  const lastErrorCode = status === "error"
7266
8909
  ? truncateJunctionSourceErrorText(provider.errorDetails?.errorType, JUNCTION_SOURCE_ERROR_CODE_MAX_LENGTH)
7267
- : null;
8910
+ : status === "disconnected"
8911
+ && sourceProviderSlug === JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG
8912
+ ? DEVICE_SYNC_SOURCE_PROVIDER_DISCONNECTED_ERROR_CODE
8913
+ : null;
7268
8914
  const lastErrorMessage = status === "error"
7269
8915
  ? truncateJunctionSourceErrorText(provider.errorDetails?.errorMessage, JUNCTION_SOURCE_ERROR_MESSAGE_MAX_LENGTH)
7270
8916
  : null;
@@ -7272,11 +8918,17 @@ function projectJunctionSourcesByProviderSlug(connectionId, providers) {
7272
8918
  if (existing) {
7273
8919
  mergeJunctionResourceAvailabilitySummary(existing.resourceAvailabilitySummary, resourceAvailabilitySummary);
7274
8920
  existing.status = mergeJunctionSourceStatus(existing.status, status);
7275
- if (existing.status !== "error") {
8921
+ if (existing.status === "disconnected"
8922
+ && sourceProviderSlug === JUNCTION_FITBIT_LEGACY_PROVIDER_SLUG) {
8923
+ existing.lastErrorCode = DEVICE_SYNC_SOURCE_PROVIDER_DISCONNECTED_ERROR_CODE;
8924
+ existing.lastErrorMessage = null;
8925
+ }
8926
+ else if (existing.status !== "error") {
7276
8927
  existing.lastErrorCode = null;
7277
8928
  existing.lastErrorMessage = null;
7278
8929
  }
7279
- else if (existing.lastErrorCode === null && existing.lastErrorMessage === null) {
8930
+ else if (existing.lastErrorCode === DEVICE_SYNC_SOURCE_PROVIDER_DISCONNECTED_ERROR_CODE
8931
+ || (existing.lastErrorCode === null && existing.lastErrorMessage === null)) {
7280
8932
  existing.lastErrorCode = lastErrorCode;
7281
8933
  existing.lastErrorMessage = lastErrorMessage;
7282
8934
  }
@@ -7318,8 +8970,8 @@ function mergeJunctionResourceAvailabilityValue(existing, next) {
7318
8970
  return existing;
7319
8971
  }
7320
8972
  function mergeJunctionSourceStatus(existing, next) {
7321
- if (existing === "connected" || next === "connected") {
7322
- return "connected";
8973
+ if (existing === "disconnected" || next === "disconnected") {
8974
+ return "disconnected";
7323
8975
  }
7324
8976
  if (existing === "error" || next === "error") {
7325
8977
  return "error";
@@ -7327,7 +8979,7 @@ function mergeJunctionSourceStatus(existing, next) {
7327
8979
  if (existing === "unavailable" || next === "unavailable") {
7328
8980
  return "unavailable";
7329
8981
  }
7330
- return "disconnected";
8982
+ return "connected";
7331
8983
  }
7332
8984
  // Derive import identity from the stable Junction user id, never from the
7333
8985
  // local device-sync account row id: row ids are re-minted whenever the
@@ -7496,6 +9148,32 @@ function readProviderSnapshotCanonicalEventExternalRefResourceIds(value) {
7496
9148
  ? resourceIds
7497
9149
  : null;
7498
9150
  }
9151
+ function readProviderSnapshotJunctionCanonicalCoverage(value) {
9152
+ const entries = readPlainObject(value)?.junctionCanonicalCoverage;
9153
+ if (!Array.isArray(entries)) {
9154
+ return [];
9155
+ }
9156
+ return entries.flatMap((entry) => {
9157
+ const evidence = readPlainObject(entry);
9158
+ const resource = typeof evidence?.resource === "string" ? evidence.resource : "";
9159
+ const coverageBoundary = normalizeJunctionCanonicalCoverageBoundary(resource, evidence?.coverageBoundary);
9160
+ if (!evidence
9161
+ || !coverageBoundary
9162
+ || !resource
9163
+ || typeof evidence.sourceProviderSlug !== "string") {
9164
+ return [];
9165
+ }
9166
+ const coverageFinalizedAt = toIsoTimestampIfValid(evidence.coverageFinalizedAt);
9167
+ return [{
9168
+ coverageBoundary,
9169
+ ...(coverageFinalizedAt
9170
+ ? { coverageFinalizedAt }
9171
+ : {}),
9172
+ resource,
9173
+ sourceProviderSlug: evidence.sourceProviderSlug,
9174
+ }];
9175
+ });
9176
+ }
7499
9177
  function uniqueJunctionProviderRecordIdentities(identities) {
7500
9178
  return [...new Set(identities.filter((identity) => identity !== null && JUNCTION_BLOOD_PRESSURE_PROVIDER_RECORD_IDENTITY_PATTERN.test(identity)))].sort();
7501
9179
  }